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

相关推荐

  • 服务器防火墙硬件和软件哪个效果更好,怎么选

    在服务器防火墙的选型中,硬件防火墙与软件防火墙并非非此即彼,而是互补关系,对于追求高安全等级与稳定性的生产环境,采用硬件防火墙做网络层隔离,配合软件防火墙做主机层加固,是当前公认效果最好的组合方案,硬件防火墙与软件防火墙的核心差异性能与资源占用硬件防火墙依托专用ASIC芯片或FPGA处理数据包,转发延迟通常在微……

    2026-07-26
    0
  • 站群服务器做GEO的核心逻辑是什么?,怎么做

    站群服务器做SEO的核心逻辑,是通过独立IP和资源隔离,彻底切断站点间的关联性,让搜索引擎对每个网站进行独立评估,从而降低连带风险并提升整体排名效率,站群服务器的核心价值:独立性与隔离性IP独立性:为什么是命门搜索引擎在评估网站时,会记录IP地址,如果多个站点共享同一个IP,它们之间很容易被判定为同一主体控制……

    2026-07-26
    0
  • 弹性云服务器到底是什么意思怎么收费,多少钱一个月

    弹性云服务器(Elastic Cloud Server,ECS)是一种可随时调整计算资源的云服务器,收费方式以按需付费和包年包月为主,用户只需为实际使用的资源买单,什么是弹性云服务器弹性云服务器本质上是一台运行在云端、配置可以灵活调整的虚拟机,它不像物理服务器那样固定规格,你可以在业务高峰期快速增加CPU、内存……

    2026-07-26
    0
  • 黑洞解封到底是什么意思,需要多久才能恢复

    黑洞解封是指将被黑洞路由策略屏蔽的IP地址恢复正常通信的过程,恢复时间通常取决于攻击流量是否彻底停止,多数情况下在攻击停止后10分钟到24小时内自动解除,用户也可通过联系服务商手动加速解封,黑洞解封到底是什么用拟人化的方式理解,黑洞就是网络世界的“强制隔离区”,当某个IP地址遭遇大量异常流量,比如DDoS攻击……

    2026-07-26
    0
  • 站群服务器IP段怎么选最安全,有哪些注意事项

    选择站群服务器IP段的安全核心在于IP地址的独立性、机房的路由策略以及服务商的合规资质,一个可靠的IP段必须来自持牌自营机房,具备独立C段和清洗能力,才能避免被关联风险,为什么IP段选择决定站群安全搜索引擎对IP关联的识别算法近年来越来越精细,同一C段或广播域的IP,爬虫通过反向DNS、路由跳数等特征很容易判断……

    2026-07-26
    0

发表回复

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