孙宇的技术专栏 大数据/机器学习

秒杀系统构建

2015-12-18

阅读:


现在秒杀业务在电商的业务中是经常用到。成为配合销售的一种常规手段了。但它背后涉及到的技术可不算常规,实际上是很复杂的。因为在短时间内,大量的请求堆积。稍有不慎就会千万服务崩溃。而最容易承压的就是数据库。

在设计一个秒杀系统时,可以对功能进行模块的划分,然后分层进行设计及优化。

  1. 限流方案
  2. 负载均衡
  3. 放号策略
  4. 并发处理
  5. 降级服务
  6. 压力测试

限流方案

在开始秒杀的时候,流量突增。其中有不少是垃圾流量。比如最觉的是反复刷新页面,疯狂点击抢购按钮,以及稍有点网络知识的人直接发起网络请求以跳过前端JS的限制;更甚者操作大量的肉机同时发起请求防止IP等限制。

为了加快页面响应速度,页面的图片,JS,CSS等文件放入CDN进行加速。减少一部分服务器的流量。同时,在用户点击抢购后,可以在JS中将按钮设为不可用。同时,在JS里往 cookie 写入状态,表示已经抢购。这样用户刷新页面后还是不可点击。当然,用户清空 cookie 后还是可以继续刷。但这样已经可以挡住一部分用户了。

同时,在抢购开始前,可以通过时间判断将按钮设置为不可点击。当然,前台的时间判断是根据用户机器上的时间。所以,在服务端还是要进行判断和筛选。但这样还是可以将一部分小白用户拦住。

对于有一些网络知识的用户,他们能够获得到我们请求后的服务端地址。可以直接发起网络请求,跳过JS的限制,这就需要我们在服务端进行判断。第一层就是根据用户ID进行请求次数的限制,这里甚至可以直接在 nginx 里使用 lua 和 redis 进行判断。

经过限流后,剩下的基本上就是较真实的请求了。这里还可以加入缓存。比如抢购页面上商品的信息,可以直接存入 memcache或 redis 等缓存服务里。

负载均衡

请求多的时候,可以使用负载均衡进行分流。由多台机器承担压力。而且扩容方便。同时,还可以引入异步策略,使用消息服务。将请求先放入队列中,再由其它工作线程进行读取并操作。

为了进行限流,当接入负载均衡时,最好是将同一个用户,根据其UID分配到同一台机器。如果是用 nginx 作负载均衡,可以设置 limit_req_zone, limit_zone 进行限流。

放号策略

经过限流削峰后,还会有许多请求到达服务层。比如有1万件商品。可能请求还会有 100 万。我们首先采用的策略是采用消息服务及队列,将请求先放入队列中。但不是所有的都要入队。这种情况下,我们只把前 10 万个请求入队甚至 5 万。其它的直接返回无货,让用户 30 分钟后再看。

这里假设我们给抢单的用户提供了 30 分钟的付款时间,超时未付款将回笼货物。这样其实就是又将一部分的请求延迟到了 30 分钟后。12306 的分时段放票其实也是这个思路,将流量分散。

使用队列后,处理下单的线程从队列读取,并处理下单,再改库存。这就是按自己的处理能力来的。可谓闲庭信步。当库存减到 0 后,队列里剩下的就都直接返回无货了,新请求也直接返回无货。

并发处理

有一点要注意的是,就算使用队列。如果我们是多台机器支撑,就会存在数据库并发时的锁问题。比如:库存还有 2 个,这时候有两台机器同时下单,我们要保证不会多卖。

方案一就是明显的数据库事务。但事务有可能出现死锁的情况:

如: 事务一:

start transaction;
update table set column1 = 1 where id = 4;
update table set column1 = 2 where id = 3;
commit;

事务二:

start transaction;
update table set column1 = 3 where id = 3;
update table set column1 = 4 where id = 4;
commit;

两个事务执行第一条语句时都没问题,而且分别取得了两行的写锁。但是执行第二条语句时都不会成功,因为锁都被对方占用了。事务就会不停的等待对方释放资源。进而进入死循环。

为了解决这种问题,数据库系统实现了各种死锁检测及死锁超时机制。InnoDB 引擎则可以预知这种循环的相关性,并立刻返回错误,并回滚拥有最少排他锁的一个事务(因为这个事务最容易回滚)。

但也要等到死锁后才会进行后续处理。效率就非常低下了。这时候要考虑的是不用事务,而在逻辑中进行自我控制。比如在非事务环境下可能是这样处理:

update goods set cnt = cnt - 2;

这时候如果程序出错了,商品库存数已经被改了就没办法回滚了。这样就造成了脏数据。所以这里我们的做法是:先查也当前库存数是多少,然后用 set 的方式替代减法处理:

update goods set cnt = 3 where cnt = 5;

这时,如果有两个请求并发。他们分别先请求总库存数,得到结果是 5。然后开始各自的处理,订单1 set 后,库中的库存是 3;这时候订单2处理时,上面的语句就不会成功,因为 cnt 这时 != 5 了。而且这样来处理还可以在程序中添加出错时的重试。

分布式锁

分布式环境下,多台机器上多个进程对一个数据进行操作,如果不做互斥,就有可能出现“余额扣成负数”,或者“商品超卖”的情况。

多个访问方对同一个资源进行操作,需要进行互斥,通常是利用一个这些访问方同时能够访问到的lock来实施互斥的。

有几种场景:

同一进程内多线程的互斥

设定一个所有线程能够访问到的lock实施互斥

  1. 多个线程同时抢锁
  2. 只一个线程抢到,未抢到的阻塞,或下次再来抢
  3. 抢到锁的线程操作临界资源
  4. 操作完临界资源后释放锁

不同进程间的互斥

如,在网站,APP,等不同应用进行下单,设定一个所有进程能够访问到的lock实施互斥(例如文件inode,OS帮我们做了)。

  1. 多个进程同时抢锁
  2. 只一个进程抢到,未抢到的阻塞,或下次再来抢
  3. 抢到锁的进程操作临界资源
  4. 操作完临界资源后释放锁

分布式环境下的互斥

分布式环境下,多台机器上多个进程对一个数据进行操作的互斥,例如同一个uid=123要避免同时进行扣款。

根据上面的原理,先找一个多台机器多个进程可以同时访问到的一个lock,例如redis。

  1. 多台机器上多个进程对这个锁进行争抢,例如在缓存上同时进行set key=123操作
  2. 只有一个进程会抢到这个锁,即只有一个进程对缓存set key=123能够成功,不成功的进程下次再来抢
  3. 抢到锁的进程对余额进行扣减
  4. 扣减完成之后释放锁,即对缓存delete key=123

降级服务

在做到了限流削峰,负载均衡,放号后,如果服务还是顶不住。这时候为了保证服务不崩溃,可以采取降级的策略。比如直接放弃 50% 甚至更多的请求,让他们直接返回无货或稍后再试。或者随机为用户返回失败。比如早年间QQ空间为了降负载,就会让一部分用户无法发表内容只能查看。

压力测试


Similar Posts

上一篇 nginx+lua防爬虫

下一篇 Docker 虚拟化

评论