缓存更新

博客围绕数据库与缓存更新时的数据一致性问题展开。分析了先更新数据库再更新缓存、先更新缓存再更新数据库等多种策略的问题,介绍了先更新数据库再删除缓存策略及可能出现的并发问题和解决办法,重点阐述了延迟双删策略,包括具体步骤、双删原因及双删失败的处理方法。

先更新数据库,再更新缓存
  首先这个逻辑是有问题的。试想,两个更新操作,一个更新数据库为A,但还没来的及更新缓存,另一个更新数据库为B,又更新了缓存值为B,这时候第一个更新操作才更新缓存为A,那样数据库值为B,缓存值为A,数据不一致。

  
先更新缓存,再更新数据库
  这个流程跟上面很类似,出现的问题也很类似。两个更新操作,一个更新缓存为A,但还没来的及更新数据库,另一个更新缓存为B,又更新了数据库为B,这时候第一个更新操作才更新数据库为A,那样数据库值为A,缓存值为B,数据不一致。

  
先删除缓存,再更新数据库
  这个逻辑也是错误的。两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

  
先更新数据库,再删除缓存(Cache-Aside pattern)
  来看下有没有问题,一个是查询操作,一个是更新操作的并发,首先,没有了删除cache数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上删除缓存,后续的查询操作再把数据从数据库中拉出来。而不会像上面一样产生的问题,后续的查询操作一直都在取老的数据。
  为什么不是写完数据库后更新缓存?主要是怕两个并发的写操作导致脏数据。
  那么,是不是这个就不会有并发问题了?不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,删除缓存,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。
  但,这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

  上面先更新数据库,再删除缓存的策略中,因为要删除缓存,但如果缓存删除失败,就会导致数据库与缓存不一致。这个问题怎么办?一般可以用消息队列解决。
  如果删除缓存失败,发送消息投递到消息中间件中,进入消息队列。这样就保证了即使删除消息失败,也会重试。
  但是,这个方案有个问题,就是和应用服务的业务代码耦合的比较厉害。代码业务不清晰。那有没有别的方案呢,对业务没有侵入呢?可以利用了mysql的底层机制,binlog日志进行删除缓存,这样就不需要和业务关联,删除缓存服务是独立的。可以利用阿里开源的canal去操作。

  关于先更新数据库,再删除缓存的策略,再来看一下另一个场景,数据库的读写分离的场景。写请求在一个库,读请求在另一个库。读写分离时,库与库之间会存在数据延迟,因为存在数据同步。
  那再回顾一下更新流程,就会发现有问题,因为请求B更新数据在主库上面,请求A去读取数据时是另一个库。比如一个请求更新数据库值为A,删除缓存,另一个请求查询值为旧值B,(读库数据还没有同步),然后更新到缓存为B。这样就导致不一致,这个场景是经常出现的。那如何处理呢?
  
  可以不可以这样?预留数据库主从复制的同步时间,将删缓存的操作改为更新缓存并设置这个缓存的失效时间为一个“经验主从同步时间(500ms?)”,这个超时时间比正常的超时时间要短。
  来验证一下,之前提到了先更新数据库,再更新缓存可能存在的问题,那这边更新缓存并重设短的失效时间会导致一样的问题吗?两个更新操作,一个更新数据库为A,但还没来的及操作缓存,另一个更新数据库为B,又更新了缓存值为B,并设置了一个短的超时时间,这时候第一个更新操作才更新缓存为A并设置了一个短的超时时间,而此时数据库值是B,这的确会有短暂的不一致性问题,但是缓存会很快失效后,马上会更新值为B,所以还是可以保证最终一致性。



读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存和数据库间的数据一致性问题。
不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举个例子:

  • 如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
  • 如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。如何解决?

所以结合前面例子的两种删除情况,可以考虑前后双删加懒加载模式。那么什么是懒加载?就是当业务读取数据的时候再从存储层加载的模式,而不是更新后主动刷新。

延迟双删

在写库前后都进行redis.del(key)操作,并且第二次删除通过延迟的方式进行。
方案一(一种思路,不严谨)具体步骤是:
1)先删除缓存;
2)再写数据库;
3)休眠500毫秒(根据具体的业务时间来定);
4)再次删除缓存。

