數據庫事務、樂觀鎖及悲觀鎖

參考:node支付寶支付及同步、異步通知、主動查詢支付寶訂單狀態
以下容結合上述鏈接查看

1. 什么是數據庫事務?

1.1. 連續執行數據庫操作

在支付成功后,我們在自定義的paidSuccess里,依次更新了訂單狀態和用戶信息。也就說這里先執行了更新訂單表的SQL,接著又執行了更新用戶表的SQL。

但是大家想一想,有沒有可能。訂單表更新成功了,但因為某些原因導致用戶表更新失敗

比方說用戶模型里,用戶組的效驗寫掉了1,導致1存進不去。

在這里插入圖片描述

又或者大會員有效期計算錯誤,導致更新失敗。

這樣就可能訂單狀態更新成已支付了,但用戶卻還是普通用戶,或者大會員有效期沒有增加,造成數據不一致。所以對于這種,連續執行多條SQL語句的操作,正確的做法是要加上事務。所謂數據庫的事務

1.2.數據庫事務基礎概念

  • 執行一組 SQL 操作,這些操作必須全部成功執行,或者全部不執行
  • 如果其中任意一條SQL執行失敗,那就全部回滾(Rollback)撤銷所有已經完成的操作,確保數據的一致性和完整性。
  • 如果所有的操作都成功了,才會提交(Commit)事務,使所有更改永久生效。

1.3. SQL事務的使用:

1.3.1.開啟事務 (START TRANSACTION)

-- 開始一個新的事務
START TRANSACTION;-- BEGIN,簡化寫法
BEGIN;

1.3.2. 連續連續執行多條 SQL 語句

-- 更新訂單表
UPDATE `Orders` SET `tradeNo` = '2024121322001495001404593598', `paidAt` = '2024-12-13 12:35:31', `status` = 1 WHERE `id` = 5;

在這里插入圖片描述

  • 現在用語句查詢一下訂單信息
-- 查詢訂單表
SELECT `tradeNo`, `paidAt`, `status` FROM `Orders` WHERE `id` = 5;

在這里插入圖片描述

發現數據已經更新進去了。但是要注意,因為現在數據庫事務還沒有提交,所以這里并不是真正的保存進去了。

  • 再繼續更新用戶表
-- 更新用戶表
UPDATE `Users` SET `role` = 1, `membershipExpiredAt` = '2025年10月10日' WHERE `id` = 2;

在這里插入圖片描述

語句里的日期,還是寫的錯誤的,這是無法存進去的。提示我們錯誤信息了:
在這里插入圖片描述

1.3.3. 回滾 (ROLLBACK)

現在就造成了訂單表更新了,但是用戶表更新失敗,兩個表的數據不一致。碰到這種情況,就可以使用回滾語句:

-- 如果執行失敗,回滾所有操作
ROLLBACK;

在這里插入圖片描述

重新選中查詢訂單表的 SQL 語句,運行一下,發現訂單表中,剛才已經修改的數據,被全部重置了

在這里插入圖片描述
這樣就不用擔心兩張表的數據不一致了。

1.3.4. 提交事務 (COMMIT)

如果所有語句都執行成功,那要繼續用COMMIT提交事務,將數據永久的保存到數據庫中。

  • 將日期改為正確的時間
UPDATE `Users` SET `role` = 1, `membershipExpiredAt` = '2025-10-10 10:10:10' WHERE `id` = 2;

在這里插入圖片描述

1.3.5 完整代碼:

-- 開始一個新的事務,也可用簡寫成 BEGIN,二選一即可
START TRANSACTION;-- 連續執行多條SQL語句:
-- 更新訂單表
UPDATE `Orders` SET ...
-- 更新用戶表
UPDATE `Users` SET ...-- 如果執行失敗,回滾所有操作
ROLLBACK;-- 如果執行成功,提交事務,使所有更改成為永久性的
COMMIT;

1.4. 在 Node(ORM) 中使用數據庫事務

  • 第一種是非托管事務,也就是必須手動執行提交和回滾。
  • 另一種是托管事務,代碼會自動處理提交和回滾。

1.4.1. 非托管事務

