解決表單重復(fù)提交問題的8種解決方案
提出問題: 解決表單重復(fù)提交
一 前置知識(shí)
1 HTTP是無狀態(tài)的超文本傳輸協(xié)議,是用于從萬維網(wǎng)服務(wù)器傳輸超文本到本地瀏覽器的傳輸協(xié)議,HTTP是在TCP/IP協(xié)議模型上的應(yīng)用層的一種傳輸協(xié)議
2 查看HTTP請(qǐng)求報(bào)文
HTTP請(qǐng)求報(bào)文由3部分組成: 請(qǐng)求行+請(qǐng)求頭+請(qǐng)求體

POST /user HTTP/1.1 // 請(qǐng)求行
Host: www.user.com
Content-Type: application/x-www-form-urlencoded
Connection: Keep-Alive
User-agent: Mozilla/5.0. // 以上是請(qǐng)求頭
name=world // 請(qǐng)求體(可選,如get請(qǐng)求時(shí)可選)
復(fù)制代碼請(qǐng)求行中包含了請(qǐng)求方法,比如上面例子中請(qǐng)求行的POST
3 HTTP協(xié)議中的9種方法(其中HTTP1.0定義了三種請(qǐng)求方法:GET, POST 和 HEAD方法,HTTP1.1新增了五種請(qǐng)求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法)
OPTIONS: OPTIONS請(qǐng)求與HEAD類似,一般也是用于客戶端查看服務(wù)器的性能。這個(gè)方法會(huì)請(qǐng)求服務(wù)器返回該資源所支持的所有HTTP請(qǐng)求方法,該方法會(huì)用'*'來代替資源名稱,向服務(wù)器發(fā)送OPTIONS請(qǐng)求,可以測(cè)試服務(wù)器功能是否正常。
HEAD: HEAD方法與GET方法一樣,都是向服務(wù)器發(fā)出指定資源的請(qǐng)求。但是,服務(wù)器在響應(yīng)HEAD請(qǐng)求時(shí)不會(huì)回傳資源的內(nèi)容部分,即:響應(yīng)主體。這樣,我們可以不傳輸全部?jī)?nèi)容的情況下,就可以獲取服務(wù)器的響應(yīng)頭信息。HEAD方法常被用于客戶端查看服務(wù)器的性能。
GET: GET請(qǐng)求會(huì)顯示請(qǐng)求指定的資源。一般來說GET方法應(yīng)該只用于數(shù)據(jù)的讀取,而不應(yīng)當(dāng)用于會(huì)產(chǎn)生副作用的非冪等的操作中。它期望的應(yīng)該是而且應(yīng)該是安全的和冪等的。這里的安全指的是,請(qǐng)求不會(huì)影響到資源的狀態(tài)。
POST: POST請(qǐng)求會(huì) 向指定資源提交數(shù)據(jù),請(qǐng)求服務(wù)器進(jìn)行處理,如:表單數(shù)據(jù)提交、文件上傳等,請(qǐng)求數(shù)據(jù)會(huì)被包含在請(qǐng)求體中。POST方法是非冪等的方法,因?yàn)檫@個(gè)請(qǐng)求可能會(huì)創(chuàng)建新的資源或/和修改現(xiàn)有資源。
PUT/PATCH: PUT請(qǐng)求會(huì)身向指定資源位置上傳其最新內(nèi)容,PUT方法是冪等的方法。通過該方法客戶端可以將指定資源的最新數(shù)據(jù)傳送給服務(wù)器取代指定的資源的內(nèi)容。
PATCH是對(duì)PUT方法的補(bǔ)充,用來對(duì)已知資源進(jìn)行局部更新
二者的不同點(diǎn):
1.PATCH一般用于資源的部分更新,而PUT一般用于資源的整體更新。
2.當(dāng)資源不存在時(shí),PATCH會(huì)創(chuàng)建一個(gè)新的資源,而PUT只會(huì)對(duì)已在資源進(jìn)行更新。
3.PUT 是冪等的,PATCH是非冪等的
4.PATCH方法出現(xiàn)的較晚,它在2010年的RFC 5789標(biāo)準(zhǔn)中被定義。\
DELETE: 請(qǐng)求服務(wù)器刪除請(qǐng)求的URI所標(biāo)識(shí)的資源,用于刪除
TRACE: TRACE請(qǐng)求服務(wù)器回顯其收到的請(qǐng)求信息,該方法主要用于HTTP請(qǐng)求的測(cè)試或診斷
CONNECT: CONNECT方法是HTTP/1.1協(xié)議預(yù)留的,能夠?qū)⑦B接改為管道方式的代理服務(wù)器。通常用于SSL加密服務(wù)器的鏈接與非加密的HTTP代理服務(wù)器的通信。\
我們看看維基百科對(duì)冪等的解釋:
冪等(idempotent、idempotence)是一個(gè)數(shù)學(xué)與計(jì)算機(jī)學(xué)概念,常見于抽象代數(shù)中。在編程中一個(gè)冪等操作的特點(diǎn)是其任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。冪等函數(shù),或冪等方法,是指可以使用相同參數(shù)重復(fù)執(zhí)行,并能獲得相同結(jié)果的函數(shù)。這些函數(shù)不會(huì)影響系統(tǒng)狀態(tài),也不用擔(dān)心重復(fù)執(zhí)行會(huì)對(duì)系統(tǒng)造成改變。所以,對(duì)于編輯表單的請(qǐng)求,我們使用PUT,可以不用做任何保護(hù)操作,即多次重復(fù)提交也不會(huì)對(duì)系統(tǒng)造成任何改變,這個(gè)時(shí)候可能會(huì)有杠精說:我是用POST請(qǐng)求后臺(tái)接口,然后使用update的SQL更新數(shù)據(jù)庫不也是一樣的嗎?
根據(jù)REST規(guī)范接口:每個(gè)資源都有對(duì)應(yīng)的URI,不同的HTTP Method對(duì)應(yīng)的對(duì)資源不同的操作,GET(讀取資源信息)、POST(添加資源)、PUT(更新資源信息)、DELETE(刪除資源)。幾乎所有的計(jì)算機(jī)語言都可以通過HTTP協(xié)議同REST服務(wù)器通信。所以POST請(qǐng)求最好只是用來添加資源,PUT請(qǐng)求用來更新資源信息。
二 解決方法
1 確保按鈕只能點(diǎn)擊一次
如用戶點(diǎn)擊查詢或提交訂單號(hào),按鈕變灰或頁面顯示loding狀態(tài)(例如展示例如遮罩層等組件)專用于防止用戶重復(fù)點(diǎn)擊。
2 在Session存放唯一標(biāo)識(shí)
用戶進(jìn)入頁面時(shí),服務(wù)端生成一個(gè)唯一的標(biāo)識(shí)值,存到session中,同時(shí)將它寫入表單的隱藏域中,用戶在輸入信息后點(diǎn)擊提交,在服務(wù)端獲取表單的隱藏域字段的值來與session中的唯一標(biāo)識(shí)值進(jìn)行比較,相等則說明是首次提交,就處理本次請(qǐng)求,然后刪除session唯一標(biāo)識(shí),不相等則標(biāo)識(shí)重復(fù)提交,忽略本次處理。
3 緩存隊(duì)列
將請(qǐng)求快速的接收下來,放入緩沖隊(duì)列中,后續(xù)使用異步任務(wù)處理隊(duì)列的數(shù)據(jù),過濾掉重復(fù)請(qǐng)求,我們可以用LinkedList來實(shí)現(xiàn)隊(duì)列,一個(gè)HashSet來實(shí)現(xiàn)去重。此方法優(yōu)點(diǎn)是異步處理、高吞吐,但是不能及時(shí)返回請(qǐng)求結(jié)果,需要后續(xù)輪詢處理結(jié)果。
4 token+redis
這種方式分成兩個(gè)階段:獲取token和業(yè)務(wù)操作階段。
以支付為例:
第一階段,在進(jìn)入到提交訂單頁面之前,需要在訂單系統(tǒng)根據(jù)當(dāng)前用戶信息向支付系統(tǒng)發(fā)起一次申請(qǐng)token請(qǐng)求,支付系統(tǒng)將token保存到redis中,作為第二階段支付使用
第二階段,前端訂單系統(tǒng)拿著申請(qǐng)到的token發(fā)起支付請(qǐng)求,第一時(shí)間刪除redis中的token,支付系統(tǒng)會(huì)檢查redis中是否存在該token,如果有,表示第一次請(qǐng)求支付,開始處理支付邏輯,處理完成后刪除redis中的token
當(dāng)重復(fù)請(qǐng)求時(shí)候,檢查redis中token是否存在,若不存在,則為重復(fù)請(qǐng)求
5 基于樂觀鎖來實(shí)現(xiàn)
如果更新已有數(shù)據(jù),可以進(jìn)行加鎖更新,也可以設(shè)計(jì)表結(jié)構(gòu)時(shí)使用version來做樂觀鎖,這樣既能保證執(zhí)行效率,又能保證冪等。樂觀鎖version字段在更新業(yè)務(wù)數(shù)據(jù)時(shí)值要自增。
sql為:update table set version = version + 1 where id =1 and version =#{version }
6 Axios攔截器
Axios的介紹: axios 是一個(gè)輕量的 HTTP客戶端
基于 XMLHttpRequest 服務(wù)來執(zhí)行 HTTP 請(qǐng)求,支持豐富的配置,支持 Promise,支持瀏覽器端和 Node.js 端。自Vue2.0起,尤大宣布取消對(duì) vue-resource 的官方推薦,轉(zhuǎn)而推薦 axios?,F(xiàn)在 axios 已經(jīng)成為大部分 Vue 開發(fā)者的首選
特性:
1 從瀏覽器中創(chuàng)建 XMLHttpRequests
2 從 node.js 創(chuàng)建 http請(qǐng)求
3 支持 Promise API
4 攔截請(qǐng)求和響應(yīng)
5 轉(zhuǎn)換請(qǐng)求數(shù)據(jù)和響應(yīng)數(shù)據(jù)
6 取消請(qǐng)求
7 自動(dòng)轉(zhuǎn)換JSON 數(shù)據(jù)
8 客戶端支持防御XSRF
復(fù)制代碼注意這個(gè)特性6取消請(qǐng)求:
6.1 基本使用
//安裝
npm install axios --S
//導(dǎo)入
import axios from 'axios'
//封裝Axios
//利用node環(huán)境變量來作判斷,用來區(qū)分開發(fā)、測(cè)試、生產(chǎn)環(huán)境
if (process.env.NODE_ENV === 'development') {
axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {
axios.defaults.baseURL = 'http://prod.xxx.com'
}
復(fù)制代碼6.2 創(chuàng)建如下文件夾

6.3 在lib目錄下創(chuàng)建axios.js文件:
/* eslint-disable */
import axios from "axios";
import { baseURL } from "@/config";
import md5 from "js-md5";
// 網(wǎng)絡(luò)請(qǐng)求記錄map結(jié)構(gòu)
let pending = {};
//取消請(qǐng)求
let CancelToken = axios.CancelToken;
class HttpRequest {
constructor(baseUrl = baseURL) {
this.baseUrl = baseUrl;
this.queue = {};
}
getInsideConfig(auth) {
var config = {
baseURL: this.baseUrl,
headers: {
Authorization: auth
}
};
return config;
}
distory(url) {
delete this.queue[url];
if (!Object.keys(this.queue).length) {
//Spin.hide()
}
}
interceptors(instance, url) {
instance.interceptors.request.use(
config => {
//檢查json數(shù)據(jù)中是否包含repetitiveRequestLimit屬性,若包含,則為此請(qǐng)求添加冪等校驗(yàn)
if(config.data.hasOwnProperty("repetitiveRequestLimit")){
let key = md5(`${config.url}&${config.method}&${JSON.stringify(config.data)}`);
config.cancelToken = new CancelToken(c => {
if (pending[key]) {
if (Date.now() - pending[key] > 5000) {
// 超過5s,刪除對(duì)應(yīng)的請(qǐng)求記錄,重新發(fā)起請(qǐng)求
delete pending[key];
} else {
// 5s以內(nèi)的已發(fā)起請(qǐng)求,取消重復(fù)請(qǐng)求
c("repeated");
}
}
});
// 記錄當(dāng)前的請(qǐng)求,已存在則更新時(shí)間戳
pending[key] = Date.now();
}else{
console.log('我是沒有repetitiveRequestLimit的請(qǐng)求')
}
return config;
},
error => {
return Promise.reject(error);
}
);
instance.interceptors.response.use(
res => {
this.distory(url);
var { data } = res;
return data;
},
error => {
// 錯(cuò)誤的請(qǐng)求結(jié)果處理,這里的代碼根據(jù)后臺(tái)的狀態(tài)碼來決定錯(cuò)誤的輸出信息
if (error && error.response) {
switch (error.response.status) {
case 400:
error.message = "錯(cuò)誤請(qǐng)求";
break;
case 401:
error.message = "未授權(quán),請(qǐng)重新登錄";
break;
case 403:
error.message = "拒絕訪問";
break;
case 404:
error.message = "請(qǐng)求錯(cuò)誤,未找到該資源";
break;
case 405:
error.message = "請(qǐng)求方法未允許";
break;
case 408:
error.message = "請(qǐng)求超時(shí)";
break;
case 500:
error.message = "服務(wù)器端出錯(cuò)";
break;
case 501:
error.message = "網(wǎng)絡(luò)未實(shí)現(xiàn)";
break;
case 502:
error.message = "網(wǎng)絡(luò)錯(cuò)誤";
break;
case 503:
error.message = "服務(wù)不可用";
break;
case 504:
error.message = "網(wǎng)絡(luò)超時(shí)";
break;
case 505:
error.message = "http版本不支持該請(qǐng)求";
break;
default:
error.message = `連接錯(cuò)誤${error.response.status}`;
}
} else {
error.message = "連接到服務(wù)器失敗";
}
return Promise.reject(error.message);
}
);
}
request(options) {
var instance = axios.create();
options = Object.assign(this.getInsideConfig(localStorage.getItem("Authorization")), options);
this.interceptors(instance, options.url);
return instance(options);
}
}
export default HttpRequest;
復(fù)制代碼6.4 config/index.js
//這里可以根據(jù)node環(huán)境來設(shè)置后臺(tái)Url
//利用node環(huán)境變量來作判斷,用來區(qū)分開發(fā)、測(cè)試、生產(chǎn)環(huán)境
/* eslint-disable */
export var baseURL = process.env.NODE_ENV === 'development'?' http://localhost:8080':' http://localhost:8081'
復(fù)制代碼6.5 api/baseIndex.js
/* eslint-disable */
import HttpRequest from "@/lib/axios";
var axios = new HttpRequest();
export default axios;
復(fù)制代碼6.6 api/requestdemo1
/* eslint-disable */
import axios from './baseIndex'
//原生redis實(shí)現(xiàn)分布式鎖測(cè)試
export var getRedisLock = (object) => {
return axios.request({
url: "/demo1/testRedisLock",
method: "post",
data:object
});
};
//redisson分布式鎖測(cè)試
export var getRedissonLock = (object) => {
return axios.request({
url: "/demo1/testRedisson",
method: "post",
data:object
});
};
復(fù)制代碼6.7 vue頁面引入
復(fù)制代碼6.8 測(cè)試


7 Redis分布式鎖
鎖我們都知道,在程序中的作用就是同步工具,保證共享資源在同一時(shí)刻只能被一個(gè)線程訪問,Java中的鎖我們都很熟悉了,像synchronized 、Lock都是我們經(jīng)常使用的,但是Java的鎖只能保證單機(jī)的時(shí)候有效,分布式集群環(huán)境就無能為力了,而對(duì)于解決表單重復(fù)提交這個(gè)問題的后臺(tái)解決方案,我們就可以使用到分布式鎖。
分布式鎖需要滿足的特性有這么幾點(diǎn):
1、互斥性:在任何時(shí)刻,對(duì)于同一條數(shù)據(jù),只有一臺(tái)應(yīng)用可以獲取到分布式鎖
2、高可用性:在分布式場(chǎng)景下,一小部分服務(wù)器宕機(jī)不影響正常使用,這種情況就需要將提供分布式鎖的服務(wù)以集群的方式部署
3、防止鎖超時(shí):如果客戶端沒有主動(dòng)釋放鎖,服務(wù)器會(huì)在一段時(shí)間之后自動(dòng)釋放鎖,防止客戶端宕機(jī)或者網(wǎng)絡(luò)不可達(dá)時(shí)產(chǎn)生死鎖
4、獨(dú)占性:加鎖解鎖必須由同一臺(tái)服務(wù)器進(jìn)行,也就是鎖的持有者才可以釋放鎖,不能出現(xiàn)你加的鎖,別人給你解鎖了
7.1 那么Redis分布式鎖的本質(zhì)是什么呢?
介紹兩個(gè)Redis獲得鎖的指令(這兩個(gè)指令包含的獲取鎖和設(shè)置過期時(shí)間這兩個(gè)操作是原子操作):
1 ?SETNX:意思是 SET if Not exists , 用法是:SETEX key seconds value
2 ?PSETEX:用法是:PSETEX key milliseconds value
(這個(gè)命令和SETEX命令相似,但它以毫秒為單位設(shè)置 key 的生存時(shí)間,而不是像SETEX命令那樣,以秒為單位)
Redis獲取鎖的最常見寫法:
從Redis 2.6.12 版本開始,SET命令可以通過參數(shù)來實(shí)現(xiàn)和SETNX、SETEX、PSETEX 三個(gè)命令相同的效果
SET key value NX EX seconds:加上NX、EX參數(shù)后,效果就相當(dāng)于SETEX
例子:

可以根據(jù)當(dāng)前登陸人的id和請(qǐng)求的uri作為鎖的名字,當(dāng)把key為lock的值設(shè)置為"Java"后,再設(shè)置成別的值就會(huì)失敗,即獲得鎖返回1,未獲得鎖返回0
所以這條命令體現(xiàn)了鎖的互斥性,即在任何時(shí)刻,對(duì)于同一條數(shù)據(jù),只有一臺(tái)應(yīng)用可以獲取到分布式鎖,設(shè)置鎖的超時(shí)時(shí)間還做到了防止鎖超時(shí)
那么問題來了,鎖的value值真的可以像上面那邊設(shè)置的很隨意嘛?
7.2 value的值如何設(shè)置?
答案: 應(yīng)該獨(dú)特唯一,這樣就實(shí)現(xiàn)了分布式鎖的獨(dú)占性
如果value值不唯一可能會(huì)出現(xiàn)如下請(qǐng)求?
1.服務(wù)器1獲取鎖成功
2.服務(wù)器1在某個(gè)操作上阻塞了太長(zhǎng)時(shí)間
3.設(shè)置的key過期了,鎖自動(dòng)釋放了
4.服務(wù)器2獲取到了對(duì)應(yīng)同一個(gè)資源的鎖
5.服務(wù)器1從阻塞中恢復(fù)過來,因?yàn)関alue值一樣,所以執(zhí)行釋放鎖操作時(shí)就會(huì)釋放掉服務(wù)器2持有的鎖,這樣就會(huì)造成問題
設(shè)置value的方法如下:
方法1:UUID
String uuid = UUID.randomUUID().toString();
復(fù)制代碼方法2:當(dāng)前線程id
String id = Thread.currentThread().getId() + "";
復(fù)制代碼方法3:分布式雪花算法id生成器
參考:基于Snowflake算法的分布式ID生成器
碼云: https://gitee.com/yu120/neural
復(fù)制代碼7.3 如何保證Redis鎖高可用呢?
高可用的大概定義是: “高可用性”(High Availability)通常來描述一個(gè)系統(tǒng)經(jīng)過專門的設(shè)計(jì),從而減少停工時(shí)間,而保持其服務(wù)的高度可用性,即在分布式場(chǎng)景下,一小部分服務(wù)器宕機(jī)不影響正常使用。
不推薦: ?Redis 單副本
不推薦原因如下:如果redis是單master模式的,當(dāng)這臺(tái)機(jī)宕機(jī)的時(shí)候,那么所有的客戶端都獲取不到鎖了
推薦:Redis 多副本(主從), Redis Sentinel(哨兵), Redis Cluster
推薦原因: 為了提高可用性,假設(shè)部署主從架構(gòu)的redis,1個(gè)master加1個(gè)slave,因?yàn)閞edis的主從同步是異步進(jìn)行的,可能會(huì)出現(xiàn)客戶端1設(shè)置完鎖后,master掛掉,原來master中的數(shù)據(jù)都會(huì)轉(zhuǎn)移到原來的slave中,然后slave提升為master,這樣就不會(huì)丟失鎖。
7.4 Demo實(shí)踐
7.4.1 項(xiàng)目中引入Jedis客戶端
org.springframework.boot
spring-boot-starter-data-redis
io.lettuce
lettuce-core
redis.clients
jedis
復(fù)制代碼Redis分布式鎖工具類:
/**
@description:
@author: geekAntony
@create: 2021-01-17 16:52
**/
public class RedisLockUtil {
// key的持有時(shí)間,5ms
private long EXPIRE_TIME = 5;
// 等待超時(shí)時(shí)間,1s
private long TIME_OUT = 1000;
// redis命令參數(shù),相當(dāng)于nx和px的命令合集
private SetParams params = SetParams.setParams().nx().px(EXPIRE_TIME);
// redis連接池,連的是本地的redis客戶端
JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);
/**
* 加鎖
*
* @param value
* 線程的id,或者其他可識(shí)別當(dāng)前線程且不重復(fù)的字段
* @return
*/
public boolean lock(String key,String value) {
Long start = System.currentTimeMillis();
Jedis jedis = jedisPool.getResource();
try {
for (;;) {
// SET命令返回OK ,則證明獲取鎖成功
String lock = jedis.set(key, value, params);
if ("OK".equals(lock)) {
return true;
}
// 否則循環(huán)等待,在TIME_OUT時(shí)間內(nèi)仍未獲取到鎖,則獲取失敗
long l = System.currentTimeMillis() - start;
if (l >= TIME_OUT) {
return false;
}
try {
// 休眠一會(huì),不然反復(fù)執(zhí)行循環(huán)會(huì)一直失敗
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
jedis.close();
}
}
/**
* 解鎖
*
* @param value
* 線程的id,或者其他可識(shí)別當(dāng)前線程且不重復(fù)的字段
* @return
*/
public boolean unlock(String key,String value) {
Jedis jedis = jedisPool.getResource();
// 刪除key的lua腳本
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then" + " return redis.call('del',KEYS[1]) " + "else"
+ " return 0 " + "end";
try {
String result =
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value)).toString();
return "1".equals(result);
} finally {
jedis.close();
}
}
}
復(fù)制代碼前端控制器:
/**
* @program: structure
* @description:
* @author: geekAntony
* @create: 2021-01-19 22:59
**/
@RestController
@RequestMapping("/demo1")
public class TestRedisLock {
private static RedisLockUtil demo = new RedisLockUtil();
@PostMapping(value = "/testRedisLock")
public String add(@RequestBody Person person) {
String id = Thread.currentThread().getId() + "";
boolean isLock = demo.lock("redislockName",id);
try {
//拿到鎖的話執(zhí)行業(yè)務(wù)操作...
if (isLock) {
//模擬3s業(yè)務(wù)操作
TimeUnit.SECONDS.sleep(3);
}else{
return "請(qǐng)不要重復(fù)發(fā)送表單請(qǐng)求";
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 在finally中釋放鎖
demo.unlock("redislockName",id);
}
return "完成業(yè)務(wù)邏輯";
}
}
復(fù)制代碼測(cè)試:

8 使用Redisson分布式鎖
引入Redisson依賴:
org.redisson
redisson
3.13.4
復(fù)制代碼application.yml:
#Redis 配置
spring:
redis:
host: 127.0.0.1
port: 6379
database: 1
password:
timeout: 10000
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
#自定義分布式 Redis 客戶端 Redisson 配置
redisson:
type: stand-alone #redis服務(wù)器部署類型,stand-alone:單機(jī)部署、cluster:機(jī)器部署.默認(rèn)為單機(jī)部署
address: redis://127.0.0.1:6379 #單機(jī)時(shí)必須是redis://開頭.
database: 1
復(fù)制代碼基礎(chǔ)信息配置類:
@Data //lombok
@ConfigurationProperties(prefix = "redisson")
public class RedssionProperties {
/**
* redis服務(wù)器部署類型。
* stand-alone:單機(jī)部署
* cluster:集群部署.
*/
private String type = "stand-alone";
/**
* Redis 服務(wù)器地址
*/
private String address;
/**
* 用于Redis連接的數(shù)據(jù)庫索引
*/
private int database = 0;
/**
* Redis身份驗(yàn)證的密碼,如果不需要,則應(yīng)為null
*/
private String password;
/**
* Redis最小空閑連接量
*/
private int connectionMinimumIdleSize = 24;
/**
* Redis連接最大池大小
*/
private int connectionPoolSize = 64;
/**
* Redis 服務(wù)器響應(yīng)超時(shí)時(shí)間,Redis 命令成功發(fā)送后開始倒計(jì)時(shí)(毫秒)
*/
private int timeout = 3000;
/**
* 連接到 Redis 服務(wù)器時(shí)超時(shí)時(shí)間(毫秒)
*/
private int connectTimeout = 10000;
}
復(fù)制代碼Redisson配置類
@Configuration
@EnableConfigurationProperties(RedssionProperties.class)
public class RedissonConfig {
private final RedssionProperties redssionProperties;
/**
* 從 Spring 容器中獲取 {@link RedssionProperties}實(shí)例
*/
public RedissonConfig(RedssionProperties redssionProperties) {
this.redssionProperties = redssionProperties;
}
/**
* redis 服務(wù)器單機(jī)部署時(shí),創(chuàng)建 RedissonClient 實(shí)例,交由 Spring 容器管理
* 只有當(dāng)配置了 redisson.type=stand-alone 時(shí),才繼續(xù)生成 RedissonClient 實(shí)例并交由 Spring 容器管理
*
* @return
*/
@Bean
@ConditionalOnProperty(prefix = "redisson", name = "type", havingValue = "stand-alone")
public RedissonClient redissonClient() {
/**
* Config:Redisson 配置基類:
* SingleServerConfig:?jiǎn)螜C(jī)部署配置類,MasterSlaveServersConfig:主從復(fù)制部署配置
* SentinelServersConfig:哨兵模式配置,ClusterServersConfig:集群部署配置類。
* useSingleServer():初始化 redis 單服務(wù)器配置。即 redis 服務(wù)器單機(jī)部署
* setAddress(String address):設(shè)置 redis 服務(wù)器地址。格式 -- redis://主機(jī):端口,不寫時(shí),默認(rèn)為 redis://127.0.0.1:6379
* setDatabase(int database): 設(shè)置連接的 redis 數(shù)據(jù)庫,默認(rèn)為 0
* setPassword(String password):設(shè)置 redis 服務(wù)器認(rèn)證密碼,沒有時(shí)設(shè)置為 null,默認(rèn)為 null
* RedissonClient create(Config config): 使用提供的配置創(chuàng)建同步/異步 Redisson 實(shí)例
* Redisson 類實(shí)現(xiàn)了 RedissonClient 接口,真正需要使用的就是這兩個(gè) API
*/
Config config = new Config();
config.useSingleServer()
.setAddress(redssionProperties.getAddress())
.setDatabase(redssionProperties.getDatabase())
.setPassword(redssionProperties.getPassword())
.setConnectionPoolSize(redssionProperties.getConnectionPoolSize())
.setConnectionMinimumIdleSize(redssionProperties.getConnectionMinimumIdleSize())
.setTimeout(redssionProperties.getTimeout())
.setConnectTimeout(redssionProperties.getConnectTimeout());
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
復(fù)制代碼測(cè)試:
具體詳細(xì)前端代碼上文可見:
復(fù)制代碼后臺(tái)控制器:
@RestController
@RequestMapping("/demo1")
public class TestRedisLock {
@Autowired
private RedissonClient redissonClient;
private static Logger logger = LoggerFactory.getLogger(TestRedisLock.class);
/**
* RedissonClient.getLock(String name):可重入鎖
* boolean tryLock(long waitTime, long leaseTime, TimeUnit unit):嘗試獲取鎖
* 1、waitTime:獲取鎖時(shí)的等待時(shí)間,超時(shí)自動(dòng)放棄,線程不再繼續(xù)阻塞,方法返回 false
* 2、leaseTime:獲取到鎖后,指定加鎖的時(shí)間,超時(shí)后自動(dòng)解鎖
* 3、如果成功獲取鎖,則返回 true,否則返回 false。
*/
@PostMapping(value = "/testRedisson")
public String addDemo1(@RequestBody Person person) {
String result = "訂單[" + person.getOrderNumber() + "]支付成功.";
//這里可以加入登錄用戶的id等數(shù)據(jù)
String key = person.getOrderNumber();
/**
* getLock(String name):按名稱返回鎖實(shí)例,實(shí)現(xiàn)了一個(gè)非公平的可重入鎖,因此不能保證線程獲得順序
* lock():獲取鎖,如果鎖不可用,則當(dāng)前線程將處于休眠狀態(tài),直到獲得鎖為止
*/
RLock lock = redissonClient.getLock(key);
boolean tryLock = false;
try {
//waitTime是嘗試加鎖時(shí)間,最多等待1s,上鎖60s以后自動(dòng)解鎖
tryLock = lock.tryLock(1, 60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
//上鎖失敗,則會(huì)進(jìn)入此if
if (!tryLock) {
return "訂單[" + person.getOrderNumber() + "]正在支付中,請(qǐng)耐心等待!";
}
try {
logger.info("查詢支付狀態(tài)");
TimeUnit.SECONDS.sleep(1);
logger.info("正在支付訂單[" + person.getOrderNumber() + "]");
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
result = "訂單號(hào)xxx [" + person.getOrderNumber() + "]支付失?。? + e.getMessage();
} finally {
/**
* boolean isLocked():檢查鎖是否被任何線程鎖定,被鎖定時(shí)返回 true,否則返回 false.
* unlock():釋放鎖, Lock 接口的實(shí)現(xiàn)類通常會(huì)對(duì)線程釋放鎖(通常只有鎖的持有者才能釋放鎖)施加限制,
* 如果違反了限制,則可能會(huì)拋出(未檢查的)異常。如果鎖已經(jīng)被釋放,重復(fù)釋放時(shí),會(huì)拋出異常。
*/
if (lock.isLocked()) {
lock.unlock();
}
}
return result;
}
}
復(fù)制代碼測(cè)試結(jié)果:

三 總結(jié)
以上方案
解決方案1,實(shí)現(xiàn)起來較為簡(jiǎn)單,項(xiàng)目開發(fā)前期或者不是特別重要的接口中可以使用此方法
解決方案2,3,4不推薦
解決方案5 基于樂觀鎖來實(shí)現(xiàn),個(gè)人感覺占硬盤存儲(chǔ)空間,但是實(shí)現(xiàn)簡(jiǎn)單,較為穩(wěn)定,建議使用
解決方案6 比較新穎,可以在項(xiàng)目中嘗試
解決方案7 是Redis實(shí)現(xiàn)分布式鎖的Demo,依賴高可用Redis
解決方案8 是生產(chǎn)環(huán)境中比較流行的解決方式,依賴高可用Redis
參考文章:
基于Redis的分布式鎖實(shí)現(xiàn)
juejin.cn/post/684490…
這才叫細(xì):帶你深入理解Redis分布式鎖
mp.weixin.qq.com/s?__biz=MzI…
作者:geekAntony
鏈接:https://juejin.cn/post/6935751997157031966
來源:稀土掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
