Redis Cluster基于客户端对mget的性能优化

  • 1 背景
  • 2 分析原因
  • 2.1 现象
  • 2.2 定位问题
  • 3 解决问题
  • 3.1使用hashtag
  • 3.2 客户端改造
  • 4 效果展示
  • 4.1 性能测试
  • 4.2 结论
  • 5 总结

一、 背景

Redis是知名的、应用广泛的NoSQL数据库,在转转也是作为主要的非关系型数据库使用。我们主要使用Codis来管理Redis分布式集群,但随着Codis官方停止更新和Redis Cluster的日益完善,转转也开始尝试使用Redis Cluster,并选择Lettuce作为客户端使用。但是在业务接入过程中发现,使用Lettuce访问Redis Cluster的mget、mset等Multi-Key命令时,性能表现不佳。

二、 分析原因

2.1 现象

业务在从Codis迁移到Redis Cluster的过程中,在Redis Cluster和Codis双写了相同的数据。结果Codis在比Redis Cluster多一次连接proxy节点的耗时下,同样是mget获取相同的数据,使用Lettuce访问Redis Cluster还是比使用Jeds访问Codis耗时要高,于是我们开始定位性能差异的原因。

2.2 定位问题

2.2.1 Redis Cluster的架构设计

导致Redis Cluster的mget性能不佳的根本原因,是Redis Cluster在架构上的设计导致的。Redis Cluster基于smart client和无中心的设计,按照槽位将数据存储在不同的节点上

图片

如上图所示,每个主节点管理不同部分的槽位,并且下面挂了多个从节点。槽位是Redis Cluster管理数据的基本单位,集群的伸缩就是槽和数据在节点之间的移动。

通过CRC16(key) % 16384计算key属于哪个槽位和哪个Redis节点。而且Redis Cluster的Multi-Key操作受槽位限制,例如我们执行mget,获取不同槽位的数据,是限制执行的:

图片

2.2.2 Lettuce的mget实现方式

lettuce对Multi-Key进行了支持,当我们调用mget方法,涉及跨槽位时,Lettuce对mget进行了拆分执行和结果合并,代码如下:

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<>();
    //对不同槽位的keys分别执行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);
    }
    // 获取、合并、排序结果
    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涉及多个key的时候,主要有三个步骤:

1、按照槽位 将key进行拆分;

2、分别对相同槽位的key去对应的槽位mget获取数据;

3、将所有执行的结果按照传参的key顺序排序返回。

所以Lettuce客户端,执行mget获取跨槽位的数据,是通过槽位分发执行mget,并合并结果实现的。而Lettuce基于Netty的NIO框架实现,发送命令不会阻塞IO,但是处理请求是单连接串行发送命令:

图片

所以Lettuce的mget的key数量越多,涉及的槽位数量越多,性能就会越差。Codis也是拆分执行mget,不过是并发发送命令,并使用pipeline提高性能,进而减少了网络的开销。

三、 解决问题

3.1使用hashtag

我们首先想到的是 客户端分别执行分到不同槽位的请求,导致耗时增加。我们可以将我们需要同时操作到的key,放到同一个槽位里去。我们是可以通过hashtag来实现

hashtag用于Redis Cluster中。hashtag 规定以key里{}里的内容来做hash,比如 user:{a}:zhangsan和user:{a}:lisi就会用a去hash,保证带{a}的key都落到同一个slot里

利用hashtag对key进行规划,使得我们mget的值都在同一个槽位里。

图片

但是这种方式需要业务方感知到Redis Cluster的分片的存在,需要对Redis Cluster的各节点存储做规划,保证数据平均的分布在不同的Redis节点上,对业务方使用上太不友好,所以舍弃了这种方案。

3.2 客户端改造

另一种方案是在客户端做改造,这样做成本较低。不需要业务方感知和维护hashtag。

我们利用pipeline对Redis节点批量发送get命令,相对于Lettuce串行发送mget命令来说,减少了多次跨槽位mget发送命令的网络耗时。具体步骤如下:

1、把所有key按照所在的Redis节点拆分;

2、通过pipeline对每个Redis节点批量发送get命令;

3、获取所有命令执行结果,排序、合并结果,并返回。

这样改造,使用pipeline一次发送批量的命令,减少了串行批量发送命令的网络耗时。

3.2.1 改造JedisCluster

由于Lettuce没有原生支持pipeline批量提交命令,而JedisCluster原生支持pipeline,并且JedisCluster没有对Multi-Key进行支持,我们对JedisCluster的mget进行了改造,代码如下:

