Redis-事務篇

同系列一:Redis 緩存數據庫入門教程
同系列二:Redis-通用指令篇
同系列三:Redis-RDB-AOF持久化篇

Redis-事務篇

Redis簡介

Redis是C語言開發的一個高性能鍵值對(key -value) 內存數據庫,可以用作數據庫,緩存和消息中間件等。

特點

  1. 作爲內存數據庫,它的性能非常優秀,數據存儲在內存當中,讀寫速度非常快,支持併發10W QPS(每秒查詢次數),單進程單線程,是線程安全的,採用IO多路複用機制。

  2. 豐富的數據類型,支持字符串,散列,列表,集合,有序集合等,支持數據持久化。可以將內存中數據保存在磁盤中,重啓時加載。

  3. 主從複製,哨兵,高可用,可用作分佈式鎖。可以作爲消息中間件使用,支持發佈訂閱。

什麼是事務(Transaction)?

事務,一般是指要做的或者所做的事情,在計算機中是指訪問並可能更新數據庫中各種數據項的一個程序執行單元,它包含了一組數據操作指令,並且所有的指令都作爲一個整體一起向系統提交或撤銷操作(要麼都執行,要麼都不執行)事務是恢復和併發控制的基本單位。

begin transaction;
    update account set money = money-100 where name = '張三';
    update account set money = money+100 where name = '李四';
commit transaction;

特徵

關係型數據庫事務具有四大特性:原子性、一致性、隔離性、持久性

簡稱:ACID屬性

  • 原子性:事務是一個完整的操作。事務中的各部操作是不可分割的(原子的),要麼都執行,要麼都不執行

  • 一致性:事務必須是使數據庫從一個一致性狀態變到另一個一致性狀態,當事務完成時,數據必須是一致狀態

  • 隔離性:對數據操作的所有併發事務都是相互隔離的,事務必須是獨立,不依賴或影響其他事務

  • 永久性:事務一旦提交完成後,它對數據庫的修改被永久保持,事務日誌能夠保持事務的永久性,其他操作不應該對其執行結果有任何影響


Redis中的事務?

Redis中的事務實際上是一組命令的集合。它的事務支持一次執行多個命令,一個事務中所有命令都會被序列化。將一系列預定義命令打包成一個整體(隊列),當執行時,一次性按照添加順序依次執行,中間不會被打斷或者干擾。在事務指向期間,服務端不會中斷事務而改去執行其他客戶端的命令請求,將事務中的所有命令執行完畢纔會去處理其他客戶端請求。

Redis事務的作用就是在一個隊列中,一次性,順序性,排他性的執行一系列命令

事務的基本操作

在 Redis 中使用事務會經過 3 個過程

  • 開啓事務multi 執行該命令表示一個事務快的開始,在開啓完事務的時候,每次操作的命令將會被插入到一個隊列中並返回QUEUED,同時這個隊列中的命令在事務沒有被提交之前不會被實際執行 –>沒有隔離級別的概念,但是總歸是具有隔離性,畢竟不會受到別的命令打斷
    例:
    在這裏插入圖片描述事務隊列結構:
    在這裏插入圖片描述

  • 執行事務exec 執行該命令後,redis會執行事務塊裏面的所有命令,該命令需要和 multi 命令成對使用,事務不保證原子性且沒有回滾,任意命令執行失敗還是會接着往下執行 –>不保證原子性
    例:
    在這裏插入圖片描述
    執行事務的流程:
    在這裏插入圖片描述

  • 取消事務discard 執行命令後,放棄執行該事務的所有命令,取消該事務,該命令需要和 multi 命令成對使用
    在這裏插入圖片描述
    總體執行流程:
    在這裏插入圖片描述


事務的工作流程

如圖:
在這裏插入圖片描述
當客戶端在給服務器發送一個 X 指令後,服務器大概會進行這麼一個操作

