1. 程式人生 > >spring boot redis分散式鎖

spring boot redis分散式鎖

隨著現在分散式架構越來越盛行,在很多場景下需要使用到分散式鎖。分散式鎖的實現有很多種,比如基於資料庫、 zookeeper 等,本文主要介紹使用 Redis 做分散式鎖的方式,並封裝成spring boot starter,方便使用

鎖是針對某個資源,保證其訪問的互斥性,在實際使用當中,這個資源一般是一個字串。使用 Redis 實現鎖,主要是將資源放到 Redis 當中,利用其原子性,當其他執行緒訪問時,如果 Redis 中已經存在這個資源,就不允許之後的一些操作。spring boot使用 Redis 的操作主要是通過 RedisTemplate 來實現,一般步驟如下:

  1. 將鎖資源放入 Redis

     (注意是當key不存在時才能放成功,所以使用 setIfAbsent 方法):

redisTemplate.opsForValue().setIfAbsent("key", "value");
  1. 設定過期時間

redisTemplate.expire("key", 30000, TimeUnit.MILLISECONDS);
  1. 釋放鎖

redisTemplate.delete("key");

一般情況下,這樣的實現就能夠滿足鎖的需求了,但是如果在呼叫 setIfAbsent 方法之後執行緒掛掉了,即沒有給鎖定的資源設定過期時間,預設是永不過期,那麼這個鎖就會一直存在。所以需要保證設定鎖及其過期時間兩個操作的原子性,spring data的 RedisTemplate

 當中並沒有這樣的方法。但是在jedis當中是有這種原子操作的方法的,需要通過 RedisTemplate 的 execute 方法獲取到jedis裡操作命令的物件,程式碼如下:

String result = redisTemplate.execute(new RedisCallback<String>() {	@Override
	public String doInRedis(RedisConnection connection) throws DataAccessException {
		JedisCommands commands = (JedisCommands) connection.getNativeConnection();		return commands.set(key, "鎖定的資源", "NX", "PX", expire);
	}
});

注意: Redis 從2.6.12版本開始 set 命令支援 NX 、 PX 這些引數來達到 setnx 、 setex 、 psetex 命令的效果,文件參見: 

NX: 表示只有當鎖定資源不存在的時候才能 SET 成功。利用 Redis 的原子性,保證了只有第一個請求的執行緒才能獲得鎖,而之後的所有執行緒在鎖定資源被釋放之前都不能獲得鎖。

PX: expire 表示鎖定的資源的自動過期時間,單位是毫秒。具體過期時間根據實際場景而定

這樣在獲取鎖的時候就能夠保證設定 Redis 值和過期時間的原子性,避免前面提到的兩次 Redis 操作期間出現意外而導致的鎖不能釋放的問題。但是這樣還是可能會存在一個問題,考慮如下的場景順序:

  • 執行緒T1獲取鎖

  • 執行緒T1執行業務操作,由於某些原因阻塞了較長時間

  • 鎖自動過期,即鎖自動釋放了

  • 執行緒T2獲取鎖

  • 執行緒T1業務操作完畢,釋放鎖(其實是釋放的執行緒T2的鎖)

按照這樣的場景順序,執行緒T2的業務操作實際上就沒有鎖提供保護機制了。所以,每個執行緒釋放鎖的時候只能釋放自己的鎖,即鎖必須要有一個擁有者的標記,並且也需要保證釋放鎖的原子性操作。

因此在獲取鎖的時候,可以生成一個隨機不唯一的串放入當前執行緒中,然後再放入 Redis 。釋放鎖的時候先判斷鎖對應的值是否與執行緒中的值相同,相同時才做刪除操作。

因此我們可以通過 Lua 指令碼來達到釋放鎖的原子操作,定義 Lua 指令碼如下:

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

具體意思可以參考上面提供的文件地址

使用 RedisTemplate 執行的程式碼如下:

// 使用Lua指令碼刪除Redis中匹配value的key,可以避免由於方法執行時間過長而redis鎖自動過期失效的時候誤刪其他執行緒的鎖// spring自帶的執行指令碼方法中,叢集模式直接丟擲不支援執行指令碼的異常,所以只能拿到原redis的connection來執行指令碼Long result = redisTemplate.execute(new RedisCallback<Long>() {	public Long doInRedis(RedisConnection connection) throws DataAccessException {
		Object nativeConnection = connection.getNativeConnection();		// 叢集模式和單機模式雖然執行指令碼的方法一樣,但是沒有共同的介面,所以只能分開執行
		// 叢集模式
		if (nativeConnection instanceof JedisCluster) {			return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
		}		// 單機模式
		else if (nativeConnection instanceof Jedis) {			return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
		}		return 0L;
	}
});

