在集羣系統中,有些資源的調用需要加鎖,由於服務是有多個資源對外提供的,資源之間的鎖使用JAVA的鎖是不可行的,所以需要使用其他的方式來實現加鎖,比如訂單與庫存的鎖。
redis是基於內存的key-value的NOSQL數據庫,效率高,單機併發可達1K左右,其中setnx可以很方便實現併發鎖機制
下面是基於SpringBoot以及redis實現的分佈式鎖的工具類,可以在需要加鎖的地方加上註解即可靈活使用
/**
* 該註解(@MethodLock)可以用在方法 上表示給發放加分佈式鎖
* 與
* @author Administrator
*
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LockMethod {
/**
* 鎖的key
* 本參數是必寫選項<br/>
*/
String preKey() default "redisLock";
/**
* 持鎖時間,超時時間,持鎖超過此時間自動丟棄鎖<br/>
* 單位毫秒,默認20秒<br/>
* 如果爲0表示永遠不釋放鎖,知道過程執行完自動釋放鎖,
* 在設置爲0的情況下toWait爲true是沒有意義的<br/>
* 但是沒有比較強的業務要求下,不建議設置爲0
*/
int expireTime() default 20 ;
/**
* 當獲取鎖失敗,是繼續等待還是放棄<br/>
* 默認爲繼續等待
*/
boolean toWait() default true;
// /**
// * 沒有獲取到鎖的情況下且toWait()爲繼續等待,睡眠指定毫秒數繼續獲取鎖,也就是輪訓獲取鎖的時間<br/>
// * 默認爲10毫秒
// *
// * @return
// */
// long sleepMills() default 10;
/**
* 鎖獲取超時時間:<br/>
* 沒有獲取到鎖的情況下且toWait()爲true繼續等待,最大等待時間,如果超時拋出
* {@link java.util.concurrent.TimeoutException.TimeoutException}
* ,可捕獲此異常做相應業務處理;<br/>
* 單位毫秒,默認3秒鐘,如果設置爲0即爲沒有超時時間,一直獲取下去;
*
* @return
*/
long maxSleepMills() default 3 * 1000;
}
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LockParameter {
/**
* 含有成員變量的複雜對象中需要加鎖的成員變量,如一個商品對象的商品ID
* 也就是複雜對象中的屬性名稱
* 比如User user 即爲複雜對象 使用對象中的屬性 username來作爲sunKey
* isObject=false 時候該功能才起作用
* @return
*/
String field() default "";
/**
* 是否是基本的簡單屬性 true表示是簡單屬性,false 表示是複雜對象
* true:表示 int,long,String,char 等基本數據類型
* false:表示對象模型 比如User
* @return
*/
boolean isSimple() default true;
}
@Aspect
@Component
public class RedisLockAop {
// @Before("@annotation(CacheLock)")
// public void requestLimit(JoinPoint point, CacheLock cacheLock)throws Throwable{
//
// }
private static Logger logger = LoggerFactory.getLogger(RedisLockAop.class);
@Autowired
private SpringJedisUtilInterface redisUtil;
// @Autowired
// private RedisUtil redisUtil;
/**
* 通過前置攔截,攔截所有經過CacheLock註解過的方法
* @param point
* @param CacheLock
* @throws Throwable
*/
@Around("@annotation(LockMethod)")
public Object requestLimit(ProceedingJoinPoint joinPoint) throws Throwable{
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
LockMethod methodLock = method.getAnnotation(LockMethod.class);
if(methodLock ==null){
throw new RedisLockException("配置參數錯誤");
}
Object[] args = joinPoint.getArgs();
// 得到被代理的方法
//獲得方法中參數的註解
Annotation[][] annotations = method.getParameterAnnotations();
//根據獲取到的參數註解和參數列表獲得加鎖的參數
String sunKey = getLockEndKey(annotations,args);
String key = methodLock.preKey()+"_"+sunKey+"_lock";
logger.info(key);
// 超時時間 (最大的等待時間)
long maxSleepMills = methodLock.maxSleepMills();
// 獲得鎖之後最大可持有鎖的時間
int expire = methodLock.expireTime();
// 是否等待
boolean toWait = methodLock.toWait();
boolean lock = false;
Object obj = null;
int radomInt = 0;// 每個線程隨機等待時間
try {
// 說明沒有得到鎖,等待輪訓的去獲得鎖
while (!lock) {
lock =getLock(key, expire);
// 得到鎖,沒有人加過相同的鎖
if (lock) {
logger.info("線程獲得鎖開始執行"+"---"+redisUtil);
obj = joinPoint.proceed();
break;
}else if(toWait){
radomInt = new Random().nextInt(99);
maxSleepMills =maxSleepMills- radomInt;
logger.info("沒獲得鎖,隨機等待毫秒數:"+radomInt+"---"+redisUtil);
if(maxSleepMills<=0){
logger.info("獲取鎖資源等待超時"+"---"+redisUtil);
break;
// throw new CacheLockException("獲取鎖資源等待超時,等待超時");
}
TimeUnit.MILLISECONDS.sleep(radomInt);
}else{
break;
}
}
} catch (Throwable e) {
e.printStackTrace();
logger.info(e.getMessage()+"--"+e);
throw e;
}finally{
if(lock){
logger.info("執行刪除操作");
redisUtil.del(key );//直接刪除
}
}
return obj;
}
/**
* 從方法參數中找出@lockedComplexOnbject的參數,在redis中取該參數對應的鎖
* @param annotations
* @param args
* @return
* @throws RedisLockException
*/
private String getLockEndKey(Annotation[][] annotations,Object[] args) throws RedisLockException{
if(null == args || args.length == 0){
throw new RedisLockException("方法參數爲空,沒有被鎖定的對象");
}
if(null == annotations || annotations.length == 0){
throw new RedisLockException("沒有被註解的參數");
}
SortedMap<Integer, String> keys = new TreeMap<Integer, String>();
String keyString;
//直接在多個參數上註解
for(int i = 0;i < annotations.length;i++){
for(int j = 0;j < annotations[i].length;j++){
if(annotations[i][j] instanceof Value){
Object arg = args[i];
logger.info("--Value args[i]--"+i+"------"+arg);
}
if(annotations[i][j] instanceof LockParameter){//註解爲LockedComplexObject
LockParameter lockParameter = (LockParameter)annotations[i][j];
Object arg = args[i];
logger.info("--lockParameter args[i]--"+i+"------"+arg);
try {
if(lockParameter.isSimple()){
keys.put(i, String.valueOf(arg));
}else{
keyString = args[i].getClass().getField(lockParameter.field()).toString();
keys.put(i,keyString);
}
} catch (NoSuchFieldException |SecurityException e) {
e.printStackTrace();
throw new RedisLockException("註解對象中沒有該屬性"+lockParameter.field());
}
}
}
}
String sunKey ="";
if (keys != null && keys.size() > 0) {
for (String key : keys.values()) {
sunKey = sunKey + key;
}
}
return sunKey;
}
public boolean getLock(String key ,int expire) throws InterruptedException{
if(redisUtil.setnx(key, String.valueOf(expire))==1){//1插入成功且key不存在,0未插入,key存在
redisUtil.expired(key, expire);
return true;
}
return false;
}
}
測試用例
@Service
public class SecKillImpl implements SeckillInterface{
public static Map<Long, Long> inventory ;
static{
inventory = new HashMap<>();
inventory.put(10000001L, 10000l);
inventory.put(10000002L, 10000l);
}
@Override
@LockMethod(preKey="Seckill",expireTime=1)
public void secKill(String arg1, @LockParameter Long arg2, int a) {
//最簡單的秒殺,這裏僅作爲demo示例
System.out.println("commodityId"+arg2+" 線程幾號="+a);
reduceInventory(arg2);
}
//模擬秒殺操作,姑且認爲一個秒殺就是將庫存減一,實際情景要複雜的多
public Long reduceInventory(Long commodityId){
inventory.put(commodityId,inventory.get(commodityId) - 1);
return inventory.get(commodityId);
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = FirstSpringBootApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SecKillTest {
private static Long commidityId1 = 10000001L;
private static Long commidityId2 = 10000002L;
@Autowired
private SeckillInterface proxy;
@Test
public void testSecKill(){
int threadCount = 1000;
int splitPoint = 500;
CountDownLatch endCount = new CountDownLatch(threadCount);
CountDownLatch beginCount = new CountDownLatch(1);
SecKillImpl testClass = new SecKillImpl();
Thread[] threads = new Thread[threadCount];
//起500個線程,秒殺第一個商品
for(int i= 0;i < splitPoint;i++){
int a = i;
threads[i] = new Thread(new Runnable() {
public void run() {
try {
//等待在一個信號量上,掛起
beginCount.await();
//用動態代理的方式調用secKill方法
proxy.secKill("test", commidityId1,a);
endCount.countDown();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
threads[i].start();
}
for(int i= splitPoint;i < threadCount;i++){
int a =i;
threads[i] = new Thread(new Runnable() {
public void run() {
try {
//等待在一個信號量上,掛起
beginCount.await();
//用動態代理的方式調用secKill方法
beginCount.await();
proxy.secKill("test", commidityId2,a );
//testClass.testFunc("test", 10000001L);
endCount.countDown();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
threads[i].start();
}
long startTime = System.currentTimeMillis();
//主線程釋放開始信號量,並等待結束信號量
beginCount.countDown();
try {
//主線程等待結束信號量
endCount.await();
//觀察秒殺結果是否正確
System.out.println(SecKillImpl.inventory.get(commidityId1));
System.out.println(SecKillImpl.inventory.get(commidityId2));
// System.out.println("error count" + CacheLockInterceptor.ERROR_COUNT);
System.out.println("total cost " + (System.currentTimeMillis() - startTime));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
以下是項目下載地址:項目中包含了redis併發鎖,redis 的session共享 分佈式緩存 druid數據庫連接池,druid監控等
源碼下載地址:https://git.oschina.net/xufan0711/firstSpringboot/tree/master/