讲解事务之前先来看一个例子,以转账为例:
小明现在有10000块钱,小红向小明借1000块,小明给小红转了1000块,现在问题是小红没有收到钱,但是小明扣了1000块,剩余9000块,那问题出现在哪里呢?
有以下两种情况:
要解决这个问题,就要保证转账过程中所有数据库的操作要么全部执行成功 ,要么全部失败,中间不能出现异常等问题。
为了防止以上有可能出现的情况,MySQL引入了事务(Transaction),事务就是针对数据库的操作,可以由一条或者多条SQL语句组成,事务具备同步的特点,如果执行语句过程中发生异常,那么在操作事务期间对数据库做的所有操作会回滚(Rollback)到开始执行前的状态。
InnoDB引擎支持事务操作,而MyISAM引擎不支持事务操作,为了保证数据的安全性,MySQL的引擎都是用InnoDB来操作事务。
接下来看一下实现事务必须坚持的4大特性:
原子性指一个事务是一个不可分割的工作单位,一个事务中的所有操作,要么全部成功,要么全部失败。如果事务中一个SQL语句执行失败,则已经执行完的语句也必须回滚,数据库回滚到事务执行前的状态。
一致性指事务执行结束后,数据库的完整性约束没有被破坏,即事务执行前和执行后都必须处于一致性状态,比如转账:小明和小红两个人的钱加起来是500,无论小明和小红之间怎么转账,事务结束后两人的金额加起来还是500,这就是事务的一致性。
隔离性是指多个用户并发访问数据库,操作同一张表时,数据库为每个用户开启事务,并发执行的各个事务之间相互隔离,不能互相干扰,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
事务一旦被提交了,那么对数据库中数据的修改就是永久的,即便是在数据库系统遇到故障也不会丢失。
那么InnoDB引擎是如何保证这4个特性的呢?
聊聊MySQL里面的undo Log、redo Log和bin Log日志的原子性和持久性已经复制和恢复数据实现过程
在没有事务隔离的时候,多个事务在同一时刻对同一数据的操作可能会影响到我们所期望的结果。可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题。
接下来我们以图讲解,看一下这些问题是怎么发生的。
在一个事务处理过程里读取了另外一个未提交的事务中的数据,这种现象称为脏读。
事务A从数据库中读取小明的余额,如何执行修改小明余额的操作,此时事务A还没有提交事务,而此时事务B也从数据库中读取小明的余额,事务B读取到的余额是事务A修改金额后还没有提交的事务,因为事务A事务没有提交,可能会发生回滚操作,如果事务A发生回滚,那么事务B读取到的数据就是脏数据。
在同一事务中,多次读取同一数据,前后两次读到的数据不一致,这种现象称为不可重复读。
事务A从数据库中读取小明的余额,然后继续执行其他的逻辑,此时事务B更新了小明的余额,并且提交了事务,当事务A再次读取小明的余额时,发现前后两次读取到的数据不一致,这时发生了不可重复读。
同一事务中,用同样的操作读取两次,得到的记录数不相同 ,这种现象称为幻读。
事务A从数据库中查询余额大于500的记录数,发现一共有5条,此时事务B也按照相同的条件查询出5条记录数,这个时候事务A新增了一条大于500的数据,并且提交了事务,此时数据库中大于500余额的记录数变成了6条,如何事务B再次查询余额大于500的记录数,此时查询到的数据是6条,前后读取到记录数不一致,就好像产生幻觉一样,这种现象称为幻读。
上面我们讲解了并发时事务产生的问题:
SQL标准定义了四种隔离级别来规避这些问题,隔离级别越高,性能效率就越低,如下:
一个事务可以读取另一个未提交事务的数据。
一个事务要等另一个事务提交后才能读取数据。
一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。InnoDB引擎的默认隔离级别。
这个会对操作的数据加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突时,后面访问的事务必须等前一个事务执行完成,才能继续执行,串行化是事务最高的隔离级别,但是这种事务隔离级别效率低下,比较耗数据库性能,我们一般不使用。
在MySQL中,实现了这四种隔离级别,可能会产生以下问题:
从上面我们可以知道,要解决脏读问题,需要使用读提交以上的隔离级别,要解决不可重复读问题,需要使用到可重复读的隔离级别。
最后要解决幻读问题是不建议将隔离级别升级到串行化(serializable ),串行化级别则是悲观的认为幻读时刻都会发生,故会自动的隐式的对事务所需资源加排它锁,其他事务访问此资源会被阻塞等待,虽然事务是安全的,但是这样会导致数据库在并发事务操作时性能非常差。
解决幻读可以使用next-key lock 锁 包含(记录锁(行锁)、间隙锁),记录锁是加在索引上的锁,间隙锁是加在索引之间的,将当前数据行与上一条数据和下一条数据之间的间隙锁定,防止其他事务在这个记录之间新增新的数据,保证此范围内读取的数据是一致的,这样就避免了幻读现象。
有个问题就是:需要判断一下版本链中的哪个版本是当前事务可见的?
InnoDB中提出了一个ReadView的概念,而读提交(read committed)和可重复读(repeatable read)隔离级别是通过Read View来实现的,而它们之间一个非常大的区别就是它们创建ReadView的时机不同:
我们来看一下大概结构图:
这个ReadView中主要包含4个比较重要的内容:
注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4
只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0
以上了解了Read View 的字段,还需要了解InnoDB引擎中聚族索引记录中的两个隐藏列,如下图所示:
了解完以上讲解的内容之后,接下来讲解可重复读和读提交隔离级别是如何实现的。
如果事务A、事务B和事务C差不多同一时刻启动,那这三个事务创建的Read View 如下图所示:
下面举个例子,现在有一个事务A,它的事务 id 为20,向表中新插入了一条为小明的用户金额1000的数据,此时对应的undo Log如下图所示:
由于是新插入的数据,所以这行数据是第一个版本,也就是它没有上一个数据版本,所以roll_pointer 为 null。
这时事务B将这行数据的金额修改为2000,因为事务B的id为21,所以此时的trx_id为21,同样也会记录一条undo Log,这条undo Log的roll_pointer指针会指向上一个数据版本的undo Log,也就是指向事务A写入的那一行 undo Log,如下图所示:
紧接着事务C将这行数据的金额修改为3000,因为事务C的id为22,所以此时的trx_id为22,如下图所示:
从上面我们可知,只要事务修改了数据,那么就会记录一条对应的undo Log,一条undo Log对应这行数据的一个版本,当这行数据有多个版本时,就会有多条undo Log日志,于是最新记录和旧版本记录通过roll_pointer指针连接,这样就形成了一个 undo Log版本链。
讲解了新增、修改操作后,接下来讲解事务读取数据时是怎么实现的。
下面举个例子来解释一下ReadView机制下,数据的读取规则。首先我们假设有一条数据,它的 trx_id=15,roll_pointer 为 null,那么此时undo Log版本链如下图所示:
如果此时事务A(trx_id=20)去读取数据,找到记录后,会先看这条记录的trx_id,发现trx_id = 15,通过和事务A的Read View中m_ids字段比较,发现该记录的事务id并不在活跃事务的列表中,并且小于事务A的事务id,意味着这条记录的事务早在事务A之前就提交过了,因此事务A可以读取到这条记录的值。
此时事务B将这行数据修改了,将小明的金额改成2000,那么就会记录一条对应的undo Log,并以链表的方式串联起来,形成版本链,如下图:
如果这时事务A再次去读取数据,发现这条记录的trx_id为21,比自己的事务id还要大,并且比下一个事务的id小,这表示事务A读取的是和自己同一时刻启动的事务B修改的数据,这时事务A并不会读取这条记录,而是沿着undo Log的版本链向前找,接着会找到该行数据的上一个版本,直到找到trx_id等于或者小于事务A的事务id的第一条记录,所以事务A再一次读取到trx_id为15的记录。
可重复读隔离级别事务读取某条数据时,就会按照如下规则来决定当前事务能读取到什么数据:
这种通过记录的版本链来控制并发事务访问同一个记录时的行为,就叫做MVCC(多版本并发控制)
读提交隔离级别是在每次查询都会生成一个新的Read View,这就可能导致事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
下面举个例子,来解释一下ReadView机制下,数据的读取规则
假设现在有事务A和事务B并发执行,事务 A 的事务id为 20,事务B的事务id为30。
如果此时事务 A(creator_trx_id=20)去读取数据,那么在undo Log版本链中,数据最新版本的trx_id为15,发现这个值小于事务A的ReadView里creator_trx_id的值,这表示这个数据的版本是事务A开启之前,其他事务提交的,因此事务A可以读取到记录数据。
接着事务B(creator_trx_id=30)去修改数据,将数据金额修改为2000 ,但是事务B还未提交。虽然不提交事务,但是仍然会记录一条undo Log,因此这条数据的undo Log的版本链就有两条记录了,新的这条undo Log的roll_pointer指针会指向前一条undo Log。如下图所示:
接着事务A(creator_trx_id=20)去读取数据,那么在undo Log版本链中,数据最新版本的事务trx_id为 30,这时事务A在找到小明这条金额为2000元的记录时,看到这条记录的trx_id,发现比事务A的Read View中的creator_trx_id还要大,而且还在m_ids列表中,这表示这个版本的数据是和自己同一时刻启动的事务B修改的,因此这个版本的数据,事务A读取不到,所以需要沿着undo Log的版本链向前找,接着会找到该行数据的上一个版本,也就是trx_id = 15的记录,由于这条记录的trx_id小于事务A,因此事务A能读取到该版本的值。
当事务B修改数据并且提交事务后,那么此时事务A再去读取数据,它能读取到什么值呢?
这时事务A(creator_trx_id=20)去读取数据,找到小明对应金额2000元的记录,会看这条记录的 trx_id,发现和事务A的Read View中creator_trx_id还要大,而且不在m_ids列表里,说明该记录的 trx_id的事务已经提交过了,于是事务A就可以读取这条记录,这就是读已提交机制。
本文主要讲解了事务并发执行的时候,可能会导致脏读、不可重复读、幻读等这些问题,而为了避免这些问题的出现,SQL标准提出了四种隔离级别规避这种问题:
解决脏读问题,需要使用读提交以上的隔离级别。
解决不可重复读问题,需要使用到可重复读的隔离级别。
解决幻读问题是不建议将隔离级别升级到串行化(serializable ),串行化级别则是悲观的认为幻读时刻都会发生,故会自动的隐式的对事务所需资源加排它锁,其他事务访问此资源会被阻塞等待,虽然事务是安全的,但是这样会导致数据库在并发事务操作时性能非常差。
解决幻读可以使用next-key lock 锁 包含(记录锁(行锁)、间隙锁),记录锁是加在索引上的锁,间隙锁是加在索引之间的,将当前数据行与上一条数据和下一条数据之间的间隙锁定,防止其他事务在这个记录之间新增新的数据,保证此范围内读取的数据是一致的,这样就避免了幻读现象。
读提交(read committed)和可重复读(repeatable read)隔离级别是通过Read View来实现的,而它们之间一个非常大的区别就是它们创建ReadView的时机不同: