背景
对于一个后端服务而言,当整体的用户体量到达一定规模,且整体资源难以扩充的情况下,增加缓存势必是一个比较好的选择,一方面加入缓存之后,对整体的资源消耗可以适当减少,因为将一部分的请求压力分担到了服务层,另一方面引入缓存对于接口的性能也会有一个大的提升
但是,在接口引入了一个缓存层之后,缓存层同数据源之间的一致性保证变成了一个新的问题,如何保证二者的一致性是今天要探讨的问题
常见缓存策略
对于缓存常见的策略,主要有以下5个: cache-aside、read-through、write-through、write-behind、write-around,下面分别对这5种策略的应用场景及一致性保障方案进行阐述,希望可以供社区的小伙伴们参考 😊
cache-aside
策略概述
读请求: 如果发现命中缓存,直接返回缓存数据,若未命中则查询数据库更新缓存之后返回
写请求: 先更新数据库,然后删除缓存
对于上图,有一些细节点这里进行简单的讨论
为什么要删除缓存而不是更新缓存?
这里主要出于两方面的考虑: 安全和性能
性能: 一些缓存的结果是需要经过计算/表间的级联得来的,如果在写的过程中重新计算,对性能会产生一定的影响
安全: 如果是更新缓存的话,可能会存在一些数据写错的风险,如下
理论上读请求最终读到的应该是写请求2更新的数据,但是实际上最终得到的是写请求1更新的数据
为什么先更新数据库,而不是先删除缓存?
首先如果是先删除缓存的话,可能会放大"缓存击穿"的风险,另外这样做也会有缓存和数据源不一致的问题,如下
同样是并发的情况,当写请求删除缓存之后,读请求使用老的源信息更新了缓存数据,但实际上最终写入源的是一个新的数据,此时两侧还是会不一致
适用场景
cache-aside适用于读多写少的场景,例如用户信息、新闻报道等变化较少的信息,这种方式一旦写入缓存,整体的更新频率较低
不一致情况
按照现有策略,cache-aside还是会存在数据不一致的风险,例如下面的情况
写请求删除缓存的时机晚于读请求更新缓存的时机,不过这种情况出现的概率较小,需要满足读请求在写请求读数据库之后&读请求的执行时间晚于写请求
除了这种情况,还会存在另一种不一致的情况,如下
读请求读取在写请求执行中,要想防止,需要在写入的key维度加互斥锁,不过业务需要在此做一个折中,因为增加锁必然会带来一定的性能损耗&机器资源消耗
补偿措施
对于cache-aside模式,在服务运行过程中,或多或少必然会存在一些不一致的情况,例如缓存删除失败,除了给缓存增加TTL进行缓解外,还可以通过一些其它的方式进行弥补
删除重试机制
直接在写请求维度重试的话,对整体的性能会有一定的影响,可以增加一个重试队列,将删除失败的key写入队列中,异步对删除失败的key进行二次删除
基于数据库日志增量解析、订阅&消费
利用阿里开源组件cacal/一些其它的binlog解析中间件,在对应的client端编写缓存删除的逻辑,在写请求删除缓存之后,基于增量的binlog日志补偿数据更新时可能的缓存删除失败问题,在绝大多数场景下,可以解决数据的不一致问题,需要注意的是binlog日志的时效性,如果binlog比较落后,应该被丢弃掉,而不是作为判断是否删除缓存的依据
read-through
策略概述
read-through意为读穿透模式,整体流程和cache-aside是类似的,不同点在于read-through中多了一个访问控制层,读请求只会与访问控制层进行交互,业务层的逻辑更加简洁
适用场景
读穿透同样适用于读多写少,数据更新频率低的场景
write-through
策略概述
write-through意为写穿透模式,整体的流程同cache-aside有一些区别,在写入数据时是更新缓存,而不是删除缓存
适用场景
写穿透所有的写操作都会经过缓存,一般更新缓存和更新数据库在一个事务中,当使用write-through的时候,一般配合read-through进行使用
write-behind
策略概述
write-behind意为异步回写模式,与write-through/read-through相同,在write-behind中,也存在一个访问控制层,不同的是write-behind在处理写请求的时候,只会写到缓存中,之后再异步刷新的数据库中
适用场景
write-behind适用于写操作比较频繁的场景,例如秒杀扣减库存,这样的策略下,写请求的延迟降低,减轻了数据库的压力,具有较好的吞吐性,但是数据库和缓存的一致性较弱,例如当更新数据还未写到数据库,直接从数据库查询数据是落后于缓存的
write-around
策略概述
write-around适用于一些非核心业务,这类业务对于一致性的要求较弱,可以选择在cache-aside的基础上给缓存增加一个过期时间,不做任何的删除/更新缓存的操作
结语
解决缓存同数据库之间的一致性,存在多种策略,需要评估当前的场景选择合理的方案,例如在读多写少的场景下,可以考虑采用cache-aside策略+消费数据库日志补偿的方案,在写多的场景下可以考虑write-through的方案,写多的极端场景下可以考虑write-behind