AI概念 一图流

RAG graph LR subgraph "Query Process 💬 问答流程" direction TB Q1((用户问题)) --> Q2{{"Embedding模型🧮<br>向量化"}} Q2 --> Q3{"检索Retrieval🔍<br>计算相似度(topK)"} Q3 -.查询.-> VectorDB VectorDB -.topk个结果.-> Q3 Q3 --> Q4[["相关片段📄"]] Q4 -.注入上下文.-> Q5("增强/Augmented🔀<br>Prompt=问题+相关片段") Q1 -.原始问题.-> Q5 Q5 -.输入.-> Q6{{"大语言模型🤖"}} Q6 --> Q7[["最终回答💬"]] end subgraph "Index Process 🔧 离线数据准备" direction TB KB["(私有知识库📚<br>PDF/Word/Wiki)"] --> Chunking("切片/Chunking🔪<br>长文本切分成小段") Chunking --> Embedding{{"Embedding模型🧮<br>向量化"}} Embedding --> VectorDB["(向量数据库🗄️<br> VectorDB ..)"] end style KB fill:#e1f5ff,stroke:#007acc,stroke-width:2px style VectorDB fill:#e1f5ff,stroke:#007acc,stroke-width:2px style Q1 fill:#fff2cc,stroke:#d6b656,stroke-width:2px style Q7 fill:#d5e8d4,stroke:#82b366,stroke-width:2px style Embedding fill:#f0e1ff,stroke:#9673a6,stroke-width:2px style Q6 fill:#f0e1ff,stroke:#9673a6,stroke-width:2px style Q4 fill:#ffe6cc,stroke:#d79b00,stroke-width:2px MCP sequenceDiagram User->>MCP Client/IDE/Agent:User Query(今天天气怎么样?) MCP Client/IDE/Agent->>MCP Server:连接 MCP Server & Get Tools MCP Server->>MCP Client/IDE/Agent:Tools List(文件工具、天气工具...) MCP Client/IDE/Agent->>LLM:User Query(今天天气怎么样?) + Tools List(这些工具可以用) LLM->>MCP Client/IDE/Agent: 建议调用的Tool + 参数(建议使用天气工具 参数:date:'20260127',city:'武汉') MCP Client/IDE/Agent->>MCP Server:使用参数调用Tool(天气工具(date, city)) MCP Server->>MCP Server:内部触发逻辑 MCP Server->>MCP Client/IDE/Agent:返回值/Error(temp:30°, weather:sunny) MCP Client/IDE/Agent->>LLM: Tool的返回值(temp:30°, weather:sunny) note over LLM: 思考、推理 LLM->>MCP Client/IDE/Agent:总结归纳后的自然语言(明日(20260127)天气为晴天,温度30°,建议穿凉爽的衣服) MCP Client/IDE/Agent->>User: 格式化结果 Agent React graph RL Input((Task/Input<br>任务输入)) --> Thought["Thought<br>思考: 分析任务,决定步骤"] subgraph ReAct_Loop ["ReAct Loop 循环"] direction TB Thought --> Action{Action<br>行动: 选择工具/动作} Action -- 调用工具 --> Execute["Environment/Tool<br>执行环境/工具"] Execute --> Observation["Observation<br>观察: 获取执行结果"] Observation --> Thought end Action -- 任务完成 --> Final["Final Answer<br>最终回答"] style Input fill:#fff2cc,stroke:#d6b656,stroke-width:2px style Final fill:#d5e8d4,stroke:#82b366,stroke-width:2px style Thought fill:#e1f5ff,stroke:#007acc,stroke-width:2px style Action fill:#f0e1ff,stroke:#9673a6,stroke-width:2px style Observation fill:#ffe6cc,stroke:#d79b00,stroke-width:2px

2025/07/02 · Aris

Kafka Rebalance

什么是Rebalance 为了提升消费效率,kafka会将Topic消息放到多个Partition,消费者Consumer可以以消费组(Consumer Group)的形式进行并行消费 在这个背景下,每个消费者节点负责一个或多个Partition。为了保证负载均衡,多个Partition能均匀分配给消费节点,Kafka需要会根据节点负载进行Rebalance以及维护这个映射 为了提升单个Topic的并发性能,将单个Topic拆分为多个Partition 为了提升横向扩展能力,将多个Partition分布在多个不同的Broker 为了提升可用性,为每个Partition加了多个副本 为了协调和管理kafka集群的数据消息,引入Zookeeper作为协调节点 Reblance过程不是瞬间完成的,而是要经历:注册旧分区👉选举Leader👉分配新分区👉初始化消费者 这一完整过程。在这个期间,所有消费者会停止消费 什么时候会触发Rebalance [最常见] 消费者数量发生变化(服务扩缩容的时候、某个节点出问题了重复重启) 调整了Topic的分区数量(kafka只支持增加),新增的分区需要分配消费者 相同消费组下订阅的Topic列表发生了变化 [隐藏的坑] 消费者心跳超过了配置的session.timeout.ms(通常是45s)没有发,kafka会把这个消费者判定为死亡,从而踢出 [隐藏的坑] 消费超时:通常单批消息超过max.poll.interval.ms(通常是5分钟)没有处理完(没有给ack),kafka会把这个消费者踢出 Rebalance期间会引发哪些问题 上述提到,Reblance过程不是瞬间完成的,且这个期间,所有消费者会停止消费,所以会产生消息积压 所以产生积压的时候,优先看下是否发生了Rebalance 消息重复和丢失(见下方) Rebalance 要靠Coordinator协调,频繁触发会占用Kafka集群的CPU和网络资源 而且Kafka默认的分区分配策略(Range 或 RoundRobin),很容易导致负载不均 比如5个分区分配给2个消费者,可能出现 3个分区 vs 2个分区 的情况,其中一个消费者压力翻倍,处理速度变慢,又会触发新的Rebalance,陷入恶性循环 什么时候会丢消息 通常情况下,Reblance不会直接导致丢消息。消费者的处理和offset提交顺序没处理好,就容易出现丢消息的现象 消费者先提交offset(比如100),然后处理消息 开启了自动提交offset(比如100),也是没处理完就提交offset了 上述两种情况,当消费者在处理过程中断了(宕机、OOM、被kill等),导致Kafka触发Rebalance,其它节点接管这个Partition的消费,会从101开始消费 sequenceDiagram participant A as Consumer A participant K as Kafka Broker participant B as Consumer B Note over A,K: 场景:自动提交 (Auto Commit) A->>K: Poll 消息 (Offset 100-200) activate A Note right of A: 放入本地队列/线程池 Note over A,K: 触发自动提交 (5秒到了) A-->>K: 自动提交 Offset = 200 Note right of K: 记录消费进度 = 200 A->>A: 正在处理 Offset 150... Note right of A: 💥 节点突然宕机 (150-199 未处理完) deactivate A Note over K: 心跳丢失 -> Rebalance K->>B: 分配分区给 B B->>K: 获取已提交 Offset K-->>B: 返回 Offset = 200 B->>K: Poll 下一批消息 (201+) Note right of B: 😱 Offset 150-199 永久丢失 正确的做法应该是先处理消息→再提交 offset,确保消息处理完才更新进度。 ...

