事务是由一个或多个 SQL 语句组成的独立工作单元,在这个单元中,每个 SQL 语句都是相互依赖的。事务作为一个不可分割的整体存在,要么全部成功,要么全部失败,不可能存在部分成功、部分失败的情况。
简而言之,事务就是一个整体,内部的 SQL 语句要么全部执行成功,要么全部执行失败。一旦某条 SQL 语句执行失败或出错,整个事务将回滚(返回最初状态),所有受到影响的数据将恢复到事务开始前的状态;若所有 SQL 语句执行成功,则事务顺利提交。
在 MySQL 中,数据存储由不同的存储引擎管理,不同存储引擎有各自特点。常见存储引擎有 InnoDB、MyISAM 和 Memory 等(更多请参考:
MySQL 存储引擎详解)。其中,InnoDB 支持事务(transaction),而 MyISAM 和 Memory 等不支持事务。
可以通过 SHOW ENGINES; 语句查看 MySQL 支持的存储引擎。
事务具有四个关键特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),简称 ACID。
事务分为隐式事务和显式事务。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; -- 回滚事务:即事务不执行,恢复到事务执行前的状态
多个事务并发运行时,如果没有适当的隔离机制,就会发生以下问题:
为了避免这些并发问题,MySQL 提供了四种事务隔离级别:
MySQL 提供四种事务隔离级别,不同隔离级别对应不同的并发控制程度和性能影响:
Oracle 支持两种事务隔离级别:Read Committed 和 Serializable。
Oracle 默认事务隔离级别是:Read Committed。
MySQL 默认事务隔离级别是:Repeatable Read。
事务隔离级别越高,数据一致性越好,但并发性能越差。开发者需要根据具体业务需求选择合适的隔离级别,以在性能和数据一致性之间取得平衡。
在高并发环境中,多个事务可能会出现死锁。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会自动检测并回滚其中一个事务
死锁解决方法:
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(多版本并发控制)
在高负载环境中,读写分离和分片技术可以提升性能。
读写分离:
-- 主库配置
[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;
在涉及多个数据库或服务的情况下,分布式事务保证各个参与方的一致性。
XA事务:
-- 开始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';
基于时间戳的并发控制是一种无锁并发控制方法,适用于高并发环境。每个事务在开始时分配一个时间戳,操作按时间戳顺序进行。
-- 示例代码展示基于时间戳的操作
-- 事务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事务的基础概念和高级特性,开发者可以更有效地管理数据库的并发操作,提升应用的稳定性和性能。