当前位置: 首页 > 科技观察

京东抢购服务的高并发实践

时间:2023-03-16 11:56:20 科技观察

服务介绍Flashsale,英文又称Flashsale,起源于法国网站VentePrivée。闪购模式是一种以互联网为媒介的B2C电子零售交易活动。以限时特卖的形式,定期定期推出国际知名品牌的产品。持续5-10天,先到先得,时间有限,送完即止。客户必须在规定的时间内(通常为20分钟)付款,否则该产品将被放回待售产品队列中。车型特点:品牌丰富——推出国内外一、二线名牌产品供消费者选择;时间短——每个品牌上线时间较短,一般为5-10天,先到先得,限量发售,售完即止;超低折扣——商品原价1-5折销售,超强折扣。摘自【百度百科】,通过这个介绍,相信我对闪购有了一定的了解,我们内部称之为闪购系统。对于抢购系统,首先要有可以抢购的活动,而这些活动都是促销性质的,比如直降500元。其次,要有丰富多样的可抢购活动,让用户有足够的选择余地。6.18(6.1-6.20)期间,增量促销活动非常多。可能某个活动特别给力,大部分用户都抢着抢。一定是对系统的考验。这样,抢购系统具有闪购的特点,并发访问量高。同时,用户还可以购买多个限时抢购商品,与普通商品一起进入购物车进行结算。这种大型活动的负载可能是平时的几十倍,通过增加硬件、优化瓶颈代码等方式很难达到目的,所以必须对抢购系统进行特殊设计。服务主要功能创建促销服务:采购销售创建促销,促销管理系统审核通过后,调用抢购系统创建促销;加急服务:对符合条件的订单进行剩余金额操作,主要是抵扣剩余金额;其中SKU目前主要针对单品促销、直降或固定价格,如:主渠道手机APP、微信、手机QQ和主站限购类型限购数量、限ip、限pin和限ip和pin系统设计积分如何实现实时盘点?这里所说的库存不是真正的库存,而是促销时可以抢购的数量。实际库存由基本库存提供。用户点击“提交订单”按钮后,在抢购系统中获得资格后,将从基础库存服务中扣除真实库存;抢购系统控制合格/剩余数量。传统方案使用数据库行锁,但在推广高峰期,数据库压力过大导致服务不可用。目前使用一个redis集群(16个shard)来缓存促销信息,比如促销id,促销剩余次数,抢到次数等,将促销id哈希到对应的shard,实时扣除剩余次数.当剩余数量为0或取消优惠时,价格将恢复原价。抢购redis的数据结构如何设计?业务员发布促销信息后,在抢购redis中生成一条记录,为抢购业务提供基础信息。每个促销对应一个促销id,促销信息是一个Hashes结构。比如促销A,对应的类型是单品促销,我们暂时认为type值为1,redis中对应的key为C_A_1,数据结构内容类似如下:o:100//originalquantityb:99//可以抢购的数量,如果抢到一个还剩99c:1//抢购记录的条数用于限流。如何保证不超卖?因为扣取资格是一组操作,所以我们使用EVAL对redis的剩余数进行操作,实现原子性伪代码如下:localkey=KEYS[1]localtag="b"localnum=tonumber(ARGV[1]);locallastNum=redis.call('HINCRBY',key,tag,-num);如果业务判断ortonumber(lastNum)==0则returnlastNumend上面代码会返回剩余的数,如果小于等于0,则没有库存。如何提高吞吐量?减少网络交互(一次性抓取数据通过EVALSHA提交到redis集群);数据库操作是异步的(使用JMQ异步记录日志)。如何保证可用性?使用JSF(京东内部SOA框架)对外开放服务(抓取服务,发布促销服务),可以降级为系统自带的webservice服务;抢购系统主要依赖redis集群,redis采用一主三从集群方案。两个机房,每个集群有16个shard,每两个shard共用一台物理机,可以通过配置中心切换主从;如果Redis挂了,如何恢复?通过汇总MySQL日志中的抢购和取消,恢复Redis抢购数量。这里的系统架构主要涉及到对rush服务架构的分析,因为它具有典型的高并发特性。以下为基本架构概览:注:此处库存为可抢购数量设置,或称合格/剩余数量,并非真正的实际库存。Redis使用单一的Lua解释器来运行所有脚本,Redis还保证脚本将以原子方式执行:当一个脚本运行时,不会执行其他脚本或Redis命令。这个特性很好的解决了在抢服务过程中并发带来的问题。REDIS+LUA抢购子流程:这个流程是通过luascript脚本实现的,我们暂时命名为q.lua(主要作用是限制流量,扣除促销活动剩余次数)。这样抢购过程结合Script脚本,一次性提交给Redis,减少网络交互,大大提高性能。q.lua伪代码:--[[--!@briefpromotionId下限流量:可以防止某个promotion过热导致服务不可用--]]localfunctionlimited()--todo:实现end--[[--!@brief限制逻辑(ipandpin):比如有些活动限制ip,这里检查ip是否存在,如果是限制ip的抢购活动,会抛出异常提示ip已经存在,无法抢购--]]localfunctioncheck_ip_pin()--todo:实现结束--[[--!@brief记录订单号:主要是为了实现grab方法的幂等性,网络超时时调用者可以重复调用,订单号会直接返回抢购成功,以免超卖--]]localfunctionrecord_order_id()--todo:implementend--[[--!@briefdeductionoftheremainingnumber--]]localfunctionscalebuy()--locallastNum=redis.call('HINCRBY',key,tag,-num);--end--调用顺序不可调整--1限流localstatus,msg=limited()ifstatus==0thenreturnmsgend--2检查status,msg=check_ip_pin()ifstatus==0thenreturnmsgend--3记录订单状态,msg=record_order_id()ifstatus==0thenreturnmsgend--4扣除剩余数量状态,msg=scalebuy()ifstatus==0thenreturnmsgend--5返回成功标记return1子流程如下:1.解析请求参数,根据推广Id根据Jedis中间的MurmurHash算法获取分片,然后发送请求参数argList按照分片打包Pipeline分批处理;2、获取系统初始化时SCRIPTLOAD加载q.lua返回的字符串shaValue;3.执行EVALSHA,伪代码如下://其他操作Pipelinep;//初始化pp.evalsha(shaValue,keyList,argList);//其他操作4.处理返回结果。只要一个碎片失败,抢购就会失败。补充:详细的脚本操作请参考Jedis中的ScriptingCommandsTest。JMQ发送子流程:REDIS+LUA抢购子流程执行成功只代表redis操作成功,发送jmq(京东mq基础服务)成功(后台异步更新real-timeinventorytoMySQL)认为抢购成功,否则认为抢购失败。之所以这样设计,主要是为了保证抢购的redis和mysql记录最终一致。如果发送失败,需要回滚REDIS+LUA抢购子流程(恢复Redis的库存和抢购资格)。当然,应该考虑降级。当jmq不可用时,直接切换到jsf服务模拟jmq,即直接写入MySQL库。前提是减少当前限制的数量,否则数据库可能存在压力过大的风险。这样虽然降低了用户体验,但是服务还是可以的。开关均在配置中心操作,一分钟内生效。资格回滚子流程:发送JMQ失败必须回滚,否则会出现超卖现象。具体过程类似于REDIS+LUA抢购子过程,是其逆过程,只是运行脚本不同。限流处理方式-级别限流,通过配置中心配置限流阈值,1分钟后生效。伪代码如下:privatestaticAtomicIntegeratomic=newAtomicInteger(0);publicvoidtest(){try{//限流intlimitNum=XXX.getLimitNum();intnowConcurrent=atomic.incrementAndGet();if(nowConcurrent>limitNum){//异常处理}//正常业务逻辑}catch(Exceptione){//异常处理}finally{atomic.decrementAndGet();}}在q.lua中提升限流的级别主要是通过抓取次数和获取次数的比较C_A_1中c的阈值。例如促销A在60秒内只能抢购60000次,超过60000次则促销失败。至此,抢购系统的核心逻辑介绍完毕。还有一些设计时需要考虑的细节,比如限购(比如每人限购2次)、取消真实库存不足、用户取消订单退货资格、Redis挂掉恢复数据、停止促销(超时停止,库存不足停止)等。作者:张子良,京东高级开发工程师,负责京东后台服务系统的架构和开发。【本文来自专栏作者张凯涛微信公众号(凯涛的博客)公众号id:kaitao-1234567】点此查看作者更多好文