2025/05/17 · Aris

Kafka vs RocketMQ: 架构、功能、性能

叠个甲:下面的内容来自于小白Debug的三期视频。从到尾看了一遍而整理成的文字稿,并加入了mermaid绘图以及补充了一些内容。下方都已经标明了出处 Kafka架构 内容来源:消息队列Kafka是什么?架构是怎么样的?5分钟快速入门 什么是消息队列 假设你维护了两个服务A和B。B服务每秒只能处理100个消息,但A服务却每秒发出200个消息,有没有办法让B在不被压垮的同时,还能处理掉A的消息? 为了保护B服务,很容易想到在B服务的内存中加入一个队列(说白了其实是个链表)。链表的每个节点就是一个消息,每个节点有一个序号,我们叫它Offset,记录消息的位置。B服务依据自己的处理能力,消费列表里的消息,能处理多少是多少,不断更新已处理消息值。 来不及处理的消息会堆积在内存里,如果B服务更新重启,这些消息就都丢了。这个好解决,将队列挪出来,变成一个单独的进程,就算B服务重启,也不会影响到了队列里的消息。这样一个简陋的队列进程,其实就是所谓的消息队列。 A服务这样负责发数据到消息队列的角色,就是生产者Producer 像B服务这样处理消息的角色就是消费者Consumer。 但这个消息队列属实过于简陋,要对其优化,使其:高性能、高扩展性、高可用 高性能 消息队列里会不断堆积数据,为了提升性能,可以扩展更多的消费者提升消费速度。生产者也可以相对得增加更多,提升了消息队列的吞吐量。 随着生产者和消费者都变多,他们会同时争抢同一个消息队列,抢不到的一方就得等待,这个如何解决? 消息分类(垂直扩展):首先是对消息进行分类,一类是一个Topic。然后根据Topic新增队列的数量,生产者将数据按投递到不同的队列中,消费者则根据需要订阅不同的Topic。这样就大大降低了Topic队列的压力。 分区拆分(水平扩展):但单个Topic的消息可能还是过多,我们可以将单个队列拆成好几段,每段就是一个分区(Partition),每个消费者负责一个Partition,这样就大大降低了争抢,提升了消息队列的性能。 graph LR P[Producer] P -->|Write| T1[TopicUser] P -->|Write| T2[TopicAudit] P -->|Write| T3[...] subgraph Topic Part1[Partition 1] Part2[Partition 2] Part3[Partition 3] Part4[Partition 4] end subgraph ConsumerGroup C1[Consumer 1] C2[Consumer 2] end T1-.-Part1 T1-.-Part2 T1-.-Part3 T1-.-Part4 Part1 -->|Read| C1 Part2 -->|Read| C1 Part3 -->|Read| C2 Part4 -->|Read| C2 高扩展性 随着Partition变多,若Partition都在同一台机器上的话,就会导致单机负载过高,影响整体系统性能。 于是可以申请更多的机器,将Partition分散部署在多台机器上,这每一台机器就代表一个Broker。通过增加Broker,缓解机器负载过高带来的性能问题。 graph TD subgraph Cluster subgraph Broker3 [Broker 3] P1_3["TopicUser-Part3"] P2_1["TopicAudit-Part1"] end subgraph Broker2 [Broker 2] P1_2["TopicUser-Part2"] P2_3["TopicAudit-Part3"] end subgraph Broker1 [Broker 1] P1_1["TopicUser-Part1"] P2_2["TopicAudit-Part2"] end end T1[TopicUser] -.-> P1_1 T1 -.-> P1_2 T1 -.-> P1_3 T2[TopicAudit] -.-> P2_1 T2 -.-> P2_2 T2 -.-> P2_3 classDef userTopic fill:#e1f5fe,stroke:#01579b,stroke-width:2px; classDef userPart fill:#e1f5fe,stroke-width:0px; classDef auditTopic fill:#fff3e0,stroke:#e65100,stroke-width:2px; classDef auditPart fill:#fff3e0,stroke-width:0px; class T1 userTopic; class P1_1,P1_2,P1_3 userPart; class T2 auditTopic; class P2_1,P2_2,P2_3 auditPart; 高可用 如果其中一个Partition所在的Broker挂了,那Broker所有Partition的消息都没了,如何优化? ...

2025/03/15 · Aris

MySQL中那些诡异的特性

先看一个现象 1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE `type_test` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `num` bigint(20) unsigned NOT NULL, `str` varchar(1024) NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `idx_num` (`num`) USING BTREE, KEY `idx_str` (`str`) USING BTREE ) DEFAULT CHARSET=utf8; EXPLAIN SELECT * FROM type_test where str=1; EXPLAIN SELECT * FROM type_test where num='13'; 你认为上述两个查询(输入的类型和实际类型不一致),是否都会命中索引呢? ...

2024/12/14 · Aris

Elasticsearch一图流

