几道反复做了好几遍的一维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)] } 经典打家劫舍,难度同样属于白给。...
回溯算法-一次搞懂
回溯算法本质上是一种深度优先搜索(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....
两道常考图算法题-每次做都不会
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] !...
二叉树的两道超高频考题
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....
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) 是解决此问题的完美组合。...
反转链表专项学习记录
变量名称定义 在开始反转链表套路学习前,先定义一组变量,这组变量会贯穿全部的题型: 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 步:...
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 地址或预设的标签来圈定灰度范围。待验证无误后,再进行全量发布。这极大地提高了配置发布的安全性。...
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: 什么是单元测试?为什么需要它? 想象一下你正在组装一台复杂的机器,比如一台汽车。单元测试就像是分别检查每一个零件(螺丝、齿轮、活塞)是否合格。只有确保每个最小零件都工作正常,你才能对最终组装好的汽车有信心。...
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,从而保证了这部分消息的顺序性。...
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....
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....
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 连接正式建立成功,可以开始双向传输数据。 三次握手的核心目标 确保客户端和服务器双方都确认了自己的发送能力和对方的接收能力都正常。...
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....
搞懂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)。...
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....
常见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....

《基于大型语言模型的检索增强生成综述》笔记
基于大型语言模型的检索增强生成综述 进阶 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....
《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 客户端连接可能会因为空闲而被回收。所以需要加入重试机制。...
《高性能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 。使用复合索引时要注意范围查询限制,若某一列使用范围查询,其右侧的列无法使用索引优化。...
《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 错误,拒绝更新(说明资源已被其他客户端修改过)。 当发生冲突时,客户端需重新获取资源的最新状态,然后基于最新数据重新提交修改。...
《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 生产者 消息发布方式:...
《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+ 树实现的。所以在索引的优化上,可以参照相关关系型数据库的索引优化经验与原则。下面是部分经验:...
《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 缓存:缓存资源的副本。 反向代理:反向代理伪装成原始服务器,不过与服务器不同的是反向代理还可以向其他服务器发送请求,以便实现按需定位所请求的内容。 ……....
《深入理解设计模式》记录
《深入理解设计模式》记录 这里只记录了相对常用、面试常问的设计模式。 单例模式 单例模式(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....
动态规划经典问题学习记录
动态规划经典问题学习记录 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])$...
《深入理解分布式共识算法》记录
《深入理解分布式共识算法》记录 从 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 表示不能提交。 第二阶段,提交/回滚:...
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 (深度优先遍历)。...
《数据密集型系统设计》记录
《数据密集型系统设计》记录 本书的电子版本链接: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 树)形成鲜明对比,后者只追加到文件(并最终删除过时的文件),但从不修改文件中已有的内容。...
《Kubernetes微服务实战》记录
《Kubernetes微服务实战》记录 面向开发人员的 Kubernetes 简介 部分核心概念 Node:Node 是 Kubernetes 集群中的一个工作单元,表示集群中的一台物理机或虚拟机。Node 的作用是为运行容器化应用提供计算资源,一个 Node 可以运行多个 Pod。Node 分为: Master Node:负责集群的控制平面,管理所有工作节点。 Worker Node:运行实际的应用程序工作负载,实际上就是 Pod。 Pod:Pod 是 Kubernetes 中最小的可部署单位,表示一个或多个容器的集合。有以下特性: Pod 内的容器共享一个 IP 地址,可以通过 localhost 相互通信。 Pod 内的容器可以共享挂载的存储卷(Volume)。 Pod 通常包含运行同一应用程序组件的容器,如主应用程序容器、日志采集容器等。 Namespace:用于逻辑上隔离资源。可以在同一集群中运行多个项目或团队的工作负载。 Service:用于将一组 Pod 的访问暴露给外部或集群内部的其他 Pod。 Deployment:用于声明和管理 Pod 的期望状态,例如副本数、滚动更新等。 Volume:提供持久化存储,允许 Pod 重启后数据仍然保留。 Kubernetes 架构 每个集群都有一个控制平面和数据平面,数据平面由多个节点组成,控制平面将在这些节点上部署并运行 Pod(容器组),监控变更并做出响应。 控制平面包含以下组件: API 服务器。 etcd 存储。 调度器:kube 调度器负责将 Pod 调度到工作节点。 控制器管理器:kube 控制器管理器是包含多个控制器的单个进程,这些控制器监控着集群事件和对集群的更改做出响应。 节点控制器:负责在节点出现故障时进行通知和响应。 副本控制器:确保每个副本集(replica set)或副本控制器对象中有正确数量的 Pod 。 端点控制器:为每个服务分配一个列出该服务的 Pod 的端点对象。 服务账户和令牌控制器:使用默认服务账户和响应的 API 访问令牌对新的命名空间进行初始化。 数据平面是集群中将容器化工作负载转为 Pod 运行的节点的集合。...
minikube:Ubuntu部署本地集群踩坑
minikube:Ubuntu 部署本地集群踩坑 官方安装教程 minikube start | minikube 再次感叹 homebrew 的伟大,一行命令就安装、后续也没有问题。 仪表盘 使用 minikube dashboard 启动仪表盘。如果在服务器上启动、本地访问,就运行类似于的下面命令: 1 kubectl proxy --address='0.0.0.0' --disable-filter=true 然后把 http://127.0.0.1:46749/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ 这样的 URL 改为 http://ServerIP:8001/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ ,就可以外网直接访问了(注意服务器开端口)。 Helm 安装 直接使用 snap 安装,方便快捷: 1 sudo snap install helm --classic kubectl 安装 snap 上大分: 1 sudo snap install kubectl --classic 安装可能遇到的问题的解决方案 The “docker” driver should not be used with root privileges 使用命令 minikube start 启动,参数 driver 默认为 docker 。如果以 root 用户运行该命令会出现如标题所示的错误,下面是解决方法: minikube - Why The “docker” driver should not be used with root privileges - Stack Overflow...