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