const { sequelize, User, Order } = require('../models');/*** 支付成功后,更新訂單狀態和會員信息* @param outTradeNo* @param tradeNo* @param paidAt* @returns {Promise<void>}*/
async function paidSuccess(outTradeNo, tradeNo, paidAt) {// 開啟事務const t = await Order.sequelize.transaction();try {// 查詢當前訂單(在事務中)const order = await Order.findOne({where: { outTradeNo: outTradeNo },transaction: t,});// 對于狀態已更新的訂單,直接返回。防止用戶重復請求,重復增加大會員有效期if (order.status > 0) {return;}// 更新訂單狀態(在事務中)await order.update({tradeNo: tradeNo,     // 流水號status: 1,            // 訂單狀態:已支付paymentMethod: 0,     // 支付方式:支付寶paidAt: paidAt,       // 支付時間}, { transaction: t });// 查詢訂單對應的用戶(在事務中)const user = await User.findByPk(order.userId, { transaction: t });// 將用戶組設置為大會員。可防止管理員創建訂單,并將用戶組修改為大會員if (user.role === 0) {user.role = 1;}// 使用moment.js,增加大會員有效期// user.membershipExpiredAt = moment(user.membershipExpiredAt || new Date())//   .add(order.membershipMonths, 'months')//   .toDate();user.membershipExpiredAt = '2025年10月10日';// 保存用戶信息(在事務中)await user.save({ transaction: t });// 提交事務await t.commit();} catch (error) {// 回滾事務await t.rollback();// 將錯誤拋出,讓上層處理throw error;}
}
  • 頂部先引用一下。
  • 代碼里,開啟了數據庫事務。
  • 除了findOne,是在where后面直接加上transaction: t之外。
  • 其他操作里,都要添加第二個參數,加上{ transaction: t }。關于這點,文檔中有說明:
  • 在這里插入圖片描述
  • 我們給查詢語句,也加上了事務。這是因為查詢語句,也有可能出錯。
  • 如果全都執行成功,就提交事務。
  • 如果有執行失敗的,就回滾。
  • 注意,最后這里,要將錯誤拋出,交給上層來處理。這樣調用paidSuccess的路由里,就會捕獲到,然后自動記錄到錯誤日志里。

1.4.2. 托管事務

