秒杀系统思考
Posted striver深度思考
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了秒杀系统思考相关的知识,希望对你有一定的参考价值。
# 1 概述
随着移动互联网的发展,网络用户数越来越多,通常为了通过网络营销某个产品的时候,往往会搞秒杀、抽奖等活动。对于后台而言,对于服务端而言,抽奖和秒杀具有相同的特点,参与人数和物品是不对等的,即大量用户竞争少量物品。而且往往在很短的时间点爆发。
# 2 秒杀业务为什么难做
为了搞清楚秒杀或者抽奖系统为什么难做,我们先看一下秒杀或者抽奖的一般业务流程:
- 1. 进入抽奖或者秒杀逻辑;
- 2. 检查参数,校验资格;
- 3. 扣除资格;
- 4. 抽奖或者发放物品。
如上四个步骤中最为重要的是资格校验、扣除;发放奖品(涉及到对同一个数据的写)。为了安全起见,通常我们会先扣除资格,但是这常常会造成很不好的体验:
- 1. 系统总是繁忙,几次过后,奖品没有了。
- 2. 积分扣除了,奖品没有得到(因为先扣除积分,而积分是具体某个用户的数据,这个数据不会是热点数据,而奖品库存却是一个数据。因此可能出现很多用户扣除了积分,奖品却没有了),体验很不好。
- 3. 补过积分后,奖品没有了,让人很失望。
可见,秒杀或者抽奖系统对比于其他系统来说具有如下特点:
1. im系统,例如qq或者微博,每个人都读自己的数据(好友列表、群列表、个人信息);
2. 微博系统,每个人读你关注的人的数据,一个人读多个人的数据;
3. 秒杀系统,库存只有一份,所有人会在集中的时间读和写这些数据,多个人读一个数据。
例如:小米手机每周二的秒杀,可能手机只有1万部,但瞬时进入的流量可能是几百几千万。
又如:12306抢票,票是有限的,库存一份,瞬时流量非常多,都读相同的库存。**读写冲突,锁非常严重**,任何强大的数据库服务器都不可能解决这个问题。这是秒杀业务难的地方。那我们怎么优化秒杀业务的架构呢,即:如何减少压到数据库的流量呢?
# 3 优化原则
1. **将请求尽量拦截在系统上游**(不要让锁冲突落到数据库上去)。传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小。以12306为例,一趟火车其实只有2000张票,200w个人来买,基本没有人能买成功,请求有效率为0。
2. **充分利用缓存**(其目的也是为了不让大量无效请求落到数据库层),秒杀买票,这是一个典型的读多写少的应用场景,大部分请求是车次查询,票查询,下单和支付才是写请求。一趟火车其实只有2000张票,200w个人来买,最多2000个人下单成功,其他人都是查询库存,写比例只有0.1%,读比例占99.9%,非常适合使用缓存、读写分离来优化。
# 4 常见的系统架构
秒杀系统常见的系统架构从大的角度看,分为几部分:
1. 浏览器端,最上层,会执行到一些JS代码。
2. 站点层,也叫接入层。这一层会访问后端数据,拼html页面返回给浏览器。
3. 服务层,向上游屏蔽底层数据细节,提供数据访问。
4. 数据层,最终的库存是存在这里的,mysql是一个典型(当然还有会缓存)。
## 5 具体如何优化
## 5.1 浏览器层
我们都参加过线上抽奖或者秒杀活动,通常的情景是这样的,秒杀开始,我们点击按钮,点击之后,系统卡住了,作为用户,我们很着急,越卡我们越点,一直点点点。但是有用么?平白无故的增加了系统的负载。但是用户不管啊,她就是点点点。一个用户点三次,平白无故增加了67%负载。那么我们该怎么办呢?
- 1. 从用户体验的角度,用户点击秒杀之后,按钮置灰,并友好的提示用户,耐心等待,禁止用户重复提交请求。
- 2. 从技术层面,限制用户在x秒之内只能提交一次请求,app层面同理。
以上两个简单的改动为我们的系统拦截住了大部分无效的请求,不过请求还是太多,而且这种策略只能拦截普通用户,对于程序员或者其他专业用户,这种策略是无效的。我们只能通过其他手段拦截。这个拦截只能在第二层 -- 站点层进行。
## 5.2 站点层
这一层主要是防止程序员写for循环来秒杀,不过这类业务也有明显的特点,必须用户登录,那么我们就可以借此做文章,例如,每个用户2秒(这个数字可以通过自己数据库最大处理的TPS和自己预估的QPS来计算)只能透过一个请求。其余的请求直接拒绝404?这恐怕影响用户体验,我们可以做页面缓存,其余的请求都响应同一个页面。这样做的代价就是在站点接入层需要消耗存储,通常是nosql系统。如此,就可以在不伤害用体验的情况下将无效请求做了进一步拦截。
按照常理来说,现在已经可以了,但是如果有些黑客控制了肉鸡,掌握了很多uid,这时我们现在的限流又无效了。按照目前的策略,在接入层拦截不住了。
## 5.3 服务层拦截(最终的兜底逻辑)
对于服务层,我清楚的知道本次活动有多少个物品,也知道自己数据库的处理能力,比如,我知道我的库存有10000个,那我肯定不会将1000000个请求放到数据库层,我会设计请求队列,只放10000个请求到我的队列中,然后将这些请求放到我的数据库层。然后其他的请求我都返回已抢完,下次再来。这样,对于到了数据库层的流量已经非常少了。 对于读请求,可以使用内存cache,不管是memcached还是redis,单机抗个每秒几万应该都是没什么问题的。如此限流,只有非常少的写请求,和非常少的读缓存mis的请求会透到数据层去。(我们可以在库存更新后更新各个缓存的库存值。不过有时会不及时)
# 6 小结
可以看到,对于秒杀系统,最核心的就是两点:
- 尽量将请求拦截在系统上游(越上游越好)
- 读多写少的常用多使用缓存(为上一个目标服务)
# 7 问题
通过限流、队列、缓存等措施保证了秒杀系统的稳定运行,但是也带来了一些其他的问题:
- 1. 使用缓存,带来的问题就是用户读到的数据不是最新的,而且不同用户读到的数据不一定相同,不过对于秒杀场景,这个不一致可接受。
# 8 扣除资格、发放物品(减少库存)部分采用分布式架构还是非分布式架构
## 8.1 单机模式
这个要看具体情况了,如果自己的秒杀活动通过如上几步限流,请求量很小了,那么这两个可以在一个数据库的多张表上操作,做成一个数据库事务。
```sql
Start transaction
Update 积分=积分-xxx where 积分>xxx
Update 限量=限量-1 where 限量>0
if accept rows = 1
{
Insert 道具流水
}
Commit
```
然后再写一个定时任务,往扫描发货表里物品进行发货。整个逻辑完全由DB控制,没有接口调用,任何一步出错,都可以实现回滚。
## 8.2 分布式架构
如果用户数非常多,分布式架构也可以(多了很多RPC调用,会增加失败的几率,但是没别的办法)。这时,扣除积分是一个独立的服务,扣除成功后,将这个用户加入要发放物品的请求队列中。这样到了数据库层的流量也是可控的很少的。当然了,扣除积分这个接口的设计要注意**幂等性**。
流程如下:
- 1. 用户请求到达服务器端后,首先去检查用户是否有资格。
- 2. 如果有则扣除积分(扣除积分和回滚资格的接口要具有幂等性,可以重试)。
- 3. 扣除用户资格(扣除资格和回滚资格的接口要具有幂等性,可以重试)。
- 4. 将三步都成功的写入请求队列。
- 5. 处理队列中的请求。
分布式架构的时候要注意接口的设计,特别是幂等性。
下一篇文章会介绍分布式系统中幂等性的重要性和设计思路。
联系方式:striverx@qq.com, striversx@gmail.com
以上是关于秒杀系统思考的主要内容,如果未能解决你的问题,请参考以下文章