MySQL

234 阅读42分钟

1 基本概念简述

1.1 逻辑架构

第一层:客户端通过连接服务,将要执行的sql指令传输过来
第二层:服务器解析并优化sql,生成最终的执行计划并执行
第三层:存储引擎,负责数据的储存和提取

以下为最新版本 MySQL(8.x)的锁机制详解,结合锁类型、实现原理、应用场景及优化策略进行系统梳理:


一、锁的分类与核心机制

1. ​行级锁​(高并发核心)
  • 记录锁(Record Lock)​
    锁定索引记录(主键/唯一索引),确保单行数据不被并发修改。

    SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 对 id=1 加排他锁
    
  • 间隙锁(Gap Lock)​
    锁定索引记录间的“空隙”(如区间 (5,10)),防止其他事务插入新行(解决幻读)。仅在 ​RR 隔离级别生效。

    SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE; -- 锁住 age 在 20-30 的间隙
    
  • 临键锁(Next-Key Lock)​
    默认行锁模式 = ​记录锁 + 间隙锁​(如 (5,10]),同时锁定记录及其前序间隙,彻底解决幻读问题。

2. ​表级锁
  • 意向锁(Intention Locks)​

    • IS 锁​(意向共享):事务计划加行级 S 锁时,先对表加 IS 锁。
    • IX 锁​(意向排他):事务计划加行级 X 锁时,先对表加 IX 锁。
      作用:快速检测表级锁与行级锁的冲突(如 LOCK TABLES ... WRITE 需等待 IX 释放)。
  • 元数据锁(MDL)​
    保护表结构变更(如 ALTER TABLE),读操作加 MDL 读锁,结构变更加 MDL 写锁(互斥阻塞所有操作)。

  • 自增锁(AUTO-INC)​
    控制自增列生成,MySQL 8.0 默认 innodb_autoinc_lock_mode=2(交错模式),提升插入并发性。

3. ​特殊锁
  • 插入意向锁(Insert Intention Lock)​
    插入前声明意向,与间隙锁兼容,避免不必要的阻塞。

  • 用户级锁(User Lock)​
    应用层控制跨会话锁,如分布式任务调度:

    SELECT GET_LOCK('task_lock', 10); -- 等待 10 秒获取锁
    

二、锁的行为影响因素

1. ​隔离级别
隔离级别锁机制特点典型问题
RC(读已提交)​仅加记录锁,无间隙锁幻读可能发生
RR(可重复读)​默认临键锁(防幻读)范围查询锁范围大
串行化全表锁,读操作隐式加 S 锁并发性能最低
2. ​索引设计
  • 主键/唯一索引​:仅加记录锁。
  • 非唯一索引​:加临键锁(锁住索引范围)。
  • 无索引​:退化为表级锁​(UPDATE ... WHERE name='A%' 无索引 → 全表锁)。

三、锁的监控与优化

1. ​锁冲突诊断
-- 查看锁等待关系
SELECT * FROM information_schema.INNODB_LOCK_WAITS; 
SELECT * FROM performance_schema.data_locks;  -- 8.0+ 详细锁信息
2. ​死锁处理
  • 自动检测:回滚代价最小的事务(SHOW ENGINE INNODB STATUS 查看死锁日志)。
  • 编程应对:重试机制(如 Spring @Retryable)。
3. ​优化策略
  • 事务设计​:短事务(减少锁持有时间)、按固定顺序访问资源(避免死锁)。

  • 索引优化​:为高频条件字段建索引(缩小锁定范围)。

  • 参数调整​:

    innodb_lock_wait_timeout = 30  # 锁等待超时(秒)
    innodb_deadlock_detect = ON     # 死锁检测开关
    

四、应用场景示例

  1. 高并发更新​(如库存扣减)

    START TRANSACTION;
    SELECT stock FROM products WHERE id = 100 FOR UPDATE; -- 行级 X 锁
    UPDATE products SET stock = stock - 1 WHERE id = 100;
    COMMIT;
    

    需配合唯一索引避免全表锁。

  2. 批量范围更新
    在 ​RR 级别下,范围更新默认用临键锁,需注意性能影响:

    DELETE FROM orders WHERE create_time < '2025-01-01'; -- 锁定所有符合条件的行及间隙
    

五、MySQL 8.x 锁机制改进

  1. 原子 DDL​:表结构变更(如 ALTER TABLE)崩溃时自动回滚,减少 MDL 阻塞。
  2. 隐藏索引​:测试索引性能时无需删除索引,避免锁表。
  3. 并行查询​:特定场景支持多线程执行,减少锁竞争(如聚合查询)。

掌握锁机制是高性能数据库设计的核心,建议结合业务场景选择隔离级别、索引策略和事务模型,并通过监控工具持续优化锁冲突问题。

1.3 事务

事务保证一组原子性的操作,要么全部成功,要么全部失败。一旦失败,回滚之前的所有操作。MySql采用自动提交,如果不是显式的开启一个事务,则每个查询都作为一个事务。 隔离级别控制了一个事务中的修改,哪些在事务内和事务间是可见的。

