Kalzn
文章20
标签13
分类7
面试-数据库基础以及MySql、ClickHost、Redis简介

面试-数据库基础以及MySql、ClickHost、Redis简介

0.数据完整性

确保数据准确和一致,主要有以下四类约束:

实体完整性 主键完整

域完整性 键值数据类型

参照完整性 外键完整

用户定义的完整性 例如计数器必须大于0等用户自定义的完整性条件# 面试-数据库基础、MySql、ClickHost、Redis简介

1.数据库并发控制

1.1事物

事物是提交给DBMS执行的一系列操作。DBMS需要确保事物的操作序列被完整执行。即要么全部执行,要么回滚至事物执行前的状态,不能执行一半。

四大特性(ACID):

原子性(Atomicity) 事物的操作序列要么全部执行完毕,要么不执行。

一致性(Consistency) 不能破坏数据库数据的完整性和一致性。

隔离性(Isolation) 事物执行不能相互干扰。

持久性(Durability) 事物一旦被提交,即被永久记录。

1.2 并发读写错误

更新丢失 事物A,B同时读取V,并都进行修改。那么第一个修改会丢失。

读脏数据 事物A要对V值进行修改,但是事物B在此期间(事物A还没有来得及修改V之前)读取了V。然后在没有修改的V上进行计算,将错误结果存回。

在这里插入图片描述

不可重复读 事物A先后读取两次V值,但是再次期间V被另一事物B修改,导致两次读取的V值不同。 在这里插入图片描述

幻读 事物A在查询或统计某条或者数条记录,但再次期间事物B删除了该条或者删加了几条需要统计的记录,导致事物A再次读取时发生变化。(幻读强调数据的删除和增添,不可重读强调对数据内容的修改。) 在这里插入图片描述

1.3 锁

1.3.1 乐观锁与悲观锁

悲观锁 指的是在操作数据的时候比较悲观,悲观地认为别人一定会同时修改数据,因此悲观锁在操作数据时是直接把数据上锁,直到操作完成之后才会释放锁,在上锁期间其他人不能操作数据。

乐观锁 指的是在操作数据的时候非常乐观,乐观地认为别人不会同时修改数据,因此乐观锁默认是不会上锁的,只有在执行更新的时候才会去判断在此期间别人是否修改了数据,如果别人修改了数据则放弃操作,否则执行操作。

1.3.2 共享锁和排他锁

共享锁(Share Locks,简记为S)又被称为读锁,其他用户可以并发读取数据,但任何事务都不能获取数据上的排他锁,直到已释放所有共享锁。

排它锁((Exclusive lock,简记为X锁))又称为写锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。

写锁所有等,写锁等所有,读锁可共入

1.3.3 行锁与表锁

见5.1.2

1.3.4 意向锁

见5.1.2

1.4 封锁协议与隔离级别

S锁(共享锁 读锁)X锁(排他锁 写锁)

一级封锁 对应 READ-UNCOMMITTED (读取未提交 )

在修改数据A之前加X锁,事物结束释放。

二级封锁 对应 READ-COMMITTED(读取已提交)

在一级封锁的基础上,在读取A之前加S锁,读完释放。 (Oracle 默认隔离级别)

三级封锁 对应 REPEATABLE-READ(可重复读)

在一级封锁的基础上,在读取A之前加S锁,事物结束释放。(MySql 5.5 InnoDB 默认隔离级别)

最高封锁 对应 SERIALIZABLE(可串行化 )

最高的隔离级别,完全服从 ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产⽣干扰

更新丢失 脏读 不可重复读 幻读
一级
二级
三级
最高

1.5 MVCC

1.5.1 概念

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,是现代数据库(包括 MySQL、Oracle、PostgreSQL 等)引擎实现中常用的处理读写冲突的手段,目的在于提高数据库高并发场景下的吞吐性能。

MVCC用更好的方式去处理读—写请求,做到在发生读—写请求冲突时不用加锁。 这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。

最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。

1.5.2 当前读与快照读

1.3提及的共享锁和排他锁都是当前锁。为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

当前读的实现方式:next-key锁(行记录锁+Gap间隙锁) 见4.1.2详细解释

间隙锁:只有在Read Repeatable、Serializable隔离级别才有,就是锁定范围空间的数据,假设id有3,4,5,锁定id>3的数据,是指的4,5及后面的数字都会被锁定,因为此时如果不锁定没有的数据,例如当加入了新的数据id=6,就会出现幻读,间隙锁避免了幻读。

快照读是数据库操作中的一种读取方式,它指的是从数据库中获取在某个时间点或事务开始之前的数据快照。与当前读不同,快照读不会看到其他事务已经提交的更改,而是提供了一个固定时间点的数据库状态。这种读取方式通常用于需要查看历史数据或生成报表的应用场景。

当前读/快照读与MVCC的关系

MVCC是设计理念:保存数据的多个版本,使得读写操作没有冲突。

快照读是MySql(InnoDB引擎)为了实现MVCC模型的一种具体的做法。

1.5.3 MVCC in InnoDB

MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段undo日志Read View 来实现MVCC。

Undolog:处理修改和回滚

Read View:处理读取时的快照生成

Undolog

事务会先使用“排他锁”锁定改行,将该行当前的值复制到undo log中,然后再真正地修改当前行的值,最后填写事务的DB_TRX_ID,使用回滚指针DB_ROLL_PTR指向undo log中修改前的行DB_ROW_ID

在这里插入图片描述

DB_TRX_ID: 6字节DB_TRX_ID字段,表示最后更新的事务id(update,delete,insert)。此外,删除在内部被视为更新,其中行中的特殊位被设置为将其标记为已软删除。

DB_ROLL_PTR: 7字节回滚指针,指向前一个版本的undolog记录,组成undo链表。如果更新了行,则撤消日志记录包含在更新行之前重建行内容所需的信息。

DB_ROW_ID: 6字节的DB_ROW_ID字段,包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。如果表中没有主键或合适的唯一索引, 也就是无法生成聚簇索引的时候, InnoDB会帮我们自动生成聚集索引, 聚簇索引会使用DB_ROW_ID的值来作为主键; 如果表中有主键或者合适的唯一索引, 那么聚簇索引中也就不会包含 DB_ROW_ID了 。

Read View

说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。

Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本

2. 数据库范式

目的:消除冗余

第一范式(1NF) 属性不可分(关系数据库的基础)

第二范式(2NF) 在1NF的基础上,不存在部分依赖,非主键列要完全依赖于主键。

例如:为了记录学生成绩,有属性studentID,studnetName,courseID,courseName,score。其中studnetName仅依赖studentID;courseName仅依赖courseID,而主键为(studentID, courseID)。所以将上述记录存储在一张表里不符合2NF。正确做法是拆分3张表存储:学生表:(studentID,studentName),课程表:(courseID,courseName),分数表:(studentID,courseID,score)。

第三范式(3NF) 在2NF的基础上,不存在传递依赖。

例如:记录人员:有属性:name,sexCode,sexText

其中sexCode依赖name,而sexText依赖sexCode。所以将上述记录存储在一张表里不符合3NF。正确做法是拆分2张表存储:人员表:(name,sexCode),性别表:(sexCode,sexText)。

BC范式(BCNF) 在3NF的基础上,关系R中的每个非平凡函数依赖X → Y,X必须是R的超键。

例如仓库管理(仓库号,存储物品号,管理员号,数量)满足一个管理员只在一个仓库工作;一个仓库可以存储多种物品,则存在如下关系:

(仓库号,存储物品号)——>(管理员号,数量)

(管理员号,存储物品号)——>(仓库号,数量)

但是(仓库号)——>(管理员号)且(管理员号)——>(仓库号)。即存在关键字段决定关键字段的情况,因此其不符合BCNF。把仓库管理关系表分解为两个关系表仓库管理表(仓库号,管理员号)和仓库表(仓库号,存储物品号,数量),这样这个数据库表是符合BCNF的,并消除了删除异常、插入异常和更新异常。

第四范式(4NF)

设R是一个关系模型,D是R上的多值依赖集合。如果D中存在凡多值依赖X->Y时,X必是R的超键,那么称R是第四范式的模式。

例如,职工表(职工编号,职工孩子姓名,职工选修课程),在这个表中,同一个职工可能会有多个职工孩子姓名,同样,同一个职工也可能会有多个职工选修课程,即这里存在着多值事实,不符合第四范式。如果要符合第四范式,只需要将上表分为两个表,使它们只有一个多值事实,例如职工表一(职工编号,职工孩子姓名),职工表二(职工编号,职工选修课程),两个表都只有一个多值事实,所以符合第四范式。

数据库范式只是理论指导规范,遵不遵守要看工程的实际情况。工程上多有为了效率而牺牲空间的例子,这些情况下数据库甚至可能不符合1NF。

此外还有BCNF、4NF直至5NF。这些范式的强度依次增强,冗余越低。

3.分布式系统CAP定理

CAP定理是分布式系统中的一个基本定理,它指出任何分布式系统最多可以具有以下三个属性中的两个。

一致性(Consistency)

在一个一致性的系统中,客户端向任何服务器发起一个写请求,将一个值写入服务器并得到响应,那么之后向任何服务器发起读请求,都必须读取到这个值(或者更加新的值)。

可用性(Availability)

在一个可用的分布式系统中,客户端向其中一个服务器发起一个请求且该服务器未崩溃,那么这个服务器最终必须响应客户端的请求。

分区容错性(Partition tolerance)

能容忍网络分区,在网络断开的情况下,被分隔的节点仍能正常对外提供服务。

CA 保证一致性和可用性,所有服务器必须互相通信才可以。所以丧失P属性。

CP 在网络分区(服务器无法通信)的情况下,为了保证C属性,只等设定一组服务器上的副本为可用的,其他分区的服务器为不可用,所以丧失A属性。

AP 在网络分区(服务器无法通信)且所有服务器都保证可用的情况下,无法保证数据一致,丧失C属性。

4.灾备

RAID0 (不含校验码的条带存储)

RAID 0 又称为Stripe(条带化),它在所有RAID级别中具有最高的存储性能,通过多块磁盘组合为RAID 0后,数据被分割并分别存储在每块硬盘中,所以能最大程度的提升存储性能与存储空间,把连续的数据分散到多个磁盘上存取,这样,系统有数据请求就可以被多个磁盘并行的执行,每个磁盘执行属于它自己的那部分数据请求,这种数据上的并行操作可以充分利用总线的带宽,显著提高磁盘整体存取性能,但是无法容错。

优:速度快、无冗余、读写并行、磁盘利用率100%。

缺:没有备份,没有灾备能力。

RAID 1 (不含校验码的镜像存储)

RAID 1 又称为Mirror 或Mirrooring(镜像),它的宗旨是最大限度的保证用户数据的可用性和可修复性,RAID 1 的操作方式是把用户写入硬盘的数据百分之百的自动复制到另外一个硬盘上,从而实现存储双份的数据。

优:保证了数据冗余,保证了数据安全,可靠性高,可以并发读取(相当于两块RAID 0)。

缺:写入效率低下,磁盘利用率低。

