一致性问题:
首先要到redis里面读取缓存,如果没有缓存,那么就到mysql里面去取数据,并且将其放置在缓存中
关于解决缓存一致性的问题,不难想到主要有两种解决方案,双更模式和删除模式
双更模式,顾名思义就是更新两次,一次更新redis,一次更新mysql
不难想到,如果我们先更新redis,再更新mysql的话,是不是就可以保证每次缓存中的数据都是最新的了?
但是这样做是有问题的,比如如果我们更新mysql的时候失败了怎么办?更新数据库可能会失败,发生了回滚。所以,最后“缓存里的数据”和“数据库的数据”就不一样了,也就是出现了数据一致性问题。
那如果先更新mysql,再更新redis呢?
由于数据库和 Redis 的操作,并不是原子的,它们的执行时长也不是可控制的。当两个请求的时序发生了错乱,就会发生缓存不一致的情况。
综上,双更模式下,数据不一致的概率较大,一般不建议使用双更模式。
删除模式即更新数据时,删除redis,查询时重新从数据库中加载数据。
先删除缓存
请求A删除了某个 key 的值,这时候有另外一个请求B 到来,那么它就会击穿到数据库,读取到旧的值。无论操作A更新数据库的操作持续多长时间,都会产生不一致的情况。
后删除缓存
后删除缓存不会出现上述问题。一般情况下这种方式可以解决大部分问题,也是最常用的解决方案。
但是在高并发的情况下,仍有可能出现不一致的情况。场景如下:
有一系列的高并发操作,一直执行着更新、删除的动作。某个时刻,它更新数据库的值为 1,然后删除了缓存。
正在这时,有两个请求发生了:
一个是读操作,读到的当然是数据库的旧值 1,我们记作操作 A;
同时,另外一个请求发起了更新操作,把数据库记录更新为 2,我们记作操作 B。
一般情况下,读取操作都是比写入操作快的,但我们要考虑两种极端情况:
一种是这个读取操作 A,发生在更新操作 B 的尾部;
一种是操作 A 的这个 Redis 的操作时长,耗费了非常多的时间。比如,这个节点正好发生了 STW。(条件比较苛刻)
那么很容易地,读操作 A 的结束时间就超过了操作 B 删除的动作。
实际上,你也无法控制它们的执行顺序。只要发生这种情况,大概率数据库和Redis的值会不一致。
延迟双删
延时双删的方案的思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再sleep一段时间,然后再次删除缓存。
sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于读写缓存的时间即可。
流程如下:
线程1删除缓存,然后去更新数据库
线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存
线程1,根据估算的时间,sleep,由于sleep的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除
如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值
设置较小的缓存时间
俗称闪电缓存,即把缓存的失效时间设置非常短,比如 5秒。一旦失效,就会再次去数据库读取最新数据到缓存,即数据不一致只会在短时间内不一致。但这种方式,在非常高的并发下,同一时间对某个 key 的请求击穿到 DB,产生缓存击穿问题
消息队列
先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。
进阶版消息队列
一般大公司本身都会有监听binlog消息的消息队列存在,主要是为了做一些核对的工作。
这样,我们可以借助监听binlog的消息队列来做删除缓存的操作。这样做的好处是,不用你自己引入,侵入到你的业务代码中,中间件帮你做了解耦,同时,中间件的这个东西本身就保证了高可用。
当然,这样消息延迟的问题依然存在,但是相比单纯引入消息队列的做法更好一点。
Apache geode是不错的Redis的替换,支持事务,缓存不一致好多时候是Redis不支持事务引起,Geode的商业版本GemFire就是当年支持12306的神器,稳定性,性能都很好
链接如下
https://github.com/apache/geode