四种常见的隔离级别:

  • 未提交读(Read UnCommitted),事务中的修改,即使没提交对其他事务也是可见的。事务可能读取未提交的数据,造成脏读。
  • 提交读(Read Committed),一个事务开始时,只能看见已提交的事务所做的修改。事务未提交之前,所做的修改对其他事务是不可见的。也叫不可重复读,同一个事务多次读取同样记录可能不同。
  • 可重复读(RepeatTable Read),同一个事务中多次读取同样的记录结果时结果相同。
  • 可串行化(Serializable),最高隔离级别,强制事务串行执行。 脏读(读取未提交数据)

A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹,此时由于B事务更改了它,这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据,但事务B良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。

不可重复读(前后多次读取,数据内容不一致)

事务A在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。

幻读(前后多次读取,数据总量不一致)

事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。

不可重复读和幻读到底有什么区别呢?

(1) 不可重复读是读取了其他事务更改的数据,针对update操作 解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。

(2) 幻读是读取了其他事务新增的数据,针对insert和delete操作 解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。

事务类别:

  • 扁平事务
  • 带有保存点的扁平事务
  • 链事务
  • 嵌套事务
  • 分布式事务

image.png

1.4 存储引擎

redolog

image.png

undolog

image.png

image.png 所以说可以根据 undo log 逻辑回滚到之前的样子,就是反着来记录,insert 记录 delete 、delete 记录 insert。 从上图可以看到每个事务自己沉浸在自己的世界里,即使后面有事务修改了数据,但在它自己看来还是之前的值,这就是多版本并发控制,MVCC。 所以利用 undo log 实现 MVCC,满足了非锁定读的需求,提高了并发度,有关 MVCC 下篇单独写,因为这个概念比较重要,面试常问。 还有,undo log 也是需要持久化保护的,所以写 undo log 也会记录相应的 redo log。

MVCC

但是MySql的存储引擎的真实实现不是简单的行级锁,一般都是实现了多版本并发控制(MVCC)。 MVCC是行级锁的变种,多数情况下避免了加锁操作,开销更低。MVCC是通过保存数据的某个时间点快照实现的。 MVCC用来实现读已提交和可重复读,如果没有 MVCC 读写操作之间就会冲突 InnoDB 不会真的存储了多个版本的数据,只是借助 undolog 记录每次写操作的反向操作,所以索引上对应的记录只会有一个版本,即最新版本。只不过可以根据 undolog 中的记录反向操作得到数据的历史版本,所以看起来是多个版本。

image.png 从图中可以得知此时插入的事务ID是1,此时插入会生成一条 undolog ,并且记录上的 roll_pointer 会指向这条 undolog ,而这条 undolog  是一个类型为TRX_UNDO_INSERT_REC的 log,代表是 insert 生成的,里面存储了主键的长度和值(还有其他值,不提)。 所以 InnoDB 可以根据 undolog  里的主键的值,找到这条记录,然后把它删除来实现回滚(复原)的效果。因此可以简单地理解 undolog 里面存储的就是当前操作的反向操作,所以认为里面存了个delete 1 就行。

此时事务1提交,然后另一个 ID 为 5 的事务再执行 update NO where id 1 这个语句,此时的记录和 undolog 就如下图所示:

image.png 而 update 产生的 undolog 不一样,它的类型为 TRX_UNDO_UPD_EXIST_REC

此时事务 5 提交,然后另一个 ID 为 11 的事务执行update Yes where id 1 这个语句,此时的记录和 undolog 就如下图所示:

image.png update 产生的 undolog 不会马上删除,因为可能有别的事务需要访问之前的版本,所以不能删。这样就串成了一个版本链,可以看到记录本身加上两条 undolog,这条 id 为 1 的记录共有三个版本。

版本链搞清楚了,这时候还需要知道一个概念 readView,这个 readView 就是用来判断哪个版本对当前事务可见的,这里有四个概念:

  • creator_trx_id,当前事务ID。
  • m_ids,生成 readView 时还活跃的事务ID集合,也就是已经启动但是还未提交的事务ID列表。
  • min_trx_id,当前活跃ID之中的最小值。
  • max_trx_id,生成 readView 时 InnoDB 将分配给下一个事务的 ID 的值(事务 ID 是递增分配的,越后面申请的事务ID越大)

可重复读和读已提交的 MVCC 判断版本的过程是一模一样的,唯一的差别在生成 readView 上。 读已提交每次查询都会重新生成一个新的 readView ,而可重复读在第一次生成  readView 之后的所有查询都共用同一个 readView 多个事务不就会争抢 undolog,为了提高 undolog 的写入性能,每个事务都有属于自己的 undolog 页面链表,这样就提高了写入并发度啦,再细一点就是 insert 类型的 undolog 和 update 类型的 undolog 属于不同的链表。普通表和临时表各有一条 insert 类型的 undolog 和 update 类型的 undolog ,所以最多一个事务可以有四条 undolog 页面链表。之所以分普通表和临时表是因为普通表的 undolog 写入是需要记录到redolog 中的需要保证崩溃恢复,而临时表则不需要记录,反正就是临时的

buffer poll

