日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側工具欄

新聞中心

這里有您想知道的互聯(lián)網營銷解決方案
RedisCluster基于客戶端對mget的性能優(yōu)化

  • 1 背景
  • 2 分析原因
  • 2.1 現(xiàn)象
  • 2.2 定位問題
  • 3 解決問題
  • 3.1使用hashtag
  • 3.2 客戶端改造
  • 4 效果展示
  • 4.1 性能測試
  • 4.2 結論
  • 5 總結

一、 背景

Redis是知名的、應用廣泛的NoSQL數據庫,在轉轉也是作為主要的非關系型數據庫使用。我們主要使用Codis來管理Redis分布式集群,但隨著Codis官方停止更新和Redis Cluster的日益完善,轉轉也開始嘗試使用Redis Cluster,并選擇Lettuce作為客戶端使用。但是在業(yè)務接入過程中發(fā)現(xiàn),使用Lettuce訪問Redis Cluster的mget、mset等Multi-Key命令時,性能表現(xiàn)不佳。

創(chuàng)新互聯(lián)公司專注于利辛企業(yè)網站建設,成都響應式網站建設,商城建設。利辛網站建設公司,為利辛等地區(qū)提供建站服務。全流程按需策劃,專業(yè)設計,全程項目跟蹤,創(chuàng)新互聯(lián)公司專業(yè)和態(tài)度為您提供的服務

二、 分析原因

2.1 現(xiàn)象

業(yè)務在從Codis遷移到Redis Cluster的過程中,在Redis Cluster和Codis雙寫了相同的數據。結果Codis在比Redis Cluster多一次連接proxy節(jié)點的耗時下,同樣是mget獲取相同的數據,使用Lettuce訪問Redis Cluster還是比使用Jeds訪問Codis耗時要高,于是我們開始定位性能差異的原因。

2.2 定位問題

2.2.1 Redis Cluster的架構設計

導致Redis Cluster的mget性能不佳的根本原因,是Redis Cluster在架構上的設計導致的。Redis Cluster基于smart client和無中心的設計,按照槽位將數據存儲在不同的節(jié)點上

圖片

如上圖所示,每個主節(jié)點管理不同部分的槽位,并且下面掛了多個從節(jié)點。槽位是Redis Cluster管理數據的基本單位,集群的伸縮就是槽和數據在節(jié)點之間的移動。

通過CRC16(key) % 16384計算key屬于哪個槽位和哪個Redis節(jié)點。而且Redis Cluster的Multi-Key操作受槽位限制,例如我們執(zhí)行mget,獲取不同槽位的數據,是限制執(zhí)行的:

圖片

2.2.2 Lettuce的mget實現(xiàn)方式

lettuce對Multi-Key進行了支持,當我們調用mget方法,涉及跨槽位時,Lettuce對mget進行了拆分執(zhí)行和結果合并,代碼如下:

public RedisFuture>> mget(Iterable keys) {
    //將key按照槽位拆分
    Map> partitioned = SlotHash.partition(codec, keys);

    if (partitioned.size() < 2) {
        return super.mget(keys);
    }
    Map slots = SlotHash.getSlots(partitioned);
    Map>>> executions = new HashMap<>();
    //對不同槽位的keys分別執(zhí)行mget
    for (Map.Entry> entry : partitioned.entrySet()) {
        RedisFuture>> mget = super.mget(entry.getValue());
        executions.put(entry.getKey(), mget);
    }
    // 獲取、合并、排序結果
    return new PipelinedRedisFuture<>(executions, objectPipelinedRedisFuture -> {
        List> result = new ArrayList<>();
        for (K opKey : keys) {
            int slot = slots.get(opKey);

            int position = partitioned.get(slot).indexOf(opKey);
            RedisFuture>> listRedisFuture = executions.get(slot);
            result.add(MultiNodeExecution.execute(() -> listRedisFuture.get().get(position)));
        }

        return result;
    });
}

mget涉及多個key的時候,主要有三個步驟:

1、按照槽位 將key進行拆分;

2、分別對相同槽位的key去對應的槽位mget獲取數據;

