引言在高并发系统中两个问题非常常见MySQL 和 Redis 如何尽量保持数据一致分布式场景下应该选择 Redis 锁还是 ZooKeeper 锁这两个问题本质上都和“状态一致性”有关。前者关注数据库与缓存之间的数据同步后者关注多个节点之间的互斥访问控制。本文将把这两部分内容放在一起梳理重点说明更新数据时缓存一致性为什么容易出问题常见方案分别解决了什么问题Redis 分布式锁和 ZooKeeper 分布式锁各自适合什么场景分布式锁为什么会失效以及常见解决思路是什么如何保证 MySQL 和 Redis 数据一致性先说结论在使用缓存时真正需要重点关注一致性的通常是“更新”和“删除”场景而不是“新增”场景。因为新增数据时通常只需要先写入 MySQL后续查询不到缓存时再回源加载即可而更新和删除场景中旧缓存很容易和新数据库内容不一致。先删 Redis再更新 MySQL会有什么问题这是一种很多人第一反应会想到的方案先删除 Redis 缓存再更新 MySQL看起来逻辑很顺但在高并发场景下会有一个经典问题。假设有两个线程线程 A删除 Redis 成功准备更新 MySQL线程 B在线程 A 更新 MySQL 之前读取数据这时线程 B 会发现缓存不存在于是去 MySQL 里查数据。由于线程 A 还没有更新完数据库线程 B 读到的还是旧值然后线程 B 又把旧值重新写回 Redis。结果就是MySQL 里是新值Redis 里又变成了旧值这就产生了缓存脏数据问题。延迟双删是什么为了解决上面的问题常见思路是“延迟双删”。典型流程如下先删除 Redis再更新 MySQL等待一小段时间再次删除 Redis这样做的目的是防止并发线程在数据库更新期间把旧数据重新写回缓存如果确实有线程在这个时间窗口里把旧值写回去了那么第二次删除可以把这份脏缓存再清掉。延迟双删的问题延迟双删不是银弹核心难点在于“延迟多久”。如果睡眠时间太短可能数据库主从同步、业务回写缓存等动作还没完成第二次删除过早执行脏数据仍可能再次进入缓存如果睡眠时间太长一致性窗口可能更小但接口耗时会明显变长影响系统吞吐和响应时间所以延迟双删只能缓解问题不能 100% 保证强一致。先更新 MySQL再删除 Redis是更常见的做法相比“先删缓存再更新数据库”工程上更常见的是先更新 MySQL再删除 Redis这样做的原因是数据库通常是最终事实来源先把数据库改对再让缓存失效后续读请求自然会从数据库加载新值这种方案整体上更符合大多数业务系统的设计习惯所以也是更常见的实践。如果删除 Redis 失败怎么办即使采用“先更新 MySQL再删除 Redis”的策略也有一个不能回避的问题如果数据库更新成功了但 Redis 删除失败怎么办这时数据库和缓存仍然会不一致。方案一通过 MQ 做失败重试常见做法是先更新 MySQL删除 Redis如果删除失败就把重试任务投递到 MQ消费端异步重试删除缓存这种方式的优点是时效性较强适合高并发系统可以把失败处理和主流程解耦这也是工程上比较推荐的方案。方案二通过 Canal 订阅 binlog 同步缓存另一种思路是只更新 MySQL使用 Canal 订阅 MySQL binlog根据 binlog 变更去删除或更新 Redis这个方案的优点是业务代码侵入性低数据变更链路更统一但它的缺点也比较明显链路更长时效性通常不如直接删缓存或 MQ 重试运维复杂度更高各方案优缺点怎么理解可以把常见方案简单归纳为下面三类。方案一延迟双删优点思路简单实现门槛低缺点增加接口耗时对睡眠时间敏感不能 100% 消除不一致窗口方案二更新 MySQL 后删除 Redis失败时通过 MQ 重试优点效率较高实时性较好更适合生产环境缺点需要额外引入 MQ 和重试机制需要处理消息可靠性和幂等问题方案三Canal 监听 binlog 同步缓存优点业务层改动较少变更同步链路集中缺点时效性相对弱一些部署和维护成本更高如果只看大多数业务系统的综合权衡通常会更倾向于“先更新 MySQL再删除 Redis删除失败时通过 MQ 补偿重试。”新增数据时要不要处理 Redis 一致性一般来说新增数据场景不需要像更新和删除那样重点处理缓存一致性。常见做法是直接写 MySQL不主动写缓存等后续读请求触发缓存加载因为这里不存在“旧缓存残留”的问题所以复杂度会低很多。Redis 分布式锁和 ZooKeeper 分布式锁有什么区别这两种方案都能实现分布式锁但它们的设计基础不同因此在性能、释放机制和一致性方面差异明显。性能RedisRedis 基于内存操作读写性能非常高在高并发场景下通常比 ZooKeeper 更快。因此如果你的核心诉求是高吞吐低延迟更轻量的接入成本Redis 分布式锁通常会更有优势。ZooKeeperZooKeeper 的锁实现通常依赖节点创建、删除和会话管理这些操作需要经过一致性协议协调因此整体性能通常不如 Redis。所以从纯性能角度看Redis 更强ZooKeeper 更稳锁释放机制RedisRedis 锁通常依赖过期时间来防止死锁。这意味着你在加锁时需要考虑锁超时时间设置多长业务执行时间是否可能超过这个时间如果超时时间太短业务还没执行完锁就自动释放了如果超时时间太长客户端异常退出后其他线程需要等较久才能重新获取锁。很多实现会配合 Watch Dog 自动续期机制尽量减少这类问题。ZooKeeperZooKeeper 通常基于临时节点实现锁。当客户端会话断开比如进程崩溃机器宕机网络断开导致会话失效ZooKeeper 会自动删除对应的临时节点从而自动释放锁。这也是 ZooKeeper 锁在可靠性上非常重要的一个优势。可靠性和一致性RedisRedis 更强调性能和可用性但在主从切换、复制延迟、故障恢复等场景下锁的严格一致性不如 ZooKeeper。因此在一些对锁可靠性要求非常高的场景里Redis 锁需要格外谨慎设计。ZooKeeperZooKeeper 是强一致协调组件天然更适合做分布式协调选主配置管理高可靠分布式锁所以如果业务对锁的正确性要求极高ZooKeeper 往往更稳妥。分布式锁为什么会失效无论是 Redis 锁还是其他实现锁失效问题都要重点关注。常见锁失效原因自动续期失败如果使用的是带自动续期机制的 Redis 锁比如 Redisson Watch Dog那么在下面这些场景里可能续期失败客户端宕机网络异常Redis 压力过大线程长时间阻塞一旦续期失败锁可能在业务执行完成前提前过期。Redis 键过期或被误删Redis 锁本质上还是一个键。如果过期时间配置不合理被其他代码误删那么锁就会提前失效。Redis 集群故障或主从切换如果 Redis 发生主从切换、复制延迟或部分节点故障锁状态可能出现短暂不一致。这也是 Redis 分布式锁争议最大的地方之一。没有正确释放锁如果业务执行时抛出异常而代码又没有在finally中执行unlock()就会导致锁释放异常。所以正确写法通常都是lock.lock();try{// 业务逻辑}finally{lock.unlock();}锁自动过期时间和业务执行时间不匹配如果锁超时时间短于实际业务执行时间那么业务还没做完锁就已经失效其他线程可能会再次获取锁。这会直接破坏互斥性。如何解决主节点宕机导致的锁失效问题如果你仍然希望使用 Redis 锁又想降低单点主节点故障带来的风险一个常见思路是 RedLock。RedLock 的基本思路RedLock 的核心思想是不依赖单个 Redis 实例而是依赖多个彼此独立的 Redis Master 节点共同判定是否加锁成功通常建议使用奇数个节点比如5个。这些节点之间相互独立不依赖主从复制不依赖单一协调者RedLock 的加锁流程一个典型流程如下客户端依次向多个 Redis 实例申请同一把锁加锁时使用相同的 key 和随机值每次请求都设置较短的超时时间避免某个实例挂掉后长时间等待客户端统计成功加锁的实例数量和总耗时只有在“超过半数节点加锁成功”并且“总耗时小于锁有效期”时才认为加锁成功例如当有 5 个节点时至少要在 3 个节点上获取成功才算真正持有锁。如果最终获取失败就应该主动去所有节点执行解锁哪怕某些节点其实根本没有加锁成功。RedLock 的问题要怎么看RedLock 的目标是提升 Redis 锁在故障场景下的可用性和可靠性但它并不等于“绝对正确”。所以在工程上通常可以这样理解如果你要的是高性能分布式锁Redis 或 Redisson 仍然很常见如果你要的是更强的协调一致性ZooKeeper 往往更适合也就是说RedLock 是一种增强方案但不是所有场景下的最终答案。Redisson RedLock 示例下面是一个常见写法RLocklock1redissonInstance1.getLock(lock1);RLocklock2redissonInstance2.getLock(lock2);RLocklock3redissonInstance3.getLock(lock3);RedissonRedLocklocknewRedissonRedLock(lock1,lock2,lock3);lock.lock();try{// 业务逻辑}finally{lock.unlock();}它的核心思路就是同时在多个独立 Redis 节点上尝试加锁只要大多数节点成功就认为整体加锁成功实际选型怎么做如果你的业务更看重性能吞吐接入简单那么 Redis 分布式锁通常更适合。如果你的业务更看重强一致性锁释放的确定性分布式协调可靠性那么 ZooKeeper 往往更适合。所以很多时候并不是“哪个更强”而是“哪个更适合你的业务容错边界。”总结这篇文章可以压缩成几条核心结论MySQL 和 Redis 一致性问题重点在更新和删除场景更常见的实践是“先更新 MySQL再删除 Redis”删除缓存失败时MQ 重试通常是更实用的补偿方案Redis 锁性能更好但在故障场景下的一致性和可靠性要谨慎评估ZooKeeper 锁性能较弱但自动释放和强一致性更适合高可靠场景RedLock 可以增强 Redis 锁的可靠性但不等于所有问题都能完全消失真正落地时不要只看技术名词而要结合业务对性能、一致性、可恢复性的要求做权衡。如果这篇文章对你有帮助欢迎继续阅读本系列后续内容。若文中有不准确或需要补充的地方也欢迎指出。