顺序访问数据页面到达一定的数量或者一个 extent(页面管理的逻辑分区)中有很多页面都已经被被加载到 Buffer Pool 的时候,Innodb 就会预读相关页面加载到 Buffer Pool 中,预读是好事,但如果用朴素的 LRU 来实现数据的淘汰,预读的数据也会被移动到头部,这样头部原本的热数据就会更靠后了,面临着被淘汰的危机 因此,Innodb 将缓冲池分为了新生代和老年代。默认头部的 63% 为新生代,尾部 37% 为老年代。 当第一次从磁盘加载数据到 Buffer Pool 时,会将数据放置在老年代的头部,而不是新生代的头部,这样即使有预读功能也不会把前面的热数据给顶一下。然后下次访问这个数据的时候,会把数据从老年代移动带新生代的头部 因为全表扫描的缘故,在老年代数据被再次访问的时间与之前被访问的时间间隔超过1s,才会晋升到新生代,否则还是在老年代,这样就不会污染新生代的热数据 这么细节大可不必死记硬背

2 创建时优化

2.1 Schema和数据类型优化

整数:

  • TinyInt,SmallInt,MediumInt,Int,BigInt 使用的存储8,16,24,32,64位存储空间。
  • 使用Unsigned表示不允许负数,可以使正数的上限提高一倍。

实数

  • Float,Double , 支持近似的浮点运算。
  • Decimal,用于存储精确的小数。

字符串

  • VarChar,存储变长的字符串。需要1或2个额外的字节记录字符串的长度。
  • Char,定长,适合存储固定长度的字符串,如MD5值。
  • Blob,Text 为了存储很大的数据而设计的。分别采用二进制和字符的方式。

时间类型

  • DateTime,保存大范围的值,占8个字节。
  • TimeStamp,推荐,与UNIX时间戳相同,占4个字节。

优化建议点:

  • 尽量使用对应的数据类型。比如,不要用字符串类型保存时间,用整型保存IP。
  • 选择更小的数据类型。能用TinyInt不用Int。
  • 标识列(identifier column),建议使用整型,不推荐字符串类型,占用更多空间,而且计算速度比整型慢。
  • 不推荐ORM系统自动生成的Schema,通常具有不注重数据类型,使用很大的VarChar类型,索引利用不合理等问题。
  • 真实场景混用范式和反范式。冗余高查询效率高,插入更新效率低;冗余低插入更新效率高,查询效率低。
  • 创建完全的独立的汇总表\缓存表,定时生成数据,用于用户耗时时间长的操作。对于精确度要求高的汇总操作,可以采用 历史结果+最新记录的结果 来达到快速查询的目的。
  • 数据迁移,表升级的过程中可以使用影子表的方式,通过修改原表的表名,达到保存历史数据,同时不影响新表使用的目的。

2.2 索引

索引包含一个或多个列的值。MySql只能高效的利用索引的最左前缀列。

索引的优势:

  • 减少查询扫描的数据量
  • 避免排序和零时表
  • 将随机IO变为顺序IO(顺序IO的效率高于随机IO)

B-Tree

使用最多的索引类型。Innodb采用B+Tree数据结构来存储数据(叶子节点采用双向链表结构从而方便叶子节点的遍历,适用于范围查询)。不同引擎内部结构不同,InnoDB使用b+tree.数据按顺序存储,每个叶子节点到根的距离相同。

B-Tree索引适用于全键值,键值范围,键前缀查找(最左前缀),支持排序。如果使用AVL树则树的层次太多,引起多次IO,因为数据存储在索引节点上导致索引节点容量有限,无法存储大量索引.

B-Tree索引限制:(根据innodb存储产生的特性)

  • 如果不是按照索引的最左列开始查询,则无法使用索引。
  • 不能跳过索引中的列。如果使用第一列和第三列索引,则只能使用第一列索引。
  • 如果查询中有个范围查询,则其右边的所有列都无法使用索引优化查询。

B树和B+树的区别?为什么用B+树 链接

B-树是专门为外部存储器设计的,如磁盘,它对于读取和写入大块数据有良好的性能,所以一般被用在文件系统及数据库中。**
每个节点有m叉树,m的大小可以根据单个页的大小做对应调整,从而使得一个页可以存储更多的数据,从磁盘中读取一个页可以读到的数据就更多,随机 IO 次数变少,大大提升效率。
B+ 树相比 B 树来说,叶子节点用双向链表相连,范围查询好。
B+树非叶子节点存储的元素更多,使得查询的IO次数更少
所有的查询都要查找到叶子节点,查询性能是稳定的,而B树,每个节点都可以查找到数据,所以不稳定。

哈希索引

只有精确匹配索引的所有列,查询才有效。存储引擎会对所有的索引列计算一个哈希码,哈希索引将所有的哈希码存储在索引中,并保存指向每个数据行的指针。

哈希索引限制: 无法用于排序 不支持部分匹配 只支持等值查询如=,IN(),不支持 < > 优化建议点 注意每种索引的适用范围和适用限制。 索引的列如果是表达式的一部分或者是函数的参数,则失效。 针对特别长的字符串,可以使用前缀索引,根据索引的选择性选择合适的前缀长度。 使用多列索引的时候,可以通过 AND 和 OR 语法连接。 重复索引没必要,如(A,B)和(A)重复。 索引在where条件查询和group by语法查询的时候特别有效。 将范围查询放在条件查询的最后,防止范围查询导致的右边索引失效的问题。 索引最好不要选择过长的字符串,而且索引列也不宜为null。

主键索引

聚簇索引(即统一称为主键索引):将数据存储与索引放到了一块,找到索引也就找到了数据

image.png

缺点 image.png innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值。 image.png

image.png

覆盖索引

只访问索引的查询,只访问索引,无需访问数据行

