<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
冪等性的定義是:一次和屢次請求某一個資源對於資源自己應該具備一樣的結果(網路超時等問題除外)。也就是說,其任意屢次執行對資源自己所產生的影響均與一次執行的影響相同。
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令牌和分散式鎖解決
<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>
這種方式分紅兩個階段:
1、使用者端向系統發起一次申請token的請求,伺服器系統生成token令牌,將token儲存到Redis快取中,並返回前端(令牌生成方式可以使用JWT
)
2、使用者端拿著申請到的token發起請求(放到請求頭中),後臺系統會在攔截器中檢查handler是否開啟冪等性校驗。取請求頭中的token,判斷Redis中是否存在該token,若是存在,表示第一次發起支付請求,刪除快取中token後開始業務邏輯處理;若是快取中不存在,表示非法請求。
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
冪等性註解。說明: 新增了該註解的介面要實現冪等性驗證
@Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ApiIdempotentAnn { boolean value() default true; }
這裡可以使用攔截器或者使用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); } }
@Configuration public class MvcConfig implements WebMvcConfigurer { @Resource private ApiIdempotentInterceptor apiIdempotentInceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(apiIdempotentInceptor).addPathPatterns("/**"); } }
@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是redis官網推薦實現分散式鎖的一個第三方類庫,通過開啟另一個服務,後臺程序定時檢查持有鎖的執行緒是否繼續持有鎖了,是將鎖的生命週期重置到指定時間,即防止執行緒釋放鎖之前過期,所以將鎖宣告週期通過重置延長)
Redission執行流程如下:(只要執行緒一加鎖成功,就會啟動一個watch dog看門狗,它是一個後臺執行緒,會每隔10秒檢查一下(鎖續命週期就是設定的超時時間的三分之一),如果執行緒還持有鎖,就會不斷的延長鎖key的生存時間。因此,Redis就是使用Redisson解決了鎖過期釋放,業務沒執行完問題。當業務執行完,釋放鎖後,再關閉守護執行緒,
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.13.6</version> </dependency>
分散式鎖註解
@Target(ElementType.METHOD) //註解在方法 @Retention(RetentionPolicy.RUNTIME) public @interface RedissonLockAnnotation { /** * 指定組成分散式鎖的key,以逗號分隔。 * 如:keyParts="name,age",則分散式鎖的key為這兩個欄位value的拼接 * key=params.getString("name")+params.getString("age") */ String keyParts(); }
分散式鎖介面
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); }
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(); } }
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); } }
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; } }
這裡可以使用攔截器或者使用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(); } } } }
統一響應實體
@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; } }
@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!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45