【Redis】-使用Lua指令碼解決多執行緒下的超賣問題以及為什麼?

2023-05-06 18:00:17

一.多執行緒下引起的超賣問題呈現
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中我們儘量使用原子指令從而避免一些並行的問題。