Redis應用學習——Redis事務與實現分佈式鎖

1. Redis事務機制

    1. 與MySQL等關係數據庫相同,Redis中也有事務機制,Redis的事務實質上是命令的集合,但Redis中的事務機制不保證事務的原子性,這與關係型數據庫中的事務不一樣,在一個事務中要麼全部命令都被執行,要麼全部事物都不執行。 一個事務從開始到執行會經歷如下三個階段:java

  • 開始事務。
  • 命令入隊。
  • 執行事務。

在MySQL中使用START TRANSACTION 或 BEGIN開啓一個事務,使用COMMIT提交一個事務;而在Redis中使用MULTI 開始一個事務,由 EXEC 命令觸發事務, 一併執行事務中的全部命令。和關係型數據庫中的事物相比,在redis事務中若是有某一條命令執行失敗,其它的命令仍然會被繼續執行,也就是Redis中不支持事務的回滾,也就不具有事務的原子性redis

    2. Redis事務機制的相關指令:sql

  • MULTI:用於標記事務的開始,其後執行的命令都將被存入命令隊列,直到執行EXEC時,這些命令纔會被原子執行
  • EXEC:執行命令隊列中的全部命令,但若是在一個事務內執行了WATCH命令,那麼只有當WATCH所監控的keys沒有被修改的前提下,EXEC命令才能執行事務隊列中的全部命令,不然EXEC將放棄當前事務中的全部命令。
  • DISCARD:取消執行事務隊列中的全部命令,同時再將當前鏈接的狀態恢復爲正常狀態,即非事務狀態。若是WATCH命令被使用,該命令將UNWATCH全部的keys。注意,該指令並非Redis的回滾指令,Redis中不支持回滾,該指令只是取消事務中的全部指令的執行
  • WATCH  key[key...]:在MULTI命令執行以前,能夠指定待監控的keys,在執行EXEC以前,若是被監控的keys發生修改,EXEC將放棄執行該事務隊列中的全部指令。WATCH命令能夠監控一個或多個鍵,一旦其中有一個鍵被修改(或刪除),以後的事務就不會執行,監控一直持續到EXEC或DISCARD命令。該命令能夠保證某個key的CAS
  • UNWATCH:取消當前事務中指定監控的keys,若是執行了EXEC或DISCARD命令,則無需再手工執行該命令了,由於在此以後,事務中全部的keys都將自動取消監控

    3. 命令使用示例:數據庫

//正常執行
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>

 

2. 分佈式鎖

    1. 產生背景:分佈式的CAP理論告訴咱們「任何一個分佈式系統都沒法同時知足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時知足兩項。」因此,不少系統在設計之初就要對這三者作出取捨。在互聯網領域的絕大多數的場景中,都須要犧牲強一致性來換取系統的高可用性,系統每每只須要保證「最終一致性」,只要這個最終時間是在用戶能夠接受的範圍內便可。在不少場景中,咱們爲了保證數據的最終一致性,須要不少的技術方案來支持,好比分佈式事務、分佈式鎖等。有的時候,咱們須要保證一個方法在同一時間內只能被同一個線程執行。在單機環境中,Java中其實提供了不少併發處理相關的API,可是這些API在分佈式場景中就無能爲力了。也就是說單純的Java Api並不能提供分佈式鎖的能力。因此針對分佈式鎖的實現目前有多種方案。緩存

    2. 實現分佈式鎖的方案:典型的方案有如下幾種bash

  • 基於數據庫實現分佈式鎖 
  • 基於緩存(redis,memcached,tair)實現分佈式鎖
  •  基於Zookeeper實現分佈式鎖

    3. 分佈式鎖的要求:服務器

  • 能夠保證在分佈式部署的應用集羣中,同一個方法在同一時間只能被一臺機器上的一個線程執行
  • 這把鎖要是一把可重入鎖(避免死鎖)
  • 這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)
  • 有高可用的獲取鎖和釋放鎖功能
  • 獲取鎖和釋放鎖的性能要好

