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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_32579557/article/details/89294808

智能推荐

大量数据写入时,初始化指定ArrayList长度_arraylist指定长度_陈晨辰呀的博客-程序员秘密

ArrayList 踩坑List&lt;String&gt; temp = new ArrayList() ;//获取一批数据List&lt;String&gt; all = getData();for(String str : all) {temp.add(str);}首先大家看看这段代码有什么问题嘛?其实在大部分情况下这都是没啥问题,无非就是循环的往 Array...

听说程序猿不会撩妹,我笑了_苏南South的博客-程序员秘密

从前我没得选,现在我只想做一个好人。 多年的人生经验告诉我。程序员,可以不帅,但是操作一定要骚 在调查了一百个小姐姐之后,我确定做那些烟花啊、粒子动画啊、3d相册真的很有用,她们真的很喜欢 总之调查完之后,我需要做29个相册,22个烟花,15个粒子动画,以及一些你看着办吧,反正我要好看的。 哈~ 我就是那个好看的 给你吧 所以一个好看的表白网页是少不了了的。还有那种做个小游戏的玩过了之后看到表白信息的,我的天啊,连看到网页会忍不住自己做一个去battle的杠精

[带修莫队] BZOJ2120: 数颜色_bzoj 带修莫队_Lynstery的博客-程序员秘密

题意给出一个n个元素的序列a。执行m次操作: 1. Q L R代表询问[L,R]有几种不同的数字。 2. R x y 把第a[x]改为y。题解带修莫队裸题。 怎么搞带修莫队呢?就是加一维时间,然后对L,R都分块。块的大小定为n23n^{\frac{2}{3}} 。 具体来说,先对L分块,对与同一L块中再对R分块。 bool operator < (const data1 &b)con

【学习总结】使用Swagger实现API规范_xclia的博客-程序员秘密

【学习总结】使用Swagger实现API规范1. 建立一个springboot工程2. 建立实体类3. 建立数据库并链接数据库,进行增删改查编写4. 为工程增加swagger配置5. 请求页面,显示结果代码如下1、建立工程2、建立实体类import io.swagger.annotations.ApiModel;import lombok.Data;import javax...

学习JVM--垃圾回收(二)GC收集器_zk65645的博客-程序员秘密

1. 前言  在上一篇文章中,介绍了JVM中垃圾回收的原理和算法。介绍了通过引用计数和对象可达性分析的算法来筛选出已经没有使用的对象,然后介绍了垃圾收集器中使用的三种收集算法:标记-清除、标记-整理、标记-复制算法。  介绍完原理,在这篇文章中,我们将介绍当前JVM中已经实现的垃圾收集器,以及与收集器主题相关的一些内容。  首先,我们将在上一篇文章中提到分代收集机制的基础上,介绍下现代

Spring Data JPA实现数据的增删改查操作_基于jparepository的增删改查_pan_junbiao的博客-程序员秘密

1、JPA(Java持久层API)1.1认识Spring DataSpring Data 是Spring的一个子项目,旨在统一和简化各类型数据的持久化存储方式,而不拘泥于是关系型数据库还是NoSQL数据库。无论是哪种持久化存储方式,数据访问对象(Data Access Objects,DAO)都会提供对对象的增加、删除、修改和查询的方法,以及排序和分页方法等。Spring Data 提...

随便推点

高德离线地图开发API_高德地图api离线使用_tangshenq17565的博客-程序员秘密

谷歌(百度、高德)离线地图开发环境搭建1.说明离线地图开发环境支持谷歌地图、百度地图、高德地图等等所有常用地图类型,支持在局域网内的地图部署、二次开发。2.实现第一步:下载安装离线地图开发环境BIEGMAP离线地图服务器(开发版)下载安装好之后,启动软件,如下图所示: ①如果你的电脑连接到网络,这里可以直接点击【进入】;如果未能链接网络,请插上离线地...

简单的网页设计,以学校官网为例_学校网页开发_Modify_QmQ的博客-程序员秘密

以本校官网为例,代码如下:下载同一个html下。注意的是:图片1.jpg19.png36.png50.png206.pngbanner2.jpglogo.jpg&lt;!DOCTYPE html&gt;&lt;html&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;title&gt;div+css长...

Delphi7--字符串处理函数与过程_zxa1334的博客-程序员秘密

1、大小写转换函数:        调用格式1:               LowerCase(const s:String): String;把字符串s中的字符全部转换为小写字母,并作为函数值返回        调用格式2:               UpperCase(const s:String): String;把字符串s中的字符全部转换为大写字母,并作为函数值返回...

4G编码器在移动户外直播推流解决方案_千视kiloview的博客-程序员秘密

4G编码器是干什么用?4G编码器是一款专业户外直播推流设备,4G直播编码器采用的是4G网络,通常用于户外现场直播,户外事件直播,活动现场直播,户外赛事等。4G直播编码器最终的结果是要将我们直播现场的画面推给直播平台,比较常见的网线传输,WIFI信号传输以及手机电话(4G)卡信号传输等等。针对户外直播应用需求,免不了在户外在没有网线的地方,所以需要采用3G/4G/5G网络将直播信号传输到播放平台。那如何使用4G编码器进行推流?4G编码器直播推流原理图:移动户外直播推流操作步骤第一步:设备安装与连

kettle MongoDB数据迁移至Hbase_mongodb转hbase_kiligsmile的博客-程序员秘密

本文使用kettle工具,实现 MongoDB数据迁移至Hbase

不要就知道leetcode,意外好用的刷题工具分享,了解高效刷题法,字节FB谷歌offer随便挑!..._九章算法的博客-程序员秘密

算法和数据结构不想花大量时间刷题?这篇文章应该能帮到你!★写在前面:经验不足的选手建议不要一上来就拿leetcode来刷题,不是说leetcode不好,是真的不适合你们啊!Leetcode题库虽说很强,但是数量太过于庞大,新手用起来最大的感受就是:一个知识点还没掌握,又来一个……结果就是最后一团浆糊,什么都记不住!自己什么coding水平心里难道没数吗??★以我从小白到ACM金牌选手多年的刷题经验...