redis 接口防重
技術點:redis/aop
說明:
簡易版本實現防止重復提交,適用范圍為所有接口適用,采用注解方式,在需要防重的接口上使用注解,可以設置防重時效。
場景:
在系統中,經常會有一些接口會莫名其妙的被調用兩次,可能在冪等接口中不會存在太大的問題,但是非冪等接口的處理就會導致出現臟數據,甚至影響系統的正確性。
選型參考:
在常見的防重處理分為多種,粗分為前端處理,后端處理
前端處理分為:
- 在按鈕觸發后便將按鈕置灰,設置為不可用,在接口調用成功后回調處理,將按鈕恢復
- 發送請求時,設置一個狀態,在接口請求時去獲取狀態,查看在保護期是否已經有調用,思路與第一條類似
后端處理分為:
- 版本號,在數據表中增加version字段,在我們需要進行防重的接口請求到達后端后,sql處理時增加版本號條件(切記:每次在修改操作后需要對版本號進行加1哦),如果不一致則不進行處理。這也是樂觀鎖實現的一種思路。
- redis,即本文所述方式
選型原因
- 在系統安全中,防重復提交也是比較重要的一個指標,也就是接口冪等性。所以我們在日常的系統開發中,一般使用的是簡化版的放重復。也就是僅僅通過前端來進行防重控制,但是這樣也是具有風險性的。如果在涉及比較重要的數據的時候,可能往往會有熱心同行來幫我們找bug,對于他們可以直接通過抓報文的方式拿到我們的交互信息,以此來進行各種騷操作(羊毛黨做派,當然了,如果要避免接口攻擊,我們還要設置ip請求限制,小黑屋,防DDOS等各種防御工作,此處只講防重咯)。所以我們在重要數據處理時在后端也是同樣需要進行防重處理的。
- 防重提交的方式非常非常多,如上提出了四種方式,也只是冰山一角了。針對于后端側防重,如上簡述了兩種方式。個人覺得在不同的時機可以進行不同的選擇。如果我們在項目初期,個人覺得使用版本號處理更為合適一點,這樣會降低對第三方工具的依賴,因為我們在每加入一個新東西的時候都是會增加系統的復雜性的。我們在考慮性能,安全,可靠的時候就會多出一個事項,有點給自己找事做的樣子。但是如果我們的系統已經較為平穩了,那么此時對數據庫進行增加字段雖然也不會太難,但是會改動一些代碼。驚喜總是從這些地方來的。使用redis我覺得就要合適一些了,我們只需要面向切面進行編程,一處編寫,處處可用。從代碼和擴展來講redis就更為合適了。
以上內容都是瞎BB的,其實我也是個菜鳥,歡迎各位大佬提建議或者意見,大家共同進步,共同完善,讓java圈充滿激情四射的愛。
代碼樣例
1
2
3
4
5
6
|
@PostMapping ( "/user/update" ) @ApiOperation (value = "修改用戶信息" , notes = "修改用戶信息" , tags = "user module" ) @AvoidReSubmit (expireTime = 1000 * 3 ) public void update( @RequestBody User user){ userMapper.updateById(user); } |
具體代碼實現
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 定義自定義注解,設置注解參數默認值 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; } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
// 定義切面進行處理 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; /** * 定義匹配規則,以便于后續攔截直接攔截submit方法,不用重復表達式 */ @Pointcut (value = "@annotation(top.withu.gaof.freehope.annotate.AvoidReSubmit)" ) public void submit() { } @Before ( "submit()&&@annotation(avoidReSubmit)" ) public void doBefore(JoinPoint joinPoint, AvoidReSubmit avoidReSubmit) { // 拼裝參數 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( "您已經提交了請求,請不要重復提交哦!" ); } valueOperations.set(key, 1 , expireTime, TimeUnit.MILLISECONDS); } @Around ( "submit()&&@annotation(avoidReSubmit)" ) public Object doAround(ProceedingJoinPoint proceedingJoinPoint, AvoidReSubmit avoidReSubmit) throws Throwable { System.out.println( "環繞通知:" ); 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實現,估計版本在網上非常多了。這里的一個思路還是mark一下,現在我這代碼只有我和上帝知道什么意思,我怕一個月以后就只有上帝知道了。
- 自定義注解,注解中申明有效時間
- 使用aop切面攔截自定義注解,獲取注解中有效時間參數,此處默認設置保護期為30 * 1000L,單位毫秒
- 在切面中獲取接口請求的參數,將參數拼接成string,然后進行md5,這樣操作是因為降低key長度,避免看起來過于惡心。但是這里有一個情況沒有進行測試,那就是key碰撞的問題。在大量數據操作下是否會產生相同key值。
- 使用md5加密后的key值到redis中查詢,如果存在記錄則表明已經有接口訪問且處于保護期,不可繼續提交。此處使用異常處理。如果不存在記錄,則表明此接口在保護期內沒有訪問過,則不操作。此處的場景在使用時可以根據自己需求而定。
- 此處在環繞通知和after通知均沒有操作,因為我們只是對于放重復提交處理,業務場景中不存在后處理的情況,故而沒有具體實現。
到此這篇關于Java結合redis實現接口防重復提交的文章就介紹到這了,更多相關redis 接口防重復提交內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://blog.csdn.net/sky_demo/article/details/102525226