使用Redis控制表單重復(fù)提交和控制接口訪問(wèn)頻率方式
場(chǎng)景一:控制表單重復(fù)提交
防重提交有很多方案,從前端的按鈕置灰,到后端synchronize鎖、Lock鎖、借助Redis語(yǔ)法實(shí)現(xiàn)簡(jiǎn)單鎖、Redis+Lua分布式鎖、Redisson分布式鎖,再到DB的悲觀鎖、樂(lè)觀鎖、借助表唯一索引等等都可以實(shí)現(xiàn)防重提交,以保證數(shù)據(jù)的安全性。
這篇文章我們介紹其中一種方案–借助Redis語(yǔ)法實(shí)現(xiàn)簡(jiǎn)單鎖,最終實(shí)現(xiàn)防重提交。
背景
我們項(xiàng)目中,為了控制表單重復(fù)提交問(wèn)題,會(huì)在點(diǎn)擊頁(yè)面按鈕(向后端發(fā)起業(yè)務(wù)請(qǐng)求)后就會(huì)置灰按鈕,直到后端響應(yīng)后解除按鈕置灰。通過(guò)按鈕置灰來(lái)防止重啟提交問(wèn)題。但Postman、Jmeter和其他服務(wù)調(diào)用(繞過(guò)前端頁(yè)面)呢?所以后端接口也要根據(jù)控制表單重復(fù)提交的問(wèn)題。
后端代碼可以在2個(gè)位置做控制:
一是放在gateway網(wǎng)關(guān)做:
- 好處是只在一個(gè)地方加上控制代碼,就可以控制所有接口的重復(fù)提交問(wèn)題。
- 壞處是控制的范圍太廣(比如查詢接口無(wú)需控制,控制了反而多余)、定義重復(fù)提交的時(shí)間段不能靈活調(diào)整。
二是放在AOP切面做:
- 好處是只有需要的地方才會(huì)被控制(哪里需要引用一下自定義注解即可),另外也能靈活調(diào)整定義重復(fù)提交的時(shí)間段(自定義注解里定義時(shí)間字段開放給使用者填寫)。
- 壞處是每個(gè)需要控制的地方都要加注解,會(huì)有侵入性和一定的工作量。
實(shí)現(xiàn)代碼
1、添加自定義注解
package com.xxx.annotations;
import java.lang.annotation.*;
/**
* 自定義注解防止表單重復(fù)提交
*
* @Author WANGLINGQIANG
* @Date 2023/9/6 10:11
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 過(guò)期時(shí)間,單位毫秒
*/
long expireTime() default 500L;
}2、添加AOP切面
package com.xxx.aop;
import com.xxx.annotations.RepeatSubmit;
import com.xxx.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* 防止表單重復(fù)提交切面
*
* @Author WANGLINGQIANG
* @Date 2023/9/6 10:13
*/
@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {
private static final String KEY_PREFIX = "repeat_submit:";
@Resource
private RedisTemplate redisTemplate;
@Pointcut("@annotation(com.xxx.annotations.RepeatSubmit)")
public void repeatSubmit() {}
@Around("repeatSubmit()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//joinPoint獲取方法對(duì)象
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//獲取方法上的@RepeatSubmit注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
//獲取HttpServletRequest對(duì)象,以獲取請(qǐng)求uri
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String uri = request.getRequestURI();
//拼接Redis的key,這里只是簡(jiǎn)單根據(jù)uri來(lái)判斷是否重復(fù)提交??梢愿鶕?jù)自己業(yè)務(wù)調(diào)整,比如根據(jù)用戶id或者請(qǐng)求token等
String cacheKey = KEY_PREFIX.concat(uri);
Boolean flag = null;
try {
//借助setIfAbsent(),key不存在才能設(shè)值成功
flag = redisTemplate.opsForValue().setIfAbsent(cacheKey, "", annotation.expireTime(), TimeUnit.MILLISECONDS);
} catch (Exception e) {
//如果Redis不可用,則打印日志記錄,但依然對(duì)請(qǐng)求放行
log.error("", e);
return joinPoint.proceed();
}
//Redis可用的情況,如果flag=true說(shuō)明單位時(shí)間內(nèi)這是第一次請(qǐng)求,放行
if (flag) {
return joinPoint.proceed();
} else {
//進(jìn)入else說(shuō)明單位時(shí)間內(nèi)進(jìn)行了多次請(qǐng)求,則攔截請(qǐng)求并提示稍后重試
throw new ServiceException("系統(tǒng)繁忙,請(qǐng)稍后重試");
}
}
}這里利用redisTemplate的setIfAbsent()實(shí)現(xiàn)的,如果存在就不能set成功,set的同時(shí)設(shè)置過(guò)期時(shí)間,可以是用使用默認(rèn),也可以自己根據(jù)業(yè)務(wù)調(diào)整。
另外,cacheKey的定義,也可以根據(jù)自己的需要去調(diào)整,比如根據(jù)當(dāng)前登錄用戶的userId、當(dāng)前登錄的token等。
3、使用
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@RepeatSubmit
@PostMapping
public AjaxResult add(@Validated @RequestBody SysUser user) {
//....
}
場(chǎng)景二:控制接口調(diào)用頻率
背景
忘記密碼后通過(guò)發(fā)送手機(jī)驗(yàn)證碼找回密碼的場(chǎng)景。因?yàn)槊堪l(fā)一條短信都需要收費(fèi),所以要控制發(fā)短信的頻率。
比如,同一個(gè)手機(jī)號(hào)在3分鐘內(nèi)只能發(fā)送3次短信,超過(guò)3次后則提示用戶“短信發(fā)送過(guò)于頻繁,請(qǐng)10分鐘后再試”。
實(shí)現(xiàn)代碼
@Slf4j
@RestController
@RequestMapping("/sms")
public class SmsController {
@Resource
private ISmsService smsService;
@Resource
public RedisTemplate redisTemplate;
@PostMapping("/sendValidCode")
public Result sendValidCode(@RequestBody @Valid SmsDTO smsDTO) {
//驗(yàn)證手機(jī)號(hào)格式
checkPhoneNumber(smsDTO.getPhoneNumber());
//...其他驗(yàn)證
//拼接Redis的key(key為手機(jī)號(hào),以控制一個(gè)手機(jī)號(hào)有限時(shí)間內(nèi)容發(fā)送的次數(shù))
String cacheKey = "sms:code:resetPwd:"+smsDTO.getPhoneNumber();
//驗(yàn)證發(fā)送短信次數(shù),超過(guò)則攔截(閾值是3次,超時(shí)時(shí)間是3分鐘,重試時(shí)間是10分鐘)
checkSendCount(cacheKey, THRESHOLD, TIMEOUT, RETRY_TIME);
return smsService.sendMsg(smsDTO);
}
/**
* 驗(yàn)證發(fā)送短信次數(shù),超過(guò)則攔截
* 該方法用lua腳本替換實(shí)現(xiàn)更好
*/
private void checkSendCount(String cacheKey, Long threshold, Long timeout, String retryTime) {
//首先進(jìn)方法就先+1
Long count = redisTemplate.opsForValue().increment(cacheKey);
//然后比較次數(shù),是否超過(guò)閾值
if (count > threshold) {
//超過(guò)則設(shè)置過(guò)期時(shí)間為10分鐘,并提示10分鐘后重試
redisTemplate.expire(cacheKey, 10L, TimeUnit.MINUTES);
throw new ServiceException("短信發(fā)送過(guò)于頻繁,請(qǐng)" + retryTime + "分鐘后再試");
} else {
//沒(méi)超過(guò)3次,則累加上這一次
redisTemplate.expire(cacheKey, timeout, TimeUnit.MINUTES);
}
}
}總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
使用Jedis線程池returnResource異常注意事項(xiàng)
這篇文章主要介紹了使用Jedis線程池returnResource異常注意事項(xiàng),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
redis實(shí)現(xiàn)多級(jí)緩存同步方案詳解
這篇文章主要介紹了redis實(shí)現(xiàn)多級(jí)緩存同步方案詳解,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12
Redis+Caffeine如何構(gòu)建高性能二級(jí)緩存
這篇文章主要介紹了Redis+Caffeine如何構(gòu)建高性能二級(jí)緩存問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-05-05
詳解Redis單線程架構(gòu)的優(yōu)勢(shì)與不足
很多人都遇到過(guò)這么一道面試題:Redis是單線程還是多線程?這個(gè)問(wèn)題既簡(jiǎn)單又復(fù)雜,說(shuō)他簡(jiǎn)單是因?yàn)榇蠖鄶?shù)人都知道Redis是單線程,說(shuō)復(fù)雜是因?yàn)檫@個(gè)答案其實(shí)并不準(zhǔn)確,本文就給大家講講Redis單線程架構(gòu)的優(yōu)勢(shì)與不足,需要的朋友可以參考下2024-02-02
Redis Stream秒殺系統(tǒng)實(shí)現(xiàn)
文章瀏覽閱讀345次,點(diǎn)贊7次,收藏3次。高性能: Lua腳本原子操作,毫秒級(jí)響應(yīng)高并發(fā): 異步處理,支持10萬(wàn)+ QPS數(shù)據(jù)一致性: 庫(kù)存不會(huì)超賣可靠性: 消息隊(duì)列確保訂單不丟失用戶體驗(yàn): 立即返回結(jié)果,無(wú)需等待。2025-11-11

