データベースキャッシュの一貫性に関する研究

データベースキャッシュの一貫性に関する研究

参考リンク:

データベースとキャッシュ データの一貫性の問題について話す

Meituan Two Sides: Redis と MySQL の二重書き込みの一貫性を確保する方法は? (キャッシュ遅延二重削除、キャッシュ再試行メカニズムの削除、binlog の非同期読み取りキャッシュ削除)

1. キャッシュを使用する理由

seckill の実際のビジネスでは、seckill 対象の製品に関する一部の情報など、アクセスの多いデータに属し、"ホット」データ、特に一部の読み取りは書き込みよりもはるかに大きいです。データベースに過負荷がかかるのを防ぐために、受信データをキャッシュし、要求がデータベースにヒットしないようにする必要があります。「速さ」を追求するためにキャッシュが存在していると言えます。

2. 2つのポイント

  1. キャッシュには有効期限が必要です
  2. データベースとキャッシュの最終的な整合性を確保するために、強い整合性を追求する必要はありません

補充:

強整合性: この整合性レベルは、ユーザーの直感に最も一致します。読み取った内容をシステムに書き込む必要があり、ユーザー エクスペリエンスは良好ですが、多くの場合、システムのパフォーマンスに大きな影響を与えます。

弱い整合性: この整合性レベルは、書き込みが成功した直後に書き込まれた値を読み取ることができることや、データが一貫性を保つのにかかる時間を約束しないようにシステムを制約しますが、一定の時間レベルを保証しようとします。 (第 2 レベルなど)、データは一貫した状態に到達できます。

結果整合性: 結果整合性は、弱い整合性の特殊なケースです. システムは、一定期間内にデータ整合性の状態を達成できることを保証します. ここで結果整合性を別途提案する理由は、弱整合性における評価の高い整合性モデルであり、大規模分散システムのデータ整合性においても業界で評価の高いモデルであるためです。

なぜ有効期限が必要なのですか?まず、キャッシュについては、ヒット率が高いほどシステムのパフォーマンスが向上します。キャッシュ アイテムに有効期限がなく、ヒットする確率が非常に低い場合、これはキャッシュ スペースの無駄です。また、有効期限があり、キャッシュ アイテムが頻繁にヒットする場合、ヒットするたびに有効期限を更新できます。これにより、ホット データが常にキャッシュに存在することが保証されます。したがって、キャッシュのヒット率は次のようになります。保証され、システムのパフォーマンスが向上します。

设置过期时间还有一个好处,就是当数据库跟缓存出现数据不一致的情况时,这个可以作为一个最后的兜底手段。也就是说,当数据确实出现不一致的情况时,过期时间可以保证只有在出现不一致的时间点到缓存过期这段时间之内,数据库跟缓存的数据是不一致的,因此也保证了数据的最终一致性。

那么为什么不应该追求数据强一致性呢?这个主要是个权衡的问题。数据库跟缓存,以Mysql跟Redis举例,毕竟是两套系统,如果要保证强一致性,势必要引入2PC或Paxos等分布式一致性协议,或者是分布式锁等等,这个在实现上是有难度的,而且一定会对性能有影响。而且如果真的对数据的一致性要求这么高,那引入缓存是否真的有必要呢?直接读写数据库不是更简单吗?所以能做的就是在最终一致性的基础上尽量的去实现强一致性。

3.数据库和缓存的读写顺序

方案一:Cache-Aside pattern(边缘缓存模式)

画像-20220819121759428

思路如下:

  1. 失效:程序先从缓存中读取数据,如果没有命中,则从数据库中读取,成功之后将数据放到缓存中
  2. 命中:程序先从缓存中读取数据,如果命中,则直接返回
  3. 更新:程序先更新数据库,在删除缓存

方案二:Read-Through/Write-Through(读写穿透)

Read/Write Through模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过抽象缓存层完成的。

Read-Through:

Read-Through的简要流程如下

画像-20220819121803516

  1. 从缓存读取数据,读到直接返回
  2. 如果读取不到的话,从数据库加载,写入缓存后,再返回响应。

其实Read-Through就是多了一层Cache-Provider,·实际只是在Cache-Aside之上进行了一层封装,它会让程序代码变得更简洁,同时也减少数据源上的负载。流程如下:

画像-20220819121807231

Write-Through:

Write-Through模式下,当发生写请求时,也是由缓存抽象层完成数据源和缓存数据的更新,流程如下:

画像-20220819121909374

方案三:Write behind (异步缓存写入)

Write behindRead-Through/Write-Through有相似的地方,都是由Cache Provider来负责缓存和数据库的读写。它两又有个很大的不同:Read/Write Through是同步更新缓存和数据的,Write Behind则是只更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库。

画像-20220819121914460

这种方式下,缓存和数据库的一致性不强,对一致性要求高的系统要谨慎使用。但是它适合频繁写的场景,MySQL的InnoDB Buffer Pool机制就使用到这种模式。

4.对于更新的四种选择

  1. 先更新缓存,再更新数据库
  2. 先更新数据库,再更新缓存
  3. 先删除缓存,再更新数据库
  4. 先更新数据库,再删除缓存

先更新缓存,再更新数据库

不管是操作数据库还是操作缓存,都有失败的可能。如果我们先更新缓存,再更新数据库,假设更新数据库失败了,那数据库中就存的是老数据。当然你可以选择重试更新数据库,那么再极端点,负责更新数据库的机器也宕机了,那么数据库中的数据将一直得不到更新,并且当缓存失效之后,其他机器再从数据库中读到的数据是老数据,然后再放到缓存中,这就导致先前的更新操作被丢失了,因此这么做的隐患是很大的。从数据持久化的角度来说,数据库当然要比缓存做的好,我们也应当以数据库中的数据为主,所以需要更新数据的时候我们应当首先更新数据库,而不是缓存。

