首頁 > 資料庫 > Redis > 聊聊Redis中的事務:事務模式、Lua腳本

聊聊Redis中的事務:事務模式、Lua腳本

青灯夜游
發布: 2023-04-10 19:29:36
轉載
1625 人瀏覽過

這篇文章帶大家吃透Redis 事務,比較一下Redis事務的兩種模式(事務模式 和 Lua 腳本),希望對大家有幫助!

準確的講,Redis 交易包含兩種模式 : 交易模式Lua 腳本

先說結論:

Redis 的交易模式具備以下特點:

  • 保證隔離性;
  • 無法保證持久性;
  • 具備了一定的原子性,但不支持回滾;
  • 一致性的概念有分歧,假設在一致性的核心是約束的語義下,Redis 的事務可以保證一致性。

但 Lua 腳本更有實用場景,它是另一種形式的事務,他具備一定的原子性,但腳本報錯的情況下,事務並不會回滾。 Lua 腳本可以保證隔離性,而且可以完美的支援後面的步驟依賴前面步驟的結果。 【相關推薦:Redis影片教學

Lua 腳本模式的身影幾乎無所不在,例如分散式鎖定、延遲佇列、搶紅包等場景。

1 事務原理

Redis 的交易包含如下指令:

序號 指令及描述
1 MULTI 標記一個交易區塊的開始。
2 EXEC 執行所有交易區塊內的指令。
3 DISCARD 取消事務,並放棄執行事務區塊內的所有指令。
4 WATCH key [key ...] 監視一個(或多個) key ,如果在交易執行之前這個(或這些) key 被其他指令所改動,那麼事務將被打斷。
5 UNWATCH 取消 WATCH 指令對所有 key 的監視。

交易包含三個階段:

  1. 交易開啟,使用MULTI , 此指令標誌著執行該指令的用戶端從非交易狀態切換至交易狀態;
  2. 指令入隊,MULTI 開啟事務之後,客戶端的指令並不會立即執行,而是放入一個事務佇列;
  3. 執行交易或丟棄。如果收到 EXEC 的命令,事務佇列裡的命令將會被執行 ,如果是 DISCARD 則事務被丟棄。

下面展示一個事務的範例。

1
 redis> MULTI 
2
 OK
3
 redis> SET msg "hello world"
4
 QUEUED
5
 redis> GET msg
6
 QUEUED
7
 redis> EXEC
8
 1) OK
9
 1) hello world
登入後複製

這裡有一個疑問?在開啟事務的時候,Redis key 可以被修改嗎?

<聊聊Redis中的事務:事務模式、Lua腳本 src="https://聊聊Redis中的事務:事務模式、Lua腳本.php.cn/upload/article/000/000/024/285180a75f4043b6a1bf3826480fc6f5-0.png" alt="聊聊Redis中的事務:事務模式、Lua腳本" loading="lazy"/>

在交易執行 EXEC 指令之前 ,Redis key 依然可以被修改

在交易開啟前,我們可以 watch 指令監聽 Redis key 。在事務執行之前,我們修改 key 值 ,事務執行失敗,回傳 nil

<聊聊Redis中的事務:事務模式、Lua腳本 src="https://聊聊Redis中的事務:事務模式、Lua腳本.php.cn/upload/article/000/000/024/285180a75f4043b6a1bf3826480fc6f5-1.png" alt="聊聊Redis中的事務:事務模式、Lua腳本" loading="lazy"/>

透過上面的例子,watch 指令可以實作類似樂觀鎖的效果

2 事務的ACID

2.1 原子性

原子性是指:一個事務中的所有操作,或全部完成,或全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾到事務開始前的狀態,就像這個事務從來沒有執行過一樣。

第一個範例:

在執行 EXEC 指令前,客戶端發送的操作指令錯誤,例如:語法錯誤或使用了不存在的指令。

1
 redis> MULTI
2
 OK
3
 redis> SET msg "other msg"
4
 QUEUED
5
 redis> wrongcommand  ### 故意写错误的命令
6
 (error) ERR unknown command &#39;wrongcommand&#39; 
7
 redis> EXEC
8
 (error) EXECABORT Transaction discarded because of previous errors.
9
 redis> GET msg
10
 "hello world"
登入後複製

在這個例子中,我們使用了不存在的命令,導致入隊失敗,整個事務都將無法執行 。

第二個範例:

交易操作入隊時,指令和操作的資料型別不符 ,入佇列正常,但執行 EXEC 指令異常 。

1
 redis> MULTI  
2
 OK
3
 redis> SET msg "other msg"
4
 QUEUED
5
 redis> SET mystring "I am a string"
6
 QUEUED
