Redis實現分布式鎖(利用分布式鎖,實現分布式定時任務)

簡述

利用Redis的Setnx命令,來實現一個分布式的加鎖方案。利用注解,在擁有該注解的方法上,進行切面處理,在方法執行前,進行加鎖,執行結束后,根據是否自動釋放鎖,進行解鎖。
將該注解用在定時任務的方法上,即可實現分布式定時任務,即獲取到鎖的方法,才會執行。

1 redis命令

  • 1.1 setnx命令
    Redis setnx(SET if Not eXists) 命令在指定的 key 不存在時,為 key 設置指定的值。(該命令無法設置過期時間)
    Redis為單進程單線程模式,采用隊列模式將并發訪問變成串行訪問,且多客戶端對Redis的連接并不存在競爭關系。redis的SETNX命令可以方便的實現分布式鎖。
    當某一個客戶端將key的值設置成功后,其他的客戶端再進行設置,將返回失敗,保證同一時間,只有一個客戶端能夠設置成功。
  • Redis事務
    watch key1 key2 ... : 監視一或多個key,如果在事務執行之前,被監視的key被其他命令改動,則事務被打斷 ( 類似樂觀鎖 )
    multi : 標記一個事務塊的開始( queued )
    exec : 執行所有事務塊的命令 ( 一旦執行exec后,之前加的監控鎖都會被取消掉 ) 
    discard : 取消事務,放棄事務塊中的所有命令
    unwatch : 取消watch對所有key的監控

事務正常使用

127.0.0.1:6379> multi
127.0.0.1:6379> set name jack
127.0.0.1:6379> exec

取消事務

127.0.0.1:6379> multi
127.0.0.1:6379> set name jack
127.0.0.1:6379> discard

watch使用

number初始為10

127.0.0.1:6379> watch number
127.0.0.1:6379> multi
127.0.0.1:6379> set number 11
127.0.0.1:6379> exec

如果在執行exec時,number沒有被其他客戶端修改,還是10,則事務執行成功;

如果被其他客戶端修改了,number不是10了,則事務執行失敗,這時候就需求程序自行處理,進行再次提交或者其他操作