3. 分佈式鎖的數據庫實現方案

    1. 基於數據庫表的實現:最簡單的方式可能就是直接建立一張鎖表,而後經過操做該表中的數據來實現,當咱們要鎖住某個方法或資源時,咱們就在該表中增長一條記錄,想要釋放鎖的時候就刪除這條記錄。併發

  • 首先建立一個分佈式鎖的表,能夠把裏面存儲的看作分佈式鎖
    CREATE TABLE `methodLock` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
      `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名',
      `desc` varchar(1024) NOT NULL DEFAULT '備註信息',
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數據時間,自動生成',
      PRIMARY KEY (`id`),
      UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
  • 若是想要對分佈式執行的某個方法加鎖,就使用這個方法名向表中插入數據
    insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

    由於咱們對method_name作了惟一性約束,這裏若是有多個插入請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。分佈式

  • 當方法執行完畢以後,想要釋放鎖的話,須要執行如下Sql刪除鎖
    delete from methodLock where method_name ='method_name'

     

    2. 產生的問題:memcached

  • 這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會致使業務系統不可用。
  • 這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在數據庫中,其餘線程沒法再得到到鎖。
  • 這把鎖只能是非阻塞的,由於數據的insert操做,一旦插入失敗就會直接報錯。沒有得到鎖的線程並不會進入排隊隊列,要想再次得到鎖就要再次觸發得到鎖操做。
  • 這把鎖是非重入的,同一個線程在沒有釋放鎖以前沒法再次得到該鎖。由於數據中數據已經存在了。

    3. 解決辦法:

  • 數據庫是單點?搞兩個數據庫,數據以前雙向同步。一旦掛掉快速切換到備庫上。
  • 沒有失效時間?只要作一個定時任務,每隔必定時間把數據庫中的超時數據清理一遍。
  • 非阻塞的?搞一個while循環,直到insert成功再返回成功。
  • 非重入的?在數據庫表中加個字段,記錄當前得到鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,若是當前機器的主機信息和線程信息在數據庫能夠查到的話,直接把鎖分配給他就能夠了。

4. 基於Redis緩存實現的分佈式鎖

    1. 相比較於基於數據庫實現分佈式鎖的方案來講,基於緩存來實如今性能方面會表現的更好一點(鏈接數據庫進行讀寫操做性能耗費比緩存大)。並且不少緩存是能夠集羣部署的,能夠解決單點問題。

    2. Redis中有直接的命令支持,並且Redis的自己命令執行是一個單線程的,這就爲分佈式鎖提供了很好的實現,實現命令以下

  • SETNX key val:當且僅當key不存在時,set纔會成功,返回1;若key存在,操做失敗,返回0。
  • expire key timeout:爲key設置一個超時時間,單位爲second,超過這個時間鎖會自動釋放,避免死鎖。
  • delete key:刪除key,即釋放鎖

    3. Jedis客戶端也提供了相應的方法,主要就是setnx(String key,String value)方法,簡單實現思想僞代碼以下:

String get(String key) {
//首先嚐試從redis(或redis集羣)中獲取key對應的數據  
   String value = redis.get(key); 
//若是爲null,則使用redis中的分佈式鎖
   if (value  == null) {  
//經過setnx方法建立分佈式鎖
    if (redis.setnx(key_mutex, "1")) {  
        // 設置分佈式鎖的過時時間,能夠避免死鎖 
        redis.expire(key_mutex, 3 * 60)  
        value = db.get(key); //從數據庫中取得數據 
        redis.set(key, value);//回寫到緩存中  
        redis.delete(key_mutex);//釋放鎖  
    } else {  
        //其餘線程休息50毫秒後重試  
        Thread.sleep(50);  
        get(key);  
    }  
  }  
}

5. 基於Zookeeper實現分佈式鎖

    1. ZooKeeper是一個爲分佈式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個惟一文件名。基於ZooKeeper實現分佈式鎖的步驟以下:

(1)建立一個目錄mylock; 
(2)線程A想獲取鎖就在mylock目錄下建立臨時順序節點; 
(3)獲取mylock目錄下全部的子節點,而後獲取比本身小的兄弟節點,若是不存在,則說明當前線程順序號最小,得到鎖; 
(4)線程B獲取全部節點,判斷本身不是最小節點,設置監聽比本身次小的節點; 
(5)線程A處理完,刪除本身的節點,線程B監聽到變動事件,判斷本身是否是最小的節點,若是是則得到鎖。

這裏推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分佈式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。

優勢:具有高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。

缺點:由於須要頻繁的建立和刪除節點,性能上不如Redis方式

相關文章
相關標籤/搜索