关于并发:随机高并发查询结果一致性设计实践
作者:京东物流 赵帅 姚再毅 王旭东 孟伟杰 孔祥东 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的问题。 引入缓存,咱们就要思考缓存数据一致性的问题。 无关缓存一致性问题,可自行百度,这个就不在叙述。 ...