亿级电商流量下Redis与MySQL数据一致性解决方案详解
亿级电商流量下Redis与MySQL数据一致性解决方案详解
在高并发场景下,如何保证Redis与MySQL之间的数据一致性是一个常见且重要的问题。本文详细探讨了四种解决方案,并分析了各自的优缺点,帮助读者在实际应用中做出合适的选择。
前言
在使用缓存(无论是本地缓存还是Redis)时,数据同步不一致的问题是不可避免的。常见的缓存使用方式是:先读取缓存,如果缓存中有数据则立即返回;如果没有,则从数据库中读取数据并更新到缓存中。然而,这种做法在数据修改或删除时容易造成缓存与数据库之间的数据不一致。为了解决这个问题,有以下四种方案:
- 先更新缓存,再更新数据库
- 先更新数据库,再更新缓存
- 先删除缓存,后更新数据库
- 先更新数据库,后删除缓存
下面将逐一分析这些方案的可行性。
一、先更新缓存,再更新数据库
这个方案一般不推荐使用。如果更新缓存成功但更新数据库出现异常,会导致缓存数据与数据库数据完全不一致,且这种问题很难察觉,因为缓存中的数据会一直存在。
二、先更新DB,再更新缓存
这个方案也一般不推荐。如果数据库更新成功但缓存更新失败,同样会出现数据不一致的问题,且这种问题也不容易被发现,因为缓存中一直存在数据。
三、先删除缓存,后更新DB
这个方案在并发场景下也会出现问题。具体来说:
- 请求A会先删除Redis中的数据,然后去更新数据库
- 此时请求B看到Redis中的数据是空的,回去数据库中查询该值,补充到Redis缓存中
- 此时请求A并没有更新成功,或者是事务还未提交,请求B去数据库查询得到旧值
这会导致数据库和Redis数据不一致。为了解决这个问题,可以采用延时双删策略:
- 先淘汰缓存
- 再写数据库
- 休眠1s,再次淘汰缓存
这样做可以将1s内造成的缓存脏数据再次删除。但是,这个1s的具体时间需要根据项目评估:
- 评估读数据业务逻辑的耗时
- 评估写数据的休眠时间(在读数据业务耗时的基础上加几百ms)
延时双删并不能完全解决问题:
- 评估的延时时间可能因系统压力而变得不准确
- MySQL的主从同步之间也会有时间差
解决方案有两个:
- 在主从同步的延时时间基础上加几百毫秒
- 对Redis进行填充数据查询,强制走主库查询
如果面试官问接口吞吐量降低怎么办,可以将第二次删除操作异步化。
四、先更新DB,后删除缓存
这种方案下依然存在数据不一致性的问题,尤其是在高并发场景下:
- 缓存刚好失效
- 请求A查询数据库得到旧值
- 请求B将新值写入数据库
- 请求B删除缓存
- 请求A将查到的旧值写入缓存
发生这种情况的概率较低,因为数据库的读操作速度远快于写操作。但是,如果业务要求强一致性,可以采用以下方案:
- 给缓存设置过期时间
- 采用异步延时删除策略
- 加锁(读多写少且对数据一致性要求高的场景)
方案补充(重要)
3、4都属于删除缓存类,其实删除缓存类都会有一个共同的问题,那就是在删除缓存的阶段出错了怎么办?此时再读取缓存的时候每次都是错误的数据了。
此时解决方案有两个:
一、利用消息队列进行删除失败的补偿
具体的业务逻辑如下:
- 请求 A 先对数据库进行更新操作
- 在对 Redis 进行删除操作的时候发现报错,删除失败
- 此时将 Redis 的 key 作为消息体发送到消息队列中
- 系统接收到消息队列发送的消息后
- 再次对 Redis 进行删除操作
但是这个方案会有一个缺点,就是会对业务代码造成大量的侵入,深深的耦合在一起。
所以还有一个优化的方案
二、订阅MySQL的binlog日志,异步删除
我们知道对 Mysql 数据库更新操作后 ,在binlog日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql数据库 的binlog日志对缓存进行操作,这样就达到了一个解耦的目的了。
业务代码流程如下:
- 更新数据库,更新完成后,触发binlog消息
- 经常B(消费者)订阅binlog消息,执行缓存删除操作
- 缓存删除失败,将删除任务丢到消息队列中
- 进程B获取删除失败任务
- 执行二次删除redis缓存
说到底就是通过数据库的 binlog 来异步淘汰 key,利用工具(canal)将 binlog
日志采集发送到 MQ 中,然后通过 ACK 机制确认处理删除缓存。
先更新 DB,后删除缓存,这种方式,被称为Cache Aside Pattern,属于缓存更新的经典设计模式之一。
所以如果大家做缓存与数据库的同步,推荐大家选择这一种方式。
总结
在高并发场景下保证Redis与MySQL数据一致性是一个复杂但重要的问题。通过本文的分析,可以发现"先更新数据库,后删除缓存"的方案在大多数场景下都是最优选择。同时,通过结合消息队列和binlog日志等技术手段,可以进一步提升系统的稳定性和数据一致性。