RAID 5 (数据块级别的分布式校验条带存储)

RAID 5 是一种存储性能,数据安全和存储成本兼顾的存储解决方案。RAID5技术是把硬盘设备的数据奇偶校验信息保存到其他硬盘设备中。RAID5磁盘阵列组中数据的奇偶校验信息并不是单独保存到某一块磁盘设备中,而是存储到除自身以外的其他每一块对应的磁盘上,这样的好处是其中任何一个磁盘损坏后不至于出现致命缺陷,但只能允许一块磁盘损坏,否则无法利用剩下的数据和校验信息进行数据的恢复。

优:RAID0和RAID1的折中,兼顾存储性能、数据安全和存储成本。

缺:写入性能相对较差,而且只允许单磁盘故障,在有磁盘离线的情况下,RAID 5 的读写性能较差,在重建数据时,性能会受到较大的影响。

RAID 6 (两种存储的奇偶校验码的磁盘结构)

RAID6技术是在RAID 5基础上,为了进一步加强数据保护而设计的一种RAID方式,实际上是一种扩展RAID 5等级。与RAID 5的不同之处于除了每个硬盘上都有同级数据XOR校验区外,还有一个针对每个数据块的XOR校验区。与RAID 5 相同的是当前磁盘数据块的校验数据不可能存在当前磁盘中,而是交错存储的。组建RAID 6 要求至少4块硬盘,而RAID 6可以允许坏掉两块硬盘。

优:RAID 6的数据冗余性能相当好,在使用大数据块时,随机读取性能好,允许两块硬盘的掉线,有更高的容错能力。

缺:由于增加了一个校验,所以写入的效率比RAID 5还差,而且RAID控制系统的设计也更为复杂,第二块的校验区也减少了硬盘有效存储空间。

RAID 10(镜像与条带存储)

RAID 10 不是独创的一种RAID级别,它由RAID 1 和 RAID 0 两种阵列形式组合而成,RAID 10继承了RAID 0 的快速与高效,同时也继承了RAID 1 的数据安全,RAID 10 至少需要四块硬盘。RAID 1+0,先使用四块硬盘组合成两个独立的RAID 1 ,然后将两个RAID 1 组合成一个RAID 0。

优: RAID10兼备了RAID1和RAID0的优点,不仅实现了数据保障的作用,也保障数据读写的效率。

缺: 由于一半的磁盘空间都用于存储冗余数据,所以RAID 10的磁盘利用率很低,只有50%。

RAID 50

RAID50是RAID5与RAID0的结合。此配置在RAID5的子磁盘组的每个磁盘上进行包括奇偶信息在内的数据的剥离。每个RAID5子磁盘组要求至少三个硬盘。RAID50具备更高的容错能力,因为它允许某个组内有一个磁盘出现故障,而不会造成数据丢失。

优: 比RAID5有更好的读性能,比相同容量的RAID5重建时间更短,可以容许N个磁盘同时失效,更高的容错能力,具备更快数据读取速率的潜力。

缺: 设计复杂,比较难实现;同一个RAID5组内的两个磁盘失效会导致整个阵列失效;磁盘故障会影响吞吐量。故障后重建信息的时间比镜像配置情况下要长。

在这里插入图片描述

5.Mysql数据库框架与引擎

5.1 Mysql Framework

5.1.1 框架

在这里插入图片描述

MySQL 可以分为 Server 层和存储引擎两部分。

Server 层包括:连接器、查询缓存、分析器、优化器、执行器等,涵盖了 MySQL 的大多数核心服务功能,以及所有的内置函数(如:日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这⼀层实现,比如:存储过程、触发器、视图等等。 存储引擎层负责:数据的存储和提取。其架构是插件式的,⽀持 InnoDB 、 MyISAM 等多个存储引擎。从 MySQL5.5.5 版本开始默认的是 InnoDB ,但是在建表时可以通过 engine = MyISAM 来指定存储引擎。不同存储引 擎的表数据存取方式不同,支持的功能也不同。

5.1.2 InnoDB & MyISAM

MyISAM是 MySQL 的默认数据库引擎( 5.5版之前)。虽然性能极佳,而且提供了大量的特性, 包括全文索引、压缩、空间函数等,但 MyISAM不支持事务和行级锁,而且最⼤的缺陷就是崩溃后无法安全恢复。不过, 5.5 版本之后, MySQL 引入了 InnoDB (事务性数据库引擎),MySQL5.5 版本后默认的存储引擎为 InnoDB 。 InnoDB vs MyISAM

  1. 是否支持行锁 MyISAM只有表锁,InnoDB支持行锁和表锁,默认为行锁。

  2. 是否支持事务 MyISAM不提供事物支持,InnoDB支持。同时,InnoDB具备rollback、crash recovery能力。

  3. 是否支持外键 MyISAM不支持,InnoDB支持。

  4. 是否支持MVCC 仅InnoDB支持。

总之一句话:MyISAM注重效率,而InnoDB更意图保证并行下的数据完整性

5.2 Lock in Mysql

InnoDB中支持表锁和行锁,而MyISAM仅支持表锁。InnoDB中的表锁和行锁都分为S锁和X锁。

表锁

Mysql中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单 ,资源消耗也比较少,加锁快,不会出现死锁 。此外表锁还存在意向锁:

而意向锁的作用就是当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以在需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。

  1. 意向共享锁(IS): 表示事务准备给数据行记入共享锁,事务在一个数据行加共享锁前必须先取得该表的IS锁。

  2. 意向排他锁(IX): 表示事务准备给数据行加入排他锁,事务在一个数据行加排他锁前必须先取得该表的IX锁。

意向锁vs普通锁:意向锁是放置在资源层次结构的一个级别上的锁,如果想要下级资源(如表中的行)加锁,则需要先将上级资源加上意向锁。(把资源层级看作一棵树,要给某个节点加锁,则其祖先必须先加意向锁。)如果一个上级资源被加了意向锁,则证明某下级资源被加锁。

行锁

Mysql中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。

  1. Record Lock 对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项

  2. Gap Lock 对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁)。(即在两个索引之前加锁)

  3. Next-key Lock 锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合。可解决幻读问题。

5.3 数据库索引

5.3.1 聚簇索引(InnoDB)和非聚簇索引(MyISAM)

聚簇索引数据存储与索引放到了一块,索引结构的叶子节点保存了行数据。表数据按照索引的顺序来存储的,也就是说索引项的顺序与表中记录的物理顺序一致。它默认的实现是这个主键索引。主键索引就是聚簇索引他实现之一,如果你这个表里没有这个主键索引,InnoDB就会选择一个唯一的非空索引代替。 如果连唯一的索引都没有的话,这个InnoDB就会在内部生成一个隐式的聚簇索引。

非聚簇索引 将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置。

InnnDB中,会在主键建立聚簇索引。当在其他键建立索引时,InnoDB是这么干的:建立一个二级索引,这个索引可以通过其他建值索引到主键,然后在使用主键再索引数据。

在MyISAM中,采用非聚簇索引,即索引的叶子节点是行指针,指向对应的行。

聚簇索引 vs 非聚簇索引

聚簇索引优点:

  1. 快速数据访问:由于数据物理存储顺序与索引顺序一致,相邻数据行通常存储在同一磁盘块中,减少了I/O操作次数,提高了查询速度。

  2. 高效存储:聚簇索引的叶子节点存储的是数据行本身,对于不需要查询全部列的查询语句,可以减少I/O操作,提高查询效率。

  3. 排序查找快速:对于主键的排序查找和范围查找,聚簇索引表现出色。

聚簇索引缺点:

  1. 插入和更新性能:插入和更新数据时需要移动其他数据行,这可能导致性能下降。

  2. 数据访问局限性:如果数据不是按照索引顺序存储的,某些数据访问可能会较慢。

  3. 主键更新代价高:更新主键时,需要移动被更新的行,这可能导致性能下降。

非聚簇索引优点:

  1. 插入和更新效率:插入和更新数据时不需要移动数据,因此不会影响性能。

  2. 维护简单:在表的数据发生变化时,不需要重新组织索引结构,减少维护索引的时间。

非聚簇索引缺点:

  1. 查询效率:查询时需要进行两次磁盘I/O操作:第一次查找索引条目,第二次查找实际数据行,这使得查询效率相对较低。

简而言之:聚簇索引查快改慢,非聚簇索引查慢改快

5.3.2 索引数据结构

MySQL索引使用的数据结构主要有 BTree 索引哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree 索引。

MySQL的 BTree 索引使用的是 B 树中的 B+Tree,但对于主要的两种存储引擎的实现方式是不同的。

BTree和B+Tree以及散列表详细介绍见数据结构章节

为什么MySql采用B+Tree而不是BTree

用 B+ 树不用 B 树考虑的是 IO 对性能的影响, B 树的每个节点都存储数据,而 B+ 树只有叶子节点才存储数据,所 以查找相同数据量的情况下, B 树所需要的内存更大, IO 更频繁。数据库索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐⼀加载每⼀个磁盘页(对应索引树的节点)。 自适应哈希索引

哈希索引能以 O(1) 时间进行查找,但是失去了有序性。无法用于排序与分组、只⽀持精确查找,无法用于部分查找和范围查找。InnoDB 存储引擎有⼀个特殊的功能叫 “自适应哈希索引 ” ,当某个索引值被使用的非常频繁时,会在 B+ 树索引之上 再创建⼀个哈希索引,这样就让 B+Tree 索引具有哈希索引的⼀些优点,比如:快速的哈希查找。

InnoDB B+树 可以存放多少行数据

InnoDB的默认最小存储单位为16k。假设每条记录占用空间为1k。一个高度为2个B+Tree包括一个根节点(16k),在其中存储多项(主键+指针)的组合。由于InnoDB默认主键为bigint 8字节,指针为硬编码的6字节,所以每条记录为14字节。根节点共存储16384/14=1170个子节点指针。而叶子节点用于存储数据,前面假设每条记录为1k,所以一个节点可以存储16k/1k=16条数据。因此,高度为2的B+Tree共计存储1170 * 16 = 18720条记录。同理可知高度为3个B+Tree可存储1170* 1170 *16=21902400条记录。

InnoDB单表最多可以存储多少数据

  1. 自建主键的情况下取决于主键类型的最大值,例如主键为int(4),那就是4字节int的最大值。如果超过则报错。

  2. 没有主键自动生成一个隐藏主键int(6),如果超过则会覆盖之前的数据而不会报错。

B+Tree vs BTree(为什么MySql选择B+Tree而不是BTree?)

  • B+树非叶子节点只存储key值,而B树存储key值和data值,这样B+树每次读取时可以读取到更多的key值。
  • mysql进行区间访问时,由于B+树叶子节点之间用指针相连,只需要遍历所有的叶子节点即可;而B树则需要中序遍历那样遍历
  • B+树非叶子节点只存储key值,而B树存储key值和data值,导致B+树的层级更少,查询效率更高
  • B+树所有关键词地址都存在叶子节点上,所以每次查询次数都相同,比B树稳定

