问题描述
- 一致性
- 缓存中有数据,缓存的数据值=数据库中的值
- 缓存中本没有数据,数据库中的值=最新值(有请求查询数据库时,会将数据写入缓存,则变为上面的"一致"状态)
- "数据不一致":
- 缓存的数据值≠数据库中的值;
- 缓存或者数据库中存在旧值,导致其他线程读到旧数据。
应对策略
- 把缓存分成读写缓存和只读缓存。
- 只读缓存:只在缓存进行数据查找,即使用"更新数据库+删除缓存"策略。
- 新增数据时,直接写入数据库;
- 更新(修改/删除)数据时,先删除缓存。
- 后续访问这些增删改的数据时,会发生缓存缺失,进而查询数据库,更新缓存
- 文中提到无并发情况 下,若步骤二失败会有一致性问题。
- 可以采用消息队列,异步重试。(多次失败后通知业务层)
- 也可采用binlog变更日志,根据日志更新/删除缓存
- 并发条件下
- 先删除缓存,再更新数据库 (其他线程在缓存为空时访问数据库获得旧值,顺便将缓存更新了,造成缓存中的值在A线程完成更新数据库后也不再更新)
- 设置缓存过期时间。淘汰缓存失败时,过期后,读请求从db中获取数据,并更新缓存。(?删除缓存可能会失败,设置超时时间就保证能成功了?)
- 延迟双删:更新完数据库后,等待一段时间(使确保其他线程能读到修改后的值),再删除缓存
- 先更新数据库,再删除缓存 (线程B读到旧值,?感觉问题不大;若有从库,会尝试去从库拿?,由于主从库的延迟,造成缓存中保存的是从库中的旧值)
- 延迟消息:发送删除缓存的消息到队列,延迟处理。如有必要,考虑主从数据库延迟
- 通过数据库的binlog来异步删除缓存
- 先更新数据库再删除缓存,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,也就是缓存穿透的问题。针对缓存穿透问题,可以用缓存空结果、布隆过滤器进行解决
- 加锁:更新数据时,加写锁;查询数据时,加读锁
- 先删除缓存,再更新数据库 (其他线程在缓存为空时访问数据库获得旧值,顺便将缓存更新了,造成缓存中的值在A线程完成更新数据库后也不再更新)
- 读写缓存 :需要在缓存中对数据进行增删改查,即使用"更新数据库+更新缓存"策略
- 同步直写:使用事务,保证缓存和数据更新的原子性,并进行失败重试
- 异步回写:写缓存时不同步写数据库,等到数据从缓存中淘汰时,再写回数据库
- 重点考虑更新数据库和缓存的顺序,读写还是读读
- 对于读写场景,延迟消息比较,发现不一致后,做业务补偿(加延迟时间?)
- 对于,写写场景,配合分布式锁处理。对于同一资源的操作,需要获取分布式锁,未获得锁的放在队列中。
- 只读缓存:只在缓存进行数据查找,即使用"更新数据库+删除缓存"策略。
总结
- 数据库和缓存存在不一致性,部分原因是因为数据库和缓存是独立的两部分。从设计上可能做了适配,但是总归有配合问题。如果数据库本身在设计上就考虑设计高并发缓存,我们在使用时就不用考虑一致性问题或者性能问题。
- 如人为设置一个等待时间,在数据库应该能完成数据更新的时,进行缓存的删除。如果数据库与缓存是一体的,可能在内部就能完成,数据库完成删除后通知缓存更新。这样效率肯定比外界干预的两部效率要高
- 性能和准确性的取舍在因场景而异,我们只能采用该架构下我们认知中,在该场景下,在有限的时间内能够完成的最优方案。