ランダムな同時実行性の高いクエリ結果の一貫性設計の実践

著者:Zhao Shuai、Yao Zaiyi、Wang Xudong、Meng Weijie、Kong Xiangdong



I.はじめに


物流契約センターは京東物流契約管理の唯一の入り口です。加盟店に契約の作成と封印を行う機能を提供し、さまざまな事業分野向けの契約のカスタマイズ、ファイリング、クエリ機能を提供します。事業分野が多数あるため、各事業分野に高可用性のクエリ機能を提供することは、物流契約センターの最優先事項です。同時に、請求システムは、各物流注文の決済時に契約センターに問い合わせて、販売者が署名した契約の内容を確認して請求の正確性を確保する必要があります。



2. ビジネスシナリオ


2.1 クエリディメンション分析

ビジネスコールの発信元から見ると、契約の大部分は請求システムであり、物流注文ごとに請求する際には、加盟店が契約を結んでいるかどうかを判断するために契約センターに電話する必要があります。

ビジネス コールの入力パラメーターから判断すると、ほとんどの場合、複数の条件を使用して契約を照会しますが、基本的には、特定のマーチャントを照会するか、マーチャントの特定の属性 (ビジネス アカウントなど) を介して契約を照会します。
呼び出しの結果から判断すると、クエリの 40% には結果がありません。そのほとんどは、販売者が契約に署名していないため、空のクエリが返されます。残りのクエリ結果については、毎回返される数量は比較的少なく、通常、販売者は 3 ~ 5 件の契約しか持っていません。

2.2 通話の分析

通話量

現在の契約の通話量は1日あたり約2000万回。
1 日の通話量統計:

呼び出し時間

1 日のピーク時間は作業時間であり、最高ピークは 4W/分です。
1 か月の通話量統計:
以上より、1日あたりの契約の通話量は比較的平均的で、主に勤務時間帯である9時から12時、13時から18時に集中しており、全体の通話量は比較的多く、基本的に急激な通話量の増加は見られないことがわかります。
全体的な分析から、契約センターのクエリ量は比較的多く、比較的平均的です。基本的にランダムなクエリであり、ホット データはありません。その中で、無効なクエリが大きな割合を占め、各クエリには多くの条件があり、返されるデータの量は比較的少ないです。


3. スキーム設計


全体的なビジネス シナリオの分析から、通話量を確実にサポートし、同時にデータの一貫性を処理する必要があるため、3 層の保護を実装することを決定しました。最初の層は、ほとんどの無効なリクエストを遮断するブルーム フィルターです。2 番目の層は、さまざまなクエリ条件を持つクエリが可能な限り Redis にヒットするようにするための Redis キャッシュ データです。3 番目のレイヤーは、データベースに直接クエリを実行するためのボトムアップ ソリューションです。同時に、データの一貫性を確保するために、ブロードキャスト mq を使用してそれを実現します。

3.1 最初の保護層

クエリのほぼ半分が空であるため、まずこれがキャッシュの侵入現象であると考えられます。
キャッシュ侵入の問題
缓存穿透 (cache penetration)是用户访问的数据既不在缓存当中,也不在数据库中。出于容错的考虑,如果从底层数据库查询不到数据,则不写入缓存。这就导致每次请求都会到底层数据库进行查询,缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻击时,数据库的压力骤增,甚至崩溃,这就是缓存穿透问题。
常规解决方案
  • 缓存特定值
一般对于缓存穿透我们比较常规的做法就是,将不存在的key 设置一个固定值,比如说NULL,&&等等,在查询返回这个值的时候,我们应用就可以认为这是一个不存在的key,那我们应用就可以决定是否继续等待,还是继续访问,还是直接放弃,如果继续等待访问的话,设置一个轮询时间,再次请求,如果取到的值不再是我们预设的,那就代表已经有值了,从而避免了透传到数据库,从而把大量的类似请求挡在了缓存之中。
  • 缓存特定值并同步更新
