Redis-使用Lua脚本解决多线程下的超卖问题以及为什么?
Posted 健康-是最好的时光
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis-使用Lua脚本解决多线程下的超卖问题以及为什么?相关的知识,希望对你有一定的参考价值。
一.多线程下引起的超卖问题呈现
1.1.我先初始化库存数量为1、订单数量为0
1.2.开启3个线程去执行业务
业务为:判断如果说库存数量大于0,则库存减1,订单数量加1
结果为:库存为-2,订单数量为3
原因:如下图所示,这是因为分别有6个指令(3个库存减1指令,3个订单数量加1指令)在redis服务端执行导致的。
namespace MengLin.Shopping.Redis.LuaScript public class SecKillOriginal static SecKillOriginal() using (RedisClient client = new RedisClient("127.0.0.1", 6379)) //删除当前数据库中的所有Key, 默认删除的是db0 client.FlushDb(); //删除所有数据库中的key client.FlushAll(); //初始化库存数量为1和订单数量为0 client.Set("inventoryNum", 1); client.Set("orderNum", 0); public static void Show() for (int i = 0; i < 3; i++) Task.Run(() => using (RedisClient client = new RedisClient("127.0.0.1", 6379)) int inventoryNum = client.Get<int>("inventoryNum"); //如果库存数量大于0 if (inventoryNum > 0) //给库存数量-1 var inventoryNum2 = client.Decr("inventoryNum"); Console.WriteLine($"给库存数量-1后的数量-inventoryNum: inventoryNum2"); //给订单数量+1 var orderNum = client.Incr("orderNum"); Console.WriteLine($"给订单数量+1后的数量-orderNum: orderNum"); else Console.WriteLine($"抢购失败: 原因是因为没有库存"); );
二.使用Lua脚本解决多线程下超卖的问题以及为什么
2.1.修改后的代码如下
结果为:如下图所示,库存为0、订单数量为1,并没有出现超卖的问题且有2个线程抢不到。
namespace MengLin.Shopping.Redis.LuaScript public class SecKillLua /// <summary> /// 使用Lua脚本解决多线程下变卖的问题 /// </summary> static SecKillLua() using (RedisClient client = new RedisClient("127.0.0.1", 6379)) //删除当前数据库中的所有Key, 默认删除的是db0 client.FlushDb(); //删除所有数据库中的key client.FlushAll(); //初始化库存数量为1和订单数量为0 client.Set("inventoryNum", 1); client.Set("orderNum", 0); public static void Show() for (int i = 0; i < 3; i++) Task.Run(() => using (RedisClient client = new RedisClient("127.0.0.1", 6379)) //如果库存数量大于0,则给库存数量-1,给订单数量+1 var lua = @"local count = redis.call(\'get\',KEYS[1]) if(tonumber(count)>0) then --return count redis.call(\'INCR\',ARGV[1]) return redis.call(\'DECR\',KEYS[1]) else return -99 end"; Console.WriteLine(client.ExecLuaAsString(lua, keys: new[] "inventoryNum" , args: new[] "orderNum" )); );
三.为什么使用Lua脚本就能解决多线程下的超卖问题呢?
是因为Lua脚本把3个指令,分别是:判断库存数量是否大于0、库存减1、订单数量加1,这3个指令打包放在一起执行了且不能分割,相当于组装成了原子指令,所以避免了超卖问题。
在redis中我们尽量使用原子指令从而避免一些并发的问题。
使用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
以上是关于Redis-使用Lua脚本解决多线程下的超卖问题以及为什么?的主要内容,如果未能解决你的问题,请参考以下文章