1. 程式人生 > >memcached 原子性操作 CAS模式

memcached 原子性操作 CAS模式

應用場景分析:

       如原來MEMCACHED中的KES的內容為A,客戶端C1和客戶端C2都把A取了出來,C1往準備往其中加B,C2準備往其中加C,這就會造成C1和C2執行後的CACHE KEYS要麼是AB要麼是AC,而不會出現我們期望的ABC。這種情況,如果不是在叢集環境中,而只是單機伺服器,可以通過在寫CACHE KEYS時增加同步鎖,就可以解決問題,可是在叢集環境中,同步鎖是顯然解決不了問題的。

memcached是原子的嗎?巨集觀
         所有的被髮送到memcached的單個命令是完全原子的。如果您針對同一份資料同時傳送了一個set命令和一個get命令,它們不會影響對方。它們將被序列化、先後執行。即使在多執行緒模式,所有的命令都是原子的;命令序列不是原子的。如果您通過get命令獲取了一個item,修改了它,然後想把它set回memcached,我們不保證這個item沒有被其他程序(process,未必是作業系統中的程序)操作過。在併發的情況下,您也可能覆寫了一個被其他程序set的item。
memcached 1.2.5以及更高版本,提供了gets和cas命令,它們可以解決上面的問題。如果您使用gets命令查詢某個key的item,memcached會 給您返回該item當前值的唯一標識。如果您覆寫了這個item並想把它寫回到memcached中,您可以通過cas命令把那個唯一標識一起傳送給 memcached。如果該item存放在memcached中的唯一標識與您提供的一致,您的寫操作將會成功。如果另一個程序在這期間也修改了這個 item,那麼該item存放在memcached中的唯一標識將會改變,您的寫操作就會失敗。

微觀分析

 memcache為了避免一些競爭,加入了一些特殊原子操作:add cas incr decr

"add" means "store this data, but only if the server *doesn't* already;

“cas” is a check and set operation which means “store this data but only if no one else has updated since I last fetched it.” ;

它們的實現原理基於CAS(check and save)模式:

下面是一個全新的CAS模式實現:

     1.預先在memcached中設定一個key值,假設為CREATKEY=1

     2.每次建立活動時,在規則校驗前先get出CREATEKEY=x;

     3.進行規則校驗

     4.執行incr CREATEKEY操作,檢驗返回值是否為所期望的x+1,如果不是,則說明在此期間有另外的程序執行了incr操作,即存在併發,放棄更新。否則

     5.執行建立活動

memcached儲存的key value都有一個唯一標識casUnique,在進行incr decr操作時,首先獲取casUnique,執行incr,檢驗返回值是否casUnique+1,如果是,則更新,否則,失敗不更新!

儘管這種設計在處理併發時還存在缺陷,但可以通過簡單的重試來解決問題!

介面分析:

返回MemcachedItem物件:

 public MemcachedItem gets(String key) {
               return client.gets(key);
          }

 public MemcachedItem gets(String key, Integer hashCode) {
                   return gets(OPCODE_GET, key, hashCode, false);
         }

普通的get方法,返回Value物件

 public Object get(String key) {
              return client.get(key);
         }

casUnique:是唯一標識
        public boolean cas(String key, Object value, long casUnique) {
             return client.cas(key, value, casUnique);
        }

 public boolean cas(String key, Object value, long casUnique) {
               return set(OPCODE_SET, key, value, null, null, casUnique, primitiveAsString);
          }

MemcachedItem類結構:

public final class MemcachedItem {
              public long casUnique;
               public Object value;

}

其它約束:

32位無符號整數

 下面一片文章是對memcache add原子性實際應用的使用:

引子

一個使用快取進行併發控制的討論,讓我學習到成本與收益間的平衡,以及何為真正的可用性......

防止併發有多種方式,本文只涉及使用快取memcached控制。

併發場景:

     用例:SNS系統中具有高階會員資格的人發起活動。

     業務規則:1.一個人同時只能建立一個活動。2.具有高階會員資格。

     基本流程如下:

memcached 原子性操作 - zhengjunwei2007 - 我的部落格

這個流程中存在明顯的併發問題,當程序A校驗過會員M有資格,並且為建立過活動,但為開始執行建立操作,此時另一個程序B也進行了規則判斷,順利通過,並完成建立操作,此時A繼續執行,則會產生兩條M的活動。(這個併發場景很簡單,很普遍)

最初的解決方案:

     計劃利用memcached的add操作的原子性來控制併發,具體方式如下:

     1.申請鎖:在校驗是否建立過活動前,執行add操作key為memberId,如果add操作失敗,則表示有另外的程序在併發的為該memberId建立活動,返回建立失敗。否則表示無併發

     2.執行建立活動

     3.釋放鎖:建立活動完成後,執行delete操作,刪除該memberId。

問題:

     如此實現存在一些問題:

     1.memcached中存放的值有有效期,即過期後自動失效,如add過M1後,M1失效,可以在此add成功

     2.即使通過配置,可以使memcached永久有效,即不設有效期,memcached有容量限制,當容量不夠後會進行自動替換,即有可能add過M1後,M1被其他key值置換掉,則再次add可以成功。

     3.此外,memcached是基於記憶體的,掉電後資料會全部丟失,導致重啟後所有memberId均可重新add。

應對問題:

     針對上述的幾個問題,根本原因是add操作有時效性,過期,被替換,重啟,都會是原來的add操作失效。解決該問題有兩個方法:

     1.採用持久化的快取解決方法,如TT(Tokyo Tyrant:http://fallabs.com/tokyotyrant/)

     2.減輕時效性的影響,使用memcached CAS(check and set)方式。

     第一種不必解釋了,很簡單,原來的所有問題都是時效性惹得禍,時效性源於memcached是基於記憶體的,那麼採用持久話儲存的TT可以徹底根治這個問題。

     第二種方式需要簡單介紹下:

     memcached中除了add操作是原子的,還有另外兩個操作也是原子的:incr和decr,使用CAS模式即:

     1.預先在memcached中設定一個key值,假設為CREATKEY=1

     2.每次建立活動時,在規則校驗前先get出CREATEKEY=x;

     3.進行規則校驗

     4.執行incr CREATEKEY操作,檢驗返回值是否為所期望的x+1,如果不是,則說明在此期間有另外的程序執行了incr操作,即存在併發,放棄更新。否則

     5.執行建立活動

     對比這兩種方法,從效果上看可以發現第一種時100%可靠的,不存在問題;第二種,可能存在誤判,即本來不存在併發,卻被判為併發,如快取重啟,或key值失效後,incr值可能不同於期望值,導致誤判。

     但是從成本上考慮,TT是持久化的快取解決方案,完美意味著高成本,我們必須維護持久化資料,而使用memcached的CAS方式,可以以幾乎0成本的方式解決時效性問題,儘管存在一點小缺陷,但這種缺陷可以通過簡單的重試即可解決。考慮實際的產出比,採用memcached的CAS方式更適合實際情況。

     成本與收益間的平衡,做科學與做工程的區別~