SpringBoot Redis實現分佈式鎖(親測可用)

集羣情況下,JDK的鎖是很容易出現問題的,這時候就需要用到分佈式鎖;最近用到了Redis實現分佈式鎖,這裏記錄一下。

基本原理:

       這裏使用了Redis的setNX,由於當某個 key 不存在的時候,SETNX 纔會設置該 key。且由於 Redis 採用單進程單線程模型,所以,不需要擔心併發的問題。那麼,就可以利用 SETNX 的特性維護一個 key,存在的時候,即鎖被某個線程持有;不存在的時候,沒有線程持有鎖。

該實例使用jMeter工具進行過壓測,可以抗住每秒幾百的併發。

1. pom文件添加依賴

<!-- redis依賴 -->
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-data-redis</artifactId>
	    <exclusions>
	        <exclusion>
	            <groupId>io.lettuce</groupId>
	            <artifactId>lettuce-core</artifactId>
	        </exclusion>
	    </exclusions>
	</dependency>
        
       <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>   

2. Redis配置

# Redis數據庫索引(默認爲0)  
spring.redis.database=0
# Redis服務器地址  
spring.redis.host=127.0.0.1
# Redis服務器連接端口  
spring.redis.port=6379
# Redis服務器連接密碼(默認爲空)  
spring.redis.password=
# 連接池最大連接數(使用負值表示沒有限制)  
spring.redis.pool.max-active=200
# 連接池最大阻塞等待時間(使用負值表示沒有限制)  
spring.redis.pool.max-wait=-1
# 連接池中的最大空閒連接  
spring.redis.pool.max-idle=10
# 連接池中的最小空閒連接  
spring.redis.pool.min-idle=1
# 連接超時時間(毫秒)  
spring.redis.timeout=10000
#是否在從池中取出連接前進行檢驗,如果檢驗失敗,則從池中去除連接並嘗試取出另一個  
redis.testOnBorrow=true  
#在空閒時檢查有效性, 默認false  
redis.testWhileIdle=true 

3. redis 分佈式鎖代碼實現

package com.example.demo.utils;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import redis.clients.jedis.Jedis;

/**
 * redis鎖工具類
 */
@Component
public class RedisLockUtil {

    private final static Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);

    @Autowired
    public StringRedisTemplate redisTemplate;

    /** 執行結果標識 */
    private static final Long SUCCESS = 1L;
    
    /** 設置成功標識 */
    private static final String LOCK_SUCCESS = "OK";
    
    /** key不存在時才設置值 */
    private static final String SET_IF_NOT_EXIST = "NX";
    
    /** 過期時間單位標識,EX:秒 */
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    
    /**
     * 鎖的過期時間(單位:秒)
     */
    private static final int LOCK_EXPIRE_TIME = 30;

    /**
     * 最大嘗試次數
     */
    private static final int MAX_ATTEMPTS = 100;

    /**
     * key前綴
     */
    private static final String KEY_PRE = "REDIS_LOCK_";

    /**
     * 獲取鎖
     * @param key
     * @return
     */
    public Boolean getLock(String key, String value) {
        return tryLock(key, value,MAX_ATTEMPTS);
    }

    /**
     * 嘗試加鎖
     * @param key
     * @param max_attempts 重試次數
     * @return
     */
    public Boolean tryLock(String key, String value, Integer max_attempts) {
        int attempt_counter = 0;
        key = KEY_PRE + key;
        while (attempt_counter < max_attempts) {
            if (setLock(key, value, LOCK_EXPIRE_TIME)) {
                System.out.println("獲取鎖成功,Redis Lock key : " + key + ", value : " + value);
                return Boolean.TRUE;
            }
          //  System.out.println("正在重試獲取鎖,Redis Lock key : " + key + ", value : " + value);
            attempt_counter++;
            if (attempt_counter >= max_attempts) {
                logger.error("獲取鎖失敗!, " + key);
            }
            try {
            	// 休眠時間 10-100 毫秒
                Thread.sleep((int)(Math.random() * 90 + 10));
            } catch (InterruptedException e) {
                logger.error("獲取鎖等待異常:{}",e.getMessage());
            }
        }

        return Boolean.FALSE;
    }

    /**
     * 加鎖 key不存在時才設置key value
     * @param lockKey   加鎖鍵
     * @param value  	加鎖客戶端唯一標識
     * @param seconds   鎖過期時間
     * @return
     */
    public Boolean setLock(String lockKey, String value, long seconds) {
        return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            String result = jedis.set(lockKey, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
            if (LOCK_SUCCESS.equals(result)) {
            	return Boolean.TRUE;
            }
            return Boolean.FALSE;
        });
    }
    

    /**
     * 解鎖
     * @param key	加鎖鍵
     * @param value
     * @return
     */
    public boolean unLock(String key, String value) {
        key = KEY_PRE + key;
        try {
            String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
            RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
            List<String> keys = Arrays.asList(key, value);
            Object result = redisTemplate.execute(redisScript, keys);
            if(SUCCESS.equals(result)) {
                System.out.println("釋放Redis鎖 : " + key + ", value : " + value);
                return true;
            }

        } catch (Exception e) {
            logger.error("釋放Redis鎖異常:",e);
        }

        return false;
    }

    /**
     * 生成加鎖的唯一字符串
     * @return 唯一字符串
     */
    public String fetchLockValue() {
        return UUID.randomUUID().toString() + "-" + System.currentTimeMillis();
    }
}

4. 使用實例

/**
   * 使用實例
   * @return
   */
 @Transactional(rollbackFor=Exception.class)
 public String updateInfoLock(){
	  // 鎖key
	  String key = "updateInfo";
	  // 鎖的唯一字符串
	  String value = redisLockUtil.fetchLockValue();  
	  // 獲取鎖
	  if (!redisLockUtil.getLock(key,value)) {
		log.info("沒搶到鎖!");
		return "服務器繁忙,請稍後重試!";
	  }
	  
	  try {
		  // 業務邏輯代碼
		} catch (Exception e) {
			log.error("XXX異常",e);
			// 捕獲異常之後需要 手動回滾事務
			TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
		}finally{
			 // 釋放鎖
			 redisLockUtil.unLock(key, value);
		}
	   return "操作成功!";
  }

注意:釋放鎖的執行代碼最好是放在 finally 代碼塊裏面,防止出現異常或者忘記釋放鎖的情況;

 

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