簡單注解實現(xiàn)集群同步鎖(spring+redis+注解)
互聯(lián)網(wǎng)面試的時候,是不是面試官常問一個問題如何保證集群環(huán)境下數(shù)據(jù)操作并發(fā)問題,常用的synchronized肯定是無法滿足了,或許你可以借助for update對數(shù)據(jù)加鎖。本文的最終解決方式你只要在方法上加一個@P4jSyn注解就能保證集群環(huán)境下同synchronized的效果,且鎖的key可以任意指定。本注解還支持了鎖的超時機(jī)制。
本文需要對Redis、spring和spring-data-redis有一定的了解。當(dāng)然你可以借助本文的思路對通過注解對方法返回數(shù)據(jù)進(jìn)行緩存,類似com.google.code.simple-spring-memcached的@ReadThroughSingleCache。
第一步: 介紹兩個自定義注解P4jSyn、P4jSynKey
P4jSyn:必選項,標(biāo)記在方法上,表示需要對該方法加集群同步鎖;
P4jSynKey:可選項,加在方法參數(shù)上,表示以方法某個參數(shù)作為鎖的key,用來保證更多的坑,P4jSynKey并不是強(qiáng)制要添加的,當(dāng)沒有P4jSynKey標(biāo)記的情況下只會以P4jSyn的synKey作為鎖key。
package com.yaoguoyin.redis.lock;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <b>同步鎖:</b><br/>
* 主要作用是在服務(wù)器集群環(huán)境下保證方法的synchronize;<br/>
* 標(biāo)記在方法上,使該方法的執(zhí)行具有互斥性,并不保證并發(fā)執(zhí)行方法的先后順序;<br/>
* 如果原有“A任務(wù)”獲取鎖后任務(wù)執(zhí)行時間超過最大允許持鎖時間,且鎖被“B任務(wù)”獲取到,在“B任務(wù)”成功貨物鎖會并不會終止“A任務(wù)”的執(zhí)行;<br/>
* <br/>
* <b>注意:</b><br/>
* 使用過程中需要注意keepMills、toWait、sleepMills、maxSleepMills等參數(shù)的場景使用;<br/>
* 需要安裝redis,并使用spring和spring-data-redis等,借助redis NX等方法實現(xiàn)。
*
* @see com.yaoguoyin.redis.lock.P4jSynKey
* @see com.yaoguoyin.redis.lock.RedisLockAspect
*
* @author partner4java
*
*/
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface P4jSyn {
/**
* 鎖的key<br/>
* 如果想增加坑的個數(shù)添加非固定鎖,可以在參數(shù)上添加@P4jSynKey注解,但是本參數(shù)是必寫選項<br/>
* redis key的拼寫規(guī)則為 "RedisSyn+" + synKey + @P4jSynKey<br/>
*
*/
String synKey();
/**
* 持鎖時間,超時時間,持鎖超過此時間自動丟棄鎖<br/>
* 單位毫秒,默認(rèn)20秒<br/>
* 如果為0表示永遠(yuǎn)不釋放鎖,在設(shè)置為0的情況下toWait為true是沒有意義的<br/>
* 但是沒有比較強(qiáng)的業(yè)務(wù)要求下,不建議設(shè)置為0
*/
long keepMills() default 20 * 1000;
/**
* 當(dāng)獲取鎖失敗,是繼續(xù)等待還是放棄<br/>
* 默認(rèn)為繼續(xù)等待
*/
boolean toWait() default true;
/**
* 沒有獲取到鎖的情況下且toWait()為繼續(xù)等待,睡眠指定毫秒數(shù)繼續(xù)獲取鎖,也就是輪訓(xùn)獲取鎖的時間<br/>
* 默認(rèn)為10毫秒
*
* @return
*/
long sleepMills() default 10;
/**
* 鎖獲取超時時間:<br/>
* 沒有獲取到鎖的情況下且toWait()為true繼續(xù)等待,最大等待時間,如果超時拋出
* {@link java.util.concurrent.TimeoutException.TimeoutException}
* ,可捕獲此異常做相應(yīng)業(yè)務(wù)處理;<br/>
* 單位毫秒,默認(rèn)一分鐘,如果設(shè)置為0即為沒有超時時間,一直獲取下去;
*
* @return
*/
long maxSleepMills() default 60 * 1000;
}
package com.yaoguoyin.redis.lock;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <b>同步鎖 key</b><br/>
* 加在方法的參數(shù)上,指定的參數(shù)會作為鎖的key的一部分
*
* @author partner4java
*
*/
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface P4jSynKey {
/**
* key的拼接順序
*
* @return
*/
int index() default 0;
}
這里就不再對兩個注解進(jìn)行使用上的解釋了,因為注釋已經(jīng)說明的很詳細(xì)了。
使用示例:
package com.yaoguoyin.redis.lock;
import org.springframework.stereotype.Component;
@Component
public class SysTest {
private static int i = 0;
@P4jSyn(synKey = "12345")
public void add(@P4jSynKey(index = 1) String key, @P4jSynKey(index = 0) int key1) {
i++;
System.out.println("i=-===========" + i);
}
}
第二步:切面編程
在不影響原有代碼的前提下,保證執(zhí)行同步,目前最直接的方式就是使用切面編程
package com.yaoguoyin.redis.lock;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
/**
* 鎖的切面編程<br/>
* 針對添加@RedisLock 注解的方法進(jìn)行加鎖
*
* @see com.yaoguoyin.redis.lock.P4jSyn
*
* @author partner4java
*
*/
@Aspect
public class RedisLockAspect {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate<String, Long> redisTemplate;
@Around("execution(* com.yaoguoyin..*(..)) && @annotation(com.yaoguoyin.redis.lock.P4jSyn)")
public Object lock(ProceedingJoinPoint pjp) throws Throwable {
P4jSyn lockInfo = getLockInfo(pjp);
if (lockInfo == null) {
throw new IllegalArgumentException("配置參數(shù)錯誤");
}
String synKey = getSynKey(pjp, lockInfo.synKey());
if (synKey == null || "".equals(synKey)) {
throw new IllegalArgumentException("配置參數(shù)synKey錯誤");
}
boolean lock = false;
Object obj = null;
try {
// 超時時間
long maxSleepMills = System.currentTimeMillis() + lockInfo.maxSleepMills();
while (!lock) {
long keepMills = System.currentTimeMillis() + lockInfo.keepMills();
lock = setIfAbsent(synKey, keepMills);
// 得到鎖,沒有人加過相同的鎖
if (lock) {
obj = pjp.proceed();
}
// 鎖設(shè)置了沒有超時時間
else if (lockInfo.keepMills() <= 0) {
// 繼續(xù)等待獲取鎖
if (lockInfo.toWait()) {
// 如果超過最大等待時間拋出異常
if (lockInfo.maxSleepMills() > 0 && System.currentTimeMillis() > maxSleepMills) {
throw new TimeoutException("獲取鎖資源等待超時");
}
TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
} else {
break;
}
}
// 已過期,并且getAndSet后舊的時間戳依然是過期的,可以認(rèn)為獲取到了鎖
else if (System.currentTimeMillis() > getLock(synKey) && (System.currentTimeMillis() > getSet(synKey, keepMills))) {
lock = true;
obj = pjp.proceed();
}
// 沒有得到任何鎖
else {
// 繼續(xù)等待獲取鎖
if (lockInfo.toWait()) {
// 如果超過最大等待時間拋出異常
if (lockInfo.maxSleepMills() > 0 && System.currentTimeMillis() > maxSleepMills) {
throw new TimeoutException("獲取鎖資源等待超時");
}
TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
}
// 放棄等待
else {
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
// 如果獲取到了鎖,釋放鎖
if (lock) {
releaseLock(synKey);
}
}
return obj;
}
/**
* 獲取包括方法參數(shù)上的key<br/>
* redis key的拼寫規(guī)則為 "RedisSyn+" + synKey + @P4jSynKey
*
*/
private String getSynKey(ProceedingJoinPoint pjp, String synKey) {
try {
synKey = "RedisSyn+" + synKey;
Object[] args = pjp.getArgs();
if (args != null && args.length > 0) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Annotation[][] paramAnnotationArrays = methodSignature.getMethod().getParameterAnnotations();
SortedMap<Integer, String> keys = new TreeMap<Integer, String>();
for (int ix = 0; ix < paramAnnotationArrays.length; ix++) {
P4jSynKey p4jSynKey = getAnnotation(P4jSynKey.class, paramAnnotationArrays[ix]);
if (p4jSynKey != null) {
Object arg = args[ix];
if (arg != null) {
keys.put(p4jSynKey.index(), arg.toString());
}
}
}
if (keys != null && keys.size() > 0) {
for (String key : keys.values()) {
synKey = synKey + key;
}
}
}
return synKey;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@SuppressWarnings("unchecked")
private static <T extends Annotation> T getAnnotation(final Class<T> annotationClass, final Annotation[] annotations) {
if (annotations != null && annotations.length > 0) {
for (final Annotation annotation : annotations) {
if (annotationClass.equals(annotation.annotationType())) {
return (T) annotation;
}
}
}
return null;
}
/**
* 獲取RedisLock注解信息
*/
private P4jSyn getLockInfo(ProceedingJoinPoint pjp) {
try {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
P4jSyn lockInfo = method.getAnnotation(P4jSyn.class);
return lockInfo;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public BoundValueOperations<String, Long> getOperations(String key) {
return redisTemplate.boundValueOps(key);
}
/**
* Set {@code value} for {@code key}, only if {@code key} does not exist.
* <p>
* See http://redis.io/commands/setnx
*
* @param key
* must not be {@literal null}.
* @param value
* must not be {@literal null}.
* @return
*/
public boolean setIfAbsent(String key, Long value) {
return getOperations(key).setIfAbsent(value);
}
public long getLock(String key) {
Long time = getOperations(key).get();
if (time == null) {
return 0;
}
return time;
}
public long getSet(String key, Long value) {
Long time = getOperations(key).getAndSet(value);
if (time == null) {
return 0;
}
return time;
}
public void releaseLock(String key) {
redisTemplate.delete(key);
}
}
RedisLockAspect會對添加注解的方法進(jìn)行特殊處理,具體可看lock方法。
大致思路就是:
1、首選借助redis本身支持對應(yīng)的setIfAbsent方法,該方法的特點是如果redis中已有該數(shù)據(jù)不保存返回false,不存該數(shù)據(jù)保存返回true;
2、如果setIfAbsent返回true標(biāo)識拿到同步鎖,可進(jìn)行操作,操作后并釋放鎖;
3、如果沒有通過setIfAbsent拿到數(shù)據(jù),判斷是否對鎖設(shè)置了超時機(jī)制,沒有設(shè)置判斷是否需要繼續(xù)等待;
4、判斷是否鎖已經(jīng)過期,需要對(System.currentTimeMillis() > getLock(synKey) && (System.currentTimeMillis() > getSet(synKey, keepMills)))進(jìn)行細(xì)細(xì)的揣摩一下,getSet可能會改變了其他人擁有鎖的超時時間,但是幾乎可以忽略;
5、沒有得到任何鎖,判斷繼續(xù)等待還是退出。
第三步:spring的基本配置
#*****************jedis連接參數(shù)設(shè)置*********************# #redis服務(wù)器ip # redis.hostName=127.0.0.1 #redis服務(wù)器端口號# redis.port=6379 #redis服務(wù)器外部訪問密碼 redis.password=XXXXXXXXXX #************************jedis池參數(shù)設(shè)置*******************# #jedis的最大分配對象# jedis.pool.maxActive=1000 jedis.pool.minIdle=100 #jedis最大保存idel狀態(tài)對象數(shù) # jedis.pool.maxIdle=1000 #jedis池沒有對象返回時,最大等待時間 # jedis.pool.maxWait=5000 #jedis調(diào)用borrowObject方法時,是否進(jìn)行有效檢查# jedis.pool.testOnBorrow=true #jedis調(diào)用returnObject方法時,是否進(jìn)行有效檢查 # jedis.pool.testOnReturn=true
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="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/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd http://www.springframework.org/schema/redis http://www.springframework.org/schema/redis/spring-redis.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
<!-- 開啟注解 -->
<aop:aspectj-autoproxy />
<bean class="com.yaoguoyin.redis.lock.RedisLockAspect" />
<!-- 掃描注解包范圍 -->
<context:component-scan base-package="com.yaoguoyin" />
<!-- 引入redis配置 -->
<context:property-placeholder location="classpath:config.properties" />
<!-- 連接池 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="minIdle" value="${jedis.pool.minIdle}" />
<property name="maxIdle" value="${jedis.pool.maxIdle}" />
<property name="maxWaitMillis" value="${jedis.pool.maxWait}" />
</bean>
<!-- p:password="${redis.pass}" -->
<bean id="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 -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="redisConnectionFactory" />
</beans>
redis的安裝本文就不再說明。
測試
package com.yaoguoyin.redis;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:META-INF/spring/redis.xml" })
public class BaseTest extends AbstractJUnit4SpringContextTests {
}
package com.yaoguoyin.redis.lock;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.yaoguoyin.redis.BaseTest;
public class RedisTest extends BaseTest {
@Autowired
private SysTest sysTest;
@Test
public void testHello() throws InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
sysTest.add("xxxxx", 111111);
}
}).start();
}
TimeUnit.SECONDS.sleep(20);
}
@Test
public void testHello2() throws InterruptedException{
sysTest.add("xxxxx", 111111);
TimeUnit.SECONDS.sleep(10);
}
}
你可以對
void com.yaoguoyin.redis.lock.SysTest.add(@P4jSynKey(index=1) String key, @P4jSynKey(index=0) int key1)
去除注解@P4jSyn進(jìn)行測試對比。
ps:本demo的執(zhí)行性能取決于redis和Java交互距離;成千山萬單鎖并發(fā)建議不要使用這種形式,直接通過redis等解決,本demo只解決小并發(fā)不想耦合代碼的形式。
以上就是本文的全部內(nèi)容,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作能帶來一定的幫助,同時也希望多多支持腳本之家!
相關(guān)文章
@FeignClient?path屬性路徑前綴帶路徑變量時報錯的解決
這篇文章主要介紹了@FeignClient?path屬性路徑前綴帶路徑變量時報錯的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07
Java數(shù)據(jù)結(jié)構(gòu)之順序表篇
順序表,全名順序存儲結(jié)構(gòu),是線性表的一種。線性表用于存儲邏輯關(guān)系為“一對一”的數(shù)據(jù),順序表自然也不例外,不僅如此,順序表對數(shù)據(jù)物理存儲結(jié)構(gòu)也有要求。順序表存儲數(shù)據(jù)時,會提前申請一整塊足夠大小的物理空間,然后將數(shù)據(jù)依次存儲起來,存儲時數(shù)據(jù)元素間不留縫隙2022-01-01

