Redis如何保证数据一致性?
Redis通常作为持久层数据库(例如MySQL)的缓存层,如果缓存或者数据库数据发生改变,如何保证双方的数据是一致的?
这其实是要分情况讨论滴,对数据一致性不同的要求有不同的解决方案,主要分为一致性要求高的(最终一致性、绝对一致性),一致性要求低的
数据一致性要求高
Cache Aside Pattern旁路缓存策略
双写一致性:当修改了数据库的数据也同时更新缓存的数据,缓存和数据库的数据保持一致
- 当数据提交更新时,先删除缓存,再更新数据库
- 当数据提高更新时,先更新数据库,再删除缓存
但是这两种方案都存在脏数据的风险
先删除缓存,再更新数据库,假如在线程1更新数据库的期间,有其他的线程访问数据,那么所有的线程都会去访问到数据库,并且拿到的是旧数据,放入到缓存中
先更新数据库,再删除缓存,假如在线程1查询数据库的期间,其他线程对数据库进行了修改,并且删除了缓存,在线程1查询完数据过后将旧的数据写入到了缓存中
总而言之,无论先删除缓存还是先更新数据库都会存在数据不一致的问题,但是先更新数据库再删除缓存产生数据不一致的概率比较低,因为写入缓存的速度一般是远远快于更新数据库的操作,并且如果数据库更新失败,先删缓存,缓存不会回滚,而后删缓存,数据库更新失败,缓存不会被删除
延迟双删
延迟双删,在写库完成后,删除缓存,写库的线程睡眠一段时间再次删除缓存
为什么要使用延迟双删?其实就是考虑到上图中的极端情况,一个读线程发现缓存中没有对应的数据,去查库,在查询完毕准备更新缓存期间,另一个写线程完成了写库以及对缓存的删除(此时数据库中的数据是新数据,而读线程将缓存更新为旧数据),一般情况下,这种事件的发生概率很低,但是使用延迟双删可以规避掉这种问题,进一步提高数据的一致性,但缺点就是性能会有所下降
那具体的超时时间要根据你具体的业务来定,一般设置几秒足够了
如何避免删除缓存失败的情况?
MQ方案
可以使用一个MQ队列接受删除失败的key,使用一个消费者监听MQ队列,一旦有删除Redis失败的消息,就去进行删除重试
监听binlog日志
阿里巴巴有一个专门的中间件,负责监听binlog日志,一旦binlog发生了变动,cannel就会通知cannel的客户端,cannel客户端负责进行缓存的删除操作(同时支持重试)
总结:这两种方案核心就是将删除缓存的操作异步化了,并且支持重试
难道没有保证数据绝对一致性的方案吗?读写锁
读写锁(数据绝对一致性)
读写锁:给读操作加读锁,给写操作加写锁,读锁允许其他线程进行读操作但是不能进行写操作,写锁既不允许其他线程进行读、写操作
例如下面这个栗子
当线程对数据进行读操作,其他所有对数据进行写操作的线程都将被阻塞,但是可以进行读操作
当线程对数据进行写操作时,其他所有对数据进行读写操作的线程都将被阻塞
优点:读写锁保证了数据库与缓存之间的数据强一致性
缺点:加上读写锁之后,访问数据的速度将变得缓慢,有点违背了加缓存的初衷,所以,但你的数据是写多于读的,不建议放入缓存中
数据一致性要求低
系统对于数据的实时一致性要求没那么高,例如视频播放量、点赞数、社交媒体动态
怎么解决?
- 异步更新策略:使用MQ作为消息中间件,更新缓存后,通知缓存删除
- 缓存失效策略:设置合适的缓存失效时间,让缓存在一定时间内保持一致性,在超时后从数据库获取新数据即可
总结
对于数据一致性高的场景,旁路缓存中先更新数据库再删缓存已经能保证绝大情况下的数据一致性,如果要求再高点可以考虑上延迟双删,同时加中间件避免缓存删除失败。不推荐上读写锁,因为缓存本身就是为了提高性能的,读写锁对性能的影响较大。