事务
事务是什么?
事务是指访问并可能跟新数据库中各项数据的一个程序执行单元(Unit)
定义
一组操作要么全部成功,要么全部失败,目的是为了保证最终数据的一致性。
开启事务操作
sql
-- 方式1:使用BEGIN
BEGIN;
UPDATE users SET balance = balance - 100 WHERE id = 1;
UPDATE users SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- 方式2:使用START TRANSACTION
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 1;
UPDATE users SET balance = balance + 100 WHERE id = 2;
COMMIT;在大多数情况下,BEGIN 和 START TRANSACTION 是完全等价的:
ACID
A - 原子性 (Atomicity)
定义: 事务是一个不可分割的工作单元,要么全部成功,要么全部失败。
实现机制: undo log (回滚日志)
sql
-- 转账操作
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1; -- 张三扣款
UPDATE accounts SET balance = balance + 1000 WHERE id = 2; -- 李四入账
COMMIT;
-- 如果任何一步失败,整个事务回滚,保证资金不会凭空消失或增加实现原理:
- 每次修改数据前,MySQL先将原始数据记录到 undo log
- 事务失败时,根据 undo log 进行回滚操作
- undo log 保证了数据修改的可逆性
C - 一致性 (Consistency)
定义: 事务执行前后,数据库必须从一个一致性状态转换到另一个一致性状态。
保证方式:
- 约束检查(主键、外键、唯一索引、检查约束)
- 触发器
- 应用逻辑
sql
-- 库存扣减必须保证库存不为负
BEGIN;
SELECT stock FROM products WHERE id = 1; -- 假设库存为 5
UPDATE products SET stock = stock - 8 WHERE id = 1; -- 尝试扣减 8
-- 违反约束:CHECK (stock >= 0),事务失败回滚
ROLLBACK;I - 隔离性 (Isolation)
定义: 并发执行的事务之间不能互相干扰,每个事务都应该感觉不到其他事务的存在。
四种隔离级别:
读未提交 (Read Uncommitted)
sqlSET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 设置事务隔离级别 -- 事务A BEGIN; UPDATE users SET balance = 500 WHERE id = 1; -- 此时事务B可以读到 balance = 500(脏读) -- 事务B SELECT balance FROM users WHERE id = 1; -- 读到500,但A可能回滚问题:
markdown脏读:一个事务读取到另一个未提交事务的修改数据 不可重复读:一个事务内多次读取同一数据,结果不一致 幻读:一个事务内多次查询,后续查询发现了之前没有的记录读已提交 (Read Committed)(alibaba)
sqlSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 事务A BEGIN; SELECT balance FROM users WHERE id = 1; -- 第一次读:1000 -- 此时事务B修改并提交了数据 SELECT balance FROM users WHERE id = 1; -- 第二次读:500(不可重复读) COMMIT;问题: 不可重复读、幻读
实现机制:
每次SELECT都创建新的Read View 基于MVCC机制,能读取到其他事务已提交的最新数据 通过undo log版本链选择合适的数据版本可重复读 (Repeatable Read) - MySQL默认
sql-- 表数据准备 -- users表:(1,'Alice',1000), (2,'Bob',2000), (3,'Charlie',1500) -- 窗口1:事务A(统计系统) SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; -- 快照读:基于事务开始时的数据快照 SELECT COUNT(*) FROM users WHERE balance > 1500; -- 第一次:1人 SELECT * FROM users WHERE balance > 1500; -- 只看到Charlie -- 窗口2:事务B(开户) BEGIN; INSERT INTO users VALUES (4, 'David', 2500); COMMIT; -- 提交新用户 -- 窗口1:事务A继续 -- 快照读:仍然看到事务开始时的数据 SELECT COUNT(*) FROM users WHERE balance > 1500; -- 仍是1人 SELECT * FROM users WHERE balance > 1500; -- 仍只看到Charlie -- ⚠️ 关键:使用当前读(加锁读取最新数据) SELECT COUNT(*) FROM users WHERE balance > 1500 LOCK IN SHARE MODE; -- 2人! SELECT * FROM users WHERE balance > 1500 FOR UPDATE; -- 看到Charlie和David! -- 幻读出现:同一事务内,相同查询条件得到不同结果集 COMMIT;问题: 可能出现幻读
实现机制:
整个事务期间只使用第一次创建的Read View 保证同一事务内多次读取同一数据结果一致 通过固定的一致性视图实现可重复读串行化 (Serializable)
sqlSET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 每个事务操作都变成了串行,前者事务没有提交后者事务就会阻塞等待。 -- 最高隔离级别,通过锁机制保证串行执行 -- 性能最差,但完全避免所有并发问题问题:解决了上面所以问题,因为全部都是串行执行,互不干扰,隔离级别最大,性能最差,阻塞执行。
实现机制: MVCC (多版本并发控制) + 锁机制
D - 持久性 (Durability)
定义: 一旦事务提交,其所做的修改就会永久保存到数据库中。
实现机制: redo log (重做日志)
sql
-- 事务提交流程
BEGIN;
UPDATE users SET balance = 1000 WHERE id = 1;
COMMIT; -- 此时:
-- 1. 写入redo log到磁盘 (WAL - Write Ahead Log)
-- 2. 返回提交成功给客户端
-- 3. 后续异步将脏页写入数据文件 【脏页 (Dirty Page):在内存中被修改但还未写入磁盘的数据页】MVCC 实现细节
MVCC是MySQL InnoDB存储引擎实现高并发的核心机制,通过为每个数据行维护多个版本来避免读写冲突。
核心原理
- 写操作:创建数据的新版本,不直接覆盖原数据
- 读操作:根据事务隔离级别和时间戳读取合适的版本
- 无锁读取:读写操作不互相阻塞
版本链概念
每行数据实际上是一个版本链,新版本指向旧版本:
最新版本 → 版本2 → 版本1 → 原始版本具体实现机制
隐藏字段
InnoDB为每行记录添加三个隐藏字段:
sql
-- 实际存储结构(简化)
CREATE TABLE users_internal (
id INT,
name VARCHAR(50),
balance DECIMAL(10,2),
-- 隐藏字段:
DB_TRX_ID BIGINT, -- 最后修改此行的事务ID
DB_ROLL_PTR BIGINT, -- 指向undo log中该行记录的指针
DB_ROW_ID BIGINT -- 隐含主键(如果表没有显式主键)
);undo log 版本链
sql
-- 初始数据
INSERT INTO users VALUES (1, 'Alice', 1000);
-- DB_TRX_ID=100, DB_ROLL_PTR=NULL
-- 事务101修改
UPDATE users SET balance = 1500 WHERE id = 1;
-- 事务102修改
UPDATE users SET balance = 2000 WHERE id = 1;undo log 版本链
当前版本: (1,'Alice',2000) DB_TRX_ID=102, DB_ROLL_PTR→undo_log_2
undo_log_2: (1,'Alice',1500) DB_TRX_ID=101, DB_ROLL_PTR→undo_log_1
undo_log_1: (1,'Alice',1000) DB_TRX_ID=100, DB_ROLL_PTR=NULL事务设计原则
保持事务短小精悍
❌ 错误示例:长事务
sql
BEGIN;
-- 复杂业务逻辑,耗时10分钟
SELECT * FROM orders WHERE create_date > '2024-01-01'; -- 大量数据
-- 处理复杂计算...
UPDATE inventory SET stock = stock - quantity; -- 长时间持锁
-- 发送邮件、调用外部API...
COMMIT;✅ 正确示例:拆分事务
sql
-- 拆分为多个小事务
-- 事务1: 快速获取必要数据
BEGIN;
SELECT order_id, product_id, quantity
FROM orders
WHERE status = 'pending' AND create_date > '2024-01-01'
LIMIT 100;
COMMIT;
-- 业务处理(在事务外)
-- 计算库存、验证逻辑等
-- 事务2: 快速更新核心数据
BEGIN;
UPDATE inventory SET stock = stock - ? WHERE product_id = ?;
UPDATE orders SET status = 'processed' WHERE order_id = ?;
COMMIT;减少事务范围
sql
-- ❌ 事务包含非数据库操作
BEGIN;
UPDATE user_balance SET amount = amount - 100 WHERE user_id = 1;
-- 调用支付接口(可能超时)
call_payment_api();
-- 发送短信通知
send_sms_notification();
UPDATE transaction_log SET status = 'success';
COMMIT;
-- ✅ 只在事务中包含数据库操作
-- 先完成外部调用
$payment_result = call_payment_api();
if ($payment_result->success) {
BEGIN;
UPDATE user_balance SET amount = amount - 100 WHERE user_id = 1;
UPDATE transaction_log SET status = 'success';
COMMIT;
}选择合适的隔离级别
sql
-- 1. 金融系统:使用 REPEATABLE READ(默认)可重复读 保证数据在某个时间点的一致性。
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
-- 复杂计算...
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
COMMIT;
-- 2. 报表查询:使用 READ COMMITTED 读已提交
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
-- 报表不需要严格一致性,允许读取已提交的最新数据
SELECT SUM(amount) FROM transactions WHERE date = CURDATE();
COMMIT;
-- 3. 统计分析:使用 READ UNCOMMITTED(谨慎使用) 读未提交
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 仅用于对准确性要求不高的统计分析
SELECT COUNT(*) FROM logs WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR);