Oracle欄位型別踩坑
事務
事務指的程式中一系列嚴密的邏輯操作,其中包含的操作必須要完成,否則在每個操作中的更改都會被撤銷。
舉個簡單的例子:一群鴨子過河,要麼都過去,要麼都不過去。
事務的特性
- 原子性(Atomicity):操作這些指令時,要麼全部執行成功,要麼全部不執行。只要其中一個指令執行失敗,所有的指令都執行失敗,資料進行回滾,回到執行指令前的資料狀態。
- 一致性(Consistency):事務的執行使資料從一個狀態轉換為另一個狀態,但是對於整個資料的完整性保持穩定。(可理解為:即A賬戶只要減去了100,B賬戶則必定加上了100)
- 隔離性(Isolation):隔離性是當多個使用者併發訪問資料庫時,比如操作同一張表時,資料庫為每一個使用者開啟的事務,不能被其他事務的操作所幹擾,多個併發事務之間要相互隔離。
- 永續性(Durability):當事務正確完成後,它對於資料的改變是永久性的。
Redis事務
Redis事務:把多個redis命令放到佇列,然後一次性的順序執行。並且在執行過程中不會被中斷,執行完所有佇列命令後才執行其他客戶端其他命令。
下面舉個簡單的例子,MULTI 開啟一個事務, 然後將多個命令入隊到事務中, 最後由 EXEC命令觸發事務, 一併執行事務中的所有命令
一個事務從開始到執行經歷三個階段
- 開始事務
- 命令入隊
- 執行事務
開始事務
MULTI 命令的執行標誌著事務的開始,這個命令做的就是, 將客戶端的REDIS_MULTI
選項開啟, 讓客戶端從非事務狀態切換到事務狀態。
命令入隊
當客戶端處於非事務狀態下,所有傳送給服務端的命令會立即被執行。
但客戶端切換到事務狀態後,服務端接受到客戶端的命令不會立即執行,而是把這些命令放到事務佇列裡,然後返回QUEUED
, 表示命令已入隊。
可以由一下流程圖表示:
事務佇列是一個數組, 每個陣列項是都包含三個屬性:
- 要執行的命令(cmd)
- 命令的引數(argv)
- 引數的個數(argc)
以上圖命令為例子,那麼程式將為客戶端建立以下事務佇列:
陣列索引 | cmd | argv | argc |
0 | SET | ["name","xiaoming"] | 2 |
1 | SET | ["age","25"] | 2 |
2 | INCR | ["age"] | 1 |
執行事務
客戶端進入到事務狀態後,客戶端傳送的命令不會直接被執行,而是會放到事務佇列裡。
但並不是所有命令都會放到事務佇列,如EXEC、 DISCARD、 MULTI和 WATCH這四個命令,無視事務狀態,直接被伺服器執行。
如果客戶端正處於事務狀態, 那麼當 EXEC命令執行時, 伺服器根據客戶端所儲存的事務佇列, 以先進先出(FIFO)的方式執行事務佇列中的命令: 最先入隊的命令最先執行, 而最後入隊的命令最後執行。
執行事務中的命令所得的結果會以 FIFO 的順序儲存到一個回覆佇列中。
如上圖,程式將為佇列中的命令建立如下回復佇列:
陣列索引 | 回覆型別 | 回覆內容 |
0 | status code reply | OK |
1 | status code reply | OK |
2 | integer reply | 26 |
當事務佇列裡的所有命令被執行完之後, EXEC命令會將回復佇列作為自己的執行結果返回給客戶端, 客戶端從事務狀態返回到非事務狀態, 至此, 事務執行完畢。
事務過程虛擬碼:
def execute_transaction(): # 建立空白的回覆佇列 reply_queue = [] # 取出事務佇列裡的所有命令、引數和引數數量 for cmd, argv, argc in client.transaction_queue: # 執行命令,並取得命令的返回值 reply = execute_redis_command(cmd, argv, argc) # 將返回值追加到回覆佇列末尾 reply_queue.append(reply) # 清除客戶端的事務狀態 clear_transaction_state(client) # 清空事務佇列 clear_transaction_queue(client) # 將事務的執行結果返回給客戶端 send_reply_to_client(client, reply_queue)
DISCARD、MULTI、WATCH命令
- DISCRAD 命令用於取消一個事務, 它清空客戶端的整個事務佇列, 然後將客戶端從事務狀態調整回非事務狀態, 最後返回字串
OK
給客戶端, 說明事務已被取消。 - MULTI命令開啟一個事務,Redis 的事務是不可巢狀的, 當客戶端已經處於事務狀態, 而客戶端又再向伺服器傳送 MULTI時, 伺服器只是簡單地向客戶端傳送一個錯誤, 然後繼續等待其他命令的入隊。 MULTI命令的傳送不會造成整個事務失敗, 也不會修改事務佇列中已有的資料。
- WATCH命令只能在客戶端進入事務狀態之前執行, 在事務狀態下發送 WATCH命令會引發一個錯誤, 但它不會造成整個事務失敗, 也不會修改事務佇列中已有的資料(和前面處理 MULTI的情況一樣)
帶 WATCH 的事務
WATCH 命令用於在事務開始之前監視任意數量的鍵: 當呼叫 EXEC命令執行事務時, 如果任意一個被監視的鍵已經被其他客戶端修改了, 那麼整個事務不再執行, 直接返回失敗。
如下圖例子:
客戶端A
此時客戶端B修改了name
客戶端A執行事務失敗
watch命令實現
在每個代表資料庫的redis.h/redisDb
結構型別中, 都儲存了一個watched_keys
字典, 字典的鍵是這個資料庫被監視的鍵, 而字典的值則是一個連結串列, 連結串列中儲存了所有監視這個鍵的客戶端。
如下圖:
WATCH命令的作用, 就是將當前客戶端和要監視的鍵在watched_keys
中進行關聯。
舉個例子,如果客戶端client5執行 WATCH key1 key2 時,上圖將變成下面這樣。
通過watched_keys
字典, 如果程式想檢查某個鍵是否被監視, 那麼它只要檢查字典中是否存在這個鍵即可; 如果程式要獲取監視某個鍵的所有客戶端, 那麼只要取出鍵的值(一個連結串列), 然後對連結串列進行遍歷即可。
WATCH 觸發
任何對Redis鍵值的修改操作成功後,multi.c/touchWatchedKey
函式都會被呼叫,它檢查資料庫的watched_keys
字典, 看是否有客戶端在監視已經被命令修改的鍵, 如果有的話, 程式將所有監視這個/這些被修改鍵的客戶端的REDIS_DIRTY_CAS
選項開啟:
當客戶端傳送 EXEC命令、觸發事務執行時, 伺服器會對客戶端的狀態進行檢查:
- 如果客戶端的
REDIS_DIRTY_CAS
選項已經被開啟,那麼說明被客戶端監視的鍵至少有一個已經被修改了,事務的安全性已經被破壞。伺服器會放棄執行這個事務,直接向客戶端返回空回覆,表示事務執行失敗。 - 如果
REDIS_DIRTY_CAS
選項沒有被開啟,那麼說明所有監視鍵都安全,伺服器正式執行事務。
虛擬碼如下
def check_safety_before_execute_trasaction(): if client.state & REDIS_DIRTY_CAS: # 安全性已破壞,清除事務狀態 clear_transaction_state(client) # 清空事務佇列 clear_transaction_queue(client) # 返回空回覆給客戶端 send_empty_reply(client) else: # 安全性完好,執行事務 execute_transaction()
最後,當一個客戶端結束它的事務時,無論事務是成功執行,還是失敗,watched_keys
字典中和這個客戶端相關的資料都會被清除。
Redis事務的ACID性質
todo
參考文獻
https://cloud.tencent.com/developer/article/1133074
http://redisdoc.com/topic/transaction.html
reids設計與實現