MySQL 事务的 12 连问,你顶得了嘛
1. 什么是数据库事务?
事务,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。
假如 A 转账给 B 100 元,先从 A 的账户里扣除 100 元,再在 B 的账户上加上 100 元。如果扣完 A 的 100 元后,还没来得及给 B 加上,银行系统异常了,最后导致 A 的余额减少了,B 的余额却没有增加。所以就需要事务,将 A 的钱回滚回去,就是这么简单。
2. 事务的四大特性
原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部都执行,要么都不执行。
一致性:指在事务开始之前和事务结束以后,数据不会被破坏,假如 A 账户给 B 账户转 10 块钱,不管成功与否,A 和 B 的总金额是不变的。
隔离性:多个事务并发访问时,事务之间是相互隔离的,一个事务不应该被其他事务干扰,多个并发事务之间要相互隔离。
持久性:表示事务完成提交后,该事务对数据库所作的操作更改,将持久地保存在数据库之中。
3. 事务的隔离级别有哪些?MySQL 的默认隔离级别是什么?
事务的隔离级别有四种,分别是:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、串行化(Serializable)。
读未提交隔离级别:只限制了两个数据不能同时修改,但是修改数据的时候,即使事务未提交,都是可以被别的事务读取到的,这级别的事务隔离有脏读、重复读、幻读的问题;
读已提交隔离级别:当前事务只能读取到其他事务提交的数据,所以这种事务的隔离级别解决了脏读问题,但还是会存在重复读、幻读问题;
可重复读: 可重复读隔离级别,限制了读取数据的时候,不可以进行修改,所以解决了重复读的问题,但是读取范围数据的时候,是可以插入数据,所以还会存在幻读问题;
串行化: 事务最高的隔离级别,在该级别下,所有事务都是进行串行化顺序执行的。可以避免脏读、不可重复读与幻读所有并发问题。但是这种事务隔离级别下,事务执行很耗性能。
Mysql 默认的事务隔离级别是可重复读(RR)。
4. Mysql 为什么选择 RR 作为默认隔离级别?
我们知道 Mysql 有四种数据库隔离级别,分别是读未提交、读已提交、可重复读、串行化。而读未提交隔离级别太低了,会有脏读问题,串行化隔离级别太高了,会影响并发读。那么就剩下读已提交(RC)和可重复读(RR)了。
那么,Mysql 为什么会选择 RR 作为默认隔离级别呢?
我们的 MySQL 数据库一般都是集群部署的,会有主库、从库。主库负责写,从库负责读。主库写入之后,会进行主从复制,把数据同步到从库。
从库是在主库拿到 bin log 日志,并执行 bin log,从而保证从库与主库的数据一致性。
实际上,bin log 有三种格式,分别是 statement,row 和 mixed。如果是 statement 格式,bin log 记录的是 SQL 的原文。Mysql 早些时候,bin log 日志格式只有 statement 这种,在 RC 的隔离级别,可能出现数据不一致的问题。
MySQL 官网上还记录了这个 bug。
我们可以复现这个 bug,假设有表结构如下:
插入两条数据
执行以下这两个事务:
执行完之后,因为事务的隔离级别是 RC,所以事务 A 在更新时,会对 b=2 加行级锁,所以执行结果为(888,2),事务 B 在执行时,不受行级锁的影响,两条数据变为(888,2),(233,2)。
在 RC 隔离级别下,我们再来看下 bin log 日志。当两个事务执行完后,会先记录事务 B 的 bin log 日志,因为它最先提交,然后才生成事务 A 的 bin log 日志。当 bin log 日志格式是 statement,binlog 记录的就是原文,也就是先记录 update t set b=2 where b = 1;,然后才记录 update t set a=888 where b=2。
酱紫的话,当主库把 binlog 同步到从库,执行 SQL 回放后,数据库中的数据就变成了(888,2)和(888,2),主数据库和从数据库数据不一致啦。而在 RR(可重复读的数据库隔离级别)下,因为会有间隙锁的存在,这种情况就不会发生,因此,Mysql 默认选择 RR 作为隔离级别。
5. 很多大厂为什么选择 RC 数据库隔离级别?
互联网大厂和一些传统企业,最明显的特点就是高并发。那么大厂就更倾向提高系统的并发读。
RC 隔离级别,并发度是会比 RR 更好的,为什么呢?
因为 RC 隔离级别,加锁过程中,只需要对修改的记录加行锁。而 RR 隔离级别,还需要加 Gap Lock 和 Next-Key Lock,即 RR 隔离级别下,出现死锁的概率大很多。并且,RC 还支持半一致读,可以大大的减少了更新语句时行锁的冲突;如果对于不满足更新条件的记录,就可以提前释放锁,提升并发度。
一致性读:又称为快照读。快照即当前行数据之前的历史版本。快照读就是使用快照信息显示基于某个时间点的查询结果,而不考虑与此同时运行的其他事务所执行的更改。
当前读: 当前读的规则,就是要能读到所有已经提交的记录的最新值。
半一致性读:一条 update 语句,如果 where 条件匹配到的记录已经加锁,那么 InnoDB 会返回记录最近提交的版本,由 MySQL 上层判断此是否需要真的加锁。
6. 并发场景,数据库存在哪些一致性问题?
脏读:如果一个事务读取到了另一个未提交事务修改过的数据,我们就称发生了脏读现象。
不可重复读:同一个事务内,前后多次读取,读取到的数据内容不一致
幻读:如果一个事务先根据某些搜索条件查询出一些记录,在该事务未提交时,另一个事务写入了一些符合那些搜索条件的记录(如 insert、delete、update),就意味着发生了幻读。
丢失更新:事务 A 和事务 B 都对同一个数据进行修改,事务 A 先修改,事务 B 随后修改,事务 B 的修改覆盖了事务 A 的修改。
7. 四大隔离级别,都会存在哪些并发问题呢?
隔离级别脏读不可重复读幻读读未提交(RU)√√√读已提交(RC)×√√可重复读(RR)××√串行化(Serializable)×××
在 RU 隔离级别下,可能发生脏读、不可重复读、幻读现象。
在 RC 隔离级别下,可能发生不可重复读、幻读现象。
在 RR 隔离级别下,可能发生幻读现象。
在 Serializable 隔离级别,会强制事务串行执行,不会存在脏读、不可重复读、幻读现象。
8. MySQL 的隔离级别是如何实现的?
MySQL 的隔离级别是通过 MVCC 和锁机制来实现的。
RU 隔离级别最低,没有加锁,存在脏读问题。事务读不加锁,不阻塞其他事务的读和写
RC 和 RR 隔离级别可以通过 MVCC 来实现。
串行化是通过锁机制实现。读加共享锁,写加排他锁,读写互斥。如果有未提交的事务正在修改某些行,所有 select 这些行的语句都会阻塞。
9. 什么是 MVCC,它的底层原理?
MVCC,即 Multi-Version Concurrency Control(多版本并发控制)。它是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问.
通俗的讲,数据库中同时存在多个版本的数据,并不是整个数据库的多个版本,而是某一条记录的多个版本同时存在,在某个事务对其进行操作的时候,需要查看这一条记录的隐藏列事务版本 id,对比事务 id 并根据事物隔离级别去判断读取哪个版本的数据。
要了解 MVCC 的底层原理,需要回顾很多相关知识点,我们按以下小提纲,来分析哈:
什么是快照读和当前读
隐式字段
什么是 Undo Log
什么是快照版本链
事务版本号
什么是 Read View
查询一条记录,基于 MVCC,是怎样流程
基于 MVCC,RC 隔离级别,存在不可重复读问题的分析
9.1 什么是快照读和当前读
快照读:读取的是记录数据的可见版本(有旧的版本)。不加锁,普通的 select 语句都是快照读。
当前读:读取的是记录数据的最新版本,显式加锁的都是当前读。
快照读是 MVCC 实现的基础。
9.2 隐式字段
对于 InnoDB 存储引擎,每一行记录都有两个隐藏列 trx_id、roll_pointer,如果表中没有主键和非 NULL 唯一键时,则还会有第三个隐藏的主键列 row_id。
9.3 什么是 Undo Log
undo log,回滚日志,用于记录数据被修改前的信息。在表记录修改之前,会先把数据拷贝到 undo log 里,如果事务回滚,即可以通过 undo log 来还原数据。
可以这样认为,当 delete 一条记录时,undo log 中会记录一条对应的 insert 记录,当 update 一条记录时,它记录一条对应相反的 update 记录。
undo log 有什么用途呢?
事务回滚时,保证原子性和一致性。
用于 MVCC 快照读。
9.4 快照版本链
多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。如下:
9.5 事务版本号
事务每次开启前,都会从数据库获得一个自增长的事务 ID,可以从事务 ID(trx_id)判断事务的执行先后顺序。这就是事务版本号。
9.6 什么是 Read View
Read View 是什么呢?它就是事务执行 SQL 语句时,产生的读视图。实际上在 innodb 中,每个 SQL 语句执行前都会得到一个 Read View。它主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据~
在 Read View 中,有这几个重要的属性。
m_ids:当前系统中,那些未提交的读写事务 ID 列表。
min_limit_id:表示在生成 Read View 时,当前系统中活跃的读写事务中最小的事务 id,即 m_ids 中的最小值。
max_limit_id:表示生成 Read View 时,系统中应该分配给下一个事务的 id 值。
creator_trx_id: 创建当前 Read View 的事务 ID
Read view 匹配条件规则(很重要)如下:
如果数据事务 ID trx_id < min_limit_id,表明生成该版本的事务在生成 Read View 前,已经提交(因为事务 ID 是递增的),所以该版本可以被当前事务访问。
如果 trx_id>= max_limit_id,表明生成该版本的事务在生成 Read View 后才生成,所以该版本不可以被当前事务访问。
如果 min_limit_id =<trx_id< max_limit_id,需腰分 3 种情况讨论
(1).如果 m_ids 包含 trx_id,则代表 Read View 生成时刻,这个事务还未提交,但是如果数据的 trx_id 等于 creator_trx_id 的话,表明数据是自己生成的,因此是可见的。
(2)如果 m_ids 包含 trx_id,并且 trx_id 不等于 creator_trx_id,则 Read View 生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的;
(3).如果 m_ids 不包含 trx_id,则说明你这个事务在 Read View 生成之前就已经提交了,修改的结果,当前事务是能看见的。
9.7 查询一条记录,基于 MVCC,是怎样的流程
获取事务自己的版本号,即事务 ID(trx_id)
获取 Read View
查询得到的数据,然后 Read View 中的事务版本号进行比较。
如果不符合 Read View 的可见性规则, 即就需要 Undo log 中历史快照;
最后返回符合规则的数据
InnoDB 实现 MVCC,是通过 Read View+ Undo Log 实现的,Undo Log 保存了历史快照,Read View 可见性规则帮助判断当前版本的数据是否可见。
9.8 基于 MVCC,RC 隔离级别,存在不可重复读问题的分析
为了加深大家对 MVCC 的理解,我们来分析一个例子吧:比如 RC 隔离级别,存在不可重复读问题,我们来分析这个过程哈。
先创建 core_user 表,插入一条初始化数据,如下:
隔离级别设置为读已提交(RC),事务 A 和事务 B 同时对 core_user 表进行查询和修改操作。
最后事务 A 查询到的结果是,name=曹操的记录,我们基于 MVCC,来分析一下执行流程:
(1) A 开启事务,首先得到一个事务 ID 为 100 (2) B 开启事务,得到事务 ID 为 101 (3) 事务 A 生成一个 Read View,read view 对应的值如下
然后回到版本链:开始从版本链中挑选可见的记录:
由图可以看出,最新版本的列 name 的内容是孙权,该版本的 trx_id 值为 100。判断 read view 可见性规则校验:
由此可得,trx_id=100 的这个记录,当前事务是可见的。所以查到是 name 为孙权的记录。
(4) 事务 B 进行修改操作,把名字改为曹操。把原数据拷贝到 undo log,然后对数据进行修改,标记事务 ID 和上一个数据版本在 undo log 的地址。
(5) 提交事务 (6) 事务 A 再次执行查询操作,新生成一个 Read View,Read View 对应的值如下
然后再次回到版本链:从版本链中挑选可见的记录:
从图可得,最新版本的列 name 的内容是曹操,该版本的 trx_id 值为 101。判断 Read View 可见性规则校验:
因此,trx_id=101 这个记录,对于当前事务是可见的。所以 SQL 查询到的是 name 为曹操的记录。
综上所述,在读已提交(RC)隔离级别下,同一个事务里,两个相同的查询,读取同一条记录(id=1),却返回了不同的数据(第一次查出来是孙权,第二次查出来是曹操那条记录),因此 RC 隔离级别,存在不可重复读并发问题。
而在 RR 隔离级别,一个事务里,每一次查询,都只会获取一次 read view,都是副本共用的,从而保证每次查询的数据都是一样的,因此它是解决了不可重复读的并发问题的哈。大家可以自己手动去分析以下,这就是 MVCC 原理所在,也可以看我之前写的这篇文章哈,写得很清晰:看一遍就理解,MVCC 原理详解
10. 如何处理大事务和长事务?请给出一些处理方法。
处理大事务和长事务是数据库设计和优化中非常重要的一部分,以下是一些常用的处理方法:
大事务拆分为小事务:将大事务拆分为多个小事务,减少每个事务操作的数据量,可以减少锁竞争和死锁的风险,提高并发性能。
优化查询语句:对于长事务中的查询操作,可以通过优化查询语句来提高查询性能,如添加索引、优化 SQL 结构等。
避免长时间占用锁:长事务会占用锁资源,导致其他事务无法访问相应的数据,因此需要尽可能缩短事务的执行时间,避免长时间占用锁。
避免长时间的事务等待:长事务可能会导致其他事务的等待时间过长,影响系统的性能和可用性,因此需要尽可能缩短事务的执行时间,避免长时间的事务等待。
优化事务日志:长事务会占用大量的事务日志,导致数据库性能下降,因此需要通过优化事务日志的写入和刷盘策略来提高性能。
使用定时任务:长时间运行的事务可以通过定时任务来定期执行,以避免长时间占用资源。
适当增加硬件资源:如果以上方法不能解决问题,可以适当增加硬件资源,如增加内存、CPU、存储等,以提高系统性能。
11. 如何优化 MySQL 事务的性能?请列举一些优化方法。
MySQL 事务性能优化是提高数据库性能的关键之一,以下是一些常用的优化方法:
选择合适的存储引擎:不同的存储引擎具有不同的特性和性能,因此需要根据具体的业务需求选择合适的存储引擎,如 MyISAM、InnoDB、Memory 等。
使用合适的索引:合理的索引可以提高查询和更新操作的效率,因此需要根据实际业务情况添加适当的索引,避免全表扫描。
避免不必要的锁定:不必要的锁定会降低并发性能,因此需要避免不必要的锁定,如优化查询语句、使用乐观锁等。
选择合适的事务隔离级别:不同的事务隔离级别具有不同的特性和性能影响,因此需要根据实际业务情况选择合适的事务隔离级别。
减少事务的范围:尽量减小事务的范围,将大事务拆分为多个小事务,可以减少锁竞争和死锁的风险,提高并发性能。
使用合适的事务提交方式:对于不需要回滚的事务,可以使用自动提交方式,减少提交操作的次数,提高性能。
避免长事务:长时间运行的事务会占用大量的资源,影响并发性能,因此需要尽可能缩短事务的执行时间,避免长时间的事务等待。
优化数据库服务器的硬件和配置:优化数据库服务器的硬件和配置可以提高数据库性能,如增加内存、优化磁盘性能、调整缓存大小等。
使用分布式数据库:对于高并发的场景,可以使用分布式数据库架构,将数据分布到多个数据库节点上,提高并发性能。
当然,这些方法可能并不适用于所有业务场景,需要根据具体情况进行选择和调整。
12. Innodb 的事务实现的基本原理
InnoDB 是 MySQL 中一种常用的存储引擎,支持事务和行级锁等高级特性。以下是 InnoDB 实现事务的基本原理:
InnoDB 中,每个事务都拥有一个唯一的事务 ID(transaction ID),通过该 ID 来区分不同的事务。
InnoDB 使用 MVCC(多版本并发控制)来实现事务的隔离性,每次修改会生成一个新版本,查询时只能看到在查询开始之前就已经提交的版本,这样可以避免读取到脏数据。
在执行事务中的更新操作时,InnoDB 会根据需要对相关的数据行加锁,以保证事务的原子性和一致性。InnoDB 中的行级锁是通过在索引节点上加锁来实现的,因此对于相同的数据行,不同的事务可以分别通过不同的索引来访问和修改数据。
InnoDB 中的事务支持 ACID 特性,即原子性、一致性、隔离性和持久性。InnoDB 通过 redo log 和 undo log 来保证事务的原子性和持久性,其中 redo log 记录了事务的修改操作,而 undo log 则记录了事务的回滚操作。当系统崩溃或者发生其他故障时,InnoDB 可以通过 redo log 和 undo log 来恢复数据到事务提交前的状态,以保证数据的一致性和持久性。
InnoDB 中的事务隔离级别包括读未提交、读已提交、可重复读和串行化。默认的隔离级别是可重复读,使用锁和 MVCC 机制来实现。在高并发的情况下,如果锁的粒度过大或者锁的竞争过激,可能会导致性能瓶颈或者死锁问题,因此需要针对具体场景进行优化。
评论