客户端的抖动,快速操作,网络通信或者服务器响应慢,造成服务器重复处理。防止重复提交,除了从前端控制,后台也需要控制。因为前端的限制不能解决彻底。接口实现,通常要求幂等性,保证多次重复提交只有一次有效。对于更新操作,达到幂等性很难。
访问请求到达服务器,服务器端生成token,分别保存在客户端和服务器。提交请求到达服务器,服务器端校验客户端带来的token与此时保存在服务器的token是否一致,如果一致,就继续操作,删除服务器的token。如果不一致,就不能继续操作,即这个请求是重复请求。
这种方案,每次提交要发送两次请求。对前端不是特别友好。
request进来,没有就先存在缓存中,继续操作业务,最后删除缓存或者缓存设置生命周期。如果存在,就直接对request进行验证,就不能继续操作业务。

从该图中可以得知,如果当前提交的请求URL已经存在于缓存中,且当前提交的请求体 跟缓存中该URL对应的请求体一毛一样 且当前请求URL的时间戳跟上次相同请求URL的时间戳 间隔在8s 内,即代表当前请求属于 “重复提交”;如果这其中有一个条件不成立,则意味着当前请求很有可能是第一次请求,或者已经过了8s时间间隔的 第N次请求了,不属于“重复提交”了。
照着这个思路,接下来我们将采用实际的代码进行实战,其中涉及到的技术:Spring Boot2.6 + 自定义注解 + 拦截器 + Redis缓存 (也可以分布式缓存Redisson);
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0.M4</version>
</dependency>
复制代码
server.port=8888
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6380
# Redis服务器连接密码(默认为空)
spring.redis.password=eco.dameng.com
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=5000
复制代码
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 防重复操作限时标记数值(存储redis限时标记数值)
*/
String value() default "value" ;
/**
* 防重复操作过期时间(借助redis实现限时控制)
*/
long expireSeconds() default 10;
}
复制代码
@Slf4j
@Component
@Aspect
public class NoRepeatSubmitAspect {
@Autowired
private RedisTemplate redisTemplate;
/**
* 定义切点
*/
@Pointcut("@annotation(com.example.learn.annotaion.RepeatSubmit)")
public void preventDuplication() {}
@Around("preventDuplication()")
public Object around(ProceedingJoinPoint joinPoint) throws Exception {
/**
* 获取请求信息
*/
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取执行方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取防重复提交注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 获取token以及方法标记,生成redisKey和redisValue
String token = request.getHeader(IdempotentConstant.TOKEN);
String url = request.getRequestURI();
/**
* 通过前缀 + url + token + 函数参数签名 来生成redis上的 key
*
*/
String redisKey = IdempotentConstant.PREVENT_DUPLICATION_PREFIX
.concat(url)
.concat(token)
.concat(getMethodSign(method, joinPoint.getArgs()));
// 这个值只是为了标记,不重要
String redisValue = redisKey.concat(annotation.value()).concat("submit duplication");
if (!redisTemplate.hasKey(redisKey)) {
// 设置防重复操作限时标记(前置通知)
redisTemplate.opsForValue()
.set(redisKey, redisValue, annotation.expireSeconds(), TimeUnit.SECONDS);
try {
//正常执行方法并返回
//ProceedingJoinPoint类型参数可以决定是否执行目标方法,
// 且环绕通知必须要有返回值,返回值即为目标方法的返回值
return joinPoint.proceed();
} catch (Throwable throwable) {
//确保方法执行异常实时释放限时标记(异常后置通知)
redisTemplate.delete(redisKey);
throw new RuntimeException(throwable);
}
} else {
// 重复提交了抛出异常,如果是在项目中,根据具体情况处理。
throw new RuntimeException("请勿重复提交");
}
}
/**
* 生成方法标记:采用数字签名算法SHA1对方法签名字符串加签
*
* @param method
* @param args
* @return
*/
private String getMethodSign(Method method, Object... args) {
StringBuilder sb = new StringBuilder(method.toString());
for (Object arg : args) {
sb.append(toString(arg));
}
return DigestUtil.sha1Hex(sb.toString());
}
private String toString(Object arg) {
if (Objects.isNull(arg)) {
return "null";
}
if (arg instanceof Number) {
return arg.toString();
}
return JSONUtil.toJsonStr(arg);
}
}
复制代码
实体测试类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private String orderNo;
private String productName;
private String purchaseName;
}
复制代码
常量类
public interface IdempotentConstant {
StringTOKEN = "token";
String PREVENT_DUPLICATION_PREFIX = "PREVENT_DUPLICATION_PREFIX:";
}
复制代码
@Slf4j
@RestController
@RequestMapping("/web")
public class IdempotentController {
@PostMapping("/sayNoDuplication")
@RepeatSubmit(expireSeconds = 8)
public String sayNoDuplication(@RequestParam("requestNum") String requestNum) {
log.info("sayNoDuplicatin requestNum:{}", requestNum);
return "sayNoDuplicatin".concat(requestNum);
}
@PostMapping("/addOrder")
@RepeatSubmit(expireSeconds = 8)
public String addOrder(@RequestBody Order order) {
log.info("addOrder requestNum:{}", order);
return JSONUtil.toJsonStr(order);
}
}
复制代码
访问 http://localhost:8888/web/sayNoDuplication

第一次访问 
多次点击

