最近公司商城订单出现重复订单数据问题,比较棘手,一直在找原因,没有发现问题,太坑了,后来决定在原有的业务基础上面加上防刷单处理和redis分布式锁,双重保证应用的安全和稳定性。
一、防刷单原理:防止一个方法,在方法参数值相同的情况下,短时间频繁调用,这里根据spring中的AOP原理来实现的,自己定义了一个注解,这个注解主要用来判断哪些方法上面加了这个注解,就做参数请求处理,先配置具体的aop切面路径扫描类中的方法,处理是根据这个请求的路径获取相应的方法中的参数做具体分析。
实现的步骤:
定义一个注解(主要用来判断哪些方法要做防重复提交处理)
- 通过spring中的AOP进行扫描,方法处理。
- 设置一个过期时间来处理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