程式碼中分為叢集模式和單機模式,並且兩者的方法、引數都一樣,原因是spring封裝的執行指令碼的方法中( RedisConnection 介面繼承於 RedisScriptingCommands 介面的 eval 方法),叢集模式的方法直接丟擲了不支援執行指令碼的異常(雖然實際是支援的),所以只能拿到 Redis 的connection來執行指令碼,而 JedisCluster 和 Jedis中的方法又沒有實現共同的介面,所以只能分開呼叫。

spring封裝的叢集模式執行指令碼方法原始碼:

# JedisClusterConnection.java/**
 * (non-Javadoc)
 * @see org.springframework.data.redis.connection.RedisScriptingCommands#eval(byte[], org.springframework.data.redis.connection.ReturnType, int, byte[][])
 */
@Override
public <T> T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) {	throw new InvalidDataAccessApiUsageException("Eval is not supported in cluster environment.");}

至此,我們就完成了一個相對可靠的 Redis 分散式鎖,但是,在叢集模式的極端情況下,還是可能會存在一些問題,比如如下的場景順序( 本文暫時不深入開展 ):

  • 執行緒T1獲取鎖成功

  • Redis 的master節點掛掉,slave自動頂上

  • 執行緒T2獲取鎖,會從slave節點上去判斷鎖是否存在,由於Redis的master slave複製是非同步的,所以此時執行緒T2可能成功獲取到鎖

為了可以以後擴充套件為使用其他方式來實現分散式鎖,定義了介面和抽象類,所有的原始碼如下:

# DistributedLock.java 頂級介面/**
 * @author fuwei.deng
 * @date 2017年6月14日 下午3:11:05
 * @version 1.0.0
 */public interface DistributedLock {	
	public static final long TIMEOUT_MILLIS = 30000;	
	public static final int RETRY_TIMES = Integer.MAX_VALUE;	
	public static final long SLEEP_MILLIS = 500;	public boolean lock(String key);	
	public boolean lock(String key, int retryTimes);	
	public boolean lock(String key, int retryTimes, long sleepMillis);	
	public boolean lock(String key, long expire);	
	public boolean lock(String key, long expire, int retryTimes);	
	public boolean lock(String key, long expire, int retryTimes, long sleepMillis);	
	public boolean releaseLock(String key);
}
# AbstractDistributedLock.java 抽象類,實現基本的方法,關鍵方法由子類去實現/**
 * @author fuwei.deng
 * @date 2017年6月14日 下午3:10:57
 * @version 1.0.0
 */public abstract class AbstractDistributedLock implements DistributedLock {	@Override
	public boolean lock(String key) {
		return lock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);
	}	@Override
	public boolean lock(String key, int retryTimes) {
		return lock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);
	}	@Override
	public boolean lock(String key, int retryTimes, long sleepMillis) {
		return lock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);
	}	@Override
	public boolean lock(String key, long expire) {
		return lock(key, expire, RETRY_TIMES, SLEEP_MILLIS);
	}	@Override
	public boolean lock(String key, long expire, int retryTimes) {
		return lock(key, expire, retryTimes, SLEEP_MILLIS);
	}

}
# RedisDistributedLock.java Redis分散式鎖的實現import java.util.ArrayList;import java.util.List;import java.util.UUID;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.dao.DataAccessException;import org.springframework.data.redis.connection.RedisConnection;import org.springframework.data.redis.core.RedisCallback;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.util.StringUtils;import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisCluster;import redis.clients.jedis.JedisCommands;/**
 * @author fuwei.deng
 * @date 2017年6月14日 下午3:11:14
 * @version 1.0.0
 */public class RedisDistributedLock extends AbstractDistributedLock {	
	private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);	
	private RedisTemplate<Object, Object> redisTemplate;	
	private ThreadLocal<String> lockFlag = new ThreadLocal<String>();	
	public static final String UNLOCK_LUA;    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }	public RedisDistributedLock(RedisTemplate<Object, Object> redisTemplate) {		super();		this.redisTemplate = redisTemplate;
	}

	@Override	public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {		boolean result = setRedis(key, expire);		// 如果獲取鎖失敗,按照傳入的重試次數進行重試
		while((!result) && retryTimes-- > 0){			try {
				logger.debug("lock failed, retrying..." + retryTimes);
				Thread.sleep(sleepMillis);
			} catch (InterruptedException e) {				return false;
			}
			result = setRedis(key, expire);
		}		return result;
	}	
	private boolean setRedis(String key, long expire) {		try {
			String result = redisTemplate.execute(new RedisCallback<String>() {
				@Override				public String doInRedis(RedisConnection connection) throws DataAccessException {
					JedisCommands commands = (JedisCommands) connection.getNativeConnection();
					String uuid = UUID.randomUUID().toString();
					lockFlag.set(uuid);					return commands.set(key, uuid, "NX", "PX", expire);
				}
			});			return !StringUtils.isEmpty(result);
		} catch (Exception e) {
			logger.error("set redis occured an exception", e);
		}		return false;
	}
	
	@Override	public boolean releaseLock(String key) {		// 釋放鎖的時候,有可能因為持鎖之後方法執行時間大於鎖的有效期,此時有可能已經被另外一個執行緒持有鎖,所以不能直接刪除
		try {
			List<String> keys = new ArrayList<String>();
			keys.add(key);
			List<String> args = new ArrayList<String>();
			args.add(lockFlag.get());			// 使用lua指令碼刪除redis中匹配value的key,可以避免由於方法執行時間過長而redis鎖自動過期失效的時候誤刪其他執行緒的鎖
			// spring自帶的執行指令碼方法中,叢集模式直接丟擲不支援執行指令碼的異常,所以只能拿到原redis的connection來執行指令碼
			
			Long result = redisTemplate.execute(new RedisCallback<Long>() {				public Long doInRedis(RedisConnection connection) throws DataAccessException {
					Object nativeConnection = connection.getNativeConnection();					// 叢集模式和單機模式雖然執行指令碼的方法一樣,但是沒有共同的介面,所以只能分開執行
					// 叢集模式
					if (nativeConnection instanceof JedisCluster) {						return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
					}					// 單機模式
					else if (nativeConnection instanceof Jedis) {						return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
					}					return 0L;
				}
			});			
			return result != null && result > 0;
		} catch (Exception e) {
			logger.error("release lock occured an exception", e);
		}		return false;
	}
	
}

