Skip to content

MySQL 事务处理机制

事务概念与重要性

事务(Transaction)是数据库管理系统中的一个核心概念,它代表了一个不可分割的工作单位。在 MySQL 中,事务确保了数据库操作的可靠性和一致性,是构建可靠应用系统的基础。

事务的定义

事务是一组数据库操作的集合,这些操作要么全部成功执行,要么全部不执行。事务将多个数据库操作绑定在一起,形成一个逻辑工作单元,确保数据的完整性和一致性。

为什么需要事务

  1. 数据一致性保障:确保相关数据始终保持一致状态
  2. 并发控制:管理多用户同时访问数据库的情况
  3. 错误恢复:在系统故障时能够恢复到一致状态
  4. 业务逻辑完整性:保证复杂业务操作的原子性
  5. 数据安全性:防止部分操作成功导致的数据不一致

ACID 特性深度解析

ACID 是事务处理的四个基本特性,是评判事务系统可靠性的标准。

原子性(Atomicity)

概念理解

原子性确保事务中的所有操作要么全部成功,要么全部失败。事务是不可分割的最小工作单位,不存在部分成功的情况。

实际示例

银行转账场景

sql
-- 开始事务
START TRANSACTION;

-- 从账户A扣除1000元
UPDATE accounts 
SET balance = balance - 1000 
WHERE account_id = 'A001';

-- 检查账户A余额是否足够
SELECT balance FROM accounts WHERE account_id = 'A001';

-- 如果余额不足,回滚事务
-- ROLLBACK;

-- 向账户B增加1000元
UPDATE accounts 
SET balance = balance + 1000 
WHERE account_id = 'B001';

-- 提交事务
COMMIT;

订单处理场景

sql
-- 创建订单事务
START TRANSACTION;

-- 1. 创建订单记录
INSERT INTO orders (order_id, customer_id, total_amount, status) 
VALUES ('ORD001', 'CUST001', 299.99, 'PENDING');

-- 2. 添加订单明细
INSERT INTO order_items (order_id, product_id, quantity, price) 
VALUES 
('ORD001', 'PROD001', 2, 99.99),
('ORD001', 'PROD002', 1, 99.99);

-- 3. 更新库存
UPDATE products 
SET stock_quantity = stock_quantity - 2 
WHERE product_id = 'PROD001';

UPDATE products 
SET stock_quantity = stock_quantity - 1 
WHERE product_id = 'PROD002';

-- 4. 检查库存是否足够
SELECT product_id, stock_quantity 
FROM products 
WHERE product_id IN ('PROD001', 'PROD002') 
AND stock_quantity < 0;

-- 如果库存不足,回滚整个事务
-- ROLLBACK;

-- 5. 更新订单状态
UPDATE orders 
SET status = 'CONFIRMED' 
WHERE order_id = 'ORD001';

-- 提交事务
COMMIT;

实现机制

撤销日志(Undo Log)

  • 记录事务执行前的数据状态
  • 在事务回滚时恢复原始数据
  • 支持多版本数据管理
  • 确保回滚操作的完整性
sql
-- 查看 InnoDB 事务信息
SELECT 
    trx_id,
    trx_state,
    trx_started,
    trx_isolation_level,
    trx_autocommit
FROM information_schema.INNODB_TRX;

-- 查看 Undo Log 使用情况
SHOW ENGINE INNODB STATUS\G

事务状态管理

  1. 活跃状态(Active):事务正在执行
  2. 部分提交状态(Partially Committed):事务执行完毕,等待提交
  3. 提交状态(Committed):事务成功完成
  4. 失败状态(Failed):事务执行失败
  5. 中止状态(Aborted):事务被回滚
sql
-- 模拟事务状态变化
START TRANSACTION; -- 进入 Active 状态

INSERT INTO test_table (id, name) VALUES (1, 'Test'); -- 仍在 Active 状态

-- 此时事务处于 Partially Committed 状态
COMMIT; -- 进入 Committed 状态

-- 或者
-- ROLLBACK; -- 进入 Aborted 状态

原子性保障机制

两阶段提交协议

  • 准备阶段:检查所有操作是否可以成功
  • 提交阶段:执行实际的数据修改
  • 确保分布式环境下的原子性
  • 处理网络分区和节点故障

检查点机制

  • 定期将内存中的数据写入磁盘
  • 减少故障恢复时间
  • 确保数据持久性
  • 优化系统性能
sql
-- 设置事务自动提交
SET autocommit = 0; -- 关闭自动提交
SET autocommit = 1; -- 开启自动提交

-- 查看当前自动提交状态
SELECT @@autocommit;

-- 设置事务超时时间
SET innodb_lock_wait_timeout = 50; -- 50秒超时

-- 查看事务相关参数
SHOW VARIABLES LIKE '%timeout%';
SHOW VARIABLES LIKE '%autocommit%';

一致性(Consistency)

概念理解

一致性确保事务执行前后,数据库从一个一致状态转换到另一个一致状态。所有的数据约束、触发器、级联操作都必须得到满足。

实际示例

约束检查示例

