1.使用场景
公司2周年准备上一个秒杀,针对商品A,价值1W,准备在上午10点整,进行一次秒杀,一共20个库存。
预测当日会有100W用户进行抢购
2.遇到的问题
高并发、库存不能超卖、不能查询数据库 否则数据库抵挡不住压力
3.实现方案
1.使用 redis 保存四件商品的库存数量
测试:0>set seckillGoods:1001 20
"OK"
2.利用lua脚本进行减库存,首先编写好lua脚本
网上有很多案例,lua脚本如果直接复制,都会存在问题(少分号、少空格、引号错误、忘记转数字等),这里已解决。
如果只想要脚本则复制以下代码即可
local isExist = redis.call('exists', KEYS[1]);
if (tonumber(isExist) > 0) then
local goodsNumber = redis.call('get', KEYS[1]);
if (tonumber(goodsNumber) > 0) then
redis.call('decr',KEYS[1]);
return 1;
else
redis.call('del', KEYS[1]);
return 0;
end;
else
return -1;
end;
3.编写测试案例
@Component
public class LuaReduceStock {
private static final String STOCK_LUA;
@Resource
private RedisTemplate redisTemplate;
static {
StringBuilder s = new StringBuilder();
s.append("local isExist = redis.call('exists', KEYS[1]); ");
s.append("if (tonumber(isExist) > 0) then ");
s.append(" local goodsNumber = redis.call('get', KEYS[1]); ");
s.append(" if (tonumber(goodsNumber) > 0) then ");
s.append(" redis.call('decr',KEYS[1]); ");
s.append(" return 1; ");
s.append(" else "
+"redis.call('del', KEYS[1]); ");
s.append(" return 0; ");
s.append(" end; ");
s.append("else ");
s.append(" return -1; ");
s.append(" end;");
STOCK_LUA = s.toString();
}
/**
* 减库存
* @param key
* @return
*/
public boolean reduceStock(String key){
List<String> keys = new ArrayList<>();
keys.add(key);
List<String> args = new ArrayList<>();
Long result = (Long) redisTemplate.execute(new RedisCallback() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
Object eval = ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);
return Long.valueOf(eval.toString());
}
});
return result > 0;
}
}
@Resource
private LuaReduceStock luaReduceStock;
@RequestMapping("/api/v4/testLuaReduceStock")
public JsonResult testLuaReduceStock(String key){
List<Thread> allThread = new ArrayList<>();
for(int i = 1; i<= 1000; i++){
Thread thread = new Thread(()-> {
boolean b = luaReduceStock.reduceStock(key);
if(b){
System.out.println("恭喜您,抢到了库存!!!" + Thread.currentThread().getName());
//MQ.sendSuccessMessage();
最终抢到库存的用户,可以发送一条消息到队列中,进行异步下单。
}else{
System.out.println("对不起,库存已卖光啦!!!" + Thread.currentThread().getName());
}
},"线程" + i);
allThread.add(thread);
}
for(Thread thread : allThread){
thread.start();
}
return JsonResult.buildSuccessResult("成功");
}
最终看控制台打印的效果可知,只有20个线程抢到了库存。
4.总结lua脚本调用redis的优势
- 多个命令合并到脚本统一处理,减少多个命令的网络开销
- 脚本能确保操作的原子性,不会受到其他客户端命令的影响
- 代码复用,redis将永久存放客户端发送的脚本