码猿技术专栏

微信公众号:码猿技术专栏

前言

  • Redis目前已经成为主流的内存数据库了,但是大部分人仅仅是停留在会用的阶段,你真的了解Redis内部的工作原理吗?
  • 今天这篇文章将为大家介绍Redis持久化的两种方案,文章将会从以下五个方面介绍:
    1. 什么是RDB,RDB如何实现持久化?
    2. 什么是AOF,AOF如何实现持久化?
    3. AOF和RDB的区别。
    4. 如何重启恢复数据?
    5. 持久化性能问题和解决方案

      RDB

  • RDB持久化是把当前进程数据生成快照保存到硬盘的过程, 触发RDB持久化过程分为手动触发和自动触发。
  • RDB完成后会自动生成一个文件,保存在dir配置的指定目录下,文件名是dbfileName指定。
  • Redis默认会采用LZF算法对生成的RDB文件做压缩处理,压缩后的文件远远小于内存大小,默认开启。

手动触发

  • 手动触发的命令有savebgsave
  • save:该命令会阻塞Redis服务器,直到RDB的过程完成,已经被废弃,因此线上不建议使用。
  • bgsave:每次进行RDB过程都会fork一个子进程,由子进程完成RDB的操作,因此阻塞只会发生在fork阶段,一般时间很短。

自动触发

  • 除了手动触发RDB,Redis服务器内部还有如下几个场景能够自动触发RDB:
    1. 根据我们的 save m n 配置规则自动触发。
    2. 如果从节点执行全量复制操作, 主节点自动执行bgsave生成RDB文件并发送给从节点。
    3. 执行debug reload命令重新加载Redis时, 也会自动触发save操作。
    4. 默认情况下执行shutdown命令时, 如果没有开启AOF持久化功能则自动执行bgsave

RDB执行流程

  • RDB的主流方式就是bgsave,通过下图我们来看看RDB的执行流程:
    RDB执行流程
  • 通过上图可以很清楚RDB的执行流程,如下:
    1. 执行bgsave命令后,会先判断是否存在AOF或者RDB的子进程,如果存在,直接返回。
    2. 父进程fork操作创建一个子进程,fork操作中父进程会被阻塞。
    3. fork完成后,子进程开始根据父进程的内存生成临时快照文件,完成后对原有的RDB文件进行替换。执行lastsave命令可以查看最近一次的RDB时间。
    4. 子进程完成后发送信号给父进程,父进程更新统计信息。

RDB的优点

  • RDB是一个紧凑压缩的二进制文件, 代表Redis在某个时间点上的数据快照。 非常适用于备份, 全量复制等场景。 比如每6小时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中,用于灾难恢复。
  • Redis加载RDB恢复数据远远快于AOF的方式。

RDB的缺点

  • RDB方式数据没办法做到实时持久化/秒级持久化。 因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。
  • RDB文件使用特定二进制格式保存, Redis版本演进过程中有多个格式的RDB版本, 存在老版本Redis服务无法兼容新版RDB格式的问题。

AOF

  • AOF(append only file) 持久化: 以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。 AOF的主要作用是解决了数据持久化的实时性, 目前已经是Redis持久化的主流方式

如何开启AOF

  • 开启AOF功能需要设置配置:appendonly yes, 默认不开启。 AOF文件名通过appendfilename配置设置, 默认文件名是appendonly.aof。 保存路径同RDB持久化方式一致,通过dir配置指定。

AOF整体的执行流程

  • AOF执行的流程大致分为命令写入文件同步文件重写重启加载四个步骤,如下图:
    AOF执行流程
  • 从上图大致了解了AOF的执行流程,下面一一分析上述的四个步骤。

命令写入

  • AOF命令写入的内容直接是文本协议格式。 例如set hello world这条命
    令, 在AOF缓冲区会追加如下文本:
    1
    *3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
  • 命令写入是直接写入到AOF的缓冲区中,至于为什么?原因很简单,Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘, 那么性能完全取决于当前硬盘负载。先写入缓冲区aof_buf中, 还有另一个好处, Redis可以提供多种缓冲区
    同步硬盘的策略,在性能和安全性方面做出平衡。

文件同步

  • Redis提供了多种AOF缓冲区同步文件策略, 由参数appendfsync控制,如下:
    • 配置为always时, 每次写入都要同步AOF文件, 在一般的SATA硬盘上,Redis只能支持大约几百TPS写入, 显然跟Redis高性能特性背道而驰,不建议配置。
    • 配置为no,由于操作系统每次同步AOF文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但数据安全性无法保证。
    • 配置为everysec(默认的配置),是建议的同步策略, 也是默认配置,做到兼顾性能和数据安全性。理论上只有在系统突然宕机的情况下丢失1秒的数据(当然,这是不太准确的)。

文件重写机制

  • 随着命令不断写入AOF, 文件会越来越大, 为了解决这个问题, Redis引入AOF重写机制压缩文件体积。 AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
  • 为什么要文件重写呢? 因为文件重写能够使得AOF文件的体积变得更小,从而使得可以更快的被Redis加载。
  • 重写过程分为手动触发和自动触发。
    • 手动触发直接使用bgrewriteaof命令。
    • 根据auto-aof-rewrite-min-sizeauto-aof-rewrite-percentage参数确定自动触发时机。
  • auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积, 默认为64MB。
  • auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size) 和上一次重写后AOF文件空间(aof_base_size) 的比值。
  • 自动触发时机相当于aof_current_size>auto-aof-rewrite-minsize&&(aof_current_size-aof_base_size) /aof_base_size>=auto-aof-rewritepercentage。其中aof_current_sizeaof_base_size可以在info Persistence统计信息中查看。
  • 那么文件重写后的AOF文件为什么会变小呢? 有如下几个原因:
    1. 进程内已经超时的数据将不会再次写入AOF文件中。
    2. 旧的AOF文件含有无效命令,如del key1hdel key2等。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。
    3. 多条写命令可以合并为一个, 如:lpush list alpush list blpush listc可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢出,对于listsethashzset等类型操作,以64个元素为界拆分为多条。
  • 介绍了文件重写的系列知识,下面来看看Redis内部是如何进行文件重写的,如下图:
    文件重写
  • 看完上图,大致了解了文件重写的流程,对于重写的流程,补充如下:
    1. 重写期间,主线程并没有阻塞,而是在执行其他的操作命令,依然会向旧的AOF文件写入数据,这样能够保证备份的最终完整性,如果数据重写失败,也能保证数据不会丢失。
    2. 为了把重写期间响应的写入信息也写入到新的文件中,因此也会为子进程保留一个缓冲区,防止新写的文件丢失数据。
    3. 重写是直接把当前内存的数据生成对应命令,并不需要读取老的AOF文件进行分析、命令合并。
    4. AOF文件直接采用的文本协议,主要是兼容性好、追加方便、可读性高可认为修改修复。
    5. 无论是RDB还是AOF都是先写入一个临时文件,然后通过重命名完成文件的替换。

AOF的优点

  • 使用 AOF 持久化会让 Redis 变得非常耐久:你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。

AOF的缺点

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间。
  • 数据恢复速度相对于RDB比较慢。

AOF和RDB的区别

  • RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
  • AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

重启加载

  • 无论是RDB还是AOF都可用于服务器重启时的数据恢复,执行流程如下图:
    重启加载流程
  • 上图很清晰的分析了Redis启动恢复数据的流程,先检查AOF文件是否开启,文件是否存在,再检查RDB是否开启,文件是否存在。

性能问题与解决方案

  • 通过上面的分析,我们都知道RDB的快照、AOF的重写都需要fork,这是一个重量级操作,会对Redis造成阻塞。因此为了不影响Redis主进程响应,我们需要尽可能降低阻塞。
  • 那么如何减少fork操作的阻塞呢?
    1. 优先使用物理机或者高效支持fork操作的虚拟化技术。
    2. 控制Redis实例最大可用内存, fork耗时跟内存量成正比, 线上建议每个Redis实例内存控制在10GB以内。
    3. 合理配置Linux内存分配策略,避免物理内存不足导致fork失败。
    4. 降低fork操作的频率,如适度放宽AOF自动触发时机,避免不必要的全量复制等。

总结

  • 本文介绍了Redis持久化的两种不同的策略,大部分内容是运维人员需要掌握的,当然作为后端人员也是需要了解一下,毕竟小公司都是一人搞全栈,哈哈。
  • 如果觉得陈某写的不错,有所收获的话,关注分享一波,你的关注将是陈某写作的最大动力,谢谢支持!!!

前言

  • 如何写出效率高的SQL语句,提到这必然离不开Explain执行计划的分析,至于什么是执行计划,如何写出高效率的SQL,本篇文章将会一一介绍。

执行计划

  • 执行计划是数据库根据 SQL 语句和相关表的统计信息作出的一个查询方案,这个方案是由查询优化器自动分析产生的。

  • 使用explain关键字可以模拟优化器执行 SQL 查询语句,从而知道 MySQL 是如何处理你的 SQL 语句的,分析你的 select 语句或是表结构的性能瓶颈,让我们知道 select 效率低下的原因,从而改进我们的查询。

  • explain 的结果如下:

  • 下面是有关各列的详细介绍,重要的有idtypekeyrowsextra

id

  • id 列的编号就是 select 的序列号,也可以理解为 SQL 执行顺序的标识,有几个 select 就有几个 id。
    • id 值不同:如果是只查询,id 的序号会递增,id 值越大优先级越高,越先被执行;
    • id 值相同:从上往下依次执行;
    • id 列为 null:表示这是一个结果集,不需要使用它来进行查询。

select_type

  • 查询的类型,主要用于区分普通查询、联合查询、子查询等复杂的查询;

    • simple:表示查询中不包括 union 操作或者子查询,位于最外层的查询的 select_type 即为 simple,且只有一个;
      explain select * from t3 where id=3952602;
    
    • primary:需要 union 操作或者含有子查询的 select,位于最外层的查询的 select_type 即为 primary,且只有一个;
    explain select * from (select * from t3 where id=3952602) a ;
    
    • derived:from 列表中出现的子查询,也叫做衍生表;mysql 或者递归执行这些子查询,把结果放在临时表里。
      explain select * from (select * from t3 where id=3952602) a ;
    
    • subquery:除了 from 子句中包含的子查询外,其他地方出现的子查询都可能是 subquery。
    explain select * from t3 where id = (select id from t3 whereid=3952602 ) ;
    
  • union:若第二个 select 出现在 union 之后,则被标记为 union;若 union 包含在 from 子句的子查询中,外层 select 将被标记为 derived。
    explain select * from t3 where id=3952602 union all select * from t3;
    
    • union result:从 union 表获取结果的 select ,因为它不需要参与查询,所以 id 字段为 null。
      explain select * from t3 where id=3952602 union all select * from t3;
    
  • dependent union:与 union 一样,出现在 union 或 union all 语句中,但是这个查询要受到外部查询的影响;
  • dependent subquery:与 dependent union 类似,子查询中的第一个 SELECT,这个 subquery 的查询要受到外部表查询的影响。

table

  • 表示 explain 的一行正在访问哪个表。
    • 如果查询使用了别名,那么这里显示的是别名;
    • 如果不涉及对数据表的操作,那么这显示为 null;
    • 如果显示为尖括号括起来的就表示这个是临时表,后边的 N 就是执行计划中的 id,表示结果来自于这个查询产生;
    • 如果是尖括号括起来的<union M,N>,与类似,也是一个临时表,表示这个结果来自于 union 查询的 id 为 M,N 的结果集。

type

  • 访问类型,即 MySQL 决定如何查找表中的行。

  • 依次从好到差:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL,除了 all 之外,其他的 type 都可以使用到索引,除了 index_merge 之外,其他的 type 只可以用到一个索引。一般来说,得保证查询至少达到 range 级别,最好能达到 ref。

    1. system:表中只有一行数据(等于系统表),这是 const 类型的特例,平时不会出现,可以忽略不计。

    2. const:使用唯一索引或者主键,表示通过索引一次就找到了,const 用于比较 primary key 或者 unique 索引。因为只需匹配一行数据,所有很快。如果将主键置于 where 列表中,mysql 就能将该查询转换为一个 const。

    3. eq_ref:唯一性索引扫描,对于每个索引键,表中只有一行数据与之匹配。常见于主键或唯一索引扫描。

    4. ref:非唯一性索引扫描,返回匹配某个单独值的所有行。本质也是一种索引。

    5. fulltext:全文索引检索,全文索引的优先级很高,若全文索引和普通索引同时存在时,mysql 不管代价,优先选择使用全文索引。

    6. ref_or_null:与 ref 方法类似,只是增加了 null 值的比较。

    7. index_merge:表示查询使用了两个以上的索引,索引合并的优化方法,最后取交集或者并集,常见 and ,or 的条件使用了不同的索引。

    8. unique_subquery:用于 where 中的 in 形式子查询,子查询返回不重复值唯一值;

    9. index_subquery:用于 in 形式子查询使用到了辅助索引或者 in 常数列表,子查询可能返回重复值,可以使用索引将子查询去重。

    10. range:索引范围扫描,常见于使用>,<,between ,in ,like等运算符的查询中。

    11. index:索引全表扫描,把索引树从头到尾扫一遍;

    12. all:遍历全表以找到匹配的行(Index 与 ALL 虽然都是读全表,但 index 是从索引中读取,而 ALL 是从硬盘读取)

    13. NULL: MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引。

possible_keys

  • 显示查询可能使用到的索引。

key

  • 显示查询实际使用哪个索引来优化对该表的访问;

  • select_type 为 index_merge 时,这里可能出现两个以上的索引,其他的 select_type 这里只会出现一个。

key_len

  • 用于处理查询的索引长度,表示索引中使用的字节数。通过这个值,可以得出一个多列索引里实际使用了哪一部分。
  • 注:key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的。另外,key_len 只计算 where 条件用到的索引长度,而排序和分组就算用到了索引,也不会计算到 key_len 中。

ref

  • 显示哪个字段或者常数与 key 一起被使用。

    1. 如果是使用的常数等值查询,这里会显示 const。

    2. 如果是连接查询,被驱动表的执行计划这里会显示驱动表的关联字段。

    3. 如果是条件使用了表达式或者函数,或者条件列发生了内部隐式转换,这里可能显示为 func。

rows

  • 表示 MySQL 根据表统计信息及索引选用情况,大致估算的找到所需的目标记录所需要读取的行数,不是精确值。

extra

  • 不适合在其他列中显示但十分重要的额外信息。

  • 这个列可以显示的信息非常多,有几十种,常用的有:

类型 说明
Using filesort MySQL 有两种方式可以生成有序的结果,通过排序操作或者使用索引,当 Extra 中出现了 Using filesort 说明 MySQL 使用了后者,但注意虽然叫 filesort 但并不是说明就是用了文件来进行排序,只要可能排序都是在内存里完成的。大部分情况下利用索引排序更快,所以一般这时也要考虑优化查询了。使用文件完成排序操作,这是可能是 ordery by,group by 语句的结果,这可能是一个 CPU 密集型的过程,可以通过选择合适的索引来改进性能,用索引来为查询结果排序。
Using temporary 用临时表保存中间结果,常用于 GROUP BY 和 ORDER BY 操作中,一般看到它说明查询需要优化了,就算避免不了临时表的使用也要尽量避免硬盘临时表的使用。
Not exists MYSQL 优化了 LEFT JOIN,一旦它找到了匹配 LEFT JOIN 标准的行, 就不再搜索了。
Using index 说明查询是覆盖了索引的,不需要读取数据文件,从索引树(索引文件)中即可获得信息。如果同时出现 using where,表明索引被用来执行索引键值的查找,没有 using where,表明索引用来读取数据而非执行查找动作。这是 MySQL 服务层完成的,但无需再回表查询记录。
Using index condition 这是 MySQL 5.6 出来的新特性,叫做“索引条件推送”。简单说一点就是 MySQL 原来在索引上是不能执行如 like 这样的操作的,但是现在可以了,这样减少了不必要的 IO 操作,但是只能用在二级索引上。
Using where 使用了 WHERE 从句来限制哪些行将与下一张表匹配或者是返回给用户。注意:Extra 列出现 Using where 表示 MySQL 服务器将存储引擎返回服务层以后再应用 WHERE 条件过滤。
Using join buffer 使用了连接缓存:Block Nested Loop,连接算法是块嵌套循环连接;Batched Key Access,连接算法是批量索引连接
impossible where where 子句的值总是 false,不能用来获取任何元组
select tables optimized away 在没有 GROUP BY 子句的情况下,基于索引优化 MIN/MAX 操作,或者对于 MyISAM 存储引擎优化 COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。
distinct 优化 distinct 操作,在找到第一匹配的元组后即停止找同样值的动作

