秒杀系统设计总结
简介
秒杀从规模上可以分为以下两类:
- 大秒:类似双十一,商品数量规模大,价格低,流量超大的活动。
- 小秒:商家自己配置的一些时段类型的活动,由商家自己指定时间上架。
秒杀系统面对的问题
秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统。
- 高并发环境下的系统稳定性:如何保证系统在面对巨大的流量情况下,不被打崩?
- 两个问题
- 并发读
- 核心优化理念:减少用户到服务端来读数据。或者让他们读取更少的数据。
- 并发写
- 核心优化理念:在数据库层面独立出来一个库,做特殊的处理。
- 并发读
- 大流量会产生以下实际待解决问题
- Redis缓存击穿/雪崩/穿透等问题
- 关系型数据库性能问题,锁竞争对性能的消耗
- 两个问题
- 保证数据的最终一致性:库存不能超卖。
- 大数据分析功能:分析本次秒杀活动的商业效益。
- 需要有一个兜底方案,以防最坏的情况发生。
设计
架构原则: “4要1不要”
架构是一种平衡的艺术,最好的架构一旦脱离了它所适应的场景,一切都将是空谈。
数据要尽量少
- 用户请求的数据能少就少,包括上传给系统的数据和系统返回给用户的数据。
HTTP请求数尽量少
- 合并CSS,JS文件
路径要尽量短
- 用户发出请求到返回数据的过程中,经过的节点要尽量短
- 通常,每经过一个节点,都会产生一个新的Socket连接。
- 会减少时延
- 可以选择将多个相互强依赖的引用部署在一起,将RPC变成JVM内部的方法调用
- 用户发出请求到返回数据的过程中,经过的节点要尽量短
依赖要尽量少
- 完成一次用户请求必须依赖的系统或服务要少(指的是强依赖)
- 比如展示秒杀页面,它强依赖商品信息,用户信息,但是优惠券,成交列表等非必要模块是可以在紧急情况下去掉。
- 对系统模块进行分级,0级,1级,2级等
- 完成一次用户请求必须依赖的系统或服务要少(指的是强依赖)
不要有单点
- 系统中的单点是系统架构上的一个大忌,单点意味着没有备份,风险不可控。
- 如何避免单点?
- 避免将服务的状态和机器绑定(服务无状态化)
- 把服务的配置动态化(使用配置中心Nacos等)
- 存储服务不好实现,因为数据持久化存储在机器的磁盘里面。文件存储可以通过冗余多个备份的方式来解决单点问题。
- 避免将服务的状态和机器绑定(服务无状态化)
- 如何避免单点?
- 系统中的单点是系统架构上的一个大忌,单点意味着没有备份,风险不可控。
流量过滤
本质:逐级过滤掉无效的流量。基本有以下一些解决方案:
- 活动开始前前端页面的Button无法点击,防止活动尚未开始时,用户进行点击产生流量。
- 同时后端需要做相关校验。避免用户直接请求秒杀接口。
- 秒杀url实现动态化,可以选择进行md5加密随机字符串,然后通过另一个接口校验秒杀接口的合法性
- 错峰:前端添加验证码或者答题,防止瞬间产生超高的流量,增加题目辨别难度,避免以图像识别等技术进行破解。
- 校验:对参与活动的用户进行校验拦截。主要从以下几个方面进行判断
- 用户白名单
- 用户终端校验:对用户终端类型进行判断
- IP、MAC、ID校验
- 参与次数校验:避免多次参与活动
- 用户黑名单:避免羊毛党等
- 限流:通过接口限流策略判断请求是否放行
- 令牌桶算法
性能优化
前面的流量过滤基本过滤掉大部分流量,但是系统性能还需进行优化,主要有以下的解决方案:
- 动静分离
- 活动预热:将参加活动的商品独立出来,不和普通的商品库存共享服务,提前将数据缓存到
Redis
,查询全部走缓存,扣减库存视情况而定。 - 选择
Nginx
- 采用微服务架构部署,提高部署量,均摊请求。
- 秒杀是典型的读多写少的场景,考虑到单体redis的性能问题,可以考虑:
Redis
集群- 主从同步
- 读写分离
- 如果使用
Redis
集群,同时需要考虑保证多节点的数据一致性
- 异步处理:采用消息队列
- 异步,削峰,解耦
动静分离
“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据。
简而言之,静态数据是能不经过后端请求,直接输出给用户的数据,
如何对静态数据做缓存?
- 把静态数据缓存到离用户最近的地方。
- 用户浏览器
- CDN
- 失效问题
- 命中率问题
- 发布更新问题
- 服务器端Cache
- 静态化改造就是直接缓存HTTP连接。
- 静态化改造是直接缓存 HTTP 连接而不是仅仅缓存数据。Web代理服务器直接根据请求
URL
,取出对应的HTTP响应头和响应体然后直接返回。
- 静态化改造是直接缓存 HTTP 连接而不是仅仅缓存数据。Web代理服务器直接根据请求
- 选择缓存性能优秀的工具进行缓存服务,可以在Web服务器层面进行缓存,例如Nginx,Apache。
如何做动静分离的改造?
- URL唯一化,每个商品由ID来标识。item.xxx.com/item.htm?id=xxxx 就可以作为唯一的 URL 标识。
- URL唯一可以根据id作为key,查询之前缓存的HTTP连接。
- 分离浏览者相关的因素。登录状态可以通过请求获取
- JWT
- 分离时间因素。服务端输出的时间也通过请求获取。
- 异步化地域因素:详情页面上的地域信息做成异步方式获取。
- 去掉Cookie,可以通过Web服务器删除服务器输出的页面中的Cookie。
动态数据的处理方式
- ESI方案:在Web代理服务器上做动态内容请求,并将请求插入到静态页面中。当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。
- CSI方案:即单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。
解决超卖
一般情况下,减库存有以下方式:
- 下单减库存:是最简单的方式,也控制的最精准。但是存在恶意锁单等问题。
- 付款减库存:大并发的情况下,可能存在买家下单后无法付款的问题。
- 预扣库存:买家下单后,库存为其保留一定时间,超时未付款自动释放库存
如果扣库存逻辑较为简单,比如没有复杂的SKU库存和总库存这种联动关系的话,可以选择在Redis
完成减库存。Redis
定时更新到MySQL
。
复杂秒杀场景选择的方式:
- 首先查询
Redis
缓存库存是否充足 - 先扣库存再落订单数据,以防订单生成没有了库存的超卖问题
- 扣库存先扣数据库,再扣
Redis
,并且两个操作需要在同一个事务中,一个执行失败全部回滚。- 数据库
MySQL
更新可以采取乐观锁方式,添加version
字段进行处理,性能较悲观锁高。
- 数据库
上述方案能一定程度解决问题,但是如果大量请求线程落在同一条库存记录上去update
时,会造成InnoDB
行锁的竞争问题,而并发度越高等待的线程会越多,TPS(Transaction Per Second)会下降,相应时间上升,数据库的吞吐量会严重受影响。
这时,需要做数据库层面的优化。
质量保障
- 熔断限流降级
- 监控:QPS监控,容器监控,CPU监控,IO监控等
- 提前压测
数据统计
- 埋点,检测
- 数据大盘,通过数据库数据配合监控系统(首选)
- 离线数据分析
秒杀系统设计总结
https://l1n.wang/2020/系统设计/seckill-summary/