前言
关于秒杀的设计,网上的讨论很多,良莠不齐,但大多会有这几个共识。
- 将流量挡在前端,可以用nginx+redis+lua限流
- 库存提前预热到redis当中,在redis中减库存
- 减库存之后,发送消息到队列,后续动作消费队列,减轻对数据库的压力
- 为解决超卖问题,扣库存的操作用redis分布式锁,升级版就是将单个redis库存分成多个,相当于分段锁,提高并发能力
- 秒杀之后,商品下架或者再等待一下次秒杀,这中间不可用。
还有对于超卖的问题,有的文章建议
- 在sql加上判断防止数据边为负数
- 数据库加唯一索引防止用户重复购买
- redis预减库存减少数据库访问 内存标记减少redis访问 请求先入队列缓冲,异步下单,增强用户体验
个人汇总了一下,感觉都比较繁琐,如果前端能限制大部分的流量,那进入后台的流量很小,这时候可以不用分布式锁,完全用redis减库存就可以了,大体来说有2种思路。第一种是先redis直接自减,当扣成负数的时候,不进行后续的数据库操作,直接返回错误,但是redis会扣成负数。第二种是用redis的lua脚本,先判断是否是大于0,若是大于0则自减,否则直接返回,这种不会扣成负数。
1. 每次都自减
先redis直接自减,当扣成负数的时候,不进行后续的数据库操作,直接返回错误,但是redis会扣成负数。缺点是如果前端显示库存数,可以做一些逻辑判断,若是小于0,直接返回0。还有就是如果要增加库存,不能直接累加,要先恢复到0。
String key = "redis:test:stock";
redisTemplate.opsForValue().set(key, 10);
Thread[] threads = new Thread[100];
for (Thread thread : threads) {
thread = new Thread(() -> {
long remaind = 0;
if ((remaind = redisTemplate.opsForValue().increment(key, -1).intValue()) > -1) {
System.out.println(Thread.currentThread().getName() + ":" + "扣除成功" + "剩余:" + remaind);
//处理后续操作
}else {
System.out.println(Thread.currentThread().getName() + ":" + "扣除失败" + "剩余:" + remaind);
//直接返回
}
});
thread.start();
}
2. 先判断再自减
用redis的lua脚本,先判断是否是大于0,若是大于0则自减,否则直接返回,这种不会扣成负数。
local num=redis.call('get',KEYS[1])
if tonumber(num)>0 then
return redis.call('decr',KEYS[1])
else
return -1
end
当返回是负数的时候,说明库存已经扣完了。
String key = "redis:test:stock";
redisTemplate.opsForValue().set(key, 10);
// 这里必须是Long
RedisScript<Long> script = RedisScript.of("local num=redis.call('get',KEYS[1])\r\n" +
"if tonumber(num)>0 then\r\n" +
" return redis.call('decr',KEYS[1])\r\n" +
"else\r\n" +
" return -1\r\n" +
"end", Long.class);
Thread[] threads = new Thread[100];
for (Thread thread : threads) {
thread = new Thread(() -> {
long remaind = 0;
if ((remaind=redisTemplate.execute(script, Arrays.asList(key), new Object[] {})) > -1) {
System.out.println(Thread.currentThread().getName() + ":" + "扣除成功" + "剩余:" + remaind);
//处理后续逻辑
}else {
System.out.println(Thread.currentThread().getName() + ":" + "扣除失败" + "剩余:" + remaind);
//直接返回
}
});
thread.start();
}
最后总结
直接用redis扣库存,必须要在前端挡掉大部分流量,这中间如果redis挂掉,就灾难了,因为没有穿透到数据库获取缓存的机制,所以保障好redis的稳定是非常有必要的,主从,哨兵等等。