什么是接口的冪等性,如何實(shí)現(xiàn)接口冪等性,? (一)冪等性概念冪等性原本是數(shù)學(xué)上的概念,用在接口上就可以理解為:同一個(gè)接口,,多次發(fā)出同一個(gè)請(qǐng)求,,必須保證操作只執(zhí)行一次。 調(diào)用接口發(fā)生異常并且重復(fù)嘗試時(shí),,總是會(huì)造成系統(tǒng)所無法承受的損失,,所以必須阻止這種現(xiàn)象的發(fā)生。 比如下面這些情況,,如果沒有實(shí)現(xiàn)接口冪等性會(huì)有很嚴(yán)重的后果: 支付接口,,重復(fù)支付會(huì)導(dǎo)致多次扣錢 訂單接口,同一個(gè)訂單可能會(huì)多次創(chuàng)建。 (二)冪等性的解決方案唯一索引 使用唯一索引可以避免臟數(shù)據(jù)的添加,,當(dāng)插入重復(fù)數(shù)據(jù)時(shí)數(shù)據(jù)庫會(huì)拋異常,,保證了數(shù)據(jù)的唯一性。 樂觀鎖 這里的樂觀鎖指的是用樂觀鎖的原理去實(shí)現(xiàn),,為數(shù)據(jù)字段增加一個(gè)version字段,,當(dāng)數(shù)據(jù)需要更新時(shí),先去數(shù)據(jù)庫里獲取此時(shí)的version版本號(hào) select version from tablename where xxx 更新數(shù)據(jù)時(shí)首先和版本號(hào)作對(duì)比,,如果不相等說明已經(jīng)有其他的請(qǐng)求去更新數(shù)據(jù)了,,提示更新失敗。
悲觀鎖 樂觀鎖可以實(shí)現(xiàn)的往往用悲觀鎖也能實(shí)現(xiàn),,在獲取數(shù)據(jù)時(shí)進(jìn)行加鎖,當(dāng)同時(shí)有多個(gè)重復(fù)請(qǐng)求時(shí)其他請(qǐng)求都無法進(jìn)行操作 分布式鎖 冪等的本質(zhì)是分布式鎖的問題,,分布式鎖正??梢酝ㄟ^redis或zookeeper實(shí)現(xiàn);在分布式環(huán)境下,,鎖定全局唯一資源,,使請(qǐng)求串行化,實(shí)際表現(xiàn)為互斥鎖,,防止重復(fù),,解決冪等。 token機(jī)制 token機(jī)制的核心思想是為每一次操作生成一個(gè)唯一性的憑證,,也就是token,。一個(gè)token在操作的每一個(gè)階段只有一次執(zhí)行權(quán),一旦執(zhí)行成功則保存執(zhí)行結(jié)果,。對(duì)重復(fù)的請(qǐng)求,,返回同一個(gè)結(jié)果。token機(jī)制的應(yīng)用十分廣泛,。 (三)token機(jī)制的實(shí)現(xiàn)這里展示通過token機(jī)制實(shí)現(xiàn)接口冪等性的案例:github文末自取 首先引入需要的依賴: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency> 3.1,、配置請(qǐng)求的方法體和枚舉類首先配置一下通用的請(qǐng)求返回體
以及返回code public enum ResponseCode { // 通用模塊 1xxxx ILLEGAL_ARGUMENT(10000, '參數(shù)不合法'), REPETITIVE_OPERATION(10001, '請(qǐng)勿重復(fù)操作'), ; ResponseCode(Integer code, String msg) { this.code = code; this.msg = msg; } private Integer code; private String msg; public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; }} 3.2 自定義異常以及配置全局異常類
配置全局異常捕獲器 @ControllerAdvicepublic class MyControllerAdvice { @ResponseBody @ExceptionHandler(ServiceException.class) public Response serviceExceptionHandler(ServiceException exception){ Response response=new Response(Integer.valueOf(exception.getCode()),exception.getMsg(),null); return response; }} 3.3 編寫創(chuàng)建Token和驗(yàn)證Token的接口以及實(shí)現(xiàn)類
具體實(shí)現(xiàn)類,,核心的業(yè)務(wù)邏輯都寫在注釋中了 @Servicepublic class TokenServiceImpl implements TokenService { @Autowired private RedisTemplate redisTemplate; @Override public Response createToken() { //生成uuid當(dāng)作token String token = UUID.randomUUID().toString().replaceAll('-',''); //將生成的token存入redis中 redisTemplate.opsForValue().set(token,token); //返回正確的結(jié)果信息 Response response=new Response(0,token.toString(),null); return response; } @Override public Response checkToken(HttpServletRequest request) { //從請(qǐng)求頭中獲取token String token=request.getHeader('token'); if (StringUtils.isBlank(token)){ //如果請(qǐng)求頭token為空就從參數(shù)中獲取 token=request.getParameter('token'); //如果都為空拋出參數(shù)異常的錯(cuò)誤 if (StringUtils.isBlank(token)){ throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getCode().toString(),ResponseCode.ILLEGAL_ARGUMENT.getMsg()); } } //如果redis中不包含該token,說明token已經(jīng)被刪除了,,拋出請(qǐng)求重復(fù)異常 if (!redisTemplate.hasKey(token)){ throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg()); } //刪除token Boolean del=redisTemplate.delete(token); //如果刪除不成功(已經(jīng)被其他請(qǐng)求刪除),,拋出請(qǐng)求重復(fù)異常 if (!del){ throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg()); } return new Response(0,'校驗(yàn)成功',null); }} 3.4 配置自定義注解這是比較重要的一步,通過自定義注解在需要實(shí)現(xiàn)接口冪等性的方法上添加此注解,實(shí)現(xiàn)token驗(yàn)證
接口攔截器 public class ApiIdempotentInterceptor implements HandlerInterceptor { @Autowired private TokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod= (HandlerMethod) handler; Method method=handlerMethod.getMethod(); ApiIdempotent methodAnnotation=method.getAnnotation(ApiIdempotent.class); if (methodAnnotation != null){ // 校驗(yàn)通過放行,,校驗(yàn)不通過全局異常捕獲后輸出返回結(jié)果 tokenService.checkToken(request); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { }} 3.5 配置攔截器以及redis配置webConfig,,添加攔截器
配置redis,使得中文可以正常傳輸 @Configurationpublic class RedisConfig { //自定義的redistemplate @Bean(name = 'redisTemplate') public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){ //創(chuàng)建一個(gè)RedisTemplate對(duì)象,,為了方便返回key為string,,value為Object RedisTemplate<String,Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); //設(shè)置json序列化配置 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper=new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance); //string的序列化 StringRedisSerializer stringRedisSerializer=new StringRedisSerializer(); //key采用string的序列化方式 template.setKeySerializer(stringRedisSerializer); //value采用jackson的序列化方式 template.setValueSerializer(jackson2JsonRedisSerializer); //hashkey采用string的序列化方式 template.setHashKeySerializer(stringRedisSerializer); //hashvalue采用jackson的序列化方式 template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; }} 最后是controller
(四)結(jié)果驗(yàn)證首先通過token接口創(chuàng)建一個(gè)token出來,此時(shí)redis中也存在了該token 在jmeter中同時(shí)運(yùn)行50個(gè)請(qǐng)求,我們可以觀察到,,只有第一個(gè)請(qǐng)求校驗(yàn)成功,,后續(xù)的請(qǐng)求均提示請(qǐng)勿重復(fù)操作。 |
|