filtered

  • 使用 explain extended 时会出现这个列,5.7 之后的版本默认就有这个字段,不需要使用 explain extended 了。
  • 这个字段表示存储引擎返回的数据在 server 层过滤后,剩下多少满足查询的记录数量的比例,注意是百分比,不是具体记录数。

关于 MySQL 执行计划的局限性

  1. EXPLAIN 不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况;
  2. EXPLAIN 不考虑各种 Cache;
  3. EXPLAIN 不能显示 MySQL 在执行查询时所作的优化工作;
  4. 部分统计信息是估算的,并非精确值;
  5. EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看。

查询计划案例分析

执行顺序

  1. (id = 4):【select id, name from t2】:select_type 为 union,说明 id=4 的 select 是 union 里面的第二个 select。

  2. (id = 3):【select id, name from t1 where address = ‘11’】:因为是在 from 语句中包含的子查询所以被标记为 DERIVED(衍生),where address = ‘11’ 通过复合索引 idx_name_email_address 就能检索到,所以 type 为 index。

  3. (id = 2):【select id from t3】:因为是在 select 中包含的子查询所以被标记为 SUBQUERY。

  4. (id = 1):【select d1.name, … d2 from … d1】:select_type 为 PRIMARY 表示该查询为最外层查询,table 列被标记为 “derived3”表示查询结果来自于一个衍生表(id = 3 的 select 结果)。

  5. (id = NULL):【 … union … 】:代表从 union 的临时表中读取行的阶段,table 列的 “union 1, 4”表示用 id=1 和 id=4 的 select 结果进行 union 操作。

本文使用 mdnice 排版

前言

  • 在实际的开发中一定会碰到根据某个字段进行排序后来显示结果的需求,但是你真的理解order by在 Mysql 底层是如何执行的吗?
  • 假设你要查询城市是苏州的所有人名字,并且按照姓名进行排序返回前 1000 个人的姓名、年龄,这条 sql 语句应该如何写?
  • 首先创建一张用户表,sql 语句如下:
CREATE TABLE user (
  id int(11) NOT NULL,
  city varchar(16) NOT NULL,
  name varchar(16) NOT NULL,
  age int(11) NOT NULL,
  PRIMARY KEY (id),
  KEY city (city)
) ENGINE=InnoDB;
  • 则上述需求的 sql 查询语句如下:
select city,name,age from user where city='苏州' order by name limit 1000;
  • 这条 sql 查询语句相信大家都能写出来,但是你了解它在 Mysql 底层的执行流程吗?今天陈某来大家聊一聊这条 sql 语句是如何执行的以及有什么参数会影响执行的流程。
  • 本篇文章分为如下几个部分进行详细的阐述:
    1. 全字段排序
    2. rowid 排序
    3. 全字段排序 VS rowid 排序
    4. 如何避免排序

全字段排序

  • 前面聊过索引能够避免全表扫描,因此我们给city这个字段上添加了索引,当然城市的字段很小,不用考虑字符串的索引问题,之前有写过一篇关于如何给字符串的加索引的文章,有不了解朋友看一下这篇文章:Mysql 性能优化:如何给字符串加索引?
  • 此时用Explain来分析一下的这条查询语句的执行情况,结果如下图: Explain分析结果
  • Extra这个字段中的Using filesort表示的就是需要排序,MySQL 会给每个线程分配一块内存用于排序,称为sort_buffer
  • 既然使用了索引进行查询,我们来简单的画一下city这棵索引树的结构,如下图: city索引树
  • 从上图可以看出,满足city='苏州'是从ID3IDX这些记录。
  • 通常情况下,此条 sql 语句执行流程如下:
    1. 初始化 sort_buffer,确定放入 name、city、age 这三个字段。
    2. 从索引 city 找到第一个满足city='苏州'条件的主键id,也就是图中的ID3
    3. 主键id索引取出整行,取namecityage三个字段的值,存入sort_buffer中。
    4. 从索引city取下一个记录的主键 id。
    5. 重复步骤 3、4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的IDX
    6. sort_buffer中的数据按照字段name做快速排序。
    7. 按照排序结果取前 1000 行返回给客户端。
  • 我们称这个排序过程为全字段排序,执行的流程图如下: 全字段排序
  • 图中按name排序这个动作,可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数sort_buffer_size
  • sort_buffer_size:就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。

rowid 排序

  • 在上面这个算法过程里面,只对原表的数据读了一遍,剩下的操作都是在sort_buffer临时文件中执行的。但这个算法有一个问题,就是如果查询要返回的字段很多的话,那么sort_buffer里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差
  • 所以如果单行很大,这个方法效率不够好。
  • 我们可以修改一个max_length_for_sort_data这个参数使其使用另外一种算法。max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。
  • citynameage 这三个字段的定义总长度是36,我把max_length_for_sort_data设置为 16,我们再来看看计算过程有什么改变。设置的 sql 语句如下:
SET max_length_for_sort_data = 16;
  • 新的算法放入 sort_buffer 的字段,只有要排序的列(即 name 字段)和主键 id。

  • 但这时,排序的结果就因为少了 city 和 age 字段的值,不能直接返回了,整个执行流程就变成如下所示的样子:

    1. 初始化sort_buffer,确定放入两个字段,即nameid
    2. 从索引 city 找到第一个满足city='苏州'条件的主键id,也就是图中的ID3
    3. 主键id索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中。
    4. 从索引city取下一个记录的主键 id。
    5. 重复步骤 3、4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的IDX
    6. sort_buffer中的数据按照字段name做快速排序。
    7. 遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。
  • 这个执行流程的示意图如下,我把它称为rowid排序rowid排序

  • 对比全字段排序rowid排序多了一次回表查询,即是多了第7步的查询主键索引树。

全字段排序 VS rowid 排序

  • 如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。
  • 如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
  • 这也就体现了 MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问
  • 对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。

如何避免排序

  • 其实,并不是所有的order by语句,都需要排序操作的。从上面分析的执行过程,我们可以看到,MySQL 之所以需要生成临时表,并且在临时表上做排序操作,其原因是原来的数据都是无序的
  • 如果能够保证从city这个索引上取出来的行,天然就是按照 name 递增排序的话,是不是就可以不用再排序了呢?
  • 因此想到了联合索引,创建(city,name)联合索引,sql 语句如下:
alter table user add index city_user(city, name);
  • 此时的索引树如下: city,name索引树
  • 在这个索引里面,我们依然可以用树搜索的方式定位到第一个满足city='苏州'的记录,并且额外确保了,接下来按顺序取“下一条记录”的遍历过程中,只要 city 的值是苏州,name 的值就一定是有序的。
  • 按照上图,整个查询的流程如下:
    1. 从索引(city,name)找到第一个满足 city='苏州’条件的主键 id。
    2. 到主键 id 索引取出整行,取 name、city、age 三个字段的值,作为结果集的一部分直接返回。
    3. 从索引(city,name)取下一个记录主键 id。
    4. 重复步骤 2、3,直到查到第 1000 条记录,或者是不满足 city='苏州'条件时循环结束。
  • 对应的流程图如下: city,name联合索引的执行流程
  • 可以看到,这个查询过程不需要临时表,也不需要排序。接下来,我们用 explain 的结果来印证一下。
  • 从图中可以看到,Extra字段中没有Using filesort了,也就是不需要排序了。而且由于(city,name)这个联合索引本身有序,所以这个查询也不用把 4000 行全都读一遍,只要找到满足条件的前 1000 条记录就可以退出了。也就是说,在我们这个例子里,只需要扫描 1000 次。
  • 难道仅仅这样就能满足了?此条查询语句是否能再优化呢?
  • 朋友们还记得覆盖索引吗?覆盖索引的好处就是能够避免再次回表查询,不了解的朋友们可以看一下陈某之前写的文章:Mysql 性能优化:如何使用覆盖索引?
  • 我们创建(city,name,age)联合索引,这样在执行上面的查询语句就能使用覆盖索引了,避免了回表查询了,sql 语句如下:
alter table user add index city_user_age(city, name, age);
  • 此时执行流程图如下: 覆盖索引使用执行流程
  • 当然,覆盖索引能够提升效率,但是维护索引也是需要代价的,因此还需要权衡使用。

总结

  • 今天这篇文章,我和你介绍了 MySQL 里面order by语句的几种算法流程。
  • 在开发系统的时候,你总是不可避免地会使用到 order by 语句。心里要清楚每个语句的排序逻辑是怎么实现的,还要能够分析出在最坏情况下,每个语句的执行对系统资源的消耗,这样才能做到下笔如有神,不犯低级错误。

本文使用 mdnice 排版

前言

  • 索引是什么?有什么利弊?一旦在面试中被问道,对于新入门的小白可能是个棘手的问题。
  • 本篇文章将会详细讲述什么是索引、索引的优缺点、数据结构等等常见的知识。

什么是索引

  • 索引就是一种的数据结构,存储表中特定列的值并对值进行排序,所以是在表的列上面创建的。索引将通过缩小一张表中需要查询的记录的数目来加快搜索的速度。如果没有索引,数据库不得不进行全表扫描。索引就好比一本书的目录,它会让你更快的找到内容。

索引的优点

  1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
  2. 可以大大加快数据的检索速度,避免进行全表的数据扫描,大大减少遍历匹配的行数,这也是创建索引的最主要的原因。
  3. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
  4. 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
  5. 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

索引的缺点

  1. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。

  2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。

  3. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。

在哪些列建立索引

  1. 在经常需要搜索的列上,可以加快搜索的速度;
  2. 在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;
  3. 在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;
  4. 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
  5. 在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
  6. 在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。

不在哪些列建索引?

  1. 对于那些在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。
  2. 对于那些只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。
  3. 对于那些定义为text, imagebit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少。
  4. 当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。

索引的数据结构

常见的索引的数据结构有:B+TreeHash索引FullText索引R-Tree索引

Hash 索引

1. 概述:

MySQL 中,只有Memory存储引擎支持Hash索引,是Memory表的默认索引类型。hash 索引把数据的索引以 hash 值形式组织起来,因此检索效率非常高,可以一次定位,不像B-/+Tree索引需要进行从根节点到叶节点的多次 IO 操作。

2. Hash 索引的缺点:

① Hash 索引仅仅能满足等值的查询,不能满足范围查询。因为数据在经过 Hash 算法后,其大小关系就可能发生变化。 ② Hash 索引不能被排序。同样是因为数据经过 Hash 算法后,大小关系就可能发生变化,排序是没有意义的。

③ Hash 索引不能避免表数据的扫描。因为发生 Hash 碰撞时,仅仅比较 Hash 值是不够的,需要比较实际的值以判定是否符合要求。

④ Hash 索引在发生大量 Hash 值相同的情况时性能不一定比 B-Tree 索引高。因为碰撞情况会导致多次的表数据的扫描,造成整体性能的低下,可以通过采用合适的 Hash 算法一定程度解决这个问题。

⑤ Hash 索引不能使用部分索引键查询。因为当使用组合索引情况时,是把多个数据库列数据合并后再计算 Hash 值,所以对单独列数据计算 Hash 值是没有意义的。

FullText 索引

1. 概述:

全文索引,目前 MySQL 中只有MyISAM存储引擎支持,并且只有charvarchartext 类型支持。它用于替代效率较低的like 模糊匹配操作,而且可以通过多字段组合的全文索引一次性全模糊匹配多个字段。

2. 存储结构:

同样使用B-Tree存放索引数据,但使用的是特定的算法,将字段数据分割后再进行索引(一般每 4 个字节一次分割),索引文件存储的是分割前的索引字符串集合,与分割后的索引信息,对应 Btree 结构的节点存储的是分割后的词信息以及它在分割前的索引字符串集合中的位置。

B-/+Tree 索引

  • B+Tree 是 mysql 使用最频繁的一个索引数据结构,是 Innodb 和 Myisam 存储引擎模式的索引类型。相对 Hash 索引,B+树在查找单条记录的速度比不上 Hash 索引,但是更适合排序等操作。

1. B+Tree 索引的优点:

  • 带顺序访问指针的 B+Tree:B+Tree 所有索引数据都在叶子结点上,并且增加了顺序访问指针,每个叶子节点都有指向相邻叶子节点的指针。这样做是为了提高区间查询效率,例如查询 key 为从 18 到 49 的所有数据记录,当找到 18 后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。

  • 大大减少磁盘 I/O 读取次数。

B-/+Tree 索引:

  • 文件系统及数据库系统普遍采用 B-/+Tree 作为索引结构:一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上。这样的话,索引查找过程中就要产生磁盘 I/O 消耗,相对于内存存取,I/O 存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘 I/O 操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘 I/O 的存取次数。

局部性处理与磁盘预读

  • 由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁盘 I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。

  • 由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高 I/O 效率。预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为 4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。

B-/+Tree 索引的性能分析

  • 上文说过一般使用磁盘 I/O 次数评价索引结构的优劣。先从 B-Tree 分析,根据 B-Tree 的定义,可知检索一次最多需要访问 h 个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。为了达到这个目的,在实际实现 B-Tree 还需要使用如下技巧:

  • 每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个节点只需一次 I/O。

  • B-Tree 中一次检索最多需要h-1次 I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)。一般实际应用中,出度 d 是非常大的数字,通常超过 100,因此 h 非常小(通常不超过 3)。

  • 综上所述,用 B-Tree 作为索引结构效率是非常高的。

  • 而红黑树这种结构,h 明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的 I/O 渐进复杂度也为 O(h),效率明显比 B-Tree 差很多。

  • 另外,B+Tree 更适合外存索引,原因和内节点出度 d 有关。从上面分析可以看到,d 越大索引的性能越好,而出度的上限取决于节点内 key 和 data 的大小,由于 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,拥有更好的性能。(详细见本部分第 3 点)

B-Tree 与 B+Tree 的对比

  • 根据 B-Tree 和 B+Tree 的结构,我们可以发现 B+树相比于 B 树,在文件系统或者数据库系统当中,更有优势,原因如下:

1. B+树的磁盘读写代价更低

B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对 B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说 I/O 读写次数也就降低了。

2. B+树的查询效率更加稳定

由于内部结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

3. B+树更有利于对数据库的扫描

B 树在提高了磁盘 IO 性能的同时并没有解决元素遍历的效率低下的问题,而 B+树只需要遍历叶子节点就可以解决对全部关键字信息的扫描,所以对于数据库中频繁使用的 range query,B+树有着更高的性能。

MySQL 索引的实现

  • 在 MySQL 中,索引属于存储引擎级别的概念,不同存储引擎对索引的实现方式是不同的,本部分主要讨论 MyISAM 和 InnoDB 两个存储引擎的索引实现方式。

MyISAM 索引的实现

1. 主键索引

MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址。下图是 MyISAM 索引的原理图:

img
img
  • 这里设表一共有三列,假设我们以 Col1 为主键,则上图是一个 MyISAM 表的主索引(Primary key)示意。可以看出 MyISAM 的索引文件仅仅保存数据记录的地址。

2. 辅助索引

MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复。如果我们在 Col2 上建立一个辅助索引,则此索引的结构如下图所示:

img
img
  • 同样也是一颗 B+Tree,data 域保存数据记录的地址。因此,MyISAM 中索引检索的算法为首先按照 B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其 data 域的值,然后以 data 域的值为地址,读取相应数据记录。

  • MyISAM 的索引方式也叫做“非聚集”的,之所以这么称呼是为了与 InnoDB 的聚集索引区分。

InnoDB 索引的实现

  • 虽然 InnoDB 也使用 B+Tree 作为索引结构,但具体实现方式却不相同。

1. 主键索引

与 MyISAM 第一个重大区别是 InnoDB 的数据文件本身就是索引文件。从上文知道,MyISAM 索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这棵树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。

img
img

上图是 InnoDB 主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为 InnoDB 的数据文件本身要按主键聚集,所以 InnoDB 要求表必须有主键(MyISAM 可以没有),如果没有显式指定,则 MySQL 系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则 MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,这个字段长度为 6 个字节,类型为长整形。

2. 辅助索引