判斷我們當前是否存在一個事務的狀態,如果當前不是有事務狀態,則去識別判斷客戶端發送過來的 X 是不是一個關於事務操作的指令,如果這個 X 是個普通的指令,那麼服務端就直接執行了 X ,最後返回執行結果。

如果這個X是一個事務操作的則有三種情況:

  1. X 爲 multi 指令,那麼給它開啓一個事務隊列,返回一個OK給客戶端
  2. X 爲 discard 指令,由於我們還沒有開啓事務狀態,所以會直接報錯:(error) ERR DISCARD without MULTI
  3. X爲 exec 指令,由於我們還沒有開啓事務狀態,也會報錯:(error) ERR EXEC without MULTI

如果當前有事務狀態,則去識別判斷客戶端發送過來的X是不是一個關於事務操作的指令,如果這個X是個普通的指令,那麼服務端就將 X 指令加到事務隊列中去,最後返回 QUEUED問題:如果在中途輸入了一個語法有誤的命令,會有什麼結果?

如果這個 X 是一個事務操作的則又有三種情況:

  1. X 爲 multi 指令,那麼它本身已經處於事務狀態了,還來一個 multi 那麼就會報錯了,Redis事務禁止套娃:(error) ERR MULTI calls can not be nestedMULTI命令的發送不會造成整個事務失敗,也不會修改事務隊列中已有的數據
  2. X 爲 discard 指令,那麼會將當前事務隊列銷燬,隊列中所有命令都不執行,然後返回 OK
  3. X 爲 exec 指令,接下來會進行執行隊列中的命令,返回結果。這裏會產生一個問題,當我們輸入一條語法正確的命令,但是運行時實際會出現異常的命令,如:set cjcc value,接下來跟着用incr cjcc,會產生什麼後果?

事務操作注意事項

定義事務過程中,命令格式輸入錯誤

在Redis中,如果所定義的事務包含命令出現了語法錯誤(後面命令照常可以入隊),那麼這個事務中的所有命令都不會執行,包括正確的命令

127.0.0.1:6379> multi   開啓了一個事務
OK
127.0.0.1:6379> set key 1  添加正確的命令到事務隊列
QUEUED
127.0.0.1:6379> sett s     添加錯誤的命令到事務隊列
(error) ERR unknown command 'sett'
127.0.0.1:6379> set k k    添加正確的命令到事務隊列,發現還能加到隊列
QUEUED
127.0.0.1:6379> exec       嘗試執行事務,發現報錯了,因爲上面出現了一條編譯錯誤的命令,而放棄了當前事務
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379>

例:
在這裏插入圖片描述結果:
在這裏插入圖片描述


定義事務過程中,命令執行錯誤

在Redis中,如果所定義的事務隊列命令語法正確,但是無法正確的執行(運行時異常,如Java的除0異常之類。。。。,在這Redis裏就是相當於對一個字符串進行自增)那麼運行錯誤的命令不會被執行,其他正確的命令都會被執行

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set cjcc value    添加正確的命令到事務隊列
QUEUED
127.0.0.1:6379> incr cjcc         添加正確的命令到事務隊列,自增字符串運行會出錯
QUEUED
127.0.0.1:6379> set cjcc1 value1  添加正確的命令到事務隊列
QUEUED
127.0.0.1:6379> exec
1) OK       執行成功
2) (error) ERR value is not an integer or out of range
3) OK       執行成功
127.0.0.1:6379>

在這裏插入圖片描述

注意:

單個 Redis 命令的執行是原子性的,但 Redis 沒有在事務上增加任何維持原子性的機制,所以 Redis 事務的執行並不是原子性的,所以也不具有一致性

redis的事務可以理解爲一個打包的批量執行腳本,但批量指令並非原子化的操作,中間某條指令的失敗不會導致前面已做指令的回滾,也不會造成後續的指令不做(前面有圖)。redis提供了一個簡單的事務,這也是和傳統關係型數據庫的區別,但凡報錯,整個事務必回滾


事務回滾