二. 基於 AOP 的 Redis 分散式鎖

在實際的使用過程中,分散式鎖可以封裝好後使用在方法級別,這樣就不用每個地方都去獲取鎖和釋放鎖,使用起來更加方便。

  • 首先定義個註解:

import java.lang.annotation.ElementType;import java.lang.annotation.Inherited;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/**
 * @author fuwei.deng
 * @date 2017年6月14日 下午3:10:36
 * @version 1.0.0
 */@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic @interface RedisLock {	/** 鎖的資源,redis的key*/
	String value() default "default";	
	/** 持鎖時間,單位毫秒*/
	long keepMills() default 30000;	
	/** 當獲取失敗時候動作*/
	LockFailAction action() default LockFailAction.CONTINUE;	
	public enum LockFailAction{        /** 放棄 */
        GIVEUP,        /** 繼續 */
        CONTINUE;
    }	
	/** 重試的間隔時間,設定GIVEUP忽略此項*/
    long sleepMills() default 200;    
    /** 重試次數*/
    int retryTimes() default 5;
}
  • 裝配分散式鎖的bean

import org.springframework.boot.autoconfigure.AutoConfigureAfter;import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.core.RedisTemplate;import com.itopener.lock.redis.spring.boot.autoconfigure.lock.DistributedLock;import com.itopener.lock.redis.spring.boot.autoconfigure.lock.RedisDistributedLock;/**
 * @author fuwei.deng
 * @date 2017年6月14日 下午3:11:31
 * @version 1.0.0
 */@[email protected](RedisAutoConfiguration.class)
public class DistributedLockAutoConfiguration {
	
	@Bean
	@ConditionalOnBean(RedisTemplate.class)
	public DistributedLock redisDistributedLock(RedisTemplate<Object, Object> redisTemplate){		return new RedisDistributedLock(redisTemplate);
	}
	
}
  • 定義切面(spring boot配置方式)

