1.引子
说到秒杀,事实上我们都非常熟悉,已经是很多平台与商家常规的商业营销活动了,你比如说双11、或者618。秒杀于商家来说,得到了营销推广,促进商品销售;于消费者用户来说,用更少的钱买到了心仪的产品。是一种商业上的双赢!
当然今天的文章,我们不从商业的角度来看待秒杀,那么我们从什么角度呢?从技术的角度来看,一起来探讨如何设计实现一个秒杀系统,我想对于很多技术小伙伴来说,更想要关注的是这个点,对吧!
我们首先假设公司需要一个秒杀系统,然后你是架构师,你该如何进行架构设计实现,都有哪些点需要考虑呢?我们来尝试考虑,我们说建设一个系统,无外乎有几个主要关注的点
- 业务诉求
- 性能诉求
- 安全性诉求
尤其秒杀系统更不能例外,任何系统首先都要满足业务需求,要能用,这是业务上的诉求;秒杀系统的特点是短时间内聚集大流量,应对高并发,这是性能上的诉求;用户信息泄露,非法刷单风险,这是安全性上的诉求。
更详细的内容,我们在案例部分来探讨!分成三个小节内容
- 秒杀系统业务设计思考
- 秒杀系统高性能设计思考
- 秒杀系统安全设计思考
让我们开始吧!
2.案例
2.1.秒杀系统业务设计思考
秒杀系统本身是电商业务系统体系的一部分,电商系统都有哪些业务元素呢?用户、商家、商品、库存、购物车、订单、支付、物流等。那么一个普通用户,他是如何参与到秒杀业务活动中的呢?我们列举一下
- 首先到平台注册一个有效账号,秒杀业务活动要求必须知道用户信息
- 用户登录到秒杀平台
- 浏览秒杀商品列表
- 查看秒杀商品详情
- 成功秒杀商品,查看秒杀订单
- 支付,然后等待收货
就这样,完成了一次秒杀活动全流程参与。我们看到观察用户一次秒杀活动全流程,主要需要聚集的元素是秒杀商品、秒杀订单,其它元素如用户、支付、物流等,都可以共享平台已有的能力(假设我们是一家电商平台)。
那么有小伙伴可能会提出疑问了,电商平台,商品不是有了吗?订单不是也有吗?增加秒杀商品、秒杀订单的意义在哪里?
我们试想
- 秒杀是临时性的业务促销活动,只是部分商品参与(不会全部商品参与秒杀互动)
- 秒杀商品有它特有的业务属性,比如说秒杀价格、秒杀商品的库存、秒杀商品参与秒杀活动的开始结束时间(这些都是要区别于普通商品的)
- 统计分析秒杀活动效果,有多少秒杀订单、哪些商品秒杀效果更好
等等,综合以上我们看到针对秒杀业务活动,不适合共享平台已有的商品、订单,需要在业务设计上考虑,增加新的
- 秒杀商品
- 秒杀订单
有了秒杀商品、秒杀订单,业务上还有需要关注的点吗?有
每一次秒杀活动,业务小伙伴肯定都会提出一定的业务规则,这里我们挑两个基础的,必须要满足的点
- 如何避免重复下单
- 如何避免超卖
重复下单,指同一个用户,多次重复秒杀到了一个商品,原则上我们应该避免,因为秒杀活动的本质是营销,越多的用户参与效果越好,因此要避免用户重复下单。
即同一个用户,同一个商品,只能秒杀成功一次。那么重点是在系统设计实现上,该如何避免用户重复下单呢?我们可以在秒杀订单表中,建立(userId + productId)的唯一索引,来避免重复下单
create unique index unique_index on 秒杀订单表(userId,productId)
超卖,老板是一定不能接受的!秒杀活动本身是赔本赚吆喝的事情,卖的越多意味着亏更多的钱,因此一定不能超卖、不能超卖、不能超卖,重要的事情说三遍!
不能超卖,说的轻松,实际上实现起来也不难!我们假定用户秒杀后,后台业务处理流程应该是这样的
- 减库存
- 创建秒杀订单
在减库存的时候,一定要有库存大于0的条件检测,比如说在数据库减库存操作,我们可以这样
update 秒杀商品表 set 库存 = 库存 - 1 where 库存 > 0
你看,这样一来超卖的问题就得到了解决。到这里,我主要跟你一起探讨了秒杀系统业务设计上需要考虑的几个点,它们是
- 增加秒杀商品
- 增加秒杀订单
- 避免重复下单
- 避免超卖
2.2.秒杀系统高性能设计思考
秒杀系统自身的特点就是性价比,比平常便宜!因此会有更多的人参与进来,需要应对高并发、大流量。性能要求极高,千万不能宕机了!
针对这种情况,该如何设计实现一个高性能的秒杀系统呢?通常我们有这么一些实现的技术手段,我分为前端、后端,从两个纬度来探讨
2.2.1.前端
前端性能优化,有哪些要素可以考虑呢?主要是页面、静态资源。
我们知道一次秒杀活动,用户交互的页面有秒杀商品列表页面、秒杀商品详情页面、秒杀订单页面,而这些页面,尤其是秒杀商品列表、秒杀商品详情页面,一旦秒杀活动开始,它们的内容信息不会在更新,即是静态的!
这个时候,我们可以考虑,将秒杀商品列表、秒杀商品详情页面静态化,且通过CDN内容分发,实现用户就近访问,提升用户体验的同时,有效降低秒杀系统后端服务的压力。
另外为了实现好看的页面效果,以及功能,往往需要CSS、js静态资源,那么对于静态资源有没有什么优化的手段呢?
我们知道基于http协议的服务是一请求一响应模型,如果静态资源大、数量多,势必要占用更多的网络带宽、且增加网络多次往返请求响应的资源浪费。
这个时候,我们可以考虑,将静态资源进行压缩、合并处理,以实现节省网络带宽资源、减少网络传输造成的资源浪费。
总结一下,关于秒杀系统,在前端可以考虑的一些提升性能的技术手段有
- 页面静态化
- CDN
- 静态资源压缩、合并处理
2.2.2.后端
前端优化处理以后,我们自然就需要考虑后端,又有哪些提升性能的技术手段呢?
我们知道,秒杀系统后端主要的业务处理流程是
- 减库存
- 写入秒杀订单
因为秒杀商品、秒杀订单存储在关系数据库中,换句话说每一次秒杀都是直接操作数据库,操作磁盘,势必存在性能瓶颈!一次IO操作,需要经过完整的IO栈,即应用程序--->文件系统--->磁盘设备。
性能太低了,高并发、大流量下千万要尽力避免!那么如何去避免呢?我们可以考虑这么一些设计实现上的方法
- 缓存,操作内容,比操作磁盘高效
- 异步化下单,避免长时间阻塞用户
- 限流,避免相同用户频繁刷单
具体实现上,针对秒杀场景流程,可以这么去实现
#1.系统初始化的时候,将秒杀商品库存信息,加载到缓存系统(redis)
#2.当秒杀请求,打到后端的时候,通过缓存系统实现【预减库存】
#3.预减库存失败,直接响应用户秒杀失败
#4.预减库存成功,将秒杀请求信息,投递得到消息队列系统(kafka/rocketmq)
#5.实现异步下单,消息投递成功,立即响应用户秒杀排队中
#6.系统后端,另开启业务线程,处理秒杀业务请求【减库存、创建秒杀订单】
#7.系统前端,开启轮询请求后端,获取秒杀结果
这样一来,通过缓存系统,实现预减库存,通过消息队列系统,实现异步化下单,通常能够有效应对秒杀系统性能上的诉求。
2.3.秒杀系统安全设计思考
我们最后再来关注秒杀系统安全上的一些考虑,有小伙伴可能不是很明白,对于一个秒杀系统,需要考虑的不就是
- 秒杀业务实现
- 高性能诉求
还有什么安全考虑呢?这里我们主要考虑两个点
- 用户敏感信息的保护
- 秒杀接口地址的保护
关于用户敏感信息的保护,比如说用户的登录密码,我们知道用户参与秒杀,请求信息是在网络上传输的。
如果用户的登录密码,直接在网络上明文传输,一旦被网络监听、网络抓包以后,用户信息就泄露了,因此我们需要考虑将用户的密码,进行加密后传输,具体如何实现呢?可以这样
#1.当用户输入明文密码后,进行MD5加密,比如说MD5(明文密码,盐值SALT)
#2.后端再次进行一次MD5加密,即:MD5(MD5(明文密码,盐值SALT),盐值)
#3.即通过两次MD5加密,最大化实现用户密码的安全性保障
用户的敏感信息,密码我们可以通过两次MD5加密,实现安全性,这个点比较好理解。但是为什么需要考虑秒杀接口地址的保护呢?
我们说保护秒杀接口地址,是为了防止非法用户直接刷单,比如以下固定接口地址
http://www.miaos.com/killSecond?productId=666
一旦我们知道秒杀地址后,就可以实现刷单,只要改一改商品Id参数即可,这不科学!
我们应该怎么做呢?
- 我们应该让秒杀地址是动态获取的,即将秒杀地址不固定
- 且在秒杀活动开始前,用户是不知道具体的秒杀接口地址的
举个实现例子,java开发的小伙伴们都知道,springmvc的@PathVariable注解,支持实现路径变量绑定
/**
*秒杀
*/
@RequestMapping("/{dynamicparam}/kellsecond")
public String doSellSecond(@PathVariable("dynamicparam") String dynamicparam){
// 1.校验dynamicparam参数有效
// 2.处理秒杀业务逻辑
// 3.响应用户
}
我们看到最关键的是请求路径 @RequestMapping("/{dynamicparam}/kellsecond") ,它是一个动态的url,在真正的秒杀请求前,需要优先获取一个秒杀的参数dynamicparam,即需要另外一个接口
/**
*获取秒杀地址
*/
@RequestMapping("/getSellSecondPath")
public String getSellSecondPath(){
String dynamicparam = jwt(token);
return dynamicparam;
}
看到这里,我们理一理秒杀请求的处理逻辑
- 通过请求getSellSecondPath,获取秒杀地址,准确说是获取一个动态参数dynamicparam
- 将动态参数dynamicparam,组装成完整的秒杀请求地址/{dynamicparam}/kellsecond
- 实现对秒杀接口地址的隐藏保护
这里可能有小伙伴会有一个疑问,这不是一样的吗?先请求getSellSecondPath,再请求/{dynamicparam}/kellsecond,你需要注意,细节上还真的不一样
- 首先通过/{dynamicparam}/kellsecond动态参数的方式,我们隐藏了真实的秒杀接口
- 其次getSellSecondPath接口的时候,我们可以增加验证码、秒杀时间范围校验、限流等方式,防止用户的刷单行为
到这里,关于秒杀系统的设计实现,我们就探讨到这里了!