什么是事务
数据库事务是mysql执行操作的最小逻辑单位,一个事务可以包含一个或者多个sql语句,这些sql要么都执行成功要么都执行失败。并发操作下,事务的控制尤为重要。
事务的特点(ACID)
原子性、一致性、隔离性、持久性
原子性(Atomicity):意思是事务中的所有操作作为一个整体,要么全部成功,要么全部失败
原子性的底层原理(如何实现):undo log日志(回滚日志);
原子性能实现的关键是在失败的时候能够发生回滚。而回滚就依赖于 undo log 日志。事务在更改数据之前会将要更改的数据备份到undo log中(undo log会保存更改前的数据,是一个行级别的历史数据),如果发生了错误或者用户执行了rollback,就可以通过undo log将数据恢复到事务开始之前的状态。
Undo log怎么做到回滚的呢?你可以理解为:
当你在事务中delete一条记录时,undo log会记录一条对应的insert记录。
当你在事务中insert一条记录的时候,undo log会记录一条对应的delete记录
当你在事务中update一条记录时,它记录一条相反的update记录。
当rollback回滚的时候,就会执行这些相反的操作。
当然了,undo log里面并不会真的记录这些一条条的sql命令,而是存着行数据的内容。
Undo log是逻辑日志,之后说的redo log是物理日志。逻辑日志的存储格式是以段的格式存储数据,物理日志的存储格式是以页的格式存储(mysql的一个页有16K,一个页可以有多个行)。
Undo日志保存在了data目录下的ibdata文件中:
Undo log除了能够实现原子性之外,还能用于实现多版本并发控制(MVCC)
PS:undo, redo这两个日志只有使用innodb这个存储引擎的时候才会用到。Binlog日志属于服务层的一个东西,所有类型的存储引擎都可以使用。
隔离性(isolation):意思是并发执行的事务不会相互影响,并发执行的操作的结果和他们串行执行时的结果一样。
隔离性的底层原理(如何实现):是通过锁来实现的。锁会再后面的章节中详述,这里先不提,只需要直到隔离性是通过锁来实现的即可。
持久性(Durability):事务一旦提交,对数据库的更新是持久的。任何的事务或者故障都不会导致数据丢失。
持久性的底层原理:通过redo log 日志(前滚日志),redo日志记录着事务中,更改后的数据。存储的格式为页。
如果事务commit之后,mysql正在将数据写入数据表的时候,mysql或者整个服务器发生宕机,此时myslq的表中还是一个旧的数据。但是下次重启的时候,mysql会根据读取redo log文件的内容,将数据恢复成新的状态。但是如果redo log也没来得及持久化,那这部分的数据就真的丢失了。不过redo log的日志数据从缓存写入redo日志文件会比数据写入数据表要快。因为redo日志文件的内容很少,它只保存事务中修改的数据,所以io写入redo日志会很快,而数据表文件内容很大,io写入表文件会比较慢。
当然了,如果没有发生任何故障,mysql不宕机什么的,数据成功写入到数据表文件,那么redo日志中保存这的这些数据以后也就用不到了,这个时候mysql会有一套机制将redo日志文件的内容删除。所以redo文件保存的日志是临时的,仅用于故障时防止数据表数据丢失,不会一直存在,否则redo文件会越来越大,而且没有意义。
一句话:数据表的数据没来得及持久化问题不大,只要redo log文件记录的数据在宕机前持久化了就OK。
一致性(Consistency):事务的执行结果必须是数据库从一个一致性状态到另一个一致性状态。比如转账前后两个账户的金额总和应该不变。
上述四个特点中,一致性是事务的最终目的。只要其他三个特性都满足了,那么一致性自然而然也就会满足,也就是说原子性,隔离性和持久性是因,一致性是果。
==========================================================
Redo 和 Undo 日志(前滚日志和回滚日志)
下面我们看看一个事务里面,undo log 和redo log到底是怎么记录的。首先要知道一个概念:我们说的undo日志和redo日志不仅仅是他们在磁盘中的日志文件,还包括他们在缓冲区buffer中的日志,要知道记录undo日志和redo日志的时候不可能直接写入到磁盘,而是要先写入到内存,再从内存fsync到磁盘。
假设有一个innodb表,我对其中的两行数据A,B(只有一个字段f,f的值分别是1,2)进行修改。
start transation
Update A set f=10 where id=1;# 此时会先把id为1的记录从磁盘(数据表文件)读到mysql进程的内存进行修改f字段。在修改前先将 f=1 这个旧数据写入到用户空间的undo日志缓冲区(undo log buffer); 修改后会将 f=10 这个新数据写入到用户空间的redo 日志缓冲区(redo log buffer); 同时也会将f=10写入到数据缓冲区(data buffer)中
Update B set f=20 where id=1;# 同上操作。
Commit;# 此时,旧数据和新数据会从用户空间的缓冲区(就是mysql这个进程的内存)拷贝到内核空间的缓冲区(OS buffer)。并执行系统调用 fsync(),将 f=1 这个老数据从内核态的内存写入到 undo log 磁盘文件(刷盘),将 f=10 这个新数据也从内核态的内存写入到redo log磁盘文件中。最后将 f=10 从数据缓冲区写入到数据表的磁盘中。
Commit的时候发生的事情,我借鉴了网上的一张图:
当执行commit的时候,mysql进程会将之前写在用户态内存(log buffer日志缓冲区)的新旧数据拷贝到内核态的内核缓冲区(os buffer),然后执行fsync()这个系统调用,把数据刷盘(从内核态的内存写入到redo和undo磁盘文件中)。注意,fsync是一个异步的io操作,无需cpu的参与。
假如,commit之前,因为一些错误发生回滚,此时会用用户态的undo buffer中的旧数据执行一些反操作(比如执行的是insert操作,回滚的时候就是一个delete操作),把数据缓冲区中的内容恢复为旧数据。
这就是undo日志在回滚是的作用,回滚的过程中是用的undo缓冲区中的日志,而不是undo磁盘文件中的日志,因为这个时候undo日志还没有写入到磁盘中。
Undo日志是事务中旧数据(f=1)的备份,redo日志是事务中新数据(f=10)的备份。
下面还有张图可以帮你理解事务执行时,redo和undo日志以及数据的刷盘过程
强调一下:在事务中执行一条非查询的sql,redo/undo日志会写入到用户态缓冲区但还没写入到内核缓冲区;只有commit的时候才会写入到内核缓冲区和刷盘。(磁盘或者内存中的)redo和undo日志记录的不是具体的sql命令,而是数据本身。
============================================
Commit的时候,将 日志buffer 写入到 日志文件这个过程有3中方式,我们可以通过修改变量 innodb_flush_log_at_trx_commit 的值来决定,该变量有3种值:0、1、2,默认为1。
0:mysql会设置一个定时任务,每隔(大约)一秒将用户态的log bufer中已有的数据(包括undo和redo的日志数据)拷贝到内核 os buffer,并马上调用fsync()刷到磁盘。在这一秒之内,可能还有其他的客户端执行了事务把redo和undo日志写到了log buffer,此时就可以将他们的日志一起刷到磁盘中。
1:当执行commit时,mysql会马上把日志数据从log buffer写入os buffer并且马上刷盘。
2:当执行commit时,mysql会马上把日志数据从log buffer写入os buffer但不会马上刷盘。同时mysql会设置一个定时任务,每隔一秒调用fsync()把数据从内核刷到磁盘。
如果1秒还没到,但是buffer中的数据已经占了buffer被分配内存的一半以上时,系统还是会把数据从buffer刷到磁盘
从效率上来说,0和2是比较高的,因为它刷盘的频率没那么高,io次数比较少。但是当mysql宕掉或者这个机器宕掉的时候会丢失1s的数据。1的安全级别最高,但是效率比较低,会增加磁盘压力。
级别0和级别2相比的区别在于,在数据从内存同步到磁盘的过程中,如果mysql进程宕掉,级别0可能会丢失1秒的数据(因为数据在mysql进程用户态的内存中),但是级别2不会丢失数据(因为数据此时再内核的内存中,已经和mysql进程脱钩了)。如果是整个机器宕掉了级别2才会丢数据。
接下来我们说说redo log的刷盘具体过程:
Redo log 文件的存放位置在data目录中,默认有两个redo log日志文件。
而且每个redo log文件的最大容量是固定的,数据超过了这个容量就会清空这个日志文件。是不是很好奇为什么会有两个?
在innodb将log buffer中的redo log block刷到这些log file中时,会以追加写入的方式轮询写入。即先在第一个log file(即ib_logfile0)的尾部追加写,直到满了之后向第二个log file(即ib_logfile1)写。当第二个log file满了会清空一部分第一个log file继续写入。
这里有一个注意点:
我们大多数人都认为像下面这样把多个sql语句打包执行和提交才是一个事务。
Start transaction;
update xxx
select xxx
delete xxx
Commit
其实不对,我们平时执行一个单独的sql语句其实也是一个事务。例如:
Insert into t values (xxxxx);
这就是一个事务,只不过mysql帮我们自动commit了。
所以其实执行这样的单句sql,也是一个事务也会记录到undo和redo中
PS:rollback和commit都会结束事务,都会释放事务中产生的锁。如执行了rollback和commit后想再使用事务,请重新执行begin。