数据库事务、乐观锁及悲观锁
参考: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包住所有代码。 - 去掉
commit和rollback。因为这种写法,会自动提交和回滚。 - 其他地方都和之前一样。
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不得不从头开始逐行扫描整个表,直到找到对应的数据,期间会将所有遇到的数据行全都加锁。这种情况虽然不是严格意义上的表锁,但在效果上几乎等同于表锁。它会阻塞其他事务对这些行的操作,显著降低并发性能。
相关文章:
数据库事务、乐观锁及悲观锁
参考:node支付宝支付及同步、异步通知、主动查询支付宝订单状态 以下容结合上述链接查看 1. 什么是数据库事务? 1.1. 连续执行数据库操作 在支付成功后,我们在自定义的paidSuccess里,依次更新了订单状态和用户信息。也就说这里…...
蓝桥王国--dij模板
#include <bits/stdc.h> // 万能头 using namespace std; typedef pair<long long ,int> PII; int n,m; long long d[300011]; struct edge///邻接表 {int v;long long w; }; int vis[300011]; vector<edge> mp[300011];///邻接表 void dij(int s)///dij单源…...
Java基础关键_017_集合(一)
目 录 一、概述 二、Collection 关系结构 1.概览 2.说明 三、Collection 接口 1.通用方法 (1)add(E e) (2)size() (3)addAll(Collection c) (4)contains(Object o) &#…...
Rust编程实战:Rust实现简单的Web服务,单线程性能问题
知识点 tcp 服务多线程处理 实现功能 启动web服务,访问链接获取页面内容。 单线程web服务 TcpListener 使用 TcpListener 开启服务端口 let listener TcpListener::bind("127.0.0.1:7878").unwrap();处理客户端连接: for stream in lis…...
GitLab 密钥详解:如何安全地使用 SSH 密钥进行身份验证
目录 一、什么是 GitLab SSH 密钥?二、为什么要使用 SSH 密钥?三、如何生成 SSH 密钥?1. Linux/macOS2. Windows 四、将公钥添加到 GitLab五、配置 SSH 客户端六、常见问题及解决方案七、总结 GitLab 是一个功能强大的 Git 仓库管理平台&…...
《论数据分片技术及其应用》审题技巧 - 系统架构设计师
论数据分片技术及其应用写作框架 一、考点概述 本论题“论数据分片技术及其应用”主要考察的是软件工程中数据分片技术的理解、应用及其实际效果分析。考点涵盖以下几个方面: 首先,考生需对数据分片的基本概念有清晰的认识,理解数据分片是…...
【C++】当一个类A中没有声明任何成员变量和成员函数,sizeof(A)是多少?
在 C 中,即使一个类没有任何数据成员(即空类),它的大小也不会是 0,而是 1。这主要有以下几个原因: 地址唯一性要求 C 标准规定,每个对象都必须有唯一的地址。如果空类的大小为 0,那么…...
Maven 私服的搭建与使用(一)
一、引言 在 Java 项目开发中,Maven 作为强大的项目管理和构建工具,极大地提高了开发效率,而 Maven 私服在开发过程中也扮演着至关重要的角色。私服是一种特殊的远程仓库,架设在局域网内,代理广域网上的远程仓库&…...
Ubuntu20.04双系统安装及软件安装(五):VSCode
Ubuntu20.04双系统安装及软件安装(五):VSCode 打开VScode官网,点击中间左侧的deb文件下载: 系统会弹出下载框,确定即可。 在文件夹的**“下载”目录**,可看到下载的安装包,在该目录下…...
linux网络(3)—— socket编程(1)socket基础认识
欢迎来到博主的专栏:linux网络 博主ID:代码小豪 文章目录 IP与端口号socket字节序问题 IP与端口号 我们现在知道了,只要发送的报文的报头包含目的IP地址和源IP地址,就能通过通信设备,是两台主机进行远程通信ÿ…...
【Kubernets】K8S内部nginx访问Service资源原理说明
文章目录 原理概述**一、核心概念****二、Nginx 访问 Service 的流程****1. Service 的作用****2. Endpoint 的作用****3. Nginx Pod 发起请求****(1) DNS 解析****(2) 流量到达 kube-proxy****(3) 后端 Pod 处理请求** **三、不同代理模式的工作原理****1. iptables 模式****2…...
使用Docker搭建Oracle Database 23ai Free并扩展MAX_STRING_SIZE的完整指南
使用Docker搭建Oracle Database 23ai Free并扩展MAX_STRING_SIZE的完整指南 前言环境准备目录创建启动Docker容器 数据库配置修改进入容器启动SQL*PlusPDB操作与字符串扩展设置配置验证 管理员用户创建注意事项总结 前言 本文将详细讲解在Docker环境中配置Oracle Database 23a…...
使用pytorch和opencv根据颜色相似性提取图像
需求:将下图中的花朵提取出来。 代码: import cv2 import torch import numpy as np import timedef get_similar_colors(image, color_list, threshold):# 将图像和颜色列表转换为torch张量device torch.device(cuda if torch.cuda.is_available() el…...
MySQL 8.X 报错处理
1.重新加载配置 reload the configuration mysql> ALTER INSTANCE RELOAD KEYRING; ERROR 1227 (42000): Access denied; you need (at least one of) the ENCRYPTION_KEY_ADMIN privilege(s) for this operation 提示需要ENCRYPTION_KEY_ADMIN权限 重新授权 GRANT ENCR…...
Ubuntu 22.04安装OpenJDK 17
步骤一:更新软件包 sudo apt update步骤二:安装openjdk-17 sudo apt install openjdk-17-jdk当系统要求输入密码时,请输入密码。然后键入 Y 并按 Enter 继续安装 步骤三:查看安装版本 java -version步骤四:查看安装…...
【时序预测】时间序列有哪些鲁棒的归一化方法
时间序列数据在金融、气象、医疗等领域中广泛存在,而股票数据作为典型的时间序列之一,具有非平稳性、噪声多、波动大等特点。为了更好地进行数据分析和建模,归一化是一个重要的预处理步骤。然而,由于时间序列数据的特殊性…...
nlp第九节——文本生成任务
一、seq2seq任务 特点:输入输出均为不定长的序列 自回归语言模型: 由前面一个字预测下一个字的任务 encoder-decoder结构: Encoder-Decoder结构是一种基于神经网络完成seq2seq任务的常用方案 Encoder将输入转化为向量或矩阵,其…...
STM32MP1xx的启动流程
https://wiki.st.com/stm32mpu/wiki/Boot_chain_overview 根据提供的知识库内容,以下是STM32 MPU启动链的详细解析: 1. 通用启动流程 STM32 MPU启动分为多阶段,逐步初始化外设和内存,并建立信任链: 1.1 ROM代码&…...
wgcloud-server端部署说明
Wgcloud 是一款开源的轻量级服务器监控系统,支持多平台,可对服务器的 CPU、内存、磁盘、网络等指标进行实时监控。 以下是 Wgcloud Server端的详细部署步骤: 环境准备 服务器: 至少准备两台服务器,一台作为监控端&a…...
大模型Agent:人工智能的崭新形态与未来愿景
在人工智能技术高歌猛进的当下,大模型 Agent 作为 AI 领域的关键研究方向,正日益彰显出其独有的魅力以及广阔无垠的应用前景。大模型 Agent 不但具备对环境的感知、自主的理解、决策的制定以及行动的执行能力,而且能够游刃有余地应对繁杂任务…...
业务系统对接大模型的基础方案:架构设计与关键步骤
业务系统对接大模型:架构设计与关键步骤 在当今数字化转型的浪潮中,大语言模型(LLM)已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中,不仅可以优化用户体验,还能为业务决策提供…...
【Java学习笔记】Arrays类
Arrays 类 1. 导入包:import java.util.Arrays 2. 常用方法一览表 方法描述Arrays.toString()返回数组的字符串形式Arrays.sort()排序(自然排序和定制排序)Arrays.binarySearch()通过二分搜索法进行查找(前提:数组是…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...
Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
大模型多显卡多服务器并行计算方法与实践指南
一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...
成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战
在现代战争中,电磁频谱已成为继陆、海、空、天之后的 “第五维战场”,雷达作为电磁频谱领域的关键装备,其干扰与抗干扰能力的较量,直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器,凭借数字射…...
Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)
目录 一、👋🏻前言 二、😈sinx波动的基本原理 三、😈波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、🌊波动优化…...
关键领域软件测试的突围之路:如何破解安全与效率的平衡难题
在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件,这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下,实现高效测试与快速迭代?这一命题正考验着…...
