为什么产生缓存一致性问题
在使用Redis等缓存中间件的时候,一定会遇到一个问题就是缓存一致性的问题(面试只要提到缓存也是必问的)。所谓缓存一致性问题就是,当某一个线程在做缓存与数据库的更新操作时,这两步操作一定是分开进行的,所以就会出现缓存与数据库的数据出现短暂的不一致,进而可能会造成其他线程读取数据不一致的问题。
更新缓存与数据库的可能策略
(1)先修改缓存,再修改数据库:这种方式乍一看,好像没啥问题,修改了缓存就可以保证其他线程也能够读取到最新数据,但是最重要的问题是,如果在第二步更新数据时,出现异常导致数据更新失败的话,缓存与数据库会出现严重的不一致,而且错误数据是在数据库上的,这是不可接受的,因为数据库数据是最后的底线,绝对不能让数据库数据出现脏数据(redis可没有事务回滚机制,数据库数据更新失败会事务回滚,但redis只要执行成功,就不会回滚,再加上假如该缓存数据设置有过期时间,一旦缓存过期或者由于某种原因数据丢失了,再去数据库中查询得到的一定是错误的旧数据了,所以这种方案一定排除)。
(2)先修改数据库,再修改缓存:这种方式的缺点就比较明显了,先修改数据库,就会导致从开始更新数据库到缓存更新成功之前这段时间内(或者缓存更新失败了),其他线程从缓存中读取到的都是旧数据,也即是脏数据。
(3)先删除缓存,再更新数据库:先删除缓存,然后更新数据库,其他线程读取的时候也会感知到缓存数据不存在了,就去数据库中查询,然后更新到缓存中。这种方式同样有个问题,在删除缓存后,到更新数据库成功之前这段时间,其他线程如果查询并重新将数据库中的数据加载到了缓存中,那么缓存中的数据还是旧数据,仅仅只是更新了数据库中的数据而已。
(4)先更新数据库,再删除缓存:很明显的,在更新数据库成功到删除缓存成功这段时间内,其他线程仍然只能读取到旧数据。而且还有另一个比较极端情况下的并发问题:假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
- 缓存刚好失效
- 请求A查询数据库,得一个旧值
- 请求B将新值写入数据库
- 请求B删除缓存
- 请求A将查到的旧值写入缓存
但是,这种情况太极端,所以大多数都会采用第4种策略进行缓存更新。
缓存一致性问题解决方案(个人思考)
上面的第4种方案虽然可行,但是高并发下肯定还是会出现问题的,所以在第四种方案的基础上还可以稍加改进。首先想到的就是分布式锁,遇事不决先上锁,并发问题统一解决方案。当然,纯属开玩笑,如果加上分布式锁,就变成了串行执行,那还要缓存有何意义,串行执行数据库都顶得住了,这样搞完全不考虑系统吞吐量的。
但改进一下,分布式锁采用读写锁模式,只有读请求可以并发,单对于写请求独占,这个可以有,系统吞吐量倒也大大提升,毕竟读请求远多于写请求,尤其以电商网站的商品信息这块为例,而且实现起来代码难度不大。可以使用通过Zookeeper实现的分布式锁方案,对于读写锁有着良好的实现,可以直接使用。第四种方案搭配分布式读写锁的方式,也得考虑一些东西,比如第二步缓存删除失败怎么办?是重试还是记录日志,还是采用异步执行,这些则是要结合具体业务对于数据的要求有多高来决定。
当然,加锁肯定是会影响性能和系统吞吐量的的,但数据一致性得到了保证,所以,如果业务对于数据一致性和实时性要求不高的话,实现的方案其实还是蛮多的,比如直接使用第四种方案即可,只需要可以保证数据最终一致性即可。
在网上还看到另一种方法是通过队列来序列化所有读写操作,方案如下:
比如说对于某一个商品信息的读写操作,为这个商品创建一个队列,凡是遇到写请求,则将写请求放入队列中,由队列对写请求统一管理,写请求处理成功,则从队列中删除。当有一个读请求过来时,到队列查询,是否有对应的写请求,如果有则放入队列中,等待写请求执行完之后再执行读请求。为防止某个请求阻塞情况,为其设置超时机制或者过期机制。
实际上这种方式也就是串行化操作,但这种方式我上面已经说了,系统吞吐量不想要了才这么干。而且都思考到这一步了,为什么想不到采用分布式锁的读写模式呢?这种方案虽可行,但是倘若访问量大,处理器来不及处理,队列内的请求数量越来越高,则会影响查询效率。出现这种情况,就要加机器集群执行,帮忙分担压力。更重要的是代码实现难度高且复杂。