MySQL中的事务基本使用和详解
事务是数据库管理系统(DBMS)的核心概念之一,用于确保数据操作的原子性、一致性、隔离性和持久性(ACID)。本文将系统讲解MySQL中事务的基本概念、使用方法和实现原理。
一、事务基础概念
1. 什么是事务?
事务是一组逻辑操作单元,要么全部执行成功,要么全部不执行。就像银行转账:
-- 转账示例(伪代码)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT;
2. 事务的ACID特性
| 特性 | 解释 |
|---|---|
| 原子性(A) | 事务是不可分割的工作单位,要么全成功,要么全失败 |
| 一致性(C) | 事务执行前后,数据库从一个一致状态变为另一个一致状态 |
| 隔离性(I) | 并发事务之间互不干扰 |
| 持久性(D) | 事务一旦提交,其结果就是永久性的 |
二、MySQL事务的基本操作
1. 事务控制语句
-- 1. 开启事务
START TRANSACTION; -- 或 BEGIN
-- 2. 执行SQL语句
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- 3. 提交事务(使更改永久生效)
COMMIT;
-- 4. 回滚事务(撤销所有更改)
ROLLBACK;
2. 自动提交模式
MySQL默认开启自动提交模式(autocommit=1),每条SQL语句都会自动作为一个事务执行。
-- 查看当前自动提交模式
SHOW VARIABLES LIKE 'autocommit';
-- 临时关闭自动提交(当前会话)
SET autocommit = 0;
-- 重新开启自动提交
SET autocommit = 1;
3. 保存点(Savepoint)
允许在事务中设置标记点,可以回滚到特定保存点而非整个事务。
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 设置保存点
SAVEPOINT sp1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- 发现问题,回滚到保存点
ROLLBACK TO SAVEPOINT sp1;
-- 继续执行其他操作
UPDATE accounts SET balance = balance + 50 WHERE account_id = 3;
COMMIT; -- 只有第一条更新和最后一条更新会被提交
三、事务隔离级别
1. 隔离级别概述
隔离级别决定了事务之间的可见性和并发程度,MySQL支持4种隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 描述 |
|---|---|---|---|---|
| READ UNCOMMITTED(读未提交) | 可能 | 可能 | 可能 | 最低级别,可能读取到其他事务未提交的数据 |
| READ COMMITTED(读已提交) | 不可能 | 可能 | 可能 | 只能读取到已提交的数据 |
| REPEATABLE READ(可重复读) | 不可能 | 不可能 | 可能 | MySQL默认级别,保证同一事务中多次读取结果一致 |
| SERIALIZABLE(可串行化) | 不可能 | 不可能 | 不可能 | 最高级别,完全串行执行,性能最低 |
2. 查看和设置隔离级别
-- 查看当前会话隔离级别
SELECT @@transaction_isolation; -- MySQL 8.0+
-- 或
SELECT @@tx_isolation; -- MySQL 5.7及以下
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;
-- 设置会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局隔离级别(需要SUPER权限)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
3. 隔离级别示例
场景:两个事务同时操作账户余额
-- 事务1
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 1; -- 假设余额1000
-- 此时事务2提交了更新,将余额改为900
SELECT balance FROM accounts WHERE account_id = 1; -- 不同隔离级别下结果不同
COMMIT;
- READ UNCOMMITTED:第二次查询可能看到900
- READ COMMITTED:第二次查询会看到900
- REPEATABLE READ:两次查询都看到1000
- SERIALIZABLE:事务2会被阻塞,直到事务1完成
四、事务相关问题
1. 脏读(Dirty Read)
一个事务读取到另一个事务未提交的数据。
-- 事务1
START TRANSACTION;
UPDATE accounts SET balance = 800 WHERE account_id = 1;
-- 不提交
-- 事务2
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 1; -- 可能读取到800(脏读)
ROLLBACK;
2. 不可重复读(Non-repeatable Read)
同一事务中多次读取同一数据,结果不一致(其他事务修改了数据)。
-- 事务1
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 1; -- 1000
-- 事务2提交了更新
SELECT balance FROM accounts WHERE account_id = 1; -- 900(不可重复读)
COMMIT;
3. 幻读(Phantom Read)
同一事务中多次查询,结果集数量不同(其他事务插入了新数据)。
-- 事务1
START TRANSACTION;
SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- 10条
-- 事务2插入了新订单
SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- 11条(幻读)
COMMIT;
4. 丢失更新(Lost Update)
两个事务同时读取并修改同一数据,后提交的事务覆盖了先提交的事务的修改。
-- 事务1
START TRANSACTION;
SELECT balance INTO @bal FROM accounts WHERE account_id = 1; -- 1000
SET @new_bal = @bal - 100;
UPDATE accounts SET balance = @new_bal WHERE account_id = 1;
-- 等待提交
-- 事务2
START TRANSACTION;
SELECT balance INTO @bal FROM accounts WHERE account_id = 1; -- 1000
SET @new_bal = @bal - 50;
UPDATE accounts SET balance = @new_bal WHERE account_id = 1;
COMMIT; -- 事务2先提交,余额变为950
-- 事务1提交
COMMIT; -- 事务1后提交,余额变为900(覆盖了事务2的修改)
解决方案:使用SELECT ... FOR UPDATE加锁
START TRANSACTION;
SELECT balance INTO @bal FROM accounts WHERE account_id = 1 FOR UPDATE;
SET @new_bal = @bal - 100;
UPDATE accounts SET balance = @new_bal WHERE account_id = 1;
COMMIT;
五、事务最佳实践
1. 事务设计原则
- 短事务:事务执行时间越短,锁竞争越小
- 明确边界:避免在事务中执行耗时操作(如网络请求)
- 合理使用隔离级别:根据业务需求选择最低必要的隔离级别
- 避免嵌套事务:MySQL不支持真正的嵌套事务
2. 典型事务场景
-- 订单创建事务示例
START TRANSACTION;
-- 1. 扣除库存
UPDATE products
SET stock = stock - 1
WHERE product_id = 1001 AND stock >= 1;
-- 检查是否影响行数(库存是否足够)
IF ROW_COUNT() = 0 THEN
ROLLBACK;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存不足';
END IF;
-- 2. 创建订单
INSERT INTO orders (user_id, product_id, quantity, amount, create_time)
VALUES (100, 1001, 1, 99.99, NOW());
-- 3. 记录订单明细
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (0, 1001, 1, 99.99);
COMMIT;
3. 分布式事务简介
对于跨多个数据库服务的事务,MySQL原生不支持分布式事务,但可以通过以下方式实现:
- XA事务:两阶段提交协议
- 消息队列+最终一致性:如RocketMQ、RabbitMQ
- Saga模式:将长事务拆分为多个本地事务,通过补偿事务回滚
六、事务相关命令
1. 查看当前事务
-- 查看当前运行的事务
SELECT * FROM information_schema.INNODB_TRX;
-- 查看锁等待情况
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
2. 杀死阻塞事务
-- 找到阻塞事务的trx_mysql_thread_id
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 杀死该线程(需要足够权限)
KILL [thread_id];
七、常见问题解答
1. 事务越大越好吗?
不是。大事务会导致:
- 锁持有时间过长,降低并发性
- 回滚成本高
- 增加死锁概率
建议:将大事务拆分为多个小事务。
2. 为什么SELECT不需要事务?
SELECT语句默认是自动提交的,但以下情况需要事务:
- 需要可重复读(REPEATABLE READ)一致性视图
- 需要加锁(FOR UPDATE/LOCK IN SHARE MODE)
3. 隐式提交的语句
以下操作会隐式提交当前事务:
- DDL语句(CREATE, ALTER, DROP等)
- 事务控制语句(START TRANSACTION, BEGIN等除外)
- 锁表语句(LOCK TABLES, UNLOCK TABLES)
- 加载数据的语句(LOAD DATA INFILE)
4. 如何避免死锁?
死锁预防策略:
- 按固定顺序访问表和行
- 保持事务简短
- 避免事务中用户交互
- 使用较低的隔离级别
- 为表添加适当的索引
通过掌握这些事务基础知识和实践技巧,你可以设计出更可靠、更高效的数据库应用。在实际开发中,建议结合业务场景选择合适的隔离级别,并通过监控工具(如SHOW ENGINE INNODB STATUS)诊断事务相关问题。