自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Redis Cluster基于客戶端對(duì)mget的性能優(yōu)化

數(shù)據(jù)庫(kù)
我們對(duì)客戶端JedisCluster的Multi-Key命令進(jìn)行改造,通過(guò)分別對(duì)Redis節(jié)點(diǎn)執(zhí)行pipeline操作,提升了mget命令的性能。

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

一、 背景

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

二、 分析原因

2.1 現(xiàn)象

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

2.2 定位問(wèn)題

2.2.1 Redis Cluster的架構(gòu)設(shè)計(jì)

導(dǎo)致Redis Cluster的mget性能不佳的根本原因,是Redis Cluster在架構(gòu)上的設(shè)計(jì)導(dǎo)致的。Redis Cluster基于smart client和無(wú)中心的設(shè)計(jì),按照槽位將數(shù)據(jù)存儲(chǔ)在不同的節(jié)點(diǎn)上

圖片圖片

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

通過(guò)CRC16(key) % 16384 來(lái)計(jì)算key屬于哪個(gè)槽位和哪個(gè)Redis節(jié)點(diǎn)。而且Redis Cluster的Multi-Key操作受槽位限制,例如我們執(zhí)行mget,獲取不同槽位的數(shù)據(jù),是限制執(zhí)行的:

圖片圖片

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

lettuce對(duì)Multi-Key進(jìn)行了支持,當(dāng)我們調(diào)用mget方法,涉及跨槽位時(shí),Lettuce對(duì)mget進(jìn)行了拆分執(zhí)行和結(jié)果合并,代碼如下:

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

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

            int position = partitioned.get(slot).indexOf(opKey);
            RedisFuture<List<KeyValue<K, V>>> listRedisFuture = executions.get(slot);
            result.add(MultiNodeExecution.execute(() -> listRedisFuture.get().get(position)));
        }

        return result;
    });
}

mget涉及多個(gè)key的時(shí)候,主要有三個(gè)步驟:

1、按照槽位 將key進(jìn)行拆分;

2、分別對(duì)相同槽位的key去對(duì)應(yīng)的槽位mget獲取數(shù)據(jù);

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

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

圖片圖片

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

三、 解決問(wèn)題

3.1使用hashtag

我們首先想到的是 客戶端分別執(zhí)行分到不同槽位的請(qǐng)求,導(dǎo)致耗時(shí)增加。我們可以將我們需要同時(shí)操作到的key,放到同一個(gè)槽位里去。我們是可以通過(guò)hashtag來(lái)實(shí)現(xiàn)

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

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

圖片圖片

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

3.2 客戶端改造

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

我們利用pipeline對(duì)Redis節(jié)點(diǎn)批量發(fā)送get命令,相對(duì)于Lettuce串行發(fā)送mget命令來(lái)說(shuō),減少了多次跨槽位mget發(fā)送命令的網(wǎng)絡(luò)耗時(shí)。具體步驟如下:

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

2、通過(guò)pipeline對(duì)每個(gè)Redis節(jié)點(diǎn)批量發(fā)送get命令;

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

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

3.2.1 改造JedisCluster

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

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

3.2.2 處理異常case

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

  • Redis Cluster擴(kuò)縮容導(dǎo)致的數(shù)據(jù)遷移

數(shù)據(jù)遷移會(huì)造成兩種錯(cuò)誤

1、MOVED錯(cuò)誤

代表數(shù)據(jù)所在的槽位已經(jīng)遷移到另一個(gè)redis節(jié)點(diǎn)上了,服務(wù)端會(huì)告訴客戶端對(duì)應(yīng)的槽的目標(biāo)節(jié)點(diǎn)信息。此時(shí)我們需要做的是更新客戶端緩存的槽位信息,并嘗試重新獲取數(shù)據(jù)。

2、ASKING錯(cuò)誤

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

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

數(shù)據(jù)遷移導(dǎo)致的兩種異常,會(huì)進(jìn)行重試。重試會(huì)導(dǎo)致耗時(shí)增加,并且如果達(dá)到最大重試次數(shù),還沒有獲取到數(shù)據(jù),則拋出異常。

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

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

四、 效果展示

4.1 性能測(cè)試

在改造完客戶端之后,我們對(duì)客戶端的mget進(jìn)行了性能測(cè)試,測(cè)試了下面三種類型的耗時(shí)

1、使用Jedis訪問(wèn)Codis

2、使用改造的JedisCluster訪問(wèn)Redis Cluster

3、使用Lettuce同步方式訪問(wèn)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 結(jié)論

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

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

五、 總結(jié)

Redis Cluster在架構(gòu)設(shè)計(jì)上對(duì)Multi-Key進(jìn)行的限制,導(dǎo)致無(wú)法跨槽位執(zhí)行mget等命令。我們對(duì)客戶端JedisCluster的Multi-Key命令進(jìn)行改造,通過(guò)分別對(duì)Redis節(jié)點(diǎn)執(zhí)行pipeline操作,提升了mget命令的性能。

責(zé)任編輯:龐桂玉 來(lái)源: 轉(zhuǎn)轉(zhuǎn)技術(shù)
相關(guān)推薦

2023-02-16 08:00:00

數(shù)據(jù)流客戶端開發(fā)數(shù)據(jù)集

2009-06-12 19:18:08

REST客戶端框架JavaScript

2011-08-15 14:09:59

JavaHBase

2009-12-07 18:26:36

WCF客戶端

2022-03-19 12:16:49

Redis高并發(fā)系統(tǒng)集群部署

2011-04-22 10:34:09

SimpleFrame

2009-03-18 14:44:34

LinuxqTwitterTwitter

2021-09-22 15:46:29

虛擬桌面瘦客戶端胖客戶端

2025-01-07 08:10:00

CefSharpWinformWindows

2022-04-15 10:52:50

模型技術(shù)實(shí)踐

2022-09-15 16:48:30

MySQL數(shù)據(jù)庫(kù)測(cè)試

2021-08-01 23:18:21

Redis Golang命令

2009-06-23 14:00:49

JavaFX開發(fā)

2011-08-17 10:10:59

2011-03-21 14:53:36

Nagios監(jiān)控Linux

2011-04-06 14:24:20

Nagios監(jiān)控Linux

2024-09-11 09:50:22

2023-10-12 07:54:02

.NETXamarin框架

2021-06-22 15:06:13

Redis客戶端 Redis-clie

2020-05-11 21:31:02

Redis 6.0緩存客戶端
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)