mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 22:23:45 +08:00
mod
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
<audio id="audio" title="33 | MySQL调优之SQL语句:如何写出高性能SQL语句?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4e/85/4e7d99ff3eb1c0a91be47629437e2985.mp3"></audio>
|
||||
|
||||
你好,我是刘超。
|
||||
|
||||
从今天开始,我将带你一起学习MySQL的性能调优。MySQL数据库是互联网公司使用最为频繁的数据库之一,不仅仅因为它开源免费,MySQL卓越的性能、稳定的服务以及活跃的社区都成就了它的核心竞争力。
|
||||
|
||||
我们知道,应用服务与数据库的交互主要是通过SQL语句来实现的。在开发初期,我们更加关注的是使用SQL实现业务功能,然而系统上线后,随着生产环境数据的快速增长,之前写的很多SQL语句就开始暴露出性能问题。
|
||||
|
||||
在这个阶段中,我们应该尽量避免一些慢SQL语句的实现。但话说回来,SQL语句慢的原因千千万,除了一些常规的慢SQL语句可以直接规避,其它的一味去规避也不是办法,我们还要学会如何去分析、定位到其根本原因,并总结一些常用的SQL调优方法,以备不时之需。
|
||||
|
||||
那么今天我们就重点看看慢SQL语句的几种常见诱因,从这点出发,找到最佳方法,开启高性能SQL语句的大门。
|
||||
|
||||
## 慢SQL语句的几种常见诱因
|
||||
|
||||
### 1. 无索引、索引失效导致慢查询
|
||||
|
||||
如果在一张几千万数据的表中以一个没有索引的列作为查询条件,大部分情况下查询会非常耗时,这种查询毫无疑问是一个慢SQL查询。所以对于大数据量的查询,我们需要建立适合的索引来优化查询。
|
||||
|
||||
虽然我们很多时候建立了索引,但在一些特定的场景下,索引还有可能会失效,所以索引失效也是导致慢查询的主要原因之一。针对这点的调优,我会在第34讲中详解。
|
||||
|
||||
### 2. 锁等待
|
||||
|
||||
我们常用的存储引擎有 InnoDB 和 MyISAM,前者支持行锁和表锁,后者只支持表锁。
|
||||
|
||||
如果数据库操作是基于表锁实现的,试想下,如果一张订单表在更新时,需要锁住整张表,那么其它大量数据库操作(包括查询)都将处于等待状态,这将严重影响到系统的并发性能。
|
||||
|
||||
这时,InnoDB 存储引擎支持的行锁更适合高并发场景。但在使用 InnoDB 存储引擎时,我们要特别注意行锁升级为表锁的可能。在批量更新操作时,行锁就很可能会升级为表锁。
|
||||
|
||||
MySQL认为如果对一张表使用大量行锁,会导致事务执行效率下降,从而可能造成其它事务长时间锁等待和更多的锁冲突问题发生,致使性能严重下降,所以MySQL会将行锁升级为表锁。还有,行锁是基于索引加的锁,如果我们在更新操作时,条件索引失效,那么行锁也会升级为表锁。
|
||||
|
||||
因此,基于表锁的数据库操作,会导致SQL阻塞等待,从而影响执行速度。在一些更新操作(insert\update\delete)大于或等于读操作的情况下,MySQL不建议使用MyISAM存储引擎。
|
||||
|
||||
除了锁升级之外,行锁相对表锁来说,虽然粒度更细,并发能力提升了,但也带来了新的问题,那就是死锁。因此,在使用行锁时,我们要注意避免死锁。关于死锁,我还会在第35讲中详解。
|
||||
|
||||
### 3. 不恰当的SQL语句
|
||||
|
||||
使用不恰当的SQL语句也是慢SQL最常见的诱因之一。例如,习惯使用<SELECT *>,<SELECT COUNT(*)> SQL语句,在大数据表中使用<LIMIT M,N>分页查询,以及对非索引字段进行排序等等。
|
||||
|
||||
## 优化SQL语句的步骤
|
||||
|
||||
通常,我们在执行一条SQL语句时,要想知道这个SQL先后查询了哪些表,是否使用了索引,这些数据从哪里获取到,获取到数据遍历了多少行数据等等,我们可以通过EXPLAIN命令来查看这些执行信息。这些执行信息被统称为执行计划。
|
||||
|
||||
### 1. 通过EXPLAIN分析SQL执行计划
|
||||
|
||||
假设现在我们使用EXPLAIN命令查看当前SQL是否使用了索引,先通过SQL EXPLAIN导出相应的执行计划如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bd/f8/bd11fa15122956719289afea2464eff8.jpg" alt="">
|
||||
|
||||
下面对图示中的每一个字段进行一个说明,从中你也能收获到很多零散的知识点。
|
||||
|
||||
- id:每个执行计划都有一个id,如果是一个联合查询,这里还将有多个id。
|
||||
- select_type:表示SELECT查询类型,常见的有SIMPLE(普通查询,即没有联合查询、子查询)、PRIMARY(主查询)、UNION(UNION中后面的查询)、SUBQUERY(子查询)等。
|
||||
- table:当前执行计划查询的表,如果给表起别名了,则显示别名信息。
|
||||
- partitions:访问的分区表信息。
|
||||
- type:表示从表中查询到行所执行的方式,查询方式是SQL优化中一个很重要的指标,结果值从好到差依次是:system > const > eq_ref > ref > range > index > ALL。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8f/0b/8fc6cb3338945524fb09a092f396fa0b.jpg" alt="">
|
||||
|
||||
- system/const:表中只有一行数据匹配,此时根据索引查询一次就能找到对应的数据。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/2b/b5ea0778ff22bdde10a57edfc353712b.jpg" alt="">
|
||||
|
||||
- eq_ref:使用唯一索引扫描,常见于多表连接中使用主键和唯一索引作为关联条件。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d3/50/d390d8c7bb90bdbf26775265ad451c50.jpg" alt="">
|
||||
|
||||
- ref:非唯一索引扫描,还可见于唯一索引最左原则匹配扫描。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/a4/4020416795c991f68fb057b3e6b80ca4.jpg" alt="">
|
||||
|
||||
- range:索引范围扫描,比如,<,>,between等操作。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7f/c7/7f7a40f88150117f6fe0bb56f52da6c7.jpg" alt="">
|
||||
|
||||
- index:索引全表扫描,此时遍历整个索引树。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d3/7b/d3d7221fec38845145ac0f365196427b.jpg" alt="">
|
||||
|
||||
- ALL:表示全表扫描,需要遍历全表来找到对应的行。
|
||||
- possible_keys:可能使用到的索引。
|
||||
- key:实际使用到的索引。
|
||||
- key_len:当前使用的索引的长度。
|
||||
- ref:关联id等信息。
|
||||
- rows:查找到记录所扫描的行数。
|
||||
- filtered:查找到所需记录占总扫描记录数的比例。
|
||||
- Extra:额外的信息。
|
||||
|
||||
### 2. 通过Show Profile分析SQL执行性能
|
||||
|
||||
上述通过 EXPLAIN 分析执行计划,仅仅是停留在分析SQL的外部的执行情况,如果我们想要深入到MySQL内核中,从执行线程的状态和时间来分析的话,这个时候我们就可以选择Profile。
|
||||
|
||||
Profile除了可以分析执行线程的状态和时间,还支持进一步选择ALL、CPU、MEMORY、BLOCK IO、CONTEXT SWITCHES等类型来查询SQL语句在不同系统资源上所消耗的时间。以下是相关命令的注释:
|
||||
|
||||
```
|
||||
SHOW PROFILE [type [, type] ... ]
|
||||
[FOR QUERY n]
|
||||
[LIMIT row_count [OFFSET offset]]
|
||||
|
||||
type参数:
|
||||
| ALL:显示所有开销信息
|
||||
| BLOCK IO:阻塞的输入输出次数
|
||||
| CONTEXT SWITCHES:上下文切换相关开销信息
|
||||
| CPU:显示CPU的相关开销信息
|
||||
| IPC:接收和发送消息的相关开销信息
|
||||
| MEMORY :显示内存相关的开销,目前无用
|
||||
| PAGE FAULTS :显示页面错误相关开销信息
|
||||
| SOURCE :列出相应操作对应的函数名及其在源码中的调用位置(行数)
|
||||
| SWAPS:显示swap交换次数的相关开销信息
|
||||
|
||||
```
|
||||
|
||||
值得注意的是,MySQL是在5.0.37版本之后才支持Show Profile功能的,如果你不太确定的话,可以通过select @@have_profiling查询是否支持该功能,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/76/45/76a42789a838dfd6b1735c41dd9f8c45.jpg" alt="">
|
||||
|
||||
最新的MySQL版本是默认开启Show Profile功能的,但在之前的旧版本中是默认关闭该功能的,你可以通过set语句在Session级别开启该功能:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/84/91/840fbe1ecdf7526fdc818f4639e22091.jpg" alt="">
|
||||
|
||||
Show Profiles只显示最近发给服务器的SQL语句,默认情况下是记录最近已执行的15条记录,我们可以重新设置profiling_history_size增大该存储记录,最大值为100。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/54/4f/5488fde01df647508d60b9a77cd1f14f.jpg" alt="">
|
||||
|
||||
获取到Query_ID之后,我们再通过Show Profile for Query ID语句,就能够查看到对应Query_ID的SQL语句在执行过程中线程的每个状态所消耗的时间了:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/23/dc7e4046ddd22438c21690e5bc38c123.jpg" alt="">
|
||||
|
||||
通过以上分析可知:SELECT COUNT(*) FROM `order`; SQL语句在Sending data状态所消耗的时间最长,这是因为在该状态下,MySQL线程开始读取数据并返回到客户端,此时有大量磁盘I/O操作。
|
||||
|
||||
## 常用的SQL优化
|
||||
|
||||
在使用一些常规的SQL时,如果我们通过一些方法和技巧来优化这些SQL的实现,在性能上就会比使用常规通用的实现方式更加优越,甚至可以将SQL语句的性能提升到另一个数量级。
|
||||
|
||||
### 1. 优化分页查询
|
||||
|
||||
通常我们是使用<LIMIT M,N> +合适的order by来实现分页查询,这种实现方式在没有任何索引条件支持的情况下,需要做大量的文件排序操作(file sort),性能将会非常得糟糕。如果有对应的索引,通常刚开始的分页查询效率会比较理想,但越往后,分页查询的性能就越差。
|
||||
|
||||
这是因为我们在使用LIMIT的时候,偏移量M在分页越靠后的时候,值就越大,数据库检索的数据也就越多。例如 LIMIT 10000,10这样的查询,数据库需要查询10010条记录,最后返回10条记录。也就是说将会有10000条记录被查询出来没有被使用到。
|
||||
|
||||
我们模拟一张10万数量级的order表,进行以下分页查询:
|
||||
|
||||
```
|
||||
select * from `demo`.`order` order by order_no limit 10000, 20;
|
||||
|
||||
```
|
||||
|
||||
通过EXPLAIN分析可知:该查询使用到了索引,扫描行数为10020行,但所用查询时间为0.018s,相对来说时间偏长了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/80/fe/80efe0ba8feb86baa20834fd48c302fe.jpg" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/58/1c/58e2377b2adcded4c454d410bbab7d1c.jpg" alt="">
|
||||
|
||||
- 利用子查询优化分页查询
|
||||
|
||||
以上分页查询的问题在于,我们查询获取的10020行数据结果都返回给我们了,我们能否先查询出所需要的20行数据中的最小ID值,然后通过偏移量返回所需要的20行数据给我们呢?我们可以通过索引覆盖扫描,使用子查询的方式来实现分页查询:
|
||||
|
||||
```
|
||||
select * from `demo`.`order` where id> (select id from `demo`.`order` order by order_no limit 10000, 1) limit 20;
|
||||
|
||||
```
|
||||
|
||||
通过EXPLAIN分析可知:子查询遍历索引的范围跟上一个查询差不多,而主查询扫描了更多的行数,但执行时间却减少了,只有0.004s。这就是因为返回行数只有20行了,执行效率得到了明显的提升。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/10/2e/10e46817482166d205f319cd0512942e.jpg" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/bb/492ddbbe2ef47d63a6dc797fd44c16bb.jpg" alt="">
|
||||
|
||||
### 2. 优化SELECT COUNT(*)
|
||||
|
||||
COUNT()是一个聚合函数,主要用来统计行数,有时候也用来统计某一列的行数量(不统计NULL值的行)。我们平时最常用的就是COUNT(*)和COUNT(1)这两种方式了,其实两者没有明显的区别,在拥有主键的情况下,它们都是利用主键列实现了行数的统计。
|
||||
|
||||
但COUNT()函数在MyISAM和InnoDB存储引擎所执行的原理是不一样的,通常在没有任何查询条件下的COUNT(*),MyISAM的查询速度要明显快于InnoDB。
|
||||
|
||||
这是因为MyISAM存储引擎记录的是整个表的行数,在COUNT(*)查询操作时无需遍历表计算,直接获取该值即可。而在InnoDB存储引擎中就需要扫描表来统计具体的行数。而当带上where条件语句之后,MyISAM跟InnoDB就没有区别了,它们都需要扫描表来进行行数的统计。
|
||||
|
||||
如果对一张大表经常做SELECT COUNT(*)操作,这肯定是不明智的。那么我们该如何对大表的COUNT()进行优化呢?
|
||||
|
||||
- 使用近似值
|
||||
|
||||
有时候某些业务场景并不需要返回一个精确的COUNT值,此时我们可以使用近似值来代替。我们可以使用EXPLAIN对表进行估算,要知道,执行EXPLAIN并不会真正去执行查询,而是返回一个估算的近似值。
|
||||
|
||||
- 增加汇总统计
|
||||
|
||||
如果需要一个精确的COUNT值,我们可以额外新增一个汇总统计表或者缓存字段来统计需要的COUNT值,这种方式在新增和删除时有一定的成本,但却可以大大提升COUNT()的性能。
|
||||
|
||||
### 3. 优化SELECT *
|
||||
|
||||
我曾经看过很多同事习惯在只查询一两个字段时,都使用select * from table where xxx这样的SQL语句,这种写法在特定的环境下会存在一定的性能损耗。
|
||||
|
||||
MySQL常用的存储引擎有MyISAM和InnoDB,其中InnoDB在默认创建主键时会创建主键索引,而主键索引属于聚簇索引,即在存储数据时,索引是基于B +树构成的,具体的行数据则存储在叶子节点。
|
||||
|
||||
而MyISAM默认创建的主键索引、二级索引以及InnoDB的二级索引都属于非聚簇索引,即在存储数据时,索引是基于B +树构成的,而叶子节点存储的是主键值。
|
||||
|
||||
假设我们的订单表是基于InnoDB存储引擎创建的,且存在order_no、status两列组成的组合索引。此时,我们需要根据订单号查询一张订单表的status,如果我们使用select * from order where order_no='xxx’来查询,则先会查询组合索引,通过组合索引获取到主键ID,再通过主键ID去主键索引中获取对应行所有列的值。
|
||||
|
||||
如果我们使用select order_no, status from order where order_no='xxx’来查询,则只会查询组合索引,通过组合索引获取到对应的order_no和status的值。如果你对这些索引还不够熟悉,请重点关注之后的第34讲,那一讲会详述数据库索引的相关内容。
|
||||
|
||||
## 总结
|
||||
|
||||
在开发中,我们要尽量写出高性能的SQL语句,但也无法避免一些慢SQL语句的出现,或因为疏漏,或因为实际生产环境与开发环境有所区别,这些都是诱因。面对这种情况,我们可以打开慢SQL配置项,记录下都有哪些SQL超过了预期的最大执行时间。首先,我们可以通过以下命令行查询是否开启了记录慢SQL的功能,以及最大的执行时间是多少:
|
||||
|
||||
```
|
||||
Show variables like 'slow_query%';
|
||||
Show variables like 'long_query_time';
|
||||
|
||||
```
|
||||
|
||||
如果没有开启,我们可以通过以下设置来开启:
|
||||
|
||||
```
|
||||
set global slow_query_log='ON'; //开启慢SQL日志
|
||||
set global slow_query_log_file='/var/lib/mysql/test-slow.log';//记录日志地址
|
||||
set global long_query_time=1;//最大执行时间
|
||||
|
||||
```
|
||||
|
||||
除此之外,很多数据库连接池中间件也有分析慢SQL的功能。总之,我们要在编程中避免低性能的SQL操作出现,除了要具备一些常用的SQL优化技巧之外,还要充分利用一些SQL工具,实现SQL性能分析与监控。
|
||||
|
||||
## 思考题
|
||||
|
||||
假设有一张订单表order,主要包含了主键订单编码order_no、订单状态status、提交时间create_time等列,并且创建了status列索引和create_time列索引。此时通过创建时间降序获取状态为1的订单编码,以下是具体实现代码:
|
||||
|
||||
```
|
||||
select order_no from order where status =1 order by create_time desc
|
||||
|
||||
```
|
||||
|
||||
你知道其中的问题所在吗?我们又该如何优化?
|
||||
|
||||
期待在留言区看到你的答案。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<audio id="audio" title="34 | MySQL调优之事务:高并发场景下的数据库事务调优" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/07/cd/07d186609c9f61cb4a4c3be85dff2bcd.mp3"></audio>
|
||||
|
||||
你好,我是刘超。
|
||||
|
||||
数据库事务是数据库系统执行过程中的一个逻辑处理单元,保证一个数据库操作要么成功,要么失败。谈到他,就不得不提ACID属性了。数据库事务具有以下四个基本属性:原子性(Atomicity)、一致性(Consistent)、隔离性(Isolation)以及持久性(Durable)。正是这些特性,才保证了数据库事务的安全性。而在MySQL中,鉴于MyISAM存储引擎不支持事务,所以接下来的内容都是在InnoDB存储引擎的基础上进行讲解的。
|
||||
|
||||
我们知道,在Java并发编程中,可以多线程并发执行程序,然而并发虽然提高了程序的执行效率,却给程序带来了线程安全问题。事务跟多线程一样,为了提高数据库处理事务的吞吐量,数据库同样支持并发事务,而在并发运行中,同样也存在着安全性问题,例如,修改数据丢失,读取数据不一致等。
|
||||
|
||||
在数据库事务中,事务的隔离是解决并发事务问题的关键, 今天我们就重点了解下事务隔离的实现原理,以及如何优化事务隔离带来的性能问题。
|
||||
|
||||
## 并发事务带来的问题
|
||||
|
||||
我们可以通过以下几个例子来了解下并发事务带来的几个问题:
|
||||
|
||||
1.数据丢失
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/db/7d/db7d28a1f27d46cf534064ab4e74f47d.jpg" alt="">
|
||||
|
||||
2.脏读
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d7/4c/d717c7e782620d2e46beb070dbc8154c.jpg" alt="">
|
||||
|
||||
3.不可重复读
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/61/9a/6173739ee9a5d7e26c8b00f2ed8d9e9a.jpg" alt="">
|
||||
|
||||
4.幻读
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/b6/280826363e1d5a3e64529dfd3443e5b6.jpg" alt="">
|
||||
|
||||
## 事务隔离解决并发问题
|
||||
|
||||
以上 4 个并发事务带来的问题,其中,数据丢失可以基于数据库中的悲观锁来避免发生,即在查询时通过在事务中使用 select xx for update 语句来实现一个排他锁,保证在该事务结束之前其他事务无法更新该数据。
|
||||
|
||||
当然,我们也可以基于乐观锁来避免,即将某一字段作为版本号,如果更新时的版本号跟之前的版本一致,则更新,否则更新失败。剩下 3 个问题,其实是数据库读一致性造成的,需要数据库提供一定的事务隔离机制来解决。
|
||||
|
||||
我们通过加锁的方式,可以实现不同的事务隔离机制。在了解事务隔离机制之前,我们不妨先来了解下MySQL都有哪些锁机制。
|
||||
|
||||
InnoDB实现了两种类型的锁机制:共享锁(S)和排他锁(X)。共享锁允许一个事务读数据,不允许修改数据,如果其他事务要再对该行加锁,只能加共享锁;排他锁是修改数据时加的锁,可以读取和修改数据,一旦一个事务对该行数据加锁,其他事务将不能再对该数据加任务锁。
|
||||
|
||||
**熟悉了以上InnoDB行锁的实现原理,我们就可以更清楚地理解下面的内容。**
|
||||
|
||||
在操作数据的事务中,不同的锁机制会产生以下几种不同的事务隔离级别,不同的隔离级别分别可以解决并发事务产生的几个问题,对应如下:
|
||||
|
||||
**未提交读(Read Uncommitted):**在事务A读取数据时,事务B读取数据加了共享锁,修改数据时加了排它锁。这种隔离级别,会导致脏读、不可重复读以及幻读。
|
||||
|
||||
**已提交读(Read Committed):**在事务A读取数据时增加了共享锁,一旦读取,立即释放锁,事务B读取修改数据时增加了行级排他锁,直到事务结束才释放锁。也就是说,事务A在读取数据时,事务B只能读取数据,不能修改。当事务A读取到数据后,事务B才能修改。这种隔离级别,可以避免脏读,但依然存在不可重复读以及幻读的问题。
|
||||
|
||||
**可重复读(Repeatable Read):**在事务A读取数据时增加了共享锁,事务结束,才释放锁,事务B读取修改数据时增加了行级排他锁,直到事务结束才释放锁。也就是说,事务A在没有结束事务时,事务B只能读取数据,不能修改。当事务A结束事务,事务B才能修改。这种隔离级别,可以避免脏读、不可重复读,但依然存在幻读的问题。
|
||||
|
||||
**可序列化(Serializable):**在事务A读取数据时增加了共享锁,事务结束,才释放锁,事务B读取修改数据时增加了表级排他锁,直到事务结束才释放锁。可序列化解决了脏读、不可重复读、幻读等问题,但隔离级别越来越高的同时,并发性会越来越低。
|
||||
|
||||
InnoDB中的RC和RR隔离事务是基于多版本并发控制(MVCC)实现高性能事务。一旦数据被加上排他锁,其他事务将无法加入共享锁,且处于阻塞等待状态,如果一张表有大量的请求,这样的性能将是无法支持的。
|
||||
|
||||
MVCC对普通的 Select 不加锁,如果读取的数据正在执行Delete或Update操作,这时读取操作不会等待排它锁的释放,而是直接利用MVCC读取该行的数据快照(数据快照是指在该行的之前版本的数据,而数据快照的版本是基于undo实现的,undo是用来做事务回滚的,记录了回滚的不同版本的行记录)。MVCC避免了对数据重复加锁的过程,大大提高了读操作的性能。
|
||||
|
||||
## 锁具体实现算法
|
||||
|
||||
我们知道,InnoDB既实现了行锁,也实现了表锁。行锁是通过索引实现的,如果不通过索引条件检索数据,那么InnoDB将对表中所有的记录进行加锁,其实就是升级为表锁了。
|
||||
|
||||
行锁的具体实现算法有三种:record lock、gap lock以及next-key lock。record lock是专门对索引项加锁;gap lock是对索引项之间的间隙加锁;next-key lock则是前面两种的组合,对索引项以其之间的间隙加锁。
|
||||
|
||||
只在可重复读或以上隔离级别下的特定操作才会取得gap lock或next-key lock,在Select 、Update和Delete时,除了基于唯一索引的查询之外,其他索引查询时都会获取gap lock或next-key lock,即锁住其扫描的范围。
|
||||
|
||||
## 优化高并发事务
|
||||
|
||||
通过以上讲解,相信你对事务、锁以及隔离级别已经有了一个透彻的了解了。清楚了问题,我们就可以聊聊高并发场景下的事务到底该如何调优了。
|
||||
|
||||
### 1. 结合业务场景,使用低级别事务隔离
|
||||
|
||||
在高并发业务中,为了保证业务数据的一致性,操作数据库时往往会使用到不同级别的事务隔离。隔离级别越高,并发性能就越低。
|
||||
|
||||
那换到业务场景中,我们如何判断用哪种隔离级别更合适呢?我们可以通过两个简单的业务来说下其中的选择方法。
|
||||
|
||||
我们在修改用户最后登录时间的业务场景中,这里对查询用户的登录时间没有特别严格的准确性要求,而修改用户登录信息只有用户自己登录时才会修改,不存在一个事务提交的信息被覆盖的可能。所以我们允许该业务使用最低隔离级别。
|
||||
|
||||
而如果是账户中的余额或积分的消费,就存在多个客户端同时消费一个账户的情况,此时我们应该选择RR级别来保证一旦有一个客户端在对账户进行消费,其他客户端就不可能对该账户同时进行消费了。
|
||||
|
||||
### 2. 避免行锁升级表锁
|
||||
|
||||
前面讲了,在InnoDB中,行锁是通过索引实现的,如果不通过索引条件检索数据,行锁将会升级到表锁。我们知道,表锁是会严重影响到整张表的操作性能的,所以我们应该避免他。
|
||||
|
||||
### 3. 控制事务的大小,减少锁定的资源量和锁定时间长度
|
||||
|
||||
你是否遇到过以下SQL异常呢?在抢购系统的日志中,在活动区间,我们经常可以看到这种异常日志:
|
||||
|
||||
```
|
||||
MySQLQueryInterruptedException: Query execution was interrupted
|
||||
|
||||
```
|
||||
|
||||
由于在抢购提交订单中开启了事务,在高并发时对一条记录进行更新的情况下,由于更新记录所在的事务还可能存在其他操作,导致一个事务比较长,当有大量请求进入时,就可能导致一些请求同时进入到事务中。
|
||||
|
||||
又因为锁的竞争是不公平的,当多个事务同时对一条记录进行更新时,极端情况下,一个更新操作进去排队系统后,可能会一直拿不到锁,最后因超时被系统打断踢出。
|
||||
|
||||
在用户购买商品时,首先我们需要查询库存余额,再新建一个订单,并扣除相应的库存。这一系列操作是处于同一个事务的。
|
||||
|
||||
以上业务若是在两种不同的执行顺序下,其结果都是一样的,但在事务性能方面却不一样:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0c/27/0c60d5685aa881cf66be43c6c4529927.jpg" alt="">
|
||||
|
||||
这是因为,虽然这些操作在同一个事务,但锁的申请在不同时间,只有当其他操作都执行完,才会释放所有锁。因为扣除库存是更新操作,属于行锁,这将会影响到其他操作该数据的事务,所以我们应该尽量避免长时间地持有该锁,尽快释放该锁。
|
||||
|
||||
又因为先新建订单和先扣除库存都不会影响业务,所以我们可以将扣除库存操作放到最后,也就是使用执行顺序1,以此尽量减小锁的持有时间。
|
||||
|
||||
## 总结
|
||||
|
||||
其实MySQL的并发事务调优和Java的多线程编程调优非常类似,都是可以通过减小锁粒度和减少锁的持有时间进行调优。在MySQL的并发事务调优中,我们尽量在可以使用低事务隔离级别的业务场景中,避免使用高事务隔离级别。
|
||||
|
||||
在功能业务开发时,开发人员往往会为了追求开发速度,习惯使用默认的参数设置来实现业务功能。例如,在service方法中,你可能习惯默认使用transaction,很少再手动变更事务隔离级别。但要知道,transaction默认是RR事务隔离级别,在某些业务场景下,可能并不合适。因此,我们还是要结合具体的业务场景,进行考虑。
|
||||
|
||||
## 思考题
|
||||
|
||||
以上我们主要了解了锁实现事务的隔离性,你知道InnoDB是如何实现原子性、一致性和持久性的吗?
|
||||
|
||||
期待在留言区看到你的见解。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。
|
||||
|
||||
|
||||
145
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/35 | MySQL调优之索引:索引的失效与优化.md
Normal file
145
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/35 | MySQL调优之索引:索引的失效与优化.md
Normal file
@@ -0,0 +1,145 @@
|
||||
<audio id="audio" title="35 | MySQL调优之索引:索引的失效与优化" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7c/ba/7cdd92613410ae7756d12d84dddd16ba.mp3"></audio>
|
||||
|
||||
你好,我是刘超。
|
||||
|
||||
不知道你是否跟我有过同样的经历,那就是作为一个开发工程师,经常被DBA叫过去“批评”,而最常见的就是申请创建新的索引或发现慢SQL日志了。
|
||||
|
||||
记得之前有一次迭代一个业务模块的开发,涉及到了一个新的查询业务,需要根据商品类型、订单状态筛选出需要的订单,并以订单时间进行排序。由于sku的索引已经存在了,我在完成业务开发之后,提交了一个创建status的索引的需求,理由是SQL查询需要使用到这两个索引:
|
||||
|
||||
>
|
||||
select * from order where status =1 and sku=10001 order by create_time asc
|
||||
|
||||
|
||||
然而,DBA很快就将这个需求驳回了,并给出了重建一个sku、status以及create_time组合索引的建议,查询顺序也改成了 sku=10001 and status=1。当时我是知道为什么要重建组合索引,但却无法理解为什么要添加create_time这列进行组合。
|
||||
|
||||
从执行计划中,我们可以发现使用到了索引,那为什么DBA还要求将create_time这一列加入到组合索引中呢?这个问题我们在[第33讲](https://time.geekbang.org/column/article/113440)中提到过,相信你也已经知道答案了。通过故事我们可以发现索引知识在平时开发时的重要性,然而它又很容易被我们忽略,所以今天我们就来详细聊一聊索引。
|
||||
|
||||
## MySQL索引存储结构
|
||||
|
||||
索引是优化数据库查询最重要的方式之一,它是在MySQL的存储引擎层中实现的,所以每一种存储引擎对应的索引不一定相同。我们可以通过下面这张表格,看看不同的存储引擎分别支持哪种索引类型:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f6/1b/f6dc2c083b30377628b3699322f6611b.jpg" alt="">
|
||||
|
||||
B+Tree索引和Hash索引是我们比较常用的两个索引数据存储结构,B+Tree索引是通过B+树实现的,是有序排列存储,所以在排序和范围查找方面都比较有优势。如果你对B+Tree索引不够了解,可以通过该[链接](https://zhuanlan.zhihu.com/p/27700617)了解下它的数据结构原理。
|
||||
|
||||
Hash索引相对简单些,只有Memory存储引擎支持Hash索引。Hash索引适合key-value键值对查询,无论表数据多大,查询数据的复杂度都是O(1),且直接通过Hash索引查询的性能比其它索引都要优越。
|
||||
|
||||
在创建表时,无论使用InnoDB还是MyISAM存储引擎,默认都会创建一个主键索引,而创建的主键索引默认使用的是B+Tree索引。不过虽然这两个存储引擎都支持B+Tree索引,但它们在具体的数据存储结构方面却有所不同。
|
||||
|
||||
InnoDB默认创建的主键索引是聚簇索引(Clustered Index),其它索引都属于辅助索引(Secondary Index),也被称为二级索引或非聚簇索引。接下来我们通过一个简单的例子,说明下这两种索引在存储数据中的具体实现。
|
||||
|
||||
首先创建一张商品表,如下:
|
||||
|
||||
```
|
||||
CREATE TABLE `merchandise` (
|
||||
`id` int(11) NOT NULL,
|
||||
`serial_no` varchar(20) DEFAULT NULL,
|
||||
`name` varchar(255) DEFAULT NULL,
|
||||
`unit_price` decimal(10, 2) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
```
|
||||
|
||||
然后新增了以下几行数据,如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/52/c8/524d7510e8c3ec264f274798d72b99c8.jpg" alt="">
|
||||
|
||||
如果我们使用的是MyISAM存储引擎,由于MyISAM使用的是辅助索引,索引中每一个叶子节点仅仅记录的是每行数据的物理地址,即行指针,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/43/eacb96b876a4db6f3f020bd1efd39243.jpg" alt="">
|
||||
|
||||
如果我们使用的是InnoDB存储引擎,由于InnoDB使用的是聚簇索引,聚簇索引中的叶子节点则记录了主键值、事务id、用于事务和MVCC的回流指针以及所有的剩余列,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9a/42/9a98b5abb7ff82530b02c6aeb15b6242.jpg" alt="">
|
||||
|
||||
基于上面的图示,如果我们需要根据商品编码查询商品,我们就需要将商品编码serial_no列作为一个索引列。此时创建的索引是一个辅助索引,与MyISAM存储引擎的主键索引的存储方式是一致的,但叶子节点存储的就不是行指针了,而是主键值,并以此来作为指向行的指针。这样的好处就是当行发生移动或者数据分裂时,不用再维护索引的变更。
|
||||
|
||||
如果我们使用主键索引查询商品,则会按照B+树的索引找到对应的叶子节点,直接获取到行数据:
|
||||
|
||||
>
|
||||
select * from merchandise where id=7
|
||||
|
||||
|
||||
如果我们使用商品编码查询商品,即使用辅助索引进行查询,则会先检索辅助索引中的B+树的serial_no,找到对应的叶子节点,获取主键值,然后再通过聚簇索引中的B+树检索到对应的叶子节点,然后获取整行数据。这个过程叫做回表。
|
||||
|
||||
在了解了索引的实现原理后,我们再来详细了解下平时建立和使用索引时,都有哪些调优方法呢?
|
||||
|
||||
## 1.覆盖索引优化查询
|
||||
|
||||
假设我们只需要查询商品的名称、价格信息,我们有什么方式来避免回表呢?我们可以建立一个组合索引,即商品编码、名称、价格作为一个组合索引。如果索引中存在这些数据,查询将不会再次检索主键索引,从而避免回表。
|
||||
|
||||
从辅助索引中查询得到记录,而不需要通过聚簇索引查询获得,MySQL中将其称为覆盖索引。使用覆盖索引的好处很明显,我们不需要查询出包含整行记录的所有信息,因此可以减少大量的I/O操作。
|
||||
|
||||
通常在InnoDB中,除了查询部分字段可以使用覆盖索引来优化查询性能之外,统计数量也会用到。例如,在[第32讲](https://time.geekbang.org/column/article/113440)我们讲 SELECT COUNT(*)时,如果不存在辅助索引,此时会通过查询聚簇索引来统计行数,如果此时正好存在一个辅助索引,则会通过查询辅助索引来统计行数,减少I/O操作。
|
||||
|
||||
通过EXPLAIN,我们可以看到 InnoDB 存储引擎使用了idx_order索引列来统计行数,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c6/6f/c689b2fd284c19fd2049b1f73091226f.jpg" alt="">
|
||||
|
||||
## 2.自增字段作主键优化查询
|
||||
|
||||
上面我们讲了 InnoDB 创建主键索引默认为聚簇索引,数据被存放在了B+树的叶子节点上。也就是说,同一个叶子节点内的各个数据是按主键顺序存放的,因此,每当有一条新的数据插入时,数据库会根据主键将其插入到对应的叶子节点中。
|
||||
|
||||
如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为不需要重新移动数据,因此这种插入数据的方法效率非常高。
|
||||
|
||||
如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为页分裂。页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率。
|
||||
|
||||
因此,在使用InnoDB存储引擎时,如果没有特别的业务需求,建议使用自增字段作为主键。
|
||||
|
||||
## 3.前缀索引优化
|
||||
|
||||
前缀索引顾名思义就是使用某个字段中字符串的前几个字符建立索引,那我们为什么需要使用前缀来建立索引呢?
|
||||
|
||||
我们知道,索引文件是存储在磁盘中的,而磁盘中最小分配单元是页,通常一个页的默认大小为16KB,假设我们建立的索引的每个索引值大小为2KB,则在一个页中,我们能记录8个索引值,假设我们有8000行记录,则需要1000个页来存储索引。如果我们使用该索引查询数据,可能需要遍历大量页,这显然会降低查询效率。
|
||||
|
||||
减小索引字段大小,可以增加一个页中存储的索引项,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。
|
||||
|
||||
不过,前缀索引是有一定的局限性的,例如order by就无法使用前缀索引,无法把前缀索引用作覆盖索引。
|
||||
|
||||
## 4.防止索引失效
|
||||
|
||||
当我们习惯建立索引来实现查询SQL的性能优化后,是不是就万事大吉了呢?当然不是,有时候我们看似使用到了索引,但实际上并没有被优化器选择使用。
|
||||
|
||||
对于Hash索引实现的列,如果使用到范围查询,那么该索引将无法被优化器使用到。也就是说Memory引擎实现的Hash索引只有在“=”的查询条件下,索引才会生效。我们将order表设置为Memory存储引擎,分析查询条件为id<10的SQL,可以发现没有使用到索引。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8e/d0/8eaac54829f3c99337e481d833a93dd0.jpg" alt="">
|
||||
|
||||
如果是以%开头的LIKE查询将无法利用节点查询数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0b/bb/0b20dbd8274be269779989b3df546dbb.jpg" alt="">
|
||||
|
||||
当我们在使用复合索引时,需要使用索引中的最左边的列进行查询,才能使用到复合索引。例如我们在order表中建立一个复合索引idx_user_order_status(`order_no`, `status`, `user_id`),如果我们使用order_no、order_no+status、order_no+status+user_id以及order_no+user_id组合查询,则能利用到索引;而如果我们用status、status+user_id查询,将无法使用到索引,这也是我们经常听过的最左匹配原则。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4c/f0/4cebb097ea9a1d2db2c3cfc98bf611f0.jpg" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/18/65/18b7427c6d7e65821284fe90ec461565.jpg" alt="">
|
||||
|
||||
如果查询条件中使用or,且or的前后条件中有一个列没有索引,那么涉及的索引都不会被使用到。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2b/35/2b8f194d32bf2387264d50258c1f2235.jpg" alt="">
|
||||
|
||||
所以,你懂了吗?作为一名开发人员,如果没有熟悉MySQL,特别是MySQL索引的基础知识,很多时候都将被DBA批评到怀疑人生。
|
||||
|
||||
## 总结
|
||||
|
||||
在大多数情况下,我们习惯使用默认的 InnoDB 作为表存储引擎。在使用InnoDB作为存储引擎时,创建的索引默认为B+树数据结构,如果是主键索引,则属于聚簇索引,非主键索引则属于辅助索引。基于主键查询可以直接获取到行信息,而基于辅助索引作为查询条件,则需要进行回表,然后再通过主键索引获取到数据。
|
||||
|
||||
如果只是查询一列或少部分列的信息,我们可以基于覆盖索引来避免回表。覆盖索引只需要读取索引,且由于索引是顺序存储,对于范围或排序查询来说,可以极大地极少磁盘I/O操作。
|
||||
|
||||
除了了解索引的具体实现和一些特性,我们还需要注意索引失效的情况发生。如果觉得这些规则太多,难以记住,我们就要养成经常检查SQL执行计划的习惯。
|
||||
|
||||
## 思考题
|
||||
|
||||
假设我们有一个订单表order_detail,其中有主键id、主订单order_id、商品sku等字段,其中该表有主键索引、主订单id索引。
|
||||
|
||||
现在有一个查询订单详情的SQL如下,查询订单号范围在5000~10000,请问该查询选择的索引是什么?有什么方式可以强制使用我们期望的索引呢?
|
||||
|
||||
```
|
||||
select * from order_detail where order_id between 5000 and 10000;
|
||||
|
||||
```
|
||||
|
||||
期待在留言区看到你的答案。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bb/67/bbe343640d6b708832c4133ec53ed967.jpg" alt="unpreview">
|
||||
136
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/36 | 记一次线上SQL死锁事故:如何避免死锁?.md
Normal file
136
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/36 | 记一次线上SQL死锁事故:如何避免死锁?.md
Normal file
@@ -0,0 +1,136 @@
|
||||
<audio id="audio" title="36 | 记一次线上SQL死锁事故:如何避免死锁?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1c/5b/1c5b75d5624f837b0d359360e5dfb95b.mp3"></audio>
|
||||
|
||||
你好,我是刘超。今天我们来聊聊死锁,开始之前,先分享个小故事,相信你可能遇到过,或能从中获得一点启发。
|
||||
|
||||
之前我参与过一个项目,在项目初期,我们是没有将读写表分离的,而是基于一个主库完成读写操作。在业务量逐渐增大的时候,我们偶尔会收到系统的异常报警信息,DBA通知我们数据库出现了死锁异常。
|
||||
|
||||
按理说业务开始是比较简单的,就是新增订单、修改订单、查询订单等操作,那为什么会出现死锁呢?经过日志分析,我们发现是作为幂等性校验的一张表经常出现死锁异常。我们和DBA讨论之后,初步怀疑是索引导致的死锁问题。后来我们在开发环境中模拟了相关操作,果然重现了该死锁异常。
|
||||
|
||||
接下来我们就通过实战来重现下该业务死锁异常。首先,创建一张订单记录表,该表主要用于校验订单重复创建:
|
||||
|
||||
```
|
||||
CREATE TABLE `order_record` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`order_no` int(11) DEFAULT NULL,
|
||||
`status` int(4) DEFAULT NULL,
|
||||
`create_date` datetime(0) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_order_status`(`order_no`,`status`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
|
||||
```
|
||||
|
||||
为了能重现该问题,我们先将事务设置为手动提交。这里要注意一下,MySQL数据库和Oracle提交事务不太一样,MySQL数据库默认情况下是自动提交事务,我们可以通过以下命令行查看自动提交事务是否开启:
|
||||
|
||||
```
|
||||
mysql> show variables like 'autocommit';
|
||||
+---------------+-------+
|
||||
| Variable_name | Value |
|
||||
+---------------+-------+
|
||||
| autocommit | ON |
|
||||
+---------------+-------+
|
||||
1 row in set (0.01 sec)
|
||||
|
||||
```
|
||||
|
||||
下面就操作吧,先将MySQL数据库的事务提交设置为手动提交,通过以下命令行可以关闭自动提交事务:
|
||||
|
||||
```
|
||||
mysql> set autocommit = 0;
|
||||
Query OK, 0 rows affected (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
订单在做幂等性校验时,先是通过订单号检查订单是否存在,如果不存在则新增订单记录。知道具体的逻辑之后,我们再来模拟创建产生死锁的运行SQL语句。首先,我们模拟新建两个订单,并按照以下顺序执行幂等性校验SQL语句(垂直方向代表执行的时间顺序):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/a0/49198c13e2dfdff0a9492a1b58cd93a0.jpg" alt="">
|
||||
|
||||
此时,我们会发现两个事务已经进入死锁状态。我们可以在information_schema数据库中查询到具体的死锁情况,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7d/47/7d6e8c42d082ac5b75882e3d171a8047.jpg" alt="">
|
||||
|
||||
看到这,你可能会想,为什么SELECT要加for update排他锁,而不是使用共享锁呢?试想下,如果是两个订单号一样的请求同时进来,就有可能出现幻读。也就是说,一开始事务A中的查询没有该订单号,后来事务B新增了一个该订单号的记录,此时事务A再新增一条该订单号记录,就会创建重复的订单记录。面对这种情况,我们可以使用锁间隙算法来防止幻读。
|
||||
|
||||
## 死锁是如何产生的?
|
||||
|
||||
上面我们说到了锁间隙,在[第33讲](https://time.geekbang.org/column/article/114194)中,我已经讲过了并发事务中的锁机制以及行锁的具体实现算法,不妨回顾一下。
|
||||
|
||||
行锁的具体实现算法有三种:record lock、gap lock以及next-key lock。record lock是专门对索引项加锁;gap lock是对索引项之间的间隙加锁;next-key lock则是前面两种的组合,对索引项以其之间的间隙加锁。
|
||||
|
||||
只在可重复读或以上隔离级别下的特定操作才会取得gap lock或next-key lock,在Select、Update和Delete时,除了基于唯一索引的查询之外,其它索引查询时都会获取gap lock或next-key lock,即锁住其扫描的范围。主键索引也属于唯一索引,所以主键索引是不会使用gap lock或next-key lock。
|
||||
|
||||
在MySQL中,gap lock默认是开启的,即innodb_locks_unsafe_for_binlog参数值是disable的,且MySQL中默认的是RR事务隔离级别。
|
||||
|
||||
当我们执行以下查询SQL时,由于order_no列为非唯一索引,此时又是RR事务隔离级别,所以SELECT的加锁类型为gap lock,这里的gap范围是(4,+∞)。
|
||||
|
||||
>
|
||||
SELECT id FROM `demo`.`order_record` where `order_no` = 4 for update;
|
||||
|
||||
|
||||
执行查询SQL语句获取的gap lock并不会导致阻塞,而当我们执行以下插入SQL时,会在插入间隙上再次获取插入意向锁。插入意向锁其实也是一种gap锁,它与gap lock是冲突的,所以当其它事务持有该间隙的gap lock时,需要等待其它事务释放gap lock之后,才能获取到插入意向锁。
|
||||
|
||||
以上事务A和事务B都持有间隙(4,+∞)的gap锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的gap锁释放,于是就造成了循环等待,导致死锁。
|
||||
|
||||
>
|
||||
INSERT INTO `demo`.`order_record`(`order_no`, `status`, `create_date`) VALUES (5, 1, ‘2019-07-13 10:57:03’);
|
||||
|
||||
|
||||
我们可以通过以下锁的兼容矩阵图,来查看锁的兼容性:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/58/e3/58b1567a4ff86460ececfd420eda80e3.jpg" alt="">
|
||||
|
||||
## 避免死锁的措施
|
||||
|
||||
知道了死锁问题源自哪儿,就可以找到合适的方法来避免它了。
|
||||
|
||||
避免死锁最直观的方法就是在两个事务相互等待时,当一个事务的等待时间超过设置的某一阈值,就对这个事务进行回滚,另一个事务就可以继续执行了。这种方法简单有效,在InnoDB中,参数innodb_lock_wait_timeout是用来设置超时时间的。
|
||||
|
||||
另外,我们还可以将order_no列设置为唯一索引列。虽然不能防止幻读,但我们可以利用它的唯一性来保证订单记录不重复创建,这种方式唯一的缺点就是当遇到重复创建订单时会抛出异常。
|
||||
|
||||
我们还可以使用其它的方式来代替数据库实现幂等性校验。例如,使用Redis以及ZooKeeper来实现,运行效率比数据库更佳。
|
||||
|
||||
## 其它常见的SQL死锁问题
|
||||
|
||||
这里再补充一些常见的SQL死锁问题,以便你遇到时也能知道其原因,从而顺利解决。
|
||||
|
||||
我们知道死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立。所以在一些经常需要使用互斥共用一些资源,且有可能循环等待的业务场景中,要特别注意死锁问题。
|
||||
|
||||
接下来,我们再来了解一个出现死锁的场景。
|
||||
|
||||
我们讲过,InnoDB存储引擎的主键索引为聚簇索引,其它索引为辅助索引。如果我们之前使用辅助索引来更新数据库,就需要修改为使用聚簇索引来更新数据库。如果两个更新事务使用了不同的辅助索引,或一个使用了辅助索引,一个使用了聚簇索引,就都有可能导致锁资源的循环等待。由于本身两个事务是互斥,也就构成了以上死锁的四个必要条件了。
|
||||
|
||||
我们还是以上面的这个订单记录表来重现下聚簇索引和辅助索引更新时,循环等待锁资源导致的死锁问题:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b6/e7/b685033798d1027dac3f2f6cb1c2c6e7.jpg" alt="">
|
||||
|
||||
出现死锁的步骤:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e0/b4/e018d73c4a00de2bc3dc6932e0fa75b4.jpg" alt="">
|
||||
|
||||
综上可知,在更新操作时,我们应该尽量使用主键来更新表字段,这样可以有效避免一些不必要的死锁发生。
|
||||
|
||||
## 总结
|
||||
|
||||
数据库发生死锁的概率并不是很大,一旦遇到了,就一定要彻查具体原因,尽快找出解决方案,老实说,过程不简单。我们只有先对MySQL的InnoDB存储引擎有足够的了解,才能剖析出造成死锁的具体原因。
|
||||
|
||||
例如,以上我例举的两种发生死锁的场景,一个考验的是我们对锁算法的了解,另外一个考验则是我们对聚簇索引和辅助索引的熟悉程度。
|
||||
|
||||
解决死锁的最佳方式当然就是预防死锁的发生了,我们平时编程中,可以通过以下一些常规手段来预防死锁的发生:
|
||||
|
||||
1.在编程中尽量按照固定的顺序来处理数据库记录,假设有两个更新操作,分别更新两条相同的记录,但更新顺序不一样,有可能导致死锁;
|
||||
|
||||
2.在允许幻读和不可重复读的情况下,尽量使用RC事务隔离级别,可以避免gap lock导致的死锁问题;
|
||||
|
||||
3.更新表时,尽量使用主键更新;
|
||||
|
||||
4.避免长事务,尽量将长事务拆解,可以降低与其它事务发生冲突的概率;
|
||||
|
||||
5.设置锁等待超时参数,我们可以通过innodb_lock_wait_timeout设置合理的等待超时阈值,特别是在一些高并发的业务中,我们可以尽量将该值设置得小一些,避免大量事务等待,占用系统资源,造成严重的性能开销。
|
||||
|
||||
## 思考题
|
||||
|
||||
除了设置 innodb_lock_wait_timeout 参数来避免已经产生死锁的SQL长时间等待,你还知道其它方法来解决类似问题吗?
|
||||
|
||||
期待在留言区看到你的见解。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。
|
||||
|
||||
|
||||
109
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/37 | 什么时候需要分表分库?.md
Normal file
109
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/37 | 什么时候需要分表分库?.md
Normal file
@@ -0,0 +1,109 @@
|
||||
<audio id="audio" title="37 | 什么时候需要分表分库?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c1/f2/c1c21fa536d818c6450385b661d830f2.mp3"></audio>
|
||||
|
||||
你好,我是刘超。
|
||||
|
||||
在当今互联网时代,海量数据基本上是每一个成熟产品的共性,特别是在移动互联网产品中,几乎每天都在产生数据,例如,商城的订单表、支付系统的交易明细以及游戏中的战报等等。
|
||||
|
||||
对于一个日活用户在百万数量级的商城来说,每天产生的订单数量可能在百万级,特别在一些活动促销期间,甚至上千万。
|
||||
|
||||
假设我们基于单表来实现,每天产生上百万的数据量,不到一个月的时间就要承受上亿的数据,这时单表的性能将会严重下降。因为MySQL在InnoDB存储引擎下创建的索引都是基于B+树实现的,所以查询时的I/O次数很大程度取决于树的高度,随着B+树的树高增高,I/O次数增加,查询性能也就越差。
|
||||
|
||||
当我们面对一张海量数据的表时,通常有分区、NoSQL存储、分表分库等优化方案。
|
||||
|
||||
分区的底层虽然也是基于分表的原理实现的,即有多个底层表实现,但分区依然是在单库下进行的,在一些需要提高并发的场景中的优化空间非常有限,且一个表最多只能支持1024个分区。面对日益增长的海量数据,优化存储能力有限。不过在一些非海量数据的大表中,我们可以考虑使用分区来优化表性能。
|
||||
|
||||
>
|
||||
分区表是由多个相关的底层表实现的,这些底层表也是由句柄对象表示,所以我们也可以直接访问各个分区,存储引擎管理分区的各个底层表和管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引只是在各个底层表上各自加上一个相同的索引,从存储引擎的角度来看,底层表和一个普通表没有任何不同,存储引擎也无须知道这是一个普通表,还是一个分区表的一部分。
|
||||
|
||||
|
||||
而NoSQL存储是基于键值对存储,虽然查询性能非常高,但在一些方面仍然存在短板。例如,不是关系型数据库,不支持事务以及稳定性方面相对RDBMS差一些。虽然有些NoSQL数据库也实现了事务,宣传具有可靠的稳定性,但目前NoSQL还是主要用作辅助存储。
|
||||
|
||||
## 什么时候要分表分库?
|
||||
|
||||
分析完了分区、NoSQL存储优化的应用,接下来我们就看看这讲的重头戏——分表分库。
|
||||
|
||||
在我看来,能不分表分库就不要分表分库。在单表的情况下,当业务正常时,我们使用单表即可,而当业务出现了性能瓶颈时,我们首先考虑用分区的方式来优化,如果分区优化之后仍然存在后遗症,此时我们再来考虑分表分库。
|
||||
|
||||
我们知道,如果在单表单库的情况下,当数据库表的数据量逐渐累积到一定的数量时(5000W行或100G以上),操作数据库的性能会出现明显下降,即使我们使用索引优化或读写库分离,性能依然存在瓶颈。此时,如果每日数据增长量非常大,我们就应该考虑分表,避免单表数据量过大,造成数据库操作性能下降。
|
||||
|
||||
面对海量数据,除了单表的性能比较差以外,我们在单表单库的情况下,数据库连接数、磁盘I/O以及网络吞吐等资源都是有限的,并发能力也是有限的。所以,在一些大数据量且高并发的业务场景中,我们就需要考虑分表分库来提升数据库的并发处理能力,从而提升应用的整体性能。
|
||||
|
||||
## 如何分表分库?
|
||||
|
||||
通常,分表分库分为垂直切分和水平切分两种。
|
||||
|
||||
垂直分库是指根据业务来分库,不同的业务使用不同的数据库。例如,订单和消费券在抢购业务中都存在着高并发,如果同时使用一个库,会占用一定的连接数,所以我们可以将数据库分为订单库和促销活动库。
|
||||
|
||||
而垂直分表则是指根据一张表中的字段,将一张表划分为两张表,其规则就是将一些不经常使用的字段拆分到另一张表中。例如,一张订单详情表有一百多个字段,显然这张表的字段太多了,一方面不方便我们开发维护,另一方面还可能引起跨页问题。这时我们就可以拆分该表字段,解决上述两个问题。
|
||||
|
||||
水平分表则是将表中的某一列作为切分的条件,按照某种规则(Range或Hash取模)来切分为更小的表。
|
||||
|
||||
水平分表只是在一个库中,如果存在连接数、I/O读写以及网络吞吐等瓶颈,我们就需要考虑将水平切换的表分布到不同机器的库中,这就是水平分库分表了。
|
||||
|
||||
结合以上垂直切分和水平切分,我们一般可以将数据库分为:单库单表-单库多表-多库多表。在平时的业务开发中,我们应该优先考虑单库单表;如果数据量比较大,且热点数据比较集中、历史数据很少访问,我们可以考虑表分区;如果访问热点数据分散,基本上所有的数据都会访问到,我们可以考虑单库多表;如果并发量比较高、海量数据以及每日新增数据量巨大,我们可以考虑多库多表。
|
||||
|
||||
这里还需要注意一点,我刚刚强调过,能不分表分库,就不要分表分库。这是因为一旦分表,我们可能会涉及到多表的分页查询、多表的JOIN查询,从而增加业务的复杂度。而一旦分库了,除了跨库分页查询、跨库JOIN查询,还会存在跨库事务的问题。这些问题无疑会增加我们系统开发的复杂度。
|
||||
|
||||
## 分表分库之后面临的问题
|
||||
|
||||
然而,分表分库虽然存在着各种各样的问题,但在一些海量数据、高并发的业务中,分表分库仍是最常用的优化手段。所以,我们应该充分考虑分表分库操作后所面临的一些问题,接下我们就一起看看都有哪些应对之策。
|
||||
|
||||
为了更容易理解这些问题,我们将对一个订单表进行分库分表,通过详细的业务来分析这些问题。
|
||||
|
||||
假设我们有一张订单表以及一张订单详情表,每天的数据增长量在60W单,平时还会有一些促销类活动,订单增长量在千万单。为了提高系统的并发能力,我们考虑将订单表和订单详情表做分库分表。除了分表,因为用户一般查询的是最近的订单信息,所以热点数据比较集中,我们还可以考虑用表分区来优化单表查询。
|
||||
|
||||
通常订单的分库分表要么基于订单号Hash取模实现,要么根据用户 ID Hash 取模实现。订单号Hash取模的好处是数据能均匀分布到各个表中,而缺陷则是一个用户查询所有订单时,需要去多个表中查询。
|
||||
|
||||
由于订单表用户查询比较多,此时我们应该考虑使用用户ID字段做Hash取模,对订单表进行水平分表。如果需要考虑高并发时的订单处理能力,我们可以考虑基于用户ID字段Hash取模实现分库分表。这也是大部分公司对订单表分库分表的处理方式。
|
||||
|
||||
### 1.分布式事务问题
|
||||
|
||||
在提交订单时,除了创建订单之外,我们还需要扣除相应的库存。而订单表和库存表由于垂直分库,位于不同的库中,这时我们需要通过分布式事务来保证提交订单时的事务完整性。
|
||||
|
||||
通常,我们解决分布式事务有两种通用的方式:两阶事务提交(2PC)以及补偿事务提交(TCC)。有关分布式事务的内容,我将在第41讲中详细介绍。
|
||||
|
||||
通常有一些中间件已经帮我们封装好了这两种方式的实现,例如Spring实现的JTA,目前阿里开源的分布式事务中间件Fescar,就很好地实现了与Dubbo的兼容。
|
||||
|
||||
### 2.跨节点JOIN查询问题
|
||||
|
||||
用户在查询订单时,我们往往需要通过表连接获取到商品信息,而商品信息表可能在另外一个库中,这就涉及到了跨库JOIN查询。
|
||||
|
||||
通常,我们会冗余表或冗余字段来优化跨库JOIN查询。对于一些基础表,例如商品信息表,我们可以在每一个订单分库中复制一张基础表,避免跨库JOIN查询。而对于一两个字段的查询,我们也可以将少量字段冗余在表中,从而避免JOIN查询,也就避免了跨库JOIN查询。
|
||||
|
||||
### 3.跨节点分页查询问题
|
||||
|
||||
我们知道,当用户在订单列表中查询所有订单时,可以通过用户ID的Hash值来快速查询到订单信息,而运营人员在后台对订单表进行查询时,则是通过订单付款时间来进行查询的,这些数据都分布在不同的库以及表中,此时就存在一个跨节点分页查询的问题了。
|
||||
|
||||
通常一些中间件是通过在每个表中先查询出一定的数据,然后在缓存中排序后,获取到对应的分页数据。这种方式在越往后面的查询,就越消耗性能。
|
||||
|
||||
通常我们建议使用两套数据来解决跨节点分页查询问题,一套是基于分库分表的用户单条或多条查询数据,一套则是基于Elasticsearch、Solr存储的订单数据,主要用于运营人员根据其它字段进行分页查询。为了不影响提交订单的业务性能,我们一般使用异步消息来实现Elasticsearch、Solr订单数据的新增和修改。
|
||||
|
||||
### 4.全局主键ID问题
|
||||
|
||||
在分库分表后,主键将无法使用自增长来实现了,在不同的表中我们需要统一全局主键ID。因此,我们需要单独设计全局主键,避免不同表和库中的主键重复问题。
|
||||
|
||||
使用UUID实现全局ID是最方便快捷的方式,即随机生成一个32位16进制数字,这种方式可以保证一个UUID的唯一性,水平扩展能力以及性能都比较高。但使用UUID最大的缺陷就是,它是一个比较长的字符串,连续性差,如果作为主键使用,性能相对来说会比较差。
|
||||
|
||||
我们也可以基于Redis分布式锁实现一个递增的主键ID,这种方式可以保证主键是一个整数且有一定的连续性,但分布式锁存在一定的性能消耗。
|
||||
|
||||
我们还可以基于Twitter开源的分布式ID生产算法——snowflake解决全局主键ID问题,snowflake是通过分别截取时间、机器标识、顺序计数的位数组成一个long类型的主键ID。这种算法可以满足每秒上万个全局ID生成,不仅性能好,而且低延时。
|
||||
|
||||
### 5.扩容问题
|
||||
|
||||
随着用户的订单量增加,根据用户 ID Hash 取模的分表中,数据量也在逐渐累积。此时,我们需要考虑动态增加表,一旦动态增加表了,就会涉及到数据迁移问题。
|
||||
|
||||
我们在最开始设计表数据量时,尽量使用2的倍数来设置表数量。当我们需要扩容时,也同样按照2的倍数来扩容,这种方式可以减少数据的迁移量。
|
||||
|
||||
## 总结
|
||||
|
||||
在业务开发之前,我们首先要根据自己的业务需求来设计表。考虑到一开始的业务发展比较平缓,且开发周期比较短,因此在开发时间比较紧的情况下,我们尽量不要考虑分表分库。但是我们可以将分表分库的业务接口预留,提前考虑后期分表分库的切分规则,把该冗余的字段提前冗余出来,避免后期分表分库的JOIN查询等。
|
||||
|
||||
当业务发展比较迅速的时候,我们就要评估分表分库的必要性了。一旦需要分表分库,就要结合业务提前规划切分规则,尽量避免消耗性能的跨表跨库JOIN查询、分页查询以及跨库事务等操作。
|
||||
|
||||
## 思考题
|
||||
|
||||
你使用过哪些分库分表中间件呢?欢迎分享其中的实现原理以及优缺点。
|
||||
|
||||
期待在留言区看到你的分享。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。
|
||||
|
||||
|
||||
122
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/38 | 电商系统表设计优化案例分析.md
Normal file
122
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/38 | 电商系统表设计优化案例分析.md
Normal file
@@ -0,0 +1,122 @@
|
||||
<audio id="audio" title="38 | 电商系统表设计优化案例分析" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/05/64/0513c30c5e40307dabb5384a60f26064.mp3"></audio>
|
||||
|
||||
你好,我是刘超。今天我将带你一起了解下电商系统中的表设计优化。
|
||||
|
||||
如果在业务架构设计初期,表结构没有设计好,那么后期随着业务以及数据量的增多,系统就很容易出现瓶颈。如果表结构扩展性差,业务耦合度将会越来越高,系统的复杂度也将随之增加。这一讲我将以电商系统中的表结构设计为例,为你详讲解在设计表时,我们都需要考虑哪些因素,又是如何通过表设计来优化系统性能。
|
||||
|
||||
## 核心业务
|
||||
|
||||
要懂得一个电商系统的表结构设计,我们必须先得熟悉一个电商系统中都有哪些基本核心业务。这部分的内容,只要你有过网购经历,就很好理解。
|
||||
|
||||
一般电商系统分为平台型和自营型电商系统。平台型电商系统是指有第三方商家入驻的电商平台,第三方商家自己开设店铺来维护商品信息、库存信息、促销活动、客服售后等,典型的代表有淘宝、天猫等。而自营型电商系统则是指没有第三方商家入驻,而是公司自己运营的电商平台,常见的有京东自营、苹果商城等。
|
||||
|
||||
两种类型的电商系统比较明显的区别是卖家是C端还是B端,很显然,平台型电商系统的复杂度要远远高于自营型电商系统。为了更容易理解商城的业务,我们将基于自营型电商系统来讨论表结构设计优化,这里以苹果商城为例。
|
||||
|
||||
一个电商系统的核心业务肯定就是销售商品了,围绕销售商品,我们可以将核心业务分为以下几个主要模块:
|
||||
|
||||
### 1. 商品模块
|
||||
|
||||
商品模块主要包括商品分类以及商品信息管理,商品分类则是我们常见的大分类了,有人喜欢将分类细化为多个层级,例如,第一个大类是手机、电视、配件等,配件的第二个大类又分为耳机、充电宝等。为了降低用户学习系统操作的成本,我们应该尽量将层级减少。
|
||||
|
||||
当我们通过了分类查询之后,就到了商品页面,一个商品Item包含了若干商品SKU。商品Item是指一种商品,例如IPhone9,就是一个Item,商品SKU则是指具体属性的商品,例如金色128G内存的IPhone9。
|
||||
|
||||
### 2. 购物车模块
|
||||
|
||||
购物车主要是用于用户临时存放欲购买的商品,并可以在购物车中统一下单结算。购物车一般分为离线购物车和在线购物车。离线购物车则是用户选择放入到购物车的商品只保存在本地缓存中,在线购物车则是会同步这些商品信息到服务端。
|
||||
|
||||
目前大部分商城都是支持两种状态的购物车,当用户没有登录商城时,主要是离线购物车在记录用户的商品信息,当用户登录商城之后,用户放入到购物车中的商品都会同步到服务端,以后在手机和电脑等不同平台以及不同时间都能查看到自己放入购物车的商品。
|
||||
|
||||
### 3. 订单模块
|
||||
|
||||
订单是盘活整个商城的核心功能模块,如果没有订单的产出,平台将难以维持下去。订单模块管理着用户在平台的交易记录,是用户和商家交流购买商品状态的渠道,用户可以随时更改一个订单的状态,商家则必须按照业务流程及时更新订单,以告知用户已购买商品的具体状态。
|
||||
|
||||
通常一个订单分为以下几个状态:待付款、待发货、待收货、待评价、交易完成、用户取消、仅退款、退货退款状态。一个订单的流程见下图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/11/3b/11c5a983340d380b07fc02d1bfcabc3b.jpg" alt="">
|
||||
|
||||
### 4. 库存模块
|
||||
|
||||
这里主要记录的是商品SKU的具体库存信息,主要功能包括库存交易、库存管理。库存交易是指用户购买商品时实时消费库存,库存管理主要包括运营人员对商品的生产或采购入库、调拨。
|
||||
|
||||
一般库存信息分为商品SKU、仓区、实时库存、锁定库存、待退货库存、活动库存。
|
||||
|
||||
现在大部分电商都实现了华南华北的库存分区,所以可能存在同一个商品SKU在华北没有库存,而在华南存在库存的情况,所以我们需要有仓区这个字段,用来区分不同地区仓库的同一个商品SKU。
|
||||
|
||||
实时库存则是指商品的实时库存,锁定库存则表示用户已经提交订单到实际扣除库存或订单失效的这段时间里锁定的库存,待退货库存、活动库存则分别表表示订单退款时的库存数量以及每次活动时的库存数量。
|
||||
|
||||
除了这些库存信息,我们还可以为商品设置库存状态,例如虚拟库存状态、实物库存状态。如果一个商品不需要设定库存,可以任由用户购买,我们则不需要在每次用户购买商品时都去查询库存、扣除库存,只需要设定商品的库存状态为虚拟库存即可。
|
||||
|
||||
### 5. 促销活动模块
|
||||
|
||||
促销活动模块是指消费券、红包以及满减等促销功能,这里主要包括了活动管理和交易管理。前者主要负责管理每次发放的消费券及红包有效期、金额、满足条件、数量等信息,后者则主要负责管理用户领取红包、消费券等信息。
|
||||
|
||||
## 业务难点
|
||||
|
||||
了解了以上那些主要模块的具体业务之后,我们就可以更深入地去评估从业务落地到系统实现,可能存在的难点以及性能瓶颈了。
|
||||
|
||||
### 1. 不同商品类别存在差异,如何设计商品表结构?
|
||||
|
||||
我们知道,一个手机商品的详细信息跟一件衣服的详细信息差别很大,手机的SKU包括了颜色、运行内存、存储内存等,而一件衣服则包含了尺码、颜色。
|
||||
|
||||
如果我们需要将这些商品都存放在一张表中,要么就使用相同字段来存储不同的信息,要么就新增字段来维护各自的信息。前者会导致程序设计复杂化、表宽度大,从而减少磁盘单页存储行数,影响查询性能,且维护成本高;后者则会导致一张表中字段过多,如果有新的商品类型出现,又需要动态添加字段。
|
||||
|
||||
比较好的方式是通过一个公共表字段来存储一些具有共性的字段,创建单独的商品类型表,例如手机商品一个表、服饰商品一个表。但这种方式也有缺点,那就是可能会导致表非常多,查询商品信息的时候不够灵活,不好实现全文搜索。
|
||||
|
||||
这时候,我们可以基于一个公共表来存储商品的公共信息,同时结合搜索引擎,将商品详细信息存储到键值对数据库,例如ElasticSearch、Solr中。
|
||||
|
||||
### 2. 双十一购物车商品数量大增,购物车系统出现性能瓶颈怎么办?
|
||||
|
||||
在用户没有登录系统的情况下,我们是通过cookie来保存购物车的商品信息,而在用户登录系统之后,购物车的信息会保存到数据库中。
|
||||
|
||||
在双十一期间,大部分用户都会提前将商品加入到购物车中,在加入商品到购物车的这段操作中,由于时间比较长,操作会比较分散,所以对数据库的写入并不会造成太大的压力。但在购买时,由于多数属于抢购商品,用户对购物车的访问则会比较集中了,如果都去数据库中读取,那么数据库的压力就可想而知了。
|
||||
|
||||
此时我们应该考虑冷热数据方案来存储购物车的商品信息,用户一般都会首选最近放入购物车的商品,这些商品信息则是热数据,而较久之前放入购物车中的商品信息则是冷数据,我们需要提前将热数据存放在Redis缓存中,以便提高系统在活动期间的并发性能。例如,可以将购物车中近一个月的商品信息都存放到Redis中,且至少为一个分页的信息。
|
||||
|
||||
当在缓存中没有查找到购物车信息时,再去数据库中查询,这样就可以大大降低数据库的压力。
|
||||
|
||||
### 3. 订单表海量数据,如何设计订单表结构?
|
||||
|
||||
通常我们的订单表是系统数据累计最快的一张表,无论订单是否真正付款,只要订单提交了就会在订单表中创建订单。如果公司的业务发展非常迅速,那么订单表的分表分库就只是迟早的事儿了。
|
||||
|
||||
在没有分表之前,订单的主键ID都是自增的,并且关联了一些其它业务表。一旦要进行分表分库,就会存在主键ID与业务耦合的情况,而且分表后新自增ID与之前的ID也可能会发生冲突,后期做表升级的时候我们将会面临巨大的工作量。如果我们确定后期做表升级,建议提前使用snowflake来生成主键ID。
|
||||
|
||||
如果订单表要实现水平分表,那我们基于哪个字段来实现分表呢?
|
||||
|
||||
通常我们是通过计算用户ID字段的Hash值来实现订单的分表,这种方式可以优化用户购买端对订单的操作性能。如果我们需要对订单表进行水平分库,那就还是基于用户ID字段来实现。
|
||||
|
||||
在分表分库之后,对于我们的后台订单管理系统来说,查询订单就是一个挑战了。通常后台都是根据订单状态、创建订单时间进行查询的,且需要支持分页查询以及部分字段的JOIN查询,如果需要在分表分库的情况下进行这些操作,无疑是一个巨大的挑战了。
|
||||
|
||||
对于JOIN查询,我们一般可以通过冗余一些不常修改的配置表来实现。例如,商品的基础信息,我们录入之后很少修改,可以在每个分库中冗余该表,如果字段信息比较少,我们可以直接在订单表中冗余这些字段。
|
||||
|
||||
而对于分页查询,通常我们建议冗余订单信息到大数据中。后台管理系统通过大数据来查询订单信息,用户在提交订单并且付款之后,后台将会同步这条订单到大数据。用户在C端修改或运营人员在后台修改订单时,会通过异步方式通知大数据更新该订单数据,这种方式可以解决分表分库后带来的分页查询问题。
|
||||
|
||||
### 4. 抢购业务,如何解决库存表的性能瓶颈?
|
||||
|
||||
在平时购买商品时,我们一般是直接去数据库检查、锁定库存,但如果是在促销活动期间抢购商品,我们还是直接去数据库检查、更新库存的话,面对高并发,系统无疑会产生性能瓶颈。
|
||||
|
||||
一般我们会将促销活动的库存更新到缓存中,通过缓存来查询商品的实时库存,并且通过分布式锁来实现库存扣减、锁定库存。分布式锁的具体实现,我会在第41讲中详讲。
|
||||
|
||||
### 5. 促销活动也存在抢购场景,如何设计表?
|
||||
|
||||
促销活动中的优惠券和红包交易,很多时候跟抢购活动有些类似。
|
||||
|
||||
在一些大型促销活动之前,我们一般都会定时发放各种商品的优惠券和红包,用户需要点击领取才能使用。所以在一定数量的优惠券和红包放出的同时,也会存在同一时间抢购这些优惠券和红包的情况,特别是一些热销商品。
|
||||
|
||||
我们可以参考库存的优化设计方式,使用缓存和分布式锁来查询、更新优惠券和红包的数量,通过缓存获取数量成功以后,再通过异步方式更新数据库中优惠券和红包的数量。
|
||||
|
||||
## 总结
|
||||
|
||||
这一讲,我们结合电商系统实战练习了如何进行表设计,可以总结为以下几个要点:
|
||||
|
||||
- 在字段比较复杂、易变动、不方便统一的情况下,建议使用键值对来代替关系数据库表存储;
|
||||
- 在高并发情况下的查询操作,可以使用缓存代替数据库操作,提高并发性能;
|
||||
- 数据量叠加比较快的表,需要考虑水平分表或分库,避免单表操作的性能瓶颈;
|
||||
- 除此之外,我们应该通过一些优化,尽量避免比较复杂的JOIN查询操作,例如冗余一些字段,减少JOIN查询;创建一些中间表,减少JOIN查询。
|
||||
|
||||
## 思考题
|
||||
|
||||
你在设计表时,是否使用过外键来关联各个表呢?目前互联网公司一般建议逻辑上实现各个表之间的关联,而不建议使用外键来实现实际的表关联,你知道这为什么吗?
|
||||
|
||||
期待在留言区看到你的见解。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。
|
||||
|
||||
|
||||
174
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/39 | 数据库参数设置优化,失之毫厘差之千里.md
Normal file
174
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/39 | 数据库参数设置优化,失之毫厘差之千里.md
Normal file
@@ -0,0 +1,174 @@
|
||||
<audio id="audio" title="39 | 数据库参数设置优化,失之毫厘差之千里" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0f/1d/0f17a4b9a98f62297d3b1ee1910e631d.mp3"></audio>
|
||||
|
||||
你好,我是刘超。
|
||||
|
||||
MySQL是一个灵活性比较强的数据库系统,提供了很多可配置参数,便于我们根据应用和服务器硬件来做定制化数据库服务。如果现在让你回想,你可能觉得在开发的过程中很少去调整MySQL的配置参数,但我今天想说的是我们很有必要去深入了解它们。
|
||||
|
||||
我们知道,数据库主要是用来存取数据的,而存取数据涉及到了磁盘I/O的读写操作,所以数据库系统主要的性能瓶颈就是I/O读写的瓶颈了。MySQL数据库为了减少磁盘I/O的读写操作,应用了大量内存管理来优化数据库操作,包括内存优化查询、排序以及写入操作。
|
||||
|
||||
也许你会想,我们把内存设置得越大越好,数据刷新到磁盘越快越好,不就对了吗?其实不然,内存设置过大,同样会带来新的问题。例如,InnoDB中的数据和索引缓存,如果设置过大,就会引发SWAP页交换。还有数据写入到磁盘也不是越快越好,我们期望的是在高并发时,数据能均匀地写入到磁盘中,从而避免I/O性能瓶颈。
|
||||
|
||||
>
|
||||
SWAP页交换:SWAP分区在系统的物理内存不够用的时候,就会把物理内存中的一部分空间释放出来,以供当前运行的程序使用。被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间的数据被临时保存到SWAP分区中,等到那些程序要运行时,再从SWAP分区中恢复保存的数据到内存中。
|
||||
|
||||
|
||||
所以,这些参数的设置跟我们的应用服务特性以及服务器硬件有很大的关系。MySQL是一个高定制化的数据库,我们可以根据需求来调整参数,定制性能最优的数据库。
|
||||
|
||||
不过想要了解这些参数的具体作用,我们先得了解数据库的结构以及不同存储引擎的工作原理。
|
||||
|
||||
## MySQL体系结构
|
||||
|
||||
我们一般可以将MySQL的结构分为四层,最上层为客户端连接器,主要包括了数据库连接、授权认证、安全管理等,该层引用了线程池,为接入的连接请求提高线程处理效率。
|
||||
|
||||
第二层是Server层,主要实现SQL的一些基础功能,包括SQL解析、优化、执行以及缓存等,其中与我们这一讲主要相关的就是缓存。
|
||||
|
||||
第三层包括了各种存储引擎,主要负责数据的存取,这一层涉及到的Buffer缓存,也和这一讲密切相关。
|
||||
|
||||
最下面一层是数据存储层,主要负责将数据存储在文件系统中,并完成与存储引擎的交互。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/52/a5/5297b9556d527dec788b5298d4810fa5.jpg" alt="">
|
||||
|
||||
接下来我们再来了解下,当数据接收到一个SQL语句时,是如何处理的。
|
||||
|
||||
### 1. 查询语句
|
||||
|
||||
一个应用服务需要通过第一层的连接和授权认证,再将SQL请求发送至SQL接口。SQL接口接收到请求之后,会先检查查询SQL是否命中Cache缓存中的数据,如果命中,则直接返回缓存中的结果;否则,需要进入解析器。
|
||||
|
||||
解析器主要对SQL进行语法以及词法分析,之后,便会进入到优化器中,优化器会生成多种执行计划方案,并选择最优方案执行。
|
||||
|
||||
确定了最优执行计划方案之后,执行器会检查连接用户是否有该表的执行权限,有则查看Buffer中是否存在该缓存,存在则获取锁,查询表数据;否则重新打开表文件,通过接口调用相应的存储引擎处理,这时存储引擎就会进入到存储文件系统中获取相应的数据,并返回结果集。
|
||||
|
||||
### 2. 更新语句
|
||||
|
||||
数据库更新SQL的执行流程其实跟查询SQL差不多,只不过执行更新操作的时候多了记录日志的步骤。在执行更新操作时MySQL会将操作的日志记录到 binlog(归档日志)中,这个步骤所有的存储引擎都有。而InnoDB除了要记录 binlog 之外,还需要多记录一个 redo log(重做日志)。
|
||||
|
||||
redo log 主要是为了解决 crash-safe 问题而引入的。我们知道,当数据库在存储数据时发生异常重启,我们需要保证存储的数据要么存储成功,要么存储失败,也就是不会出现数据丢失的情况,这就是crash-safe了。
|
||||
|
||||
我们在执行更新操作时,首先会查询相关的数据,之后通过执行器执行更新操作,并将执行结果写入到内存中,同时记录更新操作到redo log的缓存中,此时redo log中的记录状态为prepare,并通知执行器更新完成,随时可以提交事务。执行器收到通知后会执行binlog的写入操作,此时的binlog是记录在缓存中的,写入成功后会调用引擎的提交事务接口,更新记录状态为commit。之后,内存中的redo log以及binlog都会刷新到磁盘文件中。
|
||||
|
||||
## 内存调优
|
||||
|
||||
基于以上两个SQL执行过程,我们可以发现,在执行查询SQL语句时,会涉及到两个缓存。第一个缓存是刚进来时的Query Cache,它缓存的是SQL语句和对应的结果集。这里的缓存是以查询SQL的Hash值为key,返回结果集为value的键值对,判断一条SQL是否命中缓存,是通过匹配查询SQL的Hash值来实现的。
|
||||
|
||||
很明显,Query Cache可以优化查询SQL语句,减少大量工作,特别是减少了I/O读取操作。我们可以通过以下几个主要的设置参数来优化查询操作:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/db/dd/db97c34b74f0903673badc256ba46cdd.jpg" alt="">
|
||||
|
||||
我们可以通过设置合适的 query_cache_min_res_unit 来减少碎片,这个参数最合适的大小和应用程序查询结果的平均大小直接相关,可以通过以下公式计算所得:
|
||||
|
||||
(query_cache_size - Qcache_free_memory)/ Qcache_queries_in_cache
|
||||
|
||||
Qcache_free_memory 和 Qcache_queries_in_cache 的值可以通过以下命令查询:
|
||||
|
||||
```
|
||||
show status like 'Qcache%'
|
||||
|
||||
```
|
||||
|
||||
Query Cache虽然可以优化查询操作,但也仅限于不常修改的数据,如果一张表数据经常进行新增、更新和删除操作,则会造成Query Cache的失效率非常高,从而导致频繁地清除Cache中的数据,给系统增加额外的性能开销。
|
||||
|
||||
这也会导致缓存命中率非常低,我们可以通过以上查询状态的命令查看 Qcache_hits,该值表示缓存命中率。如果缓存命中率特别低的话,我们还可以通过query_cache_size = 0或者query_cache_type来关闭查询缓存。
|
||||
|
||||
经过了Query Cache缓存之后,还会使用到存储引擎中的Buffer缓存。不同的存储引擎,使用的Buffer也是不一样的。这里我们主要讲解两种常用的存储引擎。
|
||||
|
||||
### 1. MyISAM存储引擎参数设置调优
|
||||
|
||||
MyISAM存储引擎使用key buffer缓存索引块,MyISAM表的数据块则没有缓存,它是直接存储在磁盘文件中的。
|
||||
|
||||
我们可以通过key_buffer_size设置key buffer缓存的大小,而它的大小并不是越大越好。正如我前面所讲的,key buffer缓存设置过大,实际应用却不大的话,就容易造成内存浪费,而且系统也容易发生SWAP页交换,一般我是建议将服务器内存中可用内存的1/4分配给key buffer。
|
||||
|
||||
如果要更准确地评估key buffer的设置是否合理,我们还可以通过缓存使用率公式来计算:
|
||||
|
||||
1-((key_blocks_unused*key_cache_block_size)/key_buffer_size)
|
||||
|
||||
>
|
||||
<p>key_blocks_unused表示未使用的缓存簇(blocks)数<br>
|
||||
key_cache_block_size表示key_buffer_size被分割的区域大小key_blocks_unused*key_cache_block_size则表示剩余的可用缓存空间(一般来说,缓存使用率在80%作用比较合适)。</p>
|
||||
|
||||
|
||||
### 2. InnoDB存储引擎参数设置调优
|
||||
|
||||
InnoDB Buffer Pool(简称IBP)是InnoDB存储引擎的一个缓冲池,与MyISAM存储引擎使用key buffer缓存不同,它不仅存储了表索引块,还存储了表数据。查询数据时,IBP允许快速返回频繁访问的数据,而无需访问磁盘文件。InnoDB表空间缓存越多,MySQL访问物理磁盘的频率就越低,这表示查询响应时间更快,系统的整体性能也有所提高。
|
||||
|
||||
我们一般可以通过多个设置参数来调整IBP,优化InnoDB表性能。
|
||||
|
||||
- **innodb_buffer_pool_size**
|
||||
|
||||
IBP默认的内存大小是128M,我们可以通过参数innodb_buffer_pool_size来设置IBP的大小,IBP设置得越大,InnoDB表性能就越好。但是,将IBP大小设置得过大也不好,可能会导致系统发生SWAP页交换。所以我们需要在IBP大小和其它系统服务所需内存大小之间取得平衡。MySQL推荐配置IBP的大小为服务器物理内存的80%。
|
||||
|
||||
我们也可以通过计算InnoDB缓冲池的命中率来调整IBP大小:
|
||||
|
||||
(1-innodb_buffer_pool_reads/innodb_buffer_pool_read_request)*100
|
||||
|
||||
但如果我们将IBP的大小设置为物理内存的80%以后,发现命中率还是很低,此时我们就应该考虑扩充内存来增加IBP的大小。
|
||||
|
||||
- **innodb_buffer_pool_instances**
|
||||
|
||||
InnoDB中的IBP缓冲池被划分为了多个实例,对于具有数千兆字节的缓冲池的系统来说,将缓冲池划分为单独的实例可以减少不同线程读取和写入缓存页面时的争用,从而提高系统的并发性。该参数项仅在将innodb_buffer_pool_size设置为1GB或更大时才会生效。
|
||||
|
||||
在windows 32位操作系统中,如果innodb_buffer_pool_size的大小超过1.3GB,innodb_buffer_pool_instances默认大小就为innodb_buffer_pool_size/128MB;否则,默认为1。
|
||||
|
||||
而在其它操作系统中,如果innodb_buffer_pool_size大小超过1GB,innodb_buffer_pool_instances值就默认为8;否则,默认为1。
|
||||
|
||||
为了获取最佳效率,建议指定innodb_buffer_pool_instances的大小,并保证每个缓冲池实例至少有1GB内存。通常,建议innodb_buffer_pool_instances的大小不超过innodb_read_io_threads + innodb_write_io_threads之和,建议实例和线程数量比例为1:1。
|
||||
|
||||
- **innodb_read_io_threads** / **innodb_write_io_threads**
|
||||
|
||||
在默认情况下,MySQL后台线程包括了主线程、IO线程、锁线程以及监控线程等,其中读写线程属于IO线程,主要负责数据库的读取和写入操作,这些线程分别读取和写入innodb_buffer_pool_instances创建的各个内存页面。MySQL支持配置多个读写线程,即通过innodb_read_io_threads和innodb_write_io_threads设置读写线程数量。
|
||||
|
||||
读写线程数量值默认为4,也就是总共有8个线程同时在后台运行。innodb_read_io_threads和innodb_write_io_threads设置的读写线程数量,与innodb_buffer_pool_instances的大小有关,两者的协同优化是提高系统性能的一个关键因素。
|
||||
|
||||
在一些内存以及CPU内核超大型的数据库服务器上,我们可以在保证足够大的IBP内存的前提下,通过以下公式,协同增加缓存实例数量以及读写线程。
|
||||
|
||||
( innodb_read_io_threads + innodb_write_io_threads ) = innodb_buffe_pool_instances
|
||||
|
||||
如果我们仅仅是将读写线程根据缓存实例数量对半来分,即读线程和写线程各为实例大小的一半,肯定是不合理的。例如我们的应用服务读取数据库的数据多于写入数据库的数据,那么增加写入线程反而没有优化效果。我们一般可以通过MySQL服务器保存的全局统计信息,来确定系统的读取和写入比率。
|
||||
|
||||
我们可以通过以下查询来确定读写比率:
|
||||
|
||||
```
|
||||
SHOW GLOBAL STATUS LIKE 'Com_select';//读取数量
|
||||
|
||||
SHOW GLOBAL STATUS WHERE Variable_name IN ('Com_insert', 'Com_update', 'Com_replace', 'Com_delete');//写入数量
|
||||
|
||||
```
|
||||
|
||||
如果读大于写,我们应该考虑将读线程的数量设置得大一些,写线程数量小一些;否则,反之。
|
||||
|
||||
- **innodb_log_file_size**
|
||||
|
||||
除了以上InnoDB缓存等因素之外,InnoDB的日志缓存大小、日志文件大小以及日志文件持久化到磁盘的策略都影响着InnnoDB的性能。 InnoDB中有一个redo log文件,InnoDB用它来存储服务器处理的每个写请求的重做活动。执行的每个写入查询都会在日志文件中获得重做条目,以便在发生崩溃时可以恢复更改。
|
||||
|
||||
当日志文件大小已经超过我们参数设置的日志文件大小时,InnoDB会自动切换到另外一个日志文件,由于重做日志是一个循环使用的环,在切换时,就需要将新的日志文件脏页的缓存数据刷新到磁盘中(触发检查点)。
|
||||
|
||||
理论上来说,innodb_log_file_size设置得越大,缓冲池中需要的检查点刷新活动就越少,从而节省磁盘I/O。那是不是将这个日志文件设置得越大越好呢?如果日志文件设置得太大,恢复时间就会变长,这样不便于DBA管理。在大多数情况下,我们将日志文件大小设置为1GB就足够了。
|
||||
|
||||
- **innodb_log_buffer_size**
|
||||
|
||||
这个参数决定了InnoDB重做日志缓冲池的大小,默认值为8MB。如果高并发中存在大量的事务,该值设置得太小,就会增加写入磁盘的I/O操作。我们可以通过增大该参数来减少写入磁盘操作,从而提高并发时的事务性能。
|
||||
|
||||
- **innodb_flush_log_at_trx_commit**
|
||||
|
||||
这个参数可以控制重做日志从缓存写入文件刷新到磁盘中的策略,默认值为1。
|
||||
|
||||
当设置该参数为0时,InnoDB每秒种就会触发一次缓存日志写入到文件中并刷新到磁盘的操作,这有可能在数据库崩溃后,丢失1s的数据。
|
||||
|
||||
当设置该参数为 1 时,则表示每次事务的 redo log 都会直接持久化到磁盘中,这样可以保证 MySQL 异常重启之后数据不会丢失。
|
||||
|
||||
当设置该参数为 2 时,每次事务的 redo log 都会直接写入到文件中,再将文件刷新到磁盘。
|
||||
|
||||
在一些对数据安全性要求比较高的场景中,显然该值需要设置为1;而在一些可以容忍数据库崩溃时丢失1s数据的场景中,我们可以将该值设置为0或2,这样可以明显地减少日志同步到磁盘的I/O操作。
|
||||
|
||||
## 总结
|
||||
|
||||
MySQL数据库的参数设置非常多,今天我们仅仅是了解了与内存优化相关的参数设置。除了这些参数设置,我们还有一些常用的提高MySQL并发的相关参数设置,总结如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/be/4c/be83083a261bf1302aca81c122b0ac4c.jpg" alt="">
|
||||
|
||||
## 思考题
|
||||
|
||||
我们知道,InnoDB的IBP的内存大小是有限的,你知道InnoDB是如何将热点数据留在内存中,淘汰非热点数据的吗?
|
||||
|
||||
期待在留言区看到你的答案。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。
|
||||
|
||||
|
||||
109
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/40 | 答疑课堂:MySQL中InnoDB的知识点串讲.md
Normal file
109
极客时间专栏/Java性能调优实战/模块六 · 数据库性能调优/40 | 答疑课堂:MySQL中InnoDB的知识点串讲.md
Normal file
@@ -0,0 +1,109 @@
|
||||
<audio id="audio" title="40 | 答疑课堂:MySQL中InnoDB的知识点串讲" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f3/a9/f38e317137aa02fff1928c38978e3ea9.mp3"></audio>
|
||||
|
||||
你好,我是刘超。
|
||||
|
||||
模块六有关数据库调优的内容到本周也正式结束了,今天我们一起串下MySQL中InnoDB的知识点。InnoDB存储引擎作为我们最常用到的存储引擎之一,充分熟悉它的的实现和运行原理,有助于我们更好地创建和维护数据库表。
|
||||
|
||||
## InnoDB体系架构
|
||||
|
||||
InnoDB主要包括了内存池、后台线程以及存储文件。内存池又是由多个内存块组成的,主要包括缓存磁盘数据、redo log缓冲等;后台线程则包括了Master Thread、IO Thread以及Purge Thread等;由InnoDB存储引擎实现的表的存储结构文件一般包括表结构文件(.frm)、共享表空间文件(ibdata1)、独占表空间文件(ibd)以及日志文件(redo文件等)等。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f2/92/f26b2fad64a9a527b5ac0e8c7f4be992.jpg" alt="">
|
||||
|
||||
### 1. 内存池
|
||||
|
||||
我们知道,如果客户端从数据库中读取数据是直接从磁盘读取的话,无疑会带来一定的性能瓶颈,缓冲池的作用就是提高整个数据库的读写性能。
|
||||
|
||||
客户端读取数据时,如果数据存在于缓冲池中,客户端就会直接读取缓冲池中的数据,否则再去磁盘中读取;对于数据库中的修改数据,首先是修改在缓冲池中的数据,然后再通过Master Thread线程刷新到磁盘上。
|
||||
|
||||
理论上来说,缓冲池的内存越大越好。我们在[第38讲](https://time.geekbang.org/column/article/120160)中详细讲过了缓冲池的大小配置方式以及调优。
|
||||
|
||||
缓冲池中不仅缓存索引页和数据页,还包括了undo页,插入缓存、自适应哈希索引以及InnoDB的锁信息等等。
|
||||
|
||||
InnoDB允许多个缓冲池实例,从而减少数据库内部资源的竞争,增强数据库的并发处理能力,[第38讲](https://time.geekbang.org/column/article/120160)还讲到了缓冲池实例的配置以及调优。
|
||||
|
||||
InnoDB存储引擎会先将重做日志信息放入到缓冲区中,然后再刷新到重做日志文件中。
|
||||
|
||||
### 2. 后台线程
|
||||
|
||||
Master Thread 主要负责将缓冲池中的数据异步刷新到磁盘中,除此之外还包括插入缓存、undo页的回收等,IO Thread是负责读写IO的线程,而Purge Thread主要用于回收事务已经提交了的undo log,Pager Cleaner Thread是新引入的一个用于协助Master Thread刷新脏页到磁盘的线程,它可以减轻Master Thread的工作压力,减少阻塞。
|
||||
|
||||
### 3. 存储文件
|
||||
|
||||
在MySQL中建立一张表都会生成一个.frm文件,该文件是用来保存每个表的元数据信息的,主要包含表结构定义。
|
||||
|
||||
在InnoDB中,存储数据都是按表空间进行存放的,默认为共享表空间,存储的文件即为共享表空间文件(ibdata1)。若设置了参数innodb_file_per_table为1,则会将存储的数据、索引等信息单独存储在一个独占表空间,因此也会产生一个独占表空间文件(ibd)。如果你对共享表空间和独占表空间的理解还不够透彻,接下来我会详解。
|
||||
|
||||
而日志文件则主要是重做日志文件,主要记录事务产生的重做日志,保证事务的一致性。
|
||||
|
||||
## InnoDB逻辑存储结构
|
||||
|
||||
InnoDB逻辑存储结构分为表空间(Tablespace)、段(Segment)、区(Extent)、页Page)以及行(row)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/88/76/88b4ae3373eb5428c238b70423a13e76.jpg" alt="">
|
||||
|
||||
### 1. 表空间(Tablespace)
|
||||
|
||||
InnoDB提供了两种表空间存储数据的方式,一种是共享表空间,一种是独占表空间。 InnoDB 默认会将其所有的表数据存储在一个共享表空间中,即ibdata1。
|
||||
|
||||
我们可以通过设置innodb_file_per_table参数为1(1代表独占方式)开启独占表空间模式。开启之后,每个表都有自己独立的表空间物理文件,所有的数据以及索引都会存储在该文件中,这样方便备份以及恢复数据。
|
||||
|
||||
### 2. 段(Segment)
|
||||
|
||||
表空间是由各个段组成的,段一般分为数据段、索引段和回滚段等。我们知道,InnoDB默认是基于B +树实现的数据存储。
|
||||
|
||||
这里的索引段则是指的B +树的非叶子节点,而数据段则是B +树的叶子节点。而回滚段则指的是回滚数据,之前我们在讲事务隔离的时候就介绍到了MVCC利用了回滚段实现了多版本查询数据。
|
||||
|
||||
### 3. 区(Extent) / 页(Page)
|
||||
|
||||
区是表空间的单元结构,每个区的大小为1MB。而页是组成区的最小单元,页也是InnoDB存储引擎磁盘管理的最小单元,每个页的大小默认为16KB。为了保证页的连续性,InnoDB存储引擎每次从磁盘申请4-5个区。
|
||||
|
||||
### 4. 行(Row)
|
||||
|
||||
InnoDB存储引擎是面向行的(row-oriented),也就是说数据是按行进行存放的,每个页存放的行记录也是有硬性定义的,最多允许存放16KB/2-200行,即7992行记录。
|
||||
|
||||
## InnoDB事务之redo log工作原理
|
||||
|
||||
InnoDB是一个事务性的存储引擎,而InnoDB的事务实现是基于事务日志redo log和undo log实现的。redo log是重做日志,提供再写入操作,实现事务的持久性;undo log是回滚日志,提供回滚操作,保证事务的一致性。
|
||||
|
||||
redo log又包括了内存中的日志缓冲(redo log buffer)以及保存在磁盘的重做日志文件(redo log file),前者存储在内存中,容易丢失,后者持久化在磁盘中,不会丢失。
|
||||
|
||||
InnoDB的更新操作采用的是Write Ahead Log策略,即先写日志,再写入磁盘。当一条记录更新时,InnoDB会先把记录写入到redo log buffer中,并更新内存数据。我们可以通过参数innodb_flush_log_at_trx_commit自定义commit时,如何将redo log buffer中的日志刷新到redo log file中。
|
||||
|
||||
在这里,我们需要注意的是InnoDB的redo log的大小是固定的,分别有多个日志文件采用循环方式组成一个循环闭环,当写到结尾时,会回到开头循环写日志。我们可以通过参数innodb_log_files_in_group和innodb_log_file_size配置日志文件数量和每个日志文件的大小。
|
||||
|
||||
Buffer Pool中更新的数据未刷新到磁盘中,该内存页我们称之为脏页。最终脏页的数据会刷新到磁盘中,将磁盘中的数据覆盖,这个过程与redo log不一定有关系。
|
||||
|
||||
只有当redo log日志满了的情况下,才会主动触发脏页刷新到磁盘,而脏页不仅只有redo log日志满了的情况才会刷新到磁盘,以下几种情况同样会触发脏页的刷新:
|
||||
|
||||
- 系统内存不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
|
||||
- MySQL认为空闲的时间,这种情况没有性能问题;
|
||||
- MySQL正常关闭之前,会把所有的脏页刷入到磁盘,这种情况也没有性能问题。
|
||||
|
||||
在生产环境中,如果我们开启了慢SQL监控,你会发现偶尔会出现一些用时稍长的SQL。这是因为脏页在刷新到磁盘时可能会给数据库带来性能开销,导致数据库操作抖动。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2a/fd/2a32f1dc2dfbb1f9bc169ee55174d2fd.jpg" alt="">
|
||||
|
||||
## LRU淘汰策略
|
||||
|
||||
以上我们了解了InnoDB的更新和插入操作的具体实现原理,接下来我们再来了解下读的实现和优化方式。
|
||||
|
||||
InnoDB存储引擎是基于集合索引实现的数据存储,也就是除了索引列以及主键是存储在B +树之外,其它列数据也存储在B + 树的叶子节点中。而这里的索引页和数据页都会缓存在缓冲池中,在查询数据时,只要在缓冲池中存在该数据,InnoDB就不用每次都去磁盘中读取页,从而提高数据库的查询性能。
|
||||
|
||||
虽然缓冲池是一个很大的内存区域,但由于存放了各种类型的数据,加上存储数据量之大,缓冲池无法将所有的数据都存储在其中。因此,缓冲池需要通过LRU算法将最近且经常查询的数据缓存在其中,而不常查询的数据就淘汰出去。
|
||||
|
||||
InnoDB对LRU做了一些优化,我们熟悉的LRU算法通常是将最近查询的数据放到LRU列表的首部,而InnoDB则是将数据放在一个midpoint位置,通常这个midpoint为列表长度的5/8。
|
||||
|
||||
这种策略主要是为了避免一些不常查询的操作突然将热点数据淘汰出去,而热点数据被再次查询时,需要再次从磁盘中获取,从而影响数据库的查询性能。
|
||||
|
||||
如果我们的热点数据比较多,我们可以通过调整midpoint值来增加热点数据的存储量,从而降低热点数据的淘汰率。
|
||||
|
||||
## 总结
|
||||
|
||||
以上InnoDB的实现和运行原理到这里就介绍完了。回顾模块六,前三讲我主要介绍了数据库操作的性能优化,包括SQL语句、事务以及索引的优化,接下来我又讲到了数据库表优化,包括表设计、分表分库的实现等等,最后我还介绍了一些数据库参数的调优。
|
||||
|
||||
总的来讲,作为开发工程师,我们应该掌握数据库几个大的知识点,然后再深入到数据库内部实现的细节,这样才能避免经常写出一些具有性能问题的SQL,培养调优数据库性能的能力。
|
||||
|
||||
这一讲的内容就到这里,相对基础,不熟悉的同学抓紧补补课,如有疑问,欢迎留言讨论。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起学习。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user