B+Tree vs Hash (Hash明明更快O(1),MySql为什么选择B+Tree而不是Hash?)

  1. 磁盘IO特性:从内存角度上说,数据库中的索引一般时在磁盘上,数据量大的情况可能无法一次性装入内存,B+树的设计可以允许数据分批加载。

  2. Hash不适应多条范围查询:从业务场景上说,如果只选择一个数据那确实是hash更快,但是数据库中经常会选中多条这时候由于B+树索引有序,并且又有链表相连,它的查询效率比hash就快很多了。

    MySql也支持Hash索引(默认是B+Tree),如前文所述,它仅在单条查询占比较大的情况下比较快。

B+Tree vs 红黑树 (内存中,红黑树优于B+Tree。为什么Mysql选择B+Tree?)

  1. 操作系统IO特性:操作系统所组织的文件系统最小单位为页,即使只需要该页内的部分数据,也需要整页读取。而红黑树是二叉树,仅有两个孩子节点,无法充分利用单页的存储空间。而B+Tree是多叉树,单节点可以挂载很多孩子,在充分利用单页存储空间的同时减少树的高度,减少了IO次数。

ps.有关hash、BTree、红黑树的详细介绍移步数据结构.md

5.3.3 索引分类总结

从数据结构角度:1)B+Tree索引O(logn) 2) Hash索引 O(1)

从物理存储角度:1)聚簇索引 2)非聚簇索引

5.3.4 索引可用性

最左前缀原则

最左前缀原则(Leftmost Prefix Rule)是索引在数据库查询中的一种使用规则。它指的是在使用复合索引时,查询条件需要遵循索引中列的顺序,从左到右进行匹配。只有当查询条件满足最左前缀原则时,才能充分利用联合索引的优势,提高查询性能。

例如索引组(a,b,c)。当查询(a,c)时,仅有a索引可以被利用,而由于b被跳过了,所以c索引无法被利用。

再例如,查询(b,c)时,所有索引均不会被利用,这是由于a被跳过了。形式来讲当存在索引组:

而对于查询:

如果,则仅有前个索引可以被利用。

此外最左前缀碰到范围查询时也会停止!后面的索引都不会用到。

如何知道创建的索引有没有被用到?或是说怎么排除语句运行慢的原因?

使用 Explain 命令来查看语句的执行计划,MySQL 在执行某个语句之前,会将该语句过⼀遍查询优化器,之后会拿到对语句的分析,也就是执行计划,其中包含了许多信息。可以通过其中和索引有关的信息来分析是否命中了索引,例如: possilbe_key 、 key 、 key_len 等字段,分别说明了此语句可能会使用的索引、实际使用的索引以及使用的索引长度。

会导致索引失效的情况?

  1. 索引参与表达式或函数计算

  2. 通配符

  3. 字符串和数字比较无法利用索引

  4. or语句中如果有一项没有索引,则另一项索引失效

  5. 正则

  6. 优化器判定全表扫描更快时不用索引

查询优化方案?

  1. 减少请求量:仅返回必要的列和行

  2. 内存侧缓存:讲频繁查询的记录存储在内存中

  3. 使用链接(join)而不是子查询

  4. 使用union而不是or

  5. 避免范围查询:甚少在where子句中使用!=, <, >

  6. 避免null值判断:这会迫使引擎采用全表扫描而不是索引

索引优缺点(何时应该怎样应该建立索引?)

  1. 需要查询、排序、分组和联合操作的字段适合建立索引。

  2. 使用字段值不重复比例大的字段建立索引,联合索引比独立索引的效率高。

  3. 索引越多,维护成本越高,数据修改的效率越低。

  4. 唯一性索引可以确保数据唯一性。

  5. 索引可以加快表的链接效率,在实现参照完整性的方面具有特别意义。

5.4 主从复制

5.4.1 概念

主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库,主数据库一般是准实时的业务数据库。

5.4.2 Binlog

从比较宽泛的角度来探讨复制的原理,MySQL的Server之间通过二进制日志来实现实时数据变化的传输复制,这里的二进制日志是属于MySQL服务器的日志,记录了所有对MySQL所做的更改。这种复制模式也可以根据具体数据的特性分为三种:

  1. Statement:基于语句格式

    Statement模式下,复制过程中向获取数据的从库发送的就是在主库上执行的SQL原句,主库会将执行的SQL原有发送到从库中。

  2. Row:基于行格式

    Row模式下,主库会将每次DML操作引发的数据具体行变化记录在Binlog中并复制到从库上,从库根据行的变更记录来对应地修改数据,但DDL类型的操作依然是以Statement的格式记录。

  3. Mixed:基于混合语句和行格式

    MySQL 会根据执行的每一条具体的 SQL 语句来区分对待记录的日志形式,也就是在 statement 和 row 之间选择一种。

目前互联网业务的在线MySQL集群全部都是基于Row行格式的Binlog。虽然这种模式下对资源的开销会偏大,但数据变化的准确性以及可靠性是要强于Statement格式的,同时这种模式下的Binlog提供了完整的数据变更信息,可以使其应用不被局限在MySQL集群系统内。(泛用性强)

5.4.3 主从复制的流程

在这里插入图片描述
  1. 首先从库启动I/O线程,跟主库建立客户端连接。
  2. 主库启动binlog dump线程,读取主库上的binlog event发送给从库的I/O线程,I/O线程获取到binlog event之后将其写入到自己的Relay Log中。
  3. 从库启动SQL线程,将等待Relay中的数据进行重放,完成从库的数据更新。

为什么存在Relay Log而不是直接commit?

在MySQL 4.0 之前是没有Relay Log这部分的,整个过程中只有两个线程。但是这样也带来一个问题,那就是复制的过程需要同步的进行,很容易被影响,而且效率不高。例如主库必须要等待从库读取完了才能发送下一个binlog事件。这就有点类似于一个阻塞的信道和非阻塞的信道。

主从复制的好处?

  1. 实现复杂均衡

  2. 实现异地备份

    1. 提高数据库可用性(?不就是均衡负载)

主库宕机后,数据可能丢失,从库只有一个sql Thread,主库写压力大,复制可能延迟。

解决方法:

半同步复制 解决数据丢失问题

并行复制 解决从库复制延迟的问题(多个SQL程序并行执行)

异步复制(Asynchronous replication) MySQL默认的复制即是异步的,主库在执行完客户端提交的事务后会立即将结果返给给客户端,并不关心从库是否已经接收并处理,这样就会有一个问题,主如果crash掉了,此时主上已经提交的事务可能并没有传到从上,如果此时,强行将从提升为主,可能导致新主上的数据不完整。

全同步复制(Fully synchronous replication) 指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。

半同步复制(Semisynchronous replication) 介于异步复制和全同步复制之间,主库在执行完客户端提交的事务后不是立刻返回给客户端,而是等待至少一个从库接收到并写到relay log中才返回给客户端。相对于异步复制,半同步复制提高了数据的安全性,同时它也造成了一定程度的延迟,这个延迟最少是一个TCP/IP往返的时间。所以,半同步复制最好在低延时的网络中使用。

5.4.4 数据库读写分离

读写分离常用代理放式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。主服务器处 理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。

为什么进行读写分离?

  1. 各自读写,极大程度缓解锁争用。

  2. 从服务器可以使用MyISAM引擎,提高查询效率。

  3. 增加冗余,增强可用性。

5.4.5 主从同步延迟

原因: 假如⼀个服务器开放 N 个连接给客户端,这样有会有大量并发的更新操作, 但是从服务器的里面读取 binlog 的线程仅有⼀个, 当某个 SQL 在从服务器上执行的时间稍长或者由于某个 SQL要 进行锁表就会导致主服务器的 SQL 大量积压,未被同步到从服务器里。这就导致了主从不⼀致, 也就是主从延迟。 解决方法

  1. 在对数据安全要求不是很高的情况下关闭从服务器的sync_binlog。

  2. 增加服务器。(?神经)

5.5 MySQL执行分析

5.5.1 MySql基本执行框架

先简单介绍一下下图涉及的一些组件的基本作用帮助大家理解这幅图,在 1.2 节中会详细介绍到这些组件的作用。

连接器: 身份认证和权限相关(登录 MySQL 的时候)。 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。 优化器: 按照 MySQL 认为最优的方案去执行。 执行器: 执行语句,然后从存储引擎返回数据。

在这里插入图片描述

5.5.2 Server层

连接器

连接器主要和身份认证和权限相关的功能相关,主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作,如果用户账户密码已通过,连接器会到权限表中查询该用户的所有权限,之后在这个连接里的权限逻辑判断都是会依赖此时读取到的权限数据,也就是说,后续只要这个连接不断开,即时管理员修改了该用户的权限,该用户也是不受影响的。 查询缓存(MySql 8.0后移除)

查询缓存主要用来缓存我所执行的 SELECT 语句以及该语句的结果集。

连接建立后,执行查询语句的时候,会先查询缓存,MySQL 会先校验这个 sql 是否执行过,以 Key-Value 的形式缓存在内存中,Key 是查询预计,Value 是结果集。如果缓存 key 被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。 移除原因:不常用,表更新后缓存会清空。

分析器(相当于编译前端)

对sql语句进行解释,分为两个步骤:

  1. 词法分析,提取关键字,比如 select,提出查询的表,提出字段名,提出查询条件等等。做完这些操作后,就会进入第二步。

  2. 语法分析,讲词法分析得出的词串串联分析语义信息,检查语法是否正确。

优化器(相当于编译后端)

优化器的作用就是它认为的最优的执行方案去执行。

执行器

执行器会首先校验用户权限,如果鉴权通过则调用引擎接口。

5.5.3 Log系统

进行更新操作时需要同时记录日志,MySql(InnoDB引擎)有两个日志系统Binlog(Server层执行器的)和Redolog(InnoDB引擎的)。

更新流程如下:

  1. 引擎查询到要修改的数据。

  2. 修改目标数据

  3. 引擎记录Redolog,Redolog进入prepare状态,并告诉执行器执行完成。

  4. 执行器收到通知后记录Binlog,随后调用引擎,提交Redolog为提交状态。

为何要有两个日志系统?

令InnoDB具有crash-safe能力。这里分析两种情况:

  1. 先写Redolog直接提交,然后写Binlog,假设写完 redo log 后,机器挂了,binlog 日志没有被写入,那么机器重启后,这台机器会通过 redo log 恢复数据,但是这个时候 bingog 并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。

  2. 先写 Binlog,然后写 Redolog,假设写完了 binlog,机器异常重启了,由于没有 redo log,本机是无法恢复这一条记录的,但是 binlog 又有记录,那么和上面同样的道理,就会产生数据不一致的情况。

6. 数据库设计规范

6.1 数据库命名规范

  1. 所有数据库对象名称必须使用小写字母并用下划线分割

  2. 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)

  3. 数据库对象的命名要能做到见名识意,并且最好不要超过 32 个字符

  4. 临时库表必须以 tmp_为前缀并以日期为后缀,备份表必须以 bak_为前缀并以日期 (时间戳) 为后缀

  5. 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)

