8種冪等性解決重復(fù)提交的方案

在我們編程中常見(jiàn)冪等
1)select查詢天然冪等
2)delete刪除也是冪等,刪除同一個(gè)多次效果一樣
3)update直接更新某個(gè)值的,冪等
4)update更新累加操作的,非冪等
5)insert非冪等操作,每次新增一條
2.產(chǎn)生原因
由于重復(fù)點(diǎn)擊或者網(wǎng)絡(luò)重發(fā) eg:
1)點(diǎn)擊提交按鈕兩次;
2)點(diǎn)擊刷新按鈕;
3)使用瀏覽器后退按鈕重復(fù)之前的操作,導(dǎo)致重復(fù)提交表單;
4)使用瀏覽器歷史記錄重復(fù)提交表單;
5)瀏覽器重復(fù)的HTTP請(qǐng);
6)nginx重發(fā)等情況;
7)分布式RPC的try重發(fā)等;
3.解決方案

在提交后執(zhí)行頁(yè)面重定向,這就是所謂的Post-Redirect-Get (PRG)模式。
簡(jiǎn)言之,當(dāng)用戶提交了表單后,你去執(zhí)行一個(gè)客戶端的重定向,轉(zhuǎn)到提交成功信息頁(yè)面。
這能避免用戶按F5導(dǎo)致的重復(fù)提交,而其也不會(huì)出現(xiàn)瀏覽器表單重復(fù)提交的警告,也能消除按瀏覽器前進(jìn)和后退按導(dǎo)致的同樣問(wèn)題。

在服務(wù)器端,生成一個(gè)唯一的標(biāo)識(shí)符,將它存入session,同時(shí)將它寫(xiě)入表單的隱藏字段中,然后將表單頁(yè)面發(fā)給瀏覽器,用戶錄入信息后點(diǎn)擊提交,在服務(wù)器端,獲取表單中隱藏字段的值,與session中的唯一標(biāo)識(shí)符比較,相等說(shuō)明是首次提交,就處理本次請(qǐng)求,然后將session中的唯一標(biāo)識(shí)符移除;不相等說(shuō)明是重復(fù)提交,就不再處理。

比較復(fù)雜 不適合移動(dòng)端APP的應(yīng)用 這里不詳解

insert使用唯一索引 update使用 樂(lè)觀鎖 version版本法
這種在大數(shù)據(jù)量和高并發(fā)下效率依賴數(shù)據(jù)庫(kù)硬件能力,可針對(duì)非核心業(yè)務(wù)

使用select ... for update ,這種和 synchronized
鎖住先查再insert or update一樣,但要避免死鎖,效率也較差
針對(duì)單體 請(qǐng)求并發(fā)不大 可以推薦使用

