偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

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

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

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

一、redis事務(wù)的基本概念

1. redis事務(wù)的基本概念

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

redis的事務(wù)指令有3個(gè)關(guān)鍵字,分別是:

  • multi:開(kāi)啟事務(wù)
  • exec:執(zhí)行事務(wù)
  • discard:取消事務(wù)

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

2. 事務(wù)基礎(chǔ)操作示例

如上所示,事務(wù)本質(zhì)就是開(kāi)啟、入隊(duì)、提交,接下來(lái)我們就來(lái)簡(jiǎn)單演示一下,打開(kāi)客戶(hù)端首先開(kāi)啟事務(wù):

# 開(kāi)啟事務(wù)
127.0.0.1:6379> MULTI
OK

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

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

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

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

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

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

二、詳解redis事務(wù)中的原子性

1. 組隊(duì)時(shí)錯(cuò)誤

redis事務(wù)中的錯(cuò)誤分別以下兩種:

  • 組隊(duì)時(shí)錯(cuò)誤
  • 執(zhí)行命令時(shí)錯(cuò)誤

我們先來(lái)說(shuō)說(shuō)組隊(duì)時(shí)錯(cuò)誤的指令,上文我們已經(jīng)說(shuō)過(guò),redis事務(wù)開(kāi)啟后提交的指令都會(huì)存到隊(duì)列中,這也就意味著在指令提交階段redis是可以感知到語(yǔ)法上的錯(cuò)誤,所以在組隊(duì)時(shí)錯(cuò)誤,redis一旦感知到錯(cuò)誤,這些指令都不會(huì)執(zhí)行:

# 開(kāi)啟事務(wù)
127.0.0.1:6379> MULTI
OK
# 指令入隊(duì)
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.
# 指令并沒(méi)有被執(zhí)行
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379>

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

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

對(duì)應(yīng)的我們給出multi指令的源碼實(shí)現(xiàn)multiCommand,邏輯和筆者說(shuō)明的一致解:

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

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

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

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)不一致則將客戶(hù)端flags標(biāo)識(shí)標(biāo)記上REDIS_DIRTY_EXEC標(biāo)識(shí)當(dāng)前事務(wù)是臟事務(wù)       
        flagTransaction(c);
        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
            c->cmd->name);
        return REDIS_OK;
    }
    //......
}


void flagTransaction(redisClient *c) {
    //如果開(kāi)啟事務(wù)則將flags標(biāo)記上REDIS_DIRTY_EXEC,標(biāo)識(shí)當(dāng)前事務(wù)已臟
    if (c->flags & REDIS_MULTI)
        c->flags |= REDIS_DIRTY_EXEC;
}

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

void execCommand(redisClient *c) {
   //......
    //如果發(fā)現(xiàn)標(biāo)識(shí)標(biāo)記為REDIS_DIRTY_EXEC,則調(diào)用 discardTransaction釋放掉事務(wù)隊(duì)列的指令不執(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;
    }

   
 //......
}

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

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

2. 執(zhí)行時(shí)錯(cuò)誤

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

# 開(kāi)啟事務(wù)
127.0.0.1:6379> MULTI
OK
# 設(shè)置字符串k1 v1
127.0.0.1:6379(TX)> set k1 v1
QUEUED

# 設(shè)置v1進(jìn)行自增,此時(shí)redis無(wú)法感知到這個(gè)異常
127.0.0.1:6379(TX)> INCR k1
QUEUED
# 正常鍵值對(duì)設(shè)置
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事務(wù)中的樂(lè)觀鎖

1. 為什么redis需要事務(wù)

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

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

我們?cè)賮?lái)說(shuō)說(shuō)樂(lè)觀鎖(Optimistic Lock),該鎖的總是樂(lè)觀的認(rèn)為自己操作的數(shù)據(jù)不會(huì)被他人修改,進(jìn)行修改操作時(shí)不會(huì)針對(duì)臨界資源上鎖,而是修改的時(shí)候判斷一下當(dāng)前去數(shù)據(jù)版本號(hào)和修改的數(shù)據(jù)是否一致,通過(guò)比對(duì)版本號(hào)是否一致判斷是否被人修改,只要版本號(hào)一致當(dāng)前線(xiàn)程修改操作就會(huì)生效,redis中的watch關(guān)鍵字和jdk下的JUC包下的原子類(lèi)就是采用這種工作機(jī)制:

2. redis事務(wù)樂(lè)觀鎖使用示例

