Redis 完结篇——Redis 热点问题在 Spring Boot 项目中实战

本次导航

  • 我专门做了个 Redis Mall 小 demo
  • 商品详情:Cache-Aside,改价先改库再删缓存
  • 穿透、击穿、雪崩在代码里长什么样
  • 下单:Lua 扣库存,别 GET 完再 SET
  • 限流、大 Key、热 Key,用 redis-cli 对着看

JDK 21,Spring Boot 3.5.9。数据库故意做成内存里的 ProductStore,每次查询睡 30ms 并计数,方便看"打到 DB 多少次"。

一、专门写了一个能点的项目

前面 Redis 系列文章,为了方便和使用 Redis 命令,我用的是 redis-cli 进行实操,SET、GET、Lua、Bloom、HOTKEYS,一条条敲过去,先练练手感。

这篇文章主要想从实际场景出发:缓存和库谁先写、热点失效谁去回源、库存能不能 GET 完再 SET、限流脚本能不能拆成两次网络。

所以我按商品中心的样子,专门写了 Redis Mall。不是再堆一页命令清单,是一个能打开浏览器、左边切场景、对错两套路径并排跑的工程。代码在:

github.com/iweidujiang...

欢迎大家 fork、star。

启动后打开 **http://localhost:8080**。

左侧七个菜单:商品与缓存、穿透、击穿、雪崩、超卖、限流、大 Key / 热 Key。正式商品和下单接口有分钟限流,场景按钮不限,方便把问题打出来。

预置了三个商品:1001 手机壳、1002 充电头、1003 数据线。库存写在 stock:{id}。