/*** 支付成功后,更新訂單狀態和會員信息* @param outTradeNo* @param tradeNo* @param paidAt* @returns {Promise<void>}*/
async function paidSuccess(outTradeNo, tradeNo, paidAt) {try {// 開啟事務await sequelize.transaction(async (t) => {// 查詢當前訂單(在事務中)const order = await Order.findOne({where: { outTradeNo: outTradeNo },transaction: t,});// 對于狀態已更新的訂單,直接返回。防止用戶重復請求,重復增加大會員有效期if (order.status > 0) {return;}// 更新訂單狀態(在事務中)await order.update({tradeNo: tradeNo,     // 流水號status: 1,            // 訂單狀態:已支付paymentMethod: 0,     // 支付方式:支付寶paidAt: paidAt,       // 支付時間}, { transaction: t });// 查詢訂單對應的用戶(在事務中)const user = await User.findByPk(order.userId, { transaction: t });// 將用戶組設置為大會員。可防止管理員創建訂單,并將用戶組修改為大會員if (user.role === 0) {user.role = 1;}// 使用moment.js,增加大會員有效期// user.membershipExpiredAt = moment(user.membershipExpiredAt || new Date())//   .add(order.membershipMonths, 'months')//   .toDate();user.membershipExpiredAt = '2025年10月10日';// 保存用戶信息(在事務中)await user.save({ transaction: t });});} catch (error) {// 將錯誤拋出,讓上層處理throw error;  }
}
  • 非常簡單,在try里,用sequelize.transaction包住所有代碼。
  • 去掉commitrollback。因為這種寫法,會自動提交回滾
  • 其他地方都和之前一樣。

1.5. 總結一下

  • 要一下執行多個數據庫操作,最好加上事務。
  • 要么全部執行成功,要么就全部回滾。
  • 所以,事務可以保障數據的完整性和一致性。

2. 數據庫的樂觀鎖

2.1.多個事務修改同一條記錄

2.1.1 庫存問題

例如有個商品表,里面有個庫存字段。剛好這個商品現在只有1件了,這時候兩個人同時下單。但是因為事務還沒有提交,就會造成庫存的錯誤判斷。大家看這個表格:

事務一(A 下單)事務二(B 下單)
查詢商品庫存:1 件
查詢商品庫存:1 件
判斷商品庫存 > 0,繼續執行
判斷商品庫存 > 0,繼續執行
更新商品庫存:1 - 1 = 0 件
更新商品庫存:1 - 1 = 0 件
提交事務,庫存:0 件
提交事務,庫存:0 件

最終結果是,只有 1 件庫存的商品,卻同時賣給了兩個用戶。這種情況,特別是在高并發的秒殺項目里,是最容易出現問題的。

2.1.2. 例二:金融余額問題

事務一(A 給 B 轉賬)事務二(C 給 A 匯款)
查詢 A 的余額:1000 元
查詢 A 的余額:1000 元
更新 A 的余額:1000 - 500 = 500 元
更新 A 的余額:1000 + 200 = 1200 元
提交事務,余額:500 元
提交事務,余額:1200 元

因為事務二是后提交的,所以最終數據庫中保存的結果,A 的余額就成了1200元。A 用戶轉賬給別人的錢,完全沒有減少。這在金融項目中,就是災難了。

2.1.3. 例三:更新訂單和用戶信息

操作步驟操作一操作二
第一步查詢訂單狀態:0
第二步查詢訂單狀態:0
第三步判斷狀態為 0,更新狀態為:1
第四步判斷狀態為 0,更新狀態為:1
第五步增加大會員有效期
第六步提交事務
第七步再次增加大會員有效期
第八步提交事務

。這就會造成用戶的大會員時間,重復增加兩次

2.2.樂觀鎖實踐

所謂樂觀鎖,就是程序非常樂觀的認為,當前要操作的記錄不會碰到其他人同時在操作。它允許多個事務,同時對一條記錄進行操作,但是如果發現其他事務改變了數據,它就報錯,提示用戶重試。

最常見的做法是在數據庫中增加版本號(version)或者時間戳(timestamp)字段。根據Sequelize 文檔里的說明,這里要用的是版本號(version)

2.2.1 增加 version 字段

sequelize migration:create --name add-version-to-orders

打開遷移文件,直接用講義文檔中的內容覆蓋,設置了版本號的默認值是0。

'use strict';/** @type {import('sequelize-cli').Migration} */
module.exports = {async up (queryInterface, Sequelize) {await queryInterface.addColumn('Orders', 'version', {allowNull: false,defaultValue: 0,type: Sequelize.INTEGER.UNSIGNED});},async down (queryInterface, Sequelize) {await queryInterface.removeColumn('Orders', 'version');}
};

運行遷移命令

sequelize db:migrate

在這里插入圖片描述
再打開模型文件models/order.js,增加version字段相關的定義:

Order.init({// ...version: {allowNull: false,type: DataTypes.INTEGER,defaultValue: 0},// ...
}, {sequelize,modelName: 'Order',
});

2.2.3. 用 SQL 模擬更新訂單

START TRANSACTION;
SELECT `id`, `version`, `status` FROM `Orders` WHERE `id` = 5;UPDATE `Orders` SET `status` = 1, `version` = `version` + 1 WHERE `id` = 5 and version = 0;
COMMIT;

接著我們開啟兩個數據庫客戶端
在這里插入圖片描述
不太明確你說的“用上面的方式”具體所指,你是希望我按照上述事務 A 和事務 B 的處理邏輯,再舉一個類似的例子嗎?還是有其他的想法呢?以下我按照類似的邏輯,以兩個用戶同時修改商品信息的事務場景為例,再展示一遍:

事務 A(用戶 1 修改商品信息)事務 B(用戶 2 修改商品信息)
開啟事務,查詢商品信息
輸出:status: 未上架、version: 0
開啟事務,查詢商品信息
輸出:status: 未上架、version: 0
where version = 0 作為條件,
更新為:status: 已上架 version: 1
提交事務
where version = 0 作為條件,
更新為:status: 已上架 version: 1

因為 version 被事務 A 改為 1 了,
所以找不到數據,執行失敗
提交事務

2.3.在 Node 項目中實現樂觀鎖

2.3.1. 手動處理

打開routes/alipay.js,頂部先做引用:
Conflict 類專門用于表示 HTTP 409 狀態碼對應的錯誤,也就是 “沖突” 錯誤

const { Conflict } = require('http-errors');

增加一個函數,實現延遲執行: 此函數只是為了模擬 實際開發中不需要

/*** 實現延遲* @param ms* @returns {Promise<unknown>}*/
function delay(ms) {return new Promise(resolve => setTimeout(resolve, ms));
}

然后修改paidSuccess

async function paidSuccess(outTradeNo, tradeNo, paidAt) {try {// 開啟事務await sequelize.transaction(async (t) => {// 查詢當前訂單(在事務中)const order = await Order.findOne({where: {outTradeNo: outTradeNo}, transaction: t,});// 對于狀態已更新的訂單,直接返回。防止用戶重復請求,重復增加大會員有效期if (order.status > 0) {return;}await delay(5000);          // 等待5秒// // 更新訂單狀態(在事務中)// await order.update({//   tradeNo: tradeNo,     // 流水號//   status: 1,            // 訂單狀態:已支付//   paymentMethod: 0,     // 支付方式:支付寶//   paidAt: paidAt,       // 支付時間// }, {transaction: t});// 更新訂單狀態(在事務中),包括版本號檢查
// updatedRows 是數據庫中受到影響的行數const [updatedRows] = await Order.update({tradeNo: tradeNo,           // 流水號status: 1,                  // 訂單狀態:已支付paymentMethod: 0,           // 支付方式:支付寶paidAt: paidAt,             // 支付時間version: order.version + 1, // 增加版本號}, {where: {id: order.id, version: order.version,   // 只更新版本號匹配的記錄}, transaction: t,});// 如果沒有更新數據,提示錯誤if (updatedRows === 0) {throw new Conflict('請求沖突,您提交的數據已被修改,請稍后重試。');}// 查詢訂單對應的用戶(在事務中)const user = await User.findByPk(order.userId, {transaction: t});// 將用戶組設置為大會員。可防止管理員創建訂單,并將用戶組修改為大會員if (user.role === 0) {user.role = 1;}// 使用moment.js,增加大會員有效期user.membershipExpiredAt = moment(user.membershipExpiredAt || new Date()).add(order.membershipMonths, 'months').toDate();// 保存用戶信息(在事務中)await user.save({transaction: t});});} catch (error) {// 將錯誤拋出,讓上層處理throw error;}
}
  • 為了模擬并發請求,增加了延遲 5 秒再執行。
  • 注意現在調用的是大寫的Order模型,而不是剛才查詢小寫的order對象。
  • 更新代碼里,對版本號也做了自增。
  • 查詢條件里,除了id以外,還增加了之前查到的版本號
  • 執行后,會返回一個數組。數組的第一個元素,表示數據庫中受到影響的行數
  • 如果數據庫中,受影響的行數為0,表示數據沒有更新成功,就提示對應的錯誤信息。

測試

  • 重置某條訂單信息
UPDATE `Orders` SET `tradeNo` = NULL, `paidAt` = NULL,`version` = 0, `status` = 0 WHERE `id` = 4;
UPDATE `Users` SET `role` = 0, `membershipExpiredAt` = NULL WHERE `id` = 2;
  • Apifox 調用主動查詢支付寶接口 因為我們寫了等待 5 秒,所以執行的時候會卡住。
    在這里插入圖片描述
  • 快速的用數據庫客戶端,執行下剛才測試用的 SQL 語句,更新下訂單狀態,并增加版本號。
START TRANSACTION;
SELECT `id`, `version`, `status` FROM `Orders` WHERE `id` = 4;UPDATE `Orders` SET `status` = 1, `version` = `version` + 1 WHERE `id` = 4 and version = 0;
COMMIT;
  • 繼續等待 5 秒鐘過去,Apifox 中會出現錯誤提示。

在這里插入圖片描述

這就是因為版本號已經被剛才數據庫客戶端修改了,現在再執行更新的時候查不到對應的數據了,所以沒更新成功。

2.3.2. Sequelize 自動處理

  • 根據官方文檔中的說明,在模型里設置version: true,就可以自動實現樂觀鎖

  • 我們現在來試試,打開models/order.js。在最底下,增加version: true

Order.init({// ...
}, {sequelize,modelName: 'Order',version: true, // 樂觀鎖
});
  • 回到路由里,將頂部的引用刪掉:
// const { Conflict } = require('http-errors');
  • paidSuccess修改:
async function paidSuccess(outTradeNo, tradeNo, paidAt) {try {// 開啟事務await sequelize.transaction(async (t) => {// 查詢當前訂單(在事務中)const order = await Order.findOne({where: {outTradeNo: outTradeNo}, transaction: t,});// 對于狀態已更新的訂單,直接返回。防止用戶重復請求,重復增加大會員有效期if (order.status > 0) {return;}await delay(5000);          // 等待5秒// // 更新訂單狀態(在事務中)await order.update({tradeNo: tradeNo,     // 流水號status: 1,            // 訂單狀態:已支付paymentMethod: 0,     // 支付方式:支付寶paidAt: paidAt,       // 支付時間}, {transaction: t});// 更新訂單狀態(在事務中),包括版本號檢查
// updatedRows 是數據庫中受到影響的行數
//       const [updatedRows] = await Order.update({
//         tradeNo: tradeNo,           // 流水號
//         status: 1,                  // 訂單狀態:已支付
//         paymentMethod: 0,           // 支付方式:支付寶
//         paidAt: paidAt,             // 支付時間
//         version: order.version + 1, // 增加版本號
//       }, {
//         where: {
//           id: order.id, version: order.version,   // 只更新版本號匹配的記錄
//         }, transaction: t,
//       });// 如果沒有更新數據,提示錯誤if (updatedRows === 0) {throw new Conflict('請求沖突,您提交的數據已被修改,請稍后重試。');}// 查詢訂單對應的用戶(在事務中)const user = await User.findByPk(order.userId, {transaction: t});// 將用戶組設置為大會員。可防止管理員創建訂單,并將用戶組修改為大會員if (user.role === 0) {user.role = 1;}// 使用moment.js,增加大會員有效期user.membershipExpiredAt = moment(user.membershipExpiredAt || new Date()).add(order.membershipMonths, 'months').toDate();// 保存用戶信息(在事務中)await user.save({transaction: t});});} catch (error) {// 將錯誤拋出,讓上層處理throw error;}
}
  • utils/responses.js的錯誤響應中,增加一個判斷
function failure(res, error) {if (error.name === 'SequelizeValidationError') {  // Sequelize 驗證錯誤// ...} else if(error.name === 'SequelizeOptimisticLockError') {statusCode = 409;errors = '請求沖突,您提交的數據已被修改,請稍后重試。';} // ...
}

這是因為Sequelize自動實現的樂觀鎖,如果出錯了,會響應SequelizeOptimisticLockError,所以我們增加一個判斷。

測試

  • 重置某條訂單信息
UPDATE `Orders` SET `tradeNo` = NULL, `paidAt` = NULL,`version` = 0, `status` = 0 WHERE `id` = 4;
UPDATE `Users` SET `role` = 0, `membershipExpiredAt` = NULL WHERE `id` = 2;
  • Apifox 調用主動查詢支付寶接口 因為我們寫了等待 5 秒,所以執行的時候會卡住。
    在這里插入圖片描述
  • 快速的用數據庫客戶端,執行下剛才測試用的 SQL 語句,更新下訂單狀態,并增加版本號。
START TRANSACTION;
SELECT `id`, `version`, `status` FROM `Orders` WHERE `id` = 4;UPDATE `Orders` SET `status` = 1, `version` = `version` + 1 WHERE `id` = 4 and version = 0;
COMMIT;
  • 繼續等待 5 秒鐘過去,Apifox 中會出現錯誤提示。

在這里插入圖片描述

這就是因為版本號已經被剛才數據庫客戶端修改了,現在再執行更新的時候查不到對應的數據了,所以沒更新成功。

2.3.3 測試完成后去掉delay函數

// await delay(5000);  // 等待5秒

2.4.總結一下

  • 防止并發沖突:為了防止多個操作同時修改數據庫中的同一條記錄,可以使用鎖機制。
  • 樂觀鎖:假設沖突很少發生,只有在發現沖突時才進行處理,通常會提醒用戶重試。原理是在數據庫中增加一個版本號(version)字段,在更新數據時檢查版本號是否匹配,若不匹配則提示用戶重試。
  • 用戶表的樂觀鎖:我們演示了訂單模型的樂觀鎖。對于用戶表的更新,可以用同樣的方法,增加version字段,并且在模型中添加version: true
  • 與事務的關系:樂觀鎖與事務并沒有直接關系,但為了確保所有操作要么全部成功,要么全部回滾,兩者經常結合使用以保證更好的一致性。
  • 適用場景:樂觀鎖適用于并發較低、讀多寫少的場景。因為在這種情況下,沖突發生的概率較小,樂觀鎖可以減少不必要的鎖定開銷,提高系統的并發性能。
  • 優缺點:樂觀鎖的優點是系統并發性能較好,因為不預先加鎖,減少了鎖定帶來的資源占用和等待時間;缺點是需要實現重試邏輯,可能影響用戶體驗。
  • 選擇悲觀鎖的理由:由于當前項目涉及支付,對數據一致性和完整性要求極高,而且提示用戶重試會影響體驗,因此更適合使用悲觀鎖。

3.悲觀鎖

3.1悲觀鎖是什么?

它認為數據隨時都有可能被別人修改。所以,只要在操作數據之前,它就先把數據給鎖起來。

-悲觀鎖里,又分為共享鎖和排它鎖

  • 共享鎖:就是大家共享的。一個資源,允許同時存在多個共享鎖。每個事務,都可以讀到這條記錄。但是要想修改、刪除,必須等其他共享鎖都釋放后,才能執行

3.1.1. 共享鎖實踐FOR SHARE

訂單表為例:
重置一條數據

UPDATE `Orders` SET `tradeNo` = NULL, `paidAt` = NULL,`version` = 0, `status` = 0 WHERE `id` = 4;
UPDATE `Users` SET `role` = 0, `membershipExpiredAt` = NULL WHERE `id` = 2;

接著我們開啟兩個數據庫客戶端,都運行相同的 SQL
加上FOR SHARE就是共享鎖

-- 開始一個新的事務
START TRANSACTION;-- 使用共享鎖,查詢訂單表
SELECT `tradeNo`, `paidAt`, `status` FROM `Orders` WHERE `id` = 4 FOR SHARE;-- 更新訂單表
UPDATE `Orders` SET `tradeNo` = '2024121322001495001404593598', `paidAt` = '2024-12-13 12:35:31', `status` = 1 WHERE `id` = 4;-- 提交事務
COMMIT;
  • 在 A客戶端先運行 開啟事務 及 查詢訂單表sql
    在這里插入圖片描述
    運行一下,可以看到查到東西了。

  • 在 B 客戶端,也運行前兩條語句:

  • 在這里插入圖片描述

因為加的是共享鎖,所以 B 客戶端,也能查到東西

  • 繼續在 A 客戶端,運行更新訂單表:
    在這里插入圖片描述
    注意了,出現彈窗,提示我們正在運行查詢。

這就是剛說的,一條數據,可以有多個共享鎖都可以查到東西。但是想要修改數據,就必須得等其他所有的共享鎖都釋放了,才能修改

  • 現在去 B 客戶端了,運行一下提交事務:
    在這里插入圖片描述
    可以看到提示信息馬上就消失了,執行成功了。這就是因為,除了 A 客戶端里的當前事務外,其他事務的鎖都釋放了,所以可以修改了。
  • 最后在 A 客戶端里,也點一下提交事務
    在這里插入圖片描述
事務 A事務 B
開啟事務
查詢數據,并加共享鎖
查詢成功
開啟事務
查詢數據,并加共享鎖
查詢成功
修改數據,發現有其他事務的共享鎖,
等待釋放中…
提交事務,釋放了鎖
執行成功
提交事務,釋放了鎖
  • 排它鎖:一個資源,同一時間只允許存在一個排它鎖。其他事務想要加鎖,必須得等待當前事務操作完成,解鎖后才行。所以,在鎖定期間,其他事務不能讀取,更不能修改和刪除了

3.1.2 排它鎖實踐 FOR UPDATE

訂單表為例:
重置一條數據

UPDATE `Orders` SET `tradeNo` = NULL, `paidAt` = NULL,`version` = 0, `status` = 0 WHERE `id` = 4;
UPDATE `Users` SET `role` = 0, `membershipExpiredAt` = NULL WHERE `id` = 2;

接著我們開啟兩個數據庫客戶端,都運行相同的 SQL
加上FOR UPDATE就是排它鎖

  • A 客戶端 先運行前兩句
-- 開始一個新的事務
START TRANSACTION;-- 使用共享鎖,查詢訂單表
SELECT `tradeNo`, `paidAt`, `status` FROM `Orders` WHERE `id` = 4 FOR SHARE;-- 更新訂單表
UPDATE `Orders` SET `tradeNo` = '2024121322001495001404593598', `paidAt` = '2024-12-13 12:35:31', `status` = 1 WHERE `id` = 4;-- 提交事務
COMMIT;

B 客戶端,運行前兩條語句(同上)
在這里插入圖片描述
發現現在進行查詢都會卡住,這就是排它鎖A 對數據加上了排它鎖后,其他事務就不能再加排它鎖了。不給你查,更不給你修改刪除。必須等我執行完成后,你再執行。

  • 在 A 客戶端,點擊提交事務:
  • 在這里插入圖片描述
  • B 客戶端,就馬上可以執行成功了。接著在 B 客戶端里也點一下提交事務,將鎖釋放掉
  • 在這里插入圖片描述
    以下是將你提供的內容轉換為規范 Markdown 表格的形式,這樣在 CSDN 等平臺通常能正常顯示:
事務 A事務 B
開啟事務
查詢數據,并加排他鎖
查詢成功
開啟事務
查詢數據,并加排他鎖
發現有其他事務的排他鎖,
等待釋放中…
提交事務,釋放了鎖
查詢成功
提交事務,釋放了鎖

這里使用 <br> 來處理換行,保證在支持 HTML 的 Markdown 渲染環境下能正確顯示換行內容。如果在 CSDN 上還是顯示異常,可以嘗試將整個表格代碼放入代碼塊(用三個反引號包裹)中。

3.2.Node 項目中實現悲觀鎖

3.2.1.如果你跟著文章內容學習了共享鎖 請去掉version: true

  • 打開models/order.js
Order.init({// ...
}, {sequelize,modelName: 'Order',// version: true, // 樂觀鎖
});

3.2.2. 共享鎖的實現 lock: t.LOCK.SHARE

打開routes/alipay.js,找到paidSuccess這里

const order = await Order.findOne({where: { outTradeNo: outTradeNo },transaction: t,lock: t.LOCK.SHARE,   // 增加共享鎖
});// 其他...const user = await User.findByPk(order.userId, {transaction: t,lock: t.LOCK.SHARE,   // 增加共享鎖
});
  • 非常簡單的找到查詢當前訂單查詢當前用戶這兩個地方。
  • 增加上lock: t.LOCK.SHARE,這就是共享鎖了。

打開 Apifox,調用下主動查詢支付寶接口:
在這里插入圖片描述
然后觀察終端中的 SQL 語句:在這里插入圖片描述

3.2.3. 排它鎖的實現 lock: t.LOCK.UPDATE

const order = await Order.findOne({where: { outTradeNo: outTradeNo },transaction: t,lock: t.LOCK.UPDATE,   // 增加排它鎖
});// 其他...const user = await User.findByPk(order.userId, {transaction: t,lock: t.LOCK.UPDATE,   // 增加排它鎖
});

