📚 博主的专栏
🐧 Linux | 🖥️ C++ | 📊 数据结构 | 💡C++ 算法 | 🅒 C 语言 | 🌐 计算机网络 |🗃️ mysql
文章摘要:数据库事务通过ACID特性保障数据一致性:原子性确保操作全完成或全回滚;一致性维护数据完整性;隔离性管理并发事务干扰;持久性保证提交数据永久有效。MySQL的InnoDB引擎支持四种隔离级别,默认可重复读通过Next-Key锁解决幻读问题。事务需明确提交或回滚,异常时未提交操作自动回滚。合理设置隔离级别可平衡数据安全与并发性能。
目录
CURD不加控制,会有什么问题?
CURD满足什么属性,能解决上述问题?
1. 买票的过程得是原子的吧
2. 买票互相应该不能影响吧
3. 买完票应该要永久有效吧
4. 买前,和买后都要是确定的状态吧
什么是事务?
事务是:一组DML语句组成,并且在逻辑上存在相关性,这一组DML语句要么全成功,要么全失败,整体看待。
MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
事务是指一组需要执行的操作,主要用于处理数据量大、复杂度高的任务。以毕业场景为例:当学校教务系统需要从后台 MySQL 数据库中删除你的所有信息时(虽然这种情况通常不会发生),不仅需要删除你的基本信息(如姓名、电话、籍贯等),还要删除与你相关的其他数据,包括各科成绩、在校表现记录,甚至你在论坛发表的文章等。这些操作需要多条 MySQL 语句共同完成,而这一系列操作的整体就构成了一个事务。
简称为 ACID
原子性(Atomicity):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作
隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read uncommitted )、读提交( Read committed )、可重复读( repeatable read )和串行化( Serializable )持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
为什么会出现事务
事务是被 MySQL 编写者设计出来的,本质是为了当应用程序访问数据库的时候,事务(本质一定是mysql内部的对象)能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。
可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?
因此事务本质上是为了应用层服务的。而不是伴随着数据库系统天生就有的。
备注:我们后面把 MySQL中有数据库,数据库中有表,表中有一行信息,称为一行记录。往后就是对于这一行记录的操作
事务的版本支持
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持
执行
SHOW ENGINES
命令可查看数据库支持的存储引擎类型及其状态信息。通过SET
语句可配置事务提交模式,支持自动提交(autocommit=1
)和手动提交(autocommit=0
)两种方式。mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.07 sec) mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | OFF | +---------------+-------+ 1 row in set (0.00 sec) mysql> set autocommit=1; Query OK, 0 rows affected (0.00 sec) mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.00 sec)
mysql的底层也是采用tcp协议进行相关的客户端服务端通信
事务常见操作方式
准备工作:
mysql一般不要暴露在公网,mysql服务端,被多个客户端访问。
将mysql的默认隔离级别设置成读未提交。
set global transaction isolation level READ UNCOMMITTED;
需要重启终端,进行查看--REPEATABLE-READ可重复度
select @@tx_isolation; +------------------+ | @@tx_isolation | +------------------+ | READ-UNCOMMITTED |--->读未提交 +------------------+ 1 row in set, 1 warning (0.00 sec)
版本差异说明:
MySQL < 8.0: 使用
tx_isolation
和tx_read_only
。MySQL ≥ 8.0: 改用
transaction_isolation
和transaction_read_only
,旧变量名已被移除。
根据简单银行用户表演示:
创建测试表
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;
打开另一个终端,访问test_db数据库
正常演示 - 证明事务的开始与回滚
show variables like 'autocommit'; -- 查看事务是否自动提交。我们故意设置成 -- 自动提交,看看该选项是否影响begin +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.00 sec)
回滚到s1
异常退出处理:
我在这里断网了,直接退出了mysql,想要删除掉表account却出现错误:
mysql> delete from account; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
- 存在另一个未提交的事务(如未提交的
UPDATE
、SELECT ... FOR UPDATE
或DELETE
操作)占用了表锁。InnoDB 默认的锁等待超时时间为
50秒
(由参数innodb_lock_wait_timeout
控制),超时后抛出此错误。解决办法:
(1) 检查当前运行的事务
-- 查看所有正在运行的事务(关注 trx_state 为 "RUNNING" 或 "LOCK WAIT" 的记录) SELECT * FROM information_schema.INNODB_TRX\G
输出示例:
*************************** 1. row *************************** trx_id: 123456789 trx_state: LOCK WAIT <-- 阻塞的事务 trx_started: 2023-10-01 10:00:00 trx_requested_lock_id: 12345:678:3:2 trx_wait_started: 2023-10-01 10:00:05 trx_weight: 2 trx_mysql_thread_id: 5 <-- 事务对应的线程ID
(2) 终止阻塞的事务
-- 根据上一步查到的 trx_mysql_thread_id,终止对应事务 KILL 5; -- 替换为实际的线程ID
(3) 重新执行删除操作
-- 确保当前会话自动提交已开启(避免再次阻塞) SET autocommit = 1; DELETE FROM account;
开启事务后,若未设置保存点,执行回滚操作rollback将直接撤销至事务起始状态。
证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化
事务提交(commit)后,即使执行回滚(rollback)操作,数据也不会被撤销,而是永久保存在数据库中。
非正常退出演示
证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
此时,另外一个没有退出的客户端去查:未提交的赵六的信息没有了,说明在没有提交并异常退出的时候事务自动回滚(前面所看到的autocommit=1)
在次客户端进行插入操作,再关闭掉这个终端:
再在另一个终端进行查询:自动回滚
注意:autocommit=0的时候异常退出,会回滚吗?与自动提交是相同的。自动提交不影响begin、commit,只要是手动begin的事务,就需要手动commit,和是否autocommit无关。
我们再用delete语句试试:
1.set autocommit=0,启动事务删除一行记录后,commit提交,再删除一行记录,使用Ctrl + '\',关闭终端,提交后的delete被撤销
2.set autocommit=1,未启动事务,直接删除一行记录,再关闭终端,或者异常退出Ctrl+'\'。
在未关闭的这个开启事务的终端进行查看:事务没有回滚
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 2 | 李四 | 4321.60 |
| 3 | 王五 | 3321.60 |
+----+--------+---------+
2 rows in set (0.00 sec)
mysql>
提交:同样的没有回滚
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 2 | 李四 | 4321.60 |
| 3 | 王五 | 3321.60 |
+----+--------+---------+
2 rows in set (0.00 sec)
结论:
autocommit只会影响我们未开启事务、提交事务时的操作,我们写的每一个操作在mysql看来就是事务,当autocommit开启时,我们所写的操作都会被自动提交(是否异常退出),就不会回滚,若autocommit关闭,在异常退出时,事务就会回滚。
- 只要输入begin或者start transaction,事务便必须要通过commit 提交,才会持久化,与是否设置set autocommit无关。
- 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚
- 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select有特殊情况,因为MySQL 有 MVCC )
- 从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)
- 那么隔离性?一致性?
事务操作注意事项
- 如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用 rollback(前提是事务还没有提交)
- 如果一个事务被提交了(commit),则不可以回退(rollback)
- 可以选择回退到哪个保存点
- InnoDB 支持事务, MyISAM 不支持事务
- 开始事务可以使 start transaction 或者 begin
事务隔离级别
如何理解隔离性1
- MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行
- 一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。
- 但,毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。
- 就如同你妈妈给你说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你妈妈不关心。那么你的学习,对你妈妈来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此时,就需要将你的学习隔离开,保证你的学习环境是健康的。
- 数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性
- 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别
隔离级别
读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。
读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。两个事务,一个事物进行各种操作知道commit,另一个事务都看不到,直到另一个事务也运行结束了,才能看到。(两个潜水的小孩),mysql默认的隔离级别。
串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁。但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)
隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等。
注意:在事务场景中,隔离是必要的,运行中的事务进行互相隔离。在事务运行中,“不会”出现相互干扰,这就是隔离性。根据影响的程度不同,隔离级别。
查看与设置隔离性
注意:
MySQL 5.7 及之前版本使用
tx_isolation
变量名,而 MySQL 8.0 使用transaction_isolation
。mysql> select @@global.transaction_isolation; +--------------------------------+ | @@global.transaction_isolation | +--------------------------------+ | READ-UNCOMMITTED | +--------------------------------+ 1 row in set (0.00 sec) mysql> select @@session.transaction_isolation; +---------------------------------+ | @@session.transaction_isolation | +---------------------------------+ | READ-UNCOMMITTED | +---------------------------------+ 1 row in set (0.00 sec) mysql> select @@transaction_isolation; +-------------------------+ | @@transaction_isolation | +-------------------------+ | READ-UNCOMMITTED | +-------------------------+ 1 row in set (0.00 sec) --如果需要修改隔离级别,可使用以下命令: sql -- 修改全局隔离级别 SET GLOBAL transaction_isolation = 'READ-COMMITTED'; -- 修改当前会话隔离级别 SET SESSION transaction_isolation = 'REPEATABLE-READ';
全局隔离级别需要
SUPER
权限才能修改,会话级别可以在当前连接中临时修改。
mysql5.7即以下:查看
-- 查看 mysql> SELECT @@global.tx_isolation; --查看全局隔级别 +-----------------------+ | @@global.tx_isolation | +-----------------------+ | REPEATABLE-READ | +-----------------------+ 1 row in set, 1 warning (0.00 sec) mysql> SELECT @@session.tx_isolation; --查看会话(当前)全局隔级别 +------------------------+ | @@session.tx_isolation | +------------------------+ | REPEATABLE-READ | +------------------------+ 1 row in set, 1 warning (0.00 sec) mysql> SELECT @@tx_isolation; --默认同上 +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set, 1 warning (0.00 sec) --设置 -- 设置当前会话 or 全局隔离级别语法 SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE} --设置当前会话隔离性,另起一个会话,看不多,只影响当前会话 mysql> set session transaction isolation level serializable; -- 串行化 Query OK, 0 rows affected (0.00 sec) mysql> SELECT @@global.tx_isolation; --全局隔离性还是RR +-----------------------+ | @@global.tx_isolation | +-----------------------+ | REPEATABLE-READ | +-----------------------+ 1 row in set, 1 warning (0.00 sec) mysql> SELECT @@session.tx_isolation; --会话隔离性成为串行化 +------------------------+ | @@session.tx_isolation | +------------------------+ | SERIALIZABLE | +------------------------+ 1 row in set, 1 warning (0.00 sec) mysql> SELECT @@tx_isolation; --同上 +----------------+ | @@tx_isolation | +----------------+ | SERIALIZABLE | +----------------+ 1 row in set, 1 warning (0.00 sec)
--设置全局隔离性,会话的隔离级别不受影响,另起一个会话的全局隔离级别,会被影响,会话的隔离级别不受影响。
--设置全局隔离性,另起一个会话,会被影响 mysql> set global transaction isolation level READ UNCOMMITTED; Query OK, 0 rows affected (0.00 sec) mysql> SELECT @@global.tx_isolation; +-----------------------+ | @@global.tx_isolation | +-----------------------+ | READ-UNCOMMITTED | +-----------------------+ 1 row in set, 1 warning (0.00 sec) mysql> SELECT @@session.tx_isolation; +------------------------+ | @@session.tx_isolation | +------------------------+ | READ-UNCOMMITTED | +------------------------+ 1 row in set, 1 warning (0.00 sec) mysql> SELECT @@tx_isolation; +------------------+ | @@tx_isolation | +------------------+ | READ-UNCOMMITTED | +------------------+ 1 row in set, 1 warning (0.00 sec) -- 注意,如果没有现象,关闭mysql客户端,重新连接
--设置会话隔离性,全局隔离性不会被影响,另一个会话不会受影响
我的mysql是8.0+,因此演示以8.0为主
读未提交【Read Uncommitted】
--几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用--终端A
-- 设置隔离级别为 读未提交
mysql> set global transaction isolation level read uncommitted; Query OK, 0 rows affected (0.00 sec) --重启客户端 mysql> select @@session.transaction_isolation; +------------------+ | @@tx_isolation | +------------------+ | READ-UNCOMMITTED | +------------------+ 1 row in set, 1 warning (0.00 sec)
打开另一个终端,使用另一个终端中的mysql充当客户端:
手动启动begin两个终端的事务,并发运行,读到了他人未提交的数据。
--一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读(dirty read)
读提交【Read Committed】
set global transaction isolation level read committed;
设置完全局隔离级别之后需要重启客户端查看更新好的隔离级别:
--终端A commit之后,看到了! --but,此时还在当前事务中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)!! (不可重复读,是问题吗??)
这种情况下会出现重复查询的问题。例如,当一个客户端在查询某个区间内的特定人员时,另一个客户端可能会将被查询对象修改到查询者下一个要查询的区间,并在查询操作完成前提交修改。这样就会导致同一个人被查询到两次,分别出现在不同的区间中。例如按照工资发放奖品的这种情况
事务运行要保证他的原子性,对于小王来讲。
可重复读【Repeatable Read】
set global transaction isolation level repeatable read; --设置全局隔离级别RR
与读提交不同的是,修改完之后提交事务,在另外一个客户端没有结束事务的时候,所读到内容依旧是修改前的内容。
--多次查看,发现终端A在对应事务中insert的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-Key锁(GAP+行锁)解决的。这块比较难。
幻读的基本概念
在数据库事务隔离级别中,幻读是指在**可重复读(Repeatable Read, RR)**隔离级别下出现的一种特殊现象。具体表现为:事务A在同一个事务周期内多次执行相同的查询操作,却看到了其他并发事务B新插入的行数据,导致前后查询结果不一致的情况。
幻读的产生原因分析
幻读现象的产生主要源于数据库隔离性实现的机制特点:
传统锁机制的限制:数据库通常通过锁机制来实现隔离性,但对于已存在的数据行容易加锁,而对尚未插入的数据则无法预先加锁
INSERT操作的特殊性:由于待插入的数据在插入前并不存在,传统的行锁无法对这种"未来可能存在"的数据进行锁定
查询范围的动态性:范围查询(如WHERE条件)的结果集会因为新数据的插入而发生变化,即使已存在的行被锁定
MySQL的幻读解决方案
MySQL在RR隔离级别下通过Next-Key锁机制有效解决了幻读问题:
Next-Key锁的组成:
- 行锁(Record Lock):锁定已存在的记录
- 间隙锁(Gap Lock):锁定记录之间的间隙
工作机制:
- 当执行范围查询时,MySQL不仅会锁定查询到的行记录
- 还会锁定这些行记录之间的所有间隙
- 这样可以阻止其他事务在这些间隙中插入新数据
实际效果:
- 防止了其他事务在查询范围内插入新行
- 确保了同一事务内多次执行相同查询会得到一致的结果集
- 真正实现了可重复读的语义
串行化【serializable】
--对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用。一个事务跑完了,另一个事务才能跑,我在执行时,没有其他事务在跑。
set global transaction isolation level serializable;
右边的事务先开启,左边事务被阻塞。右边事务提交之后,左边事务才能进行操作(读不会被拦截) 。
mysql> set global transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
1 row in set, 1 warning (0.00 sec)
mysql> begin; --开启事务,终端B同步开启
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; --两个读取不会串行化,共享锁
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
mysql> update account set blance=1.00 where id=1; --终端A中有更新或者其他操作,会阻
塞。直到终端B事务提交。
Query OK, 1 row affected (18.19 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--终端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; --两个读取不会串行化
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
mysql> commit; --提交之后,终端A中的update才会提交。
Query OK, 0 rows affected (0.00 sec)
总结:
- 其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
- 不可重复读的重点是修改和删除:同样的条件, 你读取过的数据,再次读取出来发现值不一样了幻读的重点在于新增:同样的条件, 第1次和第2次读出来的记录数不一样
- 说明: mysql 默认的隔离级别是可重复读,一般情况下不要修改
- 上面的例子可以看出,事务也有长短事务这样的概念。事务间互相影响,指的是事务在并行执行的时候,即都没有commit的时候,影响会比较大
一致性(Consistency)
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的(不仅仅)。
其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑支撑,也就是,一致性,是由用户决定的。
而技术上,通过AID(原子性、隔离性、持久性)保证C(一致性(数据库和程序员共同维护))。mysql提供技术支持,以上的理解还是不够的,但了解以上的知识,在mysql事务使用上,没有大问题。
结语:
随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。
你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容。