為什么要實(shí)現(xiàn)這個(gè)功能呢,可能用戶在提交一份數(shù)據(jù)后,可能因?yàn)榫W(wǎng)絡(luò)的原因、處理數(shù)據(jù)的速度慢等原因?qū)е马?yè)面沒(méi)有及時(shí)將用戶剛提交數(shù)據(jù)的后臺(tái)處理結(jié)果展示給用戶,這時(shí)用戶可能會(huì)進(jìn)行如下操作:
- 1秒內(nèi)連續(xù)點(diǎn)擊提交按鈕,導(dǎo)致重復(fù)提交表單。
- 使用瀏覽器后退按鈕重復(fù)之前的操作,導(dǎo)致重復(fù)提交表單
數(shù)據(jù)庫(kù)中會(huì)存在大量重復(fù)的信息。
怎么實(shí)現(xiàn)呢:進(jìn)入正題??
0、依賴
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.21</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.6.3</version>
</dependency>
1、自定義接口
/**
* 自定義注解防止表單重復(fù)提交
* @author Yuan Haozhe
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 防重復(fù)操作過(guò)期時(shí)間,默認(rèn)1s
*/
long expireTime() default 1;
}
2、實(shí)現(xiàn)redis分布式鎖
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 該加鎖方法僅針對(duì)單實(shí)例 Redis 可實(shí)現(xiàn)分布式加鎖
* 對(duì)于 Redis 集群則無(wú)法使用
*
* 支持重復(fù),線程安全
*
* @param lockKey 加鎖鍵
* @param clientId 加鎖客戶端唯一標(biāo)識(shí)(采用UUID)
* @param seconds 鎖過(guò)期時(shí)間
* @return
*/
public boolean tryLock(String lockKey, String clientId, long seconds) {
//
return redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, seconds, TimeUnit.SECONDS);
}
}
對(duì)參數(shù)進(jìn)行介紹:
第一個(gè)為key,我們使用key來(lái)當(dāng)鎖,因?yàn)閗ey是唯一的。我們傳的是lockKey
第二個(gè)為value,通過(guò)給value賦值為clientId,我們就知道這把鎖是哪個(gè)請(qǐng)求加的了,在解鎖的時(shí)候就可以有依據(jù)。clientId可以使用UUID.randomUUID().toString()方法生成。
第三個(gè)為time,代表key的過(guò)期時(shí)間
第四個(gè)為time的單位
setIfAbsent 相當(dāng)于NX,設(shè)置過(guò)期時(shí)間相當(dāng)于EX
也可用Jedis實(shí)現(xiàn):
- 添加依賴:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
- 加鎖代碼
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
@Autowired
private Jedis jedis;
public boolean tryLock(String lockKey, String clientId, long seconds) {
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 與 tryLock 相對(duì)應(yīng),用作釋放鎖
*
* @param lockKey
* @param clientId
* @return
*/
public boolean releaseLock(String lockKey, String clientId) {
//這里使用Lua腳本的方式,盡量保證原子性。
return jedis.eval(RELEASE_LOCK_SCRIPT,Collections.singletonList(lockKey),Collections.singletonList(clientId)).equals(1L);
}
對(duì)參數(shù)進(jìn)行介紹:
第一個(gè)為key,我們使用key來(lái)當(dāng)鎖,因?yàn)閗ey是唯一的。我們傳的是lockKey
第二個(gè)為value,通過(guò)給value賦值為clientId,我們就知道這把鎖是哪個(gè)請(qǐng)求加的了,在解鎖的時(shí)候就可以有依據(jù)。clientId可以使用UUID.randomUUID().toString()方法生成。
第三個(gè)為nxxx,這個(gè)參數(shù)我們填的是NX,意思是SET IF NOT EXIST,即當(dāng)key不存在時(shí),我們進(jìn)行set操作;若key已經(jīng)存在,則不做任何操作;
第四個(gè)為expx,這個(gè)參數(shù)我們傳的是PX,意思是我們要給這個(gè)key加一個(gè)過(guò)期的設(shè)置,具體時(shí)間由第五個(gè)參數(shù)決定。
第五個(gè)為time,與第四個(gè)參數(shù)相呼應(yīng),代表key的過(guò)期時(shí)間。
可以看到,我們的加鎖就一行代碼,保證了原子性,總的來(lái)說(shuō),執(zhí)行上面的set()方法就只會(huì)導(dǎo)致兩種結(jié)果:1. 當(dāng)前沒(méi)有鎖(key不存在),那么就進(jìn)行加鎖操作,并對(duì)鎖設(shè)置個(gè)有效期,同時(shí)value表示加鎖的客戶端。2. 已有鎖存在,不做任何操作。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-480003.html
3、統(tǒng)一返回值ReturnT
public class ReturnT<T> implements Serializable {
public static final long serialVersionUID = 42L;
public static final int SUCCESS_CODE = 200;
public static final int FAIL_CODE = 500;
public static final ReturnT<String> SUCCESS = new ReturnT<String>(null);
public static final ReturnT<String> FAIL = new ReturnT<String>(FAIL_CODE, null);
private int code;
private String msg;
private T data;
public ReturnT(int code, String msg) {
this.code = code;
this.msg = msg;
}
public ReturnT(T data) {
this.code = SUCCESS_CODE;
this.data = data;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
4、CookieUtil
public class CookieUtil {
/**
* 查詢value
*
* @param request
* @param key
* @return
*/
public static String getValue(HttpServletRequest request, String key) {
Cookie cookie = get(request, key);
if (cookie != null) {
return cookie.getValue();
}
return null;
}
}
5、自定義AOP
@Component
@Aspect
@Slf4j
public class NoRepeatSubmitAspect {
@Pointcut("@annotation(com.xxl.sso.base.annotation.RepeatSubmit)")
public void repeatSubmit(){}
@Autowired
private HttpServletRequest request;
@Autowired
private RedisLock redisLock;
@Around("repeatSubmit()")
public ReturnT around(ProceedingJoinPoint joinPoint) {
log.info("校驗(yàn)重復(fù)提交");
//用戶的身份標(biāo)識(shí),這里用cookie只是方便做測(cè)試
String userId = CookieUtil.getValue(request, Conf.SSO_SESSIONID).split("_")[0];
String key = getKey(userId, request.getServletPath());
String clientId = getClientId();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 獲取防重復(fù)提交注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
boolean isSuccess = redisLock.tryLock(key, clientId, annotation.expireTime());
// 如果緩存中有這個(gè)url視為重復(fù)提交
if (isSuccess) {
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
log.error(e.getMessage());
}
//finall{
//redisLock.releaseLock(key, clientId)
//}
return ReturnT.SUCCESS;
} else {
log.error("重復(fù)提交");
return ReturnT.FAIL;
}
}
private String getKey(String token, String path) {
return token + path;
}
private String getClientId() {
return UUID.randomUUID().toString();
}
}
6、測(cè)試
//測(cè)試重復(fù)提交
@GetMapping("/test-get")
//添加RepeatSubmit注解,默認(rèn)1s,方便測(cè)試查看,寫3s
@RepeatSubmit(expireTime = 3)
public ReturnT repeatTest(){
return ReturnT.SUCCESS;
}
結(jié)果:文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-480003.html
到了這里,關(guān)于springboot實(shí)現(xiàn)后端防重復(fù)提交(AOP+redis分布式鎖)單機(jī)情況下的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!