先運行下 SQL重置一下數據庫

UPDATE `Orders` SET `tradeNo` = NULL, `paidAt` = NULL,`version` = 0, `status` = 0 WHERE `id` = 4;
UPDATE `Users` SET `role` = 0, `membershipExpiredAt` = NULL WHERE `id` = 2;

再次調用 Apifox,觀察終端中運行的語句:

在這里插入圖片描述
根據官方文檔的說明,排它鎖還可以簡寫成lock: true,我們修改下代碼

const order = await Order.findOne({where: { outTradeNo: outTradeNo },transaction: t,lock: true,   // 增加排它鎖
});// 其他...const user = await User.findByPk(order.userId, {transaction: t,lock: true,   // 增加排它鎖
});

4.使用共享鎖,還是排它鎖?

那么到底是應該用共享鎖,還是排它鎖呢?在我們這個項目里,訂單狀態未支付變為已支付,這是一個關鍵的業務操作,必須確保一致性,這里使用排它鎖更為適合:

  • 這樣當前事務在處理的時候,其他事務就不能讀取了,更不能對這條記錄進行修改操作。
  • 必須要等待當前事務執行完成后,其他事務才能進行操作。
  • 這樣就確保了,在同一時間內只有一個事務可以操作當前數據,保證了數據的一致性和完整性。

