【雜談】如何對Redis進行原子操作

貓毛·波拿巴發表於2019-07-26

什麼時候需要進行需要原子操作?

很常見的例子,就是利用Redis實現分散式鎖。

實現鎖需要哪些條件?

我們知道要實現鎖,就需要一個改變鎖狀態的方法。這個方法能原子地對鎖的狀態進行檢查並修改。如果修改成功,則意味著獲得了鎖。對於硬體,就是它提供的就是test-and-set,compare-and-swap等原語。

Redis有沒有提供類似的原語呢?

有的。Redis有提供setnx(),它會提供這樣的原子操作:如果key沒有值,則將值設定進去,如果已有值就不做處理,提示失敗。

這樣就可以基於這個原語來實現鎖,簡單原理就是:key就是對應的鎖,如果key有值就說明鎖被佔用。刪除值代表釋放鎖。如果插入值成功,則代表獲得鎖。再加上過期時間,基本就可以滿足分散式鎖的需求了。

除了鎖,還有哪些地方需要原子操作?

假如我們在操作Redis資料的時候,需要判斷Redis中某個值是否滿足條件,只有滿足條件才做這個操作

我隨便舉個例子,例如:如果key-xxx的值不為0,則加1,如果為0,則刪除。

這種情況Redis可以處理嗎?

可以,Lua指令碼。Redis支援Lua指令碼。針對上面的問題,我們只要寫這樣的Lua指令碼就可以了。

local a = redis.call('get', 'xxx') //呼叫redis的get方法,key為'xxx'
if(tonumber(a) > 0) then  //redis都是以String進行儲存的,需要轉型
    redis.call('incr', 'xxx') //呼叫redis的incr方法,key為'xxx'
    return 'OK'
else
    return 'FAIL'

為什麼Lua指令碼可以實現原子操作, 看不出來它有用鎖啊?

這與Redis的請求處理有關。Redis只用一個執行緒來處理客戶端的請求。所以在執行lua指令碼的時候,沒有其他客戶端的請求在處理。所以在lua指令碼中的對redis資料的修改操作就是原子的。

只用一個執行緒處理的過來嗎?

Redis的請求處理執行緒,利用Select和事件迴圈進行處理,大概就是下面這樣:

while(1) {
    events = getEvents(); //先利用SELECT拿到最近的請求
    for(e in events)  //然後逐個處理
        processEvent(e);
}

因為redis的操作的資料都在記憶體中。處理起來也很快,所以也不會出現響應時間太長的情況。

萬一這個執行緒阻塞了怎麼辦?

一般情況下,阻塞不了。前面也說了,客戶端上來的請求都是操作記憶體的,不會有其他呼叫(例如檔案I/O這樣的呼叫)。但是,前面也說了,這是一般情況。別忘了lua,lua指令碼里面是可以寫阻塞操作的。實測發現,如果往Redis中提交一個死迴圈的lua指令碼,Redis就掛了。所以寫lua指令碼的時候要小心。

相關文章