§1 秒杀常见问题
超卖
因并发导致且没有加锁导致的
通过 WATCH
解决
超时
redis 连接数不够,导致大并发下大量请求不能获取连接
通过连接池解决
遗留
乐观锁互相影响导致大量失败导致
乐观锁改悲观锁,使用 lua 实现
§2 实现
@Service
public class SeckillDemo {
private static String SEC_KILL_STOCK_KEY = "SK:STOCK:";
private static String SEC_KILL_USERS_KEY = "SK:USERS:";
@Resource
private RedisTemplate<String,Object> redisTemplate;
public int seckill(String prodId,String userId){
String stockKey = SEC_KILL_STOCK_KEY + prodId;
String usersKey = SEC_KILL_USERS_KEY + prodId;
//可以增加判断秒杀是否开始的操作,可以用 ttl或时间戳比较进行
redisTemplate.watch(Arrays.asList(stockKey,usersKey));
try{
int count = Integer.parseInt((String) redisTemplate.opsForValue().get(stockKey));
if(count < 1)
return -1;
if(Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(usersKey,userId)))
return -2;
redisTemplate.multi();
redisTemplate.opsForValue().decrement(stockKey);
redisTemplate.opsForSet().add(usersKey,userId);
redisTemplate.exec();
}finally {
redisTemplate.unwatch();
}
return 1;
}
}
§3 另一种思路
- 假设有 10000 库存
- 开设 n 个奖池(候选人池,set),每个奖池都增设一个启用时间,可选的设置最大候选人数
因为是秒杀,所以每级奖池开启时间相互延时几秒,比如3秒 - 开设 n+1个中奖池,除了总中奖池,其他与奖池对应,增加中奖池结束标记(bitmap)
- 设置一开始是第一级奖池,0;前 n-1 个奖池均分秒杀成功集合,或制定更丰富的策略
- 最后一级奖池作为补漏,后面说
- 用户开始秒杀
- 一大波用户的请求进入服务
- 根据时间,用户进入当前奖池,时间超过、最大候选人数超过时快速返回秒杀失败
- 其他用户可以 O(1) 的返回自己所在的奖池号
- 时间超过、最大候选人数超过时,加分布式锁
- 加锁成功的线程负责替换奖池、触发抽奖
spop
,抽出的用户进入对应的中奖池 - 失败的线程秒杀失败或者10ms后进入下一级奖池
- 加锁成功的线程负责替换奖池、触发抽奖
- 用户的秒杀动作如上面流程,循环至最后一级奖池
- 用户客户端收到返回后
- 直接失败的显示失败
- 否则显示转圈
- 转圈时周期性访问对应级别的中奖池
ismember
- 直到中奖池结束标记,池里没有自己,显示失败
- 否则自己中奖了,客户端给服务端发送确认 ack ,并显示自己秒杀成功
ack使自己从中奖池进入总中奖池 - ack 反馈失败的用户自动进入最后一级奖池
理论上好像可以同时解决超卖超时和遗留问题,推测的优缺点
-
时间可控
-
阶段性秒杀成功比例可控
-
玩法非常容易扩展,看产品脑洞
-
但是相比 lua、原始秒杀,齁重