public List<String> mget(String... keys) {
        List<Pipeline> pipelineList = new ArrayList<>();
        List<Jedis> jedisList = new ArrayList<>();
        try {
            //按照key的hash计算key位于哪一个redis节点
            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);
            }
            //分别对每个redis 执行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 最后进行close
                pipelineList.add(pipelined);
                jedisList.add(jedis);
            }
            //同步所有请求结果
            for (Pipeline pipeline : pipelineList) {
                pipeline.returnAll();
            }
            //合并、排序结果
            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 {
            //关闭所有pipeline和jedis连接
            pipelineList.forEach(Pipeline::close);
            jedisList.forEach(Jedis::close);
        }
    }

3.2.2 处理异常case

上面的代码还不足以覆盖所有场景,我们还需要处理一些异常case

  • Redis Cluster扩缩容导致的数据迁移

数据迁移会造成两种错误

1、MOVED错误

代表数据所在的槽位已经迁移到另一个redis节点上了,服务端会告诉客户端对应的槽的目标节点信息。此时我们需要做的是更新客户端缓存的槽位信息,并尝试重新获取数据。

2、ASKING错误

代表槽位正在迁移中,且数据不在源节点中,我们需要先向目标Redis节点执行ASKING命令,才能获取迁移的槽位的数据。

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) {
            //此槽位已经迁移 更新客户端的槽位信息
            this.connectionHandler.renewSlotCache(null);
        }
        boolean asking = false;
        if (jre instanceof JedisAskDataException) {
            //获取槽位目标redis节点的连接 设置asking标识,以便在重试前执行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的某个命令执行失败

不捕获执行失败的异常,抛出异常让业务服务感知到异常发生。

四、 效果展示

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是基于复杂的连接池实现,连接池的配置会影响客户端的性能。而Lettuce是基于Netty的NIO框架实现,对于大多数的Redis操作,只需要维持单一的连接即可高效支持并发请求,不需要业务考虑连接池的配置。

五、 总结

Redis Cluster在架构设计上对Multi-Key进行的限制,导致无法跨槽位执行mget等命令。我们对客户端JedisCluster的Multi-Key命令进行改造,通过分别对Redis节点执行pipeline操作,提升了mget命令的性能。

文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/238007.html<

(0)
运维的头像运维
上一篇2025-04-22 18:01
下一篇 2025-04-22 18:02

相关推荐

  • 个人主题怎么制作?

    制作个人主题是一个将个人风格、兴趣或专业领域转化为视觉化或结构化内容的过程,无论是用于个人博客、作品集、社交媒体账号还是品牌形象,核心都是围绕“个人特色”展开,以下从定位、内容规划、视觉设计、技术实现四个维度,详细拆解制作个人主题的完整流程,明确主题定位:找到个人特色的核心主题定位是所有工作的起点,需要先回答……

    2025-11-20
    0
  • 社群营销管理关键是什么?

    社群营销的核心在于通过建立有温度、有价值、有归属感的社群,实现用户留存、转化和品牌传播,其管理需贯穿“目标定位-内容运营-用户互动-数据驱动-风险控制”全流程,以下从五个维度展开详细说明:明确社群定位与目标社群管理的首要任务是精准定位,需明确社群的核心价值(如行业交流、产品使用指导、兴趣分享等)、目标用户画像……

    2025-11-20
    0
  • 香港公司网站备案需要什么材料?

    香港公司进行网站备案是一个涉及多部门协调、流程相对严谨的过程,尤其需兼顾中国内地与香港两地的监管要求,由于香港公司注册地与中国内地不同,其网站若主要服务内地用户或使用内地服务器,需根据服务器位置、网站内容性质等,选择对应的备案路径(如工信部ICP备案或公安备案),以下从备案主体资格、流程步骤、材料准备、注意事项……

    2025-11-20
    0
  • 如何企业上云推广

    企业上云已成为数字化转型的核心战略,但推广过程中需结合行业特性、企业痛点与市场需求,构建系统性、多维度的推广体系,以下从市场定位、策略设计、执行落地及效果优化四个维度,详细拆解企业上云推广的实践路径,精准定位:明确目标企业与核心价值企业上云并非“一刀切”的方案,需先锁定目标客户群体,提炼差异化价值主张,客户分层……

    2025-11-20
    0
  • PS设计搜索框的实用技巧有哪些?

    在PS中设计一个美观且功能性的搜索框需要结合创意构思、视觉设计和用户体验考量,以下从设计思路、制作步骤、细节优化及交互预览等方面详细说明,帮助打造符合需求的搜索框,设计前的规划明确使用场景:根据网站或APP的整体风格确定搜索框的调性,例如极简风适合细线条和纯色,科技感适合渐变和发光效果,电商类则可能需要突出搜索……

    2025-11-20
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注