1.什么是冪等
在我們編程中常見冪等?
1)select查詢天然冪等 ??
2)delete刪除也是冪等,刪除同一個多次效果一樣?
3)update直接更新某個值的,冪等?
4)update更新累加操作的,非冪等?
5)insert非冪等操作,每次新增一條
2.產(chǎn)生原因
由于重復(fù)點擊或者網(wǎng)絡(luò)重發(fā) ?eg: ??
1)點擊提交按鈕兩次;?
2)點擊刷新按鈕;?
3)使用瀏覽器后退按鈕重復(fù)之前的操作,導(dǎo)致重復(fù)提交表單;?
4)使用瀏覽器歷史記錄重復(fù)提交表單;?
5)瀏覽器重復(fù)的HTTP請;?
6)nginx重發(fā)等情況;?
7)分布式RPC的try重發(fā)等;
3.解決方案
在提交后執(zhí)行頁面重定向击困,這就是所謂的Post-Redirect-Get (PRG)模式涎劈。
簡言之,當(dāng)用戶提交了表單后阅茶,你去執(zhí)行一個客戶端的重定向蛛枚,轉(zhuǎn)到提交成功信息頁面。
這能避免用戶按F5導(dǎo)致的重復(fù)提交脸哀,而其也不會出現(xiàn)瀏覽器表單重復(fù)提交的警告蹦浦,也能消除按瀏覽器前進和后退按導(dǎo)致的同樣問題。
在服務(wù)器端撞蜂,生成一個唯一的標(biāo)識符盲镶,將它存入session,同時將它寫入表單的隱藏字段中蝌诡,然后將表單頁面發(fā)給瀏覽器溉贿,用戶錄入信息后點擊提交,在服務(wù)器端浦旱,獲取表單中隱藏字段的值顽照,與session中的唯一標(biāo)識符比較,相等說明是首次提交闽寡,就處理本次請求代兵,然后將session中的唯一標(biāo)識符移除;不相等說明是重復(fù)提交爷狈,就不再處理植影。
比較復(fù)雜? 不適合移動端APP的應(yīng)用 這里不詳解
insert使用唯一索引 update使用 樂觀鎖 version版本法
這種在大數(shù)據(jù)量和高并發(fā)下效率依賴數(shù)據(jù)庫硬件能力,可針對非核心業(yè)務(wù)
使用select ... for update? ,這種和 synchronized?
鎖住先查再insert or update一樣,但要避免死鎖,效率也較差?
針對單體 請求并發(fā)不大 可以推薦使用
原理:使用了 ConcurrentHashMap 并發(fā)容器 putIfAbsent 方法,和 ScheduledThreadPoolExecutor 定時任務(wù),也可以使用guava cache的機制, gauva中有配有緩存的有效時間 也是可以的key的生成 Content-MD5 Content-MD5 是指 Body 的 MD5 值,只有當(dāng) Body 非Form表單時才計算MD5涎永,計算方式直接將參數(shù)和參數(shù)名稱統(tǒng)一加密MD5思币。
MD5在一定范圍類認為是唯一的,近似唯一羡微,當(dāng)然在低并發(fā)的情況下足夠了 谷饿。
當(dāng)然本地鎖只適用于單機部署的應(yīng)用。
①配置注解
importjava.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public@interfaceResubmit {
/**
* 延時時間 在延時多久后可以再次提交
*
*@returnTime unit is one second
*/
intdelaySeconds()default20;
}
②實例化鎖
importcom.google.common.cache.Cache;
importcom.google.common.cache.CacheBuilder;
importlombok.extern.slf4j.Slf4j;
importorg.apache.commons.codec.digest.DigestUtils;
importjava.util.Objects;
importjava.util.concurrent.ConcurrentHashMap;
importjava.util.concurrent.ScheduledThreadPoolExecutor;
importjava.util.concurrent.ThreadPoolExecutor;
importjava.util.concurrent.TimeUnit;
/**
*@authorlijing
* 重復(fù)提交鎖
*/
@Slf4j
publicfinalclassResubmitLock{
privatestaticfinalConcurrentHashMapLOCK_CACHE =newConcurrentHashMap<>(200);
privatestaticfinalScheduledThreadPoolExecutor EXECUTOR =newScheduledThreadPoolExecutor(5,newThreadPoolExecutor.DiscardPolicy());
// private static final CacheCACHES = CacheBuilder.newBuilder()
// 最大緩存 100 個
// .maximumSize(1000)
// 設(shè)置寫緩存后 5 秒鐘過期
// .expireAfterWrite(5, TimeUnit.SECONDS)
// .build();
privateResubmitLock(){
}
/**
* 靜態(tài)內(nèi)部類 單例模式
*
*@return
*/
privatestaticclassSingletonInstance{
privatestaticfinalResubmitLock INSTANCE =newResubmitLock();
}
publicstaticResubmitLockgetInstance(){
returnSingletonInstance.INSTANCE;
}
publicstaticStringhandleKey(String param){
returnDigestUtils.md5Hex(param ==null?"": param);
}
/**
* 加鎖 putIfAbsent 是原子操作保證線程安全
*
*@paramkey 對應(yīng)的key
*@paramvalue
*@return
*/
publicbooleanlock(finalString key, Object value){
returnObjects.isNull(LOCK_CACHE.putIfAbsent(key, value));
}
/**
* 延時釋放鎖 用以控制短時間內(nèi)的重復(fù)提交
*
*@paramlock 是否需要解鎖
*@paramkey 對應(yīng)的key
*@paramdelaySeconds 延時時間
*/
publicvoidunLock(finalbooleanlock,finalString key,finalintdelaySeconds){
if(lock) {
EXECUTOR.schedule(() -> {
LOCK_CACHE.remove(key);
}, delaySeconds, TimeUnit.SECONDS);
}
}
}
③AOP 切面
importcom.alibaba.fastjson.JSONObject;
importcom.cn.xxx.common.annotation.Resubmit;
importcom.cn.xxx.common.annotation.impl.ResubmitLock;
importcom.cn.xxx.common.dto.RequestDTO;
importcom.cn.xxx.common.dto.ResponseDTO;
importcom.cn.xxx.common.enums.ResponseCode;
importlombok.extern.log4j.Log4j;
importorg.aspectj.lang.ProceedingJoinPoint;
importorg.aspectj.lang.annotation.Around;
importorg.aspectj.lang.annotation.Aspect;
importorg.aspectj.lang.reflect.MethodSignature;
importorg.springframework.stereotype.Component;
importjava.lang.reflect.Method;
/**
*@ClassNameRequestDataAspect
*@Description數(shù)據(jù)重復(fù)提交校驗
*@Authorlijing
*@Date2019/05/16 17:05
**/
@Log4j
@Aspect
@Component
publicclassResubmitDataAspect{
privatefinalstaticString DATA ="data";
privatefinalstaticObject PRESENT =newObject();
@Around("@annotation(com.cn.xxx.common.annotation.Resubmit)")
publicObjecthandleResubmit(ProceedingJoinPoint joinPoint)throwsThrowable{
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//獲取注解信息
Resubmit annotation = method.getAnnotation(Resubmit.class);
intdelaySeconds = annotation.delaySeconds();
Object[] pointArgs = joinPoint.getArgs();
String key ="";
//獲取第一個參數(shù)
Object firstParam = pointArgs[0];
if(firstParaminstanceofRequestDTO) {
//解析參數(shù)
JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());
JSONObject data = JSONObject.parseObject(requestDTO.getString(DATA));
if(data !=null) {
StringBuffer sb =newStringBuffer();
data.forEach((k, v) -> {
sb.append(v);
});
//生成加密參數(shù) 使用了content_MD5的加密方式
key = ResubmitLock.handleKey(sb.toString());
}
}
//執(zhí)行鎖
booleanlock =false;
try{
//設(shè)置解鎖key
lock = ResubmitLock.getInstance().lock(key, PRESENT);
if(lock) {
//放行
returnjoinPoint.proceed();
}else{
//響應(yīng)重復(fù)提交異常
returnnewResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);
}
}finally{
//設(shè)置解鎖key和解鎖時間
ResubmitLock.getInstance().unLock(lock, key, delaySeconds);
}
}
}
④注解使用案例
@ApiOperation(value ="保存我的帖子接口", notes ="保存我的帖子接口")
@PostMapping("/posts/save")
@Resubmit(delaySeconds =10)
public ResponseDTOsaveBbsPosts(@RequestBody@ValidatedRequestDTOrequestDto) {
returnbbsPostsBizService.saveBbsPosts(requestDto);
}
以上就是本地鎖的方式進行的冪等提交 ?使用了Content-MD5 進行加密 ? 只要參數(shù)不變,參數(shù)加密 密值不變,key存在就阻止提交妈倔。
當(dāng)然也可以使用 ?一些其他簽名校驗 ?在某一次提交時先 生成固定簽名 ?提交到后端 根據(jù)后端解析統(tǒng)一的簽名作為 每次提交的驗證token 去緩存中處理即可博投。
在 pom.xml 中添加上 starter-web、starter-aop盯蝴、starter-data-redis 的依賴即可
org.springframework.bootgroupId>
spring-boot-starter-webartifactId>
dependency>
org.springframework.bootgroupId>
spring-boot-starter-aopartifactId>
dependency>
org.springframework.bootgroupId>
spring-boot-starter-data-redisartifactId>
dependency>
dependencies>
屬性配置 在 application.properites 資源文件中添加 redis 相關(guān)的配置項:
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123456
主要實現(xiàn)方式: 熟悉 Redis 的朋友都知道它是線程安全的毅哗,我們利用它的特性可以很輕松的實現(xiàn)一個分布式鎖,如 opsForValue().setIfAbsent(key,value)它的作用就是如果緩存中沒有當(dāng)前 Key 則進行緩存同時返回 true 反之亦然捧挺;
當(dāng)緩存后給 key 在設(shè)置個過期時間虑绵,防止因為系統(tǒng)崩潰而導(dǎo)致鎖遲遲不釋放形成死鎖;那么我們是不是可以這樣認為當(dāng)返回 true 我們認為它獲取到鎖了闽烙,在鎖未釋放的時候我們進行異常的拋出…
packagecom.battcn.interceptor;
importcom.battcn.annotation.CacheLock;
importcom.battcn.utils.RedisLockHelper;
importorg.aspectj.lang.ProceedingJoinPoint;
importorg.aspectj.lang.annotation.Around;
importorg.aspectj.lang.annotation.Aspect;
importorg.aspectj.lang.reflect.MethodSignature;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.util.StringUtils;
importjava.lang.reflect.Method;
importjava.util.UUID;
/**
* redis 方案
*
*@authorLevin
*@since2018/6/12 0012
*/
@Aspect
@Configuration
publicclassLockMethodInterceptor{
@Autowired
publicLockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator){
this.redisLockHelper = redisLockHelper;
this.cacheKeyGenerator = cacheKeyGenerator;
}
privatefinalRedisLockHelper redisLockHelper;
privatefinalCacheKeyGenerator cacheKeyGenerator;
@Around("execution(public * *(..)) && @annotation(com.battcn.annotation.CacheLock)")
publicObjectinterceptor(ProceedingJoinPoint pjp){
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
CacheLock lock = method.getAnnotation(CacheLock.class);
if(StringUtils.isEmpty(lock.prefix())) {
thrownewRuntimeException("lock key don't null...");
}
finalString lockKey = cacheKeyGenerator.getLockKey(pjp);
String value = UUID.randomUUID().toString();
try{
// 假設(shè)上鎖成功翅睛,但是設(shè)置過期時間失效,以后拿到的都是 false
finalbooleansuccess = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());
if(!success) {
thrownewRuntimeException("重復(fù)提交");
}
try{
returnpjp.proceed();
}catch(Throwable throwable) {
thrownewRuntimeException("系統(tǒng)異常");
}
}finally{
// TODO 如果演示的話需要注釋該代碼;實際應(yīng)該放開
redisLockHelper.unlock(lockKey, value);
}
}
}
RedisLockHelper 通過封裝成 API 方式調(diào)用黑竞,靈活度更加高
packagecom.battcn.utils;
importorg.springframework.boot.autoconfigure.AutoConfigureAfter;
importorg.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.data.redis.connection.RedisStringCommands;
importorg.springframework.data.redis.core.RedisCallback;
importorg.springframework.data.redis.core.StringRedisTemplate;
importorg.springframework.data.redis.core.types.Expiration;
importorg.springframework.util.StringUtils;
importjava.util.concurrent.Executors;
importjava.util.concurrent.ScheduledExecutorService;
importjava.util.concurrent.TimeUnit;
importjava.util.regex.Pattern;
/**
* 需要定義成 Bean
*
*@authorLevin
*@since2018/6/15 0015
*/
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
publicclassRedisLockHelper{
privatestaticfinalString DELIMITER ="|";
/**
* 如果要求比較高可以通過注入的方式分配
*/
privatestaticfinalScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);
privatefinalStringRedisTemplate stringRedisTemplate;
publicRedisLockHelper(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 獲取鎖(存在死鎖風(fēng)險)
*
*@paramlockKey lockKey
*@paramvalue value
*@paramtime 超時時間
*@paramunit 過期單位
*@returntrue or false
*/
publicbooleantryLock(finalString lockKey,finalString value,finallongtime,finalTimeUnit unit){
returnstringRedisTemplate.execute((RedisCallback) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));
}
/**
* 獲取鎖
*
*@paramlockKey lockKey
*@paramuuid UUID
*@paramtimeout 超時時間
*@paramunit 過期單位
*@returntrue or false
*/
publicbooleanlock(String lockKey,finalString uuid,longtimeout,finalTimeUnit unit){
finallongmilliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();
booleansuccess = 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);
finalString[] oldValues = oldVal.split(Pattern.quote(DELIMITER));
if(Long.parseLong(oldValues[0]) +1<= System.currentTimeMillis()) {
returntrue;
}
}
returnsuccess;
}
/**
*@seeRedis Documentation: SET
*/
publicvoidunlock(String lockKey, String value){
unlock(lockKey, value,0, TimeUnit.MILLISECONDS);
}
/**
* 延遲unlock
*
*@paramlockKey key
*@paramuuid client(最好是唯一鍵的)
*@paramdelayTime 延遲時間
*@paramunit 時間單位
*/
publicvoidunlock(finalString lockKey,finalString uuid,longdelayTime, TimeUnit unit){
if(StringUtils.isEmpty(lockKey)) {
return;
}
if(delayTime <=0) {
doUnlock(lockKey, uuid);
}else{
EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);
}
}
/**
*@paramlockKey key
*@paramuuid client(最好是唯一鍵的)
*/
privatevoiddoUnlock(finalString lockKey,finalString uuid){
String val = stringRedisTemplate.opsForValue().get(lockKey);
finalString[] 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/
END
本文發(fā)于 微星公眾號「程序員的成長之路」捕发,回復(fù)「1024」你懂得,給個贊唄摊溶。
回復(fù) [ 256 ] Java 程序員成長規(guī)劃
回復(fù) [ 777 ] 接私活的七大平臺利器
回復(fù) [ 2048 ] 免費領(lǐng)取C/C++爬骤,Linux,Python莫换,Java霞玄,PHP,人工智能拉岁,單片機坷剧,樹莓派,等 5T 學(xué)習(xí)資料