《大促场景下热点数据写(库存扣减)技术难题解决方案》
已经很久没有足够的时间让自己安静下来撰写一篇技术文章,确实近年来,大部分都花在了工作和2017年的新作品上。今天难得自己给自己打了瓶100ML的鸡血,出一篇前段时间针对交易系统大促场景下热点数据写优化的相关案例。当然,不同的企业有不同的解决方案和实现,但是万变不离其宗,还是那句话,对于大型网站而言,其架构一定是简单和清晰的,而不是炫技般的复杂化,毕竟解决问题采用最直接的方式直击要害才是最见效的,否则事情只会变得越来越糟。
在大部分情况下,商品库存都是直接在关系型数据库中进行扣减,那么在限时抢购活动正式开始后,那些单价比平时更给力、更具吸引力的热卖商品大家肯定都会积极踊跃的参与抢购,这必然会产生大量针对数据库同一行记录的并发更新操作。因此数据库为了保证原子性,InnoDB引擎缺省会对同一行数据记录进行加锁,把前端的并发请求变成串行操作来确保数据更新时的一致性。
一、在RDBMS中扣减商品库存
先来看看如果是直接在数据库中扣减库存,应该如何避免商品超卖呢?在生产环境中我们可以通过乐观锁机制来避免这个问题,所谓乐观锁,简单来说,就是在item表中建立一个version字段。假设某一个热卖商品的实际库存为n,处于性能考虑对于查询库存操作是不建议加for update的,那么在并发场景下,必然会导致多个用户拿到的stock和version都一样。因此当第1个用户成功扣减商品库存后,则需要将item表中的version加1,这样一来,当第2个用户扣减库存时,由于version不匹配,那么为了提升库存扣减的成功率,可以适当进行重试,如果库存不足,则说明商品已经售罄,反之扣减库存后version继续加1。关于在数据库中使用乐观锁扣减库存的伪代码,如下所示:
public void testStock(int num) { if (version不一致时的重试次数阈值) { SELECT stock,version FROM item WHERE item_id=1; if (如果查询的指定商品存在) { if (判断stock是否够扣减) { UPDATE item SET version=version+1,stock=stock-1 WHERE item_id=1 AND version="+ version +"; if (扣减库存失败) { /* version不一致时开始尝试重试 */ testStock(--num); } else { logger.info("扣减库存成功"); } } else { logger.warn("指定商品已售罄"); } } } }
如果系统前端不配合做限流消峰等处理,随意放任大量的并发更新请求直接在数据库中扣减同一热卖商品的库存数据,这将会导致线程之间相互竞争InnoDB的行锁,由于数据库中针对同一行数据的更新操作是串行执行的,那么某一个线程在未释放锁之前,其余的线程将会全部阻塞在队列中等待拿锁,并发越高时,等待的线程也就会越多,这会严重影响数据库的TPS,从而导致RT线性上升,最终可能引发系统出现雪崩。
二、在Redis中扣减库存
InnoDB的行锁特性其实是一把利与弊都同样明显的双刃剑,在保证一致性的同时却降低了可用性,那么究竟应该如何保证大并发更新热点数据不会导致数据库沦为瓶颈,这其实是秒杀、抢购场景下最核心的技术难题之一。可以尝试将热卖商品的库存扣减操作转移至数据库外,由于Redis的读/写能力要远胜过任何类型的关系型数据库,因此在Redis中实现库存扣减将会是一个不错的替代方案,这样一来,数据库中存储的商品库存可以理解为实际库存,而Redis中存储的商品库存则为实时库存。
在Redis中扣减热卖商品的库存,或许有同学会有疑问,Redis如何保证一致性呢?如何才能做到不超卖和少买呢?答案就是Redis提供的Watch命令来实现乐观锁,和基于MySQL的乐观锁机制一样,并发环境下,通过Watch命令对目标Key进行标记后,当事务提交时,如果监控到目标Key对应的值已经发生了改变,那么也就则意味着版本号发生了改变,因此这一次的事务提交操作就失败,如图1所示:
图1 利用Redis乐观锁扣减商品库存
在Redis中扣减热卖商品的库存主要是出于以下2个目的:
1、首先是为了避免在RDBMS中,多线程之间相互竞争InnoDB引擎的行锁导致RT上升,TPS下降,最终引发雪崩的问题;
2、其次是能够利用Redis与生俱来的高效读/写能力来提升系统的整体吞吐量。
三、利用“分裂”技巧巧妙地提升库存扣减成功率
这里跟大家分享一个笔者公司的业务场景,由于特务特点,我们整点的限时抢购往往是爆款+大库存(几万至十几万不等的库存数),我们都知道限时抢购的峰值其实就是秒杀,并且还伴随的大库存。相对于普通的秒杀场景而言,由于库存并不多,如果上游系统配合交易系统做好扩容、限流保护、隔离(业务隔离、数据隔离,以及系统隔离)、动静分离、localCache等措施,秒杀场景下就能够将绝大多数流量挡在系统上游,让用户流量像漏斗模型一样逐层减少,让流量始终保持在系统可处理的容量范围之内。
由于“变态”的业务特点,业务系统除了要承受亿级流量的冲击,交易系统还要想办法提升下单时的库存扣减成功率,这对于我们来说确实是一次挑战,因为在生产环境中,一次的不小心,将会带来灾难性的后果。我们都知道架构的意义是有序的对系统进行重构,不断减少系统的“熵”,让其不断进步,但架构调整的失误,将会是不可逆的,尤其是那些成熟且用户规模较大的网站。
我们都知道,秒杀活动开始后,能够抢购到心仪的产品,是非常不容的一件事情,因为在同一个单位时间内,除了你之外,还有别的用户也在下单,那么针对同一个爆款的WATCH碰撞概率将会被无情放大,成功率自然降低。如果是小库存,直接返回商品已经售罄即可,但是多大十几万的库存,让用户看得到,买不到,心里痒痒的似乎不太友好,并且运营策略上也希望能够快速消完这些库存好制造噱头。
你不用指望能够利用某一种数据库就能够即提升吞吐量又提升成功率,首先你需要搞明白的是,这是一个实打实的单点问题,要保证一致性,就必然会牺牲成功率,这个矛盾点,该怎么解决呢?我们目前采用的做法是在Redis中,将某一个SKU的Key,拆分成N个对应的subKeys,库存服务在扣减库存的时候,通过轮询路由策略路由到不同的subKey上来降低WATCH碰撞概率,达到大幅度提升下单成功率的目的,如图2所示:
图2 将parentKey分裂为n个subKeys
分裂的概念相信大家都已经清楚了,接下来笔者再跟大家分享关于分裂操作的具体细节和一些注意事项。支撑分裂操作的主要由2部分构成,首先是嵌入在库存服务中的路由组件,其次是分裂管理服务,路由组件的任务很简单,订阅配置中心的分裂规则,然后轮询路由到不同的subKeys上做扣减即可。而分裂管理服务则相对复杂,parentKey的分裂操作就由它负责,并且它还需要处理一些相关的库存聚合(subKeys库存聚合)和下拉(重新划分库存给subKeys)任务。
分裂了,必然需要对分裂信息进行管理,比如:运营后台对某一个parentKey进行大库存扣减、调整某一个parentKey的分裂数量,以及删除某一个parentKey的分裂规则。这些操作全都包含着以下2个动作:
1、库存聚合(subKeys库存聚合),并将subKey库存设置为0;
2、然后将聚合后的库存归还给目标parentKey;
由于聚合和归还并不在同一个事物中,如果因为某些原因导致执行异常,那就悲剧了。比如聚合库存的时候成功了,这时subKeys的库存已经被设置为0,用户是无法正常下单的,但还库存给parentKey这个动作失败了,将会导致商品少卖,所以需要依靠以下2点来尽量保证商品不少卖:
1、业务上增大Redis的重试次数;
2、如果Redis故障,告警后人工介入归还库存;
为什么要区分普通用户扣减库存和运营后台扣减库存?因为这是2个截然不同的概念,因为用户扣减库存,往往会受限于业务(比如限制1个用户1次能够购买的商品数量),但运营后台则不同,有时候可能因为人为原因导致库存设超,因此需要扣减大量的库存,但是如果扣减的库存数量大于每一个subKey持有的有效库存数,则无法完成扣减操作,所以针对运营后台的扣减我们提供有单独的扣减方法,首先会聚合subKeys的库存并将subKey持有的库存数设置为0,将扣减后的库存还给parentKey,再等待重新下拉分配库存给subKeys。在此大家需要注意,如果一个商品特别爆,用户并发越大,聚合再分配的时间窗口期就会越长。
有时候,subKeys之间的库存数可能存在不均匀的情况,那么当某一个subKey持有的库存被扣减完,且无回流库存以便下拉重新分配时,只要路由到这个subKey的库存扣减动作都会是失败的,用户就会存在看得到,买不到的不友好体验,因此可以在路由组件上做动作,当某一个subKey的库存已经消完后,本地需要做剔除动作,下次不路由到这个subKey上。
最后给大家一点建议,如果parentKey的分裂数量越多,库存扣减的成功率就会越大,当然分裂数量也不是越多越好,一般来说一个parentKey分裂为10-20个subKey就够了,相对以前已经拥有了10-20倍的下单扣减成功率提升。
相关推荐
本文将深入探讨在业务复杂、数据量大、并发量高的场景下,如何优化库存扣减过程,避免数据不一致性问题。 #### 一、库存扣减的基础概念 在电子商务中,库存扣减是指用户下单后,系统自动减少相应商品的库存数量。...
总的来说,大促多级缓存方案通过合理利用本地缓存和分布式缓存,结合适当的缓存更新策略,有效地解决了高并发读取场景下的性能挑战,同时通过数据一致性策略保证了业务的正常运行。在实践中,还需要根据系统负载和...
针对大促期间热点数据的处理问题,文章第四章提出了多级缓存+RedisCluster模式下的读/写分离方案,以及基于Redis乐观锁的库存扣减方案。这些方案不仅能够有效提高系统的读写性能,还能保证数据的一致性。 **2. 高...
在千亿级电商秒杀解决方案专题中,我们探讨的是如何在大规模并发...以上就是千亿级电商秒杀解决方案涉及的关键技术点,通过这些技术的综合运用,可以构建一个高效、稳定、安全的秒杀系统,满足高并发场景下的业务需求。
总的来说,高并发业务场景下的秒杀解决方案通过Redis的队列技术和原子操作,能够有效地防止超卖问题,同时通过优化策略提高系统的稳定性和效率。在设计类似系统时,还需要考虑其他因素,如系统的扩展性、容错性和...
本文以订单生成与库存扣减为场景,深入分析分布式事务,并提出解决方案。 订单生成与库存扣减的业务流程是典型的电商系统中的一个操作。用户下单后,系统需要生成订单记录,并相应地扣减商品库存。这两个操作通常会...
在高并发电商场景下,商品超卖(即销售量超出库存)是常见问题,主要由并发扣减库存导致。常规做法是在扣减库存前检查库存充足性,但面对大量并发请求时,这种方法可能失效。为此,可采用以下几种策略: 悲观锁:在...
例如,订单支付成功后需要扣减库存,如果订单和库存分别存储在不同的数据库中,原本的本地事务就无法保证跨库操作的一致性,这就是分布式事务问题,也称作分布式数据一致性问题。 为了解决这个问题,业界提出了一些...
分布式事务是在分布式系统环境下处理数据一致性的核心概念。在单体架构中,本地事务能够很好地保证数据的一致性,但在业务复杂度增加、系统分解为分布式或微服务架构后,本地事务的局限性就显现出来,无法满足跨服务...
在车间计划中,该解决方案可以实现联动,下一车间的执行计划,根据上一车间的实际生产情况进行动态调整。此外,该解决方案还可以物料指示靠拉动,通过生产线的指定点位,动态指示物料配送。 在具体的生产过程中,该...
例如,在库存扣减业务中,用户1和用户2同时执行库存扣减操作,虽然都通过了CAS乐观锁机制的检查,但是在执行修改操作时,可能会出现数据不一致的情况。 二、ABA问题出现原因 ABA问题的出现原因是CAS乐观锁机制中,...
【电子商务进销存平台解决方案】 随着中国电商行业的爆炸性增长,企业面临日益复杂的运营挑战。2010年至2011年间,电商市场规模从4.5万亿攀升至近7万亿,网民数量达到5.13亿,这使得电子商务成为企业发展不可或缺的...
在IT行业中,系统解决方案常常涉及到复杂的技术架构设计,以满足高并发、高性能、高可用性等需求。以下将详细讨论利用消息中间件和缓存实现秒杀系统,以及双11购物限流的实现策略。 1. **秒杀系统实现** 秒杀系统...
### 分布式事务解决方案:基于Dubbo+Nacos的TCC实践 #### 一、背景介绍 随着微服务架构的普及,分布式系统面临着越来越多的复杂挑战,其中之一便是如何保证跨服务调用的一致性,尤其是在涉及多个微服务交互的场景中...
1.减少锁的时间 2.利用缓存 3.水平扩展
随着互联网技术的发展,高并发、大数据量的业务需求推动了分布式系统的普及,分布式锁在此背景下应运而生。 一、分布式锁产生的原因 1. 数据一致性:在分布式环境中,多个节点可能同时操作同一份数据,为了保证数据...