7
 redis> HMSET mystring name  "test"
8
 QUEUED
9
 redis> SET msg "after"
10
 QUEUED
11
 redis> EXEC
12
 1) OK
13
 2) OK
14
 3) (error) WRONGTYPE Operation against a key holding the wrong kind of value
15
 4) OK
16
 redis> GET msg
17
 "after"
登入後複製

這個例子裡,Redis 在執行 EXEC 命令時,如果出現了錯誤,Redis 不會終止其它命令的執行,事務也不會因為某個命令執行失敗而回滾 。

綜上,我對Redis 事務原子性的理解如下:

  1. 命令入隊時報錯, 會放棄事務執行,保證原子性;
  2. 命令入隊時正常,執行EXEC 指令後報錯,不保證原子性;

也就是:Redis 事務在特定條件下,才具備一定的原子性

2.2 隔離性

資料庫的隔離性是指:資料庫允許多個並發交易同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務並發執行時由於交叉執行而導致資料的不一致。

交易隔離分為不同級別,分別是:

  • 未提交讀取(read uncommitted)
  • #提交讀取(read committed)
  • #可重複讀取(repeatable read)
  • 串行化(serializable)

首先,需要明確一點:Redis 並沒有事務隔離層級的概念。這裡我們討論 Redis 的隔離性是指:在並發場景下,事務之間是否可以做到互不干擾

我們可以將事務執行可以分為 EXEC 命令執行前EXEC 命令執行後兩個階段,分開討論。

  1. EXEC 指令執行前

在事務原理這一小節,我們發現在事務執行之前 ,Redis key 依然可以被修改。此時,可以使用 WATCH 機制來實現樂觀鎖的效果。

  1. EXEC 指令執行後

因為 Redis 是單一執行緒執行操作指令, EXEC 指令執行後,Redis 會保證指令佇列中的所有指令執行完 。這樣就可以保證事務的隔離性。

2.3 持久性

資料庫的持久性是指:交易處理結束後,資料的修改就是永久的,即使係統故障也不會遺失。

Redis 的資料是否持久化取決於 Redis 的持久化配置模式 。

  1. 沒有設定RDB 或AOF ,交易的持久性無法保證;
  2. #使用了RDB模式,在一個交易執行後,下一次的RDB 快照還未執行前,如果發生了實例宕機,交易的持久性同樣無法保證;
  3. 使用了AOF 模式;AOF 模式的三種設定選項no 、everysec 都會存在資料遺失的情況。 always 可以確保事務的持久性,但因為效能太差,在生產環境一般不建議使用。

綜上,redis 交易的持久性是無法保證的

2.4 一致性

一致性的概念一直很令人困惑,在我搜尋的資料裡,有兩類不同的定義。

  1. 維基百科

我們先看下維基百科上一致性的定義:

Consistency ensures that a transaction can only bring the database from one valid state to another, maintaining database invariants: any data written to the database must be valid according to all defined rules, including constraints, cascades, triggers, and any combination thenof. Thiscor s data. a transaction is correct. Referential integrity guarantees the primary key – foreign key relationship.

在這段文字裡,一致性的核心是“約束”,“ any data written to the database must be valid according to all defined rules 」。

如何理解約束?這裡引用知乎問題如何理解資料庫的內部一致性和外部一致性,螞蟻金服OceanBase 研發專家韓富晟回答的一段話:

「約束」由資料庫的使用者告訴資料庫,使用者要求資料一定符合這樣或那樣的約束。當資料發生修改時,資料庫會檢查資料是否仍符合約束條件,如果約束條件不再被滿足,那麼修改操作就不會發生。

關聯式資料庫最常見的兩類約束是“唯一性約束”和“完整性約束”,表格中定義的主鍵和唯一鍵都保證了指定的資料項目絕不會出現重複,表格之間定義的參照完整性也保證了同一個屬性在不同表格中的一致性。

「 Consistency in ACID 」是如此的好用,以至於已經融化在大部分使用者的血液裡了,使用者會在表格設計的時候自覺的加上需要的約束條件,資料庫也會嚴格的執行這個約束條件。

所以交易的一致性和預先定義的約束有關,保證了約束即保證了一致性

我們細細一品這句話: This prevents database corruption by an illegal transaction, but does not guarantee that a transaction is correct

寫到這裡可能大家還是有點模糊,我們舉經典轉帳的案例。

我們開啟一個事務,張三和李四帳號上的初始餘額都是1000元,而且餘額欄位沒有任何約束。張三給李四轉帳1200元。張三的餘額更新為 -200 , 李四的餘額更新為2200。

