让我们聊聊秒杀这东西

万事皆有因

这段似乎都成我写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. 最终一致,很多时候,不一定要强一致性,只要能保证最后数据的正确,哪怕是手工修复,都能带来大规模的性能提升

转载于:https://www.cnblogs.com/liuchuanfeng/p/6908365.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/416656.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

工作总结11:vue获取数据接口

getAction(/deparment/list,{})第一个参数是请求的接口url,第二个参数是需要传的请求接口需要传的参数例如需要传page1和limit5,第二个参数就要像这样写{page:1,limit:5}

从底层重学 Java 之 Character 字符型 Gitchat连接

Gitchat连接 https://gitbook.cn/gitchat/activity/5f50804bbe67a5348a9b2c91 简介 从底层,从原理,我们来重学一次 Java。Character 是char(字符)的封装类,是String用于字符存储的类型,他的源码及实现是…

Day7: Linux基础片:系统监控

一下1,2,3,4分别代表截图中第一行,第二行,第三行,第四行表示的内容 top top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器。 当前时间&#xff0c…

工作总结12:封装组件

selectform组件 <!--封装部门选择的插件 需要的组件--> <template><el-select :value"value" placeholder"请选择所属部门" change"handleChange"><el-optionv-for"department in departments":key"depart…

Windows 环境下 Chrome浏览器崩溃“STATUS_INVALID_IMAGE_HASH”

问题 Windows环境下&#xff0c;新版的Chrome浏览器下载安装后&#xff0c;打开就报错&#xff0c;提示错误代码为“STATUS_INVALID_IMAGE_HASH”。重新打开、打开新标签页、刷新均无效。 解决 导致这个问题的原因是 Google 在79版本&#xff08;2019年12月20号左右&#xf…

工作总结13:vue官网封装组件

事件名 不同于组件和 prop&#xff0c;事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子&#xff0c;如果触发一个 camelCase 名字的事件&#xff1a; this.$emit(myEvent) 则监听这个名字的 kebab-case 版本是不会有任何…

从底层重学 Java 之 Stream 初探 Gitchat连接

Gitchat连接 https://gitbook.cn/gitchat/activity/5f85696aad812d16b498848c 简介 从底层&#xff0c;从原理&#xff0c;我们来重学一次 Java。Stream 是JDK8中新引入的&#xff0c;方便了数据列表的过滤、投影、遍历等各种处理&#xff0c;他的源码及实现是怎样的呢&…

签到题

直接查看源代码 nctf{flag_admiaanaaaaaaaaaaa}转载于:https://www.cnblogs.com/maodun/p/6912628.html

从底层重学 Java 之 Stream 并行及标志 GitChat连接

GitChat连接 https://gitbook.cn/gitchat/activity/5f8fc6cd1f577d4d9f428562 简介 从底层&#xff0c;从原理&#xff0c;我们来重学一次 Java。Stream 是JDK8中新引入的&#xff0c;方便了数据列表的过滤、投影、遍历等各种处理&#xff0c;他的源码及实现是怎样的呢&…

工作总结16:多看官网

多看官网 https://cn.vuejs.org/v2/guide/components.html

201521123121 《Java程序设计》第14周学习总结

1. 本周学习总结 1.1 以你喜欢的方式&#xff08;思维导图或其他&#xff09;归纳总结多数据库相关内容。 数据库的基本特点 1、实现数据共享 数据共享包含所有用户可同时存取数据库中的数据&#xff0c;也包括用户可以用各种方式通过接口使用数据库&#xff0c;并提供数据共享…

Java操作Mongo bulkWrite批量入库

Mongo bulkWrite示例 public boolean insertBulk(String collectionName, JSONArray array) {MongoCollection<Document> collection db.getCollection(collectionName);List<InsertOneModel<Document>> documentList array.stream().map(item -> {Doc…

工作总结17:组件封装思想

就是把部门下拉框选择这部分变成一个组件&#xff0c;在用户创建的时候引入这样就可以把功能不相关代码模块化&#xff0c;以后也便于管理如果别的地方也要用到部门选择&#xff0c;可以直接再次引入&#xff0c;就不用重复写代码了

c#程序中使用quot;like“查询access数据库查询为空的问题

今天&#xff0c;在开发的过程中发现了一个特别奇怪的问题&#xff1a;access中like查询时候。在Access数据库中运行&#xff0c;发现能够查询出结果。这是在数据库上运行。select * from KPProj where KpName like *測试*&#xff0c;可是相同的语句在c#程序中却查询为空。这是…

SpringBoot启动yaml报错

报错找不到org.yaml里的一个方法 10:45:54.742 [main] ERROR org.springframework.boot.SpringApplication - Application run failed java.lang.NoSuchMethodError: org.yaml.snakeyaml.nodes.ScalarNode.getScalarStyle()Lorg/yaml/snakeyaml/DumperOptions$ScalarStyle;at …

JS中的预编译(词法分析)阶段和执行阶段

javascript相对于其它语言来说是一种弱类型的语言&#xff0c;在其它如java语言中&#xff0c;程序的执行需要有编译的阶段&#xff0c;而在javascript中也有类似的“预编译阶段”&#xff08;javascript的预编译是以代码块为范围<script></script>&#xff0c;即每…

Excel分组最大级别为8(outlineLevel最大为7)

excel分组最大级别为8&#xff0c;超过则会删除。 事故现场 Java操作POI分组超过7级&#xff08;算上末级节点&#xff0c;一共8级&#xff09;&#xff0c; 分组作用在sheetData节点下row节点的outlineLevel上&#xff0c;一级分组没有该属性&#xff0c;2-8级分组该值对应为…