<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>数据库 on 韩永发的博客</title><link>https://thecoolboyhan.github.io/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/</link><description>Recent content in 数据库 on 韩永发的博客</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 09 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://thecoolboyhan.github.io/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/index.xml" rel="self" type="application/rss+xml"/><item><title>读《设计数据密集型应用系统》（第二版）有感-第二部分：分布式数据系统</title><link>https://thecoolboyhan.github.io/p/%E8%AF%BB%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%AF%86%E9%9B%86%E5%9E%8B%E5%BA%94%E7%94%A8%E7%B3%BB%E7%BB%9F%E7%AC%AC%E4%BA%8C%E7%89%88%E6%9C%89%E6%84%9F-%E7%AC%AC%E4%BA%8C%E9%83%A8%E5%88%86%E5%88%86%E5%B8%83%E5%BC%8F%E6%95%B0%E6%8D%AE%E7%B3%BB%E7%BB%9F/</link><pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate><guid>https://thecoolboyhan.github.io/p/%E8%AF%BB%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%AF%86%E9%9B%86%E5%9E%8B%E5%BA%94%E7%94%A8%E7%B3%BB%E7%BB%9F%E7%AC%AC%E4%BA%8C%E7%89%88%E6%9C%89%E6%84%9F-%E7%AC%AC%E4%BA%8C%E9%83%A8%E5%88%86%E5%88%86%E5%B8%83%E5%BC%8F%E6%95%B0%E6%8D%AE%E7%B3%BB%E7%BB%9F/</guid><description>&lt;img src="https://thecoolboyhan.github.io/1.png" alt="Featured image of post 读《设计数据密集型应用系统》（第二版）有感-第二部分：分布式数据系统" /&gt;&lt;h1 id="第二部分分布式数据系统"&gt;第二部分：分布式数据系统
&lt;/h1&gt;&lt;h2 id="第五章数据复制"&gt;第五章、数据复制
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;通过网络在多台机器上保存相同的副本。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;多副本的目的&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;使数据在地理位置上更接近用户，从而降低访问延迟。（CDN）&lt;/li&gt;
&lt;li&gt;当部分组件出现故障，系统依然可以继续工作，从而提高可用性。（高可用，主从）&lt;/li&gt;
&lt;li&gt;扩展至多台机器以同时提供数据访问服务，从而提高读吞量。（负载均衡，分布式）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="主从复制"&gt;主从复制
&lt;/h3&gt;&lt;p&gt;&lt;img alt="1775808893120.png" class="gallery-image" data-flex-basis="607px" data-flex-grow="253" height="372" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775808893120_1775808893148.png" srcset="https://thecoolboyhan.github.io/1775808893120_1775808893148_17492650973118821978_hu_9e25f315822177ec.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775808893120_1775808893148.png 942w" width="942"&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;指定某一个副本为主副本 （或称为主节点）。当客户写数据库时，必须将写请求首先发送给主副本，主副本首先将新数据写入本地存储。&lt;/li&gt;
&lt;li&gt;其他副本则全部称为从副本（或称为从节点）。主副本把新数据写入本地存
储后，然后将数据更改作为复制的日志或更改流发送给所有从副本。每个从副本
获得更改日志之后将其应用到本地，且严格保持与主副本相同的写入顺序。&lt;/li&gt;
&lt;li&gt;客户端从数据库中读数据时，可以在主副本或者从副本上执行查询。再次强调，
只有主副本才可以接受写请求;从客户端的角度来看，从副本都是只读的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="同步和异步"&gt;同步和异步
&lt;/h4&gt;&lt;p&gt;&lt;img alt="1775820827959.png" class="gallery-image" data-flex-basis="538px" data-flex-grow="224" height="381" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775820827959_1775820827976.png" srcset="https://thecoolboyhan.github.io/1775820827959_1775820827976_1362299592965860321_hu_9c51ea7021bfe2a0.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775820827959_1775820827976.png 855w" width="855"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;同步和异步的区别在于主节点是否需要等待从节点返回成功后才算成功。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;同步的优点&lt;/strong&gt;：从节点的数据是完整的，从节点随时可以作为一个可靠的节点来读取数据或者替换主节点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;同步的缺点&lt;/strong&gt;：虽然节点之间复制速度特别快，但只要从节点的一环出现错误，就会导致任务失败。如果同步的从节点过多，会让故障的概率指数级增加。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;半同步&lt;/strong&gt;：一个主节点，一个同步从节点，多个异步的从节点。如果同步的从节点出现问题，则将一个异步的从节点升级成为同步从节点。主节点出现问题，用同步从节点替换主节点。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="配置新的从节点"&gt;配置新的从节点
&lt;/h4&gt;
 &lt;blockquote&gt;
 &lt;p&gt;当添加新的从节点后，如何保证新从节点和主节点之间数据的一致性。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;如何在不停机的情况下，保证新节点的数据追平主节点（逻辑同样可以用来做数据库迁移）&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;在某个时间点，对主节点产出一个数据一致性快照，这样可以避免长时间锁定数据库。（MySQL的innobackupex）&lt;/li&gt;
&lt;li&gt;将此快照拷贝到新的从节点。&lt;/li&gt;
&lt;li&gt;从节点连接主节点并只请求快照点后所发生的数据修改日志（binlog）。&lt;/li&gt;
&lt;li&gt;获取日之后，从节点追平主节点数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="处理失效的节点"&gt;处理失效的节点
&lt;/h4&gt;
 &lt;blockquote&gt;
 &lt;p&gt;即使某个节点中断，也要保证系统总体的持续运行。&lt;strong&gt;高可用&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;从节点失效：追赶恢复数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据从节点数据日志情况，与主从复制日志情况，进行数据追赶。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主节点失效：节点切换&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;需要将某个从节点提升为主节点，同时客户端更新到新的主节点&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;确认主节点确实已失效。&lt;/li&gt;
&lt;li&gt;确认新的主节点。可能需要多数节点达成共识，或者手动选择最接近主节点数据的从节点。让所有从节点同意新的主节点。&lt;/li&gt;
&lt;li&gt;配置应用使用新的主节点。（写请求都到新的主节点）确保旧主节点已降级成从节点，且同意新的主节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;主从切换时可能出现的一些问题：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;异步复制，新的主节点可能没有收到所有旧主节点的数据；选举后，旧主节点又很快上线，出现旧主节点（现在的从节点）数据超过新主节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;旧主节点未完成复制的数据丢弃掉。（会导致一部分数据丢失，违背数据持久化概念）所以应该尽量保证主节点提交的数据可以被全量同步。&lt;/p&gt;
&lt;p&gt;如果程序依赖数据库的数据来生成主键，将直接导致业务系统出现问题。所以最好还是要保证有同步数据库。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超时时间设计的过短：可能导致不必要的主从切换。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应该尽量保持平衡，但其实也没有一个固定的解决方案。所以有些系统为了保证可靠，主从切换还是由运维手动操作。&lt;/p&gt;
&lt;h4 id="复制日志的实现"&gt;复制日志的实现
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;基于语句的复制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主节点把所有写请求，都当做日志发送给从节点。（aof和增量binlog）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任何非确定性的函数调用（now，随机数等）&lt;/li&gt;
&lt;li&gt;如何使用了自增id，必须保证所有库的自增键相同&lt;/li&gt;
&lt;/ol&gt;

 &lt;blockquote&gt;
 &lt;p&gt;可以把主节点执行后的结果当成转换成写语句同步给从库。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;基于预写日志（WAL）传输&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多数数据库都是基于WAL来做数据更新的，完全可以利用WAL，来向从库同步WAL，这样可以做到相同的写入操作。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WAL受限于存储引擎，如果不同的存储引擎（和版本号），采用的WAL将完全不同。（&lt;strong&gt;无法实现热升级，如果需要升级数据库版本，需要停机）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;基于行的逻辑日志复制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过自定义的逻辑日志来进行复制&lt;/p&gt;
&lt;p&gt;新增：日志包含所有相关的列新值&lt;/p&gt;
&lt;p&gt;删除：通过唯一标识来生成删除日志。&lt;/p&gt;
&lt;p&gt;更新：通过唯一标识和需要更新的新列值来生成更新日志。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于触发器的复制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不使用数据库本身，通过第三方工具（触发器）来实现复制。（canel：思想相同，不过实现采用的伪装成数据库从库）&lt;/p&gt;
&lt;p&gt;Oracle的Databus、Postgres的Bucardo&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通常开销更高，也容易出错，但非常灵活。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="复制滞后问题"&gt;复制滞后问题
&lt;/h3&gt;
 &lt;blockquote&gt;
 &lt;p&gt;主从复制要求所有写请求都经由主节点，而任何副本只能接受只读查询。如果一个应用正好从一个异步的从节点读取数据，而该副本落后于主节
点， 则应用可能会读到过期的信息。如果同时向主节点和从节点发起查询请求，可能会查到不同的值。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;主要涉及讨论对于各种复制滞后的问题应该如何解决&lt;/p&gt;
&lt;h4 id="读取自己写入的数据读写请求一致性"&gt;读取自己写入的数据（读写请求一致性）
&lt;/h4&gt;&lt;p&gt;用户提交一些信息，然后查看自己刚刚提交的内容。由于主从复制滞后问题导致读取的内容不一致。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776078935943.png" class="gallery-image" data-flex-basis="538px" data-flex-grow="224" height="477" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776078935943_1776078935961.png" srcset="https://thecoolboyhan.github.io/1776078935943_1776078935961_5656833148592083507_hu_4d0a25c89ab931ae.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776078935943_1776078935961.png 1071w" width="1071"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;读写请求一致性&lt;/strong&gt;：如果用户重新加载页面，总是能看到自己最近提交的更新。（其他用户读取此信息不保证最新）下面是几种实现方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;总是从主节点读取自己的配置信息，对于他人的配置信息从从节点读取。&lt;/li&gt;
&lt;li&gt;跟踪用户的更新请求，如果最近一分钟提交了更新操作，则从主库读取，否则从库。&lt;/li&gt;
&lt;li&gt;客户端本地记录最后一次更新的时间戳，保证查询的信息至少晚于或等于本地记录的时间戳。（这些请求不一定是从主库查询的，可以在多个从库之间轮询，直到查到满足条件的信息）&lt;strong&gt;可能由于不可靠的时钟出错&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果有多个数据中心，必须把用户请求路由到主节点所在的数据中心&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="单调读读一致性"&gt;单调读（读一致性）
&lt;/h4&gt;&lt;p&gt;第一次读取的数据与第二次读取的数据不同。&lt;strong&gt;比强一致性弱，比最终一致性强的保证&lt;/strong&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;如果某个用户依次进行多 次读取，则他绝不会看到回滚现象，即在读取较新值之后又发生读旧值的情况。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="1776079728506.png" class="gallery-image" data-flex-basis="398px" data-flex-grow="166" height="612" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776079728506_1776079728523.png" srcset="https://thecoolboyhan.github.io/1776079728506_1776079728523_7463545326944294423_hu_689df16cfb2bd48e.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776079728506_1776079728523.png 1017w" width="1017"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确保同一个用户每次都从同一个副本查询，而不是每次请求都随机路由。（但如果被路由的节点失效，失效节点的所有用户都要重新分配）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="前缀一致性读happened-before"&gt;前缀一致性读（happened-before）
&lt;/h4&gt;
 &lt;blockquote&gt;
 &lt;p&gt;对于存在因果关系的数据，必须要严格按照顺序复制。有点类似于jit需要保证乱序生成的代码，单线程结果一致性。（有序性）&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="1776080260657.png" class="gallery-image" data-flex-basis="373px" data-flex-grow="155" height="657" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776080260657_1776080260680.png" srcset="https://thecoolboyhan.github.io/1776080260657_1776080260680_1481919334157584367_hu_c40f54c1c4d3125.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776080260657_1776080260680.png 1023w" width="1023"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;微信聊天记录就存在这样的问题，产生这个情况的原因通常是：分布式写请求分片有多个，不能保证全集群写入顺序的一直。就会导致从分片读到完全乱序的情况。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;所有具有因果关系的写请求都交给同一个分片来完成。（这样做效率会大打折扣）&lt;/li&gt;
&lt;li&gt;可以用过一些happened-before算法来追踪因果关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="多主节点复制"&gt;多主节点复制
&lt;/h3&gt;&lt;p&gt;每个节点既扮演主节点，也同时扮演者其他主节点的从节点角色。&lt;/p&gt;
&lt;h4 id="适用场景"&gt;适用场景
&lt;/h4&gt;
 &lt;blockquote&gt;
 &lt;p&gt;多主模式逻辑复杂，在同一个数据中心内部使用没有意义，通过在多数据中心场景中使用。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;多数据中心&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了容忍数据中心级别的故障，或者更接近用户，可以把数据库的副本横跨多个数据中心。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776080935015.png" class="gallery-image" data-flex-basis="457px" data-flex-grow="190" height="566" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776080935015_1776080935033.png" srcset="https://thecoolboyhan.github.io/1776080935015_1776080935033_10147125442222609797_hu_16035d1be1e5d7d2.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776080935015_1776080935033.png 1079w" width="1079"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主从和多主之间的对比&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;场景&lt;/th&gt;
					&lt;th&gt;主从&lt;/th&gt;
					&lt;th&gt;多主&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;写性能&lt;/td&gt;
					&lt;td&gt;写请求必须传到主节点所在的数据中心。&lt;br /&gt;写入延迟高&lt;/td&gt;
					&lt;td&gt;写请求可以在自己最近的数据中心完成。然后把数据复制给其他数据中心&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据中心故障&lt;/td&gt;
					&lt;td&gt;如果主节点所在的数据中心发成故障，必须把另一个数据中心提升为主数据中心&lt;/td&gt;
					&lt;td&gt;每个数据中心独立运行，即使某个数据中心挂了也不影响其他数据中心&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;网络问题&lt;/td&gt;
					&lt;td&gt;对于同步的主从模式，需等待同步节点写完才能写入成功，需依赖数据中心之间的网络&lt;/td&gt;
					&lt;td&gt;每个数据中心之间异步通讯，只需要依赖数据中心本地的网络。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;

 &lt;blockquote&gt;
 &lt;p&gt;多主模式同样也带来了许多问题：&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;不同的数据中心可能会同时修改相同的数据，因而必须解决潜在的写冲突。&lt;/li&gt;
&lt;li&gt;自增id问题：可能由于同步的不及时导致每个数据中心之间，相同数据自增id不同。（多数据中心不建议使用自增id）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;离线客户端操作&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;应用在与网络断开后还需要继续工作&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;每个设备都有一个充当主节点的本地数据库（用来接受写请求）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;协作编辑&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;实时协作编辑应用程序允许多个用户同时编辑文档。（在线文档）&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;当一个用户编辑文档时 ，所做的更改会立即应用到本地副本，然后异步复制到服务器以及编辑同一文档的其他用户。&lt;/p&gt;
&lt;h4 id="处理写冲突"&gt;处理写冲突
&lt;/h4&gt;&lt;p&gt;&lt;img alt="1776159379111.png" class="gallery-image" data-flex-basis="456px" data-flex-grow="190" height="630" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776159379111_1776159379130.png" srcset="https://thecoolboyhan.github.io/1776159379111_1776159379130_5865622095274203861_hu_5fbb4a7743a84593.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776159379111_1776159379130.png 1197w" width="1197"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何避免冲突&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过应用层来指定特定记录的写请求总是通过同一个主节点，这样就不会发生冲突。（有点违背多主模式的冲突，变成了主从模式的变种）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;收敛于一致状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据更新符合顺序性原则，即如果同一个字段有多个更新，则最后一个写操作将决定该字段的最终值。（可能会导致最终值的不确定性）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何实现收敛一致&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;最大id&lt;/strong&gt;：所有写请求分配一个唯一的id（时间戳+uuid）所有数据同步时，只保留id最大的数据（最终一致）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合并一致&lt;/strong&gt;：让需合并的结果按照一定规则排序，只取序列最靠后的。&lt;/li&gt;
&lt;li&gt;同2，应用自定义合并规则。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一些常见的自动解决并发修改冲突算法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无冲突的复制数据类型（ CRDT）：多个用户同时编写map、list等。&lt;/li&gt;
&lt;li&gt;可合并的持久数据结构（Mergeable persistent data) ：类似git跟踪变更历史，三向合并。&lt;/li&gt;
&lt;li&gt;操作转换（0perationaI transformation）：Etherpad和Google
Docs等协作编辑应用背后的冲突解决算法。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="复制的拓扑结构"&gt;复制的拓扑结构
&lt;/h4&gt;&lt;p&gt;&lt;img alt="1776160336808.png" class="gallery-image" data-flex-basis="699px" data-flex-grow="291" height="417" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776160336808_1776160336825.png" srcset="https://thecoolboyhan.github.io/1776160336808_1776160336825_8677496604548692962_hu_81ee16dc969edac.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776160336808_1776160336825.png 1215w" width="1215"&gt;&lt;/p&gt;
&lt;p&gt;不同拓扑结构对于容错、是否有中心、复制成本各有优缺点，这里就不展开说明了。&lt;/p&gt;
&lt;h3 id="无主节点复制"&gt;无主节点复制
&lt;/h3&gt;
 &lt;blockquote&gt;
 &lt;p&gt;放弃主节点，允许任何副本直接接受来自客户端的写请求。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h4 id="节点失效时写入数据库"&gt;节点失效时写入数据库
&lt;/h4&gt;&lt;p&gt;当节点失效时，用户不关心自己写入的节点是否发生变化，更不需要进行节点提权等操作。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776235841983.png" class="gallery-image" data-flex-basis="416px" data-flex-grow="173" height="621" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776235841983_1776235841999.png" srcset="https://thecoolboyhan.github.io/1776235841983_1776235841999_3170939467285333751_hu_f629ae594f903dbd.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776235841983_1776235841999.png 1077w" width="1077"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;用户向多个节点同时发起写请求，只有超过半数的节点写入成功，则此请求成功。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;读修复：用户读取时决定值（通过半数以上的节点返回来确认值）&lt;/li&gt;
&lt;li&gt;反熵：后台进程自动查找节点之间的差异并修复。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但上面的例子，如果有多个用户同时写入则可能出现如下问题：&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776236143641.png" class="gallery-image" data-flex-basis="456px" data-flex-grow="190" height="525" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776236143641_1776236143662.png" srcset="https://thecoolboyhan.github.io/1776236143641_1776236143662_5889830014517212301_hu_ad9a55c3625942.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776236143641_1776236143662.png 998w" width="998"&gt;&lt;/p&gt;
&lt;h4 id="处理并发写入"&gt;处理并发写入
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;最后写入胜利法（last write wins）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于每个客户端在写入时都不会互相感知，且由于网络关系，无法区分那个写入一定在哪个写入之后。&lt;/p&gt;
&lt;p&gt;可以强制对所有的写入进行排序：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为所有写请求附加一个时间戳，然后选择最新即最大的时间戳，丢弃较早时间戳的写入。&lt;/strong&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;以上思想，在zookeeper，raft，各种分布式一致性问题中都有借鉴。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;happens-before关系和并发&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;happens-before&lt;/strong&gt;：B的操作明确依赖A，具有先后关系&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;并发&lt;/strong&gt;：A和B的操作“同时”，且完全独立的，互相不感知&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;为更好地定义并发性，我们并不依赖确切的发生时间，即不管物理的时机如何，如果两个操作并不需要意识到对方，我们即可声称它们是并发操作。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="1776237713852.png" class="gallery-image" data-flex-basis="393px" data-flex-grow="163" height="621" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776237713852_1776237713868.png" srcset="https://thecoolboyhan.github.io/1776237713852_1776237713868_4221347936311696633_hu_a243907dd09ac658.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776237713852_1776237713868.png 1017w" width="1017"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;两个客户端同时多次向一个购物车中添加值，且相互不感知。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;对于单个客户端，每次添加操作是有前后依赖关系的（happened-before），对于两个客户端之间，是“同时”发起的添加操作（并发）&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776237869975.png" class="gallery-image" data-flex-basis="740px" data-flex-grow="308" height="318" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776237869975_1776237870001.png" srcset="https://thecoolboyhan.github.io/1776237869975_1776237870001_426671764739883640_hu_f0e1cb64ff53c82a.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776237869975_1776237870001.png 981w" width="981"&gt;&lt;/p&gt;
&lt;p&gt;服务器具体处理步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;服务器为每个主键维护一个版本号，当主键新值写入时，递增版本号，并将版本号和值一起保存。&lt;/li&gt;
&lt;li&gt;当客户端读取主键时，服务器将返回所有（未被覆盖的）当前值以及最新的版本号。且要求写之前， 客户必须先发送读请求。（读取最新值）&lt;/li&gt;
&lt;li&gt;客户端写主键，写请求必须包含之前读到的版本号、读到的值和新值合并后的集合。写请求的响应可以像读操作一样，会返回所有当前值，这样就可以像购物车例子那样一步步链接起多个写入的值。&lt;/li&gt;
&lt;li&gt;当服务器收到带有特定版本号的写入时，覆盖该版本号或更低版本的所有值（因为知道这些值已经被合并到新传入的值集合中），但必须保存更高版本号的所有值（因为这些值与当前的写操作属于并发）。&lt;/li&gt;
&lt;/ol&gt;

 &lt;blockquote&gt;
 &lt;p&gt;整体逻辑有点像kafka集群的值是否写入成功逻辑，如果同步指针追上了的值，才算写入成功。这里只不过是相反的，每次写入都删除当前写入依赖版本号之前的所有版本。（算是MVCC的一种体现）&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;font color='red'&gt;有依赖关系的值可以覆盖，并发的值需保存多份。可以保证并发写入的数据不会丢失&lt;/font&gt;&lt;/p&gt;
&lt;h2 id="第六章数据分区"&gt;第六章、数据分区
&lt;/h2&gt;&lt;p&gt;分区通常与复制结合使用，即每个分区在多个节点都存有副本。这意味着某条记录属于特定的分区 ，而同样的内容会保存在不同的节点上以提高系统的容错性。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776666679919.png" class="gallery-image" data-flex-basis="396px" data-flex-grow="165" height="546" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776666679919_1776666679935.png" srcset="https://thecoolboyhan.github.io/1776666679919_1776666679935_9453130526910714878_hu_2e9a7bb3d93f022c.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776666679919_1776666679935.png 903w" width="903"&gt;&lt;/p&gt;
&lt;h3 id="键-值数据的分区常见的分区方式"&gt;键-值数据的分区（常见的分区方式）
&lt;/h3&gt;
 &lt;blockquote&gt;
 &lt;p&gt;分区的主要目标是将数据和查询负载均匀分布在所有节点上。如果节点平均分担负载，那么理论上10个节点应该能够处理10倍的数据量10倍于单个节点的读写吞吐量。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h4 id="基于关键字分区"&gt;基于关键字分区
&lt;/h4&gt;&lt;p&gt;为每个分区分配一段连续的关键字或者关键字区间范围。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776667342211.png" class="gallery-image" data-flex-basis="572px" data-flex-grow="238" height="420" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776667342211_1776667342230.png" srcset="https://thecoolboyhan.github.io/1776667342211_1776667342230_309121480110095085_hu_f63ab8b29e2355d5.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776667342211_1776667342230.png 1002w" width="1002"&gt;&lt;/p&gt;
&lt;p&gt;每个分区可以按照关键字排序保存（LSM-Trees）。这样可以轻松的支持区间查询。&lt;/p&gt;
&lt;p&gt;缺点：某些访问模式会导致热点。如果数据按照每天一个分区，每天所有的写入都会在同一哥分区，会导致单个分区负载过高，其他分区一直处于空闲状态。&lt;/p&gt;
&lt;h4 id="基于关键字hash值进行分区"&gt;基于关键字hash值进行分区
&lt;/h4&gt;&lt;p&gt;一个好的hash函数可以处理数据倾斜并使其均匀分布。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776668365147.png" class="gallery-image" data-flex-basis="611px" data-flex-grow="254" height="351" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776668365147_1776668365165.png" srcset="https://thecoolboyhan.github.io/1776668365147_1776668365165_14972998588462197277_hu_5d48927968d939c8.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776668365147_1776668365165.png 894w" width="894"&gt;&lt;/p&gt;
&lt;p&gt;基于一致性hash的分区方式，可以进行高效的查询，但是却使数据丧失了有序性。&lt;/p&gt;
&lt;p&gt;有些数据库采用hash分区的数据库直接禁用范围查询、或者把查询语句发送到所有的分区上。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cassandra的折中方案&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;声明一个由多列组成的符合主键。多列主键的第一部分用于hash分区，其他列用于对sstable的排序。（可以支持对于其他部分的区间查询）&lt;/p&gt;
&lt;h4 id="负载倾斜和热点"&gt;负载倾斜和热点
&lt;/h4&gt;&lt;p&gt;基于hash的方法可以减轻热点，但无法做到完全避免热点。一个极端的情况是所有的读/写操作都针对同一个关键字，最终所有的请求都会被路由到同一个分区。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;在社交媒体网站上，一个名人发布了热点事件，出现了大量相同关键字的写操作。此时hash起不到任何帮助，相同id的hash值相同。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;一种无奈的解决方案&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果某个关键字被认定为热点，就在关键字的开头或者结尾添加一个随机数。只需一个两位数的十进制随机数就可以将关键字的写操作分布到 100 个不同的关键字上，从而分配到不同的分区上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：但之后所有的读操作都需要额外的工作，必须从所有100个关键字中读取数据然后进行合并。因此通常只对少量关键字做随机数才有意义。&lt;/p&gt;
&lt;h3 id="分区和二级索引"&gt;分区和二级索引
&lt;/h3&gt;&lt;p&gt;二级索引通常不能唯一标记一条数据，而是用来加速特定值的查询。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;二级索引不能规整的映射到分区中。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h4 id="基于文档分区的二级索引"&gt;基于文档分区的二级索引
&lt;/h4&gt;&lt;p&gt;&lt;img alt="1776672235920.png" class="gallery-image" data-flex-basis="457px" data-flex-grow="190" height="486" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776672235920_1776672235940.png" srcset="https://thecoolboyhan.github.io/1776672235920_1776672235940_12899941974437972288_hu_2ffaef7482ad215a.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776672235920_1776672235940.png 927w" width="927"&gt;&lt;/p&gt;
&lt;p&gt;每个分区完全独立，各自维护自己的二级索引，且只负责自己分区内的文档而不关心其他分区中的数据。文档分区索引也被称为本地索引，而不是全局索引。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;二级索引的查询&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果想要查询特定颜色的车使用二级索引，需要将查询发送的所有的分区，然后合并所有返回结果。（可以采用并行查询）&lt;/p&gt;
&lt;h4 id="基于词条的二级索引分区"&gt;基于词条的二级索引分区
&lt;/h4&gt;&lt;p&gt;对所有的数据构建全局索引，而不是每个分区维护自己的本地索引。为了避免成为瓶颈，不能将全局索引存储在一个节点上，否则就破坏了设计分区均衡的目标。&lt;font color='red'&gt;全局索引也必须进行分区，且可以使用与数据关键字不同的分区策略。&lt;/font&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776672706869.png" class="gallery-image" data-flex-basis="522px" data-flex-grow="217" height="441" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776672706869_1776672706886.png" srcset="https://thecoolboyhan.github.io/1776672706869_1776672706886_4607367749372133390_hu_79dbf54378890f76.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776672706869_1776672706886.png 960w" width="960"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：查询足够高效，且不需要把查询分配给所有分区然后聚合，客户只需要向包含词条的分区发送读请求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：写入速度较慢且非常复杂，单个文档更新时，里面可能会涉及多个二级索引，二级索引的分区又可能完全不同甚至完全在不同的节点上，会引入显著的写放大。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;由于二级索更新需要一个跨多个相关分区的分布式事务支持，写入速度极慢。因此大部分数据库都不支持同步更新二级索引。对全局二级索引的更新往往是异步的。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="分区再平衡"&gt;分区再平衡
&lt;/h3&gt;
 &lt;blockquote&gt;
 &lt;p&gt;当增加节点时，如何将之前的数据进行再平衡。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;分区再平衡想要达到的效果&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;平衡之后，负载、数据存储、读写请求等应该在集群范围更均匀地分布。&lt;/li&gt;
&lt;li&gt;再平衡执行过程中，数据库应该可 继续正常提供读写服务。&lt;/li&gt;
&lt;li&gt;避免不必要的负载迁移，以加快动态再平衡，井尽量减少网络和磁盘IO影响。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="动态再平衡策略"&gt;动态再平衡策略
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;为什么不推荐使用取模&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果频繁的增加节点，会导致大量的数据频繁的迁移，大大增加了再平衡的成本。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;固定数量的分区&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;创建远超实际节点数的分区数，然后为每个节点分配多个分区。&lt;/li&gt;
&lt;li&gt;如果集群中增加了一个新节点，该新节点可以从每个分区上匀走几个分区，直到分区再次达到全局平衡。&lt;/li&gt;
&lt;li&gt;被选中的整个分区会在映射节点之间迁移，但分区的总数量仍然维持不变，也不会改变关键字到分区的映射关系。（不需要像取模一样对每个key重新计算分区值）&lt;/li&gt;
&lt;li&gt;唯一需要调整的是分区与节点的对应关系。调整可以逐步动态完成。在此期间，旧的分区仍然可以接收读取请求。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="1776674146965.png" class="gallery-image" data-flex-basis="415px" data-flex-grow="173" height="606" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776674146965_1776674146986.png" srcset="https://thecoolboyhan.github.io/1776674146965_1776674146986_2352943668059462201_hu_bb2cd90439495099.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776674146965_1776674146986.png 1050w" width="1050"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;动态分区&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当一个分区的数据量增长超过一个阈值，就会被拆分成两个分区，每个承担一半数据量。&lt;/p&gt;
&lt;p&gt;如果大量数据被删除，且分区缩小到某个阈值，则将其相邻的分区合并。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;HBase 通过HDFS分布式文件系统来实现分区文件的传输&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;按节点比例分区&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使分区数与集群节点数量成正比，每个节点都有固定数量的分区。当节点数不变，每个分区的大小与数据集大小保持正比的增长关系；&lt;font color='red'&gt;当节点数量增加，分区则会变小。大量的数据需要大量的节点来存储，这种方式可以使每个分区大小保持稳定。&lt;/font&gt;&lt;/p&gt;
&lt;h3 id="请求路由"&gt;请求路由
&lt;/h3&gt;
 &lt;blockquote&gt;
 &lt;p&gt;当客户端发送请求是，如何知道应该连接哪个节点？其实就是一个服务发现问题。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;几种常见的路由策略&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776685174145.png" class="gallery-image" data-flex-basis="496px" data-flex-grow="207" height="510" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776685174145_1776685174164.png" srcset="https://thecoolboyhan.github.io/1776685174145_1776685174164_15950665911959225207_hu_a5282f1f3721727e.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776685174145_1776685174164.png 1056w" width="1056"&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端连接任意节点，有节点把这个请求转发到正确的节点，再返回给客户端。（redis）&lt;/li&gt;
&lt;li&gt;将所有客户端的请求都发送到一个路由层，路由层负责把请求转发到对应的分区节点上。路由层本身不处理请求，只负责负载均衡。（nginx）&lt;/li&gt;
&lt;li&gt;客户端感知分区和节点关系。客户端可以直接连接到目标节点，而不需要其他中介。（springcloud注册中心的做法）&lt;/li&gt;
&lt;/ol&gt;

 &lt;blockquote&gt;
 &lt;p&gt;做出路由的组件，需要知道分区和节点的关系，以及变化情况。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;大部分数据系统通过zookeeper来维护分区和节点的映射关系。一旦分区发生变化，zookeeper主动通知路由层来保持最新状态。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776751946342.png" class="gallery-image" data-flex-basis="485px" data-flex-grow="202" height="570" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776751946342_1776751946379.png" srcset="https://thecoolboyhan.github.io/1776751946342_1776751946379_14197095360590105663_hu_4b8f677ea01085ec.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776751946342_1776751946379.png 1152w" width="1152"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;关于IP地址的变化，可以借助机器自己的DNS就可以了。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h4 id="并行查询执行"&gt;并行查询执行
&lt;/h4&gt;
 &lt;blockquote&gt;
 &lt;p&gt;查询优化器会把复杂的查询分解成许多执行阶段和分区，以便在集群的不同节点上并行执行。尤其是涉及全表扫描的查询操作，可以通过并行执行获益颇多。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="第七章事务"&gt;第七章、事务
&lt;/h2&gt;&lt;h3 id="深入理解事务"&gt;深入理解事务
&lt;/h3&gt;&lt;h4 id="acid的含义"&gt;ACID的含义
&lt;/h4&gt;
 &lt;blockquote&gt;
 &lt;p&gt;我之前多次记录过关于ACID的文章，对于ACID等详细说明推荐看我在&lt;a class="link" href="https://thecoolboyhan.github.io/p/icyfenix-argitektuur/#%e4%ba%8b%e5%8a%a1%e5%a4%84%e7%90%86" target="_blank" rel="noopener"
 &gt;凤凰架构里记录的文章&lt;/a&gt;。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;不符合ACID的系统被称为BASE，基本可用（Basically Available），软状态（Soft state）和最终 一致性（Eventual consistency)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原子性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;多线程编程中，如果某线程执行了原子操作，这意味着其他线程是无法看到该操作的中间结果。只能处于操作前和操作后的状态，而不是两者之间。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;在ACID中，多线程访问相同变量是由隔离性来保证的&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;ACID的原子性：在出错时中止事务，并将部分完成的写入全部丢弃。（可随意中止性，从而达到可重试的目的。）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一致性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ACID的一致性：对数据有特定的预期状态，任何数据更改必须满足这些状态约束（或者恒等条件）。（贷款系统中，贷款余额应和借款余额保持平衡。）&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;原子性，隔离性和持久性是数据库自身的属性，而ACID 中的一致性更多是应用层的属性。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;应用程序可能借助数据库提供的原子性和隔离性，以达到一致性，但一致性本身并不拥于数据库。&lt;/p&gt;
&lt;p&gt;&lt;font color='red'&gt;字母 其实并不应该属于ACID&lt;/font&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;隔离性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776754373410.png" class="gallery-image" data-flex-basis="679px" data-flex-grow="283" height="426" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776754373410_1776754373443.png" srcset="https://thecoolboyhan.github.io/1776754373410_1776754373443_6537138988483763361_hu_bc3468d3a15ce804.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776754373410_1776754373443.png 1206w" width="1206"&gt;&lt;/p&gt;
&lt;p&gt;ACID的隔离性：并发执行的多个事务相互隔离，它们不能互相交叉。&lt;/p&gt;
&lt;p&gt;相互交叉其实有两个表现，下面是mysql对于两个场景的措施&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读取：查询到其他事务可能在使用的变量。（通过MVCC快照读这种弱隔离性来实现）&lt;/li&gt;
&lt;li&gt;修改：修改相同的变量（通过锁机制，保证一个变量无法被两个线程修改）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;持久性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于单机程序，持久性表示数据已经写入了非易失的存储设备（如硬盘）&lt;/p&gt;
&lt;p&gt;对于支持远程复制的数据库，持久性意味着数据已成功复制到多个节点。&lt;/p&gt;
&lt;p&gt;数据库必须等到这些写入或者复制完成之后才能报告事务成功提交。&lt;/p&gt;
&lt;h3 id="弱隔离级别"&gt;弱隔离级别
&lt;/h3&gt;
 &lt;blockquote&gt;
 &lt;p&gt;关于mysql不同隔离级别的实现，已经不同级别的锁实现可以看我的&lt;a class="link" href="[mysql%e7%9a%84%e5%90%84%e7%a7%8d%e9%94%81]%28https://thecoolboyhan.github.io/p/mysql-lock/%29" &gt;这篇文章。&lt;/a&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;隔离是假装没有发生并发，可串行化隔离意味着数据库保证事务的最终执行结果与串行执行结果相同。&lt;/p&gt;
&lt;p&gt;可串行化会严重影响性能，而许多数据库却不愿意牺牲性能，因此更多倾向于采用较弱的隔离级别。&lt;font color='red'&gt;它可以防止某些但并非全部的并发问题。&lt;/font&gt;&lt;/p&gt;
&lt;h4 id="读-提交"&gt;读-提交
&lt;/h4&gt;&lt;ol&gt;
&lt;li&gt;读数据库时，只能看到已成功提交的数据（防止脏读）&lt;/li&gt;
&lt;li&gt;写数据库时，只会覆盖已成功提交的数据（防止脏写）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;防止脏读&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;脏读：一个事务看到另一个事务尚未提交的内容。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="1776770570279.png" class="gallery-image" data-flex-basis="672px" data-flex-grow="280" height="396" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776770570279_1776770570309.png" srcset="https://thecoolboyhan.github.io/1776770570279_1776770570309_17432968387963840281_hu_ceb8a2dc3e660c6a.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776770570279_1776770570309.png 1110w" width="1110"&gt;&lt;/p&gt;
&lt;p&gt;如果事务需要更新多个对象，脏读意味着另一个事务可能会看到部分更新，而非全部。&lt;/p&gt;
&lt;p&gt;如果事务发生中止，则所有写入操作都需要回滚。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;防止脏写&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;脏写：两个事务同时修改相同的值，一个事务把另一个事务未提交的值修改了。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;读已提交解决脏写的方式是一个事务等待另一个事务提交后，才能修改另一个事务已经修改了的值。（利用锁）&lt;/p&gt;
&lt;p&gt;如果事务需要更新多个对象，脏写会带来非预期的错误结果。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1776770882229.png" class="gallery-image" data-flex-basis="460px" data-flex-grow="191" height="585" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776770882229_1776770882253.png" srcset="https://thecoolboyhan.github.io/1776770882229_1776770882253_12135399061107043869_hu_c92fe73f4debc0bc.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1776770882229_1776770882253.png 1122w" width="1122"&gt;&lt;/p&gt;
&lt;p&gt;多事务的不同写入顺序导致结果不一致。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现读-提交&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;防止脏写&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;数据库通常采用行级锁来防止脏写：当事务想修改某个对象（例如行或文档）时，它必须首先获得该对象的锁；然后一直持有锁直到事务提交（或中止）。如果有另一个事务尝试更新同一个对象，则必须等待。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;防止脏读&lt;/strong&gt;：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;不能利用锁来解决脏读，因为长时间的写事务会导致许多只读的事务等待太长时间，任何局部的写入都会扩散进而影响整个应用。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;对于每个待更新的对象，数据库都会维护其旧值和当前持锁事务将要设置的新值两个版本。在事务提交之前，所有其他读操作都读取旧值；仅当写事务提交之后，才会切换到读取新值。&lt;/p&gt;
&lt;h4 id="快照级别隔离与可重复度"&gt;快照级别隔离与可重复度
&lt;/h4&gt;</description></item><item><title>读《设计数据密集型应用系统》（第二版）有感-第一部分：数据系统基础</title><link>https://thecoolboyhan.github.io/p/%E8%AF%BB%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%AF%86%E9%9B%86%E5%9E%8B%E5%BA%94%E7%94%A8%E7%B3%BB%E7%BB%9F%E7%AC%AC%E4%BA%8C%E7%89%88%E6%9C%89%E6%84%9F-%E7%AC%AC%E4%B8%80%E9%83%A8%E5%88%86%E6%95%B0%E6%8D%AE%E7%B3%BB%E7%BB%9F%E5%9F%BA%E7%A1%80/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><guid>https://thecoolboyhan.github.io/p/%E8%AF%BB%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%AF%86%E9%9B%86%E5%9E%8B%E5%BA%94%E7%94%A8%E7%B3%BB%E7%BB%9F%E7%AC%AC%E4%BA%8C%E7%89%88%E6%9C%89%E6%84%9F-%E7%AC%AC%E4%B8%80%E9%83%A8%E5%88%86%E6%95%B0%E6%8D%AE%E7%B3%BB%E7%BB%9F%E5%9F%BA%E7%A1%80/</guid><description>&lt;img src="https://thecoolboyhan.github.io/p/%E8%AF%BB%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%AF%86%E9%9B%86%E5%9E%8B%E5%BA%94%E7%94%A8%E7%B3%BB%E7%BB%9F%E7%AC%AC%E4%BA%8C%E7%89%88%E6%9C%89%E6%84%9F-%E7%AC%AC%E4%B8%80%E9%83%A8%E5%88%86%E6%95%B0%E6%8D%AE%E7%B3%BB%E7%BB%9F%E5%9F%BA%E7%A1%80/1.png" alt="Featured image of post 读《设计数据密集型应用系统》（第二版）有感-第一部分：数据系统基础" /&gt;&lt;h1 id="第一部分数据系统基础"&gt;第一部分、数据系统基础
&lt;/h1&gt;&lt;h2 id="第一章可靠可扩展与可维护的应用系统"&gt;第一章：可靠、可扩展与可维护的应用系统
&lt;/h2&gt;&lt;p&gt;&lt;img alt="1774323581795.png" class="gallery-image" data-flex-basis="482px" data-flex-grow="201" height="552" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/2026-03/1774323581795_1774323581820.png" srcset="https://thecoolboyhan.github.io/1774323581795_1774323581820_284642516321311051_hu_45b00c38fe1fe51.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/2026-03/1774323581795_1774323581820.png 1110w" width="1110"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;当今许多新型应用都属于数据密集型，而不是计算密集型。对于这些类型应用， CPU的处理能力往往不是第一限制性因素，关键在于数据量、数据的复杂度及数据的快速多变性。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;常见的数据系统模块&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据库&lt;/strong&gt;：用以存储数据，这样之后应用可以再次面问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高速缓存&lt;/strong&gt;： 缓存那些复杂或操作代价昂贵的结果，以加快下一次访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;索引&lt;/strong&gt;： 用户可以按关键字搜索数据井支持各种过滤。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流式处理&lt;/strong&gt;：持续发送消息至另一个进程，处理采用异步方式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批处理&lt;/strong&gt;： 定期处理大量的累积数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一个常见的数据系统，往往有多个模块组成，各自负责不同的工作。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1774419902112.png" class="gallery-image" data-flex-basis="329px" data-flex-grow="137" height="828" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-03/1774419902112_1774419902136.png" srcset="https://thecoolboyhan.github.io/1774419902112_1774419902136_12100887492659160016_hu_809495d2fb03f356.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-03/1774419902112_1774419902136.png 1137w" width="1137"&gt;&lt;/p&gt;
&lt;h3 id="可靠性"&gt;可靠性
&lt;/h3&gt;
 &lt;blockquote&gt;
 &lt;p&gt;当出现意外情况如硬件、软件故障、人为失误等，系统应可以继续正常运转：虽然性能可能有所降低，但确保功能正确。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;什么是一个可靠的应用？&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;应用程序执行用户所期望的功能。&lt;/li&gt;
&lt;li&gt;可以容忍用户出现错误或者不正确的软件使用方法。&lt;/li&gt;
&lt;li&gt;性能可以应对典型场景、合理负载压力和数据量。&lt;/li&gt;
&lt;li&gt;系统可防止任何未经授权的访问和滥用。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="硬件故障"&gt;硬件故障
&lt;/h4&gt;
 &lt;blockquote&gt;
 &lt;p&gt;硬盘崩溃，内存故障，电网停电，甚至有人误拔掉了网线。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;传统防止硬件故障的方式&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;磁盘配置 RAID，服务器配备双电源，甚至热插拔CPU， 数据中心添加备用电源、发电机等。&lt;/p&gt;
&lt;p&gt;当一个组件发生故障，元余组件可以快速接管，之后再更换失效的组件。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;现代硬件容错&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;随着数据量和应用计算需求的增加，现代应用运行在大规模机器上，随之而来的硬件故障率呈线性增长。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;依靠传动办法以难以应对故障发生的频率，故而需要使用软件来做容错。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;如当需要重启计算机时为操作系统打安全补丁，可以每次给一个节点打补丁然后重启，而不需要同时下线整个系统（滚动升级）&lt;/p&gt;
&lt;h4 id="软件错误"&gt;软件错误
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;常见的软件错误&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;一个应用进程使用了某些共享资源如CPU、内存、磁盘或网络带宽，但却不幸失控跑飞了。&lt;/li&gt;
&lt;li&gt;系统依赖于某些服务，但该服务突然变慢，甚至无响应或者开始返回异常的响应。&lt;/li&gt;
&lt;li&gt;级联故障，其中某个组件的小故障触发另一个组件故障，进而引发更多的系统问题。&lt;/li&gt;
&lt;/ol&gt;

 &lt;blockquote&gt;
 &lt;p&gt;软件bug通常会长时间处于引而不发的状态，知道碰到特定条件才会触发。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;软件错误有时没有快速的解决办法，只能仔细考虑更多的细节，认真检查依赖的假设条件与系统之间交互，进行全面的测试，进程隔离，允许进程崩溃并自动重启，反复评估，监控并分析生产环节的行为表现等。&lt;/p&gt;
&lt;h4 id="人为失误"&gt;人为失误
&lt;/h4&gt;
 &lt;blockquote&gt;
 &lt;p&gt;人无法做到万无一失。人是不可靠的，那么该如何保证系统的可靠性呢？&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;以最小出错的方式来设计系统&lt;/strong&gt;（接口最小依赖，单一职责）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;精心设计的抽象层，API以及管理界面，使“做正确的事情”很轻松，搞破坏很难。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;想办法分离容易出错的地方&lt;/strong&gt;（灰度环境）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;提供一个功能齐 全但非生产用的沙箱环境，使人们可以放心的尝试、体验，包括导人真实的数 据，万一出现问题，不会影响真实用户。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;充分测试&lt;/strong&gt;（自动化全面测试）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从各单元测试到全系统集成测试以及手动测试。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;当人为出现错误时，提供快速恢复机制尽量减少故障影响&lt;/strong&gt;（小范围发布和回滚）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;快速回滚配置改动，滚动发布新代码，并提供校验数据的工具。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;详细的监控&lt;/strong&gt;（链路追踪）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;包括性能指标和错误率。链路追踪等。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;推行管理流程并培训&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;重要且复杂。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="可扩展性"&gt;可扩展性
&lt;/h3&gt;
 &lt;blockquote&gt;
 &lt;p&gt;随着规模的增长，例如数据量、流量或复杂性，系统应以合理的方式来匹配这种增长。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;系统现在工作可靠，并不意味着它将来一定能够可靠运行。&lt;/p&gt;
&lt;h4 id="如何描述负载"&gt;如何描述负载
&lt;/h4&gt;&lt;p&gt;按“扇出”来描述&lt;/p&gt;
&lt;p&gt;多个明星发布消息（生产者）&lt;/p&gt;
&lt;p&gt;这些消息被推送给更多的用户（消费者）&lt;/p&gt;
&lt;p&gt;&lt;img alt="1774422363158.png" class="gallery-image" data-flex-basis="612px" data-flex-grow="255" height="459" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-03/1774422363158_1774422363215.png" srcset="https://thecoolboyhan.github.io/1774422363158_1774422363215_4957483893837895835_hu_76b804b0cd106bb5.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-03/1774422363158_1774422363215.png 1172w" width="1172"&gt;&lt;/p&gt;
&lt;p&gt;每个用户维护一个时间线缓存。当有人发布新tweet，查询其关注者，将tweet插入到每个关注者的时间线缓存中。（推送效率随着关注者数量线性增长）&lt;/p&gt;
&lt;p&gt;对于大V，让用户采用主动拉取的方式。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;所谓的负载，应当为可线性描述。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h4 id="描述性能"&gt;描述性能
&lt;/h4&gt;&lt;p&gt;批处理系统中，通常关心吞吐量，每秒可处理的记录条数，某个指定数据集运行作业需要的总时间。&lt;/p&gt;
&lt;p&gt;现在系统通常看中服务的响应时间，客户端从发送请求到接收响应之间的间隔。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;**产生延迟的原因：**上下文切换和进程调度、网络数据包丢失和TCP重传、垃圾回收暂停、缺页中断和磁盘I/O，甚至服务器机架的机械振动。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;中位数指标&lt;/strong&gt;：一半用户的请求延迟低于此指标，另一则大于，可以很好的反映总体延迟情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P999&lt;/strong&gt;：有99.9%的请求响应时间快于阈值。这将直接影响用户的总体服务体验。（亚马逊，响应时间每增加100ms，销售额就下降1%）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="应对负载增加的方法"&gt;应对负载增加的方法
&lt;/h4&gt;
 &lt;blockquote&gt;
 &lt;p&gt;当负载增加时，应该如何保证良好性能？&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="1774425974718.png" class="gallery-image" data-flex-basis="491px" data-flex-grow="204" height="561" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-03/1774425974718_1774425974831.png" srcset="https://thecoolboyhan.github.io/1774425974718_1774425974831_18075367654035235647_hu_f0ebc075d4bc79ab.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-03/1774425974718_1774425974831.png 1149w" width="1149"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;针对特定级别设计的架构，不太可能应付超出预设目标10倍的实际负载。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;因此，负载增加时，应在在垂直扩展（即升级到更强大的机器）和水平扩展（即将负载分布到多个更小的机器）之间做取舍。&lt;/p&gt;
&lt;p&gt;把无状态服务分布然后扩展至多台机器相对比较容易，而有状态服务从单个节点扩展到分布式多机环境的复杂性会大大增加。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于有状态服务，将数据库运行在一个节点上（采用垂直扩展策略）， 直到高扩展性或高可用性的 要求迫使不得不做水平扩展。&lt;/strong&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;最理想的扩展是具有弹性的，数据量增加自动扩容，数据量减少，自动缩容。&lt;strong&gt;（弹性）&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="可维护性"&gt;可维护性
&lt;/h3&gt;
 &lt;blockquote&gt;
 &lt;p&gt;随着时间的推移，许多新的人员参与到系统开发和运维， 以维护现有功能或适配新场景等，系统都应高效运转。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;监视系统的健康状况，井在服务出现异常状态时快速恢复服务。&lt;/li&gt;
&lt;li&gt;追踪问题的原因，例如系统故障或性能下降。&lt;/li&gt;
&lt;li&gt;保持软件和平台至最新状态， 例如安全补丁方面。&lt;/li&gt;
&lt;li&gt;了解不同系统如何相互影响，避免执行带有破坏性的操作。&lt;/li&gt;
&lt;li&gt;预测未来可能的问题，并在问题发生之前即使解决（例如容量规划）。&lt;/li&gt;
&lt;li&gt;建立用于部署、配置管理等良好的实践规范和工具包。&lt;/li&gt;
&lt;li&gt;执行复杂的维护任务， 例如将应用程序从一个平台迁移到另一个平台。&lt;/li&gt;
&lt;li&gt;当配置更改时，维护系统的安全稳健。&lt;/li&gt;
&lt;li&gt;制定流程来规范操作行为，并保持生产环境稳定。&lt;/li&gt;
&lt;li&gt;保持相关知识的传承（如对系统理解），例如发生团队人员离职或者新员工加入 等。&lt;/li&gt;
&lt;li&gt;提供对系统运行时行为和内部的可观测性，方便监控。&lt;/li&gt;
&lt;li&gt;支持自动化， 与标准工具集成。&lt;/li&gt;
&lt;li&gt;避免绑定特定的机器，这样在整个系统不间断运行的同时，允许机器停机维护。&lt;/li&gt;
&lt;li&gt;提供良好的文档和易于理解的操作模式，诸如“如果我做了X，会发生Y”。&lt;/li&gt;
&lt;li&gt;提供良好的默认配置，且允许管理员在需要时方便地修改默认值。&lt;/li&gt;
&lt;li&gt;尝试自我修复，在需要时让管理员手动控制系统状态。&lt;/li&gt;
&lt;li&gt;行为可预测，减少意外发生。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="第二章数据模型与查询语句"&gt;第二章：数据模型与查询语句
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;语言的边界就是世界的边界。&lt;/p&gt;
&lt;p&gt;一一Ludwig Wittgenstein, 《逻辑哲学论》 ( 1922)&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="1774426451219.png" class="gallery-image" data-flex-basis="403px" data-flex-grow="168" height="744" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-03/1774426451219_1774426451236.png" srcset="https://thecoolboyhan.github.io/1774426451219_1774426451236_8833101173672415190_hu_4ff663a5eae4d3c2.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-03/1774426451219_1774426451236.png 1251w" width="1251"&gt;&lt;/p&gt;
&lt;h3 id="关系模型"&gt;关系模型
&lt;/h3&gt;&lt;p&gt;SQL是最著名的关系数据模型，数据被组织成关系（表），每个关系都是元组的无序集合（行）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关系模型的目标是将实现细节隐藏在更简洁的接口后面。&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id="对象和关系的不匹配"&gt;对象和关系的不匹配
&lt;/h4&gt;&lt;p&gt;如果数据存储在关系表中， 那么应用层代码中的对象与表、行和列的数据库模型之间需要一个笨拙的转换层。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何在关系模型中表示简历&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="260403143010968.png" class="gallery-image" data-flex-basis="281px" data-flex-grow="117" height="813" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/260403143010968_1775197810996.png" srcset="https://thecoolboyhan.github.io/260403143010968_1775197810996_7447049051236564992_hu_7c78d48ecff89d06.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/260403143010968_1775197810996.png 954w" width="954"&gt;&lt;/p&gt;
&lt;p&gt;一个简历信息，通过userid在多个表之间关联。如果想要获取一个完整的简历，需要查询多个不同的表。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1775197951927.png" class="gallery-image" data-flex-basis="505px" data-flex-grow="210" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775197951927_1775197951949.png" srcset="https://thecoolboyhan.github.io/1775197951927_1775197951949_11493861662478232287_hu_616ade466c30d8a.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775197951927_1775197951949.png 1011w" width="1011"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：可以读取表中的任何一行或者多行，支持任意条件查询。可以使用列作为条件，匹配这些列来读取特定行。可以在任何表中插入新行，而不用关心与其他表之间的关系问题。（业务逻辑可能需要关系，但数据本身不需要关心）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="文档模型"&gt;文档模型
&lt;/h3&gt;&lt;p&gt;由一些特定的键来确定文档，此文档中存在多个不同的属性，每个属性又指向一个文档数据（此数据可能多个文档共享）。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1775198449896.png" class="gallery-image" data-flex-basis="348px" data-flex-grow="145" height="701" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775198449896_1775198449914.png" srcset="https://thecoolboyhan.github.io/1775198449896_1775198449914_17748090202711411815_hu_557f77ce97b77fa1.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775198449896_1775198449914.png 1017w" width="1017"&gt;&lt;/p&gt;
&lt;p&gt;用户可以通过键来高效的获取此文档。（一次获取文档内容）&lt;/p&gt;
&lt;h4 id="和关系模型对比"&gt;和关系模型对比
&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文档模型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文档模型主要是模式灵活，由于数据局部，所以可以带来更好的性能，对于应用程序来说，更接近应用程序所使用的数据结构。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关系模型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;强在联结操作，多对一和多对多关系更简洁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文档模型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文档模式修改如果变更了文档的大小，可能需要重写整个文档。且如果文档内容过大，即使只需要文档中的一小部分内容，也必须加载整个文档。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关系模型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果数据被划分在多个表中，查询则需要多次磁盘IO，花费大量的时间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;融合&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;大多数关系数据库系统 （MySQL除外）都支持XML。其中包括 对XML文档进行本地修改，在XML文档中进行索引和查询等，这样应用程序可以获得与文档数据库非常相似的数据模型。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;随着时间的推移，关系数据库和文档数据库变得越来越相近。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="查询语句"&gt;查询语句
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;命令式查询语言（编程语言常见）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="1775204560606.png" class="gallery-image" data-flex-basis="540px" data-flex-grow="225" height="249" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775204560606_1775204560650.png" width="561"&gt;&lt;/p&gt;
&lt;p&gt;告诉计算机以特定的顺序执行某些操作。你完全可以推理整个过程，逐行遍历代码、评估相关条件、更新对应的变量，并决定是否再循环一遍。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;声明式查询语言（关系模型常用）&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;SE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;animals&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;family&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;&lt;span class="n"&gt;Sharks&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;只需指定所需的数据模式，结果需要满足什么条件，以及如何转换数据，而不需要说明如何实现这一目标。数据库系统的查询优化器会决定采用哪些索引和联结，以及用何种顺序来执行查询的各个语句。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;能够在不改变查询语句的情况下提高性能。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;场景&lt;/th&gt;
					&lt;th&gt;命令式&lt;/th&gt;
					&lt;th&gt;声明式&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;磁盘空间回收（可能需要移动数据&lt;br /&gt;改变数据的顺序）&lt;/td&gt;
					&lt;td&gt;会受到影响，有些查询命令可能会依赖于顺序&lt;/td&gt;
					&lt;td&gt;sql只在乎结果是否符合条件，不关心数据的存储顺序。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;并行执行（现代CPU往往通过增加核心数量来提交CPU的性能，单核性能的提升微乎其微）&lt;/td&gt;
					&lt;td&gt;只能单线程运行，一个查询只能按找顺序执行&lt;/td&gt;
					&lt;td&gt;可以并行执行，多个核心多台机器一起优化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;web服务或API（这两种区别不只体现在数据库上，在服务的接口上也一样）&lt;/td&gt;
					&lt;td&gt;把命令通过API传入，非常依赖被调用服务的内部资源情况。如果资源发生变动，则会是灾难性的。&lt;/td&gt;
					&lt;td&gt;类似rest风格，只声明自己需要的资源，至于此资源是否存在，如何提供外部服务不关心。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;MapReduce查询&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MapReduc e是一种编程模型，用于在许多机器上批量处理海量数据，兴起于 Google。大部分NoSQL（Not only sql）数据库都支持有限的MapReduce方式的执行只读查询。&lt;/p&gt;
&lt;p&gt;**MapReduce既不是声明式查询语句也不是命令式查询语句。而是介于二者之间的。**主要包括map和reduce两个函数组成。&lt;/p&gt;
&lt;p&gt;假设你是一名海洋生物学家，每当你看到海洋中的动物时， 就会在数据库 中添加观察记录。现在你想生成一份报告，来说明每个月看到了多少鲨鱼。&lt;/p&gt;
&lt;p&gt;在PostgreSQL中的查询：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;observation_timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;observation_month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_animals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;total_animals&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;family&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Sharks&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;GROUP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;observation_month&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
 &lt;blockquote&gt;
 &lt;p&gt;先对物种过滤，只查询鲨鱼，然后根据月份分组，汇总每个月的动物数量。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;MongoDB中的MapReduce功能也可以实现：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mapReduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;observationTimestamp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observationTimestamp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;emit&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;numAnimals&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Sharks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;monthlySharkReport&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
 &lt;blockquote&gt;
 &lt;ol&gt;
&lt;li&gt;通过query方法指定种类为鲨鱼&lt;/li&gt;
&lt;li&gt;对于所有需要查询的文档，都会调用一次map函数&lt;/li&gt;
&lt;li&gt;map函数返回一个KV对，其中key是年份和月份字符串，value为动物的数量&lt;/li&gt;
&lt;li&gt;map方法的kv对被reduce函数接收，并汇总。&lt;/li&gt;
&lt;li&gt;最后通过out方法输出到monthlySharkReport。&lt;/li&gt;
&lt;/ol&gt;

 &lt;/blockquote&gt;
&lt;p&gt;MapReduce用于在计算集群上分布执行。必须编写两个密切协调的函数，一个对于多个数据过滤需要的条件，另一个对于过滤出的条件进行汇总。&lt;/p&gt;
&lt;h2 id="第三章-数据存储和检索"&gt;第三章： 数据存储和检索
&lt;/h2&gt;&lt;p&gt;&lt;img alt="1775206545546.png" class="gallery-image" data-flex-basis="365px" data-flex-grow="152" height="762" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775206545546_1775206545568.png" srcset="https://thecoolboyhan.github.io/1775206545546_1775206545568_879533471364837189_hu_1eb8d9ccbb8a960f.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775206545546_1775206545568.png 1161w" width="1161"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;如果你把东西整理得井井有条， 下次就不用查找了。&lt;/p&gt;
&lt;p&gt;​	&amp;mdash;&amp;ndash;德国谚语&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="数据库的核心数据结构"&gt;数据库的核心：数据结构
&lt;/h3&gt;&lt;h4 id="日志"&gt;日志
&lt;/h4&gt;&lt;p&gt;如果所有的数据都以KV的形式简单存储在一个文件中。&lt;/p&gt;
&lt;p&gt;每次新增数据：在文件末尾追加新的KV，如果多次更新某个键，旧的值不会被覆盖，文件最后一次出现的KV表示最新的值。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;只追加到末尾形式，写性能很好，但读取非常困难。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;写入开销几乎为O(1)，查询开销为O(n)&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id="哈希索引"&gt;哈希索引
&lt;/h4&gt;&lt;p&gt;数据以KV和追加的形式存储在磁盘中，在内存中建立key对应偏移量的hashmap。每次查询通过内存中的hashmap获取偏移量，通过偏移量高效的读取磁盘上的数据。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1775207524357.png" class="gallery-image" data-flex-basis="469px" data-flex-grow="195" height="462" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775207524357_1775207524380.png" srcset="https://thecoolboyhan.github.io/1775207524357_1775207524380_3583852913801684610_hu_8d5dcff9c0aacfd5.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775207524357_1775207524380.png 903w" width="903"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Bitcask（Riak中的默认存储引擎）所采用的做法。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;可以提供高效读写，只要所有的key放到内存中，而vlaue都保存在磁盘上。只需要一次磁盘IO就可以把Value加载到内存中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;此种结构非常适合值频繁更新的场景，适合key不是特别多，但更新特别频繁的情况。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;磁盘数据的压缩&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于数据依然采用文件追加的形式保存，所以当文件达到一定大小，就新建一个文件来追加。此时老文件可以进行压缩操作。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1775207838078.png" class="gallery-image" data-flex-basis="434px" data-flex-grow="180" height="501" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775207838078_1775207838092.png" srcset="https://thecoolboyhan.github.io/1775207838078_1775207838092_7435530069292469524_hu_ece8d9f9b15b2011.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775207838078_1775207838092.png 906w" width="906"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哈希索引的局限性&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;hash表必须全部存在内存中（如果放到磁盘里，修改需要大量的随机IO，效果很差）&lt;/li&gt;
&lt;li&gt;区间查询效率不高，对于范围查询只能扫描所有键，来确定当前键是否数据对应范围&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="sstable和lsm-tree"&gt;SSTable和LSM-Tree
&lt;/h4&gt;&lt;p&gt;在hash索引的基础上，新增要求，对KV要求按照顺序排序。&lt;/p&gt;
&lt;p&gt;排序字符串表（SSTable）&lt;/p&gt;
&lt;p&gt;&lt;img alt="1775209890666.png" class="gallery-image" data-flex-basis="481px" data-flex-grow="200" height="474" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775209890666_1775209890683.png" srcset="https://thecoolboyhan.github.io/1775209890666_1775209890683_6752332949310451502_hu_568f1349f8f4d1bf.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775209890666_1775209890683.png 950w" width="950"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;压缩过程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="1775210016972.png" class="gallery-image" data-flex-basis="379px" data-flex-grow="158" height="573" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775210016972_1775210016997.png" srcset="https://thecoolboyhan.github.io/1775210016972_1775210016997_5213575833468428684_hu_2e6e44814d48f720.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775210016972_1775210016997.png 906w" width="906"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何写入数据&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;存储虽然是有序，但写入并不会按照排序的顺序写入。那该如何在乱序写入时保证顺序追加？&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;在内存中维护一个AVL结构树（红黑树）&lt;/li&gt;
&lt;li&gt;当内存大于某个阈值时，就把树写入到新的磁盘段中。&lt;/li&gt;
&lt;li&gt;读取数据：先在内存表中查找键，然后去最新的磁盘段中查找，接下来是次新段。&lt;/li&gt;
&lt;li&gt;后台线程周期性合并磁盘的段。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;从SSTable到LSM-Tree&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;LSM-Tree&lt;/strong&gt;(Log-Structured Merge-Tree)：以日志结构的合并树&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果查找不存在的值&lt;/strong&gt;：需要先查询内存表，在根据内存表查找每个磁盘段，直到最后一个段。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;可以添加布隆过滤器来过滤掉不存在的值，但会占用额外空间&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;LSM-Tree的思想&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据集远远大于可用内存，它仍然能正常工作。&lt;/li&gt;
&lt;li&gt;由于数据按排序存储，可以有效的进行范围查询。&lt;/li&gt;
&lt;li&gt;由于磁盘是顺序写入，所以LSM-Tree可以支持非常高的写入吞吐量。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="b-trees"&gt;B-Trees
&lt;/h4&gt;
 &lt;blockquote&gt;
 &lt;p&gt;最常见，也几乎是最标准的数据存储结构。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;有序：按键值来排序KV对（可以范围查找）&lt;/p&gt;
&lt;p&gt;把数据库分成固定大小的块和页，传统大小为4kb，页是内部读写的最小单位。这样更接近硬件，因为磁盘也是固定大小的块排序。&lt;/p&gt;
&lt;p&gt;每个页都使用地址来进行标识，这样可以让一个页引用另一个页。（形成链表），每个指针都指向磁盘地址。&lt;/p&gt;
&lt;p&gt;某个页被指定为B-tree的根，所有查询都需要从根页开始。根页包含若干个键和对子页的引用，每个子页都负责一个连续范围内的键，相邻的键指示这些范围之间的边界。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1775547117659.png" class="gallery-image" data-flex-basis="414px" data-flex-grow="172" height="462" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775547117659_1775547117679.png" width="798"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="1775547117659.png" class="gallery-image" data-flex-basis="414px" data-flex-grow="172" height="462" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775547117659_1775547117679.png" width="798"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;页的分裂&lt;/strong&gt;：如果修改或新增值在页的范围内，修改对应页并刷盘。如果添加的新键，页中没有足够的空间容纳新键，则需要将此页分裂成两个半满的页，并且父页也需要包含分裂后新键的范围。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1775547312385.png" class="gallery-image" data-flex-basis="407px" data-flex-grow="169" height="465" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775547312385_1775547312411.png" width="789"&gt;&lt;/p&gt;
&lt;p&gt;通过分裂，可以确保树始终保持平衡，且查询效率为Logn，分支因子为500的4k页，四级树可以存储256TB的数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何使B-Tree可靠&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可能存在的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;B-Tree的写操作，需要覆盖磁盘上的旧页。（LSM-tree只在文件末尾追加写入，不会修改文件）&lt;/p&gt;
&lt;p&gt;修改操作在机械磁盘上的操作：磁头移动到正确位置，旋转盘面，用新的数据覆盖相应的扇区。&lt;/p&gt;
&lt;p&gt;SSD：必须一次擦除并重写非常大的存储片块。&lt;/p&gt;
&lt;p&gt;在分裂的情况下：需要写两个分裂页，并修改父页对两个子页的引用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果部分页写入后发生了崩溃，会导致索引被破坏，出现孤儿页。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;通过预写日志来解决（WAL：write-ahead log，重做日志）&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;mysql中对应redo log&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过一个仅支持追加修改的日志文件，每次B-tree修改操作，都需要先修改WAL，然后再修改树本身的页。当数据库崩溃后，通过该日志恢复到最近的一致状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;通过日志可以解决原子性问题，在并发状态下与锁配合，可以做到原子更新。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据连续的不同页在磁盘上可能不是连续的，可以通过分区尽量把连续的数据放到一起。但如果数据量特别大，此成本将远远高于LSM-Tree&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="lsm-tree-vs-b-tree"&gt;LSM-Tree vs B-tree
&lt;/h4&gt;
 &lt;blockquote&gt;
 &lt;p&gt;LSM-Tree写入更快，B-tree 查询效率高&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;写放大&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;指完成一次写入操作，需要向硬盘中进行多少次写操作。&lt;/p&gt;
&lt;p&gt;现代固态硬盘可以把磁盘的随机写入变成顺序写入，但更小的写放大依然可以带来更高的性能。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;场景&lt;/th&gt;
					&lt;th&gt;B-tree&lt;/th&gt;
					&lt;th&gt;LSM-Tree&lt;/th&gt;
					&lt;th&gt;结论&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;写入数据&lt;/td&gt;
					&lt;td&gt;需要至少两次磁盘写入：写入预写日志，写入树上的页（还可能发生分裂）。&lt;br /&gt;即使只写一个字段，也需要更新整个页&lt;/td&gt;
					&lt;td&gt;只需要写入紧凑的SSTable文件（且是连续的顺序写入）&lt;/td&gt;
					&lt;td&gt;LSM-Tree的顺序写入成本要远远低于b-tree的多次随机写入&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据占用空间&lt;/td&gt;
					&lt;td&gt;数据由若干个未满的页组成，成碎片分布&lt;/td&gt;
					&lt;td&gt;有多个段组成SSTables，数据密集且连续。而且会定期通过压缩来回收碎片&lt;/td&gt;
					&lt;td&gt;LSM-Tree数据占用的空间要远远小于B-tree&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;稳定性&lt;/td&gt;
					&lt;td&gt;数据写入即确定，一般不会发生变动&lt;/td&gt;
					&lt;td&gt;定期会进行压缩，压缩时可能会给读写请求带来一定的影响。（极端情况：写入量过大，导致压缩一直结束不了）&lt;/td&gt;
					&lt;td&gt;B-tree更加稳定，LSM在压缩时会很小的影响性能&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;内存数据库和磁盘数据库对比&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;内存数据库的优点并不是不需要读取磁盘。如果内存足够大，完全可以把磁盘结构的数据存储到内存中。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;内存数据库的优点在于不需要使用磁盘的格式对内存数据结构编码的开销。（&lt;strong&gt;内存数据库中可以直接按照应用使用的内存结构来存储数据，而无需转换）&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="事务处理与分析处理"&gt;事务处理与分析处理
&lt;/h3&gt;
 &lt;blockquote&gt;
 &lt;p&gt;事务主要指组成一个逻辑单元的一组读写操作。（不一定具有ACID属性）&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h4 id="oltp事务处理系统与olap分析系统"&gt;OLTP（事务处理系统）与OLAP（分析系统）
&lt;/h4&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;属性&lt;/th&gt;
					&lt;th&gt;事务处理系统（OLTP）&lt;/th&gt;
					&lt;th&gt;分析系统（OLAP）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;主要读特征&lt;/td&gt;
					&lt;td&gt;基于键每次查询返回少量的记录&lt;/td&gt;
					&lt;td&gt;对于大量记录进行汇总&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;主要写特征&lt;/td&gt;
					&lt;td&gt;随机访问，低延迟写入用户的输入&lt;/td&gt;
					&lt;td&gt;批量导入（ETL ）或事件流&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;使用场景&lt;/td&gt;
					&lt;td&gt;终端用户，通过网络应用程序&lt;/td&gt;
					&lt;td&gt;内部分析师，为决策提供支持&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据表征&lt;/td&gt;
					&lt;td&gt;最新的数据状态（当前时间点）&lt;/td&gt;
					&lt;td&gt;随着时间而变化的所有事件历史&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据规模&lt;/td&gt;
					&lt;td&gt;GB到TB&lt;/td&gt;
					&lt;td&gt;TB到PB&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;

 &lt;blockquote&gt;
 &lt;p&gt;mysql是典型的OLTP与OLAP同时处理的数据库。但随着发展，公司放弃使用OLTP 系统用于分析目的，而在单独的数据库上运行分析。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;数据仓库&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;数据仓库包含公司所有各种OLT 系统的只读副本。从OLTP数据库（使用周期性数据转储或连续更新流） 中提取数据，，转换为分析友好的模式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ETL&lt;/p&gt;
&lt;p&gt;将数据导入数据仓库的过程称为提取－转换－加载。（Extract-Transform-Load）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="1775560871475.png" class="gallery-image" data-flex-basis="346px" data-flex-grow="144" height="579" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775560871475_1775560901342.png" srcset="https://thecoolboyhan.github.io/1775560871475_1775560901342_7726462663728304353_hu_94bb30e28bdda69.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775560871475_1775560901342.png 837w" width="837"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;星型与雪花型分析模式&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;星型表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="1775617664420.png" class="gallery-image" data-flex-basis="266px" data-flex-grow="111" height="753" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775617664420_1775617664439.png" srcset="https://thecoolboyhan.github.io/1775617664420_1775617664439_3149468581500241348_hu_18e6d43cadf92bd4.png 800w, https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775617664420_1775617664439.png 837w" width="837"&gt;&lt;/p&gt;
&lt;p&gt;一个事实表关联多个维度的配置信息表&lt;/p&gt;
&lt;h3 id="列式存储"&gt;列式存储
&lt;/h3&gt;
 &lt;blockquote&gt;
 &lt;p&gt;大部分OLTP数据都是按行来存储数据，如果一行数据有上百个列（甚至更多），则按列式存储是一个更好的方式。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;列式存储的思想&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要将一行数据中的所有值存储在一起，，而是将每列中的所有值存储在一起。如果每个列存储在 个单独的文件中，查询只需要读取和解析在该查询中使用的那些列，这可以节省大量的工作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列压缩&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="1775618238270.png" class="gallery-image" data-flex-basis="316px" data-flex-grow="131" height="585" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775618238270_1775618238286.png" width="771"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;列式存储出现了大量的重复数据，对于这些大量重复的列可以做压缩。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="1775619196929.png" class="gallery-image" data-flex-basis="321px" data-flex-grow="134" height="582" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://fastly.jsdelivr.net/gh/thecoolboyhan/th_blogs@main/image/2026-04/1775619196929_1775619196945.png" width="780"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;压缩思路&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用一个超长的二进制字符串，每一位代表每一行，0/1表示是否存在，每个枚举值对应当前值的位图。这样可以把大量重复的值都存储在同一个位图里。如果多个值查询只需把多个位图的值做或运算。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;对与二进制位图本身，也可以使用游程编码的方式来进一步压缩。（第一个数字表示前置0，后一个表示1，然后交替）&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;列式存储如何排序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;规定排序的列应尽可能的让具有相同值的列排在一起，这样可以更好的压缩数据。&lt;/p&gt;
&lt;p&gt;如果业务必须要按照不同的排序条件来查询数据，可以考虑按不同的规则复制几份不同的列存储。&lt;/p&gt;
&lt;p&gt;现代生产中数据本身就会被备份多份，不如利用起来，按照不同的排序规则备份相同数据，然后根据业务决定查询哪一份。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列式存储的写操作&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;对于排序后的列式存储，如果按照常规原地更新的方式插入数据，那将重写所有的列，这是灾难性的。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;vertica的做法&lt;/strong&gt;：参考LSM-tree的解决方案，先把数据存储在内存中，当达到一定阈值，与磁盘中的列文件合并，生成新的列文件。&lt;/p&gt;
&lt;p&gt;查询时，先查询内存中是否存在，再去磁盘中搜索列。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;现代使用列式存储的数据库（扩展）&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ClickHouse:&lt;/strong&gt; 一个开源的分布式列式数据库，以其惊人的查询速度闻名。它非常适合处理海量数据，常用于实时分析、用户行为分析和日志处理等场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apache Druid:&lt;/strong&gt; 一个为实时分析设计的开源数据库。它在处理流数据、快速聚合和亚秒级查询方面表现出色，常用于实时数据仪表盘和事件驱动的分析应用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vertica:&lt;/strong&gt; 一个高性能的企业级列式数据库，专为大规模数据仓库和商业智能（BI）场景设计，支持复杂的分析查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apache HBase:&lt;/strong&gt; 一个基于 Hadoop HDFS 的分布式、面向列的数据库。它提供对海量数据的随机、实时读写访问，常用于日志分析、实时推荐等场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apache Cassandra:&lt;/strong&gt; 一个分布式、高可用的 NoSQL 数据库。它的数据模型是“宽列存储”，虽然也常被归为列式家族，但其设计更侧重于高写入吞吐量和跨数据中心的容错性。&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;ClickHouse (CK)&lt;/th&gt;
					&lt;th&gt;MySQL&lt;/th&gt;
					&lt;th&gt;Oracle&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;核心定位&lt;/td&gt;
					&lt;td&gt;极致性能的分析型数据库 (OLAP)&lt;/td&gt;
					&lt;td&gt;通用的事务型数据库 (OLTP)&lt;/td&gt;
					&lt;td&gt;企业级核心事务数据库 (OLTP)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;存储方式&lt;/td&gt;
					&lt;td&gt;列式存储 (读取仅涉及相关列，压缩率极高)&lt;/td&gt;
					&lt;td&gt;行式存储 (适合读写整行数据)&lt;/td&gt;
					&lt;td&gt;行式存储 (侧重数据一致性与锁机制)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据量级&lt;/td&gt;
					&lt;td&gt;PB 级 / 百亿行 单机可处理数十亿行，集群轻松支撑 PB 级&lt;/td&gt;
					&lt;td&gt;TB 级 / 千万~亿行 单表超过 2000 万性能下降，20 亿数据需极度复杂的分库分表&lt;/td&gt;
					&lt;td&gt;PB 级 / 海量 单机处理能力极强，适合超大规模核心业务&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;查询性能&lt;/td&gt;
					&lt;td&gt;聚合/分析查询极快 比 MySQL 快 10-100 倍甚至更多。适合全表扫描、Group By。&lt;/td&gt;
					&lt;td&gt;点查询/事务快 基于主键的查询极快。复杂分析查询（如大表 Join）在数据量大时极慢。&lt;/td&gt;
					&lt;td&gt;复杂计算稳健 优化器极强，擅长处理复杂的 SQL 逻辑和事务。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;写入性能&lt;/td&gt;
					&lt;td&gt;高吞吐批量写入 适合一次性写入大量数据，不支持高频单条插入。&lt;/td&gt;
					&lt;td&gt;高频实时写入 支持高并发的单行 Insert/Update。&lt;/td&gt;
					&lt;td&gt;稳定事务写入 保证 ACID 特性下的稳定写入，成本较高。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Join 能力&lt;/td&gt;
					&lt;td&gt;较弱 大表关联容易内存溢出，建议使用宽表模型。&lt;/td&gt;
					&lt;td&gt;中等 适合中小规模数据的关联查询。&lt;/td&gt;
					&lt;td&gt;极强 擅长处理极其复杂的多表关联和子查询。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;适用场景&lt;/td&gt;
					&lt;td&gt;日志分析、用户行为、报表 实时大屏、数据仓库、监控指标。&lt;/td&gt;
					&lt;td&gt;互联网业务、Web 应用 电商订单、用户系统、CMS、小程序。&lt;/td&gt;
					&lt;td&gt;金融核心、大型 ERP 银行交易、核心账务、复杂企业系统。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;主要短板&lt;/td&gt;
					&lt;td&gt;不支持事务 (ACID)、不擅长高频更新/删除。&lt;/td&gt;
					&lt;td&gt;复杂分析性能差、大数据量下扩展困难。&lt;/td&gt;
					&lt;td&gt;昂贵、运维复杂、资源消耗大。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;</description></item></channel></rss>