AOP的应用之分布式锁
Posted JAVA葵花宝典
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AOP的应用之分布式锁相关的知识,希望对你有一定的参考价值。
来源 :csdn | 作者 : 孙琛斌|原文:阅读原文
大家在做分布式多节点等系统的开发中为了保证某些业务操作场景的原子性操作,一定会用到锁的概念,传统的synchronized无法满足分布式多节点的系统,所以大家都会用Redis实现分布式锁,怎么实现我这里就先不多说了大家百度一下可以查到一大堆。
但还是要简单的说一下主要就是使用redis的setnx(key,value)方法配合del(key)方法,也就是在第一个请求进来的时候执行这个方法,会将一个key放到redis中,如果redis中该key已经存在了那么就返回0,否则返回1,这样当第一个请求处理完后调用del方法,后面的人就可以再进来了,ok用redis实现分布式锁大概就这个逻辑。
然后基于上面的分布式锁的逻辑,如果你要保证方法的原子性操作,那么就要写这样一串if else,多个地方用到的话冗余代码就太多而且代码看上去不美观,那么如何简化呢?这里考虑用到了aop的方法拦截器技术结合自定义注解来实现,下面直接看代码。
先定义两个注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedLock {
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锁实现方式有很多,上面只是实现了一种最简单的锁,不够严谨,没有家失效时间等,大家使用的话可根据自己的应用场景进行相应的改造
以上是关于AOP的应用之分布式锁的主要内容,如果未能解决你的问题,请参考以下文章
使用Redisson实现分布式锁,Spring AOP简化之
JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段