数据库事务、乐观锁及悲观锁

参考: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.mzph.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); 接着就通过类似递…