缓存预热系统在系统刚启动时,如何平滑地将数据加载到Redis中,避免瞬间流量拖垮数据库?
缓存预热(Cache Warming)在系统启动或大促前非常关键。如果预热策略简单粗暴(例如一次性 SELECT * 然后全部写入 Redis),会瞬间产生极高的数据库 IO 和 CPU 负载,甚至直接将数据库打挂。
为了平滑地将数据加载到 Redis,避免拖垮数据库,通常需要从数据筛选、加载速率控制、批量写入、架构解耦等几个维度进行设计。以下是具体的落地策略:
一、 核心原则:只预热“热数据”
不要试图把数据库里的所有数据都塞进 Redis。内存很贵,且全量预热不仅慢,还会对 DB 造成毁灭性打击。
- 按业务规则筛选:只加载近期活跃用户、热门商品、即将开始的秒杀库存等。
- 按数据量截断:例如只查询访问排行榜前 10 万条的数据。
二、 平滑读取 DB 的策略(防 DB 宕机)
1. 游标分批拉取(Chunking)
绝对不能使用一次性的大查询。必须分批次(Batch)从数据库拉取数据。
- ❌ 错误做法:
SELECT * FROM users WHERE status = 1 - ❌ 性能差的分批:
LIMIT 10000, 1000(深度分页会导致数据库扫描大量无效行,越往后越慢)。 - ✅ 正确做法(基于主键游标):
每次记录上一批的最大 ID,下一次查询基于此 ID。SELECT * FROM users WHERE id > #{last_max_id} AND status = 1 ORDER BY id ASC LIMIT 1000
2. 速率控制与限流(Throttling)
在代码中显式控制拉取 DB 的速率,不要让程序“跑得太快”。
- 简单休眠(Sleep):在每次处理完一批数据后,强制休眠几十到几百毫秒。java
while(hasMoreData) { List<Data> list = db.query(lastId, 1000); writeToRedis(list); Thread.sleep(100); // 核心:给 DB 喘息的时间 } - 令牌桶限流(RateLimiter):使用 Guava 的
RateLimiter或 Sentinel,严格控制每秒查询 DB 的 QPS。例如,限制预热程序每秒最多向 DB 发起 50 次查询。
3. 动态感知与自适应(高阶)
在预热过程中,实时监控数据库的健康度(如 CPU 使用率、慢 SQL 数量)。
- 如果 DB 响应时间(RT)飙升,预热程序自动降低拉取频率或增大 Sleep 时间。
- 如果 DB 毫无压力,可以适当加快速度。
三、 高效写入 Redis 的策略(防 Redis/网络 拥堵)
虽然 Redis 性能极高,但如果一条一条执行 SET,网络往返时间(RTT)会极大地拖慢预热速度,并占用大量连接。
1. 使用 Pipeline 或批量命令
- MSET / HMSET:如果数据结构简单,可以使用批量写入命令。
- Redis Pipeline(管道):最佳实践。将几百条写命令打包成一个网络请求发送给 Redis,极大地减少网络开销。java
// 伪代码 Pipeline pipeline = redis.pipelined(); for (Data d : batchList) { pipeline.set(d.getKey(), d.getValue()); } pipeline.sync(); // 一次性提交
2. 必须引入 TTL “时间打散”(防止雪崩)
这是一个极其容易踩坑的点!
如果你在预热时,给所有数据设置了相同的过期时间(例如 24 小时),那么 24 小时后,这些热点数据会在同一秒钟全部过期,导致下一次严重的“缓存雪崩”。
- ✅ 解决办法:在基础过期时间上,加上一个随机值。
TTL = 24小时 + Random(0 ~ 3600秒)
四、 架构层面的实施方案(在什么时候做?)
为了不影响系统正常启动,预热任务的触发时机和执行位置很有讲究:
方案 A:异步后台预热(推荐)
应用正常启动,不阻塞服务提供。启动后触发一个异步线程池去慢慢预热。
- 优点:系统启动快。
- 缺点:刚启动的前几分钟,可能仍有部分请求穿透到 DB。可以通过网关层的灰度发布(缓慢放量)来配合解决。
方案 B:独立任务调度(最佳实践)
不要在业务应用的启动代码里写预热逻辑。使用独立的 Job 系统(如 XXL-JOB, ElasticJob,或单独的预热微服务)来做这件事情。
- 流程:系统发布前,先手动或自动触发预热 Job -> Job 平滑搬运数据到 Redis -> 确认 Redis 就绪 -> 启动/重启业务应用。
- 优点:与业务应用完全解耦,不影响应用启动,Job 可以随时起停,方便重试。
方案 C:应用启动拦截(Spring Boot Runner)
利用 Spring Boot 的 ApplicationRunner。在数据预热完成前,不把当前节点挂载到注册中心或负载均衡器上。
- 优点:保证只要有流量进来,缓存一定是热的。
- 缺点:如果数据量大,应用启动时间可能长达十几分钟,影响 CI/CD 发布效率。
五、 兜底与保护机制
- 预热幂等性:预热脚本可能被多次触发,必须保证多次执行不会产生脏数据(通常使用
SET覆盖即可)。 - 设置断路器:如果预热过程中,发现 DB 大量报错,预热脚本必须立刻中断(Fail-fast),并发出告警,绝对不能带病强刷。
- 缓存击穿保护:即使做了预热,也可能漏掉部分数据。应用代码查询 DB 时,必须加上互斥锁(如 Redis 分布式锁
SETNX)或本地 Caffeine 缓存做兜底,确保万一缓存没预热到,也不会有成千上万的并发请求直接打到底层 DB 上。
总结流程图:
触发预热 -> 游标分页查DB (LIMIT 1000) -> 代码休眠 100ms -> 构建 Redis Pipeline -> 附加随机 TTL -> 批量写入 Redis -> 循环直至结束 -> 业务应用接入流量。