1)事务
一句话
一致性是目标,事务是手段。
一、事务(Transaction)的概念和特性
事务是一组操作的集合,要么全部成功,要么全部失败。
假如事务中的某一步操作失败,系统会回滚之前的所有操作,让数据恢复到事务开始前的状态。
事务的特性:ACID
事务具有四大特性,简称 ACID:
原子性(Atomicity):
- 事务是不可分割的,所有操作要么全部成功,要么全部失败。
- 比喻:买东西时,钱和商品的交换是原子性的,要么交易成功,要么双方恢复原样。
一致性(Consistency):
- 事务完成后,数据必须满足系统定义的规则,不能出现不符合规则的状态。
- 比喻:银行转账后,总金额不变。
隔离性(Isolation):
- 不同事务之间互不干扰,事务执行的结果不受其他事务的影响。
- 比喻:超市里,两个顾客结账互不影响。
持久性(Durability):
- 一旦事务提交成功,数据的变更将永久保存,即使系统崩溃也不会丢失。
- 比喻:存款成功后,银行的账本永远记下这笔钱。
二、事务的生命周期
事务的生命周期包括四个阶段:
- 开始事务。
- 执行操作。
- 提交事务或回滚事务。
- 结束事务。
三、事务的分类:它的类型有哪些?
按执行模式分类
- 手动事务:
- 开发人员手动控制事务的开始、提交和回滚。
- 示例:
BEGIN
,COMMIT
,ROLLBACK
。
- 自动事务:
- 数据库管理系统自动处理事务,开发人员无需显式控制。
- 示例:简单的
INSERT
或UPDATE
。
按事务操作范围分类
- 本地事务:
- 在单个数据库上执行操作。
- 示例:MySQL 的事务。
- 分布式事务:
- 涉及多个数据库或服务,通常需要两阶段提交协议(2PC)或三阶段提交协议(3PC)。
- 示例:微服务架构中的跨服务操作。
按隔离级别分类
(解决多个事务并发执行的问题)
如果没有指定隔离级别,数据库就会使用默认的隔离级别。在 MySQL 中,如果使用 InnoDB,默认的隔离级别是 Repeatable Read。
- 读未提交(Read Uncommitted):最低隔离级别,可能会读到未提交的数据。
在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是 脏读(Dirty Read)。
- 读已提交(Read Committed):只能读到已提交的数据。
在 Read Committed 隔离级别下,一个事务不会读到另一个事务还没有提交的数据,但可能会遇到 不可重复读(Non Repeatable Read)的问题。不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。
- 可重复读(Repeatable Read):保证一个事务中的多次读操作一致。
在 Repeatable Read 隔离级别下,一个事务可能会遇到 幻读(Phantom Read)的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。
- 串行化(Serializable):最高隔离级别,事务完全串行化执行。
Serializable 是最严格的隔离级别。在 Serializable 隔离级别下,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。虽然 Serializable 隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用 Serializable 隔离级别。
提示:脏读、不可重复读、幻读的出现场景
现象 | 发生条件 | 结果 | 举例 |
---|---|---|---|
脏读 | 事务 A 修改数据但未提交,事务 B 读取该数据 | 事务 B 可能读到尚未提交的脏数据 | 事务 A 扣款操作未提交,事务 B 查询余额并开始转账,最终事务 A 回滚导致事务 B 操作基于错误数据 |
不可重复读 | 事务 A 多次读取同一数据,事务 B 在此期间修改并提交数据 | 事务 A 读取数据时,数据可能发生变化 | 事务 A 查询商品库存为 10,事务 B 更新库存为 5 并提交,事务 A 再次查询时,库存变为 5 |
幻读 | 事务 A 执行查询,事务 B 插入或删除满足查询条件的数据 | 事务 A 查询的结果集发生变化 | 事务 A 查询金额大于 100 的订单,查询结果为空;事务 B 插入一笔金额为 120 的订单,事务 A 再次查询时发现该订单 |
隔离级别从 读未提交 到 串行化,随着级别的提高,系统的并发性能逐渐下降,但数据一致性和安全性得到了增强。
一般情况下,读未提交 和 串行化 的隔离级别很少使用,读已提交(RC) 和 可重复读(RR) 是较为常见的隔离级别。
按使用场景分类
- 单操作事务:简单的一次增删改操作。
- 批处理事务:需要多个步骤共同完成的任务。
- 长事务:需要长时间运行,通常用在大型数据处理或流式计算中。
四、事务的使用:它是如何使用的?
数据库中的事务使用
SQL 示例(以 MySQL 为例):
sqlSTART TRANSACTION; -- 开始事务 UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 扣款 UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 收款 COMMIT; -- 提交事务
事务回滚示例:
sqlSTART TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; ROLLBACK; -- 发生异常,撤销所有更改
在后端代码中的使用
示例 1:Java 示例(Spring JDBC):
@Transactional
public void transferMoney(int fromAccount, int toAccount, int amount) {
accountRepository.debit(fromAccount, amount);
accountRepository.credit(toAccount, amount);
}
示例 2:Go 示例(使用 database/sql
包):
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
defer tx.Rollback()
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1)
if err != nil {
log.Fatal(err)
}
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", 100, 2)
if err != nil {
log.Fatal(err)
}
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
示例 3:使用 MySQL 和 Express 的事务管理:
假设我们使用 mysql2
或 sequelize
等库来连接数据库,在 Express 路由中执行事务。
1. 使用 mysql2 库管理事务:
首先需要安装 mysql2
:
npm install mysql2
然后在 Express 路由中进行事务控制:
const express = require("express");
const mysql = require("mysql2");
const app = express();
// 创建 MySQL 连接池
const pool = mysql.createPool({
host: "localhost",
user: "root",
password: "password",
database: "bank",
});
// 转账 API
app.post("/transfer", (req, res) => {
const { fromAccount, toAccount, amount } = req.body;
// 开始一个新的事务
pool.getConnection((err, connection) => {
if (err) {
return res.status(500).json({ error: "Database connection failed" });
}
connection.beginTransaction((err) => {
if (err) {
connection.release();
return res.status(500).json({ error: "Transaction failed to start" });
}
// 扣款
connection.query(
"UPDATE accounts SET balance = balance - ? WHERE id = ?",
[amount, fromAccount],
(err, results) => {
if (err) {
return connection.rollback(() => {
connection.release();
res.status(500).json({ error: "Transaction error during debit" });
});
}
// 收款
connection.query(
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
[amount, toAccount],
(err, results) => {
if (err) {
return connection.rollback(() => {
connection.release();
res
.status(500)
.json({ error: "Transaction error during credit" });
});
}
// 提交事务
connection.commit((err) => {
if (err) {
return connection.rollback(() => {
connection.release();
res
.status(500)
.json({ error: "Transaction commit failed" });
});
}
connection.release();
res.status(200).json({ message: "Transaction successful" });
});
}
);
}
);
});
});
});
// 启动 Express 服务
app.listen(3000, () => {
console.log("Server is running on port 3000");
});
在这个例子中:
- 我们创建了一个 MySQL 连接池来连接数据库。
- 使用事务控制开始、执行操作、提交或回滚操作。
- 如果在执行任何查询时发生错误,事务会回滚,确保数据库的原子性。
2. 使用 Sequelize 库管理事务:
Sequelize 是一个 ORM 库,可以更方便地管理事务。 首先安装 Sequelize:
npm install sequelize mysql2
然后在 Express 路由中使用 Sequelize 进行事务管理:
const express = require("express");
const { Sequelize, DataTypes } = require("sequelize");
const app = express();
// 创建 Sequelize 实例
const sequelize = new Sequelize("mysql://root:password@localhost:3306/bank");
// 定义 Account 模型
const Account = sequelize.define("Account", {
id: { type: DataTypes.INTEGER, primaryKey: true },
balance: DataTypes.INTEGER,
});
// 转账 API
app.post("/transfer", async (req, res) => {
const { fromAccount, toAccount, amount } = req.body;
const t = await sequelize.transaction(); // 开始事务
try {
// 扣款
const debitAccount = await Account.findByPk(fromAccount, {
transaction: t,
});
if (!debitAccount || debitAccount.balance < amount) {
throw new Error("Insufficient balance");
}
debitAccount.balance -= amount;
await debitAccount.save({ transaction: t });
// 收款
const creditAccount = await Account.findByPk(toAccount, { transaction: t });
if (!creditAccount) {
throw new Error("Account not found");
}
creditAccount.balance += amount;
await creditAccount.save({ transaction: t });
// 提交事务
await t.commit();
res.status(200).json({ message: "Transaction successful" });
} catch (error) {
// 回滚事务
await t.rollback();
res.status(500).json({ error: error.message });
}
});
// 启动 Express 服务
app.listen(3000, () => {
console.log("Server is running on port 3000");
});
在这个示例中:
- 使用 Sequelize 创建了一个
Account
模型。 - 使用
sequelize.transaction()
来管理事务。 - 在事务中进行余额的更新,遇到错误时通过
t.rollback()
回滚事务。
分布式事务的使用
基于两阶段提交(2PC):
- 准备阶段:所有参与者锁定资源,准备提交。
- 提交阶段:如果所有参与者都准备成功,提交事务,否则回滚。
基于消息队列(MQ)的最终一致性:
- 将事务结果以消息形式写入队列。
- 消费者处理消息,完成操作,确保最终一致性。
五、事务的注意点:它有什么需要注意的?
性能影响:
- 长事务会锁定资源,可能导致其他事务等待,影响性能。
- 解决方案:将事务范围缩小到必要的部分。
解决方案:将事务范围缩小到必要的部分
精确划分事务范围
- 只对关键操作进行事务保护,避免将非关键操作(如日志记录、缓存更新等)放入事务中。
- 避免冗余的数据库查询,确保每个查询和更新都必要。
使用短小的事务
- 缩短事务执行时间,避免在事务内进行复杂计算或外部 API 调用。
- 尽早提交或回滚事务,避免长时间持有锁。
分批处理大任务
- 批量操作拆分为多个小事务,分别处理每个小任务,减少锁定时间。
- 使用分页查询和更新,减少一次性处理大量数据导致的事务锁定。
异步操作与事务结合
- 将非关键操作(如日志记录、缓存更新等)转为异步操作,避免事务中不必要的等待。
- 分离读写操作,读取操作可以在事务外进行,写入操作仅在必要时使用事务。
避免长时间持有锁
- 加锁粒度细化,采用行级锁而非表级锁,减少对其他事务的影响。
- 使用乐观锁,只有在提交时检查数据是否发生冲突。
合理配置数据库隔离级别
- 选择合适的隔离级别,如使用读已提交而不是可重复读,以减少锁竞争。
使用数据库特性进行优化
- 优化数据库索引,减少查询和更新的耗时,缩短事务执行时间。
- 优化数据库连接池配置,减少事务等待数据库连接的时间。
死锁问题:
- 两个事务因资源互相等待而无法完成。
- 解决方案:设计良好的锁顺序和超时策略。
解决方案:设计良好的锁顺序和超时策略
设计良好的锁顺序
- 统一锁的顺序,确保所有事务对资源的访问顺序一致。
- 最小化锁的持有时间,减少在锁定资源期间进行复杂操作或等待。
使用超时策略
- 设置事务超时,确保事务在规定时间内完成,否则主动回滚。
- 设置锁超时,避免事务在等待锁时一直阻塞,达到超时后回滚事务。
使用乐观锁而非悲观锁
- 使用乐观锁,在提交时检查数据是否被修改,避免死锁。
- 避免使用悲观锁,减少死锁的发生风险。
避免长时间的事务
- 保持事务尽量简短,避免长时间锁定资源。
- 将大事务拆分为多个小事务,并确保每个小事务尽早提交。
使用数据库的死锁检测机制
- 利用数据库自带的死锁检测机制,自动回滚其中一个事务解除死锁。
- 分析数据库返回的死锁信息,优化应用逻辑。
死锁预防(预防死锁的算法)
- 使用资源分配图或等待图来检查事务是否可能引发死锁,避免死锁发生。
定期检查并手动解决死锁
- 定期监控系统事务,发现长时间处于等待状态的事务,手动识别并解决死锁问题。
隔离级别选择:
- 高隔离级别会降低并发性能,需要权衡数据一致性和性能之间的关系。
分布式事务的复杂性:
- 分布式事务因网络延迟、失败风险高,需要引入补偿机制或最终一致性模型。
解决方案:补偿机制或最终一致性模型
TODO
- 幂等性:
- 确保事务操作在重试时不会引起副作用。
- 示例:向用户账户添加积分时,确保重复请求不会导致重复加分。
六、事务的扩展方向
现代事务模型
- 乐观锁与悲观锁:
- 乐观锁:假设没有冲突,提交时验证。
- 悲观锁:在操作前锁定资源。
- 无锁事务:
- 一些新型数据库使用无锁算法(如 MVCC,Multi-Version Concurrency Control)实现高并发性能。
事务与微服务
- 微服务架构中,事务多用Saga 模式和 TCC(Try-Confirm-Cancel)模式 处理分布式事务。