SpringBoot結合Redis實現接口冪等性的示例代碼

介紹

冪等性的概念是,任意多次執行所產生的影響都與一次執行產生的影響相同,按照這個含義,最終的解釋是對數據庫的影響隻能是一次性的,不能重復處理。手段如下

  • 數據庫建立唯一索引
  • token機制
  • 悲觀鎖或者是樂觀鎖
  • 先查詢後判斷

小小主要帶你們介紹Redis實現自動冪等性。其原理如下圖所示。

實現過程

引入 maven 依賴

     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

spring 配置文件寫入

server.port=8080
core.datasource.druid.enabled=true
core.datasource.druid.url=jdbc:mysql://192.168.1.225:3306/?useUnicode=true&characterEncoding=UTF-8
core.datasource.druid.username=root
core.datasource.druid.password=
core.redis.enabled=true
spring.redis.host=192.168.1.225 #本機的redis地址
spring.redis.port=16379
spring.redis.database=3
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=5s
spring.redis.jedis.pool.min-idle=10

引入 Redis

引入 Spring boot 中的redis相關的stater,後面需要用到 Spring Boot 封裝好的 RedisTemplate

package cn.smallmartial.demo.utils;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
 
import java.io.Serializable;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
 
/**
 * @Author smallmartial
 * @Date 2020/4/16
 * @Email [email protected]
 */
@Component
public class RedisUtil {
 
    @Autowired
    private RedisTemplate redisTemplate;
 
    /**
     * 寫入緩存
     *
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
 
    /**
     * 寫入緩存設置時間
     *
     * @param key
     * @param value
     * @param expireTime
     * @return
     */
    public boolean setEx(final String key, Object value, long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
 
    /**
     * 讀取緩存
     *
     * @param key
     * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }
 
    /**
     * 刪除對應的value
     *
     * @param key
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;
 
    }
 
    /**
     * 判斷key是否存在
     *
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        boolean result = false;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        if (Objects.nonNull(operations.get(key))) {
            result = true;
        }
        return result;
    }
 
 
}

自定義註解

自定義一個註解,定義此註解的目的是把它添加到需要實現冪等的方法上,隻要某個方法註解瞭其,都會自動實現冪等操作。其代碼如下

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
  
}

token 的創建和實現

token 服務接口,我們新建一個接口,創建token服務,裡面主要是有兩個方法,一個用來創建 token,一個用來驗證token

public interface TokenService {
 
    /**
     * 創建token
     * @return
     */
    public  String createToken();
 
    /**
     * 檢驗token
     * @param request
     * @return
     */
    public boolean checkToken(HttpServletRequest request) throws Exception;
 
}

token 的實現類,token中引用瞭服務的實現類,token引用瞭 redis 服務,創建token采用隨機算法工具類生成隨機 uuid 字符串,然後放入 redis 中,如果放入成功,返回token,校驗方法就是從 header 中獲取 token 的值,如果不存在,直接跑出異常,這個異常信息可以被直接攔截到,返回給前端。

package cn.smallmartial.demo.service.impl;
 
import cn.smallmartial.demo.bean.RedisKeyPrefix;
import cn.smallmartial.demo.bean.ResponseCode;
import cn.smallmartial.demo.exception.ApiResult;
import cn.smallmartial.demo.exception.BusinessException;
import cn.smallmartial.demo.service.TokenService;
import cn.smallmartial.demo.utils.RedisUtil;
import io.netty.util.internal.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
 
import javax.servlet.http.HttpServletRequest;
import java.util.Random;
import java.util.UUID;
 
/**
 * @Author smallmartial
 * @Date 2020/4/16
 * @Email sm[email protected]
 */
@Service
public class TokenServiceImpl implements TokenService {
    @Autowired
    private RedisUtil redisService;
 
