使用Redis控制表單重復(fù)提交和控制接口訪問頻率方式
場景一:控制表單重復(fù)提交
防重提交有很多方案,從前端的按鈕置灰,到后端synchronize鎖、Lock鎖、借助Redis語法實(shí)現(xiàn)簡單鎖、Redis+Lua分布式鎖、Redisson分布式鎖,再到DB的悲觀鎖、樂觀鎖、借助表唯一索引等等都可以實(shí)現(xiàn)防重提交,以保證數(shù)據(jù)的安全性。
這篇文章我們介紹其中一種方案–借助Redis語法實(shí)現(xiàn)簡單鎖,最終實(shí)現(xiàn)防重提交。
背景
我們項(xiàng)目中,為了控制表單重復(fù)提交問題,會在點(diǎn)擊頁面按鈕(向后端發(fā)起業(yè)務(wù)請求)后就會置灰按鈕,直到后端響應(yīng)后解除按鈕置灰。通過按鈕置灰來防止重啟提交問題。但Postman、Jmeter和其他服務(wù)調(diào)用(繞過前端頁面)呢?所以后端接口也要根據(jù)控制表單重復(fù)提交的問題。
后端代碼可以在2個位置做控制:
一是放在gateway網(wǎng)關(guān)做:
- 好處是只在一個地方加上控制代碼,就可以控制所有接口的重復(fù)提交問題。
- 壞處是控制的范圍太廣(比如查詢接口無需控制,控制了反而多余)、定義重復(fù)提交的時(shí)間段不能靈活調(diào)整。
二是放在AOP切面做:
- 好處是只有需要的地方才會被控制(哪里需要引用一下自定義注解即可),另外也能靈活調(diào)整定義重復(fù)提交的時(shí)間段(自定義注解里定義時(shí)間字段開放給使用者填寫)。
- 壞處是每個需要控制的地方都要加注解,會有侵入性和一定的工作量。
實(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 { /** * 過期時(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獲取方法對象 Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); //獲取方法上的@RepeatSubmit注解 RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); //獲取HttpServletRequest對象,以獲取請求uri ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); String uri = request.getRequestURI(); //拼接Redis的key,這里只是簡單根據(jù)uri來判斷是否重復(fù)提交??梢愿鶕?jù)自己業(yè)務(wù)調(diào)整,比如根據(jù)用戶id或者請求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不可用,則打印日志記錄,但依然對請求放行 log.error("", e); return joinPoint.proceed(); } //Redis可用的情況,如果flag=true說明單位時(shí)間內(nèi)這是第一次請求,放行 if (flag) { return joinPoint.proceed(); } else { //進(jìn)入else說明單位時(shí)間內(nèi)進(jìn)行了多次請求,則攔截請求并提示稍后重試 throw new ServiceException("系統(tǒng)繁忙,請稍后重試"); } } }
這里利用redisTemplate的setIfAbsent()實(shí)現(xiàn)的,如果存在就不能set成功,set的同時(shí)設(shè)置過期時(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) { //.... }
場景二:控制接口調(diào)用頻率
背景
忘記密碼后通過發(fā)送手機(jī)驗(yàn)證碼找回密碼的場景。因?yàn)槊堪l(fā)一條短信都需要收費(fèi),所以要控制發(fā)短信的頻率。
比如,同一個手機(jī)號在3分鐘內(nèi)只能發(fā)送3次短信,超過3次后則提示用戶“短信發(fā)送過于頻繁,請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ī)號格式 checkPhoneNumber(smsDTO.getPhoneNumber()); //...其他驗(yàn)證 //拼接Redis的key(key為手機(jī)號,以控制一個手機(jī)號有限時(shí)間內(nèi)容發(fā)送的次數(shù)) String cacheKey = "sms:code:resetPwd:"+smsDTO.getPhoneNumber(); //驗(yàn)證發(fā)送短信次數(shù),超過則攔截(閾值是3次,超時(shí)時(shí)間是3分鐘,重試時(shí)間是10分鐘) checkSendCount(cacheKey, THRESHOLD, TIMEOUT, RETRY_TIME); return smsService.sendMsg(smsDTO); } /** * 驗(yàn)證發(fā)送短信次數(shù),超過則攔截 * 該方法用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ù),是否超過閾值 if (count > threshold) { //超過則設(shè)置過期時(shí)間為10分鐘,并提示10分鐘后重試 redisTemplate.expire(cacheKey, 10L, TimeUnit.MINUTES); throw new ServiceException("短信發(fā)送過于頻繁,請" + retryTime + "分鐘后再試"); } else { //沒超過3次,則累加上這一次 redisTemplate.expire(cacheKey, timeout, TimeUnit.MINUTES); } } }
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis中有序集合的內(nèi)部實(shí)現(xiàn)方式的詳細(xì)介紹
本文主要介紹了Redis中有序集合的內(nèi)部實(shí)現(xiàn)方式的詳細(xì)介紹,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03redis 限制內(nèi)存使用大小的實(shí)現(xiàn)
這篇文章主要介紹了redis 限制內(nèi)存使用大小的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05Redis 數(shù)據(jù)庫忘記密碼找回或重置的解決方法
對于 Redis 數(shù)據(jù)庫,如果忘記了密碼,可以通過密碼重置來找回密碼,今天通過本文給大家分享Redis 數(shù)據(jù)庫忘記密碼找回或重置的解決方法,感興趣的朋友一起看看吧2024-01-01淺談redis的過期時(shí)間設(shè)置和過期刪除機(jī)制
本文主要介紹了redis的過期時(shí)間設(shè)置和過期刪除機(jī)制,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03Redis瞬時(shí)高并發(fā)秒殺方案總結(jié)
本文講述了Redis瞬時(shí)高并發(fā)秒殺方案總結(jié),具有很好的參考價(jià)值,感興趣的小伙伴們可以參考一下,具體如下:2018-05-05