Redis更新缓存的正确姿势(先更新DB,再删除缓存)
1.更新缓存的三种模式引入缓存势必会导致数据的一致性问题(因为分别存放到缓存以及DB),那么在数据更新时,缓存和DB都得更新,并且更新时序的不同会导致不同的结果。关于数据更新目前业界已经沉淀了Cache Aside Pattern,Read/Write through,Write Behind Caching等三种方式。Cache Aside :同时更新缓存和数据库;Read/Write Thro
缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,大家没啥疑问。但是在更新缓存方面,对于更新完数据库,是更新缓存呢,还是删除缓存?又或者是先删除缓存,再更新数据库?其实大家存在很大的争议。
1.更新缓存的三种模式
引入缓存势必会导致数据的一致性问题(因为分别存放到缓存以及DB),那么在数据更新时,缓存和DB都得更新,并且更新时序的不同会导致不同的结果。关于数据更新目前业界已经沉淀了Cache Aside Pattern,Read/Write through, Write Behind Caching等三种方式。
Cache Aside :同时更新缓存和数据库;
Read/Write Through:先更新缓存,缓存负责同步更新数据库;
Write Behind Caching:先更新缓存,缓存定时异步更新数据库。
这三种模式各有优劣,可以根据业务场景选择使用。
1.1 Cache Aside
应用直接去缓存中找数据,命中缓存则直接返回,如果未命中缓存,则需要先去数据库中查询数据,并将查询到的数据存储到缓存中。
读策略:
- 程序需要判断缓存中是否已经存在数据。
- 当缓存中已经存在数据,则直接从缓存中返回数据
- 当缓存中不存在数据,则先从数据库里读取数据,并且存入缓存,然后返回数据
写策略:
这里存在两个问题:
- 先更新DB还是先更新缓存?
- 是更新缓存还是删除缓存?
针对问题1,我们有两种方案:
- 先更新DB,后更新缓存
- 先更新缓存,后更新DB
针对问题2,我们也有两种方案:
- 先删除缓存,再更新DB
- 先删除DB,再更新缓存
接下来我们逐条分析
1.1.1 先更新DB,后更新缓存
如图,并发更新会造成DB和缓存数据不一致。
1.1.2 先更新缓存,后更新DB
如图,和上图一样,并发双写会导致数据不一致的问题。
1.1.3 先删除缓存,再更新DB
上述我们看到,更新缓存在并发双写的情况下是不安全的,那如果改成删除缓存呢?答:因为delete操作是天然幂等的,所以并发双写不会对删除缓存的方案产生影响,但如果是并发读写呢?我们看下图:
可以看到,在并发读写下,还是会存在数据不一致的问题。可以考虑通过异步延时双删(通过两次删除来解决并发读造成的脏数据)或者给数据设置过期时间来解决。异步延时双删代码:
-
cache.delete
-
db.update
-
asynchronousCache.delete
1.1.4 先更新DB,再删除缓存(业界推荐方案)
假设缓存刚好到期失效时,读请求从db中读取数据,写请求更新完数据后再失效缓存后,读请求将旧数据存入到缓存中,这种情况也会导致脏数据的问题。实际上这种情况发生的概率很低,要发生这种情况的前提条件是写数据库要先于读数据库完成,一般而言读数据库相比于写数据库要耗时更短,这种前提条件成立的概率很低。
假设,有人非要抬杠,有强迫症,一定要解决怎么办?
针对这种情况,也可以采用上面所说的异步双删策略以及过期失效的方式来避免。
给缓存设有效时间是一种方案。其次,采用策略2(先删除缓存,再更新数据库)里给出的异步延时删除策略,保证读请求完成以后,再进行删除操作。
异步延迟双删
延迟双删是一种保证数据一致性的常用策略,它的基本思想是在更新数据库后,先删除缓存,然后等待一段时间,再次删除缓存。这样做的目的是为了防止在数据库和缓存主从同步的过程中,有其他请求查询到旧的缓存数据,并写回到缓存中,具体的流程如下:
- 更新数据库数据
- 删除缓存数据
- 起一个新线程:先休眠一段时间,时间依据数据的读取耗费的时间而定。再次删除缓存数据
延迟双删的休眠时间是根据业务读取数据平均耗时来设置的,目的是确保读请求可以结束,写请求可以删除读请求造成的脏数据的问题。一般来说,休眠时间可以设置为500毫秒左右,但具体还要根据实际情况调整。
异步延迟双删的优点是简单易实现,能够提高数据的最终一致性。但是延迟双删的缺点也非常明显:
- 延迟双删不是强一致性,有等待环节,如果系统要求低延时,这种场景就不合适了
- 延迟双删不适合“秒杀”这种频繁修改数据和要求数据强一致的场景
- 延迟双删的延时时间是一个预估值,不能确保数据库和redis在这个时间段内都实时同步或持久化成功了
- 延迟双删不能完全避免redis存在脏数据的问题,只能减轻这个问题,要想彻底解决,还需要用到同步锁解决
优质博客:分布式之数据库和缓存双写一致性方案解析!_Java后端技术的博客-CSDN博客
1.2 Read/Write Through(该模式将缓存作为主要的数据源)
读数据策略:
当数据发生更新时,查询缓存时更新缓存,然后由缓存层同步的更新数据库
写数据策略:
当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己同步更新数据库。
简单来说,该模式将缓存作为主要的数据源,而数据库对于应用程序是透明的,更新数据库的任务交给缓存来处理,应用程序直接和缓存服务打交道即可。
1.3 ReadThrough/Write Behind(都是直接从缓存中读取和写入)
读数据策略:
当数据发生更新时,查询缓存时更新缓存,然后由缓存层同步的更新数据库
写数据策略:
当数据更新的时候直接更新缓存数据,然后建立异步任务去更新数据库。
这种异步方式请求响应会很快,系统的吞吐量会明显提升。但是因为是异步更新数据库,数据一致性的保障就会变弱,如果更新数据库失败则会永远的造成系统脏数据,需要很精细设计系统重试的策略,另外如果异步服务宕机的话,还要考虑更新的数据如何持久化,服务重启后能够迅速恢复。在更新数据库时,由于并发多任务的存在,还需要考虑并发写是否会造成脏数据的问题。
2.三种缓存模式小结
Cache Aside:
优点:缓存仅仅保存被请求的数据,属于懒加载模式(Lazy Loading),避免了任何数据都被写入缓存造成缓存频繁更新
缺点:
- 当发生缓存未命中的情况时响应较慢。
- 实现逻辑都在应用程序中,如果后端拆分微服务,会造成代码冗余
适用于:流量和并发不高的常规性缓存
Read/Write Through:
优点:
- 缓存不存在脏数据
- 读取效率更高,因为写操作每次都会更新缓存,所以提高了读操作命中缓存的概率
- 缓存单独抽成服务,应用程序的逻辑相对简单
缺点:对于写多读少的应用不是很合适,数据可能更改了很多次,每次都写入缓存却没有被读取,造成了大量写入延迟时间的浪费。
适用于:流量和并发较高的缓存场景
ReadThrough/Write Behind:
优点:
- 读操作和写操作效率很高,因为都是直接从缓存中读取和写入。
- 对于数据库不可用的情况有一定的容忍度,即使数据库暂时不可用,系统也整体可用,当数据库之后恢复的时候,再将数据写入数据库。
缺点:有数据丢失的风险,如果缓存挂掉而数据没有及时写到数据库中,那么缓存中的有些数据将永久丢失。
适用于:秒杀等频繁写入的场景
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)