事务简介
在MySQL中,所谓的“事务”指的是一堆的SQL语句,要么全部执行成功,要么当做没有执行过。这也被称为事务的“原子性”。
日常开发中,完成一个业务操作,通常涉及到多条SQL语句“打配合”。比如说转账,A向B转账,【A扣钱,B加钱】,这个操作是不能分割的。要保证该操作一定会成功(转账成功),或者执行失败(转账失败),就需要给在一个独立的事务中进行该操作。
在MySQL中,只有使用了 Innodb 存储引擎的数据库或表才支持事务。
事务的分类
1、显式事务和隐式事务
隐式事务
默认的事务是隐式事务,有变量【autocommit】控制。在隐式事务的环境下,我们每执行一条SQL语句(查询操作不会)都会【自动开启和关闭】事务,
show variables like "%autocommit%"
显式事务
可以通过begin
和start transaction
来显式的开启一个事务,当前的会话开启了一个事务后,可以连续执行多条SQL语句,执行完毕后,可以使用commit
提交事务,或者rollback
回滚事务:
start transaction;
update tb_student set age = 20 where id = 3333;
delete from tb_student where id = 2222;
commit;
start transaction;
update tb_student set age = 20 where id = 4444;
delete from tb_student where id = 6666;
rollback;
提交之后,脏数据会被刷入硬盘中;回滚则不会。
2、只读事务和读写事务
我们可以使用read only开启只读事务,开启只读事务模式之后,事务执行期间任何非查询语句【insert、update、delete】语句都是不允许的:
start transaction read only ;
select count(*) from tb_student;
select count(1) from tb_student;
commit ;
3、保存点
我们可以使用saveponit
关键字在事务执行过程中新建一个【保存点】,之后可以使用rollback向任意保存点回滚。
start transaction ;
delete from tb_student where id = 1000;
delete from tb_student where id = 1001;
savepoint kaiven;
delete from tb_student where id = 1003;
rollback to kaiven;
有些业务场景会用到的,积累一下。
事务的四大特性(ACID)
1、原子性(Atomicity)
一个事务中的所有操作,要么全部执行成功,要么全部执行失败,不会再中间某个环节结束。如果事务在执行过程中发生异常,会被回滚到事务开始时的状态,就像这个事务从来没有执行过一样。
2、一致性(Consistency)
在事务【开始之前和结束之后】,数据库的逻辑完整性没有受到破坏,数据库的状态应该是与业务规则保持一致。(其实也就是我们一直在强调的数据一致性,其他三个特性最终的目的也都是保障数据一致性)
3、隔离性(Isolation)
数据库【允许多个并发事务同时对相同的数据进行读取和修改】,隔离性可以防止多个事务在并发修改共享数据时产生【数据不一致】的现象。(想一下多线程并发修改数据的场景就清晰了)
事务的隔离级别:读未提交(Read Uncommited)、读已提交(Read Commit)、可重复读(Repeatable read)、串行化(Serializable)。
4、持久性(Durability)
事务处理结束后,进行了事务提交,对数据的修改就是永久的,即便是系统故障了。
事务的隔离级别
在多个事务【并发操作】相同的表数据时,为了让多个事务都可以得到正确的结果,不会因为相互的交叉操作产生干扰,同时还保证一定的执行效率,故而提出了不同的事务隔离级别。
隔离级别分类如下,数据库经典三大事务并发问题:脏读、不可重复读、幻读,后面会讲到。
在MySQL中查看和设置【事务的隔离级别】
show variables like '%isolation%'; -- 查看当前会话的默认事务隔离级别
set transaction isolation level read committed ; -- 设置下一个事务的隔离级别【read committed 表示读已提交】(很少用,仅生效一次,了解一下就行)
set session transaction isolation level read committed ; -- 设置当前会话的事务隔离级别
set global transaction isolation level read committed ; -- 设置全局的事务隔离级别
1、读未提交(RU)
该隔离级别下,一个事务可以读取到其他【未提交事务】修改后的数据,可能会存在脏读、不可重复读、幻读的问题。
脏读:指的是一个事务读取到了其他事务未提交的数据,未提交的事务就意味着在接下来的时间段可能会回滚,读到的数据不一定准确。
set global transaction isolation level read uncommitted ; -- 修改MySQL的隔离级别为读未提交
# A会话开启了事务
start transaction; -- 1
update tb_student set student_id = '2222' where id = 1; -- 3
rollback ; -- 6
# B会话开启了事务
start transaction; -- 2
select student_id from tb_student where id = 1; -- 4
commit ; -- 5
select student_id from tb_student where id = 1; -- 7
(注释中的数字代表语句的执行顺序,可以开两个MySQL客户端进行操作)
我们会发现,在【读未提交】的事务隔离级别下,B事务读取到了A事务修改了的数据,但是由于A事务会回滚,所以出现了脏读的情况。
2、读已提交(RC)
该隔离级别下,事务之间只能读取到别人已经提交了的数据,可能会产生不可重复读。
不可重复读:事务A在执行过程中修改了事务B已经读取过的数据,并且事务A进行了事务提交操作。如果事务B再次去读相同的数据时,会发现两次读取的数据不一致。也就是说在一个原子性的操作中,一个事务两次读取相同位置的数据,得到结果不一致,同一个数据不能被重复的读取。主要是【update】语句,会导致不可重复读。
set global transaction isolation level read committed ; -- 修改全局事务为读已提交
show variables like '%isolation%';
# 事务A开启事务
start transaction ; -- 1
select student_id from tb_student where id = 1; -- 2
select student_id from tb_student where id = 1; -- 5
commit ; -- 6
# 事务B开启事务
start transaction ; -- 2
update tb_student set student_id = '7777777' where id = 1; -- 3
commit ; -- 4
我们会发现,事务A两次读取的结果不一致(对同一个地方的记录进行读取,发现数据不一致),很明显,这不符合原子性的要求。
(这里可能大家会有疑惑,就是读取到别人已经提交的数据不是一件很正常的事情嘛。确实是这样的,但是原子性要求当前事务在执行的过程中,上下文保持一致,即所用到的数据不允许被别人修改,否则就违反了原子性的约定)
3、可重复读(RR)
同一个事务中,对于相同位置的结果集查询,总是与当前事物的执行逻辑一致。该隔离级别下可能出现幻读。
幻读:事务A先执行了一遍查询,然后事务B对事务A操作的表进行了【insert】或者【delete】操作。事务A再次查询的时候,可能出现多了一些数据或者少了一些数据,就像是出现了幻觉一样。
不可重复读强调的是行数据的【内容不同】,幻读强调的是【数据集不同】。
set global transaction isolation level repeatable read ; -- 设置全局事务隔离级别为可重复读
# 建立测试表
drop table if exists tb_user;
create table tb_user (
id int primary key auto_increment comment '自然主键',
nickname varchar(256) comment '昵称',
age tinyint comment '年龄'
) engine=innodb default charset=utf8mb4 collate utf8mb4_general_ci comment '用户测试表';
# 模拟数据
insert into tb_user value (null,'kaiven',20);
insert into tb_user value (null,'jack',21);
insert into tb_user value (null,'lucy',22);
# 事务A操作
start transaction ; -- 1
select * from tb_user; -- 3
select * from tb_user; -- 6
update tb_user set nickname=null where id > 0; -- 7
select * from tb_user; -- 8
commit ; -- 9
# 事务B操作
start transaction ; -- 2
insert into tb_user value (null,'小黄',23); -- 4
commit ; -- 5
我们会发现,没有执行【update】语句之前的查询语句,是感知不到第四条记录的,执行了update语句之后,再次查询,就有了第四条记录,并且对其进行了修改。
(MySQL的RR隔离级别中,innodb使用了【MVCC + 锁】的机制,解决了绝大部分的幻读情况)
4、串行化
这个没有什么好说的,性能最差了,就是事务之间必须排队等待,直到上一个事务提交或者回滚。
2024.11.15
writeBy kaiven