第二个与 MyISAM 索引的不同是 InnoDB 的辅助索引 data 域存储相应记录主键的值而不是地址。换句话说,InnoDB 的所有辅助索引都引用主键作为 data 域。例如,下图为定义在 Col3 上的一个辅助索引:

img
img
  • 这里以英文字符的 ASCII 码作为比较准则。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。

  • InnoDB 表是基于聚簇索引建立的。因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过,它的辅助索引也会包含主键列,所以,如果主键使用过长的字段,将会导致其他辅助索变得更大。如果想在表上定义 、很多索引,则争取尽量把主键定义得小一些。InnoDB 不会压缩索引。

  • 更多内容请关注微信公众号【码猿技术专栏】

本文使用 mdnice 排版

前言

  • 前几天有读者找到我,说想要一套全面的Mysql面试题,今天陈某特地为她写了一篇。
  • 由于篇幅较长,陈某已经将此文章转换为PDF,公众号回复关键词Mysql面试题即可获取。

Mysql

什么是SQL?

  • 结构化查询语言(Structured Query Language)简称SQL,是一种数据库查询语言。
  • 作用:用于存取数据、查询、更新和管理关系数据库系统。

什么是MySQL?

  • MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。在Java企业级开发中非常常用,因为 MySQL 是开源免费的,并且方便扩展。

数据库三大范式是什么?

  • 第一范式:每个列都不可以再拆分。
  • 第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
  • 第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
  • 在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。

mysql有关权限的表都有哪几个?

  • MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库里,由mysql_install_db脚本初始化。这些权限表分别user,db,table_priv,columns_priv和host。下面分别介绍一下这些表的结构和内容:
    1. user权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的。
    2. db权限表:记录各个帐号在各个数据库上的操作权限。
    3. table_priv权限表:记录数据表级的操作权限。
    4. columns_priv权限表:记录数据列级的操作权限。
    5. host权限表:配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。

MySQL的binlog有有几种录入格式?分别有什么区别?

  • 有三种格式,statement,row和mixed。
    • statement模式下,每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。
    • row级别下,不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。
    • mixed,一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。
  • 此外,新版的MySQL中对row级别也做了一些优化,当表结构发生变化的时候,会记录语句而不是逐行记录。

mysql有哪些数据类型?

  • 1、整数类型,包括TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT,分别表示1字节、2字节、3字节、4字节、8字节整数。任何整数类型都可以加上UNSIGNED属性,表示数据是无符号的,即非负整数。
    • 长度:整数类型可以被指定长度,例如:INT(11)表示长度为11的INT类型。长度在大多数场景是没有意义的,它不会限制值的合法范围,只会影响显示字符的个数,而且需要和UNSIGNED ZEROFILL属性配合使用才有意义。
    • 例子:假定类型设定为INT(5),属性为UNSIGNED ZEROFILL,如果用户插入的数据为12的话,那么数据库实际存储数据为00012。
  • 2、实数类型,包括FLOAT、DOUBLE、DECIMAL。DECIMAL可以用于存储比BIGINT还大的整型,能存储精确的小数。而FLOAT和DOUBLE是有取值范围的,并支持使用标准的浮点进行近似计算。计算时FLOAT和DOUBLE相比DECIMAL效率更高一些,DECIMAL你可以理解成是用字符串进行处理。
  • 3、字符串类型,包括VARCHAR、CHAR、TEXT、BLOB
    • VARCHAR用于存储可变长字符串,它比定长类型更节省空间。
    • VARCHAR使用额外1或2个字节存储字符串长度。列长度小于255字节时,使用1字节表示,否则使用2字节表示。
    • VARCHAR存储的内容超出设置的长度时,内容会被截断。
    • CHAR是定长的,根据定义的字符串长度分配足够的空间。
    • CHAR会根据需要使用空格进行填充方便比较。
    • CHAR适合存储很短的字符串,或者所有值都接近同一个长度。
    • CHAR存储的内容超出设置的长度时,内容同样会被截断。
  • 4、枚举类型(ENUM),把不重复的数据存储为一个预定义的集合。
    • 有时可以使用ENUM代替常用的字符串类型。
    • ENUM存储非常紧凑,会把列表值压缩到一个或两个字节。
    • ENUM在内部存储时,其实存的是整数。
    • 尽量避免使用数字作为ENUM枚举的常量,因为容易混乱。
    • 排序是按照内部存储的整数
  • 5、日期和时间类型,尽量使用timestamp,空间效率高于datetime,
    • 用整数保存时间戳通常不方便处理。
    • 如果需要存储微妙,可以使用bigint存储。
    • 看到这里,这道真题是不是就比较容易回答了。

MyISAM索引与InnoDB索引的区别?

  • InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
  • InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
  • MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
  • InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。

InnoDB引擎的4大特性

  • 插入缓冲(insert buffer)
  • 二次写(double write)
  • 自适应哈希索引(ahi)
  • 预读(read ahead)

什么是索引?

  • 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
  • 索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
  • 更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。

索引有哪些优缺点?

  • 索引的优点:
    • 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
    • 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
  • 索引的缺点:
    • 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
    • 空间方面:索引需要占物理空间。

索引有哪几种类型?

  • 主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。
  • 唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
    • 可以通过 ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引
    • 可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引
  • 普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。
    • 可以通过ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引
    • 可以通过ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引。
  • 全文索引: 是目前搜索引擎使用的一种关键技术。
    • 可以通过ALTER TABLE table_name ADD FULLTEXT (column);创建全文索引

索引的数据结构(b树,hash)

  • 索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引,B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。

1. B树索引

  • mysql通过存储引擎取数据,基本上90%的人用的就是InnoDB了,按照实现方式分,InnoDB的索引类型目前只有两种:BTREE(B树)索引和HASH索引。B树索引是Mysql数据库中使用最频繁的索引类型,基本所有存储引擎都支持BTree索引。通常我们说的索引不出意外指的就是(B树)索引(实际是用B+树实现的,因为在查看表索引时,mysql一律打印BTREE,所以简称为B树索引) B树索引

2. B+tree性质

  • n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。
  • 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  • 所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
  • B+ 树中,数据对象的插入和删除仅在叶节点上进行。
  • B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。

3. 哈希索引

  • 简要说下,类似于数据结构中简单实现的HASH表(散列表)一样,当我们在mysql中用哈希索引时,主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储。当然这只是简略模拟图。 哈希索引

索引的基本原理

  • 索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
  • 索引的原理很简单,就是把无序的数据变成有序的查询
    1. 把创建了索引的列的内容进行排序
    2. 对排序结果生成倒排表
    3. 在倒排表内容上拼上数据地址链
    4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据

索引算法有哪些?

  • 索引算法有 BTree算法和Hash算法

1. BTree算法

  • BTree是最常用的mysql数据库索引算法,也是mysql默认的算法。因为它不仅可以被用在=,>,>=,<,<=和between这些比较操作符上,而且还可以用于like操作符,只要它的查询条件是一个不以通配符开头的常量。

2. Hash算法

  • Hash Hash索引只能用于对等比较,例如=,<=>(相当于=)操作符。由于是一次定位数据,不像BTree索引需要从根节点到枝节点,最后才能访问到页节点这样多次IO访问,所以检索效率远高于BTree索引。

索引设计的原则?

  • 适合索引的列是出现在where子句中的列,或者连接子句中指定的列。
  • 基数较小的类,索引效果较差,没有必要在此列建立索引
  • 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
  • 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

创建索引的原则

  • 索引虽好,但也不是无限制的使用,最好符合一下几个原则
    • 最左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
    • 较频繁作为查询条件的字段才去创建索引
    • 更新频繁字段不适合创建索引
    • 若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
    • 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
    • 定义有外键的数据列一定要建立索引。
    • 对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。
    • 对于定义为text、image和bit的数据类型的列不要建立索引。

创建索引时需要注意什么?

  • 非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值;
  • 取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;
  • 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。

使用索引查询一定能提高查询的性能吗?

  • 通常,通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。
  • 索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况:
  • 基于一个范围的检索,一般查询返回结果集小于表中记录数的30%
  • 基于非唯一性索引的检索

百万级别或以上的数据如何删除?

  • 关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。
    1. 所以我们想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟)
    2. 然后删除其中无用数据(此过程需要不到两分钟)
    3. 删除完成后重新创建索引(此时数据较少了)创建索引也非常快,约十分钟左右。
    4. 与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚。那更是坑了。

什么是最左前缀原则?什么是最左匹配原则?

  • 顾名思义,就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。
  • 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
  • =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式

B树和B+树的区别

  • 在B树中,你可以将键和值存放在内部节点和叶子节点;但在B+树中,内部节点都是键,没有值,叶子节点同时存放键和值。
  • B+树的叶子节点有一条链相连,而B树的叶子节点各自独立。 B树和B+树的区别

使用B树的好处

  • B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。

使用B+树的好处

  • 由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间

什么是聚簇索引?何时使用聚簇索引与非聚簇索引?

  • 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
  • 非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因。

非聚簇索引一定会回表查询吗?

  • 不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。
  • 举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select age from employee where age < 20的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询。

联合索引是什么?为什么需要注意联合索引中的顺序?

  • MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。
  • MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。
  • 当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。

什么是数据库事务?

  • 事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。

事物的四大特性(ACID)介绍一下?

  • 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
  • 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  • 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

什么是脏读?幻读?不可重复读?

  • 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
  • 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
  • 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

什么是事务的隔离级别?MySQL的默认隔离级别是什么?

  • 为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。
  • SQL 标准定义了四个隔离级别:
    • READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
    • READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
    • REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
    • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
  • Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别

隔离级别与锁的关系

  • 在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突
  • 在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;
  • 在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
  • SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。

按照锁的粒度分数据库锁有哪些?

  • 行级锁:行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 表级锁: 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
  • 页级锁:页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。

从锁的类别上分MySQL都有哪些锁呢?

  • 从锁的类别上来讲,有共享锁和排他锁。
    • 共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
    • 排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。

InnoDB存储引擎的锁的算法有哪三种?

  • Record lock:单个行记录上的锁
  • Gap lock:间隙锁,锁定一个范围,不包括记录本身
  • Next-key lock:record+gap 锁定一个范围,包含记录本身

什么是死锁?怎么解决?

  • 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
  • 常见的解决死锁的方法
    1. 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
    2. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
    3. 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
  • 如果业务处理不好可以用分布式事务锁或者使用乐观锁

数据库的乐观锁和悲观锁是什么?怎么实现的?

  • 数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。
  • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制
  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。

大表数据查询,怎么优化?

  • 优化shema、sql语句+索引;
  • 第二加缓存,memcached, redis;
  • 主从复制,读写分离;
  • 垂直拆分,根据你模块的耦合度,将一个大的系统分为多个小的系统,也就是分布式系统
  • 水平切分,针对数据量大的表,这一步最麻烦,最能考验技术水平,要选择一个合理的sharding key, 为了有好的查询效率,表结构也要改动,做一定的冗余,应用也要改,sql中尽量带sharding key,将数据定位到限定的表上去查,而不是扫描全部的表

超大分页怎么处理?

  • 超大的分页一般从两个方向上来解决:
    • 数据库层面,这也是我们主要集中关注的(虽然收效没那么大),类似于select * from table where age > 20 limit 1000000,10这种查询其实也是有可以优化的余地的. 这条语句需要load1000000数据然后基本上全部丢弃,只取10条当然比较慢. 当时我们可以修改为select * from table where id in (select id from table where age > 20 limit 1000000,10).这样虽然也load了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中,所以速度会很快. 同时如果ID连续的好,我们还可以select * from table where id > 1000000 limit 10,效率也是不错的,优化的可能性有许多种,但是核心思想都一样,就是减少load的数据
    • 从需求的角度减少这种请求…主要是不做类似的需求(直接跳转到几百万页之后的具体某一页.只允许逐页查看或者按照给定的路线走,这样可预测,可缓存)以及防止ID泄漏且连续被人恶意攻击

为什么要尽量设定一个主键?

  • 主键是数据库确保数据行在整张表唯一性的保障,即使业务上本张表没有主键,也建议添加一个自增长的ID列作为主键。设定了主键之后,在后续的删改查的时候可能更加快速以及确保操作数据范围安全。

主键使用自增ID还是UUID?

  • 推荐使用自增ID,不要使用UUID。
  • 因为在InnoDB存储引擎中,主键索引是作为聚簇索引存在的,也就是说,主键索引的B+树叶子节点上存储了主键索引以及全部的数据(按照顺序),如果主键索引是自增ID,那么只需要不断向后排列即可,如果是UUID,由于到来的ID与原来的大小不确定,会造成非常多的数据插入,数据移动,然后导致产生很多的内存碎片,进而造成插入性能的下降。
  • 总之,在数据量大一些的情况下,用自增主键性能会好一些。
  • 关于主键是聚簇索引,如果没有主键,InnoDB会选择一个唯一键来作为聚簇索引,如果没有唯一键,会生成一个隐式的主键。

字段为什么要求定义为not null?

  • null值会占用更多的字节,且会在程序中造成很多与预期不符的情况。

如果要存储用户的密码散列,应该使用什么字段进行存储?

  • 密码散列,盐,用户身份证号等固定长度的字符串应该使用char而不是varchar来存储,这样可以节省空间且提高检索效率。

数据库结构优化?

  • 一个好的数据库设计方案对于数据库的性能往往会起到事半功倍的效果。
  • 需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。
  • 将字段很多的表分解成多个表:对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。
  • 增加中间表:对于需要经常联合查询的表,可以建立中间表以提高查询效率。通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。
  • 增加冗余字段:设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。

MySQL数据库cpu飙升到500%的话他怎么处理?

  • 当 cpu 飙升到 500%时,先用操作系统命令 top 命令观察是不是 mysqld 占用导致的,如果不是,找出占用高的进程,并进行相关处理。
  • 如果是 mysqld 造成的, show processlist,看看里面跑的 session 情况,是不是有消耗资源的 sql 在运行。找出消耗高的 sql,看看执行计划是否准确, index 是否缺失,或者实在是数据量太大造成。
  • 一般来说,肯定要 kill 掉这些线程(同时观察 cpu 使用率是否下降),等进行相应的调整(比如说加索引、改 sql、改内存参数)之后,再重新跑这些 SQL。
  • 也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等。

主从复制的作用?

  • 主数据库出现问题,可以切换到从数据库。
  • 可以进行数据库层面的读写分离。
  • 可以在从数据库上进行日常备份。

MySQL主从复制解决的问题?

  • 数据分布:随意开始或停止复制,并在不同地理位置分布数据备份
  • 负载均衡:降低单个服务器的压力
  • 高可用和故障切换:帮助应用程序避免单点失败
  • 升级测试:可以用更高版本的MySQL作为从库

MySQL主从复制工作原理?

  • 在主库上把数据更高记录到二进制日志
  • 从库将主库的日志复制到自己的中继日志
  • 从库读取中继日志的事件,将其重放到从库数据中。

小福利

  • 由于文章篇幅较长,陈某将其转换为PDF文档,老规矩,回复关键词Mysql面试题即可获取。

巨人的肩膀

  • https://www.cnblogs.com/hsmwlyl/p/10719152.html
  • https://www.cnblogs.com/caomusheng/p/12586895.html
  • https://article.itxueyuan.com/eoJEMj
  • https://blog.csdn.net/thinkwon/article/details/104778621#comments

本文使用 mdnice 排版

前言

  • 索引的相信大家都听说过,但是真正会用的又有几人?平时工作中写SQL真的会考虑到这条SQL如何能够用上索引,如何能够提升执行效率?
  • 此篇文章详细的讲述了索引优化的几个原则,只要在工作中能够随时应用到,相信你写出的SQL一定是效率最高,最牛逼的。
  • 文章的脑图如下: 索引优化几大规则

索引优化规则

1、like语句的前导模糊查询不能使用索引

select * from doc where title like '%XX'--不能使用索引
select * from doc where title like 'XX%'--非前导模糊查询,可以使用索引
  • 因为页面搜索严禁左模糊或者全模糊,如果需要可以使用搜索引擎来解决。

2、union、in、or 都能够命中索引,建议使用 in

  1. union能够命中索引,并且MySQL 耗费的 CPU 最少。
select * from doc where status=1
union all
select * from doc where status=2;
  1. in能够命中索引,查询优化耗费的 CPU 比 union all 多,但可以忽略不计,一般情况下建议使用 in
select * from doc where status in (1, 2);
  1. or 新版的 MySQL 能够命中索引,查询优化耗费的 CPU 比 in多,不建议频繁用or
