文章目录
- 1、事务的特性
- 2、并发事务问题
- 3、事务的隔离级别
- 4、undo log 和 redo log
- 4.1 底层结构
- 4.2 redo log
- 4.3 undo log
- 5、MVCC
- 5.1 隐式字段
- 5.2 undo log 版本链
- 5.3 ReadView
- 5.4 ReadView的匹配规则实现事务隔离
- 6、MySQL的主从同步原理
- 7、分库分表
- 7.1 垂直分库
- 7.2 垂直分表
- 7.3 水平分库
- 7.4 水平分表
- 7.5 分库分表之后的问题
- 8、面试
1、事务的特性
一组操作,同成功,同失败。开启事务后,所有操作作为一个整体去提交或撤销。
事务的特性:ACID
- 原子性A:事务是最小的操作单元,不可分割
- 一致性C:事务完成后,所有的数据保持一致的状态
- 隔离性I:数据库的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
- 持久性D:落盘,事务一旦提交,改变就是永久的
具体以银行转账的例子描述ACID:A向B转500块,这个操作不可再分割,转账过程,A账户扣除了500,B账户必须增加500,数据要一致。A向B转账的过程中,不能受其他事务和操作的干扰,即隔离性。最后事务提交,数据落盘,即持久化
2、并发事务问题
- 脏读
- 不可重复读
- 幻读
脏读即:事务B更新了数据,但还没commit,A事务就给读走了
解决:将隔离级别由读未提交 -> 改为 -> 读已提交
隔离级别改为读已提交后,发现会出现两次读取的结果不一致的情况 ⇒ 不可重复读即:A刚开始读的id=1的数据,值为x,期间事务B又更新并commit了id=1的这条数据,此时A事务第二次读,发现两次数据不一样
解决:将隔离级别由读已提交 -> 改为 -> 可重复读
改为可重复读的隔离级别后,又出现了幻读 ⇒ 幻读即:事务A先查id=1数据,发现没有。然后准备insert id=1的数据,但期间事务B写入了id=1的数据,事务Ainsert时失败,于是事务A再查id=1数据,发现还是没有(可重复读的隔离级别),事务A自己感觉见鬼了
3、事务的隔离级别
并发事务有脏读、不可重复读、幻读三个问题。以下四种隔离级别,对应的问题如下:(√即存在这个问题,x即不存在这个问题)
总之:
- 读未提交 ⇒ 存在脏读问题
- 读已提交 ⇒ 存在不可重复读问题(Oracle默认的隔离级别)
- 可重复读 ⇒ 存在幻读问题(MySQL默认的隔离级别)
- 串行化 ⇒ 无并发事务的三大问题,但性能不高
4、undo log 和 redo log
4.1 底层结构
缓冲池:
- buffer pool,主内存的一个区域,里面存了磁盘上经常操作的那些数据
- 增删改查时,先去缓冲池找,没找到再去磁盘加载
- 以一定频率把池里的数据刷回磁盘,减少IO次数,提高速度
数据页:
- InnoDB存储引擎管理磁盘的最小单元
- 每页大小默认16KB
- 每页中存的是行数据
该架构下,存在问题:如果从缓冲池刷数据到磁盘时,磁盘所在服务器宕机或IO异常,缓冲池的数据在内存,断电会丢失 ⇒ redo log
4.2 redo log
- 重做日志,包括redo log buffer(存缓冲池中)、redo log file(存磁盘中)
- 用来保证事务的持久性的
增删改的事务提交后,写到缓冲池、缓冲池的redo log、磁盘的redo log,此时缓冲池数据刷回磁盘失败时,可用磁盘的redo log去做恢复。问:你有空往磁盘的redo log写数据,没空直接把增删改的数据写回磁盘吗?⇒ 前者是追加,顺序写,后者是磁盘寻道,随机写,性能差别很大。
最后,如果缓冲池的数据成功刷回磁盘了,那磁盘里的redo log就没用了,会自动定期清理。
rodo log,简言之,用于保证持久化的,当缓冲池数据刷回磁盘失败时,靠redo log恢复。
4.3 undo log
作用:
- 用于做回滚
- 用于MVCC(多版本并发控制)
undo log是逻辑日志,客户端做delete,undo log中记录insert。客户端update,undo log也update,但update成前一版数据。总之就是逆操作。rollback时,执行undo log即可。undo log保证的是事务的一致性和原子性。
5、MVCC
Multi-Version Concurrency Control,多版本并发控制。即维护了一条数据的多个版本,使得并发事务下读写操作没有冲突(读哪个版本,是隔离级别决定的,但多个版本的维护,是MVCC支持的),或者说是保证事务隔离性的。
MVCC的实现,依赖三个东西:
- 隐式字段
- undo log日志
- readView
5.1 隐式字段
每张表会有三个额外字段:
一个字段记录最近操作这条数据的事务ID,一个字段记录上一个版本的数据的内存地址:
5.2 undo log 版本链
insert、update、delete时,产生undo log,用于回滚和版本控制。刚开始的状态:
事务2修改id = 30数据的age为3,得到版本1的数据,表里的指针指向undo log最开始的初始数据:
事务3将id = 30数据的name改为A3:此时,表里存第二个版本在undo log的内存地址,这个地址指向undo log里存版本1的数据
事务4将id = 30数据的age改为10:
不同事务或者相同事务对同一条记录进行修改,针对该记录,在undo log中生成一条数据记录,并由db_roll_prt字段形成版本链表。链表的头部是最近那个版本的数据,链表尾部是最早的原始数据。
5.3 ReadView
- 当前读:读取的是记录的最新版本
- 快照读:读已提交的隔离级别下,每次select,生成一个快照去读。可重复读的隔离级别下,开启事务后的第一个select执行,会记录一个快照,后面每次select都是在这个快照读
ReadView是快照读时,MVCC提取哪个版本数据的依据,记录并维护当前未提交的事务ID。包含字段:
以下图为例,事务5在第一次读数据的时候:活跃事务Id(即未提交的事务)有三个(3、4、5),因此m_ids数量为3,最小事务ID为3,预分配事务ID为6(5+1),creator_trx_id为5
不同的隔离级别,生成 ReadView 的时机不同:
- READ COMMITTED:在事务中每一次执行快照读时生成 ReadView
- REPEATABLE READ:仅在事务中第一次执行快照读时生成 ReadView,后续复用该 ReadView
5.4 ReadView的匹配规则实现事务隔离
前面undo log中生成了一个版本链,而一个事务可以访问哪个版本的数据,由每个版本数据的trx_id和ReadView里的值决定。规则如下(trx_id表示这个版本的数据的db_trx_id):
以上规则很好理解,比如trx_id < min_trx_id,说明产生这个版本数据的事务ID,比当前未提交的所有事务里面最小的事务ID还要小(事务ID小,说明这个事务开启的早,持续时间更久),即这个版本的数据已经提交了,那当前事务自然是可以访问的。相反trx_id > max_trx_id,说明产生这个版本的事务Id,比所有未提交的事务里最年轻的事务,还要年轻,因此,当前事务不可访问这个版本的数据
读已提交的隔离级别下:
分析事务5的两次查询,其readview如下,每次select都会产生一个select:
以第一个readview为例:并发事务产生了四个版本的数据,最近的一条数据trx_id为4,带入右边规则:
4 == 5,不成立
4 < 3,不成立
4 > 6,不成立
3 <= 4 <= 6成立,但4在3,4,5中的一个,整体不成立
都不成立,因此,事务5当前不可以访问这个版本的数据。再看上一个版本的数据,trx_id为3
3 == 5,不成立
3 < 3,不成立
3 > 6,不成立
3 <= 3 <= 6成立,但3在3,4,5中的一个,整体不成立
都不成立,因此,这个版本的数据,事务5当前还是不能访问。再看上一个版本的数据,trx_id为2
2 == 5,不成立
2 < 3,成立
成立,可以访问。根据读已提交的隔离策略,可以看到。读到的正是事务2提交的那一版,符合"读已提交"的策略。
同理,事务5在第二次select时,读已提交策略下会生成了新的ReadView,ReadWiew各个字段如图中所标:
最近的一条数据trx_id为4,带入右边规则:
4 == 5,不成立
4 < 4,不成立
4 > 6,不成立
4 <= 4 <= 6成立,但4在3,4,5中的一个,整体不成立
都不成立,因此,事务5当前不可以访问这个版本的数据。再看上一个版本的数据,trx_id为3
3 == 5,不成立
3 < 4,成立
因此,当前(第二次select时)事务5可以看到DB_TRX_ID为3的数据,正好是事务3提交的那一版本,符合"读已提交"的策略。最后,看下可重复读事务隔离策略下情况:
带入4条规则,可以得出,事务5两次select,可以拿到的版本都是事务2提交的那个版本。
由此,可以看到MVCC保证了MySQL事务隔离性。 当然,事务的隔离性还有锁在处理,比如一个事务获取了一行数据的排他锁,那其他事务就不能再获取该行的其他锁,其他事务如果尝试获取这行数据的共享锁或排他锁,都会被阻塞,直到持有排他锁的事务释放锁或事务结束。
6、MySQL的主从同步原理
一个Java服务,配置连接了主库和从库,其中,主库用于写,从库用于读
主从之间实现数据同步的核心 ⇒ 二进制日志binlog(记录了DDL,如create,以及DML,如insert)
insert一条数据,实现步骤:
- insert后,主库将变更写进Binlog
- 从库有线程IoThread去读主库的Binlog,并将变化写入到从库自己的中继日志Relay Log
- 从库重做中继日志的时间,使用线程SQLThread进行insert
- 同步完成
7、分库分表
主从结构下,分担了读数据的压力,但解决不了海量数据的存储问题。 ⇒ 分库分表
需要分库分表的时机:项目的业务数据越来越多或做的产品突然火了,单表数据量达1000万行或者20G以后。此时,正常的SQL优化或者加索引解决不了性能问题。
拆分策略:
- 垂直分库
- 垂直分表
- 水平分库
- 水平分表
7.1 垂直分库
以表为依据,按业务的不同,将不同业务的表分配到不同的库中。不同的微服务,连接自己的库,如下:
改成多个库,如此,在高并发下,提高了磁盘的IO以及数据库连接数
7.2 垂直分表
以字段为依据,根据字段属性将不同字段拆分到不同表中。拆分规则:
- 把不常用的字段单独放一个表
- 把text、blob等大数据类型的字段分出来放到一个附表
如下,商品订单表,id、name、category、brand、title字段,属于常用的字段,List或者点开购物应用就要展示的数据。而description属于点开某个商品看详情的时候才能用到的字段,且属于大字段,可做如下拆分:将description单独分一个表,注意id做关联(注意,下面垂直分表后,图中画的是分后的两个表在两个库,这个没限制,也可能在同一个库):
如此,冷热数据分离,减少了不必要的IO(在tb_sku表select * 也不会把大字段description从磁盘读出来),两表互不影响。
7.3 水平分库
将一个库的数据拆分到多个库中,表结构一样,存的数据不一样。
正常一个Java微服务连接一个库,查数据时,想成功路由到数据所在的库,可以考虑:
- 根据id节点取模
- 按id也就是范围路由,节点1(1-100万),节点2(100万-200万)
7.4 水平分表
将一个表的数据拆分到多个表中(这几个表也可以放在同一个库)
数据的查询则和水平分库一样,可以考虑取模。如此,可以解决单一表数据量过大而产生的性能问题。
7.5 分库分表之后的问题
- 分布式事务一致性问题(一个业务操作,需要更新多个数据库,控制事务,同成功同失败)
- 跨节点关联查询
- 跨节点分页、排序函数
- 主键避重
⇒ 分库分表后,引入分库分表中间件解决以上问题以及路由问题:
- sharding-sphere
- mycat
最后:
- 水平分库,将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题
- 水平分表,解决单表存储和性能的问题
- 垂直分库,根据业务进行拆分,高并发下提高磁盘IO和网络连接数
- 垂直分表,冷热数据分离,多表互不影响
8、面试