目录
1.什么是幂等
- select查询天然幂等
-
delete删除也是幂等,删除同一个多次效果一样
-
update直接更新某个值的,幂等
-
update更新累加操作的,非幂等
-
insert非幂等操作,每次新增一条
2.产生原因
由于重复点击或者网络重发 :
- 点击提交按钮两次;
- 点击刷新按钮;
- 使用浏览器后退按钮重复之前的操作,导致重复提交表单;
- 使用浏览器历史记录重复提交表单;
- 浏览器重复的HTTP请;
- nginx重发等情况;
- 分布式RPC的try重发等;
3.解决方法
3.1前端js提交禁止,按钮可以用一些js组件
3.2使用Post/Redirect/Get模式
在提交后执行重定向
3.3本地锁(重点)
先获取参数接口信息
package com.benjaming_boot.resubmit.annoation;
import java.lang.annotation.*;
/**
* @Author: Zhubencai
* @Date: create in 2019/8/27 10:20
* @Description: 锁参数
*/
@Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {
//字段名称
String name() default "";
}
接口自定义注解
package com.benjaming_boot.resubmit.annoation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @Author: Zhubencai
* @Date: create in 2019/8/26 20:53
* @Description:
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Resubmit {
/**
*description: 生成key的前缀
*@Author: Zhubencai
*@date: 2019/8/27 10:17
*/
String prefix() default "";
/**
*description: 延时时间多久后再次提交
*@Author: Zhubencai
*@date: 2019/8/26 20:54
*/
int expire() default 10;
//默认时间单位是 秒
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
*description: key的分隔符,默认 :
*@Author: Zhubencai
*@date: 2019/8/27 10:18
*/
String delimiter() default ":";
}
key的生成策略
package com.benjaming_boot.resubmit;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* @Author: Zhubencai
* @Date: create in 2019/8/27 10:06
* @Description: KEY生成器
*/
public interface CacheKeyGenerator {
/**
*description: 获取AOP参数,生成指定缓存KEY
*@Author: Zhubencai
*@date: 2019/8/27 10:07
*/
String getLockKey(ProceedingJoinPoint point);
}
package com.benjaming_boot.resubmit;
import com.benjaming_boot.resubmit.annoation.CacheParam;
import com.benjaming_boot.resubmit.annoation.Resubmit;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
/**
* @Author: Zhubencai
* @Date: create in 2019/8/27 10:08
* @Description: 主要是解析 Resubmit 注解的属性,获取对应的属性值,生成一个全新的key
*/
@Service
public class LockKeyGenerator implements CacheKeyGenerator{
@Override
public String getLockKey(ProceedingJoinPoint point) {
Method method = ((MethodSignature)point.getSignature()).getMethod();
Resubmit resubmit = method.getAnnotation(Resubmit.class);
Object[] args = point.getArgs();
Parameter[] parameters = method.getParameters();
StringBuilder builder = new StringBuilder();
//todo 默认解析方法里面带 CacheParam 注解的属性,如果没有尝试着解析实体对象中的
for(int i=0;i<parameters.length;i++){
final CacheParam param = parameters[i].getAnnotation(CacheParam.class);
if(param == null){
continue;
}
System.out.println("args[i]:"+args[i]);
builder.append(resubmit.delimiter()).append(args[i]);
}
if(StringUtils.isEmpty(builder.toString())){
Annotation[][] annotations = method.getParameterAnnotations();
System.out.println(annotations.length);
for(int i = 0 ; i < annotations.length ; i++){
final Object object = args[i];
final Field[] fields = object.getClass().getDeclaredFields();
for(Field field : fields){
System.out.println("当前字段为:"+field);
final CacheParam annotationParam = field.getAnnotation(CacheParam.class);
System.out.println(annotationParam.name());
if(annotationParam == null){
continue;
}
field.setAccessible(true);
builder.append(resubmit.delimiter()).append(ReflectionUtils.getField(field, object));
}
}
}
return resubmit.prefix() + builder.toString();
}
}
切面接口的实现
package com.benjaming_boot.resubmit;
import com.benjaming_boot.resubmit.annoation.Resubmit;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* @Author: Zhubencai
* @Date: create in 2019/8/26 21:11
* @Description: 数据重复提交校验
*/
@Aspect
@Component
public class ResubmitDataAspect {
@Autowired
private CacheKeyGenerator lockKeyGenerator;
@Around("execution(public * *(..)) && @annotation(com.benjaming_boot.resubmit.annoation.Resubmit)")
public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable{
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
//todo 获取注解信息
Resubmit resubmit = method.getAnnotation(Resubmit.class);
if(StringUtils.isEmpty(resubmit.prefix())){
throw new RuntimeException("lock key don't null ...");
}
final String lockKey = lockKeyGenerator.getLockKey(joinPoint);
String value = UUID.randomUUID().toString();
boolean lock = false;
try{
lock = ResubmitLock.getInstance().lock(lockKey, value);
if(lock){
return joinPoint.proceed();
}else {
throw new RuntimeException("重复提交");
}
}finally {
ResubmitLock.getInstance().unlock(lock,lockKey , resubmit.expire());
}
}
}
3.4Redis分布式锁
package com.benjaming_boot.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.StringUtils;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
* @Author: Zhubencai
* @Date: create in 2019/9/1 16:35
* @Description:
*/
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisLock {
private static final String DELIMITER = "|";
private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);
private final StringRedisTemplate stringRedisTemplate;
public RedisLock(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
/**
*description: 获取锁 (存在死锁的风险)
*@Author: Zhubencai
*@date: 2019/9/1 16:48
*@param: value value
* @param time 超时时间
* @param unit 过期时间单位
*@return: true or false
*/
public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit){
return stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.set(lockKey.getBytes(), value.getBytes(),Expiration.from(time,unit), RedisStringCommands.SetOption.SET_IF_ABSENT));
}
public boolean lock(String lockKey,final String uuid,long timeout,final TimeUnit unit){
final long milliseconds = Expiration.from(timeout,unit).getExpirationTimeInMilliseconds();
boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
if(success){
stringRedisTemplate.expire(lockKey,timeout,TimeUnit.SECONDS);
}else{
String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey,(System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));
if(Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()){
return true;
}
}
return success;
}
/**
* @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a>
*/
public void unlock(String lockKey, String value) {
unlock(lockKey, value, 0, TimeUnit.MILLISECONDS);
}
/**
* 延迟unlock
*
* @param lockKey key
* @param uuid client(最好是唯一键的)
* @param delayTime 延迟时间
* @param unit 时间单位
*/
public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {
if (StringUtils.isEmpty(lockKey)) {
return;
}
if (delayTime <= 0) {
doUnlock(lockKey, uuid);
} else {
EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);
}
}
/**
* @param lockKey key
* @param uuid client(最好是唯一键的)
*/
private void doUnlock(final String lockKey, final String uuid) {
String val = stringRedisTemplate.opsForValue().get(lockKey);
final String[] values = val.split(Pattern.quote(DELIMITER));
if (values.length <= 0) {
return;
}
if (uuid.equals(values[1])) {
stringRedisTemplate.delete(lockKey);
}
}
}
aop切面逻辑
package com.benjaming_boot.resubmit;
import com.benjaming_boot.resubmit.annoation.Resubmit;
import com.benjaming_boot.util.RedisLock;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* @Author: Zhubencai
* @Date: create in 2019/9/1 16:57
* @Description:
*/
@Aspect
@Configuration
public class ResubmitRedisAspect {
@Autowired
public ResubmitRedisAspect(RedisLock redisLock,CacheKeyGenerator cacheKeyGenerator){
this.redisLock = redisLock;
this.cacheKeyGenerator = cacheKeyGenerator;
}
private final RedisLock redisLock;
private final CacheKeyGenerator cacheKeyGenerator;
@Around("execution(public * *(..)) && @annotation(com.benjaming_boot.resubmit.annoation.Resubmit)")
public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable{
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
//todo 获取注解信息
Resubmit resubmit = method.getAnnotation(Resubmit.class);
if(StringUtils.isEmpty(resubmit.prefix())){
throw new RuntimeException("lock key don't null ...");
}
final String lockKey = cacheKeyGenerator.getLockKey(joinPoint);
String value = UUID.randomUUID().toString();
boolean lock = false;
try{
lock = redisLock.lock(lockKey,value , resubmit.expire(),resubmit.timeUnit());
if(lock){
return joinPoint.proceed();
}else {
throw new RuntimeException("重复提交");
}
}finally {
redisLock.unlock(lockKey,value);
}
}
}
3.5请求事栗
实体类
package com.benjaming_boot.controller;
import com.benjaming_boot.resubmit.annoation.CacheParam;
import java.io.Serializable;
/**
* @Author: Zhubencai
* @Date: create in 2019/8/27 14:12
* @Description:
*/
public class Coustomer implements Serializable {
@CacheParam(name = "name")
private String name;
@CacheParam(name = "age")
private String age;
@CacheParam(name = "测试注解")
public void say(){
System.out.println("测试注解");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
Controller层
package com.benjaming_boot.controller;
import com.benjaming_boot.resubmit.annoation.CacheParam;
import com.benjaming_boot.resubmit.annoation.Resubmit;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: Zhubencai
* @Date: create in 2019/8/27 12:54
* @Description:
*/
@RestController
public class CoustomerController {
@RequestMapping("/savePolicy")
@Resubmit(prefix = "policy")
public void savePolicy(@RequestBody Coustomer coustomer){
System.out.println("保存成功");
}
@RequestMapping("/saveUser")
@Resubmit(prefix = "user")
public void saveUser(@CacheParam(name = "name") @RequestParam String name){
System.out.println("保存成功");
}
}
4.参考地址
https://mp.weixin.qq.com/s/9fidsb3RRciqFgCf7UKCQg
https://blog.battcn.com/2018/06/13/springboot/v2-cache-redislock/