分布式锁防止订单重复提交_防止表单重复提交看这里!!!
???????? 要解決重復(fù)提交這事,先要知道什么是重復(fù)提交
假如用戶的網(wǎng)速慢,用戶點(diǎn)擊提交按鈕,卻因?yàn)榫W(wǎng)速慢,而沒有跳轉(zhuǎn)到新的頁面,這時的用戶會再次點(diǎn)擊提交按鈕,舉個例子:用戶點(diǎn)擊訂單頁面,當(dāng)點(diǎn)擊提交按鈕的時候,也許因?yàn)榫W(wǎng)速的原因,沒有跳轉(zhuǎn)到新的頁面,這時的用戶會再次點(diǎn)擊提交按鈕,如果沒有經(jīng)過處理的話,這時用戶就會生成兩份訂單,類似于這種場景都叫重復(fù)提交。
好了,為了解決這個問題,可以馬上想到通過前端將提交按鈕點(diǎn)擊后設(shè)置倒計(jì)時30秒不能再點(diǎn)擊就行啊,這些方案是前端的,我們主要說后端方案,看下面的圖。防止重復(fù)提交后端就是對提交內(nèi)容加鎖即可,判斷如果沖突提交就舍棄提交內(nèi)容,如果沒重復(fù)就執(zhí)行后續(xù)業(yè)務(wù)方法。別一提到鎖就看不懂,這里的鎖你可以理解成唯一值
秉承一貫作風(fēng),不講故事就是干,通過代碼理解,先搭建SpringBoot項(xiàng)目
pom坐標(biāo)
<parent> ? ? ? ?<groupId>org.springframework.bootgroupId> ? ? ? ?<artifactId>spring-boot-starter-parentartifactId> ? ? ? ?<version>2.1.6.RELEASEversion> ? ?parent> ? ?<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>com.google.guavagroupId> ? ? ? ? ? ?<artifactId>guavaartifactId> ? ? ? ? ? ?<version>21.0version> ? ? ? ?dependency> ? ?dependencies>入口類
@SpringBootApplicationpublic class UserApplication { ? ?public static void main(String[] args) { ? ? ? ?SpringApplication.run(UserApplication.class, args); ? }}很普通的自定義注解@LocalLock
/** * 單機(jī)防止重復(fù)提交注解 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface LocalLock { ? ?String key() default "";}LockMethodInterceptor攔截器類
package cn.wfb.demo.local;import com.google.common.cache.Cache;import com.google.common.cache.CacheBuilder;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.util.StringUtils;import java.lang.reflect.Method;import java.util.concurrent.TimeUnit;/** * 思路是通過aop的方式攔截用戶所有請求,前端需要提交唯一的token值 *后端攔截所有請求將token與Cache緩存內(nèi)容進(jìn)行比較,如果token存在就拋異常,不存在直接緩存到Cache中 */@Aspect@Configurationpublic class LockMethodInterceptor { ? ?/** ? ? * 通過CacheBuilder設(shè)置緩存?zhèn)€數(shù),設(shè)置緩存過期時間 ? ? */ ? ?private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder() ? ? ? ? ? ?// 最大緩存 1000 個 ? ? ? ? ? .maximumSize(1000) ? ? ? ? ? ?// 設(shè)置寫緩存后 5 秒鐘過期 ? ? ? ? ? .expireAfterWrite(5, TimeUnit.SECONDS) ? ? ? ? ? .build(); //所有請求并且?guī)в?#64;LocalLock注解的,進(jìn)行攔截 ? ?@Around("execution(public * *(..)) && @annotation(cn.wfb.demo.local.LocalLock)") ? ?public Object interceptor(ProceedingJoinPoint pjp) { ? ? ? ?MethodSignature signature = (MethodSignature) pjp.getSignature(); ? ? ? ?Method method = signature.getMethod(); ? ? ? ?LocalLock localLock = method.getAnnotation(LocalLock.class); ? ? ? ?String key = getKey(localLock.key(), pjp.getArgs()); ? ? ? ? ? ? ? ?//如果該key已經(jīng)在緩存中,那么拋異常,不存在則存入緩存中 ? ? ? ?if (!StringUtils.isEmpty(key)) { ? ? ? ? ? ?if (CACHES.getIfPresent(key) != null) { ? ? ? ? ? ? ? ?throw new RuntimeException("請勿重復(fù)請求"); ? ? ? ? ? } ? ? ? ? ? ?// 如果是第一次請求,就將 key 當(dāng)前對象壓入緩存中 ? ? ? ? ? ?CACHES.put(key, key); ? ? ? } ? ? ? ?try { ? ? ? ? ? ?return pjp.proceed(); ? ? ? } catch (Throwable throwable) { ? ? ? ? ? ?throw new RuntimeException("服務(wù)器異常"); ? ? ? } ? } ? ?/** ? ? * ? ? * 本例中傳入的key是user:arg[0],被替換參數(shù)名user:[token參數(shù)內(nèi)容] ? ? * 兩個參數(shù)的話user:arg[0],user:arg[1],user:可以省略只為了標(biāo)識意義明確 ? ? * ? ? * @param keyExpress 表達(dá)式:例如user:arg[0] ? ? * @param args ? ? ? 參數(shù) ? ? * @return 生成的key ? ? */ ? ?private String getKey(String keyExpress, Object[] args) { ? ? ? ?for (int i = 0; i < args.length; i++) { ? ? ? ? ? ?//將arg[0]替換成參數(shù)的內(nèi)容 ? ? ? ? ? ?keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString()); ? ? ? } ? ? ? ?return keyExpress; ? }}Controller
@RestController@RequestMapping("/user")public class UserController { ? ?/** ? ? * 通過加本地鎖防止重復(fù)提交 ? ? */ ? ?@LocalLock(key = "user:arg[0]") ? ?@GetMapping("/local") ? ?public Map queryLocal(@RequestParam String token) { ? ? ? ?HashMap map = new HashMap(); ? ? ? ?map.put("success", token); ? ? ? ?return map; ? }}測試localhost:8080/user/local?token=‘唯一ID值’ ?其實(shí)整體思路很簡單,這個token參數(shù)的值應(yīng)該讓前端通過工具類產(chǎn)生唯一值之后提交。提交到后端通過AOP攔截請求,將token內(nèi)容先判斷是否已存在Cache(google的工具類)的緩存,不存在則緩存到Cache中,如果已存在直接拋異常。整體代碼很好理解。
那么為什么說分布式系統(tǒng)要和傳統(tǒng)的方式區(qū)別開,這里的原因大家應(yīng)該能明白就是緩存位置問題,如果傳統(tǒng)項(xiàng)目用本地緩存保存即可,但是分布式就需要Redis作為共享內(nèi)存,道理是一樣的代碼都類似,只需要將本例中g(shù)oogle的本地Cache緩存改成redis保存即可
采用原生 API 來實(shí)現(xiàn)Redis的分布式鎖代碼如下
//connection.set(前端token的字節(jié),字節(jié)數(shù)組,通過Expiration設(shè)置key時間,使用的Redis的指令是SET_IF_ABSENT)final Boolean success = redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), new byte[0], Expiration.from(lock.expire(), lock.timeUnit()), RedisStringCommands.SetOption.SET_IF_ABSENT));其他想問的也可以進(jìn)群聊
總結(jié)
以上是生活随笔為你收集整理的分布式锁防止订单重复提交_防止表单重复提交看这里!!!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 字符串 CSV解析 表格 逗号分隔值 通
- 下一篇: 【共读Primer】55.[6.4]函数