代码评审-如何保证缓存与数据库的读写一致性?

咱们从近期代码评审过程当中的一段代码,开始探讨缓存和数据库的一致性问题。html

探讨前置

通常来讲,使用缓存主要为了提高应用性能和下降DB的直接负载,从场景上来讲能够接受最终一致性方案, 若是业务场景要求 “缓存+数据库” 必须保持强一致性的话,那么须要使用同步方案,好比排它锁或者队列机制+数据库事务处理 这样的话影响系统可用性,简单状况下可使用....仍是另选方案吧java

业务场景

  1. 商品系统的Cache和DB数据(或者RPC操做库存应用),大体包含商品基本属性、库存、价格,
  2. 业务特性:读多写少,同一商品id,基本属性修改并发少,库存修改并发多(下单量大的时候库存操做比较频繁)
  3. 缓存操做模式上,采用Cache Aside Pattern(图片来源
    Cache Aside Pattern
    Cache Aside Pattern
  4. 具体使用状况的伪代码
public Ware getById(long id) {
        Ware ware = Cache.get(id);
        if (ware != null) {
            return ware;
        }
        ware = Db.get(id);
        if(ware != null){
        	//缓存时间12小时,根据具体业务调整
            Cache.put(ware,60*60*12);
        }
        return ware;
}

public void update(Param param) {
        Db.update(param);
        Cache.del(param.getId());
}
//Cache
public void del(long id) {
		//异常 静默
        CacheClient.expire("key1"+id, 0);
        CacheClient.expire("key2"+ id, 0);
}
复制代码

问题

先说说这段代码已经考虑到的问题,也是使用Cache Aside Pattern的好处git

  1. Q: update 先执行了更新DB,而后删除cache,为何不能先删cache,再更新DB? A: 考虑到 getById 和 update并发执行,更新先删,可是查询并发放入,致使cache里为脏数据(读多写少使这种状况更容易出现)
  2. Q: update 先执行了更新DB,而后删除cache,为何不能直接更新cache,这样也能够避免热点数据致使缓存击穿问题?

A: 1. 考虑到 update 方法自己并发执行,Db.update和Cache.update不是原子操做,会出现先更新DB的后更新cache 时序不一致问题(库存修改存在并发状况,并要求时序一致性)github

A: 2. 商品的缓存数据可能包含多维度好比库存和价格,这儿更新了库存一个字段,若是更新缓存须要查询多表数据聚合放置缓存shell

A: 3. 这次更新的商品可能不被查询使用,好比冷数据,采用查的时候缓存起到了懒加载效果数据库

A: 4. 缓存击穿问题,这儿的更新是基于单个商品,通常状况可忽略,请求量若是特别高,好比秒杀商品须要更改缓存结构和特殊的处理方式(好比版本替换机制,队列扣减机制等等,后续有机会bob再详解...)缓存

未考虑到的问题架构

  1. 缓存操做使用异常静默,这是查询时候异常降级的思路,可是在更新或者删除的时候使用,由于Db.update和Cache.update不是原子操做, 若是发生异常静默,会致使缓存脏数据的出现,若是出现了脏数据,除了等待过时,还能怎么办?
  2. 缓存删除中咱们看到会操做多个key(伪代码中2个),若是其中1个成功,1个失败,partial failures,用户看到的数据一半对,一半错...怎么办?
  3. 在update方法并发执行时,多个请求依然有可能出现时序不一致致使的问题,好比先更新的后放置缓存。
  4. 在update和查询并行的状况下,查询接口从DB中查询出数据准备放置缓存,可是GC暂停,接着update删除缓存,查询恢复放置缓存,极端状况也可能出现脏数据

解决思路

  1. 根据场景设置合适的缓存过时时间,即便不一致,也只是缓存过时时间内的不一致,过时时间越短,数据一致性越高,可是查数据库就会越频繁
  2. 为了保持时序一致性能够采用版本化或者加锁机制(影响吞吐量)
  3. 为了达到最终一致性咱们能够引入消息队列来做补偿,在更新后咱们不删缓存而是发送消息来异步更新(技术复杂性提升)
    消息补偿
  4. 采用binlog+消息队列(项目目前使用方案),按照时序解析binlog,发送到消息队列中(使用顺序队列,延迟必定时间消费),而后业务系统顺序消费删除缓存,这样能起到最终一致性,顺序一致性 由于binlog顺序解析而且发送到顺序队列中,因此业务上能够保证顺序一致性,若是删除缓存失败能够继续重试, 为何要延迟必定时间消费呢,这是为了保证查询和删除缓存并发会出现脏数据,由于延迟了必定时间,这段时间内查询方法应完成,而后再删除,就提升了一致性的可能
  5. 从业务上将缓存动静隔离(好比将库存做为单独缓存key和基础属性分开处理)、热点隔离(好比秒杀商品采用特殊处理方式)

后续

看到最后咱们能够发现与其说是保证了一致性,不如说咱们是在 提升缓存一致性并发

从上面的业务使用场景结合问题分析,咱们也能够看出在不一样的场景下为了达到不一样的效果(一致性要求、吞吐、并发)咱们有不一样的方案,这些方案的选择离不开场景,但同时咱们也要结合技术复杂度和团队技术水平、开发维护成本综合考虑来选择适合团队的方案。异步

参考

Redis使用总结(1、几点使用心得)

高并发架构系列:Redis缓存和MySQL数据一致性方案详解

Facebook use delete to remove the key

Improving cache consistency

相关文章
相关标签/搜索