1. 程式人生 > >Redis全方位詳解--資料型別使用場景和redis分散式鎖的正確姿勢

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 function
getDistributeLock($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分散式鎖,滿足了三個特性:

  • 互斥性。同時只能又一個客戶端擁有鎖
  • 不會發生死鎖。 
  • 加鎖和解鎖的必須是同一個客戶端。

  童鞋們,有什麼疑問,可以在地下留言哦。