为什么说RedisCluster中的MGET要慎用

为什么说RedisCluster中的MGET要慎用所以 Lettuce 客户端 执行 mget 获取跨槽位的数据 是通过槽位分发执行 mget 并合并结果实现的

大家好,欢迎来到IT知识分享网。

MGET 在 redis-cluster 中的工作原理

在 redis-cluster 中,数据是通过哈希槽(slots)的方式分布在多个节点上。MGET 命令一次性获取多个 key 的值,如果这些 key 所在的哈希槽分布在不同的节点上,那么 MGET 命令需要与多个节点进行通信。Redis底层对于Key的所在槽位计算逻辑其实是根据一套CRC16算法来实现的,如下图所示:

图片

多节点通讯开销

  • 对于每一个 key,MGET 命令需要确定对应的节点。
  • 如果这些 key 分散在多个节点上,MGET 需要向多个节点发起请求并等待所有节点的响应。
  • 这种多节点通信会增加网络开销,进而影响命令的整体性能。

MGET在集群模式下的交互链路

这里我们以 客户端向 redis-cluster 发送 MGET key1 key2 key3 请求 为案例进行分析。

整个流程如下:

1.根据 key 计算哈希槽:集群根据每个 key 的哈希值(通常使用 CRC16 算法)计算出其所属的哈希槽。例如:

  • key1 的哈希槽为 slot1
  • key2 的哈希槽为 slot2
  • key3 的哈希槽为 slot3

2.定位节点:集群通过哈希槽确定每个 key 所属的节点:

  • slot1 对应节点 A
  • slot2 对应节点 B
  • slot3 对应节点 C

3.向各节点发送请求:集群分别向节点 A、B、C 发送获取对应 key 值的请求:

  • 节点 A 接收到 GET key1 请求
  • 节点 B 接收到 GET key2 请求
  • 节点 C 接收到 GET key3 请求

4.节点处理请求:各个节点分别处理收到的 GET 请求,并返回结果:

  • 节点 A 返回 value1
  • 节点 B 返回 value2
  • 节点 C 返回 value3

5.集群汇总结果:集群将各个节点返回的结果进行汇总,并返回给客户端。

集群部署中进行MGET有什么优化思路

在实际业务场景中,难免会有遇到使用了redis集群架构后要使用mget的场景,那么我们是否可以针对已有的一些redis客户端组件进行代码升级和优化呢?下边我们一起来梳理下解决方式。

 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的时候,主要有三个步骤:

  • 按照槽位 将key进行拆分;
  • 分别对相同槽位的key去对应的槽位mget获取数据;
  • 将所有执行的结果按照传参的key顺序排序返回。

所以Lettuce客户端,执行mget获取跨槽位的数据,是通过槽位分发执行mget,并合并结果实现的。而Lettuce基于Netty的NIO框架实现,发送命令不会阻塞IO,但是处理请求是单连接串行发送命令,这也就意味着,如果我们计算出来的slot个数越多,耗时也就越高。

图片

因此如果使用Lettuce的MGet如果需要进一步优化性能的话,可以从这个mget的串行的步骤进行改良。

对Jedis客户端的改造

如果说业务方需要使用jedis的客户端工具去访问redis,那么可以尝试以下这种实现思路。

在每次进行MGet之前,也是先提前计算哪些key存在于哪些slot中,然后分批发送进行查询。在Jedis的源码中有提供一个计算Slot位置的算法接口可以直接使用:

 import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisClusterCRC16; import redis.clients.jedis.HostAndPort; import java.util.*; public class JedisClusterMGetExample { private static final int SLOT_COUNT = 16384; // 计算 key 的哈希槽 public static int getSlot(String key) { return JedisClusterCRC16.getSlot(key); } }

mget部分的代码改动如下:

 import redis.clients.jedis.JedisCluster; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.params.MGetParams; import redis.clients.jedis.util.JedisClusterCRC16; import java.util.*; public class JedisClusterMGetExample { public static void main(String[] args) { // 创建 JedisCluster 对象 Set<HostAndPort> jedisClusterNodes = new HashSet<>(); jedisClusterNodes.add(new HostAndPort("127.0.0.1",6379)); JedisCluster jedisCluster = new JedisCluster(jedisClusterNodes); List<String> keys = Arrays.asList("key1","key2","key3"); Map<Integer,List<String>> slotKeyMap = new HashMap<>(); // 将 keys 按哈希槽分组 for (String key :keys) { int slot = getSlot(key); slotKeyMap.computeIfAbsent(slot,k -> new ArrayList<>()).add(key); } // 按哈希槽分批次进行 MGET 操作,并收集结果 List<String> results = new ArrayList<>(); for (Map.Entry<Integer,List<String>> entry :slotKeyMap.entrySet()) { List<String> batchKeys = entry.getValue(); // 通过批量 MGET 操作获取结果 List<String> batchResults = jedisCluster.mget(batchKeys.toArray(new String[0])); // 收集结果 results.addAll(batchResults); } System.out.println("MGET Results:" + results); jedisCluster.close(); } // 计算 key 的哈希槽 public static int getSlot(String key) { return JedisClusterCRC16.getSlot(key); } }

综上代码,整体流程如下:

  • 计算Key的哈希槽:使用 JedisClusterCRC16.getSlot(key) 计算 key 的哈希槽。
  • 按哈希槽分组:使用 Map<Integer,List<String>> 将 keys 按哈希槽分组。
  • 批量MGET:对每一个槽点组进行一次 MGET 操作。把每一批 MGET 的结果进行收集。

使用Jedis存在一个明显的缺陷,就是它底层连接池管理这块不如Lettuce好,存在一定的性能差异。

小结

在高并发场景中,对于使用的每个Redis命令的底层原理最好都应该熟悉了解后再去使用,否则很容易出现事故问题,共勉。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/126250.html

(0)
上一篇 2025-09-20 15:00
下一篇 2025-09-20 15:10

相关推荐

发表回复

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

关注微信