MySQL -- 事务浅析

mysql 事务

一、Mysql 事务

MySQL 事务主要用于处理操作量大、复杂度高的数据

  • MySQL 数据库中只有 Innodb 存储引擎支持事务操作
  • 事务处理可以用来维护数据库的完整性,保证成批的 SQL 要么全部执行,要么全部不执行
  • 事务用来管理insertupdatedelete语句

二、事务特性

一般来说,事务必须满足 4 个条件(ACID),即原子性、一致性、持久性、隔离性,具体如下:

1、原子性 (Atomicity)

一个事务(Transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像事务没有执行过一样。

2、一致性Consistency)

在事务执行前后,数据库中的数据必须满足一定的约束条件,保证数据的逻辑完整性。如果事务执行过程中出现错误,则会回滚到事务开始前的状态。

3、隔离性Isolation)

数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,事务隔离分为以下不同级别:

  • 读未提交(Read uncommited): 允许脏读,也就是可能读到其他会话中未提交事务修改的数据。

  • 读已提交(Read commited): 只能读取到已提交的数据。

  • 可重复读(Repeatable read): 在同一个事务内的查询都是从开始时刻一致的,InnoDB 存储引擎默认的事务隔离级别就是可重复读,在 SQL 标准中,该隔离级别消除了不可重复读,但还是存在幻读。

  • 串行化(Serializable): 完全串行化的读,每次读都需要获得表级的共享锁,读写相互都会阻塞。

4、持久性Durability)

事务处理结束后,对数据的修改就是永久的,几遍系统故障也不会丢失。


三、事务的并发处理

准备工作:创建数据表,插入一条数据

1
2
3
4
5
6
7
8
create table user(
  id int(10) not null auto_increment comment '主键ID',
  name varchar(30) not null default '' comment '用户名',
  primary key(id)
) engine=innodb charset=utf8mb4;

# 插入数据
insert into `user`(`name`) values('老王01');

事务并发可能出现的情况:

  • 脏读

一个事务读到了另一个未提交事务修改过的数据

dirty-read1

1、会话 B 开启一个事务,把id=1name改为老王01

2、会话 A 也开启一个事务,读取id=1name,次时的查询结果为老王02

3、会话 B 的事务回滚了修改的操作,这样会话 A 读到的数据就是不存在的;

这个现象就是脏读。(脏读只会在读未提交的隔离级别中才会出现)。

  • 不可重复读

一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。(不可重复读在读未提交和读已提交隔离级别都可能会出现)

non-repeatable-read

1、会话 A 开启事务,查询id=1的 name 是老王01

2、会话 Bid=1的 name 更新为老王02(隐式事务,autocommit=1,执行完 sql 会自动 commit);

3、会话 A 再查询时id=1的 name 为老王02

4、会话 B 又将id=1的 name 更新为老王03

5、会话 A 再查询id=1的 name 时,结果为老王03

这种现象就是不可重复读。

  • 幻读

一个事务先根据某些条件查出一些记录,之后另一个事务又向数据表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能够把另一个事务插入的数据也查出来。 (幻读在读未提交、读已提交、可重复读隔离级别中都可能会出现)

phantom-read

1、会话 A 开始事务,查询id>0的数据,结果只有 name=老王 01 的一条数据

2、会话 B 像数据表中插入了一条name=老王02的数据(隐式事务,执行 sql 后自动 commit)

3、会话 A 的事务再次查询 id>0的数据

  • 不同隔离级别下出现事务并发问题的可能
隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
串行化 不可能 不可能 不可能

四、事务的实现原理

首先了解一下 redo log 和 undo log

1、redo log(重做日志)

MySQL 为了提升性能不会把每次的修改都实时同步到磁盘,而是会优先存储到 Buffer Pool(缓冲池)里面,把这个当做缓存来用,然后使用后台线程去做缓冲池和磁盘之间的同步

