高并发查询场景下缓存与数据库一致性方案

1.缓存不一致的原因:

上图描述了通常情况下缓存不一致的原因:并发操作缓存的缓存丢失问题。

  1. t1 时刻 threadB 查询到缓存没有数值,于是去数据库查询到数值后进行set操作。
  2. t2时刻threadC 执行更新数据库操作,并在t3时刻进行了缓存删除(或者更新)
  3. t4时刻threadB将查询到的值设置到缓存中,注意 这个值将threadC更新的新值丢弃了导致的缓存的不一致。

2.解决方案概述

当前通用的数据缓存策略为,更新数据库后,设置缓存失效,下次查询的时候查询数据库更新缓存。这个策略的问题如除上图所述外,还有高热key缓存雪崩的问题。修改缓存策略,当数据库数据修改后,更新缓存数据使用SET操作,不使用DEL操作。即当数据库事务完成后,将新版本数据更新到缓存中,使用数据更新时间戳作为数据版本号更新缓存数据时,比较版本号,以此来防止过期数据的写入,同时使用原子性策略保证更新操作的原子性。

3.缓存更新原子性技术实现方案比较

3.1 基于关系型数据库本地事务的实现

使用本地事务主要是为了保证DML时,DB和redis操作的原子性

DML操作

  1. DML时,在同一个本地事务中,先操作DB,再操作redis
  2. 对于redis的操作基于lua实现,lua的作用是实现乐观锁

Select操作

  1. cache miss时,同样基于lua写入数据

优势

  1. 实现较为简单
  2. 基本解决数据不一致问题

缺点

  1. lua脚本会增加redis-server端的cpu使用率
  2. 随着lua脚本的膨胀,会造成redis-server端的慢查询等坏味道
  3. 膨胀了本地事务,造成行锁持续时间增长了一个redis的RT,同机房约200微秒
  4. 仍需设计redis降级方案,否则redis不可用时,整体服务也会不可用

3.2 先操作数据库,再删除缓存(适合修改集中的场景)

删除缓存操作也可改为set缓存操作,同样需要基于lua实现的乐观锁

DML操作

  1. 提交事务
  2. 删除缓存

Select操作

  1. 若cache miss,则写入缓存

优势

  1. 实现较为简单
  2. 性能较好

缺点

  1. 有小概率造成图一的场景,可通过以下方案控制不一致窗口期
  2. 缩短缓存TTL至可接受的最大不一致窗口大小
  3. 延迟删除策略,例如使用binglog作为延迟删除拖底方案

3.3 基于分布式锁的实现(适用于一致性要求较高的场景)

DML/Select 操作

  1. 获取分布式锁
  2. 操作DB
  3. 操作cache
  4. 释放分布式锁

优势

  1. 数据一致性等级较高(基于redis的redlock方案或redis的replication机制尽量保证一致性)

优势

  1. 实现成本略高
  2. Select cache miss时,读写完全串行化,性能损失较大
  3. 感觉不太适合咱们的场景

3.4 基于原子性lua脚本&数据版本的无锁缓存更新方式

数据更新时,强制更新数据版本号(version)字段,set version=version+1

数据更新事务提交完成后,基于版本号乐观锁,原子性更新缓存,lua脚本如下:

/**
     * 缓存 value field
     */
    private static final String ORDER_CACHE_VALUE_FIELD = "value";
    /**
     * 缓存 version field
     */
    private static final String ORDER_CACHE_VERSION_FIELD = "version";
    /**
     * 数据原子更新 lua 脚本
     */
    private static final String script =
              "local ver = redis.call('hget', KEYS[1], '" + CACHE_VERSION_FIELD + "') "
                  //版本号判断
            + "if ver ~= false and tonumber(ver) >= tonumber(ARGV[1]) then "
            + "return 0 "
            + "else "
            + "redis.call('hset', KEYS[1], '" + CACHE_VERSION_FIELD + "', ARGV[1]) "
            + "redis.call('hset', KEYS[1], '" + CACHE_VALUE_FIELD + "', ARGV[2]) "
            + "redis.call('expire', KEYS[1], ARGV[3]) "
            + "return 1 "
            + "end";

查询&或者更新场景下的缓存更新都是用基于数据版本号的无锁原子更新。

优势:

        数据一致性高且并发友好。

优势:

        方案实施难度较高,需要数据&缓存结构改造

4. 总结

  1. 要根据不同模型设置不同 TTL
  2. 核心订单模型的TTL、缓存过期方案选择,需要进一步理解订单update流程并进行命中率观察后再确定
    1. 订单update是否集中
    2. 订单频繁update时,读操作是否频繁
    3. 根据业务场景判断使用方案

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值