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

支持每秒万单闪杀扣交易_0

时间:2023-03-18 17:55:08 科技观察

该架构可支持每秒万单以上精准扣库存,在应用crash等情况下,也能保证创建订单的数据和存货的扣除最终是严格一致的。现有秒杀系统存在的问题现有的秒杀架构,为了支持高并发,通常会将库存放在Redis中,当收到订单请求时,在Redis中扣除库存。这种设计导致订单创建和库存扣除是非原子操作。如果两次操作之间出现进程崩溃等问题,就会导致数据不一致。即使库存扣除不放在Redis中,而是放在数据库中,通常也会存在不一致的问题。为了模块化和减少耦合,业务系统将库存服务与订单服务分离。只要有单独的服务,数据不一致就不可避免。虽然进程崩溃等问题出现的概率不高,但即使占百分之一,甚至千分之一,也会出现数据不一致的情况,比如扣除的库存和创建成功的订单不一致。库存和订单数据不一致是必须解决的难题。通常的做法是开发人员使用订单数据来校准库存数据。这部分工作非常繁琐复杂,消耗了大量的开发工作。此外,经常需要人工干预来更新数据。手动验证和修复。我们来看看新架构是如何优雅地解决这个问题的。整体架构明确了业务场景。我们提炼出秒杀系统的核心要点,就是以下几点:用户在执行秒杀时,会在某个时间点发出大量的请求。最后,请求量将远远高于库存量。后台需要保证扣库存和创建订单最终是严格一致的。即使中途出现进程崩溃,也不会影响最终的数据。该架构基于https://github.com/dtm-labs/dtm,是一个分布式事务框架,提供跨服务、跨数据库的数据一致性解决方案。在上面的场景中,推演库的大部分描述请求都会失败,时序图如下:本架构中使用了分布式事务框架dtm。在上面的时序图中,库存的扣除是在Redis中进行的,而dtm相关的全局事务的注册和注销也是在Redis中处理的。整个过程依赖Redis,与数据库无关,可以支持极高的并发。从下面的测试数据可以看出,该架构可以轻松应对每秒数万次的flashkill请求。虽然大部分请求是因为扣库存失败而结束,但也会有一定数量的请求,扣库存成功。这种情况的时序图如下:在这个时序图中,库存扣减成功后,会进入订单服务,创建订单并进行后续支付。在这个新的架构中,订单服务只需要处理有效的订单。这时候并发度已经明显下降了。只需要使用订单分库分表、消息队列调峰处理等常规方法,就可以轻松解决问题。原子操作在上述架构中,如果在Redis中扣除库存后,全局事务提交前发生进程崩溃,则两个操作不会同时完成。接下来会发生什么?新架构如何保证数据最终严格一致?这种情况的整个时序图如下:一旦出现这样的进程崩溃,导致两个操作中断,那么dtm服务器会轮询超时未完成的事务。如果有一个全局任务已经准备好但是没有提交,那么他会调用反查接口询问应用是否扣库存成功。如果已经扣除,则提交全局事务,并进行后续调用;如果没有扣除,则全局事务将被标记为失败,不会被处理。保证原子操作的原理,以及DTM在各种情况下的处理策略可以参考二阶段消息,这里不再赘述。核心代码#秒杀界面核心代码如下:gid:="{a}flash-sale-"+activityID+"-"+uidmsg:=dtmcli.NewMsg(DtmServer,gid).Add(busi.Busi+"/createOrder",gin.H{activity_id:activityID,UID:uid})err:=msg.DoAndSubmit(busi.Busi+"/QueryPreparedRedis",func(bb*BranchBarrier)错误{returnbb.RedisCheckAdjustAmount(rds,"{a}stock-"+stockID,-1,86400)})}第一行:对于一般的秒杀活动,一个用户只能购买一次,所以使用活动id+用户id作为全局交易id,保证用户最多可以生成一个全局事务,最多创建一个订单第二行:创建一个二阶段消息的对象,填写dtm服务器地址,全局事务id第三行:为二阶段添加一个分支事务message,事务分支是创建订单服务第4行:调用二阶段消息的DoAndSubmit,该函数第一个参数为反向查询URL(见上图反向查询);第二个参数是回调函数,其中包含业务逻辑。该函数会在成功后执行业务和提交全局事务,保证业务的执行和全局事务的提交是“原子的”第5行:调用RedisCheckAdjustAmount,该函数会进行库存扣减。该函数在进行库存扣减时,如果库存不足,将返回错误;如果库存充足,则进行库存扣减,并且库存扣减成功,可以保证操作的幂等性,保证后续的逆向校验可以得到正确的结果。反向校验的核心代码如下:app.GET(BusiAPI+"/QueryPreparedRedis",dtmutil.WrapHandler2(func(c*gin.Context)interface{}{returnMustBarrierFromGin(c).RedisQueryPrepared(rds)}))的开发者编写反查询的逻辑很简单,对于Redis中的数据,复制粘贴上面的代码即可。详细的反查原理参见第二阶段消息。第二阶段消息的文档介绍了如何在数据库中进行。这里使用Redis来完成类似的反查逻辑,不再赘述。性能#从上面的介绍我们可以看出,对于大多数扣库存失败的请求,只需要三个Redis操作,1.注册全局事务;2、扣除存货;3.修改全局事务失败。这三个操作都是通过lua脚本实现的。一个普通的redis每秒可以支持大约60,000个Lua脚本操作。根据这个分析,我们的新架构理论上可以支持每秒20000个秒杀请求。我做的性能测试报告显示,dtm和扣库存共用一个redis时,每秒可以轻松完成1.2w条秒杀命令,达到理论极限值的60%。具体可以参考下面的性能测试报告做进一步分析,扣除不同的Redis可以进行减库存和全局事务,所以减库存:如果单台Redis支持,那么减库存的理论上限是6w/s,估计实际值为6*0.6=3.6w/ss,如果更进一步,使用Redis6多线程IO,可以获得更高的性能,大约6*2.5*0.6=9w/s。全局事务操作:这里的dtm只需要部署多个组,或者以后使用集群版本,就可以提供远超9w/s的支持。因此,在采用新架构的情况下,有望实现9w/s的秒杀请求流量。以上分析仅限于在常见云厂商的虚拟机上安装Redis。如果简单的硬件升级或者使用云厂商提供的Redis,那么Redis可以提供更强的性能,上面提到的9w/s可以再提升一个档次。参考阿里巴巴双十一的峰值订单:583,000笔/秒,那么上面预估的9w/s几乎可以应付所有的秒杀活动代码示例#完整的可运行代码示例,可以参考https://github.com/dtm-labs/dtm-cases/flash秒杀性能测试详情测试环境,两台阿里云主机,类型:ecs.hfc5.3xlarge12核CPU3.1GHz/3.4GHzPPS130万一台机器运行Redis另一台机器运行测试程序测试过程:准备Redis#选择虚拟机A安装Redisapt-getinstall-yredis#修改/etc/redis/redis.conf#bind127.0.0.1=>0.0.0.0systemctlredisrestartpreparedtm选择虚拟机B进行安装dtmaptupdateaptinstall-ygitwgethttps://golang.org/dl/go1.17.1.linux-amd64.tar.gzrm-rf/usr/local/go&&tar-C/usr/local-xzfgo1.17.1。linux-amd64.tar.gz&&cp-f/usr/local/go/bin/go/usr/local/bin/gogit克隆https://github.com/dtm-labs/dtm.git&&cddtm&&gitcheckoutv1.11.0&&cdbench&&make#Modifydtm/bench/test-flash-sales.sh#exportBUSI_REDIS=localhost:6379=>虚拟机A的私网ip运行testshtest-flash-sales.sh得到结果我的结果是1.2w左右seckillrequestscanbecompletedpersecond:Requestspersecond:11970.21[#/sec](均值)总结我们提出一个全新的秒杀架构,可以保证创建订单和扣款库存是原子的,估计可以支持9w/sflashkill请求流量。帮助大家更好更快的解决秒杀的业务需求。欢迎访问我们的项目并star支持我们:https://github.com/dtm-labs/dtm