springboot幂等性_SpringBoot中幂等性问题

SpringBoot中防止请求重复提交

一、适用的场景

表单/请求重复提交,不得不说幂等性。

幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次。

1.1、常见场景:

•比如订单接口, 不能多次创建订单

•支付接口, 重复支付同一笔订单只能扣一次钱

•支付宝回调接口, 可能会多次回调, 必须处理重复回调

•普通表单提交接口, 因为网络超时,卡顿等原因多次点击提交, 只能成功一次等等

1.2、常见方案

解决思路:从数据库方面考虑,数据设计的时候,如果有唯一性,考虑建立唯一索引。

从应用层面考虑,首先判断是单机服务还是分布式服务?单机服务:考虑一些缓存Cacha,利用缓存,来保证数据的重复提交。

分布式服务,考虑将用户的信息,例如token和请求的url进行组装在一起形成令牌,存储到缓存中,例如redis,并设置超时时间为xx秒,如此来保证数据的唯一性。(利用了redis的分布式锁)

解决方案大致总结为:

- 唯一索引 -- 防止新增脏数据

- token机制 -- 防止页面重复提交,实现接口幂等性校验

- 分布式锁 -- redis(jedis、redisson)或zookeeper实现

二、单体服务项目-防止重复提交

比如你的项目是一个单独springboot项目,SSM项目,或者其他的单体服务,就是打个jar或者war直接扔服务器上跑的。

采用【AOP解析自定义注解+google的Cache缓存机制】来解决表单/请求重复的提交问题。

思路:建立自定义注解 @NoRepeatSubmit 标记所有Controller中的提交请求。

通过AOP机制对所有标记了@NoRepeatSubmit 的方法拦截。

在业务方法执行前,使用google的缓存Cache技术,来保证数据的重复提交。

业务方法执行后,释放缓存。

好了,接下里就是新建一个springboot项目,然后开整了。

2.1 pom.xml新增依赖

需要新增一个google.common.cache.Cache;

源码如下:

com.google.guava

guava

24.0-jre

2.2 新建NoRepeatSubmit.java自定义注解类

一个自定义注解接口类,是interface类哟 ,里面什么都不写,为了就是个重构。

源码如下:

package com.gitee.taven.aop;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

/**

* @title: NoRepeatSubmit

* @Description:

* @Author: lw

* @Date: 2020/10/22

*/

@Target(ElementType.METHOD) // 作用到方法上

@Retention(RetentionPolicy.RUNTIME) // 运行时有效

public @interface NoRepeatSubmit {

}

2.3 新建NoRepeatSubmitAop.java

这是个AOP的解析注解类,使用到了Cache缓存机制。

以cache.getIfPresent(key)的url值来进行if判断,如果不为空,证明已经发过请求,那么在规定时间内的再次请求,视为无效,为重复请求。如果为空,则正常响应请求。

源码如下:

package com.gitee.taven.aop;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Pointcut;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import com.google.common.cache.Cache;

/**

* @Description: aop解析注解-配合google的Cache缓存机制

* @Author: lw

* @Date: 2020/10/22

*/

@Aspect

@Component

public class NoRepeatSubmitAop {

private Log logger = LogFactory.getLog(getClass());

@Autowired

private Cache cache;

@Pointcut("@annotation(noRepeatSubmit)")

public void pointCut(NoRepeatSubmit noRepeatSubmit) {

}

@Around("pointCut(noRepeatSubmit)")

public Object arround(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) {

try {

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

String sessionId = RequestContextHolder.getRequestAttributes().getSessionId();

HttpServletRequest request = attributes.getRequest();

String key = sessionId + "-" + request.getServletPath();

if (cache.getIfPresent(key) == null) {// 如果缓存中有这个url视为重复提交

Object o = pjp.proceed();

cache.put(key, 0);

return o;

} else {

logger.error("重复请求,请稍后在试试。");

return null;

}

} catch (Throwable e) {

e.printStackTrace();

logger.error("验证重复提交时出现未知异常!");

return "{\"code\":-889,\"message\":\"验证重复提交时出现未知异常!\"}";

}

}

}

2.4 新建缓存类UrlCache.java

用来获取缓存和设置有效期,目前设置有效期为2秒。

源码如下:

package com.gitee.taven;

import java.util.concurrent.TimeUnit;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import com.google.common.cache.Cache;

import com.google.common.cache.CacheBuilder;

/**

* @Description: 内存缓存配置类

* @Author: lw

* @Date: 2020/10/22

*/

