一.什么是 冪等性
在編程中,冪等性的特點就是其任意多次執行的效果和一次執行的效果所產生的影響是一樣的。
二.Token+Redis的實現思路
1.數據提交前要向服務的申請 token(用戶登錄時可以獲取),token 放到 redis 或 jvm 內存,token 有效時間;
2. 提交后后台校驗 token,同時刪除 token,生成新的 token 返回。
注意:Redis要用刪除操作來判斷是否操作成功,刪除成功代表校驗成功。
三.具體實現
1.首先導入Redis的pom依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>
2.設置切面
package com.apps.idempotent.aspect; import com.apps.bcodemsg.MsgResponse; import com.apps.redis.service.RedisService; import org.apache.tomcat.util.http.fileupload.servlet.ServletRequestContext; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; @Order(2) @Component @Aspect public class IdempotentAspect { @Autowired private RedisService redisService; @Pointcut("@annotation(com.apps.annotation.Token)") public void tokenIdempotent(){} @Around(value = "tokenIdempotent()") public MsgResponse before(ProceedingJoinPoint jp) { System.out.println("================================IdempotentAspect=============================="); MsgResponse response = new MsgResponse(); ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); String token = request.getHeader("token"); System.out.println("=====================接收到的參數token:"+token); if(StringUtils.isEmpty(token)){ response.fail("key.err"); response.setMsg("請勿重復點擊!"); return response; } boolean contains = redisService.contains(token); if(!contains){ response.fail("key.err"); response.setMsg("請勿重復點擊!"); return response; } Object stringToken = redisService.get(token); if(stringToken!=null){ boolean flag = redisService.del(token); if(!flag){ response.fail("key.err"); response.setMsg("請勿重復點擊!"); return response; } System.out.println("==========攔截成功,成功刪除redis中的token,避免重復提交。"); } try { response = (MsgResponse) jp.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); return null; } return response; } }
注意:
1.因為之前有一個日志切面,如果不使用@Order注解標明切面執行順序就會報錯,當然如果沒有其他切面就不需要添加這個@Order注解
2.其中MsgResponse類是一個回復類
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.apps.bcodemsg; import com.apps.asysfinal.SysFinal; import com.apps.msgconfig.MyProperties; import com.github.pagehelper.Page; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import java.util.List; @ApiModel( value = "數據模型", description = "數據模型" ) public class MsgResponse extends SysFinal { @ApiModelProperty( example = "狀態碼,成功200,失敗400" ) private int code; @ApiModelProperty( example = "錯誤和成功信息" ) private String msg; @ApiModelProperty( example = "交互提示" ) private String msgText; @ApiModelProperty( example = "每頁行數" ) private Integer pageNum; @ApiModelProperty( example = "當前頁數" ) private Integer pageSize; @ApiModelProperty( example = "總行數" ) private Long pageTotal; @ApiModelProperty( example = "總頁數" ) private Integer pages; @ApiModelProperty( example = "開始行" ) private Integer startRow; @ApiModelProperty( example = "結束行" ) private Integer endRow; @ApiModelProperty( example = "返回數據" ) private Object data; public MsgResponse() { } public void success(Object obj, String key) { this.setCode(200); this.setMsg("業務處理成功!"); this.setMsgText(MyProperties.getPropertiesText(key)); this.setData(obj); } public void success(String key) { this.setCode(200); this.setMsg("業務處理成功!"); this.setMsgText(MyProperties.getPropertiesText(key)); } public void fail(Object obj, String key) { this.setCode(400); this.setMsg("業務處理失敗!"); this.setMsgText(MyProperties.getPropertiesText(key)); this.setData(obj); } public void fail(String key) { this.setCode(400); this.setMsg("業務處理失敗!"); this.setMsgText(MyProperties.getPropertiesText(key)); } public MsgResponse add(Object value) { this.setData(value); return this; } public int getCode() { return this.code; } public void setCode(int code) { this.code = code; } public String getMsg() { return this.msg; } public void setMsg(String msg) { this.msg = msg; } public Object getData() { return this.data; } public void setData(Object data) { if (data instanceof Page) { Page page = (Page)data; this.setPageNum(page.getPageNum()); this.setPageSize(page.getPageSize()); this.setPageTotal(page.getTotal()); this.setPages(page.getPages()); this.setStartRow(page.getStartRow()); this.setEndRow(page.getEndRow()); } this.data = data; } public Object returnJson() { return this; } public String getMsgText() { return this.msgText; } public void setMsgText(String msgText) { this.msgText = msgText; } public Integer getPageNum() { return this.pageNum; } public void setPageNum(Integer pageNum) { this.pageNum = pageNum; } public Integer getPageSize() { return this.pageSize; } public void setPageSize(Integer pageSize) { this.pageSize = pageSize; } public Long getPageTotal() { return this.pageTotal; } public void setPageTotal(Long pageTotal) { this.pageTotal = pageTotal; } public Integer getPages() { return this.pages; } public void setPages(Integer pages) { this.pages = pages; } public Integer getStartRow() { return this.startRow; } public void setStartRow(Integer startRow) { this.startRow = startRow; } public Integer getEndRow() { return this.endRow; } public void setEndRow(Integer endRow) { this.endRow = endRow; } public String toString() { if (!(this.data instanceof List)) { return "MsgResponse{code=" + this.code + ", msg='" + this.msg + '\'' + ", msgText='" + this.msgText + '\'' + ", pageNum=" + this.pageNum + ", pageSize=" + this.pageSize + ", pageTotal=" + this.pageTotal + ", pages=" + this.pages + ", startRow=" + this.startRow + ", endRow=" + this.endRow + ", data=" + this.data + '}'; } else { List list = (List)this.data; StringBuffer stringBuffer = new StringBuffer(); for(int i = 0; i < list.size(); ++i) { stringBuffer.append(list.get(i)); } return "MsgResponse{code=" + this.code + ", msg='" + this.msg + '\'' + ", msgText='" + this.msgText + '\'' + ", pageNum=" + this.pageNum + ", pageSize=" + this.pageSize + ", pageTotal=" + this.pageTotal + ", pages=" + this.pages + ", startRow=" + this.startRow + ", endRow=" + this.endRow + ", data=" + stringBuffer + '}'; } } }
3.添加相關的業務代碼和控制器
package com.apps.controller.Idempotent; import com.apps.annotation.Token; import com.apps.bcodemsg.MsgResponse; import com.apps.redis.service.RedisService; import com.apps.service.Idempotent.IdempotentService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import java.math.BigDecimal; @Controller @RequestMapping("/idempotent") @Api(value = "IdempotentController",tags = {"冪等測試controller"}) public class IdempotentController { @Autowired private IdempotentService idempotentService; @Autowired private RedisService redisService; @RequestMapping(value = "/create/token",method = RequestMethod.POST) @ResponseBody @ApiOperation("創建token") public MsgResponse createToken(){ MsgResponse response = idempotentService.createToken(); return response; } @RequestMapping(value = "/balance",method = RequestMethod.POST) @ResponseBody @ApiOperation("進行業務操作") @Token public MsgResponse subTract( BigDecimal count){ MsgResponse response = idempotentService.subtract(count); return response; } @RequestMapping(value = "/get/token",method = RequestMethod.POST) @ApiOperation("判斷token是否還有效") public void getToken(String key){ boolean contains = redisService.contains(key); System.out.println("==========redis是否存在:"+contains); } @RequestMapping(value = "/del/token",method = RequestMethod.POST) @ApiOperation("刪除token") public void delToken(String key){ boolean contains = redisService.contains(key); System.out.println("==========redis是否存在:"+contains); boolean del = redisService.del(key); if(del){ System.out.println("===========key刪除成功!"); }else{ System.out.println("==============key刪除失敗"); } } }
package com.apps.service.Idempotent.impl; import com.apps.bcodemsg.MsgResponse; import com.apps.redis.service.RedisService; import com.apps.service.Idempotent.IdempotentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.UUID; @Service public class IdempotentImpl implements IdempotentService { private static BigDecimal account = new BigDecimal(10000); @Autowired private RedisService redisService; @Override public MsgResponse createToken() { MsgResponse msgResponse = new MsgResponse(); try{ String token = UUID.randomUUID().toString(); System.out.println("============token: "+token); boolean isSuccess = redisService.set(token, token, 10*60*1000); if(isSuccess){ System.out.println("============token成功添加到Redis中。"); msgResponse.success("key.msg"); msgResponse.setData(token); msgResponse.setMsg("創建token成功!"); }else{ System.out.println("=============token添加到Redis中失敗!"); msgResponse.success("key.err"); msgResponse.setMsg("創建token失敗!"); } }catch(Exception ex){ System.out.println("發生異常的接口:createToken()"); ex.printStackTrace(); }finally { return msgResponse; } } @Override public MsgResponse subtract(BigDecimal count) { MsgResponse response = new MsgResponse(); try{ System.out.println("===============當前數量:"+account); BigDecimal result = account.subtract(count); account = result; if(account.setScale(2).compareTo(BigDecimal.ZERO)<=0){ response.fail("key.err"); response.setMsg("余額已經小於0,無法繼續操作!"); return response; } System.out.println("=================扣除成功,你很棒棒噠啊!"); }catch(Exception ex){ System.out.println("異常接口為:subtract(Integer count)"); ex.printStackTrace(); }finally { return response; } } }
四.使用Jmeter進行測試
會看到只有一個請求能夠成功處理業務,其余請求無法修改數據。