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节点; 导致锁丢失。概率很小,可以不考虑。
实例代码下载:https://download.csdn.net/my/downloads

猜你喜欢

转载自blog.csdn.net/qq_32579557/article/details/89294808