深入解析 MySQL 事务:从基础概念到高级应用
一、定义
事务是由一个或多个 SQL 语句组成的独立工作单元,在这个单元中,每个 SQL 语句都是相互依赖的。事务作为一个不可分割的整体存在,要么全部成功,要么全部失败,不可能存在部分成功、部分失败的情况。
简而言之,事务就是一个整体,内部的 SQL 语句要么全部执行成功,要么全部执行失败。一旦某条 SQL 语句执行失败或出错,整个事务将回滚(返回最初状态),所有受到影响的数据将恢复到事务开始前的状态;若所有 SQL 语句执行成功,则事务顺利提交。
在 MySQL 中,数据存储由不同的存储引擎管理,不同存储引擎有各自特点。常见存储引擎有 InnoDB、MyISAM 和 Memory 等(更多请参考:
MySQL 存储引擎详解)。其中,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 元:
UPDATE accounts SET balance = balance - 99 WHERE name = '张三';
显式事务
具有明显的开启和结束标记。使用显式事务的前提是禁用自动提交功能(将 autocommit 设置为 0)。
查看当前 autocommit 状态:
SHOW VARIABLES LIKE 'autocommit';
禁用自动提交:
SET autocommit = 0;
四、开启事务
显式事务需要手动开启和提交或回滚。以下是一个使用显式事务的示例:
-- 步骤一:开启事务
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。
事务隔离级别越高,数据一致性越好,但并发性能越差。开发者需要根据具体业务需求选择合适的隔离级别,以在性能和数据一致性之间取得平衡。
七、事务的高级应用
1. 死锁检测与解决
在高并发环境中,多个事务可能会出现死锁。MySQL通过检测死锁来自动解决,通常是回滚其中一个事务。
死锁示例:
-- 事务1
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 事务1在此时暂停等待事务2
-- 事务2
START TRANSACTION;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
-- 事务2在此时暂停等待事务1
-- 事务1
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 事务1等待事务2完成
-- 事务2
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
-- 事务2等待事务1完成
-- 这时发生死锁,MySQL会自动检测并回滚其中一个事务
死锁解决方法:
- 尽量减少锁定时间:在事务中尽量减少查询和更新的时间,避免长时间锁定资源。
- 合理的锁定顺序:在多个事务中按相同的顺序访问资源,减少死锁概率。
2. MVCC(多版本并发控制)
MySQL的InnoDB存储引擎通过MVCC实现高并发控制,避免了大多数锁竞争问题。
MVCC示例:
-- 创建示例表,并添加注释
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID,自动递增,主键',
name VARCHAR(50) COMMENT '用户名称',
balance DECIMAL(10, 2) COMMENT '用户余额,保留两位小数'
) COMMENT='用户信息表';
-- 插入一些数据
INSERT INTO users (name, balance) VALUES ('Alice', 100.00), ('Bob', 150.00), ('Charlie', 200.00);
-- 事务1:读取数据
START TRANSACTION;
SELECT * FROM users WHERE id = 1;
-- 事务1读取users表的所有数据。此时会生成一个一致性视图,事务将看到事务开始时的数据库状态。
-- 假设事务1在此时暂停,继续执行其他操作
-- 事务2:更新数据
START TRANSACTION;
UPDATE users SET balance = balance - 50 WHERE name = 'Alice';
-- 事务2更新Alice的余额,创建了一个新版本的行,并更新了trx_id和roll_pointer。
COMMIT;
-- 事务1:再次读取数据
SELECT * FROM users WHERE id = 1;
-- 事务1再次读取users表的数据。由于事务1的读取视图是在事务2提交之前创建的,
-- 事务1仍然看到Alice的余额为100。
COMMIT;
学习更多,请参考:
MySQL中的MVCC(多版本并发控制)
3. 读写分离和分片
在高负载环境中,读写分离和分片技术可以提升性能。
读写分离:
- 主从复制:通过设置MySQL主从复制,将写操作分配到主库,读操作分配到从库,以分担负载。
-- 主库配置
[mysqld]
server-id = 1
log-bin = mysql-bin
-- 从库配置
[mysqld]
server-id = 2
relay-log = mysql-relay-bin
分片:
- 水平分片:将数据表按行拆分成多个子表,以减小单表的数据量和访问压力。
-- 创建分片表
CREATE TABLE users_shard_1 LIKE users;
CREATE TABLE users_shard_2 LIKE users;
-- 根据某种规则插入数据
INSERT INTO users_shard_1 SELECT * FROM users WHERE id <= 1000;
INSERT INTO users_shard_2 SELECT * FROM users WHERE id > 1000;
4. 分布式事务
在涉及多个数据库或服务的情况下,分布式事务保证各个参与方的一致性。
XA事务:
- MySQL支持XA(eXtended Architecture)事务协议,实现跨多个资源的全局事务。
-- 开始XA事务
XA START 'xid';
-- 在多个资源上执行操作
INSERT INTO accounts (id, balance) VALUES (3, 100);
-- 准备XA事务
XA END 'xid';
XA PREPARE 'xid';
-- 提交XA事务
XA COMMIT 'xid';
-- 或回滚XA事务
XA ROLLBACK 'xid';
5. 基于时间戳的并发控制
基于时间戳的并发控制是一种无锁并发控制方法,适用于高并发环境。每个事务在开始时分配一个时间戳,操作按时间戳顺序进行。
-- 示例代码展示基于时间戳的操作
-- 事务1
START TRANSACTION;
-- 获取时间戳
SELECT CURRENT_TIMESTAMP;
-- 进行操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 提交事务
COMMIT;
-- 事务2
START TRANSACTION;
-- 获取时间戳
SELECT CURRENT_TIMESTAMP;
-- 进行操作
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 提交事务
COMMIT;
八、事务的优化建议
- 使用适当的隔离级别:
- 根据应用需求选择适当的事务隔离级别,以平衡性能和数据一致性。
- 尽量缩短事务的生命周期:
- 避免长时间运行的事务,减少锁定的时间,提高并发性能。
- 合理使用索引:
- 为常用查询创建合适的索引,减少锁范围,提高查询效率。
- 监控和调优:
- 使用MySQL的性能监控工具,定期检查和优化事务的执行情况。
通过深入理解和应用MySQL事务的基础概念和高级特性,开发者可以更有效地管理数据库的并发操作,提升应用的稳定性和性能。