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 数据库入门到精通