@Configuration

public class UrlCache {

@Bean

public Cache getCache() {

return CacheBuilder.newBuilder().expireAfterWrite(2L, TimeUnit.SECONDS).build();// 缓存有效期为2秒

}

}

2.5 新建CacheTestController.java

一个请求控制类,用来模拟响应请求和业务处理。

源码如下:

package com.gitee.taven.controller;

import com.gitee.taven.ApiResult;

import com.gitee.taven.aop.NoRepeatSubmit;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**

* @Description: 测试Cache方式的Controller

* @Author: lw

* @Date: 2020/10/22

*/

@RestController

public class CacheTestController {

private Object data;

@RequestMapping("/TestSubmit")

@NoRepeatSubmit()

public Object test() {

data = "程序逻辑返回,假设是一大堆DB来的数据。。。";

return new ApiResult(200, "请求成功",data);

// 也可以直接返回。return (",请求成功,程序逻辑返回");

}

}

ps:这里可以在建立一个ApiResult.java类,来规范返回的数据格式体:

ApiResult.java(非必须)

源码如下:

package com.gitee.taven;

/**

* @title: ApiResult

* @Description: 统一规范结果格式

* @Param: code, message, data

* @return: ApiResult

* @Author: lw

* @Date: 2020/10/22

*/

public class ApiResult {

private Integer code; //状态码

private String message; //提示信息

private Object data; //具体数据

public ApiResult(Integer code, String message, Object data) {

this.code = code;

this.message = message;

this.data = data;

}

public Integer getCode() {

return code;

}

public void setCode(Integer code) {

this.code = code;

}

public String getMessage() {

return message;

}

public void setMessage(String message) {

this.message = message == null ? null : message.trim();

}

public Object getData() {

return data;

}

public void setData(Object data) {

this.data = data;

}

@Override

public String toString() {

return "ApiResult{" +

"code=" + code +

", message='" + message + '\'' +

", data=" + data +

'}';

}

}

纯粹为了规范而加的,你可以不用。

2.6 启动项目

运行springboot项目,启动成功后,在浏览器输入:http://localhost:8080/TestSubmit

然后F5刷新(模拟重复发起请求),查看效果:

可以看到只有一次请求被成功响应,返回了数据,在有效时间内,其他请求被判定为重复提交,不予执行。

目前防止重复提交最简单的方案(最新)

两个关键信息,第一:防止重复提交;第二:最简单。

前提是是单机环境下。

1.前端拦截

前端拦截是指通过 HTML 页面来拦截重复请求,比如在用户点击完“提交”按钮后,我们可以把按钮设置为不可用或者隐藏状态。

实现代码:

function subAdd(){

// 按钮设置为不可用

document.getElementById("btn_sub").disabled="disabled";

document.getElementById("dv1").innerText = "按钮被点击了~";

}

执行效果:

但前端拦截有一个致命的问题,如果是懂行的程序员或非法用户可以直接绕过前端页面,所以除了前端拦截一部分正常的误操作之外,后端的拦截也是必不可少。

2.后端拦截-DCL方案

后端拦截的实现思路是在方法执行之前,先判断此业务是否已经执行过,如果执行过则不再执行,否则就正常执行。

提及将数据存储在内存中,最简单的方法就是使用 HashMap 存储,HashMap 的防重(防止重复)版本也是最原始的 。缺点是 HashMap 是无限增长的,因此它会占用越来越多的内存,并且随着 HashMap 数量的增加查找的速度也会降低,已不再推荐。

推荐使用最新的单例中著名的 DCL(Double Checked Locking,双重检测锁)来防止重复提交。

原理不需要深究,好在 Apache 为我们提供了一个 commons-collections 的框架,里面有一个非常好用的数据结构 LRUMap 可以保存指定数量的固定的数据,并且它就会按照 LRU 的算法,帮你清除最不常用的数据。LRU 是 Least Recently Used 的缩写,即最近最少使用,是一种常用的数据淘汰算法,选择最近最久未使用的数据予以淘汰。

L R U M a p 版 防 止 重 复 提 交 方 案 : \color{red}{LRUMap版防止重复提交方案:}LRUMap版防止重复提交方案:

1.首先,我们先来添加 Apache commons collections 的引用:

org.apache.commons

commons-collections4

4.4

本文已封装为一个公共的方法,以供所有类使用。

实现代码如下:

import org.apache.commons.collections4.map.LRUMap;

