上一篇回顾了项目中使用到的DB方案,这一篇就集中对其中使用最多的mysql来讲讲平时接触比较少,但是很重要的几个方面。
数据库并发问题
数据库在并发环境下常常会遇到如下几种问题:
- 脏读:事务A读到了事务B未提交的数据
- 不可重复读:事务A两次读取数据,期间事务B更新数据并提交,导致事务A读到的数据前后不一致
- 幻读:事务A对数据分类,事务B插入新数据,事务A再次读取时发现数据新增,就像幻觉一样。和不可重复读的区别在于幻读强调的是插入,即对范围内的选择和新增;而不可重复度指的是更新,尤指特定行的数据
数据库隔离机制
- 读未提交:最弱的隔离机制,上述3个问题都无法解决
- 读已提交:解决脏读,每次select时会取当前版本之前的快照,从而保证不会取到未提交事务的数据,但是无法避免不可重复读问题
- 可重复读:解决不可重复读,通过mvcc和锁来实现。每次事务使用第一次select的快照,在同一事务中保证同一快照,来避免不可重复读问题
- 可串行化:解决幻读,通过锁表来解决。相当于所有读写操作串行,互相阻塞
MVCC原理
MVCC全称是Multiversion concurrency control,多版本并发控制。提供并发访问数据库时数据并发问题的解决方案。如在经典读写问题中,A读数据库,B在事务中修改数据库,那么如何确保A能读到一致的数据就是MVCC关心的问题。
有两种方案,方案1影响运行效率,而方案2开销更低,因此InnoDB也是使用这一机制来做并发控制。
- 基于锁的并发控制,B修改时对数据上锁,保证A不能读取
- MVCC,利用快照,A在读取时读的是特定时刻的快照,不会读到B修改的数据,直到B事务提交,才能读到B修改的内容
InnoDB中的MVCC实现
一个事务在其内部看到的数据总是一致的,这就是MVCC关心的。那么在InnoDB中对写操作是非阻塞的,写操作锁特定的行,具体主要是通过在每行记录后保存两个隐藏的列来实现:
- 一个保存了行的创建时间
- 一个保存了行的过期时间(删除时间)
在RR隔离级别下,对于不同的操作具体的方案如下:
- select操作:
- 只查找版本早于或等于当前事务版本的数据行,从而确保事务读取的行是在事务开始前已存在、或是事务自身操作的记录
- 行的删除标识须为undefined或大于当前事务版本号
- insert操作: 保存新插入行的版本号为当前版本号,删除标识为undefined
- delete操作: 保存当前系统版本号为删除标识
- update操作: 变为insert和delete操作的组合,insert的行保留档期版本号为行版本号,delete保存当前版本号到原来行的删除标识
对于软删除的旧数据,系统会开启后台线程进行清理工作,这一过程被称为purge,它将删除版本号小于当前系统版本号的行。
MySQL中的锁
- record lock:单条索引记录上加锁。实现原理是锁住了索引,而非记录本身。若匹配时不走索引,则会给聚簇索引加锁
- gap lock:索引记录之间的间隙中加锁
- next-key lock:行锁和间隙锁组合起来就被称为next-key lock
索引
innoDB中mysql索引是由B+树来实现的,要理解采用B+树的理由,需要对比不同的树结构。
- 二叉树:不平衡,可能导致树变链表,时间复杂度降低为On
- 二叉搜索树:平衡,但是二叉树本身意味着节点数量增大会导致树增高
- B树:平衡,同时是多叉的,意味着它的高度保持在3~4左右就能支持千万级别的数据量。但是因为在非叶子结点也存放了数据,因此顺序遍历时需要使用中序遍历
- B+树:相比B树,它只在叶子节点上存放数据,同时叶子节点之间增加了横向的指针,这样顺序遍历所有数据会非常容易
聚簇索引 vs 非聚簇索引
- 聚簇索引:叶子节点上存储行数据。使用主索引和辅助索引,主索引上按主键组织,叶子结点上存储了行数据;辅助索引检索时先命中叶子节点,接着通过叶子节点上的主键再到主索引上检索行数据
- 非聚簇索引:叶子节点上不存行数据,而是指向了行数据存储位置。在非主键索引检索时,和主键索引相同,也是利用叶子节点上的指针来命中行数据
SQL优化
为什么要优化?
- 设计
- 字段设计
- 关联表设计
- 范式
- 存储引擎选择
- 索引
- 查询缓存
- 分区
- 水平分隔和垂直分隔
- 集群
- 典型SQL
- 慢查询日志
- profile信息
- 典型的服务器配置
- 压测工具mysqlslap
参考: