<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          解決表單重復(fù)提交問題的8種解決方案

          共 19005字,需瀏覽 39分鐘

           ·

          2021-12-13 13:26

          提出問題: 解決表單重復(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)注明出處。



          瀏覽 214
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲精品视频无码 | 操逼色网| 不卡一区二区三区四区 | 久久精品无码一区二区小草千夏 | 成人内射生活片 |