SpringBoot使用Redis 数据访问解决方案(连接池、Pipleline及分布式)

笑着哭i 提交于 2020-04-30 12:46:11

Redis操作是单线程的,使用连接池可以减少连接的创建,redis连接池有两种方式:Jedis(JedisPool) 和 Lettuce(LettucePool)。 Lettuce 和 Jedis 的定位都是Redis的client,所以他们当然可以直接连接redis server。在Lettuce和Jedis之外还有Redission ,Redisson:实现了分布式和可扩展的Java数据结构。

Redis 客户端Jedis和Lettuce的区别

Jedis

Jedis在实现上是直接连接的Redis Server,如果在多线程环境下是非线程安全的。每个线程都去拿自己的 Jedis 实例,当连接数量增多时,资源消耗阶梯式增大,连接成本就较高了。这个时候只有使用连接池,为每个Jedis实例增加物理连接。

Lettuce

Lettuce的连接是基于Netty的,Netty 是一个多线程、事件驱动的 I/O 框架。连接实例可以在多个线程间共享,当多线程使用同一连接实例时,是线程安全的。Lettuce连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。

Others

概念:

Jedis:是Redis的Java实现客户端,提供了比较全面的Redis命令的支持,

Redisson:实现了分布式和可扩展的Java数据结构。

Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。

优点:

Jedis:比较全面的提供了Redis的操作特性

Redisson:促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列

Lettuce:主要在一些分布式缓存框架上使用比较多

可伸缩:

Jedis:使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。

Redisson:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作

Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作

SpringBoot集成Redis服务

maven pom.xml依赖

 <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>
 <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.9.0</version>
 </dependency>

application.yml redis连接池配置

spring:
  ....
#缓存配置
  redis:
    #-----Redis-------自定义配置------开始---
    redisKeyWithSuffix: false # 是否REDIS数据类型KEY添加类型后缀
    usePipeline: true # 是否启用REDIS pipeline 高性能模式
    usePipelineBatchThreads: 100 # REDIS pipeline批处理线程个数
    usePipelineBatchConsumer: true # REDIS pipeline是否启用批量消费
    #-----Redis-------自定义配置------结束---
    host: 10.10.1.10
    port: 6379
    timeout: 10000
    password: dlwy@2019
    database: 1
    lettuce:
      pool:
        max-active: 50
        max-idle: 10
        min-idle: 10
        max-wait: 10000
        time-between-eviction-runs: 30

注意:自定义配置是为了区分key的数据结构类型和pipeline的实现做的配置。

Redis pipeline 批量处理

Pipeline原理分析

参考地址:https://www.cnblogs.com/pc-boke/articles/9045576.html

1. 基本原理

1.1 为什么会出现Pipeline
  Redis本身是基于Request/Response协议的,正常情况下,客户端发送一个命令,等待Redis应答,Redis在接收到命令,处理后应答。在这种情况下,如果同时需要执行大量的命令,那就是等待上一条命令应答后再执行,这中间不仅仅多了RTT(Round Time Trip),而且还频繁的调用系统IO,发送网络请求。如下图。
为了提升效率,这时候Pipeline出现了,它允许客户端可以一次发送多条命令,而不等待上一条命令执行的结果,这和网络的Nagel算法有点像(TCP_NODELAY选项)。不仅减少了RTT,同时也减少了IO调用次数(IO调用涉及到用户态到内核态之间的切换)。如下图:
客户端这边首先将执行的命令写入到缓冲中,最后再一次性发送Redis。但是有一种情况就是,缓冲区的大小是有限制的,比如Jedis,限制为8192,超过了,则刷缓存,发送到Redis,但是不去处理Redis的应答,如上图所示那样。


1.2 实现原理
  要支持Pipeline,其实既要服务端的支持,也要客户端支持。对于服务端来说,所需要的是能够处理一个客户端通过同一个TCP连接发来的多个命令,可以理解为,这里将多个命令切分,和处理单个命令一样(之前老生常谈的黏包现象),
Redis就是这样处理的。而客户端,则是要将多个命令缓存起来,缓冲区满了就发送,然后再写缓冲,最后才处理Redis的应答,如Jedis。

1.3 从哪个方面提升性能
正如上面所说的,一个是RTT,节省往返时间,但是另一个原因也很重要,就是IO系统调用。一个read系统调用,需要从用户态,切换到内核态。

1.4 注意点
  Redis的Pipeline和Transaction不同,Transaction会存储客户端的命令,最后一次性执行,而Pipeline则是处理一条,响应一条,但是这里却有一点,就是客户端会并不会调用read去读取socket里面的缓冲数据,这也就造就了,
如果Redis应答的数据填满了该接收缓冲(SO_RECVBUF),那么客户端会通过ACK,WIN=0(接收窗口)来控制服务端不能再发送数据,那样子,数据就会缓冲在Redis的客户端应答列表里面。所以需要注意控制Pipeline的大小。如下图:

2. Codis Pipeline
  在一般情况下,都会在Redis前面使用一个代理,来作负载以及高可用。这里在公司里面使用的是Codis,以Codis 3.2版本为例(3.2版本是支持Pipeline的)。
Codis在接收到客户端请求后,首先根据Key来计算出一个hash,映射到对应slots,然后转发请求到slots对应的Redis。在这过程中,一个客户端的多个请求,有可能会对应多个Redis,这个时候就需要保证请求的有序性(不能乱序),
Codis采用了一个Tasks队列,将请求依次放入队列,然后loopWriter从里面取,如果Task请求没有应答,则等待(这里和Java的Future是类似的)。内部BackenRedis是通过channel来进行通信的,dispatcher将Request通过channel发送到BackenRedis,然后BackenRedis处理完该请求,则将值填充到该Request里面。最后loopWriter等待到了值,则返回给客户端。如下图所示:


