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.具有高階會員資格。
基本流程如下:
這個流程中存在明顯的併發問題,當程序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方式更適合實際情況。
成本與收益間的平衡,做科學與做工程的區別~