6.2 数据库设计规范

  1. 使用InnoDB存储引擎

  2. 数据库和表的字符集统一使用 UTF8

  3. 所有表和字段加注释

  4. 尽量控制单表数据量大小,控制在500万以内。

    可以使用历史数据归档、分库分表等手段控制数据量

  5. 谨慎使用分区表

  6. 分离冷热数据,减小表宽度

  7. 不要预留字段

  8. 不能存储图片、文件等大二进制数据

  9. 禁止在线上做压力测试

  10. 禁止从开发环境、测试环境链接生成环境数据库

7. ClickHouse

7.1 概念

ClickHouse是高性能、MPP架构、列式存储、完备DBMS功能的OLAP数据库

特点

  1. 高性能

  2. 分布式框架

  3. 支持SQL

  4. 支持多种数据类型

  5. 支持多种压缩算法

  6. 免费开源

其中几个名词的解释:

MPP架构

MPP架构(Massively Parallel Processing)是一种设计用于处理大规模数据集的计算架构,其核心思想是将数据处理任务分布在多个处理器上以实现并行计算,从而获得高性能和可扩展性。这种架构的主要优势包括

  • 高性能。通过在多个处理器上同时执行任务,MPP架构能够实现高吞吐量和低延迟,显著加速计算过程。
  • 可扩展性。通过增加更多处理器,MPP架构可以扩展系统的计算能力,满足不断增长的数据处理需求。
  • 容错能力。MPP架构通过在多个处理器上冗余数据和计算任务,提高系统的容错性,确保在部分处理器故障时系统的稳定性。

MPP架构可以分为不同的类别,包括共享存储和分布式存储两类。共享存储MPP架构中,所有处理器都连接到同一个共享存储系统,使用高速互联网络进行通信,优点是能够实现高速数据访问,但可能存在存储系统性能瓶颈。分布式存储MPP架构中,每个处理器都有其自己的本地存储系统,也通过高速互联网络进行通信,优点是避免了存储系统成为性能瓶颈,但可能需要更复杂的数据分布和通信机制。

MPP架构广泛应用于多个领域,如大数据处理、机器学习、金融风险管理等,在大数据处理方面,它能够有效地处理大量数据,特别是在需要实时分析和处理的场景下;在机器学习和人工智能方面,它能够加速训练和推理过程;在金融风险管理方面,它帮助金融机构快速处理交易数据以实现高效风险管理。

缺点:存储不透明、单节点瓶颈

在设计上,MPP架构优先考虑一致性(Consistency),其次考虑可用性(Availability),同时尽量做到分区容错性(Partition Tolerance)。即保证CA属性。MPP架构常用于数据仓库、数据集市、大数据分析等场景,其分布式设计能够有效应对数据规模的不断增长和复杂度的提高,但也会面临一些挑战。

分布式架构

分布式架构是一种将计算任务并发分散到多个计算节点上的计算架构,主要用于处理大规模数据和复杂计算问题。这种架构也常称为大数据架构或分布式批处理架构,并且包含多个具体实现,如Hadoop、Spark等。在设计上,分布式系统通常会优先考虑分区容错性(Partition Tolerance),其次考虑可用性(Availability),尽量做到一致性(Consistency)。即保证AP属性

DBMS

即Database Management System。 数据库管理系统(Database Management System)是一种操纵和管理数据库的大型软件,用于建立、使用和维护数据库。

OLAP & OLTP

OLAP(联机分析处理)和OLTP(联机事物处理)

