本文已参与「新人创作礼」活动,一起开启掘金创作之路
事务的四大特性
1、原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
2、一致性(Consistency)
事务执行的过程中,数据总量保持不变。 一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。举例来说,假设用户A和用户B两者的钱加起来一共是1000,那么不管A和B之间如何转账、转几次账,事务结束后两个用户的钱相加起来应该还得是1000,这就是事务的一致性。
3、隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如同时操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
4、持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
事务的隔离级别
事务存在的意义:
一个事务代表了一组同时成功或者同时失败的操作,一般来说事务都会以begin transaction 开始,以commit/rollback结束,如果当我们的数据库发生死机,或者执行过程中发生错误,只要没有被commit,那么对应的这个事务的session就会被回滚。
读未提交
- 会造成脏读、不可重复读、幻读等问题。
读已提交
- 只能解决脏读问题
- 会造成不可重复读、幻读等问题
如何解决脏读问题
- 在读已提交的隔离级别下 , 事务A如果还没有将修改的数据进行commit的话,事务B读取到的是事务A修改之前这条数据的内容,只有当事务A进行commit操作的时候,事务B才能读到事务A已修改过的这条数据。
可重复读
- 能解决脏读、不可重复读等问题
- 无法解决幻读问题
如何解决不可重复读问题
基于MVCC多版本控制机制实现(简单来说就是通过快照读,读取undolog,获取该记录更新前的数据,也就是读到的是旧数据)
- 在可重复读的隔离级别下,事务A开启事务,并对一条记录进行修改,这时候事务B也开启事务,开始查询这条记录,这时候事务B读取到的是这条记录未修改之前的信息,并且会记录一个版本号,当事务A进行提交后,事务B再次去读取这条记录,就会去缓存当中看是否已经存在这条记录被读过的快照,如果已经存在的话,就会直接取出事务B上一次读取的那条数据(就是未修改之前的旧数据)。无论在这个事务B开启期间读了多少次这条记录,它都读取版本号最早的那条记录。
- 如果在事务A开启事务期间,事务B也同时开启事务,当是事务B是在事务A执行了修改操作并提交了之后才进行读取操作。那么事务B读取到的就是事务A提交之后修改过的那条记录。
造成幻读的原因
-
在可重复读隔离级别下,虽然事务B读取的时候会记录数据的版本,以此来判断数据是否被修改过,但是插入操作执行之前是没有这条记录的,所以也就不存在这条数据的版本,也就造成了幻读。
-
RR隔离级别和RC隔离级别最大的问题在于创建readView的时机不一样
MySQL如何在可重复读的级别下解决幻读的呢?
- 首先要明白如果事务中都是用快照读,那么不会产生幻读的问题,但是快照读和当前读一起使用的时候就会产生幻读。
| 时间 | 事务1 | 事务2 |
|---|---|---|
| begin; | ||
| T1 | select * from user where age =20 for update; | |
| T2 | insert into user values(25,'25',20);此时会阻塞等待锁 | |
| T3 | select * from user where age =20 for update; |
此时,可以看到事务2被阻塞了,需要等待事务1提交事务之后才能完成
- 说白了就是RR隔离级别下,通过read view以及加锁的解决了幻读,MySQL最喜欢加的其实是行锁,毕竟锁的粒度越小,并行的效率就越高,但是为了应对不同隔离界别下的情况,MySQL采用了行锁、间隙锁(左边是闭区间,右边也是闭区间比如(10,20))、临建锁(行锁+间隙锁:左边是闭区间,右边是开区间,(10,20] 因为包括行锁,把20这一行也锁了)来避免幻读。
- 加锁都是给索引加,如果说表有唯一索引,或者显示主键,加锁的时候能够定位到唯一记录的情况,加行锁就够了,不然的话就是间隙锁
- 各种隔离界别下的加锁情况详见文档
串行化
- 能解决所有问题
如何解决
将所有数据库的事务串行化,势必所有的事务之间都是先后执行的,必须等到前一个事务commit/rollback之后,下一个事务才能开启。但是执行效率会大大降低。
什么是脏读?:
事务A begin transaction ,执行了一条修改语句。与此同时事务B也begin transaction,执行了一条查询语句,查询当前事务A正在修改的这条记录。在读未提交隔离级别下,即使事务A的修改语句还没有commit,事务B读到的也是这时候事务A正在修改的内容。
- 读到了一个事务未提交时候的数据
什么是不可重复读?:
事务A未提交之前,事务B读到的是事务A未进行修改之前的记录,当事务A进行提交后,事务B又读取了一次这条记录,结果这时候读到的却是事务A修改后的内容,两次读到的结果不一致,这就是不可重复读
- 同一个事务在两次读取的过程中读到的数据是有变化的
什么是幻读?:
事务A开启事务,并且插入了一条记录,与此同时事务B开启事务,并进行了一次范围查询,这时候事务A还并未提交,所以事务B查询到的数据范围不包含事务A未提交的这条记录。之后事务A进行了提交,事务B又进行了一次范围查询,这时候查询到的数据范围包含了事务A已提交的这条记录,同一个事务两次查询的数据条数不一致,就是幻读。
- 同一个事务在两次读取的过程中,读取到的数据条数是有变化的
事务的传播机制
快照读和当前读
快照读
简单的select操作,属于快照读,不加锁。
- select * from table where ?;
快照读无法感知到当前记录最新的数据变化,因为同一个事务连续多次的快照读,实际上后面的几次读取操作,都是读取当前事务第一次读取时的快照对应的记录。当一个事务是第一次读取时,读取的是上一个事务对这条记录进行commit后的内容,并记录下当前记录的版本号。
当前读
特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁
- select * from table where ? lock in share mode;
- select * from table where ? for update;
- insert into table values (…);
- update table set ? where ?;
- delete from table where ?;
- lock in share mode 是加读锁,一行数据上可以加多个读锁
- for update 是加写锁(排他锁),一行数据上只能有一个写锁,读写互斥
- 一旦进行了当前读,其实就是人为的将事务的隔离级别调成了串行化,对一个数据的删除修改都是等到上一个事务对这个数据的操作结束并且把锁释放掉之后才能进行
快照读和当前读都可以解决的一些问题可重复读级别下幻读问题
- 快照读利用MVCC读快照,利用空间换时间,读取数据的老版本
- 当前读直接采用加锁,串行化的形式,保证了数据的一致性
当前读涉及到的锁操作
行锁(记录锁)
只锁住当前行,其他行的数据操作不会受影响
间隙锁
间隙锁的作用
保证某个间隙内的数据在锁定情况下不会发生任何变化。
运用场景
- 会用在非唯一索引或者不走索引的当前读中
- 如果,搜索条件里有多个查询条件(即使每个列都有唯一索引),也是会有间隙锁的。
表锁
我对一个数据库的表加锁,就是对这个数据库这张表的操作全部串行化,实际运用中的性能缺失是不可容忍的
总结
快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。如果需要实时显示数据,还是需要通过加锁来实现。这个时候会使用next-key技术来实现(间隙锁和行锁合称 next-key lock)。
在mysql中,提供了两种事务隔离技术,第一个是mvcc,第二个是next-key技术。这个在使用不同的语句的时候可以动态选择。不加lock in share mode之类的就使用mvcc。否则使用next-key。
- mvcc的优势是不加锁,并发性高。缺点是不是实时数据。
- next-key的优势是获取实时数据,但是需要加锁。
同时需要注意几点:
1.事务的快照时间点是以第一个select来确认的。所以即便事务先开始。但是select在后面的事务的update之类的语句后进行,那么它是可以获取后面的事务的对应的数据。
2.mysql中数据的存放还是会通过版本记录一系列的历史数据,这样,可以根据版本查找数据。