MySQL事务

12 阅读10分钟

事务是什么

事务通俗一点,将所有执行的指令看做一个整体,要么全部成功,要么全部失败。在一个事务中执行这三条sql语句,当三条sql语句中某一个出现错误,就会将所有的数据回滚到执行sql语句前的状态。

事务特性

原子性

是执行的最小单位,要么全部成功,要么全部失败。

一致性

事务执行前后要保证数据的一致性。列如银行转账 A,B两个账户,A账户要向B账户转账1000元,那数据的变化是A账户的存款要减去1000元,而B账户的存款要增加1000元。

隔离性

隔离性是什么

一个事务执行不能去干扰其他事务的执行,即使一个事务操作的数据对并发执行的事务都是隔离的,并发执行的事务是不能被干扰的。

问题
  • 脏读

    是一个事务读取到另外一个事务没有提交的数据。

  • 幻读

    事务A对某条数据进行删除,但是事务B读取到这条删除的数据。

  • 不可重复读

    在一个事务中对数据进行两次读取,但是获取到的结果不一样。

四种隔离级别
  • 读未提交 (Read Uncommitted)

    允许事务读取未被其他事务提交的数据,会出现脏读,幻读,不可复重复。

  • 读已提交 (Read Committed)

    事务只能读取到其他事务已提交的数据,会出现不可重复度,幻读。

  • 可重复读 (Repeatable Read)

    确保事务对某个字段进行多这次读取,在这个期间其他事务不能对该字段进行更新,删除操作。会出现幻读。

  • 串行化 (Serializable)

    确保事务对某一行数据进行多次读取,在这个期间其他事务不对该行进行更新,删除操作。解决问题。

持久性

事务完成提交后,数据将会永久保存。

