文章目录
1. 简介
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
MySQL数据库事务是默认自动提交的。也就是说,当执行一条DML语句,MySQL会立即隐式的提交事务。
2. 事务案例
2.1 数据准备
新建数据库transaction_study
。
新建account
表。
create table account(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
money int comment '余额'
) comment '账户表';
insert into account(id,name,money) values(null,'Linda',2000),(null,'Tom',2000);
2.2 转账操作
正常操作
# 1.查询Linda账户余额
select money from account where name='Linda';
# 2.Linda账户余额-1000
update account set money=money-1000 where name='Linda';
# 3.Tom账户+1000
update account set money=money+1000 where name='Tom';
模拟异常情况
Linda向Tom转账1000,当Linda账户余额减掉1000后,发生异常。
# 1.查询Linda账户余额
select money from account where name='Linda';
# 2.Linda账户余额-1000
update account set money=money-1000 where name='Linda';
select 模拟异常
# 3.Tom账户+1000
update account set money=money+1000 where name='Tom';
只要在sql中增加select 模拟异常
即可,因为这不是正确的sql语句,所以就相当于错误的存在。
然后将这部分sql选中,然后执行SQL。意料之中,SQL执行会出现错误。
此时,数据变为:
我们可以看到,Linda的钱减去了1000,但是Tom的钱并没有增加。
当前来说,每一条SQL语句为一个事务。事务执行完成会自动提交。
查询/设置事务的提交方式
select @@autocommit; # 查询事务的提交方式
set @@autocommit=0; # 将事务设置为手动提交
事务提交方式i:
@@autocommit
为1,是自动提交@@autocommit
为0,是手动提交注意:上诉这种方式,只针对当前会话有效!
改为手动提交事务后,我们要在事务结束时,提交事务,执行COMMIT
指令。当事务出现错误时,我们需要手动执行ROLLBACK
指令回滚事务。
先将数据恢复为初始数据。
update account set money=2000 where name='Linda' or name='Tom';
将事务提交方式改为手动提交后,我们进行测试一下。
# 1.查询Linda账户余额
select money from account where name='Linda';
# 2.Linda账户余额-1000
update account set money=money-1000 where name='Linda';
# 3.Tom账户+1000
update account set money=money+1000 where name='Tom';
执行如上SQL,当执行完毕后,可以看到表中的数据没有发生改变,这是因为我们将事务提交方式改为了手动,此时的事务还没有提交,我们需要手动执行:
commit;
执行完毕后,可以看到表中的数据发生了改变。
然后,我们再次模拟发生异常的情况。
# 1.查询Linda账户余额
select money from account where name='Linda';
# 2.Linda账户余额-1000
update account set money=money-1000 where name='Linda';
select 模拟异常
# 3.Tom账户+1000
update account set money=money+1000 where name='Tom';
执行如上SQL,当执行完毕后,可以发现报错了。此时,表中的数据也没有发生改变。由于出错了,我们便不能再提交事务,需要回滚事务,所以执行:
rollback;
执行完毕后,可以看到表中的数据和初始值相同,事务中的所有SQL语句均未生效。
开启事务
显式的开启事务,我们不再需要将事务的提交方式改为手动。所以此时,我们执行set @@autocommit=1
,将事务提交方式设置为自动。
使用如下指令开启事务:
start transaction
# 或者
begin
同样,我们需要手动提交事务或者回滚事务
commit;
# 或者
rollback;
我们使用这种方式进行测试,首先将事务的提交方式恢复为最初,设置为自动提交。
# 开启事务
start transaction;
# 1.查询Linda账户余额
select money from account where name='Linda';
# 2.Linda账户余额-1000
update account set money=money-1000 where name='Linda';
select 模拟异常
# 3.Tom账户+1000
update account set money=money+1000 where name='Tom';
同时执行上诉SQL语句,会发现报错了。可以看到表中的数据没有发生改变。
此时,上诉的4个操作会被看作为1个事务。此时事务出错,我们应该进行事务的回滚。
rollback;
3. 四大特性ACID
- 原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败。
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
4. 并发事务问题
脏读
一个事务读到另一个事务还没有提交的数据。
不可重复读
一个事务先后读取同一条记录,但两次读取的数据不同。
幻读
一个事务按照查询条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在了,好像出现了“幻影”。
4.1 脏读
4.2 不可重复读
4.3 幻读
5. 隔离级别
MySQL事务的默认隔离级别为:Repeatable Read
Oracle事务的默认隔离级别为:Read committed
Serializable隔离级别最高,但是性能最差。
查看事务的隔离级别:
select @@transaction_isolation; # REPEATABLE-READ
设置事务的隔离级别
set [session|global] transaction isolation level [read uncommitted|read committed|repeatable read|Serializable]
- session:会话级别
- global:全局级别
5.1 Read Uncommitted
下面演示脏读问题。
开启两个会话客户端,A客户端和B客户端。
将A客户端,设置事务的隔离级别为Read Uncommitted
。
set session transaction isolation level read uncommitted;
B客户端的事务隔离级别保持默认,为Repeatable read
。
此时,数据表中的数据为:
B客户端开启事务,然后执行一条更新语句,不提交事务
start transaction ;
update account set money=money-1000 where name='Linda';
A客户端开启事务,然后查询表中数据
start transaction ;
select * from account;
查询结果为:
此时,B客户端的事务并没有提交,但是A客户端已经读取到了B未提交的事务所更改的数据。这就是脏读问题。
5.2 Read committed
该隔离级别可以解决脏读问题。下面我们进行验证。
我们将客户端A的事务隔离级别设置为Read committed
。
set session transaction isolation level read committed;
B客户端的事务隔离级别保持默认,为Repeatable read
。
B客户端开启事务,然后执行一条更新语句,不提交事务
start transaction ;
update account set money=money-1000 where name='Linda';
A客户端开启事务,然后查询表中数据
start transaction ;
select * from account;
查询结果为:
此时,B客户端的事务并没有提交,A客户端并没有读取到了B未提交的事务所更改的数据。
所以,这种隔离级别解决了脏读问题。
该隔离级别下,虽然解决了脏读问题,但依然存在不可重复读问题。
不可重复读问题演示
客户端A的事务隔离级别设置为Read committed
。B客户端的事务隔离级别保持默认,为Repeatable read
。
A客户端执行如下SQL:
start transaction ;
select * from account;
B客户端执行如下SQL:
start transaction ;
update account set money=money-1000 where name='Linda';
commit;
此时A客户端继续执行
select * from account;
此时查询到的结果与之前查询到的结果不一致。在同一个事务中,前后查询结果不一致,这种就是不可重复读。
5.3 Repeatable Read(默认)
该隔离级别下可以解决脏读和不可重复读问题。
下面演示该隔离级别解决不可重复读问题。
我们将客户端A的事务隔离级别设置为Repeatable Read
。
set session transaction isolation level repeatable read;
B客户端的事务隔离级别保持默认,为Repeatable read
。
A客户端执行如下SQL:
start transaction ;
select * from account;
B客户端执行如下SQL:
start transaction ;
update account set money=money-1000 where name='Linda';
commit;
此时A客户端继续执行
select * from account;
可以看到,虽然客户端B已经将更新的事务提交了,但是A事务中查询到的数据没有发生改变,保证了前后查询的一致性,解决了不可重复读问题。
此时,客户端A提交事务,执行commit
。然后再次进行查询:
select * from account;
可以看到,当客户端A提交事务后,再次查询,查询到的结果为更新后的结果,结果正确!
下面,再测试另外一种情况,当客户端A开启事务,然后客户端B开启事务,客户端B执行了一次更新操作,并提交事务。在客户端B执行更新操作,提交事务后,客户端A再进行查询操作,那么客户端A查询到的结果是更新前的数据,还是更新后的数据呢?
初始数据:
客户端A开启事务:
start transaction ;
客户端B执行如下SQL:
start transaction ;
update account set money=money+1000 where name='Linda';
commit;
客户端A执行如下SQL:
select * from account;
可以看到,客户端A查询到的数据为更新后的数据,因为客户端A在此之前并没有进行该表的数据查询,所以不会保证和之前查询的数据保持一致,所以查询到了更新后的数据。
此时,如果客户端B再执行如下SQL:
start transaction ;
update account set money=money+1000 where name='Linda';
commit;
客户端A执行如下SQL:
select * from account;
那么此时,查询结果如何呢?
可以看到,查询结果为更新前的结果,没有发生改变,解决了不可重复读问题。
该隔离级别解决了脏读和不可重复读问题,但是依然存在幻读问题。
幻读问题演示
客户端A的事务隔离级别设置为Repeatable read
。B客户端的事务隔离级别保持默认,为Repeatable read
。
客户端A执行:
start transaction ;
select * from account;
客户端B执行如下SQL:
start transaction ;
insert into account values(5,'James',1000);
commit;
由于客户端A一开始没有查询到James
这条数据,此时客户端A继续执行:
insert into account values(5,'James',1000);
会发现,出错了!该主键已经存在了,但是客户端A刚刚查询到结果中是没有该记录的!
此时客户端A再次执行查询命令:
select * from account;
由于该隔离级别下解决了不可重复读问题,结果会和之前的查询结果一致,所以查询结果中仍然没有James
这条记录。这就是幻读问题。
5.4 Serializable
该隔离级别下可以解决脏读、不可重复读和幻读问题。
下面演示该隔离级别解决幻读问题。
我们将客户端A的事务隔离级别设置为Serializable
。
set session transaction isolation level Serializable;
B客户端的事务隔离级别保持默认,为Repeatable read
。
客户端A执行:
start transaction ;
select * from account;
客户端B执行如下SQL:
start transaction ;
insert into account values(6,'Bob',2000);
commit;
此时,会发现,SQL并没有执行完成。
SQL执行卡在了插入数据这个位置。它在等待客户端A事务的结束。客户端A事务相当于对该表加了个锁。
如果此时,客户端A不再执行任何操作,直接提交事务,执行commit
,那么客户端B这段SQL则会执行下去,执行成功。数据成功被插入。
如果此时,客户端A执行了如下操作:
insert into account values(6,'Bob',2000);
commit;
那么客户端B则会直接失败。因为主键6
已经存在。
如果客户端A一开始只是开启了事务,但是没有对account表进行查询操作,那么此时客户端B可以正常开启事务,向account表中插入数据,提交事务。
当客户端A开启了事务且对account表进行了一次操作后,客户端B就需要等待客户端A对事务结束后,才可以继续对account表进行操作。但是查询操作是可以正常进行。
事务的隔离级别越高,但是性能越低!
一般会使用数据库的默认隔离级别。