image.png

image.png

image.png

3 查询时优化

3.1 查询的三个重要指标

响应时间(服务时间,排队时间),扫描的行,返回的行

3.2 查询优化点

1.避免查询无关的行,如使用Select * 返回所有的列
2.切分查询。将一个对服务器压力较大的任务,分解到一个较长的时间中,并分多次执行。如要删除一万条数据,可以分10次执行,每次执行完成后暂停一段时间,再继续执行。过程中可以释放服务器资源给其他任务。
3.分解关联查询。将多表关联查询的一次查询,分解成对单表的多次查询。可以减少锁竞争,查询本身的查询效率也比较高。因为MySql的连接和断开都是轻量级的操作,不会由于查询拆分为多次,造成效率问题。

4.注意count的操作只能统计不为null的列,所以统计总的行数使用count(*)。
group by 按照标识列分组效率高,分组结果不宜出行分组列之外的列。
关联查询延迟关联,可以根据查询条件先缩小各自要查询的范围,再关联。
Limit分页优化。可以根据索引覆盖扫描,再根据索引列关联自身查询其他列。如
SELECT id, NAME, age WHERE student s1 INNER JOIN ( SELECT id FROM student ORDER BY age  LIMIT 50,5 ) AS s2 ON s1.id = s2.id
Union查询默认去重,如果不是业务必须,建议使用效率更高的Union All*

5.条件中的字段类型和表结构类型不一致,mysql会自动加转换函数,导致索引作为函数中的参数失效。
6.like查询前面部分未输入,以%开头无法命中索引。

7.补充25.7版本的新特性:
generated column,就是数据库中这一列由其他列计算而得
CREATE TABLE triangle (sidea DOUBLE, sideb DOUBLE, area DOUBLE AS (sidea * sideb / 2));
insert into triangle(sidea, sideb) values(3, 4);
select * from triangle;
+-------+-------+------+
| sidea | sideb | area |
+-------+-------+------+
|   3  |   4  |  6   |
+-------+-------+------+
支持JSON格式数据,并提供相关内置函数
CREATE TABLE json_test (name JSON);
INSERT INTO json_test VALUES('{"name1": "value1", "name2": "value2"}');
SELECT * FROM json_test WHERE JSON_CONTAINS(name, '$.name1');

关注explain在性能分析中的使用

EXPLAIN SELECT settleId FROM Settle WHERE settleId = "3679"

select_type,有几种值:
simple(表示简单的select,没有union和子查询),
primary(有子查询,最外面的select查询就是primary),
unionunion中的第二个或随后的select查询,不依赖外部查询结果),
dependent unionunion中的第二个或随后的select查询,依赖外部查询结果)

type,有几种值:
system(表仅有一行(=系统表),这是const连接类型的一个特例),
const(常量查询),
ref(非唯一索引访问,只有普通索引),
eq_ref(使用唯一索引或组件查询),
all(全表查询),
index(根据索引查询全表),
range(范围查询)

possible_keys: 表中可能帮助查询的索引
key,选择使用的索引
key_len,使用的索引长度
rows,扫描的行数,越大越不好

extra,有几种值:
Only index(信息从索引中检索出,比扫描表快),
where used(使用where限制),
Using filesort (可能在内存或磁盘排序),
Using temporary(对查询结果排序时使用临时表)

orm层优化

    通过mybatis日志寻找慢查询,sql优化,创建索引
    创建缓存,读写分离
    利用分区表,垂直分表,水平分表
    拆分会带来逻辑,部署和运维的各种复杂度;一般整形值为主的表在千万级以下,字符串为主的表在500万级以下都没有太大问题

关于字段:
    尽量使用tinyInt,smallInt,mediumInt作为整数类型而非int,如果非负责加上unsigned;varchar的长度只分配需要的空间
    使用枚举或整形替代字符串类型,尽量使用timestamp而非datetime
    单表不要有太多字段,建议20个以内,不然可做垂直拆分
    避免使用null字段,很难查询优化且占用额外索引空间
    用整形存储ip

关于查询 SQL:
    可通过开启慢查询日志来找出较慢的 SQL。
    不做列运算:SELECT id WHERE age + 1 = 10,任何对列的操作都将导致表扫描,它包括数据库教程函数、计算表达式等等,查询时要尽可能将操作移至等号右边。
    SQL 语句尽可能简单:一条 SQL只能在一个 CPU 运算;大语句拆小语句,减少锁时间;一条大 SQL 可以堵死整个库。
    不用SELECT *OR 改写成 INOR 的效率是 n 级别,IN 的效率是 log(n) 级别,IN 的个数建议控制在 200 以内。
    不用函数和触发器,在应用程序实现。
    避免 %xxx 式查询。
    少用 JOIN。
    使用同类型进行比较,比如用 '123''123' 比,123123 比。
    尽量避免在 WHERE 子句中使用!=<>操作符,否则引擎将放弃使用索引而进行全表扫描。
    对于连续数值,使用 BETWEEN 不用 INSELECT id FROM t WHERE num BETWEEN 1 AND 5。
    列表数据不要拿全表,要使用 LIMIT 来分页,每页数量也不要太大。

