欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot Redis實(shí)現(xiàn)接口冪等性校驗(yàn)方法詳細(xì)講解

 更新時(shí)間:2022年11月28日 15:22:05   作者:喜羊羊sk  
這篇文章主要介紹了SpringBoot Redis實(shí)現(xiàn)接口冪等性校驗(yàn)方法,近期一個(gè)老項(xiàng)目出現(xiàn)了接口冪等性校驗(yàn)問題,前端加了按鈕置灰,依然被人拉著接口參數(shù)一頓輸出,還是重復(fù)調(diào)用了接口,通過復(fù)制粘貼,完成了后端接口冪等性調(diào)用校驗(yàn)

冪等性

冪等性的定義是:一次和屢次請(qǐng)求某一個(gè)資源對(duì)于資源自己應(yīng)該具備一樣的結(jié)果(網(wǎng)絡(luò)超時(shí)等問題除外)。也就是說,其任意屢次執(zhí)行對(duì)資源自己所產(chǎn)生的影響均與一次執(zhí)行的影響相同。

WEB系統(tǒng)中: 就是用戶對(duì)于同一操作發(fā)起的一次請(qǐng)求或者多次請(qǐng)求的結(jié)果是一致的,不會(huì)因?yàn)槎啻吸c(diǎn)擊而產(chǎn)生不同的結(jié)果。

什么狀況下須要保證冪等性

以SQL為例,有下面三種場(chǎng)景,只有第三種場(chǎng)景須要開發(fā)人員使用其余策略保證冪等性:

SELECT col1 FROM tab1 WHER col2=2,不管執(zhí)行多少次都不會(huì)改變狀態(tài),是自然的冪等。

UPDATE tab1 SET col1=1 WHERE col2=2,不管執(zhí)行成功多少次狀態(tài)都是一致的,所以也是冪等操做。

UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次執(zhí)行的結(jié)果都會(huì)發(fā)生變化,這種不是冪等的。

解決方法

這里主要使用token令牌和分布式鎖解決

Pom

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.2.RELEASE</version>
    <relativePath/>
</parent>
<dependencies>
	<dependency>
	    <groupId>org.projectlombok</groupId>
	    <artifactId>lombok</artifactId>
	    <version>1.18.4</version>
	    <scope>provided</scope>
	</dependency>
	<dependency>
	   <groupId>org.springframework.boot</groupId>
	   <artifactId>spring-boot-starter-jdbc</artifactId>
	</dependency>
	<dependency>
	   <groupId>mysql</groupId>
	   <artifactId>mysql-connector-java</artifactId>
	</dependency>
	<dependency>
	   <groupId>org.springframework.boot</groupId>
	   <artifactId>spring-boot-starter-data-redis</artifactId>
	</dependency>
	<!-- springboot 對(duì)aop的支持 -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-aop</artifactId>
	</dependency>
	<!-- springboot mybatis-plus -->
	<dependency>
		<groupId>com.baomidou</groupId>
		<artifactId>mybatis-plus-boot-starter</artifactId>
		<version>3.5.2</version>
	</dependency>
</dependencies>

token令牌

這種方式分紅兩個(gè)階段:

1、客戶端向系統(tǒng)發(fā)起一次申請(qǐng)token的請(qǐng)求,服務(wù)器系統(tǒng)生成token令牌,將token保存到Redis緩存中,并返回前端(令牌生成方式可以使用JWT)

2、客戶端拿著申請(qǐng)到的token發(fā)起請(qǐng)求(放到請(qǐng)求頭中),后臺(tái)系統(tǒng)會(huì)在攔截器中檢查handler是否開啟冪等性校驗(yàn)。取請(qǐng)求頭中的token,判斷Redis中是否存在該token,若是存在,表示第一次發(fā)起支付請(qǐng)求,刪除緩存中token后開始業(yè)務(wù)邏輯處理;若是緩存中不存在,表示非法請(qǐng)求。

yml

spring:
  redis:
    host: 127.0.0.1
    timeout: 5000ms
    port: 6379
    database: 0
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/study_db?serverTimezone=GMT%2B8&allowMultiQueries=true
    username: root
    password: root
redisson:
  timeout: 10000

@ApiIdempotentAnn

@ApiIdempotentAnn冪等性注解。說明: 添加了該注解的接口要實(shí)現(xiàn)冪等性驗(yàn)證

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotentAnn {
    boolean value() default true;
}

ApiIdempotentInterceptor

這里可以使用攔截器或者使用AOP的方式實(shí)現(xiàn)。

