这是我参与[第三届青训营-后端场]笔记创作活动的第2篇笔记
方案一:为缓存设置过期时间
- 读一个数据键为key1时,如果缓存未命中,则去读数据库,然后再将读到的key1数据写回缓存并设置过期时间。如果命中,则直接返回。这时候如果修改数据库key1的内容,而缓存不变,直到缓存key1过期这段时间内,会存在数据不一致。
- 该方案适合对数据一致性不敏感的业务。
方案二:先更新缓存再更新数据库
- 并发量大的情况下,会发生并发安全问题。比如A,B两个线程,A先更新缓存,然后B更新缓存,B再更新数据库,最后A再更新数据库,这样子会导致数据库和缓存数据不一致,所谓的脏数据。
- 该缓存是读写缓存,一般适合在读写量差不多的场景下,读多写少和写多读少都不太适用。
- 如果为了保证数据一致性而加锁,性能会大打折扣。
方案三:先更新数据库再更新缓存
- 跟方案二差不多,存在并发安全问题。
方案四:先删除缓存再更新数据库
- 该缓存是只读缓存,当需要更新数据时,只需要更新数据库和删除缓存对应的键值即可。
- 下面演示一下出现数据不一致的情况
- A请求删除缓存key1。
- B请求key1,缓存未命中,向数据库请求,得到当前key1。
- A请求更新数据库key1的值。
- B请求将key1旧值写回缓存。
- 上述情况就导致了缓存保存的是key1的旧值,数据库是最新的数据。在key1过期这段时间,存在数据不一致性。
方案五:先更新数据库再删除缓存
- 同方案四,对缓存只有删除操作。
- 下面演示一下出现数据不一致的情况。
- A请求更新数据库key1。
- B请求key1,缓存未命中,向数据库请求,得到当前key1。
- A请求删除缓存key1。
- B请求将旧值key1写入缓存。
- 上述情况一般比较难出现,原因是3要比4先执行,而3比4先执行的前提是1比2先执行,数据库的写操作一般比读操作更加耗时,所以这种情况比较少发生,一般采用先更新后删缓存的策略是比较妥当的,当然如果要求强一致性就得考虑其他方案。
方案六: 延时双删
- 更新数据库前先删除缓存,更新数据库后,延迟sleep等待一段时间后再次删除缓存。sleep不好把控,太小的话无法把脏数据删除,太大会影响整体效率,我个人不太理解这个方案的可行性。
方案七:消息队列
- 更新数据库,然后将更新数据信息投递到消息队列,消费者再从消息队列消费数据,更新缓存。这个方法会导致系统复杂性增加,还得考虑消息队列的可用性,防止重复消费,乱序消费以及可重试机制。并且会对业务代码造成入侵。
方案八:binlog+消息队列
- 数据库开启binlog,用一个程序来订阅binlog,更新数据库后,订阅程序获得binlog,并自动投递到消息队列。这跟单纯用消息队列不同的时实现了业务和消息队列解耦。相对的系统复杂度会更高。
Redis
- redis是旁路缓存,一般保证缓存和数据库一致性可以采用方案五,七,八。
- redis的缓存淘汰策略:
- lru:按最近最少使用的原则来淘汰数据,redis在RedisObject结构中的LRU字段记录了对应key最近使用的时间戳。并且redis不是遍历所有key来淘汰数据,而是集合采样的方式来淘汰。
- lfu:在lru的基础上,把原本的24bit的LRU字段拆成两部分,前16bit表示时间戳idt,后8bit表示访问次数counter。而redis的lfu访问次数并不是每访问一次就加1,是首先用计数器当前的值乘以配置项lfu_log_factor再加1,再取其倒数,得到一个p值;然后把这个p值和一个取值范围在0-1间的随机数r值比较大小,只有p大于r值,计数器才加1. 为防止数据刚写入就被淘汰,计数器初始值默认是5.为了防止有的数据前期被大量访问,后期又几乎不访问,redis还设置了counter衰减策略,衰减因子配置项lfu_decay_time,LFU策略会计算当前时间和数据最近一次访问时间的差,然后换算成分钟为单位。再将这个差值除以lfu_decay_time值,所得结果就是counter要衰减的值。