事务与并发

0x01 事务基础

什么是事务?

事务是数据库操作的逻辑单位,它将一系列操作作为一个整体执行,要么全部成功,要么全部失败。

ACID 特性

特性 说明 示例
原子性(Atomicity) 事务中的操作要么全部成功,要么全部失败 转账:扣款和加款必须同时成功或失败
一致性(Consistency) 事务前后数据库保持一致状态 转账前后总金额不变
隔离性(Isolation) 并发事务之间相互隔离 两个转账事务互不干扰
持久性(Durability) 事务提交后,数据永久保存 提交后的数据不会丢失

0x02 事务操作

基本事务控制

-- 开始事务
START TRANSACTION;  -- MySQL
BEGIN;              -- PostgreSQL/通用

-- 执行操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 提交事务
COMMIT;

-- 回滚事务
ROLLBACK;

自动提交模式

-- MySQL: 查看自动提交状态
SHOW VARIABLES LIKE 'autocommit';

-- 关闭自动提交
SET autocommit = 0;

-- 开启自动提交
SET autocommit = 1;

-- PostgreSQL: 查看自动提交状态
SHOW autocommit;

保存点(Savepoint)

BEGIN;

-- 第一个操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- 创建保存点
SAVEPOINT point1;

-- 第二个操作
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 回滚到保存点(撤销第二个操作,保留第一个)
ROLLBACK TO point1;

-- 继续其他操作
UPDATE accounts SET balance = balance + 100 WHERE id = 3;

-- 提交事务
COMMIT;

0x03 并发问题

脏读(Dirty Read)

读取到其他事务未提交的数据:

-- 事务 A
BEGIN;
UPDATE accounts SET balance = 1000 WHERE id = 1;
-- 未提交

-- 事务 B(如果隔离级别允许脏读)
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- 可能读到 1000
COMMIT;

-- 事务 A 回滚
ROLLBACK;  -- 事务 B 读到的数据是无效的

不可重复读(Non-repeatable Read)

同一事务内,两次读取同一数据得到不同结果:

-- 事务 A
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- 读到 500
-- 事务 B 在此时修改并提交
SELECT balance FROM accounts WHERE id = 1;  -- 可能读到 600
COMMIT;

幻读(Phantom Read)

同一事务内,两次查询得到不同数量的行:

-- 事务 A
BEGIN;
SELECT COUNT(*) FROM accounts WHERE balance > 1000;  -- 结果: 5
-- 事务 B 在此时插入新记录并提交
SELECT COUNT(*) FROM accounts WHERE balance > 1000;  -- 可能结果: 6
COMMIT;

0x04 隔离级别

隔离级别对比

隔离级别 脏读 不可重复读 幻读 说明
READ UNCOMMITTED 最低隔离,性能最好
READ COMMITTED 多数数据库默认
REPEATABLE READ MySQL 默认
SERIALIZABLE 最高隔离,性能最差

设置隔离级别

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

-- MySQL: 设置会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- MySQL: 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- PostgreSQL: 查看当前隔离级别
SHOW transaction_isolation;

-- PostgreSQL: 设置事务隔离级别(必须在事务开始时设置)
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;

各隔离级别示例

-- READ COMMITTED 示例
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE id = 1;  -- 500
-- 其他事务修改并提交
SELECT balance FROM accounts WHERE id = 1;  -- 可能 600(不可重复读)
COMMIT;

-- REPEATABLE READ 示例
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1;  -- 500
-- 其他事务修改并提交
SELECT balance FROM accounts WHERE id = 1;  -- 仍然是 500(可重复读)
COMMIT;

-- SERIALIZABLE 示例
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT COUNT(*) FROM accounts WHERE balance > 1000;  -- 5
-- 其他事务尝试插入会等待或失败
SELECT COUNT(*) FROM accounts WHERE balance > 1000;  -- 仍然是 5
COMMIT;

0x05 锁机制

锁的类型

共享锁(Shared Lock / S Lock)