那么,这个500毫秒怎么确定的,具体该休眠多久呢?需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。

方案二,异步延迟删除:
1)先删除缓存;
2)再写数据库;
3)触发异步写人串行化mq(也可以采取一种key+version的分布式锁);
4)mq接受再次删除缓存。

异步删除对线上业务无影响,串行化处理保障并发情况下正确删除。

为什么要双删?
db更新分为两个阶段,更新前及更新后,更新前的删除很容易理解,在db更新的过程中由于读取的操作存在并发可能,会出现缓存重新写入数据,这时就需要更新后的删除。

双删失败如何处理?
1、设置缓存过期时间

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致。

2、重试方案
重试方案有两种实现,一种在业务层做,另外一种实现中间件负责处理。

缓存更新策略是用于管理缓存数据与数据库数据一致性的重要机制,常见的策略包括以下几种: ### 1. **定时过期(TTL)策略** 该策略通过设置缓存数据的过期时间,使缓存数据在一定时间后自动失效。这种策略简单易实现,适用于对数据实时性要求不高的场景。然而,它可能导致缓存数据与数据库数据之间存在短暂的不一致 [^1]。 ### 2. **主动失效策略** 在数据发生变更时,主动使相关的缓存数据失效。这种策略可以确保缓存数据的及时更新,但需要额外的逻辑来管理缓存失效,可能增加系统的复杂性 [^1]。 ### 3. **写穿透(Write-through)策略** 在写操作时,同时更新缓存和数据库。这种策略保证了缓存和数据库的一致性,但可能会增加写操作的延迟,因为每次写操作都需要同时更新两个存储系统 。 ### 4. **写回(Write-behind)策略** 在写操作时,仅更新缓存,并在后续某个时间点异步更新数据库。这种策略可以显著提高写操作的性能,但存在数据丢失的风险,因为缓存更新可能在数据库更新之前失败 [^1]。 ### 5. **读穿透(Cache-aside)策略** 在读取数据时,如果缓存中没有数据,则从数据库中加载数据并更新缓存;在写操作时,先更新数据库,然后使缓存失效。这种策略适用于读操作较多的场景,但需要应用层显式管理缓存 [^1]。 ### 6. **延迟双删** 在写操作时,先更新数据库,然后删除缓存,等待一段时间后再次删除缓存,以确保缓存中的旧数据被清除。这种策略可以减少缓存与数据库之间的不一致时间,但实现较为复杂 [^2]。 ### 7. **Read/Write Through 策略** Read/Write Through 策略将缓存作为主存储,数据库作为后备存储。在读取数据时,如果缓存中没有数据,则从数据库中加载;在写入数据时,同时更新缓存和数据库。这种策略简化了应用层的逻辑,但缓存层的实现较为复杂 [^2]。 ### 8. **Write Behind 策略** Write Behind 策略与 Write-through 类似,但写入操作仅更新缓存,并在后续异步更新数据库。这种方式可以提高写入性能,但存在数据丢失的风险 [^2]。 ### 9. **Refresh-ahead 策略** 在缓存数据即将过期之前,提前从数据库中重新加载数据到缓存中。这种策略可以减少缓存未命中时的延迟,但需要预测缓存数据的使用情况 [^2]。 ### 10. **CDC(Change Data Capture)策略** 通过捕获数据库的变更事件,实时更新缓存数据。这种策略可以实现缓存和数据库的实时一致性,但需要额外的组件来捕获和处理变更事件 。 ### 示例代码:Cache-aside 策略的实现 以下是一个简单的 Cache-aside 策略的实现示例: ```python def get_data(key): # 从缓存中获取数据 data = cache.get(key) if data is None: # 如果缓存中没有数据,则从数据库中加载 data = database.query(key) if data is not None: # 将数据写入缓存 cache.set(key, data) return data def update_data(key, new_data): # 更新数据库 database.update(key, new_data) # 使缓存失效 cache.delete(key) ``` ### 相关问题 缓存更新策略的选择需要考虑多个因素,例如性能、数据一致性、冗余数据、代码复杂度和业务逻辑的可靠性等。不同的策略适用于不同的场景,因此在实际应用中需要根据具体需求进行选择。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值