Lua腳本在Redis事務中的應用實踐

来源:https://www.cnblogs.com/Jcloud/archive/2022/09/23/16721766.html
-Advertisement-
Play Games

使用過Redis事務的應該清楚,Redis事務實現是通過打包多條命令,單獨的隔離操作,事務中的所有命令都會按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。事務中的命令要麼全部被執行,要麼全部都不執行(原子操作)。但其中有命令因業務原因執行失敗並不會阻斷後續命令的執行,且也無... ...


使用過Redis事務的應該清楚,Redis事務實現是通過打包多條命令,單獨的隔離操作,事務中的所有命令都會按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。事務中的命令要麼全部被執行,要麼全部都不執行(原子操作)。但其中有命令因業務原因執行失敗並不會阻斷後續命令的執行,且也無法回滾已經執行過的命令。如果想要實現和MySQL一樣的事務處理可以使用Lua腳本來實現,Lua腳本中可實現簡單的邏輯判斷,執行中止等操作。

1 初始Lua腳本

Lua是一個小巧的腳本語言,Redis 腳本使用 Lua 解釋器來執行腳本。 Reids 2.6 版本通過內嵌支持 Lua 環境。執行腳本的常用命令為 EVAL。編寫Lua腳本就和編寫shell腳本一樣的簡單。Lua語言詳細教程參見

示例:

--[[
    version:1.0
    檢測key是否存在,如果存在並設置過期時間
    入參列表:
        參數個數量:1
        KEYS[1]:goodsKey 商品Key

    返回列表code:
        +0:不存在
        +1:存在
--]]
local usableKey = KEYS[1]

--[ 判斷usableKey在Redis中是否存在 存在將過期時間延長1分鐘 並返回是否存在結果--]
local usableExists = redis.call('EXISTS', usableKey)
if (1 == usableExists) then
    redis.call('PEXPIRE', usableKey, 60000)
end
return { usableExists }
  1. 示例代碼中redis.call(), 是Redis內置方法,用與執行redis命令
  2. if () then end 是Lua語言基本分支語法
  3. KEYS 為Redis環境執行Lua腳本時Redis Key 參數,如果使用變數入參使用ARGV接收
  4. “—”代表單行註釋 “—[[ 多行註釋 —]]”

2 實踐應用

2.1 需求分析

經典案例需求:庫存量扣減並檢測庫存量是否充足。

基礎需求分析:商品當前庫存量>=扣減數量時,執行扣減。商品當前庫存量<扣減數量時,返回庫存不足

實現方案分析:

1)MySQL事務實現:

  • 利用DB行級鎖,鎖定要扣減商品庫存量數據,再判斷庫存量是否充足,充足執行扣減,否則返回庫存不足。
  • 執行庫存扣減,再判斷扣減後結果是否小於0,小於0說明庫存不足,事務回滾,否則提交事務。

2)方案優缺點分析:

  • 優點:MySQL天然支持事務,實現難度低。
  • 缺點:不考慮熱點商品場景,當業務量達到一定量級時會達到MySQL性能瓶頸,單庫無法支持業務時擴展問題成為難點,分表、分庫等方案對功能開發、業務運維、數據運維都須要有針對於分表、分庫方案所配套的系統或方案。對於系統改造實現難度較高。

Redis Lua腳本事務實現:將庫存扣減判斷庫存量最小原子操作邏輯編寫為Lua腳本。

  • 從DB中初始化商品庫存數量,利用Redis WATCH命令。
  • 判斷商品庫存量是否充足,充足執行扣減,否則返回庫存不足。
  • 執行庫存扣減,再判斷扣減後結果是否小於0,小於0說明庫存不足,反向操作增加減少庫存量,返回操作結果

方案優缺點分析:

  • 優點:Redis命令執行單線程特性,無須考慮併發鎖竟爭所帶來的實現複雜度。Redis天然支持Lua腳本,Lua語言學習難度低,實現與MySQL方案難度相當。Redis同一時間單位支持的併發量比MySQL大,執行耗時更小。對於業務量的增長可以擴容Redis集群分片。
  • 缺點:暫無

2.2 Redis Lua腳本事務方案實現

初始化商品庫存量:

//利用Watch 命令樂觀樂特性,減少鎖競爭所損耗的性能
 public boolean init(InitStockCallback initStockCallback, InitOperationData initOperationData) {
 //SessionCallback 會話級Rdis事務回調介面 針對於operations所有操作將在同一個Redis tcp連接上完成
List<Object> result = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
            public List<Object> execute(RedisOperations operations) {
                Assert.notNull(operations, "operations must not be null");
//Watch 命令用於監視一個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那麼事務將被打斷
//當出前併發初始化同一個商品庫存量時,只有一個能成功
                operations.watch(initOperationData.getWatchKeys());
                int initQuantity;
                try {
//查詢DB商品庫存量
                    initQuantity = initStockCallback.getInitQuantity(initOperationData);
                } catch (Exception e) {
                    //異常後釋放watch
                    operations.unwatch();
                    throw e;
                }
//開啟Reids事務
                operations.multi();
//setNx設置商品庫存量
                operations.opsForValue().setIfAbsent(initOperationData.getGoodsKey(), String.valueOf(initQuantity));
//設置商品庫存量 key 過期時間
                operations.expire(initOperationData.getGoodsKey(), Duration.ofMinutes(60000L));
///執行事事務
                return operations.exec();
            }
        });
//判斷事務執行結果
        if (!CollectionUtils.isEmpty(result) && result.get(0) instanceof Boolean) {
            return (Boolean) result.get(0);
        }
        return false;
    }

庫存扣減邏輯

--[[
    version:1.0
    減可用庫存
    入參列表:
        參數個數量:
        KEYS[1]:usableKey 商品可用量Key
        KEYS[3]:usableSubtractKey 減量記錄key
        KEYS[4]:operateKey 操作防重Key
        KEYS[5]:hSetRecord 記錄操作單號信息
        ARGV[1]:quantity操作數量
        ARGV[2]:version 操作版本號
        ARGV[5]:serialNumber 單據流水編碼
        ARGV[6]:record 是否記錄過程量
    返回列表:
        +1:操作成功
         0: 操作失敗
        -1: KEY不存在
        -2:重覆操作
        -3: 庫存不足
        -4:過期操作
        -5:缺量庫存不足
        -6:可用負庫存
--]]
local usableKey = KEYS[1];
local usableSubtractKey = KEYS[3]
local operateKey = KEYS[4]
local hSetRecord = KEYS[5]

local quantity = tonumber(ARGV[1])
local version = ARGV[2]
local serialNumber = ARGV[5]

--[ 判斷商品庫存key是否存在 不存在返回-1 --]
local usableExists = redis.call('EXISTS', usableKey);
if (0 == usableExists) then
    return { -1, version, 0, 0 };
end

--[ 設置防重key 設置失敗說明操作重覆返回-2 --]
local isNotRepeat = redis.call('SETNX', operateKey, version);
if (0 == isNotRepeat) then
    redis.call('SET', operateKey, version);
    return { -2, version, quantity, 0 };
end


--[ 商品庫存量扣減後小0 說明庫存不足 回滾扣減數量 並清除防重key立即過期 返回-3 --]
local usableResult = redis.call('DECRBY', usableKey, quantity);
if ( usableResult < 0) then
    redis.call('INCRBY', usableKey, quantity);
    redis.call('PEXPIRE', operateKey, 0);
    return { -3, version, 0, usableResult };
end

--[ 記錄扣減量並設置防重key 30天後過期 返回 1--]
-- [ 需要記錄過程量與過程單據信息 --]
local usableSubtractResult = redis.call('INCRBY', usableSubtractKey, quantity);
redis.call('HSET', hSetRecord, serialNumber, quantity)
redis.call('PEXPIRE', hSetRecord, 3600000)
redis.call('PEXPIRE', operateKey, 2592000000)
redis.call('PEXPIRE', usableKey, 3600000)
return { 1, version, quantity, 0, usableResult ,usableSubtractResult}

初始化Lua腳本到Redis伺服器

//讀取Lua腳本文件
    private String readLua(File file) {
        StringBuilder sbf = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String temp;
            while (Objects.nonNull(temp = reader.readLine())) {
                sbf.append(temp);
                sbf.append('\n');
            }
            return sbf.toString();
        } catch (FileNotFoundException e) {
            LOGGER.error("[{}]文件不存在", file.getPath());
        } catch (IOException e) {
            LOGGER.error("[{}]文件讀取異常", file.getPath());
        }
        return null;
    }
