【雜談】如何對Redis進行原子操作
什麼時候需要進行需要原子操作?
很常見的例子,就是利用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指令碼的時候要小