目录
[《Redis 经典应用场景(一):缓存、分布式锁与限流》](#《Redis 经典应用场景(一):缓存、分布式锁与限流》)
[4.1 热点数据缓存:别让数据库裸奔!](#4.1 热点数据缓存:别让数据库裸奔!)
[Redis 怎么做缓存?](#Redis 怎么做缓存?)
[坑1:缓存穿透 ------ 专查"不存在"的数据](#坑1:缓存穿透 —— 专查“不存在”的数据)
[坑2:缓存击穿 ------ 热点 key 刚好过期](#坑2:缓存击穿 —— 热点 key 刚好过期)
[坑3:缓存雪崩 ------ 大批 key 同时过期](#坑3:缓存雪崩 —— 大批 key 同时过期)
[4.2 分布式锁:多个服务别抢同一个资源!](#4.2 分布式锁:多个服务别抢同一个资源!)
[4.3 计数器与限流:别让系统被薅秃了!](#4.3 计数器与限流:别让系统被薅秃了!)
1. 热点数据缓存:别让数据库裸奔!
为啥要用缓存?
想象一下,你开了个小饭馆,每天中午12点一到,几十号人同时冲进来点"招牌红烧肉"。如果你每次都现杀猪、现炖肉,那肯定忙死,客人也等疯了。
数据库就像那个"厨房"------它能干活,但扛不住高并发猛砸。而 Redis 就是你提前炖好、放在保温桶里的红烧肉。客人一来,直接从桶里舀一碗,又快又稳。
缓存的核心思想就一句:把频繁读、不常变的数据,提前存到内存里,省得每次都去慢速的数据库里翻。
Redis 怎么做缓存?
最简单的套路:
// 伪代码
String data = redis.get("user:1001");
if (data == null) {
// 缓存没命中,去数据库查
data = db.query("SELECT * FROM user WHERE id=1001");
// 查到后塞进 Redis,设个过期时间,比如5分钟
redis.setex("user:1001", 300, data);
}
return data;
看起来很简单对吧?但实际用起来,坑特别多。
坑1:缓存穿透 ------ 专查"不存在"的数据
比如有人恶意刷 user:-1、user:999999999,这些 ID 根本不存在。每次请求都会绕过缓存,直冲数据库。查一万次,数据库就崩了。
解决办法:
- 空值缓存 :查不到也往 Redis 里塞个空值(比如
"null"),设个短过期时间(比如30秒)。下次再查,直接返回空,不查库。 - 布隆过滤器(Bloom Filter):提前把所有合法 ID 存进布隆过滤器。请求来了先问它:"这 ID 存在吗?"如果回答"不存在",直接拒绝,连 Redis 都不用查。
小贴士:空值缓存别设太久,否则垃圾数据占内存;布隆过滤器有误判率(可能把不存在的说成存在),但绝不会把存在的说成不存在,适合做"第一道防线"。
坑2:缓存击穿 ------ 热点 key 刚好过期
比如"双11"首页 banner 图,缓存5分钟。结果第5分01秒时,10万用户同时刷新,发现缓存没了,全涌向数据库查 banner。数据库瞬间被压垮。
解决办法:
-
互斥锁(Mutex Lock) :第一个发现缓存失效的线程,去数据库加载数据,同时给 Redis 加个"加载中"的标记(比如
setnx banner:loading 1)。其他线程看到这个标记,就等一会儿再试。 -
逻辑过期(不设 TTL) :缓存里不仅存数据,还存一个"逻辑过期时间"。比如:
{ "data": "...", "expireAt": 1710000000 // Unix 时间戳 }线程读到后,发现过期了,就异步起个线程去更新缓存,自己先返回旧数据。用户体验无感,数据库压力也小。
坑3:缓存雪崩 ------ 大批 key 同时过期
比如你给所有缓存都设了 30 分钟过期。结果半夜3点,几万个 key 同时失效,所有请求打到数据库,直接雪崩。
解决办法:
- 过期时间加随机值 :基础时间 + 随机偏移。比如
30分钟 + random(0~5分钟)。让 key 分批过期,别扎堆。 - 高可用架构:Redis 集群 + 主从 + 哨兵,别单点部署。
- 服务降级:实在扛不住时,返回兜底数据(比如"系统繁忙,请稍后再试"),保住数据库。
2. 分布式锁:多个服务别抢同一个资源!
问题场景
你有个秒杀活动,库存只有100件。现在有3台服务器同时处理请求。如果不加锁,可能三台都读到库存=100,各自减1,结果卖出去300件------超卖了!
单机用 synchronized 或 ReentrantLock 就行,但分布式环境下,锁必须"全局唯一",Redis 就能干这活。
最简实现(但有坑!)
# 尝试加锁
SET lock:order true EX 30 NX
NX:只有 key 不存在时才设置(相当于"占座")EX 30:30秒后自动过期,防止死锁
看起来完美?但有两个大问题:
问题1:锁过期了,业务还没跑完
比如你设了30秒锁,但业务逻辑跑了35秒。第31秒时,锁自动释放了。另一个线程趁机加锁,开始执行。结果两个线程同时操作库存------锁失效了!
解决:给锁续命(看门狗机制)
- 加锁成功后,起一个后台线程,每10秒检查一次:如果业务还在跑,就把锁的过期时间重置为30秒。
- Java 里可以用 Redisson 的
RLock.lock(),它内置了 watchdog。
问题2:谁加的锁,谁才能解
假设线程A加了锁,但因为 GC 停顿太久,锁过期了。线程B趁机加锁。这时线程A恢复,执行 DEL lock:order ------ 把线程B的锁删了!灾难!
解决:锁要带"身份证"
# 加锁时,value 设为唯一ID(比如UUID)
SET lock:order "uuid-123" EX 30 NX
# 解锁时,先判断 value 是不是自己的
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
这段 Lua 脚本保证了"判断+删除"是原子的,避免误删。
总结:别自己造轮子!直接用 Redisson、Lettuce 这些成熟库,它们把续命、防误删、可重入都实现了。
3. 计数器与限流:别让系统被薅秃了!
场景1:接口限流(防刷)
比如登录接口,限制每分钟最多10次请求。超过就返回"操作太频繁"。
Redis 的 INCR 命令天生适合做计数器,而且快!
简单限流(固定窗口):
# 每次请求
count = INCR user:1001:login:202404011200 # key 包含分钟时间
EXPIRE user:1001:login:202404011200 60 # 60秒后自动清零
if count > 10:
return "请求太频繁"
但固定窗口有漏洞:比如 12:00:59 刷了10次,12:01:00 又刷10次,实际1秒内20次!所以更推荐 滑动窗口 或 漏桶/令牌桶。
场景2:滑动窗口限流(更平滑)
用 Redis 的 ZSET(有序集合) 实现:
-
每次请求,把当前时间戳(毫秒)作为 score,插入 ZSET。
-
然后删掉 60 秒前的所有记录(
ZREMRANGEBYSCORE key 0 <now-60000>) -
再看 ZSET 长度,如果 > 100,就拒绝。
伪代码
now = timestamp_ms()
key = "rate_limit:user:1001"ZADD key now now
ZREMRANGEBYSCORE key 0 (now - 60000)
count = ZCARD keyif count > 100:
return "限流"
这个方案能精确控制"任意60秒内不超过100次",比固定窗口更合理。
场景3:库存扣减(防超卖)
秒杀时,库存=100。1000人抢,怎么保证不超卖?
错误做法:
int stock = redis.get("stock");
if (stock > 0) {
redis.decr("stock"); // 危险!非原子操作
}
两个线程同时读到 stock=1,都以为能卖,结果库存变成 -1。
正确做法:用 Lua 脚本保证原子性
-- check_and_decr.lua
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
return redis.call('DECR', KEYS[1])
else
return -1 -- 库存不足
end
调用:
Long result = redis.eval(luaScript, 1, "stock");
if (result > 0) {
// 扣减成功,继续下单
} else {
// 卖光了
}
核心思想:"读+判断+写"必须在一个命令里完成,不能拆开!
