天天看點

Redis分布式鎖的Java實作(基于Lua腳本)

※ Redis分布式鎖的實作,主要就是基于“加鎖、釋放鎖、防止出現并發”三點來展開的。網上也有很多例子,但都沒有實作守護線程,我在這裡将守護線程的實作整合進去,供大家參考。

在《Redis實作分布式鎖的思路》中我們講了Redis分布式鎖實作的思路,沒看過的童鞋可以先去看一下,因為這裡的實作就是基于設計思路展開的。

實作Redis分布式鎖,我們需要考慮

  • 加鎖,使用Redis 2.6.12以上版本提供的多參數set指令。
  • 釋放鎖,使用del指令,但需要注意在釋放鎖之前判斷是否是自己加的鎖,不要把其他線程加的鎖給删除了,具體原因《Redis實作分布式鎖的思路》中有詳細說明。另外為了保證判斷和釋放操作保證原子性,此處我們使用Lua腳本實作。
  • 防止出現并發,具體原因也在《Redis實作分布式鎖的思路》中有詳細說明,我們讓獲得鎖的線程開啟一個守護線程,給快要逾時的線程續命。

代碼

> pom檔案

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
​
    <groupId>com.felix</groupId>
    <artifactId>demo</artifactId>
    <version>1.0-SNAPSHOT</version>
​
   <dependencies>
       <dependency>
           <groupId>org.springframework</groupId>
           <artifactId>spring-context</artifactId>
           <version>5.2.3.RELEASE</version>
       </dependency>
​
       <dependency>
           <groupId>redis.clients</groupId>
           <artifactId>jedis</artifactId>
           <version>3.2.0</version>
       </dependency>
​
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>1.18.12</version>
           <scope>provided</scope>
       </dependency>
   </dependencies>
</project>
           

> Jedis初始化類

package com.felix.util;
​
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
​
/**
 * @Copyright (C), 2019-2020, Felix
 * @ClassName: JedisUtil
 * @Author: long.yuan
 * @E-mail: [email protected]
 * @Date: 2020/2/15 15:10
 * @Version: V1.0
 * @Description: Jedis初始化類
 */
@Service
public class JedisUtil {
    /**
     * 初始化Jedsi執行個體
     */
    public static final Jedis jedis;
    static{
        jedis = new Jedis("Redis主機IP",6379);
        jedis.auth("Redis連接配接密碼");
    }
}
           

> Redis分布式鎖工具類

package com.felix.util;

import org.springframework.stereotype.Service;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
 * @Copyright (C), 2019-2020, Felix
 * @ClassName: RedisDistributedLockTool
 * @Author: long.yuan
 * @E-mail: [email protected]
 * @Date: 2020/2/15 14:02
 * @Version: V1.0
 * @Description: Redis分布式鎖工具類
 */
@Service
public class RedisDistributedLockTool {

    /**
     * 加鎖成功
     */
    private static final String LOCK_SUCCESS = "OK";
    /**
     * 解鎖成功
     */
    private static final Long RELEASE_SUCCESS = 1L;
    /**
     * 如果沒有擷取到鎖,每10ms重試一次
     */
    private static final int DEFAULT_ACQUIRY_RETRY_MILLIS = 10;

    /**
     * @Title 加鎖
     * @Description 如果擷取鎖失敗,每10ms重試一次,重試最大次數為3,否則傳回false
     * @Author long.yuan
     * @Date 2020/2/15 14:49
     * @Param [key, value, expireTime]
     * @return boolean
     **/
    public static boolean lock(String key, String value, long expireTime) throws InterruptedException {
        int count = 0;
        int countMax = 3;
        while (count < countMax){
            SetParams params = new SetParams();
            // NX表示僅在key不存在時才能設定成功
            params.nx();
            // EX表示機關是秒,PX表示機關是毫秒
            params.px(expireTime);
            if (LOCK_SUCCESS.equals(JedisUtil.jedis.set(key, value, params))) {
                return true;
            }else{
                // 如果沒有拿到鎖,每10ms重試一次,最多重試3次
                TimeUnit.MILLISECONDS.sleep(DEFAULT_ACQUIRY_RETRY_MILLIS);
                count++;
            }
        }
        return false;
    }