如果还没来得及同步数据就出现宕机或者断电,就会导致丢失部分已提交事务的修改信息,

所以引入了**redo log(回滚)**来记录已成功提交事务的修改信息,并且把 redo log 持久化到磁盘,系统重启之后读取 redo log 恢复最新数据

redo log 是用来恢复数据的,用于保障已提交事务的持久化特性。

2、undo log

undo log 叫做回滚日志,用于记录数据被修改前的信息,与 redo log 记录的数据相反,redo log 是记录修改后的数据,undo log 记录的是数据的逻辑变化,为了发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚

每次写入数据或者修改数据之前都会把修改前的信息记录到 undo log

3、事务特性的具体实现原理

  • 事务的原子性通过 undo log 来实现的
  • 事务的持久性是通过 redo log 实现的
  • 事务的隔离性是通过 读写锁 + MVCC 实现的
  • 事务的一致性是通过 **原子性持久性隔离性**来实现的

3.1、原子性的实现

  • 每条数据变更(insert/update/delete)操作都会记录一条undo log,并且undo log必须先于数据持久化到磁盘上。

  • 所谓的回滚就是根据undo log做逆向操作,比如delete的逆向操作是insertinsert的逆向操作是deleteupdate的逆向操作是update等。

  • 为了做到同时成功或者同时失败,当系统发生错误或者执行rollback时需根据undo log进行回滚

3.2、持久性的实现

Mysql 的数据存储机制是将数据最终持久化到磁盘上,并且频繁的进行磁盘 IO 是非常消耗性能的,为了提升性能,InnoDB 提供了缓冲池(Buffer Pool),缓冲池中包含了磁盘数据也的映射,可以当做缓存来使用

读数据:会首先从缓冲池中读取,若没有,则从磁盘读取并放入缓冲池中

写数据:会首先写入缓冲池中,缓冲池中的数据会定期同步到磁盘中

那么问题来了,如果在缓冲池的数据还没有同步到磁盘上时,出现了机器宕机或者断电,可能会出现数据丢失的问题,因此我们需要记录已提交事务的数据,于是,redo log登场了, redo log 在执行数据变更(insert/update/delete)操作的时候,会变更后的结果记录在缓冲区,待commit事务之后同步到磁盘

至于redo log也要进行磁盘 IO,为什么还要用

(1)、redo log是顺序存储,而缓存同步是随机操作

(2)、缓存同步是以数据页为单位,每次传输的数据大小小于redo log

3.3、隔离性的实现

  • 读未提交: 读写并行,读的操作不能排斥写的操作,因此会出现脏读,不可重复读,幻读的问题

  • 读已提交: 使用排他锁X,更新数据需要获取排他锁,已经获取排他锁的数据,不可以再获取共享锁S以及排他锁X,读取数据使用了MVCC(Mutil-Version Concurrency Control)多版本并发控制机制(后续单独展开)以及Read view的概念,每次读取都会产生新的Read view,因此可以解决脏读问题,但解决不了不可重复读幻读的问题

  • 可重复读: 同上也是利用MVCC机制实现,但是只在第一次查询的时候创建Read view,后续的查询还是沿用之前的Read view,因此可以解决不可重复读的问题,具体不在这展开,但还是有可能出现幻读

  • 串行化 :读操作的时候加共享锁,其他事务可以并发读,但是不能并发写,执行写操作的时候加排他锁,其他事务既不能并发写,也不能并发读,串行化可以解决事务并发中出现的脏读不可重复读幻读问题,但是并发性能却因为加锁的开销变得很差

3.4、一致性的实现

一致性的实现其实是通过原子性持久性隔离性共同完成的

五、结束语

了解 MySQL 的事务机制,以及实现原理,对于使用或者优化都有很大的帮助,要保持知其然和知其所以然的心态和持续学习的劲头,了解更多关于 Mysql 相关的知识!


皖ICP备20014602号
Built with Hugo
Theme Stack designed by Jimmy