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脚本解决多线程下的超卖问题以及为什么?的主要内容,如果未能解决你的问题,请参考以下文章

如何解决高并发秒杀的超卖问题

使用Lua脚本通过原子减防止超卖

Redis 学习Redis事务秒杀案例

Redis 学习Redis事务秒杀案例

Redis 学习Redis事务秒杀案例

redis原子性读写操作