springboot實現redis分佈式鎖的兩種方式

一:基於 Redis 的 NX EX 參數

既然是選用了 Redis,那麼它就得具有排他性才行。同時它最好也有鎖的一些基本特性:

  • 高性能(加、解鎖時高性能)
  • 可以使用阻塞鎖與非阻塞鎖。
  • 不能出現死鎖。
  • 可用性(不能出現節點 down 掉後加鎖失敗)。

這裏利用 Redis set key 時的一個 NX 參數可以保證在這個 key 不存在的情況下寫入成功。並且再加上 EX 參數可以讓該 key 在超時之後自動刪除。

所以利用以上兩個特性可以保證在同一時刻只會有一個進程獲得鎖,並且不會出現死鎖(最壞的情況就是超時自動刪除 key)。

 

config中添加腳本配置


	@Bean
	public DefaultRedisScript<Long> defaultRedisScript() {
		DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
		defaultRedisScript.setResultType(Long.class);
		defaultRedisScript.setScriptText("if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end");
//        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("delete.lua")));
		return defaultRedisScript;
	}

添加工具類:

package com.fhgl.user.config;

import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Created by [email protected] on 2019/6/26
 */
public class RedisLock {

    private static final Long RELEASE_SUCCESS = 1L;


    //不安全
    public static boolean lock(String key, String value,RedisTemplate<String, Object> template) {
        //執行set命令
        //Boolean absent = template.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);//1
        template.setEnableTransactionSupport(true);
        template.multi();
        template.opsForValue().setIfAbsent(key,value);
        template.expire(key,2, TimeUnit.SECONDS);
        List result = template.exec(); // 這裏result會返回事務內每一個操作的結果,如果setIfAbsent操作失敗後,result[0]會爲false。
        if(true == (Boolean) result.get(0)){
            // todo something...
            return true;
        }
        return false;
    }

    public static boolean unlock(String key, String value,DefaultRedisScript<Long> redisScript,RedisTemplate<String, Object> template) {
        //使用Lua腳本:先判斷是否是自己設置的鎖,再執行刪除
        Long result = template.execute(redisScript, Arrays.asList(key,value));
        //返回最終結果
        return RELEASE_SUCCESS.equals(result);
    }

    public static Boolean setLock(String key, String value,RedisTemplate<String, Object> template) {
        SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
            List<Object> exec = null;
            @Override
            @SuppressWarnings("unchecked")
            public Boolean execute(RedisOperations operations) throws DataAccessException {
                operations.multi();
                template.opsForValue().setIfAbsent(key, value);
                template.expire(key,5, TimeUnit.SECONDS);
                exec = operations.exec();
                if(exec.size() > 0) {
                    return (Boolean) exec.get(0);
                }
                return false;
            }
        };
        return template.execute(sessionCallback);
    }



}

setLock加鎖、unLock解鎖,lock方法廢棄。在springboot2.0以上redis自帶了設置超時的命令,2.0以下要自己控制超時。具體

參考https://segmentfault.com/a/1190000017933697?utm_source=tag-newest

Test:

@Test
    public void testThread(){
        ExecutorService executorService= Executors.newFixedThreadPool(3);
        for (int i=0;i<3;i++){

            String uuid= UUID.randomUUID().toString();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("thread in ..."+Thread.currentThread().getName());

                    boolean flag= RedisLock.setLock("lock:timer", uuid,redisTemplate1);
                    System.out.println(Thread.currentThread().getName()+"開啓鎖:"+flag);
                    if(flag){
                        System.out.println("沒有線程在操作"+Thread.currentThread().getName()+"正常執行");
                        try {
                            Thread.sleep(200);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        boolean result= RedisLock.unlock("lock:timer",uuid,redisScript,redisTemplate1);
                        System.out.println(Thread.currentThread().getName()+"釋放鎖:"+result);
                    }else{
                        System.out.println("已有線程在操作"+Thread.currentThread().getName()+"退出");
                    }
                }
            });


        }

        try {
            Thread.sleep(2500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String uuid= UUID.randomUUID().toString();
        boolean flag= RedisLock.lock("lock:timer", uuid,redisTemplate1);
        System.out.println("主線程獲取鎖:"+flag);
        if(flag){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result= RedisLock.unlock("lock:timer",uuid,redisScript,redisTemplate1);
            System.out.println("主線程釋放鎖:"+result);

        }
    }

result:

thread in ...pool-7-thread-1
thread in ...pool-7-thread-2
thread in ...pool-7-thread-3
pool-7-thread-3開啓鎖:false
已有線程在操作pool-7-thread-3退出
pool-7-thread-2開啓鎖:false
已有線程在操作pool-7-thread-2退出
pool-7-thread-1開啓鎖:true
沒有線程在操作pool-7-thread-1正常執行
pool-7-thread-1釋放鎖:true
主線程獲取鎖:true
主線程釋放鎖:true

有一種風險就是操作線程阻塞後,鎖超時自動退出,這個時候新進線程會獲取到阻塞線程的鎖,同樣造成併發問題。這個可以把超時時間設置長一點,儘量保證操作線程正常退出。

二:redis官方推薦使用的Redisson高性能分佈式鎖

導入pom

<dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
         <version>2.7.0</version>
 </dependency>

component類:

package com.fhgl.user.utils;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

@Component
public class RedissonManager {
	private Config config;
	private RedissonClient client;
	private final static String LOCK_NAME = "RM:LOCK";
	@Autowired
	private RedissonConfig redissonConfig;
	@PostConstruct
	public void init() {
		System.out.println("====================redisson init");
		if(null == client) {
			System.out.println("====================redisson creating");
			config = redissonConfig.getConfig();
			client = Redisson.create(config);
			System.out.println("====================redisson create fin");
		}
	}
	
	public RedissonClient getClient() {
		config = redissonConfig.getConfig();
		RedissonClient c = Redisson.create(config);
		return c;
	}
	
	/**
	 * 獲取鎖
	 * @param timeout 自動解鎖時間(秒),不需要則傳-1
	 * */
	public RLock getLock(String key,long timeout) {
//		System.out.println("====================locking");
		RLock lock = client.getLock(key);
		if(-1 == timeout) {
			lock.lock();
		}else {
			lock.lock(timeout, TimeUnit.SECONDS);
		}
//		System.out.println("====================locked");
		return lock;
	}
	
	public boolean releaseLock(String key) {
		RLock lock = client.getLock(key);
		if(null != lock) {
//			System.out.println("hold count:" + lock.getHoldCount());
			lock.unlock();
			return true;
		}else {
//			System.out.println("=============沒有找到鎖");
			return false;
		}
//		System.out.println("=============unlock");
	}

	public boolean tryLock(String key){
		RLock lock = client.getLock(key);
		try {
			return lock.tryLock(0,3,TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			e.printStackTrace();
			return false;
		}
//		System.out.println("====================locked");
	}
}

getLock實現了阻塞獲取鎖隊列。

tryLock返回Boolean類型可以根據結果來處理業務邏輯。tryLock的第一個參數是獲取鎖阻塞時間,就是獲取不到會在這段時間一直嘗試獲取。第二個參數是超時時間。

Test:

@Test
    public void testRedis(){
        ExecutorService executorService= Executors.newFixedThreadPool(3);
        for (int i=0;i<3;i++){

            String uuid= UUID.randomUUID().toString();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("thread in ..."+Thread.currentThread().getName());

                    boolean result=manager.tryLock("lock:timer");

                    System.out.println(Thread.currentThread().getName()+"開啓鎖:"+result);
                    if(result){

                        System.out.println("沒有線程在操作"+Thread.currentThread().getName()+"正常執行");
                        try {
                            Thread.sleep(200);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        boolean result2=manager.releaseLock("lock:timer");
                        System.out.println(Thread.currentThread().getName()+"釋放鎖:"+result2);
                    }else{
                        System.out.println("已有線程在操作"+Thread.currentThread().getName()+"退出");
                    }
                }
            });


        }

        try {
            Thread.sleep(3100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String uuid= UUID.randomUUID().toString();
        boolean result =manager.tryLock("lock:timer");
        System.out.println("主線程獲取鎖:"+result);
        if(result){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean flag= manager.releaseLock("lock:timer");
            System.out.println("主線程釋放鎖:"+flag);

        }
    }

 

result:

thread in ...pool-7-thread-1
thread in ...pool-7-thread-2
thread in ...pool-7-thread-3
pool-7-thread-1開啓鎖:false
已有線程在操作pool-7-thread-1退出
pool-7-thread-2開啓鎖:true
沒有線程在操作pool-7-thread-2正常執行
pool-7-thread-3開啓鎖:false
已有線程在操作pool-7-thread-3退出
pool-7-thread-2釋放鎖:true
主線程獲取鎖:true
主線程釋放鎖:true

 

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