5.總結

5.1. 樂觀鎖與悲觀鎖的適用場景

  • 樂觀鎖:適用于讀多寫少的場景,尤其是在高并發環境下,沖突發生的概率較低時。它假設數據在大多數情況下不會被修改,因此在提交更新之前不需要加鎖,而是在提交時,通過版本號時間戳檢查是否有沖突發生。
  • 悲觀鎖:適用于寫操作頻繁、沖突可能性較高的情況下。它假設經常發生沖突,因此在執行任何可能引起沖突的操作前都會先加鎖,以確保數據的一致性和完整性。

5.2. 悲觀鎖的類型及其區別

  • 共享鎖(S 鎖或讀鎖)
    當前事務加共享鎖后,允許其他事務也對該資源加共享鎖,但禁止其他事務對該資源加排它鎖
    在其他事務的共享鎖沒有釋放之前,當前事務和其他事務都禁止對該資源進行修改操作。
  • 排它鎖(X 鎖或寫鎖)
    當前事務加排它鎖后,禁止其他事務對該資源加共享鎖排它鎖
    排它鎖確保只有一個事務可以對鎖定的數據進行讀取和修改。在排它鎖未被釋放之前,其他事務不能對該資源加任何形式的鎖。

5.3. 行鎖與全表掃描的影響

加鎖時的where條件里,命中索引至關重要。只有通過索引條件來檢索數據,才能確保MySQL的InnoDB引擎能夠精確地鎖定當前記錄,這叫做行鎖,也就是只鎖定一行記錄。
但如果查詢的數據無法命中索引,MySQL不得不從頭開始逐行掃描整個表,直到找到對應的數據,期間會將所有遇到的數據行全都加鎖。這種情況雖然不是嚴格意義上的表鎖,但在效果上幾乎等同于表鎖。它會阻塞其他事務對這些行的操作,顯著降低并發性能。

重點:

數據庫事務里加鎖,如果沒有命中索引,就會造成表鎖問題。
除了可以給字段,分別單獨加索引外,還可以創建聯合索引
聯合索引的順序非常重要,要注意最左前綴原則
加鎖后 where 后的條件 一定要加索引

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/72602.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/72602.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/72602.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Android 創建一個全局通用的ViewModel

&#xff08;推薦&#xff09;使用ViewModelStore 代碼示例&#xff1a; class MyApplication : Application(), ViewModelStoreOwner {private val mViewModelStore ViewModelStore()override fun onCreate() {super.onCreate()}override val viewModelStore: ViewModelSto…

SCI期刊推薦 | 免版面費 | 計算機領域:信息系統、軟件工程、自動化和控制

在學術研究領域&#xff0c;選擇合適的SCI期刊對科研成果的傳播與認可至關重要。了解SCI期刊的研究領域和方向是基礎&#xff0c;確保投稿內容與期刊主題相符。同時&#xff0c;要關注期刊的影響因子和評估標準&#xff0c;選擇具有較高影響力和學術認可度的期刊。閱讀期刊的投…

解鎖Android RemoteViews:跨進程UI更新的奧秘

一、RemoteViews 簡介 在 Android 開發的廣闊領域中&#xff0c;RemoteViews 是一個獨特且重要的概念&#xff0c;它為開發者提供了一種在其他進程中顯示視圖結構的有效方式。從本質上講&#xff0c;RemoteViews 并非傳統意義上在當前應用進程內直接渲染和操作的 View&#xf…

常見webshell工具的流量特征

1、蟻劍 1.1、蟻劍webshell靜態特征 蟻劍中php使用assert、eval執行&#xff1b;asp只有eval執行&#xff1b;在jsp使用的是Java類加載&#xff08;ClassLoader&#xff09;&#xff0c;同時會帶有base64編碼解碼等字符特征。 1.2、蟻劍webshell動態特征 查看流量分析會發現…

爬蟲系列之【數據解析之bs4】《四》

目錄 前言 一、用法詳解 1.1 獲取標簽內容 1.2 獲取標簽屬性 1.3 獲取標簽包裹的文本內容 1.4 獲取標簽列表 1.5 css 選擇器&#xff1a;select 二、實戰案例 完整代碼 前言 HTML數據解析 1、正則 2、xpath&#xff08;居多&#xff09; 3、css 選擇器&#xff08;bs…

Java-實現PDF合同模板填寫內容并導出PDF文件

可用于公司用戶合同導出pdf文件 效果圖 一、導入所需要jar包 <!--生成PDF--><dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.11</version></dependency><dependency&…

【人工智能】GPT-4 vs DeepSeek-R1:誰主導了2025年的AI技術競爭?

前言 2025年&#xff0c;人工智能技術將迎來更加激烈的競爭。隨著OpenAI的GPT-4和中國初創公司DeepSeek的DeepSeek-R1在全球范圍內嶄露頭角&#xff0c;AI技術的競爭格局開始發生變化。這篇文章將詳細對比這兩款AI模型&#xff0c;從技術背景、應用領域、性能、成本效益等多個方…

前端開發10大框架深度解析

摘要 在現代前端開發中&#xff0c;框架的選擇對項目的成功至關重要。本文旨在為開發者提供一份全面的前端框架指南&#xff0c;涵蓋 React、Vue.js、Angular、Svelte、Ember.js、Preact、Backbone.js、Next.js、Nuxt.js 和 Gatsby。我們將從 簡介、優缺點、適用場景 以及 實際…

【MySQL】索引(頁目錄、B+樹)