/**

* 幂等性判断

* 使用LRUMap。

*/

public class IdempotentUtils {

// 根据 LRU(Least Recently Used,最近最少使用)算法淘汰数据的 Map 集合,最大容量 100 个

private static LRUMap reqCache = new LRUMap<>(100);

/**

* 幂等性判断

* @return

*/

public static boolean judge(String id, Object lockClass) {

synchronized (lockClass) {

// 重复请求判断

if (reqCache.containsKey(id)) {

// 重复请求

System.out.println("请勿重复提交!!!" + id);

return false;

}

// 非重复请求,存储请求 ID

reqCache.put(id, 1);

}

return true;

}

}

调用代码:

import com.example.idempote.util.IdempotentUtils;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")

@RestController

public class UserController4 {

@RequestMapping("/add")

public String addUser(String id) {

// 非空判断(忽略)...

// -------------- 幂等性调用(开始) --------------

if (!IdempotentUtils.judge(id, this.getClass())) {

return "执行失败";

}

// -------------- 幂等性调用(结束) --------------

// 业务代码...

System.out.println("添加用户ID:" + id);

return "执行成功!";

}

}

当然,熟悉spring注解的朋友,可以通过自定义注解,将业务代码写到注解中,需要调用的方法只需要写一行注解就可以防止数据重复提交了。注意:

1.DCL 适用于重复提交频繁比较高的业务场景,对于相反的业务场景下 DCL 并不适用。

2.LRUMap 的本质是持有头结点的环回双链表结构,当使用元素时,就将该元素放在双链表 header 的前一个位置,在新增元素时,如果容量满了就会移除 header 的后一个元素。

效果:

上诉方式仅适用于单机环境下的重复数据拦截,如果是分布式环境需要配合数据库或 Redis 来实现。

三、分布式服务项目-防止重复提交

如果你的spirngboot项目,后面要放到分布式集群中去使用,那么这个单体的Cache机制怕是会出问题,所以,为了解决项目在集群部署时请求可能会落到多台机器上的问题,我们把内存缓存换成了redis。

利用token机制+redis的分布式锁(jedis)来防止表单/请求重复提交。

思路如下:自定义注解 @NoRepeatSubmit 标记所有Controller中的提交请求。

通过AOP 对所有标记了 @NoRepeatSubmit 的方法拦截。

在业务方法执行前,获取当前用户的 token(或JSessionId)+ 当前请求地址,形成一个唯一Key,然后去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁)。

最后业务方法执行完毕,释放锁。

3.1 Application配置redis

打开application.properties或application.yml配置redis:

内容如下:

server.port=8080

# Redis数据库索引(默认为0)

spring.redis.database=0

# Redis服务器地址

spring.redis.host=localhost

# Redis服务器连接端口

spring.redis.port=6379

# Redis服务器连接密码(默认为空)

#spring.redis.password=yourpwd

# 连接池最大连接数(使用负值表示没有限制)

spring.redis.jedis.pool.max-active=8

# 连接池最大阻塞等待时间

spring.redis.jedis.pool.max-wait=-1ms

# 连接池中的最大空闲连接

spring.redis.jedis.pool.max-idle=8

# 连接池中的最小空闲连接

spring.redis.jedis.pool.min-idle=0

# 连接超时时间(毫秒)

spring.redis.timeout=5000ms

3.2 pom.xml新增依赖

pom.xml需要一些redis的依赖,使用Redis 是为了在负载均衡部署,

直接贴出整个项目的吧:

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

4.0.0

org.springframework.boot

spring-boot-starter-parent

2.1.3.RELEASE

com.gitee.taven

repeat-submit-intercept

0.0.1-SNAPSHOT

repeat-submit-intercept

Demo project for Spring Boot

UTF-8

UTF-8

1.8

com.google.guava

guava

24.0-jre

org.springframework.boot

spring-boot-starter-data-redis

redis.clients

jedis

io.lettuce

lettuce-core

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-aop

org.springframework.boot

spring-boot-devtools

runtime

org.springframework.boot

spring-boot-starter-test

test

redis.clients

jedis

org.apache.commons

commons-pool2

org.springframework.boot

spring-boot-maven-plugin

3.3 自定义注解NoRepeatSubmit.java

也是一个自定义注解,其中设置请求锁定时间。

package com.gitee.taven.aop;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

/**

* @title: NoRepeatSubmit

* @Description: 自定义注解,用于标记Controller中的提交请求

* @Author: lw

* @Date: 2020/10/22

*/