Lucene 核心数据结构 (单机视角) Lucene 是如何通过倒排索引实现快速搜索,以及如何解决排序和存储问题的 graph TD subgraph Lucene_Segment ["Lucene Segment (最小索引单元/不可变)"] direction TB subgraph Inverted_Index ["倒排索引 (Inverted Index) - 用于搜索"] TI["Term Index (内存)"] TD["Term Dictionary (磁盘)"] PL["Posting List (磁盘)"] TI --"前缀索引/加速定位"--> TD TD --"关联文档ID"--> PL note1["> 举例: 查找 '小白'<br/>1. Term Index 定位大概位置<br/>2. Dictionary 找到 '小白'<br/>3. Posting List 得到 ID: [0, 1]"] style TI fill:#e1f5fe,stroke:#01579b style TD fill:#fff9c4,stroke:#fbc02d style PL fill:#fff9c4,stroke:#fbc02d end subgraph Storage ["数据存储"] SF["Stored Fields (行式存储)"] DV["Doc Values (列式存储)"] SF --"根据ID获取完整内容"--> Content["文档原始JSON"] DV --"用于排序/聚合"--> Sort["排序/聚合操作"] note2["> Stored Fields: 存完整数据<br/>> Doc Values: 空间换时间,优化排序"] style SF fill:#e8f5e9,stroke:#2e7d32 style DV fill:#f3e5f5,stroke:#7b1fa2 end end Query["搜索请求"] --> TI PL -.->|"得到文档ID"| Storage Elasticsearch 在Lucene基础上的分布式架构 架构图 graph Client["客户端应用"] --> Coord subgraph Cluster["Elasticsearch Cluster (集群)"] subgraph CoordLayer["协调节点层 (请求入口/分发/聚合)"] Coord["协调节点 (Coordinating Node)"] end subgraph MasterLayer["Master 节点层 (集群管理/选主/元数据)"] Master1["Master Node A"] Master2["Master Node B"] Master3["Master Node C"] end subgraph DataLayer["Data 节点层 (存储/搜索/写入)"] Data1["Data Node 1"] Data2["Data Node 2"] Data3["Data Node 3"] end subgraph IndexLayer["Index: news_index (逻辑索引)"] subgraph Shard0["Shard 0"] P0["Primary Shard 0"] R0["Replica Shard 0"] end subgraph Shard1["Shard 1"] P1["Primary Shard 1"] R1["Replica Shard 1"] end end subgraph LuceneLayer["Lucene 层 (每个分片内的底层引擎)"] subgraph SegA["Segment A (不可变)"] IIA["倒排索引"] SFA["Stored Fields"] DVA["Doc Values"] end subgraph SegB["Segment B (不可变)"] IIB["倒排索引"] SFB["Stored Fields"] DVB["Doc Values"] end end end %% === 核心链路简化 === %% 1. 请求路由: Coord -> Data Node (物理流向: 先找到持有分片的节点) Coord --"路由写入 / 分发查询"--> Data1 %% 2. 数据处理: Data Node -> Shard (节点调用内部主要分片) Data3 -- "执行请求" --> P0 %% 3. 数据同步: Primary -> Replica (跨节点同步) P1 -.-> R1 P0 -.-> R0 %% 4. 其它关系示意 Master3 -.-|"集群管理"| Data3 R0 -.-|"底层引擎"| SegA %% === 样式定义 === style Cluster fill:#f5f5f5,stroke:#333,stroke-width:2px style CoordLayer fill:#e3f2fd,stroke:#1565c0,stroke-dasharray: 5 5 style MasterLayer fill:#f3e5f5,stroke:#7b1fa2,stroke-dasharray: 5 5 style DataLayer fill:#e8f5e9,stroke:#2e7d32,stroke-dasharray: 5 5 style IndexLayer fill:#fff9c4,stroke:#fbc02d,stroke-dasharray: 5 5 style LuceneLayer fill:#fff3e0,stroke:#e65100 style SegA fill:#ffffff,stroke:#333 style SegB fill:#ffffff,stroke:#333 classDiagram class Cluster { +String cluster_name +List nodes } class Node { +String node_name +List roles +store_data() +coordinate_request() } class NodeRoles { Master Data Coordinating } note for NodeRoles "枚举类型 (Enumeration)<br/>Master: 集群管理<br/>Data: 数据存储<br/>Coordinating: 请求转发" class Index { +String index_name +List shards } class Shard { +int shard_id +LuceneInstance underlying_engine } class PrimaryShard { +sync_to_replica() } class ReplicaShard { +promote_to_primary() } class LuceneInstance { +List segments +merge_segments() } class Segment { +InvertedIndex +StoredFields +DocValues +is_immutable } Cluster *-- Node Node ..> NodeRoles : 扮演角色 Index *-- Shard Shard <|-- PrimaryShard Shard <|-- ReplicaShard Shard *-- LuceneInstance : 底层引擎 LuceneInstance "1" *-- "*" Segment : 包含多个 Node "1" o-- "*" Shard : 承载 note for PrimaryShard "负责写入,同步数据给副本" note for ReplicaShard "提供读能力,主分片挂掉后升级" note for Node "单节点可扩展为多节点集群<br/>通过 Raft 类机制选主" note for LuceneInstance "写入产生新Segment<br/>后台定期合并小Segment<br/>以优化文件句柄和查询性能" 从 Lucene 到 Elasticsearch 的演进 Lucene 作为一个单纯的搜索库,虽然功能强大,但在面对海量数据和高并发场景时存在明显的单机局限性: ...

2024/11/06 · Aris

对第一性原理的思考

