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-8000(8000个槽) 节点B:8001-12000(4000个槽) 节点C:12001-14000(2000个槽) 节点D:14001-16383(2000个槽)A 节点承载的槽数是 D 的 4 倍,数据量自然倾斜。
应对方式:用redis-cli --cluster rebalance或cluster 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 变更时主动通知失效这是优雅的方案,但需要客户端库支持。
数据倾斜的预防
事后处理不如事前预防。架构设计阶段就要考虑:
- 避免单一维度的 key 设计:不要让所有 key 都按一个 ID 分布。
- 慎用 Hash Tag:只在跨 key 原子操作时用,不要为了"看起来整齐"加。
- 预估数据规模:业务上线前估算每个 key 的数据量,超过阈值的提前拆。
- 监控告警:每个节点的内存、QPS、CPU 都要监控,倾斜要及时发现。
实践建议
- 定期扫 bigkey:
--bigkeys命令成本不高,可以每周运行一次。 - bigkey 拆分要趁早:等到把节点压垮再拆,要做的工作多得多。
- Hash Tag 只用在必要的地方:默认不要加,要加之前问清楚为什么。
- 热 key 做本地缓存:成本最低收益最大的方案。
- 关键业务做读副本:电商首页的爆款商品、社交平台的热门内容。
- 集群扩容时重新均衡:扩容只是加节点,不会自动迁移数据,必须手动 rebalance。
数据倾斜是切片集群必然会遇到的问题。理解倾斜的两种来源——数据量和访问量——才能对症下药。bigkey 拆分、Hash Tag 谨慎使用、热 key 副本和本地缓存,构成了应对数据倾斜的工具箱。把这些手段用对,集群才能真正发挥"水平扩展"的价值。
