最强的缓存数据库一致性技术方案

一、背景

面对高qps场景,单纯mysql的技术架构容易出现瓶颈,常见的解决方案是引入分布式缓存挡住大部分读流量,但增加了依赖项则意味着需要考虑的异常场景就更多,最典型的莫过于如果保证缓存和数据库一致性。

二、缓存一致性问题原因

  1. 并发的场景下,导致读取老的 DB 数据,更新到缓存中。
  2. 缓存和 DB 的操作,不在一个事务中,可能只有一个操作成功,而另一个操作失败,导致不一致。

三、业内常用方案 (Cache Aside Pattern)

  • 应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,失效缓存(业务不复杂的场景,可以直接异步更新缓存,而不是失效)

  

并发执行分析

只读

需要解决并发读时,缓存失效,查询DB,然后更新至Cache的问题,有两个数据需要更新Cache(所以需要加锁解决)

读写并发

场景一 先写后读

  1. 写流程:更新DB
  2. 写流程:删除缓存
  1. 读流程:查询缓存

场景二 写读无序(出现不一致)

  1. 写流程:更新DB
  2. 读流程:查询DB(缓存失效时的回查)
  1. 写流程:删除缓存
  2. 读流程:更新缓存

场景三 先读后写

  1. 读流程:查询缓存
  2. 写流程:更新DB
  1. 写流程:删除缓存

写与写并发

场景一

  1. 写流程1:更新DB
  2. 写流程2:更新DB
  1. 写流程2:删除缓存
  2. 写流程1:删除缓存

场景二

  1. 写流程1:更新DB
  2. 写流程2:更新DB
  1. 写流程1:删除缓存
  2. 写流程2:删除缓存

四、落地方案

详细技术方案

对于强一致性场景,要严格控制DB与缓存的数据一致性,如果不一致,就直接走DB,所以采取Cache Aside Pattern + DB与缓存版本控制的方式,其中Cache Aside Pattern在使用时,走删除缓存,版本控制基于数据库乐观锁字段,以下为本次技术方案的核心点

  1. 写操作必须要包含更新redis,本质目的就是让缓存的数据是无效的,实现方式为保证DB_Version与Cache_Version不一致,达到数据不一致的标记设置
  2. 写操作不更新数据,让读操作去更新数据,
  3. redis记录包含数据表记录、DB_Version、Cache_Version
  4. 读操作会判断redis中的数据是否是可用的,通过对比DB_Version等于Cache_Version实现
  5. 只有DB_Version等于Cache_Version时数据才是一致的

处理策略

虽然是强依赖redis,为了应对redis可能出现的抖动、不可用等系统异常问题,需要有保证系统数据稳定的能力

  1. DB策略,DB处理策略,用在redis存在问题时,通过限流方式直接在DB上读写,待redis稳定后,切换为redis
  2. redis策略,正常情况下都是为redis策略;为了在redis出现问题,切换为DB策略,后又切换为redis策略时,切换过程中的数据更新与redis数据不一致的问题,重新切换为redis策略后,需要更新redis的key前缀为新前缀,具体实现方式可以将redis的前缀进行配置

缓存数据模型

DB_Version:用于记录DB中的版本号

Cache_Version:用户记录缓存中的版本号

Data:业务数据

为了保证业务数据与DB_Version、Cache_Version的更新是一致的,需要在更新redis时将业务数据、DB_Version、Cache_Version放在一条记录中更新

总体更新流程

  1. 写操作需要获取锁,
  2. 获取锁成功,(优化后不需要写时获取锁)
  3. 更新redis
    1. redis中有记录,DB_Version=乐观锁版本+1,Cache_Version =乐观锁版本
    2. redis中无记录,DB_Version=乐观锁版本+1,Cache_Version=-2(默认值)
  4. 更新DB
  5. 获取锁失败,重试,回到第2步

问题

  1. DB_Version更新有问题时(不管是超时、真正的失败还是什么其他原因,总之不是成功的),怎么处理?
    1. redis更新失败,尝试重试,如果重试失败,直接抛出异常,基于的处理原则为,数据的一致性比弱依赖redis更重要(引入redis就会有些依赖)
  2. 更新是否需要加锁
    1. 可加可不加,原因在于已经使用版本号控制,即使在更新redis与更新DB之间进来的查询,因为redis中DB_V与Cache_V不一样,也不会导致数据出现不一致

总体读流程

  1. 查询缓存,获取缓存数据,未命中,查询DB,返回
  2. 命中,NullObjec直接返回
  3. 命中,非NullObject,此时redis记录中包含了Cache_Version与DB_Version;对比Cache_Version与DB_Version,只需要比对两者的值,判断DB_Version是否等于Cache_Version
    1. 如果一致,则返回redis中的数据
    2. 如果不一致,首先要获取分布式锁,获取锁成功后,查询DB数据,然后准备DB数据更新至redis记录,其中Cache_Version=DB_Version=乐观锁版本
  4. 未命中,首先要获取分布式锁,获取锁成功后,查询DB数据,然后准备DB数据更新至redis记录,其中Cache_Version=DB_Version=乐观锁版本

问题

  1. 获取锁的方式有点重,有替代方案么?是否获取分布式锁,可以通过开关控制,在双十一这种大促以及日常时,对比开关开启、关闭时两者的RT、DB读写QPS、缓存命中率情况,分析分数锁的具体影响

场景分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值