写点什么

集群并发下的数据覆盖问题

作者:苏格拉格拉
  • 2022-11-04
    浙江
  • 本文字数:1216 字

    阅读完需:约 4 分钟

背景

为保证数据库与缓存的一致性,采用的是数据变更时触发缓存刷新的策略。

具体的业务场景为:当某一运营策略发生变更时,需要刷新该策略有关的所有缓存数据。

如运营新增了一条关联到门店 A、B、C 的策略,则需要分别刷新门店 A、B、C 的缓存。

问题

分布式并发的场景中,极端情况下存在这种可能,导致新的数据被老数据覆盖:

线程 1:

任务一新增策略 a,门店 A 的策略为 A(a)(括号内表示该门店相关的所有策略)

任务一触发刷新任务,读取到门店 A 的策略为 A(a)

线程 2:

任务二新增策略 b,门店 A 的策略为 A(a,b)

任务二触发刷新任务,读取到门店 A 的策略为 A(a,b)

任务二更新门店 A 的缓存,更新为 A(a,b)

线程 1:

任务一更新门店 A 的缓存,更新为 A(a)


以上过程,较老的数据(a)覆盖了新的数据(a,b),由此带来数据并发数据覆盖的问题

方案

以上问题,在于 read 和 flush 操作之间存在间隔,若能保证 read 与 flush 的原子性,即可解决覆盖的问题

方案一 分布式锁

分布式锁是可以解决这个问题的,在 read 之前加锁,在 flush 后解锁,即可保证 read、flush 操作的原子性。

但是使用分布式锁的弊端在于:

1.开发成本高,需要额外开发并且维护一个锁,并且还有锁失效、死锁等诸多问题

2.性能差,采用悲观锁的方式,任何请求来了都得加锁解锁,多增加了两次远程调用

方案二 乐观锁

在方案一悲观锁的铺垫下,尝试使用乐观锁的方式来提高性能。 与悲观锁一样,我们要把 read、flush 的操作做成原子性,就要把他们框到一起,在 read 前面或者同时获取到版本号,在 flush 的同时利用 cas 检查版本号。 在当前场景下,read a 读取的是数据库,我们并不能拿到读取数据库的版本号。所以版本号就存在缓存里面,每次操作+1。

该方案对比悲观锁,减少了一次远程调用。

方案三 其他可能的优化方案

方案二有了一定的性能提升,而当前的场景并非一定要求 read、flush 的原子性,只要能保证新的数据不要被老的刷新即可,所以是否还有性能的提升空间。

比如说:

1.若乐观锁产生了冲突,说明缓存已经被更新为新的数据了,当前任务可以直接放弃这次刷新,而不需要再补偿一次刷新。

2.又或者是,用时间戳记录查询数据的时间,通过时间来判断数据的新老,从而减少没有必要的刷新次数。


以上两点理论上可行,但在实现上各存在一些问题:

1.乐观锁产生了冲突,也有可能是因为刷新了不是最新的数据,毕竟只要刷新了数据,版本号就会更新。如果最新的数据所触发的任务又因此而放弃的话,数据就不是最新的了。这个问题又绕回到:如何知道数据本身的版本号?

2.分布式系统下会产生时钟问题,即无法准确判断远程时间与机器时间的先后,即使当前时间比数据附带的时间大,也不能代表当前操作更晚发生,因为每台机器的时间可能是不一样的。而时钟问题比较好的一个替代方案就是使用单调时间或版本号,即方案二。


其他暂时没有想到好的办法,就先用方案二了。

总结

在解决数据库与缓存一致性问题上,为防止新老数据的覆盖问题,可采用乐观锁版本号的方案。

在缓存 key 上记录版本号,每次操作版本号加一,写入时使用 cas 检查并更新版本号。

发布于: 刚刚阅读数: 12
用户头像

还未添加个人签名 2018-08-22 加入

还未添加个人简介

评论

发布
暂无评论
集群并发下的数据覆盖问题_缓存_苏格拉格拉_InfoQ写作社区