原理:使用了 ConcurrentHashMap 并發(fā)容器 putIfAbsent 方法,和 ScheduledThreadPoolExecutor 定時(shí)任務(wù),也可以使用guava cache的機(jī)制, gauva中有配有緩存的有效時(shí)間 也是可以的key的生成 Content-MD5 Content-MD5 是指 Body 的 MD5 值,只有當(dāng) Body 非Form表單時(shí)才計(jì)算MD5,計(jì)算方式直接將參數(shù)和參數(shù)名稱統(tǒng)一加密MD5。
MD5在一定范圍類認(rèn)為是唯一的 近似唯一 當(dāng)然在低并發(fā)的情況下足夠了
當(dāng)然本地鎖只適用于單機(jī)部署的應(yīng)用.
①配置注解
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Resubmit {
/**
* 延時(shí)時(shí)間 在延時(shí)多久后可以再次提交
*
* @return Time unit is one second
*/
int delaySeconds() default 20;
}②實(shí)例化鎖
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author lijing
* 重復(fù)提交鎖
*/
@Slf4j
public final class ResubmitLock {
private static final ConcurrentHashMapLOCK_CACHE = new ConcurrentHashMap<>(200);
private static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5, new ThreadPoolExecutor.DiscardPolicy());
// private static final CacheCACHES = CacheBuilder.newBuilder()
// 最大緩存 100 個(gè)
// .maximumSize(1000)
// 設(shè)置寫(xiě)緩存后 5 秒鐘過(guò)期
// .expireAfterWrite(5, TimeUnit.SECONDS)
// .build();
private ResubmitLock() {
}
/**
* 靜態(tài)內(nèi)部類 單例模式
*
* @return
*/
private static class SingletonInstance {
private static final ResubmitLock INSTANCE = new ResubmitLock();
}
public static ResubmitLock getInstance() {
return SingletonInstance.INSTANCE;
}
public static String handleKey(String param) {
return DigestUtils.md5Hex(param == null ? "" : param);
}
/**
* 加鎖 putIfAbsent 是原子操作保證線程安全
*
* @param key 對(duì)應(yīng)的key
* @param value
* @return
*/
public boolean lock(final String key, Object value) {
return Objects.isNull(LOCK_CACHE.putIfAbsent(key, value));
}
/**
* 延時(shí)釋放鎖 用以控制短時(shí)間內(nèi)的重復(fù)提交
*
* @param lock 是否需要解鎖
* @param key 對(duì)應(yīng)的key
* @param delaySeconds 延時(shí)時(shí)間
*/
public void unLock(final boolean lock, final String key, final int delaySeconds) {
if (lock) {
EXECUTOR.schedule(() -> {
LOCK_CACHE.remove(key);
}, delaySeconds, TimeUnit.SECONDS);
}
}
} ③AOP 切面
import com.alibaba.fastjson.JSONObject;
import com.cn.xxx.common.annotation.Resubmit;
import com.cn.xxx.common.annotation.impl.ResubmitLock;
import com.cn.xxx.common.dto.RequestDTO;
import com.cn.xxx.common.dto.ResponseDTO;
import com.cn.xxx.common.enums.ResponseCode;
import lombok.extern.log4j.Log4j;
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.stereotype.Component;
import java.lang.reflect.Method;
/**
* @ClassName RequestDataAspect
* @Description 數(shù)據(jù)重復(fù)提交校驗(yàn)
* @Author lijing
* @Date 2019/05/16 17:05
**/
@Log4j
@Aspect
@Component
public class ResubmitDataAspect {
private final static String DATA = "data";
private final static Object PRESENT = new Object();
@Around("@annotation(com.cn.xxx.common.annotation.Resubmit)")
public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//獲取注解信息
Resubmit annotation = method.getAnnotation(Resubmit.class);
int delaySeconds = annotation.delaySeconds();
Object[] pointArgs = joinPoint.getArgs();
String key = "";
//獲取第一個(gè)參數(shù)
Object firstParam = pointArgs[0];
if (firstParam instanceof RequestDTO) {
//解析參數(shù)
JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());
JSONObject data = JSONObject.parseObject(requestDTO.getString(DATA));
if (data != null) {
StringBuffer sb = new StringBuffer();
data.forEach((k, v) -> {
sb.append(v);
});
//生成加密參數(shù) 使用了content_MD5的加密方式
key = ResubmitLock.handleKey(sb.toString());
}
}
//執(zhí)行鎖
boolean lock = false;
try {
//設(shè)置解鎖key
lock = ResubmitLock.getInstance().lock(key, PRESENT);
if (lock) {
//放行
return joinPoint.proceed();
} else {
//響應(yīng)重復(fù)提交異常
return new ResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);
}
} finally {
//設(shè)置解鎖key和解鎖時(shí)間
ResubmitLock.getInstance().unLock(lock, key, delaySeconds);
}
}
}④注解使用案例
@ApiOperation(value = "保存我的帖子接口", notes = "保存我的帖子接口")
@PostMapping("/posts/save")
@Resubmit(delaySeconds = 10)
public ResponseDTO saveBbsPosts(@RequestBody @Validated RequestDTOrequestDto) {
return bbsPostsBizService.saveBbsPosts(requestDto);
} 以上就是本地鎖的方式進(jìn)行的冪等提交 使用了Content-MD5 進(jìn)行加密 只要參數(shù)不變,參數(shù)加密 密值不變,key存在就阻止提交
當(dāng)然也可以使用 一些其他簽名校驗(yàn) 在某一次提交時(shí)先 生成固定簽名 提交到后端 根據(jù)后端解析統(tǒng)一的簽名作為 每次提交的驗(yàn)證token 去緩存中處理即可.

