背景
在互联网的很多场景下,会产生资源竞争,如果是单机环境,简单加个锁就能解决问题;但是在集群环境下(分布式环境),多个客户端在一个很短的时间内竞争同一服务端资源(如抢购场景),或者同一客户端重复提交请求,如果请求不具备幂等性,就需要用到分布式锁的解决方案。
背景知识
关于分布式锁,可以看看我之前的文章《基于Spring boot 2.1 使用redisson实现分布式锁》,当时只是利用redisson的API实现了功能,现在,我们要利用自定义注解方式,让分布锁功能更方便使用。关于自定义注解,请移步《深入理解Java:注解(Annotation)自定义注解入门》
解决方案
1、引入redis和redisson的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--spring2.X集成redis所需common-pool2,使用jedis必须依赖它 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.5</version>
</dependency>
2、接口RedissonLocker.java,定义分布式锁的一些常用API
public interface RedissonLocker {
RLock lock(String lockKey);
RLock lock(String lockKey, int timeout);
RLock lock(String lockKey, TimeUnit unit, int timeout);
boolean tryLock(String lockKey, TimeUnit unit, LockConstant lockTime);
boolean fairLock(String lockKey, TimeUnit unit, LockConstant lockTime);
void unlock(String lockKey);
void unlock(RLock lock);
}
3、实现类RedissonLockerImpl.java,接口RedissonLocker的实现类
/**
* 基于Redisson实现分布式锁
*
* @author
* 2019年4月3日
*
* {@link https://github.com/redisson/redisson/wiki}
*
*/
@Component
public class RedissonLockerImpl implements RedissonLocker {
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
/**
* key 值是否存在
*
* @param key
* @return
*/
public boolean existKey(String key) {
return redisTemplate.hasKey(key);
}
/************************** 可重入锁 **************************/
/**
* 拿不到lock就不罢休,不然线程就一直block 没有超时时间,默认30s
*
* @param lockKey
* @return
*/
@Override
public RLock lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
return lock;
}
/**
* 自己设置超时时间
*
* @param lockKey 锁的key
* @param timeout 秒 如果是-1,直到自己解锁,否则不会自动解锁
* @return
*/
@Override
public RLock lock(String lockKey, int timeout) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, TimeUnit.SECONDS);
return lock;
}
/**
* 自己设置超时时间
*
* @param lockKey 锁的key
* @param unit 锁时间单位
* @param timeout 超时时间
*
*/
@Override
public RLock lock(String lockKey, TimeUnit unit, int timeout) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, unit);
return lock;
}
/**
* 尝试加锁,最多等待waitTime,上锁以后leaseTime自动解锁
*
* @param lockKey 锁key
* @param unit 锁时间单位
* @param waitTime 等到最大时间,强制获取锁
* @param leaseTime 锁失效时间
* @return 如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false
*/
@Override
public boolean tryLock(String lockKey, TimeUnit unit, LockConstant lockTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
boolean existKey = existKey(lockKey);
if (existKey) {// 已经存在了,就直接返回
return false;
}
return lock.tryLock(lockTime.getWaitTime(), lockTime.getLeaseTime(), unit);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
/************************** 公平锁 **************************/
/**
* 尝试加锁,最多等待waitTime,上锁以后leaseTime自动解锁
*
* @param lockKey 锁key
* @param unit 锁时间单位
* @param waitTime 等到最大时间,强制获取锁
* @param leaseTime 锁失效时间
* @return 如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false
*/
public boolean fairLock(String lockKey, TimeUnit unit, LockConstant lockTime) {
RLock fairLock = redissonClient.getFairLock(lockKey);
try {
boolean existKey = existKey(lockKey);
if (existKey) {// 已经存在了,就直接返回
return false;
}
return fairLock.tryLock(lockTime.getWaitTime(), lockTime.getLeaseTime(), unit);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
/**
* 尝试加锁,最多等待waitTime,上锁以后leaseTime自动解锁
*
* @param lockKey 锁key
* @param unit 锁时间单位
* @param waitTime 等到最大时间,强制获取锁,默认是三秒钟
* @param leaseTime 锁失效时间
* @return 如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false
*/
public boolean fairLock(String lockKey, TimeUnit unit, int leaseTime) {
RLock fairLock = redissonClient.getFairLock(lockKey);
try {
boolean existKey = existKey(lockKey);
if (existKey) {// 已经存在了,就直接返回
return false;
}
return fairLock.tryLock(3, leaseTime, unit);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
/**
* 释放锁
*
* @param lockKey 锁key
*/
@Override
public void unlock(String lockKey) {
try {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
} catch (Exception e) {
}
}
/**
* 释放锁
*/
@Override
public void unlock(RLock lock) {
try {
lock.unlock();
} catch (Exception e) {
}
}
}
4、自定义注解 RlockRepeatSubmit
/**
* 防止重复提交的注解
*
* @author
* 2019年6月18日
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
@Documented
public @interface RlockRepeatSubmit {
/**
* 分布式锁枚举类
* @return
*/
LockConstant lockConstant();
}
5、枚举LockConstant.java,主要用于使用注解时传参,来区分不同的业务类型
/**
* 分布式锁枚举类
*
*/
public enum LockConstant {
CASHIER("cashierLock:", 3, 300, "请勿重复点击收银操作!!"), // 收银锁
SUBMIT_ORDER("submitOrderLock:", 3, 30, "请勿重复点击下单!!"), // 下单锁
......
COMMON_LOCK("commonLock:", 3, 120, "请勿重复点击");// 通用锁常量
private String keyPrefix; // 分布式锁前缀
private int waitTime;// 等到最大时间,强制获取锁
private int leaseTime;// 锁失效时间
private String message;// 加锁提示
// 构造方法
private LockConstant(String keyPrefix, int waitTime, int leaseTime, String message) {
this.keyPrefix = keyPrefix;
this.waitTime = waitTime;
this.leaseTime = leaseTime;
this.message = message;
}
// 省略getter,setter
}
6、RlockRepeatSubmitAspect.java,定义AOP类,解析自定义注解RlockRepeatSubmit,进行分布式锁操作
/**
*
* 防止重复提交分布式锁拦截器
*
* @author
* 2019年6月18日
*
*/
@Aspect
@Component
public class RlockRepeatSubmitAspect {
@Resource
private RedissonLockerImpl redissonLocker;
/***
* 定义controller切入点拦截规则,拦截RlockRepeatSubmit注解的业务方法
*/
@Pointcut("@annotation(xx.xxx.RlockRepeatSubmit)")
public void pointCut() {
}
/**
* AOP分布式锁拦截
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Around("pointCut()")
public Object rlockRepeatSubmit(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取类里面的方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
RlockRepeatSubmit repeatSubmit = targetMethod.getAnnotation(RlockRepeatSubmit.class);
// 如果添加了RlockRepeatSubmit这个注解,需要添加分布式锁
if (Objects.nonNull(repeatSubmit)) {
// 获取参数
Object[] args = joinPoint.getArgs();
// 进行一些参数的处理,比如获取订单号,操作人id等
......
HttpServletRequest request = getRequest();
if (null != request && request.getParameterMap().containsKey("accessToken")) {
StringBuffer lockKeyBuffer = new StringBuffer();
LockConstant lockConstant = repeatSubmit.lockConstant();
lockKeyBuffer.append(lockConstant.getKeyPrefix());
String accessToken = request.getParameterMap().get("accessToken")[0];// 当前用户的token
String path = request.getServletPath();
// 使用用户的token和请求路径作为唯一的标识,如果有orderNo,加上orderNo作为唯一标识
lockKeyBuffer.append("RlockRepeat");
String token = accessToken + "." + path;
if (null != orderNo) {
lockKeyBuffer.append(token.hashCode());
lockKeyBuffer.append(".");
lockKeyBuffer.append(orderNo);
} else {
lockKeyBuffer.append(token);
}
// 公平加锁,lockTime后锁自动释放
boolean isLocked = false;
try {
isLocked = redissonLocker.fairLock(lockKeyBuffer.toString(), TimeUnit.SECONDS, lockConstant);
if (isLocked) { // 如果成功获取到锁就继续执行
// 执行进程
return joinPoint.proceed();
} else { // 未获取到锁
//异常处理
......
}
} catch (Exception e) {
//异常处理
......
} finally {
if (isLocked) { // 如果锁还存在,在方法执行完成后,释放锁
redissonLocker.unlock(lockKeyBuffer.toString());
}
}
}
}
return joinPoint.proceed();
}
public static HttpServletRequest getRequest() {
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return ra.getRequest();
}
}
7、在controller层的业务方法使用自定义注解
@RestController
@Slf4j
@RequestMapping("/order")
public class OrderController {
/**
* 业务方法
*
* @param requestForm
* @return
* @throws Throwable
*/
@RlockRepeatSubmit(lockConstant = LockConstant.SUBMIT_ORDER)
@RequestMapping(value = "/xxx", method = RequestMethod.POST)
public String submitOrder(Order order) throws Throwable {
//业务方法
......
}
}
OK,大功告成,以后如果哪个业务方法需要加分布式锁,直接在方法上加上自定义注解,并在枚举里定义相关属性即可,是不是很简单?
PS:关于怎么利用JMeter模拟并发请求测试业务方法和分布式锁,下次再讲。