SpringBoot使用Redis 數據訪問解決方案(連接池、Pipleline及分佈式)

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章