OLTP旨在高效地处理日常的事务,如银行交易、订单处理等,它强调的是事务的处理速度和实时性,主要用于事务数据的录入和实时处理(注重实时处理

OLAP则用于处理大规模数据,支持复杂的数据分析和决策支持,强调从历史数据中提取洞察力,主要用于查询、报表生成和数据分析(注重历史分析

在这里插入图片描述

其中,OLAP还可分为ROLAP(关系型OLAP)和MOLAP。MOLAP是指带预先结果统计和存储的OLAP,是一种以空间换时间的做法。此外还有HOLAP(细节数据存储在ROLAP,聚合数据存储在MOLAP)。

列式存储

在传统的行式存储中,数据按照行的方式存储,即将每个记录的各个字段按顺序存储在一起,每行数据占用的存储空间相对较大。而列式存储是将每列的数据存储在一起,优:可以单独进行压缩与解码,占用的存储空间更少,查询效率更高。

7.2 ClickHouse的优缺点

优:查询速度快,可以在存储数据超过20万亿行的情况下,做到了90%的查询能够在1秒内返回。支持高级数据类型,支持自定义函数和聚合函数,支持多种存储引擎。

缺点:针对OLTP业务场景的支持有限:

  1. 不支持事物

  2. 不擅长根据主键按行粒度进行查询(但支持)

  3. 不擅长按行删除数据(但支持)(?内部数据结构和线段树差不多)

7.3 ClickHouse高效查询的原因

列式存储与数据压缩

减少数据扫描范围和数据传输时的大小,列式存储和数据压缩就可以做到这两点。

向量化执行

消除程序中循环的优化,是基于底层硬件实现的优化。

多样化引擎

与MySQL类似,ClickHouse也将存储部分进行了抽象,把存储引擎作为一层独立的接口。目前ClickHouse共拥有合并树、内存、文件、接口和其他6大类20多种表引擎。每一种表引擎都有着各自的特点,用户可以根据实际业务场景的要求,选择合适的表引擎使用。

多线程与分布式

多线程处理就是通过线程级并行的方式实现了性能的提升,ClickHouse将数据划分为多个partition,每个partition再进一步划分为多个index granularity,然后通过多个CPU核心分别处理其中的一部分来实现并行数据处理。这种设计下,可以使得ClickHouse单条Query就能利用整机所有CPU,极致的并行处理能力,极大的降低了查询延时。

7.4 ClickHouse执行过程

在这里插入图片描述

简单来说,就是一条sql,会经由Parser与Interpreter,解析和执行,通过调用Column、DataType、Block、Functions、Storage等模块,最终返回数据,下面是各个模块具体的介绍。

Columns

表示内存中的列(实际上是列块),需使用 IColumn 接口。该接口提供了用于实现各种关系操作符的辅助方法。几乎所有的操作都是不可变的:这些操作不会更改原始列,但是会创建一个新的修改后的列。

Field

表示单个值,有时候也可能需要处理单个值,可以使用Field。Field 是 UInt64、Int64、Float64、String 和 Array 组成的联合。与Column对象的泛化设计思路不同,Field对象使用了聚合的设计模式。在Field对象内部聚合了Null、UInt64、String和Array等13种数据类型及相应的处理逻辑。

DataType

IDataType 负责序列化和反序列化:读写二进制或文本形式的列或单个值构成的块。IDataType直接与表的数据类型相对应。比如,有 DataTypeUInt32、DataTypeDateTime、DataTypeString等数据类型。

Storage

IStorage接口表示一张表。该接口的不同实现对应不同的表引擎。比如StorageMergeTree、StorageMemory等。这些类的实例就是表。

Parser与Interpreter

Parser和Interpreter是非常重要的两组接口:Parser分析器负责创建AST对象;而Interpreter解释器则负责解释AST,并进一步创建查询的执行管道。它们与IStorage一起,串联起了整个数据查询的过程。Parser分析器可以将一条SQL语句以递归下降的方法解析成AST语法树的形式。不同的SQL语句,会经由不同的Parser实现类解析。例如,有负责解析DDL查询语句的ParserRenameQuery、ParserDropQuery和ParserAlterQuery解析器,也有负责解析INSERT语句的ParserInsertQuery解析器,还有负责SELECT语句的ParserSelectQuery等。

7.5 ClickHouse表引擎

7.5.1 合并树引擎

Clickhouse中最强大的表引擎当属MergeTree(合并树)引擎及该系列(MergeTree)中的其他引擎。

MergeTree系列的引擎被设计用于插入极大量的数据到一张表当中。数据可以以数据片段的形式一个接着一个的快速写入,数据片段在后台按照一定的规则进行合并。相比在插入时不断修改(重写)已存储的数据,这种策略会高效很多。

特点

  • 存储的数据按主键排序。这使得您能够创建一个小型的稀疏索引来加快数据检索。
  • 如果指定了分区键的话,可以使用分区。在相同数据集和相同结果集的情况下ClickHouse中某些带分区的操作会比普通操作更快。查询中指定了分区键时ClickHouse会自动截取分区数据。这也有效增加了查询性能。
  • 支持数据副本。ReplicatedMergeTree系列的表提供了数据副本功能。
  • 支持数据采样。需要的话,您可以给表设置一个采样方法。
引擎名称 说明
MergeTree 适用于查询性能要求较高的数据表,如:时间序列数据。它有一个优化排序和合并的技术,从而提高数据查询速度。
CollapsingMergeTree 与 MergeTree 相似,但是在累加数据值方面更有优势。它可以在查询中合并多个数据值。
SummingMergeTree 与 CollapsingMergeTree 相似,但它专门用于对数值累加。
ReplacingMergeTree 适用于在数据表中替换某些数据值。如果数据表中存在与新数据重复的键,则它将替换该数据。
GraphiteMergeTree 适用于操作时间序列数据,如:Graphite 应用。它对时间序列数据查询具有较高的性能。
VersionedCollapsingMergeTree 适用于数据版本控制,并且可以在多个版本之间查询数据。
Memory 将数据存储在 RAM 中,因此数据查询速度比磁盘存储快得多。不过,由于它是在内存中存储数据,因此它通常不适用于大数据量的数据表。
Buffer 缓存引擎,用于存储缓存表。适用于中间结果,可以在内存和磁盘之间快速切换 相对于内存引擎的读写速度较慢,数据不能永久保存。

7.5.2 日志引擎

用于日志存储的存储引擎,这些引擎是为了需要写入许多小数据量(少于一百万行)的表的场景而开发的。

这系列的引擎有:

  • StripeLog
  • Log
  • TinyLog

特点

  • 数据存储在磁盘上。

  • 写入时将数据追加在文件末尾。

  • 不支持突变操作,也就是更新。

  • 不支持索引。 这意味着 SELECT 在范围查询时效率不高。

  • 非原子地写入数据。 如果某些事情破坏了写操作,例如服务器的异常关闭,你将会得到一张包含了损坏数据的表。

7.6 高级数据结构

ClickHouse支持多种数据类型,包括基本数据类型复合数据类型几何数据类型

在这里插入图片描述

7.7 CilckHouse vs 传统关系型数据库

  1. ClickHouse的数据模型是基于表的,与创建表时指定的数据存储类型和存储引擎有关,支持多种高级数据类型。

  2. ClickHouse的数据模型是列式存储的(传统关系型数据库为行式存储),每个列可以单独进行压缩与解码,存储空间更少,查询效率高。

  3. ClickHouse是OLAP数据库,用于处理大规模数据,支持复杂的数据分析和决策支持,强调从历史数据中提取洞察力,主要用于查询、报表生成和数据分析(注重历史分析,查统增改快,删慢

  4. OLTP能力有限,不支持事物。

7.8 DDL & DML

ClickHouse的DDL和DML是ClickHouse SQL,它与传统的SQL具有很高的兼容性,但也有一些专门为ClickHouse设计的特殊语法和函数,以便更好地支持列式存储和分布式计算。

特点

  1. 支持多种查询

  2. 优化查询性能

  3. 支持高级数据结构

  4. 支持自定义函数和聚合函数

  5. 支持多种存储引擎

一个建表的例子

1
2
3
4
5
6
7
CREATE TABLE encoded_data (
eventdate Date CODEC(Delta, LZ4),
event_type String CODEC(ZSTD),
value UInt32
) ENGINE = MergeTree()
ORDER BY event_date
PARTITION BY(event_type);

大部分与SQL标准一致,其中CODEC指定改列的数据压缩和数据编码方法。ENGINE=MergeTree()用于指定数据存储引擎。

ORDER BY event_date是指将会按照event_date这一字段排序。

PARTITION BY(event_type) 是指数据会通过event_type这一字段进行分区。

7.9 数据压缩与编码

7.9.1 数据压缩

数据压缩是通过消除数据中的冗余信息来减少数据大小的过程。在ClickHouse中,数据压缩主要通过两种压缩算法实现:LZ4和ZSTD。

LZ4(默认压缩算法)

LZ4是一种无损压缩算法,它提供了较高的压缩速度和较低的解压缩速度。LZ4适用于大多数场景,因为它可以在不影响查询性能的情况下显著减少存储空间。

ZSTD

ZSTD(Zstandard)是一种无损压缩算法,它提供了较高的压缩比和较低的解压缩速度。相比LZ4,ZSTD可以进一步减少存储空间,但解压缩速度较慢。ZSTD适用于对存储空间有严格要求的场景。

压缩率:LZ4 < ZSTD

解压速度:LZ4 > ZSTD

7.9.2 数据编码

数据编码是通过对数据进行转换来减少数据大小的过程。在ClickHouse中,数据编码主要通过两种技术实现:Delta和Gorilla。

Delta

Delta编码是一种差分编码技术,它通过存储相邻数据之间的差值来减少数据大小。Delta编码适用于具有连续值或递增值的数据,如时间序列数据。在ClickHouse中,Delta编码可以与LZ4或ZSTD压缩算法结合使用,以进一步减少存储空间。

Gorilla

Gorilla编码是一种专为时间序列数据设计的编码技术,它通过存储相邻数据之间的XOR值来减少数据大小。与Delta编码相比,Gorilla编码可以实现更高的压缩比,但仅适用于时间序列数据。在ClickHouse中,Gorilla编码可以与LZ4或ZSTD压缩算法结合使用,以进一步减少存储空间。

7.10 分布式结构

7.10.1 角色

ClickHose是一个分布式系统,内含多种角色的服务器。

在这里插入图片描述

7.10.2 本地表和分布式表

本地表

当使用一般的CREATE语句进行建表时,所建立的表只会存在在一台ClickHouse Server上,这种表称为本地表

分布式表

一个逻辑上的表, 可以理解为数据库中的视图, 一般查询都查询分布式表. 分布式表引擎会将我们的查询请求路由本地表进行查询, 然后进行汇总最终返回给用户。

为什么不在分布式表进行写入?

  1. 分布式表接收到数据后会将数据拆分成多个parts,并转发数据到其他服务器,会引起服务器间网络流量增加、服务器merge的工作量增加,导致写入速度变慢,并且增加了Too many parts的可能性

  2. 数据的一致性问题,现在分布式表所在的机器进行落盘,然后异步的发送到本地表所在的机器上进行存储,中间没有一致性的校验,而且在分布式表所在机器如果集群出现down机,会存在数据丢失风险。

  3. 对zookeeper的压力比较大。

在实际生成环境中通常只读分布式表,写入本地表。

7.10.3 数据同步与查询(数据切片和冗余)

Replication & Sharding

ClickHouse依靠ReplicatedMergeTree引擎族与ZooKeeper实现了复制表机制, 成为其高可用的基础。ClickHouse像ElasticSearch一样具有数据分片(shard)的概念, 这也是分布式存储的特点之一, 即通过并行读写提高效率。 ClickHouse依靠Distributed引擎实现了分布式表机制, 在所有分片(本地表)上建立视图进行分布式查询。

在这里插入图片描述

Shard(切片):表内数据的一段

Replicated(冗余):表内数据的一段副本。

Replicated Table & ReplicatedMergeTree Engine

ReplicatedMergeTree是支持实现高可用的Replicated Table(复制表)的引擎,ReplicatedMergeTree引擎族非常依赖于zookeeper, 它在zookeeper中存储了大量的数据。

Distributed Table & Distributed Engine

ClickHouse分布式表的本质并不是一张表,,而是一些本地物理表(分片)的分布式视图,本身并不存储数据.。分布式表建表的引擎为Distributed。

数据同步流程

在这里插入图片描述
  1. 写入到一个节点

  2. 通过interserver HTTP port端口同步到其他实例上

  3. 更新zookeeper集群记录的信息

数据查询流程

在这里插入图片描述
  1. 各个实例之间会交换自己持有的分片的表数据

  2. 汇总到同一个实例上返回给用户

写入去重

可复制表写入去重:注意,只有对于复制表系列才有写入去重机制,并不是所有的表都有写入去重机制的。可复制表的写入去重依赖于zookeeper。

clickhouse在zookeeper上的默认路径为 /clickhouse。对于可复制表的写入,每次写入,将所有写入的数据按照规则划分为一个个block,对每一个block计算一个校验和,将校验和存储在zookeeper的 /clickhouse/tables/分片号/数据库名/表名/blocks路径下(这个路径和 可复制表的参数也有关)的一个节点上。每次写入的时候,比较数据块的校验和已有的数据块校验和关系,用以判断写入数据是否重复。 ClickHouse支持哪些分布式部署方案,如何进行分布式查询?

ClickHouse支持以下几种分布式部署方案:

  1. 分片复制部署:将数据划分为多个分片,每个分片都有多个副本,每个副本都可以读写数据。在这种部署方案中,每个节点都可以执行查询,但只有主副本可以执行写操作。当主副本出现故障时,会自动切换到备副本。

  2. 分片无复制部署:将数据划分为多个分片,每个分片只有一个副本。在这种部署方案中,每个节点都可以执行查询和写操作。

  3. 复制无分片部署:所有数据都复制到每个节点上,每个节点都可以执行查询和写操作。这种部署方案适合小型集群和低并发查询。

分布式查询可以通过以下两种方式进行:

  1. 基于分布式查询引擎:ClickHouse提供了分布式查询引擎,允许用户在多个节点上并行执行查询,并将结果汇总到一个节点上。分布式查询引擎可以处理大量数据,并且可以根据查询的特点自动调整查询计划,提高查询性能。

  2. 基于分布式存储引擎:ClickHouse支持分布式存储引擎,可以将数据分布到多个节点上,实现数据的并行读取和写入。在这种情况下,查询可以在多个节点上执行,并通过网络传输数据进行计算。这种方法适用于需要在多个节点上分析大量数据的场景。

7.11 数据组织与索引

7.11.1 分区(Partition)

1
2
3
4
5
6
7
CREATE TABLE encoded_data (
eventdate Date CODEC(Delta, LZ4),
event_type String CODEC(ZSTD),
value UInt32
) ENGINE = MergeTree()
ORDER BY event_date
PARTITION BY(event_type);

PARTITION BY(event_type) 是指数据会通过event_type这一字段进行分区。在ClickHouse进行数据组织存储时,会按照某个字段分组存储,这被称为分区。在上述例子中,encoded_data中的所有数据将会按照event_type分成数个组,分别进行存储。

在这里插入图片描述

为什么要分区?(分区优点)

提高查询速度,减少不必要额数据读取。where子句中查询某一类别只需要到该类别对应的分区查询即可。

7.11.2 列式存储

在传统的行式存储中,数据按照行的方式存储,即将每个记录的各个字段按顺序存储在一起,每行数据占用的存储空间相对较大。而列式存储是将每列的数据存储在一起。

为什么要用列式存储?(行式存储 vs 列式存储)

在进行OLAP业务有优势,在进行统计和查询时只取其中的几个列。而行式存储需要读取每一行的整个条目,然后在选择列。但是于此同时,对于记录的删除,修改则需要耗费更高的成本,这也是为什么Clickhouse不擅长删除的原因。

此外,列式存储更容易去做数据编码和压缩。

总而言之:列存储适合OLAP业务,行存储适合OLTP业务

7.11.3 MergeTree的数据组织和索引

数据组织

在这里插入图片描述

稀疏索引

稀疏索引是一套在设定的有序列上建立的索引。它是一个粗粒度索引,被记录在primay.idx。它每隔固定的行数建立一个索引指针指向当前行数。例如在下图中通过有序列age建立索引,其中每隔5列建立一个索引指针。适用稀疏索引可以快速确定所要查询的记录在表中的位置区域。在进行查询时先通过primay.idx获取要查询的区域序号,在通过各列的column.mrk2文件找到该区域的偏移量,随后到各列文件中的对应位置检索数据。

在这里插入图片描述
在这里插入图片描述

数据插入方法

当有新的一批数据插入分区时,并不会直接插入到原分区的各个列内,(Why?因为那样需要对分区重新排序,重新构建索引。)而是针对这新的一批数据建立一套独立的稀疏索引,然后挂载到MergeTree中。

在这里插入图片描述

此时要进行快速查询需要在每个part内部分别进行查询,然后将结果合并。

异步合并

为了不使part过多,ClickHouse会对同一个partition下的part进行异步合并。

ClickHouse的索引类型是什么,如何使用索引来提高查询性能?

ClickHouse支持以下三种索引类型:

  1. 稠密索引(Dense Index):稠密索引是一种基于跳跃表的索引结构,用于在有序列上进行范围查询。在ClickHouse中,稠密索引使用较少的空间来存储数据,并且可以加快查询速度。

  2. 稀疏索引(Sparse Index):稀疏索引是一种基于哈希表的索引结构,用于在无序列上进行查询。稀疏索引只在查询时创建,因此可以减少索引的空间占用。

  3. 索引光标(Index Cursor):索引光标是一种用于加速聚合查询的技术。它允许ClickHouse在数据分片和节点之间进行快速分布式聚合查询。

ClickHouse的数据写入和数据删除是如何实现的? 在往 ClickHouse 中写入数据时,它会先将数据写入的内存表中,当内存表达到一定的大小后,将内存表中的数据写入新的数据块中,根据数据的时间戳确定数据块的位置。当数据块达到一定大小后,会进行归并排序,存储到更大的数据块中,形成一个更大的分区,最终进行长期的存储。

ClickHouse 使用 alter table 关键字进行数据删除,它是一种逻辑删除。通过添加一个标记字段来区分数据是否被删除,并不会删除真正的数据,只有在分片时才会真正的清理。 ClickHouse的内存管理和垃圾回收机制是怎样的? ClickHouse 采用自己实现的内存池来管理内存,这样可以快速地进行内存分配和回收。ClickHouse 也可以设置内存限制,以确保查询过程中不会耗尽系统内存,一旦达到内存限制,ClickHouse 将会自动开始垃圾回收,以释放一些内存。

ClickHouse 使用的是一种自适应的垃圾回收机制,当内存使用达到限制时会自动进行回收,选择尽可能少的数据进行回收,同时避免产生大量的垃圾数据。 ClickHouse如何处理时序数据和实时数据? ClickHouse 支持专门的时序数据库引擎 —— MergeTree 引擎,该引擎是针对时间序列数据设计的,能够快速地处理基于时间的查询和聚合操作。

ClickHouse 提供了 Kafka 引擎,能够直接消费 Kafka 消息队列中的数据,并将其存储到 ClickHouse 中。还支持对实时数据进行预处理和聚合操作,从而实现实时数据分析和监控。

ClickHouse支持哪些数据导入和导出方式? 支持csv、json、orc、parquet、MySQL等关系型数据库、NoSQL,还支持从消息队列和存储系统中导入和导出数据,例如:Kafka、Redis等。

7.11.4 向量化计算

向量化计算是一种特殊的并行计算方式。相比于一般程序在同一时间只执行一个操作的方式,它可以一次计算一组数据(向量)。SIMD(Single Instruction Multiple Data): 即单指令流多数据流。

7.12 物化视图

物化视图是包括一个查询结果的数据库对象,利用空间换时间。可以避免多基础表的频繁查询,尤其是固定查询。可以显著提高查询的性能。

一致性保证

物化视图保证了数据一致性,即原表数据发生改变,物化视图内的数据也会跟着改变。能够完成该特性的存储引擎是AggregatingMergeTree。

8. Redis

8.1 Redis简介与适用场景

Redis是一个key-value存储系统,它支持存储的value类型相对更多,包括string、list、set、zset(sorted set --有序集合)和hash。这些数据结构都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis支持各种不同方式的排序。为了保证效率,数据都是缓存在内存中,Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。 使用场景

  1. Redis是基于内存的nosql数据库,可以通过新建线程的形式进行持久化,不影响Redis单线程的读写操作。
  2. 通过list取最新的N条数据。
  3. 模拟类似于token这种需要设置过期时间的场景。
  4. 发布订阅消息系统。
  5. 定时器、计数器。

功能

  1. 本机内存缓存

    当调用api访问数据库时,假如此过程需要2秒,如果每次请求都要访问数据库,那将对服务器造成巨大的压力,如果将此sql的查询结果存到Redis中,再次请求时,直接从Redis中取得,而不是访问数据库,效率将得到巨大的提升,Redis可以定时去更新数据(比如1分钟)。

  2. 持久化存储

  3. 哨兵和复制

    Sentinel可以管理多个Redis服务器,它提供了监控、提醒以及自动的故障转移功能;

    复制则是让Redis服务器可以配备备份的服务器;Redis也是通过这两个功能保证Redis的高可用;

  4. 集群

支持的数据类型

  1. 字符串

  2. hash

  3. list

  4. set

  5. zset (有序set)

8.2 Redis IO多路复用

8.2.1 概念

在Redis中,IO多路复用是一种技术,允许单个线程处理多个网络连接。它利用了select、poll、epoll等机制,能够同时监视多个描述符(fd),一旦某个描述符就绪(读/写/异常),就能通知程序进行相应的读写操作。

这种技术可以避免大量的无用操作,因为在空闲的时候,会将当前线程阻塞掉。当有一个或多个流有I/O事件时,就从阻塞态中唤醒,程序会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),依次顺序的处理就绪的流。

IO多路复用可以高效的处理多个连接请求(尽量减少网络IO的时间消耗),让单个线程能同时处理多个客户端的连接。在Redis中,由于内存内的操作不会成为性能瓶颈,所以IO多路复用可以让Redis具有很高的吞吐量。

8.2.2 原理

IO多路复用(IO Multiplexing)的工作原理是利用一种机制同时监视多个文件描述符,以查看它们是否就绪,然后进行读写操作。这种技术允许多个连接同时被处理,而不需要为每个连接创建一个新的线程或进程。它是一种非阻塞的IO模型,通过在单个线程中处理多个连接来提高效率和减少资源消耗。

  1. 注册:将要监视的文件描述符(sockets)注册到IO多路复用函数中(例如,select、poll或epoll)。

  2. 监视:IO多路复用函数开始监视所有注册的文件描述符。

  3. 检查:IO多路复用函数定期检查每个文件描述符的状态,查看它们是否准备好进行读或写操作。

  4. 处理:当某个文件描述符就绪时,IO多路复用函数会通知应用程序,应用程序可以执行相应的读或写操作。

  5. 轮询:应用程序可以在一个循环中轮询所有文件描述符,检查它们的状态并执行相应的操作。

8.3 Redis vs Memcached

Memcached是高性能的分布式内存缓存服务器。使用目的一般是从缓存中读取数据,减少数据库访问次数,提高动态web应用速度。

  1. Redis相比memecache,拥有更多的数据结构和支持更丰富的数据操作。

  2. 内存使用率对比,Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于memecache。

  3. 性能对比:Redis只使用单核,memecache使用多核。

  4. Redis支持磁盘持久化,memecache不支持。

  5. Redis支持分布式集群,memecache不支持。

8.4 消息队列

8.4.1 概念

消息队列(Message Queue)的主要能力应该是解耦和削峰。

  • 在有了 mq 后,producer 不需要过分关心 consumer 的身份信息,只需要把消息按照指定的协议投递到对应的 topic 即可。
  • producer 在处理请求时,只需要把消息投递到 mq 即可认为流程处理结束,相比于同步请求下游,整个流程会更加轻便灵活,拥有更高的吞吐量。
  • 因为有 mq 作为缓冲层. 下游 consumer 可以设定好合适的消费限流参数,按照指定的速率进行消费,能够在很大程度上对 consumer 起到保护作用。

流程类型

push型:mq主动将消息推送到consumer。

优:实时性强,契合发布/订阅模型

缺:对下游consumer保护力度不够。(削峰不太好)

pull型:consumer主动从mq拉取消息

优:consumer有主动权,选择在合适时机消费。

缺:实时性弱。

8.4.2 Redis Channel

Redis Channel 是一种消息传递机制,允许发布者向特定频道发布消息,而订阅者则通过订阅频道实时接收消息。

Redis Channel 的消息传输是通过 Redis PUB/SUB 模型实现的。发布者使用 PUBLISH 命令将消息发送到指定的频道,订阅者使用 SUBSCRIBE 命令订阅指定频道。值得注意的是,频道的主要作用是实现实时消息传递,频道信息并不存储在数据库中,而是在内存中动态生成,所以在 Redis 重启后信息将消失,如果要存储频道信息,需要引入另外的方案。

8.4.3 Redis Stream

Redis Stream 是 Redis 5.0 版本新增加的数据结构。

Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。

而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

Redis Channel vs Redis Stream

两者均可以完成消息队列的实现,而Redis Stream相比Channel,可以进行消息的持久化存储,提供主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

8.5 Redis机制

8.5.1 Redis过期删除策略

Redis所有的数据结构都可以设置过期时间,时间一到,这些数据就会变成过期数据,这个时候就需要进行删除。在redis中有3种过期数据删除策略:惰性删除和定期删除及定时删除

定时删除

定时删除对每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。

优点:立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。对内存来说是非常友好的。

缺点: 立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力。 定期删除

定期删除策略每隔一段时间执行一次删除过期键操作并通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。定时任务的发起的频率由redis.conf配置文件中的hz来进行配置,Redis 默认每 1 秒运行 10 次,也就是每 100 ms 执行一次,每次随机抽取一些设置了过期时间的 key(这边注意不是检查所有设置过期时间的key,而是随机抽取部分),检查是否过期,如果发现过期了就直接删除。建议不要将这个值(hz)设置超过 100,否则会对CPU造成比较大的压力。

定期清理的两种模式:

  1. SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的 hz 选项来调整这个次数

  2. FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms

定期删除注意事项:

  1. 如果删除操作执行次数过多、执行时间太长,就会导致占用大量cpu资源去进行删除操作。

  2. 如果删除操作次数太少、执行时间短,就会导致内存资源被持续占用,得不到释放。

优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。

缺点:难以确定删除操作执行的时长和频率。

惰性删除

惰性删除不会去主动删除数据,而是在访问数据的时候,再检查当前键值是否过期,如果过期则执行删除并返回 null 给客户端,如果没有过期则返回正常信息给客户端。

优点:对 CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。

缺点:内存泄漏,就是一个键已经过期,如果我们一直不去访问它,然后的话让这个键仍然保留在redis中,也就是意味着这个过期键不被删除,它所占用的内存就不会释放。因此对于内存是很不友好的, 除非我们手动执行FLUSHDB(用于清空当前数据库中的所有 key)。

删除策略 特点 对CPU资源的利用 总结
定时删除 节约内存,无占用 不分时段占用CPU资源,频度高。 时间换空间
定期删除 折中,定期清理内存。 延时进行,CPU利用率高。 随机抽查,重点抽查。
惰性删除 内存占用严重 每次查询耗费CPU资源。 拿空间换时间

8.5.2 内存淘汰机制

当内存被占满(到达所配置的最大内存占用空间后),需要进行数据的淘汰。Redis提供8种内存淘汰策略。

noeviction(默认) 不删除任何数据,拒绝写入操作并返回Out of Memory错误。

alllkeys-lru 从所有key中使用LRU算法(最近最少使用)

allkeys-lfu 从所有key中使用LFU算法(最不常用算法 4.0新增)

volatile-lru 从设置了过期时间中key中使用LRU算法

volatile-lfu 从设置了过期时间中key中使用LFU算法

allkey-random 从所有key中随机淘汰

volatile-random 从设置了过期时间中key中随机淘汰

volatile-ttl 淘汰距离过期时间最近的

注意: 当使用 volatile-lru、volatile-lfu、volatile-random、volatile-ttl 这四种淘汰策略时,如果没有 key 可以淘汰,则和 neoviction 一样返回错误。

何时应该选择何种淘汰策略?

根据应用程序的访问模式,选择正确的淘汰策略很重要,但是你可以在程序运行时重新配置策略,并使用 Redis 的 info 命令 输出来监控缓存未命中和命中的数量,以调整设置。

经验上来讲:

使用 allkeys-lru 策略场景:

  1. 当你期望元素的子集将比其他元素更频繁地被访问时,比如幂律分布,20%的数据占有80%的使用次数;

  2. 当你不确定使用哪种策略时。

使用 allkeys-random 策略场景:

  1. 当你有一个循环访问,其中所有 key 进行会被连续地访问;

  2. 当你希望所有 key 的分布比较均匀。

使用 volatile-ttl 策略场景:

  1. 当你大部分缓存都设有不同的 ttl 值,向 Redis 提供过期候选的提示时。

8.5.3 Redis持久化机制

Redis 的持久化指的是将内存中的数据持久化,Redis 服务器重启或宕机时能够恢复数据。Redis 支持两种持久化方式:RDB 和 AOF。

RDB持久化

RDB 持久化方式会将内存中的数据以快照的形式写入到磁盘上,保证了 Redis 服务器重启后数据不丢失。在执行 RDB 持久化时,Redis 会触发保存操作,并创建一个子进程来进行数据快照的写入操作。当快照写入完成后,Redis 会将生成 RDB 文件的时间戳、版本信息等元数据信息保存到服务器状态中,表示 RDB 持久化操作已经完成。

RDB 持久化方式分为两个部分:Snapshot 和 Save。其中 Snapshot 是指 Redis 的内存数据集,而 Save 则是将 Snapshot 持久化到硬盘上(即将内存数据快照写到 RDB 文件中)。以下是 RDB 持久化的流程:

  1. 触发保存操作;

    Redis 提供了多种触发保存操作的方式,如调用 SAVE 或 BGSAVE 命令、设置 SAVE 配置选项等。当触发保存操作时,Redis 会开始执行 RDB 持久化操作。

  2. 创建子进程;

    由于 Redis 是单线程的,所以在执行持久化操作时需要fork一个子进程来进行操作,防止主进程被阻塞。

  3. 将数据写入临时文件

    子进程会遍历整个内存数据集快照,并将快照写入到一个临时文件中,这个过程中内存数据集可以继续处理命令请求。

  4. 移动临时文件到目标文件

    当子进程完成快照写入操作后,会将临时文件移动到目标文件,这个过程中 Redis 会使用原子操作来保证数据的完整性和一致性。

  5. 完成持久化操作

    当目标文件生成之后,Redis 会将生成 RDB 文件的时间戳、版本信息等元数据信息保存到服务器状态中,表示 RDB 持久化操作已经完成。

SAVE & BGSAVE

Redis 的 SAVE 和 BGSAVE 命令都是用来进行 RDB 持久化的命令,它们的作用和用法不太一样。

1)SAVE 命令

SAVE 命令会在当前 Redis 进程执行期间阻塞所有客户端请求,直到 RDB 持久化完成为止。这个命令适合用于小规模数据集的保存操作,因为它会占用 Redis 主进程,可能会造成服务暂停的情况。

2)BGSAVE 命令