底层公理:第一性原理的本质 第一性原理源于亚里士多德的定义:“在每一系统的探索中,存在第一原理,是一个最基本的命题或假设,不能被省略或删除,也不能被违反”。 其核心方法论是: 拆解:将复杂问题剥离所有经验、类比、路径依赖,回归到不可证伪的底层事实/公理(比如物理定律、数学公理、客观存在的需求本质); 重构:基于底层公理,向上推导解决方案,而非通过“模仿-优化”的经验路径。 几个故事 决定火箭尺寸的马屁股 美国航天飞机的推进器宽度是4英尺8.5英寸,为什么是这个奇怪的数字?设计师也很无奈,因为这些推进器要通过火车从犹他州的工厂运到发射台,而铁轨的宽度就是4英尺8.5英寸。 铁轨为什么是这个宽度?因为早期的美国铁路是英国人修的,他们照搬了英国的标准。 英国的标准又是哪来的?是因为第一批造铁路的人以前是造电车的,而电车的轮距就是这个标准。 电车为什么定这个标准?因为最开始造电车的人是造马车的,他们沿用了马车的轮距。 如果你把马车的轮距改在英国的老路上跑,就会断车轴,因为英国老路上的车辙宽度就是这个数。 这些老路是谁修的?是古罗马人。古罗马的战车宽度决定了路面车辙的宽度。 古罗马战车的宽度又是怎么定的?它是刚好能容纳两匹战马屁股的宽度。 你说着魔幻不魔幻,人类最顶尖的航天科技,决定其尺寸的参数竟然是2000年前两匹马的屁股。这就叫路径依赖。 拉杆箱诞生 另外一个更贴近生活的例子:现在出门旅行都用带轮子的拉杆箱,这玩意儿简直是人类之光。 但你们知道吗?人类发明轮子已经5000年了,把轮子装到箱子上这个动作却是在1970年才由伯纳德·萨多完成的。 在这之前,人类即使已经登上了月球,造出了核弹,却依然像个傻子一样拎着死沉死沉的箱子在机场累得半死。 为什么当时所有的箱子制造商都在拼命优化:用更轻的皮革、更坚固的手柄、更大的空间? 他们的思维模型是箱子等于用来提的容器,既然是提的,那所有优化都在如何提得更舒服上。这是典型的类比思维。 而伯纳德·萨多的突破在于,他无意中启动了第一性原理:他没把箱子看作提的容器,而是回归到了物理本质,箱子等于重物移动。 如果是重物移动,在这个物理系统中最大的阻力是什么?是摩擦力和重力。 怎么解决摩擦力和重力?那就是轮子,就这么简单。 但当他拿着带轮子的箱子去推销时,所有的百货公司都拒绝了他,理由非常可笑:男人就该提箱子,拉着箱子走太像娘炮了。 这就是阻碍我们运用第一性原理的第二大障碍:社会这一层面的功能固着。 iPhone 与功能性剥离 乔布斯当年做 iPhone 的时候,如果他盯着“手机”这个词,他一定会去研究怎么把键盘做得更好,就像当年的诺基亚和黑莓,因为在当时的概念里,手机等于屏幕加键盘。 但乔布斯干的事是把手机这个概念剥离掉,他看到的是一个便携式智能终端。 如果是智能终端,核心需求是什么?是信息的输入和输出。键盘是最好的输入方式吗?不是,因为键盘永远在那占地方,不管你用不用。 它最好的输入工具是什么?是上帝给我们的十根手指。 所以把键盘扔了,把屏幕做大,让手指直接成为触控笔。 在当时看来这是离经叛道的,但从物理逻辑和人机交互的第一性原理来看,这才是唯一的真理。 第一性原理就是极致减法 第一性原理并不是在做加法,而是在做极致的减法。 它要求你剥离掉历史的惯性、剥离掉社会的标签、剥离掉表面的形式,最后只剩下一个光秃秃的、无法再分割的事实。 在这个事实之上构建出来的东西,往往会产生一种降维打击的效果。 所谓的第一性原理其实就是一把奥卡姆剃刀,他的第一步操作就是极其残忍的杀掉那匹马。这听起来很爽,但做起来极难,因为很多时候那个马屁股已经伪装成了真理。 社会太喜欢给事物贴标签了:手机是用来打电话的,书店是用来卖书的,学校是用来上课的。 一旦标签贴死,思考也就停止了。 所以高手的操作模型通常包含一个极其重要的步骤 功能性剥离:当面对一个难题时,试着忘掉这个东西的名字,别管它叫手机还是汽车,试着去描述它的物理属性和基本功能。 第一性原理的生活指南 第一性原理不是只属于造火箭的工具,它同样适用于日常生活的选择。与其在噪音和教条中随波逐流,不如回到事物本质,重新定义我们真正需要的是什么。 马斯克的艺术学习法 有时候努力学习却收效甚微,不是因为不够勤奋,而是方法错误。把学习当成信息收集,会导致知识碎片化、难以迁移。 第一性原理更强调构建知识体系,先找到“树干”,再长出“树枝”和“树叶”。 理财不是先背各种K线图,学各种基金配置技巧,而是理解什么是资产,什么是负债,什么是现金流,什么是复利? 金融的本质是什么?是价值交换和时间价值。 经济的底层逻辑是什么?是供需关系。 你理解了这些最基本的原理,你再去学具体的投资工具,学K线图、学基金配置,你就会发现这些东西都只是树叶,他们都是从树干长出来的 学英语传统的学习方法是什么?背单词、背语法、背句型。第一性原理的学习方法是什么? 先理解语言的本质,语言的本质是什么?是表达思想和进行交流的工具。 英语的树干是什么?是基本的语法结构,主谓宾时态语态。 当你理解了这个树干,你不需要背所有的单词, 你只需要掌握最高频的2000个单词,就可以覆盖日常交流的90% 做菜的是背各种菜谱吗? 树干不是菜谱,而是理解烹饪方式对食材的改变,调味的基本逻辑 做菜的底层就是物理化学反应 为什么孜然、羊肉和洋葱是绝配? 洋葱的硫化物能去除羊肉膻味 在高温下蛋白质发生美拉德反应,配合孜然能放出挥发性香气 为什么大厨总在起锅前沿锅边淋入料酒和醋,并特别讲究烹饪火候的? 料酒中的乙醇 + 醋中的乙酸 + 高温 = 乙酸乙酯(一种有果香的挥发性物质) 起锅前的高温,既提供了反应所需的能量,又利用瞬间的高热气流带出香气。放早了或火不够,就只剩下酸味和酒味 理解树干之后,树叶可以推导出来,学习从记忆负担转为结构性理解,当你构建了一棵知识树,新的知识就有地方挂靠了 ...

2024/10/13 · Aris

WebSocket与SSE

WebSocket WebSocket连接建立过程 sequenceDiagram note over Client,Server: TCP三次握手(传输层) Client ->> Server: SYN Server ->> Client: SYNC+ACK Client ->> Server: ACK note over Client,Server: HTTP请求 转换为WebSocket协议(应用层) Client ->> Server: HTTP GET请求<br>GET HTTP1.1<br>Connection:Upgrade<br>Upgrade:websocket<br>Sec-Websocket-Key: 字符串.base64() note over Server: Sec-Websocket-Accept=fn(字符串) Server ->> Client: HTTP 101响应<br>HTTP1.1 101 Switching Protocals<br>Sec-Websocket-Accept:base64码<br>Upgrade:websocket<br>Connection:Upgrade note over Client: check(fn(base64(字符串))=Sec-Websocket-Accept) note over Client,Server: Websocket连接成功 基于TCP长连接<br>(HTTP通常返回消息后就断开了 但是ws没断开) Server ->> Client: Websocket数据帧 Server ->> Client: Websocket数据帧 Client ->> Server: Websocket数据帧 Client --> Server: ...... SSE llm chatbot 挨个吐字的过程展示 ...

2024/07/05 · Aris

秒杀系统

