秒杀的关键要考虑两个方面:(1)不能够超卖,超卖(库存变成负数、优惠券变成负数)必然带来损失,是不允许的;(2)需要考虑性能!
1. 分布式锁
为了防止超卖,需要加锁,Java本身的内存锁在分布式的环境下并不能发挥加锁的功能,也就是说需要实现分布式锁。常见的方法有基于Redis的分布式锁和基于Zookeeper的分布式锁。
1.1 基于Redis分布式锁
1.1.1 Redis分布式锁原理
https://xiaomi-info.github.io/
Redis 锁主要利用 Redis 的 setnx 命令。
- 加锁命令:SETNX key value,当键不存在时,对键进行设置操作并返回成功,否则返回失败。KEY 是锁的唯一标识,一般按业务来决定命名。
- 解锁命令:DEL key,通过删除键值对释放锁,以便其他线程可以通过 SETNX 命令来获取锁。
- 锁超时:EXPIRE key timeout, 设置 key 的超时时间,以保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源被永远锁住。
则加锁解锁伪代码如下:
1 | if (setnx(key, 1) == 1){ |
以上存在以下问题:
- SETNX 和 EXPIRE 非原子性
- 锁错误解除
- 超时解锁导致并发
- 不可重入
1.1.2 Redis分布式锁实现
1.1.3 Redis主从模式存在的问题
1.2 基于Zookeeper分布式锁
1.2.1 Zookeeper原理及应用场景
1.2.2 Zookeeper常用的命令
1.2.3 Zookeeper分布式锁原理
1.2.4 Zookeeper分布式锁实现
2. 秒杀性能
2.1 秒杀前,页面访问压力大?
解决方案:页面静态化,CDN+Redis+Ngnix多级缓存
2.2 秒杀时,下单过于集中,作弊软件刷单?
解决方案:前端加答题环节,把URL动态化,就连写代码的人都不知道,你就通过MD5之类的加密算法加密随机的字符串去做url,然后通过前端代码获取url后台校验才能通过
2.3 秒杀时,下单对系统冲击大,影响其他正常功能?
解决方案:独立的秒杀系统
2.4 秒杀时,快速精准减库存?
解决方案:基于缓存如Redis实现快速精准减库存。
我们要开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做,然后等秒杀介绍了,再异步的去修改库存就好了。但是用了Redis就有一个问题了,我们上面说了我们采用主从,就是我们会去读取库存然后再判断然后有库存才去减库存,正常情况没问题,但是高并发的情况问题就很大了。Lua脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作。这点是关键。知道原理了,我们就写一个脚本把判断库存扣减库存的操作都写在一个脚本丢给Redis去做,那到0了后面的都Return False了是吧,一个失败了你修改一个开关,直接挡住所有的请求,然后再做后面的事情嘛。
2.5 秒杀后,快速过滤没有抢到的下单请求?
解决方案:库存减完后,快速通知Ngnix,过滤下单请求。
2.6 秒杀后,下单模块压力大?
解决方案:下单请求写入RocketMq,下单后使用RocketMq通知下游服务,完成下单。