🍊 索引数据结构
🎉 磁盘存储
mysql是从磁盘读取数据到内存的,是以磁盘块为基本单位的,位于同一磁盘块中的数据会被一次性读取出来,不是按需读取。以InnoDB存储引擎来说,它使用页作为数据读取单位,页是其磁盘管理的最小单位,默认大小是16kb。系统的一个磁盘块的存储空间往往没有这么大,所以InnoDB每次申请磁盘空间时都会是多个地址连续磁盘块来达到页的大小16KB。假设一行数据的大小是 1k,那么一个页可以存放 16 行这样的数据。那如果想查找某个页里面的一个数据的话,得首先找到他所在的页。
在查询数据时一个页中的每条数据都能定位数据记录的位置,这会减少磁盘 I/O 的次数,提高查询效率。InnoDB存储引擎在设计时是将根节点常驻内存的,力求达到树的深度不超过 3,也就是说I/O不超过3次。
树形结构的数据可以让系统高效的找到数据所在的磁盘块,这里就可以说一下这个b树和b+树了
B树的结构是每个节点中有key也有value,而每一个页的存储空间是16kb,如果数据较大时将会导致一页能存储数据量的数量很小。
B+Tree的结构是将所有数据记录节点按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储 key 值信息,这样可以大大加大每个节点存储的key 值数量,降低B+Tree的高度。B+树是为磁盘或其他直接存取辅助设备设计的一种平衡查找树。在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点上,由各叶子节点指针进行连接。 B+索引在数据库中有一个特点是高扇出性,因此在数据库中,B+树的高度一般都在2~4层,这也就是说查找某一键值的行记录时最多只需要2到4次IO,这倒不错。因为当前一般的机械磁盘每秒至少可以做100次IO, 2~4次的IO意味着查询时间只需0.02~0.04秒。 数据库中的B+树索引可以分为聚集索引和辅助索引。
🎉 假设每条sql信息为1kb,主键ID为bigint型,一颗高度为2,3,4高度的B+树分别可以存储多少行数据?
因为单个页的大小为 16kb,而一行数据的大小为 1kb,也就是说一页可以存 放 16 行数据。然后因为非叶子节点的结构是:“页指针 + 键值”,我们假设主键ID 为 bigint 类型,长度为 8 字节(byte),而指针大小在 InnoDB 源码中设置为 6 字节(byte),这样一共 14 字节(byte),因为一个页可以存放 16k 个 byte,所以一个页可以存放的指针个数为 16384/14=1170 个。因此一个两层的 B + 树可 以存放的数据行的个数为:1170*16=18720(行)。
也就是说第一层的页,即根页可以存放 1170 个指针,然后第二层的每个页也可以 存放 1170 个指针。这样一共可以存放 1170*1170 个指针,所以一共可以存放 1170*1170*16=21902400(2千万 左右) 行记录。也就是说一个三层的 B + 树就可以存放千万级别的数据了。
高度为4的B+树则是 1170*1170*1170*16 约等于 2000万*1000,1000个 2000 万就是 200亿行的数据了。
🎉 为什么选用B+树做索引而不选用二叉树或者B树?
b 树和 b + 树应用在数据库索引,可以认为是 m 叉的多路平衡查找树,但是从理论上讲,二叉树查找速度和比较次数都是最小的,为什么不用二叉树呢? 因为我们要考虑磁盘 IO 的影响,它相对于内存来说是很慢的。数据库索引是存储在磁盘上的,当数据量大时, 就不能把整个索引全部加载到内存了,只能逐一加载每一个磁盘页(对应索引树的节点)。所以我们要减少 IO 次数,对于树来说,IO 次数就是树的高度,而 “矮胖” 就是 b 树的特征之一,它的每个节点最多包含 m 个孩子, m 称为 b 树的阶。 为什么不用B树呢? b + 树,是 b 树的一种变体,查询性能更好。 b + 树相比于 b 树的查询优势:
1.b + 树的中间节点不保存数据,所以磁盘页能容纳更多节点元素,更 “矮胖”。B 树不管叶子节点还是非叶子节 点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况 下要保存大量数据,只能增加树的高度,导致 IO 操作变多,查询性能变低;
2.b + 树查询必须查找到叶子节点,b 树只要匹配到即可直接返回。因此 b + 树查找更稳定(并不慢),必须查 找到叶子节点;而B树,如果数据在根节点,最快,在叶子节点最慢,查询效率不稳定。
3.对于范围查找来说,b + 树只需遍历叶子节点链表即可,并且不需要排序操作,因为叶子节点已经对索引进行 了排序操作。b 树却需要重复地中序遍历,找到所有的范围内的节点。
🎉 为什么用 B+ 树做索引而不用哈希表做索引?
1、模糊查找不支持:哈希表是把索引字段映射成对应的哈希码然后再存放在对应的位置, 这样的话,如果我们要进行模糊查找的话,显然哈希表这种结构是不支持的,只能遍历这个 表。而 B + 树则可以通过最左前缀原则快速找到对应的数据。
2、范围查找不支持:如果我们要进行范围查找,例如查找 ID 为 100 ~ 400 的人,哈希表同 样不支持,只能遍历全表。
3、哈希冲突问题:索引字段通过哈希映射成哈希码,如果很多字段都刚好映射到相同值的 哈希码的话,那么形成的索引结构将会是一条很长的链表,这样的话,查找的时间就会大大增加。
🍊 SQL优化
- 针对SQL进行调整,在写SQL的时候遵循最左前缀原则,向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,范围列可以用到索引,但是范围列后面的列无法用到索引。
- like以通配符%开头索引失效会变成全表扫描的操作。
- 如果查询条件中含有函数或表达式,将导致索引失效而进行全表扫描。
- 只要列中包含有 NULL 值都将不会被包含在索引中,复合索引中只要有一列含有 NULL 值,那么这一列对于此复合索引就是无效的。
- 不要使用select *,改用select加字段名称,因为select *走的聚集索引,会进行全表扫描,如果一定要使用select *的话,mysql至少使用5.6版本,这个版本有一个离散读的优化,离散读的优化是将离散度大的列放到联合索引的前面,举个例子,select * from user where staff_id = 2 and customer_id = 584,这个时候索引优化会将customer_id放到前面,因为它的离散度更高,可以通过select count(distinct customer_id),count(distinct staff_id) from user查看列的离散度。
5.6版本有一个ICP的优化,以往根据索引查找记录,再根据WHERE条件来过滤记录。使用ICP优化后,会在取出索引的同时,直接根据WHERE条件过滤,将WHERE的部分过滤操作放在了存储引擎层。在某些查询下可以大大减少上层SQL层对记录的索取,从而提高性能。
5.6版本还有一个MRR优化,是批量处理对键值的查询操作ICP优化,减少缓冲池中页被替换的次数,使数据访问变得较为顺序。辅助索引查询得到书签后,先对主键进行排序,再按序进行查找。
- 另外在写sql的时候,尽量使用它的一个执行计划,去看我们的索引是不是失效了。
🍊 索引失效的几种情况
- 如果条件中有or,即使其中有部分条件带索引也不会使用。
- 对于复合索引,如果不使用前列,后续列也将无法使用。
- like以%开头。列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引。
- where中索引列有运算,有函数的,不使用索引。
- 如果mysql觉得全表扫描更快的时候,数据少的情况下,不使用索引。
🍊 聚集索引
InnoDB存储引擎表是索引组织表,即表中数据按照主键顺序存放。而聚集索引(clusteredindex)就是按照每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录数据,也将聚集索引的叶子节点称为数据页。每个数据页都通过一个双向链表来进行链接由于实际的数据页只能按照一棵B+树进行排序,因此每张表只能拥有一个聚集索引。在多数情况下,查询优化器倾向于采用聚集索引。因为聚集索引能够在B+树索引的叶子节点上直接找到数据。此外,由于定义了数据的逻辑顺序,它对于主键的排序查找和范围查找速度非常快。叶子节点的数据就是用户所要查询的数据如:用户需要查询一张注册用户的表,查询最后注册的10位用户,由于B+树索引是双向链表的,用户可以快速找到最后一个数据页,并取出10条记录SELECT * FROM Profile ORDER BY id LIMIT 10;
虽然使用ORDER BY对主键id记录进行排序,但是在实际过程中并没有进行所谓的filesort操作,而这就是因为聚集索引的特点。另一个是范围查询(range query),即如果要查找主键某一范围内的数据,通过叶子节点的上层中间节点就可以得到页的范围,之后直接读取数据页即可。如:SELECT * FROM Profile where id>1 and id < 100;
🍊 辅助索引
对于辅助索引(Secondary Index,也称非聚集索引),叶子节点并不包含行记录的全部数据。叶子节点除了包含键值以外,每个叶子节点中的索引行中还包含了一个书签(bookmark)。该书签用来告诉InnoDB存储引擎哪里可以找到与索引相对应的行数据。由于InnoDB存储引擎表是索引组织表,因此InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键。辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表上可以有多个辅助索引。当通过辅助索引来寻找数据时,InnoDB存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录。举例来说,如果在一棵高度为3的辅助索引树中查找数据,那需要对这棵辅助索引树遍历3次找到指定主键,如果聚集索引树的高度同样为3,那么还需要对聚集索引树进行3次查找,最终找到一个完整的行数据所在的页,因此一共需要6次逻辑IO访问以得到最终的一个数据页。
🍊 覆盖索引,什么情况下优化器会选择使用覆盖索引
InnoDB存储引擎支持覆盖索引(covering index,或称索引覆盖),即从辅助索引中就可以得到查询的记录(此时不能够使用select * 操作,只能对特定的索引字段进行select),而不需要查询聚集索引中的记录。使用覆盖
索引的一个好处是辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作。对于InnoDB存储引擎的辅助索引而言,由于其包含了主键信息,因此其叶子节点存放的数据为(primarykey1,primary key2,…,key1,key2,…)。例如,下列语句都可仅使用一次辅助联合索引来完成查询:
SELECT key2 FROM table WHERE key1=xxx; SELECT primary key2,key2 FROM table WHERE key1=xxx; SELECT primary key1,key2 FROM table WHERE key1=xxx; SELECT primary key1,primary key2,key2 FROM table WHERE key1=xxx;
CREATETABLEbuy_log( userid INT UNSIGNED NOT NULL, buy_date DATE )ENGINE=InnoDB; ALTER TABLE buy_log ADD KEY (userid); ALTER TABLE buy_log ADD KEY (userid,buy_date);
覆盖索引的另一个好处是对某些统计问题而言的。还是对于上题创建的表buy_log,要进行举例说明。
SELECT COUNT(*) FROM buy_log;
InnoDB存储引擎并不会选择通过查询聚集索引来进行统计。由于buy_log表上还有辅助索引,而辅助索引远小于聚集索引,选择辅助索引可以减少IO操作。
在通常情况下,诸如(a,b)的联合索引,一般是不可以选择列b中所谓的查询条件。但是如果是统计操作,并且是覆盖索引的,则优化器会进行选择,如下述语句:SELECT COUNT(*)FROM buy_log WHERE buy_date>=‘2011-01-01’ANDbuy_date<’2011-02-01’
表buy_log有(userid,buy_date)的联合索引,这里只根据列b进行条件查询,一般情况下是不能进行该联合索引的,但是这句SQL查询是统计操作,并且可以利用到覆盖索引的信息,因此优化器会选择该联合索引.
🍊 联合索引
联合索引是指对表上的多个列进行索引。
CREATE TABLE buy_log( userid INT UNSIGNED NOT NULL, buy_date DATE )ENGINE=InnoDB; ALTER TABLE buy_log ADD KEY (userid); ALTER TABLE buy_log ADD KEY (userid,buy_date);
以上代码建立了两个索引来进行比较。两个索引都包含了userid字段。
情况1: 如果只对于userid进行查询,如:SELECT * FROM buy_log WHERE userid=2;索引选择:优化器最终的选择是索引userid,因为该索引的叶子节点包含单个键值,所以理论上一个页能存放的记录应该更多。
情况2:SELECT * FROM buy_log WHERE userid=1 ORDER BY buy_date DESC LIMIT 3;索引选择:优化器使用了(userid,buy_date)的联合索引userid_2,因为在这个联合索引中buy_date已经排序好了。根据该联合索引取出数据,无须再对buy_date做一次额外的排序操作。
情况 3:假如三个字段的联合索引。如:对于联合索引(a,b,c)来说,下列语句同样可以直接通过联合索引得到结果,不需要filesort的排序操作:
SELECT...FROM TABLE WHERE a=xxx ORDER BY b SELECT...FROM TABLE WHERE a=xxx AND b=xxx ORDER BY c
但是对于下面的语句,联合索引不能直接得到结果,其还需要执行一次filesort排序操作,因为索引(a,c)并未排序:
SELECT...FROM TABLE WHERE a=xxx ORDER BY c
🍊 索引总结
聚集索引的叶子节点称为数据页,每个数据页通过一个双向链表来进行链接,而且数据页按照主键的顺序进行排列。每个数据页上存放的是完整的行记录,而在非数据页的索引页中,存放的仅仅是键值及指向数据页的偏移量,而不是一个完整的行记录。如果定义了主键,InnoDB会自动使用主键来创建聚集索引。如果没有定义主键,InnoDB会选择一个唯一的非空索引代替主键。如果没有唯一的非空索引,InnoDB会隐式定义一个主键来作为聚集索引。
辅助索引它叶子节点中没有行记录的全部数据,叶子节点除了包含键值以外,每个叶子节点的索引行还包含了一个书签,该书签用来告诉InnoDB哪里可以找到与索引相对应的行数据。
覆盖索引先遍历辅助索引,再遍历聚集索引,而如果要查询的字段值在辅助索引上就有,就不用再查聚集索引了,这显然会减少IO操作。
联合索引,它是对表上的多个列进行索引,键值都是排序的,通过叶子节点可以顺序的读出所有数据,联合索引的好处在于能起到"一个顶三个"的作用。比如建了一个(a,b,c)的复合索引,那么实际等于建了(a),(a,b),(a,b,c)三个索引,每多一个索引,都会增加写操作的开销和磁盘空间的开销,对于大数据的表,这是不小的开销。另外它还可以避免filesort排序,因为filesort的过程,一行数据会被读两次,第一次是where条件过滤时,第二个是排完序后还得用行指针去读一次。
🍊 redo log 和bin log有什么区别?binlog做什么用的
在MySQL数据库中有一种二进制日志(binlog),其用来进行POINT-IN-TIME(PIT)的恢复及主从复制(Replication)环境的建立。从表面上看其和重做日志非常相似,都是记录了对于数据库操作的日志。然而,从本质上来看,两者有着非常大的不同。首先,重做日志是在InnoDB存储引擎层产生,而二进制日志是在MySQL数据库的上层产生的,并且二进制日志不仅仅针对于InnoDB存储引擎,MySQL数据库中的任何存储引擎对于数据库的更改都会产生二进制日志。其次,两种日志记录的内容形式不同。MySQL数据库上层的二进制日志bin log是一种逻辑日志,其记录的是对应的SQL语句。而InnoDB存储引擎层面的重做日志是物理格式日志,其记录的是对于每个页的修改。
🍊 什么是undolog,有什么用?
重做日志记录了事务的行为,可以很好地通过其对页进行“重做”操作。但是事务有时还需要进行回滚操作,这时就需要undo。因此在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo。这样如果用户执行的事务或语句由于某种原因失败了,又或者用户用一条ROLLBACK语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。redo存放在重做日志文件中,与redo不同,undo存放在数据库内部的一个特殊段(segment)中,这个段称为undo段(undo segment)。undo段位于共享表空间内。可以通过py_innodb_page_info.py工具来查看当前共享表空间中undo的数量除了回滚操作,undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。最后也是最为重要的一点是,undo log会产生redo log,也就是undo log的产生会伴随着redolog的产生,这是因为undo log也需要持久性的保护。
🍊 分布式事务
InnoDB存储引擎提供了对XA事务的支持,并通过XA事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的ACID要求又有了提高。另外,在使用分布式事务时,InnoDB存储引擎的事务隔离级别必须设置为SERIALIZABLE。XA事务允许不同数据库之间的分布式事务,如一台服务器是MySQL数据库的,另一台是Oracle数据库的,又可能还有一台服务器是SQL Server数据库的,只要参与在全局事务中的每个节点都支持XA事务。分布式事务可能在银行系统的转账中比较常见,如用户David需要从上海转10 000元到北京的用户Mariah的银行卡中:
#Bank@Shanghai: UPDATE account SET money=money-10000WHEREuser='David'; #Bank@Beijing UPDATE account SET money=money+10000WHEREuser='Mariah';
在这种情况下,一定需要使用分布式事务来保证数据的安全。如果发生的操作不能全部提交或回滚,那么任何一个结点出现问题都会导致严重的结果。要么是David的账户被扣款,但是Mariah没收到,又或者是David的账户没有扣款,Mariah却收到钱了。
XA事务由一个或多个资源管理器(Resource Managers)、一个事务管理器(TransactionManager)以 及一个应用程序(ApplicationProgram)组成。❑资源管理器:提供访问事务资源的方法。通常一个数据库就是一个资源管理器。❑事务管理器:协调参与全局事务中的各个事务。需要和参与全局事务的所有资源管理器进行通信。❑应用程序:定义事务的边界,指定全局事务中的操作。在MySQL数据库的分布式事务中,资源管理器就是MySQL数据库,事务管理器为连接MySQL服务器的客户端。
分布式事务使用两段式提交(two-phasecommit)的方式。在第一阶段,所有参与全局事务的节点都开始准备(PREPARE),告诉事务管理器它们准备好提交了。在第二阶段,事务管理器告诉资源管理器执行ROLLBACK还是COMMIT。如果任何一个节点显示不能提交,则所有的节点都被告知需要回滚。可见与本地事务不同的是,分布式事务需要多一次的PREPARE操作,待收到所有节点的同意信息后,再进行COMMIT或是ROLLBACK操作。
最为常见的内部XA事务存在于binlog与InnoDB存储引擎之间。由于复制的需要,因此目前绝大多数的数据库都开启了binlog功能。在事务提交时,先写二进制日志,再写InnoDB存储引擎的重做日志。对上述两个操作的要求,也是原子的,即二进制日志和重做日志必须同时写入。若二进制日志先写了,而在写入InnoDB存储引擎时发生了宕机,那么slave可能会接收到master传过去的二进制日志并执行,最终导致了主从不一致的情况。
🎉 CAP理论
分布式环境下(数据分布)要任何时刻保证数据一致性是不可能的,只能采取妥协的方案来保证数据最终一致性。这个也就是著名的CAP定理。
C一致性:对于指定的客户端来说,读操作保证能够返回最新的写操作结果。
A可用性:非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。
P分区容错性:当出现网络分区后,系统能够继续“履行职责”。
对于一个分布式系统而言,网络失效一定会发生,分区容错性P其实就是每个服务都会有多个节点(一般都是主从),这样就可以保证此服务的一个节点挂了之后,此服务的其他节点依然可以响应,其实这就是分区容错性。
但是一个服务有多个节点之后,一个服务的多个节点之间的数据为了保持一致性就要进行的数据复制,在此过程中就会出现数据一致性C(强一致性)的问题。也就是说,分区耐受性是必须要保证的,那么在可用性和一致性就必须二选一。网络不可用的时候,如果选择了一致性,系统就可能返回一个错误码或者干脆超时,即系统不可用。如果选择了可用性,那么系统总是可以返回一个数据,但是并不能保证这个数据是最新的。
🎉 BASE理论
BASE 理论是对 CAP 理论的延伸,核心思想是即使无法做到强一致性,但应用可以采用适合的方式达到最终一致性。
基本可用: 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
软状态: 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。MySQL Replication 的异步复制也是一种体现。
最终一致性: 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
应用场景:
Erueka:erueka是SpringCloud系列用来做服务注册和发现的组件,作为服务发现的一个实现,在设计的时候就更考虑了可用性,保证了AP。
Zookeeper:Zookeeper在实现上牺牲了可用性,保证了一致性(单调一致性)和分区容错性,也即:CP。所以这也是SpringCloud抛弃了zookeeper而选择Erueka的原因。
具体根据各自业务场景所需来制定相应的策略而选择适合的产品服务等。例如:支付订单场景中,由于分布式本身就在数据一致性上面很难保证,从A服务到B服务的订单数据有可能由于服务宕机或其他原因而造成数据不一致性。因此此类场景会酌情考虑:AP,不强制保证数据一致性,但保证数据最终一致性。
分布式事务指事务的操作位于不同的节点上,需要保证事务的 AICD 特性,在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。
🎉 两阶段提交(2PC)
第一阶段:协调者询问参与者事务是否执行成功,参与者发回事务执行结果。这一阶段的协调者有超时机制,假设因为网络原因没有收到某参与者的响应或某参与者挂了,那么超时后就会判断事务失败,向所有参与者发送回滚命令。
第二阶段:如果事务在每个参与者上都执行成功,事务协调者才发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。这一阶段的协调者的没法超时,只能不断重试。
协调者是一个单点,存在单点故障问题。
假设协调者在发送准备命令之前挂了,还行等于事务还没开始。
假设协调者在发送准备命令之后挂了,这就不太行了,有些参与者等于都执行了处于事务资源锁定的状态。不仅事务执行不下去,还会因为锁定了一些公共资源而阻塞系统其它操作。
假设协调者在发送回滚事务命令之前挂了,那么事务也是执行不下去,且在第一阶段那些准备成功参与者都阻塞着。
假设协调者在发送回滚事务命令之后挂了,这个还行,至少命令发出去了,很大的概率都会回滚成功,资源都会释放。但是如果出现网络分区问题,某些参与者将因为收不到命令而阻塞着。
假设协调者在发送提交事务命令之前挂了,这个不行,傻了!这下是所有资源都阻塞着。
假设协调者在发送提交事务命令之后挂了,这个还行,也是至少命令发出去了,很大概率都会提交成功,然后释放资源,但是如果出现网络分区问题某些参与者将因为收不到命令而阻塞着。
存在的缺点:
同步阻塞 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
单点问题 协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
数据不一致 在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
太过保守 任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
🎉 三阶段提交(3PC)
相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态,3PC 包含了三个阶段,分别是准备阶段、预提交阶段和提交阶段
准备阶段的变更成不会直接执行事务,而是会先去询问此时的参与者是否有条件接这个事务,因此不会一来就干活直接锁资源,使得在某些资源不可用的情况下所有参与者都阻塞着。
而预提交阶段的引入起到了一个统一状态的作用,它像一道栅栏,表明在预提交阶段前所有参与者其实还未都回应,在预处理阶段表明所有参与者都已经回应了。
假如你是一位参与者,你知道自己进入了预提交状态那你就可以推断出来其他参与者也都进入了预提交状态。
缺点:
多引入一个阶段也多一个交互,因此性能会差一些,而且绝大部分的情况下资源应该都是可用的,这样等于每次明知可用执行还得询问一次。
和2PC对比:
2PC 是同步阻塞的,协调者挂在了提交请求还未发出去的时候是最伤的,所有参与者都已经锁定资源并且阻塞等待着,提交阶段 2PC 协调者和某参与者都挂了之后新选举的协调者不知道当前应该提交还是回滚
改进的优势:
新协调者来的时候发现有一个参与者处于预提交或者提交阶段,那么表明已经经过了所有参与者的确认了,所以此时执行的就是提交命令。
所以说 3PC 就是通过引入预提交阶段来使得参与者之间的状态得到统一,也就是留了一个阶段让大家同步一下。
但是这也只能让协调者知道该如果做,但不能保证这样做一定对,这其实和上面 2PC 分析一致,因为挂了的参与者到底有没有执行事务无法断定。
所以说 3PC 通过预提交阶段可以减少故障恢复时候的复杂性,但是不能保证数据一致,除非挂了的那个参与者恢复。
总结一下
3PC 相对于 2PC 做了一定的改进:引入了参与者超时机制,并且增加了预提交阶段使得故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致问题。
2PC 和 3PC 都不能保证数据100%一致,因此一般都需要有定时扫描补偿机制
🎉 补偿事务(TCC)
针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
Try 阶段主要是对业务系统做检测及资源预留
Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
举个例子,假入 Bob 要向 Smith 转账,思路大概是:我们有一个本地方法,里面依次调用
首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。
在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理
🎉 MQ 事务消息
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
第一阶段Prepared消息,会拿到消息的地址。第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 实现难度大,主流MQ不支持,RocketMQ事务消息部分代码也未开源。
🎉 最大努力通知
其实我觉得本地消息表也可以算最大努力,事务消息也可以算最大努力。
就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候可以记录下然后引入人工,或者直接舍弃。这其实算是最大努力了。
事务消息也是一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算最大努力。
所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。适用于对时间不敏感的业务,例如短信通知。
各个场景对比的解决方案:
2PC 和 3PC 是一种强一致性事务,不过还是有数据不一致,阻塞等风险,而且只能用在数据库层面。
而 TCC 是一种补偿性事务思想,适用的范围更广,在业务层面实现,因此对业务的侵入性较大,每一个操作都需要实现对应的三个方法。
本地消息、事务消息和最大努力通知其实都是最终一致性事务,因此适用于一些对时间不敏感的业务。
🍊 SQL的执行流程
第一步,先连接到这个数据库上,这时候接待你的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接。用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在 show processlist 命令中看到它。客户端如果长时间不发送command到Server端,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。
第二步:查询缓存。MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个value就会被直接返回给客户端。如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。大多数情况查询缓存就是个鸡肋,为什么呢?因为查询缓存往往弊大于利。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。这个鸡肋也有地方可以去使用它,比如说不会改变的表数据,极少更新的表,像一些系统配置表、字典表,全国的省份之类的表,这些表上的查询适合使用查询缓存。MySQL提供了这种“按需使用”的方式,可以将my.cnf参数query_cache_type 设置成2,query_cache_type有3个值:0代表关闭查询缓存,1代表开启,2代表当sql语句中有SQL_CACHE关键词时才缓存。确定要使用查询缓存的语句,用 SQL_CACHE显式指定,比如,select SQL_CACHE * from user where ID=5;
第三步,如果没有命中查询缓存,就要开始真正执行语句了。MySQL 需要知道你要做什么,需要对 SQL 语句做解析。
分析器先会做“词法分析”,你输入的一条 SQL 语句,MySQL需要识别出里面的字符串分别是什么,代表什么。MySQL从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“user”识别成“表名 user”,把字符串“ID”识别成“列 ID”。做完了这些识别以后,就要做“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL语句是否满足MySQL语法。如果你的语句不对,就会收到“您的SQL语法有错误”的错误提醒。语句正确之后,会丢到分析机里面执行分析,语法分析由Bison生成,经过bison语法分析之后,会生成一个语法树。比如,你的操作是select还是insert,你需要对那些字段进行操作,作用在哪张表上面,条件是什么。
经过了分析器,MySQL就知道这些字符串代表什么,要做什么了。在开始执行之前,还要先经过优化器的处理。
第四步,优化器,在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联的时候,优化器可以决定各个表的连接顺序,同一条多表查询的sql,执行的方案会有多种,比如,select * from user1 join user2 on user1.id = user2.id where user1.name=liaozhiwei and user2.name=haoshuai;既可以先从表user1 里面取出 name=liaozhiwei的 ID 值,再根据 ID 值关联到表user2,再判断user2 里面 name的值是否等于liaozhiwei。也可以先从表user2 里面取出 name=haoshuai的 ID 值,再根据 ID 值关联到user1,再判断user1 里面 name 的值是否等于haoshuai。这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。执行方案就确定下来了,然后进入执行器阶段。
第五步,开始执行的时候,要先判断一下你对这个表有没有执行查询的权限,如果没有,就会返回没有权限的错误。如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。比如,我有一条sql:select * from user where id=10;执行器调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是10,如果不是则跳过, 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行,如果是将这行保存在结果集中。执行器将遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。到这一步,这个语句就执行完成了。
🍊 Mysql调优
第一步,从初期的一个需求规划,也就是对表的设计就开始了,先从底层开始说吧,mysql底层的页的大小是16kb,假如,我有一张表对单行数据量就有16kb,那么这张表就只能存储1条数据了,这是非常恐怖的。mysql是从磁盘读取数据到内存的,是以磁盘块为基本单位的,位于同一磁盘块中的数据会被一次性读取出来,不是按需读取。以InnoDB存储引擎来说,它使用页作为数据读取单位,页是其磁盘管理的最小单位,默认大小是16kb。系统的一个磁盘块的存储空间往往没有这么大,所以InnoDB每次申请磁盘空间时都会是多个地址连续磁盘块来达到页的大小16KB。在查询数据时一个页中的每条数据都能定位数据记录的位置,这会减少磁盘 I/O 的次数,提高查询效率。InnoDB存储引擎在设计时是将根节点常驻内存的,力求达到树的深度不超过 3,也就是说I/O不超过3次。树形结构的数据可以让系统高效的找到数据所在的磁盘块,这里就可以说一下这个b树和b+树了,B树的结构是每个节点中有key也有value,而每一个页的存储空间是16kb,如果数据较大时将会导致一页能存储数据量的数量很小。B+Tree的结构是将所有数据记录节点按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储 key 值信息,这样可以大大加大每个节点存储的key 值数量,降低B+Tree的高度。所以通过mysql的底层存储的原理和数据结构,我们在设计表的时候,尽量减少单行数据的大小,字段的宽度设得尽可能小,尽可能不用text、Blob、Clob等类型,它会增加存储空间的占用,读取速度较慢。能用数字型字段就不要设计为字符型,因为字符型锁占的存储空间更大,比如,性别这个字段不用男女进行存储,改为0/1的方式,这样不仅可以控制数据量的大小,增加了同一高度下的B+树容纳的数据量,还能提高检索速度。尽量使用varchar/nvarchar代替char/nchar,因为变长字段空间小,可以节省存储空间。不在数据库中存储图片、文件等大数据,可以通过第三方云存储,存放图片或者文件地址。金额用decimal,注意长度和精度。如果存储的数据范围超过decimal的范围,建议将数据拆成整数和小树分开存储。尽可能不要给数据库留null值,尤其是时间、整数等类型,可以在建表的时候就给非空设置,NULL的列会使用更多的存储空间,在Mysql中也需要特殊处理,为NULL的列会使索引统计和值比较都更复杂,当可为NULL的列被索引时,每个索引记录需要一个额外的字节,在MyISAM里甚至还可能导致固定大小的索引(例如只有一个整数列的索引)变成可变大小的索引。
第二步,就是建索引,当对表中的数据进⾏增加、删除和修改的时候,索引也要动态的维护,降低了数据的维护速度,所以不是所有字段都适合创建索引的,比如要频繁更改数据不建议使⽤索引,又或者对于关于区分度不高的字段,比如性别,状态字段,就不适合创建索引,只有几种取值的字段,建了索引,数据库也不一定会用,只会白白增加索引维护的额外开销,因为索引也是需要存储的,所以插入和更新的写入操作,需要插入和更新这个字段的索引的。一般需要查询排序,分组和联合操作的字段适合建⽴索引,另外索引多,数据更新表越慢,所以尽量使⽤字段值不重复的字段创建索引。
mysql有聚簇索引,辅助索引,覆盖索引。聚集索引的叶子节点称为数据页,每个数据页通过一个双向链表来进行链接,而且数据页按照主键的顺序进行排列。每个数据页上存放的是完整的行记录,而在非数据页的索引页中,存放的仅仅是键值及指向数据页的偏移量,而不是一个完整的行记录。如果定义了主键,InnoDB会自动使用主键来创建聚集索引。如果没有定义主键,InnoDB会选择一个唯一的非空索引代替主键。如果没有唯一的非空索引,InnoDB会隐式定义一个主键来作为聚集索引。辅助索引它叶子节点中没有行记录的全部数据,叶子节点除了包含键值以外,每个叶子节点的索引行还包含了一个书签,该书签用来告诉InnoDB哪里可以找到与索引相对应的行数据。覆盖索引先遍历辅助索引,再遍历聚集索引,而如果要查询的字段值在辅助索引上就有,就不用再查聚集索引了,这显然会减少IO操作。除了这三种索引,还有一种联合索引,它是对表上的多个列进行索引,键值都是排序的,通过叶子节点可以顺序的读出所有数据,联合索引的好处在于能起到"一个顶三个"的作用。比如建了一个(a,b,c)的复合索引,那么实际等于建了(a),(a,b),(a,b,c)三个索引,每多一个索引,都会增加写操作的开销和磁盘空间的开销,对于大数据的表,这是不小的开销。另外它还可以避免filesort排序,因为filesort的过程,一行数据会被读两次,第一次是where条件过滤时,第二个是排完序后还得用行指针去读一次。filesort的过程是这样的:第一步先根据表的索引或者全表扫描,读取所有满足条件的记录。第二步,存储每一行排序列,就是order by用到的列值,还有行记录指针,就是指向该行数据的行指针,把这二个存储到缓冲区。第三步,当缓冲区满后,运行一个快速排序来将缓冲区中数据排序,将排序完的数据存储到一个临时文件,保存一个存储块的指针,当然如果缓冲区不满,则不会重建临时文件了。直到将所有行读完,建立相应有序的临时文件。第四步,对块级进行排序,这个类似归并排序算法,只通过两个临时文件的指针来不断交换数据,最终达到两个文件,都是有序的,直到所有的数据都排序完毕。第五步,采取顺序读的方式,将每行数据读入内存,取出数据传到客户端。为什么要说这个filesort呢?举二个场景,第一个,如果order by的条件不在索引列上会产生filesort,第二个,排序的字段不在where的条件中,没有办法走索引排序Index,而是走的文件排序filesort 。这种概率其实还是挺高的。这个时候就需要看文件排序用的是单路排序还是双路排序,单路排序会把所有需要查询的字段都放到 sort buffer 中,而双路排序只会把主键 和需要排序的字段放到 sort buffer 中进行排序,然后再通过主键回到原表查询需要的字段。mysql优化器使用双路排序还是单路排序是有自己的算法判断的,如果查询的列字段大于max_length_for_sort_data变量,则会使用双路排序,反之则会使用单路排序。单路排序速度是更快的,不过比较占据内存,如果在内存空间允许的情况下想要使用单路排序的话,可以增加max_length_for_sort_data变量的大小,max_length_for_sort_data变量默认为1024字节。另外sort_buffer_size参数修改为2M,减少在排序过程中对须要排序的数据进行分段,尽量不使用临时表来进行交换排序。这个参数如果超过 2M 的时候,就会使用 mmap(),而不是 malloc() 来进行内存分配,会导致效率降低。这个参数如果过小的话,再加上max_length_for_sort_data变大,一次返回的条数过多,那么很可能就会分很多次进行排序,然后最后将每次的排序结果再串联起来,这样就会更慢。最后read_buffer_size参数也可以根据实际情况调整,它是MySQL读入缓冲区大小,对表进行顺序扫描的请求将分配一个读入缓冲区,MySQL会为它分配一段内存缓冲区。read_buffer_size变量控制这一缓冲区的大小,默认是1M。如果对表的顺序扫描请求非常频繁,并且你认为频繁扫描进行得太慢,可以通过增加该变量值以及内存缓冲区大小提高其性能。
第三步,针对SQL进行调整,在写SQL的时候遵循最左前缀原则,向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,范围列可以用到索引,但是范围列后面的列无法用到索引。like以通配符%开头索引失效会变成全表扫描的操作。如果查询条件中含有函数或表达式,将导致索引失效而进行全表扫描。只要列中包含有 NULL 值都将不会被包含在索引中,复合索引中只要有一列含有 NULL 值,那么这一列对于此复合索引就是无效的。不要使用select *,改用select加字段名称,因为select *走的聚集索引,会进行全表扫描,如果一定要使用select *的话,mysql至少使用5.6版本,这个版本有一个离散读的优化,离散读的优化是将离散度大的列放到联合索引的前面,举个例子,
select * from user where staff_id = 2 and customer_id = 584
,这个时候索引优化会将customer_id放到前面,因为它的离散度更高,可以通过select count(distinct customer_id),count(distinct staff_id) from user
查看列的离散度。有一个ICP的优化,以往,根据索引查找记录,再根据WHERE条件来过滤记录。使用ICP优化后,会在取出索引的同时,直接根据WHERE条件过滤,将WHERE的部分过滤操作放在了存储引擎层。在某些查询下可以大大减少上层SQL层对记录的索取,从而提高性能。还有一个MRR优化,它是减少磁盘的随机访问,并且将随机访问转化为较为顺序的数据访问,在查询辅助索引时,首先根据得到的查询结果,将查询得到的辅助索引键值存放于一个缓存中,这时缓存中的数据是根据辅助索引键值排序的,然后按照主键进行排序,并按照主键排序的顺序进行书签查找。顺序查找可以对一个页进行顺序查找,无需离散加载数据页,可以减少缓冲池中页被替换的次数,能够批量处理对键值的查询操作。另外在写sql的时候,尽量使用它的一个explain执行计划,去看我们的索引是不是失效了。索引失效有这么几种情况,上面也提到过几个,如果条件中有or,即使其中有部分条件带索引也不会使用。对于复合索引,如果不使用前列,后续列也将无法使用。like以%开头。列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引。where中索引列有运算,有函数的,不使用索引。如果mysql觉得全表扫描更快的时候,数据少的情况下,不使用索引。第四步,随着数据量的增涨之后,会考虑一个数据的分区,Partition分区,表分区必须在表建立的时候创建规则,而已经存在的没有创建过表分区规则的表需要重新做导入处理。如果想修改有规则的表分区,只能新增,不要随意删除,删除表分区会造成该表分区内部数据也一起被删除掉。除此之外,分区也需要结合实际场景进行分区,而且Partition分区也是有一个局限性,因为单台的mysql服务器支持1024个分区,一旦达到这个分区上限,考虑垂直拆分和水平拆分了。垂直分区,它是将单表变多表,这样也是有些优点的,一条数据存储的数据量越小,三层b+所容纳的数据量是更多的,这样一个分区就可以承载多个数据,多个分区它承载的数据就会更多,这是一个累加的效果,不过也有缺点,就是进行表的垂直划分的时候,还需要考虑它的一个关联性,在进行sql查询的情况下,需要反复测试,考虑它的一个性能问题,最好的结果就是拆分出来的表还是能够支持铁定的业务线。比如一个包含了大text和BLOB列的表,这些text和BLOB列又不经常被访问,这时候就要把这些不经常使用的text和BLOB了划分到另一个分区,在保证它们数据相关性的同时还能提高访问速度。随着数据量持续的增涨,这个时候就需要考虑水平分区了,水平分区有多种模式,Range(范围)模式:允许DBA将数据划分不同范围。比如DBA可以将一个表通过年份划分成三个分区,80年代的数据,90年代的数据以及任何在2000年之后的数据。Hash(哈希)模式允许DBA通过对表的一个或多个列的Hash Key进行计算,最后通过这个Hash码不同数值对应的数据区域进行分区,比如DBA可以建立一个,对表的主键进行分区的表。List(预定义列表)模式:允许系统通过DBA定义列表的值所对应的行数据进行分割。例如:DBA建立了一个横跨三个分区的表,分别根据2004年,2005年,2006年值对应数据。Composite(复合模式),就是多模式的组合使用,在初始化已经进行了Range范围分区的表上,我们可以对其中一个分区再进行hash哈希分区。
第五步,就是冷热备份,对于一些无用的数据,这个时候根据实际的需求,对数据进行一个实时的备份,保证MySQL的数据保持在一个比较稳定的情况。