select * from doc where status = 1 or status = 2
  1. 补充:有些地方说在where条件中使用or,索引会失效,造成全表扫描,这是个误区:
  • ①要求where子句使用的所有字段,都必须建立索引;

  • ②如果数据量太少,mysql制定执行计划时发现全表扫描比索引查找更快,所以会不使用索引;

  • ③确保mysql版本5.0以上,且查询优化器开启了index_merge_union=on, 也就是变量optimizer_switch里存在index_merge_union且为on

3、负向条件查询不能使用索引

  • 负向条件有:!=<>not innot existsnot like 等。

  • 例如下面SQL语句:

select * from doc where status != 1 and status != 2;
  • 可以优化为 in 查询:
select * from doc where status in (0,3,4);

4、联合索引最左前缀原则

  • 如果在(a,b,c)三个字段上建立联合索引,那么他会自动建立 a| (a,b) | (a,b,c)组索引。

  • 登录业务需求,SQL语句如下:

select uid, login_time from user where login_name=? andpasswd=?
  • 可以建立(login_name, passwd)的联合索引。因为业务上几乎没有passwd 的单条件查询需求,而有很多login_name 的单条件查询需求,所以可以建立(login_name, passwd)的联合索引,而不是(passwd, login_name)。
  1. 建立联合索引的时候,区分度最高的字段在最左边
  1. 存在非等号和等号混合判断条件时,在建立索引时,把等号条件的列前置。如 where a>? and b=?,那么即使a 的区分度更高,也必须把 b 放在索引的最前列。
  1. 最左前缀查询时,并不是指SQL语句的where顺序要和联合索引一致
  • 下面的 SQL 语句也可以命中 (login_name, passwd) 这个联合索引:
select uid, login_time from user where passwd=? andlogin_name=?
  • 但还是建议 where 后的顺序和联合索引一致,养成好习惯。
  1. 假如index(a,b,c), where a=3 and b like 'abc%' and c=4a能用,b能用,c不能用。

5、不能使用索引中范围条件右边的列(范围列可以用到索引),范围列之后列的索引全失效

  • 范围条件有:<、<=、>、>=、between等。
  • 索引最多用于一个范围列,如果查询条件中有两个范围列则无法全用到索引。
  • 假如有联合索引 (empno、title、fromdate),那么下面的 SQL 中 emp_no 可以用到索引,而titlefrom_date 则使用不到索引。
select * from employees.titles where emp_no < 10010' and title='Senior Engineer'and from_date between '1986-01-01' and '1986-12-31'

6、不要在索引列上面做任何操作(计算、函数),否则会导致索引失效而转向全表扫描

  • 例如下面的 SQL 语句,即使 date 上建立了索引,也会全表扫描:
select * from doc where YEAR(create_time) <= '2016';
  • 可优化为值计算,如下:
select * from doc where create_time <= '2016-01-01';
  • 比如下面的 SQL 语句:
select * from order where date < = CURDATE();
  • 可以优化为:
select * from order where date < = '2018-01-2412:00:00';

7、强制类型转换会全表扫描

  • 字符串类型不加单引号会导致索引失效,因为mysql会自己做类型转换,相当于在索引列上进行了操作。
  • 如果 phone 字段是 varchar 类型,则下面的 SQL 不能命中索引。
select * from user where phone=13800001234
  • 可以优化为:
select * from user where phone='13800001234';

8、更新十分频繁、数据区分度不高的列不宜建立索引

  • 更新会变更 B+ 树,更新频繁的字段建立索引会大大降低数据库性能。

  • “性别”这种区分度不大的属性,建立索引是没有什么意义的,不能有效过滤数据,性能与全表扫描类似。

  • 一般区分度在80%以上的时候就可以建立索引,区分度可以使用 count(distinct(列名))/count(*) 来计算。

9、利用覆盖索引来进行查询操作,避免回表,减少select * 的使用

  • 覆盖索引:查询的列和所建立的索引的列个数相同,字段相同。
  • 被查询的列,数据能从索引中取得,而不用通过行定位符 row-locator 再到 row 上获取,即“被查询列要被所建的索引覆盖”,这能够加速查询速度。
  • 例如登录业务需求,SQL语句如下。
Select uid, login_time from user where login_name=? and passwd=?
  • 可以建立(login_name, passwd, login_time)的联合索引,由于 login_time 已经建立在索引中了,被查询的 uidlogin_time 就不用去 row 上获取数据了,从而加速查询。

10、索引不会包含有NULL值的列

  • 只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时,尽量使用not null 约束以及默认值。

11、is null, is not null无法使用索引

12、如果有order by、group by的场景,请注意利用索引的有序性

  1. order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现file_sort 的情况,影响查询性能。
  • 例如对于语句 where a=? and b=? order by c,可以建立联合索引(a,b,c)
  1. 如果索引中有范围查找,那么索引有序性无法利用,如WHERE a>10 ORDER BY b;,索引(a,b)无法排序。

13、使用短索引(前缀索引)

  • 对列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个CHAR(255)的列,如果该列在前10个或20个字符内,可以做到既使得前缀索引的区分度接近全列索引,那么就不要对整个列进行索引。因为短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作,减少索引文件的维护开销。可以使用count(distinct leftIndex(列名, 索引长度))/count(*) 来计算前缀索引的区分度。

  • 但缺点是不能用于 ORDER BYGROUP BY 操作,也不能用于覆盖索引。

  • 不过很多时候没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。

14、利用延迟关联或者子查询优化超多分页场景

  • MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
  • 示例如下,先快速定位需要获取的id段,然后再关联:
selecta.* from 表1 a,(select id from1 where 条件 limit100000,20 ) b where a.id=b.id;

15、如果明确知道只有一条结果返回,limit 1 能够提高效率

  • 比如如下 SQL 语句:
select * from user where login_name=?;
  • 可以优化为:
select * from user where login_name=? limit 1
  • 自己明确知道只有一条结果,但数据库并不知道,明确告诉它,让它主动停止游标移动。

16、超过三个表最好不要 join

  • 需要 join 的字段,数据类型必须一致,多表关联查询时,保证被关联的字段需要有索引。

  • 例如:left join是由左边决定的,左边的数据一定都有,所以右边是我们的关键点,建立索引要建右边的。当然如果索引在左边,可以用right join

17、单表索引建议控制在5个以内

18、SQL 性能优化 explain 中的 type:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts 最好

  • consts:单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。

  • ref:使用普通的索引(Normal Index)

  • range:对索引进行范围检索。

  • type=index 时,索引物理文件全扫,速度非常慢。

19、业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引

  • 不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的。另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。

20.创建索引时避免以下错误观念

  1. 索引越多越好,认为需要一个查询就建一个索引。
  2. 宁缺勿滥,认为索引会消耗空间、严重拖慢更新和新增速度。
  3. 抵制惟一索引,认为业务的惟一性一律需要在应用层通过“先查后插”方式解决。
  4. 过早优化,在不了解系统的情况下就开始优化。

索引选择性与前缀索引

  • 既然索引可以加快查询速度,那么是不是只要是查询语句需要,就建上索引?答案是否定的。因为索引虽然加快了查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,同时索引会加重插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护索引,因此索引并不是越多越好。一般两种情况下不建议建索引。

  • 第一种情况是表记录比较少,例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全表扫描就好了。至于多少条记录才算多,这个个人有个人的看法,我个人的经验是以2000作为分界线,记录数不超过 2000可以考虑不建索引,超过2000条可以酌情考虑索引。

  • 另一种不建议建索引的情况是索引的选择性较低。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值:

Index Selectivity = Cardinality / #T
  • 显然选择性的取值范围为(0, 1]``,选择性越高的索引价值越大,这是由B+Tree的性质决定的。例如,employees.titles表,如果title`字段经常被单独查询,是否需要建索引,我们看一下它的选择性:
SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM employees.titles;
+-------------+
| Selectivity |
+-------------+
|      0.0000 |
+-------------+
  • title的选择性不足0.0001(精确值为0.00001579),所以实在没有什么必要为其单独建索引。

  • 有一种与索引选择性有关的索引优化策略叫做前缀索引,就是用列的前缀代替整个列作为索引key,当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时因为索引key变短而减少了索引文件的大小和维护开销。下面以employees.employees表为例介绍前缀索引的选择和使用。

  • 假设employees表只有一个索引<emp_no>,那么如果我们想按名字搜索一个人,就只能全表扫描了:

EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 300024 | Using where |
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
  • 如果频繁按名字搜索员工,这样显然效率很低,因此我们可以考虑建索引。有两种选择,建<first_name><first_name, last_name>,看下两个索引的选择性:
SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.0042 |
+-------------+
SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.9313 |
+-------------+
  • <first_name>显然选择性太低,``<first_name, last_name>选择性很好,但是first_namelast_name加起来长度为30,有没有兼顾长度和选择性的办法?可以考虑用first_name和last_name的前几个字符建立索引,例如<first_name, left(last_name, 3)>`,看看其选择性:
SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.7879 |
+-------------+

  • 选择性还不错,但离0.9313还是有点距离,那么把last_name前缀加到4:
SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.9007 |
+-------------+
  • 这时选择性已经很理想了,而这个索引的长度只有18,比<first_name, last_name>短了接近一半,我们把这个前缀索引建上:
ALTER TABLE employees.employees
ADD INDEX `first_name_last_name4` (first_name, last_name(4));
  • 此时再执行一遍按名字查询,比较分析一下与建索引前的结果:
SHOW PROFILES;
+----------+------------+---------------------------------------------------------------------------------+
| Query_ID | Duration   | Query                                                                           |
+----------+------------+---------------------------------------------------------------------------------+
|       87 | 0.11941700 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |
|       90 | 0.00092400 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |
+----------+------------+---------------------------------------------------------------------------------+
  • 性能的提升是显著的,查询速度提高了120多倍。

  • 前缀索引兼顾索引大小和查询速度,但是其缺点是不能用于ORDER BYGROUP BY操作,也不能用于Covering index(即当索引本身包含查询所需全部数据时,不再访问数据文件本身)。

总结

  • 本文主要讲了索引优化的二十个原则,希望读者喜欢。
  • 本篇文章脑图PDF文档已经准备好,有需要的伙伴可以回复关键词索引优化获取。

本文使用 mdnice 排版

前言

  • 上篇讲了MySQL的索引优化,此篇文章从大范围讲一下MySQL数据库到底该如何优化?这个问题在面试中时常被问到,今天陈某来总结下。

SQL 优化的几个步骤

1. 通过show status 命令了解各种 SQL 的执行效率

show [session | global] status;
  • 可以根据需要加上参数来显示session级(当前连接,默认)和global级(自数据库上次启动至今)的统计结果。
show status like 'Com_%';  ---显示当前连接所有统计参数的值。
  • Com_xxx表示每个xxx语句执行的次数,通常需要注意的是下面几个参数:Com_select/Com_insert/Com_update/Com_delete

2. 定位执行效率较低的 SQL 语句

  • 通过show processlist命令实时查看当前 SQL 的执行情况;

  • 通过慢查询日志定位出现的问题。

3. 通过explaindesc分析低效 SQL 的执行计划

4. 通过show profile 分析 SQL。

  • show profile 能帮我们了解时间都耗费到哪里去了。

  • 通过show profiles我们能够更清楚了解 SQL 执行的过程;

5. 通过trace分析优化器如何选择执行计划

  • MySQL5.6提供了对 SQL 的跟踪trace,能帮我们了解为什么优化器选择执行 A 计划而不是 B 计划,进一步理解优化器的行为。

6. 确定问题并采取相应的优化措施。

MySQL 常用的 SQL 语句优化方法

  1. 应尽量避免在 where 子句中使用!=<>操作符,否则将引擎放弃使用索引而进行全表扫描。

  2. 对查询进行优化,应尽量避免全表扫描,首先应考虑在 whereorder by 涉及的列上建立索引。

  3. 应尽量避免在where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描。如:

select id from t where num is null
  • 可以在 num 上设置默认值 0,确保表中 num 列没有 null 值,然后这样查询:
select id from t where num=0
  1. 避免在where子句中使用or来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描。

  2. 前导模糊查询将导致全表扫描

  select id from t where name like ‘%c%’
  • 下面使用索引
select id from t where name like ‘c%’
  1. not in 也要慎用,否则会导致全表扫描;对于连续的数值,能用between 就不要用 in 了,尽量使用exists代替in

  2. 如果在 where 子句中使用参数,也会导致全表扫描。因为 SQL 只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:

  select id from t where num=@num
  • 可以改为强制查询使用索引:
  select id from t with(index(索引名)) where num=@num
  1. 应尽量避免在 where 子句中对字段进行表达式与函数或其他表达式运算操作,这将导致引擎放弃使用索引而进行全表扫描。如:

select id from t where num/2=100,应改为:select id from t where num=100*2

select id from t where substring(name,1,3)='abc';nameabc开头的id,应改为:select id from t where name like 'abc%'

select id from t where datediff(day,createdate,'2005-11-30')=0 –'2005-11-30′生成的 id,应改为:select id from t where createdate>=’2005-11-30′ and createdate<'2005-12-01'

  1. Update 语句,如果只更改 1、2 个字段,不要 Update 全部字段,否则频繁调用会引起明显的性能消耗,同时带来大量日志。

  2. 在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。

  3. 并不是所有索引对查询都有效,SQL 是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL 查询可能不会去利用索引。如一表中有字段 sex,male、female 几乎各一半,那么即使在 sex 上建了索引也对查询效率起不了作用。

  4. 索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insertupdate 的效率,因为 insertupdate 时有可能会重建索引。一个表的索引数较好不要超过 6 个。

  5. 应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。

  6. 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

  7. 任何地方都不要使用 select * from t ,用具体的字段列表代替*,不要返回用不到的任何字段。

  8. 对于多张大数据量(这里几百条就算大了)的表JOIN,要先分页再JOIN,否则逻辑读会很高,性能很差。

  9. 尽量使用表变量来代替临时表。

  10. 考虑使用临时表暂存中间结果。临时表并不是不可使用,适当地使用它们可以使某些查询更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。将临时结果暂存在临时表,后面的查询就在tempdb中查询了,这可以避免程序中多次扫描主表,也大大减少了程序执行中共享锁阻塞更新锁,减少了阻塞,提高了并发性能。但是,对于一次性事件,较好使用导出表。

  11. 在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert

  12. 如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。

  13. 避免频繁创建和删除临时表,以减少系统表资源的消耗。

  14. 尽量避免使用游标,因为游标的效率较差。与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。

  15. 在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF

  16. 尽量避免向客户端返回大数据量。

  17. 尽量避免大事务操作,提高系统并发能力。

  18. where子句替换Having子句

避免使用 having 子句,having 只会在检索出所有记录之后才会对结果集进行过滤,这个处理需要排序,如果能通过 where 子句限制记录的数目,就可以减少这方面的开销。on、where、having 这三个都可以加条件的子句,on 是最先执行,where 次之,having 最后。

  1. 使用 Truncate 替代 delete

当需要删除全表的记录时使用Truncate替代delete。在通常情况下, 回滚段(rollback segments ) 用来存放可以被恢复的信息. 如果你没有COMMIT事务,ORACLE 会将数据恢复到删除之前的状态(准确地说是恢复到执行删除命令之前的状况) 而当运用 TRUNCATE 时, 回滚段不再存放任何可被恢复的信息.当命令运行后,数据不能被恢复.因此很少的资源被调用,执行时间也会很短。

  1. 使用表的别名:

当在 SQL 语句中连接多个表时, 请使用表的别名并把别名前缀于每个 Column 上.这样一来,就可以减少解析的时间并减少那些由 Column 歧义引起的语法错误。

  1. 使用union all 替换 union

当 SQL 语句需要union两个查询结果集合时,这两个结果集合会以 union all 的方式被合并,然后再输出最终结果前进行排序。如果用 union all 替代料 union,这样排序就不是不要了,效率就会因此得到提高. 需要注意的是,UNION ALL 将重复输出两个结果集合中相同记录。

  1. 用 where 替代 order by:

    ORDER BY 子句只在两种严格的条件下使用索引: ①ORDER BY中所有的列必须包含在相同的索引中并保持在索引中的排列顺序; ②ORDER BY中所有的列必须定义为非空;

低效: (索引不被使用) SELECT DEPT_CODE FROM DEPT ORDER BY DEPT_TYPE

高效: (使用索引) SELECT DEPT_CODE FROM DEPT WHERE DEPT_TYPE > 0

  1. 避免索引列的类型转换:

假设 EMP_TYPE 是一个字符类型的索引列. SELECT … FROM EMP WHERE EMP_TYPE = 123 这个语句被转换为: SELECT … FROM EMP WHERE EMP_TYPE='123'; 因为内部发生的类型转换, 这个索引将不会被用到! 为了避免 ORACLE 对你的 SQL 进行隐式的类型转换, 最好把类型转换用显式表现出来. 注意当字符和数值比较时, ORACLE 会优先转换数值类型到字符类型。

  1. 优化 Group by

提高GROUP BY 语句的效率, 可以通过将不需要的记录在GROUP BY 之前过滤掉。下面两个查询返回相同结果但第二个明显就快了许多。

低效: SELECT JOB , AVG(SAL) FROM EMP GROUP by JOB HAVING JOB = ‘PRESIDENT' OR JOB = ‘MANAGER'

高效: SELECT JOB , AVG(SAL) FROM EMP WHERE JOB = ‘PRESIDENT' OR JOB = ‘MANAGER' GROUP by JOB

  1. 避免使用耗费资源的操作:

带有DISTINCT,UNION,MINUS,INTERSECT,ORDER BY的 SQL 语句会启动 SQL 引擎执行耗费资源的排序(SORT)功能. DISTINCT需要一次排序操作, 而其他的至少需要执行两次排序. 通常, 带有UNION, MINUS , INTERSECT的 SQL 语句都可以用其他方式重写. 如果你的数据库的 SORT_AREA_SIZE 调配得好, 使用 UNION , MINUS, INTERSECT 也是可以考虑的, 毕竟它们的可读性很强。

  1. 在运行代码中,尽量使用PreparedStatement来查询,不要用Statement

MySQL 常用的索引优化方法

MySQL 数据库的优化目标、常见误区和基本原则

优化目标

  • MySQL 数据库是常见的两个瓶颈是 CPU 和 I/O 的瓶颈,CPU 在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候。磁盘 I/O 瓶颈发生在装入数据远大于内存容量的时候。

    • 减少 I/O 次数:I/O 永远是数据库最容易瓶颈的地方,这是由数据库的职责所决定的,大部分数据库操作中超过 90%的时间都是 IO 操作所占用的,减少 IO 次数是 SQL 优化中需要第一优先考虑,当然,也是收效最明显的优化手段。

    • 降低 CPU 计算:除了 IO 瓶颈之外,SQL 优化中需要考虑的就是 CPU 运算量的优化了。order by, group by,distinct … 都是消耗 CPU 的大户(这些操作基本上都是 CPU 处理内存中的数据比较运算)。当我们的 IO 优化做到一定阶段之后,降低 CPU 计算也就成为了我们 SQL 优化的重要目标。

常见误区

  1. count(1)count(primary_key) 优于 count(*):

很多人为了统计记录条数,就使用 count(1)count(primary_key) 而不是 count(*),他们认为这样性能更好,其实这是一个误区。对于有些场景,这样做可能性能会更差,应为数据库对 count(*) 计数操作做了一些特别的优化。如在 MyISAM 引擎中,会对表的总行数进行记录,使用count(*)可以直接取出该值。

  1. count(column)count(*) 是一样的

实际上,count(column)count(*) 是一个完全不一样的操作,所代表的意义也完全不一样。count(column) 是表示结果集中有多少个column字段不为空的记录,只处理非空值。count(*) 是表示整个结果集有多少条记录,不会跳过null值。

  1. select a,b from …select a,b,c from …可以让数据库访问更少的数据量

    实际上,大多数关系型数据库都是按照行(row)的方式存储,而数据存取操作都是以一个固定大小的 IO 单元(被称作 block 或者 page)为单位,一般为 4KB,8KB… 大多数时候,每个 IO 单元中存储了多行,每行都是存储了该行的所有字段(lob 等特殊类型字段除外)。所以,我们是取一个字段还是多个字段,实际上数据库在表中需要访问的数据量其实是一样的。

当然,也有例外情况,那就是我们的这个查询在索引中就可以完成,也就是说当只取 a,b 两个字段的时候,不需要回表,而 c 这个字段不在使用的索引中,需要回表取得其数据。在这样的情况下,二者的 IO 量会有较大差异。

  1. order by 一定需要排序操作

    我们知道索引数据实际上是有序的,如果我们的需要的数据和某个索引的顺序一致,而且我们的查询又通过这个索引来执行,那么数据库一般会省略排序操作,而直接将数据返回,因为数据库知道数据已经满足我们的排序需求了。实际上,利用索引来优化有排序需求的 SQL,是一个非常重要的优化手段。

  2. 执行计划中有 filesort 就会进行磁盘文件排序

    有这个误区其实并不能怪我们,而是因为 MySQL 开发者在用词方面的问题。filesort 是我们在使用 explain 命令查看一条 SQL 的执行计划的时候可能会看到在 “Extra” 一列显示的信息。实际上,只要一条 SQL 语句需要进行排序操作,都会显示Using filesort,这并不表示就会有文件排序操作。

基本原则

  1. 尽量少 join

    MySQL 的优势在于简单,但这在某些方面其实也是其劣势。MySQL 优化器效率高,但是由于其统计信息的量有限,优化器工作过程出现偏差的可能性也就更多。对于复杂的多表 Join,一方面由于其优化器受限,再者在 Join 这方面所下的功夫还不够,所以性能表现离 Oracle 等关系型数据库前辈还是有一定距离。但如果是简单的单表查询,这一差距就会极小甚至在有些场景下要优于这些数据库前辈。

  2. 尽量少排序

    排序操作会消耗较多的 CPU 资源,所以减少排序可以在缓存命中率高等 IO 能力足够的场景下会较大影响 SQL 的响应时间。对于 MySQL 来说,减少排序有多种办法,比如:上面误区中提到的通过利用索引来排序的方式进行优化;减少参与排序的记录条数;非必要不对数据进行排序。

  3. 尽量避免 select *

    很多人看到这一点后觉得比较难理解,上面不是在误区中刚刚说 select 子句中字段的多少并不会影响到读取的数据吗? 是的,大多数时候并不会影响到 IO 量,但是当我们还存在 order by 操作的时候,select 子句中的字段多少会在很大程度上影响到我们的排序效率,此外,上面误区中不是也说了,只是大多数时候是不会影响到 IO 量,当我们的查询结果仅仅只需要在索引中就能找到的时候,还是会极大减少 IO 量的。

  4. 尽量用 join 代替子查询

    虽然 Join 性能并不佳,但是和 MySQL 的子查询比起来还是有非常大的性能优势。

  5. 尽量少 or

    当 where 子句中存在多个条件以“或”并存的时候,MySQL 的优化器并没有很好的解决其执行计划优化问题,再加上 MySQL 特有的 SQL 与 Storage 分层架构方式,造成了其性能比较低下,很多时候使用 union all 或者是union(必要的时候)的方式来代替or会得到更好的效果。

  6. 尽量用 union all 代替 union

    union 和 union all 的差异主要是前者需要将两个(或者多个)结果集合并后再进行唯一性过滤操作,这就会涉及到排序,增加大量的 CPU 运算,加大资源消耗及延迟。所以当我们可以确认不可能出现重复结果集或者不在乎重复结果集的时候,尽量使用 union all 而不是 union。

  7. 尽量早过滤

    这一优化策略其实最常见于索引的优化设计中(将过滤性更好的字段放得更靠前)。 在 SQL 编写中同样可以使用这一原则来优化一些 Join 的 SQL。比如我们在多个表进行分页数据查询的时候,我们最好是能够在一个表上先过滤好数据分好页,然后再用分好页的结果集与另外的表 Join,这样可以尽可能多的减少不必要的 IO 操作,大大节省 IO 操作所消耗的时间。

  8. 避免类型转换

    这里所说的“类型转换”是指 where 子句中出现 column 字段的类型和传入的参数类型不一致的时候发生的类型转换

  9. 优先优化高并发的 SQL,而不是执行频率低某些“大”SQL

    对于破坏性来说,高并发的 SQL 总是会比低频率的来得大,因为高并发的 SQL 一旦出现问题,甚至不会给我们任何喘息的机会就会将系统压跨。而对于一些虽然需要消耗大量 IO 而且响应很慢的 SQL,由于频率低,即使遇到,最多就是让整个系统响应慢一点,但至少可能撑一会儿,让我们有缓冲的机会。

  10. 从全局出发优化,而不是片面调整

SQL 优化不能是单独针对某一个进行,而应充分考虑系统中所有的 SQL,尤其是在通过调整索引优化 SQL 的执行计划的时候,千万不能顾此失彼,因小失大。

  1. 尽可能对每一条运行在数据库中的 SQL 进行 explain

优化 SQL,需要做到心中有数,知道 SQL 的执行计划才能判断是否有优化余地,才能判断是否存在执行计划问题。在对数据库中运行的 SQL 进行了一段时间的优化之后,很明显的问题 SQL 可能已经很少了,大多都需要去发掘,这时候就需要进行大量的 explain 操作收集执行计划,并判断是否需要进行优化。

MySQL 数据库的表结构优化

  • 由于 MySQL 数据库是基于行(Row)存储的数据库,而数据库操作 IO 的时候是以 page(block)的方式,也就是说,如果我们每条记录所占用的空间量减小,就会使每个 page 中可存放的数据行数增大,那么每次 IO 可访问的行数也就增多了。反过来说,处理相同行数的数据,需要访问的 page 就会减少,也就是 IO 操作次数降低,直接提升性能。此外,由于我们的内存是有限的,增加每个 page 中存放的数据行数,就等于增加每个内存块的缓存数据量,同时还会提升内存换中数据命中的几率,也就是缓存命中率。

数据类型选择

  1. 数据库操作中最为耗时的操作就是 IO 处理,大部分数据库操作 90% 以上的时间都花在了 IO 读写上面。所以尽可能减少 IO 读写量,可以在很大程度上提高数据库操作的性能。我们无法改变数据库中需要存储的数据,但是我们可以在这些数据的存储方式方面花一些心思。下面的这些关于字段类型的优化建议主要适用于记录条数较多,数据量较大的场景,因为精细化的数据类型设置可能带来维护成本的提高,过度优化也可能会带来其他的问题:

数字类型:非万不得已不要使用DOUBLE,不仅仅只是存储长度的问题,同时还会存在精确性的问题。同样,固定精度的小数,也不建议使用DECIMAL,建议乘以固定倍数转换成整数存储,可以大大节省存储空间,且不会带来任何附加维护成本。对于整数的存储,在数据量较大的情况下,建议区分开 TINYINT / INT / BIGINT 的选择,因为三者所占用的存储空间也有很大的差别,能确定不会使用负数的字段,建议添加 unsigned 定义。当然,如果数据量较小的数据库,也可以不用严格区分三个整数类型。

int类型只增主键字段=>4 字节=>每个字节 8 位=>32 位,在 CPU 加载一条指令的时候,4 字节是和 CPU 寄存器的运算有关,如:64 位,由于之前的系统一般都是 32 位的,所以在运算 4 字节的数据是刚好的,效率最高,而现今我们系统基本都是 64 位的时候,其实没有更好的利用好 CPU 运算,所以在设计表字段建议,使用 8 字节的主键bigint,而不是直接使用 int 来做主键。

