什么是事务
事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
事务的四大特性(ACID)
- 原子性(Atomicity)
事务是最小的执行单位,不允许分割。事务的原子性确保事务开始后,所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。
- 一致性(Consistency)
执行事务前后,数据保持一致,数据库的完整性约束没有被破坏。比如A向B转账,不可能A扣了钱,B却没收到。
- 隔离性(Isolation)
并发访问数据库时,一个事务不被其他事务所干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
- 持久性(Durability)
事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
并发事务带来的问题
- 脏读(Dirty read):当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
- 丢失修改(Lost to modify):指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
- 不可重复读(Unrepeatable read):指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
- 幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复度和幻读区别:
不可重复读的重点是修改,幻读的重点在于新增或者删除。
例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。
例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。
事务隔离级别
SQL定义了四个隔离级别:
- READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
只有SERIALIZABLE才能防止丢失修改。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED(读取未提交) | √ | √ | √ |
READ-COMMITTED(读取已提交) | × | √ | √ |
REPEATABLE-READ(可重复读) | × | × | √ |
SERIALIZABLE(可串行化) | × | × | × |
大多数数据库默认的事务隔离级别是 Read Committed,比如 SQL Server , Oracle。但 MySQL 的默认隔离级别是 Repeatable Read。
与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。
因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)并不会有任何性能损失。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。
下面的内容都是以Innodb为例。
数据库中的锁
一次封锁or两段锁
- 一次封锁
因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。
- 两段锁
数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)
- 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
- 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。
这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)的。
MySQL中锁的种类
MySQL中锁的种类很多,有常见的表锁和行锁,还有页锁、间隙锁,也有新加入的Metadata Lock等等。
- 表锁是对一整张表加锁,虽然可分为读锁和写锁,但毕竟是锁住整张表,会导致并发能力下降,一般是做ddl处理时使用。
DDL数据定义语言;DML数据操作语言
- 行锁则是锁住数据行,这种加锁方法比较复杂,但是由于只锁住有限的数据,对于其它数据不加限制,所以并发能力强,MySQL一般都是用行锁来处理并发事务。这里主要讨论的也就是行锁。
隔离级别实现原理
读取未提交
Read Uncommitted这种级别,数据库一般都不会用,任何操作都不会加锁。
读取已提交
在RC级别中,数据的读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。
下面以表 class_teacher 为例:
+----+--------------+------------+
| id | class_name | teacher_id |
+----+--------------+------------+
| 1 | 初三一班 | 1 |
| 3 | 初二一班 | 2 |
| 4 | 初二二班 | 2 |
+----+--------------+------------+
由于MySQL的InnoDB默认是使用的RR级别,所以我们先要将该session开启成RC级别,并且设置binlog的模式:
SET session transaction isolation level read committed;
SET SESSION binlog_format = 'ROW';(或者是MIXED)
事务A | 事务B |
---|---|
begin; | begin; |
update class_teacher set class_name=‘初三二班’ where teacher_id=1; | update class_teacher set class_name=‘初三三班’ where teacher_id=1; |
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction | |
commit; |
为了防止并发过程中的修改冲突,事务A中MySQL给teacher_id=1的数据行加锁,并一直不commit(释放锁),那么事务B也就一直拿不到该行锁,wait直到超时。
这时我们要注意到,teacher_id是有索引的,如果是没有索引的class_name呢?
update class_teacher set teacher_id=3 where class_name = '初三一班';
那么MySQL会给整张表的所有数据行的加行锁。这里听起来有点不可思议,但是当sql运行的过程中,MySQL并不知道哪些数据行是 class_name = '初三一班’的(没有索引嘛),如果一个条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由MySQL Server层进行过滤。
但在实际使用过程当中,MySQL做了一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录释放锁 (违背了二段锁协议的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见即使是MySQL,为了效率也是会违反规范的。
这种情况同样适用于MySQL的默认隔离级别RR。所以对一个数据量很大的表做批量修改的时候,如果无法使用相应的索引,MySQL Server过滤数据的的时候特别慢,就会出现虽然没有修改某些行的数据,但是它们还是被锁住了的现象。
可重复读
这是MySQL中InnoDB默认的隔离级别。分“读”和“写”两个模块来讲解。
读
RC(不可重读)模式:
事务A | 事务B | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
begin; | begin; | |||||||||
select id,class_name,teacher_id from class_teacher where teacher_id=1;
| ||||||||||
update class_teacher set class_name='初三三班' where id=1; | ||||||||||
commit; | ||||||||||
select id,class_name,teacher_id from class_teacher where teacher_id=1;
读到了事务B修改的数据,和第一次查询的结果不一样,是不可重读的。 | ||||||||||
commit; |
事务B修改id=1的数据提交之后,事务A同样的查询,后一次和前一次的结果不一样,这就是不可重读(重新读取产生的结果不一样)。这就很可能带来一些问题,那么我们来看看在RR级别中MySQL的表现:
事务A | 事务B | 事务C | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
begin; | begin; | begin; | |||||||||
select id,class_name,teacher_id from class_teacher where teacher_id=1;
| |||||||||||
update class_teacher set class_name='初三三班' where id=1; commit; | |||||||||||
insert into class_teacher values (null,'初三三班',1);commit; | |||||||||||
select id,class_name,teacher_id from class_teacher where teacher_id=1;
没有读到事务B修改的数据,和第一次sql读取的一样,是可重复读的。 没有读到事务C新添加的数据。 | |||||||||||
commit; |
我们注意到,当teacher_id=1时,事务A先做了一次读取,事务B中间修改了id=1的数据,并commit之后,事务A第二次读到的数据和第一次完全相同。所以说它是可重读的。MySQL是通过MVCC
学下MVCC先
串行化
这个级别很简单,读加共享锁,写加排他锁,读写互斥。使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差。如果你的业务并发的特别少或者没有并发,同时又要求数据及时可靠的话,可以使用这种模式。
不要看到select就说不会加锁了,在Serializable这个级别,select还是会加锁的!
参考链接:Innodb中的事务隔离级别和锁的关系
参考链接:Innodb中的事务隔离级别实现原理
参考链接:MySQL的四种事务隔离级别
参考链接:事务隔离级别