冪等性攔截器的方式實(shí)現(xiàn)

@Component
public class ApiIdempotentInterceptor extends HandlerInterceptorAdapter {
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 前置攔截器
     *在方法被調(diào)用前執(zhí)行。在該方法中可以做類似校驗(yàn)的功能。如果返回true,則繼續(xù)調(diào)用下一個(gè)攔截器。如果返回false,則中斷執(zhí)行,
     * 也就是說我們想調(diào)用的方法 不會(huì)被執(zhí)行,但是你可以修改response為你想要的響應(yīng)。
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果hanler不是和HandlerMethod類型,則返回true
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        //轉(zhuǎn)化類型
        final HandlerMethod handlerMethod = (HandlerMethod) handler;
        //獲取方法類
        final Method method = handlerMethod.getMethod();
        // 判斷當(dāng)前method中是否有這個(gè)注解
        boolean methodAnn = method.isAnnotationPresent(ApiIdempotentAnn.class);
        //如果有冪等性注解
        if (methodAnn && method.getAnnotation(ApiIdempotentAnn.class).value()) {
            // 需要實(shí)現(xiàn)接口冪等性
            //檢查token
            //1.獲取請(qǐng)求的接口方法
            boolean result = checkToken(request);
            //如果token有值,說明是第一次調(diào)用
            if (result) {
                //則放行
                return super.preHandle(request, response, handler);
            } else {//如果token沒有值,則表示不是第一次調(diào)用,是重復(fù)調(diào)用
                response.setContentType("application/json; charset=utf-8");
                PrintWriter writer = response.getWriter();
                writer.print("重復(fù)調(diào)用");
                writer.close();
                response.flushBuffer();
                return false;
            }
        }
        //否則沒有該自定義冪等性注解,則放行
        return super.preHandle(request, response, handler);
    }
    //檢查token
    private boolean checkToken(HttpServletRequest request) {
        //從請(qǐng)求頭對(duì)象中獲取token
        String token = request.getHeader("token");
        //如果不存在,則返回false,說明是重復(fù)調(diào)用
        if(StringUtils.isBlank(token)){
            return false;
        }
        //否則就是存在,存在則把redis里刪除token
        return redisTemplate.delete(token);
    }
}

MVC配置類

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Resource
    private ApiIdempotentInterceptor apiIdempotentInceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiIdempotentInceptor).addPathPatterns("/**");
    }
}

ApiController

@RestController
public class ApiController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 前端獲取token,然后把該token放入請(qǐng)求的header中
     * @return
     */
    @GetMapping("/getToken")
    public String getToken() {
        String token = UUID.randomUUID().toString().substring(1, 9);
        stringRedisTemplate.opsForValue().set(token, "1");
        return token;
    }
    //定義int類型的原子類的類
    AtomicInteger num = new AtomicInteger(100);
    /**
     * 主業(yè)務(wù)邏輯,num--,并且加了自定義接口
     * @return
     */
    @GetMapping("/submit")
    @ApiIdempotentAnn
    public String submit() {
        // num--
        num.decrementAndGet();
        return "success";
    }
    /**
     * 查看num的值
     * @return
     */
    @GetMapping("/getNum")
    public String getNum() {
        return String.valueOf(num.get());
    }
}

分布式鎖 Redisson

Redisson是redis官網(wǎng)推薦實(shí)現(xiàn)分布式鎖的一個(gè)第三方類庫(kù),通過開啟另一個(gè)服務(wù),后臺(tái)進(jìn)程定時(shí)檢查持有鎖的線程是否繼續(xù)持有鎖了,是將鎖的生命周期重置到指定時(shí)間,即防止線程釋放鎖之前過期,所以將鎖聲明周期通過重置延長(zhǎng))

