作者:禅与计算机程序设计艺术
1.背景介绍
MyISAM是MySQL默认使用的存储引擎。它是一个高性能的静态表存储引擎,它保存了表结构信息及数据索引,适合于执行大量的静态SELECT操作。但是其不支持事物(transaction)、外键约束(foreign key constraints)、FULLTEXT索引等特性。因此,对于需要使用这些特性的应用场景,就需要使用其他类型的存储引擎。
InnoDB是另一种功能更丰富的存储引擎,提供了对ACID事务的支持、外键约束、行级锁定、全文搜索能力等特性。从MySQL 5.5版本开始,InnoDB取代MyISAM成为默认的存储引擎。
在本次博客中,我们将详细介绍MyISAM存储引擎,以期帮助读者了解MyISAM存储引擎的内部工作原理和核心算法原理,并可以提高数据库管理和优化技巧。文章不会涉及InnoDB存储引擎的具体内容。如需进一步阅读,可参考官方文档。
2.核心概念与联系
2.1 MyISAM文件构成
首先,我们需要理解MyISAM文件的组成结构。这里,为了简化表格,仅显示相关字段信息:
名称 | 类型 | 描述 |
.frm 文件 | 文件 | 表单文件,存储表定义; 如 CREATE TABLE t1(id INT) ENGINE=MyISAM; 会在当前目录下生成t1.frm文件。 |
.MYD 数据文件 | 文件 | 数据文件,存储表数据记录; 可以把数据文件理解为一张表的数据集合。 |
.MYI 索引文件 | 文件 | 索引文件,存储表索引; 可以在CREATE INDEX或ALTER TABLE ADD INDEX命令创建索引时自动生成此文件。 注意:索引文件只用于快速查询,不用于排序。 |
2.2 MyISAM索引实现方式
MyISAM存储引擎采用B-Tree索引方法组织索引,该索引的基本单位为页(page),一个页中最多可以存储数据索引项。每个索引块的大小通常为16KB。
当向MyISAM插入一条新记录时,如果数据量超过某个阈值(默认64K),则会自动创建一个新的磁盘页。同时,也会在之前的页中创建数据索引项,但旧页不会删除直到所有引用索引项都被更新为指向新页,然后才删除掉旧页。也就是说,如果插入数据的过程需要分裂页,那么整个页的写入和页内数据的删除都是原子性的,不会发生页的混乱。
另外,MyISAM还会尝试保持页内的数据顺序不变,即使已经插入了新数据。这一点是通过在每个页开头设置一个指针来实现的。该指针指向第一个字节处空余空间的位置。
MyISAM索引方式类似于哈希表,不过数据不是保存在磁盘中,而是在内存中。在MyISAM存储引擎中,索引也是被存放在磁盘上的,但数据却是在内存中的。由于索引可能很大,所以可以使用更快的内存访问速度进行搜索。
2.3 MyISAM锁机制
MyISAM存储引擎支持两种类型的锁,第一种是表级别的共享锁(S lock)和排他锁(X lock)。
所谓表级别的共享锁就是允许多个用户同时读取同一表的某些记录,也就是说,除了用WHERE条件指定要锁定的记录以外,其他用户也能继续往表中插入、修改或删除记录,但前提是不能读取这个范围以外的其他记录。
所谓表级别的排他锁(又称为写锁)是禁止其他用户读取或修改表的任何记录,也就是说,除了用WHERE条件指定要锁定的记录以外,其他用户不能对表做任何插入、修改或删除操作。但是,表上仍然可以执行查询语句。
除此之外,MyISAM还支持行级锁(又称为next-key lock),也就是记录锁,能锁定单条记录。在一个事务(transaction)中,如果要对某行数据进行读或写操作,必须先获得这个记录所在的页上的S锁或X锁,才能对其进行读或写,直到事务结束才释放锁。
为了实现行级锁,MyISAM存储引擎在索引中加入了间隙锁(gap lock)。间隙锁是一种特殊的锁,它锁住了一个范围,但是不包括记录本身。比如,如果在一个范围[A, B]上加X锁,并在B上加S锁,则表示“这个范围内的所有记录均不能进行任何更新操作”。这种锁策略能够有效防止幻读的出现。
3.核心算法原理和具体操作步骤
3.1 数据读取
- 如果请求的页面不在缓冲池,则读取页对应的磁盘块到缓冲池中;
- 从缓冲池中查找请求的数据;
- 如果缓存中没有命中,则从磁盘中读取对应的数据页到缓冲池中;
- 对数据页进行解析,找出相应的数据;
- 将数据返回给客户端。
3.2 数据写入
- 检查是否已经打开了事务,若没有则打开;
- 在插入缓冲池中准备好数据记录的各种信息;
- 判断插入的记录是否属于完整的页,若不是,则将剩下的记录添加到预写日志中;
- 在事务结束时,将预写日志中的记录刷新到磁盘,并合并插入缓冲池中与磁盘中相同记录的冲突信息;
- 更新内存的数据字典。
3.3 数据删除
与插入流程相似,只是在提交时将记录的墓碑标记为可删除,再由后台线程定时扫描标记过期的墓碑,真正将数据从磁盘上物理删除。
3.4 创建索引
根据索引列值的前缀构建索引树,并生成相应的索引文件,其中树的高度决定了索引的效率。
3.5 搜索索引
按照索引文件的内容进行搜索,依据索引值的精确匹配或范围匹配,找到检索的数据地址,从数据文件中读取数据并返回结果。
3.6 分裂页
当需要插入的数据页占用空间超过一定比例时,则自动分裂该页。分裂后的新页将留出一半可用空间供插入数据,原页的剩余数据则移到新页。同时,在原页中插入一条指向新页的指针,以便于快速定位。
4.具体代码实例和详细解释说明
# 创建表
mysql> create table myisam_test (
-> id int primary key auto_increment,
-> name varchar(20),
-> age int);
# 插入数据
mysql> insert into myisam_test values (null,'Tom',25),(null,'Jack',30),(null,'Lucy',20);
# 查询数据
mysql> select * from myisam_test where name='Tom';
+----+-----+---+
| id | name|age|
+----+-----+---+
| 1 | Tom | 25|
+----+-----+---+
# 关闭myisam
mysql> show tables like'myisam%';
+-----------------+
| Tables_in_mysql |
+-----------------+
| myisam_test |
+-----------------+
mysql> alter table myisam_test engine = InnoDB;
mysql> show tables like'myisam%';
+-----------------+
| Tables_in_mysql |
+-----------------+
| myisam_test |
+-----------------+
# 查看数据页
mysql> use information_schema;
mysql> SELECT
TABLE_SCHEMA,
TABLE_NAME,
DATA_LENGTH/1024 AS DATA_LENGTH_MB,
INDEX_LENGTH/1024 AS INDEX_LENGTH_MB
FROM
TABLES WHERE
TABLE_TYPE LIKE '%myisam%' AND
ENGINE LIKE '%myisam%' ORDER BY
DATA_LENGTH DESC;
+------------------+--------------------+-------------+----------------+
| TABLE_SCHEMA | TABLE_NAME | DATA_LENGTH | INDEX_LENGTH |
+------------------+--------------------+-------------+----------------+
| mysql | columns_priv | 197 | 22 |
| mysql | db | 69 | 116 |
| mysql | event | 32 | 0 |
| mysql | func | 11049 | 5680620 |
| mysql | general_log | 0 | 0 |
| mysql | help_category | 0 | 0 |
| mysql | help_keyword | 0 | 0 |
| mysql | help_relation | 0 | 0 |
| mysql | help_topic | 0 | 0 |
| mysql | ndb_binlog_index | 168 | 0 |
| mysql | plugin | 0 | 0 |
| mysql | proc | 0 | 0 |
| mysql | procs_priv | 0 | 0 |
| mysql | proxies_priv | 0 | 0 |
| mysql | server_cost | 0 | 0 |
| mysql | servers | 69 | 40 |
| mysql | slave_master_info | 0 | 0 |
| mysql | slow_log | 0 | 0 |
| mysql | tables_priv | 0 | 0 |
| mysql | time_zone | 69 | 0 |
| mysql | time_zone_leap_second| 76 | 0 |
| mysql | time_zone_name | 0 | 0 |
| mysql | time_zone_transition| 88 | 0 |
| mysql | time_zone_transition_type| 248 | 0 |
| mysql | user | 72 | 0 |
| mysql | global_grants | 0 | 0 |
| mysql | db | 16 | 0 |
| test | myisam_test | 36 | 0 |
+------------------+--------------------+-------------+----------------+
# 使用explain查看查询计划
mysql> explain select * from myisam_test where name='Tom';
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: myisam_test
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: PRIMARY
key_len: 8
ref: NULL
rows: 3
filtered: 100.00
Extra: Using index
*************************** 1 row in set (0.00 sec)
# 删除表
mysql> drop table myisam_test;
# 打开innodb
mysql> create table innodb_test (
-> id int primary key auto_increment,
-> name varchar(20),
-> age int);
mysql> show tables like 'innodb%';
+-----------------+
| Tables_in_mysql |
+-----------------+
| innodb_test |
+-----------------+
mysql> insert into innodb_test values (null,'Tom',25),(null,'Jack',30),(null,'Lucy',20);
# 修改表引擎
mysql> alter table innodb_test engine = myisam;
mysql> show tables like 'innodb%';
+-----------------+
| Tables_in_mysql |
+-----------------+
| innodb_test |
+-----------------+
# 清除缓存
mysql> flush tables with read lock; -- 读取锁表
mysql> unlock tables; -- 释放锁表
# 查询数据
mysql> select * from innodb_test where name='Tom';
Empty set (0.00 sec)
# 使用explain查看查询计划
mysql> explain select * from innodb_test where name='Tom';
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: INNODB_SYS_TABLES
partitions: NULL
type: SYSTEM
rrn: NULL
fsn: NULL
rows: NULL
filtered: 100.00
Extra: NULL
*************************** 1 row in set (0.00 sec)