接口 干什么
GET /api/products/{id} 详情,带缓存
PUT /api/products/{id}/price?priceCents=3900 改价
POST /api/orders 下单扣库存
/api/scenarios/* 页面里各场景按钮打的对比接口

二、详情页:先 Redis,没有再库

打开 商品与缓存,点刷新。三张卡片上能看到售价、库存和缓存 TTL。改个价格后再刷新,TTL 会重新出现。

读路径在 ProductCacheService.get:

java 复制代码
public Optional<Product> get(long id) {
    String cacheKey = RedisKeys.product(id);
    String cached = redis.opsForValue().get(cacheKey);
    if (NIL.equals(cached)) {
        return Optional.empty();
    }
    if (cached != null) {
        return Optional.of(readJson(cached));
    }
    if (!mightExist(id)) {
        return Optional.empty();
    }
    return loadWithMutex(id, cacheKey);
}

__nil__ 表示查过、库里没有。真没有和"还没缓存"要分开,不然穿透会把数据库空跑穿。

第二次 dbHits 不该再涨。redis-cli 里:

bash 复制代码
GET product:1001
TTL product:1001

TTL 会落在 300 到 360 秒之间。写入时加了一点随机数,避免同一批商品同一秒过期。

改价走另一条:先改 ProductStore,再 DEL product:{id}。不要先改缓存再改库------库失败了,Redis 里就是假价。

java 复制代码
public Product updatePrice(long id, int priceCents) {
    Product current = store.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("商品不存在: " + id));
    Product updated = current.withPrice(priceCents);
    store.save(updated);
    redis.delete(RedisKeys.product(id));
    return updated;
}
bash 复制代码
curl -X PUT "http://localhost:8080/api/products/1001/price?priceCents=3900"

cli 里 GET product:1001 应该已经没了,再 GET 接口,会回源库、把 3900 写回去。删缓存后到下一次回填之间,并发读可能把旧值又 SET 回去,电商详情一般能接受这几毫秒;库存、余额别用这套。

三、穿透:没有的东西别去烦数据库

扫 product:1 到 product:9999999,库里本来就没有。项目里两道闸:

  1. 启动时把真实 ID 灌进布隆过滤器 bf:product_id(Redis 8 官方镜像自带 Bloom)。BF.EXISTS 说没有,接口直接返回,不碰库。
  2. 布隆说可能有、库查空了,缓存 __nil__,30 秒。

打开 缓存穿透,查 99999,左侧布隆状态、查询结果和 cache-meta 会写在同一块日志里。

cli:

bash 复制代码
BF.EXISTS bf:product_id 99999
GET product:99999
TTL product:99999

过滤器返回 0 的话,连 __nil__ 都不会写,这是更干净的情况。模块没有时,代码会退回只靠空值缓存,日志里有一句 Bloom 不可用。

四、击穿:热点失效,别让所有人回源

ProductStore.findById 每次 +1 并 sleep 30ms。打开 缓存击穿:左侧无锁、右侧有锁,各跑一遍。

无锁时 dbHits 会接近 20;有锁应掉到 1 附近。数字当场出来,比口头讲"加一把互斥锁"管用。对应接口是 POST /api/scenarios/stampede。

解锁用 lua/unlock.lua:对得上自己的 token 才 DEL,避免锁过期后误删别人的。

SET NX EX 3 只是回源互斥,不是跨机房的 Redlock。单实例缓存够用。

五、雪崩:别让一批 Key 同一秒过期

打开 缓存雪崩 。左侧 20 个 Key 都写成 30 秒,distinctTtl 会是 1;右侧走正式回填那套 300 到 360 秒随机,剩余时间会散开。Key 名叫 mall:av:1 到 mall:av:20。

这和商品详情写入时加抖动是同一件事。详情页改价后再刷新,TTL 也不会钉死在 300。

六、下单:库存不能先读后写

打开 库存超卖 ,先跑 GET 再 SET。库存 10、20 个线程各买 1 件,sold 往往会大于 10。

再跑 Lua,sold 应是 10,剩余 "0"。下面还可以走正式下单:商品锁 + Lua。接口:POST /api/scenarios/stock/race。

Lua 把判断和扣减捆在一次执行里:

1:7:D:\a-github-project\redis-mall\src\main\resources\lua\decr_stock.lua 复制代码
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
local buy = tonumber(ARGV[1])
if stock < buy then
    return -1
end
redis.call('DECRBY', KEYS[1], buy)
return stock - buy

正式下单还在 Lua 外面加了一把 lock:order:{id}:同一商品的下单入口串行一下,后面如果要写订单表、发消息,不会和扣库存缠在一起。

解锁同样走那段校验 Lua。

七、限流:别把 Redis 当免费的无限入口

/api/products/** 和 /api/orders/** 走 RateLimitInterceptor。

脚本是 INCR,第一次带上 60 秒过期,超过每分钟 60 次返回 429。

页面 接口限流 会连发 70 次详情,超过每分钟 60 次会出现 429。

cli:

bash 复制代码
GET rate:ip:127.0.0.1
TTL rate:ip:127.0.0.1

INCR 和 EXPIRE 必须在同一段脚本里。

拆成两次请求,进程挂在中间,这个计数 Key 可能永不过期,限流等于失效。

八、大 Key 和热 Key

打开 大 Key / 热 Key,点生成和打热即可。左边写出 2 万个 field 的 Hash,右边连读 500 次手机壳。

命令行对照:

bash 复制代码
MEMORY USAGE mall:big:hash
UNLINK mall:big:hash

不要 DEL。元素一多,主线程同步释放内存会卡。UNLINK 立刻返回,后台慢慢收。

热 Key 打完后,Redis 8.6:

bash 复制代码
HOTKEYS START METRICS 2 CPU NET
HOTKEYS GET
HOTKEYS STOP

列表里大概率能看到 product:1001。

线上热点不要只靠互斥回源,详情还可以在应用里加 Caffeine,或者把同一个商品拆成几个副本 Key 随机读。

慢查询也是老问题。应用里已经用了 Lettuce 连接池(max-active: 16)。

不要在代码或运维脚本里 KEYS *,用 SCAN,SLOWLOG GET 能看见谁拖慢了主线程。

连接打满通常不是"再加大 max-active"就能完:先看有没有大 Key、慢 Lua、没有超时的命令把连接占住。

timeout: 3s 和 max-wait: 2s 就是给这种时候用的。

九、项目里建议默认带上的几条

  • 读:Redis → 空标记 / 布隆 → 互斥回源 DB → 回填,TTL 加随机
  • 写:先 DB,再删缓存
  • 库存、余额:Lua,禁止 GET 完再 SET
  • 锁:SET NX EX + 按 token 删除;回源互斥和业务锁都这样
  • 限流:INCR 和 EXPIRE 写在同一段脚本
  • 删除大结构:UNLINK
  • 连接池要有上限和等待超时;慢查询定期看

这些不是新命令,是进 Spring 项目之后最容易省掉的那几步。

Redis Mall 把它们收进同一套页面里,方便以后对照着改自己的业务。

十、系列到这里

最后落到 Redis Mall 这个商品中心。前面把命令练熟,这篇把同一套问题放进 Spring 项目里跑一遍。

Redis 能当缓存、队列、锁、限流器、甚至向量检索,但每一个都要记得:它是单线程执行的内存服务,大 Key、热 Key、慢脚本、错误的过期策略,都会把整站拖进同一条阻塞队列。

感兴趣的同学可以 fork、star 该项目:

github.com/iweidujiang...


本次导航结束。Redis 系列完结,欢迎 关注、点赞、转发。

相关推荐
高频因子挖掘机3 小时前
只取最近 60 个交易日,却每次拉全历史:量化脚本变慢的原因与改法
后端·github·api
孟健4 小时前
从5000亿Token到83%字节匹配:AI逆向工程与重构的工程闭环实践
后端·架构·ai编程
码事漫谈4 小时前
丢得静悄悄,谈一谈 Agent 的记忆是怎么没的
后端
迪飞特科技5 小时前
智慧物业平台实战:Spring Boot 整合小程序实现报修巡检费用全流程
spring boot·后端·小程序
考虑考虑5 小时前
@JsonInclude注解使用
spring boot·后端·spring
IT_陈寒5 小时前
Vite的这个问题坑了我三天,原来如此解决!
前端·人工智能·后端
网易云信5 小时前
大模型能上岗做销售吗?我们让 12 款国产模型交了 420 份作业
前端·后端
CCblandy5 小时前
HTTP 长连接 3 类故障实测:[WinError 10053]、99 个 TIME_WAIT、69 次多余握手,4 种对策怎么选
后端·python·网络协议·http·云原生·性能优化·架构
蜗牛互联网5 小时前
Python Agent工具调用的白名单、参数校验与审批门禁
java·服务器·前端·人工智能·后端·python