@Target(ElementType.METHOD) // 作用到方法上

@Retention(RetentionPolicy.RUNTIME) // 运行时有效

public @interface NoRepeatSubmit {

/*

* 防止重复提交标记注解

* 设置请求锁定时间

* @return

*/

int lockTime() default 10;

}

3.4 AOP类RepeatSubmitAspect:

一个AOP解析注解类。

获取当前用户的Token(或者JSessionId)+ 当前请求地址,作为一个唯一 KEY,然后以Key去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁。)

源码如下:

package com.gitee.taven.aop;

import com.gitee.taven.ApiResult;

import com.gitee.taven.utils.RedisLock;

import com.gitee.taven.utils.RequestUtils;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.*;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;

import java.util.UUID;

/**

* @title: RepeatSubmitAspect

* @Description: AOP类解析注解-配合redis-解决程序集群部署时请求可能会落到多台机器上的问题。

* 作用:对标记了@NoRepeatSubmit的方法进行拦截

* @Author: lw

* @Date: 2020/10/22

*/

@Aspect

@Component

public class RepeatSubmitAspect {

private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class);

@Autowired

private RedisLock redisLock;

@Pointcut("@annotation(noRepeatSubmit)")

public void pointCut(NoRepeatSubmit noRepeatSubmit) {

}

/**

* @title: RepeatSubmitAspect

* @Description:在业务方法执行前,获取当前用户的

* token(或者JSessionId)+ 当前请求地址,作为一个唯一 KEY,

* 去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁。)

* @Author: lw

* @Date: 2020/10/22

*/

@Around("pointCut(noRepeatSubmit)")

public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {

int lockSeconds = noRepeatSubmit.lockTime();

HttpServletRequest request = RequestUtils.getRequest();

Assert.notNull(request, "request can not null");

// 此处可以用token或者JSessionId

String token = request.getHeader("Authorization");

String path = request.getServletPath();

String key = getKey(token, path);

String clientId = getClientId();

boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds);

LOGGER.info("tryLock key = [{}], clientId = [{}]", key, clientId);

// 主要逻辑

if (isSuccess) {

LOGGER.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);

// 获取锁成功

Object result;

try {

// 执行进程

result = pjp.proceed();

} finally {

// 解锁

redisLock.releaseLock(key, clientId);

LOGGER.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);

}

return result;

} else {

// 获取锁失败,认为是重复提交的请求。

LOGGER.info("tryLock fail, key = [{}]", key);

return new ApiResult(200, "重复请求,请稍后再试", null);

}

}

// token(或者JSessionId)+ 当前请求地址,作为一个唯一KEY

private String getKey(String token, String path) {

return token + path;

}

// 生成uuid

private String getClientId() {

return UUID.randomUUID().toString();

}

}

3.5 请求控制类SubmitController

这是一个测试接口的请求控制类,模拟业务场景,

package com.gitee.taven.controller;

import com.gitee.taven.ApiResult;

import com.gitee.taven.aop.NoRepeatSubmit;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RestController;

/**

* @title: SubmitController

* @Description: 测试接口

* @Author: lw

*/

@RestController

public class SubmitController {

@PostMapping("submit")

@NoRepeatSubmit()

public Object submit(@RequestBody UserBean userBean) {

try {

// 模拟业务场景

Thread.sleep(1500);

} catch (InterruptedException e) {

e.printStackTrace();

}

return new ApiResult(200, "成功", userBean.userId);

}

public static class UserBean {

private String userId;

public String getUserId() {

return userId;

}

public void setUserId(String userId) {

this.userId = userId == null ? null : userId.trim();

}

}

}

3.6 Redis分布式锁实现

需要一个工具类来实现Redis分布式锁,具体实现原理请参考另外一篇文章。这里贴出源码。

新建RedisLock类,如下:

package com.gitee.taven.utils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.RedisCallback;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.stereotype.Service;

import redis.clients.jedis.Jedis;

import java.util.Collections;

/**

* @title: RedisLock

* @Description: Redis 分布式锁实现

*/

@Service