Redission執(zhí)行流程如下:(只要線程一加鎖成功,就會(huì)啟動(dòng)一個(gè)watch dog看門狗,它是一個(gè)后臺(tái)線程,會(huì)每隔10秒檢查一下(鎖續(xù)命周期就是設(shè)置的超時(shí)時(shí)間的三分之一),如果線程還持有鎖,就會(huì)不斷的延長(zhǎng)鎖key的生存時(shí)間。因此,Redis就是使用Redisson解決了鎖過期釋放,業(yè)務(wù)沒執(zhí)行完問題。當(dāng)業(yè)務(wù)執(zhí)行完,釋放鎖后,再關(guān)閉守護(hù)線程,

pom

<dependency>
	 <groupId>org.redisson</groupId>
	 <artifactId>redisson-spring-boot-starter</artifactId>
	 <version>3.13.6</version>
</dependency>

@RedissonLockAnnotation

分布式鎖注解

@Target(ElementType.METHOD) //注解在方法
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLockAnnotation {
    /**
     * 指定組成分布式鎖的key,以逗號(hào)分隔。
     * 如:keyParts="name,age",則分布式鎖的key為這兩個(gè)字段value的拼接
     * key=params.getString("name")+params.getString("age")
     */
    String keyParts();
}

DistributeLocker

分布式鎖接口

public interface  DistributeLocker {
    /**
     * 加鎖
     * @param lockKey key
     */
    void lock(String lockKey);
    /**
     * 釋放鎖
     *
     * @param lockKey key
     */
    void unlock(String lockKey);
    /**
     * 加鎖,設(shè)置有效期
     *
     * @param lockKey key
     * @param timeout 有效時(shí)間,默認(rèn)時(shí)間單位在實(shí)現(xiàn)類傳入
     */
    void lock(String lockKey, int timeout);
    /**
     * 加鎖,設(shè)置有效期并指定時(shí)間單位
     * @param lockKey key
     * @param timeout 有效時(shí)間
     * @param unit    時(shí)間單位
     */
    void lock(String lockKey, int timeout, TimeUnit unit);
    /**
     * 嘗試獲取鎖,獲取到則持有該鎖返回true,未獲取到立即返回false
     * @param lockKey
     * @return true-獲取鎖成功 false-獲取鎖失敗
     */
    boolean tryLock(String lockKey);
    /**
     * 嘗試獲取鎖,獲取到則持有該鎖leaseTime時(shí)間.
     * 若未獲取到,在waitTime時(shí)間內(nèi)一直嘗試獲取,超過watiTime還未獲取到則返回false
     * @param lockKey   key
     * @param waitTime  嘗試獲取時(shí)間
     * @param leaseTime 鎖持有時(shí)間
     * @param unit      時(shí)間單位
     * @return true-獲取鎖成功 false-獲取鎖失敗
     */
    boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
            throws InterruptedException;
    /**
     * 鎖是否被任意一個(gè)線程鎖持有
     * @param lockKey
     * @return true-被鎖 false-未被鎖
     */
    boolean isLocked(String lockKey);
}

RedissonDistributeLocker

redisson實(shí)現(xiàn)分布式鎖接口

public class RedissonDistributeLocker implements DistributeLocker {
    private RedissonClient redissonClient;
    public RedissonDistributeLocker(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }
    @Override
    public void lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
    }
    @Override
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }
    @Override
    public void lock(String lockKey, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime, TimeUnit.MILLISECONDS);
    }
    @Override
    public void lock(String lockKey, int timeout, TimeUnit unit) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
    }
    @Override
    public boolean tryLock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock();
    }
    @Override
    public boolean tryLock(String lockKey, long waitTime, long leaseTime,
                           TimeUnit unit) throws InterruptedException {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.tryLock(waitTime, leaseTime, unit);
    }

    @Override
    public boolean isLocked(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        return lock.isLocked();
    }
}

RedissonLockUtils

redisson鎖工具類

public class RedissonLockUtils {
    private static DistributeLocker locker;
    public static void setLocker(DistributeLocker locker) {
        RedissonLockUtils.locker = locker;
    }
    public static void lock(String lockKey) {
        locker.lock(lockKey);
    }
    public static void unlock(String lockKey) {
        locker.unlock(lockKey);
    }
    public static void lock(String lockKey, int timeout) {
        locker.lock(lockKey, timeout);
    }
    public static void lock(String lockKey, int timeout, TimeUnit unit) {
        locker.lock(lockKey, timeout, unit);
    }
    public static boolean tryLock(String lockKey) {
        return locker.tryLock(lockKey);
    }
    public static boolean tryLock(String lockKey, long waitTime, long leaseTime,
                                  TimeUnit unit) throws InterruptedException {
        return locker.tryLock(lockKey, waitTime, leaseTime, unit);
    }
    public static boolean isLocked(String lockKey) {
        return locker.isLocked(lockKey);
    }
}

RedissonConfig

Redisson配置類

