各位同学们平时开发的时候除了使用到数据库(这里以mysql为例)还会用到相关的缓存(这里以redis为例)操作。
举一个常用的场景当我们写的接口性能相对比较慢的时候(高并发场景需要响应速度很快)为了保证性能的达标,我们一般会结合用缓存来提高响应,比如redis号称每秒响应次数达到10W级。
那么问题来了,如何保证数据库和缓存的接口一致性呢,本文就来简要的分析一下目前市面上大家常用的6种不通的缓存一致性解决方式。
一、方案介绍
方案一、先写数据库,在写缓存
请求 A、B 都是先写数据库,然后再写缓存,在高并发情况下,如果请求 A 在写缓存时卡了一会,请求 B 已经依次完成数据的更新,就会出现图中的问题。
对于读请求,先去读缓存,如果没有,再去读 数据库,但是读请求不会再回写缓存。
方案二、先写缓存,再写数据库
方案二和方案一相反,先写缓存,在写数据库,
方案三、先删除缓存,再写数据库
这里的请求 A 是更新请求,但是请求 B 是读请求,且请求 B 的读请求会回写数据库。
请求 A 先删除缓存,可能因为卡顿,数据一直没有更新到数据库,导致两者数据不一致。这种情况出现的概率比较大,因为请求 A 更新数据库可能耗时会比较长,而请求 B 的前两步都是查询,会非常快。
方案四、先删除缓存,再写数据库,再删除缓存
对于“先删除缓存,再写 数据库”,如果要解决最后的不一致问题,其实再对 缓存重新删除即可,这个也是常说的“缓存双删”。
“删除缓存 5”必须在“回写缓存5”后面,怎么保证一定是在后面呢?
走异步串行化删除,在任务处理完成后把最后“删除缓存 5”放到异步队列中,进行延迟删除,确保最后的一致性。
异步删除对线上业务无影响,串行化处理保障并发情况下正确删除。
如果双删失败怎么办?建议走重试机制,方案一是借助消息队列的重试机制;方案二也可以自己整个表,记录重试次数。
方案五、先写数据库,再删除缓存
对于上面这种情况,对于第一次查询,请求 B 查询的数据是 5,但是 数据库中的数据是 6,只存在这一次不一致的情况,对于不是强一致性要求的业务,可以容忍。(那什么情况下不能容忍呢,比如秒杀业务、库存服务等。)
当请求 B 进行第二次查询时,因为没有命中缓存,会重新查一次数据库,然后再回写到缓存。
这里需要满足 2 个条件:
- 缓存刚好自动失效;
- 请求 B 从数据库查出 5,回写缓存的耗时,比请求 A 写数据库,并且删除缓存的还长。
对于第二个条件,我们都知道更新数据库肯定比查询耗时要长,所以出现这个情况的概率很小,同时满足上述条件的情况更小。
方案六、先写数据库,通过 Binlog,异步更新缓存
这种方案,主要是监听数据库(mysql)的 Binlog,然后通过异步的方式,将数据更新到缓存,这种方案有个前提,查询的请求,不会回写缓存。
这个方案,会保证数据库和缓存的最终一致性,但是如果中途请求 B 需要查询数据,如果缓存无数据,就直接查数据库;如果缓存有数据,查询的数据也会存在不一致的情况。
所以这个方案,是实现最终一致性的终极解决方案,但是不能保证实时性。
二、各方案的比较
方案一、先写数据库,在写缓存
- 这种方案不建议用,万一数据库挂了,你把数据写到缓存,数据库无数据,影响肯定是灾难性的,如果真要使用,建议增加重试机制试试,如果操作失败的话;
方案二、先写缓存,再写数据库
- 这种只适合那种并发量、一致性要求不高的项目,比如那种政企项目,之前做过的政企项目就是经常这么搞,但是不建议这么做;
- 当缓存瞬间不可用的情况,需要及时告警,做后续处理。
方案三、先删除缓存,再写数据库
- 这种方法……好像没使用过,一般都是用方案五。
方案四、先删除缓存,再写数据库,再删除缓存
- 这种方式虽然可行,但是有点负责化了,还要搞个消息队列去异步删除 Redis。简单点的可以用java的异步线程去做延时删除。
方案五、先写数据库,再删除缓存
- 比较推荐这种方式,删除 Redis 如果失败,可以再多重试几次,否则报警出来;
- 这个方案,是实时性中最好的方案,在一些高并发场景中,推荐这种。
方案六、先写数据库,通过 Binlog,异步更新缓存
- 对于异地容灾、数据汇总等,建议会用这种方式,比如 binlog + kafka,数据的一致性也可以达到秒级;
- 纯粹的高并发场景,不建议用这种方案,比如抢购、秒杀等。
三、总结
- 实时一致性方案:采用“先写数据库,再删除缓存”的策略,这种情况虽然也会存在两者不一致,但是需要满足的条件有点苛刻,所以是满足实时性条件下,能尽量满足一致性的最优解。
- 最终一致性方案:采用“先写数据库,通过Binlog,异步更新缓存”,可以通过 Binlog,结合消息队列异步更新缓存,是最终一致性的最优解。