本文是最近看阿里云开发者的一遍奇怪的缓存一致性问题的总结与心得,原文放在文章末尾
缓存穿透、缓存击穿、缓存雪崩
缓存穿透、缓存击穿和缓存雪崩都是系统中可能遇到的问题,特别在高并发场景下。
缓存穿透
与后两者不同,缓存穿透是查询不存在的数据。当去查询不存在的数据,由于缓存不命中(数据根本不存在),请求就会穿过缓存,去查询数据库。如果有大量的此类请求,就会导致数据库的压力骤增,严重会让数据库直接垮掉。
在大多数情况下缓存穿透是由恶意请求导致的,所以防止缓存穿透某种意义上就是拦截恶意请求
解决方案:
1.通过布隆拦截器
在缓存之前使用布隆拦截器,一种空间效率高的数据结构,用来检测一个元素是否在集合中。tips:布隆过滤器会存在误判的可能:判断存在则可能不存在,判断不存在一定不存在。但是依旧可以通过他过滤掉绝大部分无效请求。
注:在分布式场景中,布隆过滤器也需要分布式的,可以使用Redisson提供的来实现
由于过滤器说不存在就不存在,因此在新增数据时都要先写布隆过滤器,再写库。
如果在升级系统的过程中引入,则需要现将db中的历史数据先导入到布隆过滤器中。
布隆过滤器并不支持删除数据,因此在长时间之后,数据库的数据可能发生大变动,而布隆过滤器中的数据会一直累计,存在大量无用数据,导致误判的概率增加,防护能力也会降低。那么我们就必须要定期重置布隆过滤器。在一个月黑风高的夜晚降级掉布隆过滤器的防护,清空内容,在新数据写入的同时,再将历史数据导入。
2.增加空值
将数据库中的非法值当做合法值存储在内存当中。这样会导致缓存中有垃圾数据,就需要设定一定的过期时间。在插入数据的时候也需要清空缓存数据。在我的售票系统中用户查询车次中就用到了此点,将查询不到的依旧放入到缓存当中,同时设置一定的过期时间。
增加空值也有弊端,当面对恶意请求随机生产key访问的时候,不但会打垮数据库,还会将缓存写满,可能导致逃掉掉有效的热点的key。因此也需要一定的保护措施
保护措施:
- 处理恶意请求引起的缓存穿透,可以采用限流的方式。在使用token下将token作为唯一标识,以及其他的比如ip(可能导致连坐,范围误杀),记录该唯一标识在指定时间内的无效请求数量,并在每次记录完后与配置的阈值进行比较,在时间窗口内的数量超过则返回限流结果。(动态 窗口禁流/静态窗口禁流)。
- 在某些场景下可以通过给业务增加特殊效验位来过滤恶意请求。比如:查询订单中,查询入参是订单id,攻击方发起攻击需要构造不存在的订单id。如果我们的订单是简单的数字,那么就可能轻易被构建出。但是如果我们存在一些隐形规律,那么就没有那么容易构建。
用某一个id算法生成订单id:123456 ,基于此id取最大值和最小值追加在id后面,形成最终id:12345661.就可以通过简单的算法分析合理性。
缓存击穿:
某个热点缓存数据访问的频率很高,但是热点存在过期时间,过期之后导致大量的热点请求直接访问数据库,数据库压力瞬间骤增。
解决方案:
- 设置热点数据永不过期:设置一个较长时间的过期时间,相对永不过期
- 首先识别热key,可以通过基于基础中间件提供的能力统计
- 要锁定热key。大多数的数据一致性方案就会在更新数据库数据时删除缓存,如果不进行锁定,删除了依旧会遭遇缓存击穿。锁定热key又分为两种:热key对应的数据不允许修改/热key对应的缓存在数据库数据修改时不进行更新
- 通过队列让击穿流量排队
将击穿后的流量放入队列中,并发控制流量,成为消费者
:可以通过消息队列中间件,将逻辑部分分为两部分,分为处理请求的机器和接受请求的机器,同时把处理结果同步回去。
(让我想到在我的售票系统中,在用户购票环节中将令牌大闸拿取令牌,同时生成一个订单表,与处理订单相分离,通过mq发送信息给另一端。同时由一开始的对每个订单的处理,转化为对每个车次所有订单的处理,处理完之后修改订单表的信息。前台通过另一个接口间隔时间轮训查看订单信息,成功则返回)。
在一般的业务中可能只需要内存队列就可以满足需求。
使用内存队列,每个队列只有一个消费者。数据库的压力在最坏下为服务集群n的数量。通过队列整流后,将大并发变为小并发,那么请求的等待时间就可能会被延长。可以 通过增加消费者来缓解此问题。
3. 分布式信号量+睡眠(类似分布式锁,不过并分拿不到锁就返回)。
击穿流量在访问数据库前需要设置一个分布式信号量,设置成功则访问库并把结果放在缓存中。设置不成功则睡眠一段时间,然后到缓存获取数据,不访问数据库。
缓存雪崩
缓存雪崩类似缓存击穿,不过缓存雪崩是指大量缓存信息在同一时间消失,导致所有的请求大量访问数据库。
解决方案:
- 增加随机值,在原本的过期时间上添加一个Random,让过期时间不统一
- 通过队列让流量排队
数据一致性问题
当我们的数据在 多个地方存储的时候(比如缓存和数据库),这些地方的数据可能会出现不一致的情况。可能是由于缓存更新滞后、系统故障或其他原因引起。数据一致性是分布式系统设计中的一项挑战,尤其在读写非常频繁的系统中。
数据一致性问题:当数据被更新时,如果缓存中的相应数据没有立即更新,那么缓存系统将向应用程序提供旧数据。导致应用程序得到不一样的结果,影响用户体验。
在分布式系统中,可以根据系统的设计与需求选择实时强一致性或最终一致性
实时强一致性:
定义:实时强一致性保证了在任何时刻,任何客户端看到的数据都是一样的。在分布式系统中实现强一致性就意味着,一个操作完成以后,所有的客户端立即都能看到这个操作的结果
适用场景:事务性强,对数据一致性有高要求的系统。比如:银行或财务系统
最终一致性:
定义:最终一致性是指系统会保证在没有新的更新操作下,经过一段时间以后,数据才会被同步到存储的地方中。在这种情况下,数据的副本可能会存在数据不一致的问题。
适用场景:对实时性要求不高,能够容忍短时间数据不一致场景,比如聊天信息的传递,数据分析
原文中奇怪的数据一致性问题
push中心将后台配置端(只有查询流量没有增删改,dps较高)和前台投放端(增删改)分别部署在两个应用中,都需要配置数据库连接,配置缓存
前台投放端:与用户打交道,将业务广告等推送给用户
后台配置端:面向商家、数据分析师等确保广告能够按照预期的目标和策略执行。
主要包含了四条链路
链路一:投稿端:先去查询本地缓存,未命中则查询tair,查到后更新本地缓存。以tair作为数据库的方式
链路二:采用一个定时任务,按时刷新db中的数据到tair中。
定时任务可能得实现方式:
1.暴力全量刷新,redis中缓存的数据过期时间要大于定时任务运行时间。但是在数据量大的情况下不适用。
2.数据库中的数据更新有时间戳,定时任务每次拉取上次任务开始时间搓减去定时任务执行时间所得到的时间戳之后的数据,将数据刷新到redsi中。redis中的数据设置为永不过期(删除作为软删除)
这种定时任务,相邻两个任务有一定的数据重叠,防止漏掉数据。如果本地任务负责的数据较多可能在短时间内无法完成,那么下个任务由于抢不到锁就会在后续某个间隔开始执行,因此需要使用上一个定时任务开始的时间。
链路三:为后台配置端的查询操作,先查tair,没有则查数据库,并更新到数据库中
链路四:为写入操作,先删除tair中的缓存信息后更新数据库。
核心问题:链路一与链路四可能存在冲突,当我们删除tair中的缓存信息后,定时任务还没刷新前,那么此时链路一来查询就会导致缓存穿透的问题(本地缓存中没有,tair中也没有)。
原文给出的解决方案:
方案一:修改配置端,问题的原因为删除缓存后出现问题。那么索性不删除,采取双写。
将缓存的修改放在了数据库事务中进行,最大的问题在于如果写缓存虽超时,但是数据实际已经写入tair中,这时抛出的异常会导致数据库事务回滚,而数据库没有,tair中有,会形成僵尸数据。(可以通过scan的方式定期清理)。
方案二:修改投放端:(存在问题)
tair未命中,去查数据库并刷新tair
存在一个数据不一致性的问题,当配置单删缓存后,更新数据库前。此时投放端去tair中查找但是未命中,接着去DB中查后更新到tair中(查一般比增删改要快)。配置端未刷新缓存就会导致缓存失效前一致取到的是旧值。
原文应该采用了方案一的设计。并没有通用的方案,只有适合当前系统特性和业务需求的方案。
另外我再说另一种常见的就是双删,在配置端删除缓存后修改DB后延迟再次删除缓存中的数据。但是延迟的时间不易确定,太短无效,太长可能导致长时间不一致。。
原文地址:奇怪的缓存一致性问题 (qq.com)