public class RedisLock {

private static final Long RELEASE_SUCCESS = 1L;

private static final String LOCK_SUCCESS = "OK";

private static final String SET_IF_NOT_EXIST = "NX";

// 当前设置 过期时间单位, EX = seconds; PX = milliseconds

private static final String SET_WITH_EXPIRE_TIME = "EX";

// if get(key) == value return del(key)

private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

@Autowired

private StringRedisTemplate redisTemplate;

/**

* 该加锁方法仅针对单实例 Redis 可实现分布式加锁

* 对于 Redis 集群则无法使用

*

* 支持重复,线程安全

*

* @param lockKey 加锁键

* @param clientId 加锁客户端唯一标识(采用UUID)

* @param seconds 锁过期时间

* @return

*/

public boolean tryLock(String lockKey, String clientId, long seconds) {

return redisTemplate.execute((RedisCallback) redisConnection -> {

Jedis jedis = (Jedis) redisConnection.getNativeConnection();

String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);

if (LOCK_SUCCESS.equals(result)) {

return true;

}

return false;

});

}

/**

* 与 tryLock 相对应,用作释放锁

*

* @param lockKey

* @param clientId

* @return

*/

public boolean releaseLock(String lockKey, String clientId) {

return redisTemplate.execute((RedisCallback) redisConnection -> {

Jedis jedis = (Jedis) redisConnection.getNativeConnection();

Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),

Collections.singletonList(clientId));

if (RELEASE_SUCCESS.equals(result)) {

return true;

}

return false;

});

}

}

顺便新建一个RequestUtils工具类,用来获取一下getRequest的。

RequestUtils.java 如下:

package com.gitee.taven.utils;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**

* @title: RequestUtils

* @Description: 获取 Request 信息

*/

public class RequestUtils {

public static HttpServletRequest getRequest() {

ServletRequestAttributes ra= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

return ra.getRequest();

}

}

3.7 自动测试类RunTest

在上一个示例代码中,我们采用了启动项目,访问浏览器,手动测试的方式,接下里这个,

参考以前的一篇文章springboot启动项目自动运行测试方法,使用自动测试类来模拟测试。

模拟了10个并发请求同时提交:

package com.gitee.taven.test;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.ApplicationArguments;

import org.springframework.boot.ApplicationRunner;

import org.springframework.http.HttpEntity;

import org.springframework.http.HttpHeaders;

import org.springframework.http.MediaType;

import org.springframework.http.ResponseEntity;

import org.springframework.stereotype.Component;

import org.springframework.web.client.RestTemplate;

import java.util.HashMap;

import java.util.Map;

import java.util.concurrent.CountDownLatch;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

/**

* @title: RunTest

* @Description: 多线程测试类

* @Param: 模拟十个请求并发同时提交

* @return:

*/

@Component

public class RunTest implements ApplicationRunner {

private static final Logger LOGGER = LoggerFactory.getLogger(RunTest.class);

@Autowired

private RestTemplate restTemplate;

@Override

public void run(ApplicationArguments args) throws Exception {

System.out.println("=================执行多线程测试==================");

String url="http://localhost:8000/submit";

CountDownLatch countDownLatch = new CountDownLatch(1);

ExecutorService executorService = Executors.newFixedThreadPool(10); //线程数

for(int i=0; i<10; i++){

String userId = "userId" + i;

HttpEntity request = buildRequest(userId);

executorService.submit(() -> {

try {

countDownLatch.await();

System.out.println("Thread:"+Thread.currentThread().getName()+", time:"+System.currentTimeMillis());

ResponseEntity response = restTemplate.postForEntity(url, request, String.class);

System.out.println("Thread:"+Thread.currentThread().getName() + "," + response.getBody());

} catch (InterruptedException e) {

e.printStackTrace();

}

});

}

countDownLatch.countDown();

}

private HttpEntity buildRequest(String userId) {

HttpHeaders headers = new HttpHeaders();

headers.setContentType(MediaType.APPLICATION_JSON);

headers.set("Authorization", "yourToken");

Map body = new HashMap<>();

body.put("userId", userId);

return new HttpEntity<>(body, headers);

}

}

3.8 启动项目

启动项目,先启动redis,再运行springboot,会自动执行测试方法,然后控制台查看结果。

成功防止重复提交,控制台日志,可以看到十个线程的启动时间几乎同时发起,只有一个请求提交成功了。

ps:

有些人使用jedis3.1.0版本貌似已经没有这个set方法,则可以改为:

String result = jedis.set(lockKey, clientId, new SetParams().nx().px(seconds));

也ok了

整体项目结构图:

两套解决方案都在里面了,其中NoRepeatSubmit自定义注解类是共用的,区别在于有一个int lockTime()方法,不是使用redis的时候,注释掉即可。

上述就是SpringBoot/Web项目中防止表单/请求重复提交的一个方案,分为单机和分布式环境下。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值