浅谈Java秒杀业务场景

关于可能会接触到的秒杀业务场景,不知道小伙伴们有没有实际参与过,工作之余也跟朋友、同事聊过,今天就整理了一下,简单谈谈我对Java秒杀业务的理解。

引言

看过极客时间上订阅的许令波老师的专栏《如何设计一个秒杀系统》,上面针对于秒杀业务,专栏中说到”秒杀其实主要解决两个问题,一个是并发读,一个是并发写。”其中的意思稍显学究,但是需要解决的问题就是并发读和写的问题,接下来针对并发读写展开秒杀场景分析。

架构流程图

首先看一张针对秒杀场景的架构图:
Alt 浅谈Java秒杀业务场景

针对’特殊’的秒杀场景,可以将秒杀单独独立出来单独打造了一个系统,减少了页面的复杂度;针对页面进行了动静分离,使页面刷新的数据降到最少;将热点数据(如库存数量)单独放到一个缓存系统中,提高‘读性能’;在服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台服务获取数据,甚至不需要去公共的缓存集群中查询数据,这样不仅可以减少系统调用,而且能够避免压垮公共缓存集群;增加秒杀答题,防止秒杀器抢单;最后可以做好系统限流保护,防止最坏的情况发生。

应对热点数据

对于如何发现热点数据,热点数据其实也就是秒杀业务中的商品,被称为‘秒杀商品’可以通过类似于运营系统让商家通过报名参加的方式提前把热点商品筛选出来,把参加活动的商品数据进行标记;还可以通过构建动态热点发现系统,思路就是可以通过用户访问的导购页面(包括首页、搜索页面、商品详情、购物车等等)提前识别哪些商品访问量高,收集热点数据到日志中,可以根据一张图来了解一下思路。
Alt 浅谈Java秒杀业务场景

处理热点数据

  • 对于做了动静分离的静态数据,那么可以长期缓存静态数据,但是针对于热点数据产生的‘临时缓存’,即不管是静态数据还是动态数据,都用一个队列短暂地缓存数秒钟,由于队列长度有限,可以采用LRU淘汰算法替换。
  • 针对于数据的限制,更多的是对于数据的保护作用,比如对被访问的商品ID使用一致性Hash算法,通过Hash做分桶,每个分桶设置一个处理队列,这样可以把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源。
  • 还需要考虑到对于热点数据的隔离,不要让秒杀的热点数据影响到了其他的普通数据。

流量削峰

要保证服务器能够正常顺利的处理秒杀,需要做好处理瞬时大流量的访问,一是可以通过排队、答题、分层过滤等无损用户请求的实现方案,另一种是针对于服务器稳定性考虑的限流和机器负载保护等一系列强制措施。

排队

针对于流量削峰,最容易想到的方案是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的高流量,在另一端平滑的将流量推送出去。
除了消息队列,类似的排队方式还有很多,例如:

  1. 利用线程池加锁等待也是一种常用的排队方式;
  2. 先进先出,先进后出等常用的内存排队算法的实现方式;
  3. 把请求序列化到文件中,然后再顺序地读文件(例如基于MySQL binlog的同步机制)来恢复请求等方式。
    Alt 浅谈Java秒杀业务场景

答题

可以通过答题来增加购买的复杂度,第一个目的是防止部分买家使用秒杀器在参加秒杀时作弊,第二个目的其实就是延缓请求,将请求的峰值基于时间分片了。
在验证答题的过程中,除了验证问题的答案以外,还包括用户本身身份的验证,例如是否已经登录,用户的Cookie是否完整,用户是否重复频繁提交等等。
Alt 浅谈Java秒杀业务场景

减库存

秒杀的关键是对于商品库存的操作,要防止超卖现象的发生,对于减库存的这个操作,一般有以下几种方式:
下单减库存,即当买家下单后,在商品库存中减去对应的购买数量,下单时直接通过数据库的事物机制控制商品库存,但是可能会导致的情况是会出现恶意下单情况。
付款减库存,即买家下单后,并不会立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家,但是可能会遇到当并发较高情况下部分买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
预扣库存,买家下单后,库存为其保留一定的时间,超过这个时间库存将会自动释放,在买家付款前,系统会校验该订单的库存是否还有保留,如果没有保留,则再次尝试预扣,如果库存不足,则不允许继续支付,如果预扣成功,则完成付款并实际地减去库存。

下单减库存

“下单减库存”在数据一致性上,主要就是保证大并发请求时库存数据不能为负数,一是可以在应用程序上通过事务来判断,即保证减后库存不能为负数,否则就回滚;另一种方法是直接设置数据库的字段为无符号整数,这样减后库存字段值小于0就会直接执行SQL语句来报错;再有一种就是使用CASE WHEN判断语句,例如这样的SQL语句:

1
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory - xxx ELSE inventory END

解决并发读问题,可以采用LocalCache(在秒杀系统的单机上缓存商品相关的数据)和对数据进行分层过滤的方式。

秒杀项目Demo演示以及个人想法

之前写过一个商品秒杀的项目,可以做一下参考,秒杀演示项目地址

针对于前端资源方面的我了解的不多,据我了解首先可以将JS/CSS文件压缩,让多个JS/CSS组合,减少连接数,CDN优化,就近原则,解决网络拥堵。
在我的演示项目中,解决超卖问题时,首先使用Redis存储秒杀订单,减库存时判断数量大于0,使用唯一索引限制用户和商品,user_id和goods_id。

秒杀接口优化

将秒杀的同步操作下单改为异步下单,在数据库访问的流程中:

  1. 系统初始化,把商品的库存数量加载到Redis
  2. 收到请求,Redis预减库存,库存不足,直接返回失败,否则继续下一步
  3. 请求入队,立即返回排队中
  4. 请求出队,生成订单,减少库存
  5. 客户端轮询,是否秒杀成功

需要注意的点,首先通过Redis预减库存来减少数据库访问,还可以通过添加内存标记减少Redis访问;其次请求入队缓冲,异步下单的操作,增强用户体验,而不是之前的卡顿等待操作情况。

安全和限流操作

  • 秒杀接口地址隐藏,在秒杀开启之前隐藏地址,先通过接口去访问获取秒杀页面地址
  • 数学公式验证码,在点击秒杀操作之前,先输入验证码,分散用户的请求
  • 接口限流防刷,可以用拦截器减少对业务的入侵

针对我的演示项目所用的就这么多了,更多的细节只能在代码中才能看到,如果对于演示代码有疑问的话,欢迎与我讨论。

更多部分

未完待续…

后记

针对秒杀业务部分只有这么多了,更多的细节以及经验往往只有真正实践过操作过才有感触,但是清晰的思路以及前人踏过的坑会让我们避免出错。关于秒杀部分目前就这么多了,以后想到了再进行补充,欢迎小伙伴们与我沟通交流 ^_^