sequenceDiagram participant User as 用户 participant Frontend as 前端/App participant Redis as Redis缓存 participant MQ as 消息队列 participant OrderService as 订单服务 participant DB as 数据库 User->>Frontend: 点击"立即抢购" Frontend->>Frontend: 校验(未开始/验证码) Frontend->>Redis: 请求扣减库存 (Lua脚本) alt 库存不足 或 用户已买 Redis-->>Frontend: 返回失败 (秒杀结束) Frontend-->>User: 提示"手慢了" else 库存充足 Redis->>Redis: 预扣库存 (decr) Redis-->>Frontend: 返回成功 (排队中) Frontend->>MQ: 发送"创建订单"消息 Frontend-->>User: 提示"正在为您排队..." loop 轮询结果 Frontend->>Backend: 查询订单结果 end end MQ->>OrderService: 消费消息 OrderService->>DB: 1. 插入订单 (唯一索引防重) OrderService->>DB: 2. 扣减真实库存 alt 数据库事务成功 OrderService-->>Redis: 标记订单创建成功 else 异常/重复 OrderService-->>Redis: 回滚Redis库存 end 秒杀系统的核心挑战 秒杀系统的特点是时间短、并发高、资源少,核心要解决的问题是: ...

2023/10/29 · Aris

go-micro之上的微服务体系建设

前言 毕设啊毕设,终于落下帷幕。 最后竟然还水了个校优秀毕业设计/计算机系第三名,搞回来了一波论文的打印费😄。 论文选题有点类似增删改查的项目:课堂直播系统。后面为了不让它不那么 CURD 、泯然众人,另一方面是为例论文能凑出字数,自己在前面加了个“基于微服务架构的XXX”。这样一来逼格瞬间就上去了嘛! 实习的过程,吸收了一波微服务开发思想。公司架构组开发好的微服务框架,我们在这个之上写业务。对这种高自动化、标准的开发模式渐渐有了自己的一点理解。 所以这次的课程设计就是自己使用开源框架,搭建一个微服务平台,再到这个之上进行业务开发。 目标 基于 go-micro 微服务框架,开发一个高可用、balabala(此处省略)的微服务架构,解决微服务架构存在的一些问题,最后使用这个框架开发课堂直播系统。 输出 valyria - 微服务脚手架、毕业论文一篇、直播系统一个 组件介绍 微服务框架:go-micro、micro Web路由:gin 服务治理:consul 链路追踪:Jaeger+OpenTracing API文档:Swagger CI/CD:DaoCloud 日志:uber/zap-log 集群管理:Docker 调用协议:gRPC … 想起来再补充 PPT

2020/06/13 · Aris

对Golang项目进行CI/CD化和容器化 (DaoCloud大法好!)

持续集成 Continuous Integration(CI)和持续交付 Continuous Delivery(CD),在当前 DevOps 的趋势下,具有支柱性地位。 软件交付管道以快速、自动化和可重复的方式从源代码生成发布版本,就类似于工厂里的装配线以快速、自动化、可重复的方式从原材料生产出消费品,完成这项工作的总体设计我们就称之为持续交付,启动装配线的过程我们称之为持续集成 CI/CD的好处:提升开发效率,加速开发周期,及时发现开发过程中的错误,简化部署和运维过程。 为什么选择 DaoCloud:一条🐲服务 本文基于 DaoCloud, 描述如何将自己的Golang项目进行 容器化 和 CI/CD 化。 注册DaoCloud https://www.daocloud.io/ 登陆 — 选择github登陆—授予权限—账户名和密码 定义项目(服务) 这里使用 gin 生成一个hello world 服务,提供一个/ping接口,绑定在8082端口 /hello_dao_cloud/main.go: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package main import "github.com/gin-gonic/gin" func HelloHandler(c *gin.Context) { c.JSON(200, gin.H{ "msg": "hello dao cloud", }) } func main() { r := gin.Default() r.GET("/ping", HelloHandler) r.Run(":8082") } 使用 go.mod(推荐) 1 2 3 4 5 6 // go.mod module hello_dao_cloud go 1.14 require github.com/gin-gonic/gin v1.6.3 本地运行 1 2 3 4 $ go run main.go $ curl http://localhost:8082/ping {"msg":"hello dao cloud"} # 也可以直接到浏览器访问 创建仓库 到github(注册时绑定的那个)创建一个 repo,作为代码仓库。项目代码 push 到这个仓库,CI 工具会从这里面拉取再执行启动脚本。 ...

2020/05/06 · Aris

GC 和 Golang 的垃圾回收机制

思维导图 GC的概念 GC全称是 garbage collection,即垃圾回收。 新开发的语言(java,python,php等等),在使用的时候,可以使语言用户不必关心内存对象的释放,只需要关心对象的申请即可 Golang本身就是一门基于 garbage collection 的语言,垃圾回收由语言本身机制实现。 gc与程序交互时候的效率会影响到整个程序的运行效率。通常程序本身的内存管理会影响gc和程序之间的效率,甚至造成性能瓶颈。 常见的GC模式 引用计数 流程 每个对象内部维护一个整数值,叫做这个对象的 引用计数 对象被引用的时候 +1,对象不被引用的时候 -1 引用计数 归零的时候,对象会被销毁 缺陷 循环引用问题:对象A和B互相引用,计数器都不为0,那么永远不会被收集 引用计数的赋值会造成性能消耗 Python、PHP、 c++ 标准库的 std::shared_ptr 、微软的 COM 、Objective-C 标记清除 Mark-Sweep 流程 标记:从程序根结底开始,递归遍历所有对象,并打上标记,标记为存活 清除:清除阶段,将所有未被标记的对象当作垃圾销毁 缺陷 最大的缺陷就是所谓的STW(stop the world)。在垃圾回收阶段,所有程序将被挂起暂停,等待恢复完毕后再执行 就如同现在的疫情,湖北省各大城市按下了暂停键,要对病情进行控制,疑似和确诊病例进行清理。按照现在的执行效率,相信很快就能清理完(2020.3.17 周边城市已经解封,49支医疗队陆续撤离湖北) 显而易见的,清除后产生了大量不连续的内存碎片,导致在程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。 何时进行STW? 堆中的有效内存空间(available memory)被耗尽的时候,就会让整个程序stop the world, 然后进行两项工作,第一是标记,第二是清除 为何要STW? GC的过程中,其他线程的代码可能会改变对象状态,可能把不应该回收的对象当做垃圾收集掉 eg: A刚刚被标记完,如果未暂停程序,申请了一个B对象,且A可达B。那么B是未被标记的,后面会被意外地清理掉,引起程序错误。 Golang1.5 之前所使用 2014/6 1.3版本并发清理的引入,go runtime 分离了 mark 和 sweep 的操作 和以前一样,也是先暂停所有任务执行并启动 mark( mark 这部分还是要把原程序停下来的),mark 完成后就马上就重新启动被暂停的任务了,并且让 sweep 任务和普通协程任务一样并行,和其他任务一起执行。 ...

