目录
1、什么是事务?
2、介绍下数据库事务?
3、并发事务会带来什么问题?
3.1、不可重复读和幻读有什么区别?
4、数据库隔离级别有哪几种?
5、MySQL默认使用隔离级别是啥?
6、如何控制并发事务?
6.1、锁
6.2、MVCC
1、什么是事务?
事务指的是逻辑上的一组操作,这组操作要么都执行,要么都不执行。最典型的就是转账的例子:
老板每个月给你发工资(500元)转账的操作必然会经历两个操作,一个是从老板账户扣减-500元,另一个是从你的账户增加500元。这个操作必须都成功,或者必须都失败。如果只是一部分成功,那么可能老板的账户扣减了,你的账户却没有增加,或者你的账户增加了,老板的账户却没扣减。这显然都是不允许的,因此事务会把这两个操作看作一个逻辑的整体,这组整体包含的状态必须是一致的。
2、介绍下数据库事务?
数据库事务同样的道理,是将一组数据操作打包在一个事务中,任何一个步骤失败,则必须回滚所有的步骤。MySQL中可以使用start transaction开启一个事务,然后要么使用commit提交事务将数据持久化,要么使用rollback撤销所有的步骤。事务SQL示例如下:
START TRANSACTION;
update account set money=money-500 where name = '老板';
update account set money=money+500 where name = '我们自己';
COMMIT;
事务仅仅如此还是不够的,还应该遵循4大特性(ACID)。
- 原子性(Atomicity):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。对于一个事务来说,不可能只执行其中一部分操作。
- 一致性(Consistency):数据库总是从一个一致性的状态转换到另一个一致性的状态。也就是说在执行事务前后,数据都应该保持一致性。比如前面的余额操作,不管哪一步失败了,前后的账户都不应该多出或少掉500,因为最终事务如果没有提交,事务中所做的修改也不会保存到数据库中。
- 隔离性(Isolation):这个特性通常适用在并发中。一个事务所作的修改在最终提交前,对其他事务是不可见的。如前面第2条SQL从老板账户扣钱500元的时候,如果事务还没提交,此时另一个线程查询老板的账户,其看到的账户余额是没有减去500的。
- 持久性(Durability):指一旦事务提交,则所作的修改就会永久保存到数据库中。就算此时系统崩溃,变更的数据也不会丢失。
事务的ACID特性可以确保数据不会出错,银行不会弄丢你的钱。但是在实际应用逻辑中,要实现这一点却很难,甚至可以说是不可能完成的任务。
这里的ACID,A、I、D都是为了保证最后的C。因此C(一致性)才是最终目的,不然谈A、I、D也就没了目的。《Designing Data-Intensive Application(数据密集型应用系统设计)》一书中提到:原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID 意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母 C 不属于 ACID 。
3、并发事务会带来什么问题?
如果多事务并发运行,可能会导致脏读,幻读,不可重复读,丢失更新的问题。
- 脏读(Dirty read):指一个事务读取到了另一个事务修改后还未提交的数据。一般在READ UNCOMMITTED(未提交读)隔离级别中会出现。在此隔离级别中,事务中的修改,即使没有提交,对其他事务也是可见的。
举个例子:
表中有一条数据,ID=1 Salary=1000
事务A:修改ID=1的Salary=Salary-200
事务B:读取ID=1的Salary值,此时读取到的值为800(由于事务A的操作对于事务B可见)
事务A:系统异常,进行回滚,Salary=1000
此时对于事务B读到的就是未被真正持久化的脏数据。因此被称为脏读。
- 幻读(Phantom read):幻读指的是当某个事务在读取某个范围内的记录时,另一个事务又在该范围内插入了新的记录,而之前的事务再次读取该范围内的记录时,会多读取到几行记录,有点像产生了幻觉。
举个例子:
表中有关于部门员工的数据,dept='研发部',原始员工数量=10
事务A:查询部门员工的数据,select * from xx where dept='研发部'
事务B:新增了一名员工,此时员工数量=11
事务A:再一次查询部门员工的数据,select * from xx where dept='研发部'
此时对于事务A来说前后两次读取到的员工数据不一致,第二次读取到了11条数据,多出来的一条像是产生了幻觉。因此被称为幻读。
- 不可重复读(Unrepeatable read):指在一个事务内,多次读取同一数据时,得到的结果不一致。当一个事务两次读取数据的中间,另一个事务对该数据进行了修改,那么将导致之前的事务前后两次读取到的数据不一致。
举个例子:
表中有一条数据,ID=1 Salary=1000
事务A:读取ID=1的Salary,此时读取到的值为1000
事务B:修改ID=1的Salary=Salary-200
事务A:读取ID=1的Salary值,此时读取到的值为800
此时对于事务A两次读取到的值不一致。
幻读和不可重复读,有点类似,其实可以看成幻读是不可重复读的一种场景。
- 丢失更新(Lost to modify):指一个事务在读取了一个数据后,另一个事务也读取了该数据。此时两个事务都对这个数据进行了修改,那么其中一个事务的修改就可能被另一个事务的修改所覆盖,导致另一个事务的修改丢失。
举个例子:
表中有一条数据,ID=1 Salary=1000
事务A:读取ID=1的Salary,此时读取到的值为1000
事务B:读取ID=1的Salary,此时读取到的值为1000
事务A:修改ID=1的Salary=Salary-200
事务B:修改ID=1的Salary=Salary-300
假如事务A先修改完,事务B的修改也提交了,那么此时Salary=700,而对事务A来说,Salary=800的修改丢失了。
3.1、不可重复读和幻读有什么区别?
简单来说,不可重复读是针对同一条数据的,而幻读是针对一组数据的。
- 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
- 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。
4、数据库隔离级别有哪几种?
- 未提交读(READ UNCOMMITTED):在此隔离级别中,事务中的修改,即使没有提交,其他事务也都是可见的。事务可以读取未提交的数据,此时就会产生脏读。性能上,READ UNCOMMITTED级别不会比其他级别的好太多,但却缺乏其他级别的好处。因此除非是有很必要的理由,不然实际项目中尽量不要使用。
- 提交读(READ COMMITTED):该级别是大多数数据库系统默认的级别,但是MySQL却不是。READ COMMITTED是一个事务开始时,只能“看见”已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读,因为两次执行同样的查询,可能会得到不一样的结果。
- 可重复读(REPEATABLE READ):可重复读是 MySQL 的默认事务隔离级别。REPEATABLE READ解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,可重复读隔离级别还是无法解决幻读的问题。
- 可串行化(SERIALIZABLE):SERIALIZABLE是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。简单来说,SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁竞争的问题。实际应用中很少用到这个隔离级别,只有在非常需要确保数据一致性且可以接受没有并发的情况下,才考虑使用他。
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
未提交读 | √ | √ | √ | × |
提交读 | × | √ | √ | × |
可重复读 | × | × | √ | × |
可串行化 | × | × | × | √ |
MySQL的隔离级别是通过MVCC和加锁共同实现的。其中SERIALIZABLE是通过加锁来实现的,而READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。REPEATABLE-READ在当前读的情况下,需要加锁来保证不会出现幻读。
5、MySQL默认使用隔离级别是啥?
MySQL默认使用的是可重复读(REPEATABLE READ),可以通过SQL查询:
-- mysql 8.0以前的版本
SELECT @@tx_isolation
-- mysql 8.0之后的版本
SELECT @@transaction_isolation
结果:
6、如何控制并发事务?
MySQL是通过加锁和MVCC两种方式来控制并发事务的。加锁是悲观模式,而MVCC可以看成是乐观 锁模式。而MySQL的大多数事务性存储实现都不是简单的行级锁。基于提升并发性能的考虑,一般同时实现了多版本控制MVCC。
MVCC可以看成是行级锁的变种,但是在很多情况下避免了加锁操作,因此开销相比加锁更低。
6.1、锁
MySQL中锁分为共享锁(S锁)和排他锁(X锁)。其中主要是通过读写锁来实现并发事务控制。
- 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
- 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。
读写锁可以做到读读并行,但是无法做到写读、写写并行。另外,根据根据锁粒度的不同,又被分为 表级锁(table-level locking) 和 行级锁(row-level locking) 。InnoDB 不光支持表级锁,还支持行级锁,默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类。
6.2、MVCC
MySQL中的MVCC则是通过保存数据在某个时间点的快照来实现的。不管SQL执行需要的时间多长,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。当然每个存储引擎实现的MVCC是不一样的,下面以InnoDB引擎来详细展开。
InnoDB的MVCC是通过在每行的记录后面保存两个隐藏的列来实现,其中一个列保存了行的创建时间,一个保存了行的过期时间(或)删除时间。当然存储的并不是实际的时间值,而是系统版本号。每开始一个新的事务,版本号都会自动递增,事务开始时的版本号会作为事务的版本号,用于和查询到的每行记录的版本号进行比较。
正是因为保存了这两个额外的系统版本号,使得大多数的都操作都不用加锁,性能也得到了提高,但是缺点就是每行记录都需要额外的存储空间,以及版本号的维护工作。
注:MVCC只在可重复读和提交读两个隔离级别下工作,其他的隔离级别和MVCC不兼容。因为未提交读总是读取最新的数据行,而不是符合当前事务版本的数据行,而串行化本身就会对所有的操作进行加锁。