关于索引:
    哪些情况需要创建索引?
    主键自动建立唯一索引
    频繁作为查询条件的字段
    查询中与其他表关联的字段,外键关系建立索引
    单键/组合索引的选择问题,组合索引性价比更高
    查询中排序的字段,排序字段若通过索引去访问,将大大提高排序速度.
    查询中统计或分组字段
哪些情况不需要创建索引
    表记录太少
    经常增删改的字段或表
    索引并不是越多越好,要根据查询有针对性的创建,考虑在 WHEREORDER BY 命令上涉及的列建立索引,可根据 EXPLAIN 来查看是否用了索引还是全表扫描。
    应尽量避免在 WHERE 子句中对字段进行 NULL 值判断,否则将导致引擎放弃使用索引而进行全表扫描。
    值分布很稀少的字段不适合建索引,例如“性别”这种只有两三个值的字段。
    字符字段只建前缀索引。
    字符字段最好不要做主键。
    不用外键,由程序保证约束。
    尽量不用 UNIQUE,由程序保证约束。
    使用多列索引(复合索引支持最左原则)时注意顺序和查询条件保持一致,同时删除不必要的单列索引。
    如(name,age)索引仅支持(name)(name,age)的查找

索引失效:
    1.使用like  并以%开头(以 % 结尾的查询会使用索引)
    2.使用or进行查询,并且or左右两边的列有不存在索引的列
    3.varchar类型的字段进行查询的时候,没有加上引号
hash和btree的区别
    1.hash索引不能使用范围查询,只能使用一些比如 “=”, “<>”, “in”查询。因为hash索引会计算索引列的hash值,计算出后的hash值经过了hash算法与原先的值完全不一样,
    只能进行等值的过滤,不能基于范围的过滤
    2.hash索引遇到大量hash值相同的情况下,性能比btree要差
    3.hash索引并不一定一次可以定位到数据。因为基于索引列计算出的hash值会有重复,重复的话需要扫描hash表进行比较
    4.由于hash索引中存放的是经过hash计算之后的hash值,而且hash值的大小关系并不一定和hash运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算
    5.对于组合索引,hash索引在计算hash值的时候是组合索引键合并后再一起计算hash值,而不是单独计算hash值,所以通过组合索引的前面一个或几个索引键进行查询的时候,hash索引也无法被利用
    6.nnoDB和MyISAM引擎不支持hash索引

读写分离
    也是目前常用的优化,从库读主库写,一般不要采用双主或多主引入很多复杂性,尽量采用文中的其他方案来提高性能。
    同时目前很多拆分的解决方案同时也兼顾考虑了读写分离。

缓存
    缓存可以发生在这些层次:
    MySQL 内部:在系统调优参数介绍了相关设置。
    数据访问层:比如 MyBatis 针对 SQL 语句做缓存,而 Hibernate 可以精确到单个记录,这里缓存的对象主要是持久化对象 Persistence Object。
    应用服务层:可以通过编程手段对缓存做到更精准的控制和更多的实现策略,这里缓存的对象是数据传输对象 Data Transfer Object。
    Web 层:针对 Web 页面做缓存。
    浏览器客户端:用户端的缓存。
可以根据实际情况在一个层次或多个层次结合加入缓存。这里重点介绍下服务层的缓存实现。目前主要有两种方式:
    直写式(Write Through):在数据写入数据库后,同时更新缓存,维持数据库与缓存的一致性,这也是当前大多数应用缓存框架如 Spring Cache 的工作方式。这种实现非常简单,同步好,但效率一般。
    回写式(Write Back):当有数据要写入数据库时,只会更新缓存,然后异步批量的将缓存数据同步到数据库上。 这种实现比较复杂,需要较多的应用逻辑,同时可能会产生数据库与缓存的不同步,但效率非常高。

表分区

    MySQL 在 5.1 版引入的分区是一种简单的水平拆分,用户需要在建表的时候加上分区参数,对应用是透明的无需修改代码。
    对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成,实现分区的代码实际上是通过对一组底层表的对象封装,但对 SQL 层来说是一个完全封装底层的黑盒子。
    MySQL 实现分区的方式也意味着索引也是按照分区的子表定义,没有全局索引。
    用户的 SQL 语句是需要针对分区表做优化,SQL 条件中要带上分区条件的列,从而使查询定位到少量的分区上,否则就会扫描全部分区。
    可以通过 EXPLAIN PARTITIONS 来查看某条 SQL 语句会落在那些分区上,从而进行 SQL 优化。
    如下图 5 条记录落在两个分区上:
    mysql> explain partitions select count(1) from user_partition where id in (1,2,3,4,5);
    +----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
    | id | select_type | table          | partitions | type  | possible_keys | key     | key_len | ref  | rows | Extra                    |
    +----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
    |  1 | SIMPLE      | user_partition | p1,p4      | range | PRIMARY       | PRIMARY | 8       | NULL |    5 | Using where; Using index |
    +----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
    1 row in set (0.00 sec)

分区的好处是:
    可以让单表存储更多的数据。
    分区表的数据更容易维护,可以通清除整个分区批量删除大量数据,也可以增加新的分区来支持新插入的数据。另外,还可以对一个独立分区进行优化、检查、修复等操作。
    部分查询能够从查询条件确定只落在少数分区上,速度会很快。
    分区表的数据还可以分布在不同的物理设备上,从而高效利用多个硬件设备。
    可以使用分区表来避免某些特殊瓶颈,例如 InnoDB 单个索引的互斥访问、 ext3 文件系统的 inode 锁竞争。
    可以备份和恢复单个分区。
