微服务框架【第11期】--Redis缓存和数据库一致性问题
微服务框架【第11期】--Redis缓存和数据库一致性问题
导读:
大家好,我是老田。在前一篇文章中介绍了Redis使用过程中常见的缓存问题。今天我们继续学习Redis缓存和数据库一致性的问题。对于热点数据(经常被查询,但不经常被修改的数据),我们可以将其放入redis缓存中(甚至于不设置失效时间),以增加查询效率,但需要保证从redis中读取的数据与数据库中存储的数据最终是一致的。
1.Redis缓存和数据库一致性问题
在数据读多写少的情况下作为缓存来使用,是Redis使用最普遍的场景。客户端对数据库中的数据主要有两类操作,读(select)与写(DML)操作。读操作不存在一致性问题。
如果缓存在Redis中存在,即缓存命中,则直接返回数据
如果Redis中没有对应缓存,则需要直接查询数据库,然后存入Redis,最后把数据返回
上述我们是读操作,针对放入redis中缓存的热点数据,当客户端想读取的数据在缓存中就直接返回数据,即命中缓存(cache hit),当读取的数据不在缓存内,就需要从数据库中将数据读入缓存,即未命中缓存(cache miss)。所以读操作并不会导致缓存与数据库中的数据不一致。
但是对于写操作(DML),缓存与数据库中的内容都需要被修改,但两者的执行必定存在一个先后顺序,这可能会导致缓冲与数据库中的数据不再一致,此时主要需要考虑两个问题:
执行顺序的问题:先更新缓存还是先更新数据库?
更新缓存的策略问题:当缓存中的内容变化时,是选择修改缓存(update),还是直接淘汰缓存(delete)?
针对这两点问题,一共可以分为四种方案:
先更新缓存,再更新数据库;
先更新数据库,再更新缓存;
先淘汰缓存,再更新数据库;
先更新数据库,再淘汰缓存
2.是淘汰缓存还是更新缓存?
我们先来分析一下,应该采用更新缓存还是淘汰缓存的方式。
更新缓存
优点:每次数据变化都及时更新缓存,所以查询时不容易出现未命中的情况。
缺点:更新缓存的消耗比较大。如果数据需要经过复杂的计算再写入缓存,那么频繁的更新缓存,就会影响服务器的性能。如果是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取该数据。
删除缓存
优点:操作简单,无论更新操作是否复杂,都是将缓存中的数据直接删除。
缺点:删除缓存后,下一次查询缓存会出现未命中,这时需要重新读取一次数据库。从上面的比较来看,一般情况下,删除缓存是更优的方案。
3.执行顺序的问题
究竟是先淘汰缓存还是先更新数据库?
这里主要分为两个方面来考虑:
更新数据库与淘汰缓存是两个步骤,只能先后执行,如果在执行过程中后一步执行失败,哪种方案的影响最小?
如果不考虑执行失败的情况,但更新数据库与淘汰缓存必然存在一个先后顺序,在上一个操作执行完毕,下一个操作还未完成时,如果并发较大,仍旧会导致数据库与缓存中的数据不一致,在这种情况下,用哪种方案影响最小?
另外,对于数据库而言,读写操作可以只作用在同一台服务器上,即底层只有一个数据库,也可以将读操作放在从库,写操作放在主库,即底层是主从架构,对于主从架构还需要考虑主从延迟,今天我们只针对单节点模式。
4.第一种情景:更新数据库与淘汰缓存需要先后执行,如果在执行过程中后一步执行失败,哪种方案对业务的影响最小?
方案一:先淘汰缓存,再更新数据库
如果第一步淘汰缓存成功,第二步更新数据库失败,此时再次查询缓存,最多会有一次cache miss
方案二:先更新数据库,再淘汰缓存
如果第一步更新数据库成功,第二步淘汰缓存失败,则会出现数据库中是新数据,缓存中是旧数据,即数据不一致
解决办法:为确保缓存删除成功,需要用到“重试机制”,即当删除缓存失效后,返回一个错误,由业务代码再次重试,直到缓存被删除。
但对于方案一,如果更新数据库失败其实也是一个问题,为了确保数据库中的数据被正常更新,也需要“重试机制”,即当数据库中的数据更新失败后,也需要人工或业务代码再次重试,直到更新成功。
重试机制的原理图:
【结论】总体而言,虽然方案二导致数据不一致的可能性更大,但在业务中,无论是淘汰缓存还是更新数据库,我们都需要确保它们真正完成了,所以个人认为在情景一下两种方案并没有什么优劣之分。
5.第二种情景:假设没有操作会执行失败,但执行前一个操作后无法立即完成下一个操作,在并发较大的情况下,可能会导致数据不一致。此时,哪种方案对业务的影响最小?
方案一:先淘汰缓存,再更新数据库
如上图,是先删除缓存再更新数据库,在没有出现失败时可能出现的问题:
线程A删除缓存成功;
线程B读取缓存失败;
线程B读取数据库成功,得到旧的数据;
线程B将旧的数据成功地更新到了缓存;
线程A将新的数据成功地更新到数据库。
可见,进程A的两步操作均成功,但由于存在并发,在这两步之间,进程B访问了缓存。最终结果是,缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致。
在并发量较大的情况下,采用异步更新缓存的策略:
A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取数据,但B线程只是从数据库中读取想要的数据,并不将这个数据放入缓存中,所以并不会导致缓存与数据库的不一致
A线程更新数据库后,通过订阅binlog来异步更新缓存
此时数据库与缓存的内容将一直都是一致的
如果采取同步更新缓存的策略,即如果缓存中没有数据,就读取数据库并将数据直接放入缓存,可能会导致数据长时间的不一致
在这种情况下,可以用一些方法来进行优化:
用串行化的思路
即保证对同一个数据的读写严格按照先后顺序串行化进行,避免并发较大的情况下,多个线程同时对同一数据进行操作时带来的数据不一致性。
延时双删+设置缓存的超时时间
不一致的原因是,在淘汰缓存之后,旧数据再次被读入缓存,且之后没有淘汰策略,所以解决思路就是,在旧数据再次读入缓存后,再次淘汰缓存,即淘汰缓存两次(延迟双删)
利用延迟双删,可以很好的解决数据不一致的问题,其中A线程休眠的M秒,需要根据业务上读取的时间来衡量,只要比正常读取消耗的稍大就可以。但是个人感觉实际业务中需要根据场景来设置休眠的时间,这个不好确定。
在单节点下,用“先删缓存,再更新”的策略,如果采用同步更新缓存的策略,可能会导致数据长时间的不一致,可以通过一些方法来尽量避免不一致;如果采用异步更新缓存的策略,就不会导致数据不一致
方案二:先更新数据库,再淘汰缓存
如上图,是先更新数据库再删除缓存,在没有出现失败时
可能出现的问题:
线程A更新数据库成功;
线程B读取缓存成功;
线程A删除缓存成功。
可见,最终缓存与数据库的数据是一致的,并且都是最新的数据。但线程B在这个过程里读到了旧的数据,可能还有其他线程也像线程B一样,在这两步之间读到了缓存中旧的数据,但因为这两步的执行速度会比较快,所以影响不大。对于这两步之后,其他进程再读取缓存数据的时候,就不会出现类似于进程B的问题了
最终结论:
经过对比发现,先更新数据库、再删除缓存是影响更小的方案。如果第二步出现失败的情况,则可以采用重试机制解决问题。
6.单节点下执行顺序对比
先淘汰cache,再更新数据库:
采用同步更新缓存的策略,可能会导致数据长时间不一致,如果用延迟双删来优化,还需要考虑究竟需要延时多长时间的问题——读的效率较高,但数据的一致性需要靠其它手段来保证
采用异步更新缓存的策略,不会导致数据不一致,但在数据库更新完成之前,都需要到数据库层面去读取数据,读的效率不太好。保证了数据的一致性,适用于对一致性要求高的业务
先更新数据库,再淘汰cache:
无论是同步/异步更新缓存,都不会导致数据的最终不一致,在更新数据库期间,cache中的旧数据会被读取,可能会有一段时间的数据不一致,但读的效率很好。保证了数据读取的效率,如果业务对一致性要求不是很高,这种方案最合适
博观而约取,厚积而薄发!
--END--