BGSAVE 命令会创建一个子进程,用于执行 RDB 持久化操作,然后主进程可以继续响应客户端请求。这个命令适合用于大规模数据集的保存操作,因为它不会阻塞 Redis 主进程,不会影响服务的正常运行。

需要注意的是,在执行 BGSAVE 命令时,由于子进程需要遍历整个内存数据集进行快照写入操作,可能会占用大量 CPU 和内存资源,导致 Redis 服务器的性能下降。因此,建议在系统空闲时执行 BGSAVE 命令,并设置适当的 RDB 文件大小和保存规则,以保证数据的安全性和服务的稳定性。

另外,如果 BGSAVE 命令执行失败(如磁盘空间不足等情况),Redis 会记录错误日志,并停止执行 RDB 持久化操作。如果需要强制执行 RDB 持久化操作,可以使用 SAVE 命令或手动删除旧的 RDB 文件来释放磁盘空间。

在这里插入图片描述

AOF持久化

快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化,将修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间fsync到磁盘)

AOF打开时,redis执行的每个写操作都被记录下来,并追加到AOF文件中。AOF文件是一个日志文件,包含了在服务器上执行写操作的所有指令。当redis服务器需要恢复时,它能够通过读取AOF文件来还原出命令历史,重建完整的数据状态。这种技术称为命令回放,命令回放交换和被还原在AOF文件中的操作的属于服务器的已知落后的输出。期间,服务器不再接收任何输入,而是尝试重新执行它之前已经执行过的适量输出的效果。

