面试漫谈:缓存的三大场景与分布式锁

缓存三大问题 缓存穿透 缓存穿透指的是客户端请求查询一个在缓存中和数据库中都“绝对不存在”的数据。 由于缓存中没有(缓存未命中),请求会“穿透”缓存层,直接打到数据库层。如果这种请求量很大(例如,恶意的攻击者使用大量不存在的ID进行查询),数据库将承受巨大的压力,可能导致服务宕机。 做法 (Solutions) 缓存空值 (Cache Null Values) 做法: 当数据库查询返回为空(null)时,我们依然将这个“空结果”缓存起来,但为其设置一个较短的过期时间(TTL),例如60秒。 优点: 实现简单,立竿见影。 缺点: 会占用缓存空间(尽管很短)。 存在短暂的数据不一致性:如果在这60秒内,数据库中恰好创建了这条数据,缓存返回的依然是“空”,直到缓存过期。 布隆过滤器 (Bloom Filter) 做法: 布隆过滤器是一种高效的、概率性的数据结构,它可以用极小的空间判断一个元素“一定不存在”或“可能存在”。 流程: 启动时,将数据库中所有合法的Key加载到布隆过滤器中。 当请求到来时,先去布隆过滤器查询Key是否存在。 如果布隆过滤器判断“一定不存在”,则立即返回空,绝对不会去查缓存和数据库。 如果布隆过滤器判断“可能存在”,则继续执行后续的查询缓存、查询DB的流程。 优点: 空间效率和查询效率极高,完美解决了穿透问题。 缺点: 实现相对复杂。 存在“误判率”(False Positive):它判断“可能存在”时,数据实际上可能不存在(但它绝不会把“存在”的判为“不存在”)。 数据新增时,需要同步更新布隆过滤器(删除操作不支持或很麻烦)。 面试要点 Q: 什么是缓存穿透?它和击穿有什么区别? A: 穿透是查不存在的数据,导致请求绕过缓存直达DB。击穿是查存在的数据,但这个热点数据刚过期,导致请求直达DB。 Q: 两种解决方案(缓存空值 vs 布隆过滤器)如何选择? A: 缓存空值简单,适用于恶意攻击不频繁、允许短暂不一致的场景。布隆过滤器更可靠,适用于数据“黑名单”明确、查询QPS极高、对恶意攻击防护要求高的场景(例如,防止用户ID的恶意爬取)。 缓存击穿 缓存击穿指的是某一个访问量极高的热点Key (Hotspot Key),在它失效的那一瞬间,海量的并发请求同时涌入,导致这些请求全部“击穿”缓存,直接访问数据库,造成数据库瞬时压力剧增,甚至宕机。 做法 (Solutions) 互斥锁 / 分布式锁 (Mutex Lock / Distributed Lock) 做法: 这是最经典的解决方案。当缓存未命中时,不是所有线程都去查数据库。 线程A尝试获取该Key的分布式锁(例如 lock:product:123)。 线程A获取锁成功,开始查询数据库、重建缓存。 线程B、C、D… 发现缓存未命中,尝试获取锁,但失败。 线程B、C、D… 不去查数据库,而是进入“等待”状态(例如,自旋或休眠后重试)。 线程A重建缓存后,释放锁。 线程B、C、D… 再次(或被唤醒)查询时,直接命中缓存。 优点: 强一致性,只允许一个请求“穿透”到DB,有效保护数据库。 缺点: 实现复杂,需要引入分布式锁(如Redis或Zookeeper)。 性能下降,大量线程在锁上等待,导致系统吞吐量降低。 逻辑过期 (Logical Expiration)...

November 14, 2025 · 501 words · Kurong

信也实习内容整理

第一章 全面的风险业务流程分析 为了深入理解海外风险中台的技术架构,首先必须对其服务的核心业务领域建立清晰的认知。本章将从业务视角出发,解构平台所支撑的完整风险管理生命周期,并以三个典型的业务场景——同步决策的“展期”流程、异步决策的“授信”(“戳额”)流程以及人机结合的“人工审核”流程——作为案例,详细剖析业务逻辑与系统执行之间的精密互动。 核心业务概念与范畴 在金融科技领域,风险是指在未来时间范围内,可能发生某种损失的不确定性。对于公司的海外借贷业务而言,风险特指借款人到期无力或无意偿还债务,从而造成债权人本金与收益蒙受部分或全部损失的可能性。风险控制(风控)的目标并非消除风险,而是通过一系列技术和策略手段,将潜在的风险转化为实际损失的可能性降至最低。 该海外风险中台正是为实现这一目标而构建的核心系统,其服务范围覆盖了菲律宾、巴基斯坦、墨西哥、印尼等多个国家站点,支撑着包括现金贷(Cashloan)、电商消费分期(Lazada FC, TikTok, PPL)、针对海外务工人员的专项贷款(OFW-CL)在内的多种业务线。平台处理的业务事件贯穿用户生命周期的各个关键节点,包括但不限于用户注册、授信申请、发标、提现、还款行为分析以及贷款展期等。 案例研究一:同步决策 — 贷款“展期”流程 同步决策流程适用于那些要求系统在短时间内给出明确答复、用户正在实时等待结果的业务场景。贷款展期便是一个绝佳的例子,它代表了一种低延迟、高确定性的交互模式。 根据系统时序图的分析,一个典型的展期风控流程调用链路如下: 请求发起:上游业务系统(client)因用户申请展期,向风险中台的展期服务(PhiLoanExtension)发起一个同步API调用。 流程实例化:PhiLoanExtension服务接收到请求后,立即组装并实例化一个名为RiskFlow的核心流程对象。这个对象将作为本次展期风控任务的上下文,贯穿整个执行过程。 数据扩充(变量计算):为了做出决策,系统需要获取与该用户和该笔贷款相关的风险特征。PhiLoanExtension服务会调用内部的“字段变量中心”服务(Irs)的/calVars接口,请求计算展期场景所需的特定字段变量值。Irs服务完成计算后,将结果同步返回。 规则引擎预处理:获得所有必需的变量后,PhiLoanExtension服务根据预设的模板,将这些变量和值组装成规则引擎(drools)能够理解的数据格式。 决策执行与审计:在调用规则引擎之前,系统会将格式化后的请求数据写入日志表risk_flow_log,以备审计和问题追溯。随后,PhiLoanExtension向drools服务发起决策请求。 结果处理与返回:drools引擎执行相应的规则集,并返回决策结果(如“批准展期”、“拒绝展期”)。PhiLoanExtension服务在接收到响应后,再次将其写入risk_flow_log,完成审计闭环。最后,将最终决策结果通过API响应同步返回给最初的调用方client。 案例研究二:异步决策 — “授信”(“戳额”)流程 与展期不同,新用户的授信审批是一个计算密集型且更为复杂的过程。它可能需要调用多个第三方数据源、运行多个复杂的机器学习模型,并可能涉及人工审核环节。这类操作耗时较长,无法在一次同步API调用中完成。因此,系统采用异步处理模式,以优化用户体验和系统吞吐量。 一个典型的异步授信流程如下: 请求接收:用户通过前端应用提交授信申请。上游业务系统(client)将请求发送至风险中台的异步流程入口服务(phi-quechao)。 任务入队:phi-quechao服务在接收到请求后,进行初步的校验和处理,然后将该授信任务封装成一个消息,投递到消息队列(如RabbitMQ)中。投递成功后,它会立即向上游返回一个“处理中”的响应,此时用户界面可以显示“审核中,请耐心等待”,从而将前端交互与后端复杂的处理流程解耦。 任务调度与执行:后台的异步任务批处理服务(phi-job)作为消息队列的消费者,会拉取授信任务。phi-job将任务分发给异步风控业务的调度中心(phi-risk)。 核心风控流程编排:phi-risk服务是整个异步流程的大脑,它依据预定义的Pipeline(管道),按顺序调度执行一系列子任务,这通常包括: 调用Irs服务计算上百个甚至上千个风险变量。 调用模型编排服务(phi-modelschedual),执行多个评分模型。 调用drools引擎,根据变量和模型分执行复杂的策略规则。 决策生成与通知:所有计算和规则执行完毕后,决策服务(phi-riskdecision)汇总结果,生成最终的授信决策(如授信额度、费率、评级或拒绝)。这个结果会被持久化到数据库,并通过另一个消息队列主题,将“决策完成”的事件通知给下游系统,例如更新用户状态、发送通知等。 案例研究三:人机结合 — “人工审核”流程 对于机器决策无法明确判断的“灰色”用户,系统会将其转入人工审核流程,结合人类专家的经验进行最终裁定。这是一个典型的“人机结合”场景,对系统的任务调度、状态管理和外部通信提出了更高的要求。 以巴基斯坦站的人审流程为例,其技术实现如下: 触发与任务生成:当自动化风控流程(如授信)的规则引擎决策结果为“转人工”(manualFlag: 1)时,risk-decision服务会将一个包含用户ID、业务ID和流程ID的人审任务写入任务表(tb_risk_flow_task)中。 后台任务调度:系统采用xxlJob作为分布式任务调度框架。一个名为RiskFlowTaskNonExecutionHandler的任务执行器会定时扫描任务表,捞取待处理的人审任务。 任务执行与数据准备:任务执行器获取任务后,会调用Decision服务,准备该用户用于人工审核所需的各类数据(如基础资料、风险标签等)。 结果持久化与通知:审核员在人审系统(Capital Pata)中完成审核并提交结果。该结果会被Decision服务接收并持久化到人审记录表(tb_risk_user_record)中。该表根据user_id进行了分表处理(如_00到_05),以分散写入压力。 下游通知:决策完成后,Decision服务会将最终的人审结果(如manualTags)通过消息队列(MQ)广播出去。上游业务系统或其他相关方订阅相应的主题(如prod-drools-manual-result),即可获取审核结果并进行后续处理。 失败重试:为了保证流程的健壮性,系统还配置了RiskFlowTaskFailRetryHandler,用于处理执行失败的人审任务,进行自动重试。 业务模式背后的架构考量 系统之所以同时支持同步、异步和人机结合三种处理模式,是基于对不同业务场景下延迟、吞吐量和计算复杂度三者之间权衡的结果。 展期这类操作,业务逻辑相对简单,依赖的数据大部分已存在于系统内部,决策路径短。用户正在界面上等待操作结果,因此低延迟是首要目标。采用同步模型可以提供最直接、最快速的用户反馈。 相比之下,新用户授信是典型的重决策场景。它需要从外部拉取征信数据,处理用户设备信息(SDK数据),运行复杂的反欺诈和信用评分模型。整个过程可能耗时数秒甚至数分钟。如果采用同步模型,不仅会导致用户长时间等待,造成糟糕的体验,还会长时间占用API网关和前端服务的连接资源,极大地限制系统的并发处理能力和整体吞吐量。异步架构通过消息队列作为缓冲,将前端请求的接收与后端耗时的处理彻底分离。这种设计使得系统能够平稳地处理流量洪峰,并通过水平扩展消费者(phi-job)来提高整体处理能力,保证了系统的伸缩性和鲁棒性。 人工审核流程则体现了系统处理复杂、非确定性问题的能力。通过引入任务调度系统(xxlJob)和人工干预节点,风控决策不再是非黑即白的二元判断,而是形成了一个“自动化初筛 -> 机器决策 -> 人工复核”的完整闭环,在效率和准确性之间取得了最佳平衡。 这种多模架构的设计,体现了平台在满足多样化业务需求与保证系统高性能、高可用性之间的精妙平衡,是其能够支撑多国家、多业务线快速发展的关键架构决策之一。 第二章 海外风险平台的统一架构 尽管海外风险中台服务于菲律宾、巴基斯坦等多个国家,且经历了从Python到Go语言的技术栈演进,但其核心架构思想和功能模块划分具有高度的一致性。本章将通过对不同站点的架构图进行综合分析,提炼出一个统一的、具有代表性的平台架构蓝图,并明确各个核心服务模块的职责定位。 统一架构蓝图 通过综合分析菲律宾和巴基斯坦的风控系统架构图,可以描绘出一个通用的分层架构模型。该模型自上而下分为接入层、编排层、执行层和基础支撑层。 接入层 (Entry Layer):作为风险系统的统一入口,负责接收和校验来自上游业务系统(如merchant、Rating服务)的各类风控请求。它将外部请求转化为内部标准化的风控任务,并根据请求类型(同步/异步)将其路由到相应的处理流程。 编排与调度层 (Orchestration/Scheduling Layer):这是风险决策流程的核心控制器。它负责解析风控任务,并根据预设的业务流程(Pipeline)或工作流配置,有序地调度执行层中的各个原子能力(如变量计算、模型调用、规则执行)。该层确保了复杂决策逻辑能够被准确、高效地执行。 执行层 (Execution Layer):由一系列功能高度内聚的专业化服务组成,提供风控决策所需的原子计算能力。主要包括: 变量计算服务:负责根据原始数据计算生成衍生特征(即风险变量)。 模型执行服务:负责调用和执行部署在模型平台上的各类机器学习或评分卡模型。 规则引擎服务:负责执行预定义的业务规则集(如反欺诈规则、授信策略)。 决策与持久化层 (Decision & Persistency Layer):该层负责汇总来自执行层的各种结果(变量值、模型分、规则命中情况),并做出最终的业务决策。同时,它还将整个决策过程的输入、中间结果和最终输出完整地记录下来,用于后续的审计、分析和模型迭代。 基础支撑层 (Infrastructure Layer):为整个风险中台提供稳定运行所需的通用技术组件。这包括: 数据库:采用关系型数据库(如MySQL, PolarDB)存储结构化业务数据,并使用NoSQL数据库(如MongoDB)存储三方数据、SDK埋点日志等半结构化或非结构化数据。 消息中间件:使用RabbitMQ进行服务间的异步通信和任务解耦,同时引入Kafka用于大规模数据的实时流式处理。 配置中心:利用Apollo实现所有微服务配置的集中化、动态化管理,提高运维效率和系统灵活性。 数据服务:统一的数据中台(data-hub)提供对外的标准数据查询接口,屏蔽底层数据源的复杂性。 核心微服务及其功能职责 下表详细梳理了风险中台在技术栈演进过程中,核心功能模块与具体微服务之间的映射关系。它清晰地展示了从Python实现的旧版服务到Go语言重构的新版服务,其 underlying 的功能职责是如何传承和演进的。...

October 2, 2025 · 559 words · Kurong

几道反复做了好几遍的一维DP

70.爬楼梯 1 2 3 4 5 6 7 8 9 10 func climbStairs(n int) int { cache := make([]int, n+1) cache[0], cache[1] = 1, 1 for i := 2; i <= n; i++ { cache[i] = cache[i-1] + cache[i-2] } return cache[n] } 最简单的一维 DP 题,遇见属于白给,最标准的没有套路的 DP,完全不需要解释。 198.打家劫舍 1 2 3 4 5 6 7 8 9 10 func rob(nums []int) int { cache := make([]int, len(nums)+1) cache[0], cache[1] = 0, nums[0] for i := 2; i <= len(nums); i++ { cache[i] = max(nums[i-1]+cache[i-2], cache[i-1]) } return cache[len(nums)] } 经典打家劫舍,难度同样属于白给。...

September 28, 2025 · 521 words · Kurong

回溯算法-一次搞懂

回溯算法本质上是一种深度优先搜索(DFS),通过递归来模拟“探索所有可能性”的过程。它的核心思想可以总结为三个步骤: 选择 (Choose): 在当前状态下,从所有可能的选择中,选择一个。 探索 (Explore): 基于这个选择,进入下一层决策,即调用递归函数。 撤销选择 (Unchoose): 当下一层的探索全部结束后(无论是找到了解还是走到了死路),退回到当前状态,撤销刚才的选择,然后尝试下一个可用的选择。 几乎所有的回溯问题,都可以用下面这个伪代码模板来解决: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 result = [] // 存放最终结果 path = [] // 存放当前路径 func backtrack(参数列表) { // 1. 终止条件:判断是否找到了一个可行的解 if (满足结束条件) { // 将当前路径的一个副本加入结果集 result.add(path.clone()) return } // 2. 遍历所有可能的选择 for choice in (当前状态下的所有选择) { // 剪枝:如果这个选择不合法,则跳过 if (isPruned(choice)) { continue } // 3....

September 25, 2025 · 639 words · Kurong

两道常考图算法题-每次做都不会

200. 岛屿数量 - 力扣(LeetCode) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func numIslands(grid [][]byte) int { m, n := len(grid), len(grid[0]) var dfs func(x, y int) ans := 0 dfs = func(x, y int) { if x < 0 || y < 0 || x >= m || y >= n || grid[x][y] !...

September 24, 2025 · 490 words · Kurong

二叉树的两道超高频考题

236. 二叉树的最近公共祖先 - 力扣(LeetCode) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { // 跳出递归 if root == nil { return nil } // 祖先可能是 p,q if root == p || root == q { return root } // 祖先不是 p,q ,在左右子树上再次寻找 left := lowestCommonAncestor(root....

September 22, 2025 · 187 words · Kurong

LRU缓存算法题学习记录

LRU 缓存到底要做什么? 在动手写代码之前,最重要的一步是明确我们的目标。LRU 缓存机制的核心是:当缓存满了以后,优先淘汰最久没有被使用过的数据。 我们来分解一下 LRUCache 需要实现的功能: Constructor(capacity int): 初始化一个固定容量的缓存。 Get(key int): 如果 key 存在,返回对应的 value。 关键点:这次访问使得该 key 成为了“最近使用的”,所以需要更新它的位置。 如果 key 不存在,返回 -1。 Put(key int, value int): 如果 key 存在,更新 value。 关键点:这次更新也使得该 key 成为了“最近使用的”,需要更新它的位置。 如果 key 不存在,插入这个新的键值对。 关键点:插入后,如果缓存的尺寸超过了 capacity,则必须淘汰“最久未使用的”那个元素。 总结一下,我们需要一个数据结构,它必须同时满足以下几个要求: 快速查找:Get 和 Put 都需要快速地判断一个 key 是否存在。 有序存储:需要一种机制来记录所有 key 的“新旧”顺序,方便我们找到“最久未使用的”那个。 快速增删:当一个 key 被访问(Get 或 Put)时,我们要能快速地将它移动到“最新”的位置。当缓存满了,也要能快速地删除“最旧”的那个。 哈希表 + 双向链表 这是本题的核心套路。没有任何单一的基础数据结构能同时满足上述所有要求,所以需要组合。 为什么需要哈希表 (map)? 为了实现 O(1) 复杂度的快速查找。我们可以用 map[int]*Node 来存储 key 到链表节点的映射。 为什么需要链表? 为了维护元素的“新旧”顺序。链表天然有序。我们可以规定,越靠近链表头部的元素,就是“最近使用”的;越靠近链表尾部的,就是“最久未使用”的。 为什么必须是双向链表,而不是单向? 因为我们需要 O(1) 复杂度的删除操作。想象一下,当我们要删除一个节点(比如缓存满了要淘汰尾部节点,或者 Get 了一个中间节点要把它移动到头部),我们需要修改这个节点前面那个节点的 Next 指针。在单向链表中,找到前驱节点需要从头遍历,时间复杂度是 O(n)。而双向链表每个节点都存了 Pre 和 Next 指针,使得我们可以在 O(1) 的时间内完成任意节点的删除。 结论:哈希表 (map) + 双向链表 (Doubly Linked List) 是解决此问题的完美组合。...

September 20, 2025 · 1025 words · Kurong

反转链表专项学习记录

变量名称定义 在开始反转链表套路学习前,先定义一组变量,这组变量会贯穿全部的题型: cur:指向当前正在进行反转的节点和反转完成后的下一个待反转链表的头部; pre:指向已完成反转链表的头部(头插法的缘故),初始值为nil(仅在 206 中是这样,其他情况下都是pre := &ListNode{}进行定义)。; next:指向cur的下一个节点。 后面还会根据不同的题型有不同的变量引入。 206. 反转链表 - 力扣(LeetCode) 1 2 3 4 5 6 7 8 9 10 11 12 13 func reverseList(head *ListNode) *ListNode { var pre *ListNode // 需要注意,不要写成 pre := *ListNode{},这样就完成了初始化、值不是 nil 了 cur := head for cur != nil { next := cur.Next // 保存 next,防止丢失 cur.Next = pre // 断 cur 尾,使其指向反转链表头部 pre = cur // 更新 pre,让其移动到 cur 的位置,此时 cur 成为新头部,pre 符合其定义仍然指向反转链表的头部 cur = next // cur 移动,指向下一个待反转节点 } return pre } 循环中的这 4 步:...

September 19, 2025 · 403 words · Kurong

apollo简单入门

在现代微服务架构中,服务数量的激增使得传统的配置文件管理方式(如本地文件、Git 仓库)变得越来越难以维护。配置的修改、发布、回滚、权限控制以及实时生效等需求,催生了分布式配置中心这一关键中间件。 Apollo (阿波罗) 是由携程框架部门研发的开源配置管理中心,它功能强大、生态丰富,能够集中化管理应用在不同环境、不同集群的配置。本文将带你深入了解 Apollo 的世界,从核心概念到架构原理,再到 Go 语言的实战集成。 一、Apollo 核心概念 要用好 Apollo,首先需要理解它的几个核心概念,这些概念构成了其灵活、强大的配置管理模型。 1. Application (应用) 这是配置管理的基本单元,代表一个独立的应用或服务。每个应用都有一个全局唯一的 AppId,这是客户端与服务端交互时的身份标识。 2. Environment (环境) 用于隔离不同部署环境的配置,如 DEV (开发环境)、FAT (测试环境)、UAT (预发布环境)、PRO (生产环境)。Apollo 的权限管理也是基于环境的,可以精细控制不同环境的配置修改和发布权限。 3. Cluster (集群) 一个应用在同一个环境下,可以部署在不同的集群中。这个概念主要用于支持多数据中心(IDC)或者隔离部署。例如,你可以为上海机房和北京机房创建两个不同的 Cluster,为它们配置不同的数据库地址。默认情况下,会有一个 default 集群。 4. Namespace (命名空间) 这是配置项的集合,是配置隔离的最小单元。通过 Namespace,可以把不同类型的配置分门别类地管理。 私有 Namespace:其作用域仅限于当前应用。每个应用创建时都会有一个默认的 application 命名空间。 公共 Namespace:可以被多个应用关联和共享。常用于存储一些公共的配置,如数据库连接信息、中间件地址等。 关联 Namespace (继承):应用可以关联(继承)公共 Namespace 的配置,实现配置的复用。如果私有配置与公共配置有相同的 Key,私有配置会覆盖公共配置。 格式支持:Namespace 支持多种格式,如 properties (默认)、XML、JSON、YAML、TXT。 5. Release (发布) 当一个 Namespace 的配置修改完成后,需要通过“发布”操作才能被客户端感知到。每一次发布都会生成一个唯一的版本号,且发布后的配置是 不可修改的。如果需要变更,只能创建一个新的发布。这个机制保证了配置的可追溯和快速回滚。 6. Gray Release (灰度发布) 这是 Apollo 的一大亮点。它允许你将新发布的配置先生效于指定的 部分 客户端实例上。你可以根据客户端的 IP 地址或预设的标签来圈定灰度范围。待验证无误后,再进行全量发布。这极大地提高了配置发布的安全性。...

September 14, 2025 · 539 words · Kurong

golang单元测试与冒烟测试入门

概念解析:单元测试 vs. 冒烟测试 1. 单元测试 (Unit Testing) 单元测试是针对程序中最小的可测试单元进行的测试。在 Go 语言中,这个“单元”通常指一个函数(Function)或一个方法(Method)。 核心目标:验证单个函数或方法的逻辑是否正确。它确保给定特定的输入,该函数能返回预期的输出。 关键原则:隔离。单元测试应该与其依赖项(如数据库、文件系统、外部API等)隔离开来。如果一个函数依赖了这些外部服务,我们通常会使用“模拟(Mock)”技术来创建一个假的依赖项,从而保证测试的稳定性和速度。 特点: 速度快:因为不涉及外部I/O操作,通常毫秒级就能完成。 粒度细:精确到每个函数,能够快速定位问题。 编写频繁:是开发过程中最常编写的测试类型。 2. 冒烟测试 (Smoke Testing) 冒烟测试,又称“构建验证测试(Build Verification Test)”,是一种非常基础的测试,用于确定软件的关键功能是否能够正常工作。它像是在给一个新设备通电后,看看它是否会冒烟——如果不冒烟,说明最基本的功能没问题,可以进行更详细的测试。 核心目标:验证应用程序的核心、关键流程是否能够跑通,确保程序在主要路径上不会崩溃。 关键原则:快速、宽泛、但不深入。它不关心业务逻辑的细枝末节,只关心“主干”是否健康。 特点: 速度较快:比完整的端到端测试快,但比单元测试慢。 覆盖面广:覆盖的是最关键的业务流程。 执行时机:通常在新版本构建完成后、进行全面测试之前执行。对于后端服务来说,可能是在服务启动后,立即调用几个核心API(如健康检查/health,登录接口等)看看是否返回200 OK。 Go 中的测试库 标准库 Go 语言内置了强大的测试支持,主要通过 testing 包来实现。这是所有 Go 测试的基础,你必须首先掌握它。 testing: 提供了编写测试的基本框架,例如 *testing.T 类型用于报告测试状态。 net/http/httptest: 专门用于测试 HTTP 客户端和服务器,无需真正监听端口即可模拟 HTTP 请求和响应,非常适合后端 API 测试。 流行的第三方库 虽然标准库很强大,但在某些方面,社区提供了更高效、更易读的工具。 stretchr/testify: 这是 Go 社区中最受欢迎的断言库。它提供了丰富的断言函数(如 assert.Equal(), assert.NoError()),让你的测试代码更简洁、更具可读性。 golang/mock (Gomock): Google 官方开发的 Mock 框架。当你需要隔离测试单元与它的依赖时,Gomock 可以根据接口自动生成模拟对象,让你完全控制依赖项的行为。 h2non/gock: 如果你的代码需要调用外部 HTTP API,这个库可以轻松地模拟这些外部请求,让你的测试不依赖于网络和第三方服务的稳定性。 从单元测试到冒烟测试入门 Part 1: 什么是单元测试?为什么需要它? 想象一下你正在组装一台复杂的机器,比如一台汽车。单元测试就像是分别检查每一个零件(螺丝、齿轮、活塞)是否合格。只有确保每个最小零件都工作正常,你才能对最终组装好的汽车有信心。...

September 14, 2025 · 569 words · Kurong

Kafka简单入门

Apache Kafka 是一个开源的分布式事件流平台,被数千家公司用于高性能数据管道、流分析、数据集成和关键任务应用。无论你是后端开发者、数据工程师,还是希望构建实时数据系统的架构师,掌握 Kafka 都是一项至关重要的技能。 本文将带你系统地学习 Kafka,从核心概念到命令行实战,再到面试高频考点,最后用 Go 语言构建一个简单的生产者和消费者应用。 一、Kafka 核心概念解析 要理解 Kafka,首先必须掌握其核心术语。我们可以把 Kafka 想象成一个高度有序、可分区、可复制的数字“邮局系统”。 1. Broker Kafka 集群中的一台服务器就是一个 Broker。你可以把它想象成邮局系统中的一个“邮局分拣中心”。多个 Broker 共同组成一个 Kafka 集群,协同工作,提供高可用和负载均衡。 2. Topic(主题) Topic 是消息的类别。所有发布到 Kafka 集群的消息都有一个指定的 Topic。这就像邮局里的不同“信件类型”,比如“平信”、“挂号信”或“国际邮件”。生产者将消息发送到特定的 Topic,消费者订阅特定的 Topic 来接收消息。 3. Partition(分区) 每个 Topic 都可以被分成一个或多个 Partition。Partition 是 Kafka 实现高吞吐量和水平扩展的关键。你可以将 Partition 理解为某个信件类型(Topic)的专用“处理通道”。 有序性:在单个 Partition 内,消息是严格有序的。消息被追加到 Partition 的末尾,并被分配一个唯一的、递增的 ID,称为 Offset。 并发性:不同的 Partition 可以分布在不同的 Broker 上,这使得多个消费者可以并行地从一个 Topic 中读取数据,极大地提高了读取效率。 4. Offset(偏移量) Offset 是 Partition 中每条消息的唯一标识符,是一个单调递增的整数。消费者通过 Offset 来追踪自己已经消费到 Partition 中的哪个位置。这个机制非常强大,因为它允许消费者自由地控制消费进度,可以重复消费或跳过某些消息。 5. Producer(生产者) 负责创建消息并将其发布(发送)到 Kafka Topic 的应用程序。生产者在发送消息时,可以指定 Topic,也可以指定消息的 Key。如果指定了 Key,Kafka 会使用哈希算法将这个 Key 映射到某个特定的 Partition,确保所有具有相同 Key 的消息都进入同一个 Partition,从而保证了这部分消息的顺序性。...

September 13, 2025 · 1137 words · Kurong

xxl-job简单入门

在任何复杂的业务系统中,定时任务、延时任务、周期性报表生成等调度需求都不可或令。虽然 Linux 的 cron 和编程语言内置的定时器可以解决简单问题,但在微服务架构下,它们面临着单点故障、无法统一管理、不支持集群和分片等诸多挑战。 XXL-JOB 是一个由大众点评开源的轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。凭借其清晰的架构、丰富的功能和极低的接入成本,XXL-JOB 已成为国内最流行的任务调度解决方案之一。 本文将带你全面解析 XXL-JOB,从核心概念、架构原理,到面试中的高频问题,并最终使用 Go 语言构建一个可以实际运行的执行器。 一、XXL-JOB 核心概念 理解 XXL-JOB 的工作模式,需要先掌握它的几个基本组件和术语。 1. 调度中心 (Admin) 调度中心是 XXL-JOB 的“大脑”,是一个独立部署的 Web 应用。它负责: 任务管理:提供可视化的 UI,用于创建、编辑、启动/停止任务。 调度触发:内置调度器,根据任务配置的 Cron 表达式或固定延时,准时地向执行器发起调度请求。 注册管理:维护在线的执行器列表,感知执行器的上下线。 日志监控:汇集所有任务的执行日志,方便开发者查看任务执行情况和排查问题。 2. 执行器 (Executor) 执行器是集成在业务应用中的一个组件(通常是一个 SDK 或库)。它负责: 自我注册:启动后,执行器会周期性地向调度中心发送心跳,报告自己的地址,完成“服务注册”。 接收调度:内置一个嵌入式的 HTTP 服务器,用于接收来自调度中心的任务触发请求。 执行任务:根据请求中的参数,调用应用内具体的业务代码(JobHandler)。 上报结果:将任务的执行结果(成功/失败)和执行日志回调给调度中心。 3. 任务 (Job) 与 JobHandler 任务:在调度中心 UI 上创建的一个调度单元,包含了 Cron 表达式、路由策略、负责人、超时时间等元数据。 JobHandler:在执行器项目中,具体承载任务业务逻辑的代码块。在 Java 中通常是一个方法,在 Go 中则是一个注册的函数。一个任务必须绑定一个唯一的 JobHandler 名称。 4. 路由策略 (Route Strategy) 当一个执行器以集群模式部署(即多个应用实例)时,路由策略决定了调度中心如何从集群中选择一个或多个实例来执行任务。常用策略包括: 第一个/最后一个:选择集群中第一个或最后一个在线的实例。 轮询 (Round Robin):按顺序轮流选择。 一致性哈希 (Consistent HASH):根据任务 ID 计算哈希值,选择固定的一个实例,可用于实现粘性调度。 分片广播 (Sharding):核心功能。将任务“分片”给集群中的所有实例并行执行。例如,处理 100 万条数据,可以分给 10 个实例,每个实例处理 10 万条。调度中心会向每个实例传递分片参数(分片总数、当前分片索引)。 5....

September 13, 2025 · 493 words · Kurong

sync包详解

一、sync 包概述 sync 包是 Go 标准库中用于处理并发和同步的核心工具包。在 Go 语言中,我们通过 Goroutine 实现并发,而当多个 Goroutine 需要访问共享资源时,为了避免数据竞争(Data Race)等问题,就需要使用 sync 包提供的同步原语来保证并发安全。 面试官通过 sync 包相关的问题,主要考察你对 Go 并发模型的理解、处理并发问题的能力以及对底层原理的掌握程度。 二、sync.Mutex与sync.RWMutex (互斥锁与读写锁) 1. 作用与目的 这是最基础和最常用的同步原语。 sync.Mutex (互斥锁): 保证在同一时刻,只有一个 Goroutine 可以访问被其保护的共享资源。当一个 Goroutine 获取了锁,其他试图获取该锁的 Goroutine 都会被阻塞,直到锁被释放。 sync.RWMutex (读写锁): Mutex 的一种更细粒度的实现,它将访问分为“读”和“写”。 多个读:可以共存,多个 Goroutine 可以同时获取读锁。 写与任何操作:互斥,当一个 Goroutine 获取了写锁后,其他任何 Goroutine(无论是读还是写)都必须等待。 核心解决的问题: RWMutex 旨在优化“读多写少”的场景。如果共享资源的读取频率远高于写入频率,使用 RWMutex 可以显著提高并发性能,因为它允许多个读操作并行执行。 2. 实现原理简介 Mutex: 内部通过一个 state 字段和信号量 sema 实现。state 字段的比特位分别表示锁的锁定状态、是否有被唤醒的 Goroutine、是否有等待的 Goroutine,以及是否处于饥饿模式。 正常模式: 新来的 Goroutine 会和等待队列头部的 Goroutine 竞争锁,这种模式吞吐量高,但可能导致队列中的 Goroutine 长时间等待(饥饿)。 饥饿模式: 当一个 Goroutine 等待锁超过 1ms,锁会切换到饥饿模式。在此模式下,锁会直接交给等待队列头部的 Goroutine,保证公平。 RWMutex: 内部组合了一个 sync....

August 30, 2025 · 605 words · Kurong

TCP的三次握手与四次挥手

深入理解TCP:三次握手与四次挥手(含面试高频考点) TCP (Transmission Control Protocol) 作为互联网核心协议簇中的基石,保证了数据传输的可靠性和顺序性。而要理解 TCP 的可靠性,就必须掌握其经典的连接管理机制——三次握手与四次挥手。 本文将用通俗易懂的比喻和清晰的技术图解,带你彻底搞懂这两个过程,并深入剖析面试中关于它们的高频考点。 一、三次握手:建立可靠的连接 在进行数据传输之前,通信双方(客户端和服务器)必须建立一个可靠的连接。这个建立连接的过程,就是“三次握手”。 一个生动的比喻:打电话 想象一下你给朋友打电话确认双方都能正常通信的过程: 你:“喂,能听到我说话吗?” (发送 SYN) 朋友:“我能听到你,你能听到我吗?” (回复 SYN+ACK) 你:“我也能听到你,那我们可以开始聊天了。” (发送 ACK) 经过这三步,双方都确认了彼此的发送和接收能力都正常,通话(连接)才正式建立。 技术流程详解 第一次握手 (SYN) 从哪到哪:客户端 -> 服务器 做什么:客户端发送一个 SYN (Synchronize) 报文段,其中包含一个随机生成的初始序列号 seq = x。 状态变化:客户端进入 SYN_SENT 状态,等待服务器的确认。 第二次握手 (SYN+ACK) 从哪到哪:服务器 -> 客户端 做什么:服务器收到客户端的 SYN 后,如果同意连接,则回复一个报文段。该报文段包含两个关键信息: 自己的 SYN 标志位,以及一个随机生成的初始序列号 seq = y。 一个 ACK (Acknowledgment) 标志位,以及确认号 ack = x + 1,表示已成功收到客户端的第一个报文。 状态变化:服务器进入 SYN_RCVD 状态。 第三次握手 (ACK) 从哪到哪:客户端 -> 服务器 做什么:客户端收到服务器的 SYN+ACK 后,会发送最后一个 ACK 报文段作为确认。该报文段包含确认号 ack = y + 1。 状态变化:此报文发送后,客户端和服务器都进入 ESTABLISHED 状态,标志着 TCP 连接正式建立成功,可以开始双向传输数据。 三次握手的核心目标 确保客户端和服务器双方都确认了自己的发送能力和对方的接收能力都正常。...

August 30, 2025 · 275 words · Kurong

WebSocket面试详解

第一部分:核心概念 1.1 什么是 WebSocket? WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它诞生于 HTML5,旨在解决浏览器与服务器之间低延迟、实时、双向通信的需求。 全双工 (Full-duplex):与 HTTP 的“请求-响应”模式不同,WebSocket 连接一旦建立,客户端和服务器双方可以随时主动向对方发送数据,无需等待对方的请求。 单个 TCP 连接:所有通信都在一个 TCP 连接上完成,减少了重复建立和关闭连接的开销。 协议标识符:WebSocket 的 URL 方案是 ws://(非加密)和 wss://(加密,运行在 TLS 之上)。 1.2 为什么需要 WebSocket?(解决了什么问题?) 在 WebSocket 出现之前,为了模拟实时通信,开发者通常采用以下技术,但它们都有明显的缺陷: 轮询 (Polling):客户端定时向服务器发送 HTTP 请求,询问是否有新数据。 缺点:大量无效请求,浪费带宽和服务器资源;数据延迟高,实时性差。 长轮询 (Long Polling):客户端发送一个请求,服务器“挂起”这个连接,直到有新数据时才返回响应。客户端处理完响应后,立即发送下一个请求。 缺点:仍然有连接开销;服务器长时间持有连接会消耗资源;实现相对复杂。 服务器发送事件 (Server-Sent Events, SSE):一种允许服务器向客户端单向推送数据的技术。 缺点:只能实现服务器到客户端的单向通信,客户端无法向服务器发送数据(除非通过另一个 HTTP 请求)。 WebSocket 解决的核心问题:提供了一种低开销、低延迟、真正意义上的双向实时通信方案,完美适用于即时通讯、在线游戏、实时数据更新(股票行情、体育比分)等场景。 1.3 WebSocket 与 HTTP 的关系和区别 特性 HTTP (HyperText Transfer Protocol) WebSocket 通信模式 半双工 (Half-duplex)。客户端发起请求,服务器响应,一次完整的事务结束。 全双工 (Full-duplex)。连接建立后,双方可随时互相发送数据。 连接状态 无状态 (Stateless)。每个请求都是独立的,服务器不保留之前请求的信息。 有状态 (Stateful)。连接在整个会话期间保持,服务器和客户端都维护连接状态。 协议开销 较高。每个请求都包含完整的 HTTP 头部,通常有几百字节甚至更多。 极低。握手之后,数据帧的头部非常小(通常 2-10 字节),开销很小。 连接建立 每次通信都需要建立 TCP 连接(或在 Keep-Alive 下复用,但仍是请求-响应模式)。 借用 HTTP 协议进行一次“握手”,之后升级为 WebSocket 协议,后续通信与 HTTP 无关。 应用场景 文档、图片、API 数据等一次性资源获取。 即时聊天、在线协作、实时数据推送、在线游戏等需要高实时性的场景。 协议标识 http://, https:// ws://, wss:// 1....

August 30, 2025 · 691 words · Kurong

搞懂BIO与AIO

彻底搞懂 AIO 与 epoll 维度一:必须厘清的两个核心概念 在深入 I/O 模型之前,我们必须先区分两组完全独立的概念,很多人在这里就已混淆: 阻塞 (Blocking) vs. 非阻塞 (Non-blocking) 关注点:应用程序在等待结果时的状态。 阻塞:发起 I/O 操作后,如果数据未就绪,当前线程会被挂起,直到操作完成。 非阻塞:发起 I/O 操作后,如果数据未就绪,函数会立刻返回一个错误码,线程不会被挂起,可以继续执行其他任务。 同步 (Synchronous) vs. 异步 (Asynchronous) 关注点:内核与应用程序之间如何交付 I/O 结果。 同步:应用程序自己负责发起 I/O 操作,并且主动去查询或等待结果。即便是非阻塞轮询,只要是“我”去问内核“好了没”,都算同步。 异步:应用程序发起 I/O 操作后便立即返回。内核会独立完成整个 I/O 过程(包括将数据从内核空间拷贝到用户空间),当一切完成后,内核会通知应用程序。 经典烧水比喻: 同步阻塞 (BIO):你守在水壶边,一直等到水烧开。 同步非阻塞 (NIO):你边看电视边烧水,每隔几分钟就跑去厨房看水开了没。 异步非阻塞 (AIO):你用一个智能水壶,告诉它水开后提醒你,然后你就可以安心做任何事,直到水壶通知你。 I/O 模型的演进之路 模型一:BIO (同步阻塞 I/O) - 一夫当关 这是最古老、最简单的模型:一个连接对应一个处理线程。 工作方式:主线程 accept 一个新连接,然后创建一个新线程来处理这个连接的 read() 和 write()。 致命缺点: 资源开销巨大:一个连接一个线程,线程的创建和上下文切换成本极高。 性能瓶颈:当成千上万个线程大部分时间都在 read() 等待数据而阻塞时,CPU 大量时间被浪费在无效的上下文切换上。它无法应对 C10K (单机一万并发) 的挑战。 模型二:I/O 多路复用 - 一人看多路 为了解决 BIO 的问题,诞生了 I/O 多路复用技术。其核心思想是:用一个(或少量)线程来监视和管理大量的网络连接(文件描述符 File Descriptor, fd)。...

August 30, 2025 · 314 words · Kurong

pprof 指南

Go 性能分析利器 pprof 指南 pprof 是 Go 语言生态中功能最强大的性能分析工具,它内置于 Go 的标准库中,能够帮助开发者精准定位程序中的性能瓶颈,无论是 CPU 的过度消耗、内存的异常增长,还是并发程序中的各种“慢”问题。 本文将在 kurongtohsaka 的 pprof 指南基础上,深入探讨两个额外的关键领域:Goroutine 泄漏 和 锁竞争。 第一部分:PProf 基础与开启 要在 Web 服务中开启 pprof,我们只需引入 net/http/pprof 包。对于使用 Gin 等框架的服务,可以将其 Handler 包装后注册到路由中。 一个关键的准备步骤是,为了能分析到 阻塞 和 锁竞争,我们需要在程序启动时设置采样率。如果不设置,相关的 profile 文件将是空的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package main import ( "log" "net/http" _ "net/http/pprof" // 关键:匿名导入pprof包,它会自动注册handler "runtime" "github....

August 27, 2025 · 808 words · Kurong

常见linux命令

好的,这个命令非常重要,尤其是在排查 “too many open files” 这类经典问题时。我会把它加上,并放在一个新的“高级调试与诊断”分类里,让整个指南的结构更清晰。 这是更新后的完整版本,你可以直接替换掉之前的博客文章。 后端实习Linux命令指南 一、文件与目录管理 (基础中的基础) 这是最基本的操作,你需要像在Windows图形界面里操作文件夹一样熟练。 命令 完整名称 作用 常用示例 ls list 列出目录内容 ls -lha (列出所有文件,含隐藏文件,以人类可读的格式显示详细信息) cd change directory 切换目录 cd /var/log (切换到日志目录), cd .. (返回上一级), cd ~ (返回家目录), cd - (返回上次所在目录) pwd print working directory 显示当前所在路径 pwd cp copy 复制文件或目录 cp source.txt dest.txt, cp -r sourcedir destdir (递归复制整个目录) mv move 移动或重命名文件/目录 mv oldname.txt newname.txt (重命名), mv file.txt /tmp/ (移动到/tmp目录) rm remove 删除文件或目录 rm file.txt, rm -rf somedir (递归强制删除目录,慎用!) mkdir make directory 创建目录 mkdir logs, mkdir -p /data/app/logs (-p可以递归创建多层不存在的目录) touch touch 创建空文件或更新时间戳 touch app....

August 27, 2025 · 383 words · Kurong

《基于大型语言模型的检索增强生成综述》笔记

基于大型语言模型的检索增强生成综述 进阶 RAG 范式与模块化 RAG 表 1 RAG 方法总结 RAG 过程 文献 检索增强生成方法 检索预处理 SKR[30]、Adaptive-RAG[31] 基于问题精准的文本解析 检索预处理 DenseX[32]、TableGPT[33]、Iseeq[34]、knowledgpt[35]、G-Retriever[36] 基于外部检索源的文本解析 检索预处理 LLAMAi[37]、Small2Big[38]、Meta-Chunking[39]、GLMwCFiCR[40]、RAAT[41] Chunk 分割(动态分块、元数据增强) 嵌入表示 C-Pack[46]、RetroMAE-2[47]、REPLUG[48]、Arl2[49]、Promptagator[50]、BGE[51]、AngIE[52]、ListT5[53] 微调嵌入(领域适配、对齐生成器) 索引 HyDE[54] 分层索引(假设文档嵌入优化) 索引 RAPTOR[55] 树形索引(分层摘要树) 索引 KGP[56] 知识图谱(KG)索引(多文档逻辑关系建模) 查询 RAG-Fusion[57] 多响应扩展(并行子查询) 查询 LTMP-CLR[58] 子查询(最小到最大提示分解) 查询 RRR[28]、BEQUE[59] 查询转换 / 重写(LLM 驱动的长尾查询优化) 查询 Step-back prompt[26]、FCTG-ICRA[60]、DPR[61] 查询路由(动态路径选择) 外部适配器 UP-RISE[62] 轻量级提示检索器(零样本提示池) 外部适配器 Search-Adaptor[63]、PRCA[64]、CRAG[65] 特定任务检索器(插件式适配器) 外部适配器 GenRead[66]、PKG[67] 引入 LLMs 生成器(替代传统检索器) 检索信息生成 RAG-Fusion[57]、Filter-Reranker[68]、G-RAG[69]、RankRAG[70] 重排(LLM+SLM 协同排序) 检索信息生成 Lingua[71]、Longllmlingua[72]、FILCO[73]、RECOMP[74]、ACD-RAG[75] 选择性上下文(噪声过滤与压缩) 检索信息生成 RAAT[39]、IBP-ENFoRAG[76]、Chatlaw[77] 信息过滤(LLM 自评相关性) 提示输出 URR-RAIC[78] 微调数据(多样化标题增强) 提示输出 SANTA[79]、DFK-TOD[80]、RA-DIT[81] 强化学习 / 对齐(KL 散度对齐检索 - 生成偏好) 高级流程优化 Efficient RAG [85]、Rat [88] 等 迭代检索(多轮精炼查询) 高级流程优化 IRCoT[89]、ToC[90] 递归检索(链式思维 + 澄清树) 高级流程优化 Self-RAG[95]、IM-RAG[98] 自适应增强(反射令牌自主决策) 表 2 RAG 开源框架 名称 项目地址 简介 LlamaIndex2 https://github....

August 1, 2025 · 4602 words · Kurong

《Redis深度历险》记录

《Redis深度历险》记录 基础与应用 分布式锁 分布式锁用于高并发或分布式事务中保护临界资源。 最常见的实现方式是使用 SET 命令的 NX (不存在时设置)和 PX (设置过期时间)选项: 1 SET lock_name my_random_value NX PX 30000 lock_name : 锁的名称。 my_random_value : 唯一标识(通常使用 UUID)。 NX : 只有键不存在时才设置。 PX 30000 : 设置 30 秒的过期时间。 释放分布式锁时需要验证唯一标识是否匹配,防止误删其他客户端的锁。 超时问题 超时问题有三类: 锁过期,可以通过锁续约机制,即获取锁后启动一个守护线程,定期检查锁是否仍由自己持有,如果是则延长锁的过期时间。 客户端获取锁后崩溃,没有主动释放锁,锁就只能等待超时后才能被其他客户端获取。所以要设置合理的锁超时时间。 Redis 服务器和客户端之间的时钟不同步,可能导致锁提前或延后过期。尽量确保服务器和客户端时钟同步(NTP),同时适当增加锁的超时时间作为缓冲。 分布式锁的实现有 Redlock 算法、go-redis 中的简易 mutex 分布式锁。 延时队列 通过 List 来作为简易的消息队列使用。 通过 rpush , lpush 控制入队,rpop , lpop 控制出队。 使用 List 实现延时队列会出现一个问题,假设 List 为空,那么之后的 pop 操作就是死循环。此时可以使用阻塞读来避免这种情况,即使用 blpop, brpop 两个命令代替之前的出队命令。阻塞读在队列没有数据的时候,会立即进入休眠状态。 同时需要注意,List 长时间为空,Redis 客户端连接可能会因为空闲而被回收。所以需要加入重试机制。...

April 29, 2025 · 914 words · Kurong

《高性能MySQL》记录

《高性能MySQL》记录 创建高性能的索引 索引基础 B-Tree 索引 InnoDB 引擎的默认索引类型是 B+ Tree,按照原数据格式进行存储。 B-Tree 意味着所有的值都是按顺序存储的,并且每一个叶子到根的距离相同。B-Tree 索引能够加快访问数据的速度,通过根节点的指针不断向下比较查找,最终存储引擎要么找到对应的值,要么记录不存在。B-Tree 对索引列的顺序存储结构适合查找范围数据,如全值匹配、匹配最左前缀、匹配范围值等。除了按值查找,索引还可以用于 ORDER BY 操作。 B-Tree 索引存在以下限制: 如果不是按照索引的最左列开始查找,则无法使用索引。对于复合索引(A,B,C),查询条件必须包含A列才能有效使用该索引。如果查询只使用B或C列,索引通常不会被使用。 不能跳过索引中的列,必须按照索引定义的列顺序使用。 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查询。 哈希索引 哈希索引是基于哈希表实现的索引类型,通过哈希函数将索引列值转换为哈希码来快速定位数据行。在MySQL中,只有MEMORY存储引擎显式支持哈希索引,并且这是MEMORY引擎的默认索引类型。 哈希索引是无序存储,而且只支持精确匹配。哈希索引的限制如下: 不支持范围查询,如>、<、BETWEEN等。 不支持部分列匹配,必须使用索引的所有列才能生效,因为哈希索引始终是使用索引项的全部内容来计算哈希值的。 哈希冲突问题,不同值可能产生相同哈希值,需要额外处理。 InnoDB 有一个自适应哈希索引,InnoDB 引擎会自动监控查询模式,对频繁访问的索引值创建自适应哈希索引。这实际上是 B-Tree 索引上的优化层,使用哈希值而非键本身进行查找。 空间数据索引(R-Tree) 空间数据索引是一种专门用于处理多维空间数据(如地理坐标、几何图形等)的索引技术,主要解决传统 B-Tree 索引无法有效处理空间数据的问题。 全文索引 全文索引是专门为文本内容搜索设计的索引技术,通过建立倒排索引实现高效文本检索。 高性能的索引策略 独立的列 索引列必须作为独立的表达式存在,不能参与计算或作为函数参数,否则索引会失效。MySQL 无法解析包含运算或函数的表达式,必须将列单独提取为查询条件,才能触发索引优化。 前缀索引与索引选择性 针对长字符串或文本类型,通过截取前 N 个字符建立索引,减少存储空间和 I/O 消耗。 关于如何选择合适的前缀长度,可以通过计算选择性进行评估: 1 SELECT COUNT(DISTINCT LEFT(column, N)) / COUNT(*) FROM table; 选择接近完整列选择性的最短前缀(如对邮箱地址反转后建前缀索引)。 复合索引与最左前缀原则 最左前缀原则:查询必须从复合索引的最左列开始,且不能跳过中间列。示例:索引 (A,B,C) 支持 A=1 AND B=2 ,但不支持 B=2 或 B=2 AND C=3 。使用复合索引时要注意范围查询限制,若某一列使用范围查询,其右侧的列无法使用索引优化。...

April 12, 2025 · 193 words · Kurong

《Kubernetes编程》记录

《Kubernetes编程》记录 概论 控制器和 Operator 控制器实现了一个控制循环,通过 API 服务器监测集群中的共享状态,进行必要的变更。Operator 也是一种控制器,但是包含了一些运维逻辑,如应用程序的生命周期管理。 控制循环的基本流程如下: 读取资源的状态,通常采用事件驱动模式,使用 Watch 来实现。 改变集群中的对象状态或集群外部系统的状态。 通过 API 服务器更新第 1 步中所提到的资源的状态,状态放在 etcd 中。 循环执行这些逻辑。 控制器通常会使用以下的数据结构: Informer :Informer 负责观察资源的目标状态,它还负责实现重新同步机制从而保证定时地解决冲突。 工作队列:事件处理器把状态变化情况放入工作队列。 多个相互独立的控制循环通过 API 服务器上的对象状态变化相互通信,对象状态的变化由 Informer 以事件的方式通知到控制器。这些通过 Watch 机制发送的事件对用户来说几乎不可见,在 API 服务器的审计机制中也看不到这些事件,只能看到对象更新的动作。控制器在处理事件时,以输出日志的方式记录它做的动作。 Watch 事件:在 API 服务器与控制器之间通过 HTTP 流的方式发送,用于驱动 Informer 。 Event 对象:是一种用户可见的日志机制,如 kubelet 通过 Event 来汇报 Pod 的生存周期事件。 乐观并发 Kubernetes 采用乐观并发机制,假设多用户操作同一资源的冲突概率较低,允许并发修改,仅在提交时检测冲突。 每个 Kubernetes 资源对象(如 Pod、Deployment)的元数据中都有一个 resourceVersion 字段,用于标识资源的当前版本号(由 etcd 生成)。这是乐观并发的核心: 读取资源时:客户端获取对象的 resourceVersion(例如 123)。 更新资源时:客户端必须在请求中携带该版本号(通过 metadata.resourceVersion 字段)。 服务器端校验:API Server 比较请求中的 resourceVersion 与当前存储中的版本: 匹配:允许更新,resourceVersion 递增。 不匹配:返回 409 Conflict 错误,拒绝更新(说明资源已被其他客户端修改过)。 当发生冲突时,客户端需重新获取资源的最新状态,然后基于最新数据重新提交修改。...

April 9, 2025 · 212 words · Kurong

《Kafka权威指南》记录

《Kafka权威指南》记录 初识 Kafka 消息和批次 消息有一个可选的元数据,也称为键。键是一个字节数组,可以用来实现如分布式键等应用。 为了提高效率,消息会被分批次写入 Kafka,批次包含了一组同属于一个主题和分区的消息。 模式 模式就是消息的格式,如Json、XML。 主题和分区 消息通过主题进行分类,主题又被分为若干个分区。 生产者和消费者 默认情况下,生产者会把消息均衡地分布到主题的所有分区中,也可以通过自定义设置决定消息发布到哪里。 消费者通过检查消息的偏移量来区分已经读取过的消息。 broker 和集群 一台单独的 Kafka 服务器称为 broker,单个 broker 可以处理数千个分区和每秒百万级的消息量。 集群由 broker 组成,同时也有集群中都有的分布式共识算法。Kafka 使用 ZAB 作为共识算法,2.8 版本后支持 KRaft 管理元数据。 Kafka 与其他消息队列的区别 Kafka 与其他消息队列相比,有以下几个显著的区别: 高吞吐量与低延迟 Kafka 设计上支持高吞吐量和低延迟,能够处理海量数据和高并发写入、读取场景。 其他消息队列(如 RabbitMQ、ActiveMQ)通常适用于较小规模或实时性要求较高的场景,但在海量数据和高并发场景下可能会遇到性能瓶颈。 持久化与分布式存储 Kafka 使用磁盘持久化和日志分段技术,并通过分区(Partition)和副本(Replica)机制实现分布式存储和容错。 其他消息队列有的采用内存队列(例如 RabbitMQ 默认使用内存),虽然也支持持久化,但在大规模数据存储和高可靠性需求下不如 Kafka 灵活。 消息模型与消费模式 Kafka 基于发布-订阅模型,采用拉取(Pull)的消费模式。消费者主动拉取消息,可以灵活控制消费速率和消息重放。 其他消息队列(例如 RabbitMQ)多采用推送(Push)模式,消息直接推送给消费者,实时性较好,但可能在处理慢消费者时需要额外机制(如 ACK、流控等)。 数据持久性和消息重放 Kafka 中的消息存储在日志中,并且消息不会在被消费后自动删除(可以根据保留策略删除),这使得消费者可以根据需求重放消息。 其他消息队列通常在消息被消费确认后就删除,无法轻易重放历史消息。 扩展性与容错性 Kafka 天生支持分布式集群部署,通过增加 Broker 节点来横向扩展,且内置副本机制保证容错。 其他消息队列虽然也有集群部署方案,但在大规模扩展和高容错要求下往往不如 Kafka 那样简单高效。 应用场景不同 Kafka 更适用于日志收集、流处理、大数据实时分析等场景。 其他消息队列则更适合需要复杂路由、即时响应、任务队列等场景,比如电商下单、实时通知等。 Kafka 生产者 消息发布方式:...

March 26, 2025 · 132 words · Kurong

《MongoDB 权威指南》记录

《MongoDB 权威指南》记录 MongoDB 简介 MongoDB 是面向文档的非关系型数据库。 MongoDB 具有以下特点: 易于使用:没有“行记录”的概念,而使用“文档”模型取代之。同时也没有预定义模式,文档键值的类型和大小是动态的。 易于扩展:MongoDB 采用了横向扩展方式,面向文档的数据模型使得跨多台服务器拆分数据更加容易。 性能强大:使用了机会锁,提高并发和吞吐量。同时会尽可能多的使用内存作为缓存,并尝试为查询自动选择合适的索引。 入门指南 文档 文档是一组有序键值的集合,如以下形式: 1 {"q": "ans"} 集合 集合就是一组文档,相当于关系型数据库的表结构。 集合具有动态模式的特性,每个文档的长度大小、键数量都可以不一样。不过开发中并不推荐这样做,还是将“形状”相同的文档存在同一个集合中好,因为这样查询起来性能不会受到影响。 在 MongoDB 中通常会用 . 字符分隔不同命名空间的子集合,如 blog.posts 。 数据库 MongoDB 使用数据库对集合进行分组。 数据类型 基本数据类型:除了 int 等,还有 Regex、Array、ObjectID(12字节的文档的唯一ID 标识)、Bytes、JSCode。 Array: {"things": ["pie", 3.14]},数组内可以混有不同类型的数据。 内嵌文档:文档作为值。 ObjectID:每个文档都有一个 _id ,默认是 ObjectID ,这个 ID 在集合内是唯一的。可以看到这个 ID 不是自增的,类似于 UUID,这说明 MongoDB 在设计之初就是作为分布式数据库存在的。以下是它的生成规则: 前 4 字节为时间戳,中间 5 个字节是随机值,后 3 个字节是计数器(起始值随机)。 索引 MongoDB 中可以设置单一索引和复合索引,方式如下: 1 db.users.createIndex({"age": 1, "username": 1}) # 复合索引 MongoDB 的默认存储引擎是 WiredTiger ,索引是 B+ 树实现的。所以在索引的优化上,可以参照相关关系型数据库的索引优化经验与原则。下面是部分经验:...

March 22, 2025 · 143 words · Kurong

《HTTP权威指南》记录

《HTTP权威指南》记录 本书中的 HTTP 协议版本为 HTTP/1.1 。 URL 与资源 URL 语法 在 URL 中以分号 ; 作为参数的分隔符,如下: 1 http://xxx.xxxx.com/hammers;sale=false/index.html;graphics=true 有些资源需要通过查询字符串来缩小查找范围,查询字符串通过 ? 分割,如常见的 GET 请求中的查询参数。 URL 支持使用片段组建来表示一个资源内部的片段,以 # 进行分割: 1 http://xxx.xxx.com/index.html#head Web 服务器 Web 服务器应该做的: 接受客户端连接:客户端收到一条连接之后,那么它将会把新连接添加到现存web服务器连接列表中,用于监视当前连接上的数据传输情况。期间服务器还应该做到通过一定的设备机制阻止未认证或已知恶意黑名客户端的连接。 接收请求报文: 这一步需要解析请求报文: 解析请求行,查找请求方式、URI 、版本号以及 CRLF 分隔符。 读取以 CRLF 结尾的报文首部。 检测到以 CRLF 结尾的,标识首部结尾的空行。 解析得到请求体。 处理请求:其他章节会详解。 对资源的映射及访问:找到客户端请求资源在服务器的上的目录路径。 Web 服务器存放内容的文件夹称为文档的根目录(doc root),Web 服务器会从请求报文中获取 URI ,并将其附加在 doc root 的后面。 在一个服务器上挂载多个 Web 站点,那么这样当请求的资源路径相同时,服务器应该从请求报文首部的 HOST 和 URI 字段找出真正的资源目录,这些目录都可以更改配置。 构建响应: 正确设置响应主体的长度(content-length)。 设置报文的 MIME 类型(content-type)。 有时候资源不在原地,需要进行重定向。 发送响应。 记录事务日志。 代理 Web 的中间实体 Web 代理服务器是代表客户端对事务请求处理的中间人,代理分为私有代理(只代理一个客户端)和公共代理(代理多个客户端)。 代理和网关的对比:代理的两端使用相同的协议,而网关的两端使用不同的协议,网关负责协议转换。 代理应用 内容过滤。 文档访问控制。 安全防火墙。 Web 缓存:缓存资源的副本。 反向代理:反向代理伪装成原始服务器,不过与服务器不同的是反向代理还可以向其他服务器发送请求,以便实现按需定位所请求的内容。 ……....

March 14, 2025 · 315 words · Kurong

《深入理解设计模式》记录

《深入理解设计模式》记录 这里只记录了相对常用、面试常问的设计模式。 单例模式 单例模式(Singleton Pattern)是一种创建型设计模式,其核心目标是确保一个类仅有一个实例,并提供全局访问点。它适用于需要全局唯一对象且频繁访问的场景,例如配置管理、线程池、日志记录器等。 单例模式需要注意三点: 私有化构造函数:防止外部通过 new 直接创建实例。 自行创建实例:类内部负责生成唯一实例。 全局访问方法:提供静态方法供外部获取实例。 懒汉模式 特点:延迟实例化,首次调用时创建实例,需处理线程安全问题。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package singleton import "sync" type lazySingleton struct { } var ( instance *lazySingleton once sync.Once ) func GetLazyInstance() *lazySingleton { once.Do(func() { instance = &lazySingleton{} }) return instance } 饿汉模式 特点:在包加载时直接初始化实例,无需考虑线程安全。 1 2 3 4 5 6 7 8 9 10 11 package singleton type eagerSingleton struct { } var instance = &eagerSingleton{} func GetEagerInstance() *eagerSingleton { return instance } 懒汉模式的另一实现形式:静态内部类 特点:通过嵌套结构体延迟加载,结合 sync....

March 8, 2025 · 1500 words · Kurong

动态规划经典问题学习记录

动态规划经典问题学习记录 0-1 背包 问题描述 有 $n$ 个物品,第 $i$ 个物品的体积为 $w[i]$ ,价值为 $v[i]$ ,每个物品最多选一次,求体积和不超过 $capacity$ 时的最大价值和。 通过回溯去思考解题思路 先去思考当前的操作,即枚举第 $i$ 个物品选与不选: 不选,剩余容量不变。 选,剩余容量减少 $w[i]$ 。 递归到子问题,在剩余容量为 $c$ 时,从前 $i$ 个物品中得到的最大价值和。 下一个递归子问题: 不选,在剩余容量为 $c$ 时,从前 $i-1$ 个物品中得到的最大价值和。 选,在剩余容量为 $c-w[i]$ 时,从前 $i-1$ 个物品中得到的最大价值和。 最终得到的递归公式:$dfs(i,c) = max(dfs(i-1, c),dfs(i-1, c-w[i])+v[i])$ 对应的代码就是: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def zero_one_backpack(capacity: int, w: List[int], v: List[int]) -> int: n = len(w) @cache # 记忆化搜索数组 def dfs(i: int, c: int) -> int: # 跳出递归条件 if i < 0: return 0 # 容量不足时 if c < w[i]: return dfs(i-1, c) # 将问题进一步分解 return max(dfs(i-1, c), dfs(i-1, c-w[i]) + v[i]) return dfs(n-1, capacity) 以上就是 0-1 背包的“至多装 capacity”的递归+记忆化搜索形式,其实也可以写成递推式:$dp[i][c] = max(dp[i-1][c],dp[i-1] [c-w[i]]+v[i])$...

March 8, 2025 · 872 words · Kurong

《深入理解分布式共识算法》记录

《深入理解分布式共识算法》记录 从 ACID 和 BASE 到 CAP BASE 理论 BASE 包含; BA(Basically Available):基本可用,当分布式系统出现故障的时候,允许损失部分可用性。这里损失指响应时间的损失和功能上降级,前者是在增加响应时间上来提供服务,后者是在高峰期不请求后端而直接返回降级数据、以保证系统的稳定性。 S(Soft State):软状态,允许系统中的数据存在中间状态,并认为该状态不会影响系统的整体可用性,即允许节点之间的数据同步存在延迟。 E(Eventually Consistent):最终一致性。 为应用 BASE 理论,可以采用异步补偿机制。如果用这个机制,需要明确哪些操作属于非关键操作,如果非关键操作失败,应允许业务流程继续执行,然后在异步补偿非关键操作。 补偿指事务补偿:在分布式事务中,由于各个服务可能分布在不同的节点上,无法像单机事务那样简单地通过“回滚”来撤销操作。因此,事务补偿通过执行与原始操作相反的操作来达到类似的效果。 补偿操作需要注意两点: 幂等性:补偿操作可能需要多次执行,因此需要保证其幂等性,即多次执行的效果与一次执行相同。 最终一致性:补偿机制通常用于实现最终一致性,而不是强一致性,因此需要权衡一致性和性能。 CAP 理论 CAP 包含: C(Consistency)一致性:分布式系统的全序线性一致性。 A(Availability)可用性:任何情况都能够处理客户端的每个请求。 P(Partition Tolerance)分区容错性:发生网络分区时,系统仍能提供服务。 任何分布式系统都可分为三类架构: CP(锁定事务资源):所有节点的数据需要实时保持一致性,这需要锁定各分支事务的资源。当发生分区时,此期间为确保返回客户端数据的准确性且不破坏一致性,可能会因为无法响应最新数据而拒绝响应,即放弃 A 。 AP(尽力提供服务):要求每个节点拥有集群的所有能力或数据,能独立处理客户端的每个请求。发生分区时,可以通过缓存后本地副本来处理请求,以达到可用性,但会出现各节点不一致性,即放弃 C 。 CA(本地一致性):在不发生分区时,C、A 正常满足。一旦发生分区,会保证各子分区满足 CA 。 但是网络分区不可避免,CAP 理论就变成了在 C 和 A 中的权衡问题。 2PC、3PC,分布式事务的解决方案 2PC 2PC 中的字母含义是 P(Participant,参与者),C(Coordinator,协调者)。 2PC 即两阶段提交协议,用于解决分布式事务问题。第一阶段用于各个分支事务的资源锁定,第二阶段用于全局事务的提交或回滚: 第一阶段,准备: 开启全局事务。当协调者收到客户端的请求后,它将各个分支事务需要处理的内容通过 Prepare 请求发送给所有参与者,并询问能否正常处理自己的分支事务,然后等待各个参与者的响应。 处理分支事务。当参与者收到 Prepare 请求后便锁定事务资源,然后尝试执行,记录 Undo 和 Redo 信息,但不提交分支事务。 汇报分支事务状态。参与者根据第 2 步执行的结果,向协调者汇报各自的分支事务状态,Yes 表示可以提交,No 表示不能提交。 第二阶段,提交/回滚:...

March 7, 2025 · 298 words · Kurong

Leetcode-75 总结

Leetcode-75 总结 代码链接:KurongTohsaka/my-lc75-go Array&String 151 反转字符串中的单词:双指针。 238 除自身以外数组的乘积:线性 DP 。 334 递增的三元子序列:双指针。 345 反转字符串中的元音字母:双指针。 443 压缩字符串:双指针。 605 种花问题:列出三种种花情况,依次进行处理即可。 1071 字符串的最大公因式:根据特殊的 str1 + str2 == str2 + str1 公式和辗转相除法解题。 1431 拥有最多糖果的孩子:先找最大值,然后进行比较。 1768 交替合并字符串:取较小长度最为切割位置,然后交替合并,最后处理剩余部分。 DoublePointers 11 接水问题:双指针的首尾指针。 283 移动零:双指针的快慢指针。 392 判断子序列:双指针的快慢指针。 1679 K 和数对的最大数目:解法与两数之和相似,使用哈希表记录为匹配数组次数,然后更新 hashMap[k-v] 的数值。 SlidingWindow 643 子数组最大平均数-1 :定长滑动窗口,应用通用解法,即新元素进入、更新统计量、right-left+1 元素离开三步。 1004 最大连续1的个数-2 :变长滑动窗口,通过快慢指针实现。 1456 定长子串中元音的最大数目:定长滑动窗口,通用解法。 1493 删掉一个元素后全为1的最长子数组:变长滑动窗口,解法与 1004 高度相似。 PrefixSum 724 寻找数组的中心下标:双指针可解,不过还是推导出的公式更快。 1732 找到最高海拔:很简单,直接秒。 HashMap 1207 独一无二的出现次数:用了两个哈希,第一个是对数组中所有元素进行计数,第二个是对第一个哈希元素的出现次数进行计数,目的是得到独一无二的出现次数。 1657 确定两个字符串是否接近:操作 1 和 2 能实现的大前提是两个字符串的长度一致,操作 1 只要两个字符串对应的字母的计数一致就成立,操作 2 则进一步要求每个“次数”一致。 2215 找出两数组的不同:双哈希表记录每个元素出现次数,然后分别进行比较。 2352 相等行列对:一个哈希表解决。先行遍历,把每一行作为键存储,然后记录相同的行出现的次数。再列遍历,把每列作为键去查询,然后把次数加上相同的行的个数。 Stack 394 字符串解码: 本题要点在于只要遇到右括号就开始处理栈内元素: 先处理直到左括号内的所有元素,拼接后再反转(顺序是反的🐎),然后别忘记让左括号出栈。 之后统计出现次数,此时注意数字可能为两位数。在处理完成后,就开始按照次数重复拼接子串,然后再把该子串入栈(处理嵌套字符串的方式)。 735 小行星碰撞:只有当栈顶向右(正)且当前行星向左(负)时才处理碰撞,使用循环处理可能的多次碰撞,直到当前行星被摧毁或无法继续碰撞。 2390 从字符串中移除星号:常规栈解法,很简单。 Queue 649 Dota2 参议院:使用两个队列存储每位参议院出现的位置,通过比较位序来选择被淘汰的议员,同时保证每轮必有两名议员被踢出队列。 933 最近的请求次数:维护一个队列,本题想要的结果就是最终的队列长度。超出时间范围的请求出队、新请求进队。 LinkedList 206 反转链表:经典操作,三指针反转链表,属于基本功了。 328 奇偶链表:用两个变量记录奇偶两个链表的头节点,再用两个变量作为构建奇偶链表的指针。然后遍历整个链表去构建奇偶链表,最后把奇链表的指针指向偶链表的头部。 2095 删除链表中的中间节点:通过快慢指针找到中间节点,同时记录慢指针的前一个节点方便后续删除操作。 2130 链表最大孪生和:本题属于缝合题,先是快慢指针找中间节点,再是反转后半部分链表,最后又是一个双指针遍历。 Tree - DFS 所有题都是。DFS (深度优先遍历)。...

March 2, 2025 · 511 words · Kurong

《数据密集型系统设计》记录

《数据密集型系统设计》记录 本书的电子版本链接:Vonng/ddia: 《Designing Data-Intensive Application》DDIA中文翻译 3. 存储与检索 本章围绕两大类存储引擎:日志结构(log-structured)的存储引擎,以及面向页面(page-oriented)的存储引擎(例如 B 树)。 数据库核心:数据结构 这里所使用的“日志”是一个更一般性的概念:一个支持 append-only 仅追加操作的记录序列。 为了高效查找数据库中特定键的值,需要用到索引。索引是从主要数据中衍生的额外结构,维护它会产生额外开销,特别是写入时。任何类型的索引通常都会减慢写入速度,因为每次写入数据时都需要更新索引。这是存储系统中一个重要的权衡:精心选择的索引加快了读查询的速度,但是每个索引都会拖慢写入速度。因为这个原因,数据库默认并不会索引所有的内容,而需要程序员或数据库管理员(DBA),基于对应用的典型查询模式的了解来手动选择索引。 SSTable 把键值对的序列按照键进行排序,这个格式称为排序字符串表(Sorted String Table, SSTable)。此时再要求每个键值在每个合并的段文件中出现一次。与使用散列索引的日志段相比,SSTable 有几个大的优势: 即使文件大于可用内存,合并段的操作仍然是简单高效: 一开始并排读取多个输入文件,查看每个文件中的第一个键,复制最低的键(根据排序顺序)到输出文件,不断重复此步骤,将产生一个新的合并段文件,而且它也是也按键排序的。 如果在几个输入段中出现相同的键,该怎么办?请记住,每个段都包含在一段时间内写入数据库的所有值。这意味着一个输入段中的所有值一定比另一个段中的所有值都更近(假设我们总是合并相邻的段)。当多个段包含相同的键时,我们可以保留最近段的值,并丢弃旧段中的值。 为了在文件中找到一个特定的键,你不再需要在内存中保存所有键的索引: 假设你正在内存中寻找键 handiwork,但是你不知道这个键在段文件中的确切偏移量。然而,你知道 handbag 和 handsome 的偏移,而且由于排序特性,你知道 handiwork 必须出现在这两者之间。这意味着你可以跳到 handbag 的偏移位置并从那里扫描,直到你找到 handiwork(或没找到,如果该文件中没有该键)。 由于读取请求无论如何都需要扫描所请求范围内的多个键值对,因此可以将这些记录分组为块(block),并在将其写入硬盘之前对其进行压缩。稀疏内存索引中的每个条目都指向压缩块的开始处。除了节省硬盘空间之外,压缩还可以减少对 I/O 带宽的使用。 那么,可以构建 SSTable 了,可以让存储引擎这样工作: 有新写入时,将其添加到内存中的平衡树数据结构(例如红黑树),这个内存树有时被称为内存表(memtable)。 当内存表大于某个阈值(通常为几兆字节)时,将其作为 SSTable 文件写入硬盘。这可以高效地完成,因为树已经维护了按键排序的键值对。新的 SSTable 文件将成为数据库中最新的段。当该 SSTable 被写入硬盘时,新的写入可以在一个新的内存表实例上继续进行。 收到读取请求时,首先尝试在内存表中找到对应的键,如果没有就在最近的硬盘段中寻找,如果还没有就在下一个较旧的段中继续寻找,以此类推。 后台进程周期性地执行合并和压缩过程,以合并段文件,并将已覆盖或已删除的值丢弃。 这样的索引结构被称为 LSM-Tree(Log-Structured Merge-Tree)。 当查找数据库中不存在的键时,LSM 树算法可能会很慢。为了优化这种访问,存储引擎通常会使用布隆过滤器。还可以使用大小分级和分层压缩。对于大小分级,较新和较小的 SSTables 相继被合并到较旧的和较大的 SSTable 中。对于分层压缩,键的范围被拆分到多个较小的 SSTables,而较旧的数据被移动到单独的层级,这使得压缩能够逐步进行并且使用较少的硬盘空间。 B-Tree 前面看到的日志结构索引将数据库分解为可变大小的段,通常是几兆字节或更大的大小,并且总是按顺序写入段。相比之下,B 树将数据库分解成固定大小的块或分页,传统上大小为 4KB(有时会更大),并且一次只能读取或写入一个页面。这种设计更接近于底层硬件,因为硬盘空间也是按固定大小的块来组织的。 在 B 树的底层中,写操作是用新数据覆写硬盘上的页面,并假定覆写不改变页面的位置。即当页面被覆写时,对该页面的所有引用保持完整。这与日志结构索引(如 LSM 树)形成鲜明对比,后者只追加到文件(并最终删除过时的文件),但从不修改文件中已有的内容。...

February 7, 2025 · 585 words · Kurong