import java.lang.reflect.Method;import java.util.Arrays;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.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.autoconfigure.AutoConfigureAfter;import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;import org.springframework.context.annotation.Configuration;import org.springframework.util.StringUtils;import com.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock;import com.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock.LockFailAction;import com.itopener.lock.redis.spring.boot.autoconfigure.lock.DistributedLock;/**
 * @author fuwei.deng
 * @date 2017年6月14日 下午3:11:22
 * @version 1.0.0
 */@[email protected]@ConditionalOnClass(DistributedLock.class)@AutoConfigureAfter(DistributedLockAutoConfiguration.class)public class DistributedLockAspectConfiguration {	
	private final Logger logger = LoggerFactory.getLogger(DistributedLockAspectConfiguration.class);	
	@Autowired
	private DistributedLock distributedLock;	@Pointcut("@annotation(com.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock)")
	private void lockPoint(){
		
	}	
	@Around("lockPoint()")
	public Object around(ProceedingJoinPoint pjp) throws Throwable{
		Method method = ((MethodSignature) pjp.getSignature()).getMethod();
		RedisLock redisLock = method.getAnnotation(RedisLock.class);
		String key = redisLock.value();		if(StringUtils.isEmpty(key)){
			Object[] args = pjp.getArgs();
			key = Arrays.toString(args);
		}
		int retryTimes = redisLock.action().equals(LockFailAction.CONTINUE) ? redisLock.retryTimes() : 0;
		boolean lock = distributedLock.lock(key, redisLock.keepMills(), retryTimes, redisLock.sleepMills());		if(!lock) {
			logger.debug("get lock failed : " + key);			return null;
		}		
		//得到鎖,執行方法,釋放鎖
		logger.debug("get lock success : " + key);		try {			return pjp.proceed();
		} catch (Exception e) {
			logger.error("execute locked method occured an exception", e);
		} finally {
			boolean releaseResult = distributedLock.releaseLock(key);
			logger.debug("release lock : " + key + (releaseResult ? " success" : " failed"));
		}		return null;
	}
}
  • spring boot starter還需要在 resources/META-INF 中新增 spring.factories 檔案

# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itopener.lock.redis.spring.boot.autoconfigure.DistributedLockAutoConfiguration,\
com.itopener.lock.redis.spring.boot.autoconfigure.DistributedLockAspectConfiguration

這樣封裝之後,使用spring boot開發的專案,直接依賴這個starter,就可以在方法上加 RedisLock 註解來實現分散式鎖的功能了,當然如果需要自己控制,直接注入分散式鎖的bean即可

@Autowiredprivate DistributedLock distributedLock;

如果需要使用其他的分散式鎖實現,繼承 AbstractDistributedLock 後實現獲取鎖和釋放鎖的方法即可

原始碼地址 :

  •  (目錄:itopener-parent / spring-boot-starters-parent / lock-redis-spring-boot-starter-parent)

文章來源:https://my.oschina.net/dengfuwei/blog/1600681

相關推薦

spring boot redis分散式

隨著現在分散式架構越來越盛行,在很多場景下需要使用到分散式鎖。分散式鎖的實現有很多種,比如基於資料庫、 zookeeper 等,本文主要介紹使用 Redis 做分散式鎖的方式,並封裝成spring boot starter,方便使用 鎖是針對某個資源,保證其訪問的

spring boot專案中redis分散式實現 程式碼模板

1,在application.properties中配置redis主機:spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password=XXX2,新增redis配置檔案:cache/RadisL

Spring Boot Redis 實現分散式,真香!!

之前看很多人手寫分散式鎖,其實 Spring Boot 現在已經做的足夠好了,開箱即用,支援主流的 Redis、Zookeeper 中介軟體,另外還支援 JDBC。 本篇棧長以 Redis 為例(這也是用得最多的方案),教大家如何利用 Spring Boot 整合 Redis 實現快取,如何簡單、快速實現

spring boot redis分布式

supported 分布式架構 tsig utils ali 成了 down eva -- 隨著現在分布式架構越來越盛行,在很多場景下需要使用到分布式鎖。分布式鎖的實現有很多種,比如基於數據庫、 zookeeper 等,本文主要介紹使用 Redis 做分布式鎖的方式,並封

spring-boot+Redis實現簡單的分散式叢集session共享

  寫在前面:      首先宣告,筆者是一名Java程式設計屆的小學生。前面一直在幾家公司裡面做開發,其實都是一些傳統的專案,對於像分散式啦,叢集啦一些大型的專案接觸的很少,所以一直沒有自己整合和實現過。由於最近幾天專案不是很忙,自己又有點時間

Redis實現分散式spring定時任務叢集應用Redis分散式

         之前2片文章介紹了 描述:              不管用不用動態執行,單機服務都是沒有問題的,但是如果服務是叢集模式下,那麼一個任務在每臺機器都會執行一次,這肯定不是我們需要的,我們要實現的是整個叢集每次只有一個任務執行成功,但是spring

