文档章节

让我们聊聊秒杀这东西

通九互联科技
 通九互联科技
发布于 2015/10/28 14:27
字数 2658
阅读 2540
收藏 64

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

万事皆有因

这段似乎都成我写blog标准开头。言归正转,公司以前业务涉及到秒杀,并且是白天从10点起到晚上10点每小时一次(TT天天心惊肉跳的),周六还有个大礼包活动(重量级,经常会出一些你意想不到的事情,例如不活跃的用户突然间活跃了,量级飙升TT)。同时,最近随着创业的兴起,还是有很多人关注秒杀这技术怎么做。虽然很多NB的大厂(小米,淘宝,JD等)已经讲过这东西了,但是我还是想讲讲这件事情。下面我就说说一个小厂是如何做秒杀的。

 

小厂有多小,小厂有多大

后端只有2个研发工程师和2个前端工程师,当时还没有全职的运维,不过服务器的数量有40多台(还是挺多的)。用户量呢,下载和注册都在千万级别了,活跃也在百万级别。好了,小厂很小,但是小厂也很大。

 

初出茅庐

很多人感觉,敢用初出茅庐这标题,应该很牛吧,然而并没有。并且是意想不到的惨,惨不忍睹。第一个版本的秒杀系统,完全是依赖MySQL的事务,不言而喻,大家都会知道有多惨。我直接告诉大家结果就可以了:

  1. 整个系统在秒杀期间基本上停摆了,500和超时异常的多。

  2. 准备秒杀的产品数量是100,最后卖出去了400份。

我们来分析下为什么会这样:

  1. MySQL本身能承载链接数量有限,在秒杀的时候大量的链接处在事务状态,且绝大部分事务是需要回滚的,这就造成了很大的IO压力和计算压力

  2. 那为什么会超卖呢,因为最开始使用的主从结构,读写是分离的,主库压力那么大,从库同步跟不上,造成了卖出去的产品在毫秒级内再查询结果看起来就是没卖出去。简而言之就是就是技术不熟悉导致设计失误。

 

初窥门径

出第一次事故的时候,说句心里话,对一个刚毕业1年的工程师还是挺蒙,然后就各种猜想。不过好在当时淘宝的一个人的blog上提了MySQL句事务的问题,算是找到方向了。然后就这样,秒杀活动就先暂停了一个星期,这个星期中我和同事都做了什么呢?

  1. 搭建了一个测试环境,模拟了下秒杀的情况,观察了MySQL的事务和主从的整体情况

  2. 修改秒杀流程

我先说下第一版的流程:

  1. 从用户数据库查询用户积分是否充足,从规则数据库中查询用户是否符合条件

  2. 从数据库中读出一个产品的ID

  3. 然后事务性的将产品ID和用户ID关联,减少用户积分和更新用户规则数据,更新产品ID的状态

那么问题就明显了,读产品ID的时候是没有事务的,这必然会存在问题的。那么我们是如何修改的呢?将读取产品ID这件事放入了整个事务中。那么整个流程就变成了:

  1. 从用户数据库查询用户积分是否充足,从规则数据库中查询用户是否符合条件

  2. 事务性的读出符合条件的产品,并立刻更新状态,接着完成用户ID和产品ID的关联及减少积分等工作

那这样还有问题吗?依然有,最后还是超卖了,大家会问为什么?这里面我们犯了另一个错误,使用代码判断产品的状态而非存储过程,这样即便是在数据库事务内,但没有可以触发数据库事务回滚的条件,所以还会错误的将卖出的产品再次更新为卖出的状态。经历两次惨痛的教训,我们才逐步的走上正轨,一个地方不会跌倒三次。

 

登堂入室

我们已经发现了很多问题,最后该怎么解决,我们决定先解决正确性,再解决速度的问题,我们使用了一段时间的存储过程加关键ID做成唯一主键的方式,整个秒杀流程的第二部分,就是个完整的存储过程(往事不堪回首,天天被用户骂非常慢)。这个时候唯一能做的就是补充理论知识,发奋图强了。