手動進行事務回滾

記錄操作過程中被影響的數據之前的狀態,把他存儲起來,對於單個的對於string這種,存儲前先獲取值存起來,對於hash,list,set,zset 先拷貝一份副本,萬一出問題了用將整個副本恢復回去。

對於一些重要的操作,我們必須通過程序去檢測數據的正確性,以保證 Redis 事務的正確執行,避免出現數據不一致的情況。Redis 之所以保持這樣簡易的事務,完全是爲了保證移動互聯網的核心問題——性能。

Redis——鎖

watch 命令

watch key1 [key2……] 

WATCH 是一個樂觀鎖(CAS(Compare And Swap)CAS會出現ABA問題)。命令用於監視一個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那麼事務將被拒絕執行,並向客戶端返回代表事務執行失敗的空回覆,其實就是大家都監控同一個或幾個點,我想操作什麼東西的時候,只要大家都沒有動它,那麼我就會進行操作,如果發現有人動了即會被監控發現觸發了這個條件,那我就取消這次事務操作

在這裏插入圖片描述

每個redis數據庫都保存着一個 watched_keys 字典,這個字典的鍵是某個被 watch 命令監視的數據庫鍵,而字典的值則是一個鏈表,鏈表中記錄了所有監視相應數據庫鍵的客戶端,數據結構如下圖:

c1…c2代表有c1和c2兩個客戶端在監控name這個key
在這裏插入圖片描述
所有對數據庫進行修改命令,在執行之後都會對 watched_keys 字典進行檢查,查看是否有客戶端正在監視剛剛被命令修改過的數據庫鍵,如果有,則會將監視被修改鍵的 reids_dirty_cas 標識打開,
表示該客戶端的事務安全性已經被破壞.如果在事務提交時,檢測到該標識被打開,則會拒絕執行它們提交的事務,以此來保證事務的安全性.
在這裏插入圖片描述
WATCH 只能在客戶端進入事務狀態之前執行,在事務狀態下發送 WATCH 命令會引發一個錯誤,但它不會造成整個事務失敗,也不會修改事務隊列中已有的數據(和前面處理 MULTI 的情況一樣

那麼 watch 會不會出現ABA問題?

ABA問題:一個線程把數據A變爲了B,然後又重新變成了A。此時另外一個線程讀取的時候,發現A沒有變化,就誤以爲是原來的那個A

代碼位置:multi.c >> void touchWatchedKey(redisDb *db, robj *key)

/* "Touch" a key, so that if this key is being WATCHed by some client the
 * next EXEC will fail. */
void touchWatchedKey(redisDb *db, robj *key) {
    list *clients;
    listIter li;
    listNode *ln;

    if (dictSize(db->watched_keys) == 0) return;
    clients = dictFetchValue(db->watched_keys, key);
    if (!clients) return;

    /* Mark all the clients watching this key as CLIENT_DIRTY_CAS */
    /* Check if we are already watching for this key */
    listRewind(clients,&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);

        c->flags |= CLIENT_DIRTY_CAS;
    }
}

每當key的內容被修改時,則遍歷所有watch了該 key 的客戶端,設置相應的狀態爲CLIENT_DIRTY_CAS,所以不會出現 ABA 問題

看圖:
在這裏插入圖片描述


unwatch 命令

unwatch  //取消對所有key的監控

如果在執行 WATCH 命令之後, EXEC 命令或 DISCARD 命令先被執行了的話,那麼就不需要再執行 UNWATCH 。因爲 EXEC 命令會執行事務,因此 WATCH 命令的效果已經產生了;而 DISCARD 命令在取消事務的同時也會取消所有對 key 的監視,因此這兩個命令執行之後,就沒有必要執行 UNWATCH

分佈式鎖

setnx(set if not exists)實現

命令介紹

set if not exists 如果不存在,則 SET

  • 使用setnx 設置一個公共鎖
    • 有值則返回設置失敗: 0
    • 無值則返回設置成功: 1