spring boot redis

redis spring bootdependency <dependency> <groupId>org.springframework.boot</groupId> <artifactId>sp

Spring Boot Redis Cluster 實戰幹貨

code ring from 連接超時 row comm reply with 大連 添加配置信息 spring.redis: database: 0 # Redis數據庫索引(默認為0) #host: 192.168.1.8 #port: 6379 pas

如何設計redis分散式

文章目錄 分散式鎖的實現有哪些? 1.Memcached分散式鎖 2.Redis分散式鎖 3.Zookeeper分散式鎖 4.Chubby 如何用Redis實現分散式鎖? 1.加鎖 2.解鎖

註解形式實現,Redis分散式

Redis工具類參考我的博文:https://blog.csdn.net/weixin_38399962/article/details/82753763 一個註解就可以實現分散式鎖?這麼神奇麼? 首先定義註解: /** * Description:分散式Redis鎖 * User:

redis分散式的實現方式

    前言:分散式鎖的實現方式一般有三種,1:基於資料庫的樂觀鎖。2:基於redis的分散式鎖。3:基於zk的分散式鎖,本文主要介紹第二種實現,由於以前一直是單機寫筆記,所以第一次寫有寫的不好的地方歡迎大家指正。     網上對於redis分散式鎖的實現

Redis實踐 -使用Redis分散式

使用Redis來實現分散式鎖 public class LockTest { private final Jedis jedis; public LockTest() { jedis=new Jedis("localhost",6379); je

Spring Boot Redis session共享

Spring Boot Redis session共享 配置Maven依賴 RedisSessionConfig application.properties application.yml 配置Maven依賴 <dep

redis-分散式的正確實現方式

分散式鎖一般有三種實現方式:1. 資料庫樂觀鎖;2. 基於Redis的分散式鎖;3. 基於ZooKeeper的分散式鎖。本篇部落格將介紹第二種方式,基於Redis實現分散式鎖。雖然網上已經有各種介紹Redis分散式鎖實現的部落格,然而他們的實現卻有著各種各樣的問題,為了避免誤人子弟,本篇部落格將詳細

Redis 分散式(二)

續上篇 Redis 分散式鎖 超時問題 Redis 的分散式鎖不能解決超時問題,如果在加鎖和釋放鎖之間的邏輯執行的太長,以至於超出了鎖的超時限制,就會出現問題。因為這時候鎖A過期了,第二個執行緒重新持有了這把鎖A,但是緊接著第一個執行緒執行完了業務邏輯,就把鎖A給釋放了,問題是第

redis - 分散式的正確實現方式2

分散式應用進行邏輯處理時經常會遇到併發問題。 比如一個操作要修改使用者的狀態,修改狀態需要先讀出使用者的狀態,在記憶體裡進行修改,改完了再存回去。如果這樣的操作同時進行了,就會出現併發問題,因為讀取和儲存狀態這兩個操作不是原子的。(Wiki 解釋:所謂原子操作是指不會被執行緒排程機制打斷的操作;

redis - 分散式的正確實現方式

前言 分散式鎖一般有三種實現方式:1. 資料庫樂觀鎖;2. 基於Redis的分散式鎖;3. 基於ZooKeeper的分散式鎖。本篇部落格將介紹第二種方式,基於Redis實現分散式鎖。雖然網上已經有各種介紹Redis分散式鎖實現的部落格,然而他們的實現卻有著各種各樣的問題,為了避免誤人子弟,本篇部

Java 正確實現 redis 分散式

Java 正確實現 redis 分散式鎖 Java 正確實現 redis 分散式鎖 1 源起 2 我想要的效果 3 擼起袖子開幹 3.1 匯入 jedis 依賴

拜託,面試請不要再問我Redis分散式的實現原理!【石杉的架構筆記】

歡迎關注個人公眾號:石杉的架構筆記(ID:shishan100) 週一至五早8點半!精品技術文章準時送上! 目錄 一、寫在前面 二、Redisson實現Redis分散式鎖的底層原理       (1)加鎖機制       (2)鎖互斥機制  

拜託,面試請不要再問我Redis分散式的實現原理!

目錄 一、寫在前面 二、Redisson實現Redis分散式鎖的底層原理       (1)加鎖機制       (2)鎖互斥機制       (3)watch dog自動延期機制   &nbs