在这个第二个版本的设计中,我们开始采用Redis,我们测试了Redis的pubsub机制,最开始想使用Redis的pubsub进行排队(现在想想有点幼稚,但是老天帮了我一把,当时鬼使神差的就感觉这机制不靠谱)。但是最终的方案嗯,使用了正向队列。何为正向队列?我们将产品的ID在秒杀开始前,全部读入指定的队列中,秒杀流程就变成了:

  1. 判读Redis队列是否为0,为0结束

  2. 判读用户是否符合规则,是否有足够多的积分

  3. 从队列pop出一个产品ID,如果pop不出来就结束

  4. 开事务,改变产品ID的状态,关联用户ID和产品ID,更新规则和积分

这个时候基本上彻底解决了超卖和性能的问题了,但是还会有用户在骂,为什么?因为还不够快。

 

渐入佳境

我们发现为什么会慢,因为数据库的事务,回滚虽然少了,但是还是处理不过来,1s也就那100多个事务能完成,剩下的各种跟不上。此时此刻,我们直接采购了当时算是比较强劲的数据库服务器,事务量一下提高到了1000tps。但是这远远跟不上用户的增长速度(TT没业务也哭,有业务也哭)。

我们既然已经发现了排队理论这么有用,我们决定使用RabbitMQ,延迟处理队列。经过这次改造,我们秒杀的流程就变成了:

  1. 判断Redis队列是否为0,为0结束

  2. 判读用户是否符合规则,是否有足够积分

  3. 从队列pop出一个产品ID,如果pop不出来就结束

  4. 将用户ID和产品ID放入RabbitMQ中,后面的消费者慢慢的吞下去

这时候用户在速度上算是基本满意了,不过却带来了新的问题。判断用户是否符合规则的时候,由于消费者慢慢的消化而数据库没有实时的更新,导致一个用户可以秒杀多个商品,很多用户就不满意了(TT用户是上帝)。

 

略有小成

我们再次拿出了强大的Redis,我们将Redis当作缓存。我们把秒杀的业务逻辑直接变成了这样:

  1. 先判断Redis的队列是否为0,为0结束

  2. 判断Redis中用户的信息是否符合规则,积分是否符合规则

  3. 从队列pop出一个产品ID,如果pop不出来就立刻结束

  4. 立刻更新Redis中用户的缓存信息和积分信息,再放入RabbitMQ,让消费者消费

这样看起起来似乎没什么问题了,但是还是存在问题的,就是pop出产品ID到更新Redis用户信息的一瞬间还是能让部分用户钻空子的,毕竟Redis没有MySQL那种强事务机制。

 

心领神会

在这个阶段,我们用Erlang的mnesia写了一个Redis特定功能替代品,但使用了段时间很快放弃了,因为我们找到了更好的解决方式。让RabbitMQ的消费者使用一致性的hash,那么特定的用户一定会落到特定的消费者身上,消费者做去重判断。这样减少了,我们自己维护基础软件的成本(2个后端工程师TT,别瞎折腾)。

 

随心所欲

当我们的用户量逐步上升,系统依然出现吃紧和性能跟不上的阶段。

这个时候,我们大量使用一致性Hash和随机算法,其中过程就变成了。

  1. 将秒杀的产品ID分成多个队列放在Redis集群上,然后将一个产品总数量放在一个Redis上(这个Redis是瓶颈,但是基本上20W的TPS满满的达到了)

  2. 为用户随机一个数字,在一定范围内,直接告诉秒杀失败(纯看运气,纯丢给应用服务器去玩了)

  3. 检查用户规则和用户积分,还有产品总数量,总数量为0,直接结束。

  4. 为用户随机一个产品ID队列,尝试pop,pop不出数据,直接结束(还是看运气)

  5. 更新用户Redis的缓存和产品总数量的缓存(decr),然后交给RabbitMQ和消费者慢慢处理。

这个时候,基本上30wTPS,随便玩。

 

