Redis之分佈式鎖的實現方案 - 如何優雅地實現分佈式鎖(JAVA)
分佈式鎖
: 是控制分佈式系統之間同步訪問共享資源的一種方式。spring-data-redis
: Spring針對redis的封裝, 配置簡單, 提供了與Redis存儲交互的抽象封裝, 十分優雅, 也極具擴展性, 推薦讀一讀源碼Lua
: Lua 是一種輕量小巧的腳本語言, 可在redis執行.本文闡述了Redis分佈式鎖的一種簡單JAVA實現及優化進階, 實現了自動解鎖、自定義異常、重試、註解鎖等功能, 嘗試用更優雅簡潔的代碼完成分佈式鎖.java
Redis具備極高的性能, 且其命令對分佈式鎖支持友好, 藉助SET
命令便可實現加鎖處理.git
SET
github
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.
簡單實現
作法爲set if not exist(若是不存在則賦值), redis命令爲原子操做, 因此單獨使用set
命令時不用擔憂併發致使異常.redis
具體代碼實現以下: (spring-data-redis:2.1.6
)spring
<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); }
以上代碼即完成了一個簡單的分佈式鎖功能: shell
其中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 + "]"; } }
對比鎖的值
+刪除
成爲原子操做, 確保解鎖操做的正確性. 簡單來講就是防止刪了別人的鎖
.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()
在高併發狀況下性能不佳.此代碼已經能夠知足自動解鎖和重試的需求了, 使用方法:dom
// 根據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); }
[版權聲明]
本文發佈於樸瑞卿的博客, 非商業用途容許轉載, 但轉載必須保留原做者樸瑞卿 及連接:blog.piaoruiqing.com. 若有受權方面的協商或合做, 請聯繫郵箱: piaoruiqing@gmail.com.
自定義異常: 前文中針對分佈式鎖的封裝可知足多數業務場景, 可是考慮這樣一種狀況, 若是業務自己會返回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實現, 完成了自動解鎖、自定義異常、重試、註解鎖等功能, 源碼見地址.
本實現還有諸多能夠優化之處, 如:
篇幅有限, 後續再行闡述.
[版權聲明]
本文發佈於樸瑞卿的博客, 非商業用途容許轉載, 但轉載必須保留原做者樸瑞卿 及連接:blog.piaoruiqing.com. 若有受權方面的協商或合做, 請聯繫郵箱: piaoruiqing@gmail.com.