3. 总结
  1、Pipeline减少了RTT,也减少了IO调用次数(IO调用涉及到用户态到内核态之间的切换)
  2、需要控制Pipeline的大小,否则会消耗Redis的内存
  3、Codis 3.2 Pipeline默认10K,3.1则是1024Jedis客户端缓存是8192,超过该大小则刷新缓存,或者直接发送


4. 参考资料
Redis官方文档:https://redis.io/topics/pipelining

Pipeline 回调方法示例

redis回调实现:

package com.xxx.position.redis.pipeline.position;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xxx.position.bean.XhyPosition;
import com.xxx.position.service.MobileWebService;
import com.xxx.position.util.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import java.util.List;

/**
 * @Copyright: 2019-2021
 * @FileName: OnlinePositionRedisCallback.java
 * @Author: PJL
 * @Date: 2020/4/22 18:02
 * @Description: 实时位置相关pipeline批量处理
 */
@Slf4j
public class OnlinePositionRedisCallback implements RedisCallback<Object> {

    MobileWebService mobileService;

    XhyPosition position;

    List<XhyPosition> positionList;

    long mobilePositionTimeout;

    int pointLimit;

    /**
     * 实时位置批量执行指令构造
     *
     * @param position
     * @param positionList
     */
    public OnlinePositionRedisCallback(MobileWebService mobileService, XhyPosition position, List<XhyPosition> positionList, long mobilePositionTimeout,int pointLimit){
        this.mobileService = mobileService;
        this.position = position;
        this.positionList = positionList;
        this.mobilePositionTimeout = mobilePositionTimeout;
        this.pointLimit = pointLimit;
    }

    @Override
    public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
        // 验证指令执行方式
        if(null != positionList){
            for (XhyPosition position : positionList){
                doOnlineCommand(redisConnection,position);
            }
        }else{
            doOnlineCommand(redisConnection,position);
        }
        return null;
    }

    /**
     * 执行单个指令
     *
     * @param redisConnection
     */
    private void doOnlineCommand(RedisConnection redisConnection,XhyPosition position) {
        String userId = position.getId();
        String json = JSON.toJSONString(position);
        Point point = new Point(position.getX(),position.getY());
        String timeoutKey = new StringBuffer(Constants.MOBILE_POSITION_TIMEOUT_KEY ).append(userId).toString();
        try {
            /********** 用户单位位置存储***********/
            // 命令1:保存用户当前位置及今日巡护公里和时间
            redisConnection.hSet(Constants.MOBILE_TRACK_AGGREGATION_HLY_KEY.getBytes(), userId.getBytes(), json.getBytes());
            // 命令2:保存用户实时轨迹
            String trackKey = new StringBuffer(Constants.MOBILE_TRACK_LIST_KEY ).append(userId).toString();
            redisConnection.lPush(trackKey.getBytes(), JSONObject.toJSONString(point).getBytes());
            // 命令3:保存用户实时轨迹限制点个数
            redisConnection.lRange(trackKey.getBytes(),0,pointLimit);
            // 命令4:保存全国级别位置
            redisConnection.geoAdd(Constants.MOBILE_POSITION_QG_KEY.getBytes(),point,userId.getBytes());
            // 命令5:获取用户父级单位数据
            Object[] parentIds = mobileService.getParentIdsWithoutType(userId);
            for (Object o : parentIds) {
                String dwCode = o.toString();
                /***********用户单位数量原子增长(说明:多线程有并发问题,用户上线是非原子操作(有状态),同步操作会降低效率弃用原子操作)****/
                // 命令6-n:获取用户是否在线的标志
               /* Object obj = mobileService.getUserWithoutType(userOnlineKey);
                if (null == obj) {
                    String incrementKey = new StringBuffer(Constants.MOBILE_POSITION_DW_CAS_SUM_KEY ).append(dwCode).toString();
                    // 命令7-n:保存全国级别位置
                    redisConnection.incr(incrementKey.getBytes());
                }*/
                String  dwCodeNew =new StringBuffer(Constants.MOBILE_POSITION_DW_KEY ).append(dwCode).toString();
                // 命令8-n:保存全国级别位置
                redisConnection.geoAdd(dwCodeNew.getBytes(),point,userId.getBytes());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            /*************位置数据过期标记*********/
            // 命令9-n:保存全国级别位置
            redisConnection.setEx(timeoutKey.getBytes(), mobilePositionTimeout,json.getBytes());
            log.debug("用户userId="+userId+"已上线!");
        }
    }
}

调用方法:

 /**
     * 上线单个位置
     *
     * @param position
     */
    public void online(XhyPosition position){
        redisTemplate.executePipelined(new OnlinePositionRedisCallback(mobileService,position,null,MOBILE_POSITION_OUT_TIME,trackListRange));
    }

    /**
     * 上线多个位置
     *
     * @param positionList
     */
    public void onlineList(List<XhyPosition> positionList){
        redisTemplate.executePipelined(new OnlinePositionRedisCallback(mobileService,null,positionList,MOBILE_POSITION_OUT_TIME,trackListRange));
    }

...............有待补充

参考文章:

https://www.cnblogs.com/liyan492/p/9858548.html

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!