返璞归真

说了这么多废话,总结下吧。对于秒杀这种业务,优先保稳定和正确,最后才能保服务量。不稳定没得玩,不正确,很可能一单亏死。技术上,我个人认为小厂也能做看似很NB的秒杀只要用好以下几个相关技术:

  1. 削峰,不管是随机丢弃,还是多层筛选,尽可能减少进入核心业务的用户数

  2. 排队,在秒杀场景下,排队不单单可以减少系统压力,还能保证正确性

  3. 分区,使用分区可以降低一个节点当机带来整体性的损害或者雪崩性的系统不可用

  4. 最终一致,很多时候,不一定要强一致性,只要能保证最后数据的正确,哪怕是手工修复,都能带来大规模的性能提升

 

转自TechTalk https://www.ttalk.im/topics/18

 

© 著作权归作者所有

通九互联科技

通九互联科技

粉丝 133
博文 37
码字总数 21775
作品 1
大连
个人站长
私信 提问
加载中

评论(21)

开源划水
开源划水
ChronosLiu
ChronosLiu
mark
通九互联科技
通九互联科技 博主

引用来自“pandudu”的评论

好赞!

回复@pandudu : 经验之谈而已,大公司大概也不会像我样搞,很多电商公司都有大秒系统。
_森屿海巷_
_森屿海巷_
好赞!
通九互联科技
通九互联科技 博主

引用来自“林林9”的评论

良心制作。制作过程很给力。
就是经历了一些很坑的事情。
林林9
林林9
良心制作。制作过程很给力。
awu
awu
虽然过程惨惨的,结局不错!
通九互联科技
通九互联科技 博主

引用来自“cwt1357”的评论

大赞,
小弟正在学习,有些业务想仔细了解一下 【】里面的是小弟的疑问,还望赐教
将秒杀的产品ID【这里有没有相同的产品ID呢】分成多个队列放在Redis集群上,然后将一个产品总数量放在一个Redis上(这个Redis是瓶颈,但是基本上20W的TPS满满的达到了)

为用户随机一个数字,在一定范围内,直接告诉秒杀失败(纯看运气,纯丢给应用服务器去玩了)

检查用户规则和用户积分,还有产品总数量,总数量为0,直接结束。

为用户随机一个产品ID队列,尝试pop,pop不出数据,直接结束(还是看运气)

更新用户Redis的缓存和产品总数量的缓存(decr),然后交给RabbitMQ和消费者慢慢处理【秒杀后返回给用户什么结果呢,因为消费者还在处理】。

这几步业务逻辑都在一段代码里面吗,怎么保证事务呢

”排队,在秒杀场景下,排队不单单可以减少系统压力,还能保证正确性“这里的排队是用于请求(request)的排队吗。
你说的一个问题,我们产品ID不重复,例如说产品ID是10有100个,我们可以产生一个序列10_1,10_2....10_100,这样就不会产生重复。第二个问题,进入队列之后,就代表着秒杀成功,给用户的就是成功的结果。第三个问题,我们使用了两次排队技术,Redis中的排队保证业务部重复,RabbitMQ排队减少业务压力同时进行去重操作,这样我们就不需要一个大事务去保证业务,而是采用多次日志和最终一致性来进行保证业务的最终正确性。
cwt1357
cwt1357
大赞,
小弟正在学习,有些业务想仔细了解一下 【】里面的是小弟的疑问,还望赐教
将秒杀的产品ID【这里有没有相同的产品ID呢】分成多个队列放在Redis集群上,然后将一个产品总数量放在一个Redis上(这个Redis是瓶颈,但是基本上20W的TPS满满的达到了)

为用户随机一个数字,在一定范围内,直接告诉秒杀失败(纯看运气,纯丢给应用服务器去玩了)

检查用户规则和用户积分,还有产品总数量,总数量为0,直接结束。

为用户随机一个产品ID队列,尝试pop,pop不出数据,直接结束(还是看运气)

