关于重复请求,指的是咱们服务端接收到很短的时间内的多个相同内容的重复请求。而这样的重复请求若是是幂等的(每次请求的结果都相同,如查询请求),那其实对于咱们没有什么影响,但若是是非幂等的(每次请求都会对关键数据形成影响,如删除关系、创建关系等),那就会轻则产生脏数据,重则致使系统错误。前端
所以,在当前广泛分布式服务的状况下,如何避免和解决重复请求给咱们带来的数据异常成为了亟待解决的问题。而避免重复请求,最好的作法是先后端共同去作。java
1. 前端或客户端在非幂等的按钮上直接作禁止提交重复请求的操做。git
2. 后端在接收到请求时加锁,完成后解锁。redis
这篇博客主要讲的是在后端基于分布式锁的概念去出一个关于解决重复请求的通用解决方案。算法
为什么要使用分布式锁来解决呢?由于咱们当前广泛的架构都是分布式的服务端,前端请求经过网关层转发至后端,以下图所示,所以若是只在一个单独的服务器上作限制,就没法在分布式的服务中完成应对高频次的重复请求了。spring
思路基本上是对须要作防止重复请求的接口加上分布式锁,步骤以下:后端
便可完成对当前请求的重复请求禁止。若是想作通用的解决方案,那就须要把上述步骤作出一个小功能出来,因为本人对java、spring框架比较熟悉,就拿这个来作个示例。bash
想必一些熟悉spring的同窗已经知道我想采用什么方式了,作通用型的,确定要用到spring的aop特性,注解+切面+md5key+反射+redis实现,具体以下:服务器
代码以下:架构
定义名称为RepeatOperationLock的注解,参数有锁过时时间及忽略属性(即不参与分布式锁标识MD5计算的属性)。
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Component
public @interface RepeatOperationLock {
/**
* 锁时长,默认500ms
* @return
*/
long timeOut() default 500;
/**
* 忽略上锁参数位置,从0开始
* @return
*/
int[] ignoreIndex();
}
复制代码
切点为上述注解,切面中作了如下几件事,获取方法名、获取注解属性(过时时间、忽略属性)、计算方法+属性的md5值、调用外部分布式锁的方法。
@Aspect
@Slf4j
@Component
public class LockAspect {
@Autowired
RepeatLockService repeatLockService;
@Pointcut("@annotation(com.ls.javabase.aspect.annotation.RepeatOperationLock)")
public void serviceAspect() {
}
@Before("serviceAspect()")
public void setLock(JoinPoint point) {
log.info("防止方法重复调用接口锁,上锁,point:{}", point);
Method method = ((MethodSignature) point.getSignature()).getMethod();
RepeatOperationLock repeatOperationLock = method.getAnnotation(RepeatOperationLock.class);
if (Objects.isNull(repeatOperationLock)) {
log.warn("---repeatOperationLock is null---");
return;
}
long timeOut = repeatOperationLock.timeOut();
int [] ignoreIndex = repeatOperationLock.ignoreIndex();
log.info("lockTime——{}", timeOut);
if (Objects.isNull(timeOut)) {
log.warn("---timeOut is null");
return;
}
String methodName = method.getName();
Object[] args = point.getArgs();
repeatLockService.setRepeatLock(methodName, args, timeOut);
}
@After("serviceAspect()")
public void removeLock(JoinPoint point) {
log.info("防止方法重复调用接口锁,解锁,point:{}",point);
Method method = ((MethodSignature) point.getSignature()).getMethod();
RepeatOperationLock repeatOperationLock = method.getAnnotation(RepeatOperationLock.class);
if (Objects.isNull(repeatOperationLock)) {
log.warn("---repeatOperationLock is null---");
return;
}
long timeOut = repeatOperationLock.timeOut();
log.info("lockTime——{}", timeOut);
if (Objects.isNull(timeOut)) {
log.warn("---timeOut is null");
return;
}
String methodName = method.getName();
Object[] args = point.getArgs();
repeatLockService.removeRepeatLock(methodName, args);
}
/**
*
* @param args
* @param ignoreIndex
* @return
*/
private Object [] getEffectiveArgs(Object[] args,int [] ignoreIndex) {
for (int i:ignoreIndex){
args[i] = null;
}
for (Object obj:args){
if (obj==null){
}
}
return args;
}
}
复制代码
public class Md5Encode {
/**
* constructors
*/
private Md5Encode() {
}
/**
* @param s 须要hash的字符串
* @return hash以后的字符串
*/
public static final String md5(final String s) {
char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
byte[] btInput = s.getBytes(Charset.defaultCharset());
// 得到MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字节更新摘要
mdInst.update(btInput);
// 得到密文
byte[] md = mdInst.digest();
// 把密文转换成十六进制的字符串形式
int j = md.length;
char[] str = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
复制代码
这里的分布式锁使用redis,好比锁绘本误解,后续会作出改进,实现一个完整的分布式锁方案,写到博客里。
@Slf4j
@Service
public class RepeatLockService {
@Autowired
RepeatRedisUtil repeatRedisUtil;
public void setRepeatLock(String methodName, Object[] args, Long expireTime) {
for (Object obj : args) {
log.info("方法名:{},对象:{},对象hashcode:{}", methodName, obj, obj.hashCode());
}
Boolean lock = repeatRedisUtil.setRepeatLock(methodName, args, expireTime);
if (!lock) {
log.info("已有相同请求");
}
}
public void removeRepeatLock(String methodName, Object[] args) {
repeatRedisUtil.removeRepeatLock(methodName, args);
}
}
@Component
public class RepeatRedisUtil {
@Autowired
RedisTemplate redisTemplate;
private static final String repeatLockPrefix = "repeat_lock_";
/**
* 设置重复请求锁,这一块的分布式锁的加与释放有问题,后续会专门出个文章总结redis分布式锁
* @param methodName
* @param args
* @param expireTime 过时时间ms
* @return
*/
public boolean setRepeatLock(String methodName, Object[] args,long expireTime) {
String key = getRepeatLockKey(methodName, args);
try {
boolean b = (boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
Jedis jedis = null;
try {
jedis = (Jedis) connection.getNativeConnection();
String status = jedis.set(key, "1", NX, EX, expireTime);
if (setNXStatus.equals(status)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}finally {
connection.close();
}
}
});
return b;
} catch (Exception e) {
log.error("redis操做异常:{}",e);
return Boolean.FALSE;
}
}
/**
* 删除重复请求锁
* @param methodName
* @param args
*/
public void removeRepeatLock(String methodName, Object[] args){
String key = getRepeatLockKey(methodName, args);
redisTemplate.delete(key);
}
/**
* 获取重复请求锁Key
*
* @param methodName
* @param args
* @return
*/
public String getRepeatLockKey(String methodName, Object[] args) {
String repeatLockKey = repeatLockPrefix + methodName;
for (Object obj : args) {
repeatLockKey = repeatLockKey+"_"+ obj.hashCode();
}
return repeatLockKey;
}
}
复制代码
即在方法上使用注解便可,表明过时时间为200000ms,忽略第二个参数。
@Slf4j
@Service
public class TestLockService {
@RepeatOperationLock(timeOut = 200000, ignoreIndex = 1)
public void testLock(UserDto userDto,int i){
log.info("service中属性:{},{}",userDto,i);
log.info("service中hashcode,userDto:{},i:{}",userDto.hashCode(),i);
}
}
复制代码
这样一个基于spring的通用分布式锁解决方案就分享完毕了,确实还存在着一些瑕疵,好比解锁时没有判断是否会被误解等等,后续会专门做出分布式锁的总结并改进,上面也只是提出了一个基于分布式锁解决重复请求的思想,也但愿能多多交流。