MySQL 事务详解

1. 事务的四大特性:

特性说明实现原理
原子性(Atomicity事务包含的所有操作要么全部成功,要么全部失败回滚;成功必须要完全应用到数据库,失败则不能对数据库产生影响redo log 和 undo log,事务提交时,所有操作日志持久化成功;事务回滚时,undo log 逻辑日志,能将所有执行成功的语句进行逆向操作,从而使数据回滚
一致性(Consistency一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态
隔离性(Isolation隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。快照读是MVCC实现,当前读是锁实现。
持久性(Durability是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作redo log(物理日志,数据页上的0 1),必须将该事务的所有日志都写入到日志文件中进行持久化,待事务的 COMMIT 操作完成才算完成

  其中原子性、一致性、持久性是通过数据库的 redo log 和 undo log 来完成的,隔离性是通过 MVCC 和 锁来实现的。

2. undo log 和 redo log

  • undo log:回滚日志。
  • redo log:重做日志。

  redo 和 undo 的作用都可以视为一种恢复操作,redo 恢复提交事务修改的页操作,而 undo 回滚行记录到某个特定版本。因此两者记录的内容不同,redo 是物理日志,记录的是页的物理修改操作(某个数据页做了什么修改)。undo 是逻辑日志,根据每行记录进行记录

2.1 redo

  重做日志用来是实现事务的持久性,即是事务 ACID 中的 D。其由两部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),其是持久的

  InnoDB 是事务的存储引擎,其通过 Force log at Commit 机制实现事务的持久性,即当事务提交(commit)时,必须先将该事务的所有日志都写入到事务日志文件中进行持久化,待事务的 COMMIT 操作完成才算完成。这里的日志是指,redo log 和 undo log。redo log 用来保证事务的持久性,undo log 用来帮助事务回滚及 MVCC 功能。

  为了确保每次日志都能写入日志文件,在每次将 log buffer 中的日志写入日志文件的过程中都需要调用一次 fsync 操作(fsync:系统调用,因为日志缓冲是在用户内存,中间还要经过操作系统内核空间的os buffer,调用fsync() 的作用就是将OS buffer中的日志刷到磁盘上的log file中。)。由于 fsync 的效率取决于磁盘的性能,因此磁盘性能决定了事务提交的性能,也就是数据库的增、删、改操作的性能。

在这里插入图片描述

  InnoDB 存储引擎允许用户手动设置非持久化的情况,以此提高数据库的性能。即当事务提交时,日志缓冲的日志不写入日志文件,而是等待一个时间周期后再执行 fsync 操作。显然这可以减少 fsync 操作,提高数据库性能。但是当数据库发送宕机时,由于部分日志未刷新到磁盘,因此会丢失最后一段时间的事务。

  参数 innodb_flush_log_at_trx_commit 用来控制日志缓冲刷新到磁盘的策略。该参数的默认值为 1,表示事务提交必须调用一次 fsync 操作。0 表示事务提交时不进行写入日志文件,这个操作仅在 master thread 中完成,而在 master therad 中每 1 秒会进行一次日志的 fsync 操作。2 表示事务提交时将日志写入日志文件,但仅写入到 os buffer,还没持久化到磁盘上。

2.2 undo

  重做日志记录了事务的行为,可以很好地通过其对页进行 ”重做“ 操作。但是事务有时还需要进行回滚操作,这时就需要 undo。因此对数据库进行修改时,InnoDB 存储引擎不但会产生 redo,还会产生一定量的 undo。这样如果用户执行的事务或语句由于某种原因失败了,又或者用户用一条 ROLLBACK 语句请求回滚,就可以利用这些 undo 日志将数据回滚到修改之前的样子。

  undo long 是逻辑日志,因此至少将数据库逻辑的恢复到原来的样子。所有修改都被逻辑的取消了,但是数据库和页本身在回滚之后可能大不相同。比如:一个事务在修改当前一个页中某几条记录,同时还有其它事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响到其它事务正在进行的工作。

  当 InnoDB 存储引擎回滚时,可以认为它实际做的于先前相反的工作。对于每一个 INSERT,InnoDB 存储引擎会完成一个 DELETE;对于每一个 DELETE,INNODB 存储引擎会执行一个 INSERT;对于每个 UPDATE,INNODB 存储引擎会执行一个相反的 UPDATE。

  除了回滚操作,undo 的另外一个作用是 MVCC,即在 InnoDB 存储引擎中的 MVCC 是通过 undo 来完成的。当用户快照读取一行记录时,若该记录已经被其实事务修改为新的版本,当前事务可以通过 undo 来读取之前的旧版本行记录。

undo log主要分为两种:

  • insert undo log
    代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  • update undo log
    事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

2.3 binlog

额外提一个重要的日志 binlog

  binlog 是 MySQL Server 层记录的二进制日志文件,数据库服务器启动的那一刻起,它记录了所有的DDL和DML语句。

常见的使用场景

  • 数据恢复:误删数据之后可以通过mysqlbinlog工具恢复数据
  • 主从复制:主库将binlog传给从库,从库接收到之后读取内容写入从库,实现主库和从库数据一致性
2.3.4 主从复制

这里我们延申讲下mysql主从复制

  MySQL主从复制工作过程:

  1. 在主库上,把事务更新数据的操作记录写到binlog日志文件
  2. 从库将主库的binlog日志复制到自己的中继日志
  3. 从库读取中继日志的事件,将其重放到从库数据中基本原理流程

  该流程主要会涉及 3个线程

  • dump 线程(主):在 slave 与其正常连接的情况下,将 binlog 发送到 slave 上
  • io线程(从):通过读取 master 节点的binlog日志名称以及偏移量信息将其拷贝到本地relay log日志文件
  • sql执行线程(从):读取 relay log,并顺序执行该日志中的 SQL 事件,从而与主数据库中的数据保持一致

  主从复制方式有哪些?

  • 全同步复制:主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响

  • 半同步复制:应用发送事务请求,在主库执行后写入 binlog,主库 master 把 binlog 日志推送给从库 salve1 和 slave2 ,半同步主库需要等待其中任意一个从库更新数据到 relay log 成功并且告知主库,主库才提交事务。
    在这里插入图片描述

  • 异步复制:应用发送事务请求,master 经过执行之后写入 binlog.master 把 binlog 日志推送给从库 slave1 和 slave2,主库不需要等到从库是否成功更新数据到 relay log,主库直接提交事务即可。
    在这里插入图片描述

  • MGR 组复制:DB1 、DB2 、DB3 构成的 MGR 集群, 集群中每个 DB 都有 MGR 层,MGR 层功能也可简单理解为由 Paxos 模块和冲突检测 Certify 模块实现(SQL线程扩展为多线程,所有需要进行冲突检测)。
    在这里插入图片描述
      应用发起事务请求,在 commit 提交之前,MGR会拦截请求,通过 Paxos 模块广播给 MGR 集群各个节点,半数以上的节点同意并且达成共识,之后共识信息进入各个节点的冲突检测 certify 模块,各个节点各自进行冲突检测验证,在冲突检测通过之后,本地事务在 DB1 直接提交即可,否则直接回滚。
      为什么要冲突检测?不同服务器上并发执行的事务之间可能存在冲突

3. 事务并发时带来的问题

事务并发问题说明
脏读事务A 快照读取了 事务B 更新但是未提交的数据
不可重复读事务A 多次快照读取同一数据,事务B 在事务A 多次读取过程中对数据做了更新并提交,导致事务A 多次读取的数据不一致
幻读指在同一个事务中,前后两个相同的查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。RR隔离级别下的幻读主要发生在快照读和当前读不一致(根本原因:当前读和快照读实现隔离级别机制不一样),举个例子:事务1 开启事务 select count(1) from table,读取到了2条,然后事务2,开启事务 执行insert语句插入1条,再提交。然后事务1 执行delete from table 发现删除了3条,这里就出现幻读,事务1的快照读和当前读数据不一致

4. MySQL事务隔离级别

MySQL事务隔离级别脏读不可重复读幻读
读未提交(read-uncommitted)
读已提交(read-committed)
可重复读(repeatable-read)
串行化(serializable)

5. 事务四大隔离级别的实现

5.1 当前读事务隔离级别的实现-锁

  当前读(current read):读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁。也就是当前读的事务隔离级别实现是靠锁来完成的

  特殊的读操作,插入、更新、删除操作,都属于当前读,需要加锁。如下所示:

select * from table where ? lock in share mode;   /** S锁(共享锁) */

select * from table where ? for update; 		/** X锁(排他锁) */	

insert into table values (…);					/** X锁(排他锁) */	

update table set ? where ?;						/** X锁(排他锁) */	

delete from table where ?;						/** X锁(排他锁) */

  本人写过 mysql锁的原理:具体可以看这篇文章:mysql锁机制

5.2 快照读事务隔离级别的实现-MVCC

MVCC:多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。

5.2.1 隐式字段

数据库每行记录除了我们自定义的字段外,还有数据库隐式定义的字段,比如:DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID,DELETE_FLAG

  • DB_TRX_ID:最近修改(修改/插入)事务ID,记录创建这条记录/最后一次修改该记录的事务ID
  • DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
  • DB_ROW_ID:隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
  • DELETE_MASK:删除标记

在这里插入图片描述

5.2.2 undo log

多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,旧版本数据存放在 undo log中,并形成一条版本链。
对于 DML 操作来说

  • INSERT:创建一条数据,DB_TRX_ID 的值为当前事务 id, DB_ROLL_PTR为 null 。
  • UPDATE:复制一行数据,将当前复制后这一行的 DB_TRX_ID 置为当前事务的 id,DB_ROLL_PTR是一个指针,指向复制前的那一条的,复制前的旧数据在 undo log 里面。
  • DELETE:复制一行数据,将当前复制后这一行的 DB_TRX_ID 置为当前事务的 id,DB_ROLL_PTR 是一个指针,指向复制前的那一条的。并把 DELETE_MASK置为 true 。
  1. 有个事务1插入user表插入了一条新记录,记录如下,name为janne, age为24岁,隐式主键是1,事务ID = 1
    在这里插入图片描述
  2. 现在来了一个事务2对该记录的name做出了修改,改为Tom
  • 在事务2修改该行(记录)数据时,数据库会先对该行加排他锁
  • 然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本
  • 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务2 的ID,回滚指针指向拷贝到undo log的副本记录
  • 事务提交后,释放锁
    在这里插入图片描述
  1. 又来了个事务3修改user表的同一个记录,将age修改为30岁
    在这里插入图片描述
    同一条记录在系统中可以存在多个版本,从而形成 undo log 版本链,这个就是数据库的多版本并发控制。
5.2.3 Read View(读视图)
  1. Read View是什么呢?:是事务进行快照读操作的时候生产的读视图(Read View)。在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

  2. Read View有什么用呢?:它主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据

  3. Read View是如何保证可见性判断的呢?我们先看看Read view 的几个重要属性

    • m_ids:当前系统中那些活跃(未提交)的读写事务ID, 它数据结构为一个List
    • min_limit_id:表示在生成Read View时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值
    • max_limit_id:表示生成Read View时,系统中应该分配给下一个事务的id值
    • creator_trx_id: 创建当前Read View的事务ID

Read view 匹配条件规则如下:

  1. 如果行数据事务ID trx_id < min_limit_id,表明生成该版本的事务在生成Read View前,已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。

  2. 如果trx_id>= max_limit_id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。

  3. 如果 min_limit_id =<trx_id< max_limit_id,需要分3种情况讨论

    • 如果m_ids包含trx_id,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id等于creator_trx_id的话,表明数据是自己生成的,因此是可见的。
    • 如果m_ids包含trx_id,并且trx_id不等于creator_trx_id,则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的;
    • 如果m_ids不包含trx_id,则说明你这个事务在Read View生成之前就已经提交了,修改的结果,当前事务是能看见的。
5.2.4 不同隔离级别下,Read View的工作方式不同

实际上,各种事务隔离级别下的Read view工作方式,是不一样的,RR可以解决不可重复读问题,就是跟Read view工作方式有关。

隔离级别MVCC
读未提交未使用
读已提交当前读受 mvcc 控制。事务内的每个快照读语句都会重新创建 Read View
可重复读当前读受 mvcc 控制。事务内开始时创建Read View , 在事务结束这段时间内 每一次查询都不会重新重建Read View
序列化未使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值