事务的使用

  • 准备

    CREATE TABLE t_account( idint(10) NOT NULL AUTO_INCREMENT, namevarchar(255) DEFAULT NULL, money int(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    INSERT INTO test_ecs.t_account(id, name, money) VALUES (1, '张三', 1000); INSERT INTO test_ecs.t_account(id, name, money) VALUES (2, '李四', 1000);

  • 基本的指令

    开启事务 start transaction

    提交 commit

    回滚 rollbark

  • 演示

    • 目标

      张三向李四进行转账100,转账出现问题,数据要回滚之前的状态

    • 没有开启事务

      image.png

      从图中我们可看出我们的目标没有完成,因为sql执行错误没有进行回滚。

    • 开启事务

      image.png

      从图中可以看到sql执行错误数据进行回滚。

事务实现

实现的组件

redolog
redolog 简介

Redo Log(重做日志)是InnoDB存储引擎用来确保事务的ACID特性中的持久性(Durability)。它记录了可能对数据页(在内存中的数据)进行修改的所有操作。即使数据库发生故障,使用Redo Log也可以保证数据不会丢失。

Redo Log的工作原理

1. 写入Redo Log Buffer

  • 当事务对某个数据页进行修改时,首先修改内存中的数据页,同时将这次修改操作记录到Redo Log Buffer中。

2. 刷新到磁盘

  • 事务提交时,或者Redo Log Buffer满了时,会将Redo Log Buffer的内容刷新到磁盘上的Redo Log文件中。这个过程通常称为“同步(flush)”。

3. 保证持久性

  • 在MySQL宕机、掉电等情况下,已经提交的事务不会丢失,因为其修改已记录在Redo Log中。数据库重启时,可以通过Redo Log进行数据页的恢复工作。
undolog 回滚日志
undo log 简介

是Innodb存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和MVCC

记录update,insert,delete操作方便数据回滚。在数据更新之前,就会将更新前的数据存储在undolog中,方便后续回滚,主要的作用原子性,mvcc(版本控制)。undolog主要分为两类:

  1. insert undo log:在insert执行时,就会记录undolog,事务提交后就会将该条日志删除。
  2. update undo log: update、delete的时候,产生的undolog日志不仅要进行数据回滚,还要进行mvcc的快照读,所以事务提交后不能即可删除该条日志,只能将该条日志数据存入undolog日志链中,等待purge线程删除。
隐藏字段
  1. DB_TRX_ID:最近一次修改这一行记录的事务的id。比如DB_TRX_ID = 4,表示最近一次修改这行 记录的事务是4。

  2. DB_ROLL_PTR:回滚指针,指针指向这行记录的上一个版本,用于配合undo log回滚日志来找到这行记录的上一个版本(执行增删改之前的版本)

  3. DB_ROLL_ID:隐藏主键,要是创建表时没有主键就会创建隐藏主键。

    image.png

版本链

不同的事务对同一条数据进行修改,就会产生一条版本链,使用隐藏字段的回滚指针(DB_ROLL_PTR)指向旧的版本,未删除的日志数据。

什么是锁

锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。

相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。

锁的分类
  • 以锁粒度的维度划分

    • 全局锁:锁定数据库中的所有表。加上全局锁之后,整个数据库只能允许读,不允许做任何写操作

    • 表级锁:每次操作锁住整张表。主要分为三类

      • 表锁(分为表共享读锁 read lock、表独占写锁 write lock)
      • 元数据锁(meta data lock,MDL):基于表的元数据加锁,加锁后整张表不允许其他事务操作。这里的元数据可以简单理解为一张表的表结构
      • 意向锁(分为意向共享锁、意向排他锁):这个是InnoDB中为了支持多粒度的锁,为了兼容行锁、表锁而设计的,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查
    • 行级锁:每次操作锁住对应的行数据。主要分为三类

      • 记录锁 / Record 锁:也就是行锁,一条记录和一行数据是同一个意思。防止其他事务对此行进行update和delete,在 RC、RR隔离级别下都支持
      • 间隙锁 / Gap 锁:锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持
      • 临键锁 / Next-Key 锁:间隙锁的升级版,同时具备记录锁+间隙锁的功能,在RR隔离级别下支持
  • 以互斥性的角度划分

    • 共享锁 / S锁:不同事务之间不会相互排斥、可以同时获取的锁
    • 排他锁 / X锁:不同事务之间会相互排斥、同时只能允许一个事务获取的锁
    • 共享排他锁 / SX锁:MySQL5.7版本中新引入的锁,主要是解决SMO带来的问题
  • 以操作类型的维度划分

    • 读锁:查询数据时使用的锁
    • 写锁:执行插入、删除、修改、DDL语句时使用的锁
  • 以加锁方式的维度划分

    • 显示锁:编写SQL语句时,手动指定加锁的粒度
    • 隐式锁:执行SQL语句时,根据隔离级别自动为SQL操作加锁
  • 以思想的维度划分

    • 乐观锁:每次执行前认为自己会成功,因此先尝试执行,失败时再获取锁
    • 悲观锁:每次执行前都认为自己无法成功,因此会先获取锁,然后再执行
mvcc版本控制
实现的组件
  • undolog

    在undolog有详细介绍

  • 快照readview

    1. m_ids:记录所有活跃事务id的集合。
    2. create_trx_id:创建readview的事务id。
    3. min_trx_id:记录最小活跃事务id。
    4. max_trx_id:记录不是活跃事务id集合的最大值,而是预分配的事务id,即最大活跃事务id为m_ids集合中最大值+1。
    5. 版本访问的规则:trx_id 是每行的隐藏字段的DB_TRX_ID,会使用下面四点进行匹配,只要满足一条要求,就可以读取数据,匹配上就是寻找下一个版本。
      • 若trx_id 与 create_trx_id一致,证明该条数据是当前事务修改的,是可以读取的。
      • 若trx_id < min_trx_id,证明该事务已经提交,是可以读取的。
      • 若trx_id > max_trx_id,说明当前事务是readview创建后启动的,所以无法读取。
      • 若min_trx_id < trx_id < max_trx_id,并且当前事务id不在ids集合中,证明已提交事务,可以读取事务。
    6. 快照readview的创建是与隔离级别有关系的。当前的隔离基本要是为读已提交每次快照读都会创建一个readview,若隔离级别为不可重复读只会创建一个readview。
mvcc演示
  1. 事务详情 image.png

  2. 隔离级别是读已提交

    事务五会创建两次快照

    1. 第一次快照数据

      • 快照信息

        image.png

      • 进行比对

        根据事务详情可以看出,最近一次对id=1数据修改的事务为4,即trx_id=4,依据匹配原则,只能根据undolog版本向下寻找。

        trx_id = 3 = min_trx_id = 3,不符合匹配原则,只能再次往下找。

        trx_id = 2 < min_trx_id = 3,说明trx_id = 2 的数据已提交,所以读取到的数据为 10.

    2. 第二次快照数据

      • 快照信息

        image.png

      • 进行匹配

        根据事务详情可以看出,最近一次对id=1数据修改的事务为4,即trx_id=4,依据匹配原则,只能根据undolog版本向下寻找。

        trx_id = 3 < min_trx_id = 4,说明事务3的已经提交,所以读取数据为18 。

  3. 隔离级别为可重复读

    事务五只会创建一次快照,

    1. 快照

      image.png

    2. 进行匹配

      • 第一次读取

        根据事务详情可以看出,最近一次对id=1数据修改的事务为4,即trx_id=4,依据匹配原则,只能根据undolog版本向下寻找。

        trx_id = 3 = min_trx_id = 3,不符合匹配原则,只能再次往下找。

        trx_id = 2 < min_trx_id = 3,说明trx_id = 2 的数据已提交,所以读取到的数据为 10.

      • 第二次读取

        由于快照只会创建一次,所以获取的数据一样的。

ACID 特性的底层实现

  • 原子性

    通过undolog链式日志来实现要么全部成功,要么全部失败。

  • 一致性

    隔离性,原子性在加上主外键约束。

  • 隔离性

mvcc版本控制+锁

  • 持久性

    redolog日志