AOF存在三种不同的策略:

  1. always(每个命令立刻输入)

    对于每个redis对服务器执行的命令都立即将其内容同步到aof文件中。这种方法能够保证非常高的数据安全性,因为aof文件总是包含最新的数据,但性能相对较低,可能会对性能造成一定的影响。

  2. everysec(每秒钟同步一次)

    这种方式每隔一秒钟将所有未同步的命令同步到磁盘上的aof文件中,这样可以减少硬盘io,提高性能和安全性的平衡。

  3. no (让操作系统来决定何时进行同步)

    当使用no方式时,redis会把aof缓冲区中的每条消息都直接交给系统内核来处理。内核再根据运行状态(包括机器负载、各进程等待时间等)来进行下一步操作。相对于其他两种,no方式在性能上有很大优势,但也存在最小程度的数据不可恢复风险。

RBD vs AOF

在这里插入图片描述

Redis 4.0 混合持久化

重启 Redis 时,我们很少使用 RDB来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 RDB来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。 AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。 于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的AOF 全量文件重放,因此重启效率大幅得到提升。 简而言之:检查点重构+日志重放。每次AOF重写时将已经写入的日志重构成RDB快照,然后续写日志。从上次重写的地方开始可以直接利用RDB快照重构,然后余下的部分利用日志回放。

8.5.4 缓存穿透

查询一个不存在的数据,因为mysql查询不到数据,所以不会直接写入缓存,就会导致每次请求都去查数据库。(恶意攻击者常用此手段DoS服务器IO资源)

解决方法-缓存空数据

查询返回的数据为空,仍把这个空结果进行缓存;

优点:实现简单。
缺点:①如果有大量查询的数据都不存在,则redis中会缓存大量空数据,这会消耗内存(这里可以给缓存添加一个TTL,减少内存消耗);②如果原先查的数据不存在,但是后来数据库中又添加上了,可能存在数据不一致的问题。

解决方法-布隆过滤器

在这里插入图片描述

优点:内存占用较少,没有多余key。
缺点:①实现复杂;②存在误判。

布隆过滤器主要是用于检索一个元素是否在一个集合中,可以使用redisson实现布隆过滤器。 它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,根据计算结果找到对应数组下标,然后把数组中原来的0改为1,这样的话,三个数组的位置就能表明一个key的存在。查找的过程也是一样的。 由于hash过程没有冲突处理,所以有一定的的误判率,但是,只可能是不存在被误判为存在,而不可能是存在被误判为不存在。 原因显而易见,由hash性质决定。

8.5.5 缓存雪崩

Redis缓存雪崩是指在某个特定时间段,缓存中的大部分数据都过期失效,导致大量的请求直接访问数据库,造成数据库压力过大,甚至引起数据库崩溃的情况。

原因

  1. 缓存数据同时过期;

    大量的缓存数据在同一时间段过期,例如由于误配置或由于某些缓存服务器的故障,缓存数据无法及时更新,那么当有大量请求访问这些数据时,它们将直接落到数据库上,导致数据库请求量骤增,产生压力。

  2. 缓存服务器宕机;

    缓存服务器遇到故障或宕机,所有请求将无法从缓存中获取数据,强迫它们直接访问数据库。由于缓存未能发挥作用,数据库将面临大量请求,导致性能下降,并可能导致数据库崩溃。

解决方案

  1. 设置合理的缓存过期时间 缓存过期时间的设置需要根据业务需求和数据的变化频率来确定。对于不经常变化的数据,可以设置较长的过期时间,以减少对数据库的频繁访问。对于经常变化的数据,可以设置较短的过期时间,确保缓存数据的实时性。 注意的是,缓存过期时间设置过长可能导致数据的实时性降低,而设置过短可能增加缓存失效和数据库压力。因此,需要根据具体应用场景和需求来综合考虑,进行合理的设置。

  2. 使用热点数据预加载 预先将热点数据加载到缓存中,并设置较长的过期时间,可以避免在同一时间点大量请求直接访问数据库。可以根据业务需求,在系统启动或低峰期进行预热操作,将热点数据提前加载到缓存中。 热点数据预加载可以提升系统的性能和响应速度,减轻数据库的负载。要注意的是,预加载操作可能会占用系统资源,因此需要合理安排预加载执行的时间和频率,避免对系统正常业务的影响。另外,需要根据实际情况监控和调整预加载策略,以保持缓存数据的实时性和准确性。

  3. 缓存数据分布均衡 将缓存数据进行分散存储,可以使用一致性哈希算法或数据分片来将缓存数据分散存储在多个缓存服务器上,避免将所有数据集中存储在同一台缓存服务器上。这样可以提高系统的容错性,避免某个缓存服务器故障导致大量的缓存失效。通过合理的数据分布策略和动态的节点管理,可以确保缓存数据在不同节点之间均衡分布,提高系统的性能和可扩展性。

  4. 使用多级缓存架构 使用多级缓存架构可以提高系统的性能和容错性。内存缓存(如Redis)可以提供快速的数据访问能力,而分布式缓存(如Memcached)可以通过多台服务器组成集群,提高系统的可用性。可以根据数据的访问频率和重要程度,将数据存储在不同级别的缓存中。 使用多级缓存架构可以根据数据的访问频率和重要性,将数据存储在不同级别的缓存中,以提高数据访问的速度和稳定性。同时,通过合理的缓存策略和同步机制,保证多级缓存中的数据的实时性和一致性。综合考虑业务需求和技术条件,可以选择适合的多级缓存架构来提升系统的性能和用户体验。

  5. 缓存故障转移和降级策略 当缓存服务器发生故障或宕机时,需要有相应的故障转移和降级策略。可以通过监控系统来及时发现缓存故障,并进行自动切换到备份缓存服务器。同时,可以实现降级策略,当缓存失效时,系统可以直接访问数据库,保证系统的可用性。 通过缓存故障转移和降级策略,可以保证系统在缓存不可用或故障的情况下仍然可以正常运行,提高系统的稳定性和容错性。

8.5.4 缓存击穿

缓存击穿就是大量请求同时查询一个key(或一批)时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去,也就是热点key突然都失效了,MySQL承受高并发量

简单说就是:热点key突然失效,导致MySQL被暴打

解决方案

  1. 设置热点数据的永不过期策略:对于一些非常热门的数据,可以将其缓存时间设置为永不过期,这样可以避免缓存失效导致的击穿问题。但需要注意,这种方式可能会导致缓存数据不及时更新的问题。

  2. 互斥更新,采用双检加锁机制。即设置key值为独占资源,串行执行所有请求。(由于第一个请求查回key值后会缓存至Redis,此时其他请求直接缓存命中走人,无需在请求数据库。)

8.5.5 缓存预热

缓存预热是指在系统启动或者高峰期之前,提前将数据加载到缓存中,避免在用户请求的时候,先查询数据库(这样第一个查询的人就会比较慢),再把查询结果回写到redis当中去。

ps. 8.5.4缓存穿透中的布隆过滤器在此时被构建。

在这里插入图片描述

8.5.6 缓存降级

缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;

警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;

错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;

严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

8.6 Redis缓存和数据库的一致性

8.6.1 问题

如何保证 Redis 缓存和数据库一致性?

  • 到底是更新缓存还是删除缓存?
  • 到底选择先更新数据库,再删除缓存,还是先删除缓存,再更新数据库?

8.6.2 缓存的引入

前期业务正处在开始阶段,流量非常小,当客户端请求过来,无论是读请求还是写请求,直接操作数据库就可以,前期架构框架如下图。

在这里插入图片描述

但是随着业务量的增长,项目请求量越来越大,这时如果每次都从数据库中读数据,就会出现大问题了。这个时候我们项目通常都会引入缓存来提高读性能,架构框架如下图。

在这里插入图片描述

