首頁 > 軟體

SpringBoot Redis實現介面冪等性校驗方法詳細講解

2022-11-28 22:01:55

冪等性

冪等性的定義是:一次和屢次請求某一個資源對於資源自己應該具備一樣的結果(網路超時等問題除外)。也就是說,其任意屢次執行對資源自己所產生的影響均與一次執行的影響相同。

WEB系統中: 就是使用者對於同一操作發起的一次請求或者多次請求的結果是一致的,不會因為多次點選而產生不同的結果。

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

以SQL為例,有下面三種場景,只有第三種場景須要開發人員使用其餘策略保證冪等性:

SELECT col1 FROM tab1 WHER col2=2,不管執行多少次都不會改變狀態,是自然的冪等。

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

UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次執行的結果都會發生變化,這種不是冪等的。

解決方法

這裡主要使用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 對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令牌

這種方式分紅兩個階段:

1、使用者端向系統發起一次申請token的請求,伺服器系統生成token令牌,將token儲存到Redis快取中,並返回前端(令牌生成方式可以使用JWT)

2、使用者端拿著申請到的token發起請求(放到請求頭中),後臺系統會在攔截器中檢查handler是否開啟冪等性校驗。取請求頭中的token,判斷Redis中是否存在該token,若是存在,表示第一次發起支付請求,刪除快取中token後開始業務邏輯處理;若是快取中不存在,表示非法請求。

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冪等性註解。說明: 新增了該註解的介面要實現冪等性驗證

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

ApiIdempotentInterceptor

這裡可以使用攔截器或者使用AOP的方式實現。

冪等性攔截器的方式實現

@Component
public class ApiIdempotentInterceptor extends HandlerInterceptorAdapter {
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 前置攔截器
     *在方法被呼叫前執行。在該方法中可以做類似校驗的功能。如果返回true,則繼續呼叫下一個攔截器。如果返回false,則中斷執行,
     * 也就是說我們想呼叫的方法 不會被執行,但是你可以修改response為你想要的響應。
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果hanler不是和HandlerMethod型別,則返回true
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        //轉化型別
        final HandlerMethod handlerMethod = (HandlerMethod) handler;
        //獲取方法類
        final Method method = handlerMethod.getMethod();
        // 判斷當前method中是否有這個註解
        boolean methodAnn = method.isAnnotationPresent(ApiIdempotentAnn.class);
        //如果有冪等性註解
        if (methodAnn && method.getAnnotation(ApiIdempotentAnn.class).value()) {
            // 需要實現介面冪等性
            //檢查token
            //1.獲取請求的介面方法
            boolean result = checkToken(request);
            //如果token有值,說明是第一次呼叫
            if (result) {
                //則放行
                return super.preHandle(request, response, handler);
            } else {//如果token沒有值,則表示不是第一次呼叫,是重複呼叫
                response.setContentType("application/json; charset=utf-8");
                PrintWriter writer = response.getWriter();
                writer.print("重複呼叫");
                writer.close();
                response.flushBuffer();
                return false;
            }
        }
        //否則沒有該自定義冪等性註解,則放行
        return super.preHandle(request, response, handler);
    }
    //檢查token
    private boolean checkToken(HttpServletRequest request) {
        //從請求頭物件中獲取token
        String token = request.getHeader("token");
        //如果不存在,則返回false,說明是重複呼叫
        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放入請求的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);
    /**
     * 主業務邏輯,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官網推薦實現分散式鎖的一個第三方類庫,通過開啟另一個服務,後臺程序定時檢查持有鎖的執行緒是否繼續持有鎖了,是將鎖的生命週期重置到指定時間,即防止執行緒釋放鎖之前過期,所以將鎖宣告週期通過重置延長)

Redission執行流程如下:(只要執行緒一加鎖成功,就會啟動一個watch dog看門狗,它是一個後臺執行緒,會每隔10秒檢查一下(鎖續命週期就是設定的超時時間的三分之一),如果執行緒還持有鎖,就會不斷的延長鎖key的生存時間。因此,Redis就是使用Redisson解決了鎖過期釋放,業務沒執行完問題。當業務執行完,釋放鎖後,再關閉守護執行緒,

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,以逗號分隔。
     * 如:keyParts="name,age",則分散式鎖的key為這兩個欄位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);
    /**
     * 加鎖,設定有效期
     *
     * @param lockKey key
     * @param timeout 有效時間,預設時間單位在實現類傳入
     */
    void lock(String lockKey, int timeout);
    /**
     * 加鎖,設定有效期並指定時間單位
     * @param lockKey key
     * @param timeout 有效時間
     * @param unit    時間單位
     */
    void lock(String lockKey, int timeout, TimeUnit unit);
    /**
     * 嘗試獲取鎖,獲取到則持有該鎖返回true,未獲取到立即返回false
     * @param lockKey
     * @return true-獲取鎖成功 false-獲取鎖失敗
     */
    boolean tryLock(String lockKey);
    /**
     * 嘗試獲取鎖,獲取到則持有該鎖leaseTime時間.
     * 若未獲取到,在waitTime時間內一直嘗試獲取,超過watiTime還未獲取到則返回false
     * @param lockKey   key
     * @param waitTime  嘗試獲取時間
     * @param leaseTime 鎖持有時間
     * @param unit      時間單位
     * @return true-獲取鎖成功 false-獲取鎖失敗
     */
    boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
            throws InterruptedException;
    /**
     * 鎖是否被任意一個執行緒鎖持有
     * @param lockKey
     * @return true-被鎖 false-未被鎖
     */
    boolean isLocked(String lockKey);
}

