
深入探讨MySQL
MySQL是全球使用最广泛的数据库系统之一,本篇博客将从MySQL的索引、事务、MVCC、锁机制等方面出发来深入解析MySQL数据库。
MySQL是一个开源的关系型数据库管理系统,广泛地应用于网站和应用程序的数据存储和管理。它最初由瑞典的公司MySQL AB开发,后来被甲骨文公司收购。尽管如此MySQL仍然是全球使用最广泛的数据库系统之一。
由于InnoDB是MySQL当前的默认存储引擎,本文的所有内容都将基于此进行讨论。
索引相当于“数据库的书签“,能够帮助我们快速定位数据。
索引作为优化数据库查询性能的重要工具,其作用毋庸置疑,但是过多或者不合理的索引反而会导致性能下降和存储空间的浪费。因此如何在合适的时机添加索引就显得尤其重要。
索引的主要作用就是加速查询,当某个字段经常被用作查询条件或者经常用于排序或分组操作时,如WHERE子句后、或者在ORDER BY、GROUP BY或DISTINCT中频繁使用时,就可以考虑为该字段添加索引。此外,当两张表或者多张表进行JOIN操作时,也可以在连接条件的字段上添加索引,以加速数据的匹配过程。
索引能够显著提升查询的性能,但在一些情况下,尽管我们已经对列设定了索引,但是MySQL的查询优化器会放弃使用索引,转而使用全表扫描,此时就失去了设定索引的意义。
当然实际时间当中我们可以通过EXPLAIN语句来查看执行计划,从而判断其是否使用了索引,如果使用了索引,则索引字段会显示在key列中,而type列则显示range表示索引范围扫描。
在数据库架构原理 | async已经简要介绍了B+树,使用B+树索引还可进一步细分为聚簇索引和非聚簇索引。
聚簇索引就是将数据存储和索引放在了一起,如果找到了索引,也就找到了数据,一般情况下主键会默认创建聚簇索引,且一张表仅允许存在一个聚簇索引。InnoDB中每张表都有一个聚簇索引,通常由主键列实现,如果没有定义主键,InnoDB会选择一个唯一的非空的索引列作为聚簇索引,如果没有合适的列,InnoDB会隐式创建一个内部主键列。
非聚簇索引叶子节点存储的是指向数据行的指针,而不是实际的数据行。再通过非聚簇索引查询的时候,需要进行回表操作,即通过主键从聚簇索引中再找到相应的数据行。设计非聚簇索引叶子节点仅存储指针而非实际的数据行的原因是一旦数据发生了迁移,不需要重新组织维护所有的索引。
此外,如果查询的数据列只需要通过索引就能够获取,就不再需要回表,这也称为覆盖索引。
如图所示:
假设要查找name=C的数据,那么会先在非聚簇索引下中通过C查找到主键id=9,然后再在聚簇索引的叶子节点中找到真正的数据。因此通过非聚簇索引进行检索,需要检索两次索引。
锁机制是数据库管理系统中并发控制的重要手段,主要用于解决数据并发访问冲突,保障数据的一致性和完整性。MySQL中不同的存储引擎实现了不同的锁机制,而在实际应用中,InnoDB存储引擎的锁机制是重点讨论对象。
按照锁的粒度进行划分,可以划分为以下两种:
如果按照锁的行为进行划分,则又可以分为以下两种:
SELECT * FROM table_name LOCK IN SHARE MODE;SELECT * FROM table_name FOR UPDATE;对于行级锁,在InnoDB中支持三种行锁定方式:
此外还有意向锁,这是一个表级锁,由存储引擎自己维护,用户无法手动操作。这个锁是在为数据行添加共享锁/排他锁之前存储引擎要先取得的,也一样分为意向共享锁和意向排他锁:
事务就是满足ACID的一组操作,可以通过Commit提交一个事务,可以通过Rollback进行回滚。
ACID简要来说就是以下四点:
在高并发的情况下,事务的隔离性是很难保证的,可能会出现很多并发一致性的问题:
MySQL中提供了四种事务隔离级别来解决这些问题。
MVCC (Multi-Version Concurrency Control),即多版本并发控制。MVCC是一种实现并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MySQL的InnoDB引擎中的MVCC实现主要针对已提交读和可重复读这两种隔离级别下的事务,因为未提交读会直接读取最新的未提交的版本,串行化通过加锁实现严格隔离,不依赖于MVCC,这两种隔离级别下的MVCC作用有限。
MySQL的InnoDB引擎对MVCC的实现:
每行数据在InnoDB中会存储两个隐藏的系统列,用于实现MVCC的多版本支持:
DB_TRX_ID:用于记录最后一次修改该行的事务ID。
DB_ROLL_PTR:指向Undo Log的指针,用于访问改行的旧版本。
这两列在用户表中是不可见的,但是在内部主要用于维护版本链。
在不同事务或者相同事务对于同一记录的修改,会导致该记录的Undo Log成为一条记录版本的线性表,即链表,Undo Log的链首就是最新的旧记录,链表尾就是最早的旧记录:
已提交读和可重复读的区别就在于其生成一致性视图的策略不同。一致性视图在事务启动时生成,用于定义哪些版本对当前事务可见。一般包含有以下内容:
m_ids:当前系统中活跃事务的ID列表。min_trx_id:m_ids中最小的事务ID。max_trx_id:当前系统分配的下一个事务ID。需要注意的是,对于事务 ID 位于 min_trx_id 和 max_trx_id 之间的情况,min_trx_id 和 max_trx_id 无法区分事务是已提交还是未提交,因为这个范围内包含了所有活跃事务(未提交事务)和刚刚提交的事务。
假设当前的min_trx_id为80,而max_trx_id为100,如果要访问的记录版本的事务ID为50,比min_trx_id小,则说明这个事务在之前就已经提交,因此对当前活动的事务而言是可以访问的。如果访问的记录版本的事务Id为90,在max_trx_id与min_trx_id之间,就再判断一下该事务是否在列表内,如果在就说明该事务还没有被提交,此时不能够访问,如果不在就说明事务已经提交,可以访问。如果要访问的的记录版本的事务Id为110,就说明其是在此一致性视图生成之后才发生的,因此不能够被访问。
所有的记录都是从Undo Log链中查找的,先查找最近的记录,如果最近这一条记录的事务Id不符合条件,不可见的话就再找上一个版本,比较当前事务Id和上一个版本的事务Id来进行比较,直到返回可见的版本。
已提交读隔离界别下的事务在每一次查询的开始都会生成一个独立的一致性视图,而可重复读隔离级别则是在第一次读的时候生成一个一致性视图,之后的读都会复用之前的一致性视图。
随着业务发展和数据量的快速增长,MySQL单库单表的架构难以满足高并发和大数据量的场景需求。这个时候就需要进行分库分表。分库分表的好处主要有减少单表的数据量,提升索引的效率,减少全表扫描。
分库的方法有垂直分库,分表有垂直分表和水平分表。
水平分表的时候如果查询时使用的条件不是分表键(用于决定数据分布规则的字段),会导致查询无法直接定位到某个具体的子表或者分库,这个时候我们可以采用冗余数据的方式,将非分表键的数据或者索引冗余存储到一个独立的全局表或者分库的其他表中,查询的时候先通过冗余表获取分表键,再根据分表键查询具体的分表。
主从复制主要有以下几种类型:
MySQL的主从复制主要涉及到三个线程:binlog线程、I/O线程以及SQL线程。
读写分离是MySQL在主从复制的基础上实现的一种数据库架构优化方案,核心的思想是写操作交由主库处理,而读操作分散到从库处理。这样可以减轻主库的压力,提高系统的并发处理能力,特别适合读多写少的业务场景。
读写分离能够提高性能的原因主要在于主从服务器负责各自的读和写,极大程度缓解了锁的争用,从服务器可以使用MyISAM存储引擎,从而提升查询性能,节约系统的开销。
读写分离通常使用代理的方式来实现,代理服务器接收应用层传来的读写请求,然后再决定转发到哪一个服务器。
在实际应用中,随着数据量和业务的持续增长,合理设计 MySQL 的架构、优化查询性能、保障数据一致性和系统可用性,都是我们需要不断深入和实践的领域。结合业务特点与需求,灵活运用索引优化、分库分表、读写分离等技术,将为系统性能和稳定性带来显著提升。
MySQL 的强大不仅在于其功能和性能,更在于它的灵活性与可扩展性。深入理解 MySQL 的底层原理与实现机制,将帮助我们在实际项目中更好地解决问题、优化系统,为业务的发展提供坚实的数据支撑。
参考资料:
MySQL :: MySQL 8.4 参考手册 :: 17.6.2.1 聚集索引和二级索引 --- MySQL :: MySQL 8.4 Reference Manual :: 17.6.2.1 Clustered and Secondary Indexes
Java 全栈知识点问题汇总(上) | Java 全栈知识体系
了解 MySQL 复制坐标 |吉尔伯特·马图塞维奇乌斯 --- Understanding MySQL replication coordinates | gilbertas matusevičius