特定值做了缓存,那就意味着需要更多的内存存储空间。当存储层数据变化了,缓存层与存储层的数据会不一致。有人会说,这个问题,给key 加上一个过期时间不就可以了,确实,这样是最简单的,也能在一定程度上解决这两个问题,但是当并发比较高的时候(缓存并发),其实我是不建议使用缓存过期这个策略的,我更希望缓存一直存在;通过后台系统来更新缓存中的数据一致性的目的。
  • 布隆过滤器
布隆过滤器的核心思想是这样的,它不保存实际的数据,而是在内存中建立一个定长的位图用0,1来标记对应数据是否存在系统;过程是将数据经过多个哈希函数计算出不同的哈希值,然后用哈希值对位图的长度进行取模,最后得到位图的下标位,然后在对应的下标位上进行标记;找数的时候也是一样,先通过多个哈希函数得到哈希值,然后哈希值与位图的长度进行取模得到多个下标。如果多个下标都被标记成1了,那么说明数据存在于系统,不过只要有一个下标为0那么就说明该数据肯定不存在于系统中。
在这里先通过一个示例介绍一下布隆过滤器的场景:
以ID查询文章为例,如果我们要知道数据库是否存在对应的文章,那么最简单的方式就是我们把所有数据库存在的ID都保存到缓存去,这个时候当请求过进入系统,先从这个缓存数据里判断系统是否存在对应的数据ID,如果不存在的话直接返回出去,避免请求进入到数据库层,存在的话再从获取文章的信息。但是这个不是最好的方式,因为当文章的数量很多很多的时候,那缓存中就需要存大量的文档id而且只能持续增长,所以我们得想一种方式来节省内存资源但又能请求都能命中缓存,这个就是布隆过滤器要做的。
我们分析布隆过滤器的优缺点
优点
  • 不需要存储数据,只用比特表示,因此在空间占用率上有巨大的优势
  • 检索效率高,插入和查询的时间复杂度都为 O(K)(K 表示哈希函数的个数)
  • 哈希函数之间相互独立,可以在硬件指令层次并行计算,因此效率较高。
缺点:
  • 存在不确定的因素,无法判断一个元素是否一定存在,所以不适合要求 100% 准确率的场景
  • 只能插入和查询元素,不能删除元素。
布隆过滤器分析 :面对优点,完全符合我们的诉求,针对缺点1,会有极少的数据穿透对系统来说并无压力。针对缺点2,合同的数据,本来就是不可删除的。如果合同过期,我们可以查出单个商家的所有合同,从合同的结束时间来判断合同是否有效,并不需要去删除布隆过滤器里的元素。
考虑到调用redis布隆过滤器,会走一次网络,而我们的查询近一半都是无效查询,我们决定使用本地布隆过滤器,这样就可以减少一次网络请求。但是如果是本地布隆过滤器,在更新时,就需要对所有机器的本地布隆过滤器更新,我们监听合同的状态来更新,通过mq的广播模式,来对布隆过滤器插入元素,这样就做到了所有机器上的布隆过滤器统一元素插入。

3.2 第二层防护

面对高并发,我们首先想到的是缓存。
引入缓存,我们就要考虑缓存穿透,缓存击穿,缓存雪崩的三大问题。
其中缓存穿透,我们已在第一层防护中处理,这里只解决缓存击穿,缓存雪崩的问题。
缓存击穿 (Cache Breakdown)缓存雪崩是指只大量热点key同时失效的情况,如果是单个热点key,在不停的扛着大并发,在这个key失效的瞬间,持续的大并发请求就会击破缓存,直接请求到数据库,好像蛮力击穿一样。这种情况就是缓存击穿。
常规解决方案
  • 缓存失效分散
这个问题其实比较好解决,就是在设置缓存的时效时间的时候增加一个随机值,例如增加一个1-3分钟的随机,将失效时间分散开,降低集体失效的概率;把过期时间控制在系统低流量的时间段,比如凌晨三四点,避过流量的高峰期。
  • 加锁