文章目錄 1. 引入索引2. MySQL與磁盤交互的基本單位3. 索引的理解3.1 頁目錄3.2 B樹 4. 聚簇索引、非聚簇索引5. 索引的操作5.1 索引的創建5.1.1 創建主鍵索引5.1.2 創建唯一索引5.1.3 普通索引的創建5.1.4 全文索引的創建 5.2 索引的查詢5.3 刪除索引 1. 引入索引 索引&#…

python-串口助手(OV7670圖傳)

代碼 主python文件 import serial import serial.tools.list_ports import time import tkinter as tk from tkinter import ttk import numpy as np from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure import threadi…

筑牢網絡安全防線:守護您的數據安全

在數字化時代&#xff0c;數據安全已成為企業和個人不容忽視的重要議題。近日印尼國家數據中心遭黑客襲擊的事件&#xff0c;不僅擾亂了機場的移民檢查&#xff0c;還影響了眾多機構的服務運行。黑客利用惡意軟件對數據中心進行攻擊&#xff0c;索要巨額贖金&#xff0c;給印尼…

Vue 3 整合 WangEditor 富文本編輯器:從基礎到高級實踐

本文將詳細介紹如何在 Vue 3 項目中集成 WangEditor 富文本編輯器&#xff0c;實現圖文混排、自定義擴展等高階功能。 一、為什么選擇 WangEditor&#xff1f; 作為國內流行的開源富文本編輯器&#xff0c;WangEditor 具有以下優勢&#xff1a; 輕量高效&#xff1a;壓縮后僅…

FastGPT 引申:信息抽取到知識圖譜的銜接流程

文章目錄 信息抽取到知識圖譜的銜接流程步驟1&#xff1a;原始信息抽取結果步驟2&#xff1a;數據標準化處理&#xff08;Python示例&#xff09;步驟3&#xff1a;Cypher代碼動態生成&#xff08;Python驅動&#xff09; 關鍵銜接邏輯說明1. 唯一標識符生成規則2. 數據映射策略…

Webshell 入侵與防御全攻略

Webshell&#xff0c;是指攻擊者上傳到網站的遠程控制后門&#xff0c;允許黑客像管理員一樣遠程控制網站&#xff0c;執行惡意命令&#xff0c;甚至完全接管網站。本文將帶你深入了解 Webshell 的入侵方式以及相應的防御措施&#xff0c;幫助你加固自己的網站防線。 什么是 W…

NL2SQL-基于Dify+阿里通義千問大模型,實現自然語音自動生產SQL語句

本文基于Dify阿里通義千問大模型&#xff0c;實現自然語音自動生產SQL語句功能&#xff0c;話不多說直接上效果圖 我們可以試著問他幾個問題 查詢每個部門的員工數量SELECT d.dept_name, COUNT(e.emp_no) AS employee_count FROM employees e JOIN dept_emp de ON e.emp_no d…

雙鏈路提升網絡傳輸的可靠性擴展可用帶寬

為了提升網絡傳輸的可靠性或增加網絡可用帶寬&#xff0c; 通常使用雙鏈路冗余備份或者雙鏈路聚合的方式。 本文介紹幾種雙鏈路網絡通信的案例。 5GWiFi冗余傳輸 雙Socket綁定不同網絡接口&#xff1a;通過Android的ConnectivityManager綁定5G蜂窩網絡和WiFi的Socket連接&…

Ubuntu22.04安裝Ollama部署DeepSeek-R1:32B模型

一、環境準備 1.硬件要求 GPU: 至少 NVIDIA A30/A100 (顯存 ≥ 24GB)內存: ≥ 64GB RAM存儲: ≥ 100GB 可用空間 (模型文件約 60GB)2.軟件依賴 # 驗證NVIDIA驅動 nvidia-smi二、Ollama安裝 方法 1:install.sh安裝 運行一下安裝命令: curl -fsSL https://ollama.com/inst…

LeetCode 解題思路 10(Hot 100)

解題思路&#xff1a; 上邊&#xff1a; 從左到右遍歷頂行&#xff0c;完成后上邊界下移&#xff08;top&#xff09;。右邊&#xff1a; 從上到下遍歷右列&#xff0c;完成后右邊界左移&#xff08;right–&#xff09;。下邊&#xff1a; 從右到左遍歷底行&#xff0c;完成后…

Checkpoint 模型與Stable Diffusion XL(SDXL)模型的區別

Checkpoint 模型與 Stable Diffusion XL&#xff08;SDXL&#xff09;模型 在功能、架構和應用場景上有顯著區別&#xff0c;以下是主要差異的總結&#xff1a; 1. 基礎架構與定位 Checkpoint 模型 是基于 Stable Diffusion 官方基礎模型&#xff08;如 SD 1.4/1.5&#xff09;…

GCC RISCV 后端 -- C語言語法分析過程

在 GCC 編譯一個 C 源代碼時&#xff0c;先會通過宏處理&#xff0c;形成 一個叫轉譯單元&#xff08;translation_unit&#xff09;&#xff0c;接著進行語法分析&#xff0c;C 的語法分析入口是 static void c_parser_translation_unit(c_parser *parser); 接著就通過類似遞…