    /**
     * @Title 解鎖
     * @Description 解鎖時與傳進來的value作對比,看是否是自己加的鎖,如果是自己加的鎖再删除
     * @Author long.yuan
     * @Date 2020/2/15 14:55
     * @Param [key, value]
     * @return boolean
     **/
    public static boolean unLock(String key, String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = JedisUtil.jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}
           

> 守護線程類

package com.felix.util;

import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * @Copyright (C), 2019-2020, Felix
 * @ClassName: DaemonThread
 * @Author: long.yuan
 * @E-mail: [email protected]
 * @Date: 2020/2/15 15:12
 * @Version: V1.0
 * @Description: 守護線程類
 */
@Service
@Getter
@Setter
public class DaemonThread implements Runnable{

    /**
     * 續命鎖的key
     */
    private String key;
    /**
     * 每隔多久續命一次(機關:ms)
     */
    private long aTime;
    /**
     * 每次續命多長時間(機關:ms)
     */
    private long bTime;

    public void run() {
        while (true){
            try {
                // aTime時間後再續命
                TimeUnit.MILLISECONDS.sleep(aTime);
                // 每次續命bTime時間
                System.out.println("守護線程續命"+bTime+"毫秒");
                JedisUtil.jedis.expire(this.key, (int)bTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
           

> Redis分布式鎖測試類

package com.felix.service;

import com.felix.util.DaemonThread;
import com.felix.util.RedisDistributedLockTool;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @Copyright (C), 2019-2020, Felix
 * @ClassName: RedisDistributedLockTest
 * @Author: long.yuan
 * @E-mail: [email protected]
 * @Date: 2020/2/15 15:35
 * @Version: V1.0
 * @Description: Redis分布式鎖測試類
 */
public class RedisDistributedLockTest {

    public static void main(String[] args) {
        try {
            // 鎖key
            String key = "REDIS_KEY_ORDER_ID_014";
            // UUID作value,在删除的時候用來判斷是不是目前線程加的鎖
            String value = UUID.randomUUID().toString();
            // 逾時時間(機關:ms)
            long expireTime = 10000;

            if (RedisDistributedLockTool.lock(key,value,expireTime)){
                // 1.加鎖完成後開啟一個守護線程給自己續命
                DaemonThread daemonThread = new DaemonThread();
                // 續命key
                daemonThread.setKey(key);
                // 過期前5s續命
                daemonThread.setATime(expireTime-5000);
                // 每次續命時長
                daemonThread.setBTime(expireTime);
                Thread thread = new Thread(daemonThread);
                thread.setDaemon(true);
                thread.start();

                //2.執行業務邏輯(為了看到效果,我們這裡用睡眠60s來模拟)
                System.out.println("執行業務邏輯start...");
                TimeUnit.MILLISECONDS.sleep(60000);
                System.out.println("執行業務邏輯end...");

                //3.解鎖 & 關閉守護線程
                RedisDistributedLockTool.unLock(key,value);
                thread.interrupt();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

分析:運作main方法結合代碼我們可以看出,剛開始加鎖時設定的逾時時間是10s,為了看的明顯,我們模拟執行業務邏輯sleep 60s,在設定的逾時時間10s過期前5s第一次給鎖續命,續命時重新設定逾時時間為10s,以後每隔(10s-5s)續命一次,直到業務邏輯執行完守護線程被中斷。

Redis分布式鎖的Java實作(基于Lua腳本)
Redis分布式鎖的Java實作(基于Lua腳本)

感興趣的小夥伴可以關注一下部落客的公衆号,1W+技術人的選擇,緻力于原創技術幹貨,包含Redis、RabbitMQ、Kafka、SpringBoot、SpringCloud、ELK等熱門技術的學習&資料。

Redis分布式鎖的Java實作(基于Lua腳本)