阅读更多

《大促场景下热点数据写(库存扣减)技术难题解决方案》

 

已经很久没有足够的时间让自己安静下来撰写一篇技术文章,确实近年来,大部分都花在了工作和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倍的下单扣减成功率提升。

转发前,请询问我,3Q!

2
0
评论 共 1 条 请登录后发表评论
1 楼 zhoucanji 2017-12-17 22:15
第三种我从来没想过,不错有收获,算是扩展了

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 大促下热点数据写(库存扣减解决方案

    针对交易系统大促场景下热点数据写优化的相关案例。当然,不同的企业有不同的解决方案和实现,但是万变不离其宗,还是那句话,对于大型网站而言,其架构一定是简单和清晰的,而不是炫技般的复杂化,毕竟解决问题采用...

  • 库存扣减

      何时扣减扣减库存,目前有两种主流方式: 方式1:下单减库存——即用户下单成功时减少库存数量 优点:用户体验好,系统逻辑简单; 缺点:会导致恶意下单或下单后却不买,使得真正有需求的用户无法购买,影响真实...

  • 异地多活架构新突破:库存单元化部署技术思路揭秘

    挑战库存跨机房单元化部署,实现真正的交易单元封闭。

  • redis缓存一致性问题 & 秒杀场景下的实战分析

    本篇文章讲述了在高并发场景下 redis缓存一致性问题 & 秒杀场景下的实战分析, 数据库缓存不一致解决方案, 缓存与数据库双写一致以及秒杀场景下缓存一致性问题的实战解决方案

  • 《超大流量分布式系统架构解决方案》

    超大流量分布式系统架构解决方案

  • 20年IT老民工苦心编撰成超大流量分布式系统架构解决方案文档

    本文融入了作者及其团队实践中的思考、心得与方法,可以帮助大家解决大型网站架构演变过程中遇到的诸多难题。 本文的所有内容,并不是对架构理论的泛泛而谈,而是云集技术架构从 0 到 1 演变的宝贵实践经验。我...

  • 阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史

    本文原题“阿里数据库十年变迁,那些你不知道的二三事”,来自阿里巴巴官方技术公号的分享。 1、引言 第十个双11即将来临之际,阿里技术推出《十年牧码记》系列,邀请参与历年双11备战的核心技术大牛,一起回顾...

  • 系统设计——秒杀系统设计

    自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见。简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程。从架构视角来看,秒杀系统本质是一个高性能、高一致、高...

  • 分布式事务及解决方案

    一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录,正常情况下,两边数据库各自更新成功,两边数据保持一致性,但是在非正常情况下,有可能库存的扣减完成了,随后的订单记录却...

  • 一个秒杀系统的设计思考

    秒杀场景下的一致性问题,主要就是库存扣减的准确性问题。 1 、减库存的方式 电商场景下的购买过程一般分为两步:下单和付款。“提交订单”即为下单,“支付订单”即为付款。基于此设定,减库存一般有以下几个方式:...

  • 数据分析思维扫盲

    技术与能力其他概念数据赋能数据产品二.数据分析可以解决问题类型:1.“是多少”问题的解决思路2.“是什么”问题的解决方法3.“为什么”问题的解决方法4.“会怎样”问题的解决方法5.属于“怎么做”的方法总结三.数据...

  • 经验:一个秒杀系统的设计思考

    点击上方「蓝字」关注我们前言秒杀大家都不陌生。自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见。简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完...

  • 解密京东618技术:重构多中心交易平台 11000个Docker支撑

    在日前的京东技术开放日618技术分享专场,多位京东技术专家联袂解析了京东的技术研发体系如何在高强度的负载压力下,保证业务系统的平稳运行,并介绍大型互联网平台技术升级、备战思路、应急预案设计、问题应对等各...

  • 菜鸟积分系统稳定性建设 - 分库分表&百亿级数据迁移

    点击上方“服务端思维”,选择“设为星标”回复”669“获取独家整理的精选资料集回复”加群“加入全国服务端高端社群「后端圈」作者 | 星花出品| 阿里技术一 前言拆库&数据迁移说...

  • BAT 大厂Java 面试题集锦之核心篇附参考答案

    核心篇数据结构与算法网路:TCP/IP,HTTP操作系统, 文件, shell, CPU, IO, epoll, 非阻塞IO,进程/线程/协程,锁HashMap, Co...

  • A级景区数据文件json

    A级景区数据文件json

  • 使用Java编写的坦克大战小游戏.zip学习资料

    python 使用Java编写的坦克大战小游戏.zip学习资料

  • 【python毕设】p073基于Spark的温布尔登特色赛赛事数据分析预测及算法实现_flask(5).zip

    项目资源包含:可运行源码+sql文件+; python3.7+flask+spark+mysql5.7+vue 适用人群:学习不同技术领域的小白或进阶学习者;可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 项目具有较高的学习借鉴价值,也可拿来修改、二次开发。 有任何使用上的问题,欢迎随时与博主沟通,博主看到后会第一时间及时解答。 系统是一个很好的项目,结合了后端服务(flask)和前端用户界面(Vue.js)技术,实现了前后端分离。 后台路径地址:localhost:8080/项目名称/admin/dist/index.html 前台路径地址:localhost:8080/项目名称/front/index.html

  • C#编写的OPCClient 利用OPCDAAuto.dll

    1.执行setup64.bat注册com组件。文件是64位系统,如果是32位系统请自行修改(C:\Windows\System32) 2.程序目标框架改为.net4,否则报错。

Global site tag (gtag.js) - Google Analytics