《高性能MySQL》读书笔记(上)

《High Performance MySQL》真是本经典好书,从应用层到数据库到硬件平台,各种调优技巧、常见问题全都有所提及。数据库的各种概念技巧平时都有接触,像索引、分区、Sharding等等,但要想真正提高还是得如此系统学习一下。

Chapter 1: MySQL Architecture and History


1.1 Transaction Isolation Level

事务隔离级别真是个老生常谈的问题的,但大多材料一提到脏读、幻读、重复读就讲得云里雾里,所以还是自己动手实践能体会最深。推荐文章:MySQL数据库事务隔离级别

1.2 Implicit and Explicit Locking

InnoDB默认自动根据事务隔离级别管理锁,同时支持两种标准SQL未提及的显示锁(Explicit Locking):

  • SELECT ... LOCK IN SHARE MODE
  • SELECT ... FOR UPDATE
  • LOCK/UNLOCK TABLES

InnoDB采用两阶段锁协议(Two-phase Locking Protocol)。在事务内任意时刻加锁,最后提交或回滚时一起释放所有锁。两阶段锁协议(跟分布式事务XA的两阶段提交)具体如下:

“一次性锁协议:事务开始时,一次性申请所有的锁,之后不会再申请任何锁。如果其中某个锁不可用,则整个申请就不成功,事务就不会执行,在事务尾端,一次性释放所有的锁。一次性锁协议不会产生死锁的问题,但事务的并发度不高。

“两阶段锁协议:整个事务分为两个阶段,前一个阶段为加锁,后一个阶段为解锁。在加锁阶段,事务只能加锁,也可以操作数据,但不能解锁。直到事务释放第一个锁,就进入解锁阶段,此过程中事务只能解锁,也可以操作数据,不能再加锁。

两阶段锁协议使得事务具有较高的并发度,因为解锁不必发生在事务结尾。它的不足是没有解决死锁的问题,因为它在加锁阶段没有顺序要求。如两个事务分别申请了A, B锁,接着又申请对方的锁,此时进入死锁状态。

1.3 Multiversion Concurrency Control

类似于乐观锁机制,但一些文章介绍到InnoDB实现不是纯粹的MVCC。先标注一下,回头进行深入源码研究。收藏文章:何登成的InnoDB多版本(MVCC)实现简要分析,老码农的MySQL中的MVCC

“Innodb的实现真算不上MVCC,因为并没有实现核心的多版本共存,undo log中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。但理想的MVCC是难以实现的,当事务仅修改一行记录使用理想的MVCC模式是没有问题的,可以通过比较版本号进行回滚;但当事务影响到多行数据时,理想的MVCC据无能为力了。
“理想MVCC难以实现的根本原因在于企图通过乐观锁代替二段提交。修改两行数据,但为了保证其一致性,与修改两个分布式系统中的数据并无区别,而二提交是目前这种场景保证一致性的唯一手段。二段提交的本质是锁定,乐观锁的本质是消除锁定,二者矛盾,故理想的MVCC难以真正在实际中被应用,Innodb只是借了MVCC这个名字,提供了读的非阻塞而已

Chapter 4: Optimizing Schema and Data Types


4.1 Choosing Optimal Data Types

本章一上来就精辟的提出了关于模式和数据类型的总设计原则,那就是:

  1. Smaller is usually better(越小通常越好):因为占用更少磁盘空间,内存以及CPU缓存,所以越小通常代表越快。
  2. Simple is good(简单的就是好的):因为字符集和排序规则(Collation)使得字符串的比较很复杂,所以我们应当用Integer等内建类型而非字符串来保存日期时间或IP地址。
  3. Avoid NULL if possible(尽可能避免NULL):MySQL对NULL有特殊的处理逻辑,所以NULL会使索引、索引统计、值比较都变得更加复杂。

4.2 Using ENUM Instead Of A String Type

MySQL内部将枚举保存为整数,通过一张Lookup Table保存枚举与整数的对应关系。所以使用枚举非常节省空间(原则1越小越好越快),根据枚举总个数而定,只会占用1或2个字节。

但是随之而来的问题是:添加删除枚举值都要ALTER TABLE。并且使用Lookup Table进行转换时也会有开销,尤其是与CHAR或VARCHAR类型的列做联接时,但有时这种开销可以被枚举节省空间的优势所抵消。

4.3 Cons of A Normalized Schema

规范化范式(Normalized Schema)不仅增加JOIN数,并且会使本可以属于一个索引的列分隔到不同的表中。

例如:SELECT ... FROM message INNER JOIN user USING(user_id)

WHERE user.account_type = ‘premium‘

ORDER BY message.published DESC LIMIT 10

则有两种执行计划:

  1. 倒序走published索引扫描message表,每行都去user表检查是否type为‘premium‘,直到找到10行。
  2. 走account_type索引扫描user表找到所有type为‘premium‘的行,进行filesort后返回10行。

上面的问题本质在于:JOIN使我们无法通过一个索引就同时完成排序和过滤

改为非规范化 => SELECT .. FROM user_message

WHERE account_type = ‘premium‘

ORDER BY published DESC LIMIT 10

则(account_type, published)上的索引能高效地完成任务!

4.4 Cache and Summary Tables

这一部分紧接上面关于Normalized和Denormalized Schema的Pros and Cons的讨论,从4.4到4.6提出了几种冗余数据的常用且实用的方法。这几种技术本质上都是为了加速查询操作,但代价是拖慢了写操作,并且会增加开发的复杂度。

缓存表(Cache Table)指那些包含能够轻松从Schema中获得的数据的表,即表中的数据是逻辑冗余(Logically Redundant)。汇总表(Summary/Roll-up Table)是说包含通过聚合函数得到的数据的表,例如表中数据是通过GROUP BY得到的。

为什么需要它们呢?最常见的场景就是报表等统计工作。生成这些统计数据要扫描大量数据,实时计算成本很高且很多时候没有必要。而且查询这些数据还要加大量组合索引(各种维度的)才能提高性能,然而这些索引又会对平时的更新和插入等操作造成影响。于是常用技术就是添加中间表到其他引擎(利用MyISAM更小的索引和全文检索能力),甚至其他系统(Lucene或Sphinx)。

有了中间表作为缓存,我们需要定期的更新或者重建它。影子表(Shadow Table)是一种不错的技术!

mysql> DROP TABLE IF EXISTS my_summary_new, my_summary_old;

mysql> CREATE TABLE my_summary_new LIKE my_summary;

mysql> RENAME TABLE my_summary TO my_summary_old, my_summary_new TO my_summary

只需一条rename操作,我们就可以原子地将影子表替换上去(swap with an atomic rename),并且之前的表也保留下来以防需要回滚。

4.5 Materialized Views

物化视图即预先计算并真正存储在磁盘上的视图(一般视图是不会实际存储,在访问视图时执行对应的SQL获得数据)。MySQL没有物化视图,但有一个很棒的开源实现Flexviews Tools。它有一些很有用的功能,例如:

  1. CDC(Change Data Capture)工具能够读取日志(Binary Logs),并提取对应的行变化。
  2. 一组帮助定义和管理视图的存储过程
  3. 将改变反应到物化视图数据上的工具

具体来说,它利用基于行的日志(Row-based Binary Log)包含了变化行的前后数据,所以Flextviews能够在无需访问源表的情况下,知道变化前和变化后的数据,并重新计算物化视图。这是它相比我们自己维护的Cache表或Summary表的优势。

4.6 Counter Tables

Web应用一个常见问题就是并发访问计数表,此书中提出方案来提高并发量。总体设计思路是:添加更多的槽来分散并发的访问。与Java的Concurrent并发包中的ConcurrentHashMap的设计理念有些像。

计数表和对应访问SQL可以简化如下:

mysql> CREATE TABLE hit_counter(cnt int unsigned not null) ENGINE=InnoDB;

mysql> UPDATE hit_counter SET cnt = cnt + 1;

可以看出,表中的一行计数器数据其实相当于全局锁,对它的更新将会被串行化。所以,首先建表时加入Slot一列。并初始化100条数据。

CREATE TABLE hit_counter(

slot tinyint unsigned not null primary key,

cnt int unsigned not null

) ENGINE=InnoDB;

之后将更新和查询SQL改为:

mysql> UPDATE hit_counter SET cnt = cnt + 1 WHERE slot = RAND() * 100;

mysql> SELECT SUM(cnt) FROM hit_counter;

ps:如果需要每天刷新计数器的话,那么建表时就加入时间列:

CREATE TABLE daily_hit_counter(

day date not null,

slot tinyint unsigned not null primary key,

cnt int unsigned not null,

primary key(day, slot)

) ENGINE=InnoDB;

pss:如果不想每天都插入初始数据的话,可以用下面的SQL:

mysql> INSERT INTO daily_hit_counter(day, slot, cnt)

VALUES(CURRENT_DATE, RAND() * 100, 1)

ON DUPLICATE KEY UPDATE cnt = cnt + 1;

psss:如果想减少计数器的行数来节约空间,那么可以执行一个定期任务来合并所有记录到Slot 0:

Chapter 5: Indexing for High Performance


5.1 B-Tree Family

