事务介绍
事务概述
MySQL 是一个服务器/客户端架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称之为一个会话( Session )。我们可以同时在不同的会话里输入各种语句,这些语句可以作为事务的一部分进行处理。不同的会话可以同时发送请求,也就是说服务器可能同时在处理多个事务,这样子就会导致不同的事务可能同时访问到相同的记录。我们前边说过事务有一个特性称之为 隔离性 ,理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样子的话对性能影响太大,所以设计数据库的大叔提出了各种 隔离级别 ,来最大限度的提升系统并发处理事务的能力,但是这也是以牺牲一定的隔离性来达到的
事务的四大特性
-
原子性(atomicity) :事务最小工作单元,要么全成功,要么全失败
-
一致性(consistency): 事务开始和结束后,数据库的完整性不会被破坏
-
隔离性(isolation) :不同事务之间互不影响,四种隔离级别为RU(读未提交)、RC(读已提交)、RR(可重复读)、SERIALIZABLE (串行化)
-
持久性(durability) :事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失
隔离级别
-
未提交读(READ UNCOMMITTED/RU)
如果一个事务读到了另一个未提交事务修改过的数据,那么这种 隔离级别 就称之为 未提交读 (英文名:READ UNCOMMITTED )
脏读:一个事务读取到另一个事务未提交的数据
脏读 违背了现实世界的业务含义,所以这种 READ UNCOMMITTED 算是十分不安全的一种 隔离级别
-
已提交读(READ COMMITTED/RC)
如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那么这种 隔离级别 就称之为 已提交读 (英文名: READ COMMITTED )
不可重复读:一个事务因读取到另一个事务已提交的update。导致对同一条记录读取两次以上的结果不一致
-
可重复读(REPEATABLE READ/RR)
一个事务只能读到另一个已经提交的事务修改过的数据,但是第一次读过某条记录后,即使其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。那么这种 隔离级别 就称之为 可重复读 (英文名: REPEATABLE READ )
幻读:一个事务因读取到另一个事务已提交的insert数据。导致对同一张表读取两次以上的结果不一致
-
串行化(SERIALIZABLE)
以上3种隔离级别都允许对同一条记录进行 读-读 、 读-写 、 写-读 的并发操作,如果我们不允许读-写 、 写-读 的并发操作,可以使用 SERIALIZABLE 隔离级别
事务和MVCC底层原理
MVCC,多版本的并发控制,英文全称:Multi Version Concurrency Control
MVCC使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提高了数据库的并发处理能力。借助MVCC,数据库可以实现READ COMMITTED,REPEATABLE READ等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本,保证了ACID中的I特性(隔离性)。
InnoDB的MVCC实现
多版本并发控制仅仅是一种技术概念,并没有统一的实现标准, 其核心理念就是数据快照,不同的事务访问不同版本的数据快照,从而实现不同的事务隔离级别。虽然字面上是说具有多个版本的数据快照,但这并不意味着数据库必须拷贝数据,保存多份数据文件,这样会浪费大量的存储空间。InnoDB通过事务的undo日志巧妙地实现了多版本的数据快照
MVCC 在mysql 中的实现依赖的是 undo log 与 read view
nnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的事务ID,一个保存了行的回滚指针。每开始一个新的事务,都会自动递增产生一个新的事务id。事务开始时刻的会把事务id放到当前事务影响的行事务id中,当查询时需要用当前事务id和每行记录的事务id进行比较
MVCC只在REPEATABLE READ和READ COMMITIED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容 ,因为READ UNCOMMITIED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁
undo log
根据行为的不同,undo log分为两种: insert undo log 和 update undo log
-
insert undo log
是在 insert 操作中产生的 undo log
因为 insert 操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以 insert undo log 可以在事务提交后直接删除而不需要进行 purge 操作
-
update undo log
是 update 或 delete 操作中产生的 undo log
因为会对已经存在的记录产生影响,为了提供 MVCC机制,因此 update undo log 不能在事务提交时就进行删除,而是将事务提交时放到入 history list 上,等待 purge 线程进行最后的删除操作
为了更好的支持并发,InnoDB的多版本一致性读是采用了基于回滚段的的方式。另外,对于更新和删除操作,InnoDB并不是真正的删除原来的记录,而是设置记录的delete mark为1。因此为了解决数据Page和Undo Log膨胀的问题,需要引入purge机制进行回收
为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo log的并发写入和持久化。回滚段实际上是一种 Undo 文件组织方式
ReadView
对于使用 READ UNCOMMITTED 隔离级别的事务来说,直接读取记录的最新版本就好了。对于使用SERIALIZABLE 隔离级别的事务来说,使用加锁的方式来访问记录。对于使用 READ COMMITTED 和REPEATABLE READ 隔离级别的事务来说,就需要用到我们上边所说的 版本链 了。核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的。所以设计 InnoDB 的设计者提出了一个ReadView的概念,这个 ReadView 中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids,并确定三个变量的值
-
m_up_limit_id:m_ids事务列表中的最小事务id,如果当前列表为空那么就等于m_low_limit_id。事务id的下限
-
m_low_limit_id:系统中将要产生的下一个事务id的值。事务id的上限
-
m_creator_trx_id:当前事务id,m_ids中不包含当前事务id
这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本(版本链中的版本)是否可见
-
如果被访问版本的 trx_id 属性值小于 m_up_limit_id ,表明生成该版本的事务在生成 ReadView前已经提交,所以该版本可以被当前事务访问
-
如果被访问版本的 trx_id 属性值等于 m_creator_trx_id 既当前事务id,可以被访问
-
如果被访问版本的 trx_id 属性值大于等于 m_low_limit_id ,在生成 ReadView 后才生成,所以该版本不可以被当前事务访问
-
如果被访问版本的 trx_id 属性值在 m_up_limit_id 和 m_low_limit_id 之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中
-
如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问
-
如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问
-
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录
在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同
-
READ COMMITTED
每次读取数据前都生成一个ReadView
-
REPEATABLE READ
在事务开始后第一次读取数据时生成一个ReadView
事务回滚和数据恢复
事务进行过程中,每次DML sql语句执行,都会记录undo log和redo log,然后更新数据形成脏页,然后redo log按照时间或者空间等条件进行落盘,undo log和脏页按照checkpoint进行落盘,落盘后相应的redo log就可以删除了。此时,事务还未COMMIT,如果发生崩溃,则首先检查checkpoint记录,使用相应的redo log进行数据和undo log的恢复,然后查看undo log的状态发现事务尚未提交,然后就使用undo log进行事务回滚。事务执行COMMIT操作时,会将本事务相关的所有redo log都进行落盘,只有所有redo log落盘成功,才算COMMIT成功。然后内存中的数据脏页继续按照checkpoint进行落盘。 如果此时发生了崩溃,则只使用redo log恢复数据