Java面试专题-MySQL篇事务等
把面试专题的MySQL看完了,但是感觉不全,还得看看其他的八股,不过准备跟着敲一下黑马点评,家人们推荐吗
二、事务
1.事务特性
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
-
ACID是什么?可以详细说一下吗?
-
原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
-
一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
-
隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
-
持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
-
InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?
- 持久性是通过redo log(重做日志)来保证的;
- 原子性是通过 undo log(回滚日志)来保证的;
- 隔离性是通过MVCC(多版本并发控制)或锁机制来保证的;
- 一致性则是通过持久性+原子性+隔离性来保证;
2.并发事务问题、隔离级别
-
并发事务带来哪些问题?怎么解决这些问题呢?MySQL的默认隔离级别是?
-
并发事务问题:脏读、不可重复读、幻读
-
隔离级别:读未提交、读已提交、可重复读、串行化
关系:先问问题,然后通过隔离级别解决
-
(一)并发事务问题
| 问题 | 描述 |
|---|---|
| 脏读 | 一个事务读到另外一个事务还没有提交的数据。 |
| 不可重复读 | 一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。 |
| 幻读 | 在一个事务内多次查询某个符合查询条件的记录数量,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了幻读现象。 |
脏读:
没提交但是已经暂时修改了数据库的内容,事务B读到了修改后的数据
如果那个事务最终回滚,则读到的数据是无效的。
不可重复读:
在同一个事务中,多次读取同一数据,由于其他事务的提交修改(UPDATE),导致读取结果不一致。
幻读:
不可重复读和幻读有什么区别?
- 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
- 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。
(二)隔离级别
-
怎么解决并发事务的问题呢?
解决方案:对事务进行隔离
隔离级别 脏读 不可重复读 幻读 Read uncommitted 读未提交 √ √ √ Read committed 读已提交 × √ √ Repeatable Read(默认)可重复读 × × √ Serializable 串行化 × × × -
可重复读(REPEATABLE READ)能解决部分幻读问题,但不能完全解决所有幻读场景!
具体来说:
✅ 能解决:快照读(普通SELECT) 的幻读
❌ 不能解决:当前读 + 写操作 导致的幻读
-- 可重复读下的锁机制: -- 1. 快照读:不加锁,只读快照 -- 2. 当前读:加记录锁(Record Lock) -- 问题:记录锁只能锁已存在的记录! -- 对于"不存在"的记录(即其他事务将要插入的记录),无法预先加锁场景1:纯读操作 - ✅ 解决
-- 事务A BEGIN; SELECT * FROM users WHERE age > 20; -- 返回3条记录 -- 事务B插入并提交 INSERT INTO users(name, age) VALUES('新用户', 25); COMMIT; -- 事务A再次查询 SELECT * FROM users WHERE age > 20; -- 还是返回3条记录! -- 可重复读通过MVCC避免了幻读场景2:读后写 - ❌ 未解决!
-- 事务A BEGIN; -- 第一次查询:统计年龄>20的人数 SELECT COUNT(*) FROM users WHERE age > 20; -- 返回3 -- 事务B插入 INSERT INTO users(name, age) VALUES('新用户', 25); COMMIT; -- 事务A尝试更新这些用户 UPDATE users SET vip = 1 WHERE age > 20; -- 影响了4行!包括事务B插入的那行 -- 事务A再次查询 SELECT COUNT(*) FROM users WHERE age > 20; -- 返回4! -- 出现了幻读! -
SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
注意:事务隔离级别越高,数据越安全,性能越差
-
3.undo log和redo log
-
缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度
-
数据页(page):是InnoDB存储引擎磁盘管理的最小单元,每个页的大小默认为16KB。页中存储的是行数据
1)redo log
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。
顺序磁盘 IO 写操作可以媲美内存随机读写,性能很高!
(2)undo log
回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚和MVCC(多版本并发控制)。undo log和redo log记录物理日志不一样,它是逻辑日志。
-
可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,
-
当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
undo log可以实现事务的一致性和原子性
总结
-
undo log和redo log的区别
-
redo log:记录的是数据页的物理变化,服务宕机可用来同步数据(持久性)
-
undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
-
redo log保证了事务的持久性,undo log保证了事务的原子性和一致性
4.mvcc
-
事务中的隔离性是如何保证的呢?
- 锁:排他锁(如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁)
- mvcc:多版本并发控制
-
解释一下MVCC?
全称Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突。
MVCC的具体实现,主要依赖于数据库记录中的隐藏字段、undo log日志、readView。
MVCC-实现原理
1.记录中的隐藏字段
| id | age | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
|---|
| 隐藏字段 | 含义 |
|---|---|
| DB_TRX_ID | 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID |
| DB_ROLL_PTR | 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本 |
| DB_ROW_ID | 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段 |
2.undo log
回滚日志, 在insert、update、delete的时候产生的便于数据回滚的日志。
当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。
而update、delete的时候,产生的undo log日志不仅在回滚时需要,mvcc版本访问也需要,不会立即被删除。
- undo log版本链
不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链条,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
3.read view
ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。
- 当前读 读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:
select ... lock in share mode(共享锁),select ... for update、update、insert、delete(排他锁)都是一种当前读。
-
快照读
简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
-
Read Committed:每次select,都生成一个快照读。
-
Repeatable Read:开启事务后第一个select语句才是快照读的地方。
-
ReadView中包含了四个核心字段:
| 字段 | 含义 |
|---|---|
| m_id | 当前活跃的事务ID集合 |
| min_trx_id | 最小活跃集合ID |
| max_trx_id | 预分配事务ID,当前最大事务ID + 1(因为事务ID是自增的) |
| creator_trx_id | ReadView创建者的事务ID |
例如,对于上面这个事务图,在事务5第一次“查询id为30的记录”时,它的
m_ids是3,4,5(因为事务2已提交)
min_trx_id是3
max_trx_id是6
creator_trx_id是5 简单地说,就是 可以访问已提交的事务数据,或者当前版本的事务数据
-
版本链数据访问规则(trx_id:代表是当前事务ID):
-
trx_id == creator_trx_id ?可以访问该版本 ——>成立,说明数据是当前这个事务更改的。
-
trx_id<min_trx_id ?可以访问该版本 ——>成立,说明数据已经提交了。
-
trx_id>max_trx_id ?不可以访问该版本 ——>成立,说明该事务是在ReadView生成后才开启。
-
min_trx_id <= trx_id <= max_trx_id ?如果trx_id不在m_ids中是可以访问该版本的 ——>成立,说明数据已经提交。
-
不同的隔离级别,生成ReadView的时机不同:
READ COMMITTED:在事务中每一次执行快照读时生成ReadView。 REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
(1)RC隔离级别下,在事务中每一次执行快照读时生成ReadView。
例如,对于上面这个事务图,在事务5第一次“查询id为30的记录”时,它的
ReadView
m_ids: {3,4,5}
min_trx_id: 3
max_trx_id: 6
creator_trx_id: 5
trx_id就是拿版本链中的DB_TRX_ID来依次对比,到2的时候满足第二个条件,读到数据0x0002
不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链条,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
3.read view
ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。
- 当前读 读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:
select ... lock in share mode(共享锁),select ... for update、update、insert、delete(排他锁)都是一种当前读。
-
快照读
简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
-
Read Committed:每次select,都生成一个快照读。
-
Repeatable Read:开启事务后第一个select语句才是快照读的地方。
-
ReadView中包含了四个核心字段:
| 字段 | 含义 |
|---|---|
| m_id | 当前活跃的事务ID集合 |
| min_trx_id | 最小活跃集合ID |
| max_trx_id | 预分配事务ID,当前最大事务ID + 1(因为事务ID是自增的) |
| creator_trx_id | ReadView创建者的事务ID |
例如,对于上面这个事务图,在事务5第一次“查询id为30的记录”时,它的
m_ids是3,4,5(因为事务2已提交)
min_trx_id是3
max_trx_id是6
creator_trx_id是5 简单地说,就是 可以访问已提交的事务数据,或者当前版本的事务数据
-
版本链数据访问规则(trx_id:代表是当前事务ID):
-
trx_id == creator_trx_id ?可以访问该版本 ——>成立,说明数据是当前这个事务更改的。
-
trx_id<min_trx_id ?可以访问该版本 ——>成立,说明数据已经提交了。
-
trx_id>max_trx_id ?不可以访问该版本 ——>成立,说明该事务是在ReadView生成后才开启。
-
min_trx_id <= trx_id <= max_trx_id ?如果trx_id不在m_ids中是可以访问该版本的 ——>成立,说明数据已经提交。
-
不同的隔离级别,生成ReadView的时机不同:
READ COMMITTED:在事务中每一次执行快照读时生成ReadView。 REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
(1)RC隔离级别下,在事务中每一次执行快照读时生成ReadView。
例如,对于上面这个事务图,在事务5第一次“查询id为30的记录”时,它的
ReadView
m_ids: {3,4,5}
min_trx_id: 3
max_trx_id: 6
creator_trx_id: 5
trx_id就是拿版本链中的DB_TRX_ID来依次对比,到2的时候满足第二个条件,读到数据0x0002
ReadView
m_ids: {4,5}
min_trx_id: 4
max_trx_id: 6
creator_trx_id: 5
(2)RR隔离级别下,仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
访问的是事务2
总结
- 好的,事务中的隔离性是如何保证的呢?(你解释一下MVCC)
MySQL中的多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突。他的底层实现主要分为了三个部分:
- 隐藏字段:
- trx_id(事务id),记录每一次操作的事务id,是自增的
- roll_pointer(回滚指针),指向上一个版本的事务版本记录地址
- undo log:
- 回滚日志,存储老版本数据
- 版本链:多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表
-
readView解决的是一个事务查询选择版本的问题
-
根据readView的匹配规则和当前的一些事务id判断该访问那个版本的数据
-
不同的隔离级别快照读是不一样的,最终的访问的结果不一样
-
RC:每一次执行快照读时生成ReadView
RR:仅在事务中第一次执行快照读时生成ReadView,后续复用
三、主从同步原理
MySQL主从复制的核心就是二进制日志
**二进制日志(BINLOG)**记录了所有DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句。
复制分成三步:
-
Master 主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog中。
-
从库读取主库的二进制日志文件Binlog,写入到从库的中继日志 Relay Log。
-
slave重做中继日志中的事件,将改变反映它自己的数据。
四、分库分表
- 你们项目用过分库分表吗?
主库从库分担了访问压力,但解决不了海量数据存储
分库分表的时机:
- 前提,项目业务数据逐渐增多,或业务发展比较迅速(比如:单表的数据量达1000W或20G以后)
- 优化已解决不了性能问题(主从读写分离、查询索引。。)
- IO瓶颈(磁盘IO、网络IO)、CPU瓶颈(聚合查询、连接数太多)
1.垂直分库
垂直分库:以表为依据,根据业务将不同表拆分到不同库中。
特点:
- 按业务对数据分级管理、维护、监控、扩展
- 在高并发下,提高磁盘IO和数据量连接数
2.垂直分表
垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中。
特点:
- 冷热数据分离
- 减少IO过度争抢,两表互不影响
拆分规则:
- 把不常用的字段单独放在一张表
- 把text,blob等大字段拆分出来放在附表中
3.水平分库
水平分库:将一个库的数据拆分到多个库中。
特点:
- 解决了单库大数量,高并发的性能瓶颈问题
- 提高了系统的稳定性和可用性
路由规则:
- 根据id节点取模
- 按id也就是范围路由,节点1(1-100万),节点2(100万-200万)
4.水平分表
水平分表:将一个表的数据拆分到多个表中(可以在同一个库内)。
特点:
- 优化单一表数据量过大而产生的性能问题;
- 避免IO争抢并减少锁表的几率。
分库之后的问题:
-
分布式事务一致性问题
-
跨节点关联查询
-
跨节点分页、排序函数
-
主键避重
分库分表中间件:
-
sharding-sphere
-
mycat
总结
-
你们项目用过分库分表吗
-
业务介绍
1,根据自己简历上的项目,想一个数据量较大业务(请求数多或业务累积大)
2,达到了什么样的量级(单表1000万或超过20G)
- 具体拆分策略
1,水平分库,将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题 sharding-sphere, mycat
2,水平分表,解决单表存储和性能的问题 sharding-sphere, mycat
3,垂直分库,根据业务进行拆分,高并发下提高磁盘IO和网络连接数
4,垂直分表,冷热数据分离,多表互不影响
#面试被问“你的缺点是什么?”怎么答##数据人的面试交流地##面试时最害怕被问到的问题##你觉得哪一届的校招最难?##我的求职总结#
查看12道真题和解析