This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
<audio id="audio" title="15 | MySQL存储海量数据的最后一招分库分表" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/94/756366dc7668dc61c10b859f8965a994.mp3"></audio>
你好,我是李玥。
从这节课开始我们课程将进入最后一部分“海量数据篇”这节课也是我们最后一节主要讲MySQL的课程。解决海量数据的问题必须要用到分布式的存储集群因为MySQL本质上是一个单机数据库所以很多场景下不是太适合存TB级别以上的数据。
但是绝大部分的电商大厂它的在线交易这部分的业务比如说订单、支付相关的系统还是舍弃不了MySQL原因是**只有MySQL这类关系型数据库才能提供金融级的事务保证**。我们之前也讲过分布式事务,那些新的分布式数据库提供的所谓的分布式事务,多少都有点儿残血,目前还达不到这些交易类系统对数据一致性的要求。
那既然MySQL支持不了这么大的数据量这么高的并发还必须要用它怎么解决这个问题呢还是按照我上节课跟你说的思想**分片**也就是拆分数据。1TB的数据一个库撑不住我把它拆成100个库每个库就只有10GB的数据了这不就可以了么这种拆分就是所谓的MySQL分库分表。
不过,思路是这样没错,分库分表实践起来是非常不容易的,有很多问题需要去思考和解决。
## 如何规划分库分表?
还是拿咱们的“老熟人”订单表来举例子。首先需要思考的问题是分库还是分表分库呢就是把数据拆分到不同的MySQL库中去分表就是把数据拆分到同一个库的多张表里面。
在考虑到底是分库还是分表之前,我们需要先明确一个原则,**那就是能不拆就不拆,能少拆不多拆**。原因也很简单,你把数据拆分得越散,开发和维护起来就越麻烦,系统出问题的概率就越大。
基于这个原则我们想一下,什么情况下适合分表,什么情况下不得不分库?
那我们分库分表的目的是为了解决两个问题:
第一是数据量太大查询慢的问题。这里面我们讲的“查询”其实主要是事务中的查询和更新操作因为只读的查询可以通过缓存和主从分离来解决这个我们在之前的“MySQL如何应对高并发”的两节课中都讲过。那我们上节课也讲到过**解决查询慢,只要减少每次查询的数据总量就可以了,也就是说,分表就可以解决问题**。
第二,是为了应对高并发的问题。应对高并发的思想我们之前也说过,一个数据库实例撑不住,就把并发请求分散到多个实例中去,所以,**解决高并发的问题是需要分库的**。
简单地说,**数据量大,就分表;并发高,就分库**。
一般情况下,我们的方案都需要同时做分库分表,这时候分多少个库,多少张表,分别用预估的并发量和数据量来计算就可以了。
另外,我个人不建议你在方案中考虑二次扩容的问题,也就是考虑未来的数据量,把这次分库分表设计的容量都填满了之后,数据如何再次分裂的问题。
现在技术和业务变化这么快,等真正到了那个时候,业务早就变了,可能新的技术也出来了,你之前设计的二次扩容方案大概率是用不上的,所以没必要为了这个而增加方案的复杂程度。还是那句话,**越简单的设计可靠性越高**。
## 如何选择Sharding Key
分库分表还有一个重要的问题是选择一个合适的列或者说是属性作为分表的依据这个属性一般称为Sharding Key。像我们上节课讲到的归档历史订单的方法它的Sharding Key就是订单完成时间。每次查询的时候查询条件中必须带上这个时间我们的程序就知道三个月以前的数据查订单历史表三个月内的数据查订单表这就是一个简单的按照时间范围来分片的算法。
选择合适Sharding Key和分片算法非常重要直接影响了分库分表的效果。我们首先来说如何选择Sharding Key的问题。
**选择这个Sharding Key最重要的参考因素是我们的业务是如何访问数据的**
比如我们把订单ID作为Sharding Key来拆分订单表那拆分之后如果我们按照订单ID来查订单就需要先根据订单ID和分片算法计算出我要查的这个订单它在哪个分片上也就是哪个库哪张表中然后再去那个分片执行查询就可以了。
但是当我打开“我的订单”这个页面的时候它的查询条件是用户ID这里没有订单ID那就没法知道我们要查的订单在哪个分片上就没法查了。当然你要强行查的话那就只能把所有分片都查一遍再合并查询结果这个就很麻烦而且性能很差还不能分页。
那要是把用户ID作为Sharding Key呢也会面临同样的问题使用订单ID作为查询条件来查订单的时候就没办法找到订单在哪个分片了。这个问题的解决办法是在生成订单ID的时候把用户ID的后几位作为订单ID的一部分比如说可以规定18位订单号中第10-14位是用户ID的后四位这样按订单ID查询的时候就可以根据订单ID中的用户ID找到分片。
那我们系统对订单的查询方式肯定不只是按订单ID或者按用户ID这两种啊。比如说商家希望看到的是自己店铺的订单还有各种和订单相关的报表。对于这些查询需求我们一旦对订单做了分库分表就没法解决了。那怎么办呢
一般的做法是把订单数据同步到其他的存储系统中去在其他的存储系统里面解决问题。比如说我们可以再构建一个以店铺ID作为Sharding Key的只读订单库专门供商家来使用。或者把订单数据同步到HDFS中然后用一些大数据技术来生成订单相关的报表。
所以你看,一旦做了分库分表,就会极大地限制数据库的查询能力,之前很简单的查询,分库分表之后,可能就没法实现了。所以我们在之前的课程中,先讲了各种各样的方法,来缓解数据多、并发高的问题,而一直没讲分库分表。**分库分表一定是,数据量和并发大到所有招数都不好使了,我们才拿出来的最后一招。**
## 如何选择分片算法?
在上节课我给你留的思考题中我们提到过能不能用订单完成时间作为Sharding Key呢比如说我分12个分片每个月一个分片这样对查询的兼容要好很多毕竟查询条件中带上时间范围让查询只落到某一个分片上还是比较容易的我在查询界面上强制用户必须指定时间范围就行了。
这种做法有个很大的问题比如现在是3月份那基本上所有的查询都集中在3月份这个分片上其他11个分片都闲着这样不仅浪费资源很可能你3月那个分片根本抗不住几乎全部的并发请求。这个问题就是“热点问题”。
也就是说,我们希望并发请求和数据能均匀地分布到每一个分片上,尽量避免出现热点。这是选择分片算法时需要考虑的一个重要的因素。一般常用的分片算法就那么几种,刚刚讲到的按照时间范围分片的方法是其中的一种。
基于范围来分片容易产生热点问题不适合作为订单的分片方法但是这种分片方法的优点也很突出那就是对查询非常友好基本上只要加上一个时间范围的查询条件原来该怎么查分片之后还可以怎么查。范围分片特别适合那种数据量非常大但并发访问量不大的ToB系统。比如说电信运营商的监控系统它可能要采集所有人手机的信号质量然后做一些分析这个数据量非常大但是这个系统的使用者是运营商的工作人员并发量很少。这种情况下就很适合范围分片。
一般来说订单表都采用更均匀的哈希分片算法。比如说我们要分24个分片选定了Sharding Key是用户ID那我们决定某个用户的订单应该落到那个分片上的算法是拿用户ID除以24得到的余数就是分片号。这是最简单的取模算法一般就可以满足大部分要求了。当然也有一些更复杂的哈希算法像一致性哈希之类的特殊情况下也可以使用。
需要注意的一点是哈希分片算法能够分得足够均匀的前提条件是用户ID后几位数字必须是均匀分布的。比如说你在生成用户ID的时候自定义了一个用户ID的规则最后一位0是男性1是女性这样的用户ID哈希出来可能就没那么均匀可能会出现热点。
还有一种分片的方法查表法。查表法其实就是没有分片算法决定某个Sharding Key落在哪个分片上全靠人为来分配分配的结果记录在一张表里面。每次执行查询的时候先去表里查一下要找的数据在哪个分片中。
查表法的好处就是灵活,怎么分都可以,你用上面两种分片算法都没法分均匀的情况下,就可以用查表法,人为地来把数据分均匀了。查表法还有一个特好的地方是,它的分片是可以随时改变的。比如我发现某个分片已经是热点了,那我可以把这个分片再拆成几个分片,或者把这个分片的数据移到其他分片中去,然后修改一下分片映射表,就可以在线完成数据拆分了。
<img src="https://static001.geekbang.org/resource/image/0f/9d/0faac5967ca1f9385d8f7eda8eedd09d.jpg" alt="">
但你需要注意的是,分片映射表本身的数据不能太多,否则这个表反而成为热点和性能瓶颈了。查表法相对其他两种分片算法来说,缺点是需要二次查询,实现起来更复杂,性能上也稍微慢一些。但是,分片映射表可以通过缓存来加速查询,实际性能并不会慢很多。
## 小结
对MySQL这样的单机数据库来说分库分表是应对海量数据和高并发的最后一招分库分表之后将会对数据查询有非常大的限制。
分多少个库需要用并发量来预估分多少表需要用数据量来预估。选择Sharding Key的时候一定要能兼容业务最常用的查询条件让查询尽量落在一个分片中分片之后无法兼容的查询可以把数据同步到其他存储中去来解决这个问题。
我们常用三种分片算法,范围分片容易产生热点问题,但对查询更友好,适合适合并发量不大的场景;哈希分片比较容易把数据和查询均匀地分布到所有分片中;查表法更灵活,但性能稍差。
对于订单表进行分库分表一般按照用户ID作为Sharding Key采用哈希分片算法来均匀分布用户订单数据。为了能支持按订单号查询的需求需要把用户ID的后几位放到订单号中去。
最后还需要强调一下我们这节课讲的这些分片相关的知识不仅仅适用于MySQL的分库分表你在使用其他分布式数据库的时候一样会遇到如何分片、如何选择Sharding Key和分片算法的问题它们的原理都是一样的所以我们讲的这些方法也都是通用的。
## 思考题
课后请你想一下,把订单表拆分之后,那些和订单有外键关联的表,该怎么处理?欢迎你在留言区与我讨论。
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,114 @@
<audio id="audio" title="16 | 用Redis构建缓存集群的最佳实践有哪些" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b6/56/b68d0c726750b5cea58bb69314f47b56.mp3"></audio>
你好,我是李玥。
之前连续几节课我们都在以MySQL为例子讲如何应对海量数据如何应对高并发如何实现高可用我先带你简单复习一下。
- 数据量太大查询慢怎么办?存档历史数据或者分库分表,这是数据分片。
- 并发太高扛不住怎么办?读写分离,这是增加实例数。
- 数据库宕机怎么办?增加从节点,主节点宕机的时候用从节点顶上,这是主从复制。但是这里面要特别注意数据一致性的问题。
我在之前课程中,也多次提到过,**这些方法不仅仅是MySQL特有的对于几乎所有的存储系统都是适用的。**
今天这节课我们来聊一聊如何构建一个生产系统可用的Redis缓存集群。你将会看到几种集群解决方案用到的思想基本和我们上面讲的都是一样的。
## Redis Cluster如何解决数据量大、高可用和高并发问题
Redis从3.0版本开始提供了官方的集群支持也就是Redis Cluser。**Redis Cluster相比于单个节点的Redis能保存更多的数据支持更多的并发并且可以做到高可用在单个节点故障的情况下继续提供服务。**
为了能够保存更多的数据和MySQL分库分表的方式类似Redis Cluster也是通过分片的方式把数据分布到集群的多个节点上。
Redis Cluster是如何来分片的呢它引入了一个“槽Slot”的概念这个槽就是哈希表中的哈希槽槽是Redis分片的基本单位每个槽里面包含一些Key。每个集群的槽数是固定的1638416 * 1024每个Key落在哪个槽中也是固定的计算方法是
```
HASH_SLOT = CRC16(key) mod 16384
```
这个算法很简单先计算Key的CRC值然后把这个CRC之后的Key值直接除以16384余数就是Key所在的槽。这个算法就是我们上节课讲过的哈希分片算法。
这些槽又是如何存放到具体的Redis节点上的呢这个映射关系保存在集群的每个Redis节点上集群初始化的时候Redis会自动平均分配这16384个槽也可以通过命令来调整。这个分槽的方法也是我们上节课讲到过的分片算法查表法。
客户端可以连接集群的任意一个节点来访问集群的数据当客户端请求一个Key的时候被请求的那个Redis实例先通过上面的公式计算出这个Key在哪个槽中然后再查询槽和节点的映射关系找到数据所在的真正节点如果这个节点正好是自己那就直接执行命令返回结果。如果数据不在当前这个节点上那就给客户端返回一个重定向的命令告诉客户端应该去连哪个节点上请求这个Key的数据。然后客户端会再连接正确的节点来访问。
解决分片问题之后Redis Cluster就可以通过水平扩容来增加集群的存储容量但是每次往集群增加节点的时候需要从集群的那些老节点中搬运一些槽到新节点你可以手动指定哪些槽迁移到新节点上也可以利用官方提供的[redis-trib.rb](http://download.redis.io/redis-stable/src/redis-trib.rb)脚本来自动重新分配槽,自动迁移。
分片可以解决Redis保存海量数据的问题并且客观上提升了Redis的并发能力和查询性能。但是并不能解决高可用的问题每个节点都保存了整个集群数据的一个子集任何一个节点宕机都会导致这个宕机节点上的那部分数据无法访问。
那Redis Cluster是怎么解决高可用问题的
参见上面我们讲到的方法:**增加从节点,做主从复制**。Redis Cluster支持给每个分片增加一个或多个从节点每个从节点在连接到主节点上之后会先给主节点发送一个SYNC命令请求一次全量复制也就是把主节点上全部的数据都复制到从节点上。全量复制完成之后进入同步阶段主节点会把刚刚全量复制期间收到的命令以及后续收到的命令持续地转发给从节点。
因为Redis不支持事务所以它的复制相比MySQL更简单连Binlog都省了直接就是转发客户端发来的更新数据命令来实现主从同步。如果某个分片的主节点宕机了集群中的其他节点会在这个分片的从节点中选出一个新的节点作为主节点继续提供服务。新的主节点选举出来后集群中的所有节点都会感知到这样如果客户端的请求Key落在故障分片上就会被重定向到新的主节点上。
最后我们看一下Redis Cluster是如何应对高并发的。
一般来说Redis Cluster进行了分片之后每个分片都会承接一部分并发的请求加上Redis本身单节点的性能就非常高所以大部分情况下不需要再像MySQL那样做读写分离来解决高并发的问题。默认情况下集群的读写请求都是由主节点负责的从节点只是起一个热备的作用。当然了Redis Cluster也支持读写分离在从节点上读取数据。
以上就是Redis Cluster的基本原理你可以对照下图来加深理解。
<img src="https://static001.geekbang.org/resource/image/c4/fd/c405e73a4fd797ca0cda82b383e46ffd.png" alt="">
你可以看到Redis Cluster整体的架构完全就是照抄MySQL构建集群的那一套东西当然这些设计和方法也不是MySQL发明的抄作业抄的就差把名字一起也抄上了。
具体如何搭建Redis Cluster以及相关的操作命令你可以看一下[Redis官方的这篇教程](https://redis.io/topics/cluster-tutorial)。
## 为什么Redis Cluster不适合超大规模集群
Redis Cluster的优点是易于使用。分片、主从复制、弹性扩容这些功能都可以做到自动化通过简单的部署就可以获得一个大容量、高可靠、高可用的Redis集群并且对于应用来说近乎于是透明的。
所以,**Redis Cluster是非常适合构建中小规模Redis集群**这里的中小规模指的是大概几个到几十个节点这样规模的Redis集群。
**但是Redis Cluster不太适合构建超大规模集群主要原因是它采用了去中心化的设计。**刚刚我们讲了Redis的每个节点上都保存了所有槽和节点的映射关系表客户端可以访问任意一个节点再通过重定向命令找到数据所在的那个节点。那你有没有想过一个问题这个映射关系表它是如何更新的呢比如说集群加入了新节点或者某个主节点宕机了新的主节点被选举出来这些情况下都需要更新集群每一个节点上的映射关系表。
Redis Cluster采用了一种去中心化的[流言(Gossip)协议](https://en.wikipedia.org/wiki/Gossip_protocol)来传播集群配置的变化。一般涉及到协议都比较复杂,这里我们不去深究具体协议和实现算法,我大概给你讲一下这个协议原理。
所谓流言,就是八卦,比如说,我们上学的时候,班上谁和谁偷偷好上了,搞对象,那用不了一天,全班同学都知道了。咋知道的?张三看见了,告诉李四,李四和王小二特别好,又告诉了王小二,这样人传人,不久就传遍全班了。这个就是八卦协议的传播原理。
这个八卦协议它的好处是去中心化,传八卦不需要组织,吃瓜群众自发就传开了。这样部署和维护就更简单,也能避免中心节点的单点故障。八卦协议的缺点就是传播速度慢,并且是集群规模越大,传播的越慢。这个也很好理解,比如说,换成某两个特别出名的明星搞对象,即使是全国人民都很八卦,但要想让全国每一个人都知道这个消息,还是需要很长的时间。在集群规模太大的情况下,数据不同步的问题会被明显放大,还有一定的不确定性,如果出现问题很难排查。
## 如何用Redis构建超大规模集群
Redis Cluster不太适合用于大规模集群所以很多大厂都选择自己去搭建Redis集群。这里面每一家的解决方案都有自己的特色但其实总体的架构都是大同小异的。
一种是基于代理的方式在客户端和Redis节点之间还需要增加一层代理服务。这个代理服务有三个作用。
第一个作用是负责在客户端和Redis节点之间转发请求和响应。客户端只和代理服务打交道代理收到客户端的请求之后再转发到对应的Redis节点上节点返回的响应再经由代理转发返回给客户端。
第二个作用是负责监控集群中所有Redis节点状态如果发现有问题节点及时进行主从切换。
第三个作用就是维护集群的元数据,这个元数据主要就是集群所有节点的主从信息,以及槽和节点关系映射表。这个架构和我在《[12 | MySQL如何应对高并发读写分离](https://time.geekbang.org/column/article/215330)》这节课中给你讲过的用HAProxy+Keepalived来代理MySQL请求的架构是类似的只是多了一个自动路由分片的功能而已。
<img src="https://static001.geekbang.org/resource/image/03/d6/03c2901db83c02cbfe90a8c9b78d04d6.jpg" alt="">
像开源的Redis集群方案[twemproxy](https://github.com/twitter/twemproxy)和[Codis](https://github.com/CodisLabs/codis),都是这种架构的。
这个架构最大的优点是对客户端透明在客户端视角来看整个集群和一个超大容量的单节点Redis是一样的。并且由于分片算法是代理服务控制的扩容也比较方便新节点加入集群后直接修改代理服务中的元数据就可以完成扩容。
不过,这个架构的缺点也很突出,增加了一层代理转发,每次数据访问的链路更长了,必然会带来一定的性能损失。而且,代理服务本身又是集群的一个单点,当然,我们可以把代理服务也做成一个集群来解决单点问题,那样集群就更复杂了。
另外一种方式是不用这个代理服务把代理服务的寻址功能前移到客户端中去。客户端在发起请求之前先去查询元数据就可以知道要访问的是哪个分片和哪个节点然后直连对应的Redis节点访问数据。
当然客户端不用每次都去查询元数据因为这个元数据是不怎么变化的客户端可以自己缓存元数据这样访问性能基本上和单机版的Redis是一样的。如果某个分片的主节点宕机了新的主节点被选举出来之后更新元数据里面的信息。对集群的扩容操作也比较简单除了迁移数据的工作必须要做以外更新一下元数据就可以了。
<img src="https://static001.geekbang.org/resource/image/dc/da/dcaced0a9ce9842ef688c9626accdcda.jpg" alt="">
虽然说这个元数据服务仍然是一个单点但是它的数据量不大访问量也不大相对就比较容易实现。我们可以用ZooKeeper、etcd甚至MySQL都能满足要求。这个方案应该是最适合超大规模Redis集群的方案了在性能、弹性、高可用几方面表现都非常好缺点是整个架构比较复杂客户端不能通用需要开发定制化的Redis客户端只有规模足够大的企业才负担得起。
## 小结
今天这节课我们讲了从小到大三种构建Redis集群的方式。
- 小规模的集群建议使用官方的Redis Cluster在节点数量不多的情况下各方面表现都不错。
- 再大一些规模的集群可以考虑使用twemproxy或者Codis这类的基于代理的集群架构虽然是开源方案但是已经被很多公司在生产环境中验证过。
- 相比于代理方案,使用定制客户端的方案性能更好,很多大厂采用的都是类似的架构。
还有一个小问题需要注意的是这几种集群方案对一些类似于“KEYS”这类的多KEY命令都没法做到百分百支持。原因很简单数据被分片了之后这种多KEY的命令很可能需要跨多个分片查询。当你的系统从单个Redis库升级到集群时可能需要考虑一下这方面的兼容性问题。
## 思考题
很多存储系统之间都存在“互相抄作业”的嫌疑其实这对于我们这些存储系统的使用者来说是好事儿比如我们把MySQL都学透了你再去看Redis知道它抄了哪些作业这部分我们就可以迅速掌握了只要再研究一下不一样的那一小部分内容我们就可以精通Redis了是不
课后请你再去看一下HDFS它在解决分片、复制和高可用这几方面哪些是“抄作业”哪些又是自己独创的。欢迎你在留言区与我讨论。
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,186 @@
<audio id="audio" title="17 | 大厂都是怎么做MySQL to Redis同步的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ce/f8/ce70cc33d669d707ede0e8e862ee7ef8.mp3"></audio>
你好,我是李玥。
之前我们在《[11 | MySQL如何应对高并发使用缓存保护MySQL](https://time.geekbang.org/column/article/213230)》这一节课中讲到了Read/Write Through和Cache Aside这几种更新缓存的策略这几种策略都存在缓存穿透的可能如果缓存没有命中那就穿透缓存去访问数据库获取数据。
一般情况下只要我们做好缓存预热这个缓存的命中率很高能穿透缓存打到数据库上的请求比例就非常低这些缓存的策略都是没问题的。但是如果说我们的Redis缓存服务的是一个超大规模的系统那就又不一样了。
今天这节课,我们来说一下,在超大规模系统中缓存会面临什么样的问题,以及应该使用什么样的策略来更新缓存。
## 缓存穿透:超大规模系统的不能承受之痛
我们上节课讲到了如何构建Redis集群由于集群可以水平扩容那只要集群足够大理论上支持海量并发也不是问题。但是因为并发请求的数量这个基数太大了即使有很小比率的请求穿透缓存打到数据库上请求的绝对数量仍然不小。加上大促期间的流量峰值还是存在缓存穿透引发雪崩的风险。
那这个问题怎么解决呢其实方法你也想得到不让请求穿透缓存不就行了反正现在存储也便宜只要你买得起足够多的服务器Redis集群的容量就是无限的。不如把全量的数据都放在Redis集群里面处理读请求的时候干脆只读Redis不去读数据库。这样就完全没有“缓存穿透”的风险了实际上很多大厂它就是这么干的。
在Redis中缓存全量的数据又引发了一个新的问题那就是如何来更新缓存中的数据呢因为我们取消了缓存穿透的机制这种情况下从缓存读到数据可以直接返回如果没读到数据那就只能返回错误了所以当系统更新数据库的数据之后必须及时去更新缓存。
说到这儿又绕回到那个老问题上了怎么保证Redis中的数据和数据库中的数据同步更新我们之前讲过用分布式事务来解决数据一致性的问题但是这些方法都不太适合用来更新缓存**因为分布式事务,对数据更新服务有很强的侵入性**。我们拿下单服务来说如果为了更新缓存增加一个分布式事务无论我们用哪种分布式事务或多或少都会影响下单服务的性能。还有一个问题是如果Redis本身出现故障写入数据失败还会导致下单失败等于是降低了下单服务性能和可用性这样肯定不行。
**对于像订单服务这类核心的业务一个可行的方法是我们启动一个更新订单缓存的服务接收订单变更的MQ消息然后更新Redis中缓存的订单数据。**因为这类核心的业务数据,使用方非常多,本来就需要发消息,增加一个消费订阅基本没什么成本,订单服务本身也不需要做任何更改。
<img src="https://static001.geekbang.org/resource/image/7c/8e/7cec502808318409dbc719c0b1cbbc8e.jpg" alt="">
唯一需要担心的一个问题是如果丢消息了怎么办因为现在消息是缓存数据的唯一来源一旦出现丢消息缓存里缺失的那条数据永远不会被补上。所以必须保证整个消息链条的可靠性不过好在现在的MQ集群比如像Kafka或者RocketMQ它都有高可用和高可靠的保证机制只要你正确配置好是可以满足数据可靠性要求的。
像订单服务这样,本来就有现成的数据变更消息可以订阅,这样更新缓存还是一个不错的选择,因为实现起来很简单,对系统的其他模块完全没有侵入。
## 使用Binlog实时更新Redis缓存
如果我们要缓存的数据本来没有一份数据更新的MQ消息可以订阅怎么办很多大厂都采用的也是更通用的解决方案是这样的。
数据更新服务只负责处理业务逻辑更新MySQL完全不用管如何去更新缓存。负责更新缓存的服务把自己伪装成一个MySQL的从节点从MySQL接收Binlog解析Binlog之后可以得到实时的数据变更信息然后根据这个变更信息去更新Redis缓存。
<img src="https://static001.geekbang.org/resource/image/91/12/918380c0e43de2f4ef7ad5e8e9d5d212.jpg" alt="">
这种收Binlog更新缓存的方案和刚刚我们讲到的收MQ消息更新缓存的方案其实它们的实现思路是一样的都是异步订阅实时数据变更信息去更新Redis。只不过直接读取Binlog这种方式它的通用性更强。不要求订单服务再发订单消息了订单更新服务也不用费劲去解决“发消息失败怎么办”这种数据一致性问题了。
而且在整个缓存更新链路上减少了一个收发MQ的环节从MySQL更新到Redis更新的时延更短出现故障的可能性也更低所以很多大厂更青睐于这种方案。
这个方案唯一的缺点是实现订单缓存更新服务有点儿复杂毕竟不像收消息拿到的直接就是订单数据解析Binlog还是挺麻烦的。
有很多开源的项目就提供了订阅和解析MySQL Binlog的功能下面我们以比较常用的开源项目[Canal](https://github.com/alibaba/canal)为例来演示一下如何实时接收Binlog更新Redis缓存。
Canal模拟MySQL 主从复制的交互协议把自己伪装成一个MySQL的从节点向MySQL主节点发送dump请求MySQL收到请求后就会开始推送Binlog给CanalCanal解析Binlog字节流之后转换为便于读取的结构化数据供下游程序订阅使用。下图是Canal的工作原理
<img src="https://static001.geekbang.org/resource/image/45/e4/452211795717190e55c5b0ff2ab208e4.jpg" alt="">
在我们这个示例中MySQL和Redis都运行在本地的默认端口上MySQL的端口为3306Redis的端口为6379。为了便于大家操作我们还是以《[04 | 事务:账户余额总是对不上账,怎么办?](https://time.geekbang.org/column/article/206544)》这节课中的账户余额表account_balance作为演示数据。
首先下载并解压Canal 最新的1.1.4版本到本地:
```
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
```
然后来配置MySQL我们需要在MySQL的配置文件中开启Binlog并设置Binlog的格式为ROW格式。
```
[mysqld]
log-bin=mysql-bin # 开启Binlog
binlog-format=ROW # 设置Binlog格式为ROW
server_id=1 # 配置一个ServerID
```
给Canal开一个专门的MySQL用户并授权确保这个用户有复制Binlog的权限
```
CREATE USER canal IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
```
重启一下MySQL确保所有的配置生效。重启后检查一下当前的Binlog文件和位置
<img src="https://static001.geekbang.org/resource/image/01/8f/01293d0ccc372418f3e01c785e204b8f.png" alt="">
记录下File和Position两列的值然后我们来配置Canal。编辑Canal的实例配置文件canal/conf/example/instance.properties以便让Canal连接到我们的MySQL上。
```
canal.instance.gtidon=false
# position info
canal.instance.master.address=127.0.0.1:3306
canal.instance.master.journal.name=binlog.000009
canal.instance.master.position=155
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=.*\\..
```
这个配置文件需要配置MySQL的连接地址、库名、用户名和密码之外还需要配置canal.instance.master.journal.name和canal.instance.master.position这两个属性取值就是刚刚记录的File和Position两列。然后就可以启动Canal服务了
```
canal/bin/startup.sh
```
启动之后看一下日志文件canal/logs/example/example.log如果里面没有报错就说明启动成功并连接到我们的MySQL上了。
Canal服务启动后会开启一个端口11111等待客户端连接客户端连接上Canal服务之后可以从Canal服务拉取数据每拉取一批数据正确写入Redis之后给Canal服务返回处理成功的响应。如果发生客户端程序宕机或者处理失败等异常情况Canal服务没收到处理成功的响应下次客户端来拉取的还是同一批数据这样就可以保证顺序并且不会丢数据。
接下来我们来开发账户余额缓存的更新程序以下的代码都是用Java语言编写的
```
while (true) {
Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
long batchId = message.getId();
try {
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
Thread.sleep(1000);
} else {
processEntries(message.getEntries(), jedis);
}
connector.ack(batchId); // 提交确认
} catch (Throwable t) {
connector.rollback(batchId); // 处理失败, 回滚数据
}
}
```
这个程序逻辑也不复杂程序启动并连接到Canal服务后就不停地拉数据如果没有数据就睡一会儿有数据就调用processEntries方法处理更新缓存。每批数据更新成功后就调用ack方法给Canal服务返回成功响应如果失败抛异常就回滚。下面是processEntries方法的主要代码
```
for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {
if (eventType == CanalEntry.EventType.DELETE) { // 删除
jedis.del(row2Key(&quot;user_id&quot;, rowData.getBeforeColumnsList()));
} else if (eventType == CanalEntry.EventType.INSERT) { // 插入
jedis.set(row2Key(&quot;user_id&quot;, rowData.getAfterColumnsList()), row2Value(rowData.getAfterColumnsList()));
} else { // 更新
jedis.set(row2Key(&quot;user_id&quot;, rowData.getAfterColumnsList()), row2Value(rowData.getAfterColumnsList()));
}
}
```
这里面根据事件类型来分别处理如果MySQL中的数据删除了就删除Redis中对应的数据。如果是更新和插入操作那就调用Redis的SET命令来写入数据。
把这个账户缓存更新服务启动后,我们来验证一下,我们在账户余额表插入一条记录:
```
mysql&gt; insert into account_balance values (888, 100, NOW(), 999);
```
然后来看一下Redis缓存
```
127.0.0.1:6379&gt; get 888
&quot;{\&quot;log_id\&quot;:\&quot;999\&quot;,\&quot;balance\&quot;:\&quot;100\&quot;,\&quot;user_id\&quot;:\&quot;888\&quot;,\&quot;timestamp\&quot;:\&quot;2020-03-08 16:18:10\&quot;}&quot;
```
可以看到数据已经自动同步到Redis中去了。我把这个示例的完整代码放在了[GitHub](https://github.com/liyue2008/canal-to-redis-example)上供你参考。
## 小结
在处理超大规模并发的场景时,由于并发请求的数量非常大,即使少量的缓存穿透,也有可能打死数据库引发雪崩效应。对于这种情况,我们可以缓存全量数据来彻底避免缓存穿透问题。
对于缓存数据更新的方法可以订阅数据更新的MQ消息来异步更新缓存更通用的方法是把缓存更新服务伪装成一个MySQL的从节点订阅MySQL的Binlog通过Binlog来更新Redis缓存。
需要特别注意的是无论是用MQ还是Canal来异步更新缓存对整个更新服务的数据可靠性和实时性要求都比较高数据丢失或者更新慢了都会造成Redis中的数据与MySQL中数据不同步。在把这套方案应用到生产环境中去的时候需要考虑一旦出现不同步问题时的降级或补偿方案。
## 思考题
课后请你思考一下,如果出现缓存不同步的情况,在你负责的业务场景下,该如何降级或者补偿?欢迎你在留言区与我讨论。
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,82 @@
<audio id="audio" title="18 | 分布式存储:你知道对象存储是如何保存图片文件的吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0f/b3/0fe9c3058098500a18c7624399b193b3.mp3"></audio>
你好,我是李玥。
我们都知道,保存像图片、音视频这类大文件,最佳的选择就是对象存储。对象存储不仅有很好的大文件读写性能,还可以通过水平扩展实现近乎无限的容量,并且可以兼顾服务高可用、数据高可靠这些特性。
对象存储之所以能做到这么“全能”,最主要的原因是,**对象存储是原生的分布式存储系统**。这里我们讲的“原生分布式存储系统”是相对于MySQL、Redis这类单机存储系统来说的。虽然这些非原生的存储系统也具备一定的集群能力但你也能感受到用它们构建大规模分布式集群的时候其实是非常不容易的。
随着云计算的普及,很多新生代的存储系统,都是原生的分布式系统,它们一开始设计的目标之一就是分布式存储集群,比如说[Elasticsearch](https://www.elastic.co/cn/)、[Ceph](http://about:blank)和国内很多大厂推出的新一代数据库,大多都可以做到:
- 近乎无限的存储容量;
- 超高的读写性能;
- 数据高可靠:节点磁盘损毁不会丢数据;
- 实现服务高可用:节点宕机不会影响集群对外提供服务。
那这些原生分布式存储是如何实现这些特性的呢?
实际上不用我说,你也能猜得到,这里面同样存在严重的“互相抄作业”的情况。这个也可以理解,除了存储的数据结构不一样,提供的查询服务不一样以外,这些分布式存储系统,它们面临的很多问题都是一样的,那实现方法差不多也是可以理解。
对象存储它的查询服务和数据结构都非常简单,是最简单的原生分布式存储系统。这节课,我们就来一起来研究一下对象存储这种最简单的原生分布式存储,通过对象存储来认识一下分布式存储系统的一些共性。掌握了这些共性之后,你再去认识和学习其他的分布式存储系统,也会感觉特别容易。
## 对象存储数据是如何保存大文件的?
对象存储对外提供的服务其实就是一个近乎无限容量的大文件KV存储所以对象存储和分布式文件系统之间没有那么明确的界限。对象存储的内部肯定有很多的存储节点用于保存这些大文件这个就是数据节点的集群。
另外,我们为了管理这些数据节点和节点中的文件,还需要一个存储系统保存集群的节点信息、文件信息和它们的映射关系。这些为了管理集群而存储的数据,叫做元数据(Metadata)。
元数据对于一个存储集群来说是非常重要的,所以保存元数据的存储系统必须也是一个集群。但是元数据集群存储的数据量比较少,数据的变动不是很频繁,加之客户端或者网关都会缓存一部分元数据,所以元数据集群对并发要求也不高。一般使用类似[ZooKeeper](https://zookeeper.apache.org/)或者[etcd](https://github.com/etcd-io/etcd)这类分布式存储就可以满足要求。
另外,存储集群为了对外提供访问服务,还需要一个网关集群,对外接收外部请求,对内访问元数据和数据节点。网关集群中的每个节点不需要保存任何数据,都是无状态的节点。有些对象存储没有网关,取而代之的是客户端,它们的功能和作用都是一样的。
<img src="https://static001.geekbang.org/resource/image/92/0b/925a6309372b30f660c9b8bc198f860b.jpg" alt="">
那么对象存储是如何来处理对象读写请求的呢这里面处理读和写请求的流程是一样的我们一起来说。网关收到对象读写请求后首先拿着请求中的Key去元数据集群查找这个Key在哪个数据节点上然后再去访问对应的数据节点读写数据最后把结果返回给客户端。
以上是一个比较粗略的大致流程,实际上这里面包含很多的细节,我们暂时没有展开讲。目的是让你在整体上对对象存储,以至于分布式存储系统,有一个清晰的认知。
上面这张图虽然我画的是对象存储集群的结构但是把图上的名词改一改完全可以套用到绝大多数分布式文件系统和数据库上去比如说HDFS。
## 对象是如何拆分和保存的?
接下来我们说一下对象存储到底是如何来保存大文件对象的。一般来说,对象存储中保存的文件都是图片、视频这类大文件。在对象存储中,每一个大文件都会被拆成多个大小相等的**块儿Block**拆分的方法很简单就是把文件从头到尾按照固定的块儿大小切成一块儿一块儿最后一块儿长度有可能不足一个块儿的大小也按一块儿来处理。块儿的大小一般配置为几十KB到几个MB左右。
把大对象文件拆分成块儿的目的有两个:
1. 第一是为了提升读写性能,这些块儿可以分散到不同的数据节点上,这样就可以并行读写。
1. 第二是把文件分成大小相等块儿,便于维护管理。
对象被拆成块儿之后,还是太过于碎片化了,如果直接管理这些块儿,会导致元数据的数据量会非常大,也没必要管理到这么细的粒度。所以一般都会再把块儿聚合一下,放到块儿的容器里面。这里的“容器”就是存放一组块儿的逻辑单元。容器这个名词,没有统一的叫法,比如在[ceph](https://ceph.io/)中称为Data Placement你理解这个含义就行。容器内的块儿数大多是固定的所以容器的大小也是固定的。
到这里这个容器的概念就比较类似于我们之前讲MySQL和Redis时提到的“分片”的概念了都是复制、迁移数据的基本单位。每个容器都会有N个副本这些副本的数据都是一样的。其中有一个主副本其他是从副本主副本负责数据读写从副本去到主副本上去复制数据保证主从数据一致。
这里面有一点儿和我们之前讲的不一样的是对象存储一般都不记录类似MySQL的Binlog这样的日志。主从复制的时候复制的不是日志而是整块儿的数据。这么做有两个原因
1. 第一个原因是基于性能的考虑。我们知道操作日志里面实际上就包含着数据。在更新数据的时候先记录操作日志再更新存储引擎中的数据相当于在磁盘上串行写了2次数据。对于像数据库这种每次更新的数据都很少的存储系统这个开销是可以接受的。但是对于对象存储来说它每次写入的块儿很大两次磁盘IO的开销就有些不太值得了。
1. 第二个原因是它的存储结构简单,即使没有日志,只要按照顺序,整块儿的复制数据,仍然可以保证主从副本的数据一致性。
以上我们说的对象(也就是文件)、块儿和容器,都是逻辑层面的概念,数据落实到副本上,这些副本就是真正物理存在了。这些副本再被分配到数据节点上保存起来。这里的数据节点就是运行在服务器上的服务进程,负责在本地磁盘上保存副本的数据。
<img src="https://static001.geekbang.org/resource/image/8d/0b/8d6616675ca90df023d1622aa1f2ef0b.jpg" alt="">
了解了对象是如何被拆分并存储在数据节点上之后我们再来回顾一下数据访问的流程。当我们请求一个Key的时候网关首先去元数据中查找这个Key的元数据。然后根据元数据中记录的对象长度计算出对象有多少块儿。接下来的过程就可以分块儿并行处理了。对于每个块儿还需要再去元数据中找到它被放在哪个容器中。
我刚刚讲过,容器就是分片,怎么把块儿映射到容器中,这个方法就是我们在《[15 | MySQL存储海量数据的最后一招分库分表](https://time.geekbang.org/column/article/217568)》这节课中讲到的几种分片算法。不同的系统选择实现的方式也不一样有用哈希分片的也有用查表法把对应关系保存在元数据中的。找到容器之后再去元数据中查找容器的N个副本都分布在哪些数据节点上。然后网关直接访问对应的数据节点读写数据就可以了。
## 小结
对象存储是最简单的分布式存储系统主要由数据节点集群、元数据集群和网关集群或者客户端三部分构成。数据节点集群负责保存对象数据元数据集群负责保存集群的元数据网关集群和客户端对外提供简单的访问API对内访问元数据和数据节点读写数据。
为了便于维护和管理大的对象被拆分为若干固定大小的块儿块儿又被封装到容器也就分片每个容器有一主N从多个副本这些副本再被分散到集群的数据节点上保存。
对象存储虽然简单,但是它具备一个分布式存储系统的全部特征。所有分布式存储系统共通的一些特性,对象存储也都具备,比如说数据如何分片,如何通过多副本保证数据可靠性,如何在多个副本间复制数据,确保数据一致性等等。
希望你通过这节课的学习不仅是学会对象存储还要对比分析一下对象存储和其他分布式存储系统比如MySQL集群、HDFS、Elasticsearch等等这些它们之间有什么共同的地方差异在哪儿。想通了这些问题你对分布式存储系统的认知绝对会上升到一个全新的高度。然后你再去看一些之前不了解的存储系统就非常简单了。
## 思考题
我们刚刚说到过,对象存储并不是基于日志来进行主从复制的。假设我们的对象存储是一主二从三个副本,采用半同步方式复制数据,也就是主副本和任意一个从副本更新成功后,就给客户端返回成功响应。主副本所在节点宕机之后,这两个从副本中,至少有一个副本上的数据是和宕机的主副本上一样的,我们需要找到这个副本作为新的主副本,才能保证宕机不丢数据。
但是没有了日志,如果这两个从副本上的数据不一样,我们如何确定哪个上面的数据是和主副本一样新呢?欢迎你在留言区与我交流讨论。
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,71 @@
<audio id="audio" title="19 | 跨系统实时同步数据,分布式事务是唯一的解决方案吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e7/50/e76664d75360b92e4fecd62a9bb18c50.mp3"></audio>
你好,我是李玥。
我们在《[15 | MySQL存储海量数据的最后一招分库分表](https://time.geekbang.org/column/article/217568)》这节课中讲过,数据量太大的时候,单个存储节点存不下,那就只能把数据分片存储。
数据分片之后我们对数据的查询就没那么自由了。比如订单表如果按照用户ID作为Sharding Key来分片那就只能按照用户维度来查询。如果我是一个商家我想查我店铺的订单对不起做不到了。当然强行查也不是不行在所有分片上都查一遍再把结果聚合起来又慢又麻烦实际意义不大。
对于这样的需求普遍的解决办法是用空间换时间毕竟现在存储越来越便宜。再存一份订单数据到商家订单库然后以店铺ID作为Sharding Key分片专门供商家查询订单。
另外,之前我们在《[06 | 如何用Elasticsearch构建商品搜索系统](https://time.geekbang.org/column/article/208675)》这节课也讲到过同样一份商品数据如果我们是按照关键字搜索放在ES里就比放在MySQL快了几个数量级。原因是数据组织方式、物理存储结构和查询方式对查询性能的影响是巨大的而且海量数据还会指数级地放大这个性能差距。
所以在大厂中对于海量数据的处理原则都是根据业务对数据查询的需求反过来确定选择什么数据库、如何组织数据结构、如何分片数据这样才能达到最优的查询性能。同样一份订单数据除了在订单库保存一份用于在线交易以外还会在各种数据库中以各种各样的组织方式存储用于满足不同业务系统的查询需求。像BAT这种大厂它的核心业务数据存个几十上百份是非常正常的。
那么问题来了,如何能够做到让这么多份数据实时地保持同步呢?
我们之前讲过分布式事务,可以解决数据一致性的问题。比如说,你可以用本地消息表,把一份数据实时同步给另外两、三个数据库,这样还可以接受,太多的话也是不行的,并且对在线交易业务还有侵入性,所以分布式事务是解决不了这个问题的。
今天这节课我们就来说一下,如何把订单数据实时、准确无误地同步到这么多异构的数据中去。
## 使用Binlog和MQ构建实时数据同步系统
早期大数据刚刚兴起的时候大多数系统还做不到异构数据库实时同步那个时候普遍的做法是使用ETL工具定时同步数据在T+1时刻去同步上一个周期的数据然后再做后续的计算和分析。定时ETL对于一些需要实时查询数据的业务需求就无能为力了。所以这种定时同步的方式基本上都被实时同步的方式给取代了。
怎么来做这么大数据量、这么多个异构数据库的实时同步呢?你还记得我在《[17 | 大厂都是怎么做MySQL to Redis同步的](https://time.geekbang.org/column/article/217593)》这节课中讲到的方法吧利用Canal把自己伪装成一个MySQL的从库从MySQL实时接收Binlog然后写入Redis中。把这个方法稍微改进一下就可以用来做异构数据库的同步了。
为了能够支撑下游众多的数据库从Canal出来的Binlog数据肯定不能直接去写下游那么多数据库一是写不过来二是对于每个下游数据库它可能还有一些数据转换和过滤的工作要做。所以需要增加一个MQ来解耦上下游。
<img src="https://static001.geekbang.org/resource/image/df/d8/dfa37d67d87fc7c8a8de50681f8134d8.jpg" alt="">
Canal从MySQL收到Binlog并解析成结构化数据之后直接写入到MQ的一个订单Binlog主题中然后每一个需要同步订单数据的业务方都去订阅这个MQ中的订单Binlog主题消费解析后的Binlog数据。在每个消费者自己的同步程序中它既可以直接入库也可以做一些数据转换、过滤或者计算之后再入库这样就比较灵活了。
## 如何保证数据同步的实时性
这个方法看起来不难但是非常容易出现性能问题。有些接收Binlog消息的下游业务对数据的实时性要求比较高不能容忍太高的同步时延。比如说每个电商在大促的时候都会有一个大屏幕实时显示现在有多少笔交易交易额是多少。这个东西都是给老板们看的如果说大促的时候你让老板们半小时之后才看到数字那估计你就得走人了。
大促的时候数据量大、并发高、数据库中的数据变动频繁同步的Binlog流量也非常大。为了保证这个同步的实时性整个数据同步链条上的任何一个环节它的处理速度都必须得跟得上才行。我们一步一步分析可能会出现性能瓶颈的环节。
源头的订单库如果它出现繁忙对业务的影响就不只是大屏延迟了那就影响到用户下单了这个问题是数据库本身要解决的这里我们不考虑。再顺着数据流向往下看Canal和MQ这两个环节由于没什么业务逻辑性能都非常好。所以**一般容易成为性能瓶颈的就是消费MQ的同步程序**因为这些同步程序里面一般都会有一些业务逻辑而且如果下游的数据库写性能跟不上表象也是这个同步程序处理性能上不来消息积压在MQ里面。
那我们能不能多加一些同步程序的实例数,或者增加线程数,通过增加并发来提升处理能力呢?这个地方的并发数,还真不是随便说扩容就可以就扩容的,我来跟你讲一下为什么。
我们知道MySQL主从同步Binlog是一个单线程的同步过程。为什么是单线程原因很简单在从库执行Binlog的时候必须按顺序执行才能保证数据和主库是一样的。**为了确保数据一致性Binlog的顺序很重要是绝对不能乱序的。** 严格来说对于每一个MySQL实例整个处理链条都必须是单线程串行执行MQ的主题也必须设置为只有1个分区队列这样才能保证数据同步过程中的Binlog是严格有序的写到目标数据库的数据才能是正确的。
那单线程处理速度上不去,消息越积压越多,这不无解了吗?其实办法还是有的,但是必须得和业务结合起来解决。
还是拿订单库来说啊其实我们并不需要对订单库所有的更新操作都严格有序地执行比如说A和B两个订单号不同的订单这两个订单谁先更新谁后更新并不影响数据的一致性因为这两个订单完全没有任何关系。但是同一个订单如果更新的Binlog执行顺序错了那同步出来的订单数据真的就错了。
也就是说,我们只要保证每个订单的更新操作日志的顺序别乱就可以了。这种一致性要求称为**因果一致性Causal Consistency**,有因果关系的数据之间必须要严格地保证顺序,没有因果关系的数据之间的顺序是无所谓的。
基于这个理论基础,我们就可以并行地来进行数据同步,具体的做法是这样的。
首先根据下游同步程序的消费能力计算出需要多少并发然后设置MQ中主题的分区队列数量和并发数一致。因为MQ是可以保证同一分区内消息是不会乱序的所以我们需要把具有因果关系的Binlog都放到相同的分区中去就可以保证同步数据的因果一致性。对应到订单库就是相同订单号的Binlog必须发到同一个分区上。
这是不是和之前讲过的数据库分片有点儿像呢那分片算法就可以拿过来复用了比如我们可以用最简单的哈希算法Binlog中订单号除以MQ分区总数余数就是这条Binlog消息发往的分区号。
Canal自带的分区策略就支持按照指定的Key把Binlog哈希到下游的MQ中去具体的配置可以看一下[Canal接入MQ的文档](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart)。
## 小结
对于海量数据必须要按照查询方式选择数据库类型和数据的组织方式才能达到理想的查询性能。这就需要把同一份数据按照不同的业务需求以不同的组织方式存放到各种异构数据库中。因为数据的来源大多都是在线交易系统的MySQL数据库所以我们可以利用MySQL的Binlog来实现异构数据库之间的实时数据同步。
为了能够支撑众多下游数据库实时同步的需求可以通过MQ解耦上下游Binlog先发送到MQ中下游各业务方可以消费MQ中的消息再写入各自的数据库。
如果下游处理能力不能满足要求可以增加MQ中的分区数量实现并发同步但需要结合同步的业务数据特点把具有因果关系的数据哈希到相同分区上才能避免因为并发乱序而出现数据同步错误的问题。
## 思考题
在我们这种数据同步架构下如果说下游的某个同步程序或数据库出了问题需要把Binlog回退到某个时间点然后重新同步这个问题该怎么解决欢迎你在留言区与我讨论。
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,86 @@
<audio id="audio" title="20 | 如何在不停机的情况下,安全地更换数据库?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3e/8c/3e831695f95ca084d076ad803f05c68c.mp3"></audio>
你好,我是李玥。
随着我们的系统规模逐渐增长,总会遇到需要更换数据库的问题。我们来说几种常见的情况。
- 对MySQL做了分库分表之后需要从原来的单实例数据库迁移到新的数据库集群上。
- 系统从传统部署方式向云上迁移的时候,也需要从自建的数据库迁移到云数据库上。
- 一些在线分析类的系统MySQL性能不够用的时候就需要更换成一些专门的分析类数据库比如说HBase。
更换数据库这个事儿,是一个非常大的技术挑战,因为我们需要保证整个迁移过程中,既不能长时间停服,也不能丢数据。
那么,今天这节课我们就来说一下,如何在不停机的情况下,安全地迁移数据更换数据库。
## 如何实现不停机更换数据库?
我们都知道墨菲定律:“如果事情有变坏的可能,不管这种可能性有多小,它总会发生。”放到这里呢,也就是说,我们在更换数据库的过程中,只要有一点儿可能会出问题的地方,哪怕是出现问题的概率非常小,它总会出问题。
实际上,无论是新版本的程序,还是新的数据库,即使我们做了严格的验证测试,做了高可用方案,刚刚上线的系统,它的稳定性总是没有那么好的,需要一个磨合的过程,才能逐步达到一个稳定的状态,这是一个客观规律。这个过程中一旦出现故障,如果不能及时恢复,造成的损失往往是我们承担不起的。
所以我们在设计迁移方案的时候,一定要做到,每一步都是可逆的。**要保证,每执行一个步骤后,一旦出现问题,能快速地回滚到上一个步骤**。这是很多同学在设计这种升级类技术方案的时候,容易忽略的问题。
接下来我们还是以订单库为例子,说一下这个迁移方案应该如何来设计。
首先要做的就是,把旧库的数据复制到新库中。因为旧库还在服务线上业务,所以不断会有订单数据写入旧库,我们不仅要往新库复制数据,还要保证新旧两个库的数据是实时同步的。所以,我们需要用一个同步程序来实现新旧两个数据库实时同步。
怎么来实现两个异构数据库之间的数据实时同步这个方法我们上节课刚刚讲过我们可以使用Binlog实时同步数据。如果源库不是MySQL的话就麻烦一点儿但也可以参考我们讲过的复制状态机理论来实现。这一步不需要回滚原因是只增加了一个新库和一个同步程序对系统的旧库和程序都没有任何改变。即使新上线的同步程序影响到了旧库只要停掉同步程序就可以了。
<img src="https://static001.geekbang.org/resource/image/ba/58/ba2a44c70d4766b281107f4134fe9d58.jpg" alt="">
然后我们需要改造一下订单服务业务逻辑部分不需要变DAO层需要做如下改造
1. 支持双写新旧两个库,并且预留热切换开关,能通过开关控制三种写状态:只写旧库、只写新库和同步双写。
1. 支持读新旧两个库,同样预留热切换开关,控制读旧库还是新库。
然后上线新版的订单服务,这个时候订单服务仍然是只读写旧库,不读写新库。让这个新版的订单服务需要稳定运行至少一到二周的时间,期间除了验证新版订单服务的稳定性以外,还要验证新旧两个订单库中的数据是否是一致的。这个过程中,如果新版订单服务有问题,可以立即下线新版订单服务,回滚到旧版本的订单服务。
稳定一段时间之后,就可以开启订单服务的双写开关了。开启双写开关的同时,需要停掉同步程序。这里面有一个问题需要注意一下,就是**这个双写的业务逻辑,一定是先写旧库,再写新库,并且以写旧库的结果为准**。
旧库写成功,新库写失败,返回写成功,但这个时候要记录日志,后续我们会用到这个日志来验证新库是否还有问题。旧库写失败,直接返回失败,就不写新库了。这么做的原因是,不能让新库影响到现有业务的可用性和数据准确性。上面这个过程如果出现问题,可以关闭双写,回滚到只读写旧库的状态。
切换到双写之后,新库与旧库的数据可能会存在不一致的情况,原因有两个:一是停止同步程序和开启双写,这两个过程很难做到无缝衔接,二是双写的策略也不保证新旧库强一致,这时候我们需要上线一个对比和补偿的程序,这个程序对比旧库最近的数据变更,然后检查新库中的数据是否一致,如果不一致,还要进行补偿。
<img src="https://static001.geekbang.org/resource/image/e0/ce/e0c3864866fe1ff3408e2589669b62ce.jpg" alt="">
开启双写后,还需要至少稳定运行至少几周的时间,并且期间我们要不断地检查,确保不能有旧库写成功,新库写失败的情况出现。对比程序也没有发现新旧两个库的数据有不一致的情况,这个时候,我们就可以认为,新旧两个库的数据是一直保持同步的。
接下来就可以用类似灰度发布的方式,把读请求一点儿一点儿地切到新库上。同样,期间如果出问题的话,可以再切回旧库。全部读请求都切换到新库上之后,这个时候其实读写请求就已经都切换到新库上了,实际的切换已经完成了,但还有后续的收尾步骤。
再稳定一段时间之后,就可以停掉对比程序,把订单服务的写状态改为只写新库。到这里,旧库就可以下线了。注意,整个迁移过程中,只有这个步骤是不可逆的。但是,这步的主要操作就是摘掉已经不再使用的旧库,对于在用的新库并没有什么改变,实际出问题的可能性已经非常小了。
到这里,我们就完成了在线更换数据库的全部流程。双写版本的订单服务也就完成了它的历史使命,可以在下一次升级订单服务版本的时候,下线双写功能。
## 如何实现对比和补偿程序?
在上面的整个切换过程中,如何实现这个对比和补偿程序,是整个这个切换设计方案中的一个难点。这个对比和补偿程序的难度在于,我们要对比的是两个都在随时变换的数据库中的数据。这种情况下,我们没有类似复制状态机这样理论上严谨实际操作还很简单的方法,来实现对比和补偿。但还是可以根据业务数据的实际情况,来针对性地实现对比和补偿,经过一段时间,把新旧两个数据库的差异,逐渐收敛到一致。
像订单这类时效性强的数据,是比较好对比和补偿的。因为订单一旦完成之后,就几乎不会再变了,那我们的对比和补偿程序,就可以依据订单完成时间,每次只对比这个时间窗口内完成的订单。补偿的逻辑也很简单,发现不一致的情况后,直接用旧库的订单数据覆盖新库的订单数据就可以了。
这样,切换双写期间,少量不一致的订单数据,等到订单完成之后,会被补偿程序修正。后续只要不是双写的时候,新库频繁写入失败,就可以保证两个库的数据完全一致。
比较麻烦的是更一般的情况比如像商品信息这类数据随时都有可能会变化。如果说数据上有更新时间那我们的对比程序可以利用这个更新时间每次在旧库取一个更新时间窗口内的数据去新库上找相同主键的数据进行对比发现数据不一致还要对比一下更新时间。如果新库数据的更新时间晚于旧库数据那可能是对比期间数据发生了变化这种情况暂时不要补偿放到下个时间窗口去继续对比。另外时间窗口的结束时间不要选取当前时间而是要比当前时间早一点儿比如1分钟前避免去对比正在写入的数据。
如果数据连时间戳也没有那只能去旧库读取Binlog获取数据变化然后去新库对比和补偿。
有一点需要说明的是,上面这些方法,如果严格推敲,都不是百分之百严谨的,都不能保证在任何情况下,经过对比和补偿后,新库的数据和旧库就是完全一样的。但是,在大多数情况下,这些实践方法还是可以有效地收敛新旧两个库的数据差异,你可以酌情采用。
## 小结
设计在线切换数据库的技术方案,首先要保证安全性,确保每一个步骤一旦失败,都可以快速回滚。此外,还要确保迁移过程中不丢数据,这主要是依靠实时同步程序和对比补偿程序来实现。
我把这个复杂的切换过程的要点,按照顺序总结成下面这个列表,供你参考:
1. 上线同步程序,从旧库中复制数据到新库中,并实时保持同步;
1. 上线双写订单服务,只读写旧库;
1. 开启双写,同时停止同步程序;
1. 开启对比和补偿程序,确保新旧数据库数据完全一样;
1. 逐步切量读请求到新库上;
1. 下线对比补偿程序,关闭双写,读写都切换到新库上;
1. 下线旧库和订单服务的双写功能。
## 思考题
我们整个切换的方案中,只有一个步骤是不可逆的,就是由双写切换为单写新库这一步。如果说不计成本,如何修改我们的迁移方案,让这一步也能做到快速回滚?你可以思考一下这个问题,欢迎你在留言区与我交流讨论。
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,73 @@
<audio id="audio" title="21 | 类似“点击流”这样的海量数据应该如何存储?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/45/44/45df906626d9a53e9eafebf5e3880f44.mp3"></audio>
你好,我是李玥。
对于大部分互联网公司来说数据量最大的几类数据是点击流数据、监控数据和日志数据。这里面“点击流”指的是在App、小程序和Web页面上的埋点数据这些埋点数据记录用户的行为比如你打开了哪个页面点击了哪个按钮在哪个商品上停留了多久等等这些。
当然你不用太担心自己的隐私问题,记录的这些行为数据不是为了监控用户,主要目的是为了从统计上分析群体用户的行为,从而改进产品和运营。比如,某件商品看的人很多,停留时间很长,最后下单购买的人却很少,那采销人员就要考虑是不是这件商品的定价太高了。
除了点击流数据以外,监控和日志数据都是大家常用的,我就不再多解释了。
这类数据都是真正“海量”的数据相比于订单、商品这类业务的数据数据量要多出23个数量级。每天产生的数据量就可能会超过TB1 TB = 1024 GB级别经过一段时间累积下来有些数据会达到PB1 PB = 1024 TB级别。
这种量级的数据在大数据技术出现之前是没法保存和处理的只能是通过抽样的方法来凑合着做分析。Hadoop等大数据技术出现以后才使得存储和计算海量数据成为可能。
今天这节课,我们来说说,应该选择什么样的存储系统,来保存像“点击流”这样的海量数据。
## 使用Kafka存储海量原始数据
早期对于这类海量原始数据,都倾向于**先计算再存储**。也就是,在接收原始数据的服务中,先进行一些数据过滤、聚合等初步的计算,将数据先收敛一下,再落存储。这样可以降低存储系统的写入压力,也能节省磁盘空间。
这几年,随着存储设备越来越便宜,并且,数据的价值被不断地重新挖掘,更多的大厂都倾向于**先存储再计算**,直接保存海量的原始数据,再对数据进行实时或者批量计算。这种方案,除了贵以外都是优点:
- 不需要二次分发就可以同时给多个流和批计算任务提供数据;
- 如果计算任务出错,可以随时回滚重新计算;
- 如果对数据有新的分析需求,上线后直接就可以用历史数据计算出结果,而不用去等新数据。
但是,这种方式对保存原始数据的存储系统要求就很高了:既要有足够大的容量,能水平扩容,还要读写都足够快,跟得上数据生产的写入速度,还要给下游计算提供低延迟的读服务。什么样的存储能满足这样的要求呢?这里我给出几种常用的解决方案。
第一种方案是使用Kafka来存储。有的同学会问了Kafka不是一个消息队列么怎么成了存储系统了那我告诉你**现代的消息队列,本质上就是分布式的流数据存储系统。**
如果你感兴趣的话你可以仔细去研究一下Kafka它的数据是如何存储、分片、复制的它是如何保证高可用如何保证数据一致性的那你会发现它和我们之前讲过的那些分布式存储系统并没有什么太大的区别。唯一的区别就是它的查询语言生产和消费消息和存储引擎的数据结构Commit Log比一般的存储系统要简单很多。但也正是因为这个原因使得Kafka的读写性能远远好于其他的存储系统。Kafka官方给自己的定位也是“分布式流数据平台”不只是一个MQ。
Kafka提供“无限”的消息堆积能力具有超高的吞吐量可以满足我们保存原始数据的大部分要求。写入点击流数据的时候每个原始数据采集服务作为一个生产者把数据发给Kafka就可以了。下游的计算任务可以作为消费者订阅消息也可以按照时间或者位点来读取数据。并且Kafka作为事实标准和大部分大数据生态圈的开源软件都有非常好的兼容性和集成度像Flink、Spark等大多计算平台都提供了直接接入Kafka的组件。
<img src="https://static001.geekbang.org/resource/image/ba/8c/ba6bae1b4e59ba2000f0789886248d8c.jpg" alt="">
当然Kafka也不是万能的你可能注意到了我刚刚讲Kafka提供“无限”的消息堆积能力我在这个“无限”上打了个引号这里面还是有一些限制需要注意的。Kafka也支持把数据分片这个在Kafka中叫Partition每个分片可以分布到不同的存储节点上。
写入数据的时候,可以均匀地写到这些分片上,理论上只要分片足够多,存储容量就可以是“无限”的。但是,单个分片总要落到某一个节点上,而单节点的存储容量毕竟是有限的,随着时间推移,单个分片总有写满的时候。
即使它支持扩容分片数量也没办法像其他分布式存储系统那样重新分配数据把已有分片上的数据迁移一部分到新的分片上。所以扩容分片也解决不了已有分片写满的问题。而Kafka又不支持按照时间维度去分片所以**受制于单节点的存储容量Kafka实际能存储的数据容量并不是无限的**。
## Kafka之外还有哪些解决方案
所以,需要长时间(几个月-几年保存的海量数据就不适合用Kafka存储。这种情况下只能退而求其次使用第二种方案了。
第二种方案是使用HDFS来存储。使用HDFS存储数据也很简单就是把原始数据写成一个一个文本文件保存到HDFS中。我们需要按照时间和业务属性来组织目录结构和文件名以便于下游计算程序来读取比如说**“click/20200808/Beijing_0001.csv”**代表2020年8月8日从北京地区用户收集到的点击流数据这个是当天的第一个文件。
对于保存海量的原始数据这个特定的场景来说HDFS的吞吐量是远不如Kafka的。按照平均到每个节点上计算Kafka的吞吐能力很容易达到每秒钟大几百兆而HDFS只能达到百兆左右。这就意味着要达到相同的吞吐能力使用HDFS就要比使用Kafka多用几倍的服务器数量。
但HDFS也有它的优势第一个优势就是它能提供真正无限的存储容量如果存储空间不够了水平扩容就可以解决。另外一个优势是HDFS能提供比Kafka更强的数据查询能力。Kafka只能按照时间或者位点来提取数据而HDFS配合Hive直接就可以支持用SQL对数据进行查询虽然说查询的性能比较差但查询能力要比Kafka强大太多了。
以上这两种方案因为都有各自的优势和不足,在实际生产中,都有不少的应用,你可以根据业务的情况来选择。那有没有兼顾这二者优势的方案呢?最好能做到,既有超高的吞吐能力,又能无限扩容,同时还能提供更好的查询能力,有这样的好事儿么?
我个人的判断是,目前还没有可用大规模于生产的,成熟的解决方案,但未来应该会有的。目前已经有一些的开源项目,都致力于解决这方面的问题,你可以关注一下。
一类是**分布式流数据存储**,比较活跃的项目有[Pravega](https://github.com/pravega/pravega)和Pulsar的存储引擎[Apache BookKeeper](https://github.com/apache/bookkeeper)。我所在的团队也在这个方向上持续探索中,也开源了我们的流数据存储项目[JournalKeeper](https://github.com/chubaostream/journalkeeper)也欢迎你关注和参与进来。这些分布式流数据存储系统走的是类似Kafka这种流存储的路线在高吞吐量的基础上提供真正无限的扩容能力更好的查询能力。
还有一类是**时序数据库Time Series Databases**,比较活跃的项目有[InfluxDB](https://github.com/influxdata/influxdb)和[OpenTSDB](https://github.com/OpenTSDB/opentsdb)等。这些时序数据库,不仅有非常好的读写性能,还提供很方便的查询和聚合数据的能力。但是,它们不是什么数据都可以存的,它们专注于类似监控数据这样,有时间特征并且数据内容都是数值的数据。如果你有存储海量监控数据的需求,可以关注一下这些项目。
## 小结
在互联网行业,点击流、监控和日志这几类数据,是海量数据中的海量数据。对于这类数据,一般的处理方式都是先存储再计算,计算结果保存到特定的数据库中,供业务系统查询。
所以对于海量原始数据的存储系统我们要求的是超高的写入和读取性能和近乎无限的容量对于数据的查询能力要求不高。生产上可以选择Kafka或者是HDFSKafka的优点是读写性能更好单节点能支持更高的吞吐量。而HDFS则能提供真正无限的存储容量并且对查询更友好。
未来会有一些开源的流数据存储系统和时序数据库逐步成熟,并陆续应用到生产系统中去,你可以持续关注这些项目。
## 思考题
课后请你想一下为什么Kafka能做到几倍于HDFS的吞吐能力技术上的根本原因是什么欢迎你在留言区与我讨论。
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,70 @@
<audio id="audio" title="22 | 面对海量数据,如何才能查得更快?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6d/db/6da98294605e6f78fdfa9e7ced4ec8db.mp3"></audio>
你好,我是李玥。
我们接着上节课的话题,来继续说海量数据。上节课我们讲了,如何来保存原始数据,那我们知道,原始数据的数据量太大了,能存下来就很不容易了,这个数据是没法直接来给业务系统查询和分析的。有两个原因,一是数据量太大了,二是也没有很好的数据结构和查询能力,来支持业务系统查询。
所以一般的做法是用流计算或者是批计算把原始数据再进行一次或者多次的过滤、汇聚和计算把计算结果落到另外一个存储系统中去由这个存储再给业务系统提供查询支持。这里的“流计算”指的是Flink、Storm这类的实时计算批计算是Map-Reduce或者Spark这类的非实时计算。
上节课我们说过像点击流、监控和日志这些原始数据是“海量数据中的海量数据”这些原始数据经过过滤汇总和计算之后大多数情况下数据量会有量级的下降比如说从TB级别的数据量减少到GB级别。
有的业务,计算后的数据非常少,比如说一些按天粒度的汇总数据,或者排行榜类的数据,用什么存储都能满足要求。那有一些业务,没法通过事先计算的方式解决全部的问题。原始数据经过计算后产生的计算结果,数据量相比原始数据会减少一些,但仍然是海量数据。并且,我们还要在这个海量数据上,提供性能可以接受的查询服务。
今天这节课我们就来聊一聊,面对这样的海量数据,如何才能让查询更快一些。
## 常用的分析类系统应该如何选择存储?
查询海量数据的系统,大多都是离线分析类系统,你可以简单地理解为类似于做报表的系统,也就是那些主要功能是对数据做统计分析的系统。这类系统是重度依赖于存储的。选择什么样的存储系统、使用什么样的数据结构来存储数据,直接决定了数据查询、聚合和分析的性能。
分析类系统对存储的需求一般是这样的:
1. 一般用于分析的数据量都会比在线业务大出几个数量级,这需要存储系统能保存海量数据;
1. 能在海量的数据上做快速的聚合、分析和查询。注意这里面所说的“快速”前提是处理GB、TB甚至PB级别的海量数据在这么大的数据量上做分析几十秒甚至几分钟都算很快了和在线业务要求的毫秒级速度是不一样的
1. 由于数据大多数情况下都是异步写入,对于写入性能和响应时延,一般要求不高;
1. 分析类系统不直接支撑前端业务,所以也不要求高并发。
然后我们看有哪些可供选择的存储产品。如果你的系统的数据量在GB量级以下MySQL仍然是可以考虑的因为它的查询能力足以应付大部分分析系统的业务需求。并且可以和在线业务系统合用一个数据库不用做ETL数据抽取省事儿并且实时性好。这里还是要提醒你最好给分析系统配置单独的MySQL实例避免影响线上业务。
如果数据量级已经超过MySQL极限可以选择一些列式数据库比如HBase、Cassandra、ClickHouse这些产品对海量数据都有非常好的查询性能在正确使用的前提下10GB量级的数据查询基本上可以做到秒级返回。高性能的代价是功能上的缩水这些数据库对数据的组织方式都有一些限制查询方式上也没有MySQL那么灵活。大多都需要你非常了解这些产品的脾气秉性按照预定的姿势使用才能达到预期的性能。
另外一个值得考虑的选择是ElasticsearchESES本来是一个为了搜索而生的存储产品但是也支持结构化数据的存储和查询。由于它的数据都存储在内存中并且也支持类似于Map-Reduce方式的分布式并行查询所以对海量结构化数据的查询性能也非常好。
最重要的是ES对数据组织方式和查询方式的限制没有其他列式数据库那么死板。也就是说ES的查询能力和灵活性是要强于上述这些列式数据库的。在这个级别的几个选手中我个人强烈建议你优先考虑ES。但是ES有一个缺点就是你需要给它准备大内存的服务器硬件成本有点儿高。
数据量级超过TB级的时候对这么大量级的数据做统计分析无论使用什么存储系统都快不到哪儿去。这个时候的性能瓶颈已经是磁盘IO和网络带宽了。这种情况下实时的查询和分析肯定做不了。解决的办法都是定期把数据聚合和计算好然后把结果保存起来在需要时对结果再进行二次查询。这么大量级的数据一般都选择保存在HDFS中配合Map-Reduce、Spark、Hive等等这些大数据生态圈产品做数据聚合和计算。
## 转变你的思想:根据查询来选择存储系统
面对海量数据,仅仅是根据数据量级来选择存储系统,是远远不够的。
经常有朋友会问“我的系统每天都产生几个GB的数据量现在基本已经慢得查不出来了你说我换个什么数据库能解决问题呢”那我的回答都是对不起换什么数据库也解决不了你的问题。为什么这么说呢
因为在过去的几十年里面存储技术和分布式技术在基础理论方面并没有什么本质上突破。技术发展更多的是体现在应用层面上比如说集群管理简单查询更加自动化像Map-Reduce这些。不同的存储系统之间并没有本质的差异。它们的区别只是存储引擎的数据结构、存储集群的构建方式以及提供的查询能力这些方面的差异。这些差异使得每一种存储在它擅长的一些领域或者场景下会有很好的性能表现。
比如说最近很火的RocksDB、LevelDB它们的存储结构LSM-Tree其实就是日志和跳表的组合单从数据结构的时间复杂度上来说和“老家伙”MySQL采用的B+树有本质的提升吗没有吧时间复杂度都是O(log n)。但是LSM-Tree在某些情况下它利用日志有更好的写性能表现。没有哪种存储能在所有情况下都具有明显的性能优势所以说**存储系统没有银弹,<strong><strong>不要指望简单**</strong>地**<strong>更换一种数据库**</strong>,就可以解决数据量大,查询慢的问题。</strong>
但是,在特定的场景下,通过一些优化方法,把查询性能提升几十倍甚至几百倍,这个都是有可能的。这里面有个很重要的思想就是,**根据查询来选择存储系统和数据结构**。我们前面的课程《[06 | 如何用Elasticsearch构建商品搜索系统](https://time.geekbang.org/column/article/208675)》就是把这个思想实践得很好的一个例子。ES采用的倒排索引的数据结构并没有比MySQL的B+树更快或者说是更先进但是面对“全文搜索”这个查询需求选择使用ES的倒排索引就比使用其他的存储系统和数据结构性能上要高出几十倍。
再举个例子,大家都知道,京东的物流速度是非常快的。经常是,一件挺贵的衣服,下单之后,还没来得及后悔,已经送到了。京东的物流之所以能做到这么快,有一个很重要的原因是,它有一套智能的补货系统,根据历史的物流数据,对未来的趋势做出预测,来给全国每个仓库补货。这样京东就可以做到,你下单买的商品,很大概率在离你家几公里那个京东仓库里就有货,这样自然很快就送到了。这个系统的背后,它需要分析每天几亿条物流数据,每条物流数据又细分为几段到几十段,那每天的物流数据就是几十亿的量级。
这份物流数据,它的用途也非常多,比如说,智能补货系统要用;调度运力的系统也要用;评价每个站点儿、每个快递小哥的时效达成情况,还要用这个数据;物流规划人员同样要用这个数据进行分析,对物流网络做持续优化。
那用什么样的存储系统保存这些物流数据,才能满足这些查询需求呢?显然,任何一种存储系统,都满足不了这么多种查询需求。我们需要根据每一种需求,去专门选择合适的存储系统,定义适合的数据结构,各自解决各自的问题。而不是用一种数据结构,一个数据库去解决所有的问题。
对于智能补货和运力调度这两个系统,它的区域性很强,那我们可以把数据按照区域(省或者地市)做分片,再汇总一份全国的跨区物流数据,这样绝大部分查询都可以落在一个分片上,查询性能就会很好。
对于站点儿和人的时效达成情况这种业务的查询方式以点查询为主那可以考虑事先在计算的时候按照站点儿和人把数据汇总好存放到一些分布式KV存储中基本上可以做到毫秒级查询性能。而对于物流规划的查询需求查询方式是多变的可以把数据放到Hive表中按照时间进行分片。
我们之前也讲到过,按照时间分片是对查询最友好的分片方式。物流规划人员可以在上面执行一些分析类的查询任务,一个查询任务即使是花上几个小时,用来验证一个新的规划算法,也是可以接受的。
## 小结
海量数据的主要用途就是支撑离线分析类业务的查询根据数据量规模不同由小到大可以选择关系型数据库列式数据库和一些大数据存储系统。对于TB量级以下的数据如果可以接受相对比较贵的硬件成本ES是一个不错的选择。
对于海量数据来说,选择存储系统没有银弹,重要的是转变思想,根据业务对数据的查询方式,反推数据应该使用什么存储系统、如何分片,以及如何组织。即使是同样一份数据,也要根据不同的查询需求,组织成不同的数据结构,存放在适合的存储系统中,才能在每一种业务中都达到理想的查询性能。
## 思考题
今天的课后思考题是这样的,我们要做一个日志系统,收集全公司所有系统的全量程序日志,给开发和运维人员提供日志的查询和分析服务,你会选择用什么存储系统来存储这些日志?原因是什么?欢迎你在留言区与我讨论。
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,105 @@
<audio id="audio" title="23 | MySQL经常遇到的高可用、分片问题NewSQL是如何解决的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/57/e0/5767b04db553d7a03aaff673fe06cae0.mp3"></audio>
你好,我是李玥。
在这个系列课程中,我们讲的都是如何解决生产系统中面临的一些存储系统相关的问题。在最后两节课里面,我们来说点儿新东西,看一下存储这个技术领域,可能会有哪些值得关注的新技术。当然,技术圈每天都有很多新的技术出现,也会经常发很多论文,出现很多的开源项目,这些大多数都不太靠谱儿。
今天我给你要说的这个New SQL它是我个人认为非常靠谱甚至在未来可能会取代MySQL这样的关系型数据库的一个技术。MySQL是几乎每一个后端开发人员必须要精通的数据库既然New SQL非常有可能在将来替代MySQL那我们就非常有必要提前去了解一下了。
## 什么是New SQL
什么是New SQL这个说来话长了还要从存储技术发展的历史来解读。我们知道早期只有像MySQL这样的关系数据库这种关系型数据库因为支持SQL语言后来被叫做SQL或者Old SQL。
然后出现了Redis和很多KV存储系统性能上各种吊打MySQL而且因为存储结构简单所以比较容易组成分布式集群并且能够做到水平扩展、高可靠、高可用。因为这些KV存储不支持SQL为了以示区分被统称为No SQL。
No SQL本来希望能凭借高性能和集群的优势替代掉Old SQL。但用户是用脚投票的这么多年实践证明你牺牲了SQL这种强大的查询能力和ACID事务支持用户根本不买账直到今天Old SQL还是生产系统中最主流的数据库。
这个时候大家都开始明白了无论你其他方面做的比Old SQL好再多SQL和ACID是刚需这个命你革不掉的。你不支持SQL就不会有多少人用。所以你看近几年很多之前不支持SQL的数据库都开始支持SQL了甚至于像Spark、Flink这样的流计算平台也都开始支持SQL。当然虽然说支持SQL但这里面各个产品的支持程度是参差不齐的多多少少都有一些缩水。对于ACID的支持基本上等同于就没有。
这个时候New SQL它来了简单地说New SQL就是兼顾了Old SQL和No SQL的优点
- 完整地支持SQL和ACID提供和Old SQL隔离级别相当的事务能力
- 高性能、高可靠、高可用,支持水平扩容。
像Google的Cloud Spanner、国产的OceanBase以及开源的[CockroachDB](https://github.com/cockroachdb/cockroach)都属于New SQL数据库。Cockroach这个英文单词是蟑螂的意思所以一般我们都把CockroachDB俗称为小强数据库。
这些New SQL凭什么就能做到Old SQL和No SQL做不到的这些特性呢那我们就以开源的CockroachDB为例子来看一下New SQL是不是真的这么厉害。
## CockroachDB是如何实现数据分片和弹性扩容的
首先我们一起先来简单看一下CockroachDB的架构从架构层面分析一下它是不是真的像宣传的那么厉害。我们先来看一下它的架构图(图片来自于[官方文档](https://github.com/cockroachdb/cockroach/blob/master/docs/design.md))
<img src="https://static001.geekbang.org/resource/image/8c/82/8c78db973e66bb62b23c8e85afe78082.jpg" alt="">
这是一个非常典型的分层架构我们从上往下看。最上层是SQL层SQL层支持和关系型数据库类似的逻辑数据结构比如说库、表、行和列这些逻辑概念。SQL层向下调用的是一个抽象的接口层Structured Data API实际实现这个API的是下面一层Distributed, Monolithic KV Store这就是一个分布式的KV存储系统。
我们先不深入进去看细节从宏观层面上分析一下这个架构。你可以看到这个架构仍然是我们之间讲过的大部分数据库都采用的二层架构执行器和存储引擎。它的SQL层就是执行器下面的分布式KV存储集群就是它的存储引擎。
那我们知道MySQL的存储引擎InnoDB实际上是基于文件系统的B+树像Hive和HBase它们的存储引擎都是基于HDFS构建的。那CockroachDB这种使用分布式KV存储来作为存储引擎的设计理论上也是可行的并没有什么特别难以逾越的技术壁垒。
而且使用分布式KV存储作为存储引擎实现高性能、高可靠、高可用以及支持水平扩容这些特性就不是什么难事儿了其中很多分布式KV存储系统已经做到了这里面使用的一些技术和方法大多我们在之前的课程中也都讲到过。CockroachDB在实现它的存储引擎这一层就是大量地借鉴甚至是直接使用了已有的一些成熟技术。
它的分片算法采用的是范围分片我们之前也讲到过范围分片对查询是最友好的可以很好地支持范围扫描这一类的操作这样有利于它支撑上层的SQL查询。
它采用[Raft](https://raft.github.io/)一致性协议来实现每个分片的高可靠、高可用和强一致。这个Raft协议它的一个理论基础就是我们之前讲的复制状态机并且在复制状态机的基础上Raft实现了集群自我监控和自我选举来解决高可用的问题。Raft也是一个被广泛采用的、非常成熟的一致性协议比如etcd也是基于Raft来实现的。
CockroachDB的元数据直接分布在所有的存储节点上依靠流言协议来传播这个流言协议我们在《[16 | 用Redis构建缓存集群的最佳实践有哪些](https://time.geekbang.org/column/article/217590)》这节课中也讲到过在Redis Cluster中也是用流言协议来传播元数据变化的。
CockroachDB用上面这些成熟的技术解决了集群问题在单机的存储引擎上更是直接使用了RocksDB作为它的KV存储引擎。RocksDB也是值得大家关注的一个新的存储系统下节课我们会专门讲RocksDB。
你可以看到CockroachDB的存储引擎也就是它的分布式KV存储集群基本上没有什么大的创新就是重用了已有的一些成熟的技术这些技术在我们之前讲过的其他存储系统中全部都见到过。我讲这些并没有贬低CockroachDB的意思相反站在巨人的肩膀上才能看得更远飞得更高这是一种非常务实的做法。
## CockroachDB能提供金融级的事务隔离性么
接下来我们说一下CockroachDB是怎么实现ACID的它的ACID是不是类似于分布式事务的残血版这是一个非常关键的问题直接影响到它有没有可能在未来取代MySQL。
在说ACID之前我们还是要简单说一下CockroachDB是怎么解析和执行SQL的。我们在《[10 | 走进黑盒SQL是如何在数据库中执行的](https://time.geekbang.org/column/article/213176)》这节课中讲过SQL是如何在MySQL中执行的在CockroachDB中这个执行的流程也是差不多的。同样是先解析SQL生成语法树转换成逻辑执行计划再转换为物理执行计划优化后执行物理执行计划返回查询结果这样一个流程。
只是在CockroachDB中物理执行计划就更加复杂了因为它的物理执行计划面对的是一个分布式KV存储系统在涉及到查找、聚合这类操作的时候有可能需要涉及到多个分片Range。大致过程就是类似于Map-Reduce的逻辑先查找元数据确定可能涉及到的分片然后把物理执行计划转换成每个分片上的物理执行计划在每个分片上去并行执行最后再对这些执行结果做汇总。
然后我们再来说CockroachDB的ACID。我们在《[04 | 事务:账户余额总是对不上账,怎么办?](https://time.geekbang.org/column/article/206544)》这节课中讲到过四种事务隔离级别分别是RU、RC、RR和SERIALIZABLE那CockroachDB能提供哪种隔离级别呢答案是以上四种都不是。
CockroachDB提供了另外两种隔离级别分别是**Snapshot Isolation (SI)** 和 **Serializable Snapshot Isolation (SSI)**其中SSI是CockroachDB默认的隔离级别。
这两种隔离级别和之前提到的四种隔离级别是什么关系呢我们通过下面这张表和MySQL默认的隔离级别RR做一个对比。
<img src="https://static001.geekbang.org/resource/image/20/67/20e20d983ad7519e6eae11821a3f1567.jpg" alt="">
首先我们看SI这一行。我们之前讲到过RR这种隔离级别可以很好地解决脏读和不可重复读的问题虽然可能会产生幻读但实际上对绝大多数事务影响不大。SI不会发生脏读、不可重复读也不会发生幻读的情况这个隔离级别似乎比RR还要好。
但你要注意一下我们这个表格比之前那个表格多了一列写倾斜。可以看到RR是不会出现写倾斜问题的但是SI会有写倾斜问题。
什么是写倾斜我们还是拿账户余额的例子来说明。比如说我们的账户需要支持主副卡主卡和副卡都分别有自己的余额并且这个余额是可以透支的只要满足主副卡的余额之和大于0就行了。如果我们需要给主卡支出100元那SQL只要这样写就可以了
```
update account
set balance = balance - 100 -- 在主卡中扣减100元
where id = ? and
(select balance from account where id = ?) -- 主卡余额
+
(select balance from account where id = ?) -- 附卡余额
&gt;= 100; -- 主副卡余额之和必须大于100元
```
在传统的RR隔离级别下由于更新数据时会对记录加锁即使更新主副卡的两个SQL分别在两个事务中并发执行也不会出现把主副卡的余额之和扣减成负数的情况。
但是在SI级别下由于它没有加锁而是采用快照的方式来实现事务的隔离这个时候如果并发地去更新主副卡余额是有可能出现把主副卡余额之和扣减为负数的情况的。这种情况称为**写倾斜**。这里顺便提一句,写倾斜是普遍的译法,我个人觉得“倾斜”这个词翻译得并不准确,实际上它表达的,就是因为没有检测读写冲突,也没有加锁,导致数据写错了。
SSI隔离级别在SI的基础上加入了冲突检测的机制通过检测读写冲突然后回滚事务的方式来解决写倾斜的问题当然这种方式付出的代价是降低性能并且冲突严重的情况下会频繁地出现事务回滚。
从理论上来说CockroachDB支持的SI和SSI这两种事务隔离级别能提供的事务隔离性已经与传统的RC和RR隔离级别不相上下了可以满足大多数在线交易类系统对ACID的要求。
## 小结
New SQL是新一代的分布式数据库它具备原生分布式存储系统高性能、高可靠、高可用和弹性扩容的能力同时还兼顾了传统关系型数据库的SQL支持。更厉害的是它还提供了和传统关系型数据库不相上下的、真正的事务支持具备了支撑在线交易类业务的能力。
CockroachDB是开源的New SQL数据库。它的存储引擎是一个分布式KV存储集群执行器则大量借鉴了PostgreSQL的一些设计和实现是一个集很多现有数据库和分布式存储系统技术于一身这样的一个数据库产品。
从设计上来看CockroachDB这类New SQL数据库有非常大的潜质可以真正地取代MySQL这类传统的关系型数据库。但是我们也应该看到目前这些New SQL数据库都还处于高速发展阶段并没有被大规模地应用到生产系统中去。我也不建议你做小白鼠在重要的系统上去使用它。
## 思考题
课后请你去看一下[Raft](https://raft.github.io/)一致性协议然后简单总结一下CockroachDB是如何利用Raft协议实现Range高可用、高可靠和强一致的。欢迎你在留言区与我讨论。
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。

View File

@@ -0,0 +1,73 @@
<audio id="audio" title="24 | RocksDB不丢数据的高性能KV存储" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5a/21/5a5315e1a88d43a49d589c55f334a121.mp3"></audio>
你好,我是李玥。
上节课我们在讲解CockroachDB的时候提到过CockroachDB的存储引擎是一个分布式的KV存储集群它用了一系列成熟的技术来解决集群问题但是在集群的每个节点上还需要一个单机的KV存储来保存数据这个地方CockroachDB直接使用RocksDB作为它的KV存储引擎。
[RocksDB](https://github.com/facebook/rocksdb)是Facebook开源的一个高性能持久化KV存储。目前你可能很少见到过哪个项目会直接使用RocksDB来保存数据在未来RocksDB大概率也不会像Redis那样被业务系统直接使用。那我们为什么要关注它呢
因为越来越多的新生代数据库都不约而同地选择RocksDB作为它们的存储引擎。在将来很有可能出现什么样的情况呢我们使用的很多不同的数据库它们背后采用的存储引擎都是RocksDB。
我来给你举几个例子。我们上节课讲到的CockroachDB用到了RocksDB作为它的存储引擎。再说几个比较有名的[MyRocks](http://myrocks.io/)这个开源项目你看它这个名字就知道它是干什么的了。它在用RocksDB给MySQL做存储引擎目的是取代现有的InnoDB存储引擎。并且MySQL的亲兄弟MariaDB已经接纳了MyRocks作为它的一个可选的存储引擎。还有大家都经常用的实时计算引擎[Flink](https://flink.apache.org/)用过的同学都知道Flink的State就是一个KV的存储它使用的也是RocksDB。还有包括MongoDB、Cassandra等等很多的数据库都在开发基于RocksDB的存储引擎。
今天这节课我们就一起来了解一下RocksDB这颗“未来之星”。
## 同样是KV存储RocksDB有哪些不同
说到KV存储我们最熟悉的就是Redis了接下来我们就来对比一下RocksDB和Redis这两个KV存储。
其实Redis和RocksDB之间没什么可比性一个是缓存一个是数据库存储引擎放在一起比就像“关公战秦琼”一样。那我们把这两个KV放在一起对比目的不是为了比谁强谁弱而是为了让你快速了解RocksDB能力。
我们知道Redis是一个内存数据库它之所以能做到非常好的性能主要原因就是它的数据都是保存在内存中的。从Redis官方给出的测试数据来看它的随机读写性能大约在50万次/秒左右。而RocksDB相应的随机读写性能大约在20万次/秒左右虽然性能还不如Redis但是已经可以算是同一个量级的水平了。
这里面你需要注意到的一个重大差异是Redis是一个内存数据库并不是一个可靠的存储。数据写到内存中就算成功了它并不保证安全地保存到磁盘上。而RocksDB它是一个持久化的KV存储它需要保证每条数据都要安全地写到磁盘上这也是很多数据库产品的基本要求。这么一比我们就看出来RocksDB的优势了我们知道磁盘的读写性能和内存读写性能差着一两个数量级读写磁盘的RocksDB能和读写内存的Redis做到相近的性能这就是RocksDB的价值所在了。
RocksDB为什么能在保证数据持久化的前提下还能做到这么强的性能呢我们之前反复讲到过一个存储系统它的读写性能主要取决于什么取决于它的存储结构也就是数据是如何组织的。
RocksDB采用了一个非常复杂的数据存储结构并且这个存储结构采用了内存和磁盘混合存储方式使用磁盘来保证数据的可靠存储并且利用速度更快的内存来提升读写性能。或者说RocksDB的存储结构本身就自带了内存缓存。
那我们知道内存缓存可以很好地提升读性能但是写入数据的时候你是绕不过要写磁盘的。因为要保证数据持久化数据必须真正写到磁盘上才行。RocksDB为什么能做到这么高的写入性能还是因为它特殊的数据结构。
大多数存储系统为了能做到快速查找都会采用树或者哈希表这样的存储结构数据在写入的时候必须写入到特定的位置上。比如说我们在往B+树中写入一条数据必须按照B+树的排序方式,写入到某个固定的节点下面。哈希表也是类似,必须要写入到特定的哈希槽中去。
这些数据结构会导致在写入数据的时候不得不在磁盘上这里写一点儿再去那里写一点儿这样跳来跳去地写也就是我们说的“随机写”。而RocksDB它的数据结构可以让绝大多数写入磁盘的操作都是顺序写。那我们知道无论是SSD还是HDD顺序写的性能都要远远好于随机写这就是RocksDB能够做到高性能写入的根本原因。
那我们在《[21 | 类似“点击流”这样的海量数据应该如何存储?](https://time.geekbang.org/column/article/224162)》这节课中讲到过Kafka也是采用顺序读写的方式所以它的读写性能也是超级快。但是这种顺序写入的数据基本上是没法查询的因为数据没有结构想要查询的话只能去遍历。RocksDB究竟使用了什么样的数据结构在保证数据顺序写入的前提下还能兼顾很好的查询性能呢这种数据结构就是**LSM-Tree**。
## LSM-Tree如何兼顾读写性能
LSM-Tree的全称是**The Log-Structured Merge-Tree**是一种非常复杂的复合数据结构它包含了WALWrite Ahead Log、跳表SkipList和一个分层的有序表SSTableSorted String Table。下面这张图就是LSM-Tree的结构图图片来自于论文: [An Efficient Design and Implementation of LSM-Tree based Key-Value Store on Open-Channel SSD](http://ranger.uta.edu/~sjiang/pubs/papers/wang14-LSM-SDF.pdf)
<img src="https://static001.geekbang.org/resource/image/c0/6e/c0ba7aa330ea79a8a1dfe3a58547526e.jpg" alt="">
看起来非常复杂是吧?实际上它的结构比这个图更复杂。那我们尽量忽略没那么重要的细节,把它的核心原理讲清楚。首先需要注意的是,这个图上有一个横向的实线,是内存和磁盘的分界线,上面的部分是内存,下面的部分是磁盘。
我们先来看数据是如何写入的。当LSM-Tree收到一个写请求比如说PUT foo bar把Key foo的值设置为bar。首先这条操作命令会被写入到磁盘的WAL日志中图中右侧的Log这是一个顺序写磁盘的操作性能很好。这个日志的唯一作用就是用于故障恢复一旦系统宕机可以从日志中把内存中还没有来得及写入磁盘的数据恢复出来。这个地方用的还是之前我们多次讲过的复制状态机理论。
写完日志之后数据可靠性的问题就解决了。然后数据会被写入到内存中的MemTable中这个MemTable就是一个按照Key组织的跳表SkipList跳表和平衡树有着类似的查找性能但实现起来更简单一些。写MemTable是个内存操作速度也非常快。数据写入到MemTable之后就可以返回写入成功了。这里面有一点需要注意的是**LSM-Tree在处理写入的过程中直接就往MemTable里写并不去查找这个Key是不是已经存在了**。
这个内存中MemTable不能无限地往里写一是内存的容量毕竟有限另外MemTable太大了读写性能都会下降。所以MemTable有一个固定的上限大小一般是32M。MemTable写满之后就被转换成Immutable MemTable然后再创建一个空的MemTable继续写。这个Immutable MemTable也就是只读的MemTable它和MemTable的数据结构完全一样唯一的区别就是不允许再写入了。
Immutable MemTable也不能在内存中无限地占地方会有一个后台线程不停地把Immutable MemTable复制到磁盘文件中然后释放内存空间。每个Immutable MemTable对应一个磁盘文件MemTable的数据结构跳表本身就是一个有序表写入的文件也是一个按照Key排序的结构这些文件就是SSTable。把MemTable写入SSTable这个写操作因为它是把整块内存写入到整个文件中这同样是一个顺序写操作。
到这里虽然数据已经保存到磁盘上了但还没结束因为这些SSTable文件虽然每个文件中的Key是有序的但是文件之间是完全无序的还是没法查找。这里SSTable采用了一个很巧妙的分层合并机制来解决乱序的问题。
SSTable被分为很多层越往上层文件越少越往底层文件越多。每一层的容量都有一个固定的上限一般来说下一层的容量是上一层的10倍。当某一层写满了就会触发后台线程往下一层合并数据合并到下一层之后本层的SSTable文件就可以删除掉了。合并的过程也是排序的过程除了Level 0第0层也就是MemTable直接dump出来的磁盘文件所在的那一层。以外每一层内的文件都是有序的文件内的KV也是有序的这样就比较便于查找了。
然后我们再来说LSM-Tree如何查找数据。查找的过程也是分层查找先去内存中的MemTable和Immutable MemTable中找然后再按照顺序依次在磁盘的每一层SSTable文件中去找只要找到了就直接返回。这样的查找方式其实是很低效的有可能需要多次查找内存和多个文件才能找到一个Key但实际的效果也没那么差因为这样一个分层的结构它会天然形成一个非常有利于查找的情况越是被经常读写的热数据它在这个分层结构中就越靠上对这样的Key查找就越快。
比如说最经常读写的Key很大概率会在内存中这样不用读写磁盘就完成了查找。即使内存中查不到真正能穿透很多层SStable一直查到最底层的请求还是很少的。另外在工程上还会对查找做很多的优化比如说在内存中缓存SSTable文件的Key用布隆过滤器避免无谓的查找等来加速查找过程。这样综合优化下来可以获得相对还不错的查找性能。
## 小结
RocksDB是一个高性能持久化的KV存储被很多新生代的数据库作为存储引擎。RocksDB在保证不错的读性能的前提下大幅地提升了写入性能这主要得益于它的数据结构LSM-Tree。
LSM-Tree通过混合内存和磁盘内的多种数据结构将随机写转换为顺序写来提升写性能通过异步向下合并分层SSTable文件的方式让热数据的查找更高效从而获得还不错的综合查找性能。
通过分析LSM-Tree的数据结构可以看出来这种数据结构还是偏向于写入性能的优化更适合在线交易类场景因为在这类场景下需要频繁写入数据。
## 思考题
我们刚刚讲了LSM-Tree是如何读写数据的但是并没有提到数据是如何删除的。课后请你去看一下RocksDB或者是LevelDB相关的文档总结一下LSM-Tree删除数据的过程也欢迎你在留言区分享你的总结。
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。