04-Redis 场景 数据库和缓存双写一致性
官网保平安:https://redis.io/
如何保证缓存与数据库双写时的数据一致性 ★★★★
你只要用缓存,就可能会涉及到缓存与数据库双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
一般来说,就是如果你的系统不是严格要求 缓存+数据库的一致性,缓存可以偶尔有稍微的跟数据库不一致的情况,最好不要做这个方案。
如果非要做,一个方案是:读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。一种我觉得比较好的方式是:
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存。
这个方案可能会暂时产生不一致的情况,但是发生的几率特别小。
问题1:为什么是删除缓存,而不是更新缓存?
很多时候在复杂点的缓存场景,缓存不单是数据库中直接取出来的值。
比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
更新缓存的代价有时候是很高的,更新的缓存到底会不会被频繁访问到?
也许有的场景可以每次修改数据库的时候,都将其对应的缓存更新一份这样,但是对于比较复杂的缓存数据计算的场景,如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。更新之后,会有另一个问题,这个缓存到底会不会被频繁访问到?
举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。
实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新生成一次,开销大幅度降低。用到缓存才去算缓存。
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。
像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都把里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。
问题2:那如果先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致怎么办。
我们看下 先删除缓存,再更新数据库。 有两种情况:
- 如果先删除了缓存,然后要去修改数据库,数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。
- 如果先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了…
现在看两种方式都有问题,那看一下为什么上亿流量高并发场景下,缓存会出现这个问题?
上述场景只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是如果每天是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的 数据库+缓存 不一致的情况。
问题解决思路:延迟双删,添加重试机制。
- 延迟双删,先删除缓存再更新数据库,延迟一段时间后再删除缓存,注意延迟时间要大于一次写缓存的时间,这样就能清除掉删除缓存与更新数据库之间读入缓存的旧数据。第二次删除可以作为异步的,提交一个延迟的执行任务。时间根据实际而定。
- 添加重试机制,例如:将删除失败的 key,写入消息队列。该方案有一个缺点,对业务线代码造成大量的侵入。
- 使用任务队列进行删除。