Please enable Javascript to view the contents

秒杀系统

 ·  ☕ 6 分钟
    🏷️

秒杀系统涉及到的知识点:

A, 高并发,cache,锁机制

B, 基于缓存架构redis,Memcached的先进先出队列。

C, 稍微大一点的秒杀,肯定是分布式的集群的,并发来自于多个节点的JVM,synchronized所有在JVM上加锁是不行了

D, 数据库压力

E, 秒杀超卖问题

F, 如何防止用户来刷, 黑名单?IP限制?

G, 利用memcached的带原子性特性的操作做并发控制.

1.架构层面:

秒杀架构设计原则:

尽量将请求拦截在系统上游

读多写少的常用多使用缓存

扩容

说白了加机器

系统隔离

为了避免短时间内的大访问量对现有网站业务造成的冲击,可以将秒杀系统独立部署。系统隔离更多是运行时的隔离,可以通过分组部署的方式和另外99%分开。秒杀还申请了单独的域名,目的也是让请求落到不同的集群中。即使秒杀系统崩溃了,也不会对网站造成影响。

数据隔离

将即将被秒杀的热数据维护到redis。秒杀所调用的数据大部分都是热数据,比如会启用单独cache集群或MySQL数据库来放热点数据,目前也是不想0.01%的数据影响另外99.99%。

减库存操作

一种是拍下减库存 另外一种是付款减库存;目前采用的“拍下减库存”的方式,拍下就是一瞬间的事,对用户体验会好些。

2.产品层面:

1.控制秒杀商品页面抢购按钮的可用/禁用。

购买按钮只有在秒杀开始的时候才能点亮,在此之前是灰色的,显示活动未开始。

2.增加了秒杀答题,基于时间分片削峰

秒杀答题一个很重要的目的是为了防止秒杀器。还有一个重要的功能,就是把峰值的下单请求给拉长了,从以前的1s之内延长到2~10s左右,请求峰值基于时间分片了,这个时间的分片对服务端处理并发非常重要,会减轻很大压力,另外由于请求的先后,靠后的请求自然也没有库存了,也根本到不了最后的下单步骤,所以真正的并发写就非常有限了。其实这种设计思路目前也非常普遍,如支付宝的“咻一咻”已及微信的摇一摇。

3.秒杀页面设计简化:

秒杀场景业务需求与一般购物不同,用户更在意的是能够抢到商品而不是用户体验。所以秒杀商品页面应尽可能简单并且拍下后地址等个人信息应该使用默认信息,减轻秒杀进行时系统负载,若有更改可以在秒杀结束后进行更改。

3.前端层面

静态化以及页面缓存
将页面能够静态的部分都静态化,并将静态页面缓存于CDN,以及反向代理服务器,可能还要临时租借服务器。
利用 页面静态化、数据静态化,反向代理 等方法可以避免 带宽和sql压力 ,但是随之而来一个问题,页面抢单按钮也不会刷新了,可以把 js 文件单独放在js服务器上,由另外一台服务器写 定时任务 来控制js 推送。 另外还有一个问题,js文件会被大部分浏览器缓存,我们可以使用xxx.js?v=随机数 的方式来避免js被缓存。

限流(反作弊)
1.针对同一个用户id来实现,前端js控制一个客户端几秒之内只能发送同一个请求,后端校验同一个uid在几秒之内返回同一个页面

2.针对同一个ip来实现,进行ip检测,同一个ip几秒之内不发送请求或者只返回同一个页面

3.针对多用户多ip来实现,依靠数据分析

4.为了避免用户直接访问下单页面URL,需要将改URL动态化,即使秒杀系统的开发者也无法在秒杀开始前访问下单页面的URL。办法是在下单页面URL加入由服务器端生成的随机数作为参数,在秒杀开始的时候才能得到。

4.后端层面:

1.加入缓存redis:

因为秒杀是典型的读多写少的场景,适合操作内存而非操作硬盘;缓存工具redis本身的操作是保证原子性的,所以可以保证请求了redis的写的操作的线程安全性。

2.加入消息队列,利用队列进行削峰:

将用户请求放置于一个或多个队列中,队列中元素总和等于该商品库存总和,未进入队列的请求均失败。利用多线程轮询分别从一个或多个队列中取出用户请求。操作redis进行减库存操作,成功减库存之后返回成功,并将用户信息与商品信息存入另一个队列当中,进行生成订单的操作。利用两个队列异步处理业务减轻秒杀高峰时期服务器负载。

3.程序计数器:

队列与缓存为了保证请求redis的次数不超过总的库存量,利用一个程序计数器来这一点。程序计数器用JUC包下原子类可以实现。

4.分布式锁

分布式情况下可以利用分布式锁来解决任务每次只能由一次服务来执行且不能重复执行。
分布式锁的实现:zk、redis
分布式锁的优化:先考虑是否可以去锁,然后考虑尽可能多用乐观锁,少用悲观锁。这里有一个问题,乐观锁如果每一次都会有并发冲突的话性能反而不如悲观锁,那么难道真的多用乐观锁性能会比悲观锁高吗?选举考虑ha,比如心跳检测。

5.分布式去锁 方案

利用集群并发加入队列,选举队列处理服务单点执行,这样可以保证并发实现和加锁一样的并发量但不会影响性能。

常见秒杀架构

(1)浏览器端,最上层,会执行到一些JS代码

(2)站点层,这一层会访问后端数据,拼html页面返回给浏览器

(3)服务层,向上游屏蔽底层数据细节,提供数据访问

(4)数据层,最终的库存是存在这里的,mysql是一个典型(当然还有会缓存)

各层次优化细节

第一层,客户端怎么优化(浏览器层,APP层)

a)产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求;
b)JS层面,限制用户在x秒之内只能提交一次请求;

第二层,站点层面的请求拦截

1
2
3
4
5
6
7
8
怎么拦截?怎么防止程序员写for循环调用,有去重依据么?ip?cookie-id?…想复杂了,这类业务都需要登录,用uid即可。在站点层面,对uid进行请求计数和去重,甚至不需要统一存储计数,直接站点层内存存储(这样计数会不准,但最简单)。一个uid,5秒只准透过1个请求,这样又能拦住99%的for循环请求。

5s只透过一个请求,其余的请求怎么办?缓存,页面缓存,同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面。同一个item的查询,例如车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面。如此限流,既能保证用户有良好的用户体验(没有返回404)又能保证系统的健壮性(利用页面缓存,把请求拦截在站点层了)。

页面缓存不一定要保证所有站点返回一致的页面,直接放在每个站点的内存也是可以的。优点是简单,坏处是http请求落到不同的站点,返回的车票数据可能不一样,这是站点层的请求拦截与缓存优化。

好,这个方式拦住了写for循环发http请求的程序员,有些高端程序员(黑客)控制了10w个肉鸡,手里有10w个uid,同时发请求(先不考虑实名制的问题,小米抢手机不需要实名制),这下怎么办,站点层按照uid限流拦不住了。

第三层 服务层来拦截(反正就是不要让请求落到数据库上去)

总结

浏览器和APP:做限速

站点层:按照uid做限速,做页面缓存

服务层:按照业务做写请求队列控制流量,做数据缓存

数据层:闲庭信步

并且:结合业务做优化