使用Lua脚本通过原子减防止超卖
需求
双十二要搞一个一分钱门票抢购的活动。
分析
性能分析,抢购时会发生高并发,如果仅仅依靠Mysql数据库,有可能因为大量的请求频繁访问数据库造成服务器雪崩,所以考虑通过Redis减库存,最终的数据落地到DB中。
在高并发的情况下,还要考虑到超卖的问题,因而打算使用Lua脚本完成原子减的操作。
在这里,我们只针对减库存的操作进行分析。
实现
不使用原子操作,出现超卖的情况。第一步:先从redis中查出库存进行判断,第二步:如果库存>0,则进行减库存的操作。
代码实现:
1 // 第一步:从redis中查出库存 2 Integer stock = (Integer) RedisUtils.get("stock"); 3 4 // 第二步:如果库存>0,则进行减库存的操作 5 if (stock > 0) { 6 long spareStock = RedisUtils.decr("stock", 1); 7 System.out.println(getName() + "抢到了第" + spareStock + "件"); 8 } else { 9 System.out.println("库存不足"); 10 }
用多线程模拟并发请求:库存为500,创建505个线程去抢购。
1 for(int i =1;i<=505;i++){ 2 MyThread2 thread =new MyThread2("线程"+i); 3 thread.start(); 4 }
执行结果:出现超卖问题,原因是:查询库存及减库存不是原子性操作。
使用原子性操作:直接减库存。
1 public void run() { 2 long stock = RedisUtils.stock("stock"); 3 if (stock > 0) { 4 System.out.println(getName() + "抢到了第" + stock + "件"); 5 } else { 6 System.out.println("库存不足"); 7 } 8 9 }
Lua脚本实现减库存操作:
/** * 库存不足 */ public static final int LOW_STOCK = 0; /** * 不限库存 */ public static final long UNINITIALIZED_STOCK = -1L; /** * 执行扣库存的脚本 */ public static final String STOCK_LUA; static { // 初始化减库存lua脚本 StringBuilder sb = new StringBuilder(); sb.append("if (redis.call('exists', KEYS[1]) == 1) then"); sb.append(" local stock = tonumber(redis.call('get', KEYS[1]));"); sb.append(" if (stock == -1) then"); sb.append(" return 1;"); sb.append(" end;"); sb.append(" if (stock > 0) then"); sb.append(" redis.call('incrby', KEYS[1], -1);"); sb.append(" return stock;"); sb.append(" end;"); sb.append(" return 0;"); sb.append("end;"); sb.append("return -1;"); STOCK_LUA = sb.toString(); } /** * 扣库存 * * @param key 库存key * @return 扣减之前剩余的库存【0:库存不足; -1:库存未初始化; 大于0:扣减库存之前的剩余库存】 */ public static Long stock(String key) { // 脚本里的KEYS参数 List<String> keys = new ArrayList<>(); keys.add(key); // 脚本里的ARGV参数 List<String> args = new ArrayList<>(); Long result = (Long)redisTemplate.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { Object nativeConnection = connection.getNativeConnection(); // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行 // 集群模式 if (nativeConnection instanceof JedisCluster) { return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args); } // 单机模式 else if (nativeConnection instanceof Jedis) { return (Long)((Jedis) nativeConnection).eval(STOCK_LUA, keys, args); } return UNINITIALIZED_STOCK; } }); return result; }
执行结果:505个线程去抢500个商品,有五个线程会抢不到,测试结果与预期一致,解决了超卖的问题。
参考:https://blog.csdn.net/xiaolyuh123/article/details/79208959