引言
Redis早已不是简单的缓存工具,而是后端系统的性能引擎。在高并发场景下,你不仅要会用get、set,更要深入理解其事务机制、性能优化手段和高可用架构,才能在流量洪峰中立于不败之地。本文将带你从底层到实战,全面进阶Redis,掌握那些"高级玩家"必懂的底层优化与架构实战。
一、Redis事务原理:从MULTI到WATCH
1.1 事务的基本使用
Redis通过MULTI、EXEC、DISCARD、WATCH等命令实现事务。一个典型的事务流程如下:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET user:name "张三"
QUEUED
127.0.0.1:6379> INCR user:age
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (integer) 1
在MULTI和EXEC之间的命令会依次进入命令队列,直到EXEC时才原子性地执行。
1.2 乐观锁:WATCH命令
Redis不支持传统意义上的回滚,但通过WATCH实现了乐观锁(CAS,Check-and-Set)。
WATCH user:balance
balance = GET user:balance
if balance >= 100:
MULTI
DECRBY user:balance 100
INCR user:order
EXEC
else:
UNWATCH
1.3 事务的ACID特性
- 原子性:命令队列中的命令要么全部执行,要么一个都不执行(但注意:如果某个命令在
EXEC时报错,整个队列不会执行;但如果执行过程中出现运行时错误,其他命令仍会继续执行,Redis不会回滚) - 一致性:依赖开发者保证
- 隔离性:事务执行期间,所有命令是串行执行的
- 持久性:只有在开启了AOF且
appendfsync=always时,事务才具备持久性
1.4 为什么Lua脚本更优?
从Redis 2.6开始,Lua脚本成为实现复杂原子操作的更佳选择。脚本在Redis服务器内以原子方式执行。
-- 扣减库存并创建订单
local stock = redis.call('get', KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
redis.call('decrby', KEYS[1], ARGV[1])
redis.call('rpush', KEYS[2], ARGV[2])
return 1
else
return 0
end
在Java中使用Jedis调用:
String script = "...";
List<String> keys = Arrays.asList("stock:iphone", "order:list");
List<String> args = Arrays.asList("1", "order:123");
Object result = jedis.eval(script, keys, args);
总结:对于简单的事务,MULTI/EXEC足够;对于需要逻辑判断的复杂场景,Lua脚本是首选。
二、性能优化进阶:Pipeline、Lua、BigKey、HotKey
2.1 Pipeline:批量操作,减少RTT
当需要连续执行多条命令时,Pipeline可以将它们打包一次性发送,显著减少网络往返时间(RTT)。
Jedis jedis = new Jedis("localhost");
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
pipeline.set("key" + i, "value" + i);
}
pipeline.sync(); // 发送所有命令
注意:Pipeline是非原子的,中间如果发生错误,后续命令仍会执行。如果需要原子性,请使用Lua或事务。
2.2 Lua脚本:原子性与性能的完美结合
Lua脚本不仅能实现复杂逻辑,还能减少网络开销。
for i, key in ipairs(KEYS) do
redis.call('incr', key)
end
return 'ok'
一次eval即可完成,性能远超多次incr调用。
2.3 BigKey:大Key的危害与处理
BigKey是指单个key存储的数据量过大(如String超过10KB,集合元素超过万级)。
危害:
- 阻塞Redis:读取/删除大Key耗时较长
- 网络拥塞:返回大量数据占用带宽
- 集群分片不均:在Cluster中可能导致数据倾斜
排查:
redis-cli --bigkeys
MEMORY USAGE key
处理:
- 拆分:将大集合拆分为多个小集合(如
hash:0、hash:1) - 压缩:对于大String,使用压缩算法(如snappy)存储
- 异步删除:使用
UNLINK替代DEL,在后台线程删除
2.4 HotKey:热点Key的应对策略
HotKey是指被大量请求访问的key,可能导致单个Redis节点CPU飙高。
识别:
redis-cli --hotkeys # 需要开启LFU淘汰策略
解决方案:
- 本地缓存:在客户端(如JVM)中缓存热点数据
- 多级缓存:结合CDN、本地缓存、Redis
- 读写分离:搭建主从,将读流量分散到从节点
- 分散热点:在key后加随机后缀(如
hotkey:1、hotkey:2)
三、缓存三大坑:穿透、击穿、雪崩的解决方案
3.1 缓存穿透
现象:查询一个根本不存在的数据,由于缓存不命中,每次请求都打到DB,可能压垮数据库。
解决方案:
- 缓存空对象:将不存在的数据也缓存起来,设置较短的过期时间(如5分钟)
- 布隆过滤器:将所有可能存在的key预先放入布隆过滤器
RBloomFilter<String> filter = redisson.getBloomFilter("userFilter");
filter.tryInit(100000, 0.03);
if (!filter.contains(userId)) {
return null;
}
// 再查询缓存/DB
3.2 缓存击穿
现象:一个热点key在失效的瞬间,大量请求同时穿透到DB。
解决方案:
- 互斥锁:只允许一个线程去DB加载数据
- 逻辑过期:不设置物理过期时间,而在value中保存逻辑过期时间
String lockKey = "lock:hotKey";
String requestId = UUID.randomUUID().toString();
try {
if (jedis.setnx(lockKey, requestId) == 1) {
jedis.expire(lockKey, 10); // 从DB加载数据
// 写入缓存
} else {
Thread.sleep(50); // 等待50ms后重试
}
} finally {
// 释放锁时校验value
}
3.3 缓存雪崩
现象:大量缓存key在同一时间段失效,或者Redis宕机,导致所有请求落向DB。
解决方案:
- 过期时间加随机值:
expire = base + random(0, 300) - 多级缓存:本地缓存 + Redis
- 限流降级:在DB层前增加限流组件
- 高可用部署:使用Redis集群
四、慢查询与内存碎片
4.1 慢查询日志
Redis的慢查询日志用于记录执行时间超过指定阈值的命令。
配置:
slowlog-log-slower-than 10000 # 微秒,超过10ms记录
slowlog-max-len 128 # 最多保存128条
查看:
127.0.0.1:6379> SLOWLOG GET 10
优化:对于频繁出现的慢命令(如KEYS *),应使用SCAN替代。
4.2 内存碎片
原因:频繁修改不同大小的数据,导致内存分配器无法完全复用。
查看:
127.0.0.1:6379> INFO memory
# Memory
used_memory: 2.0G
used_memory_rss: 2.8G
mem_fragmentation_ratio: 1.4
解决:
-
重启实例:最直接,但会短暂中断服务
-
开启自动碎片整理(Redis 4.0+):
activedefrag yes active-defrag-threshold-lower 10 active-defrag-cycle-min 25
五、高可用架构:Sentinel与Cluster详解
5.1 Redis Sentinel(哨兵)
作用:为主从架构提供自动故障转移、监控、通知。
核心原理:
- 监控:Sentinel定期向主从发送
PING - 主观下线:若某个Sentinel未收到主节点回复,则标记为"主观下线"
- 客观下线:当足够数量的Sentinel都认为下线,则进行"客观下线"
- 领导者选举:Sentinel之间通过Raft算法选出领导者
- 故障转移:领导者选出新主节点,修改其他从节点的复制目标
部署建议:至少3个Sentinel实例,且分布在不同机器上。
5.2 Redis Cluster(集群)
目标:实现数据自动分片、高可用,支持线性扩展。
核心概念:
- 哈希槽(Hash Slot) :16384个槽,每个key根据CRC16分配到某个槽
- 节点通信:节点间使用Gossip协议交换状态信息
- 客户端路由:客户端随机连接任一节点,若key不在该节点上,返回
MOVED重定向
搭建示例(6节点,3主3从):
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \
127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1
优势:自动分片,水平扩容;部分节点故障不影响整体服务。
局限:不支持跨节点的事务;批量操作要求所有key在同一个slot。
5.3 Sentinel vs Cluster 如何选择?
| 场景 | 推荐 |
|---|---|
| 数据量小,需要自动故障转移 | Sentinel + 主从 |
| 数据量大,需要水平扩展 | Cluster |
| 读写分离,读QPS极高 | Sentinel + 多从节点 |
| 对多key事务有强需求 | Sentinel(或单机) |
六、总结
Redis的世界远不止 get 和 set。从事务的原子性保障到Pipeline与Lua的性能优化,从BigKey与HotKey的治理到缓存三大坑的预防,再到慢查询与内存碎片的排查,最后到Sentinel与Cluster的高可用架构,每一个知识点都是走向Redis高阶玩家的必经之路。
在实际项目中,建议:
- 合理使用Lua脚本代替事务,减少网络开销
- 定期分析BigKey和HotKey,采取对应策略
- 监控慢查询和内存碎片,防患于未然
- 根据业务规模和可用性要求选择Sentinel或Cluster架构
只有深入底层原理,才能在面对复杂问题时游刃有余。希望本文能帮助你构建更系统的Redis知识体系,让Redis真正成为你项目中的性能发动机。
本文为原创内容,转载请注明出处。如果你在Redis应用中遇到问题,欢迎在评论区留言讨论!
搜索关注「卷毛的技术笔记」微信公众号,获取Redis深度解析与实战技巧,告别缓存陷阱,让系统性能飙升!