自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

掌握 Redis 事務,提升數(shù)據(jù)處理效率的必備秘籍

數(shù)據(jù)庫 Redis
本文從 Redis 事務和底層源碼兩個角度深入分析了其工作機制和使用注意事項,希望對你有幫助。

在實際的軟件開發(fā)項目中,我們經(jīng)常會遇到需要對數(shù)據(jù)進行一系列連續(xù)操作的情況,而且這些操作必須作為一個整體要么全部成功,要么全部失敗,以保證數(shù)據(jù)的一致性。比如在電商系統(tǒng)中,下單、扣庫存、記錄訂單信息等操作需要作為一個不可分割的整體來執(zhí)行。

Redis作為一款常用的數(shù)據(jù)庫,其事務功能就為解決這類問題提供了有力的支持。那么,如何在項目中正確、高效地使用Redis事務呢?

一、redis事務的基本概念

1. redis事務的基本概念

redis的事務是一個單獨隔離的操作,它會將一系列指令按需排隊并順序執(zhí)行,期間不會被其他客戶端的指令插隊,所以redis事務是保證組合命令的原子性。

redis的事務指令有3個關鍵字,分別是:

  • multi:開啟事務
  • exec:執(zhí)行事務
  • discard:取消事務

通過multi,當前客戶端就會開啟事務,后續(xù)用戶鍵入的都指令都會保證到隊列中暫不執(zhí)行,當用戶鍵入exec后,這些指令都會按順序執(zhí)行。 需要注意的是,若開啟multi后輸入若干指令,客戶端輸入discard,則之前的指令通通取消執(zhí)行。

2. 事務基礎操作示例

如上所示,事務本質(zhì)就是開啟、入隊、提交,接下來我們就來簡單演示一下,打開客戶端首先開啟事務:

# 開啟事務
127.0.0.1:6379> MULTI
OK

然后將需要執(zhí)行的操作提交:

# 將兩個指令組隊
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED

完成后,我們就可以通過exec指令提交并執(zhí)行:

# 執(zhí)行兩個指令
127.0.0.1:6379(TX)> EXEC
1) OK
2) OK

最后查看執(zhí)行驗證一下結(jié)果:

# 查看執(zhí)行結(jié)果
127.0.0.1:6379> keys *
1) "k1"
2) "k2"

二、詳解redis事務中的原子性

1. 組隊時錯誤

redis事務中的錯誤分別以下兩種:

  • 組隊時錯誤
  • 執(zhí)行命令時錯誤

我們先來說說組隊時錯誤的指令,上文我們已經(jīng)說過,redis事務開啟后提交的指令都會存到隊列中,這也就意味著在指令提交階段redis是可以感知到語法上的錯誤,所以在組隊時錯誤,redis一旦感知到錯誤,這些指令都不會執(zhí)行:

# 開啟事務
127.0.0.1:6379> MULTI
OK
# 指令入隊
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k33
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
# 執(zhí)行指令
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
# 指令并沒有被執(zhí)行
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379>

這一點我們也可以從源碼的角度分析,redis會為每一個redis客戶端分配一個結(jié)構體維護其內(nèi)部信息,這其中flag字段就代表著客戶端各種狀態(tài)標識,這其中低3位就表示客戶端是否開啟事務標識,如果1就代表開啟,反之代表未開啟:

我們都知道redis開啟事務需要multi指令,客戶端鍵入該指令之后,redis首先就會通過按位與判斷這個二進制為是否被標識為1,如果是則說明已經(jīng)開啟事務,直接拋出嵌套事務異常告知客戶端不可重復調(diào)用multi指令,反之通過或運算將其設置為1:

對應的我們給出multi指令的源碼實現(xiàn)multiCommand,邏輯和筆者說明的一致解:

void multiCommand(redisClient *c) {
    //REDIS_MULTI值為1<<3 如果按位與發(fā)現(xiàn)當前客戶端已經(jīng)被標識為開啟事務,則直接跑錯事務不可嵌套的異常
    if (c->flags & REDIS_MULTI) {
        addReplyError(c,"MULTI calls can not be nested");
        return;
    }
    //REDIS_MULTI值為1<<3 通過 | 符號將低3位標識為1,意為開啟事務
    c->flags |= REDIS_MULTI;
    addReply(c,shared.ok);
}

后續(xù)用戶的指令提交處理都會走到公用處理函數(shù)processCommand,一旦感知到某條指令處理異常,redis就會將客戶端標識flag標記為臟事務REDIS_DIRTY_EXEC,后續(xù)指令提交時如果發(fā)現(xiàn)這個標志位為1:

對應我們給出所有指令提交前的通用邏輯函數(shù)processCommand,可以看到如果服務端感知到指令的指令參數(shù)不一致等異常就會調(diào)用flagTransaction將事務標記為臟:

int processCommand(redisClient *c) {
    //.......
    c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
    if (!c->cmd) {
     //......
    } else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||
               (c->argc < -c->cmd->arity)) {//檢查參數(shù)數(shù)和命令表配置是否一致
        //如果發(fā)現(xiàn)不一致則將客戶端flags標識標記上REDIS_DIRTY_EXEC標識當前事務是臟事務       
        flagTransaction(c);
        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
            c->cmd->name);
        return REDIS_OK;
    }
    //......
}


void flagTransaction(redisClient *c) {
    //如果開啟事務則將flags標記上REDIS_DIRTY_EXEC,標識當前事務已臟
    if (c->flags & REDIS_MULTI)
        c->flags |= REDIS_DIRTY_EXEC;
}

有了上述的基礎,我們執(zhí)行的exec就會通過判斷flags查看是否被標記為REDIS_DIRTY_EXEC ,如果是則調(diào)用discardTransaction也就是discard清除隊列中的指令不執(zhí)行:

void execCommand(redisClient *c) {
   //......
    //如果發(fā)現(xiàn)標識標記為REDIS_DIRTY_EXEC,則調(diào)用 discardTransaction釋放掉事務隊列的指令不執(zhí)行
    if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) {
        addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr :
                                                  shared.nullmultibulk);
        discardTransaction(c);
        goto handle_monitor;
    }

   
 //......
}

來小結(jié)一下,redis組隊時異?;貪L的底層實現(xiàn):

  • multi開啟事務
  • 提交指令,如果發(fā)現(xiàn)指令異常則將當前客戶端事務標記為臟事務
  • 調(diào)用exec時判斷客戶端標識,如果包含臟標記則清除事務隊列中的指令不執(zhí)行

2. 執(zhí)行時錯誤

有了上述基礎我們就很好理解執(zhí)行時錯誤了,執(zhí)行時錯誤比較特殊,他在按序處理所有指令,即時遇到錯誤就按正常流程處理繼續(xù)執(zhí)行下去,如下示例所示,可以看到我們將k1對應的value是字符串類型,第二條指令執(zhí)行錯誤后,k2還是正常設置進去了:

# 開啟事務
127.0.0.1:6379> MULTI
OK
# 設置字符串k1 v1
127.0.0.1:6379(TX)> set k1 v1
QUEUED

# 設置v1進行自增,此時redis無法感知到這個異常
127.0.0.1:6379(TX)> INCR k1
QUEUED
# 正常鍵值對設置
127.0.0.1:6379(TX)> set k2 v2
QUEUED
# 提交執(zhí)行,1、3指令執(zhí)行成功
127.0.0.1:6379(TX)> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
# 即使指令2失敗,指令3還是正常提交
127.0.0.1:6379> keys *
1) "k1"
2) "k2"
127.0.0.1:6379>

三、詳解redis事務中的樂觀鎖

1. 為什么redis需要事務

通過redis事務解決需要高性能且需要保證原子性的符合指令操作,最經(jīng)典的就是秒殺場景,如下圖,假設一個秒殺活動中有3個用戶,同時通過get指令發(fā)現(xiàn)庫存剩下1,全部通過原子扣減指令進行扣減,導致超賣:

常見的解決方案有悲觀鎖和樂觀鎖,悲觀鎖(Pessimistic Lock)的原理是認為自己操作的數(shù)據(jù)很可能會被他人修改,所以對臨界資源操作都持有悲觀的態(tài)度,每次進行操作前都會對數(shù)據(jù)上鎖保證互斥,常見的關系型數(shù)據(jù)庫MySQL的行鎖、表鎖等都是基于這種鎖機制:

我們再來說說樂觀鎖(Optimistic Lock),該鎖的總是樂觀的認為自己操作的數(shù)據(jù)不會被他人修改,進行修改操作時不會針對臨界資源上鎖,而是修改的時候判斷一下當前去數(shù)據(jù)版本號和修改的數(shù)據(jù)是否一致,通過比對版本號是否一致判斷是否被人修改,只要版本號一致當前線程修改操作就會生效,redis中的watch關鍵字和jdk下的JUC包下的原子類就是采用這種工作機制:

2. redis事務樂觀鎖使用示例

這里我們就演示一下redis樂觀鎖的實現(xiàn),原理比較簡單,通過watch指令監(jiān)聽事務操作要操作的一個或者多個key值,當用戶提交修改事務時,watch指令沒有檢測到key發(fā)生變化,則提交成功。

為方便演示,我們假設需要用事務操作名稱為key的數(shù)據(jù),我們首先初始化一下這個鍵值對:

# 設置key值
127.0.0.1:6379> set key 10
OK

然后開始watch指令監(jiān)聽這個key:

# 監(jiān)聽key
127.0.0.1:6379> WATCH key
OK

此時我們就可以開啟事務提交要執(zhí)行的操作:

# 開啟事務
127.0.0.1:6379> MULTI
OK

同理我們在這時候起一個客戶端2同樣執(zhí)行watch和multi操作:

# 監(jiān)聽key
127.0.0.1:6379> WATCH key
OK
# 開啟事務
127.0.0.1:6379> MULTI
OK

此時我們回到客戶端1執(zhí)行修改操作,可以看到因為watch到key沒有發(fā)生改變,修改操作成功:

# 指令加入隊列
127.0.0.1:6379(TX)> INCR key
QUEUED

# 執(zhí)行指令,可以看到執(zhí)行成功,修改了一條數(shù)據(jù),值被更新為11
127.0.0.1:6379(TX)> EXEC
1) (integer) 11

此時我們回到客戶端2提交指令并提交,可以看到提交結(jié)果失敗了,返回nil:

127.0.0.1:6379(TX)> INCR key
QUEUED
127.0.0.1:6379(TX)> exec
(nil)

這里我們也從源碼的角度解釋一下redis對于watch樂觀鎖的實現(xiàn),如上操作,當我們客戶端鍵入watch指令時監(jiān)控key時,redis就會將當前客戶端的信息掛到一個watched_keys的字典中,用key作為鍵,客戶端信息作為value追加到這個key的鏈表中。

我們客戶端1提交時,因為之前沒有客戶端進行修改,所以成功提交修改操作,并將watched_keys中監(jiān)聽key的所有客戶端的flags標識為已被CAS修改即枚舉變量REDIS_DIRTY_CAS數(shù)值為1<<5。 然后客戶端2進行修改操作時,看到自己的flags被修改為REDIS_DIRTY_CAS就知道了當前key被人修改了,所以樂觀修改操作失?。?/p>

對應源碼如下,當客戶端1執(zhí)行exec時發(fā)現(xiàn)監(jiān)聽的key沒有被人修改,執(zhí)行incr操作之后,就會走到下面這個方法touchWatchedKey將watched_keys中監(jiān)聽key的客戶端標識標記為REDIS_DIRTY_CAS,告知當前這個key已被我們修改:

void touchWatchedKey(redisDb *db, robj *key) {
   //......
    //從watched_keys找到監(jiān)聽當前key的所有客戶端
    clients = dictFetchValue(db->watched_keys, key);
   //......
    //遍歷訂閱這個key的所有客戶端
    listRewind(clients,&li);
    while((ln = listNext(&li))) {
        redisClient *c = listNodeValue(ln);
        //標識為REDIS_DIRTY_CAS
        c->flags |= REDIS_DIRTY_CAS;
    }
}

所以當客戶端2的執(zhí)行exec時,調(diào)用來到了execCommand,當他發(fā)現(xiàn)自己的標識即flags字段被客戶端1標記為REDIS_DIRTY_CAS,就知道當前key被人修改了,于是就執(zhí)行discard取消執(zhí)行當前指令:

void execCommand(redisClient *c) {
  
 //......
    //如果發(fā)現(xiàn)標識標記為REDIS_DIRTY_EXEC或REDIS_DIRTY_CAS(當前watch的key被人修改),則調(diào)用 discardTransaction釋放掉事務隊列的指令不執(zhí)行
    if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) {
        addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr :
        //執(zhí)行discard操作清除當前客戶端提交的執(zhí)行,且不執(zhí)行                                         shared.nullmultibulk);
        discardTransaction(c);
        goto handle_monitor;
    }

 //......

四、詳解redis事務的一些常見問題

1. 為什么redis不支持事務回滾

redis實際上是支持事務回滾的,只不過這種回滾是僅僅支持組隊時的異常,只有組隊時感知到指令錯誤,redis服務端才會標記異常,后續(xù)執(zhí)行exec時就會將提交隊列的指令清除且不執(zhí)行,由此原子性,對應的我們也有在上面的源碼給出解釋說明。

2. 如何理解redis的事務與ACID

(1) 原子性: redis設計者認為他們是支持原子性的,因為原子性的概念是:所有指令要么全部執(zhí)行,要么全部不執(zhí)行,只要客戶端提交的指令能夠在組隊階段被感知,它就能做到指令操作的原子性。

(2) 一致性: 針對數(shù)據(jù)的一致性,我們從3種情況進行討論:

  • 組隊階段:如果在事務組隊階段感知到異常,redis會主動事務中的指令且不執(zhí)行,可以保證一致性。
  • 執(zhí)行時異常:在事務執(zhí)行階段出現(xiàn)異常,redis還是會順序執(zhí)行后續(xù)的指令,一致性就會被破壞
  • 事務提交前redis宕機:如果開啟了rdb或者aof持久化機制,可以在服務重啟時重新加載提交到隊列中的數(shù)據(jù),保證一致性。

(3) 隔離性: 隔離性要求避免所有的客戶端事務操作并發(fā)交叉執(zhí)行時導致數(shù)據(jù)不一致問題,如上樂觀鎖的說明,我們可以通過watch關鍵字監(jiān)聽key的變化保證事務提交時感知到其他客戶端的修改,如果發(fā)生修改就不提交事務,由此避免隔離性遭到破壞。

(4) 持久性: 持久性的定義為事務處理結(jié)束后,對數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會丟失。),考慮到性能問題,redis無論rdb還是aof都是異步持久化,所以并不能保證持久性。

3. Redis事務的其他實現(xiàn)方式了解過嘛?

基于lua腳本可以保證redis指令一次性執(zhí)按順序執(zhí)行完成,并且不會被其他客戶端打斷,但是這種方式卻無法實現(xiàn)事務回滾,所以我們可以需要在lua腳本的實現(xiàn)上進行響應的處理。

4. Redis事務三特性是什么?

  • 單獨的隔離操作:事務中的命令都會序列化并且按序執(zhí)行,執(zhí)行過程中不會被其他客戶端的指令打斷。
  • 沒有隔離級別的概念:事務提交前所有指令都不會被執(zhí)行。
  • 無原子性:上文示例已經(jīng)演示過,執(zhí)行時出錯某段指令,事務過程中的指令仍然會生效。

5. 如何使用 Redis 事務?

Redis 可以通過 MULTI,EXEC,DISCARD 和 WATCH 等命令來實現(xiàn)事務(transaction)功能。

6. 如何解決 Redis 事務的缺陷?

從上文我們看出基于redis事務進行秒殺方面的需求時會出現(xiàn)庫存遺留問題,這就是redis事務樂觀鎖機制的缺陷。 為了保證所有事務都能一次性的執(zhí)行,我們可以使用lua腳本更快(lua腳本可以輕易調(diào)用C語言庫函數(shù)以及被C語言直接調(diào)用)、更有效(基于lua腳本可以保證指令一次性被執(zhí)行不會被其他線程打斷),但是這種方案不支持回滾。

責任編輯:趙寧寧 來源: 寫代碼的SharkChili
相關推薦

2024-09-12 17:39:27

2024-04-01 12:33:19

PyCudaGPUPython

2023-10-10 08:52:36

射與分析相開源

2021-04-29 08:13:49

Mac 工具軟件

2015-03-02 16:48:40

數(shù)據(jù)處理大數(shù)據(jù)原則

2019-06-12 16:21:52

時間序列PythonPandas

2021-08-13 17:26:55

數(shù)字化

2024-02-22 10:14:40

Filter函數(shù)Python

2010-06-30 13:49:02

SQL Server數(shù)

2021-06-21 11:05:30

CSS前端代碼

2024-09-09 16:50:21

2024-08-22 14:30:32

前端開發(fā)VS Code

2010-07-07 10:02:46

SQL Server數(shù)

2024-03-06 15:57:56

ShellLinux

2009-10-14 14:27:44

DataPlatforInformatica數(shù)據(jù)平臺

2024-12-03 09:28:54

元組數(shù)據(jù)庫

2020-10-22 15:05:43

開發(fā)者技能工具

2013-10-29 10:36:34

2023-07-12 12:02:06

WOT大數(shù)據(jù)流式數(shù)據(jù)湖

2020-09-24 06:49:34

PythonRedis
點贊
收藏

51CTO技術棧公眾號