sql
-- 创建带约束的表
CREATE TABLE customers (
    customer_id INT PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(255) UNIQUE NOT NULL,
    age INT CHECK (age >= 0 AND age <= 150),
    balance DECIMAL(10,2) DEFAULT 0.00 CHECK (balance >= 0),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    customer_id INT NOT NULL,
    order_amount DECIMAL(10,2) NOT NULL CHECK (order_amount > 0),
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

-- 测试约束检查
START TRANSACTION;

-- 插入有效数据
INSERT INTO customers (email, age, balance) 
VALUES ('john@example.com', 25, 1000.00);

-- 尝试插入无效数据(违反检查约束)
-- INSERT INTO customers (email, age, balance) 
-- VALUES ('invalid@example.com', -5, 1000.00); -- 年龄不能为负数

-- 尝试插入重复邮箱(违反唯一约束)
-- INSERT INTO customers (email, age, balance) 
-- VALUES ('john@example.com', 30, 500.00); -- 邮箱重复

COMMIT;

外键约束示例

sql
-- 测试外键约束
START TRANSACTION;

-- 为存在的客户创建订单(成功)
INSERT INTO orders (customer_id, order_amount) 
VALUES (1, 299.99);

-- 尝试为不存在的客户创建订单(失败)
-- INSERT INTO orders (customer_id, order_amount) 
-- VALUES (999, 199.99); -- 客户ID不存在

COMMIT;

业务规则一致性示例

sql
-- 库存管理的一致性
START TRANSACTION;

-- 检查库存是否足够
SELECT stock_quantity 
FROM products 
WHERE product_id = 'PROD001' AND stock_quantity >= 5;

-- 如果库存足够,减少库存并创建订单
UPDATE products 
SET stock_quantity = stock_quantity - 5 
WHERE product_id = 'PROD001' AND stock_quantity >= 5;

-- 检查更新是否成功(受影响行数应该为1)
SELECT ROW_COUNT() as affected_rows;

-- 如果库存更新成功,创建订单
INSERT INTO order_items (order_id, product_id, quantity) 
VALUES (1, 'PROD001', 5);

COMMIT;

一致性类型

实体完整性

  • 主键约束:确保主键唯一且非空
  • 唯一约束:确保指定列的值唯一
  • 非空约束:确保重要字段不为空
  • 检查约束:确保数据满足特定条件
sql
-- 查看表的约束信息
SELECT 
    CONSTRAINT_NAME,
    CONSTRAINT_TYPE,
    TABLE_NAME,
    COLUMN_NAME
FROM information_schema.KEY_COLUMN_USAGE 
WHERE TABLE_SCHEMA = 'your_database';

-- 查看检查约束
SELECT 
    CONSTRAINT_NAME,
    CHECK_CLAUSE
FROM information_schema.CHECK_CONSTRAINTS 
WHERE CONSTRAINT_SCHEMA = 'your_database';

参照完整性

  • 外键约束:确保引用关系的有效性
  • 级联操作:自动维护相关数据的一致性
  • 引用检查:防止无效引用的产生
  • 孤儿记录处理:清理无效的关联数据
sql
-- 创建带级联操作的外键
CREATE TABLE order_items (
    item_id INT PRIMARY KEY AUTO_INCREMENT,
    order_id INT NOT NULL,
    product_id VARCHAR(50) NOT NULL,
    quantity INT NOT NULL CHECK (quantity > 0),
    price DECIMAL(10,2) NOT NULL CHECK (price > 0),
    FOREIGN KEY (order_id) REFERENCES orders(order_id) 
        ON DELETE CASCADE ON UPDATE CASCADE
);

-- 测试级联删除
START TRANSACTION;

-- 删除订单时,相关的订单项也会被自动删除
DELETE FROM orders WHERE order_id = 1;

COMMIT;

用户定义完整性

  • 业务规则约束:确保数据符合业务逻辑
  • 触发器机制:自动执行一致性检查
  • 存储过程验证:复杂业务逻辑的一致性保障
  • 应用层验证:多层次的一致性检查
sql
-- 创建审计触发器
DELIMITER //
CREATE TRIGGER customer_audit_trigger
AFTER UPDATE ON customers
FOR EACH ROW
BEGIN
    INSERT INTO customer_audit_log (
        customer_id,
        old_balance,
        new_balance,
        change_amount,
        change_time
    ) VALUES (
        NEW.customer_id,
        OLD.balance,
        NEW.balance,
        NEW.balance - OLD.balance,
        NOW()
    );
END//
DELIMITER ;

-- 创建库存检查触发器
DELIMITER //
CREATE TRIGGER stock_check_trigger
BEFORE UPDATE ON products
FOR EACH ROW
BEGIN
    IF NEW.stock_quantity < 0 THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = '库存不能为负数';
    END IF;
END//
DELIMITER ;

存储过程实现复杂业务逻辑

sql
-- 创建转账存储过程
DELIMITER //
CREATE PROCEDURE transfer_money(
    IN from_account INT,
    IN to_account INT,
    IN amount DECIMAL(10,2)
)
BEGIN
    DECLARE from_balance DECIMAL(10,2);
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;
    
    START TRANSACTION;
    
    -- 检查源账户余额
    SELECT balance INTO from_balance 
    FROM customers 
    WHERE customer_id = from_account FOR UPDATE;
    
    IF from_balance < amount THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = '余额不足';
    END IF;
    
    -- 扣除源账户金额
    UPDATE customers 
    SET balance = balance - amount 
    WHERE customer_id = from_account;
    
    -- 增加目标账户金额
    UPDATE customers 
    SET balance = balance + amount 
    WHERE customer_id = to_account;
    
    COMMIT;
END//
DELIMITER ;

-- 调用存储过程
CALL transfer_money(1, 2, 100.00);

一致性实现

约束检查时机

  • 即时检查:操作执行时立即检查
  • 延迟检查:事务提交时统一检查
  • 级联检查:相关数据的连锁检查
  • 批量检查:批量操作的整体检查
sql
-- 设置约束检查模式
SET foreign_key_checks = 0; -- 临时禁用外键检查
-- 执行批量操作
SET foreign_key_checks = 1; -- 重新启用外键检查

-- 延迟约束检查(在支持的数据库中)
SET CONSTRAINTS ALL DEFERRED;

一致性恢复

  • 违反约束时的回滚机制
  • 数据修复和清理程序
  • 一致性检查工具
  • 数据验证报告
sql
-- 检查数据一致性的查询
-- 检查订单总额与订单明细总额是否一致
SELECT 
    o.order_id,
    o.order_amount as order_total,
    SUM(oi.quantity * oi.price) as items_total,
    o.order_amount - SUM(oi.quantity * oi.price) as difference
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY o.order_id, o.order_amount
HAVING ABS(difference) > 0.01; -- 查找不一致的订单

-- 检查库存一致性
SELECT 
    p.product_id,
    p.stock_quantity as current_stock,
    COALESCE(SUM(oi.quantity), 0) as total_ordered,
    p.stock_quantity + COALESCE(SUM(oi.quantity), 0) as should_be_stock
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
GROUP BY p.product_id, p.stock_quantity;

-- 修复数据不一致的存储过程
DELIMITER //
CREATE PROCEDURE fix_order_totals()
BEGIN
    DECLARE done INT DEFAULT FALSE;
    DECLARE order_id_var INT;
    DECLARE calculated_total DECIMAL(10,2);
    
    DECLARE order_cursor CURSOR FOR
        SELECT 
            o.order_id,
            SUM(oi.quantity * oi.price) as calc_total
        FROM orders o
        JOIN order_items oi ON o.order_id = oi.order_id
        GROUP BY o.order_id
        HAVING ABS(o.order_amount - calc_total) > 0.01;
    
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
    
    START TRANSACTION;
    
    OPEN order_cursor;
    
    read_loop: LOOP
        FETCH order_cursor INTO order_id_var, calculated_total;
        IF done THEN
            LEAVE read_loop;
        END IF;
        
        UPDATE orders 
        SET order_amount = calculated_total 
        WHERE order_id = order_id_var;
    END LOOP;
    
    CLOSE order_cursor;
    
    COMMIT;
END//
DELIMITER ;

隔离性(Isolation)

概念理解

隔离性确保并发执行的事务之间不会相互干扰,每个事务都感觉像是在独立环境中执行。

并发问题分析

脏读(Dirty Read)

  • 读取了其他事务未提交的数据
  • 可能读到最终会被回滚的数据
  • 导致基于错误数据的决策
  • 影响数据的可靠性
sql
-- 脏读演示(READ UNCOMMITTED 级别)
-- 会话1
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 'A001'; -- 假设余额为1000

-- 会话2(同时进行)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE accounts SET balance = 500 WHERE account_id = 'A001';
-- 注意:此时事务2还未提交

-- 回到会话1
SELECT balance FROM accounts WHERE account_id = 'A001'; -- 读到500(脏读)

-- 会话2回滚
ROLLBACK; -- 实际上余额还是1000

-- 会话1再次读取
SELECT balance FROM accounts WHERE account_id = 'A001'; -- 又变回1000
COMMIT;

不可重复读(Non-Repeatable Read)

  • 同一事务中多次读取同一数据得到不同结果
  • 其他事务在读取间隙修改了数据
  • 影响数据分析的准确性
  • 破坏事务内部的一致性
sql
-- 不可重复读演示(READ COMMITTED 级别)
-- 会话1
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 'A001'; -- 读到1000

-- 会话2(同时进行)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE accounts SET balance = 800 WHERE account_id = 'A001';
COMMIT; -- 提交事务

-- 回到会话1
SELECT balance FROM accounts WHERE account_id = 'A001'; -- 读到800(不可重复读)
COMMIT;

幻读(Phantom Read)

  • 同一查询在事务中返回不同的行集
  • 其他事务插入或删除了符合条件的行
  • 影响聚合操作的准确性
  • 导致统计结果的不一致
sql
-- 幻读演示(REPEATABLE READ 级别,在某些数据库中)
-- 会话1
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT COUNT(*) FROM accounts WHERE balance > 500; -- 假设结果为5

-- 会话2(同时进行)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
INSERT INTO accounts (account_id, balance) VALUES ('A999', 1000);
COMMIT;

-- 回到会话1
SELECT COUNT(*) FROM accounts WHERE balance > 500; -- 可能读到6(幻读)
-- 注意:MySQL InnoDB 在 REPEATABLE READ 级别通过 MVCC 避免了幻读
COMMIT;

更新丢失(Lost Update)

  • 多个事务同时修改同一数据
  • 后提交的事务覆盖了先提交的修改
  • 导致数据修改的丢失
  • 影响数据的完整性
sql
-- 更新丢失演示
-- 会话1
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 'A001'; -- 读到1000
-- 基于读取的值进行计算
UPDATE accounts SET balance = 1000 + 100 WHERE account_id = 'A001'; -- 设为1100

-- 会话2(同时进行)
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 'A001'; -- 也读到1000
-- 基于读取的值进行计算
UPDATE accounts SET balance = 1000 + 200 WHERE account_id = 'A001'; -- 设为1200
COMMIT; -- 先提交

-- 回到会话1
COMMIT; -- 后提交,覆盖了会话2的修改(更新丢失)

-- 正确的做法是使用乐观锁或悲观锁
-- 乐观锁示例
START TRANSACTION;
SELECT balance, version FROM accounts WHERE account_id = 'A001';
-- 假设读到 balance=1000, version=1
UPDATE accounts 
SET balance = balance + 100, version = version + 1 
WHERE account_id = 'A001' AND version = 1;
-- 检查受影响行数,如果为0说明版本已变化
COMMIT;

隔离级别详解

读未提交(Read Uncommitted)

  • 最低的隔离级别
  • 允许读取未提交的数据
  • 可能出现脏读、不可重复读、幻读
  • 性能最好,但数据一致性最差
  • 适用于对一致性要求极低的场景
sql
-- 设置读未提交隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 演示读未提交的行为
START TRANSACTION;
-- 可以读取到其他事务未提交的数据
SELECT * FROM accounts WHERE account_id = 'A001';
COMMIT;

-- 适用场景:数据仓库的 ETL 过程,对一致性要求不高的统计查询

读已提交(Read Committed)

  • 只能读取已提交的数据
  • 避免了脏读问题
  • 仍可能出现不可重复读和幻读
  • 大多数数据库的默认隔离级别
  • 平衡了性能和一致性
sql
-- 设置读已提交隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 演示读已提交的行为
START TRANSACTION;
-- 只能读取到已提交的数据
SELECT * FROM accounts WHERE account_id = 'A001';
-- 但同一事务中多次读取可能得到不同结果
SELECT * FROM accounts WHERE account_id = 'A001';
COMMIT;

-- 适用场景:Web 应用的大部分查询操作,报表生成

可重复读(Repeatable Read)

  • MySQL InnoDB 的默认隔离级别
  • 确保事务中多次读取同一数据结果一致
  • 避免了脏读和不可重复读
  • 在 MySQL 中通过 MVCC 也避免了幻读
  • 提供了较好的一致性保障
sql
-- 设置可重复读隔离级别(MySQL 默认)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 演示可重复读的行为
START TRANSACTION;
SELECT * FROM accounts WHERE account_id = 'A001'; -- 第一次读取
-- 即使其他事务修改并提交了数据,再次读取结果相同
SELECT * FROM accounts WHERE account_id = 'A001'; -- 第二次读取,结果相同

-- 演示 MySQL 中的幻读防护
SELECT COUNT(*) FROM accounts WHERE balance > 500; -- 第一次统计
-- 其他事务插入新记录后
SELECT COUNT(*) FROM accounts WHERE balance > 500; -- 第二次统计,结果相同
COMMIT;

-- 适用场景:金融系统的查询操作,需要数据一致性的业务逻辑

串行化(Serializable)

  • 最高的隔离级别
  • 完全避免所有并发问题
  • 事务串行执行,性能最差
  • 适用于对一致性要求极高的场景
  • 可能导致大量的锁等待
sql
-- 设置串行化隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 演示串行化的行为
START TRANSACTION;
SELECT * FROM accounts WHERE balance > 500;
-- 其他事务无法修改满足条件的数据,直到当前事务结束
-- 甚至无法插入新的满足条件的记录
COMMIT;

-- 适用场景:关键业务数据的修改,审计操作,数据迁移

隔离级别管理

sql
-- 查看和设置全局隔离级别
SELECT @@global.transaction_isolation;
SET GLOBAL transaction_isolation = 'READ-COMMITTED';

-- 查看和设置会话隔离级别
SELECT @@session.transaction_isolation;
SET SESSION transaction_isolation = 'REPEATABLE-READ';

-- 为单个事务设置隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
-- 事务操作
COMMIT;

-- 隔离级别对比测试表
CREATE TABLE isolation_test (
    id INT PRIMARY KEY,
    value VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO isolation_test VALUES (1, 'initial', NOW());

-- 测试不同隔离级别的性能影响
SHOW STATUS LIKE 'Innodb_rows_locked';
SHOW STATUS LIKE 'Innodb_lock_time%';

隔离级别选择指南

sql
-- 根据业务场景选择合适的隔离级别

-- 1. 高并发读取场景(如商品浏览)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM products WHERE category = 'electronics';
COMMIT;

-- 2. 数据分析场景(需要一致性快照)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT AVG(price) FROM products;
SELECT COUNT(*) FROM products;
-- 确保两个查询看到的是同一时刻的数据
COMMIT;

-- 3. 关键业务操作(如财务结算)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
-- 执行关键的财务计算和更新
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 'A001';
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 'A002';
COMMIT;

-- 4. 批量数据处理(可以容忍不一致)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
-- 执行大量数据的统计分析
SELECT category, COUNT(*), AVG(price) FROM products GROUP BY category;
COMMIT;

持久性(Durability)

概念理解

持久性确保已提交事务的修改永久保存在数据库中,即使系统故障也不会丢失。

实际示例

事务持久性测试

sql
-- 创建测试表
CREATE TABLE durability_test (
    id INT PRIMARY KEY AUTO_INCREMENT,
    data VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 测试事务持久性
START TRANSACTION;
INSERT INTO durability_test (data) VALUES ('重要数据1');
INSERT INTO durability_test (data) VALUES ('重要数据2');
COMMIT; -- 提交后数据必须持久化

-- 即使此时系统崩溃,上述数据也不会丢失
SELECT * FROM durability_test;

配置持久性参数

sql
-- 查看当前持久性相关配置
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
SHOW VARIABLES LIKE 'sync_binlog';
SHOW VARIABLES LIKE 'innodb_doublewrite';

-- 最高安全级别配置(性能较低)
SET GLOBAL innodb_flush_log_at_trx_commit = 1; -- 每次提交都刷新日志
SET GLOBAL sync_binlog = 1; -- 每次提交都同步 binlog

-- 平衡性能和安全的配置
SET GLOBAL innodb_flush_log_at_trx_commit = 2; -- 每秒刷新一次
SET GLOBAL sync_binlog = 100; -- 每100个事务同步一次

-- 高性能配置(安全性较低)
SET GLOBAL innodb_flush_log_at_trx_commit = 0; -- 由系统控制刷新
SET GLOBAL sync_binlog = 0; -- 由系统控制同步

实现机制

重做日志(Redo Log)

  • 记录事务对数据的修改操作
  • 在系统故障后重新执行已提交事务
  • 确保已提交数据不会丢失
  • 支持快速故障恢复
sql
-- 查看 Redo Log 配置
SHOW VARIABLES LIKE 'innodb_log%';

-- 重要的 Redo Log 参数
SELECT 
    @@innodb_log_file_size as 'Log File Size',
    @@innodb_log_files_in_group as 'Log Files Count',
    @@innodb_log_buffer_size as 'Log Buffer Size';

-- 查看 Redo Log 使用情况
SHOW ENGINE INNODB STATUS\G

-- 监控 Redo Log 写入
SHOW STATUS LIKE 'Innodb_os_log_written';
SHOW STATUS LIKE 'Innodb_log_writes';

写前日志(Write-Ahead Logging, WAL)

  • 先写日志,再写数据
  • 确保日志记录在数据修改之前持久化
  • 提供故障恢复的基础
  • 保证数据的一致性

二进制日志(Binlog)

  • 记录所有修改数据的 SQL 语句
  • 支持主从复制
  • 用于数据恢复和审计
  • 提供时间点恢复能力
sql
-- 启用 binlog
SET GLOBAL log_bin = ON;

-- 查看 binlog 配置
SHOW VARIABLES LIKE 'log_bin%';
SHOW VARIABLES LIKE 'binlog%';

-- 查看 binlog 文件
SHOW BINARY LOGS;

-- 查看 binlog 内容
SHOW BINLOG EVENTS IN 'mysql-bin.000001';

-- 设置 binlog 格式
SET GLOBAL binlog_format = 'ROW'; -- 行格式
-- SET GLOBAL binlog_format = 'STATEMENT'; -- 语句格式
-- SET GLOBAL binlog_format = 'MIXED'; -- 混合格式

-- 手动刷新 binlog
FLUSH BINARY LOGS;

检查点机制

  • 定期将内存中的脏页写入磁盘
  • 减少故障恢复时需要重做的操作
  • 提高系统性能
  • 确保数据的及时持久化
sql
-- 查看检查点相关参数
SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct';
SHOW VARIABLES LIKE 'innodb_io_capacity';
SHOW VARIABLES LIKE 'innodb_flush_neighbors';

-- 手动触发检查点
SET GLOBAL innodb_fast_shutdown = 0;
-- 然后重启 MySQL

-- 监控脏页情况
SELECT 
    VARIABLE_NAME,
    VARIABLE_VALUE
FROM performance_schema.global_status 
WHERE VARIABLE_NAME IN (
    'Innodb_buffer_pool_pages_dirty',
    'Innodb_buffer_pool_pages_total',
    'Innodb_buffer_pool_pages_flushed'
);

双写缓冲(Doublewrite Buffer)

  • 防止页面部分写入导致的数据损坏
  • 先写入双写缓冲区,再写入实际位置
  • 提供额外的数据保护
  • 确保页面写入的原子性
sql
-- 查看双写缓冲状态
SHOW VARIABLES LIKE 'innodb_doublewrite';

-- 查看双写缓冲统计
SHOW STATUS LIKE 'Innodb_dblwr%';

-- 双写缓冲相关的性能监控
SELECT 
    VARIABLE_NAME,
    VARIABLE_VALUE
FROM performance_schema.global_status 
WHERE VARIABLE_NAME LIKE 'Innodb_dblwr%';

持久性保障策略

同步写入

  • 事务提交时强制刷新到磁盘
  • 确保数据不会因断电丢失
  • 可能影响性能
  • 提供最高的可靠性
sql
-- 强制同步写入配置
SET GLOBAL innodb_flush_log_at_trx_commit = 1;
SET GLOBAL sync_binlog = 1;

-- 测试同步写入性能
START TRANSACTION;
INSERT INTO durability_test (data) VALUES ('同步写入测试');
COMMIT; -- 此时会强制刷新到磁盘

-- 查看写入统计
SHOW STATUS LIKE 'Innodb_data_fsyncs';
SHOW STATUS LIKE 'Innodb_os_log_fsyncs';

异步写入

  • 延迟写入磁盘以提高性能
  • 存在数据丢失的风险
  • 适用于对性能要求高的场景
  • 需要权衡可靠性和性能
sql
-- 异步写入配置
SET GLOBAL innodb_flush_log_at_trx_commit = 0;
SET GLOBAL sync_binlog = 0;

-- 测试异步写入性能
START TRANSACTION;
INSERT INTO durability_test (data) VALUES ('异步写入测试');
COMMIT; -- 不会立即刷新到磁盘

-- 手动强制刷新
FLUSH LOGS;

数据恢复示例

sql
-- 基于时间点的恢复
-- 1. 恢复到指定时间点
-- mysqldump --single-transaction --routines --triggers your_db > backup.sql
-- mysql your_db < backup.sql
-- mysqlbinlog --start-datetime="2024-01-01 10:00:00" 
--            --stop-datetime="2024-01-01 11:00:00" 
--            mysql-bin.000001 | mysql your_db

-- 2. 基于位置的恢复
-- mysqlbinlog --start-position=1000 --stop-position=2000 
--            mysql-bin.000001 | mysql your_db

-- 查看当前 binlog 位置
SHOW MASTER STATUS;

-- 创建恢复点
FLUSH LOGS;
SHOW MASTER STATUS; -- 记录当前位置

-- 模拟数据丢失和恢复
CREATE TABLE recovery_test (
    id INT PRIMARY KEY AUTO_INCREMENT,
    data VARCHAR(100),
    backup_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO recovery_test (data) VALUES ('恢复测试数据1');
INSERT INTO recovery_test (data) VALUES ('恢复测试数据2');

-- 记录当前状态
SELECT * FROM recovery_test;
SHOW MASTER STATUS;

锁机制原理

锁是实现事务隔离性的重要机制,通过控制对数据的并发访问来避免数据不一致。

锁的分类

按锁的粒度分类

表级锁(Table Lock)

  • 锁定整个表
  • 实现简单,开销小
  • 并发性能较差
  • 适合读多写少的场景
  • MyISAM 存储引擎主要使用

行级锁(Row Lock)

  • 锁定具体的数据行
  • 并发性能好
  • 实现复杂,开销较大
  • 适合高并发场景
  • InnoDB 存储引擎主要使用

页级锁(Page Lock)

  • 锁定数据页
  • 介于表锁和行锁之间
  • 平衡了性能和并发性
  • 使用较少

按锁的类型分类

共享锁(Shared Lock, S锁)

  • 也称为读锁
  • 多个事务可以同时持有
  • 阻止其他事务获取排他锁
  • 允许并发读取
  • 提高查询性能
sql
-- 共享锁示例
-- 会话1:获取共享锁
START TRANSACTION;
SELECT * FROM accounts WHERE account_id = 'A001' LOCK IN SHARE MODE;
-- 或者使用新语法
-- SELECT * FROM accounts WHERE account_id = 'A001' FOR SHARE;

-- 会话2:也可以获取共享锁(成功)
START TRANSACTION;
SELECT * FROM accounts WHERE account_id = 'A001' LOCK IN SHARE MODE;

-- 会话3:尝试获取排他锁(阻塞)
START TRANSACTION;
-- SELECT * FROM accounts WHERE account_id = 'A001' FOR UPDATE; -- 会等待

-- 释放锁
COMMIT; -- 在所有会话中执行

排他锁(Exclusive Lock, X锁)

  • 也称为写锁
  • 只能被一个事务持有
  • 阻止其他事务获取任何锁
  • 确保写操作的独占性
  • 保证数据修改的一致性
sql
-- 排他锁示例
-- 会话1:获取排他锁
START TRANSACTION;
SELECT * FROM accounts WHERE account_id = 'A001' FOR UPDATE;

-- 会话2:尝试获取任何锁(都会阻塞)
START TRANSACTION;
-- SELECT * FROM accounts WHERE account_id = 'A001'; -- 普通读取也会阻塞
-- SELECT * FROM accounts WHERE account_id = 'A001' LOCK IN SHARE MODE; -- 阻塞
-- SELECT * FROM accounts WHERE account_id = 'A001' FOR UPDATE; -- 阻塞

-- 实际应用:银行转账
START TRANSACTION;
-- 锁定账户防止并发修改
SELECT balance FROM accounts WHERE account_id = 'A001' FOR UPDATE;
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 'A001';
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 'A002';
COMMIT;

意向锁(Intention Lock)

  • 表级锁,表示事务意图
  • 意向共享锁(IS):表示事务想要获取行级共享锁
  • 意向排他锁(IX):表示事务想要获取行级排他锁
  • 提高锁冲突检测效率
  • 避免全表扫描检查锁冲突
sql
-- 查看当前锁信息
SELECT 
    r.trx_id,
    r.trx_mysql_thread_id,
    r.trx_query,
    b.locked_table,
    b.locked_index,
    b.lock_type,
    b.lock_mode,
    r.trx_rows_locked,
    r.trx_rows_modified
FROM information_schema.INNODB_TRX r
LEFT JOIN information_schema.INNODB_LOCKS b 
    ON r.trx_id = b.lock_trx_id
WHERE r.trx_state = 'RUNNING';

-- 查看锁等待情况
SELECT 
    r.trx_id waiting_trx_id,
    r.trx_mysql_thread_id waiting_thread,
    r.trx_query waiting_query,
    b.trx_id blocking_trx_id,
    b.trx_mysql_thread_id blocking_thread,
    b.trx_query blocking_query
FROM information_schema.INNODB_LOCK_WAITS w
INNER JOIN information_schema.INNODB_TRX r 
    ON r.trx_id = w.requesting_trx_id
INNER JOIN information_schema.INNODB_TRX b 
    ON b.trx_id = w.blocking_trx_id;

锁的实现机制

锁管理器

锁表结构

  • 维护所有锁的信息
  • 记录锁的类型、粒度、持有者
  • 支持快速锁查找和冲突检测
  • 管理锁的生命周期

锁等待队列

  • 管理等待锁的事务
  • 实现公平的锁分配
  • 支持锁等待超时
  • 防止锁饥饿现象

死锁检测

  • 定期检测锁等待图中的环
  • 选择代价最小的事务进行回滚
  • 打破死锁循环
  • 记录死锁信息用于分析

锁优化策略

锁升级

  • 当行锁数量过多时升级为表锁
  • 减少锁管理开销
  • 可能降低并发性
  • 需要权衡性能和并发

锁分离

  • 读写锁分离
  • 减少锁冲突
  • 提高并发性能
  • 适合读多写少的场景

锁超时

  • 设置锁等待超时时间
  • 避免长时间等待
  • 防止系统阻塞
  • 需要合理设置超时值

死锁检测与处理

死锁产生原因

死锁的四个必要条件

  1. 互斥条件:资源不能被多个事务同时使用
  2. 持有和等待条件:事务持有资源的同时等待其他资源
  3. 不可剥夺条件:资源不能被强制从事务中剥夺
  4. 循环等待条件:存在事务等待环路

常见死锁场景

不同顺序访问资源

  • 事务 A:锁定资源 1,请求资源 2
  • 事务 B:锁定资源 2,请求资源 1
  • 形成循环等待
  • 最常见的死锁类型
sql
-- 死锁演示:不同顺序访问资源
-- 会话1
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A001'; -- 锁定A001
-- 等待一段时间,然后执行下一条
UPDATE accounts SET balance = balance + 100 WHERE account_id = 'A002'; -- 请求A002锁

-- 会话2(同时进行)
START TRANSACTION;
UPDATE accounts SET balance = balance - 50 WHERE account_id = 'A002';  -- 锁定A002
-- 等待一段时间,然后执行下一条
UPDATE accounts SET balance = balance + 50 WHERE account_id = 'A001';  -- 请求A001锁(死锁!)

-- MySQL 会自动检测死锁并回滚其中一个事务

索引锁冲突

  • 不同索引的锁定顺序不同
  • 复合索引的部分锁定
  • 索引范围锁的重叠
  • 需要仔细设计索引策略
sql
-- 创建测试表和索引
CREATE TABLE deadlock_test (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    INDEX idx_name (name),
    INDEX idx_age (age)
);

INSERT INTO deadlock_test VALUES 
(1, 'Alice', 25),
(2, 'Bob', 30),
(3, 'Charlie', 35);

-- 索引死锁演示
-- 会话1
START TRANSACTION;
SELECT * FROM deadlock_test WHERE name = 'Alice' FOR UPDATE;
SELECT * FROM deadlock_test WHERE age = 30 FOR UPDATE;

-- 会话2(同时进行)
START TRANSACTION;
SELECT * FROM deadlock_test WHERE age = 30 FOR UPDATE;
SELECT * FROM deadlock_test WHERE name = 'Alice' FOR UPDATE; -- 可能死锁

外键约束死锁

  • 父表和子表的锁定冲突
  • 级联操作引起的死锁
  • 需要注意操作顺序
  • 考虑外键设计的影响
sql
-- 外键死锁演示
CREATE TABLE parent_table (
    id INT PRIMARY KEY,
    name VARCHAR(50)
);

CREATE TABLE child_table (
    id INT PRIMARY KEY,
    parent_id INT,
    data VARCHAR(50),
    FOREIGN KEY (parent_id) REFERENCES parent_table(id)
);

INSERT INTO parent_table VALUES (1, 'Parent1'), (2, 'Parent2');
INSERT INTO child_table VALUES (1, 1, 'Child1'), (2, 2, 'Child2');

-- 会话1
START TRANSACTION;
UPDATE parent_table SET name = 'Updated Parent1' WHERE id = 1;
UPDATE child_table SET data = 'Updated Child2' WHERE parent_id = 2;

-- 会话2(同时进行)
START TRANSACTION;
UPDATE parent_table SET name = 'Updated Parent2' WHERE id = 2;
UPDATE child_table SET data = 'Updated Child1' WHERE parent_id = 1; -- 可能死锁

死锁检测算法

等待图算法

图构建

  • 节点代表事务
  • 边代表等待关系
  • 动态维护图结构
  • 支持快速环检测

环检测

  • 深度优先搜索
  • 检测图中的环路
  • 识别死锁参与者
  • 选择回滚目标

检测频率

  • 定期检测:固定时间间隔
  • 触发检测:锁等待时触发
  • 平衡检测开销和响应时间
  • 避免过度检测影响性能

死锁处理策略

死锁预防

资源排序

  • 按固定顺序访问资源
  • 避免循环等待
  • 需要应用层配合
  • 可能影响性能
sql
-- 死锁预防:统一资源访问顺序
-- 错误的做法(可能死锁)
-- 事务1: UPDATE table_a, UPDATE table_b
-- 事务2: UPDATE table_b, UPDATE table_a

-- 正确的做法:按表名或ID顺序访问
DELIMITER //
CREATE PROCEDURE safe_transfer(
    IN from_account VARCHAR(10),
    IN to_account VARCHAR(10),
    IN amount DECIMAL(10,2)
)
BEGIN
    DECLARE account1 VARCHAR(10);
    DECLARE account2 VARCHAR(10);
    
    -- 按账户ID排序,确保访问顺序一致
    IF from_account < to_account THEN
        SET account1 = from_account;
        SET account2 = to_account;
    ELSE
        SET account1 = to_account;
        SET account2 = from_account;
    END IF;
    
    START TRANSACTION;
    
    -- 按排序后的顺序锁定账户
    SELECT balance FROM accounts WHERE account_id = account1 FOR UPDATE;
    SELECT balance FROM accounts WHERE account_id = account2 FOR UPDATE;
    
    -- 执行转账操作
    UPDATE accounts SET balance = balance - amount WHERE account_id = from_account;
    UPDATE accounts SET balance = balance + amount WHERE account_id = to_account;
    
    COMMIT;
END//
DELIMITER ;

超时机制

  • 设置锁等待超时
  • 超时后自动回滚
  • 简单有效的预防方法
  • 可能导致误判
sql
-- 设置锁等待超时
SET SESSION innodb_lock_wait_timeout = 10; -- 10秒超时

-- 查看当前超时设置
SELECT @@innodb_lock_wait_timeout;

-- 测试超时机制
START TRANSACTION;
SELECT * FROM accounts WHERE account_id = 'A001' FOR UPDATE;
-- 如果其他事务持有锁,10秒后会超时

-- 处理超时异常的示例
DELIMITER //
CREATE PROCEDURE timeout_safe_operation()
BEGIN
    DECLARE EXIT HANDLER FOR 1205 -- Lock wait timeout
    BEGIN
        ROLLBACK;
        SELECT 'Operation failed due to lock timeout' as message;
    END;
    
    START TRANSACTION;
    -- 可能超时的操作
    SELECT * FROM accounts WHERE account_id = 'A001' FOR UPDATE;
    UPDATE accounts SET balance = balance + 100 WHERE account_id = 'A001';
    COMMIT;
END//
DELIMITER ;

一次性锁定

  • 事务开始时锁定所有需要的资源
  • 避免持有和等待
  • 可能降低并发性
  • 难以预测所有需要的资源
sql
-- 一次性锁定示例
DELIMITER //
CREATE PROCEDURE batch_transfer(
    IN account_list TEXT,
    IN amount_list TEXT
)
BEGIN
    START TRANSACTION;
    
    -- 一次性锁定所有相关账户
    SELECT * FROM accounts 
    WHERE FIND_IN_SET(account_id, account_list) > 0
    FOR UPDATE;
    
    -- 执行批量操作
    -- 这里简化处理,实际应该解析参数列表
    UPDATE accounts SET balance = balance - 100 
    WHERE account_id IN ('A001', 'A002');
    
    UPDATE accounts SET balance = balance + 100 
    WHERE account_id IN ('A003', 'A004');
    
    COMMIT;
END//
DELIMITER ;

死锁恢复

受害者选择

  • 选择代价最小的事务回滚
  • 考虑事务的执行时间
  • 考虑事务修改的数据量
  • 考虑事务的优先级

回滚策略

  • 完全回滚:回滚整个事务
  • 部分回滚:回滚到安全点
  • 选择性回滚:只回滚冲突操作
  • 需要权衡恢复成本

MVCC 多版本并发控制

MVCC 是 MySQL InnoDB 存储引擎实现高并发的核心技术,通过维护数据的多个版本来避免读写冲突。

MVCC 基本原理

版本管理

数据版本

  • 每次数据修改创建新版本
  • 保留历史版本用于读取
  • 版本通过事务 ID 标识
  • 支持并发读写操作

版本链

  • 通过指针连接同一行的不同版本
  • 从新到旧的版本链表
  • 支持快速版本查找
  • 便于垃圾回收

读视图(Read View)

  • 事务开始时创建的一致性视图
  • 确定哪些版本对当前事务可见
  • 实现事务隔离
  • 保证读取的一致性

可见性判断

版本可见性规则

  1. 版本的创建事务 ID 小于当前事务的最小活跃事务 ID
  2. 版本的创建事务 ID 等于当前事务 ID
  3. 版本的创建事务 ID 不在当前事务的活跃事务列表中
  4. 版本没有被删除或删除事务 ID 不满足可见性条件

快照读与当前读

  • 快照读:读取事务开始时的数据快照
  • 当前读:读取数据的最新版本
  • 不同的读取方式适用不同场景
  • 影响事务的隔离级别

MVCC 实现细节

行记录格式

隐藏列

  • DB_TRX_ID:最后修改该行的事务 ID
  • DB_ROLL_PTR:指向 undo log 的指针
  • DB_ROW_ID:行 ID(如果没有主键)
  • 支持版本管理和回滚
sql
-- 查看 InnoDB 表的内部结构
-- 创建测试表
CREATE TABLE mvcc_test (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT
) ENGINE=InnoDB;

INSERT INTO mvcc_test VALUES (1, 'Alice', 25);

-- 查看表的内部信息
SELECT 
    table_name,
    engine,
    row_format,
    table_rows,
    avg_row_length,
    data_length
FROM information_schema.tables 
WHERE table_name = 'mvcc_test';

-- 查看当前事务ID
SELECT TRX_ID FROM information_schema.innodb_trx;

-- 模拟查看行记录的版本信息(概念演示)
-- 实际的隐藏列无法直接查询,这里用注释说明
/*
行记录格式示例:
+----+-------+-----+------------+-------------+-----------+
| id | name  | age | DB_TRX_ID  | DB_ROLL_PTR | DB_ROW_ID |
+----+-------+-----+------------+-------------+-----------+
| 1  | Alice | 25  | 12345      | 0x7f8b...  | 1         |
+----+-------+-----+------------+-------------+-----------+
*/

记录头信息

  • deleted_flag:删除标记
  • min_rec_flag:最小记录标记
  • n_owned:拥有的记录数
  • heap_no:堆中的位置
  • record_type:记录类型
  • next_record:下一条记录的偏移量
sql
-- 演示记录的删除标记
START TRANSACTION;

-- 查看删除前的记录
SELECT * FROM mvcc_test WHERE id = 1;

-- 删除记录(实际是标记删除)
DELETE FROM mvcc_test WHERE id = 1;

-- 在当前事务中,记录已被删除
SELECT * FROM mvcc_test WHERE id = 1; -- 无结果

-- 但在其他事务中,记录仍然可见(如果隔离级别允许)
-- 这就是 MVCC 的体现

ROLLBACK; -- 回滚以保持测试数据

Undo Log 链

  • 记录数据修改前的状态
  • 形成版本链表
  • 支持事务回滚
  • 实现 MVCC 读取
sql
-- 演示 Undo Log 链的概念
-- 创建测试数据
CREATE TABLE version_test (
    id INT PRIMARY KEY,
    value VARCHAR(50),
    version INT DEFAULT 1
) ENGINE=InnoDB;

INSERT INTO version_test (id, value) VALUES (1, 'Version 1');

-- 模拟版本链的形成
START TRANSACTION;
SELECT * FROM version_test WHERE id = 1; -- 读取初始版本

-- 在另一个会话中修改数据
-- 会话2:
-- START TRANSACTION;
-- UPDATE version_test SET value = 'Version 2', version = 2 WHERE id = 1;
-- COMMIT;

-- 回到原会话,由于 REPEATABLE READ 隔离级别,仍能看到旧版本
SELECT * FROM version_test WHERE id = 1; -- 仍然是 'Version 1'
COMMIT;

-- 现在可以看到新版本
SELECT * FROM version_test WHERE id = 1; -- 现在是 'Version 2'

-- 查看 Undo Log 相关信息
SHOW ENGINE INNODB STATUS\G
-- 在输出中查找 "TRANSACTIONS" 部分,可以看到 undo log 的使用情况

读取流程

快照读取

  • 根据事务开始时间创建读视图
  • 判断记录版本的可见性
  • 如果当前版本不可见,通过 Undo Log 查找历史版本
  • 返回可见的版本数据
sql
-- 快照读取演示
-- 会话1:创建长事务
START TRANSACTION;
SELECT NOW() as transaction_start_time;
SELECT * FROM mvcc_test; -- 建立读视图

-- 会话2:修改数据
START TRANSACTION;
UPDATE mvcc_test SET name = 'Bob', age = 30 WHERE id = 1;
COMMIT;

-- 回到会话1:快照读取,仍能看到旧数据
SELECT * FROM mvcc_test WHERE id = 1; -- 仍然是 Alice, 25

-- 查看读视图信息
SELECT 
    trx_id,
    trx_state,
    trx_started,
    trx_isolation_level,
    trx_is_read_only
FROM information_schema.innodb_trx;

COMMIT;

-- 事务结束后,可以看到最新数据
SELECT * FROM mvcc_test WHERE id = 1; -- 现在是 Bob, 30

当前读取

  • 读取记录的最新版本
  • 需要加锁保证数据一致性
  • 用于 SELECT ... FOR UPDATE
  • 用于 UPDATE、DELETE 操作
sql
-- 当前读取演示
-- 会话1:使用当前读
START TRANSACTION;
SELECT * FROM mvcc_test WHERE id = 1 FOR UPDATE; -- 当前读,加排他锁

-- 会话2:尝试修改(会被阻塞)
START TRANSACTION;
-- 这个操作会等待,因为会话1持有排他锁
-- UPDATE mvcc_test SET age = 35 WHERE id = 1;

-- 回到会话1
-- 即使其他事务修改了数据,当前读总是能看到最新版本
UPDATE mvcc_test SET age = age + 1 WHERE id = 1;
SELECT * FROM mvcc_test WHERE id = 1; -- 看到最新的修改
COMMIT;

-- 会话2的操作现在可以继续
-- COMMIT;

-- 对比快照读和当前读
START TRANSACTION;
-- 快照读
SELECT * FROM mvcc_test WHERE id = 1;
-- 当前读
SELECT * FROM mvcc_test WHERE id = 1 FOR UPDATE;
COMMIT;

版本查找过程

  1. 从当前版本开始
  2. 检查版本可见性
  3. 如果不可见,沿 undo log 链查找
  4. 找到第一个可见版本
  5. 返回该版本的数据

性能优化

  • 版本链长度控制
  • 热点数据缓存
  • 并发读取优化
  • 垃圾回收机制

MVCC 的优势与限制

优势

  1. 高并发性能:读写不互相阻塞
  2. 一致性读取:提供事务级别的一致性
  3. 无锁读取:大部分读操作不需要加锁
  4. 历史数据访问:支持查看历史版本

限制

  1. 存储开销:需要额外存储空间保存版本
  2. 版本链长度:过长的版本链影响性能
  3. 垃圾回收:需要定期清理无用版本
  4. 写操作开销:每次写操作需要创建新版本

事务性能优化

事务设计原则

事务边界控制

最小化事务范围

  • 只在必要时使用事务
  • 尽快提交或回滚
  • 避免长时间持有锁
  • 减少锁竞争
sql
-- 错误的做法:事务范围过大
-- 不推荐
START TRANSACTION;
-- 大量业务逻辑处理
SELECT * FROM large_table; -- 可能很慢的查询
-- 复杂的计算逻辑
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A001';
-- 更多业务逻辑
COMMIT;

-- 正确的做法:最小化事务范围
-- 推荐
-- 先进行查询和计算(不在事务中)
SELECT balance FROM accounts WHERE account_id = 'A001';
-- 进行业务逻辑计算

-- 只在需要修改数据时开启事务
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A001';
UPDATE accounts SET balance = balance + 100 WHERE account_id = 'A002';
COMMIT;

避免长事务

  • 分解大事务为小事务
  • 使用批处理策略
  • 定期提交中间结果
  • 监控事务执行时间
sql
-- 错误的做法:长事务批量处理
-- 不推荐
START TRANSACTION;
UPDATE large_table SET status = 'processed' WHERE status = 'pending'; -- 可能影响百万行
COMMIT;

-- 正确的做法:分批处理
-- 推荐
DELIMITER //
CREATE PROCEDURE batch_update_status()
BEGIN
    DECLARE done INT DEFAULT FALSE;
    DECLARE batch_size INT DEFAULT 1000;
    DECLARE affected_rows INT;
    
    REPEAT
        START TRANSACTION;
        
        UPDATE large_table 
        SET status = 'processed' 
        WHERE status = 'pending' 
        LIMIT batch_size;
        
        SET affected_rows = ROW_COUNT();
        COMMIT;
        
        -- 短暂休息,释放资源
        SELECT SLEEP(0.1);
        
    UNTIL affected_rows < batch_size END REPEAT;
END//
DELIMITER ;

-- 监控长事务
SELECT 
    trx_id,
    trx_state,
    trx_started,
    TIMESTAMPDIFF(SECOND, trx_started, NOW()) as duration_seconds,
    trx_isolation_level,
    trx_tables_in_use,
    trx_tables_locked,
    trx_rows_locked,
    trx_rows_modified
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 30
ORDER BY duration_seconds DESC;

事务大小控制

小事务优势

  • 减少锁持有时间
  • 降低死锁概率
  • 提高并发性能
  • 便于错误恢复

事务拆分策略

  • 按业务逻辑拆分
  • 按数据访问模式拆分
  • 考虑一致性要求
  • 平衡性能和正确性
sql
-- 事务拆分示例:订单处理
-- 错误的做法:一个大事务
-- 不推荐
START TRANSACTION;
-- 1. 验证用户信息
SELECT * FROM users WHERE user_id = 123;
-- 2. 检查商品库存
SELECT stock FROM products WHERE product_id = 456;
-- 3. 计算价格和优惠
SELECT price FROM products WHERE product_id = 456;
-- 4. 创建订单
INSERT INTO orders (user_id, total_amount) VALUES (123, 99.99);
-- 5. 添加订单项
INSERT INTO order_items (order_id, product_id, quantity) VALUES (LAST_INSERT_ID(), 456, 1);
-- 6. 更新库存
UPDATE products SET stock = stock - 1 WHERE product_id = 456;
-- 7. 记录日志
INSERT INTO order_logs (order_id, action) VALUES (LAST_INSERT_ID(), 'created');
COMMIT;

-- 正确的做法:拆分为多个小事务
-- 推荐
-- 第一步:验证和准备(不需要事务)
SELECT user_id, status FROM users WHERE user_id = 123;
SELECT product_id, stock, price FROM products WHERE product_id = 456;

-- 第二步:核心业务事务(需要强一致性)
START TRANSACTION;
-- 再次检查库存(防止并发修改)
SELECT stock FROM products WHERE product_id = 456 FOR UPDATE;
-- 创建订单
INSERT INTO orders (user_id, total_amount, status) VALUES (123, 99.99, 'pending');
SET @order_id = LAST_INSERT_ID();
-- 添加订单项
INSERT INTO order_items (order_id, product_id, quantity, price) 
VALUES (@order_id, 456, 1, 99.99);
-- 更新库存
UPDATE products SET stock = stock - 1 WHERE product_id = 456;
COMMIT;

-- 第三步:后续处理(可以异步或单独事务)
START TRANSACTION;
INSERT INTO order_logs (order_id, action, created_at) 
VALUES (@order_id, 'created', NOW());
COMMIT;

-- 监控事务大小
SELECT 
    trx_id,
    trx_tables_in_use,
    trx_tables_locked,
    trx_rows_locked,
    trx_rows_modified,
    CASE 
        WHEN trx_rows_locked > 10000 THEN 'Large Transaction'
        WHEN trx_rows_locked > 1000 THEN 'Medium Transaction'
        ELSE 'Small Transaction'
    END as transaction_size
FROM information_schema.innodb_trx
ORDER BY trx_rows_locked DESC;

锁策略优化

锁顺序一致性

  • 统一资源访问顺序
  • 减少死锁发生
  • 提高系统稳定性
  • 需要应用层配合

锁粒度选择

  • 根据并发需求选择
  • 考虑数据访问模式
  • 平衡性能和一致性
  • 动态调整策略

隔离级别选择

业务场景分析

高一致性场景

  • 金融交易系统
  • 库存管理系统
  • 选择较高隔离级别
  • 可以接受性能损失

高性能场景

  • 日志记录系统
  • 统计分析系统
  • 选择较低隔离级别
  • 优先考虑性能

混合场景

  • 不同操作使用不同隔离级别
  • 读操作使用低隔离级别
  • 写操作使用高隔离级别
  • 灵活配置策略

监控与诊断

性能指标

事务相关指标

  • 事务提交率
  • 事务回滚率
  • 平均事务执行时间
  • 事务等待时间
sql
-- 监控事务相关指标
-- 查看事务统计信息
SHOW ENGINE INNODB STATUS\G

-- 查看当前活跃事务
SELECT 
    trx_id,
    trx_state,
    trx_started,
    TIMESTAMPDIFF(SECOND, trx_started, NOW()) as duration_seconds,
    trx_isolation_level,
    trx_tables_in_use,
    trx_tables_locked,
    trx_rows_locked,
    trx_rows_modified,
    trx_mysql_thread_id,
    LEFT(trx_query, 100) as query_preview
FROM information_schema.innodb_trx
ORDER BY trx_started;

-- 事务提交和回滚统计
SELECT 
    VARIABLE_NAME,
    VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME IN (
    'Com_commit',
    'Com_rollback',
    'Handler_commit',
    'Handler_rollback'
);

-- 计算事务成功率
SELECT 
    commits.VARIABLE_VALUE as commits,
    rollbacks.VARIABLE_VALUE as rollbacks,
    ROUND(
        commits.VARIABLE_VALUE / 
        (commits.VARIABLE_VALUE + rollbacks.VARIABLE_VALUE) * 100, 2
    ) as success_rate_percent
FROM 
    (SELECT VARIABLE_VALUE FROM performance_schema.global_status 
     WHERE VARIABLE_NAME = 'Com_commit') commits,
    (SELECT VARIABLE_VALUE FROM performance_schema.global_status 
     WHERE VARIABLE_NAME = 'Com_rollback') rollbacks;

锁相关指标

  • 锁等待次数
  • 锁等待时间
  • 死锁发生频率
  • 锁冲突统计
sql
-- 监控锁相关指标
-- 查看当前锁等待情况
SELECT 
    r.trx_id as waiting_trx_id,
    r.trx_mysql_thread_id as waiting_thread,
    r.trx_query as waiting_query,
    b.trx_id as blocking_trx_id,
    b.trx_mysql_thread_id as blocking_thread,
    b.trx_query as blocking_query,
    TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW()) as wait_time_seconds
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

-- 锁等待统计
SELECT 
    VARIABLE_NAME,
    VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME IN (
    'Innodb_row_lock_current_waits',
    'Innodb_row_lock_time',
    'Innodb_row_lock_time_avg',
    'Innodb_row_lock_time_max',
    'Innodb_row_lock_waits'
);

-- 死锁统计
SELECT 
    VARIABLE_NAME,
    VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_deadlocks';

-- 查看最近的死锁信息
SHOW ENGINE INNODB STATUS\G
-- 在输出中查找 "LATEST DETECTED DEADLOCK" 部分

MVCC 相关指标

  • 版本链长度
  • Undo log 大小
  • 读视图创建频率
  • 垃圾回收效率
sql
-- 监控 MVCC 相关指标
-- Undo log 相关统计
SELECT 
    VARIABLE_NAME,
    VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME LIKE '%undo%' OR VARIABLE_NAME LIKE '%purge%';

-- 查看 Undo 表空间使用情况
SELECT 
    tablespace_name,
    file_name,
    file_size / 1024 / 1024 as size_mb,
    allocated_size / 1024 / 1024 as allocated_mb
FROM information_schema.files
WHERE tablespace_name LIKE '%undo%';

-- 监控历史列表长度(版本链长度的间接指标)
SELECT 
    VARIABLE_NAME,
    VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_history_list_length';

-- 查看 purge 进度
SHOW ENGINE INNODB STATUS\G
-- 在输出中查找 "BACKGROUND THREAD" 部分的 purge 信息

问题诊断

常见问题

  • 长事务阻塞
  • 频繁死锁
  • 锁等待超时
  • 版本链过长
sql
-- 诊断长事务问题
-- 查找运行时间超过30秒的事务
SELECT 
    trx_id,
    trx_state,
    trx_started,
    TIMESTAMPDIFF(SECOND, trx_started, NOW()) as duration_seconds,
    trx_mysql_thread_id,
    trx_query,
    trx_tables_in_use,
    trx_tables_locked,
    trx_rows_locked,
    trx_rows_modified
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 30
ORDER BY duration_seconds DESC;

-- 查找阻塞其他事务的长事务
SELECT DISTINCT
    b.trx_id as blocking_trx_id,
    b.trx_started as blocking_started,
    TIMESTAMPDIFF(SECOND, b.trx_started, NOW()) as blocking_duration,
    b.trx_mysql_thread_id as blocking_thread,
    b.trx_query as blocking_query,
    COUNT(w.requesting_trx_id) as blocked_transactions_count
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
GROUP BY b.trx_id
ORDER BY blocked_transactions_count DESC, blocking_duration DESC;

诊断方法

  • 查看事务状态
  • 分析锁等待情况
  • 检查死锁日志
  • 监控系统性能
sql
-- 诊断锁等待超时问题
-- 查看锁等待超时配置
SELECT @@innodb_lock_wait_timeout;

-- 查看当前锁等待情况
SELECT 
    r.trx_id as waiting_trx,
    r.trx_mysql_thread_id as waiting_thread,
    TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW()) as wait_seconds,
    r.trx_query as waiting_query,
    b.trx_id as blocking_trx,
    b.trx_mysql_thread_id as blocking_thread,
    b.trx_query as blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
ORDER BY wait_seconds DESC;

-- 诊断死锁问题
-- 查看死锁发生频率
SELECT 
    VARIABLE_NAME,
    VARIABLE_VALUE,
    VARIABLE_VALUE / (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Uptime') * 3600 as deadlocks_per_hour
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_deadlocks';

-- 诊断版本链过长问题
-- 查看历史列表长度
SELECT 
    VARIABLE_NAME,
    VARIABLE_VALUE,
    CASE 
        WHEN VARIABLE_VALUE > 1000000 THEN 'Critical - Very Long'
        WHEN VARIABLE_VALUE > 100000 THEN 'Warning - Long'
        WHEN VARIABLE_VALUE > 10000 THEN 'Caution - Moderate'
        ELSE 'Normal'
    END as status
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_history_list_length';

-- 查找可能导致版本链过长的长事务
SELECT 
    trx_id,
    trx_state,
    trx_started,
    TIMESTAMPDIFF(MINUTE, trx_started, NOW()) as duration_minutes,
    trx_isolation_level,
    trx_is_read_only,
    LEFT(trx_query, 200) as query_preview
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(MINUTE, trx_started, NOW()) > 5
ORDER BY duration_minutes DESC;

-- 综合诊断查询
-- 系统整体事务健康状况
SELECT 
    'Active Transactions' as metric,
    COUNT(*) as value
FROM information_schema.innodb_trx
UNION ALL
SELECT 
    'Lock Waits' as metric,
    COUNT(*) as value
FROM information_schema.innodb_lock_waits
UNION ALL
SELECT 
    'Long Transactions (>30s)' as metric,
    COUNT(*) as value
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 30
UNION ALL
SELECT 
    'History List Length' as metric,
    VARIABLE_VALUE as value
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_history_list_length';

总结

MySQL 的事务处理机制是一个复杂而精密的系统,涉及多个层面的技术实现:

  1. ACID 特性:提供了事务处理的理论基础
  2. 锁机制:实现了并发控制和数据一致性
  3. MVCC:提供了高性能的并发读写能力
  4. 死锁处理:确保了系统的稳定性和可用性

理解这些机制对于:

  • 设计高性能的数据库应用
  • 优化事务处理性能
  • 诊断和解决并发问题
  • 确保数据的一致性和可靠性

都具有重要意义。在实际应用中,需要根据具体的业务需求和性能要求,合理选择事务策略和配置参数。