更新用户Redis的缓存和产品总数量的缓存(decr),然后交给RabbitMQ和消费者慢慢处理【秒杀后返回给用户什么结果呢,因为消费者还在处理】。

这几步业务逻辑都在一段代码里面吗,怎么保证事务呢

”排队,在秒杀场景下,排队不单单可以减少系统压力,还能保证正确性“这里的排队是用于请求(request)的排队吗。
通九互联科技
通九互联科技 博主

引用来自“JFinal”的评论

秒杀之前,将相关数据加载到内存,自己写程序做好同步
是的,确实需要先将数据加载到内存,然后要做好各个组件间的协调一致。
从构建分布式秒杀系统聊聊Disruptor高性能队列

前言 秒杀架构持续优化中,基于自身认知不足之处在所难免,也请大家指正,共同进步。文章标题来自码友的建议,希望可以把阻塞队列ArrayBlockingQueue这个队列替换成Disruptor,由于之前曾接触...

小柒2012
2018/05/24
0
0
从构建分布式秒杀系统聊聊限流特技

前言 俗话说的好,冰冻三尺非一日之寒,滴水穿石非一日之功,罗马也不是一天就建成的。两周前秒杀案例初步成型,分享到了中国最大的同性交友网站-码云。同时也收到了不少小伙伴的建议和投诉。...

小柒2012
2018/06/12
0
0
从构建分布式秒杀系统聊聊限流的多种实现

前言 俗话说的好,冰冻三尺非一日之寒,滴水穿石非一日之功,罗马也不是一天就建成的。两周前秒杀案例初步成型,分享到了中国最大的同×××友网站-码云。同时也收到了不少小伙伴的建议和投诉...

小柒2015
2018/06/10
0
0
从构建分布式秒杀系统聊聊WebSocket推送通知

前言 秒杀架构到后期,我们采用了消息队列的形式实现抢购逻辑,那么之前抛出过这样一个问题:消息队列异步处理完每个用户请求后,如何通知给相应用户秒杀成功? 场景映射 首先,我们举一个生...

小柒2012
2018/07/19
0
0
从构建分布式秒杀系统聊聊分布式锁

前言 最近懒成一坨屎,学不动系列一波接一波,大多还都是底层原理相关的。上周末抽时间重读了周志明大湿的 JVM 高效并发部分,每读一遍都有不同的感悟。路漫漫,借此,把前段时间搞着玩的秒杀...

小柒2012
2018/08/01
0
0

没有更多内容

加载失败,请刷新页面

加载更多

在C语言中“静态”是什么意思?

我已经在C代码的不同地方看到了static一词。 这就像C#中的静态函数/类(实现在对象之间共享)吗? #1楼 多文件变量作用域示例 在这里,我说明了静态如何影响多个文件中函数定义的范围。 交流...

javail
25分钟前
15
0
利用 FC + OSS 快速搭建 Serverless 实时按需图像处理服务

作者:泽尘 简介 随着具有不同屏幕尺寸和分辨率设备的爆炸式增长,开发人员经常需要提供各种尺寸的图像,从而确保良好的用户体验。目前比较常见的做法是预先为一份图像存放多份具有不同尺寸的...

阿里巴巴云原生
27分钟前
14
0
前端架构最佳实践

Folders-by-Feature Structure 胜过 Folders-by-Type Structure

lilugirl
38分钟前
11
0
Seata AT 模式启动源码分析

从上一篇文章「分布式事务中间件Seata的设计原理」讲了下 Seata AT 模式的一些设计原理,从中也知道了 AT 模式的三个角色(RM、TM、TC),接下来我会更新 Seata 源码分析系列文章。今天就来分...

后端进阶
39分钟前
19
0
Python中“自我”一词的目的是什么?

Python中self词的目的是什么? 我知道它是指从该类创建的特定对象,但是我看不到为什么要将它显式地作为参数添加到每个函数中。 为了说明这一点,在Ruby中,我可以这样做: class myClass ...

技术盛宴
41分钟前
16
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部