简单注解实现集群同步锁(spring+redis+注解)
互联网面试的时候,是不是面试官常问一个问题如何保证集群环境下数据操作并发问题,常用的synchronized肯定是无法满足了,或许你可以借助forupdate对数据加锁。本文的最终解决方式你只要在方法上加一个@P4jSyn注解就能保证集群环境下同synchronized的效果,且锁的key可以任意指定。本注解还支持了锁的超时机制。
本文需要对Redis、spring和spring-data-redis有一定的了解。当然你可以借助本文的思路对通过注解对方法返回数据进行缓存,类似com.google.code.simple-spring-memcached的@ReadThroughSingleCache。
第一步: 介绍两个自定义注解P4jSyn、P4jSynKey
P4jSyn:必选项,标记在方法上,表示需要对该方法加集群同步锁;
P4jSynKey:可选项,加在方法参数上,表示以方法某个参数作为锁的key,用来保证更多的坑,P4jSynKey并不是强制要添加的,当没有P4jSynKey标记的情况下只会以P4jSyn的synKey作为锁key。
packagecom.yaoguoyin.redis.lock;
importjava.lang.annotation.ElementType;
importjava.lang.annotation.Inherited;
importjava.lang.annotation.Retention;
importjava.lang.annotation.RetentionPolicy;
importjava.lang.annotation.Target;
/**
*<b>同步锁:</b><br/>
*主要作用是在服务器集群环境下保证方法的synchronize;<br/>
*标记在方法上,使该方法的执行具有互斥性,并不保证并发执行方法的先后顺序;<br/>
*如果原有“A任务”获取锁后任务执行时间超过最大允许持锁时间,且锁被“B任务”获取到,在“B任务”成功货物锁会并不会终止“A任务”的执行;<br/>
*<br/>
*<b>注意:</b><br/>
*使用过程中需要注意keepMills、toWait、sleepMills、maxSleepMills等参数的场景使用;<br/>
*需要安装redis,并使用spring和spring-data-redis等,借助redisNX等方法实现。
*
*@seecom.yaoguoyin.redis.lock.P4jSynKey
*@seecom.yaoguoyin.redis.lock.RedisLockAspect
*
*@authorpartner4java
*
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public@interfaceP4jSyn{
/**
*锁的key<br/>
*如果想增加坑的个数添加非固定锁,可以在参数上添加@P4jSynKey注解,但是本参数是必写选项<br/>
*rediskey的拼写规则为"RedisSyn+"+synKey+@P4jSynKey<br/>
*
*/
StringsynKey();
/**
*持锁时间,超时时间,持锁超过此时间自动丢弃锁<br/>
*单位毫秒,默认20秒<br/>
*如果为0表示永远不释放锁,在设置为0的情况下toWait为true是没有意义的<br/>
*但是没有比较强的业务要求下,不建议设置为0
*/
longkeepMills()default20*1000;
/**
*当获取锁失败,是继续等待还是放弃<br/>
*默认为继续等待
*/
booleantoWait()defaulttrue;
/**
*没有获取到锁的情况下且toWait()为继续等待,睡眠指定毫秒数继续获取锁,也就是轮训获取锁的时间<br/>
*默认为10毫秒
*
*@return
*/
longsleepMills()default10;
/**
*锁获取超时时间:<br/>
*没有获取到锁的情况下且toWait()为true继续等待,最大等待时间,如果超时抛出
*{@linkjava.util.concurrent.TimeoutException.TimeoutException}
*,可捕获此异常做相应业务处理;<br/>
*单位毫秒,默认一分钟,如果设置为0即为没有超时时间,一直获取下去;
*
*@return
*/
longmaxSleepMills()default60*1000;
}
packagecom.yaoguoyin.redis.lock;
importjava.lang.annotation.ElementType;
importjava.lang.annotation.Inherited;
importjava.lang.annotation.Retention;
importjava.lang.annotation.RetentionPolicy;
importjava.lang.annotation.Target;
/**
*<b>同步锁key</b><br/>
*加在方法的参数上,指定的参数会作为锁的key的一部分
*
*@authorpartner4java
*
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public@interfaceP4jSynKey{
/**
*key的拼接顺序
*
*@return
*/
intindex()default0;
}
这里就不再对两个注解进行使用上的解释了,因为注释已经说明的很详细了。
使用示例:
packagecom.yaoguoyin.redis.lock;
importorg.springframework.stereotype.Component;
@Component
publicclassSysTest{
privatestaticinti=0;
@P4jSyn(synKey="12345")
publicvoidadd(@P4jSynKey(index=1)Stringkey,@P4jSynKey(index=0)intkey1){
i++;
System.out.println("i=-==========="+i);
}
}
第二步:切面编程
在不影响原有代码的前提下,保证执行同步,目前最直接的方式就是使用切面编程
packagecom.yaoguoyin.redis.lock;
importjava.lang.annotation.Annotation;
importjava.lang.reflect.Method;
importjava.util.SortedMap;
importjava.util.TreeMap;
importjava.util.concurrent.TimeUnit;
importjava.util.concurrent.TimeoutException;
importorg.aspectj.lang.ProceedingJoinPoint;
importorg.aspectj.lang.annotation.Around;
importorg.aspectj.lang.annotation.Aspect;
importorg.aspectj.lang.reflect.MethodSignature;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.beans.factory.annotation.Qualifier;
importorg.springframework.data.redis.core.BoundValueOperations;
importorg.springframework.data.redis.core.RedisTemplate;
/**
*锁的切面编程<br/>
*针对添加@RedisLock注解的方法进行加锁
*
*@seecom.yaoguoyin.redis.lock.P4jSyn
*
*@authorpartner4java
*
*/
@Aspect
publicclassRedisLockAspect{
@Autowired
@Qualifier("redisTemplate")
privateRedisTemplate<String,Long>redisTemplate;
@Around("execution(*com.yaoguoyin..*(..))&&@annotation(com.yaoguoyin.redis.lock.P4jSyn)")
publicObjectlock(ProceedingJoinPointpjp)throwsThrowable{
P4jSynlockInfo=getLockInfo(pjp);
if(lockInfo==null){
thrownewIllegalArgumentException("配置参数错误");
}
StringsynKey=getSynKey(pjp,lockInfo.synKey());
if(synKey==null||"".equals(synKey)){
thrownewIllegalArgumentException("配置参数synKey错误");
}
booleanlock=false;
Objectobj=null;
try{
//超时时间
longmaxSleepMills=System.currentTimeMillis()+lockInfo.maxSleepMills();
while(!lock){
longkeepMills=System.currentTimeMillis()+lockInfo.keepMills();
lock=setIfAbsent(synKey,keepMills);
//得到锁,没有人加过相同的锁
if(lock){
obj=pjp.proceed();
}
//锁设置了没有超时时间
elseif(lockInfo.keepMills()<=0){
//继续等待获取锁
if(lockInfo.toWait()){
//如果超过最大等待时间抛出异常
if(lockInfo.maxSleepMills()>0&&System.currentTimeMillis()>maxSleepMills){
thrownewTimeoutException("获取锁资源等待超时");
}
TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
}else{
break;
}
}
//已过期,并且getAndSet后旧的时间戳依然是过期的,可以认为获取到了锁
elseif(System.currentTimeMillis()>getLock(synKey)&&(System.currentTimeMillis()>getSet(synKey,keepMills))){
lock=true;
obj=pjp.proceed();
}
//没有得到任何锁
else{
//继续等待获取锁
if(lockInfo.toWait()){
//如果超过最大等待时间抛出异常
if(lockInfo.maxSleepMills()>0&&System.currentTimeMillis()>maxSleepMills){
thrownewTimeoutException("获取锁资源等待超时");
}
TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
}
//放弃等待
else{
break;
}
}
}
}catch(Exceptione){
e.printStackTrace();
throwe;
}finally{
//如果获取到了锁,释放锁
if(lock){
releaseLock(synKey);
}
}
returnobj;
}
/**
*获取包括方法参数上的key<br/>
*rediskey的拼写规则为"RedisSyn+"+synKey+@P4jSynKey
*
*/
privateStringgetSynKey(ProceedingJoinPointpjp,StringsynKey){
try{
synKey="RedisSyn+"+synKey;
Object[]args=pjp.getArgs();
if(args!=null&&args.length>0){
MethodSignaturemethodSignature=(MethodSignature)pjp.getSignature();
Annotation[][]paramAnnotationArrays=methodSignature.getMethod().getParameterAnnotations();
SortedMap<Integer,String>keys=newTreeMap<Integer,String>();
for(intix=0;ix<paramAnnotationArrays.length;ix++){
P4jSynKeyp4jSynKey=getAnnotation(P4jSynKey.class,paramAnnotationArrays[ix]);
if(p4jSynKey!=null){
Objectarg=args[ix];
if(arg!=null){
keys.put(p4jSynKey.index(),arg.toString());
}
}
}
if(keys!=null&&keys.size()>0){
for(Stringkey:keys.values()){
synKey=synKey+key;
}
}
}
returnsynKey;
}catch(Exceptione){
e.printStackTrace();
}
returnnull;
}
@SuppressWarnings("unchecked")
privatestatic<TextendsAnnotation>TgetAnnotation(finalClass<T>annotationClass,finalAnnotation[]annotations){
if(annotations!=null&&annotations.length>0){
for(finalAnnotationannotation:annotations){
if(annotationClass.equals(annotation.annotationType())){
return(T)annotation;
}
}
}
returnnull;
}
/**
*获取RedisLock注解信息
*/
privateP4jSyngetLockInfo(ProceedingJoinPointpjp){
try{
MethodSignaturemethodSignature=(MethodSignature)pjp.getSignature();
Methodmethod=methodSignature.getMethod();
P4jSynlockInfo=method.getAnnotation(P4jSyn.class);
returnlockInfo;
}catch(Exceptione){
e.printStackTrace();
}
returnnull;
}
publicBoundValueOperations<String,Long>getOperations(Stringkey){
returnredisTemplate.boundValueOps(key);
}
/**
*Set{@codevalue}for{@codekey},onlyif{@codekey}doesnotexist.
*<p>
*Seehttp://redis.io/commands/setnx
*
*@paramkey
*mustnotbe{@literalnull}.
*@paramvalue
*mustnotbe{@literalnull}.
*@return
*/
publicbooleansetIfAbsent(Stringkey,Longvalue){
returngetOperations(key).setIfAbsent(value);
}
publiclonggetLock(Stringkey){
Longtime=getOperations(key).get();
if(time==null){
return0;
}
returntime;
}
publiclonggetSet(Stringkey,Longvalue){
Longtime=getOperations(key).getAndSet(value);
if(time==null){
return0;
}
returntime;
}
publicvoidreleaseLock(Stringkey){
redisTemplate.delete(key);
}
}
RedisLockAspect会对添加注解的方法进行特殊处理,具体可看lock方法。
大致思路就是:
1、首选借助redis本身支持对应的setIfAbsent方法,该方法的特点是如果redis中已有该数据不保存返回false,不存该数据保存返回true;
2、如果setIfAbsent返回true标识拿到同步锁,可进行操作,操作后并释放锁;
3、如果没有通过setIfAbsent拿到数据,判断是否对锁设置了超时机制,没有设置判断是否需要继续等待;
4、判断是否锁已经过期,需要对(System.currentTimeMillis()>getLock(synKey)&&(System.currentTimeMillis()>getSet(synKey,keepMills)))进行细细的揣摩一下,getSet可能会改变了其他人拥有锁的超时时间,但是几乎可以忽略;
5、没有得到任何锁,判断继续等待还是退出。
第三步:spring的基本配置
#*****************jedis连接参数设置*********************# #redis服务器ip# redis.hostName=127.0.0.1 #redis服务器端口号# redis.port=6379 #redis服务器外部访问密码 redis.password=XXXXXXXXXX #************************jedis池参数设置*******************# #jedis的最大分配对象# jedis.pool.maxActive=1000 jedis.pool.minIdle=100 #jedis最大保存idel状态对象数# jedis.pool.maxIdle=1000 #jedis池没有对象返回时,最大等待时间# jedis.pool.maxWait=5000 #jedis调用borrowObject方法时,是否进行有效检查# jedis.pool.testOnBorrow=true #jedis调用returnObject方法时,是否进行有效检查# jedis.pool.testOnReturn=true
<?xmlversion="1.0"encoding="UTF-8"?>
<beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"xmlns:jee="http://www.springframework.org/schema/jee"xmlns:tx="http://www.springframework.org/schema/tx"xmlns:aop="http://www.springframework.org/schema/aop"xmlns:redis="http://www.springframework.org/schema/redis"xmlns:cache="http://www.springframework.org/schema/cache"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-4.2.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.2.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-4.1.xsdhttp://www.springframework.org/schema/redishttp://www.springframework.org/schema/redis/spring-redis.xsd
http://www.springframework.org/schema/cachehttp://www.springframework.org/schema/cache/spring-cache.xsd">
<!--开启注解-->
<aop:aspectj-autoproxy/>
<beanclass="com.yaoguoyin.redis.lock.RedisLockAspect"/>
<!--扫描注解包范围-->
<context:component-scanbase-package="com.yaoguoyin"/>
<!--引入redis配置-->
<context:property-placeholderlocation="classpath:config.properties"/>
<!--连接池-->
<beanid="poolConfig"class="redis.clients.jedis.JedisPoolConfig">
<propertyname="minIdle"value="${jedis.pool.minIdle}"/>
<propertyname="maxIdle"value="${jedis.pool.maxIdle}"/>
<propertyname="maxWaitMillis"value="${jedis.pool.maxWait}"/>
</bean>
<!--p:password="${redis.pass}"-->
<beanid="redisConnectionFactory"class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"p:host-name="${redis.hostName}"p:port="${redis.port}"
p:password="${redis.password}"p:pool-config-ref="poolConfig"/>
<!--类似于jdbcTemplate-->
<beanid="redisTemplate"class="org.springframework.data.redis.core.RedisTemplate"p:connection-factory-ref="redisConnectionFactory"/>
</beans>
redis的安装本文就不再说明。
测试
packagecom.yaoguoyin.redis;
importorg.junit.runner.RunWith;
importorg.springframework.test.context.ContextConfiguration;
importorg.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
importorg.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:META-INF/spring/redis.xml"})
publicclassBaseTestextendsAbstractJUnit4SpringContextTests{
}
packagecom.yaoguoyin.redis.lock;
importjava.util.concurrent.TimeUnit;
importorg.junit.Test;
importorg.springframework.beans.factory.annotation.Autowired;
importcom.yaoguoyin.redis.BaseTest;
publicclassRedisTestextendsBaseTest{
@Autowired
privateSysTestsysTest;
@Test
publicvoidtestHello()throwsInterruptedException{
for(inti=0;i<100;i++){
newThread(newRunnable(){
@Override
publicvoidrun(){
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}
sysTest.add("xxxxx",111111);
}
}).start();
}
TimeUnit.SECONDS.sleep(20);
}
@Test
publicvoidtestHello2()throwsInterruptedException{
sysTest.add("xxxxx",111111);
TimeUnit.SECONDS.sleep(10);
}
}
你可以对
voidcom.yaoguoyin.redis.lock.SysTest.add(@P4jSynKey(index=1)Stringkey,@P4jSynKey(index=0)intkey1)
去除注解@P4jSyn进行测试对比。
ps:本demo的执行性能取决于redis和Java交互距离;成千山万单锁并发建议不要使用这种形式,直接通过redis等解决,本demo只解决小并发不想耦合代码的形式。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持毛票票!