【杂类】应对 MySQL 处理短时间高并发的请求:缓存预热
一、什么是缓存预热?
1. 核心概念
缓存预热(Cache Warm-up) 是指在系统正式对外提供服务之前,或某个高并发场景来临之前,主动将后续极有可能被访问的热点数据从数据库(MySQL)加载到缓存(如 Redis)中的过程。
2. 一个生动的比喻
- 没有预热:就像冬天开一辆停了一夜的车,发动机和车厢都是冰凉的。你一上来就猛踩油门(应对高并发),发动机负荷大,油耗高(数据库压力大),车内温度上升慢(响应慢,用户体验差)。
- 有预热:你提前 10 分钟远程启动了汽车,打开了暖风。等你上车时,发动机已经达到最佳工作温度,车厢内温暖如春。此时再开车,不仅发动机运行顺畅,你的体验也非常好。
缓存预热就是那个“远程启动”和“提前开暖风”的动作。
二、为什么需要缓存预热?(解决什么问题?)
在高并发场景下,如果缓存是冷的(空缓存),所有请求都会直接穿透(Cache Penetration)到数据库,导致:
- 数据库瞬时压力过大:MySQL 的连接数、CPU、IO 瞬间被打满,可能导致数据库僵死或响应极慢,引发雪崩效应。
- 首屏响应延迟:第一批用户访问时,需要等待数据从慢速的磁盘数据库读出并填入缓存,体验非常差。
- 缓存击穿风险:如果某个热点 key 失效,恰逢高并发请求到来,大量请求同样会瞬间压垮数据库。
缓存预热的目的就是避免这种“冷启动”问题,让系统一起跑就处于最佳状态。
三、如何实施缓存预热?(具体方案)
预热的时机和策略是关键。下图清晰地展示了三种主要的预热时机及其流程:
flowchart TDA[缓存预热时机] --> B["系统启动时<br>全量预热"]A --> C["定时任务<br>增量预热"]A --> D["热点数据<br>特殊关照"]B --> B1[加载全部热点数据] --> B2[写入Redis] --> B3["服务启动完成<br>缓存已热"]C --> C1["定时扫描<br>(如根据销量、访问量)"] --> C2[识别新热点数据] --> C3[写入Redis] --> C4["持续维护缓存热度"]D --> D1["业务预测<br>(如大促、秒杀)"] --> D2["提前加载特定<br>热点数据至Redis"] --> D3["甚至提前加载到<br>本地缓存(Guava)"] --> D4["极致性能准备"]
1. 实现方式
- 全量预热:适用于数据量不大且热点固定的场景(如商品分类、城市列表)。在服务启动时一次性加载所有热点数据。
- 增量预热:适用于数据量大或热点动态变化的场景。通过定时任务扫描业务库,识别出新热点(如最近24小时销量最高的商品、点击量最高的文章),并将其加载入缓存。
2. 代码示例(以 Spring Boot + Redis 为例)
方案一:应用启动时全量预热(使用 @PostConstruct
)
@Component
public class CacheWarmUpOnStart {@Autowiredprivate ProductService productService; // 业务服务@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 在Bean初始化后执行预热*/@PostConstructpublic void warmUpCache() {// 1. 从数据库查询热点数据列表(例如:所有上架的商品)List<Product> hotProducts = productService.getAllHotProducts();// 2. 遍历列表,将数据存入Redisfor (Product product : hotProducts) {String redisKey = "product:" + product.getId();// 通常使用Hash或String结构存储redisTemplate.opsForValue().set(redisKey, product, 12, TimeUnit.HOURS); // 设置TTL}System.out.println("缓存预热完成,共预热" + hotProducts.size() + "个商品数据");}
}
方案二:定时任务增量预热(使用 @Scheduled
)
@Component
public class ScheduledCacheWarmUp {@Autowiredprivate ProductService productService;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 每天凌晨2点执行,预热最新热点商品*/@Scheduled(cron = "0 0 2 * * ?")public void dailyWarmUp() {// 1. 查询最近24小时的热销商品或新上架商品List<Product> newHotProducts = productService.getRecentHotProducts(24);// 2. 更新缓存for (Product product : newHotProducts) {String redisKey = "product:" + product.getId();// 使用SET操作,如果已存在则更新redisTemplate.opsForValue().set(redisKey, product, 24, TimeUnit.HOURS);}}
}
四、预热策略与注意事项
- 数据筛选:不是所有数据都需要预热。只预热真正的热点数据,如首页商品、热门文章、高频查询的配置信息等。可以通过数据分析平台(如ELK)来识别热点。
- 缓存结构设计:选择合适的数据结构。例如:
- 使用 Hash 存储对象,方便更新部分字段。
- 使用 Sorted Set (ZSet) 存储排行榜数据。
- 过期时间(TTL):为预热的缓存设置合理的过期时间(如12-24小时),并配合定时任务重新预热,避免数据长期不更新。
- 避免脏数据:在数据更新时,要采用 “先写数据库,再删缓存” 的策略,确保缓存的一致性。下次请求时自然会从数据库拉取最新数据并回填缓存。
- 分级缓存:对于极端热点数据(如秒杀商品),可以预热到本地缓存(如 Caffeine、Guava Cache)中,速度比 Redis 更快,进一步减轻 Redis 和 MySQL 的压力。
- 监控与告警:对缓存命中率进行监控。如果命中率过低,告警提示可能需要检查预热任务或重新评估热点数据。
五、总结
核心要点 | 说明 |
---|---|
是什么 | 系统启动或高峰前,主动加载热点数据到缓存的过程。 |
为什么 | 防止冷启动时大量请求穿透到数据库,导致数据库崩溃。 |
何时做 | 1. 系统重启后 2. 每日低谷期(如凌晨) 3. 已知的高并发活动前(如大促、秒杀)。 |
怎么做 | 1. 全量预热:启动时加载所有热点。 2. 增量预热:定时任务更新热点。 3. 分级预热:本地缓存+分布式缓存。 |
关键点 | 只预热真热点、设置合理的TTL、保证缓存一致性、做好监控。 |
缓存预热是构建高并发、高可用系统的一道重要防线,它与缓存穿透、击穿、雪崩的解决方案结合使用,能极大地提升系统的稳定性和用户体验。