一、事务的ACID特性与实现原理
1.1 什么是事务?
事务是一组操作,要么全部成功,要么全部失败,目的是为了保证数据最终的一致性。
1.2 ACID特性详解
| 特性 | 含义 | MySQL实现机制 |
|---|---|---|
| 原子性 (Atomicity) | 事务的操作要么同时成功,要么同时失败 | 通过undo log实现,记录事务操作前的数据版本,用于回滚 |
| 一致性 (Consistency) | 事务前后数据的完整性约束不被破坏 | 由原子性、隔离性、持久性以及业务逻辑共同保证 |
| 隔离性 (Isolation) | 事务并发执行时,内部操作不能互相干扰 | 通过锁机制和MVCC实现 |
| 持久性 (Durability) | 事务提交后,对数据库的改变是永久性的 | 通过redo log实现,先写日志后写磁盘 |
1.3 关键实现机制
redo log(重做日志):
-
物理日志,记录"在某个数据页上做了什么修改"
-
顺序写入,性能高
-
保证事务的持久性,用于崩溃恢复
undo log(回滚日志):
-
逻辑日志,记录事务操作前的数据状态
-
用于事务回滚和MVCC
-
保证事务的原子性
注意 :redo log保证事务提交后数据不丢失;undo log保证事务可以回滚到之前的状态。
二、并发事务带来的问题
2.1 四大并发问题
| 问题 | 描述 | 示例 |
|---|---|---|
| 脏读 (Dirty Read) | 读到其他事务未提交的数据 | 事务A读到事务B修改但未提交的数据,B回滚后A读到的是脏数据 |
| 不可重复读 (Non-Repeatable Read) | 同一事务内多次读取同一数据结果不一致 | 事务A两次读取同一条记录,中间事务B修改了该记录并提交 |
| 幻读 (Phantom Read) | 同一事务内多次查询的结果集不一致 | 事务A两次查询同一条件,中间事务B插入了满足条件的新记录 |
| 更新丢失 (Lost Update) | 多个事务同时更新同一行,后提交的覆盖了先提交的 | 事务A和B同时读取并修改同一数据,A先提交,B后提交覆盖了A的修改 |
2.2 MySQL 5.7 vs MySQL 8.0 差异
MySQL 5.7:
-- 查看事务隔离级别
SHOW VARIABLES LIKE 'tx_isolation';
-- 设置事务隔离级别
SET tx_isolation = 'REPEATABLE-READ';
MySQL 8.0:
-- 查看事务隔离级别(参数名变了!)
SHOW VARIABLES LIKE '%isolation%';
-- 设置事务隔离级别
SET transaction_isolation = 'REPEATABLE-READ';
重要 :MySQL 8.0将参数tx_isolation改名为transaction_isolation,Java开发者在连接不同版本MySQL时需要注意。
三、事务隔离级别与解决方案
3.1 四种隔离级别对比
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 实现机制 |
|---|---|---|---|---|---|
| 读未提交 (Read Uncommitted) | ✅ 可能 | ✅ 可能 | ✅ 可能 | 最高 | 无锁,直接读最新数据 |
| 读已提交 (Read Committed) | ❌ 不可能 | ✅ 可能 | ✅ 可能 | 较高 | 语句级快照读 |
| 可重复读 (Repeatable Read) | ❌ 不可能 | ❌ 不可能 | ✅ 可能 | 中等 | 事务级快照读 + MVCC |
| 串行化 (Serializable) | ❌ 不可能 | ❌ 不可能 | ❌ 不可能 | 最低 | 读写锁 + 范围锁 |
3.2 实战演示
创建测试表:
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `account` (`name`, `balance`) VALUES ('lilei', 450);
INSERT INTO `account` (`name`, `balance`) VALUES ('hanmei', 16000);
INSERT INTO `account` (`name`, `balance`) VALUES ('lucy', 2400);
场景1:脏读(读未提交级别)
-- 客户端A:设置读未提交
SET SESSION transaction_isolation = 'READ-UNCOMMITTED';
START TRANSACTION;
SELECT * FROM account WHERE id = 1; -- 余额450
-- 客户端B:更新但未提交
SET SESSION transaction_isolation = 'READ-UNCOMMITTED';
START TRANSACTION;
UPDATE account SET balance = 400 WHERE id = 1;
-- 客户端A:再次读取,看到未提交的数据(脏读)
SELECT * FROM account WHERE id = 1; -- 余额400(脏数据)
场景2:不可重复读(读已提交级别)
-- 客户端A:设置读已提交
SET SESSION transaction_isolation = 'READ-COMMITTED';
START TRANSACTION;
SELECT * FROM account WHERE id = 1; -- 余额450
-- 客户端B:更新并提交
SET SESSION transaction_isolation = 'READ-COMMITTED';
START TRANSACTION;
UPDATE account SET balance = 400 WHERE id = 1;
COMMIT;
-- 客户端A:再次读取,看到已提交的数据(不可重复读)
SELECT * FROM account WHERE id = 1; -- 余额400(不可重复读)
场景3:幻读(可重复读级别)
-- 客户端A:设置可重复读
SET SESSION transaction_isolation = 'REPEATABLE-READ';
START TRANSACTION;
SELECT * FROM account WHERE id > 3; -- 无记录
-- 客户端B:插入新记录并提交
INSERT INTO account VALUES (4, 'lily', 700);
COMMIT;
-- 客户端A:再次查询,看不到新记录(避免幻读)
SELECT * FROM account WHERE id > 3; -- 无记录
-- 但是!如果客户端A执行更新,会更新到新记录
UPDATE account SET balance = 888 WHERE id = 4; -- 成功更新
SELECT * FROM account WHERE id > 3; -- 现在能看到id=4的记录
注意 :MySQL的REPEATABLE-READ级别通过MVCC避免了大部分的幻读,但当前读(UPDATE/DELETE)仍可能遇到幻读问题。
四、MySQL锁机制详解
4.1 锁的分类
按粒度分:
| 锁类型 | 开销 | 加锁速度 | 死锁 | 并发度 | 使用场景 |
|---|---|---|---|---|---|
| 表锁 | 小 | 快 | 不会 | 最低 | 全表数据迁移 |
| 行锁 | 大 | 慢 | 会 | 最高 | 高并发OLTP |
按类型分:
| 锁类型 | 简称 | 特性 | SQL示例 |
|---|---|---|---|
| 共享锁 | S锁 | 允许多个事务读,不允许写 | SELECT ... LOCK IN SHARE MODE |
| 排他锁 | X锁 | 不允许其他事务读写 | SELECT ... FOR UPDATE |
按实现分:
| 锁类型 | 描述 | 解决什么问题 |
|---|---|---|
| 记录锁 | 锁住单行记录 | 防止并发修改同一行 |
| 间隙锁 | 锁住记录之间的间隙 | 防止幻读 |
| 临键锁 | 记录锁 + 间隙锁 | MySQL默认行锁实现 |
4.2 MyISAM vs InnoDB 锁机制对比
MyISAM(表锁):
-- 手动加表锁
LOCK TABLE mylock READ; -- 读锁
LOCK TABLE mylock WRITE; -- 写锁
-- 查看表锁
SHOW OPEN TABLES;
-- 释放锁
UNLOCK TABLES;
特性:
-
读锁:不会阻塞其他读,但会阻塞写
-
写锁:会阻塞其他读写
-
自动加锁:SELECT加读锁,INSERT/UPDATE/DELETE加写锁
InnoDB(行锁):
-- 手动加行锁
SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 排他锁
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE; -- 共享锁
特性:
-
默认隔离级别为REPEATABLE-READ
-
SELECT不加锁(快照读)
-
INSERT/UPDATE/DELETE自动加排他锁
-
支持行锁、间隙锁、临键锁
4.3 间隙锁与临键锁
间隙锁示例:
-- 表数据:id=1,2,3,10,20
-- 间隙有:(3,10), (10,20), (20,+∞)
-- 会话1:锁住id在(8,18)的范围,实际会锁住(3,20)的间隙
START TRANSACTION;
UPDATE account SET name = 'zhuge' WHERE id > 8 AND id < 18;
-- 会话2:以下操作都会被阻塞
INSERT INTO account VALUES (5, 'test', 100); -- id=5在(3,10)区间
INSERT INTO account VALUES (15, 'test', 100); -- id=15在(10,20)区间
UPDATE account SET balance = 100 WHERE id = 10; -- id=10在边界上
临键锁 = 记录锁 + 间隙锁
-
锁住记录本身和记录之前的间隙
-
MySQL默认的行锁实现方式
-
在REPEATABLE-READ级别下工作
4.4 行锁升级为表锁
重要规则:InnoDB的行锁是针对索引加的锁!
-- 情况1:使用索引字段,加行锁
UPDATE account SET balance = 800 WHERE id = 1; -- 行锁
-- 情况2:使用非索引字段,可能升级为表锁!
UPDATE account SET balance = 800 WHERE name = 'lilei'; -- name无索引,表锁!
-- 情况3:索引失效,也会升级为表锁
UPDATE account SET balance = 800 WHERE name LIKE '%lei%'; -- 模糊查询可能不走索引
优化建议:
-
为WHERE条件字段建立索引
-
避免索引失效(函数、类型转换等)
-
使用EXPLAIN检查SQL执行计划
五、MVCC多版本并发控制
5.1 MVCC是什么?
MVCC(Multi-Version Concurrency Control)多版本并发控制,通过保存数据的历史版本,实现读写不阻塞。
5.2 实现原理
核心组件:
-
隐藏字段:
-
DB_TRX_ID:最近修改事务ID -
DB_ROLL_PTR:回滚指针,指向undo log -
DB_ROW_ID:隐藏自增ID(无主键时)
-
-
undo log:
-
记录数据的历史版本
-
形成版本链,用于实现快照读
-
-
ReadView:
-
事务开启时创建的数据快照
-
决定了事务能看到哪些版本的数据
-
5.3 快照读 vs 当前读
| 读类型 | SQL示例 | 读取的数据 | 加锁情况 |
|---|---|---|---|
| 快照读 | SELECT |
历史版本数据(ReadView) | 不加锁 |
| 当前读 | SELECT ... FOR UPDATE |
最新数据 | 加锁 |
| 当前读 | INSERT/UPDATE/DELETE |
最新数据 | 加锁 |
不同隔离级别的ReadView生成时机:
-
READ-COMMITTED:语句级快照,每个SELECT生成新的ReadView -
REPEATABLE-READ:事务级快照,第一个SELECT生成ReadView,后续复用
5.4 MVCC工作流程
-- 示例:可重复读级别下的MVCC
-- 事务A(事务ID=100)
START TRANSACTION;
SELECT * FROM account WHERE id = 1; -- 创建ReadView,看到balance=450
-- 事务B(事务ID=200)
START TRANSACTION;
UPDATE account SET balance = 400 WHERE id = 1;
COMMIT; -- 提交后生成新版本,balance=400
-- 事务A再次查询
SELECT * FROM account WHERE id = 1; -- 复用ReadView,仍然看到balance=450(可重复读)
-- 事务A执行更新
UPDATE account SET balance = balance - 50 WHERE id = 1; -- 当前读,读取balance=400
-- 实际计算:400-50=350,不是450-50=400!
六、死锁与锁监控
6.1 死锁示例
-- 会话1
START TRANSACTION;
SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 锁住id=1
-- 会话2
START TRANSACTION;
SELECT * FROM account WHERE id = 2 FOR UPDATE; -- 锁住id=2
-- 会话1
SELECT * FROM account WHERE id = 2 FOR UPDATE; -- 等待会话2释放锁
-- 会话2
SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 等待会话1释放锁,死锁!
6.2 死锁检测与解决
MySQL会自动检测死锁并回滚其中一个事务:
-- 查看近期死锁信息
SHOW ENGINE INNODB STATUS\G
-- 查看锁等待信息
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
-- 查看当前事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
-- 查看当前锁
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
-- 杀死事务
KILL [trx_mysql_thread_id];
6.3 锁监控指标
-- 查看行锁争用情况
SHOW STATUS LIKE 'innodb_row_lock%';
-- 重要指标:
-- Innodb_row_lock_current_waits:当前等待锁的数量
-- Innodb_row_lock_time_avg:平均等待时间(ms)
-- Innodb_row_lock_waits:总等待次数
-- Innodb_row_lock_time:总等待时间(ms)
监控阈值建议:
-
平均等待时间 > 50ms:需要优化
-
总等待次数 > 1000/小时:需要分析原因
七、阿里巴巴事务优化最佳实践
7.1 大事务的危害
-
连接池撑爆:长时间占用连接,导致连接池耗尽
-
锁竞争严重:锁定大量数据,阻塞其他事务
-
主从延迟:执行时间长,从库复制延迟
-
回滚困难:回滚时间长,undo log膨胀
-
死锁风险:事务复杂,容易产生死锁
7.2 优化原则
1. 事务粒度最小化
// ❌ 错误示例:大事务
@Transactional
public void processOrder(Order order) {
// 1. 查询验证(可以放在事务外)
validateOrder(order);
// 2. 远程调用(可能超时)
inventoryService.deductStock(order);
// 3. 本地数据库操作
orderDao.save(order);
orderItemDao.saveItems(order.getItems());
// 4. 发送消息
messageService.sendOrderCreated(order);
}
// ✅ 优化示例:拆分事务
public void processOrderOptimized(Order order) {
// 1. 查询验证(非事务)
validateOrder(order);
// 2. 远程调用(设置超时)
inventoryService.deductStock(order);
// 3. 核心操作(小事务)
saveOrderCore(order);
// 4. 异步发送消息
asyncSendMessage(order);
}
@Transactional
public void saveOrderCore(Order order) {
orderDao.save(order);
orderItemDao.saveItems(order.getItems());
}
2. 避免事务中的远程调用
// ❌ 远程调用在事务内
@Transactional
public void createUser(User user) {
userDao.save(user);
// 远程调用,可能超时导致事务长时间持有锁
remoteService.syncUser(user);
}
// ✅ 远程调用在事务外
public void createUserOptimized(User user) {
userDao.save(user); // 本地事务
// 异步远程调用
asyncExecutor.execute(() -> {
try {
remoteService.syncUser(user);
} catch (Exception e) {
log.error("同步用户失败", e);
// 补偿机制
compensateSyncUser(user);
}
});
}
3. 加锁操作靠后执行
// ❌ 加锁操作在前
@Transactional
public void updateBalance(Long userId, BigDecimal amount) {
// 先加锁
Account account = accountDao.selectForUpdate(userId);
// 复杂的业务逻辑(可能耗时)
complexBusinessLogic();
// 最后更新
account.setBalance(account.getBalance().add(amount));
accountDao.update(account);
}
// ✅ 加锁操作在后
@Transactional
public void updateBalanceOptimized(Long userId, BigDecimal amount) {
// 先执行非加锁操作
complexBusinessLogic();
// 最后加锁更新
Account account = accountDao.selectForUpdate(userId);
account.setBalance(account.getBalance().add(amount));
accountDao.update(account);
}
4. 使用乐观锁减少锁竞争
// 使用版本号实现乐观锁
@Entity
public class Account {
@Id
private Long id;
private BigDecimal balance;
@Version // JPA乐观锁注解
private Integer version;
}
@Repository
public class AccountRepository {
public boolean updateBalanceWithOptimisticLock(Long id, BigDecimal amount) {
int rows = jdbcTemplate.update(
"UPDATE account SET balance = balance + ?, version = version + 1 " +
"WHERE id = ? AND version = ?",
amount, id, currentVersion
);
return rows > 0;
}
}
7.3 索引设计优化
-
覆盖索引:避免回表,减少锁竞争
-
索引字段顺序:区分度高的字段在前
-
避免索引失效:防止行锁升级为表锁
-
使用索引下推:减少回表次数
-- 创建合适的索引
CREATE INDEX idx_account_user_balance ON account(user_id, balance);
-- 查询使用索引
SELECT * FROM account WHERE user_id = 100 AND balance > 0; -- 使用索引
SELECT * FROM account WHERE balance > 0; -- 可能全表扫描,升级为表锁
7.4 应用层补偿机制
@Component
public class OrderService {
@Autowired
private TransactionTemplate transactionTemplate;
/**
* 最终一致性:补偿事务
*/
public void createOrderWithCompensation(Order order) {
try {
// 主事务
transactionTemplate.execute(status -> {
orderDao.save(order);
return null;
});
// 异步执行依赖操作
asyncExecuteDependencies(order);
} catch (Exception e) {
// 补偿:回滚或重试
compensateOrder(order, e);
}
}
/**
* 异步执行,不阻塞主事务
*/
@Async
public void asyncExecuteDependencies(Order order) {
try {
inventoryService.deductStock(order);
messageService.sendOrderCreated(order);
} catch (Exception e) {
// 记录异常,定时任务重试
retryService.scheduleRetry(order);
}
}
}
7.5 监控与告警
# Spring Boot监控配置
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
# 自定义事务监控
@Aspect
@Component
@Slf4j
public class TransactionMonitorAspect {
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object monitorTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
// 记录慢事务
if (duration > 1000) { // 超过1秒的事务
log.warn("慢事务告警: method={}, duration={}ms",
joinPoint.getSignature(), duration);
Metrics.counter("slow_transaction_total").increment();
}
return result;
} catch (Exception e) {
// 事务失败统计
Metrics.counter("transaction_failure_total").increment();
throw e;
}
}
}
八、Java开发实战建议
8.1 Spring事务使用规范
@Configuration
public class TransactionConfig {
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager manager) {
TransactionTemplate template = new TransactionTemplate(manager);
// 设置事务属性
template.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
template.setTimeout(30); // 30秒超时
return template;
}
}
@Service
public class UserService {
@Transactional(
isolation = Isolation.READ_COMMITTED, // 明确指定隔离级别
timeout = 30, // 设置超时时间
rollbackFor = Exception.class // 明确回滚异常
)
public User createUser(User user) {
// 业务逻辑
return userRepository.save(user);
}
/**
* 只读事务优化
*/
@Transactional(readOnly = true)
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
}
8.2 连接池配置优化
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20 # 根据业务量调整
minimum-idle: 5
connection-timeout: 30000 # 连接超时30秒
idle-timeout: 600000 # 空闲连接超时10分钟
max-lifetime: 1800000 # 连接最大生命周期30分钟
connection-test-query: SELECT 1
8.3 数据库配置优化
# MySQL配置文件优化(my.cnf)
[mysqld]
# 事务相关
transaction_isolation = READ-COMMITTED # 默认隔离级别
innodb_lock_wait_timeout = 50 # 锁等待超时50秒
innodb_rollback_on_timeout = ON # 超时自动回滚
# InnoDB缓冲池
innodb_buffer_pool_size = 2G # 根据内存调整
innodb_buffer_pool_instances = 4 # 缓冲池实例数
# 日志配置
innodb_log_file_size = 512M # redo log大小
innodb_log_files_in_group = 3 # redo log组数
innodb_flush_log_at_trx_commit = 1 # 每次提交刷盘
sync_binlog = 1 # 每次提交同步binlog
九、总结
9.1 核心要点回顾
-
事务ACID:理解每个特性的实现机制
-
隔离级别:根据业务需求选择合适的级别
-
锁机制:理解各种锁的作用和使用场景
-
MVCC:掌握快照读和当前读的区别
-
死锁:知道如何预防、检测和解决
-
优化:遵循阿里巴巴的最佳实践
9.2 实战检查清单
✅ 事务是否尽可能小?
✅ 远程调用是否移出事务?
✅ 加锁操作是否靠后执行?
✅ 是否使用了合适的索引?
✅ 是否配置了事务超时时间?
✅ 是否监控了慢事务和死锁?
✅ 是否考虑了最终一致性方案?
9.3 性能优化路线图
-
识别问题:监控慢查询、锁等待、死锁
-
分析原因:使用EXPLAIN、SHOW ENGINE INNODB STATUS
-
优化SQL:添加索引、重写查询、减少锁范围
-
优化事务:拆分大事务、异步处理、补偿机制
-
架构优化:读写分离、分库分表、缓存