一般我们讨论数据库索引时,其实指的都是B树索引,MySQL的CREATE TABLE及其他语句中也的确使用这种说法。然而实际上,存储引擎内部可能会使用不同的存储结构。例如NDB使用T树(关于不同的索引类型,在我的另一篇介绍内存数据库中也有所提及。T树就非常适合内存存储),而InnoDB使用B+树。

所以准确地说我们使用的是B树大家族里B树的各种变形。各种变形的核心是围绕着内结点出度(例如基于内存的T树和基于磁盘的B树)、存储使用率(B树和B+树)等方面进行的。

首先B树与其他数据结构如红黑树、普通AVL树的最大区别就是:B树的结点有很多个子结点。而这一点正是为减少磁盘I/O读取开销而设计。因为子结点很多,所以树的总体高度很低,这样就只需加载少量的磁盘页就能查找到目标数据。那关于B树和B+树的区别呢:B+树的内结点不存data(即指向key所在数据行的指针),只存key。

B+树的优势:

  • 因为内部结点不存data了,所以在一个磁盘页上能存更多的key了,树的高度进一步降低,从而加快key的查找命中。
  • 需要全树遍历时(如某字段的范围查询甚至full scan,这都是很常见而频繁的查询操作),只需要对B+树的叶子结点进行线性遍历即可,而B树则需要树遍历。而线性遍历比树遍历命中率更高(因为相邻数据都很近,不会分散在结点的左右子树中,跨页的概率能低一些吧)

B树的优势:

  • 在B树中查找可能在内部结点结束,而B+树则必须在叶子结点结束。

首先引用一个B树查找的例子

“下面,咱们来模拟下查找文件29的过程:

  1. 根据根结点指针找到文件目录的根磁盘块1,将其中的信息导入内存。【磁盘IO操作 1次】
  2. 此时内存中有两个文件名17、35和三个存储其他磁盘页面地址的数据。根据算法我们发现:17<29<35,因此我们找到指针p2。
  3. 根据p2指针,我们定位到磁盘块3,并将其中的信息导入内存。【磁盘IO操作 2次】
  4. 此时内存中有两个文件名26,30和三个存储其他磁盘页面地址的数据。根据算法我们发现:26<29<30,因此我们找到指针p2。
  5. 根据p2指针,我们定位到磁盘块8,并将其中的信息导入内存。【磁盘IO操作 3次】
  6. 此时内存中有两个文件名28,29。根据算法我们查找到文件名29,并定位了该文件内存的磁盘地址。

分析上面的过程,发现需要3次磁盘IO操作和3次内存查找操作。关于内存中的文件名查找,由于是一个有序表结构,可以利用折半查找提高效率。至于IO操作是影响整个B树查找效率的决定因素。当然,如果我们使用平衡二叉树的磁盘存储结构来进行查找,磁盘4次,最多5次,而且文件越多,B树比平衡二叉树所用的磁盘IO操作次数将越少,效率也越高。

而B+树就是这个样子:

了解了B树的物理结构,那么下面就总结一下能够利用B树索引的查询类型吧。这对我们进行SQL优化非常有帮助。以:

mysql> CREATE TABLE People(

last_name varchar(50) not null,

first_name varchar(50) not null,

date_of_birth date not null,

gender enum(‘m‘, ‘f‘) not null,

key(last_name, first_name, date_of_birth)

);

  • 全列匹配查询:
  • 部分列匹配查询:
  • 列最左匹配查询:
  • 范围查询:
  • Index-only查询:即只访问索引,而无需访问整个行数据。
时间: 2025-01-03 22:30:52

《高性能MySQL》读书笔记(上)的相关文章

C++Windows核心编程读书笔记

转自:http://www.makaidong.com/%E5%8D%9A%E5%AE%A2%E5%9B%AD%E6%96%87/71405.shtml "C++Windows核心编程读书笔记": 关键词:c++windows 核心 编程 读书笔记 这篇笔记是我在读<windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和windows实际机制可能有出入,但应该是合理的.开头几章由于我追求简洁

Windows核心编程读书笔记1

今天特别困啊,这是为什么?!!刚刚把第一章看了一下,困到不行,所以写blog清醒一下. 第一章标题是"错误处理",看了之后吓了一跳,难道第一章就讲这么高大上的东西?!不是不是,我现在的理解是,这章主要还是带我们感受一下以后遇到问题如何知道这个问题的描述. 我们在使用函数后,总会有一个结果:失败也好,成功也好.而这个结果,最终还是要以标识的形式落在地上的,章节内主要讲的是返回LONG/DWORD类型的情况.每个函数最后会返回一个错误代码给调用线程. 想要看返回的是啥,这里提供了一种方法:

Windows核心编程读书笔记-第六章线程基础

1.相较于线程,进程所使用的系统资源更多.其原因在于地址空间.为一个进程创建一个虚拟的地址空间需要大量系统资源.线程只有一个内核对象和一个栈. 2.线程的入口函数 DWORD WINAPI ThreadFunc(PVOID pvParam){ DWORD dwResult = 0; ... return(dwResult); } 线程函数的几点说明 线程函数可以任意命名. 线程函数只有一个参数,而其意义由我们(而非操作系统)来定义.因此,我们不必担心ANSI/Unicode问题. 线程函数必须返

Windows核心编程读书笔记-第四章进程

1.进程组成 一个内核对象,操作系统用它来管理进程. 一个地址空间,其中包含所有可执行文件或DLL模块的代码和数据.此外,它还包含动态内存分配,比如线程堆栈和堆的分配. 2.一个进程可以有多个线程,所有线程都在进程的地址空间中"同时"执行代码.每个进程至少要有一个线程来执行进程地址空间包含的代码. 3.用Microsoft Visual Studio来创建一个应用程序项目时,集开发环境会设置各种链接器开关,使链接器将子系统的正确类型嵌入最终生成的可执行文件.对于CUI程序,这个链接器开

pthon核心编程-读书笔记:知识点摘录与总结(方便理解和快速记忆)

Python 中的列表(大小可变的数组)和字典(哈希表)就是内建于语言本身的.在核心语言中提供这些重要的构建单元,可以鼓励人们使用它们, 缩短开发时间与代码量,产生出可读性更好的代码.C不提供, c++功能不够简洁. 面向对象, 可升级:Python 提倡简洁的代码设计.高级的数据结构和模块化的组件,确保灵活性. 一致性并缩短必要的调试时间 扩展性:兼容扩展c和java 易读写,易维护 健壮性:Python 提供了"安全合理"的退出机制, Python由于错误崩溃,解释程序就会转出一个

《windows核心编程》笔记(API)

第一章:错误处理 1.UNREFERENCED_PARAMETER的用处 2.LoadString的用法 3.MAKEINTRESOURCE,LoadIcon,LoadCursor的用法 4.RegisterClassEx,CreateWindow,ShowWindow,UpdateWindow,详情见百科 5.LOWORD,HIWORD的用法 6.DialogBox,DestroyWindow,DefWindowProc,PostQuitMessage,BeginPaint,EndPaint的

《Windows核心编程》读书笔记 上

[C++]<Windows核心编程>读书笔记 这篇笔记是我在读<Windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和Windows实际机制可能有出入,但应该是合理的.开头几章由于我追求简洁,往往是很多单独的字句,后面的内容更为连贯. 海量细节. 第1章    错误处理 1.         GetLastError返回的是最后的错误码,即更早的错误码可能被覆盖. 2.         GetLas

【转】《windows核心编程》读书笔记

这篇笔记是我在读<Windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和Windows实际机制可能有出入,但应该是合理的.开头几章由于我追求简洁,往往是很多单独的字句,后面的内容更为连贯. 海量细节. 第1章    错误处理 1.         GetLastError返回的是最后的错误码,即更早的错误码可能被覆盖. 2.         GetLastError可能用于描述成功的原因(CreatEvent)

读书笔记----《windows核心编程》第三章 内核对象1(句柄与安全性)

最近一直没有更新博客,因为一直在想一个问题,内核对象这一章内容很多很重要,自己没有掌握好也没有把握写好这一章,最后还是决定能写多少写多少,一面写一面学,后续学到新的再更新吧; <windows核心编程>提了几种内核对象: 访问令牌对象:与windows的安全性有关,目前不是很懂,了解后再写; 事件对象: Event对象,可跨进程同步; 由CreateEvent创建; 文件对象: File对象,比较常见; 由CreateFile创建; 文件映射对象: 通过文件映射可以方便的操作文件(如同文件数据

Windows核心编程笔记(1)

最近工作比较闲了,一直没来得及看的核心编程最近开始看了,分享下笔记. 1.内核句柄用完不释放一定会造成内存泄漏吗? 不一定,内核句柄在进程退出时会被系统释放掉(遍历内核句柄表,只要每个句柄指向的内核对象的引用计数为0,内核就会销毁该对象,适用于所有的内核对象.资源(GDI对象在内).内存块): 2.内核对象如何关闭? 调用CloseHandle(),内核会查找该进程的句柄表,如果没找到该句柄,返回FALSE(Debug下抛出异常);如果找到,则使该句柄指向的内核对象引用计数减一,若引用计数为0,