MySQL事务
一、什么是数据库事务?
事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。
事务是逻辑上的一组操作,要么都执行,要么都不执行。
一个经典的例子:银行转账
A给B转账500元,那么A的账户就要减少500元,B的账户就要增加500,这两个操作是要么都执行成功,要么都执行失败,不能够存在A的账户减少了而B的账户没增加这样的情况。
二、事务的四大特性(ACID原则)
MySQL的事务要遵循ACID原则(好像关系型数据库都要遵循ACID原则),其基本内容如下:
- 原子性(Atomicity):事务是数据库的逻辑工作单位,事务中包括的诸操作要么都做,要么都不做;
- 一致性(Consistency):事务的执行结果必须从一个一致性状态变到另一个一致性状态;
- 隔离性(Isolation):一个事务的执行不能其他事务干扰,即一个事务的内部操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能互相干扰;
- 持久性(Durability):一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的,接下来的其他操作或故障不应该对其执行结果有任何影响。
这里对一致性进一步解释一下,拿银行转账的例子来说,A向B转账500元,这就是一个事务,包括两个操作:A账户减少500元和B账户增加500元,这两操作全做和全部做,数据库都处于一致性状态,但是如果只做了一个操作而另一个操作没有做,则逻辑上就会发生错误,这时数据库就是处于一个不一致的状态。
三、脏读、不可重复读、幻读
脏读:事务A对某个数据进行了更新,此时事务B读取了这个数据,接着由于某些原因事务A进行了回滚操作,导致事务B所读取的数据并不是正确的数据;
不可重复读:事务A先查询了一次数据,此时事务B对数据进行了更新,那么事务A再次查询的时候就发向两次查询的结果是不一致的(简单来说就是一个事务两次查询的结果内容不一致);
幻读:事务A先查询了一次数据,此时事务B又插入了新的数据,那事务A再次查询的时候就会发现两次查询的数据条数是不一致的(简单来说就是一个事务两次查询的结果条数不一致);
幻读和不可重复读不同的地方在于前者是一个事务两次查询的结果条数不一致,后者是一个事务两次查询的结果内容是不一致的。
四、MySQL的事务隔离级别
MySQL定义了四种事务隔离级别来解决脏读、幻读、不可重复读这三个问题。
- READ-UNCOMMITTED(读未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
每种隔离级别可能导致的问题如下:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED(读未提交) | √ | √ | √ |
READ-COMMITTED(读已提交) | × | √ | √ |
REPEATABLE-READ(可重复读) | × | × | √ |
SERIALIZABLE(可串行化) | × | × | × |
MySQL默认的事务隔离级别是REPEATABLE-READ(可重复读RR)。事实上其他很多关系型数据库默认的事务隔离级别是READ-COMMITTED(读已提交RC),比如Oracle数据库默认的事务隔离级别就是RC,那为什么MySQL要选择RR呢?
首先肯定是不会使用READ-UNCOMMITTED(读已提交)和SERIALIZABLE(可串行化),原因很简单,读未提交导致一个事务能够读到另一个事务未提交的数据,这在逻辑上是说不过去的;可串行化导致每次读写操作都会加锁,降低并发度,使得性能不高,一般都不会使用。
那在RC和RR之间,MySQL选择了RR,原因是与MySQL的日志文件binlog和主从复制有关。
binlog的三种格式:
- statement格式,每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。
- row格式,不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。
- mixed格式,一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。
在mysql5.1之前,binlog采用的是statement格式,当事务提交的时候就会将修改数据的sql记录到binlog中。
如果出现下面的情况(事务隔离级别是RC的情况下):
-- session1
begin; -- 开启事务
delete from t where id < 5; -- 删除id<5的数据,在事务提交之前打开另一个会话窗口
-- session2
begin; -- 开启事务
insert into t(id) value(4); -- 增加一条id为4的数据
commit; -- 提交事务,先提交session2的事务,再提交session1的事务
-- session1
commit; -- 提交session2的事务后提交session1的事务
上述操作执行后再查询t表的时候,结果是存在id为4的数据,也就是先执行了session1的删除事务,再执行了session2的插入事务。
但是由于session2的事务是先提交的,就会导致并binlog中先写入seesion2的插入操作,再写入session1的删除操作。
那么就出现这样的情况,主机Master中是先删除后插入,而从机Slave在基于binlog进行复制的时候就出现先插入后删除的情况,导致主从不一致的问题出现。
解决这样的情况有两种方法:
- 将隔离级别设置为可重复读,在这个隔离级别下引入了间隙锁,当session1执行delete语句时会锁住间隙,那么session2执行insert语句时就会被阻塞住;
- 将binlog的格式设置为row,row格式下binlog记录的是行记录的修改,不存在sql语句执行顺序的问题,所以就不会出现主从不一致的情况。
(对于什么是间隙锁,我也不清楚,后续讲到锁的时候在去了解)
参考文章:
https://thinkwon.blog.csdn.net/article/details/104778621
https://blog.csdn.net/qq_43255017/article/details/106442887
https://blog.csdn.net/zhangyingjie09/article/details/103484181