PHP 如何在Redis中实现事物(事物提交和事物回滚)
講了這么多Redis的使用,今天我們來講下Redis的事物
1.首先,我們來看一下Redis中事物相關的指令,
命令原型 ? ? ? 命令描述
MULTI ? 用于標記事務的開始,其后執行的命令都將被存入命令隊列,直到執行EXEC時,這些命令才會被原子執行.
EXEC ? 執行在一個事務內命令執行了WATCH命令,那么只有當WATCH所監控的keys沒有被修改的前提下,EXEC命令才能執行事務隊列中的所有命令,那么只有
當WATCH所監控的keys沒有被修改的前提下,EXEC命令才能執行事務隊列中的所有命令,否則EXEC將放棄當前事務中的所有命令。
DISCARD ? 回滾事務隊列中的所有命令,同時再將當前連接的狀態恢復為正常狀態,即非事務狀態。如 果WATCH命令被使用,該命令將UNWATCH所有的keys.
WATCH ?key[key...] ? 在MULTI命令執行之前,可以指定待監控的keys,然而在執行EXEC之前,如果被監控的keys發生修改,EXEC將放棄執行該事務隊列中的所有指令。
UNWATCH ? ?取消當前事務中指定監控的keys,如果執行了EXEC或DISCARD命令,則無需再手工執行 該命令了,因為在此之后,事務中所有的keys都將自動取消,
和關系型數據庫中的事物相比,在redis事物中如果有某一條命令執行失敗,其后的命令仍然會被繼續執行。
我們可以通過MULTI命令開啟一個事務,有關系型數據庫開發經驗的人可以理解為BEGIN TRANSACTION語句,在該語句之后執行的命令都將被視為事務之內的操作,最后我
們可以通過執行EXEC/DISCARD命令來提交/回滾該事務內的所有操作。這兩個Redis命令可被視為等同于關系型數據庫中的COMMIT/ROLLERBACK語句。
在事物開啟之前,如果客戶端與服務端之間出現通訊故障并導致網絡斷開,其后所有帶執行的語句都將不會被服務器執行,然而如果網絡中短事件是在客戶端執行EXEC命令之后,那么該事務中所有命令都會被服務器執行。
當使用Append-Only模式時,Redis會通過調用系統函數write將該事物內的所有寫操作在本次調用全部寫入磁盤。然而如果在寫入的過程中會出現系統崩潰,如電源故障導致的宕機,那么此時也許只有部分數據被寫入到磁盤,而另外一部分數據卻已經丟失,Redis服務會在重新啟動時執行一系列必要的一致性檢測,一旦發現類似問題,就會立即退出并給出相應的錯誤提示。此刻,我們就要充分利用Redis工具包中提供的Redis-check-aof工具,該工具可以幫助我們定位到數據不一致的錯誤,并將已經寫入的部分數據進行回滾。修復之后我們就可以再次重新啟動Redis服務器。
WATCH 命令
WATCH為MULTI執行之前的某個key提供監控(樂觀鎖)的功能,如果key的值發生了變化,就會放棄事務的執行。
當事務EXEC執行成功之后,就會自動UNWATCH。
下面我們根據一個命令行指令來看一下:
(1)第一步:
127.0.0.1:6379> redis-cli -h 127.0.0.1 -p 6379 ? ?//命令拼接redis服務器
ok
127.0.0.1:6379> get test ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?//獲取test的鍵值
"hello world"
127.0.0.1:6379> multi ? ? ? ? ? //生成事務
ok
127.0.0.1:6379> set test "hello mygod" ? ? ? ? ? ? ? //修改指令
QUEUED
127.0.0.1:6379>exec ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //提交事務
1) OK
127.0.0.1:6379>
從上面的命令執行,我們可以看出,當我們生成事務后,執行set指令時,反饋的信息是QUEUED,最后我們再執行
exec,這些命令才會真正的執行。在這里可能還會有人說事務中還有一個rollback操作,但是redis里面好像并沒有發現,的確,redis里面是沒有rollback操作的,下面我們再進行一個例子的演示:
127.0.0.1:6379>multi
ok
127.0.0.1:6379>set test "my name is god"
QUEUED
127.0.0.1:6379>lpush testName 99
QUEEUED
127.0.0.1:6379>exec
1)OK
2)(error) WRONGTYPE Opertion against a key holding the wrong kind of value
127.0.0.1:6379>
在上面的例子中故意用lpush命令執行string ,可想而知自然不會執行成功,但從結果來看,你看到了,一個OK一個Error,這就違反了事務的原子性,
redis僅僅是個數據結構服務器,多簡單的一件事情,退一萬步來說,很明顯的錯誤指令它會直接返回的,比如我故意把lpush 寫成了lpush1:
127.0.0.1:6379>multi
OK
127.0.0.1:6379> set test "woqunimeimeide"
QUEUED
127.0.0.1:6379> lpush1 testName 44
(error) ERR unknow command 'lpush1'
127.0.0.1:6379>
上面可以看到,命令終止了你的任何輸入。
下面我們探索一下Redis事務的原理:
關于事務操作的源代碼,大多數都在redis源碼中的multi.c文件中,接下來我會一個一個簡單剖析一下:
1.multi
在redis的源碼中,它大概是這么寫的:
void multiCommand(redisClient *c)
{
if(c->flags & REDIS_MULTI){
addReplyError(c,"MULTI calls can not be nested");
return;?
}
c->flags |= REDIS_MULTI;
addReply(c,shared.ok);
}
2.生成命令
在redisClient 中,里面有一個multiState命令:
typedef struct redisClient{
。。。
multiState mstate; ? ? ? ? ? /* MULTI/EXEC state */
}redisClient;
從注釋中我們看到了命令和multi/exec肯定有關系,接下來我很好奇的看看multiState的定義:
typedef struct multiState{
multiState *command; ? ? /** Array of MULTI commands */
int count ; ? ?/*Total number of MULTI comands*/
int minreplicas; ? ? ? ? ? ? ? ?/*MINREPLICAS for synchronous as unixtime */
time_t minreplicas_timeout ? ? ?/*MINREPLICAS timeout as unixtime*/
}multiState;
從multiState這個舉例中,你可以看到下面有一個*commad命令,從注釋中我們可以看到它其實指向一個數組,這個數組就是若干條在指令,下面還有一個count,可以看到是實際的command的總數。
3.watch?
為了方便說一下后面的exec,這里想說一下watch 大概是怎樣實現的,在multi.c源碼中是這樣寫的:
typedef struct watchedKey {
? ? robj *key;
? ? redisDb *db;
} watchedKey;
void watchCommand(redisClient *c) {
? ? int j;
? ? if (c->flags & REDIS_MULTI) {
? ? ? ? addReplyError(c,"WATCH inside MULTI is not allowed");
? ? ? ? return;
? ? }
? ? for (j = 1; j < c->argc; j++)
? ? ? ? watchForKey(c,c->argv[j]);
? ? addReply(c,shared.ok);
}
/* Watch for the specified key */
void watchForKey(redisClient *c, robj *key) {
? ? list *clients = NULL;
? ? listIter li;
? ? listNode *ln;
? ? watchedKey *wk;
? ? /* Check if we are already watching for this key */
? ? listRewind(c->watched_keys,&li);
? ? while((ln = listNext(&li))) {
? ? ? ? wk = listNodeValue(ln);
? ? ? ? if (wk->db == c->db && equalStringObjects(key,wk->key))
? ? ? ? ? ? return; /* Key already watched */
? ? }
? ? /* This key is not already watched in this DB. Let's add it */
? ? clients = dictFetchValue(c->db->watched_keys,key);
? ? if (!clients) {
? ? ? ? clients = listCreate();
? ? ? ? dictAdd(c->db->watched_keys,key,clients);
? ? ? ? incrRefCount(key);
? ? }
? ? listAddNodeTail(clients,c);
? ? /* Add the new key to the list of keys watched by this client */
? ? wk = zmalloc(sizeof(*wk));
? ? wk->key = key;
? ? wk->db = c->db;
? ? incrRefCount(key);
? ? listAddNodeTail(c->watched_keys,wk);
}
這段代碼中最大的核心點就是:
?/* This key is not already watched in this DB. Let's add it */
? ? clients = dictFetchValue(c->db->watched_keys,key);
關于這個key的所有client ,最后還會塞入到redisclientde watched_keys字典中,如下代碼:
?/* Add the new key to the list of keys watched by this client */
? ? wk = zmalloc(sizeof(*wk));
? ? wk->key = key;
? ? wk->db = c->db;
? ? incrRefCount(key);
? ? listAddNodeTail(c->watched_keys,wk);
如果畫圖大概就是下面這樣:
其中watched_key是個字典結構,字典的鍵為上面的key1,key2....,,value為client的鏈表,這樣的話,就可以非常的清楚知道某個key中是被哪些client監視著的。
4.exec
這個命令大概做了兩件事:
<1>:判斷c-flags = REDIS_DIRTY_EXEC 打開與否,如果是的話,取消事務discardTransation(c),也就是說這個key已經被別的client修改了。
<2>:如果沒有修改,那么就for循環執行command[]中命令,如圖所示的兩處信息:
接下來是一段在php中使用Redis事務的案例:
//關于redis事務的案例
header("content-type:text/html;charset=utf-8");
$redis = new redis();
$redis->connect('localhost', 6379);
//$result = $redis->connect('localhost', 6379);
//$redis = Yii::app()->redis;
$redis->set("testName","33");
//$mywatchkey = $redis->get("mywatchkey");
$mywatchkey = $redis->get('testName');
//($test);exit;
$rob_total = 100; ? //搶購數量
if($mywatchkey<$rob_total){
? ? $redis->watch("testName");
? ? $redis->multi();
? ? //設置延遲,方便測試效果。
? ? sleep(5);
? ? //插入搶購數據
? ? $redis->hSet("testName","user_id_".mt_rand(1, 9999),time());
? ? $redis->set("testName",$mywatchkey+1);
? ? $rob_result = $redis->exec();
? ? if($rob_result){
? ? ? ? $mywatchlist = $redis->hGetAll("testName");
? ? ? ? echo "搶購成功!<br/>";
? ? ? ? echo "剩余數量:".($rob_total-$mywatchkey-1)."<br/>";
? ? ? ? echo "用戶列表:<pre>";
? ? ? ? var_dump($mywatchlist);
? ? }else{
? ? ? ? echo "手氣不好,再搶購!";exit;
? ? }
}
在上例是一個秒殺的場景,該部分搶購的功能會被并行執行
通過已銷售數量(mywhtchkey)的監控,達到了控制庫存,避免超賣的作用。
WHTCH是一個樂觀鎖,有利于減少并發中的沖突,提高吞吐量。
樂觀鎖和共享鎖
樂觀鎖(Optimistic Lock)又叫做共享鎖,每次別人拿數據的時候都認為別人不會修改數據,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用于多讀得應用類型,這樣會提高吞吐量。
悲觀鎖(Pessimistic Lock)又叫做排它鎖(x鎖),每次拿刀數據的時候都認為別人會修改數據,所以每次在拿到數據的時候都會上鎖,這樣別人想拿到這個數據就會block直到
它拿到鎖。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖,都是在操作之前先上鎖。
最后我們介紹一下Redis事務錯誤處理:
如果一個事務中的某個命令執行出錯,Redis將會怎么處理呢?要回答這個問題,我們首先需要知道是什么原因導致命令執行出錯:
1.語法錯誤:
語法錯誤表示命令不存在或者參數錯誤
這種情況需要區別Redis版本,Redis2.65之前的版本會忽略錯誤的命令,執行其他正確的命令,2.65之后的版本會忽略這個事務中的所有命令,都不執行,就比如上面的例子(使用的Redis版本是2.8的);
2.運行錯誤:
運行錯誤表示命令執行過程中出現錯誤,就比如用GET命令去獲取一個散列表類型的鍵值。
這種錯誤在命令執行之前Redis是無法發現的,所以在事務里這樣的命令都會被Redis接受并執行.如果事務里有一條命令執行錯誤,其他命令依舊會執行(包括出錯后的命令)。
---------------------?
作者:想念-忘記了?
來源:CSDN?
原文:https://blog.csdn.net/dannyiscoder/article/details/78285549?utm_source=copy?
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
來源:https://blog.csdn.net/dannyiscoder/article/details/78285549
總結
以上是生活随笔為你收集整理的PHP 如何在Redis中实现事物(事物提交和事物回滚)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: redis常见使用场景下PHP实现
- 下一篇: 社会公德(说一说社会公德的简介)