@Configuration
public class RedissonConfig {
    @Autowired
    private Environment env;
    /**
     * Redisson客戶端注冊(cè)
     * 單機(jī)模式
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient createRedissonClient() {
        Config config = new Config();
        SingleServerConfig singleServerConfig = config.useSingleServer();
        singleServerConfig.setAddress("redis://" + env.getProperty("spring.redis.host") + ":" + env.getProperty("spring.redis.port"));
        singleServerConfig.setTimeout(Integer.valueOf(env.getProperty("redisson.timeout")));
        return Redisson.create(config);
    }
    /**
     * 分布式鎖實(shí)例化并交給工具類
     * @param redissonClient
     */
    @Bean
    public RedissonDistributeLocker redissonLocker(RedissonClient redissonClient) {
        RedissonDistributeLocker locker = new RedissonDistributeLocker(redissonClient);
        RedissonLockUtils.setLocker(locker);
        return locker;
    }
}

RedissonLockAop

這里可以使用攔截器或者使用AOP的方式實(shí)現(xiàn)。

分布式鎖AOP切面攔截方式實(shí)現(xiàn)

@Aspect
@Component
@Slf4j
public class RedissonLockAop {
    /**
     * 切點(diǎn),攔截被 @RedissonLockAnnotation 修飾的方法
     */
    @Pointcut("@annotation(cn.zysheep.biz.redis.RedissonLockAnnotation)")
    public void redissonLockPoint() {
    }
    @Around("redissonLockPoint()")
    @ResponseBody
    public ResultVO checkLock(ProceedingJoinPoint pjp) throws Throwable {
        //當(dāng)前線程名
        String threadName = Thread.currentThread().getName();
        log.info("線程{}------進(jìn)入分布式鎖aop------", threadName);
        //獲取參數(shù)列表
        Object[] objs = pjp.getArgs();
        //因?yàn)橹挥幸粋€(gè)JSON參數(shù),直接取第一個(gè)
        JSONObject param = (JSONObject) objs[0];
        //獲取該注解的實(shí)例對(duì)象
        RedissonLockAnnotation annotation = ((MethodSignature) pjp.getSignature()).
                getMethod().getAnnotation(RedissonLockAnnotation.class);
        //生成分布式鎖key的鍵名,以逗號(hào)分隔
        String keyParts = annotation.keyParts();
        StringBuffer keyBuffer = new StringBuffer();
        if (StringUtils.isEmpty(keyParts)) {
            log.info("線程{} keyParts設(shè)置為空,不加鎖", threadName);
            return (ResultVO) pjp.proceed();
        } else {
            //生成分布式鎖key
            String[] keyPartArray = keyParts.split(",");
            for (String keyPart : keyPartArray) {
                keyBuffer.append(param.getString(keyPart));
            }
            String key = keyBuffer.toString();
            log.info("線程{} 要加鎖的key={}", threadName, key);
            //獲取鎖
            if (RedissonLockUtils.tryLock(key, 3000, 5000, TimeUnit.MILLISECONDS)) {
                try {
                    log.info("線程{} 獲取鎖成功", threadName);
                    // Thread.sleep(5000);
                    return (ResultVO) pjp.proceed();
                } finally {
                    RedissonLockUtils.unlock(key);
                    log.info("線程{} 釋放鎖", threadName);
                }
            } else {
                log.info("線程{} 獲取鎖失敗", threadName);
                return ResultVO.fail();
            }
        }
    }
}

ResultVO

統(tǒng)一響應(yīng)實(shí)體

@Data
public class ResultVO<T> {
    private static final ResultCode SUCCESS = ResultCode.SUCCESS;
    private static final ResultCode FAIL = ResultCode.FAILED;
    private Integer code;
    private String message;
    private T  data;
    public static <T> ResultVO<T> ok() {
        return result(SUCCESS,null);
    }
    public static <T> ResultVO<T> ok(T data) {
        return result(SUCCESS,data);
    }
    public static <T> ResultVO<T> ok(ResultCode resultCode) {
        return result(resultCode,null);
    }
    public static <T> ResultVO<T> ok(ResultCode resultCode, T data) {
        return result(resultCode,data);
    }
    public static <T> ResultVO<T> fail() {
        return result(FAIL,null);
    }
    public static <T> ResultVO<T> fail(ResultCode resultCode) {
        return result(FAIL,null);
    }
    public static <T> ResultVO<T> fail(T data) {
        return result(FAIL,data);
    }
    public static <T> ResultVO<T> fail(ResultCode resultCode, T data) {
        return result(resultCode,data);
    }
    private static <T>  ResultVO<T> result(ResultCode resultCode, T data) {
        ResultVO<T> resultVO = new ResultVO<>();
        resultVO.setCode(resultCode.getCode());
        resultVO.setMessage(resultCode.getMessage());
        resultVO.setData(data);
        return resultVO;
    }
}

BusiController

@RestController
public class ApiController {
	@PostMapping(value = "testLock")
	@RedissonLockAnnotation(keyParts = "name,age")
	public ResultVO testLock(@RequestBody JSONObject params) {
	    /**
	     * 分布式鎖key=params.getString("name")+params.getString("age");
	     * 此時(shí)name和age均相同的請(qǐng)求不會(huì)出現(xiàn)并發(fā)問題
	     */
	    //TODO 業(yè)務(wù)處理dwad
	    return ResultVO.ok();
	}
}

到此這篇關(guān)于SpringBoot Redis實(shí)現(xiàn)接口冪等性校驗(yàn)方法詳細(xì)講解的文章就介紹到這了,更多相關(guān)SpringBoot Redis接口冪等性校驗(yàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • java實(shí)現(xiàn)服務(wù)器巡查的代碼

    java實(shí)現(xiàn)服務(wù)器巡查的代碼

    接到上級(jí)領(lǐng)導(dǎo)任務(wù),需要實(shí)現(xiàn)一個(gè)這樣的需求,一大批服務(wù)器,需要檢查服務(wù)器能否ping通,ssh密碼是否正常,以及檢查服務(wù)器的cpu,內(nèi)存,硬盤占用情況,下面通過java代碼實(shí)現(xiàn)服務(wù)器巡查功能,需要的朋友一起看看吧
    2021-12-12
  • Java利用Jackson序列化實(shí)現(xiàn)數(shù)據(jù)脫敏

    Java利用Jackson序列化實(shí)現(xiàn)數(shù)據(jù)脫敏

    這篇文章主要介紹了利用Jackson序列化實(shí)現(xiàn)數(shù)據(jù)脫敏,首先在需要進(jìn)行脫敏的VO字段上面標(biāo)注相關(guān)脫敏注解,具體實(shí)例代碼文中給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2021-10-10
  • Java使用正則表達(dá)式進(jìn)行匹配且對(duì)匹配結(jié)果逐個(gè)替換

    Java使用正則表達(dá)式進(jìn)行匹配且對(duì)匹配結(jié)果逐個(gè)替換

    這篇文章主要介紹了Java使用正則表達(dá)式進(jìn)行匹配且對(duì)匹配結(jié)果逐個(gè)替換,文章圍繞主題展開詳細(xì)的內(nèi)容戒殺,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-09-09
  • Hibernate命名策略詳解

    Hibernate命名策略詳解

    本文主要介紹了Hibernate命名策略。具有很好的參考價(jià)值,下面跟著小編一起來看下吧
    2017-01-01
  • 如何基于JAVA讀取yml配置文件指定key內(nèi)容

    如何基于JAVA讀取yml配置文件指定key內(nèi)容

    這篇文章主要介紹了如何基于JAVA讀取yml配置文件指定key內(nèi)容,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-01-01
  • springboot讀取resources下文件的方式詳解

    springboot讀取resources下文件的方式詳解

    最近寫讀取模板文件做一些后續(xù)的處理,將文件放在了項(xiàng)目的resources下,發(fā)現(xiàn)了一個(gè)好用的讀取方法,下面這篇文章主要給大家介紹了關(guān)于springboot讀取resources下文件的相關(guān)資料,需要的朋友可以參考下
    2022-06-06
  • Kotlin中常見的List使用示例教程

    Kotlin中常見的List使用示例教程

    filter 就像其本意一樣,可以通過 filter 對(duì) Kotlin list 進(jìn)行過濾,本文重點(diǎn)給大家介紹Kotlin中常見的List使用,感興趣的朋友一起看看吧
    2023-11-11
  • IDEA插件指南之Mybatis?log插件安裝及使用方法

    IDEA插件指南之Mybatis?log插件安裝及使用方法

    這篇文章主要給大家介紹了關(guān)于IDEA插件指南之Mybatis?log插件安裝及使用的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2024-02-02
  • 使用.NET Core3.0創(chuàng)建一個(gè)Windows服務(wù)的方法

    使用.NET Core3.0創(chuàng)建一個(gè)Windows服務(wù)的方法

    這篇文章主要介紹了使用.NET Core3.0創(chuàng)建一個(gè)Windows服務(wù)的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2019-04-04
  • Spring 注入static屬性值方式

    Spring 注入static屬性值方式

    文本介紹了Spring如何從屬性文件給static屬性注入值,在寫一些與配置相關(guān)的工具類時(shí)常用。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-09-09

最新評(píng)論