事务是数据库中一个非常重要的特性,它保证了我们数据的正确性,既然是重要的技术,那么我们就有必要去探究其中原理。
什么是事务
事务能把数据库中的数据由一种【一致状态】转换成另一种【一致状态】。
事务由一条或一组sql语句组成,一个事务代表一个程序执行单元,对数据的修改要么一起成功,要么一起失败。
InnoDB中的事务具有以下4个特性:原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)
原子性,保证一个事务就是一个原子操作,要么都做,要么都不做。如在银行取款有以下流程输入取款金额
从银行数据库中更新金额信息
ATM出款
整个取款过程是一个原子操作,如果银行卡扣减了金额,但ATM未出款,那么用户是无法接受的。
一致性,将数据库从一种一致状态转成另一种一致状态,并且数据库的完整性约束没有破坏。例如表中的姓名字段不能重复,是唯一性约束,但是一个事务操作之后,姓名变得不唯一了,这就破坏了事务的一致性要求,将数据库由一种一致状态转变成一种不一致的状态,此时系统会自动撤销该事务的修改——返回到初始状态。
隔离性,即事务之间的操作的相互分离、互不干扰的。例如原来有条记录的姓名字段为【小明】,事务A修改为【小红】后,还没提交事务,事务B就去读取这条记录。因为事务的隔离性,所以事务B读取的还是【小明】,等事务A提交以后,事务B再次读取该行记录,得到的就是【小红】。
持久性,事务一旦提交成功,其结果就是永久性的,即使发生宕机等故障,数据库也能正确恢复数据。
事务的分类扁平事务(Flat Transactions)
带有保存点的扁平事务(FT with Savepoints)
链事务(Chained Transactions)
嵌套事务(Nested Transactions)
分布式事务(Distributed Transactions)
扁平事务(FT)是最简单且最常用的一种,它由begin work开始,由commit work或rollback work结束,期间的操作是原子的。扁平事务的主要限制是无法提交或回滚某部分的事务,所以就出现了带有保存点的扁平事务。
带有保存点的扁平事务(FTS)允许在一个事务操作中设置多个保存点,当出现回滚时,可以指定回滚到哪个保存点的状态
链事务(CT)可当作是保存点模式的一个变种,当发生系统崩溃时,带有保存点的扁平事务中的所有保存点都会消失,那么恢复时事务还是从初始状态开始恢复,而不是从最近的保存点开始。
链事务的思想是:在提交一个事务时,释放不需要的数据对象,并将必要的数据传给下一个要开始的事务(提交事务和开始下一个事务是一个原子操作)。
嵌套事务(NT)是一个层次结构,由一个顶层事务控制着各个子事务
分布式事务(DT)是在分布式环境下运行的扁平事务,一般需要根据数据所在位置访问网络中的不同节点
事务的使用
默认情况下,事务都是自动提交的(隐式提交),即执行sql语句后立马就会执行commit操作。可以通过以下命令查看事务是否开启自动提交
show variables like 'autocommit';
我们也可以通过以下命令开启或关闭自动提交
#关闭自动提交
set global autocommit=0
#开启
set global autocommit=1
一般情况下,我们都需要使用begin等命令显示的开启一个事务,一个事务的完整执行过程如下
#开启事务
begin;
或
start transaction;
#自定义的sql语句
insert....
update....
#提交事务
commit;
#回滚事务
rollback;
事务的实现
事务的特性ACID那么牛逼,到底是怎么实现的呢?
原子性、一致性、持久性是通过数据库的redo log(重做日志)和undo log(回滚日志)来完成的,而隔离性是通过锁机制来完成的,这会在下篇文章锁机制中详解。
1、redo
重做日志redo log用来实现事务的持久性,它通常是物理日志,记录的是页的物理修改操作。它有两部分组成:一是内存中的重做日志缓冲(redo log buffer),二是重做日志文件(redo log file)。
当事务commit时,必须先将该事务的所有日志写入到重做日志文件中进行持久化才能提交成功。redo log基本上都是顺序写的,所以省去了读取操作,比undo log的随机读取效率要高一点。
在将重做日志缓冲redo log buffer写入到重做日志文件redo log file时,InnoDB都要调用一次fsync操作,该同步操作是将内存中的缓存写入到磁盘中的文件,所以其效率取决于磁盘的写能力,这也决定了数据库的性能。
我们可以通过参数innodb_flush_log_at_trx_commit来设置同步redo log的策略,默认为1,代表每次事务提交都要调用fsync同步一次。还可以设置为0和2,0代表提交事务不写入重做日志,2代表提交事务后,将重做日志缓冲写入到文件系统的缓冲中,不执行fsync操作,这种效率比默认的相对较高,但是若文件系统发生宕机,事务的重做日志缓冲就会丢失。
重做日志文件是以块的方式存储的,称之为重做日志块(redo log block)。每个块的大小是512字节,由于和磁盘扇区大小一样,因此重做日志的写入可以保证原子性,不需要两次写技术(doublewrite)。每个块由块头(12字节)、块尾(8字节)和实际日志(492字节)组成
InnoDB在启动时,不管数据库上次是否正常关闭,都会尝试恢复数据。因为redo log是物理日志,因此恢复的速度比逻辑日志快很多。
2、undo
回滚日志undo log用于保证事务的一致性,它是逻辑日志,通过每行数据进行记录。undo是存放在数据库内部的一个特殊段(segment),称之为undo段,位于共享表空间ibdata中。
大家可能会误解为通过undo log可以将数据库物理的恢复到初始状态,就是将数据页恢复到原来的状态。但是undo是逻辑日志,它只能将数据库在逻辑上恢复到原来的数据状态,回滚后的数据页结构可能大不相同。
为什么呢?在数据库中可能有成百上千个事务,若事务A修改了一个页中的几条记录,此时事务B修改了该页中的另外几条记录,如果事务A发生错误进行回滚,直接将页修改为初始状态,那么就影响了事务B的操作了,所以这是不成立的。
事务的隔离级别
首先要了解几个并发会出现的问题脏读:读到了无效的数据
幻读:同一个事务内的几次查询出现了不同的结果(重点在insert)
不可重复读:同一个事务内的几次查询出现了不同的结果(重点在update和delete)
事务与事务之间并非一定是独立的,innodb提供了四种不同的事务隔离级别
读未提交:事务A新增了数据,但是还没有commit,此时事务B就能读到这些数据。若是事务A发生异常而回滚,那么事务B就读到了不存在的数据,即脏读。读未提交的性能是最好的,它没有锁带来的开销,但同时它的安全性是最低的,连脏读都无法解决。
读已提交:事务A修改了数据且未提交,此时事务B是无法读到该数据的,当事务A提交完毕后,事务B又读了一次,发现数据已改变,这解决了脏读的问题,但是事务B在一次事务内,两次读取的结果不一样,这就出现了不可重复读。可见读已提交解决了脏读的问题,但是会出现幻读和不可重复读的问题。
可重复读:保证一次事务内所读的数据都是相同的。事务B查询了一条记录【小明】,此时事务A修改该记录为【小红】。事务B再次查询,发现还是【小明】,证明两次所读一样,此时事务B新增一条记录【小丽】然后commit。事务A修改为【小红】后去查询记录发现多了一条记录【小丽】,这是事务B新增的,所以事务A出现了幻读。
串行化:串行化是将一个个事务变成顺序执行,相当于单线程。它解决了脏读、幻读、可重复读的问题,隔离效果最好,但是性能最差。
查看数据库的隔离级别可以使用以下命名
show variables like 'tx_isolation'
设置隔离级别使用以下命令
#设置全局事务隔离级别为读已提交
set global transaction isolation level read committed;