加锁,就是在查询请求未命中缓存时,查询数据库操作前进行加锁,加锁后后面的请求就会阻塞,避免了大量的请求集中进入到数据库查询数据。
  • 永久不失效
我们可以不设置过期时间来保证缓存永远不会失效,然后通过后台的线程来定时把最新的数据同步到缓存里去
解决方案: 使用分布式锁,针对同一个商家,只让一个线程构建缓存,其他线程等待构建缓存执行完毕,重新从缓存中获取数据。
缓存雪崩 (Cache Avalanche)当缓存中大量热点缓存采用了相同的实效时间,就会导致缓存在某一个时刻同时实效,请求全部转发到数据库,从而导致数据库压力骤增,甚至宕机。从而形成一系列的连锁反应,造成系统崩溃等情况,这就是缓存雪崩。
解决方案: 缓存雪崩的解决方案是将key的过期设置为固定时间范围内的一个随机数,让key均匀的失效即可。
我们考虑使用redis缓存,因为每次查询的条件都不一样,返回的结果数据又比较少,我们考虑限制查询都必须有一个固定的查询条件,商家编码。如果查询条件中没有查商家编码,我们可以通过商家名称,商家业务账号这些条件来反查商家编码。
这样我们就可以缓存单个商家编码的所有合同,然后再通过代码使用filter对其他查询条件做支持,避免不同的查询条件都去缓存数据而引发的缓存数据更新,缓存数据淘汰以及缓存数据一致等问题。
同时只缓存单个商家编码的所有合同,缓存的数据量也是可控,每个缓存的大小也可控,基本不会出现redis大key的问题。
引入缓存,我们就要考虑缓存数据一致性的问题。
有关缓存一致性问题,可自行百度,这个就不再叙述。
如图所示,对于商家编码维度的缓存数据,我们通过监听合同的状态,使用mq广播来删除对应商家的缓存,从而避免出现缓存和数据一致性的相关问题。

3.3 第三层防护

第三层防护,自然是数据库,如果有查询经过了第一层和第二层,那我们需要直接查询数据库来返回结果,同时,我们对直接调用到数据库的线程进行监控。
为避免一些未知的查询大量查询涌入,导致数据库调用保证的问题,尤其是大促时,我们可以提前对数据库里的所有商家合同进行提前缓存。在缓存时,为避免缓存雪崩问题,我们对将key的过期设置为固定时间范围内的一个随机数,让key均匀的失效。
同时,为避免依然存在意外的情况,有大量查询涌入。我们通过ducc开关控制数据库的查询,如调用量太高导致无法支撑,则直接关闭数据库的调用,保证数据库不会直接宕机导致整个业务不可用。


四、总结


本文主要分析了面对高并发调用的调用场景设计及的技术方案,在引入缓存的同时,也要考虑实际的调用入参及结果,面对增加的网络请求,是否可以进一步减少。面对redis缓存,是否可以通过一些手段避免所有查询条件都需要缓存,带来的缓存爆炸,缓存淘汰策略等问题,以及解决缓存与数据一致等一系列问题。
本方案是根据具体的查询业务场景设计具体的技术方案,针对不同的业务场景,对应的技术方案也是不一样的。

-end-

本文分享自微信公众号 - 京东云开发者(JDT_Developers)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

2023 年需求最大的 8 种编程语言:PHP 强劲,C/C++ 需求放缓 马斯克宣布 Twitter 将改名为 X,并更换 Logo 历时五年,Cython 3.0 正式发布 GPT-4 越来越笨?准确率从 97.6% 降至 2.4% MySQL 8.1 及 MySQL 8.0.34 正式发布 C# 和 TypeScript 之父宣布最新开源项目:TypeChat Meta 放大招:发布开源大语言模型 Llama 2,可免费商用 力不从心,React 核心开发者 Dan Abramov 宣布从 Meta 离职 ChatGPT for Android 将于下周上线,现在开始预注册 不想搬砖,又想完成需求?也许这个 5k 星的 GitHub 开源项目能帮上忙 - MetaGPT
{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/4090830/blog/10090668