暴力小熊
已于 2022-07-01 16:13:00 修改
3624
收藏 25
分類專欄: redis java 數(shù)據(jù)庫 文章標簽: java 前端 面試
版權(quán)
redis
同時被 3 個專欄收錄
4 篇文章1 訂閱
訂閱專欄
java
25 篇文章2 訂閱
訂閱專欄
數(shù)據(jù)庫
8 篇文章0 訂閱
訂閱專欄
開場白:老鐵們對于文章有錯誤、不準確,或需要補充的請留言討論 ,大家共同學習。如果覺得還不錯的請關(guān)注、留言、點贊 、收藏。 創(chuàng)作不易,且看且珍惜
一、產(chǎn)生原因
對于重復提交的問題,主要由于重復點擊或者網(wǎng)絡(luò)重發(fā)請求, 我要先了解產(chǎn)生原因幾種方式:
點擊提交按鈕兩次;
點擊刷新按鈕;
使用瀏覽器后退按鈕重復之前的操作,導致重復提交表單;
使用瀏覽器歷史記錄重復提交表單;
瀏覽器重復的HTTP請;
nginx重發(fā)等情況;
分布式RPC的try重發(fā)等點擊提交按鈕兩次;
等… …
二、冪等
對于重復提交的問題 主要涉及到時 冪等 問題,那么先說一下什么是冪等。
冪等:F(F(X)) = F(X)多次運算結(jié)果一致;簡單點說就是對于完全相同的操作,操作一次與操作多次的結(jié)果是一樣的。
在開發(fā)中,我們都會涉及到對數(shù)據(jù)庫操作。例如:
select 查詢天然冪等
delete 刪除也是冪等,刪除同一個多次效果一樣
update 直接更新某個值(如:狀態(tài) 字段固定值),冪等
update 更新累加操作(如:商品數(shù)量 字段),非冪等
(可以采用簡單的樂觀鎖和悲觀鎖 個人更喜歡樂觀鎖。
樂觀鎖:數(shù)據(jù)庫表加version字段的方式;
悲觀鎖:用了 select…for update 的方式,* 要使用悲觀鎖,我們必須關(guān)閉mysql數(shù)據(jù)庫的自動提交屬性。
這種在大數(shù)據(jù)量和高并發(fā)下效率依賴數(shù)據(jù)庫硬件能力,可針對并發(fā)量不高的非核心業(yè)務(wù);)
insert 非冪等操作,每次新增一條 重點 (數(shù)據(jù)庫簡單方案:可采取數(shù)據(jù)庫唯一索引方式;這種在大數(shù)據(jù)量和高并發(fā)下效率依賴數(shù)據(jù)庫硬件能力,可針對并發(fā)量不高的非核心業(yè)務(wù);)
三、解決方案
1. 方案對比
序號 前端/后端 方案 優(yōu)點 缺點 代碼實現(xiàn)
1) 前端 前端js提交后禁止按鈕,返回結(jié)果后解禁等 簡單 方便 只能控制頁面,通過工具可繞過不安全 略
2) 后端 提交后重定向到其他頁面,防止用戶F5和瀏覽器前進后退等重復提交問題 簡單 方便 體驗不好,適用部分場景,若是遇到網(wǎng)絡(luò)問題 還會出現(xiàn) 略
3) 后端 在表單、session、token 放入唯一標識符(如:UUID),每次操作時,保存標識一定時間后移除,保存期間有相同的標識就不處理或提示 相對簡單 表單:有時需要前后端協(xié)商配合; session、token:加大服務(wù)性能開銷 略
4) 后端 ConcurrentHashMap 、LRUMap 、google Cache 都是采用唯一標識(如:用戶ID+請求路徑+參數(shù)) 相對簡單 適用于單機部署的應(yīng)用 見下
5) 后端 redis 是線程安全的,可以實現(xiàn)redis分布式鎖。設(shè)置唯一標識(如:用戶ID+請求路徑+參數(shù))當做key ,value值可以隨意(推薦設(shè)置成過期的時間點),在設(shè)置key的過期時間 單機、分布式、高并發(fā)都可以決絕 相對復雜需要部署維護redis 見下
2. 代碼實現(xiàn)
4). google cache 代碼實現(xiàn) 注解方式 Single lock
pom.xml 引入
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
1
2
3
4
5
配置文件 .yml
resubmit:
local:
timeOut: 30
1
2
3
實現(xiàn)代碼
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LocalLock {
}
1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.JSONObject;
import com.example.mydemo.common.utils.IpUtils;
import com.example.mydemo.common.utils.Result;
import com.example.mydemo.common.utils.SecurityUtils;
import com.example.mydemo.common.utils.sign.MyMD5Util;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author: xx
* @description: 單機放重復提交
*/
@Data
@Aspect
@Configuration
public class LocalLockMethodInterceptor {
@Value("${spring.profiles.active}")
private String springProfilesActive;
@Value("${spring.application.name}")
private String springApplicationName;
private static int expireTimeSecond =5;
@Value("${resubmit:local:timeOut}")
public void setExpireTimeSecond(int expireTimeSecond) {
LocalLockMethodInterceptor.expireTimeSecond = expireTimeSecond;
}
//定義緩存,設(shè)置最大緩存數(shù)及過期日期
private static final Cache<String,Object> CACHE =
CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(expireTimeSecond, TimeUnit.SECONDS).build();
@Around("execution(public * *(..)) && @annotation(com.example.mydemo.common.interceptor.annotation.LocalLock)")
public Object interceptor(ProceedingJoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// LocalLock localLock = method.getAnnotation(LocalLock.class);
try{
String key = getLockUniqueKey(signature,joinPoint.getArgs());
if(CACHE.getIfPresent(key) != null){
return Result.fail("不允許重復提交,請稍后再試");
}
CACHE.put(key,key);
return joinPoint.proceed();
}catch (Throwable throwable){
throw new RuntimeException(throwable.getMessage());
}finally {
}
}
/**
* 獲取唯一標識key
*
* @param methodSignature
* @param args
* @return
*/
private String getLockUniqueKey(MethodSignature methodSignature, Object[] args) throws NoSuchAlgorithmException {
//請求uri, 獲取類名稱,方法名稱
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
// HttpServletResponse responese = servletRequestAttributes.getResponse();
//獲取用戶信息
String userMsg = SecurityUtils.getUsername(); //獲取登錄用戶名稱
//1.判斷用戶是否登錄
if (StringUtils.isEmpty(userMsg)) { //未登錄用戶獲取真實ip
userMsg = IpUtils.getIpAddr(request);
}
String hash = "";
List list = new ArrayList();
if (args.length > 0) {
String[] parameterNames = methodSignature.getParameterNames();
for (int i = 0; i < parameterNames.length; i++) {
Object obj = args[i];
list.add(obj);
}
hash = JSONObject.toJSONString(list);
}
//項目名稱 + 環(huán)境編碼 + 獲取類名稱 + 方法名稱 + 唯一key
String key = "locallock:" + springApplicationName + ":" + springProfilesActive + ":" + userMsg + ":" + request.getRequestURI();
if (StringUtils.isNotEmpty(key)) {
key = key + ":" + hash;
}
key = MyMD5Util.getMD5(key);
return key;
}
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
使用:
@LocalLock
public void save(@RequestBody User user) {
}
1
2
3
4
5)redis
pom.xml 引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1
2
3
4
.yml文件 redis 配置
spring:
redis:
host: localhost
port: :6379
password: 123456
1
2
3
4
5
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisLock {
int expire() default 5;
}
1
2
3
4
5
6
7
8
9
10
11
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.heshu.sz.blockchain.utonhsbs.common.utils.MyMD5Util;
import com.heshu.sz.blockchain.utonhsbs.common.utils.SecurityUtils;
import com.heshu.sz.blockchain.utonhsbs.common.utils.ip.IpUtils;
import com.heshu.sz.blockchain.utonhsbs.framework.interceptor.annotation.RedisLock;
import com.heshu.sz.blockchain.utonhsbs.framework.system.domain.BaseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
/**
* @author :xx
* @description:
* @date : 2022/7/1 9:41
*/
@Slf4j
@Aspect
@Configuration
public class RedisLockMethodInterceptor {
@Value("${spring.profiles.active}")
private String springProfilesActive;
@Value("${spring.application.name}")
private String springApplicationName;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Pointcut("@annotation(com.heshu.sz.blockchain.utonhsbs.framework.interceptor.annotation.RedisLock)")
public void point() {
}
@Around("point()")
public Object doaround(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedisLock localLock = method.getAnnotation(RedisLock.class);
try {
String lockUniqueKey = getLockUniqueKey(signature, joinPoint.getArgs());
Integer expire = localLock.expire();
if (expire < 0) {
expire = 5;
}
ArrayList<String> keys = Lists.newArrayList(lockUniqueKey);
String result = stringRedisTemplate.execute(setNxWithExpireTime, keys, expire.toString());
if (!"ok".equalsIgnoreCase(result)) {//不存在
return BaseResult.error("不允許重復提交,請稍后再試");
}
return joinPoint.proceed();
} catch (Throwable throwable) {
throw new RuntimeException(throwable.getMessage());
}
}
/**
* lua腳本
*/
private RedisScript<String> setNxWithExpireTime = new DefaultRedisScript<>(
"return redis.call('set', KEYS[1], 1, 'ex', ARGV[1], 'nx');",
String.class
);
/**
* 獲取唯一標識key
*
* @param methodSignature
* @param args
* @return
*/
private String getLockUniqueKey(MethodSignature methodSignature, Object[] args) throws NoSuchAlgorithmException {
//請求uri, 獲取類名稱,方法名稱
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
// HttpServletResponse responese = servletRequestAttributes.getResponse();
//獲取用戶信息
String userMsg = SecurityUtils.getUsername(); //獲取登錄用戶名稱
//1.判斷用戶是否登錄
if (StringUtils.isEmpty(userMsg)) { //未登錄用戶獲取真實ip
userMsg = IpUtils.getIpAddr(request);
}
String hash = "";
List list = new ArrayList();
if (args.length > 0) {
String[] parameterNames = methodSignature.getParameterNames();
for (int i = 0; i < parameterNames.length; i++) {
Object obj = args[i];
list.add(obj);
}
String param = JSONObject.toJSONString(list);
hash = MyMD5Util.getMD5(param);
}
//項目名稱 + 環(huán)境編碼 + 獲取類名稱 + 加密參數(shù)
String key = "lock:" + springApplicationName + ":" + springProfilesActive + ":" + userMsg + ":" + request.getRequestURI();
if (StringUtils.isNotEmpty(key)) {
key = key + ":" + hash;
}
return key;
}
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
使用
@RedisLock
public void save(@RequestBody User user) {
}
1
2
3
4
文章知識
————————————————
版權(quán)聲明:本文為CSDN博主「暴力小熊」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。