1、数据库事务
1.1 什么是事务
数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。
1.2 事务隔离级别
Serializable(串行化执行,SQLite默认级别):最高隔离性级别。同时执行的两个事务完全隔离,每个事务有独立的运行空间,避免了脏读、不可重复读和幻读等问题,但是会影响并发性能。
Repeatable Read(可重读,MySQL默认级别):通过快照,保证在一个事务内多次读取同一数据时,能够得到相同的结果。这种隔离级别可以避免脏读和不可重复读问题,但是可能会出现幻读问题。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)以及gap锁(间隙锁)机制解决了幻读问题。
Read Committed(Oracle、PostgreSQL、SQLServer默认支持的级别):允许一个事务只能读取已经提交的数据。这种隔离级别可以避免脏读问题,但是可能会出现不可重复读和幻读问题。
Read uncommitted:最低的隔离级别,允许一个事务读取另一个事务未提交的数据。这种隔离级别会导致脏读、不可重复读和幻读等问题。
幻读: 幻读(Phantom Read)是指一个事务执行了两次相同的查询,但是得到的结果集却不同。例如,事务A在某个范围内查询了数据,然后事务B在该范围内插入了新的数据,此时事务A再次查询该范围内的数据,发现结果集中出现了之前不存在的数据,这就是幻读。
幻读与脏读不同之处在于,幻读是读取了其他事务插入的数据,而脏读是读取了其他事务未提交的数据。幻读可能会导致系统出现不一致的情况,因此在实际应用中应尽量避免幻读问题。可以通过设置适当的事务隔离级别来避免幻读问题。
脏读: 脏读(Dirty Read)是指一个事务读取了另一个事务未提交的数据。例如,事务A修改了某些数据但是还没有提交,此时事务B读取了这些未提交的数据,后来事务A因为某种原因回滚了,那么事务B读取的数据就是不正确的。这种情况下,事务B读取到的数据是脏数据,称为脏读。
脏读可能会导致系统出现不一致的情况,因此在实际应用中应尽量避免脏读。可以通过设置适当的事务隔离级别来避免脏读问题。
不可重复读: 不可重复读是指在同一个事务中,多次读取同一数据,但是每次读取结果不一致的现象。这种现象通常发生在读已提交的隔离级别中。
举个例子,假设一个事务在读取某个数据之后,另一个事务对该数据进行了修改并提交。如果第一个事务再次读取该数据,则会得到不同的结果,这就是不可重复读问题。
为了避免不可重复读问题,可以使用可重复读或串行化的隔离级别。在可重复读隔离级别中,事务在读取数据时会创建一个快照,并在整个事务期间使用该快照,因此可以避免不可重复读。而在串行化隔离级别中,所有的事务都是串行执行的,因此也可以避免不可重复读问题。
注意:mysql的innodb在RR级别下,通过mvcc以及GAP锁解决了幻读问题,但其他引擎如myisam则依然存在幻读。
————————————————
1.3 事务日志
包括:redo log、undo log、bin log
重做日志redo:在Innodb存储引擎中,事务日志是通过redo和innodb的存储引擎日志缓冲(Innodb log buffer)来实现的,当开始一个事务的时候,会记录该事务的lsn(log sequence number)号; 当事务执行时,会往InnoDB存储引擎的日志的日志缓存里面插入事务日志;当事务提交时,必须将存储引擎的日志缓冲写入磁盘(通过innodb_flush_log_at_trx_commit来控制),也就是写数据前,需要先写日志。这种方式称为“预写日志方式”,innodb通过此方式来保证事务的完整性。也就意味着磁盘上存储的数据页和内存缓冲池上面的页是不同步的,是先写入redo log,然后写入data file,因此是一种异步的方式。通过 show engine innodb status\G 来观察之间的差距。记录文件是ib_logfile0 ib_logfile1。
那么思考下为什么要有redo log,而不是直接写入数据文件?原因很简单,在机械硬盘的年代,顺序IO的性能要比随机IO的性能高上不止10倍!!!而redo log的记录就是顺序追加到日志文件的,速度非常快,而加入要直接写入到数据文件的话,每条数据所属的页,在磁盘上的位置都可能是分散不连续的,要找到某个页并写入该笔数据,需要通过随机IO的方式,如果每笔数据的写入都是直接随机io写入的话,那么数据库的性能就会受到极大的影响!!
回滚日志undo:用于事务的回滚恢复,undo的记录正好与redo的相反,insert变成delete,update变成相反的update,redo放在redo file里面。而undo放在一个内部的一个特殊segment上面,存储与共享表空间内(ibdata1或者ibdata2中)。
在回滚段中的undo logs分为: insert undo log 和 update undo log
insert undo log : 事务对insert新记录时产生的undolog, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
update undo log : 事务对记录进行delete和update操作时产生的undo log, 不仅在事务回滚时需要, 一致性读也需要,所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。
事务日志恢复事务:一般情况下,mysql在崩溃之后,重启服务,innodb通过回滚日志undo将所有已完成并写入磁盘的未完成事务进行rollback,然后redo中的事务全部重新执行一遍即可恢复数据,但是随着redo的量增加,每次从redo的第一条开始恢复就会浪费长的时间,所以引入了checkpoint机制(如果在某个时间点,脏页的数据被刷新到了磁盘,系统就把这个刷新的时间点记录到redo log的结尾位置,在进行恢复数据的时候,checkpoint时间点之前的数据就不需要进行恢复了,可以缩短时间)
mysql 二进制日志:
MySQL的二进制日志可以说是MySQL最重要的日志了,它记录了所有的DDL和DML(除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间,主要是在主从复制的场景下开启二进制日志,那么二进制日志跟事务又有什么关系啊?事实上,前面说的事物完成标记是redo log持久化,指的是在没有开启二进制日志的情况下的,假如mysql开启了二进制日志,那么事务的完成标识就不再是redo log持久化了,而是该语句在二进制日志的持久化(异步复制的模式下,同步复制的时候,事务完成标志是slaver同步成功后)。mysql内部为了保证redo log与binlog之间的一致性,使用了XA协议,也就是当开启了binlog之后,mysql事务的提交变成了两阶段了(prepare&commit),XA协议是就是一个两阶段提交分布式事务的协议,事实上也可以认为mysql内部也是通过了分布式事务去保证了redo log和binlog的数据一致性了。
1.4 事务的完成标识
事务的完成标识要分以下几种情况:
1.单数据库实例,没有开启binlog
这种场景下,事务的完成标识是redo log持久化成功。也就是当我们提交事务的时候,redo log 会被flush到磁盘上,持久化完成则返回事务完成。
2.单数据库实例,但开启了binlog
这种场景下,与上面第一种不同的是,事务提交之后,需要redo log 和 binlog持久化完成(利用了XA协议)才算是一个完整的事务完成。
3.数据库主从架构下的事务
在这种主从架构下(一主一从或者一主多从)的情况下,首先,binlog是肯定需要开启的,其次,由主从复制的机制来决定事务的完成时机。
a.异步复制模式: 该模式下,事务的完成标识与上面的第二点原理一样,redo log和binlog 持久化,事务完成。
b.全同步复制模式: 这种复制模式对事务的影响较大,它需要保证所有从节点的io线程都返回同步完成才返回完整的事务完成,对性能影响较大
c.半同步复制模式: 跟上面的全同步类似,都是需要复制的io线程返回同步完成才返回事务完成,但是差别在于它并不需要所有从节点都必须同步完成,而是只需要其中某个从节点同步完成返回即可。但是尽管如此,一个事务的完成需要等待io线程同步到从库并返回,过程多了这个io等待,影响事务的耗时,所以并不是说半同步复制就是最好的,如果是一主一从的架构下,其实等同于全同步复制。
选用哪种复制模式,决定着对事务时效影响程度,具体还是得根据实际业务场景而定,并发一概而论。
思考一个问题,事务的耗时越大,对业务有什么影响?
可以这么想:假如这个大事务对很多资源加了锁,但很久都没完成事务,也就没办法释放锁,那么影响什么不言自明,所以在一个事务中,对某个资源的加锁操作,应该尽量放到靠近commit的地方。
2.1 MVCC介绍
MVCC:多版本控制协议,InnoDB是基于undolog和快照实现的,在RR可重复读隔离级别下,在开启事务之后的第一条select操作时, 会创建一个快照(read view), 将当前系统中活跃的事务记录起来;
在RC读已提交隔离级别下, (事务中)每条select语句都会创建一个快照(read view);
是所有数据的快照,不是当前select出的数据快照
最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。
来自《高性能MySQL》中对MVCC的部分介绍:
MySQL的大多数事务型存储引擎实现的其实都不是简单的行级锁。基于提升并发性能的考虑, 它们一般都同时实现了多版本并发控制(MVCC)。不仅是MySQL, 包括Oracle,PostgreSQL等其他数据库系统也都实现了MVCC, 但各自的实现机制不尽相同, 因为MVCC没有一个统一的实现标准。
可以认为MVCC是行级锁的一个变种, 但是它在很多情况下避免了加锁操作, 因此开销更低。虽然实现机制有所不同, 但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
MVCC的实现方式有多种, 典型的有乐观(optimistic)并发控制 和 悲观(pessimistic)并发控制。
MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。