干货,springboot自定义注解实现分布式锁详解

20 篇文章 0 订阅
15 篇文章 0 订阅

背景

在互联网的很多场景下,会产生资源竞争,如果是单机环境,简单加个锁就能解决问题;但是在集群环境下(分布式环境),多个客户端在一个很短的时间内竞争同一服务端资源(如抢购场景),或者同一客户端重复提交请求,如果请求不具备幂等性,就需要用到分布式锁的解决方案。

背景知识

关于分布式锁,可以看看我之前的文章《基于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模拟并发请求测试业务方法和分布式锁,下次再讲。

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值