数据库事务的四大特性和隔离级别,一文带你看通透

1. 什么是事务

事务

事务是一个整体,由一条或者多条SQL语句组成,这些SQL语句要么都执行成功,要么都执行失败,

只要有一条SQL出现异常,整个操作就会回滚,整个业务执行失败

比如: 银行的转账业务,张三给李四转账500元 , 至少要操作两次数据库, 张三 -500, 李四 + 500,这中间任何一步出现问题,整个操作就必须全部回滚, 这样才能保证用户和银行都没有损失

回滚

即在事务运行的过程中发生了某种故障,事务不能继续执行,系统将事务中对数据库的所有已完成的操作全部撤销,滚回到事务开始时的状态

比如:张三转账完成后,卧槽,发现转多了,叫李四退回来,最终张三和李四的钱还是保持原样,这个就是回滚

 

MYSQL 中可以有两种方式进行事务的操作:

  • 自动提交事务
  • 手动提交事务

自动提交事务

MySQL 默认每一条 DML(增删改)语句都是一个单独的事务,每条语句都会自动开启一个事务,语句执行完毕自动提交事务,MySQL默认是自动提交事务

手动提交事务

开启事务 start transaction; 或者 BEGIN;
提交事务 commit;
回滚事务 rollback;

  • 执行成功的情况: 开启事务 -> 执行多条 SQL 语句 -> 成功提交事务
  • 执行失败的情况: 开启事务 -> 执行多条 SQL 语句 -> 事务的回滚

 
如何取消自动提交
 
MySQL 默认是自动提交事务,如有需要,可 设置为手动提交。设置步骤如下:
 
1) 登录 mysql ,查看 autocommit 状态。
 
       SHOW VARIABLES LIKE 'autocommit' ;
 

on :自动提交
off : 手动提交
 
2) autocommit 改成 off;
 
     SET @@autocommit =off;
 

这样,以后执行修改操作前就需要开启事务,执行sql完成,必须提交后才能生效

 

2. 事务的四大特性(ACID)

原子性(Atomic)

事务是一个不可分割的整体,事务中所有的sql语句要么全部执行成功,要么全部失败

比如:张三转账给李四500,需要执行两条sql,张三账户 -500,李四账户+500

update account set money = money - 500 where name = '张三';
update account set money = money + 500 where name = '李四';

这两个sql必须全部成功,才能算是真正的转账成功,这就是事务的原子性,不可拆分。

一致性(Consistency)

事务在执行前数据库的状态与执行后数据库的状态保持一致

比如:转账前2个人的总金额是 2000,中间不管怎么转账,转几次账,转账后2个人总金额也是2000。这就是事务的一致性。

隔离性(Isolation)

一个事务的执行不能被其他事务干扰。
即一个事务内部的操作及使用的数据,对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
这个与事务设置的隔离级别有密切的关系

比如:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

持久性(Durability)

一旦事务执行成功,对数据库的修改是持久的。就算关机,数据也是要保存下来的

比如:我成功修改了数据库的一条数据后,突然停电了,那条数据依然在数据库中,永久的保存着。

3. 事务的隔离级别

数据库的隔离级别有4种,级别从低到高为:
读未提交(read uncommitted)、读已提交(read committed)、可重复读(repeatable read)、串行化(serializable)

谈到事务的隔离级别,首先就得来了解一下事务并发访问产生的问题:脏读、不可重复读、幻读
这几个问题和数据库隔离级别密切相关,如果不设置合理的隔离级别,那么就可能出现这三个并发问题

脏读:

一个事务读取到了另一个事务中尚未提交的数据

举个栗子:
如果当前的数据库隔离级别为最低——读未提交(read uncommitted)
假定当前有两个事务在并发执行,分别是:事务A、事务B

事务A: 张三向李四转账500元
对应SQL命令如下(在同一个事务):

update account set money = money - 500 where name = '张三';
update account set money = money + 500 where name = '李四';

事务B:李四查询账户余额
对应SQL命令如下:

SELECT money FROM account where name = '李四';

当张三给李四转账后(事务A执行,但是事务未提交),
李四此时正好去查询账户(事务B执行),发现账户增加了500(此时即发生了脏读),非常高兴。
但是,突然张三突然反悔了,于是迅速回滚了事务。
现在李四的账户变成了初始的状态,但是李四读取出来的金额,却多了500。

李四啊,别人都还没提交呢,你就去读取了,别人万一回滚咋办,所以你读的是脏数据,这叫脏读,是有问题的!

如何解决脏读问题?

脏读非常危险的,比如张三向李四购买商品,张三开启事务,向李四账号转入 500 块,然后打电话给李四说钱 已经转了。李四一查询钱到账了,发货给张三。张三收到货后回滚事务,李四的再查看钱没了。

解决方案:
将全局的隔离级别进行提升为: 读已提交(read committed)
--  设置事务隔离级别为 read committed
set global transaction isolation level read committed;

重新开启DOS窗口, 查看设置是否成功:
-- 查看事务隔离级别
select @@tx_isolation;

设置完成后,再次重新执行转账操作(事务A),不提交,然后李四执行查询操作(事务B),
内心毫无波澜,因为账户金额还是最初的样子,说明现在没有脏读问题了。

大多数数据库的默认级别就是read committed,比如Sql Server , Oracle

但是,这种隔离级别还不够呢,还可能存在不可重复读的问题。

不可重复读:

同一个事务中,进行查询操作,但是每次读取的数据内容是不一样的,
这是由于在查询间隔中,数据被另一个事务修改并提交了

举个栗子:
如果当前的数据库隔离级别为 读已提交(read uncommitted)
假定当前有两个事务在并发执行,还是事务A、事务B

