Redis實現分佈式鎖

非SpringBoot項目

基於jedis

package com.blog.www.util.lock;

import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;

import java.util.Collections;
import java.util.UUID;


/**
 * Redis實現分佈式鎖,基於 jedis
 * <br/>
 * 請使用最新實現 {@link RedisLock}
 * <br/>
 * 分佈式鎖一般有三種實現方式:1. 數據庫樂觀鎖;2. 基於Redis的分佈式鎖;3. 基於ZooKeeper的分佈式鎖。<br/>
 * 可靠性:<br/>
 * 爲了確保分佈式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:<br/>
 * <ul>
 * <li>
 * 1. 互斥性。在任意時刻,只有一個客戶端能持有鎖。
 * </li>
 * <li>
 * 2. 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
 * </li>
 * <li>
 * 3. 具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
 * </li>
 * <li>
 * 4. 解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
 * </li>
 * </ul>
 * 參考:<br/>
 * <ul>
 * <li>
 * <a href='https://www.cnblogs.com/linjiqin/p/8003838.html'>Redis分佈式鎖的正確實現方式</a>
 * </li>
 * <li>
 * <a href='https://www.cnblogs.com/cmyxn/p/9047848.html'>什麼是分佈式鎖及正確使用redis實現分佈式鎖</a>
 * </li>
 * <li>
 * <a href='https://blog.csdn.net/crystalqy/article/details/89024653'>基於Spring boot 2.1 使用redisson實現分佈式鎖</a>
 * </li>
 * </ul>
 *
 * @author :leigq
 * @date :2019/7/2 11:22
 */
@Slf4j
@Deprecated
public class RedisLockForJedis {

	private static final String LOCK_SUCCESS = "OK";
	private static final String SET_IF_NOT_EXIST = "NX";
	private static final String SET_WITH_EXPIRE_TIME = "PX";
	private static final Long RELEASE_SUCCESS = 1L;

	/**
	 * 嘗試獲取分佈式鎖
	 *
	 * @param jedis      Redis客戶端
	 * @param lockKey    鎖
	 * @param requestId  請求標識 可以使用UUID.randomUUID().toString()方法生成
	 * @param expireTime 超期時間 單位毫秒
	 * @return 是否獲取成功
	 */
	public static boolean lock(Jedis jedis, String lockKey, String requestId, int expireTime) {
		String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
		return LOCK_SUCCESS.equals(result);
	}

	/**
	 * 釋放分佈式鎖
	 *
	 * @param jedis     Redis客戶端
	 * @param lockKey   鎖
	 * @param requestId 請求標識 可以使用UUID.randomUUID().toString()方法生成
	 * @return 是否釋放成功
	 */
	public static boolean unLock(Jedis jedis, String lockKey, String requestId) {
		String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
		Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
		return RELEASE_SUCCESS.equals(result);
	}


    /**
     * 測試加鎖解鎖(測試通過)
     */
	public static void main(String[] args) {
        String requestId = UUID.randomUUID().toString();


	    /* 單機 jedis連接使用參考:https://blog.csdn.net/qianqian666888/article/details/79087930*/
		Jedis jedis = new Jedis("127.0.0.1", 6379);
        // 設置密碼
        jedis.auth("111111");
        // 加鎖
        boolean locked = lock(jedis, "lockKey", requestId, 60 * 60 * 1000);
        log.warn("locked result is : [{}]", locked);
        // 解鎖
        boolean unLocked = unLock(jedis, "lockKey", requestId);
        log.warn("unLocked result is : [{}]", unLocked);

    }

}

SpringBoot項目

客戶端選用 jedisLettuce 均可

package com.blog.www.util.lock;

import com.blog.www.config.RedisConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.Objects;


/**
 * Redis實現分佈式鎖,基於 RedisTemplate
 * <br/>
 * jedis 實現請看:<a href='https://blog.csdn.net/qq_28397259/article/details/80839072'>基於redisTemplate的redis的分佈式鎖正確打開方式</a>
 * <br/>
 * 分佈式鎖一般有三種實現方式:1. 數據庫樂觀鎖;2. 基於Redis的分佈式鎖;3. 基於ZooKeeper的分佈式鎖。<br/>
 * 可靠性:<br/>
 * 爲了確保分佈式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:<br/>
 * <ul>
 * <li>
 * 1. 互斥性。在任意時刻,只有一個客戶端能持有鎖。
 * </li>
 * <li>
 * 2. 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
 * </li>
 * <li>
 * 3. 具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
 * </li>
 * <li>
 * 4. 解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
 * </li>
 * </ul>
 * 參考:<br/>
 * <ul>
 * <li>
 * <a href='https://www.cnblogs.com/linjiqin/p/8003838.html'>Redis分佈式鎖的正確實現方式</a>
 * </li>
 * <li>
 * <a href='https://blog.csdn.net/long2010110/article/details/82911168'>springboot的RedisTemplate實現分佈式鎖</a>
 * </li>
 * </ul>
 * <a href='https://blog.csdn.net/weixin_38399962/article/details/82753763'>使用示例參考</a>:
 * <pre>
 * {@link @Autowired}
 * private {@link RedisLock} redisLock;
 *
 * boolean locked = redisLock.lock(lockKey, requestId, expireTime);
 * if (locked) {
 *     // 執行邏輯操作
 *     ......
 *     ......
 *     redisLock.unLock(lockKey, requestId);
 * } else {
 *     // 設置失敗次數計數器, 當到達5次時, 返回失敗
 *     int failCount = 1;
 *     while(failCount <= 5){
 *         // 等待100ms重試
 *         try {
 *             Thread.sleep(100l);
 *         } catch (InterruptedException e) {
 *             e.printStackTrace();
 *         }
 *         if (redisLock.lock(lockKey, requestId, expireTime)){
 *            // 執行邏輯操作
 *            ......
 *            ......
 *            redisLock.unLock(lockKey, requestId);
 *         }else{
 *             failCount ++;
 *         }
 *     }
 *     throw new RuntimeException("現在創建的人太多了, 請稍等再試");
 * }
 * </pre>
 *
 * @author :leigq
 * @date :2019/7/2 11:22
 */
@Slf4j
@Service
@SuppressWarnings(value = "unchecked")
public final class RedisLock {

	private final RedisTemplate redisTemp;

	/**
	 * 使用 RedisConfig 中的 redisTemp,自定義序列化 及 兼容 java8 時間
	 * @see RedisConfig#getRedisTemplate(RedisConnectionFactory)
	 */
	public RedisLock(@Qualifier("redisTemp") RedisTemplate redisTemp) {
		this.redisTemp = redisTemp;
	}

	/**
	 * 嘗試獲取分佈式鎖
	 *
	 * @param lockKey    鎖key
	 * @param requestId  請求標識 可以使用UUID.randomUUID().toString()方法生成
	 * @param expireTime 超期時間 單位秒
	 * @return 是否獲取成功
	 */
	public boolean lock(String lockKey, String requestId, int expireTime) {
		// 使用腳本,保證原子性
		RedisScript redisScript = RedisScript.of(LOCK_LUA, Long.class);
		Object lockResult = redisTemp.execute(redisScript, Collections.singletonList(lockKey), requestId, expireTime);
		log.warn("lock executeResult is [{}]", lockResult);
		return Objects.equals(SUCCESS, lockResult);
		// 不符合原子性
//		Boolean setIfAbsentResult = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId);
//		Boolean setExpireResult = stringRedisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
//		return setIfAbsentResult && setExpireResult;
	}

	/**
	 * 釋放分佈式鎖
	 *
	 * @param lockKey   鎖key
	 * @param requestId 請求標識 可以使用UUID.randomUUID().toString()方法生成
	 * @return 是否釋放成功
	 */
	public boolean unLock(String lockKey, String requestId) {
		RedisScript redisScript = RedisScript.of(UNLOCK_LUA, Long.class);
		Object unLockResult = redisTemp.execute(redisScript, Collections.singletonList(lockKey), requestId);
		log.warn("unLock executeResult is [{}]", unLockResult);
		return Objects.equals(SUCCESS, unLockResult);
	}

	private static final Long SUCCESS = 1L;

	// 加鎖 Lua 腳本
	private static final String LOCK_LUA;

	// 解鎖 Lua 腳本
	private static final String UNLOCK_LUA;

	static {
		// if redis.call('setNx', KEYS[1], ARGV[1]) then if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end end
		LOCK_LUA = "if redis.call('setNx', KEYS[1], ARGV[1]) " +
				"then " +
				"    if redis.call('get', KEYS[1]) == ARGV[1] " +
				"    then " +
				"        return redis.call('expire', KEYS[1], ARGV[2]) " +
				"    else " +
				"        return 0 " +
				"    end " +
				"end ";

		// if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
		UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] " +
				"then " +
				"    return redis.call('del', KEYS[1]) " +
				"else " +
				"    return 0 " +
				"end ";
	}

}

redisTemp 請在這獲取:SpringBoot2.0.X配置Redis

測試

package com.blog.www.util.lock;

import com.blog.www.base.BaseApplicationTests;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.UUID;

/**
 * RedisLock Tester.
 *
 * @author leigq
 * @version 1.0
 * @since <pre>10/17/2019</pre>
 */
public class RedisLockTest extends BaseApplicationTests {

	@Autowired
	private RedisLock redisLock;

	/**
	 * Redis分佈式鎖測試,基於RedisTemplate,測試通過
	 */
	@Test
	public void testLockAndUnLock() throws Exception {
		String requestId = UUID.randomUUID().toString();

		// 加鎖
		boolean locked = redisLock.lock("lockKey", requestId, 40);
		log.warn("locked result is : [{}]", locked);
		// 解鎖
		boolean unLocked = redisLock.unLock("lockKey", requestId);
		log.warn("unLocked result is : [{}]", unLocked);
	}

}

BaseApplicationTests.java

package com.blog.www.base;

import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * 測試基類,其他類繼承此類
 * <br/>
 * @author     :leigq
 * @date       :2019/8/13 17:17
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public abstract class BaseApplicationTests {

    protected Logger log = LoggerFactory.getLogger(this.getClass());

    private Long time;

    @Before
    public void setUp() {
        this.time = System.currentTimeMillis();
        log.info("==> 測試開始執行 <==");
    }

    @After
    public void tearDown() {
        log.info("==> 測試執行完成,耗時:{} ms <==", System.currentTimeMillis() - this.time);
    }
}

測試結果如下:

20191018111839.png

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