事务与并发控制
大约 14 分钟约 4085 字
事务与并发控制
简介
数据库事务是保证数据一致性和完整性的核心机制,ACID 特性确保了即使在系统故障或并发访问的情况下,数据依然可靠。理解事务隔离级别、多版本并发控制(MVCC)、锁机制以及死锁处理,对于构建高可靠性的数据库应用至关重要。
特点
ACID 特性
事务基本操作
-- ACID 特性说明
-- | 特性 | 英文 | 说明 |
-- |-------|--------------|------------------------------------------|
-- | 原子性 | Atomicity | 事务中的操作要么全部成功,要么全部回滚 |
-- | 一致性 | Consistency | 事务执行前后,数据始终处于一致状态 |
-- | 隔离性 | Isolation | 并发事务之间互不干扰 |
-- | 持久性 | Durability | 事务提交后,数据永久保存,即使系统崩溃也不丢失 |
-- 银行转账示例:展示事务的原子性
BEGIN TRANSACTION;
-- 从账户 A 扣款
UPDATE accounts SET balance = balance - 1000 WHERE account_no = 'A001';
-- 向账户 B 加款
UPDATE accounts SET balance = balance + 1000 WHERE account_no = 'B001';
-- 记录转账日志
INSERT INTO transfer_log (from_account, to_account, amount, created_at)
VALUES ('A001', 'B001', 1000, GETDATE());
-- 检查余额是否充足
DECLARE @balance DECIMAL(10,2);
SELECT @balance = balance FROM accounts WHERE account_no = 'A001';
IF @balance < 0
BEGIN
ROLLBACK TRANSACTION;
PRINT '余额不足,转账失败';
END
ELSE
BEGIN
COMMIT TRANSACTION;
PRINT '转账成功';
END;
-- MySQL 事务操作
START TRANSACTION;
UPDATE accounts SET balance = balance - 1000 WHERE account_no = 'A001';
UPDATE accounts SET balance = balance + 1000 WHERE account_no = 'B001';
COMMIT;
-- 设置保存点
START TRANSACTION;
UPDATE accounts SET balance = balance - 1000 WHERE account_no = 'A001';
SAVEPOINT after_deduct;
UPDATE accounts SET balance = balance + 1000 WHERE account_no = 'B001';
-- 如果出错可以回滚到保存点
ROLLBACK TO SAVEPOINT after_deduct;
COMMIT;隔离级别
四种隔离级别
-- SQL 标准定义了四种隔离级别
-- | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
-- |-----------------|------|-----------|-------|---------------------------|
-- | READ UNCOMMITTED | 可能 | 可能 | 可能 | 最低级别,性能最好 |
-- | READ COMMITTED | 不会 | 可能 | 可能 | Oracle/SQL Server 默认级别 |
-- | REPEATABLE READ | 不会 | 不会 | 可能 | MySQL InnoDB 默认级别 |
-- | SERIALIZABLE | 不会 | 不会 | 不会 | 最高级别,性能最差 |
-- MySQL 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- SQL Server 设置隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- PostgreSQL 设置隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;并发问题演示
-- 脏读(Dirty Read)
-- 会话 A:未提交的数据被会话 B 读取
-- 会话 A
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE account_no = 'A001';
-- 未 COMMIT
-- 会话 B(隔离级别 READ UNCOMMITTED)
SELECT balance FROM accounts WHERE account_no = 'A001';
-- 读到了 A 未提交的修改(脏读)
-- 会话 A
ROLLBACK; -- A 回滚了,B 读到的数据不存在
-- 不可重复读(Non-Repeatable Read)
-- 同一事务中两次读取同一行数据结果不同
-- 会话 A
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM accounts WHERE account_no = 'A001'; -- 结果:5000
-- 会话 B
UPDATE accounts SET balance = 4000 WHERE account_no = 'A001';
COMMIT;
-- 会话 A(再次读取)
SELECT balance FROM accounts WHERE account_no = 'A001'; -- 结果:4000(变了!)
COMMIT;
-- 幻读(Phantom Read)
-- 同一事务中两次范围查询结果不同
-- 会话 A
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM accounts WHERE balance > 3000; -- 返回 3 条记录
-- 会话 B
INSERT INTO accounts (account_no, balance) VALUES ('C001', 5000);
COMMIT;
-- 会话 A(再次查询)
SELECT * FROM accounts WHERE balance > 3000; -- 返回 4 条记录(幻读!)
COMMIT;MVCC 多版本并发控制
MVCC 原理
-- MVCC 核心思想:通过保存数据的多个版本,实现读写互不阻塞
-- 每行数据包含隐藏列:
-- DB_TRX_ID — 最后修改该行的事务 ID
-- DB_ROLL_PTR — 回滚指针,指向 undo log 中的上一个版本
-- DB_ROW_ID — 隐藏自增 ID(无主键时使用)
-- InnoDB MVCC 工作机制
-- 1. READ COMMITTED:每次 SELECT 都生成新的 Read View
-- 2. REPEATABLE READ:事务开始时生成 Read View,后续复用
-- Read View 的可见性判断规则
-- | 条件 | 可见性 |
-- |----------------------------------|--------|
-- | 事务 ID < Read View 最小事务 ID | 可见 |
-- | 事务 ID > Read View 最大事务 ID | 不可见 |
-- | 事务 ID 在活跃事务列表中 | 不可见 |
-- | 事务 ID 不在活跃事务列表中 | 可见 |
-- 查看 InnoDB 事务状态
-- MySQL
SELECT * FROM information_schema.INNODB_TRX;
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM performance_schema.data_locks; -- MySQL 8.0+
-- 查看当前活跃事务
SELECT trx_id, trx_state, trx_started, trx_query
FROM information_schema.INNODB_TRX
ORDER BY trx_started;
-- PostgreSQL MVCC 实现
-- 每行数据包含 xmin(创建事务 ID)和 xmax(删除事务 ID)
SELECT xmin, xmax, * FROM accounts WHERE account_no = 'A001';
-- 查看 PostgreSQL 事务 ID
SELECT txid_current();
-- 手动清理 dead tuples(PostgreSQL)
VACUUM accounts;
-- 自动清理分析
VACUUM ANALYZE accounts;锁机制
共享锁与排他锁
-- 锁类型对比
-- | 锁类型 | 英文 | 说明 | 兼容性 |
-- |------------|-------------------|-------------------------------|----------------|
-- | 共享锁 | Shared Lock (S) | 读锁,允许多个事务同时读 | 与 S 锁兼容 |
-- | 排他锁 | Exclusive Lock (X) | 写锁,阻止其他事务读写 | 与任何锁不兼容 |
-- | 意向共享锁 | IS | 表级锁,表示打算加行级 S 锁 | 与 IS/IX 兼容 |
-- | 意向排他锁 | IX | 表级锁,表示打算加行级 X 锁 | 与 IS/IX 兼容 |
-- MySQL 加锁查询
-- 共享锁(S Lock)
SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE;
-- MySQL 8.0+ 推荐语法
SELECT * FROM orders WHERE id = 1 FOR SHARE;
-- 排他锁(X Lock)
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
-- SQL Server 加锁提示
SELECT * FROM orders WITH (ROWLOCK, XLOCK) WHERE id = 1;
SELECT * FROM orders WITH (NOLOCK) WHERE id = 1; -- 等价于 READ UNCOMMITTED
-- PostgreSQL 加锁
SELECT * FROM orders WHERE id = 1 FOR SHARE;
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
SELECT * FROM orders WHERE id = 1 FOR NO KEY UPDATE;
SELECT * FROM orders WHERE id = 1 FOR KEY SHARE;行级锁与表级锁
-- 行级锁(InnoDB 默认)
-- 只锁定匹配的行,其他行仍可被其他事务操作
START TRANSACTION;
UPDATE orders SET status = 'processing' WHERE id = 1;
-- 只锁定 id=1 这一行,其他行不受影响
COMMIT;
-- 表级锁
-- MySQL 显式加表锁
LOCK TABLES orders READ; -- 读锁
LOCK TABLES orders WRITE; -- 写锁
UNLOCK TABLES;
-- SQL Server 表锁提示
SELECT * FROM orders WITH (TABLOCKX) WHERE id = 1; -- 排他表锁
SELECT * FROM orders WITH (TABLOCK) WHERE id = 1; -- 共享表锁
-- 间隙锁(Gap Lock)— InnoDB 特有
-- 防止在索引记录之间的间隙中插入数据,解决幻读问题
START TRANSACTION;
SELECT * FROM orders WHERE id BETWEEN 10 AND 20 FOR UPDATE;
-- 锁定 id 10-20 的范围(包括间隙),防止其他事务在此范围插入
COMMIT;
-- 临键锁(Next-Key Lock)= 行锁 + 间隙锁
-- InnoDB 在 REPEATABLE READ 级别的默认行锁算法
-- 锁定索引记录本身 + 索引记录前面的间隙
-- MySQL 8.0 查看锁信息
SELECT
ENGINE_TRANSACTION_ID,
OBJECT_NAME,
INDEX_NAME,
LOCK_TYPE,
LOCK_MODE,
LOCK_DATA
FROM performance_schema.data_locks
WHERE OBJECT_SCHEMA = 'mydb';
-- 锁等待超时设置
SET innodb_lock_wait_timeout = 30; -- 默认 50 秒死锁处理
死锁检测与解决
-- 死锁场景:两个事务互相等待对方持有的锁
-- 事务 A
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_no = 'A001';
-- 此时事务 A 持有 A001 的行锁
-- 事务 B(另一个会话)
START TRANSACTION;
UPDATE accounts SET balance = balance - 200 WHERE account_no = 'B001';
-- 此时事务 B 持有 B001 的行锁
-- 事务 A 继续
UPDATE accounts SET balance = balance + 100 WHERE account_no = 'B001';
-- 等待事务 B 释放 B001 的锁...
-- 事务 B 继续
UPDATE accounts SET balance = balance + 200 WHERE account_no = 'A001';
-- 等待事务 A 释放 A001 的锁... 死锁!
-- InnoDB 自动检测死锁并回滚代价最小的事务
-- ERROR 1213 (40001): Deadlock found when trying to get lock
-- 查看最近的死锁信息
SHOW ENGINE INNODB STATUS;
-- 查看 LATEST DETECTED DEADLOCK 部分
-- MySQL 8.0+ 死锁监控
SELECT * FROM performance_schema.events_transactions_current
WHERE STATE = 'LOCKED';
-- 开启死锁日志
SET GLOBAL innodb_print_all_deadlocks = ON;死锁预防策略
-- 1. 按固定顺序访问表和行
-- 好的做法:总是先操作 A001 再操作 B001
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_no = 'A001';
UPDATE accounts SET balance = balance + 100 WHERE account_no = 'B001';
COMMIT;
-- 2. 保持事务简短,减少锁持有时间
-- 避免在事务中执行耗时操作(如网络请求、文件操作)
BEGIN;
-- 只做数据库操作,不要夹杂业务逻辑
UPDATE accounts SET balance = balance - 100 WHERE account_no = 'A001';
UPDATE accounts SET balance = balance + 100 WHERE account_no = 'B001';
COMMIT;
-- 3. 使用较低的隔离级别(在业务允许的情况下)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 4. 添加合适的索引,避免锁升级
-- 没有索引时,UPDATE 可能锁住全表
CREATE INDEX idx_account_no ON accounts(account_no);
-- 5. 设置锁等待超时
SET innodb_lock_wait_timeout = 10; -- 10 秒超时
-- 6. 使用乐观锁替代悲观锁
-- 乐观锁:通过版本号实现
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;
-- 如果影响行数为 0,说明版本号已被其他事务修改
-- 7. SQL Server 死锁优先级设置
SET DEADLOCK_PRIORITY LOW; -- 低优先级,死锁时优先被回滚
SET DEADLOCK_PRIORITY HIGH; -- 高优先级,死锁时不会被回滚编程语言中的事务管理
C# 事务实现
// 1. ADO.NET 手动事务
public async Task TransferAsync(string fromAccount, string toAccount, decimal amount)
{
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
await using var transaction = await connection.BeginTransactionAsync(
IsolationLevel.RepeatableRead);
try
{
// 扣款
await using var debitCmd = new SqlCommand(
"UPDATE accounts SET balance = balance - @amount WHERE account_no = @account",
connection, transaction);
debitCmd.Parameters.AddWithValue("@amount", amount);
debitCmd.Parameters.AddWithValue("@account", fromAccount);
var debitRows = await debitCmd.ExecuteNonQueryAsync();
if (debitRows == 0)
throw new Exception($"账户 {fromAccount} 不存在");
// 检查余额
await using var balanceCmd = new SqlCommand(
"SELECT balance FROM accounts WHERE account_no = @account",
connection, transaction);
balanceCmd.Parameters.AddWithValue("@account", fromAccount);
var balance = (decimal)await balanceCmd.ExecuteScalarAsync();
if (balance < 0)
throw new Exception("余额不足");
// 加款
await using var creditCmd = new SqlCommand(
"UPDATE accounts SET balance = balance + @amount WHERE account_no = @account",
connection, transaction);
creditCmd.Parameters.AddWithValue("@amount", amount);
creditCmd.Parameters.AddWithValue("@account", toAccount);
await creditCmd.ExecuteNonQueryAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
// 2. EF Core 事务
public async Task TransferEFCoreAsync(string from, string to, decimal amount)
{
await using var context = new AppDbContext();
await using var transaction = await context.Database.BeginTransactionAsync();
try
{
var fromAccount = await context.Accounts
.FirstOrDefaultAsync(a => a.AccountNo == from);
var toAccount = await context.Accounts
.FirstOrDefaultAsync(a => a.AccountNo == to);
fromAccount.Balance -= amount;
toAccount.Balance += amount;
await context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
// 3. 跨数据库事务(分布式事务)
// 使用 TransactionScope 实现
public async Task DistributedTransactionAsync()
{
var transactionOptions = new TransactionOptions
{
IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted,
Timeout = TransactionManager.MaximumTimeout
};
using var scope = new TransactionScope(
TransactionScopeOption.Required,
transactionOptions,
TransactionScopeAsyncFlowOption.Enabled);
// 操作数据库 A
using (var contextA = new DbContextA())
{
await contextA.Database.ExecuteSqlRawAsync("UPDATE ...");
}
// 操作数据库 B
using (var contextB = new DbContextB())
{
await contextB.Database.ExecuteSqlRawAsync("UPDATE ...");
}
scope.Complete(); // 提交分布式事务
}Java Spring 事务管理
// Spring Boot 事务管理
@Service
@Transactional
public class AccountService {
@Autowired
private AccountRepository accountRepository;
// 声明式事务 — 最常用
@Transactional(
isolation = Isolation.REPEATABLE_READ,
propagation = Propagation.REQUIRED,
rollbackFor = Exception.class,
timeout = 30
)
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
Account from = accountRepository.findByAccountNo(fromAccount)
.orElseThrow(() -> new BusinessException("账户不存在: " + fromAccount));
Account to = accountRepository.findByAccountNo(toAccount)
.orElseThrow(() -> new BusinessException("账户不存在: " + toAccount));
if (from.getBalance().compareTo(amount) < 0) {
throw new BusinessException("余额不足");
}
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
}
// 只读事务 — 优化查询性能
@Transactional(readOnly = true)
public Account getAccount(String accountNo) {
return accountRepository.findByAccountNo(accountNo)
.orElseThrow(() -> new BusinessException("账户不存在"));
}
// 嵌套事务 — 支持独立回滚
@Transactional(propagation = Propagation.NESTED)
public void logTransaction(String accountNo, BigDecimal amount) {
// 即使外层事务回滚,这里的日志也可以保留
transactionLogRepository.save(new TransactionLog(accountNo, amount));
}
}
// 编程式事务 — 更精细的控制
@Service
public class OrderService {
@Autowired
private TransactionTemplate transactionTemplate;
public Order createOrder(OrderRequest request) {
return transactionTemplate.execute(status -> {
try {
Order order = new Order(request);
orderRepository.save(order);
inventoryRepository.decrement(request.getProductId(), request.getQuantity());
return order;
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
});
}
}优点
事务性能优化
长事务诊断与优化
-- MySQL 查找长事务
SELECT
trx_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_seconds,
trx_query,
trx_tables_locked,
trx_rows_locked
FROM information_schema.INNODB_TRX
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 10
ORDER BY duration_seconds DESC;
-- SQL Server 查找长事务
SELECT
session_id,
start_time,
DATEDIFF(SECOND, start_time, GETDATE()) AS duration_seconds,
status,
command,
wait_type,
blocking_session_id
FROM sys.dm_exec_requests
WHERE session_id > 50
ORDER BY duration_seconds DESC;
-- 事务优化建议
-- 1. 避免在事务中执行网络调用(HTTP 请求、RPC 等)
-- 2. 将查询操作移到事务外,只在最终写入时开启事务
-- 3. 合理使用只读事务,减少锁持有时间
-- 4. 使用小批量提交代替大批量事务
-- 5. 监控事务持有锁的时间,设置超时告警SQL Server 快照隔离
-- SQL Server RCSI(Read Committed Snapshot Isolation)
-- 不阻塞读操作,使用行版本控制
-- 启用 RCSI(数据库级别)
ALTER DATABASE MyDB SET READ_COMMITTED_SNAPSHOT ON;
-- 启用快照隔离
ALTER DATABASE MyDB SET ALLOW_SNAPSHOT_ISOLATION ON;
-- 使用快照隔离
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
-- 读取的数据是事务开始时的版本,不会被其他事务修改阻塞
SELECT * FROM accounts WHERE account_no = 'A001';
COMMIT;
-- RCSI vs 快照隔离的区别:
-- RCSI:每次语句读取最新已提交版本(语句级一致性)
-- 快照隔离:事务内所有语句读取事务开始时的版本(事务级一致性)
-- 快照隔离需要检查更新冲突,RCSI 不需要缺点
总结
事务与并发控制是数据库保证数据一致性和可靠性的核心机制。ACID 特性为事务提供了坚实的理论基础,四种隔离级别在一致性和性能之间提供了灵活的权衡。MVCC 通过多版本实现了读写互不阻塞,而各种锁机制则确保了并发操作的正确性。在实际开发中,需要根据业务特点选择合适的隔离级别和锁策略,并遵循最佳实践来预防死锁的发生。
关键知识点
- 数据库主题一定要同时看数据模型、读写模式和执行代价。
- 很多性能问题不是 SQL 语法问题,而是索引、统计信息、事务和数据分布问题。
- 高可用、备份、迁移和治理与查询优化同样重要。
- 先把数据模型、访问模式和执行代价绑定起来理解。
项目落地视角
- 所有优化前后都保留执行计划、样本 SQL 和关键指标对比。
- 上线前准备回滚脚本、备份点和校验方案。
- 把连接池、锁等待、慢查询和容量增长纳入日常巡检。
- 保留执行计划、样本 SQL、索引定义和优化前后指标。
常见误区
- 脱离真实数据分布讨论索引或分片。
- 只看单条 SQL,不看整条业务链路的事务和锁。
- 把测试环境结论直接等同于生产环境结论。
- 脱离真实数据分布设计索引。
进阶路线
- 继续向执行计划、存储引擎、复制机制和数据治理层深入。
- 把主题与 ORM、缓存、消息队列和归档策略联动起来思考。
- 沉淀成数据库设计规范、SQL 审核规则和变更流程。
- 继续深入存储引擎、复制机制、归档与冷热分层治理。
适用场景
- 当你准备把《事务与并发控制》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合数据建模、查询优化、事务控制、高可用和迁移治理。
- 当系统开始遇到慢查询、锁冲突、热点数据或容量增长时,这类主题价值最高。
落地建议
- 先分析真实查询模式、数据量级和写入特征,再决定索引或分片策略。
- 所有优化结论都结合执行计划、样本数据和监控指标验证。
- 高风险操作前准备备份、回滚脚本与校验 SQL。
排错清单
- 先确认瓶颈在 CPU、I/O、锁等待、网络还是 SQL 本身。
- 检查执行计划是否走错索引、是否发生排序或全表扫描。
- 排查长事务、隐式类型转换、统计信息过期和参数嗅探。
复盘问题
- 如果把《事务与并发控制》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《事务与并发控制》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《事务与并发控制》最大的收益和代价分别是什么?