這里我們就演示一下redis樂(lè)觀鎖的實(shí)現(xiàn),原理比較簡(jiǎn)單,通過(guò)watch指令監(jiān)聽(tīng)事務(wù)操作要操作的一個(gè)或者多個(gè)key值,當(dāng)用戶(hù)提交修改事務(wù)時(shí),watch指令沒(méi)有檢測(cè)到key發(fā)生變化,則提交成功。

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

# 設(shè)置key值
127.0.0.1:6379> set key 10
OK

然后開(kāi)始watch指令監(jiān)聽(tīng)這個(gè)key:

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

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

# 開(kāi)啟事務(wù)
127.0.0.1:6379> MULTI
OK

同理我們?cè)谶@時(shí)候起一個(gè)客戶(hù)端2同樣執(zhí)行watch和multi操作:

# 監(jiān)聽(tīng)key
127.0.0.1:6379> WATCH key
OK
# 開(kāi)啟事務(wù)
127.0.0.1:6379> MULTI
OK

此時(shí)我們回到客戶(hù)端1執(zhí)行修改操作,可以看到因?yàn)閣atch到key沒(méi)有發(fā)生改變,修改操作成功:

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

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

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

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

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

我們客戶(hù)端1提交時(shí),因?yàn)橹皼](méi)有客戶(hù)端進(jìn)行修改,所以成功提交修改操作,并將watched_keys中監(jiān)聽(tīng)key的所有客戶(hù)端的flags標(biāo)識(shí)為已被CAS修改即枚舉變量REDIS_DIRTY_CAS數(shù)值為1<<5。 然后客戶(hù)端2進(jìn)行修改操作時(shí),看到自己的flags被修改為REDIS_DIRTY_CAS就知道了當(dāng)前key被人修改了,所以樂(lè)觀修改操作失敗:

對(duì)應(yīng)源碼如下,當(dāng)客戶(hù)端1執(zhí)行exec時(shí)發(fā)現(xiàn)監(jiān)聽(tīng)的key沒(méi)有被人修改,執(zhí)行incr操作之后,就會(huì)走到下面這個(gè)方法touchWatchedKey將watched_keys中監(jiān)聽(tīng)key的客戶(hù)端標(biāo)識(shí)標(biāo)記為REDIS_DIRTY_CAS,告知當(dāng)前這個(gè)key已被我們修改:

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

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

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

 //......

四、詳解redis事務(wù)的一些常見(jiàn)問(wèn)題

1. 為什么redis不支持事務(wù)回滾

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

2. 如何理解redis的事務(wù)與ACID

(1) 原子性: redis設(shè)計(jì)者認(rèn)為他們是支持原子性的,因?yàn)樵有缘母拍钍?所有指令要么全部執(zhí)行,要么全部不執(zhí)行,只要客戶(hù)端提交的指令能夠在組隊(duì)階段被感知,它就能做到指令操作的原子性。

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

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

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

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

3. Redis事務(wù)的其他實(shí)現(xiàn)方式了解過(guò)嘛?

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

4. Redis事務(wù)三特性是什么?

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

5. 如何使用 Redis 事務(wù)?

Redis 可以通過(guò) MULTI,EXEC,DISCARD 和 WATCH 等命令來(lái)實(shí)現(xiàn)事務(wù)(transaction)功能。

6. 如何解決 Redis 事務(wù)的缺陷?

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

責(zé)任編輯:趙寧寧 來(lái)源: 寫(xiě)代碼的SharkChili
相關(guān)推薦

2024-09-12 17:39:27

2024-04-01 12:33:19

PyCudaGPUPython

2023-10-10 08:52:36

射與分析相開(kāi)源

2021-04-29 08:13:49

Mac 工具軟件

2025-05-19 08:28:00

2015-03-02 16:48:40

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

2019-06-12 16:21:52

時(shí)間序列PythonPandas

2021-08-13 17:26:55

數(shù)字化

2021-06-21 11:05:30

CSS前端代碼

2024-09-09 16:50:21

2024-02-22 10:14:40

Filter函數(shù)Python

2010-06-30 13:49:02

SQL Server數(shù)

2024-08-22 14:30:32

前端開(kāi)發(fā)VS Code

2025-07-03 02:00:00

2024-03-06 15:57:56

ShellLinux

2010-07-07 10:02:46

SQL Server數(shù)

2020-10-22 15:05:43

開(kāi)發(fā)者技能工具

2009-10-14 14:27:44

DataPlatforInformatica數(shù)據(jù)平臺(tái)

2024-12-03 09:28:54

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

2023-07-12 12:02:06

WOT大數(shù)據(jù)流式數(shù)據(jù)湖
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)