画像-20220819121918480

  1. 写请求1更新缓存,设置age为1
  2. 写请求2更新缓存,设置age为2
  3. 写请求2更新数据库,设置age为2
  4. 写请求1更新数据库,设置age为1

执行结果就是,缓存里age被设置2,数据库里的age被设置成1,导致数据不一致,此方案不可行。

先更新数据库,再更新缓存

这里主要有两个问题,首先是并发的问题:假设线程A(或者机器A,道理是一样的)和线程B需要更新同一个数据,A先于B但时间间隔很短,那么就有可能会出现:

  1. 线程A更新了数据库
  2. 线程B更新了数据库
  3. 线程B更新了缓存
  4. 线程A更新了缓存

按理说线程B应该最后更新缓存,但是可能因为网络等原因,导致线程B先于线程A对缓存进行了更新,这就导致缓存中的数据不是最新的。

第二个问题是,我们不确定要更新的这个缓存项是否会被经常读取,假设每次更新数据库都会导致缓存的更新,有可能数据还没有被读取过就已经再次更新了,这就造成了缓存空间的浪费。另外,缓存中的值可能是经过一系列计算的,而并不是直接跟数据库中的数据对应的,频繁更新缓存会导致大量无效的计算,造成机器性能的浪费。

综上所述,更新缓存这一方案是不可取的,我们应当考虑删除缓存。

先删除缓存,再更新数据库

这个方案的问题也是很明显的,假设现在有两个请求,一个是写请求A,一个是读请求B,那么可能出现如下的执行序列:

  1. 请求A删除缓存
  2. 请求B读取缓存,发现不存在,从数据库中读取到旧值
  3. 请求A将新值写入数据库
  4. 请求B将旧值写入缓存

这样就会导致缓存中存的还是旧值,在缓存过期之前都无法读到新值。这个问题在数据库读写分离的情况下会更明显,因为主从同步需要时间,请求B获取到的数据很可能还是旧值,那么写入缓存中的也会是旧值。

先更新数据库,再删除缓存

这是最常用的方案,但是最常用并不是说就一定不会有任何问题,假设有两个请求,请求A是查询请求,请求B是更新请求,那么可能会出现下述情形:

  1. 先前缓存刚好失效
  2. 请求A查询缓存未命中,继续查询数据库,得到旧值
  3. 请求B更新数据库
  4. 请求B删除缓存
  5. 请求A将旧值写入缓存

上述情况确实有可能出现,但是出现的概率可能不高,因为上述情形成立的条件是在读取数据时,缓存刚好失效,并且此时正好又有一个并发的写请求。考虑到数据库上的写操作一般都会比读操作要慢,(这里指的是在写数据库时,数据库一般都会上锁,而普通的查询语句是不会上锁的。当然,复杂的查询语句除外,但是这种语句的占比不会太高)并且联系常见的数据库读写分离的架构,可以合理认为在现实生活中,读请求的比例要远高于写请求,因此我们可以得出结论。这种情况下缓存中存在脏数据的可能性是不高的。写缓存比写数据库快出几个量级,读写缓存都是内存操作,速度非常快。

那如果是读写分离的场景下呢?如果按照如下所述的执行序列,一样会出问题:

  1. 请求A更新主库
  2. 请求A删除缓存
  3. 请求B查询缓存,没有命中,查询从库得到旧值
  4. 从库同步完毕
  5. 请求B将旧值写入缓存

如果数据库主从同步比较慢的话,同样会出现数据不一致的问题。事实上就是如此,毕竟我们操作的是两个系统,在高并发的场景下,我们很难去保证多个请求之间的执行顺序,或者就算做到了,也可能会在性能上付出极大的代价。那为什么我们还是应当采用先更新数据库,再删除缓存这个策略呢?因为缓存在数据持久化这方面往往没有数据库做得好,而且数据库中的数据是不存在过期这个概念的,我们应当以数据库中的数据为主,缓存因为有着过期时间这一概念,最终一定会跟数据库保持一致。

问:在秒杀场景中,是否有必要保持缓存与数据库的强一致性?

seckill のシナリオでは、データベースが最も脆弱な場所であることが多いため、最も重要なことは現在の制限と非同期処理です。主な設計は次のとおり. インターフェイス自体の現在の制限を除いて、ローカル メモリと redis の両方で商品在庫、つまり、スパイクが繰り返されているかどうか、および商品在庫がゼロであるかどうかを前処理できます。もちろん、両者が保存するデータは最新のものではないかもしれませんが、これは大きな問題ではありません。なぜなら、彼らが保存するデータが製品在庫がゼロであることを示している場合、スパイクは失敗しており、ヒットする必要がないからです。データベース層。local と redis レベルを渡すと、非同期処理、つまり操作データベースがメッセージとして mq に書き込まれ、コンシューマー側がそれを消費します。コンシューマー側は特定のデータベース トランザクションを実行し、実行が成功した場合のみ、2 回目の kill が成功したことを意味し、それ以外の場合は失敗を意味します。結果に関係なく、最終データを redis に書き込むだけです。データベースとキャッシュの間で強い一貫性を実現する必要がないことがわかります。もちろん、mq を書くことは、同じユーザー要求が同じコンシューマーによってのみ処理されることを保証するために、もう少し複雑です.これは、例として Kafka で同じメッセージ キーを指定することによって行うことができます。要求 ID は、重複排除の基礎として使用されます。

おすすめ

転載: juejin.im/post/7133437611242651661