深入解析 MySQL 事务:从基础概念到高级应用
一、定义
事务是由一个或多个 SQL 语句组成的独立工作单元,在这个单元中,每个 SQL 语句都是相互依赖的。事务作为一个不可分割的整体存在,要么全部成功,要么全部失败,不可能存在部分成功、部分失败的情况。
简而言之,事务就是一个整体,内部的 SQL 语句要么全部执行成功,要么全部执行失败。一旦某条 SQL 语句执行失败或出错,整个事务将回滚(返回最初状态),所有受到影响的数据将恢复到事务开始前的状态;若所有 SQL 语句执行成功,则事务顺利提交。
在 MySQL 中,数据存储由不同的存储引擎管理,不同存储引擎有各自特点。常见存储引擎有 InnoDB、MyISAM 和 Memory 等。其中,InnoDB 支持事务(transaction),而 MyISAM 和 Memory 等不支持事务。
可以通过 SHOW ENGINES; 语句查看 MySQL 支持的存储引擎。
二、特性(ACID)
事务具有四个关键特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),简称 ACID。
- 原子性(Atomicity):事务是一个不可分割的最小工作单位,事务中的操作要么全部发生,要么全部不发生。
- 一致性(Consistency):事务必须使数据库从一个一致状态变换到另一个一致状态。例如,李二给王五转账 50 元,要么李二账户减 50 元、王五账户加 50 元,要么都不发生。
- 隔离性(Isolation):一个事务的执行不能被其他事务干扰,事务内部的操作及使用的数据对并发的其他事务是隔离的。
- 持久性(Durability):事务一旦提交成功,它对数据库的改变将是永久性的,后续的操作或故障不会影响其结果。
三、分类
事务分为隐式事务和显式事务。DML 语句(INSERT、UPDATE、DELETE)是隐式事务。
隐式事务
没有明显的开启和结束标记,自动提交事务。比如,UPDATE 语句修改数据,相当于一个隐式事务。
举例:张三同学购买一个 CSDN 定制保温杯花 99 元,这相当于一个 UPDATE 语句,减去张三余额中的 99 元:
sql
UPDATE accounts SET balance = balance - 99 WHERE name = '张三';
显式事务
具有明显的开启和结束标记。使用显式事务的前提是禁用自动提交功能(将 autocommit 设置为 0)。
查看当前 autocommit 状态:
sql
SHOW VARIABLES LIKE 'autocommit';
禁用自动提交:
sql
SET autocommit = 0;
四、开启事务
显式事务需要手动开启和提交或回滚。以下是一个使用显式事务的示例:
sql
-- 步骤一:开启事务
START TRANSACTION;
-- 步骤二:编写事务中的 SQL 语句(INSERT、UPDATE、DELETE)
-- 例如,实现"李二给王五转账"的事务过程
UPDATE t_account SET balance = balance - 50 WHERE vname = '李二';
UPDATE t_account SET balance = balance + 50 WHERE vname = '王五';
-- 步骤三:结束事务
COMMIT; -- 提交事务
-- ROLLBACK; -- 回滚事务:即事务不执行,恢复到事务执行前的状态
五、并发问题
多个事务并发运行时,如果没有适当的隔离机制,就会发生以下问题:
- 脏读(Dirty Read):事务 T1 读取了事务 T2 更新但未提交的数据,如果 T2 回滚,T1 读取的数据是无效的。
- 不可重复读(Non-repeatable Read):事务 T1 读取某字段后,事务 T2 更新了该字段,T1 再次读取时,值不同。
- 幻读(Phantom Read):事务 T1 读取某表的数据后,事务 T2 向该表插入新数据,T1 再次读取时,发现多了几行数据。
为了避免这些并发问题,MySQL 提供了四种事务隔离级别:
六、隔离级别
MySQL 提供四种事务隔离级别,不同隔离级别对应不同的并发控制程度和性能影响:
- 读未提交(Read Uncommitted):允许读取未提交的变更,可能出现脏读、不可重复读和幻读。适用于对数据一致性要求低的场景。
- 读已提交(Read Committed):只能读取已提交的变更,可以避免脏读,但仍可能出现不可重复读和幻读。适用于对数据一致性要求较高,但允许部分并发的场景。
- 可重复读(Repeatable Read):确保多次读取同一字段的值一致,可以避免脏读和不可重复读,但仍可能出现幻读。MySQL 的默认隔离级别。
- 串行化(Serializable):完全锁定事务执行期间的数据,避免所有并发问题,但性能较低。适用于对数据一致性要求极高,且允许性能降低的场景。
Oracle 支持两种事务隔离级别:Read Committed 和 Serializable。
Oracle 默认事务隔离级别是:Read Committed。
MySQL 默认事务隔离级别是:Repeatable Read。
事务隔离级别越高,数据一致性越好,但并发性能越差。开发者需要根据具体业务需求选择合适的隔离级别,以在性能和数据一致性之间取得平衡。
七、示例
假设我们有一个简单的银行账户转账系统,包含两个账户表(t_account)。我们希望在两个账户之间进行转账操作时,保证事务的一致性。
1. 创建数据库和表
sql
CREATE DATABASE bank;
USE bank;
CREATE TABLE t_account (
id INT AUTO_INCREMENT PRIMARY KEY,
vname VARCHAR(50) NOT NULL,
balance DECIMAL(10, 2) NOT NULL
);
INSERT INTO t_account (vname, balance) VALUES ('李二', 100.00), ('王五', 100.00);
2. 实现转账操作
sql
-- 开启事务
START TRANSACTION;
-- 从李二账户扣除 50 元
UPDATE t_account SET balance = balance - 50 WHERE vname = '李二';
-- 向王五账户增加 50 元
UPDATE t_account SET balance = balance + 50 WHERE vname = '王五';
-- 提交事务
COMMIT;
-- 检查结果
SELECT * FROM t_account;
在实际应用中,开发者可以通过编程语言(如 Java、Python)结合数据库驱动(如 JDBC、SQLAlchemy)来管理事务。例如,在 Java 中,可以使用 JDBC 的 Connection 对象的事务管理方法:
java
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement stmt1 = conn.prepareStatement("UPDATE t_account SET balance = balance - ? WHERE vname = ?")) {
stmt1.setBigDecimal(1, new BigDecimal("50.00"));
stmt1.setString(2, "李二");
stmt1.executeUpdate();
}
try (PreparedStatement stmt2 = conn.prepareStatement("UPDATE t_account SET balance = balance + ? WHERE vname = ?")) {
stmt2.setBigDecimal(1, new BigDecimal("50.00"));
stmt2.setString(2, "王五");
stmt2.executeUpdate();
}
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
通过这种方式,可以在应用程序层面确保事务的原子性、一致性、隔离性和持久性(ACID)。