AOP的应用之分布式锁

大家在做分布式多节点等系统的开发中为了保证某些业务操作场景的原子性操作,一定会用到锁的概念,传统的synchronized无法满足分布式多节点的系统,所以大家都会用Redis实现分布式锁,怎么实现我这里就先不多说了大家百度一下可以查到一大堆。

但还是要简单的说一下主要就是使用redis的setnx(key,value)方法配合del(key)方法,也就是在第一个请求进来的时候执行这个方法,会将一个key放到redis中,如果redis中该key已经存在了那么就返回0,否则返回1,这样当第一个请求处理完后调用del方法,后面的人就可以再进来了,ok用redis实现分布式锁大概就这个逻辑。

然后基于上面的分布式锁的逻辑,如果你要保证方法的原子性操作,那么就要写这样一串if else,多个地方用到的话冗余代码就太多而且代码看上去不美观,那么如何简化呢?这里考虑用到了aop的方法拦截器技术结合自定义注解来实现,下面直接看代码。

先定义两个注解

/**
 * @author chenbin.sun
 * @description 在需要加锁的方法上打上该注解后,LockMethodHelper会帮助你统一管理这个方法的锁
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedLock {
	
	/** key的一部分
	  * 注静态key的组成: LockMethodHelper.BEFORE_KEY+fieldKey() 
	  * 注动态key的组成: LockMethodHelper.BEFORE_KEY+fieldKey()+@KeyParam注解对应的动态参数值
	  */
	String fieldKey();
}
/**
 * @author chenbin.sun
 * 动态key需要配置的注解,只有配置了该注解才能够开启动态key功能
 */
@Documented
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface KeyParam {

	/**
	 * 如果动态参数在command对象中,那么就需要设置columns的值为command对象中的属性名可以为多个,否则不需要设置该值
	 * <p>例1:public void test(@KeyParam({"id"}) MemberCommand member)
	 * <p>例2:public void test(@KeyParam({"id","loginName"}) MemberCommand member)
	 * <p>例3:public void test(@KeyParam String memberId)
	 * @return
	 */
	String[] columns() default {};
}
然后编写具体方法,这里实现了aop的方法拦截器,这种效果类似于aop的环绕通知

/**
 * @author chenbin.sun
 * @description 对分布式系统的方法进行统一加锁解锁(使用了aop的环绕)
 * <p> 使用该方法进行加锁,必须在要加锁的方法上使用@NeedLock注解,如果要使用动态key的话需要配合@KeyParam注解
 */
@Service("lockMethodHelper")
public class LockMethodHelper implements MethodInterceptor {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(LockMethodHelper.class);
	
	private Long status = 0L;
	
	public static final String BEFORE_KEY = "LOCK_METHOD_";
	
	@Autowired
	private LotteryRedisManager lotteryRedisManager;
	
	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		Method method = invocation.getMethod();
		Object[] args = invocation.getArguments();
		
		// 执行方法前
		boolean before = before(method, args);
		
		// 加锁失败
		if (!before) {
			return new ResultReturnCommand(ResultReturnCodeEnum.LOTTERY_REDIS_LOCK_FAIL);
			//return new Object();
		}
		
