# 秒杀场景

# 面临的问题

# 超卖问题

  • 数据准确性问题,是所有系统的根基,
  • 从厂商的角度看,应该是最严重的问题

# 高并发

  • 击垮应用服务器

  • 缓存击穿或者失效

  • 击垮数据库

# 接口防刷

  • 针对秒杀的软件,一秒上百次的请求都很常见;
  • 大量无效请求,造成拒绝服务攻击;

# 秒杀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强;