Java后端必备技能(四):MySQL进阶篇
这是一个系列文章,完整列表在这里:
Java Version:如果没有特别说明,那么默认Java8,hotspot虚拟机。
MySQL Version:如果没有特别说明那就是最后一个5.x版本5.7.44。
SQL子语言
SQL:Structured Query Language,结构化查询语言,主要对数据库管理和操作,主要由以下几个子语言组成:
DDL
Data Definition Language,数据定义语言。用于定义和管理表,索引,视图等,如CREATE
,ALTER
,DROP
DML:Data Manipulation Language,数据操作语言。用于对数据库中数据进行操作,如:INSERT
,DELETE
,UPDATE
DQL
Data Query Language,数据查询语言。不涉及修改,如SELECT
DCL
Data Control Language,数据控制语言。主要控制对数据库的访问权限,如:GRANT
、REVOKE
TCL
Transaction Control Language,事务控制语言。主要用来管理数据库事务,保证数据库的ACID属性,常见操作:COMMIT
、ROLLBACK
、SAVEPOINT
MySQL中xxxlog比较
在MySQL中有很多种日志类型文件,面试中经常被问到的主要有三个:binlog(二进制日志)、redolog(重做日志)、undolog(回滚日志),下面说说他们的作用和区别。
binlog
binlog是MySQL服务层日志,只要使用MySQL就会有binlog。binlog记录了数据库所有DDL和DML操作(不包括没有引起数据变动的操作,如select),属于逻辑日志,以追加的方式写日志文件,日志文件不会被覆盖。
主要用于数据备份,数据恢复,主从复制(下面的MySQL主从复制流程就用到了binlog👇)。
binlog格式
STATEMENT
记录的是执行的SQL语句本身。他的优点是记录文件比较小,缺点是对于某些依赖于上下文的语句或函数(uuid,now等)可能会发生数据不一致,即二次执行结果不同。
ROW
记录的是表中每一行数据的具体更改。他的优点是不会导致数据不一致,他记录了所有数据行的变更细节。缺点也很明显,就是体积会比较大,网络IO和磁盘IO较高。
MIXED
结合了前面两者的优点,MySQL自行选择使用statement+row格式存储。大多数情况下会使用statement记录,当遇到会引起数据不一致情况时自动切换到row格式记录。
如何设置
1 | // 设置binlog格式,仅对新会话有效 |
⚠️MySQL默认的事务隔离级别是RR(可重复读取)。有些场景为了提高并发量会修改事务隔离级别为RC(读取已提交),在RC下binlog是不能使用statement格式的,设置后MySQL会报错,因为会发生数据不一致问题。而RR模式因为有锁🔒,可以避免一些问题(具体可以看下面的MySQL事务隔离级别👇)。
redolog
redolog是InnoDB存储引擎层日志。redolog记录的是每个数据页做了什么改动,属于物理日志。他主要用于保证事务持久性和数据库奔溃恢复。一般在数据持久化到磁盘前会写入redolog,所以可以恢复未持久化的数据。redolog采用循环写的方式记录日志,先写内存再写磁盘。在内存中有一块redologbuffer区域,可以理解为环形缓冲区,每当有以下情况,会进行刷盘操作:
redologbuffer空间不足
redologbuffer是一块固定大小的区域,当空间快满时会刷盘,给新的日志留出空间
提交事务时
事务提交时MySQL会将redolog写入磁盘,保证持久性,即使系统在事务提交后奔溃,已提交事务数据也不会丢失。
InnoDB检查点(checkpoint)
MySQL周期性执行检查点操作,在检查点时会刷入磁盘。主要是为了减少在奔溃时需要重做的日志量。
MySQL正常关闭
当执行shutdown命令时,MySQL会刷盘,确保没有未持久化的数据。
设置事务日志参数
1 | // redolog刷新策略设置 |
- 值为0时:日志缓冲区内容大约每秒钟写入磁盘一次,事务提交时不一定会写入磁盘。这种设置减少了磁盘I/O,但如果系统崩溃,可能会丢失最近一秒内的事务数据。
- 值为1时:每个事务提交时,日志缓冲区内容会立即写入磁盘,这是最安全的设置,确保每个提交的事务都被持久化。
- 值为2时:每个事务提交时,日志缓冲区内容会写入操作系统的缓存,由操作系统负责将其写入磁盘,通常每秒钟写入一次。
undolog
redolog是InnoDB存储引擎层日志。他主要用来保证事务的原子性,即事务失败或回滚时能撤销已执行的操作。他记录的是事务执行之前的原始数据版本,通常在事务提交后写入。另外undolog还用于支持MVCC(关于什么是MVCC请看下面👇mysql并发解决方案)。
MySQL主从复制流程
下面这幅图,可以清晰的描述MySQL主从复制流程:
这是MySQL官网关于主从复制的描述,完整内容可以去链接看看:
异步复制
当主服务器上有数据变动(update,delete,insert)时,会先写入二进制日志binlog,主服务器有一个dump线程,负责接收从服务器的请求。
复制时,从服务器中的IO线程会主动连接主服务器dump线程,并指定的文件和位置,获取binlog,然后写入从服务器的relay log(中继日志)中。从服务器中还有一个SQL线程,他负责读取relay log中的数据并重放,使从库和主库数据保持一致。
默认是采用异步复制的方式:即主服务器数据变动,写入日志后立即返回给客户端结果,不会等待从服务器同步成功。还可以选择同步复制方式:主服务器会等待从服务器复制完成再返回,但是会增大延迟和客户端响应时间。MySQL在后期版本中优化了复制机制,引入了并行复制。
并行复制
有两种并行复制的模式:
1.基于schema:每个数据库都有自己的SQL线程,多个数据库事务可以并行执行。— 适用于多个独立数据库场景
2.基于组提交:组提交就是将多个事务合并为一组事务提交,复制时在从库上并行执行。 — 适用于单个数据库场景
1 | // 启用并行复制并设置并行线程数,默认为0 |
MySQL事务隔离级别
脏读/幻读/不可重复读
事务隔离,实际上就是为了解决并发场景下数据的脏读、幻读、不可重复读这些问题。
脏读
T1读到了T2还没有提交的数据。
幻读
T1做范围查询过程中,T2新增或删除了行,导致范围查询结果条数不一致。
1 | // 例如,假设有一个事务A在RR级别下执行如下查询: |
不可重复读
T1多次读取数据,T2修改了数据,导致T1两次读取数据不一致。
幻读和不可重复读区别
幻读 -> 多次查询同一dql记录条数不同
不可重复读 -> 多次读取同一条记录值不同
事务隔离级别
读取未提交
Read Uncommitted,允许一个事务读取另一个事务未提交的数据。最低隔离级别,容易出现脏读。
读取已提交
Read Committed,事务只能读取另一个事务已提交的数据。由于可能会存在读取时提交了新事务导致多次读取获取不同的值,所以会产生不可重复读问题。
可以重复读
RR(Repeatable Read),MySQL默认隔离级别,无法彻底解决幻读。这是因为在RR隔离级别下,MVCC通过快照读只能保证现有数据的更新和删除操作是快照数据不会发生幻读,而对于新插入的数据是没办法保证不发生幻读的。所以,要想真正解决幻读问题,只能通过加锁或者使用更高的隔离级别,就是下面的可串行化。
可串行化
Serializable,最高隔离级别。强制事务按顺序执行,有效防止脏读,幻读,不可重复读。但性能也是最低的,需要频繁加锁和等待释放锁。
MySQL并发解决方案
并发场景
MySQL并发场景主要是考虑多个事务同时对数据库进行操作时如何保证数据ACID属性的完整性问题。考虑以下几种并发场景:
读-读并发:不涉及对数据的变更,不会有并发问题,但是频繁读取可能会影响数据库性能,可以增加缓存,采用主从架构等分摊读的压力。
读-写并发:这种情况下因为同时存在对数据的读和写操作,理论上脏读、幻读、不可重复读都会发生。通过设置MySQL事务隔离级别为RR或者可串行化,以及MVCC机制可以解决大部分读写并发问题,解决不了的只能通过加锁解决:在读操作中使用共享锁,防止其他事务写入;在写操作中使用排他锁,防止其他事务读取或写入。
写-写并发:多个事务对同一数据进行修改可能会导致数据不一致或丢失。可以通过对数据行添加行锁、乐观锁、悲观锁来解决。
共享锁和排他锁
共享锁
Shared Lock,S锁。当一个事务对某行数据添加了共享锁,那么其它事务也可以给这行数据添加共享锁,但是不可添加排他锁。
1 | // 这个语句会对符合条件的记录加上共享锁,防止其他事务修改这些记录 |
共享锁适用于需要确保读取场景数据不会被其他事务操作修改。
排他锁
Exclusive Lock,X锁。当一个事务对某行数据添加了排他锁,那么其它事务既不可以给这行数据添加共享锁,也不可以添加排他锁。
1 | // 这个语句会对符合条件的记录加上排他锁,防止其他事务读取或修改这些记录 |
排他锁适用于需要确保修改期间不会有其他事务对数据修改或被读取。
行锁、悲观锁、乐观锁
行级锁
使用行级锁来防止不同事务同时修改同一行数据,从而避免写写冲突。
悲观锁
事务在开始修改数据前,会锁定数据行,从而确保其他事务无法同时修改该数据。
乐观锁
通过版本号或时间戳来控制并发修改,在提交时检测冲突,若发生冲突,则回滚或重试。
MVCC实现原理
在MySQL中,MVCC主要通过InnoDB存储引擎来实现,用来确保在高并发场景下,读写数据互不干扰。
行版本控制
InnoDB在每行记录增加了两个隐藏参数来进行行版本控制:
DB_TRX_ID:记录最后一次修改数据的事务ID
DB_ROLL_PTR:回滚指针,用来定位来版本记录
在数据修改时,Innodb会写入新版本数据,同时也会将老版本数据记录到undolog,而这两个隐藏参数也一并被记录,用于帮助构建快照读。
读视图Read View
这是MVCC实现的核心。当事务启动时InnoDB会生成读视图,包含了其他正在执行的事务列表。读视图就是用来确定哪些事务的修改是可见的,哪些是不可见的:当前事务的修改、读视图生成前提交的事务修改都是可见的,其它不可见。
MVCC通过undolog快照读保存多个快照版本,通过读视图判断可见性,以此来保证读写并发下的事务。
快照读和当前读
快照读(Snapshot Read)
快照读是MVCC的典型使用场景,读取的是数据的历史版本,而不是当前版本。在执行SELECT语句时,如果使用的是快照读,InnoDB将根据读视图和Undo Log来返回合适版本的数据。例如:SELECT * FROM table WHERE ...
是一种快照读。
当前读(Current Read)
当前读读取的是数据的最新版本,可能需要加锁以确保读到的版本是最新的。例如:SELECT ... FOR UPDATE
和 SELECT ... LOCK IN SHARE MODE
都是当前读。