缓存一致性问题
在日常开发中,为了提高数据响应速度,可能会将一些热点数据保存在缓存中,这样就不用每次都去数据库中查询了,可以有效提高服务端的响应速度,那么目前我们最常使用的缓存就是 Redis 了。
考虑一个问题,如果数据库修改了一个数据,而这个数据缓存中也有,这时再从缓存中拿数据,就是一条旧数据。
缓存数据一致性模式
双写模式
先更新数据库,再更新缓存。
1、A 线程更新数据库值为 1。
2、B 线程更新数据库值为 2。
3、由于卡顿,B 线程先更新了缓存值为 2。
4、最后 A 线程更新了缓存值为 1。
此时,缓存中保存的数据就是一个旧的脏数据了。
解决方案一:
加锁,产生并发写的时候,因为要更新数据库,同时要更新缓存,那么就可以对整个操作加一个锁,保证更新数据库和更新缓存是一个原子性。
这样就可以避免脏数据了。
解决方案二:
对缓存设置一个过期时间,过期时间一到,就会得到最新的数据,只是会出现暂时的数据不一致,最终会一致性的。
就要看业务是否允许这种情况,允许的情况下,容忍是多大。
失效模式
先更新数据库,再淘汰缓存。
A、B 线程更新数据库,再删除缓存。
C 线程读取数据库,在更新缓存。
1、A 线程更新数据库值为 1,删除缓存,A 执行完。
2、B 线程更新数据库值为 2,但是由于机器慢,花的的时间比较长。
3、这时 C 线程读取数据,从缓存读取不到,再从 DB 读取数据,这时读取到值为 1。
4、这时 B 线程执行完了,将数据值更新为 2,并且删除缓存。
5、最后 C 线程更新缓存值,因为 C 读取到的是 A 线程更新的数据,将旧数据更新到了缓存了。
此时,缓存中保存的数据就是一个旧的脏数据了。
解决方案:
上面的问题,其实就是写和读的并发问题,可以通过加读写锁进行解决问题。
对更新数据加写锁,对读数据加读锁。写读、读写、写写互斥,读读不互斥。
但是不管怎么样,只要用了锁,系统性能肯定会变低,但保证了数据的一致性,需要进行取舍。
对于经常修改的数据,与其进行加锁用缓存,还不如直接读数据库。
对于经常读,偶尔写的情况下,其实用缓存也就基本不会出现脏数据,因为并发写的情况基本不会出现,如果怕以防万一,可以用读写锁。因为读写锁适用于经常读的情况。
缓存数据一致性-解决方案
无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?
- 1、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加 上过期时间,每隔一段时间触发读的主动更新即可。
- 2、如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
- 3、缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
- 4、通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心 脏数据,允许临时脏数据可忽略);
总结:
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保 证每天拿到当前最新数据即可。
- 我们不应该过度设计,增加系统的复杂性。
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
总结系统的一致性解决方案
1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新。
2、读写数据的时候,加上分布式的读写锁。 经常写(会有影响)。很少写,经常读(不会有影响)。