Redis之分佈式鎖的實現方案 - 如何優雅地實現分佈式鎖(JAVA)html
分佈式鎖
: 是控制分佈式系統之間同步訪問共享資源的一種方式。spring-data-redis
: Spring針對redis的封裝, 配置簡單, 提供了與Redis存儲交互的抽象封裝, 十分優雅, 也極具擴展性, 推薦讀一讀源碼Lua
: Lua 是一種輕量小巧的腳本語言, 可在redis執行.本文闡述了Redis分佈式鎖的一種簡單JAVA實現及優化進階, 實現了自動解鎖、自定義異常、重試、註解鎖等功能, 嘗試用更優雅簡潔的代碼完成分佈式鎖.git
Redis具備極高的性能, 且其命令對分佈式鎖支持友好, 藉助SET
命令便可實現加鎖處理.github
SET
redis
EX
seconds -- Set the specified expire time, in seconds.PX
milliseconds -- Set the specified expire time, in milliseconds.NX
-- Only set the key if it does not already exist.XX
-- Only set the key if it already exist.
簡單實現spring
作法爲set if not exist(若是不存在則賦值), redis命令爲原子操做, 因此單獨使用set
命令時不用擔憂併發致使異常.shell
具體代碼實現以下: (spring-data-redis:2.1.6
)數據庫
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
複製代碼
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisSerializer keySerializer = new StringRedisSerializer();
RedisSerializer<?> serializer = new StringRedisSerializer();
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(factory);
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
複製代碼
/** * try lock * @author piaoruiqing * * @param key lock key * @param value value * @param timeout timeout * @param unit time unit * @return */
public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) {
return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
}
複製代碼
以上代碼即完成了一個簡單的分佈式鎖功能:bash
其中redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
即爲執行redis命令:併發
redis> set dlock:test-try-lock a EX 10 NX
OK
redis> set dlock:test-try-lock a EX 10 NX
null
複製代碼
spring-data-redis
分佈式鎖實現及注意事項方法
Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);
是在2.1版本中新增的, 早期版本中setIfAbsent沒法同時指定過時時間, 若先使用setIfAbsent
再設置key的過時時間, 會存在產生死鎖的風險, 故舊版本中須要使用另外的寫法進行實現. 以spring-data-redis:1.8.20
爲例
/** * try lock * @author piaoruiqing * * @param key lock key * @param value value * @param timeout timeout * @param unit time unit * @return */
public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) {
return redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
JedisCommands commands = (JedisCommands)connection.getNativeConnection();
String result = commands.set(key, value, "NX", "PX", unit.toMillis(timeout));
return "OK".equals(result);
}
});
}
複製代碼
spring-data-redis:1.8.20
默認redis客戶端爲jedis
, 可經過getNativeConnection
直接調用jedis方法進行操做. 新舊版本實現方式最終效果相同.
基於AOP實現分佈式鎖註解工具 - 不只能用, 並且好用
自動解鎖、重試: 上一節針對分佈式鎖的簡單實現可知足基本需求, 但仍有較多可優化改進之處, 本小節將針對分佈式鎖自動解鎖及重試進行優化
實現
AutoCloseable
接口, 可以使用try-with-resource
方便地完成自動解鎖.
/** * distributed lock * @author piaoruiqing * * @since JDK 1.8 */
abstract public class DistributedLock implements AutoCloseable {
private final Logger LOGGER = LoggerFactory.getLogger(getClass());
/** * release lock * @author piaoruiqing */
abstract public void release();
/* * (non-Javadoc) * @see java.lang.AutoCloseable#close() */
@Override
public void close() throws Exception {
LOGGER.debug("distributed lock close , {}", this.toString());
this.unlock();
}
}
複製代碼
RedisDistributedLock
是Redis分佈式鎖的抽象, 繼承了DistributedLock
並實現了unlock接口.
/** * redis distributed lock * * @author piaoruiqing * @date: 2019/01/12 23:20 * * @since JDK 1.8 */
public class RedisDistributedLock extends DistributedLock {
private RedisOperations<String, String> operations;
private String key;
private String value;
private static final String COMPARE_AND_DELETE = // (一)
"if redis.call('get',KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call('del',KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
/** * @param operations * @param key * @param value */
public RedisDistributedLock(RedisOperations<String, String> operations, String key, String value) {
this.operations = operations;
this.key = key;
this.value = value;
}
/* * (non-Javadoc) * @see com.piaoruiqing.demo.distributed.lock.DistributedLock#release() */
@Override
public void release() { // (二)
List<String> keys = Collections.singletonList(key);
operations.execute(new DefaultRedisScript<String>(COMPARE_AND_DELETE), keys, value);
}
/* * (non-Javadoc) * @see java.lang.Object#toString() */
@Override
public String toString() {
return "RedisDistributedLock [key=" + key + ", value=" + value + "]";
}
}
複製代碼
對比鎖的值
+刪除
成爲原子操做, 確保解鎖操做的正確性. 簡單來講就是防止刪了別人的鎖
. 例如: 線程A方法未執行完畢時鎖超時了, 隨後B線程也獲取到了該鎖(key相同), 但此時若是A線程方法執行完畢嘗試解鎖, 若是不比對value, 那麼A將刪掉B的鎖, 這時候C線程又能加鎖, 業務將產生更嚴重的混亂.(不要過度依賴分佈式鎖, 在數據一致性要求較高的狀況下, 數據庫層面也要進行必定的處理, 例如惟一鍵約束、事務等來確保數據的正確)RedisOperations
執行Lua腳本進行解鎖操做./** * @author piaoruiqing * @param key lock key * @param timeout timeout * @param retries number of retries * @param waitingTime retry interval * @return * @throws InterruptedException */
public DistributedLock acquire(String key, long timeout, int retries, long waitingTime) throws InterruptedException {
final String value
= RandomStringUtils.randomAlphanumeric(4) + System.currentTimeMillis(); // (一)
do {
Boolean result
= stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS); // (二)
if (result) {
return new RedisDistributedLock(stringRedisTemplate, key, value);
}
if (retries > NumberUtils.INTEGER_ZERO) {
TimeUnit.MILLISECONDS.sleep(waitingTime);
}
if(Thread.currentThread().isInterrupted()){
break;
}
} while (retries-- > NumberUtils.INTEGER_ZERO);
return null;
}
複製代碼
UUID.randomUUID()
在高併發狀況下性能不佳.此代碼已經能夠知足自動解鎖和重試的需求了, 使用方法:
// 根據key加鎖, 超時時間10000ms, 重試2次, 重試間隔500ms
try(DistributedLock lock = redisLockService.acquire(key, 10000, 2, 500);){
// do something
}
複製代碼
但還能夠再優雅一點, 將模板代碼封裝起來, 可支持Lambda表達式:
/** * lock handler * @author piaoruiqing * * @since JDK 1.8 */
@FunctionalInterface // (一)
public interface LockHandler<T> {
/** * the logic you want to execute * * @author piaoruiqing * * @return * @throws Throwable */
T handle() throws Throwable; // (二)
}
複製代碼
使用LockHandler
完成加鎖的實現:
public <T> T tryLock(String key, LockHandler<T> handler, long timeout, boolean autoUnlock, int retries, long waitingTime) throws Throwable {
try (DistributedLock lock = this.acquire(key, timeout, retries, waitingTime);) {
if (lock != null) {
LOGGER.debug("get lock success, key: {}", key);
return handler.handle();
}
LOGGER.debug("get lock fail, key: {}", key);
return null;
}
}
複製代碼
此時能夠經過比較優雅的方式使用分佈式鎖來完成編碼:
@Test
public void testTryLock() throws Throwable {
final String key = "dlock:test-try-lock";
AnyObject anyObject = redisLockService.tryLock(key, () -> {
// do something
return new AnyObject();
}, 10000, true, 0, 0);
}
複製代碼
自定義異常: 前文中針對分佈式鎖的封裝可知足多數業務場景, 可是考慮這樣一種狀況, 若是業務自己會返回
NULL
當前的實現方式可能會存在錯誤的處理, 由於獲取鎖失敗也會返回NULL
. 避免返回NULL
當然是一種解決方式, 但沒法知足全部的場景, 此時支持自定義異常或許是個不錯的選擇.
實現起來很容易, 在原代碼的基礎之上增長onFailure
參數, 若是鎖爲空直接拋出異常便可.
public <T> T tryLock(String key, LockHandler<T> handler, long timeout, boolean autoUnlock, int retries, long waitingTime, Class<? extends RuntimeException> onFailure) throws Throwable { // (一)
try (DistributedLock lock = this.getLock(key, timeout, retries, waitingTime);) {
if (lock != null) {
LOGGER.debug("get lock success, key: {}", key);
return handler.handle();
}
LOGGER.debug("get lock fail, key: {}", key);
if (null != onFailure) {
throw onFailure.newInstance(); // (二)
}
return null;
}
}
複製代碼
Class<? extends RuntimeException>
限定onFailure
必須是RuntimeException
或其子類. 筆者認爲使用RuntimeException
在語義上更容易理解. 若有須要使用其餘異常也何嘗不可(如獲取鎖失敗須要統一處理等狀況).結合APO優雅地使用註解完成分佈式鎖:
爲了減少篇幅摺疊部分註釋
/** * distributed lock * @author piaoruiqing * @date: 2019/01/12 23:15 * * @since JDK 1.8 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributedLockable {
/** timeout of the lock */
long timeout() default 5L;
/** time unit */
TimeUnit unit() default TimeUnit.MILLISECONDS;
/** number of retries */
int retries() default 0;
/** interval of each retry */
long waitingTime() default 0L;
/** key prefix */
String prefix() default "";
/** parameters that construct a key */
String[] argNames() default {};
/** construct a key with parameters */
boolean argsAssociated() default true;
/** whether unlock when completed */
boolean autoUnlock() default true;
/** throw an runtime exception while fail to get lock */
Class<? extends RuntimeException> onFailure() default NoException.class;
/** no exception */
public static final class NoException extends RuntimeException {
private static final long serialVersionUID = -7821936618527445658L;
}
}
複製代碼
timeout
: 超時時間unit
: 時間單位retries
: 重試次數waitingTime
: 重試間隔時間prefix
: key前綴, 默認爲包名
+類名
+方法名
argNames
: 組成key的參數註解可以使用在方法上, 須要注意的是, 本文註解經過spring AOP實現, 故對象內部方法間調用將無效.
/** * distributed lock aspect * @author piaoruiqing * @date: 2019/02/02 22:35 * * @since JDK 1.8 */
@Aspect
@Order(10) // (一)
public class DistributedLockableAspect implements KeyGenerator { // (二)
private final Logger LOGGER = LoggerFactory.getLogger(getClass());
@Resource
private RedisLockClient redisLockClient;
/** * {@link DistributedLockable} * @author piaoruiqing */
@Pointcut(value = "execution(* *(..)) && @annotation(com.github.piaoruiqing.dlock.annotation.DistributedLockable)")
public void distributedLockable() {}
/** * @author piaoruiqing * * @param joinPoint * @param lockable * @return * @throws Throwable */
@Around(value = "distributedLockable() && @annotation(lockable)")
public Object around(ProceedingJoinPoint joinPoint, DistributedLockable lockable) throws Throwable {
long start = System.nanoTime();
final String key = this.generate(joinPoint, lockable.prefix(), lockable.argNames(), lockable.argsAssociated()).toString();
Object result = redisLockClient.tryLock(
key, () -> {
return joinPoint.proceed();
},
lockable.unit().toMillis(lockable.timeout()), lockable.autoUnlock(),
lockable.retries(), lockable.unit().toMillis(lockable.waitingTime()),
lockable.onFailure()
);
long end = System.nanoTime();
LOGGER.debug("distributed lockable cost: {} ns", end - start);
return result;
}
}
複製代碼
KeyGenerator
爲自定義的key生成策略, 使用 prefix+argName+arg
做爲key, 具體實現見源碼.此時能夠經過註解的方式使用分佈式鎖, 這種方式對代碼入侵較小, 且簡潔.
@DistributedLockable(
argNames = {"anyObject.id", "anyObject.name", "param1"},
timeout = 20, unit = TimeUnit.SECONDS,
onFailure = RuntimeException.class
)
public Long distributedLockableOnFaiFailure(AnyObject anyObject, String param1, Object param2, Long timeout) {
try {
TimeUnit.SECONDS.sleep(timeout);
LOGGER.info("distributed-lockable: " + System.nanoTime());
} catch (InterruptedException e) {
}
return System.nanoTime();
}
複製代碼
分佈式鎖的實現有多種方式, 可根據實際場景和需求選擇不一樣的介質進行實現:
樂觀鎖
/悲觀鎖
實現, 性能通常, 高併發場景下不推薦本文闡述了Redis分佈式鎖的JAVA實現, 完成了自動解鎖、自定義異常、重試、註解鎖等功能, 源碼見地址.
本實現還有諸多能夠優化之處, 如:
篇幅有限, 後續再行闡述.