Redis全方位詳解--資料型別使用場景和redis分散式鎖的正確姿勢
一、Redis資料型別
1.string
string是Redis的最基本資料型別,一個key對應一個value,每個value最大可儲存512M。string一半用來存圖片或者序列化的資料。
2.hash
相當於一個string型別的對映表。特別適合用來儲存物件。例如可以儲存使用者資訊,使用者ID作為hash型別裡的每一個key。
案例:我們這邊需要對接微信粉絲的資料到我們自己的平臺上,但微信提供的介面只支援單天查詢,那麼如果我們想要檢視最近一個月微信粉絲的狀況,就需要迴圈30次呼叫微信的介面。一個月勉強還可以接受,那麼如果想要查半年,甚至一年呢?那麼我們的接口裡就需要迴圈365次調微信的介面,這就會使我們的介面變得非常慢,甚至超時。還有這些資料,比如單天新增粉絲數,是不會變得,而且每天都有一個數據,這樣就特別適合存在redis的hash型別裡,以日期(2018-10-10)作為hash的key。
3.list
list型別是簡單的字串列表,每個列表可以儲存232 - 1 個值。可以從頭部或者尾部順序插入資料。list型別可以用來做電商裡的秒殺營銷系統或關注列表。
4.set
set是string型別的無序集合。該集合是通過雜湊實現的,新增、刪除的複雜度都是O(1),所以查詢非常快。
案例:我們這邊是以手機號為唯一標示符,防止重複使用者註冊,會判斷該手機號有沒有註冊過,那麼如果用set型別儲存註冊過的使用者手機號,就會很快判斷出該使用者是否註冊過,而不用去查資料庫了。
5.zset
和set一樣,但zset多了一個score來讓set變得有序,且不允許有重複的成員。
案例:我們這邊有一個賬戶記錄需要按記錄時間排序,那麼就可以將時間戳當作score儲存zset中。
二、redis分散式鎖
網上很多redis分散式鎖的實現方式不能說錯誤的,但至少不夠嚴謹,在某些極端情況下是會出問題的。一旦出現問題,還是挺麻煩的事情,所以我們要知道redis分散式鎖的正確姿勢。
其實很簡單,利用redis的原子性。關於原子性,官方的一段描述為:
大概意思就是redis執行lua指令碼的時候,會被當成一條命令執行,在此期間,不會執行其他命令,所以lua指令碼儘量是快指令碼而不是慢指令碼。
所以,正確的姿勢是:
public functiongetDistributeLock($redis, string $key, int $userId, int $expire) { $luaScript = <<<LUA if (redis.call('exists', KEYS[1]) == 0) and redis.call('setex', KEYS[1], ARGV[1], ARGV[2]) then return 1 else return 0 end LUA; return $redis->eval($luaScript, 1, $key, $expire, $userId) > 0 ? true : false; }
這裡我們把一段lua指令碼放到redis的eval方法裡執行,這樣就可以保證這一段命令的原子性。
那麼如果不用lua指令碼,姿勢應該是這樣的:
public function wrongGetDistributeLock($redis, string $key, int $userId, int $expire) { // 若鎖不存在,則加鎖 if(!$redis->exists($key) && ($redis->setex($key, $expire, $userId) == 'OK')) { return true; } return false; }
前面提到過,這種姿勢在某些情況下會出問題:如果同時好幾個客戶端同時請求,同時通過了上面if條件的第一層,那麼這時候就會出現多個同時拿到鎖,並且前面人的鎖會被覆蓋。
然後,正確的解鎖姿勢是:
public function releaseDistributeLock($redis, string $key, int $userId) { $luaScript = <<<LUA if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end LUA; $redis->eval($luaScript, 1, $key, $userId); }
同樣需要使用lua指令碼來達到原子性。那麼如果不使用lua指令碼的姿勢是:
public function wrongReleaseLock($redis, string $key, int $userId) { if($userId == $redis->get($key)) { $redis->del($key); } }
會有這樣一種情況:A請求通過if語句後,這時候這個redis的key剛好過期了,然後B客戶端加鎖成功,這時候A請求就會把客戶端B剛加的鎖給解除了。
雖然我上面提到的兩種情況都是很極端、很少出現的。但如果可以用很簡單的方法避免掉,so why not?
上面提到的redis分散式鎖,滿足了三個特性:
- 互斥性。同時只能又一個客戶端擁有鎖
- 不會發生死鎖。
- 加鎖和解鎖的必須是同一個客戶端。
童鞋們,有什麼疑問,可以在地下留言哦。