SpringBoot/Web項目防止表單/請求重複提交(單體和分佈式)

SpringBoot/Web項目防止表單/請求重複提交(單體和分佈式)

一、場景/方案

說起web項目的防止表單/請求重複提交,不得不說冪等性。

冪等性, 通俗的說就是一個接口, 多次發起同一個請求, 必須保證操作只能執行一次。

1.1、常見場景:

	•	訂單接口, 不能多次創建訂單
	•	支付接口, 重複支付同一筆訂單隻能扣一次錢
	•	支付寶回調接口, 可能會多次回調, 必須處理重複回調
	•	普通表單提交接口, 因爲網絡超時,卡頓等原因多次點擊提交, 只能成功一次等等

1.2、常見方案

解決思路:

  1. 從數據庫方面考慮,數據設計的時候,如果有唯一性,考慮建立唯一索引。
  2. 從應用層面考慮,首先判斷是單機服務還是分佈式服務?
    • 單機服務:考慮一些緩存Cacha,利用緩存,來保證數據的重複提交。
    • 分佈式服務,考慮將用戶的信息,例如token和請求的url進行組裝在一起形成令牌,存儲到緩存中,例如redis,並設置超時時間爲xx秒,如此來保證數據的唯一性。(利用了redis的分佈式鎖)

解決方案大致總結爲:

	- 唯一索引 -- 防止新增髒數據
	- token機制 -- 防止頁面重複提交,實現接口冪等性校驗
	- 分佈式鎖 -- redis(jedis、redisson)或zookeeper實現
	- 悲觀鎖 -- 獲取數據的時候加鎖(鎖表或鎖行)
	- 樂觀鎖 -- 基於版本號version實現, 在更新數據那一刻校驗數據
	- 狀態機 -- 狀態變更, 更新數據時判斷狀態

前三種方式最爲常見,本文則從應用層面考慮,給出單機服務還是分佈式服務下的解決方案。

二、單體服務項目:

比如你的項目是一個單獨springboot項目,SSM項目,或者其他的單體服務,就是打個jar或者war直接扔服務器上跑的。

採用【AOP解析自定義註解+google的Cache緩存機制】來解決表單/請求重複的提交問題。

思路:

  1. 建立自定義註解 @NoRepeatSubmit 標記所有Controller中的提交請求。
  2. 通過AOP機制對所有標記了@NoRepeatSubmit 的方法攔截。
  3. 在業務方法執行前,使用google的緩存Cache技術,來保證數據的重複提交。
  4. 業務方法執行後,釋放緩存。

好了,接下里就是新建一個springboot項目,然後開整了。
在這裏插入圖片描述

2.1 pom.xml新增依賴

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

源碼如下:

  <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>24.0-jre</version>
  </dependency>

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:  自定義註解,用於標記Controller中的提交請求
 * @Author: ZouTao
 * @Date: 2020/4/14
 */
@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: Zoutao
 * @Date: 2020/4/14
 */
@Aspect
@Component
public class NoRepeatSubmitAop {

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

    @Autowired
    private Cache<String, Integer> 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: Zoutao
 * @Date: 2020/4/14
 */

@Configuration
public class UrlCache {
    @Bean
    public Cache<String, Integer> 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: Zoutao
 * @Date: 2020/4/14
 */
@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: ZouTao 
 * @Date: 2020/4/14 
 */
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刷新(模擬重複發起請求),查看效果:

在這裏插入圖片描述
可以看到只有一次請求被成功響應,返回了數據,在有效時間內,其他請求被判定爲重複提交,不予執行。

三、分佈式服務項目

如果你的spirngboot項目,後面要放到分佈式集羣中去使用,那麼這個單體的Cache機制怕是會出問題,所以,爲了解決項目在集羣部署時請求可能會落到多臺機器上的問題,我們把內存緩存換成了redis。

利用token機制+redis的分佈式鎖(jedis)來防止表單/請求重複提交。

思路如下:

  1. 自定義註解 @NoRepeatSubmit 標記所有Controller中的提交請求。
  2. 通過AOP 對所有標記了 @NoRepeatSubmit 的方法攔截。
  3. 在業務方法執行前,獲取當前用戶的 token(或JSessionId)+ 當前請求地址,形成一個唯一Key,然後去獲取 Redis 分佈式鎖(如果此時併發獲取,只有一個線程會成功獲取鎖)。
  4. 最後業務方法執行完畢,釋放鎖。

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 是爲了在負載均衡部署,

直接貼出整個項目的吧:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.3.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.gitee.taven</groupId>
	<artifactId>repeat-submit-intercept</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>repeat-submit-intercept</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<!--方式一:緩存類-->
		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>24.0-jre</version>
		</dependency>
		<!--方式二:redis類-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
			<exclusions>
				<exclusion>
					<groupId>redis.clients</groupId>
					<artifactId>jedis</artifactId>
				</exclusion>
				<exclusion>
					<groupId>io.lettuce</groupId>
					<artifactId>lettuce-core</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

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: ZouTao
 * @Date: 2020/4/14
 */
@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: ZouTao
 * @Date: 2020/4/14
 */
@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: ZouTao
     * @Date: 2020/4/14
     */

    @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: ZouTao
 * @Date: 2020/4/14
 */
@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 分佈式鎖實現
 * @Author: ZouTao 
 * @Date: 2020/4/14 
 */
@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<Boolean>) 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<Boolean>) 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 信息
 * @Author: ZouTao 
 * @Date: 2020/4/14 
 */
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:  
 * @Author: ZouTao 
 * @Date: 2020/4/14 
 */
@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<String> 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<String, Object> 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項目中防止表單/請求重複提交的一個方案,分爲單機和分佈式環境下。有什麼疑問請留言吧。需要源碼可以評論留下郵箱,後期會貼出git地址。

參考地址:
[1]: https://www.jianshu.com/p/09c6b05b670a

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章