從應用程式層面來看,這個事務明顯不合法,因為現實場景中,使用者餘額不可能小於0 , 但是它完全遵循資料庫的約束,所以從資料庫層面來看,這個事務依然保證了一致性。

Redis 的交易一致性是指:Redis 交易在執行過程中符合資料庫的約束,沒有包含非法或無效的錯誤資料。

我們分三種異常場景分別討論:

  1. 執行EXEC 指令前,客戶端發送的操作指令錯誤,交易終止,資料保持一致性;

  2. 執行EXEC 命令後,命令和操作的資料類型不匹配,錯誤的命令會報錯,但事務不會因為錯誤的命令而終止,而是會繼續執行。正確的指令正常執行,錯誤的指令報錯,從這個角度來看,資料也可以保持一致性;

  3. 在執行交易的過程中,Redis 服務宕機。這裡需要考慮服務配置的持久化模式。

    • 無持久化的記憶體模式:服務重啟之後,資料庫沒有保持數據,因此數據都是保持一致性的;
    • RDB / AOF 模式: 服務重啟後,Redis透過RDB / AOF 檔案恢復數據,資料庫會還原到一致的狀態。

綜上所述,在一致性的核心是約束的語意下,Redis 的交易可以保證一致性

  1. 《設計資料密集型應用》

這本書是分散式系統入門的神書。在事務這一章節有一段關於ACID 的解釋:

<聊聊Redis中的事務:事務模式、Lua腳本 src="https://聊聊Redis中的事務:事務模式、Lua腳本.php.cn/upload/article/000/000/024/6b0d6c63a72d670e9b545b5560e86cd0-2.jpg" alt="聊聊Redis中的事務:事務模式、Lua腳本" loading="lazy"/>

Atomicity, isolation, and durability are properties of the database,whereas consistency (in the ACID sense ) is a property of the application. The application may rely on the database's atomicity and isolation properties in order to achieve consistency, but it's not up to the database alone. Thus, the letter C doesn't really belup to the database alone. Thus, the letter C doesn't really belong in the AC.#ong

原子性,隔離性和持久性是資料庫的屬性,而一致性(在ACID 意義上)是應用程式的屬性。應用可能依賴資料庫的原子性和隔離屬性來實現一致性,但這並不僅取決於資料庫。因此,字母 C 不屬於 ACID 。

很多時候,我們一直在糾結的一致性,其實就是指

符合現實世界的一致性,現實世界的一致性才是事務追求的最終目標。

為了實現現實世界的一致性,需要滿足以下幾點:

  1. 保證原子性,持久性和隔離性,如果這些特徵都無法保證,那麼交易的一致性也無法保證;
  2. 資料庫本身的約束,例如字串長度不能超過列的限製或唯一性約束;
  3. 業務層面同樣需要進行保障。

2.5 事務特點

#我們通常稱Redis 為記憶體資料庫, 不同於傳統的關係資料庫,為了提供了更高的效能,更快的寫入速度,在設計和實現層面做了一些平衡,並不能完全支援事務的ACID。

Redis 的事務具備以下特點:

  • 保證隔離性;
  • 無法保證持久性;
  • 具備了一定的原子性,但不支持回滾;
  • 一致性的概念有分歧,假設在一致性的核心是約束的語義下,Redis 的事務可以保證一致性。

從工程角度來看,假設事務操作中每個步驟需要依賴上一個步驟傳回的結果,則需要透過 watch 來實作樂觀鎖 。

3 Lua 腳本

3.1 簡介

<聊聊Redis中的事務:事務模式、Lua腳本 src="https://聊聊Redis中的事務:事務模式、Lua腳本.php.cn/upload/article/000/000/024/6b0d6c63a72d670e9b545b5560e86cd0-3.png" alt="" loading="lazy"/>

Lua 由標準C 編寫而成,程式碼簡潔優美,幾乎在所有作業系統和平台上都可以編譯,運行。 Lua 腳本可以很容易的被 C/C 程式碼調用,也可以反過來調用 C/C 的函數,這使得 Lua 在應用程式中可以被廣泛應用。

Lua 腳本在遊戲領域大放異彩,大家耳熟能詳的《大話西遊II》,《魔獸世界》都大量使用 Lua 腳本。 Java 後端工程師接觸過的 api 網關,例如 OpenrestyKong 都可以看到 Lua 腳本的身影。

從 Redis 2.6.0 版本開始, Redis內建的 Lua 解釋器,可以實現在 Redis 中執行 Lua 腳本。

使用 Lua 腳本的好處 :

  • #減少網路開銷。將多個請求透過腳本的形式一次發送,減少網路延遲。
  • 原子運算。 Redis會將整個腳本作為一個整體執行,中間不會被其他指令插入。
  • 復用。客戶端發送的腳本會永久存在 Redis 中,其他客戶端可以重複使用此腳本而不需要使用程式碼完成相同的邏輯。

