一、背景
一般情况下,Redis是用来实现应用和数据库之间读操作的缓存层,主要目的是减少数据库IO,还可以用来提升数据的IO性能
二、设计思想
当应用需要读取某个数据的时候,会有几个步骤:
- 尝试去Redis查询,如果命中就直接返回
- 如果没有命中,则从数据库查询
- 同步数据库数据到Redis
三、分析可能出现问题的场景
当数据发生变化的时候,需要同时更新Redis和MySQL,原先方案有:
先更新数据库,再更新缓存:
- 问题1:如果更新数据库成功,还没来得及更新缓存,这时候有另外一个客户端去访问了缓存,访问到了是旧的数据,这时候就会出现数据不一致
- 问题2:如果缓存更新失败,也会导致数据不一致
先删除缓存,再更新数据库:
- 预期:应用下次请求数据,发现缓存没数据,则从数据库同步数据到缓存里
- 分析:极端情况下,由于删除缓存和更新数据库的这两个操作不是原子的,所以还是会出现数据不一致
- 问题:如果删除了缓存,还没来得及更新数据库,这时候有另外一个客户端去访问了缓存,发现没数据,所以去数据库同步了旧的数据,这时候数据库才更新了新的数据,就会出现数据不一致
四、解决方案:延迟双删
解决数据一致性比较简单的方案有延迟双删,步骤为:
- 删除缓存
- 可能有其他客户端读操作(查到缓存为空,去数据库差旧的数据)
- 更新数据库
- 延迟删除缓存(因为可能第二部的读操作还没执行完成,如果立马删除的画,读操作还没来得及写缓存,那么又会把缓存更新成旧的数据)
但是延迟双删也有缺点,就是性能低,不适合高并发场景
所以在极端情况下仍然需要保证Redis和MySQL的数据一致性,就只能采用最终一致性方案
五、解决方案:最终一致性
介绍:由于延迟双删性能低,在高并发场景下并不合适,所以出现了最终一致性的方案,它保证了最终一致性,但是可能会出现短期不一致性(如果场景不能接受短期不一致性,可以采用读写锁等的方式实现强一致性,不过性能上会有一定影响)
1、采用MQ的重试机制
步骤:
- 更新数据库
- 删除缓存(需要保证删除成功!!!)
- 如果删除失败,则发送到MQ,使用其重试+手动确认的机制保证删除成功
缺点:高耦合,每个步骤都要发MQ,比较麻烦
2、用canal组件
原理:通过监听MySQL中binlog日志,把更新后的数据同步到Redis里面
优点:低耦合,省去了每步都需要发MQ的流程