我在上一篇文章末尾留给你的问题是:两个 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 的位置bash
能够看到,内存表 t1 的返回结果里面 0 在最后一行,而 InnoDB 表 t2 的返回结果里 0 在第一行。网络
出现这个区别的缘由,要从这两个引擎的主键索引的组织方式提及。session
表 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)
从中咱们能够看出,这两个引擎的一些典型不一样:
1. InnoDB 表的数据老是有序存放的,而内存表的数据就是按照写入顺序存放的;
2. 当数据文件有空洞的时候,InnoDB 表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就能够插入新值;
3. 数据位置发生变化的时候,InnoDB 表只须要修改主键索引,而内存表须要修改全部索引;
4. InnoDB 表用主键索引查询时须要走一次索引查找,用普通索引查询的时候,须要走两
5. 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 篇文章的内容。那若是要让内存表支持范围扫描,应该怎么办呢 ?
实际上,内存表也是支 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<5 的时候,优化器会选择 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 基本架构
咱们来看一下下面这个时序:
1. 业务正常访问主库;
2. 备库硬件升级,备库重启,内存表 t1 内容被清空;
3. 备库重启后,客户端发送一条 update 语句,修改表 t1 的数据行,这时备库应用线程就会报错“找不到要更新的行”。
这样就会致使主备同步中止。固然,若是这时候发生主备切换的话,客户端会看到,表 t1的数据“丢失”了。
在图 8 中这种有 proxy 的架构里,你们默认主备切换的逻辑是由数据库系统本身维护的。这样对客户端来讲,就是“网络断开,重连以后,发现内存表数据丢失了”。
你可能说这还好啊,毕竟主备发生切换,链接会断开,业务端可以感知到异常。
可是,接下来内存表的这个特性就会让使用现象显得更“诡异”了。因为 MySQL 知道重启以后,内存表的数据会丢失。因此,担忧主库重启以后,出现主备不一致,MySQL 在
实现上作了这样一件事儿:在数据库重启以后,往 binlog 里面写入一行 DELETE FROMt1。
若是你使用是如图 9 所示的双 M 结构的话:
图 9 双 M 结构
在备库重启的时候,备库 binlog 里的 delete 语句就会传到主库,而后把主库内存表的内容删除。这样你在使用的时候就会发现,主库的内存表数据忽然被清空了。
基于上面的分析,你能够看到,内存表并不适合在生产环境上做为普通数据表使用。
有同窗会说,可是内存表执行速度快呀。这个问题,其实你能够这么分析:
1. 若是你的表更新量大,那么并发度是一个很重要的参考指标,InnoDB 支持行锁,并发度比内存表好;
2. 能放到内存表的数据量都不大。若是你考虑的是读的性能,一个读 QPS 很高而且数据量不大的表,即便是使用 InnoDB,数据也是都会缓存在 InnoDB Buffer Pool 里的
所以,使用 InnoDB 表的读性能也不会差。
因此,我建议你把普通内存表都用 InnoDB 表来代替。可是,有一个场景倒是例外的。
这个场景就是,咱们在第 35 和 36 篇说到的用户临时表。在数据量可控,不会耗费过多内存的状况下,你能够考虑使用内存表。
内存临时表恰好能够无视内存表的两个不足,主要是下面的三个缘由:
1. 临时表不会被其余线程访问,没有并发性的问题;
2. 临时表重启后也是须要删除的,清空数据这个问题不存在;
3. 备库的临时表也不会影响主库的用户线程。
如今,咱们回过头再看一下第 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);
了解了内存表的特性,你就知道了, 其实这里使用内存临时表的效果更好,缘由有三个:
1. 相比于 InnoDB 表,使用内存表不须要写磁盘,往表 temp_t 的写数据的速度更快;
2. 索引 b 使用 hash 索引,查找的速度比 B-Tree 索引快;
3. 临时表数据只有 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 引擎表
假设当时的业务场景暂时不容许你修改引擎,你能够加上什么自动化逻辑,来避免主备同步中止呢?
你能够把你的思考和分析写在评论区,我会在下一篇文章的末尾跟你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一块儿阅读。
今天文章的正文内容,已经回答了咱们上期的问题,这里就再也不赘述了。
评论区留言点赞板
@老杨同志、@poppy、@长杰 这三位同窗给出了正确答案,春节期间还持续保持跟进学习,给大家点赞。