Redis Lua 腳本常用指令:

1EVAL script numkeys key [key ...] arg [arg ...] 執行Lua 腳本。 2EVALSHA sha1 numkeys key [key ...] arg [arg ...] 執行 Lua 腳本。 3
#序號#指令及描述
###SCRIPT EXISTS script [script ...] 查看指定的腳本是否已經儲存在快取當中。 ############4######SCRIPT FLUSH 從腳本快取中移除所有腳本。 ############5######SCRIPT KILL 殺死目前正在執行的 Lua 腳本。 ############6######SCRIPT LOAD script 將腳本 script 新增到腳本快取中,但不會立即執行這個腳本。 ############

3.2 EVAL 命令

命令格式:

1
 EVAL script numkeys key [key ...] arg [arg ...]
登入後複製

说明:

  • script是第一个参数,为 Lua 5.1脚本;
  • 第二个参数numkeys指定后续参数有几个 key;
  • key [key ...],是要操作的键,可以指定多个,在 Lua 脚本中通过KEYS[1], KEYS[2]获取;
  • arg [arg ...],参数,在 Lua 脚本中通过ARGV[1], ARGV[2]获取。

简单实例:

1
 redis> eval "return ARGV[1]" 0 100 
2
 "100"
3
 redis> eval "return {ARGV[1],ARGV[2]}" 0 100 101
4
 1) "100"
5
 2) "101"
6
 redis> eval "return {KEYS[1],KEYS[2],ARGV[1]}" 2 key1 key2 first second
7
 1) "key1"
8
 2) "key2"
9
 3) "first"
10
 4) "second"
登入後複製

下面演示下 Lua 如何调用 Redis 命令 ,通过redis.call()来执行了 Redis 命令 。

1
 redis> set mystring &#39;hello world&#39;
2
 OK
3
 redis> get mystring
4
 "hello world"
5
 redis> EVAL "return redis.call(&#39;GET&#39;,KEYS[1])" 1 mystring
6
 "hello world"
7
 redis> EVAL "return redis.call(&#39;GET&#39;,&#39;mystring&#39;)" 0
8
 "hello world"
登入後複製

3.3 EVALSHA 命令

使用 EVAL 命令每次请求都需要传输 Lua 脚本 ,若 Lua 脚本过长,不仅会消耗网络带宽,而且也会对 Redis 的性能造成一定的影响。

思路是先将 Lua 脚本先缓存起来 , 返回给客户端 Lua 脚本的 sha1 摘要。 客户端存储脚本的 sha1 摘要 ,每次请求执行 EVALSHA 命令即可。

<聊聊Redis中的事務:事務模式、Lua腳本 src="https://聊聊Redis中的事務:事務模式、Lua腳本.php.cn/upload/article/000/000/024/f3c746beb29f4037c0bdd3d9780ec302-4.png" alt="聊聊Redis中的事務:事務模式、Lua腳本" loading="lazy"/>

EVALSHA 命令基本语法如下:

1
 redis> EVALSHA sha1 numkeys key [key ...] arg [arg ...]
登入後複製

实例如下:

1
 redis> SCRIPT LOAD "return &#39;hello world&#39;"
2
 "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
3
 redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
4
 "hello world"
登入後複製

4 事务 VS Lua 脚本

从定义上来说, Redis 中的脚本本身就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快

因为脚本功能是 Redis 2.6 才引入的, 而事务功能则更早之前就存在了, 所以 Redis 才会同时存在两种处理事务的方法。

不过我们并不打算在短时间内就移除事务功能, 因为事务提供了一种即使不使用脚本, 也可以避免竞争条件的方法, 而且事务本身的实现并不复杂。

-- redis.io/

Lua 脚本是另一种形式的事务,他具备一定的原子性,但脚本报错的情况下,事务并不会回滚。Lua 脚本可以保证隔离性,而且可以完美的支持后面的步骤依赖前面步骤的结果

Lua 脚本模式的身影几乎无处不在,比如分布式锁、延迟队列、抢红包等场景。

不过在编写 Lua 脚本时,要注意如下两点:

  1. 为了避免 Redis 阻塞,Lua 脚本业务逻辑不能过于复杂和耗时;
  2. 仔细检查和测试 Lua 脚本 ,因为执行 Lua 脚本具备一定的原子性,不支持回滚。

更多编程相关知识,请访问:编程视频!!

以上是聊聊Redis中的事務:事務模式、Lua腳本的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:juejin.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板