		Object result = null;
		try {
			
			// 通过反射机制调用目标方法
			result = invocation.proceed();
		} catch (Exception e) {
			LOGGER.error("执行目标方法抛出异常",e);
			return new ResultReturnCommand(ResultReturnCodeEnum.LOTTERY_REDIS_UNLOCK_FAIL);
		} finally {
			
			// 执行方法后解锁
			afterReturning(method, args);
		}
		return result;
	}

	/**
	 * 方法执行前的加锁操作
	 * @param method
	 * @param args
	 * @return
	 * @throws Throwable
	 */
	public boolean before(Method method, Object[] args) throws Throwable {
		
		// 构造锁key
		String key = buildRedisKey(method, args);
		if (null == key) {
			return true;
		}
		Long status = lotteryRedisManager.setnx(key, key);
		if (status.equals(status)) {
			return false;
		}
		return true;
	}
	
	/**
	 * 方法执行后的解锁操作
	 * @param method
	 * @param args
	 * @return
	 * @throws Throwable
	 */
	public boolean afterReturning(Method method, Object[] args) throws Throwable {
		
		// 构造锁key
		String key = buildRedisKey(method, args);
		if (null == key) {
			return true;
		}
		lotteryRedisManager.delKeyAndValue(key);
		return true;
	}

	/**
	 * 构造锁key
	 * @param method
	 * @param args
	 * @return
	 * @throws NoSuchFieldException
	 * @throws IllegalAccessException
	 */
	private String buildRedisKey(Method method, Object[] args)
			throws NoSuchFieldException, IllegalAccessException {
		NeedLock needLock = method.getAnnotation(NeedLock.class);
		if (null == needLock) {
			return null;
		}
		
		// 锁的前半部分key
		String key = BEFORE_KEY + needLock.fieldKey();
		
		// 迭代全部参数的注解,根据使用KeyParam的注解的参数所在的下标,来获取args中对应下标的参数值拼接到前半部分key上
		Annotation[][] parameterAnnotations = method.getParameterAnnotations();
		for (int i = 0; i < parameterAnnotations.length; i++) {
			
			// 循环该参数全部注解
			for (Annotation annotation : parameterAnnotations[i]) {
				
				// 当前参数的注解不包含keyparam
				if(!annotation.annotationType().isInstance(KeyParam.class)){
					continue;
				}
				
				// 当前参数的注解包含keyparam,获取注解配置的值
				String[] columns = ((KeyParam)annotation).columns();
				if (columns.length == 0) {
					
					// 普通数据类型直接拼接
					if (null == args[i]) {
						LOGGER.error("动态参数不能为null!");
						throw new RuntimeException("动态参数不能为null!");
					}
					key += args[i];
				}else{
					
					// keyparam的columns值不为null,所以当前参数应该是对象类型
					for (int j = 0; j < columns.length; j++) {
						Class<? extends Object> clasz = args[i].getClass();
						Field declaredField = clasz.getDeclaredField(columns[j]);
						declaredField.setAccessible(true);
						Object value = declaredField.get(clasz);
						key += value;
					}
				}
				
			}
		}
		return key;
	}
}
怎么使用呢?看下面这个例子

@Override
	@NeedLock(fieldKey = LotteryActivityConstants.REDIS_LOCK_MARK)
	public ResultReturnCommand create(@KeyParam String code, @KeyParam Long memLaunchId) {

		// 对活动做相关校验
		ResultReturnCommand resultReturnCommand = lotteryActivityDetailManager.checkLotteryActivity(code);

		if (!LotteryActivityConstants.SUCCESS_CODE.equals(resultReturnCommand.getCode())) {
			return resultReturnCommand;
		}

		// 1根据code查询是否有对应的活动信息
		LotteryActivityCommand lotteryActivityCommand = (LotteryActivityCommand) resultReturnCommand.getData();

		// 2根据活动ID 和发起者ID查询对应的信息
		LotteryMemLaunch lotteryMemLaunch = lotteryMemLaunchDao.findLotteryMemLaunchBycodeActivityIdAndMemberId(lotteryActivityCommand.getId(), memLaunchId);

		// 5校验是否瞒足发起活动的条件
		if (!LotteryValidator.checkEffective(lotteryMemLaunch)) {
			return new ResultReturnCommand(ResultReturnCodeEnum.ACTIVITY_NOT_FINISHED);
		}

		// 3插入发起活动记录
		LotteryMemLaunch ret = savelotteryMemLaunch(lotteryActivityCommand, memLaunchId);

		return new ResultReturnCommand(ResultReturnCodeEnum.SUCCESS, ret);

	}
只要在方法上加上@NeedLock,然后给个锁的key就可以了,如果你的锁的key是动态的,那么在方法的参数上结合使用@KeyParam注解就可以了,详细使用方法请看最上面,注解的注释即可。

要真正使用这个东西还有一点必不可少的东西就是要在spring的配置中配置aop的拦截范围,配置如下:

<!-- 统一加锁aop方法 -->
	<aop:config>
	    <aop:pointcut expression="execution(* com.chenbin.sun.plugin.lottery.red.packet.manager..*(..))" id="lockMethodPointcut"/>
	    <aop:advisor advice-ref="lockMethodHelper" pointcut-ref="lockMethodPointcut"/>
	</aop:config>
本文只是描述一个思想,具体的redis锁实现方式有很多,上面只是实现了一种最简单的锁,不够严谨,没有家失效时间等,大家使用的话可根据自己的应用场景进行相应的改造








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浮生(FS)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值