RedissonDistributeLocker

redisson實現分散式鎖介面

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使用者端註冊
     * 單機模式
     */
    @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);
    }
    /**
     * 分散式鎖範例化並交給工具類
     * @param redissonClient
     */
    @Bean
    public RedissonDistributeLocker redissonLocker(RedissonClient redissonClient) {
        RedissonDistributeLocker locker = new RedissonDistributeLocker(redissonClient);
        RedissonLockUtils.setLocker(locker);
        return locker;
    }
}

RedissonLockAop

這裡可以使用攔截器或者使用AOP的方式實現。

分散式鎖AOP切面攔截方式實現

@Aspect
@Component
@Slf4j
public class RedissonLockAop {
    /**
     * 切點,攔截被 @RedissonLockAnnotation 修飾的方法
     */
    @Pointcut("@annotation(cn.zysheep.biz.redis.RedissonLockAnnotation)")
    public void redissonLockPoint() {
    }
    @Around("redissonLockPoint()")
    @ResponseBody
    public ResultVO checkLock(ProceedingJoinPoint pjp) throws Throwable {
        //當前執行緒名
        String threadName = Thread.currentThread().getName();
        log.info("執行緒{}------進入分散式鎖aop------", threadName);
        //獲取參數列
        Object[] objs = pjp.getArgs();
        //因為只有一個JSON引數,直接取第一個
        JSONObject param = (JSONObject) objs[0];
        //獲取該註解的範例物件
        RedissonLockAnnotation annotation = ((MethodSignature) pjp.getSignature()).
                getMethod().getAnnotation(RedissonLockAnnotation.class);
        //生成分散式鎖key的鍵名,以逗號分隔
        String keyParts = annotation.keyParts();
        StringBuffer keyBuffer = new StringBuffer();
        if (StringUtils.isEmpty(keyParts)) {
            log.info("執行緒{} keyParts設定為空,不加鎖", 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

統一響應實體

@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");
	     * 此時name和age均相同的請求不會出現並行問題
	     */
	    //TODO 業務處理dwad
	    return ResultVO.ok();
	}
}

到此這篇關於SpringBoot Redis實現介面冪等性校驗方法詳細講解的文章就介紹到這了,更多相關SpringBoot Redis介面冪等性校驗內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com