Redis - 数据分布优化:如何应对数据倾斜

Redis - 数据分布优化:如何应对数据倾斜

文章目录

  • 数据量倾斜
    • Bigkey 导致的倾斜
    • Hash Tag 滥用
    • 槽分配不均
  • 数据访问倾斜
    • 热 key 的来源
    • 定位热 key
    • 应对方式:本地缓存
    • 应对方式:热 key 副本
    • 应对方式:客户端缓存(Redis 6.0)
  • 数据倾斜的预防
  • 实践建议

切片集群把数据均匀打散到多个节点,理论上能让每个节点的负载差不多。但生产环境经常出现这样的情况:明明用了集群,某个节点还是被打爆了,其他节点却很闲。这就是数据倾斜——数据或访问没有均匀分布。倾斜分两种:数据量倾斜和数据访问倾斜,原因和应对方式都不一样。

数据量倾斜

数据量倾斜指某个或某几个节点存的数据明显多于其他节点。原因主要有三个。

Bigkey 导致的倾斜

某个 key 本身的 value 极大——一个 List 几百万元素、一个 Hash 几百万 field、一个 Set 几亿成员。这种 key 不管路由到哪个节点,都会把那个节点占满。

定位 bigkey:

redis-cli--bigkeys# 扫描所有 key,输出每种类型最大的几个

或者用MEMORY USAGE命令查看具体 key 的内存占用:

MEMORY USAGE user:profile:big_one

应对方式:拆分 bigkey

原 key: user:followers:1001 = Set(1000万成员) 拆分后: user:followers:1001:0 = Set(100万) user:followers:1001:1 = Set(100万) ... 拆成 10 份

读取时按规则去多个 key 聚合。代价是逻辑变复杂、跨节点查询多了,但解决了数据倾斜的根本问题。

Hash Tag 滥用

Redis Cluster 支持 Hash Tag——key 中{}内的部分用于计算槽位:

user:{1001}:profile → CRC16("1001") → 槽 X user:{1001}:orders → CRC16("1001") → 槽 X

这能把同一用户的多个 key 聚到同一节点,方便做跨 key 操作。但如果大量 key 用了相同的 Hash Tag,所有这些 key 都会落到同一个槽、同一个节点。

最常见的错误是用业务前缀做 Hash Tag:

order:{shop_1}:1001 order:{shop_1}:1002 order:{shop_1}:1003 ... 该店铺所有订单全到一个节点

如果店铺 1 是大客户,几百万订单全压在一个节点上。

应对方式:只在确实需要原子操作的 key 上用 Hash Tag,不要随意加。

槽分配不均

集群初始化或扩容时,如果手工分配槽,可能没分均匀。比如 4 个节点:

节点A:0-80008000个槽) 节点B:8001-120004000个槽) 节点C:12001-140002000个槽) 节点D:14001-163832000个槽)

A 节点承载的槽数是 D 的 4 倍,数据量自然倾斜。

应对方式:用redis-cli --cluster rebalancecluster reshard重新均匀分配。

数据访问倾斜

数据量分布均匀,访问量却不均匀。某个 key 被频繁访问,导致它所在的节点 CPU、网络打满,而其他节点很闲。这就是热 key 问题。

热 key 的来源

  • 明星动态:某条微博突然爆火。
  • 热销商品:双 11 的爆款。
  • 热点事件:突发新闻、明星八卦。
  • 抢购页面:限时优惠。

热 key 往往是业务侧无法预测的。

定位热 key

# Redis 4.0+ 支持redis-cli--hotkeys# 需要 maxmemory-policy 设为 allkeys-lfu 或 allkeys-lru

也可以通过MONITOR抽样观察请求分布,但 MONITOR 性能开销大,慎用。

应对方式:本地缓存

热 key 的访问大部分是只读的。在应用层加本地缓存(如 Caffeine、Guava Cache),让大部分请求在应用本地就返回,不走 Redis:

LoadingCache<String,String>localCache=Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(5,TimeUnit.SECONDS).build(key->redis.get(key));

短 TTL(几秒)能保证一致性,长 TTL 能更好挡流量。具体值看业务容忍。

应对方式:热 key 副本

把热 key 复制到多个 key,分散到多个节点:

原 key:hot_news_1001 副本: hot_news_1001:0,hot_news_1001:1,...,hot_news_1001:N

读取时随机选一个副本:

copy_id=random.randint(0,N)key=f"hot_news_1001:{copy_id}"

写入时所有副本都更新(牺牲一致性换取读性能)。

这个方案适合读多写少的热 key。如果是写多读少的(比如计数器),副本方案就不行了。

应对方式:客户端缓存(Redis 6.0)

Redis 6.0 引入了 Tracking 机制,服务端在 key 变更时主动通知客户端失效。客户端可以放心做本地缓存,不用担心读到旧数据。

CLIENT TRACKING ON GET hot_news_1001# 服务端会记住这个客户端读过这个 key# key 变更时主动通知失效

这是优雅的方案,但需要客户端库支持。

数据倾斜的预防

事后处理不如事前预防。架构设计阶段就要考虑:

  1. 避免单一维度的 key 设计:不要让所有 key 都按一个 ID 分布。
  2. 慎用 Hash Tag:只在跨 key 原子操作时用,不要为了"看起来整齐"加。
  3. 预估数据规模:业务上线前估算每个 key 的数据量,超过阈值的提前拆。
  4. 监控告警:每个节点的内存、QPS、CPU 都要监控,倾斜要及时发现。

实践建议

  1. 定期扫 bigkey--bigkeys命令成本不高,可以每周运行一次。
  2. bigkey 拆分要趁早:等到把节点压垮再拆,要做的工作多得多。
  3. Hash Tag 只用在必要的地方:默认不要加,要加之前问清楚为什么。
  4. 热 key 做本地缓存:成本最低收益最大的方案。
  5. 关键业务做读副本:电商首页的爆款商品、社交平台的热门内容。
  6. 集群扩容时重新均衡:扩容只是加节点,不会自动迁移数据,必须手动 rebalance。

数据倾斜是切片集群必然会遇到的问题。理解倾斜的两种来源——数据量和访问量——才能对症下药。bigkey 拆分、Hash Tag 谨慎使用、热 key 副本和本地缓存,构成了应对数据倾斜的工具箱。把这些手段用对,集群才能真正发挥"水平扩展"的价值。