Java基于注解实现的锁实例解析
某些场景下,有可能一个方法不能被并发执行,有可能一个方法的特定参数不能被并发执行。比如不能将一个消息发送多次,创建缓存最好只创建一次等等。为了实现上面的目标我们就需要采用同步机制来完成,但同步的逻辑如何实现呢,是否会影响到原有逻辑呢?
嵌入式
这里讲的嵌入式是说获取锁以及释放锁的逻辑与业务代码耦合在一起,又分分布式与单机两种不同场景的不同实现。
单机版本
下面方法,每个productId不允许并发访问,所以这里可以直接用synchronized来锁定不同的参数。
@Service
publicclassProductAppService{
publicvoidinvoke(IntegerproductId){
synchronized(productId){
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.print("productId:"+productId+"time:"+newDate());
}
}
}
测试脚本:三个相同的参数0,两个不同的参数1和2,通过一个多线程的例子来模似。如果有并发请求的测试工具可能效果会更好。
privatevoidtestLock(){
ExecutorServiceexecutorService=Executors.newFixedThreadPool(5);
executorService.submit(newRunnable(){
@Override
publicvoidrun(){
productAppService.invoke2(0);
}
});
executorService.submit(newRunnable(){
@Override
publicvoidrun(){
productAppService.invoke2(0);
}
});
executorService.submit(newRunnable(){
@Override
publicvoidrun(){
productAppService.invoke2(0);
}
});
executorService.submit(newRunnable(){
@Override
publicvoidrun(){
productAppService.invoke2(1);
}
});
executorService.submit(newRunnable(){
@Override
publicvoidrun(){
productAppService.invoke2(2);
}
});
executorService.shutdown();
}
测试结果如下,0,1,2三个请求未被阻塞,后面的两个0被阻塞。
分布式版本
分布式的除了锁机制不同之外其它的测试方法相同,这里只贴出锁的部分:
publicvoidinvoke2(IntegerproductId){
RLocklock=this.redissonService.getRedisson().getLock(productId.toString());
try{
booleanlocked=lock.tryLock(3000,500,TimeUnit.MILLISECONDS);
if(locked){
Thread.sleep(1000);
System.out.print("productId:"+productId+"time:"+newDate());
}
}catch(InterruptedExceptione){
e.printStackTrace();
}
finally{
lock.unlock();
}
}
嵌入式的缺点
比较明显的就是锁的逻辑与业务逻辑混合在一起,增加了程序复杂度而且也不利于锁机制的更替。
注解式
能否将锁的逻辑隐藏起来,通过在特定方法上增加注解来实现呢?就像SpringCache的应用。当然是可以的,这里我们只需要解决如下三个问题:
定义注解
锁一般有如下几个属性:
- key,锁对象的标识,就是上面提到的方法的某些参数。一般由方法所属类的完全限定名,方法名以及指定的参数构成。
- maximumWaiteTime,最大等待时间,避免线程死循环。
- expirationTime,锁的生命周期,可以有效避免因特殊原因未释放锁导致其它线程永远获取不到锁的局面。
- timeUnit,配合上面两个属性使用,时间单位。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public@interfaceRequestLockable{
String[]key()default"";
longmaximumWaiteTime()default2000;
longexpirationTime()default1000;
TimeUnittimeUnit()defaultTimeUnit.MILLISECONDS;
}
实现注解
由于我们的目标是注解式锁,这里通过AOP的方式来实现,具体依赖AspectJ,创建一个拦截器:
publicabstractclassAbstractRequestLockInterceptor{
protectedabstractLockgetLock(Stringkey);
protectedabstractbooleantryLock(longwaitTime,longleaseTime,TimeUnitunit,Locklock)throwsInterruptedException;
/**
*包的表达式目前还有待优化TODO
*/
@Pointcut("execution(*com.chanjet.csp..*(..))&&@annotation(com.chanjet.csp.product.core.annotation.RequestLockable)")
publicvoidpointcut(){}
@Around("pointcut()")
publicObjectdoAround(ProceedingJoinPointpoint)throwsThrowable{
Signaturesignature=point.getSignature();
MethodSignaturemethodSignature=(MethodSignature)signature;
Methodmethod=methodSignature.getMethod();
StringtargetName=point.getTarget().getClass().getName();
StringmethodName=point.getSignature().getName();
Object[]arguments=point.getArgs();
if(method!=null&&method.isAnnotationPresent(RequestLockable.class)){
RequestLockablerequestLockable=method.getAnnotation(RequestLockable.class);
StringrequestLockKey=getLockKey(method,targetName,methodName,requestLockable.key(),arguments);
Locklock=this.getLock(requestLockKey);
booleanisLock=this.tryLock(requestLockable.maximumWaiteTime(),requestLockable.expirationTime(),requestLockable.timeUnit(),lock);
if(isLock){
try{
returnpoint.proceed();
}finally{
lock.unlock();
}
}else{
thrownewRuntimeException("获取锁资源失败");
}
}
returnpoint.proceed();
}
privateStringgetLockKey(Methodmethod,StringtargetName,StringmethodName,String[]keys,Object[]arguments){
StringBuildersb=newStringBuilder();
sb.append("lock.").append(targetName).append(".").append(methodName);
if(keys!=null){
StringkeyStr=Joiner.on(".").skipNulls().join(keys);
if(!StringUtils.isBlank(keyStr)){
LocalVariableTableParameterNameDiscovererdiscoverer=newLocalVariableTableParameterNameDiscoverer();
String[]parameters=discoverer.getParameterNames(method);
ExpressionParserparser=newSpelExpressionParser();
Expressionexpression=parser.parseExpression(keyStr);
EvaluationContextcontext=newStandardEvaluationContext();
intlength=parameters.length;
if(length>0){
for(inti=0;i
注意如下几点:
为什么会存在抽象方法?那是为下面的将注解机制与具体的锁实现解耦服务的,目的是希望注解式锁能够得到复用也便于扩展。锁的key生成规则是什么?前缀一般是方法所在类的完全限定名,方法名称以及spel表达式来构成,避免重复。SPEL表达式如何支持?
LocalVariableTableParameterNameDiscoverer它在SpringMVC解析Controller的参数时有用到,可以从一个Method对象中获取参数名称列表。
SpelExpressionParser是标准的spel解析器,利用上面得来的参数名称列表以及参数值列表来获取真实表达式。
问题
基于aspectj的拦截器,@Pointcut中的参数目前未找到动态配置的方法,如果有解决方案的可以告诉我。
将注解机制与具体的锁实现解耦
注解式锁理论上应该与具体的锁实现细节分离,客户端可以任意指定锁,可以是单机下的ReentrantLock也可以是基于redis的分布式锁,当然也可以是基于zookeeper的锁,基于此目的上面我们创建的AbstractRequestLockInterceptor这个拦截器是个抽象类。看下基于redis的分布式锁的子类实现:
@Aspect
publicclassRedisRequestLockInterceptorextendsAbstractRequestLockInterceptor{
@Autowired
privateRedissonServiceredissonService;
privateRedissonClientgetRedissonClient(){
returnthis.redissonService.getRedisson();
}
@Override
protectedLockgetLock(Stringkey){
returnthis.getRedissonClient().getLock(key);
}
@Override
protectedbooleantryLock(longwaitTime,longleaseTime,TimeUnitunit,Locklock)throwsInterruptedException{
return((RLock)lock).tryLock(waitTime,leaseTime,unit);
}
}
注解式锁的应用
只需要在需要同步的方法上增加@RequestLockable,然后根据需要指定或者不指定key,也可以根据实际场景配置锁等待时间以及锁的生命周期。
@RequestLockable(key={"#productId"})
publicvoidinvoke3(IntegerproductId){
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.print("productId:"+productId+"time:"+newDate());
}
当然为了拦截器生效,我们需要在配置文件中配置上拦截器。
注解式锁的优点:锁的逻辑与业务代码完全分离,降低了复杂度。灵活的spel表达式可以灵活的构建锁的key。支持多种锁,可以随意切换而不影响业务代码。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。