分区的限制和缺点:
    一个表最多只能有 1024 个分区。
    如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来。
    分区表无法使用外键约束。
    NULL值会使分区过滤无效。
    所有分区必须使用相同的存储引擎。
分区的类型:
    RANGE 分区:基于属于一个给定连续区间的列值,把多行分配给分区。
    LIST 分区:类似于按 RANGE 分区,区别在于 LIST 分区是基于列值匹配一个离散值集合中的某个值来进行选择。
    HASH 分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。
    这个函数可以包含 MySQL 中有效的、产生非负整数值的任何表达式。
    KEY 分区:类似于按 HASH 分区,区别在于 KEY 分区只支持计算一列或多列,且 MySQL 服务器提供其自身的哈希函数。必须有一列或多列包含整数值。
分区适合的场景有:最适合的场景数据的时间序列性比较强,则可以按时间来分区。
如下所示:
CREATE TABLE members (
    firstname VARCHAR(25) NOT NULL,
    lastname VARCHAR(25) NOT NULL,
    username VARCHAR(16) NOT NULL,
    email VARCHAR(35),
    joined DATE NOT NULL
)PARTITION BY RANGE( YEAR(joined) ) (
    PARTITION p0 VALUES LESS THAN (1960),
    PARTITION p1 VALUES LESS THAN (1970),
    PARTITION p2 VALUES LESS THAN (1980),
    PARTITION p3 VALUES LESS THAN (1990),
    PARTITION p4 VALUES LESS THAN MAXVALUE
);

查询时加上时间范围条件的效率会非常高,同时对于不需要的历史数据能很容易的批量删除。
如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将热点数据单独放在一个分区,让这个分区的数据能够有机会都缓存在内存中,
查询时只访问一个很小的分区表,能够有效使用索引和缓存。
另外 MySQL 有一种早期的简单的分区实现 - 合并表(merge table),限制较多且缺乏优化,不建议使用,应该用新的分区机制来替代。

垂直拆分
    垂直分库是根据数据库里面的数据表的相关性进行拆分,比如:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分可以把用户数据放到用户库、把订单数据放到订单库。
    垂直分表是对数据表进行垂直拆分的一种方式,常见的是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,
    只是字段不一样,使用主键关联。
    垂直拆分的优点是:
    可以使得行数据变小,一个数据块(Block)就能存放更多的数据,在查询时就会减少 I/O 次数(每次查询时读取的 Block 就少)。
    可以达到最大化利用 Cache 的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起。
    数据维护简单。
    缺点是:
    主键出现冗余,需要管理冗余列。
    会引起表连接 JOIN 操作(增加 CPU 开销)可以通过在业务服务器上进行JOIN 来减少数据库压力。
    依然存在单表数据量过大的问题(需要水平拆分)。
    事务处理复杂。
水平拆分
    水平拆分是通过某种策略将数据分片来存储,分库内分表和分库两部分,每片数据会分散到不同的 MySQL 表或库,以达到分布式的效果,能够支持非常大的数据量。
    前面的表分区本质上也是一种特殊的库内分表。
    库内分表,由于没有把表的数据分布到不同的机器上,仅仅是单纯的解决了单一表数据过大的问题。
    因此对于减轻 MySQL 服务器的压力来说,并没有太大的作用,大家还是竞争同一个物理机上的 IO、CPU、网络,这个就要通过分库来解决。
    解决方案
  由于水平拆分牵涉的逻辑比较复杂,当前也有了不少比较成熟的解决方案。这些方案分为两大类:客户端架构和代理架构。
   
客户端架构
    通过修改数据访问层,如 JDBC、Data Source、MyBatis,通过配置来管理多个数据源,直连数据库,并在模块内完成数据的分片整合,一般以 Jar 包的方式呈现。

    客户端架构的优点是:
        应用直连数据库,降低外围系统依赖所带来的宕机风险。
        集成成本低,无需额外运维的组件。
        缺点是:
        限于只能在数据库访问层上做文章,扩展性一般,对于比较复杂的系统可能会力不从心。
        将分片逻辑的压力放在应用服务器上,造成额外风险。

     代理架构
        通过独立的中间件来统一管理所有数据源和数据分片整合,后端数据库集群对前端应用程序透明,需要独立部署和运维代理组件。
        这是一个代理架构的例子:
        代理组件为了分流和防止单点,一般以集群形式存在,同时可能需要 ZooKeeper 之类的服务组件来管理。
        代理架构的优点是:
        能够处理非常复杂的需求,不受数据库访问层原来实现的限制,扩展性强。
        对于应用服务器透明且没有增加任何额外负载。
        缺点是:
        需部署和运维独立的代理中间件,成本高。
        应用需经过代理来连接数据库,网络上多了一跳,性能有损失且有额外风险。

    各方案比较
        目前来说,业界还是有很多的方案可供选择,但应该如何进行选择?我认为,可以按以下思路来考虑:
        确定是使用客户端架构还是代理架构。中小型规模或是比较简单的场景倾向于选择客户端架构,复杂场景或大规模系统倾向选择代理架构。
        具体功能是否满足,比如需要跨节点 ORDER BY,那么支持该功能的优先考虑。
        不考虑一年内没有更新的产品,说明开发停滞,甚至无人维护和技术支持。
        最好按大公司→社区→小公司→个人这样的出品方顺序来选择。
        选择口碑较好的,比如 Github 星数、使用者数量质量和使用者反馈。
        开源的优先,往往项目有特殊需求可能需要改动源代码。

    按照上述思路,推荐以下选择:
        客户端架构:ShardingJDBC
        代理架构:MyCat 或者 Atlas
        兼容 MySQL 且可水平扩展的数据库
        目前也有一些开源数据库兼容 MySQL 协议,如:
        TiDB
        Cubrid 但其工业品质和 MySQL 尚有差距,且需要较大的运维投入,如果想将原始的 MySQL 迁移到可水平扩展的新数据库中,可以考虑一些云数据库:
