Skip to content

事务

事务是什么?

事务是指访问并可能跟新数据库中各项数据的一个程序执行单元(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;

在大多数情况下,BEGINSTART 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)

定义: 并发执行的事务之间不能互相干扰,每个事务都应该感觉不到其他事务的存在。

四种隔离级别:

  1. 读未提交 (Read Uncommitted)

    sql
    SET 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
    脏读:一个事务读取到另一个未提交事务的修改数据
    不可重复读:一个事务内多次读取同一数据,结果不一致
    幻读:一个事务内多次查询,后续查询发现了之前没有的记录
  2. 读已提交 (Read Committed)(alibaba)

    sql
    SET 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版本链选择合适的数据版本
  3. 可重复读 (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
    保证同一事务内多次读取同一数据结果一致
    通过固定的一致性视图实现可重复读
  4. 串行化 (Serializable)

    sql
    SET 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);

如有转载或 CV 的请标注本站原文地址

访客数 --| 总访问量 --