前言说明:本文旨在让希望能快速使用redis分布式锁的java后台工程师能快速了解分布式锁的基本原理和代码实现。由于网上讲解redis的锁的文章很多,所以这里不详细说明了,旨在讲一些简单的,让大家能快速使用的方式。
要求:本文默认读者是对于spring、redis、dubbo有一定了解的,相关的知识请参考其他文章。
由于业务的发展,公司的一些事务流程越来越复杂,又由于是分布式的,所以不得不采用分布式锁来解决一些问题。在网上查了一些流程,汇总出了自己的一个解决方案。这里作为笔记记录一下。
分布式锁的实现方式很多,比如用数据库的乐观锁,redis的分布式锁,zookeeper的分布式锁。我们主要采用了redis的分布式锁。
实现代码前要确定分布式锁的需求。(摘自另一篇文章:http://www.cnblogs.com/linjiqin/p/8003838.html)
1. 互斥性。在任意时刻,只有一个客户端能持有锁。
2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
5. 分布式锁还需要能尽量不改动其他代码的前提下让其他开发者方便使用。这里通过注解方式实现,只需要在原有的服务中增加注解就可以方便使用分布式锁,而不需要改动原先的代码。
redis分布式锁主要是通过redis的NX特性来做的。该特性就是判断一个key是否存在,不存在则set操作。通过这个特性我们可以将一个事物进来以后通过判断key是否存在来进行同步锁设置。不存在则加同步锁,同时进入事务处理,处理完成以后解锁。如果key已经存在,则根据自己的逻辑需要选择循环等待或者直接返回提示信息。
流程图如下:
了解了上面的内容,就可以开始构建分布式锁了。下面直接上代码。
首先是分布式锁的工具类。
import javax.annotation.Resource;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import com.alibaba.druid.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;
import java.util.Collections;
/**
* redis分布式同步锁类
*
* @author kane
*
*/
public class RedisDistributedLock {
@Resource
private RedisTemplate<String, Object> redisTemplate;
public static final String UNLOCK_LUA;
// 默认超时时间1分钟
private static final long EXPIRE = 60;
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();
}
/**
* 设置锁
*
* @param key
* key值
* @param expire
* 超时时间,单位秒,小于等于0则使用默认设置:1分钟超时时间
* @return
*/
public boolean setLock(String key, long expire,String requestId) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
NX是不存在时才set, XX是存在时才set, EX是秒,PX是毫秒
return commands.set(key, requestId, "NX", "EX", (expire<=0)?EXPIRE:expire);
};
String result = redisTemplate.execute(callback);
System.out.println("result="+result);
return !StringUtils.isEmpty(result);
} catch (Exception e) {
LoggerUtil.DebugLogger.error("set redis occured an exception", e);
}
return false;
}
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) {
LoggerUtil.DebugLogger.error("get redis occured an exception", e);
}
return "";
}
//释放锁
public boolean releaseLock(String key, String requestId) {
// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
try {
// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
RedisCallback<Long> callback = (connection) -> {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
System.out.println("集群解锁");
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, Collections.singletonList(key), Collections.singletonList(requestId));
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
System.out.println("单机解锁");
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, Collections.singletonList(key), Collections.singletonList(requestId));
}
return 0L;
};
Long result = redisTemplate.execute(callback);
return result != null && result > 0;
} catch (Exception e) {
LoggerUtil.DebugLogger.error("release lock occured an exception", e);
} finally {
// 清除掉ThreadLocal中的数据,避免内存溢出
// lockFlag.remove();
}
return false;
}
}
代码说明:
1. 为了保证redis的操作原子性,采用lua脚本执行删除redis操作。UNLOCK_LUA是lua脚本的内容。
2. setLock是加锁操作。releaseLock是释放锁操作。其中requestId是请求ID,通过UUID生成。为了保证谁提交的请求就由谁释放。
有了上面的工具类,下面开始讲加锁和解锁操作放到一个dubbo的provider中,方便其他的服务使用。
@Service("redisAPIImpl")
public class RedisAPIImpl implements RedisAPI{
@Autowired
RedisDistributedLock redisDistributedLock;
/**
* redis分布式锁——加锁操作
* @param key key值
* @param expire 超时时间,单位秒,小于等于0则使用默认设置:1分钟超时时间
* @param requestId 请求ID,解锁时的依据,防止其他client解锁
* @return
*/
@Override
public boolean distributedLock(String key, long expire, String requestId) {
return redisDistributedLock.setLock(key, expire, requestId);
}
/**
* redis分布式锁——解锁操作
* @param key key值
* @param requestId 请求ID,加锁时用的请求ID
* @return
*/
@Override
public boolean releaseDistributedLock(String key, String requestId) {
return redisDistributedLock.releaseLock(key, requestId);
}
/**
* redis分布式锁——获取锁值(既:加锁的请求ID的值)操作。
* @param key
* @return
*/
@Override
public String getLock(String key) {
return redisDistributedLock.get(key);
}
代码说明:讲刚才的工具类RedisDistributedLock注入到provider中。 然后编写provider的接口实现。并通过dubbo发布。
然后在dubbo的xml中暴露接口
<!-- redis相关的接口 -->
<dubbo:service interface="包名.RedisAPI"
ref="redisAPIImpl" />
接口写完以后,剩下的就是具体的事务服务使用了。为了保证代码不改动的前提下可以使用,我们这里采用spring的aop来实现。保证需要使用分布式锁的接口只需要添加注解就可以实现。
由于公司的项目是老项目了,spring的版本,dubbo的版本都不能改变。所以aop没有采用注解的形式,而是使用的spring提供的MethodInterceptor。
public class RedisLockAround implements MethodBeforeAdvice, AfterReturningAdvice, MethodInterceptor {
@Autowired
RedisAPI redisAPI;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
//这里的RedisLockAnnoation是注解,通过该注解可以设置分布式锁的一些常用参数。代码后面有
RedisLockAnnoation redisLockAnnoation = method.getAnnotation(RedisLockAnnoation.class);
if (null == redisLockAnnoation)
return 3; //拦截器异常
//判断拦截的方法是否位需要做同步锁的方法
if (method.getName().equals("createHotelOrder")) {
// 如果方法是创建酒店订单方法,则对酒店订单做同步锁支持
HotelOrderBean bean = null; //这里的bean是要加同步锁的方法参数传入的bean,这里是为了获取bean的关键信息作key使用
Object[] os = invocation.getArguments(); //通过拦截器获取拦截方法的参数
for (Object oBean : os) {
//判断参数中如果有需要的参数,则跳出循环
if (oBean instanceof HotelOrderBean) {
bean = (HotelOrderBean) oBean;
break;
}
}
if (null != bean) {
//取锁操作
return runLock(invocation, redisLockAnnoation, bean);
}
}
}
//取锁的操作
private Object runLock(MethodInvocation invocation, RedisLockAnnoation redisLockAnnoation, HotelOrderBean orderBean)
throws Throwable {
// 获取分布式锁的key,这里的key是常量(RedisKeyConstant.order_hotel_lock)+参数的信息拼凑的
String key = RedisKeyConstant.order_hotel_lock + orderBean.getHotelInfoId() + "_" + orderBean.getHotelRoomId()
+ "_" + orderBean.getHotelProductId();
// 生成本次请求的ID
String requestId = UUID.randomUUID().toString();
LoggerUtil.DebugLogger.info("runLock 准备取锁:key=" + key + ",requestId=" + requestId);
Map<String, Object> ret = dealLock(invocation, redisLockAnnoation, requestId, key);
int retState = (int) ret.get("state");
if (retState == 2) {
// 如果取锁超时,则设置返回失败
return 2;
} else if (retState == 0) {
// 如果出现异常,则设置返回失败
return 0;
} else {
return ret.get("retBean");
}
}
/**
* 处理取锁操作
*
* @param invocation
* @param redisLockAnnoation
* @param requestId
* @param key
* @return
* @throws Throwable
*/
private Map<String, Object> dealLock(MethodInvocation invocation, RedisLockAnnoation redisLockAnnoation,
String requestId, String key) throws Throwable {
Map<String, Object> ret = new HashMap<>();
if (redisLockAnnoation.isSpin()) {
// 阻塞锁
int lockRetryTime = 0; // 尝试取锁次数,默认为0
try {
while (!redisAPI.distributedLock(key, redisLockAnnoation.expireTime(), requestId)) {
if (lockRetryTime++ > redisLockAnnoation.retryTimes()) {
ret.put("state", 2); // state 1:成功 2:超时 0:异常
ret.put("desc", "超时");
ret.put("retBean", null);
return ret; // 超时操作
}
LoggerUtil.DebugLogger.info("取锁失败:key=" + key + ",requestId=" + requestId + ",lockRetryTime="
+ lockRetryTime + ",retryTimes=" + redisLockAnnoation.retryTimes());
Thread.sleep(redisLockAnnoation.waitTime()); //如果没有取到锁,休眠一定时间后在获取
}
Object retBean = invocation.proceed();
ret.put("state", 1); // state 1:成功 2:超时 0:异常
ret.put("desc", "成功");
ret.put("retBean", retBean);
return ret; // 超时操作
} catch (Exception e) {
// ret.put("state", 0); // state 1:成功 2:超时 0:异常
// ret.put("desc", e.getMessage());
// ret.put("retBean", null);
// return ret;
throw e;
} finally {
// 释放锁
LoggerUtil.DebugLogger.info("释放锁:key=" + key + ",requestId=" + requestId);
redisAPI.releaseDistributedLock(key, requestId);
}
} else {
// 非阻塞锁 ——目前无用
try {
if (!redisAPI.distributedLock(key, redisLockAnnoation.expireTime(), requestId)) {
throw new Exception("订单超时,请稍候再试");
}
Object retBean = invocation.proceed();
ret.put("state", 1); // state 1:成功 2:超时 0:异常
ret.put("desc", "成功");
ret.put("retBean", retBean);
return ret;
} catch (Exception e) {
ret.put("state", 0); // state 1:成功 2:超时 0:异常
ret.put("desc", e.getMessage());
ret.put("retBean", null);
return ret;
} finally {
// 释放锁
redisAPI.releaseDistributedLock(key, requestId);
}
}
}
}
代码说明:
1. 通过aop的方式拦截,拦截方式是around的方式。通过对MethodInterceptor实现invoke方法,我们可以对要拦截的方法执行前和执行后都作处理。分布式锁主要是对事务方法执行前加锁,然后在方法执行后释放锁。
2. Object retBean = invocation.proceed();就是事务方法执行
然后是注解的代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface RedisLockAnnoation {
/**
* 是否阻塞锁; 1. true:获取不到锁,阻塞一定时间; 2. false:获取不到锁,立即返回
*/
boolean isSpin() default true;
/**
* 超时时间
*/
int expireTime() default 30;
/**
* 等待时间
*/
int waitTime() default 1000;
/**
* 获取不到锁的等待时间
*/
int retryTimes() default 20;
}
代码说明:这段代码很简单。就是自定义一个注解,设置一个同步锁的参数。包括:是否需要阻塞,锁的时间,取锁的等待时间和循环次数等。
最后就是使用了。只需要在需要使用分布式锁的地方加上注解就好了。这里是一个订单使用同步锁的例子
/**
* 创建一个酒店订单
* @param orderId
* @param orderBean
* @return 0:失败 1:成功 2:超时 3:未执行取锁操作 4:价格异常 5:产品信息错误 6:产品规则错误 7:参数错误 8:没有库存了
*/
@RedisLockAnnoation(isSpin = true, expireTime = 10, retryTimes = 5)
public int createHotelOrder(String orderId,HotelOrderBean orderBean) throws Exception;