数据库事务的四大特性和隔离级别,一文带你看通透
Posted 格子衫111
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据库事务的四大特性和隔离级别,一文带你看通透相关的知识,希望对你有一定的参考价值。
1. 什么是事务
事务
事务是一个整体,由一条或者多条SQL语句组成,这些SQL语句要么都执行成功,要么都执行失败,
只要有一条SQL出现异常,整个操作就会回滚,整个业务执行失败
比如: 银行的转账业务,张三给李四转账500元 , 至少要操作两次数据库, 张三 -500, 李四 + 500,这中间任何一步出现问题,整个操作就必须全部回滚, 这样才能保证用户和银行都没有损失
回滚
即在事务运行的过程中发生了某种故障,事务不能继续执行,系统将事务中对数据库的所有已完成的操作全部撤销,滚回到事务开始时的状态
比如:张三转账完成后,卧槽,发现转多了,叫李四退回来,最终张三和李四的钱还是保持原样,这个就是回滚
mysql 中可以有两种方式进行事务的操作:
- 自动提交事务
- 手动提交事务
自动提交事务
MySQL 默认每一条 DML(增删改)语句都是一个单独的事务,每条语句都会自动开启一个事务,语句执行完毕自动提交事务,MySQL默认是自动提交事务
手动提交事务
开启事务 start transaction; 或者 BEGIN;
提交事务 commit;
回滚事务 rollback;
- 执行成功的情况: 开启事务 -> 执行多条 SQL 语句 -> 成功提交事务
- 执行失败的情况: 开启事务 -> 执行多条 SQL 语句 -> 事务的回滚
这样,以后执行修改操作前就需要开启事务,执行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在当前读事务开启时,
- 给涉及到的行加写锁(行锁)防止写操作;
- 给涉及到的行两端加 间隙锁(Gap Lock) 防止新增行写入;从而解决了幻读问题。
顾名思义,行锁就是锁住行的锁,
间隙锁,锁的就是两个值之间的空隙。比如一个表,初始化插入了 7 个记录,这就产生了 8 个间隙。
由于ID大于7,被间隙锁(7,+∞)锁住。这样就确保了无法再插入新的记录。
MySQL将行锁 + 间隙锁组合统称为 next-key lock,通过 next-key lock 解决了幻读问题。
注意
next-key lock的确是解决了幻读问题,但是next-key lock在并发情况下也经常会造成死锁。死锁检测和处理也会花费时间,一定程度上影响到并发量。
以上是关于数据库事务的四大特性和隔离级别,一文带你看通透的主要内容,如果未能解决你的问题,请参考以下文章
一文带你理解脏读,幻读,不可重复读与mysql的锁,事务隔离机制
一文带你理解脏读,幻读,不可重复读与mysql的锁,事务隔离机制
什么是脏读不可重复读幻读?一文带你搞定MySQL事务隔离级别