SpringBoot Redis實現(xiàn)接口冪等性校驗方法詳細講解
冪等性
冪等性的定義是:一次和屢次請求某一個資源對于資源自己應該具備一樣的結(jié)果(網(wǎng)絡超時等問題除外)。也就是說,其任意屢次執(zhí)行對資源自己所產(chǎn)生的影響均與一次執(zhí)行的影響相同。
WEB系統(tǒng)中: 就是用戶對于同一操作發(fā)起的一次請求或者多次請求的結(jié)果是一致的,不會因為多次點擊而產(chǎn)生不同的結(jié)果。
什么狀況下須要保證冪等性
以SQL為例,有下面三種場景,只有第三種場景須要開發(fā)人員使用其余策略保證冪等性:
SELECT col1 FROM tab1 WHER col2=2,不管執(zhí)行多少次都不會改變狀態(tài),是自然的冪等。
UPDATE tab1 SET col1=1 WHERE col2=2,不管執(zhí)行成功多少次狀態(tài)都是一致的,所以也是冪等操做。
UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次執(zhí)行的結(jié)果都會發(fā)生變化,這種不是冪等的。
解決方法
這里主要使用token令牌和分布式鎖解決
Pom
<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>
token令牌
這種方式分紅兩個階段:
1、客戶端向系統(tǒng)發(fā)起一次申請token的請求,服務器系統(tǒng)生成token令牌,將token保存到Redis緩存中,并返回前端(令牌生成方式可以使用JWT)
2、客戶端拿著申請到的token發(fā)起請求(放到請求頭中),后臺系統(tǒng)會在攔截器中檢查handler是否開啟冪等性校驗。取請求頭中的token,判斷Redis中是否存在該token,若是存在,表示第一次發(fā)起支付請求,刪除緩存中token后開始業(yè)務邏輯處理;若是緩存中不存在,表示非法請求。
yml
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
@ApiIdempotentAnn冪等性注解。說明: 添加了該注解的接口要實現(xiàn)冪等性驗證
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotentAnn {
boolean value() default true;
}
ApiIdempotentInterceptor
這里可以使用攔截器或者使用AOP的方式實現(xiàn)。
冪等性攔截器的方式實現(xiàn)
@Component
public class ApiIdempotentInterceptor extends HandlerInterceptorAdapter {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 前置攔截器
*在方法被調(diào)用前執(zhí)行。在該方法中可以做類似校驗的功能。如果返回true,則繼續(xù)調(diào)用下一個攔截器。如果返回false,則中斷執(zhí)行,
* 也就是說我們想調(diào)用的方法 不會被執(zhí)行,但是你可以修改response為你想要的響應。
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果hanler不是和HandlerMethod類型,則返回true
if (!(handler instanceof HandlerMethod)) {
return true;
}
//轉(zhuǎn)化類型
final HandlerMethod handlerMethod = (HandlerMethod) handler;
//獲取方法類
final Method method = handlerMethod.getMethod();
// 判斷當前method中是否有這個注解
boolean methodAnn = method.isAnnotationPresent(ApiIdempotentAnn.class);
//如果有冪等性注解
if (methodAnn && method.getAnnotation(ApiIdempotentAnn.class).value()) {
// 需要實現(xiàn)接口冪等性
//檢查token
//1.獲取請求的接口方法
boolean result = checkToken(request);
//如果token有值,說明是第一次調(diào)用
if (result) {
//則放行
return super.preHandle(request, response, handler);
} else {//如果token沒有值,則表示不是第一次調(diào)用,是重復調(diào)用
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.print("重復調(diào)用");
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,說明是重復調(diào)用
if(StringUtils.isBlank(token)){
return false;
}
//否則就是存在,存在則把redis里刪除token
return redisTemplate.delete(token);
}
}MVC配置類
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private ApiIdempotentInterceptor apiIdempotentInceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiIdempotentInceptor).addPathPatterns("/**");
}
}ApiController
@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);
/**
* 主業(yè)務邏輯,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
Redisson是redis官網(wǎng)推薦實現(xiàn)分布式鎖的一個第三方類庫,通過開啟另一個服務,后臺進程定時檢查持有鎖的線程是否繼續(xù)持有鎖了,是將鎖的生命周期重置到指定時間,即防止線程釋放鎖之前過期,所以將鎖聲明周期通過重置延長)
Redission執(zhí)行流程如下:(只要線程一加鎖成功,就會啟動一個watch dog看門狗,它是一個后臺線程,會每隔10秒檢查一下(鎖續(xù)命周期就是設置的超時時間的三分之一),如果線程還持有鎖,就會不斷的延長鎖key的生存時間。因此,Redis就是使用Redisson解決了鎖過期釋放,業(yè)務沒執(zhí)行完問題。當業(yè)務執(zhí)行完,釋放鎖后,再關(guān)閉守護線程,
pom
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.13.6</version> </dependency>
@RedissonLockAnnotation
分布式鎖注解
@Target(ElementType.METHOD) //注解在方法
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLockAnnotation {
/**
* 指定組成分布式鎖的key,以逗號分隔。
* 如:keyParts="name,age",則分布式鎖的key為這兩個字段value的拼接
* key=params.getString("name")+params.getString("age")
*/
String keyParts();
}
DistributeLocker
分布式鎖接口
public interface DistributeLocker {
/**
* 加鎖
* @param lockKey key
*/
void lock(String lockKey);
/**
* 釋放鎖
*
* @param lockKey key
*/
void unlock(String lockKey);
/**
* 加鎖,設置有效期
*
* @param lockKey key
* @param timeout 有效時間,默認時間單位在實現(xiàn)類傳入
*/
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時間內(nèi)一直嘗試獲取,超過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);
}RedissonDistributeLocker
redisson實現(xiàn)分布式鎖接口
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();
}
}
RedissonLockUtils
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);
}
}
RedissonConfig
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;
}
}RedissonLockAop
這里可以使用攔截器或者使用AOP的方式實現(xiàn)。
分布式鎖AOP切面攔截方式實現(xiàn)
@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);
//獲取參數(shù)列表
Object[] objs = pjp.getArgs();
//因為只有一個JSON參數(shù),直接取第一個
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();
}
}
}
}ResultVO
統(tǒng)一響應實體
@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;
}
}BusiController
@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均相同的請求不會出現(xiàn)并發(fā)問題
*/
//TODO 業(yè)務處理dwad
return ResultVO.ok();
}
}
到此這篇關(guān)于SpringBoot Redis實現(xiàn)接口冪等性校驗方法詳細講解的文章就介紹到這了,更多相關(guān)SpringBoot Redis接口冪等性校驗內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java利用Jackson序列化實現(xiàn)數(shù)據(jù)脫敏
這篇文章主要介紹了利用Jackson序列化實現(xiàn)數(shù)據(jù)脫敏,首先在需要進行脫敏的VO字段上面標注相關(guān)脫敏注解,具體實例代碼文中給大家介紹的非常詳細,需要的朋友可以參考下2021-10-10
Java使用正則表達式進行匹配且對匹配結(jié)果逐個替換
這篇文章主要介紹了Java使用正則表達式進行匹配且對匹配結(jié)果逐個替換,文章圍繞主題展開詳細的內(nèi)容戒殺,具有一定的參考價值,需要的小伙伴可以參考一下2022-09-09
使用.NET Core3.0創(chuàng)建一個Windows服務的方法
這篇文章主要介紹了使用.NET Core3.0創(chuàng)建一個Windows服務的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-04-04

