Java結合redis實現(xiàn)接口防重復提交
redis 接口防重
技術點:redis/aop
說明:
簡易版本實現(xiàn)防止重復提交,適用范圍為所有接口適用,采用注解方式,在需要防重的接口上使用注解,可以設置防重時效。
場景:
在系統(tǒng)中,經(jīng)常會有一些接口會莫名其妙的被調(diào)用兩次,可能在冪等接口中不會存在太大的問題,但是非冪等接口的處理就會導致出現(xiàn)臟數(shù)據(jù),甚至影響系統(tǒng)的正確性。
選型參考:
在常見的防重處理分為多種,粗分為前端處理,后端處理
前端處理分為:
- 在按鈕觸發(fā)后便將按鈕置灰,設置為不可用,在接口調(diào)用成功后回調(diào)處理,將按鈕恢復
- 發(fā)送請求時,設置一個狀態(tài),在接口請求時去獲取狀態(tài),查看在保護期是否已經(jīng)有調(diào)用,思路與第一條類似
后端處理分為:
- 版本號,在數(shù)據(jù)表中增加version字段,在我們需要進行防重的接口請求到達后端后,sql處理時增加版本號條件(切記:每次在修改操作后需要對版本號進行加1哦),如果不一致則不進行處理。這也是樂觀鎖實現(xiàn)的一種思路。
- redis,即本文所述方式
選型原因
- 在系統(tǒng)安全中,防重復提交也是比較重要的一個指標,也就是接口冪等性。所以我們在日常的系統(tǒng)開發(fā)中,一般使用的是簡化版的放重復。也就是僅僅通過前端來進行防重控制,但是這樣也是具有風險性的。如果在涉及比較重要的數(shù)據(jù)的時候,可能往往會有熱心同行來幫我們找bug,對于他們可以直接通過抓報文的方式拿到我們的交互信息,以此來進行各種騷操作(羊毛黨做派,當然了,如果要避免接口攻擊,我們還要設置ip請求限制,小黑屋,防DDOS等各種防御工作,此處只講防重咯)。所以我們在重要數(shù)據(jù)處理時在后端也是同樣需要進行防重處理的。
- 防重提交的方式非常非常多,如上提出了四種方式,也只是冰山一角了。針對于后端側防重,如上簡述了兩種方式。個人覺得在不同的時機可以進行不同的選擇。如果我們在項目初期,個人覺得使用版本號處理更為合適一點,這樣會降低對第三方工具的依賴,因為我們在每加入一個新東西的時候都是會增加系統(tǒng)的復雜性的。我們在考慮性能,安全,可靠的時候就會多出一個事項,有點給自己找事做的樣子。但是如果我們的系統(tǒng)已經(jīng)較為平穩(wěn)了,那么此時對數(shù)據(jù)庫進行增加字段雖然也不會太難,但是會改動一些代碼。驚喜總是從這些地方來的。使用redis我覺得就要合適一些了,我們只需要面向切面進行編程,一處編寫,處處可用。從代碼和擴展來講redis就更為合適了。
以上內(nèi)容都是瞎BB的,其實我也是個菜鳥,歡迎各位大佬提建議或者意見,大家共同進步,共同完善,讓java圈充滿激情四射的愛。
代碼樣例
@PostMapping("/user/update") @ApiOperation(value = "修改用戶信息", notes = "修改用戶信息", tags = "user module") @AvoidReSubmit(expireTime = 1000 * 3) public void update(@RequestBody User user){ userMapper.updateById(user); }
具體代碼實現(xiàn)
// 定義自定義注解,設置注解參數(shù)默認值 package top.withu.gaof.freehope.annotate; import java.lang.annotation.*; /** * @author Gaofan * @date 2019年10月12日 下午2:54:45 * @describe 防止重復提交 */ @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AvoidReSubmit { /** * 失效時間,即可以第二次提交間隔時長 * @return */ long expireTime() default 30 * 1000L; }
// 定義切面進行處理 package top.withu.gaof.freehope.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import top.withu.gaof.freehope.annotate.AvoidReSubmit; import javax.annotation.Resource; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.concurrent.TimeUnit; /** * @Description: TODO * @Author: gaofan * @Date: 2019/10/12 16:10 * @Copyright: 2019 www.blog.freehope.top Inc. All rights reserved. **/ @Aspect @Component public class AvoidResumitAspect { @Resource private RedisTemplate redisTemplate; /** * 定義匹配規(guī)則,以便于后續(xù)攔截直接攔截submit方法,不用重復表達式 */ @Pointcut(value = "@annotation(top.withu.gaof.freehope.annotate.AvoidReSubmit)") public void submit() { } @Before("submit()&&@annotation(avoidReSubmit)") public void doBefore(JoinPoint joinPoint, AvoidReSubmit avoidReSubmit) { // 拼裝參數(shù) StringBuffer sb = new StringBuffer(); for(Object object : joinPoint.getArgs()){ sb.append(object); } String key = md5(sb.toString()); long expireTime = avoidReSubmit.expireTime(); ValueOperations valueOperations = redisTemplate.opsForValue(); Object object = valueOperations.get(key); if(null != object){ throw new RuntimeException("您已經(jīng)提交了請求,請不要重復提交哦!"); } valueOperations.set(key, 1, expireTime, TimeUnit.MILLISECONDS); } @Around("submit()&&@annotation(avoidReSubmit)") public Object doAround(ProceedingJoinPoint proceedingJoinPoint, AvoidReSubmit avoidReSubmit) throws Throwable { System.out.println("環(huán)繞通知:"); Object result = null; result = proceedingJoinPoint.proceed(); return result; } @After("submit()") public void doAfter() { System.out.println("******攔截后的邏輯******"); } private String md5(String str){ if (str == null || str.length() == 0) { throw new IllegalArgumentException("String to encript cannot be null or zero length"); } StringBuffer hexString = new StringBuffer(); try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(str.getBytes()); byte[] hash = md.digest(); for (int i = 0; i < hash.length; i++) { if ((0xff & hash[i]) < 0x10) { hexString.append("0" + Integer.toHexString((0xFF & hash[i]))); } else { hexString.append(Integer.toHexString(0xFF & hash[i])); } } } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return hexString.toString(); } }
思路:
簡單的通過redis實現(xiàn),估計版本在網(wǎng)上非常多了。這里的一個思路還是mark一下,現(xiàn)在我這代碼只有我和上帝知道什么意思,我怕一個月以后就只有上帝知道了。
- 自定義注解,注解中申明有效時間
- 使用aop切面攔截自定義注解,獲取注解中有效時間參數(shù),此處默認設置保護期為30 * 1000L,單位毫秒
- 在切面中獲取接口請求的參數(shù),將參數(shù)拼接成string,然后進行md5,這樣操作是因為降低key長度,避免看起來過于惡心。但是這里有一個情況沒有進行測試,那就是key碰撞的問題。在大量數(shù)據(jù)操作下是否會產(chǎn)生相同key值。
- 使用md5加密后的key值到redis中查詢,如果存在記錄則表明已經(jīng)有接口訪問且處于保護期,不可繼續(xù)提交。此處使用異常處理。如果不存在記錄,則表明此接口在保護期內(nèi)沒有訪問過,則不操作。此處的場景在使用時可以根據(jù)自己需求而定。
- 此處在環(huán)繞通知和after通知均沒有操作,因為我們只是對于放重復提交處理,業(yè)務場景中不存在后處理的情況,故而沒有具體實現(xiàn)。
到此這篇關于Java結合redis實現(xiàn)接口防重復提交的文章就介紹到這了,更多相關redis 接口防重復提交內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Mybatis中SqlSession下的四大對象之執(zhí)行器(executor)
mybatis中sqlsession下的四大對象是指:executor, statementHandler,parameterHandler,resultHandler對象。這篇文章主要介紹了Mybatis中SqlSession下的四大對象之執(zhí)行器(executor),需要的朋友可以參考下2019-04-04Spring Security實現(xiàn)驗證碼登錄功能
這篇文章主要介紹了Spring Security實現(xiàn)驗證碼登錄功能,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-01-01