不知道你有没有遇到过这样的场景:深夜正酣睡,忽然告警电话炸响------数据库 CPU 飙到 90%,服务濒临崩溃。排查后发现,不过是一个刚上线的活动页面,涌入了超出预期的流量。又或者,一名恶意用户拿着随机生成的 ID 疯狂请求,直接打穿了你的缓存层,数据库瞬间被拖垮。
这就是现实。企业级网站动辄每秒上万次查询,直接把所有压力甩给数据库无异于自掘坟墓。而 Redis,就是你手里的那张王牌。说白了,Redis 就是为扛住高并发而生的内存保险箱。
今天这篇,我们不谈高深莫测的源码,只聊如何把 Redis 用对、用好,让你的系统稳得像块磐石。
1. 重新认识 Redis:不只是个缓存
Redis(Remote Dictionary Server),可以简单理解成一个放在内存 里的数据库。因为数据在内存,读写速度比 MySQL 快几个数量级。它通过 Key-Value 的方式存取数据,同时支持丰富的结构:String、Hash、List、Set、Sorted Set。这些数据结构就是你解决各种并发难题的乐高积木。
但 Redis 的真正威力,在于它可以作为挡在数据库前面的盾牌:
markdown
用户请求 → Redis(查缓存) → 有数据,直接返回
→ 无数据,查数据库 → 回写 Redis
这就是经典的 Cache Aside 模式,也是解决 80% 性能问题的银弹。
2. 缓存三大"夺命坑":穿透、击穿、雪崩
只要用缓存,这三个问题绕不过去。但别怕,每一个都有对症的解法。
2.1 缓存穿透:查一个根本不存在的数据
恶意用户拿一个 ID=-1 无限请求,缓存里没有,数据库里也没有。每次请求都直达数据库,这就是穿透。
破解之道 :缓存空对象。
kotlin
public Item getItem(Long id) {
String cacheKey = "item:" + id;
Item item = redis.get(cacheKey);
if (item != null) {
return item;
}
// 缓存未命中,查数据库
item = db.query(id);
if (item == null) {
// 即使数据库也没有,也在 Redis 里存一个"空值标记",并设置较短过期时间
redis.set(cacheKey, EMPTY_OBJECT, 60); // 60 秒过期
return null;
}
redis.set(cacheKey, item, 3600);
return item;
}
你也可以配合布隆过滤器 提前拦截非法 Key,但空对象方案最简单有效。记住一点:宁缓存一个空,也别放过一次无效的数据库查询。
2.2 缓存击穿:热点数据突然过期
一个爆款商品的缓存刚好到期,瞬间成千上万的请求直接涌向数据库------这就是击穿。数据库能扛得住才怪。
破解之道 :互斥锁(Mutex) ,让第一个请求去加载数据库,其余请求排队等。
kotlin
public Item getHotItem(Long id) {
String cacheKey = "hot_item:" + id;
Item item = redis.get(cacheKey);
if (item != null) return item;
// 获取分布式锁,只让一个请求重建缓存
String lockKey = "lock:item:" + id;
try {
if (redis.setnx(lockKey, 1, 10)) { // 加锁,10秒自动过期
// 双重检查,避免在加锁间隙有其他线程已重建
item = redis.get(cacheKey);
if (item != null) return item;
item = db.query(id);
redis.set(cacheKey, item, 3600);
} else {
// 没抢到锁的线程,休眠一会儿后重试(或直接走本地缓存兜底)
Thread.sleep(50);
return getHotItem(id); // 递归重试
}
} finally {
redis.del(lockKey);
}
return item;
}
有人可能会说递归重试会卡线程,实际你可以返回一个旧值或降级数据,但核心思想不变:第一个冲进去的兄弟扛下所有,其他人等着乘凉。
2.3 缓存雪崩:大量数据同时过期
当你在同一时间点设置了大量缓存的相同过期时间,一旦到期,海量请求会像雪崩一样砸向数据库。
破解之道 :过期时间加随机盐。
ini
int baseExpire = 3600; // 基础1小时
int randomDelta = new Random().nextInt(600); // 随机0~600秒
redis.set(key, value, baseExpire + randomDelta);
这样缓存过期的时间被均匀打散,避免了集体失效。防雪崩的真谛就是------不要让大家的步调过于一致。
3. 分布式 Session:让用户登录不再"串台"
在分布式架构下,用户第一次登录被负载均衡打到服务器 A,第二次请求却到了服务器 B,B 上没有 Session,用户被迫重复登录------体验直接崩盘。
把 Session 存入 Redis,所有服务器共享同一份登录态:
dart
// 登录成功时
String sessionId = UUID.randomUUID().toString();
redis.setex("session:" + sessionId, 1800, userJson);
// 任意服务器校验
String userJson = redis.get("session:" + sessionId);
if (userJson != null) {
// 登录有效,重置过期时间
redis.expire("session:" + sessionId, 1800);
}
如此一来,用户状态不再与某台机器强绑定,水平扩展变得轻而易举。
4. Lua 脚本:保证操作的原子性
Redis 支持执行 Lua 脚本,且脚本在执行期间是原子的。这意味着你可以把"查询-判断-更新"这样多个步骤的操作打包,避免并发下的数据竞争。
比如一个抢优惠券的场景,避免超发:
lua
-- KEYS[1]: 优惠券库存 key, ARGV[1]: 用户ID
local stock = tonumber(redis.call('get', KEYS[1]))
if stock > 0 then
redis.call('decr', KEYS[1])
redis.call('sadd', 'coupon:users', ARGV[1])
return 1 -- 抢券成功
else
return 0 -- 已抢光
end
在 Java 中调用:
ini
String script = "..."; // 上面的 Lua 脚本
Long result = (Long) jedis.eval(script, Collections.singletonList("coupon:stock"),
Collections.singletonList(userId));
你会发现,用 Lua 脚本能轻松实现乐观锁、限流等复杂逻辑,而且性能极高。当你需要多个 Redis 命令同生共死时,请想起 Lua。
5. 高并发下的组合拳:多级缓存与预热
多级缓存
浏览器缓存 → CDN → Nginx 本地缓存 → Redis → 数据库。层层设防,每一层都能挡下一部分流量。常见做法是先用 Caffeine 在应用内做一级缓存(进程内,速度极快),Redis 做二级缓存,可以大幅降低 Redis 的压力。
热点数据预热
大促来临前,你肯定不会等到秒杀那一刻才去加载商品详情。提前把热数据灌进 Redis,并设置为常驻内存(过期时间设得很长或手动延期),流量洪峰到来时直接命中缓存,数据库毫发无伤。
6. 持久化:别让你的数据"一重启就蒸发"
Redis 数据在内存,一宕机就全丢。务必开启持久化:
- RDB(快照) :定期将内存全量快照写入硬盘。恢复速度快,但可能丢失最后一次快照之后的数据。
- AOF(追加日志) :记录每一个写操作命令,重启时通过回放日志恢复数据。数据更安全,但文件体积大,恢复慢。
生产环境通常会同时开启两者,用 RDB 做冷备,AOF 保底,牺牲一点写入性能换来数据不丢。
7. 高可用:主从 + 哨兵,宕机也不怕
单机 Redis 挂了就是灭顶之灾,所以你需要:
- 主从复制:一主多从,主负责写,从负责读。主从之间通过复制协议保持数据一致。
- 哨兵(Sentinel) :监控所有节点,一旦主节点失联,自动发起投票,将从节点提升为新主,并通知客户端切换连接。
如果你的数据量太大单机放不下,那就需要 Redis Cluster,它把数据分片存储在多台机器上,同时兼具高可用能力。
8. 内存不够用?淘汰策略来兜底
Redis 配置了最大内存后,当数据写满时,会根据淘汰策略清理部分数据。常见策略:
noeviction:拒绝写入,直接报错(生产不推荐)。allkeys-lru:在全体 Key 中,淘汰最近最少使用的(适合缓存场景)。allkeys-lfu:淘汰使用频率最低的(适合有冷热数据区分的场景)。volatile-ttl:在设置了过期时间的 Key 中,优先淘汰剩余存活时间最短的。
你可以在 redis.conf 里这样配:
maxmemory 4gb
maxmemory-policy allkeys-lru
选策略就像过日子:该丢的丢,把空间留给最有用的数据。
写在最后
Redis 用得好,它是性能怪兽、并发利器;用得不好,它也会成为数据的坟墓、故障的源头。记住下面这条心法:
缓存解决的是读多写少的场景,它不是银弹。
设计缓存时,永远先想好"怎么失效"和"怎么兜底"。
把这些实战技巧融入你的项目,你不仅能解决面试中八股文般的问题,更能真正交付一个稳定、抗造的系统。如果觉得有帮助,不妨点个赞收藏,咱们下篇见!