# 秒杀场景
- 典型的高并发,大批量消费者同时涌入;
- 摘录于博客园秒杀系统设计 (opens new window)
# 面临的问题
# 超卖问题
- 数据准确性问题,是所有系统的根基,
- 从厂商的角度看,应该是最严重的问题
# 高并发
击垮应用服务器
缓存击穿或者失效
击垮数据库
# 接口防刷
- 针对秒杀的软件,一秒上百次的请求都很常见;
- 大量无效请求,造成拒绝服务攻击;
# 秒杀url
- 秒杀url参数如果是可以预测的,容易被截获联合秒杀软件突破规则;
# 数据库设计
- 秒杀有把服务器击垮的风险,应该尽量单独使用物理机,与其它业务独立;
# 大量请求问题
- 如果使用redis缓存,单台redis服务器可承受的qps大概是4W左右,秒杀吸引的用户量足够多时,单QPS可能达到几十万,单体redis不足以支撑如此巨大的请求量;
- 缓存会被击穿,直接渗透到DB,从而击垮应用服务;
# 解决的方案
# 单独设计秒杀库
- 用尽可能少的字段,尽可能少的表以及表结构
- 秒杀库应该单独放置于一个主机
# 秒杀url的设计
将url动态化,即使是该系统的开发人员,也无法在秒杀前知道秒杀的url
md5加密一串随机字符串,作为秒杀的url,前端访问后台获取具体的url,后台校验通过后才能继续秒杀;
从技术层面,可以结合
@PathVariable
实现
# 秒杀页面静态化
- 也就是常规的前后端分离,现代的大部分应用系统都是如此设计
# 单体redis升级为集群
- 秒杀是一个读多写少的场景,使用redis做缓存在合适不过;
- 考虑到缓存击穿问题,应该构建redis集群,采用哨兵模式,可提供redis的性能和可用性;
# 使用负载均衡,如nginx
nginx
并发可达到几万,而tomcat
只有几百- 通过
nginx分发
可大大提高并发能力;
# 精简sql
- 传统的扣库存做法是,先查询库存,再去update,这样需要两次sql执行;
- 通过一条语句也可执行:
pdate miaosha_goods set stock =stock-1 where goos_id ={#goods_id} and version = #{version} and sock>0;
- 通过版本号的乐观锁,可以保证库存不会超卖并且一条语句完成操作,相比较悲观锁,它的性能更好;
# redis预减库存
- 很多请求进来,需要查询库存,这是一个频繁读场景,可在秒杀前将库存值作为常量放入redis
- 下单成功后,常量值减去1,如果取消下单,则库存常量需要增加1,查询库存和扣减库存需要原子操作,可以借助lua脚本实现
# 接口限流
- 秒杀的最终本质是数据库的更新,但是有很多大量无效的请求,限流就是要将这些无效的请求过滤掉;
- 限流的第一步,可以从源头,前端限流处理,如每隔500ms才能再次触发;
- 通过redis的键过期策略,基本原理是同一个用户再xx秒内重复的请求就直接拒绝,一般限定为10秒内拒绝;
- 令牌桶算法,每个请求尝试获取一个令牌,后端只处理持有令牌的请求;
# 异步下单
- 一般为了提升下单效率,防止下单服务的失败,需要将下单操作异步处理;
- 最常用的办法是使用队列,如rabbitmq;
# 服务降级
- 如果秒杀过程中,某个服务不可用,应该做好后备工作,如返回用户一个友好的提示,而不是直接卡死,服务器错误等生硬的反馈;
# 容量预估

- 该秒杀流程图可以支撑起几十万的流量,如果破千万破亿则需要更多的优化
- 比如,可以增加数据库的分库分表、队列改为kafka,redis增加集群数量等手段
# 金融场景
- 金融场景的一个显著特点是,数据不能出错,严格的不能出错
# 事务
- 一类操作要么全部成功,要么全部失败,用于确保业务准确性
- 从单线程角度去看待这个问题;
# 锁机制
- 多线程环境下,需要确保准确性,则必须利用锁,不论是基于数据库,还是应用程序代码本身;
- 分布式应用系统,理论上可以从两个层面确保数据准确性,分布式锁,以及数据库锁,其中,分布式锁需要将所有的业务方法都锁定才能生效
- 具体内容参见
锁
章节
# 计算
- java领域中,double和float用于二进制浮点型计算,无法得到精确的结果。而**
BigDecimal
用于精确的计算,能够支持超过16位有效数字的商业运算**,(double和float可支持不超过16位的科学和工程运算) - 由于浮点型的具有不精确性,在不进行运算时,使用String表示金额都比double和float强;