    /**
     * 創建token
     *
     * @return
     */
    @Override
    public String createToken() {
        String str = UUID.randomUUID().toString().replace("-", "");
        StringBuilder token = new StringBuilder();
        try {
            token.append(RedisKeyPrefix.TOKEN_PREFIX).append(str);
            redisService.setEx(token.toString(), token.toString(), 10000L);
            boolean empty = StringUtils.isEmpty(token.toString());
            if (!empty) {
                return token.toString();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
 
    /**
     * 檢驗token
     *
     * @param request
     * @return
     */
    @Override
    public boolean checkToken(HttpServletRequest request) throws Exception {
 
        String token = request.getHeader(RedisKeyPrefix.TOKEN_NAME);
        if (StringUtils.isEmpty(token)) {// header中不存在token
            token = request.getParameter(RedisKeyPrefix.TOKEN_NAME);
            if (StringUtils.isEmpty(token)) {// parameter中也不存在token
                throw new BusinessException(ApiResult.BADARGUMENT);
            }
        }
 
        if (!redisService.exists(token)) {
            throw new BusinessException(ApiResult.REPETITIVE_OPERATION);
        }
 
        boolean remove = redisService.remove(token);
        if (!remove) {
            throw new BusinessException(ApiResult.REPETITIVE_OPERATION);
        }
        return true;
    }
}

攔截器的配置

用於攔截前端的 token,判斷前端的 token 是否有效

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
 
    @Bean
    public AuthInterceptor authInterceptor() {
        return new AuthInterceptor();
    }
 
    /**
     * 攔截器配置
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor());
//                .addPathPatterns("/ksb/**")
//                .excludePathPatterns("/ksb/auth/**", "/api/common/**", "/error", "/api/*");
        super.addInterceptors(registry);
    }
 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations(
                "classpath:/static/");
        registry.addResourceHandler("swagger-ui.html").addResourceLocations(
                "classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars/");
        super.addResourceHandlers(registry);
    }
 
 
}

攔截處理器:主要用於攔截掃描到 Autoldempotent 到註解方法,然後調用 tokenService 的 checkToken 方法校驗 token 是否正確,如果捕捉到異常就把異常信息渲染成 json 返回給前端。這部分代碼主要和自定義註解部分掛鉤。其主要代碼如下所示

@Slf4j
public class AuthInterceptor extends HandlerInterceptorAdapter {
 
    @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();
        //被ApiIdempotment標記的掃描
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {
            try {
                return tokenService.checkToken(request);// 冪等性校驗, 校驗通過則放行, 校驗失敗則拋出異常, 並通過統一異常處理返回友好提示
            } catch (Exception ex) {
                throw new BusinessException(ApiResult.REPETITIVE_OPERATION);
            }
        }
        return true;
    }
 
}

測試用例

這裡進行相關的測試用例 模擬業務請求類,通過相關的路徑獲得相關的token,然後調用 testidempotence 方法,這個方法註解瞭 @Autoldempotent,攔截器會攔截所有的請求,當判斷到處理的方法上面有該註解的時候,就會調用 TokenService 中的 checkToken() 方法,如果有異常會跑出,代碼如下所示

/**
 * @Author smallmartial
 * @Date 2020/4/16
 * @Email [email protected]
 */
@RestController
public class BusinessController {
 
 
    @Autowired
    private TokenService tokenService;
 
    @GetMapping("/get/token")
    public Object  getToken(){
        String token = tokenService.createToken();
        return ResponseUtil.ok(token) ;
    }
 
 
    @AutoIdempotent
    @GetMapping("/test/Idempotence")
    public Object testIdempotence() {
        String token = "接口冪等性測試";
        return ResponseUtil.ok(token) ;
    }
}

用瀏覽器進行訪問

用獲取到的token第一次訪問

用獲取到的token再次訪問

可以看到,第二次訪問失敗,即,冪等性驗證通過。

到此這篇關於SpringBoot結合Redis實現接口冪等性的示例代碼的文章就介紹到這瞭,更多相關SpringBoot Redis接口冪等性內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: