保证数据库和缓存之间的一致性是在许多应用程序中面临的挑战。数据库和缓存是两个不同的存储层,具有不同的特性和行为。在使用缓存的同时,确保数据库和缓存之间的数据一致性是至关重要的。
针对读请求,流程较简单,先读取缓存,缓存命中则返回结果,缓存未命中则读取数据库,并将读取的数据缓存到缓存中。
而针对写请求,就比较复杂了,有多种情况需要考虑。
1.先更新数据库,再更新缓存
A 请求先将数据库的数据更新为 1,然后在更新缓存前,请求 B 将数据库的数据更新为 2,紧接着也把缓存更新为 2,然后 A 请求更新缓存为 1
此时,数据库中的数据是 2,而缓存中的数据却是 1,出现了缓存和数据库中的数据不一致的现象。
如果业务对缓存命中率有很高的要求,可以采用这种方案,因为更新缓存并不会出现缓存未命中的情况。
优化数据不一致的方法:
- 在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,就会不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响。
- 在更新完缓存时,给缓存加上较短的过期时间,这样即时出现缓存不一致的情况,缓存的数据也会很快过期,对业务还是能接受的。
2.先更新缓存,再更新数据库
A 请求先将缓存的数据更新为 1,然后在更新数据库前,B 请求来了, 将缓存的数据更新为 2,紧接着把数据库更新为 2,然后 A 请求将数据库的数据更新为 1
此时,数据库中的数据是 1,而缓存中的数据却是 2,依然出现了缓存和数据库中的数据不一致的现象。
3.先删除缓存,再更新数据库
A请求要更新数据,先删除缓存,在更新数据库的过程中,B请求读取该数据,因缓存未命中,读取了数据库中未更新的数据1,并将结果写入了缓存
此时,数据库中的数据是 2,而缓存中的数据却是 1,所以,在读写并发时,缓存和数据库中的数据还是有可能不一致。
优化方法:延时双删
先执行缓存清除操作,再执行数据库更新操作,延迟 N 秒之后再执行一次缓存清除操作,这样就不用担心缓存中的数据和数据库中的数据不一致了。
不过,这个N秒是多少需要结合业务情况来判断;另外,如果删除缓存后,在更新数据库时失败了,会导致缓存未命中而访问数据库,但数据库未更新,最终会访问到旧数据。
4.先更新数据库,再删除缓存
从上面的理论上分析,先更新数据库,再删除缓存也是会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高。
因为缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。
而一旦请求 A 早于请求 B 删除缓存之前更新了缓存,那么接下来的请求就会因为缓存不命中而从数据库中重新读取数据,所以不会出现这种不一致的情况。
不过,以上都是基于更新数据库与删除缓存都成功的情况下,实际中,删除缓存操作可能失败,需要别的方法进行优化。
如上先更新数据库,再删除缓存的策略就是目前广泛应用的Cache-Aside 策略,也称为旁路缓存模式。
针对Cache-Aside 策略并发高时,读取数据不一致的问题,可通过加锁解决:在写请求中保证“更新数据库&删除缓存”的串行执行为原子性操作(同理也可对读请求中缓存的更新加锁),不过会带来性能损耗。
对与删除缓存可能失败的问题,则通过重试机制或订阅 MySQL binlog的方式进行优化。
重试机制:
通过引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。
- 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存;当重试超过的一定次数,还是没有成功,则向业务层发送报错信息。
- 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
订阅 MySQL binlog,再操作缓存:
更新数据库成功时,mysql会产生一条变更日志,记录在 binlog 里,可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,如阿里巴巴开源的Canal 中间件。
Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
5.为什么是删除缓存,而不是更新缓存?
在保证缓存与数据库一致性时,通常会选择删除缓存而不是更新缓存的原因有以下几点:
- 数据一致性保证:通过删除缓存,可以确保下一次读取时从数据库中获取最新的数据。如果只是更新缓存,存在并发读取的情况下,可能会导致读取到旧的或不一致的数据。通过删除缓存,可以强制应用程序在下一次读取时重新从数据库中获取最新的数据,从而保证了数据的一致性。
- 简化实现和处理复杂性:更新缓存可能引入更多的复杂性和实现难度。例如,如果在更新缓存时发生错误,可能会导致缓存和数据库之间的数据不一致。而删除缓存的操作相对简单,只需将缓存中的数据删除,下一次读取时再重新加载最新的数据。
- 避免缓存中脏数据的存在:在某些情况下,数据库中的数据可能因为错误或异常情况而被修改或删除,导致缓存中的数据变为脏数据。通过删除缓存,可以避免脏数据的存在,确保下一次读取时从数据库中获取最新的、正确的数据。
参考:https://xiaolincoding.com/redis/architecture/mysql_redis_consistency.html
- 删除重试机制