这个实现方式如果有请求过来,所有读请求都可以直接从缓存中读取到数据,不需要再查数据库,性能非常高。

但是会存在以下2点问题:

  1. 不设置过期时间,不经常访问的数据还存在缓存中

  2. 因为是定时执行同步数据,会导致缓存和数据库的数据不一致的问题(看任务执行的频率)

这种方式适合数据量小,对数据一致性要求不高的业务场景。

想要缓存利用率最大化,我们很容易想到的方案是,缓存中只保留最近访问的热数据。具体要怎么做呢?如下几点:

  • 写请求只写数据库
  • 读请求首先读缓存,如果缓存中不存在,再从数据库中读取,并更新到缓存
  • 写入缓存中的数据,都设置失效时间
在这里插入图片描述

8.6.3 缓存一致性问题

当数据发生更新时,我们不仅要操作数据库,还要一并操作缓存。具体操作就是,修改一条数据时,不仅要更新数据库,也要连带缓存一起更新。

如果缓存和数据库都更新的话,就会存在以下两个问题:

  • 是先更新数据库还是后更新缓存
  • 是先更新缓存还是后更新数据库

异常导致的一致性问题

  1. 先更新数据库,后更新缓存 首先执行数据库更新的操作并且成功了,这时再去更新缓存并且失败了,那么此时数据库中是最新的值,而缓存中还是旧的数据值。如果一个读请求过来,首先读取缓存中的数据,这时都是旧值,只有当缓存过期失效后,才能重新在数据库中得到新的值。

  2. 先更新缓存,后更新数据库 首先执行缓存更新的操作并且成功了,这时再去更新数据库并且失败了,那么此时缓存中是最新的值,而数据库中还是旧的数据值。虽然此时读请求可以命中缓存,拿到正确的值,但是缓存过期失效以后就会从数据库中读取到旧值,重新同步缓存也是这个旧值。

并发导致的一致性问题

  1. 线程 A 更新数据库(X = 1)
  2. 线程 B 更新数据库(X = 2)
  3. 线程 B 更新缓存(X = 2)
  4. 线程 A 更新缓存(X = 1)

8.6.4 解决方案

缓存单删

即业界俗称的缓存删除模式。在更新数据前先删除缓存;然后再更新库,每次查询的时候发现缓存无数据,再从库里加载数据放入缓存。

优点

  • 此种实现方案简单
  • 无需依赖三方中间件
  • 缓存中的数据基本能和库里的数据保持一致

缺点

  • 缓存逻辑和正常业务逻辑耦合在一起
  • 在高并发的读流量下,还是会存在缓存和库里的数据不一致。见下图
在这里插入图片描述

延时双删

延迟双删其实是为了解决缓存单删,在高并发读情况下,数据不一致的问题。具体过程为: 操作数据前,先删除缓存;接着更新DB;然后延迟一段时间再删除缓存

优点

1、技术架构上简单
2、不依赖三方中间件
3、操作速度上挺快的,直接操作DB和缓存
缺点

1、落地难度有点大,主要是延迟时间太不好确认了
2、缓存操作逻辑和业务逻辑进行了耦合

极端情况仍有问题:

在这里插入图片描述

定时+增量更新

定时更新+增量查询:主要是利用库里行数据的更新时间字段+定时增量查询

具体为:每次更新库里的行数据,记录当前行的更新时间;然后把更新时间做为一个索引字段。定时任务:会每隔5秒钟(间隔时间可自定义);把库里最近更新5秒钟的数据查询出来;然后放入缓存,并记录本次查询结束时间。

优点

1、实现方案,和架构很简单
2、也能把缓存逻辑和业务逻辑进行解耦

缺点

1、数据库里的数据和缓存中数据,会在极短时间内,存在不一致,但最终会是一致的。这个极短的时间,取决于定时调度间隔时间,一般在秒级。
2、如果是分库分表的业务,编写这个查询逻辑,估计会稍显复杂

监听binlog+MQ

通过监听数据库(比如mysql binlog);通过binlog把数据库数据的更新操作日志(比如insert,update,delete),采集到后,通过MQ的方式,把数据同步给下游对应的消费者;下游消费者拿到数据的操作日志并拿到对应的业务数据后,再放入缓存。

在这里插入图片描述 优点

1、把操作缓存的代码逻辑,从正常的业务逻辑里解耦出来;业务代码更加清爽和简洁,两者互不干扰和影响,独立发展。用非人类的话说,减少业务代码侵入性
2、曾经有幸在大厂里实践过此种方案,速度还贼快,虽然从库到缓存经过了类canal和mq中间件,但基本上耗时都是在毫秒级,99.9%都是10毫秒内能完成库里的数据和缓存数据同步(大厂的优势出来了)

缺点

1、技术方案和架构,非常复杂
2、中间件的运维和维护,是个不小的工作量
3、由于引入了MQ需要解决引入MQ后带来的问题。比如数据乱序问题:同一条数据先发后至,后发先至的到达消费者后,从而引起的MQ乱序消费问题,但一般都能解决。

总结

在这里插入图片描述

8.7 主从复制

8.7.1 概念

Redis 的主从复制是指将一个 Redis 实例(称为主节点)的数据复制到其他 Redis 实例(称为从节点)的过程。主从复制可以实现数据备份、读写分离、负载均衡等功能。

主机数据更新后根据配置和策略,自动同步到从机的 master/slave 机制,Master 以写为主,Slave 以读为主。主少从多、主写从读、读写分离、主写同步复制到从。

作用(为什么要主从复制?)

1.数据的热备份 2.故障恢复:在主服务器挂掉的时候,从服务器可以顶替过来 3.负载均衡:读写分离,写数据可以主服务器来做,读操作从服务器来操作 备注:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

8.7.2 主从复制方式

在 Redis 主从复制中,主节点负责接收客户端的写操作,并将其同步到从节点。从节点只能接收读操作请求,不能进行写操作。主节点将数据同步到从节点的方式有两种:

全量复制 主节点将所有数据发送给从节点进行复制,适用于从节点第一次复制数据或者从节点数据丢失需要重新复制的情况。

在这里插入图片描述

全量复制是非常重型的操作的具体表现在下面几点:

(1)主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的

(2)主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗

(3)从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行bgrewriteaof,也会带来额外的消耗

ps.全量复制走的是AOF持久化流程

增量复制 主节点只发送最新的修改数据给从节点进行复制,适用于从节点已经复制过数据,只需要同步最新数据的情况。 在这里插入图片描述

过程如下:

首先,从节点根据当前状态,决定如何调用psync命令:

1
2
1.如果从节点之前未执行过slveof或最近执行了slaveof no one,则从节点发送命令为psync ? -1,向主节点请求全量复制;
2.如果从节点之前执行了slaveof,则发送命令为psync <runid> <offset>,其中runid为上次复制的主节点的runid,offset为上次复制截止时从节点保存的复制偏移量。

复制

其次,主节点根据收到的psync命令,及当前服务器状态,决定执行全量复制还是部分复制:

1
2
3
4
5
6
7
8
9
10
1.如果主节点版本低于Redis2.8,则返回-ERR回复,
此时从节点重新发送sync命令执行全量复制;
2.如果主节点版本够新,且runid与从节点发送的runid相同,
且从节点发送的offset之后的数据在复制积压缓冲区中都存在,
则回复+CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可;
3.如果主节点版本够新,但是runid与从节点发送的runid不同,
或从节点发送的offset之后的数据已不在复制积压缓冲区中(在队列中被挤出了),
则回复+FULLRESYNC <runid> <offset>,表示要进行全量复制,
其中runid表示主节点当前的runid,offset表示主节点当前的offset,
从节点保存这两个值,以备使用。

Redis主从复制 vs Redis集群

Redis 的主从复制和 Redis 集群都是实现高可用性的方式,但它们的实现方式和应用场景有所不同。

  1. 主从复制是将一个 Redis 实例(主节点)的数据复制到多个 Redis 实例(从节点)中,可以实现数据的备份和读写分离。主节点负责写操作,从节点负责读操作,可以提高读写性能。但是主从复制不能扩展写性能,因为所有的写操作都需要在主节点上执行。
  2. Redis 集群是将多个 Redis 实例组成一个集群,每个实例负责部分数据,可以实现数据的分片存储和负载均衡。不同的实例负责不同的数据,可以提高写操作的性能。同时, Redis 集群还可以实现自动故障转移,当某个实例出现故障时,集群会自动将该实例的数据迁移到其他实例上,保证数据的可用性。

因此,主从复制适用于读多写少的场景,而 Redis 集群适用于读写都比较频繁的场景。如果需要提高读写性能和数据的可用性,可以采用主从复制和 Redis 集群的组合方式。

参考文献

Redis持久化机制看这一篇就够了!-CSDN博客

字节面试杂谈——MySQL、Redis_redis数据库使用的memery数据引擎嘛-CSDN博客

Redis知识详解(超详细)-CSDN博客

redis面试题总结(附答案)-CSDN博客

万字长文解析如何基于Redis实现消息队列 - 知乎 (zhihu.com)

Redis Stream | 菜鸟教程 (runoob.com)

深入了解下 「Redis 发布/订阅机制」的原理与实战运用_redis订阅发布的实际应用-CSDN博客

Redis实战(5)——Redis实现消息队列_redis消息队列实现-CSDN博客

【Redis】IO多路复用机制_redis io多路复用-CSDN博客

「Clickhouse系列」分布式表&本地表详解_51CTO博客_clickhouse 分布式表

ClickHouse之存储格式揭秘_哔哩哔哩_bilibili

数据库的三大设计范式和BCNF_bcnf范式-CSDN博客

BCNF 范式详解-CSDN博客

聚簇索引和非聚簇索引-CSDN博客

当前读和快照读-CSDN博客

悲观锁和乐观锁的区别_乐观锁和悲欢锁的区别-CSDN博客

CAP原则_百度百科 (baidu.com)

数据库并发控制、事物的四大特性、原子性、一致性、隔离性、持久性,简称ACID、事物的概念、数据概念(脏读,不可重复读,幻读)、封锁协议、一级封锁协议、二级封锁协议、三级封锁协议、最强封锁协议_数据库 原子性 隔离性-CSDN博客

【Redis篇】Redis缓存之缓存穿透-CSDN博客

Redis--缓存雪崩及解决方案_redis缓存雪崩-CSDN博客

Redis 之 缓存预热 & 缓存雪崩 & 缓存击穿 & 缓存穿透-CSDN博客

面试必问:缓存预热、降级?-腾讯云开发者社区-腾讯云 (tencent.com)

RAID0、RAID1、RAID5、RAID6、RAID10、RAID50的异同与应用-腾讯云开发者社区-腾讯云 (tencent.com)

如何保证 Redis 缓存与数据库双写一致性?看这篇就够了_redis如何保证缓存和数据库一致性-CSDN博客

redis 如何保证缓存和数据库一致性_reidis如何保证数据和缓存一致性-CSDN博客

本文作者:Kalzn
本文链接:http://kalzncc.github.io/2024/04/09/137570823/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
除此之外,本文不做正确性担保,本人不对产生的问题负责。
×