在 pom.xml 中添加上 starter-web、starter-aop、starter-data-redis 的依賴即可
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
dependencies>屬性配置 在 application.properites 資源文件中添加 redis 相關(guān)的配置項(xiàng)
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123456主要實(shí)現(xiàn)方式: 熟悉 Redis 的朋友都知道它是線程安全的,我們利用它的特性可以很輕松的實(shí)現(xiàn)一個(gè)分布式鎖,如 opsForValue().setIfAbsent(key,value)它的作用就是如果緩存中沒(méi)有當(dāng)前 Key 則進(jìn)行緩存同時(shí)返回 true 反之亦然;當(dāng)緩存后給 key 在設(shè)置個(gè)過(guò)期時(shí)間,防止因?yàn)橄到y(tǒng)崩潰而導(dǎo)致鎖遲遲不釋放形成死鎖;那么我們是不是可以這樣認(rèn)為當(dāng)返回 true 我們認(rèn)為它獲取到鎖了,在鎖未釋放的時(shí)候我們進(jìn)行異常的拋出…
package com.battcn.interceptor;
import com.battcn.annotation.CacheLock;
import com.battcn.utils.RedisLockHelper;
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.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* redis 方案
*
* @author Levin
* @since 2018/6/12 0012
*/
@Aspect
@Configuration
public class LockMethodInterceptor {
@Autowired
public LockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator) {
this.redisLockHelper = redisLockHelper;
this.cacheKeyGenerator = cacheKeyGenerator;
}
private final RedisLockHelper redisLockHelper;
private final CacheKeyGenerator cacheKeyGenerator;
@Around("execution(public * *(..)) && @annotation(com.battcn.annotation.CacheLock)")
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
CacheLock lock = method.getAnnotation(CacheLock.class);
if (StringUtils.isEmpty(lock.prefix())) {
throw new RuntimeException("lock key don't null...");
}
final String lockKey = cacheKeyGenerator.getLockKey(pjp);
String value = UUID.randomUUID().toString();
try {
// 假設(shè)上鎖成功,但是設(shè)置過(guò)期時(shí)間失效,以后拿到的都是 false
final boolean success = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());
if (!success) {
throw new RuntimeException("重復(fù)提交");
}
try {
return pjp.proceed();
} catch (Throwable throwable) {
throw new RuntimeException("系統(tǒng)異常");
}
} finally {
// TODO 如果演示的話需要注釋該代碼;實(shí)際應(yīng)該放開(kāi)
redisLockHelper.unlock(lockKey, value);
}
}
}RedisLockHelper 通過(guò)封裝成 API 方式調(diào)用,靈活度更加高
package com.battcn.utils;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.StringUtils;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
* 需要定義成 Bean
*
* @author Levin
* @since 2018/6/15 0015
*/
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisLockHelper {
private static final String DELIMITER = "|";
/**
* 如果要求比較高可以通過(guò)注入的方式分配
*/
private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);
private final StringRedisTemplate stringRedisTemplate;
public RedisLockHelper(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 獲取鎖(存在死鎖風(fēng)險(xiǎn))
*
* @param lockKey lockKey
* @param value value
* @param time 超時(shí)時(shí)間
* @param unit 過(guò)期單位
* @return true or false
*/
public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit) {
return stringRedisTemplate.execute((RedisCallback) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));
}
/**
* 獲取鎖
*
* @param lockKey lockKey
* @param uuid UUID
* @param timeout 超時(shí)時(shí)間
* @param unit 過(guò)期單位
* @return true or false
*/
public boolean lock(String lockKey, final String uuid, long timeout, final TimeUnit unit) {
final long milliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();
boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
if (success) {
stringRedisTemplate.expire(lockKey, timeout, TimeUnit.SECONDS);
} else {
String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));
if (Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()) {
return true;
}
}
return success;
}
/**
* @see Redis Documentation: SET
*/
public void unlock(String lockKey, String value) {
unlock(lockKey, value, 0, TimeUnit.MILLISECONDS);
}
/**
* 延遲unlock
*
* @param lockKey key
* @param uuid client(最好是唯一鍵的)
* @param delayTime 延遲時(shí)間
* @param unit 時(shí)間單位
*/
public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {
if (StringUtils.isEmpty(lockKey)) {
return;
}
if (delayTime <= 0) {
doUnlock(lockKey, uuid);
} else {
EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);
}
}
/**
* @param lockKey key
* @param uuid client(最好是唯一鍵的)
*/
private void doUnlock(final String lockKey, final String uuid) {
String val = stringRedisTemplate.opsForValue().get(lockKey);
final String[] values = val.split(Pattern.quote(DELIMITER));
if (values.length <= 0) {
return;
}
if (uuid.equals(values[1])) {
stringRedisTemplate.delete(lockKey);
}
}
} redis的提交參照博客:
https://blog.battcn.com/2018/06/13/springboot/v2-cache-redislock/
推薦閱讀:
【重磅分享】從零到一搭建推薦系統(tǒng)指南白皮書(shū).pdf(附48頁(yè)下載鏈接)
億級(jí)(無(wú)限級(jí))并發(fā),沒(méi)那么難
世界的真實(shí)格局分析,地球人類社會(huì)底層運(yùn)行原理
數(shù)據(jù)中臺(tái):基于標(biāo)簽體系的360°用戶畫(huà)像
不是你需要中臺(tái),而是一名合格的架構(gòu)師(附各大廠中臺(tái)建設(shè)PPT)
