Redisson實(shí)現(xiàn)防重復(fù)提交
防止重復(fù)提交的重要性?
在業(yè)務(wù)開(kāi)發(fā)當(dāng)中,為什么我們要去想辦法解決重復(fù)提交這一問(wèn)題的發(fā)生呢?網(wǎng)上的概念有很多:導(dǎo)致表 單重復(fù)提交的,造成數(shù)據(jù)重復(fù)的,增加服務(wù)器負(fù)載的,甚至?xí)斐煞?wù)器宕機(jī)的,那么為什么會(huì)出現(xiàn)這 種情況呢?頁(yè)面快速的操作以及網(wǎng)絡(luò)Io問(wèn)題或者數(shù)據(jù)庫(kù)響應(yīng)慢,這些都會(huì)增加后端重復(fù)處理的概率, 就拿之前做的項(xiàng)目來(lái)說(shuō),因?yàn)闃I(yè)務(wù)性較強(qiáng),需要進(jìn)行一個(gè)"點(diǎn)贊"的操作,寫(xiě)好業(yè)務(wù)以后在測(cè)試時(shí)連續(xù)點(diǎn) 擊幾下,重復(fù)地進(jìn)行點(diǎn)贊和取消點(diǎn)贊操作,因?yàn)椴僮鬟^(guò)于頻繁,而服務(wù)器走過(guò)來(lái)的響應(yīng)速度沒(méi)有那么快 地進(jìn)行處理,就會(huì)導(dǎo)致數(shù)據(jù)的重復(fù)插入情況,最后導(dǎo)致服務(wù)器報(bào)錯(cuò),這時(shí),防止重復(fù)提交就顯得很重要 了。
如何防重復(fù)提交?
實(shí)現(xiàn)方法有非常多,但是原理大概都是相通的,我選擇的是通過(guò)使用AOP切面+Redisson來(lái)進(jìn)行處理, 前端發(fā)起請(qǐng)求的時(shí)候需要在請(qǐng)求頭里將token給我,然后我這邊通過(guò)token+ip再加上請(qǐng)求的路徑作為一 個(gè)key存到redis里面去,設(shè)置一個(gè)過(guò)期時(shí)間,下一次再?gòu)木彺娈?dāng)中讀取和當(dāng)前時(shí)間進(jìn)行一個(gè)判斷,若大 于我們自己設(shè)定的超時(shí)時(shí)間,就進(jìn)行攔截,不讓它進(jìn)行后面的業(yè)務(wù)代碼,并給出提示“操作頻繁,請(qǐng)稍 后重試” 分布式鎖方案采用Redisson,這也是官網(wǎng)比較推薦的使用方法。利用分布式鎖的特性來(lái)防止重復(fù)提交。
1.導(dǎo)入依賴(lài)
????<dependency>?
????<groupId>org.redisson</groupId>
???? <artifactId>redisson-spring-boot-starter</artifactId>?
????<version>3.15.0</version>?
????</dependency>
2.使用注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)?
@Documented?
public @interface NoRepeat {?
/**
* 鎖名稱(chēng)
*/?
String lockName() default "no-repeat-default-lock-";?
/**
* 參數(shù)key,支持Spel?
*/
String key() default "";
/**
* 獲取鎖的最長(zhǎng)時(shí)間,單位默認(rèn)為秒?
*/?
long waitTime() default 5;
/**
* 租賃時(shí)間,單位默認(rèn)為秒
* 加鎖的時(shí)間。超過(guò)這個(gè)時(shí)間后鎖便自動(dòng)解開(kāi)了?
*/
3.編寫(xiě)切面?
????@Slf4j?
????@Aspect?
????@Component?
????public class SystemAspect {?
????@Resource private RedissonClient redissonClient;?
????/**
????* SPEL表達(dá)式解析器?
????*/?
????private final SpelExpressionParser parser = new SpelExpressionParser();?
????LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); @Around(value = "@annotation(NoRepeat)")?
????public Object doNoRepeatAround(ProceedingJoinPoint pjp) throws InterruptedException?
????{?
????NoRepeat noRepeat = CsUtil.getJoinPointAnnotation(pjp, NoRepeat.class);?
????String key = parseKey(pjp, noRepeat);?
????RLock fairLock = redissonClient.getFairLock(noRepeat.lockName() + key);?
????boolean res = fairLock.tryLock(noRepeat.waitTime(), noRepeat.leaseTime(), TimeUnit.SECONDS);?
????if (res) {?
????log.info("拿到鎖");
???? try {return pjp.proceed();?
????} catch (Throwable e) {?
????log.error(e.getMessage(), e);?
????throw new ServiceException(e.getMessage());?
????} finally { fairLock.unlock(); log.info("釋放鎖操作"); }
???? } else { log.error("重復(fù)的請(qǐng)求");?
????return new Response<Boolean>(999, "重復(fù)的請(qǐng)求");?
????????}?
????}
????private String parseKey(ProceedingJoinPoint pjp, NoRepeat noRepeat) {?
????Object[] args = pjp.getArgs();?
????Method method = ((MethodSignature) pjp.getSignature()).getMethod();?
????String[] params = discoverer.getParameterNames(method);?
????EvaluationContext context = new StandardEvaluationContext();?
????for (int len = 0; len < Objects.requireNonNull(params).length; len++) {?
????context.setVariable(params[len], args[len]); }
????String keySpel = noRepeat.key(); Expression keyExpression = parser.parseExpression(keySpel);
???? return keyExpression.getValue(context, String.class);?
????????}?
????}
4. 使用方式
????@RestController?
????public class TestController {?
????@ApiOperation("身份綁定(注冊(cè))接口")?
????@NoRepeat(key = "#user.name")?
????@PostMapping("/v1/test/noRepeat")?
????public Response<User> doSome(@RequestBody User user) {
???? return new Response<>();?
????}
????@Data?
????static class User {?
????private String name;
???? ????}
???? }
5.展示效果
