SpringBoot Redis實(shí)現(xiàn)接口冪等性校驗(yàn)方法詳細(xì)講解
冪等性
冪等性的定義是:一次和屢次請(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利用Jackson序列化實(shí)現(xiàn)數(shù)據(jù)脫敏
這篇文章主要介紹了利用Jackson序列化實(shí)現(xiàn)數(shù)據(jù)脫敏,首先在需要進(jìn)行脫敏的VO字段上面標(biāo)注相關(guān)脫敏注解,具體實(shí)例代碼文中給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-10-10Java使用正則表達(dá)式進(jìn)行匹配且對(duì)匹配結(jié)果逐個(gè)替換
這篇文章主要介紹了Java使用正則表達(dá)式進(jìn)行匹配且對(duì)匹配結(jié)果逐個(gè)替換,文章圍繞主題展開詳細(xì)的內(nèi)容戒殺,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09使用.NET Core3.0創(chuàng)建一個(gè)Windows服務(wù)的方法
這篇文章主要介紹了使用.NET Core3.0創(chuàng)建一個(gè)Windows服務(wù)的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-04-04