3、將所有執(zhí)行的結果按照傳參的key順序排序返回。

所以Lettuce客戶端,執(zhí)行mget獲取跨槽位的數據,是通過槽位分發(fā)執(zhí)行mget,并合并結果實現(xiàn)的。而Lettuce基于Netty的NIO框架實現(xiàn),發(fā)送命令不會阻塞IO,但是處理請求是單連接串行發(fā)送命令:

圖片

所以Lettuce的mget的key數量越多,涉及的槽位數量越多,性能就會越差。Codis也是拆分執(zhí)行mget,不過是并發(fā)發(fā)送命令,并使用pipeline提高性能,進而減少了網絡的開銷。

三、 解決問題

3.1使用hashtag

我們首先想到的是 客戶端分別執(zhí)行分到不同槽位的請求,導致耗時增加。我們可以將我們需要同時操作到的key,放到同一個槽位里去。我們是可以通過hashtag來實現(xiàn)

hashtag用于Redis Cluster中。hashtag 規(guī)定以key里{}里的內容來做hash,比如 user:{a}:zhangsan和user:{a}:lisi就會用a去hash,保證帶{a}的key都落到同一個slot里

利用hashtag對key進行規(guī)劃,使得我們mget的值都在同一個槽位里。

圖片

但是這種方式需要業(yè)務方感知到Redis Cluster的分片的存在,需要對Redis Cluster的各節(jié)點存儲做規(guī)劃,保證數據平均的分布在不同的Redis節(jié)點上,對業(yè)務方使用上太不友好,所以舍棄了這種方案。

3.2 客戶端改造

另一種方案是在客戶端做改造,這樣做成本較低。不需要業(yè)務方感知和維護hashtag。

我們利用pipeline對Redis節(jié)點批量發(fā)送get命令,相對于Lettuce串行發(fā)送mget命令來說,減少了多次跨槽位mget發(fā)送命令的網絡耗時。具體步驟如下:

1、把所有key按照所在的Redis節(jié)點拆分;

2、通過pipeline對每個Redis節(jié)點批量發(fā)送get命令;

3、獲取所有命令執(zhí)行結果,排序、合并結果,并返回。

這樣改造,使用pipeline一次發(fā)送批量的命令,減少了串行批量發(fā)送命令的網絡耗時。

3.2.1 改造JedisCluster

由于Lettuce沒有原生支持pipeline批量提交命令,而JedisCluster原生支持pipeline,并且JedisCluster沒有對Multi-Key進行支持,我們對JedisCluster的mget進行了改造,代碼如下:

public List mget(String... keys) {
        List pipelineList = new ArrayList<>();
        List jedisList = new ArrayList<>();
        try {
            //按照key的hash計算key位于哪一個redis節(jié)點
            Map> pooling = new HashMap<>();
            for (String key : keys) {
                JedisPool pool = connectionHandler.getConnectionPoolFromSlot(JedisClusterCRC16.getSlot(key));
                pooling.computeIfAbsent(pool, k -> new ArrayList<>()).add(key);
            }
            //分別對每個redis 執(zhí)行pipeline get操作
            Map> resultMap = new HashMap<>();
            for (Map.Entry> entry : pooling.entrySet()) {
                Jedis jedis = entry.getKey().getResource();
                Pipeline pipelined = jedis.pipelined();
                for (String key : entry.getValue()) {
                    Response response = pipelined.get(key);
                    resultMap.put(key, response);
                }
                pipelined.flush();
                //保存所有連接和pipeline 最后進行close
                pipelineList.add(pipelined);
                jedisList.add(jedis);
            }
            //同步所有請求結果
            for (Pipeline pipeline : pipelineList) {
                pipeline.returnAll();
            }
            //合并、排序結果
            List list = new ArrayList<>();
            for (String key : keys) {
                Response response = resultMap.get(key);
                String o = response.get();
                list.add(o);
            }
            return list;
        }finally {
            //關閉所有pipeline和jedis連接
            pipelineList.forEach(Pipeline::close);
            jedisList.forEach(Jedis::close);
        }
    }

3.2.2 處理異常case

上面的代碼還不足以覆蓋所有場景,我們還需要處理一些異常case

  • Redis Cluster擴縮容導致的數據遷移

數據遷移會造成兩種錯誤

1、MOVED錯誤

代表數據所在的槽位已經遷移到另一個redis節(jié)點上了,服務端會告訴客戶端對應的槽的目標節(jié)點信息。此時我們需要做的是更新客戶端緩存的槽位信息,并嘗試重新獲取數據。

2、ASKING錯誤

代表槽位正在遷移中,且數據不在源節(jié)點中,我們需要先向目標Redis節(jié)點執(zhí)行ASKING命令,才能獲取遷移的槽位的數據。

List list = new ArrayList<>();
for (String key : keys) {
    Response response = resultMap.get(key);
    String o;
    try {
        o = response.get();
        list.add(o);
    } catch (JedisRedirectionException jre) {
        if (jre instanceof JedisMovedDataException) {
            //此槽位已經遷移 更新客戶端的槽位信息
            this.connectionHandler.renewSlotCache(null);
        }
        boolean asking = false;
        if (jre instanceof JedisAskDataException) {
            //獲取槽位目標redis節(jié)點的連接 設置asking標識,以便在重試前執(zhí)行asking命令
            asking = true;
 askConnection.set(this.connectionHandler.getConnectionFromNode(jre.getTargetNode()));
        } else {
            throw new JedisClusterException(jre);
        }
        //重試獲取這個key的結果
        o = runWithRetries(this.maxAttempts, asking, true, key);
        list.add(o);
    }
}

數據遷移導致的兩種異常,會進行重試。重試會導致耗時增加,并且如果達到最大重試次數,還沒有獲取到數據,則拋出異常。

  • pipeline的某個命令執(zhí)行失敗

不捕獲執(zhí)行失敗的異常,拋出異常讓業(yè)務服務感知到異常發(fā)生。

四、 效果展示

4.1 性能測試

在改造完客戶端之后,我們對客戶端的mget進行了性能測試,測試了下面三種類型的耗時

1、使用Jedis訪問Codis

2、使用改造的JedisCluster訪問Redis Cluster

3、使用Lettuce同步方式訪問Redis Cluster

4.1.1 mget 100key

Codis

JedisCluster(改造)

Lettuce

avg

0.411ms

0.224ms

0.61ms

tp99

0.528ms

0.35ms

1.53ms

tp999

0.745ms

1.58ms

3.87ms

4.1.2 mget 500key

Codis

JedisCluster(改造)

Lettuce

avg

0.96ms

0.511ms

2.14ms

tp99

1.15ms

0.723ms

3.99ms

tp999

1.81ms

1.86ms

6.88ms

4.1.3 mget 1000key

Codis

JedisCluster(改造)

Lettuce

avg

1.56ms

0.92ms

5.04ms

tp99

1.83ms

1.22ms

8.91ms

tp999

3.15ms

3.88ms

32ms

4.2 結論

  • 使用改造的客戶端訪問Redis Cluster,比使用Lettuce訪問Redis Cluster要快1倍以上;
  • 改造的客戶端比使用codis稍微快一點,tp999不如codis性能好。

但是改造的客戶端相對于Lettuce也有缺點,JedisCluster是基于復雜的連接池實現(xiàn),連接池的配置會影響客戶端的性能。而Lettuce是基于Netty的NIO框架實現(xiàn),對于大多數的Redis操作,只需要維持單一的連接即可高效支持并發(fā)請求,不需要業(yè)務考慮連接池的配置。

五、 總結

Redis Cluster在架構設計上對Multi-Key進行的限制,導致無法跨槽位執(zhí)行mget等命令。我們對客戶端JedisCluster的Multi-Key命令進行改造,通過分別對Redis節(jié)點執(zhí)行pipeline操作,提升了mget命令的性能。


新聞標題:RedisCluster基于客戶端對mget的性能優(yōu)化
瀏覽路徑:http://m.5511xx.com/article/ccdssos.html