在spring boot 中,我們用StringRedisTemplate來操作Redis,它的方法:stringRedisTemplate.opsForValue().setIfAbsent()方法即對應setnx命令,這個方法有兩個重載的方法:
1、Boolean setIfAbsent(K key, V value); 設置key value,返回成功/失敗
2、Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit); 設置key value,返回成功/失敗,同時設置過期時間,redisTemplate 會調用 EXPIRE進行過期時間的設定,同時在設置值和過期時間時,會開啟事務,保存全部成功。
```// org.springframework.data.redis.core 中實現的方法
@Override
public Boolean setIfAbsent(K key, V value) {

    byte[] rawKey = rawKey(key);
    byte[] rawValue = rawValue(value);
    return execute(connection -> connection.setNX(rawKey, rawValue), true);
}
@Override
public Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit) {

    byte[] rawKey = rawKey(key);
    byte[] rawValue = rawValue(value);

    Expiration expiration = Expiration.from(timeout, unit);
    return execute(connection -> connection.set(rawKey, rawValue, expiration, SetOption.ifAbsent()), true);
    }

1.2 DEL命令、lua腳本
在加鎖之后,解鎖時,需要判斷鎖,是否是當前線程所擁有的,如果是當前線程擁有的,則刪除該key,刪除key,用del命令。

del key_name
我們會先取出key對應的值,然后判斷是否和當前線程的定義的值一致。如果一致,則說明是該線程擁有的key。如果我們在代碼中取出key的值,然后判斷通過后,調用redis del 刪除key,這就不是一個原子操作了。如果在我們取出key的值后,然后在刪除前,其他線程獲取了鎖,當前線程刪除的動作,就會導致刪除其他線程擁有的鎖。所以釋放鎖,需要利用lua腳本進行,將判斷和刪除,這兩個動作,合為一個原子性的操作。
所以我們會利用代碼去執行下面的lua腳本,保證判斷和刪除的原子性。

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

一般教程中,利用RedisTemplate來執行lua腳本時,會將lua腳本放到靜態資源目錄中。而在下面的代碼中,利用ByteArrayResource直接從String字符串中讀取了lua腳本內容:

    /*
     * 保存lua腳本
     */
    private DefaultRedisScript<List> getRedisScript;

    @PostConstruct
    public void init(){
        // 定義lua腳本資源
        // 也可以放到文件中,加載進來: new ResourceScriptSource(new ClassPathResource("redis/demo.lua"))
        String luaStr = "if redis.call('get',KEYS[1]) == ARGV[1] then return   redis.call('del',KEYS[1])  else return 0 end";
        ByteArrayResource resource = new ByteArrayResource(luaStr.getBytes());

        getRedisScript = new DefaultRedisScript<>();
        getRedisScript.setResultType(List.class);
        getRedisScript.setScriptSource(new ResourceScriptSource(resource));
    }

2 分布式鎖實現

下面是實現的核心類:

RedisLock: reids分布式鎖工具類
EmLock: 分布式鎖注解
LockRangeEnum: 分布式鎖的范圍枚舉
EmLockAspect: 分布式鎖切面
2.1 RedisLock,reids分布式鎖工具類
代碼如下:

package com.emdata.lowvis.common.redislock;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.concurrent.TimeUnit;

/**
 * reids分布式鎖工具類
 *
 * @version 1.0
 * @date 2020/12/8 14:37
 */
@Slf4j
@Component
public class RedisLock {

    private static final String SPLIT = "_";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 加鎖解鎖工具類
     * @param lockKey 加鎖的key
     * @param uuid 線程的標志
     * @param timeout 超時時間
     * @param timeUnit 超時時間粒度
     * @return true:獲取成功
     */
    public boolean lock(String lockKey, String uuid, long timeout, TimeUnit timeUnit) {
        // 根據key獲取值
        String currentLock = stringRedisTemplate.opsForValue().get(lockKey);

        // 值為:uuid_時間
        String value = uuid + SPLIT + (timeUnit.toMillis(timeout) + System.currentTimeMillis());

        // 如果為空,則設置值
        if (StringUtils.isEmpty(currentLock)) {
            if (stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, timeout, timeUnit)) {
                // 對應setnx命令,可以成功設置,也就是key不存在,獲得鎖成功
                return true;
            } else {
                return false;
            }
        } else {
            // 可重入鎖,如果是這個uuid持有的鎖,則更新時間
            if (currentLock.startsWith(uuid)) {
                stringRedisTemplate.opsForValue().set(lockKey, value, timeout, timeUnit);
                return true;
            } else {
                return false;
            }
        }
    }
    
    /*
     * 保存lua腳本
     */
    private DefaultRedisScript<List> getRedisScript;

    @PostConstruct
    public void init(){
        // 定義lua腳本資源
        // 也可以放到文件中,加載進來: new ResourceScriptSource(new ClassPathResource("redis/demo.lua"))
        String luaStr = "if redis.call('get',KEYS[1]) == ARGV[1] then return   redis.call('del',KEYS[1])  else return 0 end";
        ByteArrayResource resource = new ByteArrayResource(luaStr.getBytes());

        getRedisScript = new DefaultRedisScript<>();
        getRedisScript.setResultType(List.class);
        getRedisScript.setScriptSource(new ResourceScriptSource(resource));
    }

    /**
     * 釋放鎖
     *
     * @param lockKey 加鎖的key
     * @param uuid 線程的標志
     */
    public void release(String lockKey, String uuid) {
        try {
            List<Integer> execute = stringRedisTemplate.execute(getRedisScript, Collections.singletonList(lockKey), uuid);
            log.debug("解鎖結果: {}", execute.get(0) == 0);
        } catch (Exception e) {
            log.error("解鎖異常, key: {}, uuid: {}", lockKey, uuid);
            log.error("", e);
        }
    }

}

2.2 EmLock,分布式鎖注解

package com.emdata.lowvis.common.redislock;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * 分布式鎖注解
 *
 * @version 1.0
 * @date 2020/12/8 17:59
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface EmLock {

    /**
     * 鎖的范圍,默認應用級別
     * @return 鎖的范圍
     */
    LockRangeEnum lockRange() default LockRangeEnum.APPLICATION;

    /**
     * 鎖對應的key
     * @return key
     */
    String key();

    /**
     * 鎖超時時間
     * @return 時間
     */
    int timeout() default 5;

    /**
     * 鎖超時時間粒度
     * @return 粒度
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 是否自動釋放鎖
     * @return true: 方法完成后,自動釋放
     */
    boolean autoRelease() default true;
}

2.3 LockRangeEnum, 分布式鎖的范圍枚舉

package com.emdata.lowvis.common.redislock;

/**
 * 分布式鎖的范圍枚舉
 *
 * @author pupengfei
 * @version 1.0
 * @date 2020/12/10 13:46
 */
public enum LockRangeEnum {

    /**
     * 應用級別,鎖的級別在整個應用容器內
     */
    APPLICATION,

    /**
     * 線程級別,鎖的級別在每個線程
     */
    THREAD

}

2.4 EmLockAspect,分布式鎖切面

package com.emdata.lowvis.common.redislock;

import com.emdata.lowvis.common.utils.UUIDUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * 分布式鎖切面
 *
 * @version 1.0
 * @date 2020/12/8 17:59
 */
@Slf4j
@Component
@Aspect
@Configuration
public class EmLockAspect {

    @Autowired
    private RedisLock redisLock;

    /**
     * 應用級別的容器的id
     */
    private final String appUUID = UUIDUtils.get();

    /**
     * 線程級別的線程的id
     */
    private final ThreadLocal<String> threadUUID = ThreadLocal.withInitial(UUIDUtils::get);

    /**
     * 定義切點
     */
    @Pointcut("@annotation(com.emdata.lowvis.common.redislock.EmLock)")
    public void lockAop() {

    }

    @Around("lockAop()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        // 獲取方法
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        // 看有沒有日志注解
        EmLock emLock = method.getAnnotation(EmLock.class);
        if (emLock == null) {
            return point.proceed();
        }

        // 獲取鎖的級別
        LockRangeEnum lockRangeEnum = emLock.lockRange();
        String uuid = lockRangeEnum == LockRangeEnum.APPLICATION ? appUUID : threadUUID.get();

        // 獲取鎖的key和超時時間
        String key = emLock.key();
        int timeout = emLock.timeout();
        TimeUnit timeUnit = emLock.timeUnit();

        // 加鎖
        boolean lock = redisLock.lock(key, uuid, timeout, timeUnit);

        Object proceed = null;

        try {
            if (lock) {
                log.info("獲取到鎖,繼續執行...");
                // 繼續執行
                proceed = point.proceed();
            }
        } finally {
            // 自動釋放,則釋放鎖
            if (emLock.autoRelease()) {
                redisLock.release(key, uuid);
            }
        }

        return proceed;
    }

}

3 使用示例

3.1 使用RedisLock

  @Autowired
private RedisLock redisLock;

public void useLock() {
    // 定義鎖的key
    String lockKey = "camera_update_key";
    String uuid = UUIDUtils.get();

    // 定義超時時間
    long timeout = 5;
    TimeUnit timeUnit = TimeUnit.SECONDS;

    // 加鎖
    boolean lock = redisLock.lock(lockKey, uuid, timeout, timeUnit);
    try {
        if (lock) {
            log.info("執行...");
        } else {
            throw new IllegalStateException("未獲取到鎖,放棄執行");
        }
    } finally {
        // 在finally里面進行解鎖
        redisLock.release(lockKey, uuid);
    }
}

3.2 使用EmLock

@Component
@Slf4j
public class ScheduleTask {

    /**
     * 用在定時任務方法上,鎖的key為test_lock,指定了超時時間為2秒鐘
     * 鎖的級別為默認的應用級別(LockRangeEnum.APPLICATION),在這個如果應用啟動了多個容器運行,在只會有一個容器獲取到鎖,
     * 自動釋放鎖為false,即方法執行完成后,也不會自動釋放鎖,只有到超時時間了,鎖才會釋放
     */
    @Scheduled(cron = "0 0/1 * * * ? ")
    @EmLock(key = "test_lock", timeout = 2, timeUnit = TimeUnit.SECONDS, autoRelease = false)
    public void recordUpdateTask() {
        log.info("執行任務.......");
    }
   
   /**
     * 用在普通的方法上,鎖的key為method_Lock,指定了超時時間為1分鐘,
     * 鎖的級別為默認的線程級別,在該應用內多個線程執行該方法,則只會有一個線程獲取到鎖
     * 如果啟動了多個應用容器,同樣多個容器內的所有線程,也只會有一個線程獲取到鎖
     */
    @EmLock(key = "method_Lock", timeout = 1, timeUnit = TimeUnit.MINUTES, lockRange = LockRangeEnum.THREAD)
    public void recordUpdate() {
        log.info("執行任務2.......");
    }
}

4 使用注意

使用Redis作為分布式鎖的實現,依賴于Redis服務,如果Redis服務無法正常訪問,則會導致整個方法無法執行。
如果EmLock注解用在定時任務上時,如果應用運行在不同的服務器上,或者不同的docker容器里面時,必須保證運行環境的時間一致。
如果設置了定時任務上面的鎖,不是自動釋放的,則運行環境的時間,相差不大于鎖超時時間的時候,也可以保證定時任務,唯一執行。因為在超時時間范圍內,某個應用容器持有該鎖,其他應用來獲取鎖時,同樣獲取不到,方法不會執行。

作者:Knight_9
鏈接:http://www.lxweimin.com/p/5190600e44c1
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容

  • 簡述 利用Redis的Setnx命令,來實現一個分布式的加鎖方案。利用注解,在擁有該注解的方法上,進行切面處理,在...
    Knight_9閱讀 1,767評論 2 2
  • 1、一個tomcat是一個進程,其中有很多線程(與有多少個app無關) 2、一個tomcat啟動一個JVM,其中可...
    ZHL_e522閱讀 455評論 0 0
  • 目前實現分布式鎖的方式主要有數據庫、Redis和Zookeeper三種,本文主要闡述利用Redis的相關命令來實現...
    Aldeo閱讀 2,095評論 0 6
  • 一、背景 我們在開發很多業務場景會使用到鎖,例如庫存控制,抽獎等。一般我們會使用內存鎖的方式來保證線性的執行。但現...
    楊健kimyeung閱讀 324評論 0 0
  • 16宿命:用概率思維提高你的勝算 以前的我是風險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數的可能。 ...
    yichen大刀閱讀 6,076評論 0 4