learn-tech/专栏/MySQL实战45讲/38都说InnoDB好,那还要不要使用Memory引擎?.md
2024-10-16 06:37:41 +08:00

243 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
38 都说InnoDB好那还要不要使用Memory引擎
我在上一篇文章末尾留给你的问题是:两个 group by 语句都用了 order by null为什么使用内存临时表得到的语句结果里0 这个值在最后一行而使用磁盘临时表得到的结果里0 这个值在第一行?
今天我们就来看看,出现这个问题的原因吧。
内存表的数据组织结构
为了便于分析,我来把这个问题简化一下,假设有以下的两张表 t1 和 t2其中表 t1 使用 Memory 引擎, 表 t2 使用 InnoDB 引擎。
create table t1(id int primary key, c int) engine=Memory;
create table t2(id int primary key, c int) engine=innodb;
insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
然后,我分别执行 select * from t1 和 select * from t2。
图 1 两个查询结果 -0 的位置
可以看到,内存表 t1 的返回结果里面 0 在最后一行,而 InnoDB 表 t2 的返回结果里 0 在第一行。
出现这个区别的原因,要从这两个引擎的主键索引的组织方式说起。
表 t2 用的是 InnoDB 引擎,它的主键索引 id 的组织方式你已经很熟悉了InnoDB 表的数据就放在主键索引树上,主键索引是 B+ 树。所以表 t2 的数据组织方式如下图所示:
图 2 表 t2 的数据组织
主键索引上的值是有序存储的。在执行 select * 的时候就会按照叶子节点从左到右扫描所以得到的结果里0 就出现在第一行。
与 InnoDB 引擎不同Memory 引擎的数据和索引是分开的。我们来看一下表 t1 中的数据内容。
图 3 表 t1 的数据组织
可以看到,内存表的数据部分以数组的方式单独存放,而主键 id 索引里,存的是每个数据的位置。主键 id 是 hash 索引,可以看到索引上的 key 并不是有序的。
在内存表 t1 中,当我执行 select * 的时候走的是全表扫描也就是顺序扫描这个数组。因此0 就是最后一个被读到,并放入结果集的数据。
可见InnoDB 和 Memory 引擎的数据组织方式是不同的:
InnoDB 引擎把数据放在主键索引上,其他索引上保存的是主键 id。这种方式我们称之为索引组织表Index Organizied Table
而 Memory 引擎采用的是把数据单独存放索引上保存数据位置的数据组织形式我们称之为堆组织表Heap Organizied Table
从中我们可以看出,这两个引擎的一些典型不同:
InnoDB 表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的;
当数据文件有空洞的时候InnoDB 表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值;
数据位置发生变化的时候InnoDB 表只需要修改主键索引,而内存表需要修改所有索引;
InnoDB 表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的。
InnoDB 支持变长数据类型,不同记录的长度可能不同;内存表不支持 Blob 和 Text 字段,并且即使定义了 varchar(N),实际也当作 char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同。
由于内存表的这些特性,每个数据行被删除以后,空出的这个位置都可以被接下来要插入的数据复用。比如,如果要在表 t1 中执行:
delete from t1 where id=5;
insert into t1 values(10,10);
select * from t1;
就会看到返回结果里id=10 这一行出现在 id=4 之后,也就是原来 id=5 这行数据的位置。
需要指出的是,表 t1 的这个主键索引是哈希索引,因此如果执行范围查询,比如
select * from t1 where id<5;
是用不上主键索引的需要走全表扫描你可以借此再回顾下[ 4 篇文章]的内容那如果要让内存表支持范围扫描应该怎么办呢
hash 索引和 B-Tree 索引
实际上内存表也是支 B-Tree 索引的 id 列上创建一个 B-Tree 索引SQL 语句可以这么写
alter table t1 add index a_btree_index using btree (id);
这时 t1 的数据组织形式就变成了这样
4 t1 的数据组织 增加 B-Tree 索引
新增的这个 B-Tree 索引你看着就眼熟了这跟 InnoDB b+ 树索引组织形式类似
作为对比你可以看一下这下面这两个语句的输出
5 使用 B-Tree hash 索引查询返回结果对比
可以看到执行 select * from t1 where id 的时候优化器会选择 B-Tree 索引所以返回结果是 0 4 使用 force index 强行使用主键 id 这个索引id=0 这一行就在结果集的最末尾了
其实一般在我们的印象中内存表的优势是速度快其中的一个原因就是 Memory 引擎支持 hash 索引当然更重要的原因是内存表的所有数据都保存在内存而内存的读写速度总是比磁盘快
但是接下来我要跟你说明为什么我不建议你在生产环境上使用内存表这里的原因主要包括两个方面
锁粒度问题
数据持久化问题
内存表的锁
我们先来说说内存表的锁粒度问题
内存表不支持行锁只支持表锁因此一张表只要有更新就会堵住其他所有在这个表上的读写操作
需要注意的是这里的表锁跟之前我们介绍过的 MDL 锁不同但都是表级的锁接下来我通过下面这个场景跟你模拟一下内存表的表级锁
6 内存表的表锁 复现步骤
在这个执行序列里session A update 语句要执行 50 在这个语句执行期间 session B 的查询会进入锁等待状态session C show processlist 结果输出如下
7 内存表的表锁 结果
跟行锁比起来表锁对并发访问的支持不够好所以内存表的锁粒度问题决定了它在处理并发事务的时候性能也不会太好
数据持久性问题
接下来我们再看看数据持久性的问题
数据放在内存中是内存表的优势但也是一个劣势因为数据库重启的时候所有的内存表都会被清空
你可能会说如果数据库异常重启内存表被清空也就清空了不会有什么问题啊但是在高可用架构下内存表的这个特点简直可以当做 bug 来看待了为什么这么说呢
我们先看看 M-S 架构下使用内存表存在的问题
8 M-S 基本架构
我们来看一下下面这个时序
业务正常访问主库
备库硬件升级备库重启内存表 t1 内容被清空
备库重启后客户端发送一条 update 语句修改表 t1 的数据行这时备库应用线程就会报错找不到要更新的行”。
这样就会导致主备同步停止当然如果这时候发生主备切换的话客户端会看到 t1 的数据丢失
在图 8 中这种有 proxy 的架构里大家默认主备切换的逻辑是由数据库系统自己维护的这样对客户端来说就是网络断开重连之后发现内存表数据丢失了”。
你可能说这还好啊毕竟主备发生切换连接会断开业务端能够感知到异常
但是接下来内存表的这个特性就会让使用现象显得更诡异由于 MySQL 知道重启之后内存表的数据会丢失所以担心主库重启之后出现主备不一致MySQL 在实现上做了这样一件事儿在数据库重启之后 binlog 里面写入一行 DELETE FROM t1
如果你使用是如图 9 所示的双 M 结构的话
9 M 结构
在备库重启的时候备库 binlog 里的 delete 语句就会传到主库然后把主库内存表的内容删除这样你在使用的时候就会发现主库的内存表数据突然被清空了
基于上面的分析你可以看到内存表并不适合在生产环境上作为普通数据表使用
有同学会说但是内存表执行速度快呀这个问题其实你可以这么分析
如果你的表更新量大那么并发度是一个很重要的参考指标InnoDB 支持行锁并发度比内存表好
能放到内存表的数据量都不大如果你考虑的是读的性能一个读 QPS 很高并且数据量不大的表即使是使用 InnoDB数据也是都会缓存在 InnoDB Buffer Pool 里的因此使用 InnoDB 表的读性能也不会差
所以我建议你把普通内存表都用 InnoDB 表来代替但是有一个场景却是例外的
这个场景就是我们在第 35 36 篇说到的用户临时表在数据量可控不会耗费过多内存的情况下你可以考虑使用内存表
内存临时表刚好可以无视内存表的两个不足主要是下面的三个原因
临时表不会被其他线程访问没有并发性的问题
临时表重启后也是需要删除的清空数据这个问题不存在
备库的临时表也不会影响主库的用户线程
现在我们回过头再看一下第 35 join 语句优化的例子当时我建议的是创建一个 InnoDB 临时表使用的语句序列是
create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);
了解了内存表的特性,你就知道了, 其实这里使用内存临时表的效果更好,原因有三个:
相比于 InnoDB 表,使用内存表不需要写磁盘,往表 temp_t 的写数据的速度更快;
索引 b 使用 hash 索引,查找的速度比 B-Tree 索引快;
临时表数据只有 2000 行,占用的内存有限。
因此,你可以对[第 35 篇文章]的语句序列做一个改写,将临时表 t1 改成内存临时表,并且在字段 b 上创建一个 hash 索引。
create temporary table temp_t(id int primary key, a int, b int, index (b))engine=memory;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);
图 10 使用内存临时表的执行效果
可以看到,不论是导入数据的时间,还是执行 join 的时间,使用内存临时表的速度都比使用 InnoDB 临时表要更快一些。
小结
今天这篇文章,我从“要不要使用内存表”这个问题展开,和你介绍了 Memory 引擎的几个特性。
可以看到,由于重启会丢数据,如果一个备库重启,会导致主备同步线程停止;如果主库跟这个备库是双 M 架构,还可能导致主库的内存表数据被删掉。
因此,在生产上,我不建议你使用普通内存表。
如果你是 DBA可以在建表的审核系统中增加这类规则要求业务改用 InnoDB 表。我们在文中也分析了,其实 InnoDB 表性能还不错,而且数据安全也有保障。而内存表由于不支持行锁,更新语句会阻塞查询,性能也未必就如想象中那么好。
基于内存表的特性,我们还分析了它的一个适用场景,就是内存临时表。内存表支持 hash 索引,这个特性利用起来,对复杂查询的加速效果还是很不错的。
最后,我给你留一个问题吧。
假设你刚刚接手的一个数据库上,真的发现了一个内存表。备库重启之后肯定是会导致备库的内存表数据被清空,进而导致主备同步停止。这时,最好的做法是将它修改成 InnoDB 引擎表。
假设当时的业务场景暂时不允许你修改引擎,你可以加上什么自动化逻辑,来避免主备同步停止呢?
你可以把你的思考和分析写在评论区,我会在下一篇文章的末尾跟你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。