一、思考
今天程序过程中突然想到了一个问题,怎么保证redis缓存和mysql数据库中的数据相同(一致性)。即在更新数据时怎样保证数据库和redis缓存始终相同。从理论上讲,给缓存设置过期时间是保证最终一致性的解决方案。这种方案下,所有写操作以数据库为准,对缓存做最大努力即可。下面介绍的是不依赖于给缓存设置过期时间的更新数据的方案。
首先必须明确一点,所有的数据都是必须以数据库持久化的数据为主,缓存是为了加快获取数据的方式,所以无论怎样保证一致性都必须保证数据库中的数据是逻辑正确的。
二、几种常见的更新(写)策略
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存
2.1 先更新数据库,再更新缓存
如下的不一致的情况:
- 线程A更新了数据库;
- 线程B更新了数据库;
- 线程B更新了缓存;
- 线程A更新了缓存;
如图,可能是网络原因或者是更新数据大小写入操作耗时等问题,导致先更新的最后才写入到缓存,从而导致 缓存与数据库不一致。此场景不易被消除,所以此中方式不可取。
业务场景不可取原因:
- 若存在大量且快速的更新操作,那么还未来得及读取缓存,缓存数据就又被更新了,这样就有很多更新是不必要的,浪费性能。
2.2 先删除缓存,再更新数据库
如下的不一致的情况:
- 线程A先删除缓存;
- 线程B读取缓存,读取不到;
- 线程B到数据库读取到旧值;
- 线程B读取到的旧值写入缓存;
- 线程A更新数据库;
同样在这种情况下,若是采用了读写分离的策略,则有如下不一致的情况:
- 线程A先删除缓存;
- 线程A更新数据到写数据库;
- 线程B读取缓存,获取不到;
- 线程B从读数据库读取获取到旧值;
- 线程B把旧值写入缓存;
- 数据库完成主从复制,读数据库更新了值
2.3 先更新数据库,再删除缓存
如下的不一致的情况:
- 缓存刚好失效;
- 线程B读取缓存无效;
- 线程B读取数据库旧值;;
- 线程A更新数据库;
- 线程A删除缓存;
- 线程B把旧值写入缓存;
这种情况相对来说少一些,因为一般写数据库的操作更加耗时,并且还得线程A一次完成两步,所以此方案是更容易接受的。
三、解决方案
3.1 延时双删
基本的过程分为三步:
- 先删除缓存;
- 更新数据库;
- 过一定时间后再删除缓存一次(1~2s)
存在的问题:
这是一次同步的操作的话,这个1~2s会大大的降低系统的性能。所以可以就爱能第二步的删除做一个异步删除。
但是如果第二次删除失败呢??
3.2 二次删除失败重试
- 更新数据库数据;
- 数据库会将操作信息写入binlog日志当中;
- 订阅程序提取出所需要的数据以及key;
- 另起一段非业务代码,获得该信息;
- 尝试删除缓存操作,发现删除失败;
- 将这些信息发送至消息队列;
- 重新从消息队列中获得该数据,重试操作;
备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。另外,重试机制,博主是采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。