求求你,別再開發的時候一用redis分佈式鎖,就急着去複製粘貼了!lua腳本的實現思路

隨着業務越來越負責,現在的業務,能夠支持分佈式和高併發是基本的要求,涉及到高併發和分佈式就一定會涉及到分佈式鎖機制,分佈式鎖就是爲了保證分佈式環境下,只有一個機器能夠拿到鎖對象,其餘的都需等待該鎖釋放,再進行申請鎖資源!

分佈式鎖必須遵循以下原則:

  1. 同一時刻只能有一個機器(進程或線程)能夠拿到鎖對象!
  2. 擁有過期機制,防止機器宕機沒有釋放鎖的情況下造成死鎖!
  3. 加鎖和解鎖的必須是一個機器(線程、進程)!
  4. 集羣環境下,存活機器依舊何以做完整的加解鎖操作!

一、思路圖

二、思路圖實現

1.正確實現

該實現由Jedis實現,模擬多線程環境下使用分佈式鎖

 <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.3.0</version>
</dependency>

定義接口

package com.lock.jedis;

import java.util.concurrent.TimeUnit;

/**
 * 分佈式鎖
 * @author huangfu
 */
public interface DistributedLock {

    /**
     * 嘗試給線程加鎖
     * @param lockName 鎖名稱
     * @param lockValue 鎖值
     * @param timeUnit 時間單位
     * @param time 時間
     * @return 是否加鎖成功
     */
    boolean tryLock(String lockName, String lockValue, TimeUnit timeUnit, long time);

    /**
     * 上鎖
     * @param lockName 鎖名稱
     * @param lockValue 鎖值
     * @param timeUnit 時間單位
     * @param time 時間
     */
    void lock(String lockName, String lockValue, TimeUnit timeUnit, long time);
    /**
     * 線程解鎖
     * @param lockName 即將解鎖的鎖名稱
     */
    void unLock(String lockName);
}

Jedis實現分佈式鎖

package com.lock.jedis.impl;

import com.lock.jedis.DistributedLock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * jedis實現分佈式鎖
 * @author huangfu
 */
public class JedisDistributedLock implements DistributedLock {
    private final static String SCRIPT_UNLOCK_LUA = "if redis.call(\"get\",KEYS[1]) == 																			ARGV[1] then\n" +
                                                 "return redis.call(\"del\",KEYS[1])\n" +
                                             "else\n" +
                                                  "return 0\n" +
                                              "end";
    private final static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>();
    public static final String OK = "OK";
    JedisPool pool;
    public JedisDistributedLock() {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxIdle(8);
        config.setMaxTotal(18);
        pool = new JedisPool(config, "192.168.1.4", 6379, 2000);
    }


    @Override
    public boolean tryLock(String lockName, String lockValue, TimeUnit timeUnit, 
                           											long time) {
        Jedis jedis = pool.getResource();
        try {
            long timeout = timeUnit.toSeconds(time);
            SetParams setParams = new SetParams();
            setParams.nx();
            setParams.ex((int)timeout);
            String result = jedis.set(lockName, lockValue, setParams);
            if (OK.equals(result)) {
                THREAD_LOCAL.set(lockValue);
                return true;
            }else{
                return false;
            }
        }finally {
            jedis.close();
        }

    }

    @Override
    public void lock(String lockName, String lockValue, TimeUnit timeUnit, long time) {
        if (tryLock(lockName,lockValue,timeUnit,time)) {
            return;
        } else {
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(2000,3000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock(lockName,lockValue,timeUnit,time);
        }
    }
    @Override
    public void unLock(String lockName) {
        Jedis jedis = pool.getResource();
        Object eval = jedis.eval(SCRIPT_UNLOCK_LUA, Collections.singletonList(lockName), 										Collections.singletonList(THREAD_LOCAL.get()));
        if(Integer.parseInt(eval.toString()) == 0){
            jedis.close();
            throw new RuntimeException("解鎖失敗!");
        }
        THREAD_LOCAL.remove();
    }
}

2. 代碼註釋

  1. 爲什麼要設置過期時間

    • 保證再服務宕機時,鎖依舊能夠被正常釋放!
  2. 爲什麼刪除鎖的時候會進行判斷值操作?

    • 多線程環境下,對於鎖而言,本線程只能刪除本線程加的鎖,無法刪除別的線程加的鎖!這裏舉例用UUID來實現值的唯一性
  3. tryLock方法中爲什麼要使用 SetParams承載 setnx參數?爲很麼刪除鎖時要使用lua腳本來實現?

    • 因爲無論時加鎖(setnx)操作,還是解鎖(del)操作,都必須保證其代碼的原子性
SetParams setParams = new SetParams();
setParams.nx();
setParams.ex((int)timeout);
String result = jedis.set(lockName, lockValue, setParams);

這段文中的代碼等價與

jedis.setnx(lockName,lockValue);
jedis.expire(lockName,(int)timeout);

但是爲什麼不這樣寫呢?因爲redis時單線程,同一時間只能執行一個命令,而這種寫法再redis認爲是兩個命令,那麼在多機或者多線程環境下執行時就可能出現問題!

我們看個圖,正常情況:

但是,非正常情況下:

爲很麼刪除鎖時要使用lua腳本來實現?

文中代碼

if redis.call("get",KEYS[1]) == ARGV[1] then
	return redis.call("del",KEYS[1])
else
	return 0
end

這段lua腳本等同於代碼

String s = jedis.get(lockName);
if(THREAD_LOCAL.get().equals(s)){
    jedis.del(lockName);
}

這段代碼和上面的一樣,都保證不了原子性!一起看個圖!

正常情況

異常情況

好了,本期就是使用lua腳本來實現分佈式鎖的內容了,這個是分佈式鎖的基本實現的原理,但是再生產環境上使用會有很嚴重的問題!

  1. 現代生產環境都是追求高可用,那麼redis在集羣環境和哨兵集羣環境下如何保證分佈式鎖的高可用呢?
  2. 當前問題並沒有保證業務時間絕對小於鎖的過期時間,但不確定的業務場景下,依舊會出現分佈式鎖失效的情況!

以上問題如何解決呢?這個就是明天的內容了!怕文章太長,你們看不下去!哈哈哈!


才疏學淺,如果文章中理解有誤,歡迎大佬們私聊指正!歡迎關注作者的公衆號,一起進步,一起學習!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章