作为数据库的重要组成部分,是时候来讨论一下事务了。所谓事务一般是指数据库的一组操作的集合。这组操作要么都成功执行,要么都不执行。不管是成功还是失败(事务执行过程宕机,导致事务未提交),数据总是保持正确的关系。通常在事务提交前,其他事务或操作对当前事务内对数据的修改是无感知的。一旦事务提交,事务对数据的修改将永久保存到数据库中。不过值得一提的是这里的永久并不是100%永久保存下来,因为如果数据库是在单机中,单机自身如果出现问题导致数据丢失,数据也会丢失,这和事务的数据永久保存不是同一层意思。以上就是事务所谓:原子性,一致性,隔离性以及持久性的理论。
刚提到通常其他操作或事务对当前事务是无感知的,这主要是因为事务的隔离性细分出了不同的隔离级别。出现不同的隔离级别主要是基于性能与数据完整的考虑,具体我们来讨论下不同级别的隔离:
未提交读(read uncommit):当前事务一旦对数据库执行了操作,其他事务和操作会马上感知到数据的更新,如果当前事务因为某些原因没有提交或是执行了回滚,其他操作或事务就会读取到很多奇怪的数据,这些奇怪的数据我们称为脏数据。另外,如果在正是因为脏数据的存在,这个隔离级别仅限于理论阶段。
提交读(read commit):为了解决uncommit带来的问题,于是提出了commit的解决方案,就是当前事务提交后,其他事务和操作才能感知到当前事务绑定数据的变化。这个是大多数数据库默认的级别(MYSQL除外),该级别有时也被称为不可重复读(nonrepeatable read)。所谓不可重复读是指在A事务运行期间,如果B事务往A事务操作的数据范围更新了数据,在B事务提交后,A事务马上就能感知到B事务的修改,导致同一个事务A中前后读取的相同范围的数据不一致。
通过操作我们发现,该级别下,事务A前后两次的相同条件的读取,结果却不同,这时候事务A就有点搞不清该条或该范围的数据到底正不正确了,事务A就像出现了幻觉,这种现象被称为幻读,而图二是幻读中的另外一种具体情景幻行
可重复读(repeatable read):为了解决提交读带来的事务多次读取记录不一致的问题,进一步提出了可重复读的概念。通过对行加锁锁定行数据,保证多次读取相同的记录都是一致的。因此从理论上说,可重复读并有完全解决幻读的问题。也就是该级别解决了图一的问题,而未解决图二的问题。但是我们将隔离级别调整为repeatable-read级别,按照上述步骤重新执行,发现似乎已经解决了幻行的问题。懵圈了?实际上这不是我们的理解错误,而是MySQL Innodb通过多版本并发控制(MVCC)已经解决了相关的问题,因此我们在实践中没有出现相应的问题,另外该级别也是MySQL默认的隔离级别。至于什么是多版本并发控制呢?鉴于具体的实现是多种多样的,因此此处仅讨论Innodb中的多版本并发控制。Innodb引擎中系统会在每条记录的后边都会加上隐藏的两列信息:一个行创建时间,一个行过期(删除)时间。这两个时间本质就是版本号。每当开启一个新事物,该版本号就自动递增,该版本就被用来控制数据的curd操作。具体操作过程:
①在创建表的过程中就自动添加两个隐藏字段(这里通过为test_mvcc表手动添加字段来模拟):
当我们开启事务后,系统先自动生成版本号1,再插入数据:
事务中的查询:
此时事务内部无论怎么查询,都只能查询到当前事务版本之前的数据行。现在如果要对数据进行更新,那么:
删除过程就和更新过程中的删除过程相似了,也是将相关记录的删除字段更新为当前版本号:
可串行化(serializable):为了解决重复读的幻读中的换行问题(图二的问题),就提出了串行化执行事务的解决方案,就是每读取一条数据都给锁定,宣布这条数据我先用了,你们不准来瞎掺和。但是在实践中发现,该级别太过度了,数据是完整了,但是性能受到灾难性的影响,因此生产过程中,并不常见这种级别。毕竟有了上面的mvcc的方案。
在上面的过程中我们反复提到一个字——锁。本质上锁就是一种解决并发的方案。食堂通过打饭师傅控制打饭流程,MySQL通过锁控制资源访问顺序。访问时用读锁,数据库变更用写锁,查询不会引起数据的变化,因此读锁就像问师傅饭做好了没,大家可以一起问,因此也被称为共享锁,而写入数据就像排队打饭,会导致饭的总量发生变化,必须排队,因此被称为排他锁。就像食堂师傅总是先完成打饭的任务,才能回答你还有没有饭的问题,写锁的等级总是要高于读锁,写锁总是会插入到读锁的前面执行。我们知道食堂的通常是两个窗口打同一锅菜,好巧不巧,刚好只剩一份土豆丝和回锅肉,你选了酸辣土豆丝+回锅肉,另一个窗口的同学选了回锅肉+土豆丝,你打了土豆丝,另一个同学打了回锅肉加一半的土豆丝,此时就尬住了,你打了同学的菜,同学打了你的菜,两个人都差菜。数据库也有类似时候,我们称这种情况为死锁。对于死锁,Innodb选择将持有最少行级排它锁的事务进行回滚,也就是说因为你只打了一点土豆丝,食堂师傅就和你商量说,你把土豆丝让出来给另一个同学,食堂师傅在给你另外现炒。
事务日志是用来加速事务执行速度的一种方式。具体就是是在在执行过程中,修改内存中的对应数据,将本次数据的具体修改行为持久化追加到事务日志中,内存中被修改的数据再慢慢刷新到磁盘中持久化。如果系统崩溃了,内存中的数据还没写入磁盘,存储引擎在重启时将根据事务日志进行修复这部分数据。