//初始化Lua腳本到Redis伺服器 成功後會返回腳本對應的sha1碼,系統緩存腳本sha1碼,
//通過sha1碼可以在Redis伺服器執行對應的腳本
public String scriptLoad(File file) {
String script = readLua(file)
   return stringRedisTemplate.execute((RedisCallback<String>) connection -> connection.scriptLoad(script.getBytes()));
}

腳本執行

 public OperationResult evalSha(String redisScriptSha1,OperationData operationData) {
        List<String> keys = operationData.getKeys();
        String[] args = operationData.getArgs();
//執行Lua腳本 keys 為Lua腳本中使用到的KEYS args為Lua腳本中使用到的ARGV參數
//如果是在Redis集群模式下,同一個腳本中的多個key,要滿足多個key在同一個分片
//伺服器開啟hash tag功能,多個key 使用{}將相同部分包裹 
//例:usableKey:{EMG123} operateKey:operate:{EMG123} 
Object result = stringRedisTemplate.execute(redisScriptSha1, keys, args);
//解析執行結果        
return parseResult(operationData, result);
    }

3 總結

Redis在小數據操作併發可達到10W,針對與業務中對資源強校驗且高併發場景下使用Redis配合Lua腳本完成簡單邏輯處理抗併發量是個不錯的選擇。

註:Lua腳本邏輯儘量簡單,Lua腳本實用於耗時短且原子操作。耗時長影響Redis伺服器性能,非原子操作或邏輯複雜會增加於腳本調試與維度難度。理想狀態是將業務用Lua腳本包裝成一個如Redis命令一樣的操作。


作者:王純


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. 融合效果 在 CSS 中有一種實現融合效果的技巧,使用模糊濾鏡(blur)疊加對比度濾鏡(contrast)使兩個接近的元素看上去“粘”在一起,如下圖所示: 博客園的 ChokCoco 就用這個技巧實現了很多不同的玩法並寫了很多文章,例如這篇: 你所不知道的 CSS 濾鏡技巧與細節 我一直對這 ...
  • zookeeper ##協調機制 選舉leader 多個flower 客戶端 伺服器 ##特點 半數以上 數據一致性 在有限時間範圍內,執行順序同步於發送順序 文件結構類unix 樹狀每一個結點既是文件夾也可以是值。記為znode ? 本質上zookeeper 是文件系統+通知機制 ##啟動zook ...
  • 1.加拿大創新、科學和經濟發展部 (ISED) 於 2022 年 9 月 9 日發佈了第 2022-CEB001 號通知。 該通知包括關於無線電標準規範 RSS-195 “無線通信服務 (WCS) 設備在 2305-2320 MHz 和 2345-2360 MHz 頻段”第 2 版的指南,旨在重申 ...
  • 安裝MySQL版本為:8.0.16 1、首次安裝,下載命令: wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.20-linux-glibc2.12-x86_64.tar.xz 2、解壓 tar xvJf mysql-8.0.2 ...
  • 摘要:先哲說,萬物莫不相異,而在今天,萬物也可相通。 本文分享自華為雲社區《打破聯接壁壘,華為雲IoT到底強在哪?》,作者:華為IoT雲服務。 “凡物莫不相異”, 是十七世紀哲學家萊布尼茨提出的著名論斷。這句至理名言,卻為難了今天的物聯網從業者們。 在物聯網領域內,設備的差異表現為協議不同和數據模型 ...
  • 1 DBeaver介紹 DBeaver是一個通用的資料庫管理工具和 SQL 客戶端,支持多種相容 JDBC 的資料庫。DBeaver 提供一個圖形界面用來查看資料庫結構、執行SQL查詢和腳本,瀏覽和導出數據,處理BLOB/CLOB 數據,修改資料庫結構等。 2 安裝DBeaver 下載地址:http ...
  • ​ 這次來聊聊Hadoop中使用廣泛的分散式計算方案——MapReduce。MapReduce是一種編程模型,還是一個分散式計算框架。 MapReduce作為一種編程模型功能強大,使用簡單。運算內容不只是常見的數據運算,幾乎大數據中常見的計算需求都可以通過它來實現。使用的時候僅僅需要通過實現Map和 ...
  • 在最新一屆國際資料庫頂級會議 ACM SIGMOD 2022 上,來自清華大學的李國良和張超兩位老師發表了一篇論文:《HTAP Database: What is New and What is Next》,並做了 《HTAP Database:A Tutorial》 的專項報告。這幾期學術分享會的 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...