基于JAVA注解+AOP切面快速实现防重复提交功能,该方案实现可以完全胜任非高并发场景下实施应用。但是在高并发场景下仍然有不足之处,存在线程安全问题(可以采用Jemeter复现问题)
if (!redisTemplate.hasKey(redisKey)) { // 设置防重复操作限时标记(前置通知) redisTemplate.opsForValue() .set(redisKey, redisValue, annotation.expireSeconds(), TimeUnit.SECONDS);
主要是这个操作不是原子的,在高并发场景会有问题。可以使用 redisson 分布式锁进行解决。
文章目录git常用命令(简介,详细参数往下看)Git提交代码步骤gitpullgitstatusgitaddgitcommitgitpushgit代码冲突合并问题方法一:放弃本地代码方法二:合并代码常用命令以及详细参数gitadd将文件添加到仓库:gitdiff比较文件异同gitlog查看历史记录gitreset代码回滚版本库相关操作远程仓库相关操作分支相关操作创建分支查看分支:gitbranch合并分支:gitmerge删除分支:gitbranch-ddev查看分支合并图:gitlog–graph–pretty=oneline–abbrev-commit撤消某次提交git用户名密码相关配置g
我有一个ActiveRecord对象,我想在不对模型进行永久验证的情况下阻止它被保存。您过去可以使用errors.add执行类似的操作,但它看起来不再有效了。user=User.lastuser.errors.add:name,"namedoesn'trhymewithorange"user.valid?#=>trueuser.save#=>true或user=User.lastuser.errors.add:base,"myuniqueerror"user.valid?#=>trueuser.save#=>true如何在不修改用户对象模型的情况下防止将用户对象保存在Rails3.2中
这就是我做的a="%span.rockets#diamonds.ribbons.forever"a=a.match(/(^\%\w+)([\.|\#]\w+)+/)putsa.inspect这是我得到的#这就是我想要的#帮助?我尝试过但失败了:( 最佳答案 通常,您不能获得任意数量的捕获组,但如果您使用扫描,您可以为您想要捕获的每个标记获得一个匹配:a="%span.rockets#diamonds.ribbons.forever"a=a.scan(/^%\w+|\G[.|#]\w+/)putsa.inspect["%span","
我在ruby表单中有一个提交按钮f.submitbtn_text,class:"btnbtn-onemgt12mgb12",id:"btn_id"我想在不使用任何javascript的情况下通过ruby禁用此按钮 最佳答案 添加disabled:true选项。f.submitbtn_text,class:"btnbtn-onemgt12mgb12",id:"btn_id",disabled:true 关于ruby-on-rails-如何在Rails中添加禁用的提交按钮,我们在St
Ruby中防止SQL注入(inject)的好方法是什么? 最佳答案 直接使用ruby?使用准备好的语句:require'mysql'db=Mysql.new('localhost','user','password','database')statement=db.prepare"SELECT*FROMtableWHEREfield=?"statement.execute'value'statement.fetchstatement.close 关于ruby-防止SQL注入(inject
保存成功后可以回滚吗?让我有一个带有属性名称、电子邮件等的用户模型。例如u=User.newu.name="test_name"u.email="test@email.com"u.save现在记录将成功保存在数据库中,之后我想回滚我的事务(不是销毁或删除)。有什么想法吗? 最佳答案 您可以通过交易来做到这一点,请参阅http://markdaggett.com/blog/2011/12/01/transactions-in-rails/例子:User.transactiondoUser.create(:username=>'Nemu
我无法使用传统的Ruby方法从下面的数组user_list中删除所有重复对象,从而获得预期的结果。有解决这个问题的聪明方法吗?users=[]user_list.eachdo|u|user=User.find_by_id(u.user_id)users 最佳答案 这个怎么样?users=User.find(user_list.map(&:user_id).uniq)这具有作为一个数据库调用而不是user_list.size数据库调用的额外好处。 关于Ruby从数组中删除重复的对象,我们在
ruby中有没有一个很好的方法来删除可枚举列表中的重复项(即拒绝等) 最佳答案 对于数组你可以使用uniq()方法a=["a","a","b","b","c"]a.uniq#=>["a","b","c"]所以如果你只是(1..10).to_a.uniq或%w{antbatcatant}.to_a.uniq因为无论如何,几乎所有您实现的方法都将作为Array类返回。 关于Ruby删除可枚举列表中的重复项,我们在StackOverflow上找到一个类似的问题: h
我知道如何创建值数组的排列。例如:[*1..3].permutation(2)这导致以下六种排列:[1,2][1,3][2,1][2,3][3,1][3,2]但这个结果缺少三个排列,它们是相同值的组合,即:[1,1][2,2][3,3]如何获得所有排列,包括上面重复的排列? 最佳答案 尝试#repeated_permutation:[*1..3].repeated_permutation(3).to_a>pp[*1..3].repeated_permutation(3).to_a[[1,1,1],[1,1,2],[1,1,3],[1
像这样转换数组的最快/单行方法是什么:[1,1,1,1,2,2,3,5,5,5,8,13,21,21,21]...进入像这样的对象数组:[{1=>4},{2=>2},{3=>1},{5=>3},{8=>1},{13=>1},{21=>3}] 最佳答案 要获得所需的格式,您可以附加一个调用以映射到您的解决方案:array.inject({}){|h,v|h[v]||=0;h[v]+=1;h}.map{|k,v|{k=>v}}虽然它仍然是单行的,但它开始变得凌乱了。 关于ruby-在Ruby