标题:[MySQL初阶]MySQL(9)事物机制
@水墨不写bug
一、认识事务
1、多线程访问数据库出现的问题
MySQL是一款网络服务,那么必定有多个客户端同时访问服务器的场景出现:假如有一个抢票系统,有两个客户端抢票,一个客户端抢票之后,数据库还没有及时更新,这张票又被另一个客户端抢了一次,一张票被卖了两次。这不符合逻辑!
于是,必须要对数据库的CURD操作进行一定的限制,才能解决上面的问题。
2、对CURD的限制是通过事务机制实现的
事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。
事务主要用于处理操作量大,复杂度高的数据。比如要删除一个被开除的员工的所有历史信息以及和他相关的信息,就需要多条 MySQL 语句构成,那么所有这些操作合起来,就构成了一个事务。
但是要知道,同一时刻并不是只有一个事务在运行,而是有多个事务同时在运行。如果大量的事务在不加保护的情况下访问同一个表数据,就一定会出现问题。因为事务由多条sql语句组成,一个事务执行了一半,突然mysql客户端挂了,那么这执行了一半的事务该怎么处理?
3、事务的四个属性
于是,MySQL规定,一个完整的事务,不仅仅是简单的sql语句的组合
,还要满足下面的四个属性
:
原子性(Atomicity,或称不可分割性): 一个事务(transaction)中的所有操作,要么全部完成
,要么全部不完成
,不会结束在中间某个环节。事务在执行过程中发生错误(导致客户端挂了),执行了一半的事务会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性(Consistency): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。
隔离性(Isolation,又称独立性): 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read uncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化( Serializable )。
持久性(Durability): 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
此外,事务本质上是为应用层服务的。因为我们并不是直接访问数据库的,而是通过上层的各种语言来范围访问数据库的,于是多条sql语句封装出来的事务可以简化上层的编程模型和逻辑。
4、哪些引擎支持事务
使用指令查询:
mysql> show engines\G;
*************************** 1. row ***************************
Engine: ndbcluster
Support: NO
Comment: Clustered, fault-tolerant tables
Transactions: NULL
XA: NULL
Savepoints: NULL
*************************** 2. row ***************************
Engine: MEMORY
Support: YES
Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 3. row ***************************
Engine: InnoDB
Support: DEFAULT
Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES
XA: YES
Savepoints: YES
*************************** 4. row ***************************
Engine: PERFORMANCE_SCHEMA
Support: YES
Comment: Performance Schema
Transactions: NO
XA: NO
Savepoints: NO
*************************** 5. row ***************************
Engine: MyISAM
Support: YES
Comment: MyISAM storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 6. row ***************************
Engine: FEDERATED
Support: NO
Comment: Federated MySQL storage engine
Transactions: NULL
XA: NULL
Savepoints: NULL
*************************** 7. row ***************************
Engine: ndbinfo
Support: NO
Comment: MySQL Cluster system information storage engine
Transactions: NULL
XA: NULL
Savepoints: NULL
*************************** 8. row ***************************
Engine: MRG_MYISAM
Support: YES
Comment: Collection of identical MyISAM tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 9. row ***************************
Engine: BLACKHOLE
Support: YES
Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
XA: NO
Savepoints: NO
*************************** 10. row ***************************
Engine: CSV
Support: YES
Comment: CSV storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 11. row ***************************
Engine: ARCHIVE
Support: YES
Comment: Archive storage engine
Transactions: NO
XA: NO
Savepoints: NO
11 rows in set (0.01 sec)
从查询结果可知,目前只有InnodeDB引擎支持事务机制
。
二、事务的提交与autocommit设置
事务的提交(commit)
方式有两种:
自动提交;
手动提交;
autocommit变量是设置是否自动提交。
查看autocommit变量:
show variables like 'autocommit';
设置是否自动提交:
SET AUTOCOMMIT=1; #SET AUTOCOMMIT=1 开启自动提交
SET AUTOCOMMIT=0; #SET AUTOCOMMIT=0 禁止自动提交
当我们想要开始一个事务,需要指令:
begin;
#或者
start transaction;
当我们想要提交一个事务,需要指令:
commit;
而这两个指令之间的sql语句就是事务的内容。
为了方便后面的演示,这里我们创建一个表结构如下:
create table if not exists account(
id int primary key,
name varchar(50) not null default '',
blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;
此时暂时把全局隔离级别
设置为读未提交
【为了方便通过另一个客户端查看表中的数据】:
set global transaction isolation level READ UNCOMMITTED;
查看隔离级别:
SELECT @@global.transaction_isolation AS '全局隔离级别', @@session.transaction_isolation AS '当前会话隔离级别';
重启mysql,同步当前会话和全局的隔离级别,然后查看:
- 特性一:如果事务通过begin/start transation方式开始,则必须通过commit提交,这样才会持久化,与是否设置autocommit无关(默认情况下autocommit是ON打开的,至于具体有什么用,在后文会讲)。
如果有两个客户端同时访问同一张表,同时启动了两个事务,一个客户端事务执行了一半,崩溃了,MySQL会自动回滚
。
什么是回滚?
当我们开始一个事务之后,在事务执行的过程中,可以创建保存点,这个保存点可以理解为游戏中的存档点,如果对于最新的操作不满意,可以回滚(读取存档点):
savepoint s1; #创建一个保存点s1
首先,通过begin;开启一个事务。
然后,我们在已经创建的表中插入数据:
此时设置一个savepoint s1;
然后再次进行一些增删改操作(比如新插入一条数据):
但是发现在新进行的操作出错了,不想要了,于是可以回滚(读取存档点s1)
:
rollback to s1;
结果:
于是,新插入的一条“haha”的数据就被删除了。这就是回滚
。
回到特性一的演示:
证明:未commit,客户端崩溃,MySQL自动会回滚[因为通过手动开始,autocommit不会自动提交]:
启动两个事务,左侧插入一个新数据lisi,右侧可以看到新插入的数据:
左侧事务的客户端被ctrl+\ 被发送abort信号,进程被杀死,右侧客户端发现新插入的lisi被MySQL自动回滚了。
证明:commit了,客户端崩溃,MySQL数据不会再受影响,已经持久化。[因为通过手动开始,autocommit不会自动提交]
启动两个事务,左侧客户端插入数据jimmay,commit之后被信号杀死,右侧查询可以找到新插入的数据:
证明:没有手动通过begin;/start transaction;启动一个事务,autocommit会对提交产生影响。
在autocommit=1;(开启)的情况下:
对于没有手动通过begin;/start transaction;启动一个事务,一条sql语句就是一个事务,因为每一条语句都会被包装为一个事务。
即使一条语句被执行后,客户端直接崩溃,语句仍然被持久化:
在autocommit=0;(开启)的情况下:
需要手动commit,无论执行了多少操作,如果在客户端退出之前没有commit,则所有的操作都会被回滚:
如果commit了,做的操作就会被持久化:
到这里,我们可以总结一下:
- 事务开始是用
begin/start transaction
,需要手动commit
提交,才会持久化,不收到autocommit设置的影响。 - 事务可以通过设置
savepoint
手动回滚;当操作异常时,MySQL会自动回滚。 - InnoDB 每一条 SQL 语言都
默认封装成事务
,自动提交
。(select有特殊情况,因为MySQL 有 MVCC )。 - 从上面的演示,我们能看到事务本身的原子性(回滚),持久性(commit)。
从哪里体现隔离性?
三、事务的隔离性和隔离级别
数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性。
数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别。
(1) 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
-- 输出示例:transaction_isolation = REPEATABLE-READ
(2) 设置隔离级别
-- 全局设置(重启后生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL+隔离登记;
-- 当前会话设置
SET SESSION TRANSACTION ISOLATION LEVEL+隔离登记;
MySQL InnoDB 存储引擎
支持四种标准的事务隔离级别,它们通过不同的锁机制
和 多版本并发控制(MVCC)
实现事务之间的数据可见性和并发控制。
(3) 事务隔离级别的作用
事务隔离级别定义了多个并发事务之间的数据可见性规则,核心目标是解决以下问题:
- 脏读(Dirty Read):读到其他事务未提交的数据。
- 不可重复读(Non-Repeatable Read):同一事务中多次读取同一数据,结果不一致(其他事务修改了该行)。
- 幻读(Phantom Read):在同一事务中,由于其他事务插入或删除了符合查询条件的行,导致多次相同查询的结果集行数不一致。
(4)MySQL InnoDB 的四种隔离级别
按隔离级别从低到高排序:
(1) 读未提交(Read Uncommitted)
- 特点:所有的事务都可以看到其他事务没有提交的
执行结果(可能读到“脏数据”)相当于没有任何隔离性。 - 问题:存在 脏读、不可重复读、幻读。
- 实现方式:几乎不加锁,依赖最低限度的锁机制。
- 使用场景:极少使用,仅在需要最高并发且不关心数据一致性的场景。
(2) 读已提交(Read Committed)
- 特点:一个事务只能看到其他的已经提交的事务所做的改变。
- 解决的问题:避免脏读。
- 遗留问题:存在 不可重复读、幻读。
- 实现方式:
- 锁机制:使用 行级锁(Record Locks),写操作锁定当前行。
- MVCC:每次
SELECT
生成一个独立的快照(一致性视图),基于当前已提交的数据。
- 使用场景:适用于大多数 OLTP 系统(如 Oracle 默认级别)。
(3) 可重复读(Repeatable Read)
- 特点: MySQL 默认的隔离级别,同一事务中多次读取同一数据的结果一致。
- 解决的问题:避免脏读、不可重复读。
- 遗留问题:仍可能发生 幻读(但 InnoDB 通过 间隙锁 基本消除)。
- 实现方式:
- 锁机制:使用 记录锁(Record Locks) + 间隙锁(Gap Locks)(合称 临键锁 Next-Key Locks)。
- MVCC:事务首次
SELECT
生成一致性视图,后续读取沿用该视图。
- 默认级别:InnoDB 的默认隔离级别。
- 幻读的解决:
-- 事务A BEGIN; SELECT * FROM users WHERE age > 20; -- 假设返回3条记录 -- 事务B INSERT INTO users (id, age) VALUES (4, 25); -- 提交 -- 事务A再次查询 SELECT * FROM users WHERE age > 20; -- 在可重复读下,仍然返回3条记录(避免幻读)
(4) 串行化(Serializable)
- 特点:事务的最高隔离级别,所有事务串行执行,完全隔离。
- 解决的问题:避免脏读、不可重复读、幻读。
- 实现方式:
- 锁机制:所有
SELECT
自动转换为SELECT ... FOR SHARE
,使用共享锁,导致读写冲突时阻塞。 - MVCC:基本失效,强制串行化。
- 锁机制:所有
- 使用场景:对数据一致性要求极高,但性能极低,很少使用。
(5)各隔离级别对比表
隔离级别 | 脏读 | 不可重复读 | 幻读 | 锁机制 | 性能 |
---|---|---|---|---|---|
读未提交 | 可能 | 可能 | 可能 | 行级写锁 | 最高 |
读已提交 | 无 | 可能 | 可能 | 行级写锁 + MVCC | 高 |
可重复读(InnodeDB默认) | 无 | 无 | 无* | 临键锁 + MVCC | 中等 |
串行化 | 无 | 无 | 无 | 表级锁或严格的行级锁 | 最低 |
*InnoDB 在可重复读级别下通过间隙锁基本消除幻读。
四、事务的一致性
事务的 一致性(Consistency) 需要 原子性(Atomicity)、隔离性(Isolation)、持久性(Durability) 以及 应用层的正确逻辑(用户的配合) 共同维护。
1. AID 特性与一致性的关系
事务的 ACID
特性中,一致性(Consistency)是最终目标,而其他三个特性(原子性、隔离性、持久性)是实现一致性的技术手段。同时,应用层逻辑(用户代码)的合理设计是确保一致性的必要条件。
(1) 原子性(Atomicity)
- 作用:事务内的操作要么全部成功,要么全部失败回滚。
- 对一致性的贡献:防止部分操作失败导致数据处于
“半完成”
状态。-- 示例:转账操作(原子性保证) BEGIN; UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; -- 扣款 UPDATE accounts SET balance = balance + 100 WHERE user_id = 2; -- 入账 COMMIT; -- 若任何一步失败,整个事务回滚,避免数据不一致。
(2) 隔离性(Isolation)
- 作用:控制并发事务之间的相互影响。
- 对一致性的贡献:防止
脏读、不可重复读、幻读
等问题破坏数据逻辑。-- 示例:隔离级别避免中间状态暴露 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; SELECT balance FROM accounts WHERE user_id = 1; -- 基于快照的一致性视图 -- 其他事务的修改不会影响当前事务的查询结果 COMMIT;
(3) 持久性(Durability)
- 作用:事务提交后,修改必须永久保存到存储介质(如磁盘)。
- 对一致性的贡献:确保提交后的数据在故障后仍可恢复,避免数据丢失导致不一致。
-- 提交后数据写入磁盘(通过 Redo Log 等机制保证) COMMIT;
(4) 应用层逻辑(用户的配合)
- 作用:开发者需确保业务规则被正确编码。
- 对一致性的贡献:数据库无法自动理解业务规则,需通过代码显式维护。
-- 示例:应用层必须检查转账的用户减去金额,转到的用户加上相应的金额。 BEGIN; UPDATE accounts SET balance = balance - amount WHERE user_id = 1; UPDATE accounts SET balance = balance + amount WHERE user_id = 2; COMMIT;
完~
转载请注明出处