共计 3820 个字符,预计需要花费 10 分钟才能阅读完成。
作者:京东物流 赵帅 姚再毅 王旭东 孟伟杰 孔祥东
1 前言
物流合约核心是京东物流合同治理的惟一入口。为商家提供合同的创立,盖章等能力,为不同业务条线提供合同的定制,归档,查问等性能。因为各个业务条线泛滥,为各个业务条线提供高可用查问能力是物流合约核心重中之重。同时计费零碎在每个物流单结算时,都须要查问合约核心,确保商家签订的合同内容来保障计费的准确性。
2 业务场景
1. 查问维度剖析
从业务调用的起源来看,合同的大部分是计费零碎在每个物流单计费的时候,须要调用合约核心来判断,该商家是否签订合同。
从业务调用的入参来看,绝大部分是多个条件来查问合同,但根本都是查问某个商家,或通过商家的某个属性(例如业务账号)来查问合同。
从调用的后果来看,40% 的查问是没有后果的,其中绝大部分是因为商家没有签订过合同,导致查问为空。其余的查问后果,每次返回的数量较少,个别一个商家只有 3 到 5 个合同。
2. 调用量分析
调用量
目前合同的调用量,大略是在每天 2000W 次。
一天的调用量统计:
调用工夫
每天高峰期为上班时间,最高峰为 4W/min。
一个月的调用量统计:
由上能够看出,合同每日的调用量比拟均匀,次要集中在 9 点到 12 点和 13 点到 18 点,也就是上班时间,整体调用量较高,根本不存在调用暴增的状况。
总体剖析来看,合约核心的查问,调用量较高,且较均匀,根本都是随机查问,也并不存在热点数据,其中有效查问占比拟多,每次查问条件较多,返回数据量比不大。
3 方案设计
从整体业务场景剖析来看,咱们决定做三层防护来保障调用量的撑持,同时须要对数据一致性做好解决。第一层是布隆过滤器,来拦挡绝大部分有效的申请。第二层是 redis 缓存数据,来保障各种查问条件的查问尽量命中 redis。第三层是间接查询数据库的兜底计划。同时再保证数据一致性的问题,咱们借助于播送 mq 来实现。
1. 第一层防护
因为近一半的查问都是空,咱们首先这是缓存穿透的景象。
缓存穿透问题
缓存穿透(cache penetration)是用户拜访的数据既不在缓存当中,也不在数据库中。出于容错的思考,如果从底层数据库查问不到数据,则不写入缓存。这就导致每次申请都会到底层数据库进行查问,缓存也失去了意义。当高并发或有人利用不存在的 Key 频繁攻打时,数据库的压力骤增,甚至解体,这就是缓存穿透问题。
惯例解决方案
缓存特定值
个别对于缓存穿透咱们比拟惯例的做法就是,将不存在的 key 设置一个固定值,比如说 NULL,&& 等等,在查问返回这个值的时候,咱们利用就能够认为这是一个不存在的 key, 那咱们利用就能够决定是否持续期待,还是持续拜访,还是间接放弃,如果持续期待拜访的话,设置一个轮询工夫,再次申请,如果取到的值不再是咱们预设的,那就代表曾经有值了,从而防止了透传到数据库,从而把大量的相似申请挡在了缓存之中。
缓存特定值并同步更新
特定值做了缓存,那就意味着须要更多的内存存储空间。当存储层数据变动了,缓存层与存储层的数据会不统一。有人会说,这个问题,给 key 加上一个过期工夫不就能够了,的确,这样是最简略的,也能在肯定水平上解决这两个问题,然而当并发比拟高的时候(缓存并发),其实我是不倡议应用缓存过期这个策略的,我更心愿缓存始终存在;通过后盾零碎来更新缓存中的数据一致性的目标。
布隆过滤器
布隆过滤器的核心思想是这样的,它不保留理论的数据,而是在内存中建设一个定长的位图用 0,1 来标记对应数据是否存在零碎;过程是将数据通过多个哈希函数计算出不同的哈希值,而后用哈希值对位图的长度进行取模,最初失去位图的下标位,而后在对应的下标位上进行标记;找数的时候也是一样,先通过多个哈希函数失去哈希值,而后哈希值与位图的长度进行取模失去多个下标。如果多个下标都被标记成 1 了,那么阐明数据存在于零碎,不过只有有一个下标为 0 那么就阐明该数据必定不存在于零碎中。
在这里先通过一个示例介绍一下布隆过滤器的场景:
以 ID 查问文章为例,如果咱们要晓得数据库是否存在对应的文章,那么最简略的形式就是咱们把所有数据库存在的 ID 都保留到缓存去,这个时候当申请过进入零碎,先从这个缓存数据里判断零碎是否存在对应的数据 ID,如果不存在的话间接返回进来,防止申请进入到数据库层,存在的话再从获取文章的信息。然而这个不是最好的形式,因为当文章的数量很多很多的时候,那缓存中就须要存大量的文档 id 而且只能持续增长,所以咱们得想一种形式来节俭内存资源当又能是申请都能命中缓存,这个就是布隆过滤器要做的。
咱们剖析布隆过滤器的优缺点
长处
1. 不须要存储数据,只用比特示意,因而在空间占用率上有微小的劣势
2. 检索效率高,插入和查问的工夫复杂度都为 O(K)(K 示意哈希函数的个数)
3. 哈希函数之间互相独立,能够在硬件指令档次并行计算,因而效率较高。
毛病
1. 存在不确定的因素,无奈判断一个元素是否肯定存在,所以不适宜要求 100% 准确率的场景
2. 只能插入和查问元素,不能删除元素。
布隆过滤器剖析:面对长处,完全符合咱们的诉求,针对毛病 1,会有极少的数据穿透对系统来说并无压力。针对毛病 2,合同的数据,原本就是不可删除的。如果合同过期,咱们能够查出单个商家的所有合同,从合同的完结工夫来判断合同是否无效,并不需要取删除布隆过滤器里的元素。
思考到调用 redis 布隆过滤器,会走一次网络,而咱们的查问近一半都是有效查问,咱们决定应用本地布隆过滤器,这样就能够缩小一次网络申请。然而如果是本地布隆过滤器,在更新时,就须要对所有机器的本地布隆过滤器更新,咱们监听合同的状态来更新,通过 mq 的播送模式,来对布隆过滤器插入元素,这样就做到了所有机器上的布隆过滤器对立元素插入。
2. 第二层防护
面对高并发,咱们首先想到的是缓存。
引入缓存,咱们就要思考缓存穿透,缓存击穿,缓存雪崩的三大问题。
其中缓存穿透,咱们已再第一层防护中解决,这里只解决缓存击穿,缓存雪崩的问题。
缓存击穿(Cache Breakdown)缓存雪崩是指只大量热点 key 同时生效的状况,如果是单个热点 key,在不停的扛着大并发,在这个 key 生效的霎时,继续的大并发申请就会击破缓存,间接申请到数据库,如同蛮力击穿一样。这种状况就是缓存击穿。
惯例解决方案
缓存生效扩散
这个问题其实比拟好解决,就是在设置缓存的时效工夫的时候减少一个随机值,例如减少一个 1 - 3 分钟的随机,将生效工夫扩散开,升高个体生效的概率;把过期工夫管制在零碎低流量的时间段, 比方凌晨三四点,避过流量的高峰期。
加锁
加锁,就是在查问申请未命中缓存时,查询数据库操作前进行加锁,加锁后前面的申请就会阻塞,防止了大量的申请集中进入到数据库查问数据了。
永恒不生效
咱们能够不设置过期工夫来保障缓存永远不会生效,而后通过后盾的线程来定时把最新的数据同步到缓存里去
解决方案:应用分布式锁,针对同一个商家,只让一个线程构建缓存,其余线程期待构建缓存执行结束,从新从缓存中获取数据。
缓存雪崩(Cache Avalanche)当缓存中大量热点缓存采纳了雷同的实效工夫,就会导致缓存在某一个时刻同时实效,申请全副转发到数据库,从而导致数据库压力骤增,甚至宕机。从而造成一系列的连锁反应,造成零碎解体等状况,这就是缓存雪崩。
解决方案:缓存雪崩的解决方案是将 key 的过期设置为固定工夫范畴内的一个随机数,让 key 平均的生效即可。
咱们思考应用 redis 缓存,因为每次查问的条件都不一样,返回的后果数据又比拟少,咱们思考限度查问都必须有一个固定的查问条件,商家编码。如果查问条件中没有查商家编码,咱们能够通过商家名称,商家业务账号这些条件来反查查商家编码。
这样咱们就能够缓存单个商家编码的所有合同,而后再通过代码应用 filter 对其余查问条件做反对,防止不同的查问条件都去缓存数据而引发的缓存数据更新,缓存数据淘汰曾经缓存数据统一等问题。
同时只缓存单个商家编码的所有合同,缓存的数据量也是可控,每个缓存的大小也可控,根本不会呈现 redis 大 key 的问题。
引入缓存,咱们就要思考缓存数据一致性的问题。
无关缓存一致性问题,可自行百度,这个就不在叙述。
如图所示 对于商家编码维度的缓存数据,咱们通过监听合同的状态,应用 mq 播送来删除对应商家的缓存,从而避免出现缓存和数据一致性的相干问题。
3. 第三层防护
第三层防护,天然是数据库,如果有查问通过了第一层和第二层,那咱们须要间接查询数据库来返回后果,同时,咱们对间接调用到数据库的线程进行监控。
为防止一些未知的查问大量查问涌入,导致数据库调用保障的问题,尤其是大促时,咱们能够提前对数据库里的所有商家合同进行提前缓存。在缓存时,为防止缓存雪崩问题,咱们对将 key 的过期设置为固定工夫范畴内的一个随机数,让 key 平均的生效。
同时,为防止仍然存在意外的状况,有大量查问涌入。咱们通过 ducc 开关管制数据库的查问,如调用量太高导致无奈撑持,则间接敞开数据库的调用,保障数据库不会间接宕机导致整个业务不可用。
4 总结
本文次要剖析了面对高并发调用的调用场景设计及的技术计划,在引入缓存的同时,也要思考理论的调用入参及后果,面对减少的网络申请,是否能够进一步缩小。面对 redis 缓存,是否能够通过一些伎俩防止所有查问条件都须要缓存,带来的缓存爆炸,缓存淘汰策略等问题,以及解决缓存与数据统一等一系列问题。
本计划是依据具体的查问业务场景设计具体的技术计划,针对不同的业务场景,对应的技术计划也是不一样的。