溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

SpringBoot使用本地鎖搞定重復(fù)提交

發(fā)布時(shí)間:2020-07-28 20:15:54 來源:網(wǎng)絡(luò) 閱讀:1244 作者:Java_老男孩 欄目:編程語言

SpringBoot?是為了簡化?Spring?應(yīng)用的創(chuàng)建、運(yùn)行、調(diào)試、部署等一系列問題而誕生的產(chǎn)物,自動(dòng)裝配的特性讓我們可以更好的關(guān)注業(yè)務(wù)本身而不是外部的XML配置,我們只需遵循規(guī)范,引入相關(guān)的依賴就可以輕易的搭建出一個(gè) WEB 工程

在平時(shí)開發(fā)中,如果網(wǎng)速比較慢的情況下,用戶提交表單后,發(fā)現(xiàn)服務(wù)器半天都沒有響應(yīng),那么用戶可能會(huì)以為是自己沒有提交表單,就會(huì)再點(diǎn)擊提交按鈕重復(fù)提交表單,我們?cè)陂_發(fā)中必須防止表單重復(fù)提交….

重復(fù)提交

字面意思就是提交了很多次,這種情況一般都是前端給你挖的坑….

前段時(shí)間在開發(fā)中遇到一個(gè)這樣的問題;前端小哥哥調(diào)用接口的時(shí)候存在?循環(huán)調(diào)用?的問題,正常情況下發(fā)送一個(gè)請(qǐng)求添加一條數(shù)據(jù),結(jié)果變成了同一時(shí)刻并發(fā)的發(fā)送了 N 個(gè)請(qǐng)求,服務(wù)端瞬間懵逼的插入了 N 條一模一樣的數(shù)據(jù),前端小哥哥也不知道問題在哪里(恩...坑就這樣挖好了,反正不填坑,氣死你) 這時(shí)候咋辦呢;后端干唄,反正臟活累活,背鍋的事情也沒少干了,多一件也不多….

本章目標(biāo)

利用?自定義注解、Spring Aop、Guava Cache?實(shí)現(xiàn)表單防重復(fù)提交(不適用于分布式哦,后面會(huì)講分布式方式...

具體代碼

非常簡單…

導(dǎo)入依賴

在?pom.xml?中添加上?spring-boot-starter-web?的依賴即可

<dependencies>
    <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>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>21.0</version>
    </dependency>
</dependencies>

Lock 注解

創(chuàng)建一個(gè)?LocalLock?注解,簡單點(diǎn)就一個(gè)?key?可以了,由于暫時(shí)未用到?redis?所以?expire?是擺設(shè)….

package com.battcn.annotation;

import java.lang.annotation.*;

/**
 * 鎖的注解
 *
 * @author Levin
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LocalLock {

    /**
     * @author fly
     */
    String key() default "";

    /**
     * 過期時(shí)間 TODO 由于用的 guava 暫時(shí)就忽略這屬性吧 集成 redis 需要用到
     *
     * @author fly
     */
    int expire() default 5;
}

Lock 攔截器(AOP)

首先通過?CacheBuilder.newBuilder()?構(gòu)建出緩存對(duì)象,設(shè)置好過期時(shí)間;其目的就是為了防止因程序崩潰鎖得不到釋放(當(dāng)然如果單機(jī)這種方式程序都炸了,鎖早沒了;但這不妨礙我們寫好點(diǎn))

在具體的?interceptor()?方法上采用的是?Around(環(huán)繞增強(qiáng))?,所有帶?LocalLock?注解的都將被切面處理;

如果想更為靈活,key 的生成規(guī)則可以定義成接口形式(可以參考:org.springframework.cache.interceptor.KeyGenerator),這里就偷個(gè)懶了;

package com.battcn.interceptor;

import com.battcn.annotation.LocalLock;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
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.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * 本章先基于 本地緩存來做,后續(xù)講解 redis 方案
 *
 * @author Levin
 * @since 2018/6/12 0012
 */
@Aspect
@Configuration
public class LockMethodInterceptor {

    private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()
            // 最大緩存 100 個(gè)
            .maximumSize(1000)
            // 設(shè)置寫緩存后 5 秒鐘過期
            .expireAfterWrite(5, TimeUnit.SECONDS)
            .build();

    @Around("execution(public * *(..)) && @annotation(com.battcn.annotation.LocalLock)")
    public Object interceptor(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        LocalLock localLock = method.getAnnotation(LocalLock.class);
        String key = getKey(localLock.key(), pjp.getArgs());
        if (!StringUtils.isEmpty(key)) {
            if (CACHES.getIfPresent(key) != null) {
                throw new RuntimeException("請(qǐng)勿重復(fù)請(qǐng)求");
            }
            // 如果是第一次請(qǐng)求,就將 key 當(dāng)前對(duì)象壓入緩存中
            CACHES.put(key, key);
        }
        try {
            return pjp.proceed();
        } catch (Throwable throwable) {
            throw new RuntimeException("服務(wù)器異常");
        } finally {
            // TODO 為了演示效果,這里就不調(diào)用 CACHES.invalidate(key); 代碼了
        }
    }

    /**
     * key 的生成策略,如果想靈活可以寫成接口與實(shí)現(xiàn)類的方式(TODO 后續(xù)講解)
     *
     * @param keyExpress 表達(dá)式
     * @param args       參數(shù)
     * @return 生成的key
     */
    private String getKey(String keyExpress, Object[] args) {
        for (int i = 0; i < args.length; i++) {
            keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString());
        }
        return keyExpress;
    }
}

控制層

在接口上添加?@LocalLock(key = "book:arg[0]");意味著會(huì)將?arg[0]?替換成第一個(gè)參數(shù)的值,生成后的新 key 將被緩存起來;

package com.battcn.controller;

import com.battcn.annotation.LocalLock;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * BookController
 *
 * @author Levin
 * @since 2018/6/06 0031
 */
@RestController
@RequestMapping("/books")
public class BookController {

    @LocalLock(key = "book:arg[0]")
    @GetMapping
    public String query(@RequestParam String token) {
        return "success - " + token;
    }
}

主函數(shù)

package com.battcn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author Levin
 */
@SpringBootApplication
public class Chapter21Application {

    public static void main(String[] args) {

        SpringApplication.run(Chapter21Application.class, args);

    }
}

測試

完成準(zhǔn)備事項(xiàng)后,啟動(dòng)?Chapter21Application?自行測試即可,測試手段相信大伙都不陌生了,如?瀏覽器、postman、junit、swagger,此處基于?postman,如果你覺得自帶的異常信息不夠友好,那么配上巧用SpringBoot輕松搞定全局異常?可以輕松搞定…

第一次請(qǐng)求

SpringBoot使用本地鎖搞定重復(fù)提交

第二次請(qǐng)求

SpringBoot使用本地鎖搞定重復(fù)提交

本文的重點(diǎn)是你有沒有收獲與成長,其余的都不重要,希望讀者們能謹(jǐn)記這一點(diǎn)。同時(shí)我經(jīng)過多年的收藏目前也算收集到了一套完整的學(xué)習(xí)資料,包括但不限于:分布式架構(gòu)、高可擴(kuò)展、高性能、高并發(fā)、Jvm性能調(diào)優(yōu)、Spring,MyBatis,Nginx源碼分析,Redis,ActiveMQ、Mycat、Netty、Kafka、Mysql、Zookeeper、Tomcat、Docker、Dubbo、Nginx等多個(gè)知識(shí)點(diǎn)高級(jí)進(jìn)階干貨,希望對(duì)想成為架構(gòu)師的朋友有一定的參考和幫助

有需要的可以加一下三千人Java技術(shù)交流分享群:“708 701 457”免費(fèi)獲取

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI