Redis Cluster
构建缓存集群
Mysql 大数据量的问题解决思路:
数据量太大, 查询慢 --> 数据分片 ( 存档历史数据, 分库分表 )
并发量太大 --> 增加实例数 ( 读写分离 )
提供高可用, 防止数据库宕机 --> 主从复制 (增加从节点,主节点宕机的时候用从节点)
Redis
从 3.0 版本
开始,提供了官方的集群支持
,也就是 Redis Cluser
。
Redis Cluster 相比于单个节点的 Redis
,能保存更多的数据,支持更多的并发,并且可以做到高可用
在单个节点故障的情况下,继续提供服务。
构建一个 生产系统 可用的 Redis 缓存集群:
Redis Cluster
如何分片
它引入了一个“槽(Slot)”
的概念,这个槽就是哈希表
中的哈希槽
槽
是 Redis 分片的基本单位
,每个槽里面包含一些 Key
每个集群
的槽数
是固定的 16384(16 * 1024)
个,每个 Key 落在哪个槽中也是固定的,计算方法是:
HASH_SLOT = CRC16(key) mod 16384
先计算 Key 的 CRC 值,
然后把这个 CRC 之后的 Key 值直接除以 16384,
余数就是 Key 所在的槽
哈希分片算法
槽
如何存放到具体的 Redis 节点
上: 分槽的方法--查表法
槽 和 Redis节点 的 映射关系保存在集群的每个 Redis 节点上,
集群初始化的时候,Redis 会自动平均分配这 16384 个槽,也可以通过命令来调整
Redis Cluster 调用流程:
客户端 连接集群的任意一个节点 (请求一个 Key) --> 计算出这个 Key 在哪个槽(哈希分片算法) -->
--> 再查询槽和节点的映射关系 (槽和Redis节点的映射关系保存在集群的每个 Redis 节点上)
--> 找到数据所在的真正节点(本节点--直接返回, 其他节点--给客户端返回一个重定向的命令)
扩容与槽的迁移:
Redis Cluster 可以通过水平扩容
来增加集群的存储容量
每次往集群增加节点的时候,需要从集群的那些老节点中,搬运一些槽到新节点 (槽数是固定的
)
可以手动指定哪些槽迁移到新节点上
也可以利用官方提供的 redis-trib.rb脚本 来自动重新分配槽,自动迁移
Redis Cluster
解决高可用 –增加从节点
,做主从复制
Redis Cluster 支持给每个分片 槽
增加一个或多个从节点
每个从节点在连接到主节点上之后,会先给主节点发送一个 SYNC 命令,请求一次全量复制, 也就是把主节点上全部的数据都复制到从节点上。
全量复制完成之后,进入同步阶段,主节点会把刚刚全量复制期间收到的命令,以及后续收到的命令持续地转发给从节点。
直接 转发客户端发来的更新数据命令,来实现主从同步
Redis Cluster
应对高并发
一般来说,Redis Cluster 进行了分片之后
,每个分片都会承接一部分并发的请求,
加上 Redis 本身单节点的性能就非常高, 所以大部分情况下不需要再像 MySQL 那样做读写分离
来解决高并发的问题
默认情况下,集群的读写请求都是由主节点负责的,从节点只是起一个热备的作用
Redis Cluster 也支持读写分离,在从节点上读取数据
Redis Cluster 的基本原理:
Redis Cluster 不适合超大规模集群: (适合中小规模几个到几十个节点
这样规模的 Redis 集群)
主要原因 它采用了去中心化的设计 (流言 (Gossip) 协议)
好处 去中心化, 部署和维护就更简单,也能避免中心节点的单点故障
缺点 传播速度慢,并且是集群规模越大,传播的越慢
三种构建 Redis 集群的方式:
在客户端和 Redis 节点之间,还需要增加一层 代理服务
1 负责在客户端和 Redis 节点之间转发请求和响应。
客户端只和代理服务打交道,代理收到客户端的请求之后,再转发到对应的 Redis 节点上,
节点返回的响应再经由代理转发返回给客户端。
2 负责监控集群中所有 Redis 节点状态
如果发现有问题节点,及时进行主从切换。
3 维护集群的元数据
这个元数据主要就是集群所有节点的主从信息,以及槽和节点关系映射表。
优点
对客户端透明: 在客户端视角来看,整个集群和一个超大容量的单节点 Redis 是一样的
分片算法是代理服务控制的: 扩容也比较方便, 直接修改代理服务中的元数据完成扩容
缺点
增加了一层代理转发: 每次数据访问的链路更长了,必然会带来一定的性能损失
代理服务本身是集群的一个单点
- 定制客户端的方案: (性能更好)
把代理服务的寻址功能前移到客户端中去
客户端在发起请求之前,先去查询元数据
,就可以知道要访问的是哪个分片和哪个节点,
然后直连对应的 Redis 节点访问数据。
缓存 元数据
这个元数据服务仍然是一个单点: 但数据量不大,访问量也不大
保证 数据库的数据
和 redis缓存的数据
一致
分布式事务
对数据更新服务
有很强的侵入性
例如: 下单服务
为了更新缓存增加一个分布式事务
1 无论我们用哪种分布式事务,或多或少都会影响下单服务的性能
2 Redis 本身出现故障,写入数据失败,还会导致下单失败, 降低了下单服务性能和可用性
方案1:
消息队列
+更新订单缓存的服务
启动一个更新订单缓存的服务
,接收订单变更的 MQ 消息,然后更新 Redis 中缓存的订单数据
注意: 数据可靠性 (丢消息
)
MQ 集群,比如像 Kafka 或者 RocketMQ,都有高可用和高可靠的保证机制
只要正确配置,可以满足数据可靠性要求
使用
Binlog
实时更新 Redis 缓存
数据更新服务 只负责处理业务逻辑,更新 MySQL
更新缓存的服务 伪装成一个 MySQL 的从节点, 收 Binlog, 解析 Binlog 之后,可以得到实时的数据变更信息,然后根据这个变更信息去更新 Redis 缓存
整个缓存更新链路上 减少了一个收发 MQ 的环节
有很多开源的项目就提供了订阅和解析 MySQL Binlog
的功能,下面我们以比较常用的开源项目Canal
Canal的工作原理
:
Canal的使用
:
1 下载并解压:
wget https://github.com/alibaba/canal/releases/download/canal-1.1.4/canal.deployer-1.1.4.tar.gz
tar zvfx canal.deployer-1.1.4.tar.gz
2 配置 MySQL,我们需要在 MySQL 的配置文件中开启 Binlog
,并设置 Binlog 的格式为 ROW 格式
[mysqld]
log-bin=mysql-bin # 开启Binlog
binlog-format=ROW # 设置Binlog格式为ROW
server_id=1 # 配置一个ServerID
3 给 Canal 开一个专门的 MySQL 用户
并授权,确保这个用户有复制 Binlog 的权限
:
CREATE USER canal IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
4 重启 MySQL,确保所有的配置生效
# 重启后检查一下当前的 Binlog 文件和位置
show master status;
+----------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+----------------+----------+--------------+------------------+-------------------+
| bin-log.000018 | 154 | | | |
+----------------+----------+--------------+------------------+-------------------+
5 记录 File
和 Position
值, 配置 Canal, 让 Canal 连接到我们的 MySQL
# canal/conf/example/instance.properties
canal.instance.gtidon=false
# position info
canal.instance.master.address=127.0.0.1:3306
canal.instance.master.journal.name=bin-log.000018 # file
canal.instance.master.position=154 # position
canal.instance.master.timestamp=
canal.instance.master.gtid=
# username/password
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
canal.instance.defaultDatabaseName=test
# table regex
canal.instance.filter.regex=.*\\..
6 启动 Canal 服务
canal/bin/startup.sh
# 查看日志文件 canal/logs/example/example.log , 确保无异常正常启动
# Canal 服务启动后,会开启一个端口(11111)等待客户端连接,
客户端连接上 Canal 服务之后,可以从 Canal 服务拉取数据
每拉取一批数据,正确写入 Redis 之后,给 Canal 服务返回处理成功的响应。
如果发生客户端程序宕机或者处理失败等异常情况,Canal 服务没收到处理成功的响应,
下次客户端来拉取的还是同一批数据,这样就可以保证顺序并且不会丢数据。
7 更新服务逻辑
根据 CanalEntry.EventType 类型来分别处理
如果 MySQL 中的数据删除了,就删除 Redis 中对应的数据。
如果是更新和插入操作,那就调用 Redis 的 SET 命令来写入数据。
if (eventType == CanalEntry.EventType.DELETE) { // 删除
jedis.del(row2Key("user_id", rowData.getBeforeColumnsList()));
}
else if (eventType == CanalEntry.EventType.INSERT) { // 插入
jedis.set(row2Key("user_id", rowData.getAfterColumnsList()), row2Value(rowData.getAfterColumnsList()));
}
else { // 更新
jedis.set(row2Key("user_id", rowData.getAfterColumnsList()), row2Value(rowData.getAfterColumnsList()));
}
8 验证mysql 与 redis中数据的一致性
# 账户余额表插入一条记录
mysql> insert into account_balance values (888, 100, NOW(), 999);
# Redis 缓存
127.0.0.1:6379> get 888
"{\"log_id\":\"999\",\"balance\":\"100\",\"user_id\":\"888\",\"timestamp\":\"2020-03-08 16:18:10\"}"
对象存储
是(最简单的)
原生的分布式存储系统
对象存储
不仅有很好的大文件读写性能
,还可以通过水平扩展实现近乎无限的容量
,
并且可以兼顾服务高可用
、数据高可靠
这些特性
原生分布式存储系统(分布式存储集群)
近乎无限的存储容量;
超高的读写性能;
数据高可靠:
节点磁盘损毁不会丢数据;
实现服务高可用:
节点宕机不会影响集群对外提供服务。
MySQL、Redis 单机存储系统
对象存储 原生分布式存储系统
对象存储数据 如何保存大文件:
-
数据节点的集群:
存储节点,用于保存这些大文件
-
元数据(管理集群而存储的数据):
管理这些数据节点和节点中的文件,还需要一个存储系统保存集群的节点信息、文件信息和它们的映射关系
-
网关集群:
对外接收外部请求
对内访问元数据和数据节点
读写请求的流程:
对象读写请求后 -->> 网关 -->> 根据请求中的 Key,去元数据集群查找这个 Key 在哪个数据节点上
-->> 访问对应的数据节点读写数据 -->> 返回结果给客户端
对象读写请求后 -->> 网关 -->> 根据请求中的 Key,找这个 Key 的元数据 --> 根据元数据中记录的对象长度,计算出对象有多少块儿
-->> 分块儿并行处理 -->> 需要再去元数据中,找到它被放在哪个容器 (分片算法) -->> 找到容器之后,再去元数据中查找容器的 N 个副本都分布在哪些数据节点
-->> 访问对应的数据节点读写数据 -->> 返回结果给客户端
对象的拆分和保存
一般来说,对象存储中保存的文件都是图片、视频这类大文件
在对象存储中,每一个大文件
都会被拆成
多个大小相等
的块儿(Block)
拆分方法:
把文件从头到尾按照固定的块儿大小,切成一块儿一块儿,最后一块儿长度有可能不足一个块儿的大小,也按一块儿来处理
块儿的大小一般配置为几十 KB 到几个 MB 左右
容器(分片):
一般都会再把块儿聚合一下,放到块儿的容器里面
容器内的块儿数大多是固定的,所以容器的大小也是固定的
对象被拆成块儿之后,还是太过于碎片化了,如果直接管理这些块儿,会导致元数据的数据量会非常大,
也没必要管理到这么细的粒度
副本(物理存在于数据节点上):
每个容器都会有 N 个副本,这些副本的数据都是一样的
其中有一个主副本,其他是从副本,主副本负责数据读写,从副本去到主副本上去复制数据,保证主从数据一致
数据节点:
运行在服务器上的服务进程
拆分成块的目的:
1 提升读写性能 分散到不同的数据节点上,这样就可以(并行读写)
2 便于维护管理 (大小相等)
跨多系统的多份数据
实时同步
使用
Binlog
和MQ
构建实时数据同步系统
从 Canal
出来的 Binlog 数据
不能直接去写下游那么多数据库
1 写不过来
2 对于每个下游数据库,它可能还有一些数据转换和过滤的工作要做。
所以需要增加一个 MQ
来解耦上下游
1 Canal 从 MySQL 收到 Binlog 并解析成结构化数据之后,直接写入到 MQ 的一个订单 Binlog 主题
2 然后每一个需要同步订单数据的业务方,都去订阅这个 MQ 中的订单 Binlog 主题,消费解析后的 Binlog 数据。
3 在每个消费者自己的同步程序中,它既可以直接入库,也可以做一些数据转换、过滤或者计算之后再入库,这样就比较灵活了
保证数据同步的实时性(
大促时大数据量
)
大促的时候,数据量大、并发高、数据库中的数据变动频繁
顺着数据流向往下
订单库 数据库本身
Canal 和 MQ 没什么业务逻辑,性能都非常好
消费 MQ 的同步程序
注意(每个mysql实例不要并发写入):
保证数据同步过程中的 Binlog 是严格有序的,写到目标数据库的数据是正确的
对于每一个 MySQL 实例,整个处理链条都必须是单线程串行执行
1 根据下游同步程序的消费能力,计算出需要多少并发
2 设置 MQ 中主题的分区(队列)数量和并发数一致(MQ 可以保证同一分区内,消息不会乱序)
3 把具有因果关系的 Binlog 都放到相同的分区中去,保证同步数据的 因果一致性
Binlog 中订单号除以 MQ 分区总数,余数就是这条 Binlog 消息发往的分区号
因果一致性(Causal Consistency)
:
有因果关系的数据之间必须要 严格地保证顺序
,没有因果关系的数据之间的顺序是无所谓的
Canal 自带的分区策略就支持按照指定的 Key,把 Binlog 哈希到下游的 MQ 中去
数据库
不停机平滑切换
需要切换数据库的场景:
1 分库分表之后 需要从原来的单实例数据库迁移到新的数据库集群上
2 传统部署方式向云上迁移 需要从自建的数据库迁移到云数据库上
3 MySQL --> 分析类数据库 需要(线分析类的系统)更换成一些专门的分析类数据库,比如说 HBas
切换数据库需要注意:
不能长时间停服
不能丢失数据
实现
不停机
更换数据库
要保证,每执行一个步骤后,一旦出现问题,能快速地回滚到上一个步骤
1 把旧数据复制到新数据库中 (增加同步程序 -- Binlog)
2 DAO 层 支持
双写新旧两个库: 预留热切换开关 (切换 只写旧库、只写新库和同步双写)
读新旧两个库: 预留热切换开关,控制读旧库还是新库
3 上线新版本, 稳定后(验证数据一致性),开启双写(停掉 同步服务)
双写 先写旧库,再写新库,并且以写旧库的结果为准
对双写不一致的数据进行补偿
新库与旧库的数据可能会存在不一致的情况
停止同步程序和开启双写,这两个过程很难做到无缝衔接
二是双写的策略也不保证新旧库强一致
4 增加对比和补偿的程序, 稳定后(验证数据一致性), 读请求切到新库(类似灰度发布)
5 旧库下线
切换的完整流程:
上线同步程序,从旧库中复制数据到新库中,并实时保持同步;
上线双写订单服务,只读写旧库;
开启双写,同时停止同步程序;
开启对比和补偿程序,确保新旧数据库数据完全一样;
逐步切量读请求到新库上;
下线对比补偿程序,关闭双写,读写都切换到新库上;
下线旧库和订单服务的双写功能
点击流
数据的存储
现代的消息队列,本质上就是`分布式的流数据存储系统`
通常情况下数据量大
的数据
点击流数据
监控数据
日志数据
点击流数据
: App、小程序和 Web 页面上的埋点数据
(记录用户的行为,比如你打开了哪个页面,点击了哪个按钮,在哪个商品上停留了多久)
点击流数据
较 (订单、商品这类)业务数据
数据量会高出 2 ~ 3个数量级
每天产生的数据量 可能会超过 TB(1 TB = 1024 GB)级别,
经过一段时间累积 有些数据会达到 PB(1 PB = 1024 TB)级别
大数据技术
出现之前,没法保存和处理的,只能 通过抽样的方法
来凑合着做分析
使用
Kafka
存储海量原始数据
先存储再计算
不需要二次分发就可以同时给多个流和批计算任务提供数据;
如果计算任务出错,可以随时回滚重新计算;
如果对数据有新的分析需求,上线后直接就可以用历史数据计算出结果,而不用去等新数据。
对 保存原始数据的存储系统 要求很高:
1 有足够大的容量,能水平扩容
2 读写都足够快
跟得上数据生产的 写入速度
给下游计算提供低延迟的读服务
Kafka
提供“无限”的消息堆积能力,具有超高的吞吐量,可以满足我们保存原始数据的大部分要求
写入点击流数据
每个原始数据采集服务 (生产者) --->> kafka
下游的计算任务 (消费者订阅消息)
Kafka 性能高的原因
1.采用批处理的方式提升吞吐量
2.利用了磁盘文件顺序读写性能高的特点在设计存储
3.利用了操作系统的 PageCache 做缓存,减少 IO
4.采用零拷贝技术加速消费流程
Kafka 支持把数据分片,这个在 Kafka 中叫 Partition
,每个分片可以分布到不同的存储节点上
写入数据的时候,可以均匀地写到这些分片
理论上只要分片足够多,存储容量就可以是“无限”的
但是 单个分片 要落到某一个节点上(存储容量有限)
随着时间推移,单个分片总有写满的时候
受制于单节点的存储容量
,Kafka 实际能存储的数据容量并不是无限的
扩容分片 解决不了已有分片写满的问题
Kafka 不支持按照时间维度去分片
需要长时间(几个月 - 几年)
保存的海量数据
,就不适合用 Kafka 存储
HDFS
来存储
把原始数据写成一个一个文本文件
,保存到 HDFS
需要按照 时间 和 业务属性 来组织 目录结构和文件名,以便于下游计算程序来读取
click/20200808/Beijing_0001.csv
2020 年 8 月 8 日,从 北京地区用户 收集到的点击流数据的 第一个文件
HDFS
的吞吐量
是远不如Kafka
也就是说 要达到相同的吞吐能力
使用 HDFS
就要比使用 Kafka
,多用几倍的服务器数量
。
平均到每个节点上计算
Kafka 很容易达到 每秒钟大几百兆
HDFS 只能达到 百兆左右
优势
提供真正无限的存储容量 (水平扩容)
比 Kafka 更强的数据查询能力
Kafka 只能按照 时间 或者 位点 来提取数据
HDFS 配合 Hive 直接就可以支持用 SQL 对数据进行查询
性能比较差
查询能力强
分布式流数据存储
类似 Kafka 这种流存储的路线
高吞吐量
时序数据库(Time Series Databases)
仅有非常好的读写性能,还提供很方便的查询和聚合数据的能力
专注于 有时间特征并且数据内容都是数值的数据 ( 类似监控数据 )
海量数据加速查询
大量的原始数据 不能直接给业务使用
数据量太大
没有很好的数据结构和查询能力,来支持业务系统查询
一般的做法是,用流计算
或者是批计算
,把原始数据再进行一次或者多次的过滤、汇聚和计算
,把计算结果落到另外一个存储系统
中去,由这个存储再给业务系统提供查询支持
流计算 Flink、Storm 这类的 实时计算
批计算 Map-Reduce 或者 Spark 这类的 非实时计算
查询海量数据的系统,大多都是离线分析类系统
(类似于做报表的系统)
选择存储
1 数据量在 1 GB 量级以下
MySQL
2 数据量级已经超过 MySQL 极限
列式数据库,比如:HBase、Cassandra、ClickHouse (10GB 量级的数据查询基本上可以做到秒级 )
数据的组织方式 有限制
查询方式 不灵活
Elasticsearch(ES)-- 优先考
数据都存储在内存 大内存的服务器,硬件成本高
3 数据量级超过 TB 级的时候
(性能瓶颈已经是磁盘 IO 和网络带宽)
解决的办法
定期把数据聚合和计算好,然后把结果保存起来
需要时对结果再进行二次查询
根据业务对数据的查询方式,反推数据应该
使用什么存储系统、
如何分片,
如何组织
New SQL 数据库
New SQL
兼顾了 Old SQL 和 No SQL 的优点
Old SQL: 完整地支持 SQL 和 ACID,提供和 Old SQL 隔离级别相当的事务能力;
No SQL : 高性能、高可靠、高可用,支持水平扩容
New SQL
是新一代的分布式数据库,它具备原生分布式存储系统
高性能、高可靠、高可用和弹性扩容的能力,
同时还兼顾了传统关系型数据库
的 SQL 支持。
它还提供了和传统关系型数据库不相上下的、真正的事务支持
,具备了支撑在线交易类业务的能力
常用的 New SQL
:
Google 的 Cloud Spanner
Ali 的 OceanBase
CockroachDB
CockroachDB 小强数据库
1 最上层是 SQL 层 (执行器)
支持和关系型数据库类似的逻辑数据结构,比如说库、表、行和列这些逻辑概念
2 向下调用的是一个抽象的接口层 Structured Data API
3 Distributed, Monolithic KV Store 分布式的 KV 存储系统 (存储引擎)
Structured Data API 的实际实现
MySQL 存储引擎 InnoDB 基于文件系统的 B+ 树
Hive 和 HBase 存储引擎都是基于 HDFS 构建
CockroachDB 存储引擎 分布式 KV 存储
4 分片算法 是范围分片 查询友好
(Raft一致性协议 实现每个分片的高可靠、高可用和强一致)
(理论基础,是 复制状态机)
5 元数据 直接分布在所有的存储节点, 靠流言协议来传播
6 单机的存储引擎 直接使用 RocksDB 作为 KV 存储引擎
CockroachDB
实现的ACID
CockroachDB
提供了另外两种隔离级别,分别是:Snapshot Isolation (SI)
和 Serializable Snapshot Isolation (SSI)
` SSI 是 CockroachDB
默认的隔离级别`
与 MySQL 默认的隔离级别 RR
的对比:
隔离级别 | 脏读(DR, Dirty Read) | 不可重复读(NR, NotRepeatedRead) | 幻读 (PR, Phatom Read) | 写倾斜(Write Skew) |
---|---|---|---|---|
RR 可重复读 (Repeated Read) |
N | N | Y | N |
SI 快照 (Snapshot Isolation) |
N | N | Y | N |
SSI 串行快照 (Serializable Snapshot Isolation) |
N | N | N | N |
RR 这种隔离级别,可以很好地解决脏读和不可重复读的问题,虽然可能会产生幻读
SI 不会发生脏读、不可重复读,也不会发生幻读的情况,这个隔离级别似乎比 RR 还要好
但是 SI 会有写倾斜问题 (并发情况下)
写倾斜
一些“读-修改-写入”这样的事务
,写入的东西可能是由读的结果决定的(比如申请会议室,要先读会议室有没有被占用,再决定写入是否成功),
这时查询的结果可能是“幻读”,即这个查询结果已经被其它事务改变了,这时就会产生 写倾斜
update accountset balance = balance - 100 -- 在主卡中扣减100元
where id = ? and
(select balance from account where id = ?) -- 主卡余额
+ (select balance from account where id = ?) -- 附卡余额
>= 100; -- 主副卡余额之和必须大于100元
(可能 并发地去更新主副卡余额,把主副卡余额之和扣减为负数)
RocksDB
RocksDB
是 Facebook 开源的一个高性能持久化 KV 存储
越来越多的新生代数据库,都 选择 RocksDB
作为它们的存储引擎
CockroachDB RocksDB 作为存储引擎
MySQL 的亲兄弟 MariaDB 已经接纳了 MyRocks,作为它的一个可选的存储引擎
MongoDB、Cassandra 等等很多的数据库,都在开发基于 RocksDB 的存储引擎
KV 存储
Redis 内存缓存
vsRocksDB
RocksDB
采用了一个非常复杂的数据存储结构,并且这个存储结构采用了内存和磁盘混合存储方式
,
使用磁盘来保证数据的可靠存储,
并且利用速度更快的内存来提升读写性能。
或者说,RocksDB 的存储结构本身就自带了内存缓存
随机读写性能 redis 50 万次 / 秒
RocksDB 20 万次 / 秒
-
RocksDB
如何 保证数据持久化
+强性能
?存储系统的读写性能 –> 存储结构 (数据的组织方式)
高性能写入 – 顺序写
1 大多数存储系统 --> 快速查找 --> 树或者哈希表(存储结构)--> 写到固定的节点(随机写) 2 SSD 还是 HDD 顺序写的性能都要远远好于随机写 3 RocksDB 的数据结构 (LSM-Tree),让绝大多数写入磁盘的操作都是 顺序写
-
LSM-Tree
LSM-Tree
的全称是:The Log-Structured Merge-Tree
,是一种非常复杂的复合数据结构,它包含了
WAL(Write Ahead Log) 操作日志
跳表(SkipList)
一个分层的有序表(SSTable,Sorted String Table)
LSM-Tree 通过混合内存和磁盘内的多种数据结构
,将随机写
转换为顺序写
来提升写性能,
通过异步向下合并分层 SSTable 文件
的方式,让热数据的查找更高效,从而获得还不错的综合查找性能。
横向的实线,是内存和磁盘的分界线,上面的部分是内存,下面的部分是磁盘
1 数据写入:
写请求 -> 磁盘的 WAL 日志(图右 Log)(用于故障恢复)
-> 写入到内存中的 MemTable (按照 Key 组织的有序跳表(SkipList)) (平衡树有着类似的查找性能,但实现起来更简单一些)
-> MemTable 写满之后,就被转换成 Immutable MemTable (只读),然后再创建一个空的 MemTable 继续写
-> 后台线程,不停地把 Immutable MemTable 复制到磁盘文件中,然后释放内存空间 (每个 Immutable MemTable 对应一个磁盘文件) , 即 SSTable
-> SSTable 分成合并, 排序
tips : LSM-Tree 在处理写入的过程中,直接就往 MemTable 里写,并不去查找这个 Key 是不是已经存在了
MemTable 有一个固定的上限大小,一般是 32M
SSTable 被分为很多层,越往上层,文件越少,
越往底层,文件越多。
每一层的容量都有一个固定的上限,一般来说,下一层的容量是上一层的 10 倍
当某一层写满了,就会触发后台线程往下一层合并,数据合并到下一层之后,本层的 SSTable 文件就可以删除掉了。
合并的过程也是排序的过程,除了 Level 0(第 0 层,也就是 MemTable 直接 dump 出来的磁盘文件所在的那一层。)以外,每一层内的文件都是有序的,文件内的 KV 也是有序的,这样就比较便于查找了
2 数据的读取
1 先去内存中的 MemTable 和 Immutable MemTable 中找
2 再按照顺序依次在磁盘的每一层 SSTable 文件中去找,只要找到了就直接返回
天然形成一个非常有利于查找的情况:
越是被经常读写的热数据,它在这个分层结构中就越靠上,对这样的 Key 查找就越快