推薦閱讀:
什麼是接口的冪等性,如何實現接口冪等性?
(一)冪等性概念
冪等性原本是數學上的概念,用在接口上就可以理解爲:同一個接口,多次發出同一個請求,必須保證操作只執行一次。 調用接口發生異常並且重複嘗試時,總是會造成系統所無法承受的損失,所以必須阻止這種現象的發生。 比如下面這些情況,如果沒有實現接口冪等性會有很嚴重的後果: 支付接口,重複支付會導致多次扣錢 訂單接口,同一個訂單可能會多次創建。
(二)冪等性的解決方案
唯一索引 使用唯一索引可以避免髒數據的添加,當插入重複數據時數據庫會拋異常,保證了數據的唯一性。
樂觀鎖 這裏的樂觀鎖指的是用樂觀鎖的原理去實現,爲數據字段增加一個version字段,當數據需要更新時,先去數據庫裏獲取此時的version版本號
select version from tablename where xxx
更新數據時首先和版本號作對比,如果不相等說明已經有其他的請求去更新數據了,提示更新失敗。
update tablename set count=count+1,version=version+1 where version=#{version}
悲觀鎖 樂觀鎖可以實現的往往用悲觀鎖也能實現,在獲取數據時進行加鎖,當同時有多個重複請求時其他請求都無法進行操作
分佈式鎖 冪等的本質是分佈式鎖的問題,分佈式鎖正常可以通過redis或zookeeper實現;在分佈式環境下,鎖定全局唯一資源,使請求串行化,實際表現爲互斥鎖,防止重複,解決冪等。
token機制 token機制的核心思想是爲每一次操作生成一個唯一性的憑證,也就是token。一個token在操作的每一個階段只有一次執行權,一旦執行成功則保存執行結果。對重複的請求,返回同一個結果。token機制的應用十分廣泛。
(三)token機制的實現
這裏展示通過token機制實現接口冪等性的案例: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、配置請求的方法體和枚舉類
首先配置一下通用的請求返回體
public class Response {
private int status;
private String msg;
private Object data;
//省略get、set、toString、無參有參構造方法
}
以及返回code
public enum ResponseCode {
// 通用模塊 1xxxx
ILLEGAL_ARGUMENT(10000, "參數不合法"),
REPETITIVE_OPERATION(10001, "請勿重複操作"),
;
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 自定義異常以及配置全局異常類
public class ServiceException extends RuntimeException{
private String code;
private String msg;
//省略get、set、toString以及構造方法
}
配置全局異常捕獲器
@ControllerAdvice
public 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 編寫創建Token和驗證Token的接口以及實現類
@Service
public interface TokenService {
public Response createToken();
public Response checkToken(HttpServletRequest request);
}
具體實現類,核心的業務邏輯都寫在註釋中了
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private RedisTemplate redisTemplate;
@Override
public Response createToken() {
//生成uuid當作token
String token = UUID.randomUUID().toString().replaceAll("-","");
//將生成的token存入redis中
redisTemplate.opsForValue().set(token,token);
//返回正確的結果信息
Response response=new Response(0,token.toString(),null);
return response;
}
@Override
public Response checkToken(HttpServletRequest request) {
//從請求頭中獲取token
String token=request.getHeader("token");
if (StringUtils.isBlank(token)){
//如果請求頭token爲空就從參數中獲取
token=request.getParameter("token");
//如果都爲空拋出參數異常的錯誤
if (StringUtils.isBlank(token)){
throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getCode().toString(),ResponseCode.ILLEGAL_ARGUMENT.getMsg());
}
}
//如果redis中不包含該token,說明token已經被刪除了,拋出請求重複異常
if (!redisTemplate.hasKey(token)){
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg());
}
//刪除token
Boolean del=redisTemplate.delete(token);
//如果刪除不成功(已經被其他請求刪除),拋出請求重複異常
if (!del){
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg());
}
return new Response(0,"校驗成功",null);
}
}
3.4 配置自定義註解
這是比較重要的一步,通過自定義註解在需要實現接口冪等性的方法上添加此註解,實現token驗證
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
接口攔截器
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){
// 校驗通過放行,校驗不通過全局異常捕獲後輸出返回結果
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,添加攔截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiIdempotentInterceptor());
}
@Bean
public ApiIdempotentInterceptor apiIdempotentInterceptor() {
return new ApiIdempotentInterceptor();
}
}
配置redis,使得中文可以正常傳輸
@Configuration
public class RedisConfig {
//自定義的redistemplate
@Bean(name = "redisTemplate")
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
//創建一個RedisTemplate對象,爲了方便返回key爲string,value爲Object
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
//設置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
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
private TokenService tokenService;
@GetMapping
public Response token(){
return tokenService.createToken();
}
@PostMapping("checktoken")
public Response checktoken(HttpServletRequest request){
return tokenService.checkToken(request);
}
}
(四)結果驗證
首先通過token接口創建一個token出來,此時redis中也存在了該token
在jmeter中同時運行50個請求,我們可以觀察到,只有第一個請求校驗成功,後續的請求均提示請勿重複操作。