事务A: 张三向李四转账500元
对应SQL命令如下(在同一个事务):

update account set money = money - 500 where name = '张三';
update account set money = money + 500 where name = '李四';

事务B:李四查询账户余额
对应SQL命令如下:

SELECT money FROM account where name = '李四';

李四着急忙慌的,先去查询一次自己的账户余额(事务B),嗯,有1000,不错。
当张三给李四转账成功后(事务A执行并提交),
李四老婆叫他再次确认下自己的余额,于是李四又去查询一次自己的账户余额(事务B),我嚓,怎么和之前查询不一样哦,多了500。
我这还处于同一个事务中呢,这读取结果却不一样,到底哪次是对的?
这就是所谓的不可重复读问题。

下面这张图很好的诠释了这个执行过程:

如何解决不可重复读问题?

我们可以考虑这样一种实际情况:
比如银行程序需要将查询结果分别输出到电脑屏幕和发短信给客 户,结果在一个事务中针对不同的输出目的地进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了

解决方案:
将全局的隔离级别进行提升为:可重复读(repeatable read) 
-- 设置事务隔离级别为 repeatable read 
set global transaction isolation level repeatable read;

重新开启DOS窗口, 查看设置是否成功:
-- 查看事务隔离级别
select @@tx_isolation;

设置完成后,恢复数据,再次重复上述操作,然后可以发现,李四两次查询的金额是相同的,说明已经没有不可重复读问题了。

mysql 数据库的默认级别就是repeatable read

但是,这样就万事大吉了吗,no,还可能存在幻读问题。

幻读: 

在同一个事务中,前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
这是由于事务在执行期间,另一个事务新增了数据并提交了

举个栗子:
假定当前有两个事务在并发执行,还是事务C、事务D

事务C: 先查询有没有id为3的记录,如果没有,则进行插入
对应SQL命令如下(在同一个事务):

select * from account where id = 3;
INSERT INTO account VALUES(3,'王五',1000);

事务D:插入一条新记录
对应SQL命令如下:

INSERT INTO account VALUES(3,'王五',1000);

执行顺序为:
  1)事务C执行第一条SQL, 查询是否有id为3的记录
        结果发现,没有,那我可以插入了。
  2)事务D新增一条记录,并提交
  3)事务C执行第二条SQL,新增id为3的记录,发现报错——主键重复
       见鬼了,我刚才读到的结果应该可以支持我这样操作才对啊,为什么现在不可以

select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读

如何解决幻读问题?

我们可以考虑这样一种实际情况:
比如银行程序需要将查询结果分别输出到电脑屏幕和发短信给客 户,结果在一个事务中针对不同的输出目的地进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作 人员就不知道以哪个为准了

解决方案:
将事务隔离级别设置到最高:串行化(serializable)

如果一个事务,使用了SERIALIZABLE——可串行化隔离级别时,在这个事务没有被提交之前 , 
其他的线程,只能等到当前操作完成之后,才能进行操作,这样会非常耗时,
而且,影响数据库的性能,数据库不会使用这种隔离级别

-- 设置事务隔离级别为 serializable
set global transaction isolation level serializable;

重新开启DOS窗口, 查看设置是否成功:
-- 查看事务隔离级别
select @@tx_isolation;

设置完成后,恢复数据,再次重复上述操作

执行顺序为:
   1)事务C执行第一条SQL, 查询是否有id为3的记录
        结果发现,没有,那我可以插入了。
   2)事务D插入一条记录,这个操作无法完成,光标一直闪烁
   3)事务C执行第二条SQL,新增id为3的记录, 提交事务 数据插入成功.
   4)事务D在事务C提交之后, 再执行,但是主键冲突出现错误

这就解决了幻读问题

tips:
serializable 串行化可以彻底解决幻读,但是事务只能排队执行,严重影响效率,数据库不会使用这种隔离级别

并发问题区分

不可重复读和脏读的区别脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
幻读和不可重复读的区别:都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读针对的是另外事务的update操作,而幻读针对的是另外事务的insert操作。

总结

  • 脏读说的是事务知道了自己本不应该知道的东西,强调的动作是查询,我看到了自己不该看的东西 ;

  • 不可重复读强调的是一个人查的时候,其他人却可以增删改, 等我再查的时候,和之前查询的不一致,也就是我两次看到了不一样的东西;

  • 幻读说的是我看到了数据是这么多,其他人又插了一条新的,等我拿刚才的查询结果再执行操作的时候,冷不丁发现又多了一条数据

MySQ如何解决幻读问题?

既然不设置串行化,MySQL到底怎么解决幻读问题?

先说结论,MySQL 存储引擎 InnoDB 在可重复读(repeatable read)隔离级别下是解决了幻读问题的。
方法:
是通过next-key lock在当前读事务开启时,

  1. 给涉及到的行加写锁行锁防止写操作
  2. 给涉及到的行两端加 间隙锁(Gap Lock) 防止新增行写入;从而解决了幻读问题

顾名思义,行锁就是锁住行的锁,
间隙锁,锁的就是两个值之间的空隙。比如一个表,初始化插入了 7 个记录,这就产生了 8 个间隙。

由于ID大于7,被间隙锁(7,+∞)锁住。这样就确保了无法再插入新的记录。

MySQL将行锁 + 间隙锁组合统称为 next-key lock,通过 next-key lock 解决了幻读问题。

注意
next-key lock的确是解决了幻读问题,但是next-key lock在并发情况下也经常会造成死锁。死锁检测和处理也会花费时间,一定程度上影响到并发量.
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值