1、分布式锁常见实现方式
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。
本文是基于redis实现,为了方便使用,也提供了封装成spring boot starter方式的redis分布式锁。
2、实现分布式锁需要满足的条件
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
1)互斥性。在任意时刻,只有一个客户端能持有锁。
2)可重入性,即不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3)具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4)同一性。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
3、基于lua脚本方式实现
锁是针对某个资源,保证其访问的互斥性,在实际使用当中,这个资源一般是一个字符串。使用 Redis 实现锁,主要是将资源放到 Redis 当中,利用其原子性,当其他线程访问时,如果 Redis 中已经存在这个资源,就不允许之后的一些操作。spring boot使用 Redis 的操作主要是通过 RedisTemplate 来实现,一般步骤如下:
1)将锁资源放入 Redis (注意是当key不存在时才能放成功,所以使用 setIfAbsent 方法):
redisTemplate.opsForValue().setIfAbsent("key", "value");
2)设置过期时间
redisTemplate.expire("key", 30000, TimeUnit.MILLISECONDS);
3)释放锁
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 命令的效果。
文档参见: http://doc.redisfans.com/string/set.html
NX: 表示只有当锁定资源不存在的时候才能 SET 成功。利用 Redis 的原子性,保证了只有第一个请求的线程才能获得锁,而之后的所有线程在锁定资源被释放之前都不能获得锁。
PX: expire 表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定。
这样在获取锁的时候就能够保证设置 Redis 值和过期时间的原子性,避免前面提到的两次 Redis 操作期间出现意外而导致的锁不能释放的问题。但是这样还是可能会存在一个问题,考虑如下的场景顺序:
- 线程T1获取锁
- 线程T1执行业务操作,由于某些原因阻塞了较长时间
- 锁自动过期,即锁自动释放了
- 线程T2获取锁
- 线程T1业务操作完毕,释放锁(其实是释放的线程T2的锁)
按照这样的场景顺序,线程T2的业务操作实际上就没有锁提供保护机制了。所以,每个线程释放锁的时候只能释放自己的锁,即锁必须要有一个拥有者的标记,并且也需要保证释放锁的原子性操作。
因此在获取锁的时候,可以生成一个随机不唯一的串放入当前线程中,然后再放入 Redis 。释放锁的时候先判断锁对应的值是否与线程中的值相同,相同时才做删除操作。
Redis 从2.6.0开始通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行求值,文档参见: http://doc.redisfans.com/script/eval.html
因此我们可以通过 Lua 脚本来达到释放锁的原子操作,定义 Lua 脚本如下:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
使用 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可能成功获取到锁
代码实现如下:
3.1 组件依赖
首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
3.2 具体代码
第一种方式:
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;
import java.util.ArrayList;
import java.util.List;
/**
* @description redis分布式锁工具类
* @date 2019/5/15 9:29
*/
@Component
public class RedisDistributedLock {
private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 将key的值设为value,当且仅当key不存在,等效于SETNX
*/
private static final String SET_IF_NOT_EXIST = "NX";
/**
* 以毫秒为单位设置key的过期时间,等效于EXPIRE key milliseconds
*/
private static String SET_WITH_EXPIRE_TIME = "PX";
/**
* 以秒为单位设置key的过期时间,等效于EXPIRE key seconds
*/
private static String SET_WITH_EXPIRE_TIME_E = "EX";
/**
* 调用尝试获取分布式锁后的返回值
*/
public static final String OK = "OK";
/**
* 使用lua脚本删除redis中匹配value的key
* 使用lua脚本原因:保证操作的原子性
*/
private static final String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* 尝试获取分布式锁
* @param key 锁标识
* @param value 一般为请求id(可以使用随机生成的uuid,保证加锁和解锁是同一个客户端)
* @param expire 过期时间
*/
public boolean lock(String key, String value, long expire) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expire);
};
String result = redisTemplate.execute(callback);
// 设置成功为OK,否则为null
return OK.equals(result);
} catch (Exception e) {
logger.error("redis加锁失败,key值:" + key, e);
}
return false;
}
/**
* 获取锁key的值
*/
public String get(String key) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.get(key);
};
String result = redisTemplate.execute(callback);
return result;
} catch (Exception e) {
logger.error("redis获取值失败,key值:" + key, e);
}
return "";
}
/**
* 释放锁
* @param key 锁标识
* @param value 一般为请求id(可以使用随机生成的uuid,保证加锁和解锁是同一个客户端)
*/
public boolean unLock(String key,String value) {
// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
try {
List<String> keys = new ArrayList<>();
keys.add(key);
List<String> values = new ArrayList<>();
values.add(value);
// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
RedisCallback<Long> callback = (connection) -> {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, values);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, values);
}
return 0L;
};
Long result = redisTemplate.execute(callback);
return result != null && result >= 0;
} catch (Exception e) {
logger.error("redis解锁失败,key值:" + key, e);
}
return false;
}
public boolean unLockList(List<String> keys,List<String> values) {
// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
try {
// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
RedisCallback<Long> callback = (connection) -> {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, values);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, values);
}
return 0L;
};
Long result = redisTemplate.execute(callback);
return result != null && result > 0;
} catch (Exception e) {
logger.error("redis解锁失败,key值:" + JSON.toJSONString(keys), e);
}
return false;
}
}
第二种方式:
为了可以以后扩展为使用其他方式来实现分布式锁,定义了接口和抽象类,代码如下:
/**
* 分布式锁接口
* @Author xf
* @Date 2019/5/22 20:39
*/
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);
}
/**
* 分布式锁抽象类,实现基本的方法,关键方法由子类去实现
* @Author xf
* @Date 2019/5/22 20:41
*/
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);
}
}
/**
* 分布式锁实现类
* @Author xf
* @Date 2019/5/22 20:43
*/
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;
}
}
4、基于AOP方式实现
在实际的使用过程中,分布式锁可以封装好后使用在方法级别,这样就不用每个地方都去获取锁和释放锁,使用起来更加方便。
首先定义个注解:
/**
* @Author xf
* @Date 2019/5/22 20:50
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @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
/**
* @Author xf
* @Date 2019/5/22 20:55
*/
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class DistributedLockAutoConfiguration {
@Bean
@ConditionalOnBean(RedisTemplate.class)
public DistributedLock redisDistributedLock(RedisTemplate<Object, Object> redisTemplate){
return new RedisDistributedLock(redisTemplate);
}
}
定义切面(spring boot配置方式)
/**
* @Author xf
* @Date 2019/5/22 20:57
*/
@Aspect
@Configuration
@ConditionalOnClass(DistributedLock.class)
@AutoConfigureAfter(DistributedLockAutoConfiguration.class)
public class DistributedLockAspectConfiguration {
private final Logger logger = LoggerFactory.getLogger(DistributedLockAspectConfiguration.class);
@Autowired
private DistributedLock distributedLock;
private ExpressionParser parser = new SpelExpressionParser();
private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
@Pointcut("@annotation(com.itcode.lock.redis.spring.boot.autoconfigure.annotations.LockAction)")
private void lockPoint(){
}
@Around("lockPoint()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
LockAction lockAction = method.getAnnotation(LockAction.class);
String key = lockAction.value();
Object[] args = pjp.getArgs();
key = parse(key, method, args);
int retryTimes = lockAction.action().equals(LockFailAction.CONTINUE) ? lockAction.retryTimes() : 0;
boolean lock = distributedLock.lock(key, lockAction.keepMills(), retryTimes, lockAction.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);
throw e;
} finally {
boolean releaseResult = distributedLock.releaseLock(key);
logger.debug("release lock : " + key + (releaseResult ? " success" : " failed"));
}
}
/**
* @description 解析spring EL表达式
* @author xf
* @date 2019/5/22 20:57
* @param key 表达式
* @param method 方法
* @param args 方法参数
* @return
*/
private String parse(String key, Method method, Object[] args) {
String[] params = discoverer.getParameterNames(method);
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < params.length; i ++) {
context.setVariable(params[i], args[i]);
}
return parser.parseExpression(key).getValue(context, String.class);
}
}
spring boot starter还需要在 resources/META-INF 中添加 spring.factories 文件
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itcode.lock.redis.spring.boot.autoconfigure.DistributedLockAutoConfiguration,\
com.itcode.lock.redis.spring.boot.autoconfigure.DistributedLockAspectConfiguration
这样封装之后,使用spring boot开发的项目,直接依赖这个starter,就可以在方法上加 RedisLock 注解来实现分布式锁的功能了,当然如果需要自己控制,直接注入分布式锁的bean即可
@Autowired
private DistributedLock distributedLock;
如果需要使用其他的分布式锁实现,继承 AbstractDistributedLock 后实现获取锁和释放锁的方法即可。
参考:
https://www.cnblogs.com/linjiqin/p/8003838.html
http://www.cnblogs.com/number7/p/8320259.html