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实现, 完成了自动解锁、自定义异常、重试、注解锁等功能, 源码见地址.
本实现还有诸多能够优化之处, 如:
篇幅有限, 后续再行阐述.