-- MySQL: 共享锁
BEGIN;
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;
-- 其他事务可以读取,但不能修改
COMMIT;

-- PostgreSQL: 共享锁
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
COMMIT;

排他锁(Exclusive Lock / X Lock)

-- MySQL: 排他锁
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 其他事务不能读取(除非使用非锁定读)或修改
COMMIT;

-- PostgreSQL: 排他锁
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
COMMIT;

悲观锁与乐观锁

悲观锁

-- 使用 SELECT ... FOR UPDATE 实现悲观锁
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

乐观锁

-- 使用版本号实现乐观锁
CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    stock INT,
    version INT DEFAULT 1
);

-- 更新时检查版本号
UPDATE products 
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 1;  -- 如果版本号不匹配,更新失败

死锁处理

-- 死锁示例
-- 事务 A
BEGIN;
UPDATE accounts SET balance = 100 WHERE id = 1;  -- 锁定 id=1
UPDATE accounts SET balance = 200 WHERE id = 2;  -- 等待事务 B 释放 id=2
COMMIT;

-- 事务 B
BEGIN;
UPDATE accounts SET balance = 200 WHERE id = 2;  -- 锁定 id=2
UPDATE accounts SET balance = 100 WHERE id = 1;  -- 等待事务 A 释放 id=1(死锁)
COMMIT;

-- 解决方案:按固定顺序访问资源
-- 事务 A 和 B 都先访问 id=1,再访问 id=2

0x06 MVCC 多版本并发控制

MVCC 原理

MVCC(Multi-Version Concurrency Control)通过保存数据的多个版本来实现并发控制,读操作不阻塞写操作,写操作不阻塞读操作。

PostgreSQL MVCC

-- 查看事务 ID
SELECT txid_current();

-- 查看行版本信息
SELECT xmin, xmax, * FROM accounts WHERE id = 1;

-- 创建快照
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT pg_export_snapshot();  -- 导出快照

MySQL InnoDB MVCC

-- InnoDB 使用 undo log 实现 MVCC
-- 查看当前事务 ID
SELECT trx_id FROM information_schema.innodb_trx 
WHERE trx_mysql_thread_id = CONNECTION_ID();

0x07 分布式事务

两阶段提交(2PC)

-- MySQL XA 事务
-- 阶段 1: 准备
XA START 'tx1';
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
XA END 'tx1';
XA PREPARE 'tx1';

-- 阶段 2: 提交/回滚
XA COMMIT 'tx1';  -- 或 XA ROLLBACK 'tx1'

PostgreSQL 两阶段提交

-- 准备事务
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
PREPARE TRANSACTION 'tx1';

-- 提交/回滚
COMMIT PREPARED 'tx1';  -- 或 ROLLBACK PREPARED 'tx1'

0x08 事务最佳实践

事务设计原则

-- 1. 保持事务简短
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;  -- 快速提交

-- 2. 避免长事务
-- 不好:事务中包含大量操作或用户交互
BEGIN;
-- 大量操作...
-- 等待用户输入...
COMMIT;

-- 3. 合理使用隔离级别
-- 大多数应用使用 READ COMMITTED 即可
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

错误处理

-- MySQL 存储过程中的事务处理
DELIMITER //
CREATE PROCEDURE transfer_money(IN from_id INT, IN to_id INT, IN amount DECIMAL(10,2))
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Transfer failed';
    END;

    START TRANSACTION;
    UPDATE accounts SET balance = balance - amount WHERE id = from_id;
    UPDATE accounts SET balance = balance + amount WHERE id = to_id;
    COMMIT;
END //
DELIMITER ;

-- PostgreSQL 错误处理
DO $$
BEGIN
    BEGIN
        UPDATE accounts SET balance = balance - 100 WHERE id = 1;
        UPDATE accounts SET balance = balance + 100 WHERE id = 2;
    EXCEPTION WHEN OTHERS THEN
        RAISE NOTICE 'Error occurred: %', SQLERRM;
        -- 自动回滚
    END;
END $$;

参考