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.