Consistency Design Practice of Random High Concurrency Query Results

Author: Zhao Shuai, Yao Zaiyi, Wang Xudong, Meng Weijie, Kong Xiangdong



I. Introduction


The logistics contract center is the only entrance for JD logistics contract management. Provide merchants with the ability to create and seal contracts, and provide contract customization, filing, and query functions for different business lines. Due to the large number of business lines, providing high-availability query capabilities for each business line is the top priority of the logistics contract center. At the same time, the billing system needs to query the contract center when each logistics order is settled to ensure the content of the contract signed by the merchant to ensure the accuracy of billing.



2. Business scenarios


2.1 Query dimension analysis

From the perspective of the source of the business call, most of the contract is the billing system. When billing each logistics order, it needs to call the contract center to judge whether the merchant has signed the contract.

Judging from the input parameters of business calls, most of them use multiple conditions to query the contract, but basically they query a certain merchant, or query the contract through a certain attribute of the merchant (such as a business account).
Judging from the results of the calls, 40% of the queries have no results, and most of them are because the merchants have not signed the contract, resulting in empty queries. For the rest of the query results, the quantity returned each time is relatively small, and generally a merchant only has 3 to 5 contracts.

2.2 Analysis of calls

call volume

The current call volume of the contract is about 20 million times per day.
Call volume statistics for a day:

call time

The daily peak period is working time, and the highest peak is 4W/min.
Call volume statistics for one month:
It can be seen from the above that the daily call volume of the contract is relatively average, mainly concentrated from 9:00 to 12:00 and 13:00 to 18:00, that is, during working hours. The overall call volume is relatively high, and there is basically no sudden increase in calls.
From the overall analysis, the query volume of the contract center is relatively high and relatively average. It is basically a random query, and there is no hot data. Among them, invalid queries account for a large proportion, and each query has many conditions, and the amount of returned data is relatively small.


3. Scheme design


From the analysis of the overall business scenario, we decided to implement three layers of protection to ensure the support of call volume, and at the same time, we need to handle data consistency. The first layer is a Bloom filter to intercept most invalid requests. The second layer is redis cache data to ensure that queries with various query conditions hit redis as much as possible. The third layer is a bottom-up solution for directly querying the database. At the same time, to ensure data consistency, we use broadcast mq to achieve it.

3.1 The first layer of protection

Since nearly half of the queries are empty, we first of all this is the phenomenon of cache penetration.
Cache penetration problem
缓存穿透 (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}}

Guess you like

Origin my.oschina.net/u/4090830/blog/10090668