阿里云 PetaData 阿里云 OceanBase  腾讯云 DCDB

  实际情况中往往会是垂直拆分和水平拆分的结合
    水平拆分的优点是:
    不存在单库大数据和高并发的性能瓶颈。
    应用端改造较少。
    提高了系统的稳定性和负载能力。
    缺点是:
    分片事务一致性难以解决。
    跨节点 JOIN 性能差,逻辑复杂。
    数据多次扩展难度跟维护量极大。

分片原则
    能不分就不分,参考单表优化。
    分片数量尽量少,分片尽量均匀分布在多个数据结点上,因为一个查询 SQL 跨分片越多,则总体性能越差,虽然要好于所有数据在一个分片的结果,
    只在必要的时候进行扩容,增加分片数量。
    分片规则需要慎重选择做好提前规划,分片规则的选择,需要考虑数据的增长模式,数据的访问模式,分片关联性问题,以及分片扩容问题。

最近的分片策略为范围分片,枚举分片,一致性 Hash 分片,这几种分片都有利于扩容。
    尽量不要在一个事务中的 SQL 跨越多个分片,分布式事务一直是个不好处理的问题。
    查询条件尽量优化,尽量避免 Select * 的方式,大量数据结果集下,会消耗大量带宽和 CPU 资源,查询尽量避免返回大量结果集,并且尽量为频繁使用的查询语句建立索引。
    通过数据冗余和表分区来降低跨库JOIN 的可能。
这里特别强调一下分片规则的选择问题,如果某个表的数据有明显的时间特征,比如订单、交易记录等。他们通常比较合适用时间范围分片,因为具有时效性的数据,我们往往关注其近期的数据,查询条件中往往带有时间字段进行过滤。
比较好的方案是,当前活跃的数据,采用跨度比较短的时间段进行分片,而历史性的数据,则采用比较长的跨度存储。
总体上来说,分片的选择是取决于最频繁的查询 SQL 的条件,因为不带任何 Where 语句的查询 SQL,会遍历所有的分片,性能相对最差,
因此这种 SQL 越多,对系统的影响越大,所以我们要尽量避免这种 SQL 的产生。

4.分库分表

sharding jdbc

读写分离

主库不建查询的索引,从库建查询的索引。 官方的MySQL-Proxy、360的Atlas、Mycat 等。 shardingjdbc读写分离示例 image.png 主库提交事务会写binlog,会由一个 dump 线程推送给从库,从库接受之后会有一个I/O线程将其写到 relay log 中,慢慢消化,由 SQL 线程来重放更新数据。

异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的;所以有同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后才会给客户端响应,这样的话性能很差,一般不会选择同步复制。

MySQL 5.7 之后搞了个半同步复制,有个参数可以选择“成功同步几个从库就返回响应。” 比如一共有 3 个从库,我参数配置 1,那么只要有一个从库响应说复制成功了,主库就直接返回响应给客户端,不会等待其他两个从库。

复制的延迟性解决

image.png

分库分表

  • 分表:是为了解决由于单张表数据量多大,而导致查询慢的问题。大致三、四千万行数据就得拆分,不过具体还是得看每一行的数据量大小,有些字段都很小的可能支持更多行数,有些字段大的可能一千万就顶不住了。 订单表 Sharding-Key 用 UserID 来作为 Sharding-Key 商家使用呢,将订单同步到另一张表中给商家使用,这个表按商家 ID 来作为 Sharding-Key, 也可以将数据同步到 ES 中。一般而言这里的数据同步都是异步处理,不会影响正常流程。

  • 分库:是为了解决服务器资源受单机限制,顶不住高并发访问的问题,把请求分配到多台服务器上,降低服务器压力。

5.系统调优参数

可以使用下面几个工具来做基准测试:
sysbench:一个模块化,跨平台以及多线程的性能测试工具。
https://github.com/akopytov/sysbench
iibench-mysql:基于 Java 的 MySQL / Percona / MariaDB 索引进行插入性能测试工具。
https://github.com/tmcallaghan/iibench-mysql
tpcc-mysql:Percona 开发的 TPC-C 测试工具。
https://github.com/Percona-Lab/tpcc-mysql

调优参数内容较多,具体可参考官方文档,这里介绍一些比较重要的参数:

- back_log:back_log 值可以指出在 MySQL 暂时停止回答新请求之前的短时间内多少个请求可以被存在堆栈中。也就是说,如果 MySQL 的连接数据达到 max_connections 时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 back_log,如果等待连接的数量超过 back_log,将不被授予连接资源。可以从默认的 50 升至 500- wait_timeout:数据库连接闲置时间,闲置连接会占用内存资源。可以从默认的 8 小时减到半小时。
- max_user_connection:最大连接数,默认为 0 无上限,最好设一个合理上限。
- thread_concurrency:并发线程数,设为 CPU 核数的两倍。
- skip_name_resolve:禁止对外部连接进行 DNS 解析,消除 DNS 解析时间,但需要所有远程主机用 IP 访问。
- key_buffer_size:索引块的缓存大小,增加会提升索引处理速度,对 MyISAM 表性能影响最大。对于内存 4G 左右,可设为 256M 或 384M,通过查询 show status like 'key_read%',保证 key_reads / key_read_requests 在 0.1% 以下最好。
- innodb_buffer_pool_size:缓存数据块和索引块,对 InnoDB 表性能影响最大。通过查询 show status like 'Innodb_buffer_pool_read%',保证 (Innodb_buffer_pool_read_requests – Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests 越高越好。
- innodb_additional_mem_pool_size:InnoDB 存储引擎用来存放数据字典信息以及一些内部数据结构的内存空间大小。当数据库对象非常多的时候,适当调整该参数的大小以确保所有数据都能存放在内存中提高访问效率,当过小的时候,MySQL 会记录 Warning 信息到数据库的错误日志中,这时就需要调整这个参数大小。
- innodb_log_buffer_size:InnoDB 存储引擎的事务日志所使用的缓冲区,一般来说不建议超过 32MB。
- query_cache_size:缓存 MySQL 中的 ResultSet,也就是一条 SQL 语句执行的结果集,所以仅仅只能针对 Select 语句。当某个表的数据有任何变化,都会导致所有引用了该表的 Select 语句在 Query Cache 中的缓存数据失效.所以,当我们数据变化非常频繁的情况下,使用 Query Cache 可能得不偿失。根据命中率(Qcache_hits/(Qcache_hits+Qcache_inserts)*100))进行调整,一般不建议太大,256MB 可能已经差不多了,大型的配置型静态数据可适当调大。可以通过命令 show status like 'Qcache_%' 查看目前系统 Query Cache 使用大小。
- read_buffer_size:MySQL 读入缓冲区大小。对表进行顺序扫描的请求将分配一个读入缓冲区,MySQL 会为它分配一段内存缓冲区。如果对表的顺序扫描请求非常频繁,可以通过增加该变量值以及内存缓冲区大小提高其性能。
- sort_buffer_size:MySQL 执行排序使用的缓冲大小。如果想要增加 ORDER BY 的速度,首先看是否可以让 MySQL 使用索引而不是额外的排序阶段。如果不能,可以尝试增加 sort_buffer_size 变量的大小。
- read_rnd_buffer_size:MySQL 的随机读缓冲区大小。当按任意顺序读取行时(例如按照排序顺序),将分配一个随机读缓存区。进行排序查询时,MySQL 会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,如果需要排序大量数据,可适当调高该值。 但 MySQL 会为每个客户连接发放该缓冲空间,所以应尽量适当设置该值,以避免内存开销过大。
- record_buffer:每个进行一个顺序扫描的线程为其扫描的每张表分配这个大小的一个缓冲区。如果你做很多顺序扫描,可能想要增加该值。
- thread_cache_size:保存当前没有与连接关联但是准备为后面新的连接服务的线程,可以快速响应连接的线程请求而无需创建新的。
- table_cache:类似于 thread_cache _size,但用来缓存表文件,对 InnoDB 效果不大,主要用于 MyISAM。
- querycachesize:缓存MySQL中的ResultSet,也就是一条SQL语句执行的结果集,所以仅仅只能针对select语句。当某个表的数据有任何任何变化,都会导致所有引用了该表的select语句在Query Cache中的缓存数据失效。所以,当我们的数据变化非常频繁的情况下,使用Query Cache可能会得不偿失。根据命中率(Qcachehits/(Qcachehits+Qcache_inserts)*100))进行调整,一般不建议太大,256MB可能已经差不多了,大型的配置型静态数据可适当调大.可以通过命令show status like 'Qcache_%'查看目前系统Query catch使用大小*

升级硬件 Scale Up,这个不多说了,根据 MySQL 是 CPU 密集型还是 I/O 密集型,通过提升 CPU 和内存、使用 SSD,都能显著提升 MySQL 性能。

NoSQL

事实上很多大表本身对 MySQL 这种 RDBMS 的需求并不大,并不要求 ACID。 可以考虑将这些表迁移到 NoSQL,彻底解决水平扩展问题,例如: 日志类、监控类、统计类数据 非结构化或弱结构化数据 对事务要求不强,且无太多关联操作的数据

其他资料

MYSQL内核
https://mp.weixin.qq.com/s/tmkRAmc1M_Y23ynduBeP3Q
https://www.jianshu.com/p/d7665192aaaf

mysql索引优化:
https://mp.weixin.qq.com/s/KL7tzwwaPvO6NJBJmfUA1A
https://mp.weixin.qq.com/s/Sue10OuK0cB2ApcQhE6PWg

共享锁和排它锁
https://zhuanlan.zhihu.com/p/46502248

mysql8.0参考手册
https://dev.mysql.com/doc/refman/8.0/en/

面试相关
https://mp.weixin.qq.com/s/fIUFg1lueDg0gm7cWJVt4Q