MySQL事务和SQL语句执行过程


MySQL事务

事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败

控制事务一

  • 查看/设置事务提交方式
SELECT @@autocommit;
SET @@autocommit = 0;
  • 提交事务
COMMIT
  • 回滚事务
ROLLBACK

注意:上述的这种方式,我们是修改了事务的自动提交行为,把默认的自动提交修改为了手动提交,此时我们执行的 DML 语句都不会提交,需要手动的执行 commit 进行提交

控制事务二

  • 开启事务
START TRANSACTION;
-- 或
BEGIN;
  • 提交事务
COMMIT
  • 回滚事务
ROLLBACK

以转账作为案例,我们先做好数据准备:

drop table if exists account;
create table account(
    id int primary key AUTO_INCREMENT comment 'ID',
    name varchar(10) comment '姓名',
    money double(10,2) comment '余额'
) comment '账户表';

insert into account(name, money) VALUES ('张三',2000), ('李四',2000);

利用事务执行操作:

-- 开启事务
start transaction;

-- 1. 查询张三余额
select * from account where name = '张三';

-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';

-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';

-- 如果正常执行完毕, 则提交事务
commit;

-- 如果执行过程中报错, 则回滚事务
-- rollback;

事务会把这两个操作就可以看成逻辑上的一个整体,这个整体包含的操作要么都成功,要么都要失败。这样就不会出现张三余额减少而李四的余额却并没有增加的情况

事务的四大特性

  • 原子性Atomicity) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用

  • 一致性Consistency): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的

  • 隔离性Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的

  • 持久性Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

上述就是事务的四大特性,简称 ACID,这里要额外补充一点:只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障

并发事务问题

脏读

一个事务读到另一个事务还没有提交的数据,称之为脏读

比如 B 读取了 A 未提交的数据

不可重复读

一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读

比如事务A两次读取同一条记录,但是读取到的数据却是不一样的

幻读

一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了 “幻影”,这种就是幻读

事务隔离级别

为了解决并发事务引发的问题,在数据库中引入事务隔离级别。主要有以下四种:

  • READ-UNCOMMITTED(读取未提交) : 最低的隔离级别,允许读取尚未提交的数据变更

  • READ-COMMITTED(读取已提交) : 允许读取并发事务已经提交的数据

  • REPEATABLE-READ(可重复读) : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改

  • SERIALIZABLE(可串行化) : 最高的隔离级别,完全服从 ACID 的隔离级别

隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED
READ-COMMITTED ×
REPEATABLE-READ(InnoDB默认) × ×
SERIALIZABLE × × ×

查看事务隔离级别

SELECT @@TRANSACTION_ISOLATION;

设置事务隔离级别

SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE };

注意:事务隔离级别越高,数据越安全,但性能越低

SQL语句执行过程

在学习 SQL 语句在 MySQL 中的执行过程之前,我们首先了解一下 MySQL 的基础架构

MySQL基础架构

下图是 MySQL 的一个简要架构图,从下图我们可以很清晰的看到 SQL 语句在 MySQL 内部是如何执行的。

先简单介绍一下下图涉及的一些组件的基本作用

  • 连接器: 身份认证和权限相关(登录 MySQL 的时候)

  • 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)

  • 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确

  • 优化器: 按照 MySQL 认为最优的方案去执行,比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序等

  • 执行器: 执行语句,执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果

  • 存储引擎: 主要负责数据的存储和读取,采用可以替换的插件式架构,支持 InnoDB、MyISAM、Memory 等多个存储引擎

语法分析

SQL 可以分为两种,一种是查询,一种是更新(增加、修改、删除)

查询语句

-- 以一个简单语句为例
select * from tb_student  A where A.id=1;

结合 MySQL 基础架构,我们分析这个语句的执行流程:

  • 先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 SQL 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步

  • 通过分析器进行词法分析,提取 SQL 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id=1。然后判断这个 SQL 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。

  • 接下来就是优化器确定执行方案,优化器根据自己的优化算法选择执行效率最好的一个方案

  • 执行,执行之前进行权限校验,如果没有权限则返回错误信息,如果有权限则调用数据库引擎接口,返回引擎的执行结果

执行流程即:权限校验(如果命中缓存)—>查询缓存—>分析器—>优化器—>权限校验—>执行器—>引擎

更新语句

-- 以一个简单语句为例
update tb_student A set A.age='19' where A.name=' 张三 ';

其实这条语句基本也会沿着上一个查询的流程走,只不过执行更新的时候要记录日志,以 MySQL 默认引擎 InnoDB 为例,这条语句的执行流程如下:

  • 先查询到张三这一条数据,如果有缓存,也是会用到缓存

  • 然后拿到查询的语句,把 age 改为 19,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交

  • 执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态,更新完成

执行流程即:分析器---->权限校验---->执行器—>引擎—redo log(prepare 状态)—>binlog—>redo log(commit状态)

这里使用采用 redo log 和 binlog 两个日志的原因是为了解决数据一致性问题

为了论述上述执行过程采用这两个日志是怎样解决这个问题,我们首先采用反证法来说明为什么 redo log 先要进入 prepare 状态,然后根据 binlog 提交?

  • 先写 redo log 直接提交,然后写 binlog,假设写完 redo log 后,机器挂了,binlog 日志没有被写入,那么机器重启后,这台机器会通过 redo log 恢复数据,但是这个时候 binlog 并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据

  • 先写 binlog,然后写 redo log,假设写完了 binlog,机器异常重启了,由于没有 redo log,本机是无法恢复这一条记录的,但是 binlog 又有记录,那么和上面同样的道理,就会产生数据不一致的情况

如果采用 redo log 两阶段提交的方式就不一样了,写完 binlog 后,然后再提交 redo log 就会防止出现上述的问题,从而保证了数据的一致性。那么问题来了,有没有一个极端的情况呢?假设 redo log 处于预提交状态,binlog 也已经写完了,这个时候发生了异常重启会怎么样呢? 这个就要依赖于 MySQL 的处理机制了,MySQL 的处理过程如下:

  • 判断 redo log 是否完整,如果判断是完整的,就立即提交
  • 如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log,不完整就回滚事务

这样就解决了数据一致性的问题

参考

  • JavaGuide(Java面试+学习指南)
  • 黑马程序员 MySQL 数据库入门到精通

文章作者: 不才叶某
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 不才叶某 !
评论
  目录