setnx lock-key value

在這裏插入圖片描述
利用分佈式鎖控制商城超賣場景的問題: 電商618活動熱賣商品 X ,非常多客戶搶購,3S內將所有商品購買完畢,如何防止最後一件商品被多人同時購買。

這時候,使用上面的watch監控一個庫存的key,還能不能解決問題?

不能,這個key會一直變化,如從100一點點的減到了0 ,這樣的話,做了一次,剩下的事務全部被放棄了,一個人訂購了一個個X商品,其他人訂購的就全部被取消掉,這是不現實的。畢竟庫存都還沒一定到最後一件,因爲watch 主要是監控某一個 key 值,有沒有被其他客戶端改變過,而不是控制其他客戶端能不能修改這個值,所以這個時候用 watch 就不合適了,所以需要引入新的方法 setnx

watch setnx
key 有沒被別人改過 key 能不能被別人改

用法:利用setnx 命令的返回值特徵

  • 對於返回設置成功的,擁有控制權,進行下一步的具體業務操作,如:incr cjcc
  • 對於返回設置失敗的,沒有控制權,進行排隊或者等待操作完畢
  • 操作完成後,通過del命令操作釋放鎖

在這裏插入圖片描述


改良1 setnx + expire

需要注意的是: 當我們用了上述方案設計了簡單的分佈式鎖,已經可以實現控制客戶端能不能具體控制對應的某個業務了,問題來了,當我們設置鎖了之後,客戶端掛了/停電了(),但是它偏偏已經獲得鎖了,沒來得及打開,這樣就容易產生了死鎖的風險。所以我們要有一個保底機制,當用戶控制加鎖後,不能僅僅由用戶進行解鎖,我們在系統層面也需要做到能控制鎖的釋放,所以改進方案如下:使用 expire 命令爲鎖定的key添加一個時間限定,到時釋放鎖
在這裏插入圖片描述
命令

expire key second
pexpire key milliseconds

於是現在命令變成了:
setnx cjcc 1
expire cjcc 10 //設置cjcc值爲1,cjcc過期時間爲10秒

在這裏插入圖片描述


改良2 set擴展參數

但是,上面改良的方案還有一種可能,這種解決方案終歸還是2條命令組成的,萬一在設置完成值後,還沒來得及設置expire過期時間,系統就已經掛掉了。鎖未釋放,也會引發死鎖問題,新的解決方案:
set key value NX [EX seconds] [PX milliseconds]
在這裏插入圖片描述

set參考: SET 命令的行爲可以通過一系列參數來修改by http://redisdoc.com/string/set.html

EX seconds : 將鍵的過期時間設置爲 seconds 秒。 執行 SET key value EX seconds 的效果等同於執行 SETEX key seconds value 。

PX milliseconds : 將鍵的過期時間設置爲 milliseconds 毫秒。 執行 SET key value PX milliseconds 的效果等同於執行 PSETEX key milliseconds value 。

NX : 只在鍵不存在時, 纔對鍵進行設置操作。 執行 SET key value NX 的效果等同於執行 SETNX key value 。

XX : 只在鍵已經存在時, 纔對鍵進行設置操作。

因爲 SET 命令可以通過參數來實現 SETNXSETEX 以及 PSETEX 命令的效果, 所以 Redis 將來的版本可能會移除並廢棄 SETNXSETEXPSETEX 這三個命令。

鎖過期時間設置

上面設置的鎖的時間都是爲了方便測試,截圖,要是生產上真設置幾秒一次,那就真的玩蛇了。
一般操作通常都是微秒或者毫秒級,所以鎖定時間不合適設置太大了,至於具體時間,看自己業務測試來確定區間【持有鎖的最短執行時間,最長執行時間】以及網絡請求耗時之類的來定。

其他文章

同系列一:Redis 緩存數據庫入門教程
同系列二:Redis-通用指令篇
同系列三:Redis-RDB-AOF持久化篇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章