在构建高并发后端系统时,Redis常被用作MySQL的前置缓存来分担数据库压力。然而一旦数据发生变更,内存中的缓存与磁盘中的数据库可能处于不同状态,这种不一致若处理不当就会引发用户看到过期订单、库存超卖等问题。本文从时序原理、典型方案对比以及生产落地细节三个角度,详细说明Redis与MySQL之间缓存一致性的主流实现方式。

缓存与数据库不一致的根本时序问题
要设计一致性方案,必须先理解读写并发时的时序竞争。假设有两个线程,线程A负责更新数据,线程B负责查询数据。若先更新MySQL再删除Redis,在A写库后、删缓存前的窗口内,B可能读取到旧缓存;若先删缓存再更新MySQL,在A删缓存后、写库完成前,B又会从库里读出旧值并重新填回缓存,导致缓存长期脏数据。这两种基础操作的顺序差异,决定了后续所有方案的补偿逻辑。
另一个容易被忽视的点是Redis删除本身的失败。网络分区或Redis实例抖动都可能让删除命令未生效,而此时MySQL已经提交,系统就陷入了“库新缓存旧”的状态。因此任何一致性方案都不能只依赖单次删除成功,必须有可重试或异步校验机制。这也是为什么在金融、电商核心链路中,单纯“写库后删缓存”常被认为不够稳妥。
从底层看,MySQL与Redis是两套独立存储,不支持跨系统事务。我们经常说的“一致性”其实是最终一致性,即经过短暂延迟后两者数据相同,而非强一致。明确这一点有助于在方案里合理引入消息队列、定时任务等组件来收敛不一致时间窗,而不是盲目追求瞬时同步。
主流缓存一致性方案及代码实现
先更新数据库再删除缓存(Cache Aside变形)是最常见做法。它的核心是写流程永远以MySQL为准,成功后Best Effort删除缓存;读流程未命中时回源数据库并写回缓存。该方案在并发写较少、读多写多场景下表现良好,但需防范删除失败。下面是一段简化的Java服务层示例:
// 更新用户信息并删除缓存
public void updateUser(User user) {
// 1. 先更新MySQL
userMapper.updateById(user);
// 2. 再删除Redis缓存,失败可记录日志由定时任务补偿
try {
redisTemplate.delete("user:" + user.getId());
} catch (Exception e) {
log.error("删除缓存失败,等待补偿任务处理", e);
}
}
// 查询用户,采用 Cache Aside 读模式
public User getUser(Long id) {
String key = "user:" + id;
User u = redisTemplate.opsForValue().get(key);
if (u == null) {
u = userMapper.selectById(id);
redisTemplate.opsForValue().set(key, u, 30, TimeUnit.MINUTES);
}
return u;
}
延时双删策略是在“先删缓存再更新库”基础上,写完成后延迟几百毫秒再次删除,用来清除写期间其他线程可能写入的旧缓存。该方案能显著降低脏缓存存活时间,但延迟时间需结合业务读写耗时评估,过短无效、过长浪费。另一种更优雅的做法是基于MySQL binlog的异步淘汰:通过Canal等工具订阅binlog,拿到行变更后在下游统一删缓存,将删除逻辑与业务代码解耦。
下面给出基于binlog思路的伪代码,展示如何订阅变更并删除缓存,业务代码因此无需关心缓存操作:
# 伪代码:消费binlog消息删除缓存
def on_binlog_event(event):
if event.type == 'UPDATE' or event.type == 'INSERT':
table = event.table
if table == 'user':
user_id = event.row['id']
# 删除对应缓存
redis.delete('user:' + str(user_id))
# 也可发送延时消息做二次删除
对比来看,先更新库再删缓存在代码上最简单,适合大部分中型系统;延时双删适合写后立刻有高并发读的场景;binlog异步方案运维复杂但最彻底,适合数据变更频繁且要求业务无侵入的架构。团队应根据人员能力与故障容忍度做选择。
生产环境中的补偿与监控设计
无论采用哪种方案,删除缓存失败都必须有补偿通道。最常见的是将失败Key写入消息队列或本地文件,由独立Worker定期重试。例如把user:123这样的键推送到延时队列,十秒后再次删除,若仍失败则告警人工介入。这样即使Redis瞬间不可用,也不会让脏数据永久留存。
监控层面,建议对缓存命中率、数据库与缓存不一致投诉量做看板。可以写脚本定期抽样比对MySQL行与Redis值,发现差异自动触发修复。同时在API层对关键读请求增加“强制回源”开关,当怀疑大范围不一致时,运维可一键让某些接口绕过缓存,直接读库并刷新缓存,快速止血。
此外还要注意缓存穿透与击穿对一致性判断的干扰。当大量请求查询不存在的数据,若直接回源并缓存空值,应与真实删除逻辑区分;热点Key失效时可用互斥锁或逻辑过期避免并发回源冲垮MySQL。把这些边界情况与一致性方案结合,才能构建出真正经得起大促考验的系统。
RedisMySQLcache_consistency修改时间:2026-08-15 03:18:32