Java 利用分佈式共享鎖實現防止方法重複調用(防刷單及redis分佈式鎖的實現)

   最近公司商城訂單出現重複訂單數據問題,比較棘手,一直在找原因,沒有發現問題,太坑了,後來決定在原有的業務基礎上面加上防刷單處理和redis分佈式鎖,雙重保證應用的安全和穩定性。


一、防刷單原理:防止一個方法,在方法參數值相同的情況下,短時間頻繁調用,這裏根據spring中的AOP原理來實現的,自己定義了一個註解,這個註解主要用來判斷哪些方法上面加了這個註解,就做參數請求處理,先配置具體的aop切面路徑掃描類中的方法,處理是根據這個請求的路徑獲取相應的方法中的參數做具體分析。

實現的步驟:

  1. 定義一個註解(主要用來判斷哪些方法要做防重複提交處理)
  2. 通過spring中的AOP進行掃描,方法處理。
  3. 設置一個過期時間來處理redis分佈式鎖處理(這裏會在redis分佈式鎖中實現
 

 

/*********定義防重複請求方法註解*********/

package com.lolaage.common.annotations;
import java.lang.annotation.*;
/**
 * 定義一個註解(主要用來判斷哪些方法要做防重複提交處理)
 * @Description 防止同一個方法被頻繁執行(是否需要頻繁執行看參數params是否不一樣)
 * @Date 19:35 2019/4/9
 * @Param
 * @return
 **/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SameMethodFrequentlyRun {
	/**
	 * @Description 當方法的參數是實體對象,對象必須對象重寫equal和hashcode方法
	 **/
	String params()  default "";
	String description()  default "";
	/**
	 * @Description
	 **/
	long milliseconds()  default 30000L;
}    

  
/*************下面是具體的方法處理請求參數過程***************/


package com.lolaage.common.aop;
import com.lolaage.base.po.JsonModel;
import com.lolaage.common.annotations.SameMethodFrequentlyRun;
import com.lolaage.helper.util.RedisLockTemplate;
import com.lolaage.util.StringUtil;
import org.apache.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @Description  防止同一個方法被頻繁執行AOP(是否需要頻繁執行看參數params是否不一樣)
 **/

@Aspect
@Component
public class SameMethodFrequentlyRunAop {
	private static Logger logger = Logger.getLogger(SameMethodFrequentlyRunAop.class);


	// 配置接入點,即爲所要記錄的action操作目錄
	@Pointcut("execution(* com.lolaage.helper.web.controller..*.*(..))")
	private void controllerAspect() {

	}

	@Around("controllerAspect()")
	public Object around(ProceedingJoinPoint pjp) {
		Object returnObj=null;
		StringBuilder sb=new StringBuilder();

		// 攔截的實體類,就是當前正在執行的controller
		Object target = pjp.getTarget();
		//獲取全類名
		String className=target.getClass().getName();
		// 攔截的方法名稱。當前正在執行的方法
		String methodName = pjp.getSignature().getName();
		// 攔截的方法參數
		Object[] args = pjp.getArgs();

		// 攔截的放參數類型
		Signature sig = pjp.getSignature();
		MethodSignature msig = (MethodSignature) sig ;

		Class[] parameterTypes = msig.getMethod().getParameterTypes();
		sb.append(className);
		for (Object o : args) {
			if(o==null){
				continue;
			}
			int i = o.hashCode();
			sb.append(":");
			sb.append(i);
		}
		// 獲得被攔截的方法
		Method method = null;
		try {
			method = target.getClass().getMethod(methodName, parameterTypes);
			SameMethodFrequentlyRun sameMethodFrequentlyRun = method.getAnnotation(SameMethodFrequentlyRun.class);
			if (sameMethodFrequentlyRun != null) {
				String description = sameMethodFrequentlyRun.description();
				String params = sameMethodFrequentlyRun.params();
				if(StringUtil.isEmpty(params)){
					params=sb.toString();
				}
				long milliseconds = sameMethodFrequentlyRun.milliseconds();
				Boolean isGetLock = RedisLockTemplate.distributedLock_v2(params, description, milliseconds, false);
				if(!isGetLock){
					//提示不要重複操作
					JsonModel result = new JsonModel();
					return result.setErrCode(5004);
				}
			}
		} catch (NoSuchMethodException e) {
			logger.error("分佈式防重複操作異常:AOP只會攔截public方法,非public會報異常,如果你要將你的方法加入到aop攔截中,請修改方法的修飾符:"+e.getMessage());
		}
		try {
			  returnObj = pjp.proceed();
		} catch (Throwable e) {
			logger.error("分佈式防重複操作異常Throwable:"+e.getMessage());
			e.printStackTrace();
		}
		return returnObj;
	}


}


/**
* 分佈式鎖壓力測試,和防重複測試
* @return
*/
@SameMethodFrequentlyRun(description="查詢操作日誌",milliseconds = 10000L)
@RequestMapping("/pressureLock")
public void pressureLock(String key,QuitParam quitParam) {
 System.out.println(this.hashCode()+"---"+Thread.currentThread().getName()+":測試開始");
 System.out.println(this.hashCode()+"---"+Thread.currentThread().getName()+"測試結束");
}

二、redis分佈式對象鎖的原理:

 解釋:  針對某種資源,需要被整個系統的各臺服務器共享訪問,但是隻允許一臺服務器同時訪問。比如說訂單服務是做成集羣的,當兩個以上結點同時收到一個相同訂單的創建指令,這時併發就產生了,系統就會重複創建訂單。而分佈式共享鎖就是解決這類問題 

原理:對高併發請求的時候,我們使用redis分佈式共享鎖來處理,通過set方法設置對應的key-value和milliseconds過期時間,在規定的時間內保證鎖可以釋放出來,通過eval來解鎖。

實現代碼:

/**
 * @Description 分佈式鎖模板
 * @Date 10:39 2019/4/9
 * @Param [key, actionLog, expireSecond]
 * @return java.lang.Boolean
 **/
public static  Boolean distributedLock_v2(String key,String actionLog, long milliseconds,boolean isDelLock){
	RedisBaseDao redisDao = RedisUtil.getRedisDao();
	boolean isGetLock=false;
   String requestId = UUID.randomUUID().toString();
	try {
		isGetLock = redisDao.getDistributedLock(key,requestId , milliseconds);
		if(!isGetLock){
			logger.error("分佈式鎖攔截,不能重複操作,"+key+",actionLog="+actionLog);
		}
		return isGetLock;
	} catch (Exception e) {
		e.printStackTrace();
		if(e instanceof RedisException){
			logger.error("redis 分佈式鎖異常,可能存在重複操作的的可能性,key="+key+",actionLog="+actionLog+",e="+e);
			return true;
		}
	}finally {
		if(isGetLock&&isDelLock){
			try {
				redisDao.releaseDistributedLock(key,requestId);
			} catch (Exception e) {
				e.printStackTrace();
				logger.error("分佈式鎖釋放鎖失敗,key="+key+",actionLog="+actionLog+","+e);
			}
		}
	}
	return false;
}

/**
 * 嘗試獲取分佈式鎖
 * @param lockKey 鎖
 * @param requestId 請求標識
 * @param milliseconds 超期時間
 * @return 是否獲取成功
 */

private static final Long RELEASE_SUCCESS = 1L;
public   boolean getDistributedLock(String lockKey, String requestId, Long milliseconds) {
	  return   this.setNx(lockKey, requestId, milliseconds);
}
/**
 * 釋放分佈式鎖
 * @param lockKey 鎖
 * @param requestId 請求標識
 * @return 是否釋放成功
 */
public   boolean releaseDistributedLock( String lockKey, String requestId) {
	return this.deleteKeyForSameValue(lockKey,requestId);
}


 public Boolean setNx(  String key,   String value,Long expireTime) {
	Boolean isSet = redisTemplate.execute(new RedisCallback<Boolean>() {
		@Override
		public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
			//過期時間好處:即使服務器宕機了,也能保證鎖被正確釋放。
			//setNx原子性操作,防止同一把鎖在同一時間可能被不同線程獲取到
			Jedis jedis = (Jedis) redisConnection.getNativeConnection();
			String result = jedis.set(key, value, "nx", "px", expireTime);
			if("OK".equals(result)){
				return true;
			}
			return false;
		}
	});
	return isSet;
}
public Boolean deleteKeyForSameValue(  String key,   String value) {
  return  redisTemplate.execute(new RedisCallback<Boolean>() {
	 @Override
	public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
		Jedis jedis = (Jedis) redisConnection.getNativeConnection();
			//刪除key的時候,先判斷該key對應的value是否等於先前設置的隨機值,只有當兩者相等的時候才刪除該key
	//防止釋放其他客戶端獲取到的鎖
	//原子性操作
	String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return 
  redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
			if (RELEASE_SUCCESS.equals(result)) {
				return true;
			}
			return false;
		}
	});
}

## 方案優點
> 多個服務器競爭資源,需要排隊,解決類似一個訂單被多個服務器提交問題。

## 方案缺點 
- 試用與一主多從的redis集羣,如果多主多從,不能解決共享鎖問題   
    -這個問題解決方案[https://yq.aliyun.com/articles/674394](https://yq.aliyun.com/articles/674394),[https://blog.csdn.net/chen_kkw/article/details/81433470](https://blog.csdn.net/chen_kkw/article/details/81433470)
- 同時當一主多從服務器,主機宕機,有丟失鎖的風險,概率很小。 
    - **場景**
    - 在Redis的master節點上拿到了鎖,但是這個加鎖的key還沒有同步到slave節點,master故障,發生故障轉移,slave節點升級爲master節點; 導致鎖丟失。概率很小,可以不考慮。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章