2020/02/26 · Aris

Golang实现LRU

LRU LRU算法即淘汰最近一段时间内没有被访问(命中)的数据,最理想的实现方法为双链表+Hash表。 双链表:新加入/新命中的节点放到表尾,空间用完需要淘汰的时候,删除表头(最近一段时间没有被命中)。 双链表可以使添加删除的时间复杂度降到O(1) Hash表:访问的时间复杂度降到O(1) 实现 具体流程写在注释内: 使用List包 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 package Cache import ( "container/list" "errors" ) type node struct { key string value interface{} } type LRUCache struct { Cap int // 容量 dList *list.List // 双链表 hashMap map[string]*list.Element } func New(cap int) *LRUCache { return &LRUCache{ Cap: cap, dList: list.New(), hashMap: make(map[string]*list.Element, cap), } } func (lc *LRUCache) Volume() int { return lc.dList.Len() } func (lc *LRUCache) Get(k string) (interface{}, error) { if lc.dList == nil { return nil, errors.New("LRU not initialized") } // 命中就更新到list尾 if v, ok := lc.hashMap[k]; ok { lc.dList.MoveToBack(v) return v.Value.(*node).value, nil } return nil, nil } func (lc *LRUCache) Set(k string, v interface{}) error { if lc.dList == nil { return errors.New("LRU not initialized") } // 命中,直接更新 if v, ok := lc.hashMap[k]; ok { lc.dList.MoveToBack(v) lc.hashMap[k].Value.(*node).value = v return nil } // 未命中 // 超出容量需要淘汰表头 if lc.dList.Len() >= lc.Cap { ft := lc.dList.Front() if ft == nil { return nil } // 移除头部 并 从map删除(remove回返回ft的value对象,所以直接取值) delete(lc.hashMap, lc.dList.Remove(ft).(*node).key) } // List尾部插入 并 插入map lc.hashMap[k] = lc.dList.PushBack(&node{key: k, value: v}) return nil } func (lc *LRUCache) Del(k string) error { if lc.dList == nil { return errors.New("LRU not initialized") } if v, ok := lc.hashMap[k]; ok { // 从list移除 并 从map删除 delete(lc.hashMap, lc.dList.Remove(v).(*node).key) return nil } return nil } 手动构造双链表 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 package LRU // 双向链表 type Node struct { Key string // 键和值 Value interface{} pre *Node // 头节点 next *Node // 尾节点 } // LRU缓存 type LRUCache struct { volume int // 空间大小 HashMap map[string]*Node // 哈希表 head *Node // 头节点 end *Node // 尾节点 } func New(volume int) *LRUCache { return &LRUCache{ volume: volume, HashMap: make(map[string]*Node, volume), head: nil, end: nil, } } // 取数据 func (l *LRUCache) Get(key string) interface{} { // 命中:放到表尾,返回值 if v, ok := l.HashMap[key]; ok { l.refreshNode(v) return v.Value } return -1 } func (l *LRUCache) Set(key string, value interface{}) { // 设置命中:放到表尾 if v, ok := l.HashMap[key]; ok { v.Value = value l.refreshNode(v) return } // 未命中,添加节点 if len(l.HashMap) >= l.volume { // 1. 淘汰表头 k := l.removeNode(l.head) // 2. 删除Hash内节点 delete(l.HashMap, k) } node := &Node{ Key: key, Value: value, } l.addNode(node) l.HashMap[key] = node } // 移除双链表节点,返回键值 func (l *LRUCache) removeNode(node *Node) string { // 尾节点 if node == l.end { l.end = l.end.pre l.end.next = nil return node.Key } // 头节点 if node == l.head { l.head = l.head.next l.head.pre = nil return node.Key } // 中间 node.pre.next = node.next node.next.pre = node.pre return node.Key } func (l *LRUCache) addNode(node *Node) { if l.end != nil { l.end.next = node node.next = nil } l.end = node if l.head == nil { l.head = node } } func (l *LRUCache) refreshNode(node *Node) { if node == l.end { return } l.removeNode(node) l.addNode(node) } 快速提取接口 在接收者结构体内任意位置->右键->Refactor->Extrace Interface... ...

2020/02/19 · Aris

分布式锁

分布式锁 基于数据库 乐观锁(数据更新之前先查状态,起冲突就更新失败) 悲观锁(for update,会占有,不利于并发) 获取锁的前提:结果集中的数据没有使用排他锁或共享锁时,才能获取锁,否则将会阻塞。 需要注意的是, FOR UPDATE 生效需要同时满足两个条件时才生效: 数据库的引擎为 innoDB 操作位于事务块中(BEGIN/COMMIT) 实现: 表加唯一索引 加锁:insert语句,报错证明加锁失败 解锁:delete 实现简单 数据库开销大、性能不高 没有自动超时机制,有死锁风险 基于Redis缓存 加锁:SET k v EX 300 NX 解锁:DELETE 可能会删除不是这个线程拥有的锁 解决:验证value(写value的时候也得保证唯一) 如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况: 在Redis的master节点上拿到了锁; 但是这个加锁的key还没有同步到slave节点; master故障,发生故障转移,slave节点升级为master节点; 导致锁丢失。 基于Zookeper 获得锁和等待 创建一个持久结点 C1获得锁:从持久结点创建临时结点Lock1。C1查找所有临时结点并排序,是最前的 一个就获得锁 C2获取锁,创建临时结点Lock2。查找所有临时结点并排序,发现不是最前,创建watch观察最近的Lock1 C3获取锁,创建临时结点观察最近的Lock2 形成等待队列 删除锁 C1调用删除Lock1 的指令(临时结点在C1断开连接时也会自动删除Lock1) C2因为watch着Lock1,删除后立即得到通知 再次查询所有临时结点,并排序,发现自己是最小的,获得锁 acquire方法用户获取锁,release方法用于释放锁。 性能不高,因为要创建临时结点 对于客户端Client断开的情况,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。

2020/02/09 · Aris

分库分表和平滑扩容

最近用到了分库分表,也见识到了某业务表数已经到了几万张。下面主要是对分库分表平滑扩容 这篇文章的学习和整理 分库分表 为了增加db的并发能力,常见的方案就是对数据进行分库分表 如果所有数据都塞到一个表里面,数据条数达到一定量级的时候,数据查询操作将会是一件很恐怖的事情 这个需要在初期对数据规划有一个预期,从而预先分配出足够的库来处理。 1 2 3 4 5 Service / | \ uid%3= 0 1 2 | | | DB A B C 数据会均衡地分到每个DB里面,查询的时候会根据余数来定位数据所在库或表 扩容及产生的问题 后续业务发展的速度很快,用户量数据大量上升,当前容量不足以支撑,应该怎么办? 需要对数据库进行水平扩容,再增加新库来分解。新库加入之后,原先sharding到3个库的数据,就可以sharding到四个库里面了 1 2 3 4 5 6 — Service — / / \ \ / | | \ uid%4= 0 1 2 3 | | | | DB A B C D 不过此时由于分片规则进行了变化(uid%3 变为uid%4),大部分的数据,无法命中在原有的数据库上了,需要重新分配,大量数据需要迁移。 ...

2020/02/02 · Aris

Redis汇总 pro max

起因 实习也有两个多月了,Redis算做是使用得比较多的技术,各种数据结构在日常开发中也是用了个遍。 不过基本是在运维同学搭好的实例上玩耍、在公司基础库封装好的API上进行一通操作。 高效是高效,香也是很香得很,但Redis本身的很多细节其实是被屏蔽的。其本身的架构设计、底层运作是非常值得学习研究的。 翻阅过好多优秀博客后进行的简单汇总 Redis简介 也就不多说了,引用某百科的一句话: Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。 关键词:C编写、基于内存、可持久化、Key-Value、多语言API 常用的数据结构及场景 字符串String 缓存:比如授权用到的各种Token,一段时间内会过期,又不可频繁获取,临时存储到reids 计数器:通过INCRBY可以简单实现,通过原子递增保持计数 分布式锁:【1】 解决资源竞争造成的一致性问题 分布式环境下的共同资源,给一个key设定值和过期时间,获得锁的消费者有权读写 分布式ID生成器 哈希Hash 就是一个需要临时存储的Map! 比如用户资料投审核, 以 uid为 key, 用户信息映射用fields 和 values 进一一对应存储 投审时直接放入,审核完对相应field进行删除 列表List 双向链表 消息队列:一边往里面push,一边往外面pop 比如 用户下单 和 库存处理 其实是可以分开处理,而且现在基本都放在两个服务里面 订单接收服务:下单完成即可返回给用户状态,同时push到公共队列里 订单处理服务:依次pop进行进行库存操作 方便做分布式和扩容 集合Set 当作临时存储的数组使用,注意的是set可以 自动排重 起初纯把Set当作数组用,某天发现数据少了一些觉得踩坑了! 后面用到了跟高阶的交集、并集、差集等操作,就暗自在内心说出了 卢本伟🐂🍺 两个用户求公共粉丝:分两组Set存,SINTER一键求交集它不香吗~ 有序集合Zset 无重复(成员不允许重复,分数是允许的)、有序的集合(按照Score进行排序) 天然的排序结构,用来做排行榜再合适不过了 BUT 不过在某些领域Redis也不是最适合的: 比如消息队列,优先选择的是 Kafka 以及一些“正儿八经的MQ”,相信很少人去维护List,自己废大力气去搞一致性和幂等,以及 真•持久化 实际上redis并不适合任何有保障数据持久性的场景。它适合做cache,不重要的存储,或者是可以反复重来的批处理计算任务的临时存储等。【2】 底层实现 引用 【1】分布式锁之Redis实现 【2】Redis 怎么做消息队列? - 大宽宽的回答 - 知乎

2020/01/29 · Aris

理解select和epoll

背景 高并发场景常常想到用多线程来接收网络请求 但是多线程会涉及到CPU的上下问切换,会处理一些操作句柄,连接数比较多的时候,会带来极大的开销 多线程并不是最佳的解决方案! 单线程处理 使用单线程的方式处理大量客户端的连接 Linux中一切皆文件,每个网络连接在内核中以文件描述符的形式存在,简称fd 多I/O事件处理 因为一个线程只能处理一个fd,如果想处理多个,可以利用非阻塞轮询的方式实现 1 2 3 4 5 6 7 8 9 for true { // 轮询获取就绪 for _, fd := range []fds { if fd.Data != nil { read(fd) // 读取 deal(buffer) // 处理 } } } 如果所有fds都没有数据要处理,那么用来检测的的CPU时间就被浪费了 为了避免CPU空转,不让线程做流检测工作,而是找一个代理专门来处理检测工作 select 代理可以同时观察许多流的fd事件,如果没有事件,代理就阻塞,线程就不会不间断去轮询了 流程 使用bitmap对fd进行标记,select中交给内核来判断 fd 中是否有数据 没有数据:阻塞于此继续判断 有数据:将bitmap中对应位置位标记 select返回,再遍历fd看哪个被标记,进行处理 1 2 3 4 5 6 7 8 9 10 for true { select(fds) // 阻塞于此,直到有I/O事件才返回 // 轮询获取就绪 for _, fd := range fds { if fd.Data != nil { read(fd) // 读取 deal(buffer) // 处理 } } } 问题 bitmap有上限,为1024 bitmap不可复用,每次轮询需要申请新的 bitmap从用户态到内核态仍然有很大开销 select返回的是“至少有一个fd”,不知道具体是哪个fd,线程还是会O(n)遍历所有流。 poll 1 2 3 4 5 struct poolfd { int fd; short events; // 事件类型,读或者写 short revents; // events的回馈,开始是0 } poll相比于select没有使用bitmap,而是置位poolfd结构体内的 revents 字段(默认为0,poll操作将fd有数据的置1),这样就可以实现复用 ...

2020/01/15 · Aris

Linux常用命令记录

进程、线程查看 1 2 3 4 5 6 top -H # 监控 Linux 系统状况,比如cpu、内存的使用 ps -T # ps命令用来列出系统中当前运行的那些进程 # -b 以批处理模式启动top命令。当你想要在文件中保存输出时是很有用的。 # -n 设置top退出前迭代的次数 # -p 监控特定进程 # -i 空闲进程/非空闲进程切换 查看进程的线程 1 top -H -p PID 各项指标 内存 1 2 3 4 [root@VM_0_7_centos ~]# free total used free shared buff/cache available Mem: 1015564 299588 72512 1792 643464 535316 Swap: 1049596 111104 938492 磁盘使用情况 df 1 2 3 4 5 6 7 8 [root@VM_0_7_centos ~]# df -h 文件系统 容量 已用 可用 已用% 挂载点 /dev/vda1 50G 9.7G 37G 21% / devtmpfs 486M 0 486M 0% /dev tmpfs 496M 1.2M 495M 1% /dev/shm tmpfs 496M 464K 496M 1% /run tmpfs 496M 0 496M 0% /sys/fs/cgroup tmpfs 100M 0 100M 0% /run/user/0 目录使用情况 du 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 [root@VM_0_7_centos ~]# du #查看当前目录 8 ./.cache/abrt 8 ./.cache/pip 20 ./.cache 8 ./.pip 4 ./.config/abrt 8 ./.config 4 ./.pki/nssdb 8 ./.pki 4 ./.ssh 112 ./.acme.sh/deploy 512 ./.acme.sh/dnsapi 816 ./.acme.sh 908 . [root@VM_0_7_centos ~]# du -h # 加上常用单位 8.0K ./.cache/abrt 8.0K ./.cache/pip 20K ./.cache 8.0K ./.pip 4.0K ./.config/abrt 8.0K ./.config 4.0K ./.pki/nssdb 8.0K ./.pki 4.0K ./.ssh 112K ./.acme.sh/deploy 512K ./.acme.sh/dnsapi 816K ./.acme.sh 908K . [root@VM_0_7_centos ~]# du -s # 当前目录 908 . [root@VM_0_7_centos ~]# du -sh 908K . 端口使用情况 1 lsof -i:8080 杀死进程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 kill -9 PID # 查看支持的信号 [root@VM_0_7_centos ~]# kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX -9 即发送了 SIGKILL 信号,这是进程间的通信的一种方式 ...

2020/01/09 · Aris

Goroutine池

问题提出 场景一 维持长链接,为每个长链接分配三个goroutine 一个负责读数据 一个负责写数据 一个负责维持心跳 高并发场景,goroutine的数量会是连接数的三倍! golang号称可以百万级别并发,但goroutine也不应该无限制的创建吧,毕竟每次都向系统申请内存,系统内存总有耗尽的一天。 无休止的开辟Goroutine依然会出现高频率的调度Groutine,那么依然会浪费很多上下文切换的资源,导致做无用功。所以设计一个Goroutine池限制Goroutine的开辟个数在大型并发场景还是必要的。 场景二 数据库查询模块,希望并发控制到40 如果请求少于40个请求那么就执行查询 1个查询完了就释放席位(也就是一共n个任务,1个查完了,那么还能再提交40-n+1个任务) 如果请求过多于40个就阻塞,直到有任务退出腾出席位 goroutine池实现 深入浅出Golang的协程池设计 Golang worker pool 实现

2020/01/07 · Aris

缓存穿透、缓存击穿、缓存雪崩

缓存机制 受制于数据库操作速度(磁盘操作)和连接池数量限制,在一些高并发场景,通常会设计缓存(内存操作)来减轻DB压力,进行流量削峰。 本质上来说就是更多地延缓用户请求,将请求拦截在上游,以及层层过滤用户的访问需求,遵从“最后落地到数据库的请求数要尽量少”的原则。 穿透:利用不存在的key去攻击mysql数据库 雪崩:缓存中的很多key失效,导致数据库负载过重宕机 击穿:在正常的访问情况下,如果缓存失效,mysql自我保护,重启缓存的过程 缓存基本流程 graph LR; start(开始)--> case1{缓存内是否有数据}; do2-->ends(结束); case1--否-->case2{数据库内是否有数据}; case2--是-->do1[更新缓存数据]; do1-->do2[返回正常数据]; case2--否-->do3[返回空数据]; do3-->ends; case1--是-->do2; do1-->ends; 缓存穿透 概念场景 所谓穿透,指的就是流量没有命中缓存直接到达DB时: 正常流程:缓存未读取到->到DB查->查到后回写缓存 穿透流程:缓存未读取到->到DB查->DB也没查到->缓存内也不能有这个值。 这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。 这种情况常见于攻击,或者业务BUG。比如重复使用uid=-1或uid特别大或不存在的数据请求查询。 解决方案 创建uid的时候是有约束规范的,对于不在约束范围内的做拦截处理; 用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤; 访问key未在DB查询到值,也将空值写进缓存 设置较短的缓存过期时间。 缓存击穿 概念场景 热点key,在不停的扛着大并发,大并发集中对这一个点进行访问 在一些特殊的时间点,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。 解决方案 设置热点数据永远不过期。 在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。 缓存雪崩 概念场景 大量的key设置了相同的过期时间,同时/大批到过期时间,查询数据量巨大,引起数据库压力过大甚至down机 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。 解决方案 缓存数据的过期时间设置随机,不会集中在同一时刻失效。 使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。 设置热点数据永远不过期(或通过观察用户行为,合理设置缓存过期时间)。 预热数据:在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。 限流降级:这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。 参考 帮你解读什么是Redis缓存穿透和缓存雪崩(包含解决方案) 缓存穿透、缓存击穿、缓存雪崩概念及解决方案 分布式锁和缓存击穿 缓存穿透、缓存击穿、缓存雪崩区别和解决方案 应对缓存击穿的解决方法

2020/01/02 · Aris

leetcode_二进制合集

371. 两整数之和 题目 不使用运算符 + 和 - ,计算两整数 a 、b 之和。 示例 1: 1 2 输入: a = 1, b = 2 输出: 3 示例 2: 1 2 输入: a = -2, b = 3 输出: 1 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/sum-of-two-integers 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 解答 两个整数a, b; a ^ b是无进位的相加; a&b得到每一位的进位;让无进位相加的结果与进位不断的异或, 直到进位为0; 思路写在代码中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func getSum(a int, b int) int { var sum, carry int // 结果 和 进位 sum = a ^ b // 不进位加法 0101 ^ 0011 = 0110 carry = (a&b) << 1 // 获取进位(全一则一,左移表示进位) 0101&0011 = 0001 // 还有进位没加进去,递归做加法 if carry != 0 { return getSum(sum, carry) } return sum } // 简写 func getSum(a int, b int) int { if b == 0 { return a } return getSum(a^b, (a&b)<<1) } 136. 只出现一次的数字 I 题目 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 ...

2019/12/25 · Aris