【缓存】分布式数据库与缓存一致性问题

缓存的读取,比较常规

get cache 
if cache hit 
	return cache 
else if cache miss 
	get db  and write cache 

在这里插入图片描述
但是在更新缓存方面比较有争议,具体有以下几种方式

  1. 先更新db,再更新cache
  2. 先更新db,再删除cache
  3. 先删除cache,再更新db

先更新db,再更新cache

  1. 线程安全 (数据不一致)
    同时请求A和请求B进行更新操作,那么
    1. A更新了数据库
    2. B更新了数据库
    3. B更新了缓存
    4. A更新了缓存
      这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据。
  2. 业务场景
    1. 数据类型
      • 朴素类型的数据,例如:int
        修改成本较低,直接set修改后的值即可
      • 文本数据,例如:json或者html
        一般也需要先get文本,parse成doom树对象,修改相关元素,序列化为文本,再set数据
    2. 性能
      del cache ,只是经历了一次cache miss
      如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁地更新,浪费性能;
      如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。

先更新db,再删除cache

国外提出了一个缓存更新套路,名为《Cache-Aside pattern》[1],其中就指出:

失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中;
命中:应用程序从cache中取数据,取到后返回;
更新:先把数据存到数据库中,成功后,再让缓存失效。
另外, Facebook也在论文《Scaling Memcache at Facebook》[2]中提出,他们用的也是先更新数据库,再删缓存的策略。

这种情况不存在并发问题么?

不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生:

  1. 缓存刚好失效;
  2. 请求A查询数据库,得一个旧值;
  3. 请求B将新值写入数据库;
  4. 请求B删除缓存;
  5. 请求A将查到的旧值写入缓存。

如果发生上述情况,确实是会发生脏数据。

然而,发生这种情况的概率又有多少?
发生上述情况有一个先天性条件,就是步骤3的写数据库操作比步骤2的读数据库操作耗时更短,才有可能使得步骤4先于步骤5。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤3耗时比步骤2更短,这一情形很难出现。
假设,有人非要抬杠,有强迫症,一定要解决怎么办?

如何解决上述并发问题?

  1. 给缓存设有效时间是一种方案。
  2. 异步延时删除策略,保证读请求完成以后,再进行删除操作。
    延时双删策略
	public void write(String key,Object data){

        redis.delKey(key);

        db.updateData(data);

        Thread.sleep(1000);

        redis.delKey(key);

    }

转化为中文描述就是:

  1. 先淘汰缓存;
  2. 再写数据库(这两步和原来一样);
  3. 休眠1秒,再次淘汰缓存。

这么做,可以将1秒内所造成的缓存脏数据,再次删除。
那么,这个1秒是怎么确定的,具体该休眠多久呢?

针对上面的情形,应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

还有其他造成不一致的原因么?

有的,如果删缓存失败了怎么办,那不是会有不一致的情况出现么。比如一个写数据请求,然后写入数据库了,删缓存失败了,这会就出现不一致的情况了。这也是缓存更新策略二里留下的最后一个疑问。

如何解决?
提供一个保障的重试机制即可,这里给出两套方案。
方案一:
如下图所示:
在这里插入图片描述
流程如下所示:

  1. 更新数据库数据;
  2. 缓存因为种种问题删除失败;
  3. 将需要删除的key发送至消息队列;
  4. 自己消费消息,获得需要删除的key;
  5. 继续重试删除操作,直到成功。

然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。

方案二:
流程如下图所示:

在这里插入图片描述更新数据库数据;

  1. 数据库会将操作信息写入binlog日志当中;
  2. 订阅程序提取出所需要的数据以及key;
  3. 另起一段非业务代码,获得该信息;
  4. 尝试删除缓存操作,发现删除失败;
  5. 将这些信息发送至消息队列;
  6. 重新从消息队列中获得该数据,重试操作。

备注说明:

上述的订阅binlog程序在MySQL中有现成的中间件叫Canal,可以完成订阅binlog日志的功能。至于Oracle中,笔者目前不清楚有没有现成中间件可以使用。另外,重试机制,笔者采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。

先删除cache,再更新db

  1. 数据不一致的原因:
    请求A进行更新操作,请求B进行查询操作
    1. A进行写操作,先删除缓存
    2. B查询发现缓存不存在
    3. B查询数据库,得到旧值
    4. B将旧值写入缓存
    5. A将新值写入数据库

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

那么,如何解决呢?采用延时双删策略。

如果你用了MySQL的读写分离架构怎么办?

在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。

两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。

  1. 请求A进行写操作,删除缓存;
  2. 请求A将数据写入数据库;
  3. 请求B查询缓存发现,缓存没有值;
  4. 请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值;
  5. 请求B将旧值写入缓存;
  6. 数据库完成主从同步,从库变为新值。

上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。

采用这种同步淘汰策略,吞吐量降低怎么办?

那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间再返回。这么做,加大吞吐量。

第二次删除,如果删除失败怎么办?

这是个非常好的问题,因为第二次删除失败,就会出现如下情形。还是有两个请求,一个请求A进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库:

  1. 请求A进行写操作,先删除缓存;
  2. 请求B查询发现缓存不存在;
  3. 请求B去数据库查询得到旧值;
  4. 请求B将旧值写入缓存;
  5. 请求A将新值写入数据库;
  6. 请求A试图去删除请求B写入对缓存值,结果失败了。

这也就是说,如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。参考策略二

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值