字符类型:非万不得已不要使用 TEXT 数据类型,其处理方式决定了他的性能要低于 char 或者是 varchar 类型的处理。定长字段,建议使用 CHAR 类型,不定长字段尽量使用 VARCHAR,且仅仅设定适当的最大长度,而不是非常随意的给一个很大的最大长度限定,因为不同的长度范围,MySQL 也会有不一样的存储处理。 `char(10)`` 不管该字段是否存储数据,都占 10 个字符的存储空间,char(10) 同时存在一个坑,就是存储 abc 数据后改数据库字段的值为“abc 7 个空格 ”,在精准查询(where)就必须带上后面的 7 个空格。varchar 不存的时候不占空间,存多长数据就占多少空间。

时间类型:尽量使用TIMESTAMP类型,因为其存储空间只需要 DATETIME 类型的一半。对于只需要精确到某一天的数据类型,建议使用 DATE 类型,因为他的存储空间只需要 3 个字节,比 TIMESTAMP 还少。不建议通过 INT 类型类存储一个 unix timestamp 的值,因为这太不直观,会给维护带来不必要的麻烦,同时还不会带来任何好处。

ENUM & SET:对于状态字段,可以尝试使用 ENUM 来存放,因为可以极大的降低存储空间,而且即使需要增加新的类型,只要增加于末尾,修改结构也不需要重建表数据。如果是存放可预先定义的属性数据呢?可以尝试使用 SET 类型,即使存在多种属性,同样可以游刃有余,同时还可以节省不小的存储空间。

LOB类型:强烈反对在数据库中存放 LOB 类型数据,虽然数据库提供了这样的功能,但这不是他所擅长的,我们更应该让合适的工具做他擅长的事情,才能将其发挥到极致。在数据库中存储 LOB 数据就像让一个多年前在学校学过一点 Java 的营销专业人员来写 Java 代码一样。

字符编码:字符集直接决定了数据在 MySQL 中的存储编码方式,由于同样的内容使用不同字符集表示所占用的空间大小会有较大的差异,所以通过使用合适的字符集,可以帮助我们尽可能减少数据量,进而减少 IO 操作次数。 ① 纯拉丁字符能表示的内容,没必要选择 latin1 之外的其他字符编码,因为这会节省大量的存储空间; ② 如果我们可以确定不需要存放多种语言,就没必要非得使用 UTF8 或者其他 UNICODE 字符类型,这回造成大量的存储空间浪费; ③MySQL 的数据类型可以精确到字段,所以当我们需要大型数据库中存放多字节数据的时候,可以通过对不同表不同字段使用不同的数据类型来较大程度减小数据存储量,进而降低 IO 操作次数并提高缓存命中率。

适当拆分:有些时候,我们可能会希望将一个完整的对象对应于一张数据库表,这对于应用程序开发来说是很有好的,但是有些时候可能会在性能上带来较大的问题。当我们的表中存在类似于 TEXT 或者是很大的 VARCHAR 类型的大字段的时候,如果我们大部分访问这张表的时候都不需要这个字段,我们就该义无反顾的将其拆分到另外的独立表中,以减少常用数据所占用的存储空间。这样做的一个明显好处就是每个数据块中可以存储的数据条数可以大大增加,既减少物理 IO 次数,也能大大提高内存中的缓存命中率。

  1. 上面几点的优化都是为了减少每条记录的存储空间大小,让每个数据库中能够存储更多的记录条数,以达到减少 IO 操作次数,提高缓存命中率。下面这个优化建议可能很多开发人员都会觉得不太理解,因为这是典型的反范式设计,而且也和上面的几点优化建议的目标相违背。

适度冗余: 为什么我们要冗余?这不是增加了每条数据的大小,减少了每个数据块可存放记录条数吗?确实,这样做是会增大每条记录的大小,降低每条记录中可存放数据的条数,但是在有些场景下我们仍然还是不得不这样做: ① 被频繁引用且只能通过 Join 2 张(或者更多)大表的方式才能得到的独立小字段:这样的场景由于每次 Join 仅仅只是为了取得某个小字段的值,Join 到的记录又大,会造成大量不必要的 IO,完全可以通过空间换取时间的方式来优化。不过,冗余的同时需要确保数据的一致性不会遭到破坏,确保更新的同时冗余字段也被更新。

尽量使用 NOT NULL: NULL 类型比较特殊,SQL 难优化。虽然 MySQL NULL 类型和 Oracle 的 NULL 有差异,会进入索引中,但如果是一个组合索引,那么这个 NULL 类型的字段会极大影响整个索引的效率。很多人觉得 NULL 会节省一些空间,所以尽量让 NULL 来达到节省 IO 的目的,但是大部分时候这会适得其反,虽然空间上可能确实有一定节省,倒是带来了很多其他的优化问题,不但没有将 IO 量省下来,反而加大了 SQL 的 IO 量。所以尽量确保 DEFAULT 值不是 NULL,也是一个很好的表结构设计优化习惯。

MySQL 数据库的缓存参数优化

  • 用处不大,忽略

总结

  • 数据库最常用的优化方式有:SQL 语句和索引、数据库表结构、系统配置、硬件。
  • 优化效果:SQL 语句和索引 > 数据库表结构 > 系统配置 > 硬件,但成本从低到高。
  • 数据库的优化方法小结:
    1. 设计符合范式的数据库
    2. 选择合适的存储引擎
    3. SQL 语句优化
    4. 索引优化:高分离字段建立索引
    5. SQL 表结构、字段优化
    6. 数据库参数优化:IO 参数、CPU 参数
    7. 分库分表:垂直切分与水平切分
    8. 分区:将表的数据按照特定的规则放在不同的分区,提高磁盘的 IO 效率,提高数据库的性能
    9. 主从复制与读写分离:三个主要线程与 bin-log 文件、relay_log 文件,主数据库负责写操作,从数据库负责读操作
    10. 负载均衡
    11. 数据库集群
    12. 硬件
  • 更多文章欢迎关注微信公众号【码猿技术专栏】

![](https://gitee.com/chenjiabing666/Blog-file/raw/master/%E4%B8%83%E7%A7%92%E7%BC%96%E7%A8%8B.jpg

本文使用 mdnice 排版

前言

  • 前几天有读者找到我,说想要一套全面的Mysql面试题,今天陈某特地为她写了一篇。
  • 由于篇幅较长,陈某已经将此文章转换为PDF,公众号回复关键词Mysql面试题即可获取。

Mysql

什么是SQL?

  • 结构化查询语言(Structured Query Language)简称SQL,是一种数据库查询语言。
  • 作用:用于存取数据、查询、更新和管理关系数据库系统。

什么是MySQL?

  • MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。在Java企业级开发中非常常用,因为 MySQL 是开源免费的,并且方便扩展。

数据库三大范式是什么?

  • 第一范式:每个列都不可以再拆分。
  • 第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
  • 第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
  • 在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。

mysql有关权限的表都有哪几个?

  • MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库里,由mysql_install_db脚本初始化。这些权限表分别user,db,table_priv,columns_priv和host。下面分别介绍一下这些表的结构和内容:
    1. user权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的。
    2. db权限表:记录各个帐号在各个数据库上的操作权限。
    3. table_priv权限表:记录数据表级的操作权限。
    4. columns_priv权限表:记录数据列级的操作权限。
    5. host权限表:配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。

MySQL的binlog有有几种录入格式?分别有什么区别?

  • 有三种格式,statement,row和mixed。
    • statement模式下,每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。
    • row级别下,不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。
    • mixed,一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。
  • 此外,新版的MySQL中对row级别也做了一些优化,当表结构发生变化的时候,会记录语句而不是逐行记录。

mysql有哪些数据类型?

  • 1、整数类型,包括TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT,分别表示1字节、2字节、3字节、4字节、8字节整数。任何整数类型都可以加上UNSIGNED属性,表示数据是无符号的,即非负整数。
    • 长度:整数类型可以被指定长度,例如:INT(11)表示长度为11的INT类型。长度在大多数场景是没有意义的,它不会限制值的合法范围,只会影响显示字符的个数,而且需要和UNSIGNED ZEROFILL属性配合使用才有意义。
    • 例子:假定类型设定为INT(5),属性为UNSIGNED ZEROFILL,如果用户插入的数据为12的话,那么数据库实际存储数据为00012。
  • 2、实数类型,包括FLOAT、DOUBLE、DECIMAL。DECIMAL可以用于存储比BIGINT还大的整型,能存储精确的小数。而FLOAT和DOUBLE是有取值范围的,并支持使用标准的浮点进行近似计算。计算时FLOAT和DOUBLE相比DECIMAL效率更高一些,DECIMAL你可以理解成是用字符串进行处理。
  • 3、字符串类型,包括VARCHAR、CHAR、TEXT、BLOB
    • VARCHAR用于存储可变长字符串,它比定长类型更节省空间。
    • VARCHAR使用额外1或2个字节存储字符串长度。列长度小于255字节时,使用1字节表示,否则使用2字节表示。
    • VARCHAR存储的内容超出设置的长度时,内容会被截断。
    • CHAR是定长的,根据定义的字符串长度分配足够的空间。
    • CHAR会根据需要使用空格进行填充方便比较。
    • CHAR适合存储很短的字符串,或者所有值都接近同一个长度。
    • CHAR存储的内容超出设置的长度时,内容同样会被截断。
  • 4、枚举类型(ENUM),把不重复的数据存储为一个预定义的集合。
    • 有时可以使用ENUM代替常用的字符串类型。
    • ENUM存储非常紧凑,会把列表值压缩到一个或两个字节。
    • ENUM在内部存储时,其实存的是整数。
    • 尽量避免使用数字作为ENUM枚举的常量,因为容易混乱。
    • 排序是按照内部存储的整数
  • 5、日期和时间类型,尽量使用timestamp,空间效率高于datetime,
    • 用整数保存时间戳通常不方便处理。
    • 如果需要存储微妙,可以使用bigint存储。
    • 看到这里,这道真题是不是就比较容易回答了。

MyISAM索引与InnoDB索引的区别?

  • InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
  • InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
  • MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
  • InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。

InnoDB引擎的4大特性

  • 插入缓冲(insert buffer)
  • 二次写(double write)
  • 自适应哈希索引(ahi)
  • 预读(read ahead)

什么是索引?

  • 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
  • 索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
  • 更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。

索引有哪些优缺点?

  • 索引的优点:
    • 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
    • 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
  • 索引的缺点:
    • 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
    • 空间方面:索引需要占物理空间。

索引有哪几种类型?

  • 主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。
  • 唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
    • 可以通过 ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引
    • 可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引
  • 普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。
    • 可以通过ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引
    • 可以通过ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引。
  • 全文索引: 是目前搜索引擎使用的一种关键技术。
    • 可以通过ALTER TABLE table_name ADD FULLTEXT (column);创建全文索引

索引的数据结构(b树,hash)

  • 索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引,B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。

1. B树索引

  • mysql通过存储引擎取数据,基本上90%的人用的就是InnoDB了,按照实现方式分,InnoDB的索引类型目前只有两种:BTREE(B树)索引和HASH索引。B树索引是Mysql数据库中使用最频繁的索引类型,基本所有存储引擎都支持BTree索引。通常我们说的索引不出意外指的就是(B树)索引(实际是用B+树实现的,因为在查看表索引时,mysql一律打印BTREE,所以简称为B树索引)
    B树索引

2. B+tree性质

  • n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。
  • 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  • 所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
  • B+ 树中,数据对象的插入和删除仅在叶节点上进行。
  • B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。

3. 哈希索引

  • 简要说下,类似于数据结构中简单实现的HASH表(散列表)一样,当我们在mysql中用哈希索引时,主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储。当然这只是简略模拟图。
    哈希索引

索引的基本原理

  • 索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
  • 索引的原理很简单,就是把无序的数据变成有序的查询
    1. 把创建了索引的列的内容进行排序
    2. 对排序结果生成倒排表
    3. 在倒排表内容上拼上数据地址链
    4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据

索引算法有哪些?

  • 索引算法有 BTree算法和Hash算法

1. BTree算法

  • BTree是最常用的mysql数据库索引算法,也是mysql默认的算法。因为它不仅可以被用在=,>,>=,<,<=和between这些比较操作符上,而且还可以用于like操作符,只要它的查询条件是一个不以通配符开头的常量。

2. Hash算法

  • Hash Hash索引只能用于对等比较,例如=,<=>(相当于=)操作符。由于是一次定位数据,不像BTree索引需要从根节点到枝节点,最后才能访问到页节点这样多次IO访问,所以检索效率远高于BTree索引。

索引设计的原则?

  • 适合索引的列是出现在where子句中的列,或者连接子句中指定的列。
  • 基数较小的类,索引效果较差,没有必要在此列建立索引
  • 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
  • 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

创建索引的原则

  • 索引虽好,但也不是无限制的使用,最好符合一下几个原则
    • 最左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
    • 较频繁作为查询条件的字段才去创建索引
    • 更新频繁字段不适合创建索引
    • 若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
    • 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
    • 定义有外键的数据列一定要建立索引。
    • 对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。
    • 对于定义为text、image和bit的数据类型的列不要建立索引。

创建索引时需要注意什么?

  • 非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值;
  • 取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;
  • 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。

使用索引查询一定能提高查询的性能吗?

  • 通常,通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。
  • 索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况:
  • 基于一个范围的检索,一般查询返回结果集小于表中记录数的30%
  • 基于非唯一性索引的检索

百万级别或以上的数据如何删除?

  • 关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。
    1. 所以我们想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟)
    2. 然后删除其中无用数据(此过程需要不到两分钟)
    3. 删除完成后重新创建索引(此时数据较少了)创建索引也非常快,约十分钟左右。
    4. 与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚。那更是坑了。

什么是最左前缀原则?什么是最左匹配原则?

  • 顾名思义,就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。
  • 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
  • =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式

B树和B+树的区别

  • 在B树中,你可以将键和值存放在内部节点和叶子节点;但在B+树中,内部节点都是键,没有值,叶子节点同时存放键和值。
  • B+树的叶子节点有一条链相连,而B树的叶子节点各自独立。
    B树和B+树的区别

使用B树的好处

  • B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。

使用B+树的好处

  • 由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间

什么是聚簇索引?何时使用聚簇索引与非聚簇索引?

  • 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
  • 非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因。

非聚簇索引一定会回表查询吗?

  • 不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。
  • 举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select age from employee where age < 20的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询。

联合索引是什么?为什么需要注意联合索引中的顺序?

  • MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。
  • MySQL使用索引时需要索引有序,假设现在建立了”name,age,school”的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。
  • 当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。

什么是数据库事务?

  • 事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。

事物的四大特性(ACID)介绍一下?

  • 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
  • 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  • 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

什么是脏读?幻读?不可重复读?

  • 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
  • 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
  • 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

什么是事务的隔离级别?MySQL的默认隔离级别是什么?

  • 为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。
  • SQL 标准定义了四个隔离级别:
    • READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
    • READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
    • REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
    • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
  • Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别

隔离级别与锁的关系

  • 在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突
  • 在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;
  • 在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
  • SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。

按照锁的粒度分数据库锁有哪些?

  • 行级锁:行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 表级锁: 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
  • 页级锁:页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。

从锁的类别上分MySQL都有哪些锁呢?

  • 从锁的类别上来讲,有共享锁和排他锁。
    • 共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
    • 排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。

InnoDB存储引擎的锁的算法有哪三种?

  • Record lock:单个行记录上的锁
  • Gap lock:间隙锁,锁定一个范围,不包括记录本身
  • Next-key lock:record+gap 锁定一个范围,包含记录本身

什么是死锁?怎么解决?

  • 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
  • 常见的解决死锁的方法
    1. 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
    2. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
    3. 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
  • 如果业务处理不好可以用分布式事务锁或者使用乐观锁

数据库的乐观锁和悲观锁是什么?怎么实现的?

  • 数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。
  • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制
  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。

大表数据查询,怎么优化?

  • 优化shema、sql语句+索引;
  • 第二加缓存,memcached, redis;
  • 主从复制,读写分离;
  • 垂直拆分,根据你模块的耦合度,将一个大的系统分为多个小的系统,也就是分布式系统
  • 水平切分,针对数据量大的表,这一步最麻烦,最能考验技术水平,要选择一个合理的sharding key, 为了有好的查询效率,表结构也要改动,做一定的冗余,应用也要改,sql中尽量带sharding key,将数据定位到限定的表上去查,而不是扫描全部的表

超大分页怎么处理?

  • 超大的分页一般从两个方向上来解决:
    • 数据库层面,这也是我们主要集中关注的(虽然收效没那么大),类似于select * from table where age > 20 limit 1000000,10这种查询其实也是有可以优化的余地的. 这条语句需要load1000000数据然后基本上全部丢弃,只取10条当然比较慢. 当时我们可以修改为select * from table where id in (select id from table where age > 20 limit 1000000,10).这样虽然也load了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中,所以速度会很快. 同时如果ID连续的好,我们还可以select * from table where id > 1000000 limit 10,效率也是不错的,优化的可能性有许多种,但是核心思想都一样,就是减少load的数据
    • 从需求的角度减少这种请求…主要是不做类似的需求(直接跳转到几百万页之后的具体某一页.只允许逐页查看或者按照给定的路线走,这样可预测,可缓存)以及防止ID泄漏且连续被人恶意攻击

为什么要尽量设定一个主键?

  • 主键是数据库确保数据行在整张表唯一性的保障,即使业务上本张表没有主键,也建议添加一个自增长的ID列作为主键。设定了主键之后,在后续的删改查的时候可能更加快速以及确保操作数据范围安全。

主键使用自增ID还是UUID?

  • 推荐使用自增ID,不要使用UUID。
  • 因为在InnoDB存储引擎中,主键索引是作为聚簇索引存在的,也就是说,主键索引的B+树叶子节点上存储了主键索引以及全部的数据(按照顺序),如果主键索引是自增ID,那么只需要不断向后排列即可,如果是UUID,由于到来的ID与原来的大小不确定,会造成非常多的数据插入,数据移动,然后导致产生很多的内存碎片,进而造成插入性能的下降。
  • 总之,在数据量大一些的情况下,用自增主键性能会好一些。
  • 关于主键是聚簇索引,如果没有主键,InnoDB会选择一个唯一键来作为聚簇索引,如果没有唯一键,会生成一个隐式的主键。

字段为什么要求定义为not null?

  • null值会占用更多的字节,且会在程序中造成很多与预期不符的情况。

如果要存储用户的密码散列,应该使用什么字段进行存储?

  • 密码散列,盐,用户身份证号等固定长度的字符串应该使用char而不是varchar来存储,这样可以节省空间且提高检索效率。

数据库结构优化?

  • 一个好的数据库设计方案对于数据库的性能往往会起到事半功倍的效果。
  • 需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。
  • 将字段很多的表分解成多个表:对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。
  • 增加中间表:对于需要经常联合查询的表,可以建立中间表以提高查询效率。通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。
  • 增加冗余字段:设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。

MySQL数据库cpu飙升到500%的话他怎么处理?

  • 当 cpu 飙升到 500%时,先用操作系统命令 top 命令观察是不是 mysqld 占用导致的,如果不是,找出占用高的进程,并进行相关处理。
  • 如果是 mysqld 造成的, show processlist,看看里面跑的 session 情况,是不是有消耗资源的 sql 在运行。找出消耗高的 sql,看看执行计划是否准确, index 是否缺失,或者实在是数据量太大造成。
  • 一般来说,肯定要 kill 掉这些线程(同时观察 cpu 使用率是否下降),等进行相应的调整(比如说加索引、改 sql、改内存参数)之后,再重新跑这些 SQL。
  • 也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等。

主从复制的作用?

  • 主数据库出现问题,可以切换到从数据库。
  • 可以进行数据库层面的读写分离。
  • 可以在从数据库上进行日常备份。

MySQL主从复制解决的问题?

  • 数据分布:随意开始或停止复制,并在不同地理位置分布数据备份
  • 负载均衡:降低单个服务器的压力
  • 高可用和故障切换:帮助应用程序避免单点失败
  • 升级测试:可以用更高版本的MySQL作为从库

MySQL主从复制工作原理?

  • 在主库上把数据更高记录到二进制日志
  • 从库将主库的日志复制到自己的中继日志
  • 从库读取中继日志的事件,将其重放到从库数据中。

小福利

  • 由于文章篇幅较长,陈某将其转换为PDF文档,老规矩,回复关键词Mysql面试题即可获取。

巨人的肩膀

导读

  • 真是有人()的地方就有江湖(事务),今天不谈江湖,来撩撩人。
  • 分布式锁的概念、为什么使用分布式锁,想必大家已经很清楚了。前段时间作者写过Redis是如何实现分布式锁,今天这篇文章来谈谈Zookeeper是如何实现分布式锁的。
  • 陈某今天分别从如下几个方面来详细讲讲ZK如何实现分布式锁:
    1. ZK的四种节点
    2. 排它锁的实现
    3. 读写锁的实现
    4. Curator实现分步式锁

ZK的四种节点

  • 持久性节点:节点创建后将会一直存在
  • 临时节点:临时节点的生命周期和当前会话绑定,一旦当前会话断开临时节点也会删除,当然可以主动删除。
  • 持久有序节点:节点创建一直存在,并且zk会自动为节点加上一个自增的后缀作为新的节点名称。
  • 临时有序节点:保留临时节点的特性,并且zk会自动为节点加上一个自增的后缀作为新的节点名称。

排它锁的实现

  • 排他锁的实现相对简单一点,利用了zk的创建节点不能重名的特性。如下图:

  • 根据上图分析大致分为如下步骤:
    1. 尝试获取锁:创建临时节点,zk会保证只有一个客户端创建成功。
    2. 创建临时节点成功,获取锁成功,执行业务逻辑,业务执行完成后删除锁。
    3. 创建临时节点失败,阻塞等待。
    4. 监听删除事件,一旦临时节点删除了,表示互斥操作完成了,可以再次尝试获取锁。
    5. 递归:获取锁的过程是一个递归的操作,获取锁->监听->获取锁
  • 如何避免死锁:创建的是临时节点,当服务宕机会话关闭后临时节点将会被删除,锁自动释放。

代码实现

  • 作者参照JDK锁的实现方式加上模板方法模式的封装,封装接口如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @Description ZK分布式锁的接口
* @Author 陈某
* @Date 2020/4/7 22:52
*/
public interface ZKLock {
/**
* 获取锁
*/
void lock() throws Exception;

/**
* 解锁
*/
void unlock() throws Exception;
}
  • 模板抽象类如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* @Description 排他锁,模板类
* @Author 陈某
* @Date 2020/4/7 22:55
*/
public abstract class AbstractZKLockMutex implements ZKLock {

/**
* 节点路径
*/
protected String lockPath;

/**
* zk客户端
*/
protected CuratorFramework zkClient;

private AbstractZKLockMutex(){}

public AbstractZKLockMutex(String lockPath,CuratorFramework client){
this.lockPath=lockPath;
this.zkClient=client;
}

/**
* 模板方法,搭建的获取锁的框架,具体逻辑交于子类实现
* @throws Exception
*/
@Override
public final void lock() throws Exception {
//获取锁成功
if (tryLock()){
System.out.println(Thread.currentThread().getName()+"获取锁成功");
}else{ //获取锁失败
//阻塞一直等待
waitLock();
//递归,再次获取锁
lock();
}
}

/**
* 尝试获取锁,子类实现
*/
protected abstract boolean tryLock() ;


/**
* 等待获取锁,子类实现
*/
protected abstract void waitLock() throws Exception;


/**
* 解锁:删除节点或者直接断开连接
*/
@Override
public abstract void unlock() throws Exception;
}
  • 排他锁的具体实现类如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/**
* @Description 排他锁的实现类,继承模板类 AbstractZKLockMutex
* @Author 陈某
* @Date 2020/4/7 23:23
*/
@Data
public class ZKLockMutex extends AbstractZKLockMutex {

/**
* 用于实现线程阻塞
*/
private CountDownLatch countDownLatch;

public ZKLockMutex(String lockPath,CuratorFramework zkClient){
super(lockPath,zkClient);
}

/**
* 尝试获取锁:直接创建一个临时节点,如果这个节点存在创建失败抛出异常,表示已经互斥了,
* 反之创建成功
* @throws Exception
*/
@Override
protected boolean tryLock() {
try {
zkClient.create()
//临时节点
.withMode(CreateMode.EPHEMERAL)
//权限列表 world:anyone:crdwa
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath(lockPath,"lock".getBytes());
return true;
}catch (Exception ex){
return false;
}
}


/**
* 等待锁,一直阻塞监听
* @return 成功获取锁返回true,反之返回false
*/
@Override
protected void waitLock() throws Exception {
//监听节点的新增、更新、删除
final NodeCache nodeCache = new NodeCache(zkClient, lockPath);
//启动监听
nodeCache.start();
ListenerContainer<NodeCacheListener> listenable = nodeCache.getListenable();

//监听器
NodeCacheListener listener=()-> {
//节点被删除,此时获取锁
if (nodeCache.getCurrentData() == null) {
//countDownLatch不为null,表示节点存在,此时监听到节点删除了,因此-1
if (countDownLatch != null)
countDownLatch.countDown();
}
};
//添加监听器
listenable.addListener(listener);

//判断节点是否存在
Stat stat = zkClient.checkExists().forPath(lockPath);
//节点存在
if (stat!=null){
countDownLatch=new CountDownLatch(1);
//阻塞主线程,监听
countDownLatch.await();
}
//移除监听器
listenable.removeListener(listener);
}

/**
* 解锁,直接删除节点
* @throws Exception
*/
@Override
public void unlock() throws Exception {
zkClient.delete().forPath(lockPath);
}
}

可重入性排他锁如何设计

  • 可重入的逻辑很简单,在本地保存一个ConcurrentMapkey是当前线程,value是定义的数据,结构如下:
1
private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
  • 重入的伪代码如下:
1
2
3
4
5
6
public boolean tryLock(){
//判断当前线程是否在threadData保存过
//存在,直接return true
//不存在执行获取锁的逻辑
//获取成功保存在threadData中
}

读写锁的实现

  • 读写锁分为读锁和写锁,区别如下:
    • 读锁允许多个线程同时读数据,但是在读的同时不允许写线程修改。
    • 写锁在获取后,不允许多个线程同时写或者读。
  • 如何实现读写锁?ZK中有一类节点叫临时有序节点,上文有介绍。下面我们来利用临时有序节点来实现读写锁的功能。

读锁的设计

  • 读锁允许多个线程同时进行读,并且在读的同时不允许线程进行写操作,实现原理如下图:

  • 根据上图,获取一个读锁分为以下步骤:
    1. 创建临时有序节点(当前线程拥有的读锁或称作读节点)。
    2. 获取路径下所有的子节点,并进行从小到大排序
    3. 获取当前节点前的临近写节点(写锁)。
    4. 如果不存在的临近写节点,则成功获取读锁。
    5. 如果存在临近写节点,对其监听删除事件。
    6. 一旦监听到删除事件,**重复2,3,4,5的步骤(递归)**。

写锁的设计

  • 线程一旦获取了写锁,不允许其他线程读和写。实现原理如下:

  • 从上图可以看出唯一和写锁不同的就是监听的节点,这里是监听临近节点(读节点或者写节点),读锁只需要监听写节点,步骤如下:
    1. 创建临时有序节点(当前线程拥有的写锁或称作写节点)。
    2. 获取路径下的所有子节点,并进行从小到大排序。
    3. 获取当前节点的临近节点(读节点和写节点)。
    4. 如果不存在临近节点,则成功获取锁。
    5. 如果存在临近节点,对其进行监听删除事件。
    6. 一旦监听到删除事件,**重复2,3,4,5的步骤(递归)**。

如何监听

  • 无论是写锁还是读锁都需要监听前面的节点,不同的是读锁只监听临近的写节点,写锁是监听临近的所有节点,抽象出来看其实是一种链式的监听,如下图:

  • 每一个节点都在监听前面的临近节点,一旦前面一个节点删除了,再从新排序后监听前面的节点,这样递归下去。

代码实现

  • 作者简单的写了读写锁的实现,先造出来再优化,不建议用在生产环境。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
public class ZKLockRW  {

/**
* 节点路径
*/
protected String lockPath;

/**
* zk客户端
*/
protected CuratorFramework zkClient;

/**
* 用于阻塞线程
*/
private CountDownLatch countDownLatch=new CountDownLatch(1);


private final static String WRITE_NAME="_W_LOCK";

private final static String READ_NAME="_R_LOCK";


public ZKLockRW(String lockPath, CuratorFramework client) {
this.lockPath=lockPath;
this.zkClient=client;
}

/**
* 获取锁,如果获取失败一直阻塞
* @throws Exception
*/
public void lock() throws Exception {
//创建节点
String node = createNode();
//阻塞等待获取锁
tryLock(node);
countDownLatch.await();
}

/**
* 创建临时有序节点
* @return
* @throws Exception
*/
private String createNode() throws Exception {
//创建临时有序节点
return zkClient.create()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath(lockPath);
}

/**
* 获取写锁
* @return
*/
public ZKLockRW writeLock(){
return new ZKLockRW(lockPath+WRITE_NAME,zkClient);
}

/**
* 获取读锁
* @return
*/
public ZKLockRW readLock(){
return new ZKLockRW(lockPath+READ_NAME,zkClient);
}

private void tryLock(String nodePath) throws Exception {
//获取所有的子节点
List<String> childPaths = zkClient.getChildren()
.forPath("/")
.stream().sorted().map(o->"/"+o).collect(Collectors.toList());


//第一个节点就是当前的锁,直接获取锁。递归结束的条件
if (nodePath.equals(childPaths.get(0))){
countDownLatch.countDown();
return;
}

//1. 读锁:监听最前面的写锁,写锁释放了,自然能够读了
if (nodePath.contains(READ_NAME)){
//查找临近的写锁
String preNode = getNearWriteNode(childPaths, childPaths.indexOf(nodePath));
if (preNode==null){
countDownLatch.countDown();
return;
}
NodeCache nodeCache=new NodeCache(zkClient,preNode);
nodeCache.start();
ListenerContainer<NodeCacheListener> listenable = nodeCache.getListenable();
listenable.addListener(() -> {
//节点删除事件
if (nodeCache.getCurrentData()==null){
//继续监听前一个节点
String nearWriteNode = getNearWriteNode(childPaths, childPaths.indexOf(preNode));
if (nearWriteNode==null){
countDownLatch.countDown();
return;
}
tryLock(nearWriteNode);
}
});
}

//如果是写锁,前面无论是什么锁都不能读,直接循环监听上一个节点即可,直到前面无锁
if (nodePath.contains(WRITE_NAME)){
String preNode = childPaths.get(childPaths.indexOf(nodePath) - 1);
NodeCache nodeCache=new NodeCache(zkClient,preNode);
nodeCache.start();
ListenerContainer<NodeCacheListener> listenable = nodeCache.getListenable();
listenable.addListener(() -> {
//节点删除事件
if (nodeCache.getCurrentData()==null){
//继续监听前一个节点
tryLock(childPaths.get(childPaths.indexOf(preNode) - 1<0?0:childPaths.indexOf(preNode) - 1));
}
});
}
}

/**
* 查找临近的写节点
* @param childPath 全部的子节点
* @param index 右边界
* @return
*/
private String getNearWriteNode(List<String> childPath,Integer index){
for (int i = 0; i < index; i++) {
String node = childPath.get(i);
if (node.contains(WRITE_NAME))
return node;

}
return null;
}

}

Curator实现分步式锁

  • Curator是Netflix公司开源的一个Zookeeper客户端,与Zookeeper提供的原生客户端相比,Curator的抽象层次更高,简化了Zookeeper客户端的开发量。
  • Curator在分布式锁方面已经为我们封装好了,大致实现的思路就是按照作者上述的思路实现的。中小型互联网公司还是建议直接使用框架封装好的,毕竟稳定,有些大型的互联公司都是手写的,牛逼啊。
  • 创建一个排他锁很简单,如下:
1
2
3
4
5
6
//arg1:CuratorFramework连接对象,arg2:节点路径
lock=new InterProcessMutex(client,path);
//获取锁
lock.acquire();
//释放锁
lock.release();
  • 更多的API请参照官方文档,不是此篇文章重点。

  • 至此ZK实现分布式锁就介绍完了,如有想要源码的朋友,老规矩,回复关键词分布式锁获取。

一点小福利

  • 对于Zookeeper不太熟悉的朋友,陈某特地花费两天时间总结了ZK的常用知识点,包括ZK常用shell命令、ZK权限控制、Curator的基本操作API。目录如下:
  • 需要上面PDF文件的朋友,老规矩,回复关键词ZK总结

导读

  • Zookeeper 相信大家都听说过,最典型的使用就是作为服务注册中心。今天陈某带大家从零基础入门 Zookeeper,看了本文,你将会对 Zookeeper 有了初步的了解和认识。
  • 注意:本文基于 Zookeeper 的版本是 3.4.14,最新版本的在使用上会有一些出入,但是企业现在使用的大部分都是 3.4x 版本的。

Zookeeper 概述

  • Zookeeper 是一个分布式协调服务的开源框架。主要用来解决分布式集群中应用系统的一致性问题,例如怎样避免同时操作同一数据造成脏读的问题。
  • ZooKeeper 本质上是一个分布式的小文件存储系统。提供基于类似于文件系 统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。从而用来维护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达 到基于数据的集群管理。诸如:统一命名服务分布式配置管理分布式消息队列分布式锁分布式协调等功能。

Zookeeper 特性

  1. 全局数据一致:每个 server 保存一份相同的数据副本,client 无论连 接到哪个 server,展示的数据都是一致的,这是最重要的特征;

  2. 可靠性:如果消息被其中一台服务器接受,那么将被所有的服务器接受。

  3. 顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上 消息 a 在消息 b 前发布,则在所有 Server 上消息 a 都将在消息 b 前被 发布;偏序是指如果一个消息 b 在消息 a 后被同一个发送者发布,a 必将排在 b 前面。

  4. 数据更新原子性:一次数据更新要么成功(半数以上节点成功),要么失 败,不存在中间状态;

  5. 实时性:Zookeeper 保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。

Zookeeper 节点类型

  • Znode 有两种,分别为临时节点和永久节点。
    • 临时节点:该节点的生命周期依赖于创建它们的会话。一旦会话结束,临时节点将被自动删除,当然可以也可以手动删除。临时节点不允许拥有子节点。
    • 永久节点:该节点的生命周期不依赖于会话,并且只有在客户端显示执行删除操作的时候,他们才能被删除。
  • 节点的类型在创建时即被确定,并且不能改变。
  • Znode 还有一个序列化的特性,如果创建的时候指定的话,该 Znode 的名字后面会自动追加一个不断增加的序列号。序列号对于此节点的父节点来说是唯一的,这样便会记录每个子节点创建的先后顺序。它的格式为"%10d"(10 位数字,没有数值的数位用 0 补充,例如“0000000001”)。
  • 这样便会存在四种类型的 Znode 节点,分类如下:
    • PERSISTENT:永久节点
    • EPHEMERAL:临时节点
    • PERSISTENT_SEQUENTIAL:永久节点、序列化
    • EPHEMERAL_SEQUENTIAL:临时节点、序列化

ZooKeeper Watcher

  • ZooKeeper 提供了分布式数据发布/订阅功能,一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使他们能够做出相应的处理。
  • 触发事件种类很多,如:节点创建,节点删除,节点改变,子节点改变等。
  • 总的来说可以概括 Watcher 为以下三个过程:客户端向服务端注册 Watcher、服务端事件发生触发 Watcher、客户端回调 Watcher 得到触发事件情况。

Watcher 机制特点

  • 一次性触发 :事件发生触发监听,一个 watcher event 就会被发送到设置监听的客户端,这种效果是一次性的,后续再次发生同样的事件,不会再次触发。

  • 事件封装 :ZooKeeper 使用 WatchedEvent 对象来封装服务端事件并传递。WatchedEvent 包含了每一个事件的三个基本属性: 通知状态(keeperState),事件类型(EventType)和节点路径(path)。

  • event 异步发送 :watcher 的通知事件从服务端发送到客户端是异步的。

  • 先注册再触发 :Zookeeper 中的 watch 机制,必须客户端先去服务端注册监听,这样事件发送才会触发监听,通知给客户端。

常用 Shell 命令

新增节点

1
create [-s] [-e] path data
  • -s:表示创建有序节点
  • -e:表示创建临时节点
  • 创建持久化节点:
1
2
3
4
create /test 1234

## 子节点
create /test/node1 node1
  • 创建持久化有序节点:
1
2
3
4
5
6
7
## 完整的节点名称是a0000000001
create /a a
Created /a0000000001

## 完整的节点名称是b0000000002
create /b b
Created /b0000000002
  • 创建临时节点:
1
create -e /a a
  • 创建临时有序节点:
1
2
3
## 完整的节点名称是a0000000001
create -e -s /a a
Created /a0000000001

更新节点

1
set [path] [data] [version]
  • path:节点路径
  • data:数据
  • version:版本号
  • 修改节点数据:
1
2
3
4
set /test aaa

## 修改子节点
set /test/node1 bbb
  • 基于数据版本号修改,如果修改的节点的版本号(dataVersion)不正确,拒绝修改
1
set /test aaa 1

删除节点

1
delete [path] [version]
  • path:节点路径
  • version:版本号,版本号不正确拒绝删除
  • 删除节点
1
2
3
4
delete /test

## 版本号删除
delete /test 2
  • 递归删除,删除某个节点及后代
1
rmr /test

查看节点数据和状态

  • 命令格式如下:
1
get path
  • 获取节点详情:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## 获取节点详情
get /node1

## 节点内容
aaa
cZxid = 0x6
ctime = Sun Apr 05 14:50:10 CST 2020
mZxid = 0x6
mtime = Sun Apr 05 14:50:10 CST 2020
pZxid = 0x7
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 1
  • 节点各个属性对应的含义如下:
    • cZxid:数据节点创建时的事务 ID。
    • ctime:数据节点创建时间。
    • mZxid:数据节点最后一次更新时的事务 ID。
    • mtime:数据节点最后一次更新的时间。
    • pZxid:数据节点的子节点最后一次被修改时的事务 ID。
    • cversion:子节点的更改次数。
    • dataVersion:节点数据的更改次数。
    • aclVersion :节点 ACL 的更改次数。
    • ephemeralOwner:如果节点是临时节点,则表示创建该节点的会话的 SessionID。如果节点是持久化节点,值为 0。
    • dataLength :节点数据内容的长度。
    • numChildren:数据节点当前的子节点的个数。

查看节点状态

1
stat path
  • stat命令和get命令相似,不过这个命令不会返回节点的数据,只返回节点的状态属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
stat /node1

## 节点状态信息,没有节点数据
cZxid = 0x6
ctime = Sun Apr 05 14:50:10 CST 2020
mZxid = 0x6
mtime = Sun Apr 05 14:50:10 CST 2020
pZxid = 0x7
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 1

查看节点列表

  • 查看节点列表有ls pathls2 path两个命令。后者是前者的增强,不仅会返回节点列表还会返回当前节点的状态信息。
  • ls path
1
2
3
4
ls /

## 仅仅返回节点列表
[zookeeper, node1]
  • ls2 path
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ls2 /

## 返回节点列表和当前节点的状态信息
[zookeeper, node1]
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x6
cversion = 2
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 2

监听器 get path watch

  • 使用get path watch注册的监听器在节点内容发生改变时,向客户端发送通知,注意 Zookeeper 的触发器是一次性的,触发一次后会立即生效。
1
2
3
4
5
6
7
8
get /node1 watch

## 改变节点数据
set /node1 bbb

## 监听到节点内容改变了
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/node1

监听器 stat path watch

  • stat path watch注册的监听器能够在节点状态发生改变时向客户端发出通知。比如节点数据改变、节点被删除等。
1
2
3
4
5
6
7
8
stat /node2 watch

## 删除节点node2
delete /node2

## 监听器监听到了节点删除
WATCHER::
WatchedEvent state:SyncConnected type:NodeDeleted path:/node2

监听器 ls/ls2 path watch

  • 使用ls path watch或者ls2 path watch注册的监听器,能够监听到该节点下的子节点的增加删除操作。
1
2
3
4
5
6
7
8
ls /node1 watch

## 创建子节点
create /node1/b b

## 监听到了子节点的新增
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/node1

Zookeeper 的 ACL 权限控制

  • zookeeper 类似文件控制系统,client 可以创建,删除,修改,查看节点,那么如何做到权限控制的呢?zookeeper 的access control list 访问控制列表可以做到这一点。
  • ACL 权限控制,使用scheme:id:permission来标识。
    • 权限模式(scheme):授权的策略
    • 授权对象(id):授权的对象
    • 权限(permission):授予的权限
  • 权限控制是基于每个节点的,需要对每个节点设置权限。
  • 每个节点支持设置多种权限控制方案和多个权限。
  • 子节点不会继承父节点的权限,客户端无权访问某节点,但可能可以访问它的子节点。
  • 例如:根据 IP 地址进行授权,命令如下:
1
setACl /node1 ip:192.168.10.1:crdwa

权限模式

  • 权限模式即是采用何种方式授权。
  • world:只有一个用户,anyone,表示登录 zookeeper 所有人(默认的模式)。
  • ip:对客户端使用 IP 地址认证。
  • auth:使用已添加认证的用户认证。
  • digest:使用用户名:密码方式认证。

授权对象

  • 给谁授权,授权对象的 ID 指的是权限赋予的实体,例如 IP 地址或用户。

授予的权限

  • 授予的权限包括createdeletereadwriteradmin。也就是增、删、改、查、管理的权限,简写cdrwa
  • 注意:以上 5 种权限中,delete是指对子节点的删除权限,其他 4 种权限是对自身节点的操作权限。
  • create:简写c,可以创建子节点。
  • delete:简写d,可以删除子节点(仅下一级节点)。
  • read:简写r,可以读取节点数据以及显示子节点列表。
  • write:简写w,可以更改节点数据。
  • admin:简写a,可以设置节点访问控制列表权限。

授权相关命令

  • getAcl [path]:读取指定节点的 ACL 权限。
  • setAcl [path] [acl]:设置 ACL
  • addauth <scheme> <auth>:添加认证用户,和 auth,digest 授权模式相关。

world 授权模式案例

  • zookeeper 中默认的授权模式,针对登录 zookeeper 的任何用户授予指定的权限。命令如下:
1
setAcl [path] world:anyone:[permission]
  • path:节点
  • permission:授予的权限,比如cdrwa
  • 去掉不能读取节点数据的权限:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
## 获取权限列表(默认的)
getAcl /node2

'world,'anyone
: cdrwa

## 去掉读取节点数据的的权限,去掉r
setAcl /node2 world:anyone:cdwa

## 再次获取权限列表
getAcl /node2

'world,'anyone
: cdwa

## 获取节点数据,没有权限,失败
get /node2

Authentication is not valid : /node2

IP 授权模式案例

  • 针对登录用户的 ip 进行限制权限。命令如下:
1
setAcl [path] ip:[ip]:[acl]
  • 远程登录 zookeeper 的命令如下:
1
./zkCli.sh -server ip
  • 设置192.168.10.1这个 ip 的增删改查管理的权限。
1
setAcl /node2 ip:192.168.10.1:crdwa

Auth 授权模式案例

  • auth 授权模式需要有一个认证用户,添加命令如下:
1
addauth digest [username]:[password]
  • 设置 auth 授权模式命令如下:
1
setAcl [path] auth:[user]:[acl]
  • chenmou这个账户添加 cdrwa 权限:
1
2
3
4
5
## 添加一个认证账户
addauth digest chenmou:123456

## 添加权限
setAcl /node2 auth:chenmou:crdwa

多种模式授权

  • zookeeper 中同一个节点可以使用多种授权模式,多种授权模式用,分隔。
1
2
3
4
5
6
7
8
## 创建节点
create /node3

## 添加认证用户
addauth chenmou:123456

## 添加多种授权模式
setAcl /node3 ip:192.178.10.1:crdwa,auth:chenmou:crdwa

ACL 超级管理员

  • zookeeper 的权限管理模式有一种叫做super,该模式提供一个超管可以方便的访问任何权限的节点。
  • 假设这个超管是super:admin,需要先为超管生成密码的密文:
1
2
3
4
echo -n super:admin | openssl dgst  -binary -sha1 |openssl base64

## 执行完生成了秘钥
xQJmxLMiHGwaqBvst5y6rkB6HQs=
  • 打开zookeeper目录下/bin/zkServer.sh,找到如下一行:
1
nohup JAVA&quot;−Dzookeeper.log.dir=JAVA"−Dzookeeper.log.dir={ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}"
  • 在后面添加一行脚本,如下:
1
"-Dzookeeper.DigestAuthenticationProvider.superDigest=super:xQJmxLMiHGwaqBvst5y6rkB6HQs="
  • 此时完整的脚本如下:
1
2
nohup "$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" "-Dzookeeper.DigestAuthenticationProvider.superDigest=super:xQJmxLMiHGwaqBvst5y6rkB6HQs=" \
-cp "$CLASSPATH" $JVMFLAGS $ZOOMAIN "$ZOOCFG" > "$_ZOO_DAEMON_OUT" 2>&1 < /dev/null &
  • 重启 zookeeper
  • 重启完成之后此时超管即配置完成,如果需要使用,则使用如下命令:
1
addauth digest super:admin

Curator 客户端

  • Curator 是 Netflix 公司开源的一个 Zookeeper 客户端,与 Zookeeper 提供的原生客户端相比,Curator 的抽象层次更高,简化了 Zookeeper 客户端的开发量。

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
</dependency>

建立连接

  • 客户端建立与 Zookeeper 的连接,这里仅仅演示单机版本的连接,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
//创建CuratorFramework,用来操作api
CuratorFramework client = CuratorFrameworkFactory.builder()
//ip地址+端口号,如果是集群,逗号分隔
.connectString("120.26.101.207:2181")
//会话超时时间
.sessionTimeoutMs(5000)
//超时重试策略,RetryOneTime:超时重连仅仅一次
.retryPolicy(new RetryOneTime(3000))
//命名空间,父节点,如果不指定是在根节点下
.namespace("node4")
.build();
//启动
client.start();

重连策略

  • 会话连接策略,即是当客户端与 Zookeeper 断开连接之后,客户端重新连接 Zookeeper 时使用的策略,比如重新连接一次。
  • RetryOneTime:N 秒后重连一次,仅仅一次,演示如下:
1
.retryPolicy(new RetryOneTime(3000))
  • RetryNTimes:每 n 秒重连一次,重连 m 次。演示如下:
1
2
//每三秒重连一次,重连3次。arg1:多长时间后重连,单位毫秒,arg2:总共重连几次
.retryPolicy(new RetryNTimes(3000,3))
  • RetryUntilElapsed:设置了最大等待时间,如果超过这个最大等待时间将会不再连接。
1
2
//每三秒重连一次,等待时间超过10秒不再重连。arg1:总等待时间,arg2:多长时间重连,单位毫秒
.retryPolicy(new RetryUntilElapsed(10000,3000))

新增节点

  • 新增节点
1
2
3
4
5
6
7
client.create()
//指定节点的类型。PERSISTENT:持久化节点,PERSISTENT_SEQUENTIAL:持久化有序节点,EPHEMERAL:临时节点,EPHEMERAL_SEQUENTIAL临时有序节点
.withMode(CreateMode.PERSISTENT)
//指定权限列表,OPEN_ACL_UNSAFE:world:anyone:crdwa
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
//写入节点数据,arg1:节点名称 arg2:节点数据
.forPath("/a", "a".getBytes());
  • 自定义权限列表:withACL(acls)方法中可以设置自定义的权限列表,代码如下:
1
2
3
4
5
6
7
8
9
10
11
//自定义权限列表
List<ACL> acls=new ArrayList<>();
//指定授权模式和授权对象 arg1:授权模式,arg2授权对象
Id id=new Id("ip","127.0.0.1");
//指定授予的权限,ZooDefs.Perms.ALL:crdwa
acls.add(new ACL(ZooDefs.Perms.ALL,id));
client.create()
.withMode(CreateMode.PERSISTENT)
//指定自定义权限列表
.withACL(acls)
.forPath("/b", "b".getBytes());
  • 递归创建节点:creatingParentsIfNeeded()方法对于创建多层节点,如果其中一个节点不存在的话会自动创建
1
2
3
4
5
6
7
8
//递归创建节点
client.create()
//递归方法,如果节点不存在,那么创建该节点
.creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
//test节点和b节点不存在,递归创建出来
.forPath("/test/a", "a".getBytes());
  • 异步创建节点:inBackground()方法可以异步回调创建节点,创建完成后会自动回调实现的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 //异步创建节点
client.create()
.withMode(CreateMode.PERSISTENT)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
//异步创建
.inBackground(new BackgroundCallback() {
/**
* @param curatorFramework 客户端对象
* @param curatorEvent 事件对象
*/
@Override
public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
//打印事件类型
System.out.println(curatorEvent.getType());
}
})
.forPath("/test1", "a".getBytes());

更新节点数据

  • 更新节点,当节点不存在会报错,代码如下:
1
2
client.setData()
.forPath("/a","a".getBytes());
  • 携带版本号更新节点,当版本错误拒绝更新
1
2
3
4
client.setData()
//指定版本号更新,如果版本号错误则拒绝更新
.withVersion(1)
.forPath("/a","a".getBytes());
  • 异步更新节点数据:
1
2
3
4
5
6
7
8
9
client.setData()
//异步更新
.inBackground(new BackgroundCallback() {
//回调方法
@Override
public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
}
})
.forPath("/a","a".getBytes());

删除节点

  • 删除当前节点,如果有子节点则拒绝删除
1
2
3
client.delete()
//删除节点,如果是该节点包含子节点,那么不能删除
.forPath("/a");
  • 指定版本号删除,如果版本错误则拒绝删除
1
2
3
4
5
client.delete()
//指定版本号删除
.withVersion(1)
//删除节点,如果是该节点包含子节点,那么不能删除
.forPath("/a");
  • 如果当前节点包含子节点则一并删除,使用deletingChildrenIfNeeded()方法
1
2
3
4
5
client.delete()
//如果删除的节点包含子节点则一起删除
.deletingChildrenIfNeeded()
//删除节点,如果是该节点包含子节点,那么不能删除
.forPath("/a");
  • 异步删除节点,使用inBackground()
1
2
3
4
5
6
7
8
9
10
11
client.delete()
.deletingChildrenIfNeeded()
//异步删除节点
.inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
//回调监听
}
})
//删除节点,如果是该节点包含子节点,那么不能删除
.forPath("/a");

获取节点数据

  • 同步获取节点数据
1
2
byte[] bytes = client.getData().forPath("/node1");
System.out.println(new String(bytes));
  • 获取节点状态和数据
1
2
3
4
5
6
7
8
9
//保存节点状态
Stat stat=new Stat();
byte[] bytes = client.getData()
//获取节点状态存储在stat对象中
.storingStatIn(stat)
.forPath("/node1");
System.out.println(new String(bytes));
//获取节点数据的长度
System.out.println(stat.getDataLength());
  • 异步获取节点数据
1
2
3
4
5
6
7
client.getData()
//异步获取节点数据,回调监听
.inBackground((curatorFramework, curatorEvent) -> {
//节点数据
System.out.println(new String(curatorEvent.getData()));
})
.forPath("/node1");

获取子节点

  • 同步获取全部子节点
1
2
3
4
List<String> strs = client.getChildren().forPath("/");
for (String str:strs) {
System.out.println(str);
}
  • 异步获取全部子节点
1
2
3
4
5
6
7
8
9
client.getChildren()
//异步获取
.inBackground((curatorFramework, curatorEvent) -> {
List<String> strs = curatorEvent.getChildren();
for (String str:strs) {
System.out.println(str);
}
})
.forPath("/");

查看节点是否存在

  • 同步查看
1
2
//如果节点不存在,stat为null
Stat stat = client.checkExists().forPath("/node");
  • 异步查看
1
2
3
4
5
6
7
//如果节点不存在,stat为null
client.checkExists()
.inBackground((curatorFramework, curatorEvent) -> {
//如果为null则不存在
System.out.println(curatorEvent.getStat());
})
.forPath("/node");

Watcher API

  • curator 提供了两种 watcher 来监听节点的变化
    • NodeCache:监听一个特定的节点,监听新增和修改
    • PathChildrenCache:监听一个节点的子节点,当一个子节点增加、删除、更新时,path Cache 会改变他的状态,会包含最新的子节点的数据和状态。
  • NodeCache 演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//arg1:连接对象 arg2:监听的节点路径,/namespace/path
final NodeCache nodeCache = new NodeCache(client, "/w1");
//启动监听
nodeCache.start();
//添加监听器
nodeCache.getListenable().addListener(() -> {
//节点路径
System.out.println(nodeCache.getCurrentData().getPath());
//节点数据
System.out.println(new String(nodeCache.getCurrentData().getData()));
});
//睡眠100秒
Thread.sleep(1000000);
//关闭监听
nodeCache.close();
  • PathChildrenCache演示:
1
2
3
4
5
6
7
8
9
10
11
12
//arg1:连接对象 arg2:节点路径  arg3:是否能够获取节点数据
PathChildrenCache cache=new PathChildrenCache(client,"/w1", true);
cache.start();
cache.getListenable().addListener((curatorFramework, pathChildrenCacheEvent) -> {
//节点路径
System.out.println(pathChildrenCacheEvent.getData().getPath());
//节点状态
System.out.println(pathChildrenCacheEvent.getData().getStat());
//节点数据
System.out.println(new String(pathChildrenCacheEvent.getData().getData()));
});
cache.close();

小福利

  • 是不是觉得文章太长看得头晕脑胀,为此陈某特地将本篇文章制作成 PDF 文本,需要回去仔细研究的朋友,老规矩,回复关键词ZK入门指南