码农戏码

新生代农民工的自我修养

0%

一句话概括

RAG是一种将“信息检索”与“文本生成”相结合的人工智能框架。它让大语言模型(LLM)能够访问并引用外部知识库来生成更准确、更可信、且能追溯源头的回答。


核心思想:弥补LLM的固有缺陷

传统的LLM(如GPT-4、Llama等)虽然知识渊博,但存在几个关键问题:

  1. 知识可能过时:其训练数据有截止日期,无法获取最新信息。
  2. 存在“幻觉”:可能生成看似合理但实际错误或编造的内容。
  3. 无法溯源:用户无法验证其回答的依据来源。
  4. 涉及私有/专业数据时无能为力:LLM没有学习过公司内部文档、个人笔记等非公开数据。

RAG就是为了解决这些问题而生的。 它的核心思路是:当LLM需要回答一个问题时,不是让它凭空回忆,而是先帮它去“查资料”。


RAG的工作流程(两步走)

想象一个超级图书管理员(RAG系统)和一位才华横溢的作家(LLM)的合作:

第一步:检索(Retrieval)——“去资料室查书”

  1. 知识库准备:将外部的文档(如公司手册、产品文档、最新新闻、数据库、PDF等)进行切分、处理,并转换成向量形式,存储到专门的“向量数据库”中。
  2. 问题转换:当用户提出一个问题时,系统将这个问题也转换成向量
  3. 相似性搜索:系统在向量数据库中,快速查找与问题向量最相似的文本片段(即与问题最相关的资料)。
  4. 获取参考上下文:系统检索出最相关的几个文本片段,作为“参考依据”。

第二步:增强生成(Augmented Generation)——“参考资料写文章”

  1. 组合提示:系统将用户的原始问题和刚刚检索到的参考上下文,一起组合成一个新的、更丰富的“提示”,交给LLM。例如:

    “请根据以下信息回答问题:
    [此处插入检索到的相关文本片段]
    问题是:[用户的原始问题]”

  2. 生成答案:LLM基于这个包含了明确依据的提示来生成答案。由于答案被“限制”在提供的上下文中,因此它更可能准确、相关,并且能够直接引用来源。
  3. 返回答案与引用:最终,系统将生成的答案返回给用户,并可以附上所使用的文本片段的来源(如文档名称和页码),实现可追溯。

RAG的核心优势

  1. 准确性更高,减少幻觉:答案基于提供的真实文档,编造可能性大大降低。
  2. 信息可更新:要更新模型的知识,只需更新向量数据库中的文档即可,无需耗费巨资重新训练整个LLM。
  3. 可追溯与可信:可以提供答案的来源引用,让用户自行查证,建立信任。
  4. 成本效益高:相对于为特定领域从头训练一个大模型,搭建RAG系统的成本和门槛要低得多。
  5. 处理私有数据:可以轻松地将企业内部数据、个人数据作为知识库,构建专属的智能问答系统。

典型应用场景

  • 智能客服/客服机器人:基于最新的产品文档和FAQ,回答用户问题。
  • 企业知识库问答:员工可以快速查询公司内部的规章制度、项目报告、会议纪要等。
  • 学术与研究助手:基于大量论文和研究报告,提供总结和问答。
  • 个人AI助理:基于你的个人笔记、邮件、聊天记录,帮你回忆和查找信息。
  • 需要最新信息的场景:例如,基于最新的市场报告、新闻来生成分析总结。

RAG vs. 微调(Fine-Tuning)

两者都是让大模型适应特定任务的重要技术,但思路不同:

  • RAG:侧重于为模型提供外部知识。像给模型一本随时可以查阅的参考书。
  • 微调:侧重于调整模型内部的参数和权重,改变其“思维方式”或“写作风格”。像是对模型本身进行再训练。
  • 在实践中,RAG和微调经常结合使用,以达到最佳效果:用微调让模型更懂某个领域的“语言”,用RAG为其提供该领域的最新“资料”。

总结

RAG本质上是一个“即插即用”的知识扩展方案。它巧妙地将强大的检索系统与强大的生成模型结合在一起,让AI不仅会“说”,而且说得有据可查、与时俱进、精准可靠,是目前构建行业AI应用和解决LLM幻觉问题的首选架构之一。

服务通过 - Xmx=6G 指定最大堆分配为 6G,但实际 RSS 已达到 11G,开始怀疑堆外内存是否有内存泄露。

RSS从启动之后,就是一路增长,由于是测试机,业务量小,以53M/小时的增量增长。

JVM 的进程总内存 ≈

Java Heap + Metaspace + CodeCache + Thread Stack + JNI/Direct Buffer + GC 结构 + 内部 allocator fragment + C 库分配 + mmap 区域 + …

由于现象是RES比较高,先看一下java堆是否有异常。把java堆dump下来仔细排查一下,jmap -histo:live pid,发现整个堆回收完也才几百兆,远不到8G的Xmx的上限值,GC日志看着也没啥异常。基本排查java堆内存泄露的可能性。

1
jcmd 1 VM.native_memory detail scale=MB > nmt-`date +%F-%H-%M-%S`.log & pmap -x 1 > pmap-`date +%F-%H-%M-%S`.log

整体思路是:通过多次收集,进行比较,查看明显的差异

1
icdiff pmap-2023-07-27-09-46-36.log pmap-2023-07-28-09-29-55.log | less -SR

排查顺序

1️⃣ 先开 -XX:NativeMemoryTracking=detail

2️⃣ jcmd VM.native_memory summary 定位哪一类区域增长

3️⃣ 如果是 Internal / Arena Chunk → JNI / DirectBuffer 泄漏

4️⃣ 用 heaptrack / tcmalloc 进一步追踪分配源

5️⃣ 如果是 Thread 区域 → 检查线程数

6️⃣ 若都是正常,考虑 libc allocator fragment,可尝试使用 -XX:+UseG1GC-XX:MaxDirectMemorySize 或更换 allocator(jemalloc)

这一次排查,先通过NMT,发现Java能追踪到的内存区域一切正常

再通过pmap工具,发现了JNI调用C++有内存泄漏

C++修复后,内存增长大幅降低,但RSS依然会增长

最后,只能使用jemalloc替换原生的glibc来排查一下

替换后,内存还真的不再增长。

cat /etc/os-release

1
2
3
4
5
6
7
8
9
10
11
12
PRETTY_NAME="Ubuntu 22.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.4 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

ldd –version

1
2
3
4
5
ldd (Ubuntu GLIBC 2.35-0ubuntu3.6) 2.35
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

RSS 持续增长的几种可能情况:

  1. 正常情况(常见):你的程序频繁地分配和释放大小不一的内存。这会导致堆内存出现“空洞”(外部碎片),即使有大量空闲内存,也因为不连续而无法从堆顶归还给 OS。RSS 会稳定在一个较高的水平,这是 ptmalloc2 的设计特点,目的是提高分配效率。
  2. 内存泄漏(需要警惕):你的程序持续分配内存,但忘记释放。这会导致堆不断增长,即使通过 brkmmap 向 OS 索要更多内存,RSS 会无限制地增长,直到被系统 OOM Killer 杀死。
  3. 峰值使用后的稳定期:程序在某个阶段需要大量内存,之后虽然释放了,但 glibc 没有立即归还给 OS(堆未收缩)。这部分内存成为了进程的“缓存”,如果程序后续再次需要分配内存,就可以快速重用,避免了系统调用的开销。
  • RSS 增长不一定是内存泄漏,很可能是 glibc 的内存池策略。
  • glibc 不会因系统内存紧张而主动释放内存,它的行为是预定义的。
  • 真正的回收保障来自 Linux 内核,当系统内存不足时,它会强制回收,可能包括将你的进程内存换出(Swap)。
  • 如果 RSS 增长是无界的、持续的,并且与你的业务逻辑预期不符,那么首先应该怀疑和排查内存泄漏

Reference

Jemalloc简介及使用方法

使用jemalloc解决JVM内存泄露问题

Java 进程内存占用及可观测性调研&内存异常排查最佳实践

《管好技术债》- 第13章 与技术债务共存

解决技术债务的通用路径

1、感知

确保所有相关人员都对什么是技术债务以及它如何影响项目有共同的认识

2、评估信息

了解项目的状态,你目前面临的债务,什么原因,以及它的后果是什么

3、建立一个登记表

建立某种形式的技术债务清单

4、决定要解决的问题

当你计划发布时,查看技术债务登记表,力求该发布可以减少技术债务并且可实际解决技术债务问题

5、采取行动

把技术债务的识别和管理纳入所有软件开发和业务治理实践中

重复这个过程,因为不太可能一下子完全摆脱技术债务。

如果你想要明确地管理组织中的技术债务,不要将债务处理作为一个单独的过程,将其集成到流程中,以补充当前的实践。

循序渐进,而不是大幅度地修改。第一步,选择能带来最直接利益的修改。

感知

给你的技术债务起个名字。确保所有参与项目或接近项目的人员对技术债务有共同的理解:什么是技术债务,什么不是技术债务,以及它如何影响项目。

提高感知能力的一些方法:

1、为项目提供一个清晰、简单的技术债务定义。

2、向团队讲解什么是技术债务及其产生的原因

3、直接在项目环境中给大家(管理人员、分析师和产品经理)讲解关于技术债务的知识

4、在问题跟踪系统中创建技术债务类别,使技术债务与缺陷或新功能有所区别

5、将已知的技术债务作为长期技术路线图的一部分

6、将感知活动扩展到作为项目一部分的任何外部供应商

使用能够帮助团队沟通的方法,帮助每个人在概念上都达成一致。

评估信息

在尝试补救技术责备之前,客观评估项目的状态。根据你在技术债务时间线上的位置,考虑以下行动:

1、确定评估技术债务的目标和标准

2、通过分析代码、架构和生产基础设施来监控你的产品,以了解导致团队经历种种症状的技术债务

3、纳入轻量检查 ,以持续监控技术债务

建立技术债务登记表

1、将你的问题跟踪工具中的“技术债务”分类细化为技术债务描述,并将其指向所涉及的特定软件工件:代码、架构或生产基础设施

2、至少包括最常见的两类技术债务:

2.1、简单的、本地化的、代码级的债务

2.2、广泛的、结构化的、架构债务,并指向所涉及的软件工件:代码、架构或生产基础设施

3、分析包含非故意的技术债务的代码和架构,并在技术债务登记表中描述结果

4、在你的迭代审查和回顾会中包括对技术债务的讨论,并对其加以特别的重视。将它作为一个待办事项参与优先排序

在收集信息以获得一些具体示例和评估用的数据时,可能需要做一些清点工作。

决定修复什么

如果你面对的是一个庞大的、有点繁杂的技术债务登记表,则需要确定修复什么以及什么时候修复。

应该根据你对情况的评估来做出决定,包括下一步软件产品的发展方向。要做出决定,需要收集有关补救策略、成本和权衡的其他信息。

审查技术债务登记表中的各项,确保其中包含适当的项目,并确定处理它们的优先顺序,从而决定下一步交付什么:

1、不仅要估计偿还技术债务的成本,还要估计不偿还技术债务项的成本:延迟偿还会在多大程度上减缓当前的进度?如果你无法提供实际成本,请使用“T恤尺码”策略

2、分配时间以偿还技术债务

3、制订一个与上下文相关的偿还计划。

采取行动

根据你的特定情况,并结合项目上下文来综合实施技术债务管理方法。这样做能更加积极主动地了解技术债务产生的原因,并控制新债务的引入 。

采取一些行动来识别和管理软件开发和业务治理实践中的技术债务

1、通过将一些技术债务项添加到迭代待办事项列表中,以保持技术债务处于较低水平,从而减少每个开发周期的技术债务。

2、将技术债务纳入有关延迟功能交付和降低作为短期或长期投资,并计划对其进行管理。

3、收集与技术债务相关的工作或成本的关键指标,以协助未来的决策。

从最简单的情况开始并迭代这些活动,在每次迭代中逐步改进流程。

《技术为径:带领公司走向卓越的工程师》–项目管理进阶

作为一个研发经理,你需要承担起为团队制订工作计划的责任。在你的上级管理层尝试制订整个季度,甚至整年的工作计划过程中,经常需要你来评估自己的团队是否能够承担某些项目,包括评估这些项目所需要的工作量,以及这些项目是否与你组员的技能相匹配。

虽然作为一个团队经理,你可以将一些项目管理工作下放给你的技术小组长来进行,但是肯定还需要自己承担一部分相关工作。

你所面临的主要任务是,决定哪些项目可以承担,而哪些项目需要推掉。同时,你可能还需要适应敏捷状态下需求的不断迭代,为每个项目预估一个大概的完成日期。

若要做好对团队工作的管理工作,你需要对团队的工作节奏十分熟悉。万幸的是,有一些窍门可以帮助你做到这一点。

敏捷与项目管理

项目管理并不意味着进入瀑布式管理模式,从一开始就将项目中的每一个细节都计划清楚。

然而,对于大部分团队来说,长期的、抽象的目标,以及短期的、具体的、面向长期抽象目标的工作计划是并存的。

当需要实际规划小的项目目标时,团队应该采用敏捷过程来快速评估项目并且分解项目目标,这样效率最高。

作为团队管理者,你不应该过多干预,甚至应该推动团队自主进行。你需要对更大范围的项目目标–那些需要数月,而不是数个星期才能完成的事情负起责任来。

预留20%的工程时间

预留20%的工程时间来处理研发可持续性工作。“研发可持续性工作”指的是添加测试用例、调试问题、重构老旧代码、更换编程语言和升级语言版本,以及其他类似的工程杂事等。

如果能够坚持这样做,你就能将一些中等大小的陈旧代码逐渐消化、转换掉。每个季度做一点就够。随时清理陈旧系统,可以降低其维护难度,从而给团队留出更多时间来进行新功能的开发。

就算不这样做,你也可以用这个时间来补偿项目过程中的意外情况。如果真的把项目研发计划填满100%,你很快会发现由于各种情况导致计划流产。监近交付时间时,管理有说“不”的义务。

设定截止日期在项目管理中是很常见的,有时这个截止日期是团队自我设定的,也有的时候是来自更高管理层的要求。

按照截止日期按时交付项目的唯一方法,就是在项目末期不停地裁剪末完成的项目需求。

这就意味着,作为研发经理,你必须和自己麾下的技术小组长与产品负责人/业务代表一起开会商讨到底哪些需求是必须交付的,而哪些需求是可以删减的。这意味着,作为研发经理,你经常需要对两边都说“不”。

说到底,你需要向技术团队交付一系列定义清晰、实现成本合理的产品需求,同时需要向产品团队解释清楚完成所有需求所需的额外时间。

更多的计划时间

软件开发时间预估中有一个“翻倍定律”:当需要测算软件开发时间时,永远将你预期的时间翻一倍。

不要每个需求都需要让技术团队预估实现时间

研发经理的时间预估与项目计划职责的的原因之一,是因为工程师针对每个需求都交付一个预估时间,不仅浪费其时间,而且徒增其压力。

管理层承担不确定性是责无旁贷的,管理都不应该将不确定性统统甩给团队成员来承担。

管理者不仅仅是技术团队和公司其他部门的传声筒。这样不仅不解决问题,反倒干扰正在实际做事的人。但是,你也不能只是团队的“挡箭牌”,而不管公司其他部门的实际需求。

你需要帮助公司建立一个针对新需求和用户反馈的持续沟通流程,减少意外情况和紧急需求的发生。

《技术为径:带领公司走向卓越的工程师》–管理项目

制定一份项目计划,需要搞清楚各部分之间的相互依赖关系。所有依赖关系都必须详加考虑,一个一个地反复讨论每个子任务的描述信息、时间安排、以及如何拆分。

领导能帮助我解决一些问题,但是更多的问题需要我自己去解决。

我一点都不喜欢这样的工作。这种体验给我留下了极为深刻的印象。我清晰地记得自己对任何一点疏忽的恐惧,对计划中的任何一点不确定性的反复斟酌。

而这一切都是为了制订一份能让领导看上的计划表。接下来我们又需要将这份计划表转化为可以展示给公司管理层的格式,说服他们接受这一计划。感觉自己简直要烦死了。但是,这也是我职业我生涯中最重要的学习经历。

现在都是敏捷开发,是不是不再需要项目管理了?

敏捷开发只是一种拆分工作的好方式,它让你将注意力集中在小型目标上,从而不停地持续交付这些目标。

但是,这不能取代项目管理。大型项目不可能在一个或两个冲刺周期内完成。你还需要向管理层预估项目的完成时间,同时附带对项目所需时间的解释信息。

另外,某些项目,比如基础设施项目、平台项目、系统设计项目,通常需要详细的系统设计,或者非常超前的项目计划。

在面对这样的项目时,存在大量的未知信息和刚性很强的时间节点,敏捷模式根本不适用。

随着职业生涯的不断进步,学会如何将超出个人能力的复杂项目进行拆分是不可或缺的。

对于一个时间周期很长、多人参加的项目进行项目管理,并不是每个人都会喜欢的工作。

这项工作过于烦琐,同时还有点可怕。我喜欢构建系统、交付功能,而不太喜欢每天思考一个细节模糊的系统如何进行拆分。

同时,我很怕自己在项目计划中某个疏忽导致项目最终失败。但是,如果我不亲自参与的话,项目也不会自己完成,而且有很大概率会浪费大家的更多时间。

项目管理不能事无巨细地进行,这一点很多组织都意识不到。

不要招专门的项目经理,因为他们往往会成为工程师推卸责任的“挡箭牌”,而非一起合作的的伙伴。专门的项目经理往往导致项目以瀑布流的方式进行,而非采用敏捷的方式推进。然而,一定程度的项目管理是不可避免的,技术小组长对技术性强的项目应该根据需要进行合理的项目计划。

于项目提前规划的意义并不在于你是否事先想到了每一个细节,对未来考虑得是否周全,或者项目进度是否完美推进。

其真正的意义在于,制订项目计划这个过程强迫工程师在动手实干之前,先对项目进行一些深度考量。

++制订项目计划的真正目标在于,推动工程师在合适的进行合理的预测,并制订针对意外的应对计划。++

至于这个计划到底有多么准确,并不一定真的那么重要。

我们提前确定了想要达成的目标,同时也提前识别出一些可能导致项目失败的问题,并且为此提前做了准备。

项目管理,让每个人认真地花时间去面对这些非技术性、无法提前预测的难题。这些困难的工作帮助每个人完成自己的复杂项目,并且帮助成长。让每个人都能带领更大的团队,更好地拆分任务,以构建更复杂的系统。

作为技术人员,我们往往假设管理层十分这解我们的所做所想。“有问题就看看代码吧”作为技术领域内沉浸多年的技术人员,我们很容易假设别人和我们一样对这些问题了如指掌。

但是,这是错误的。技术管理者竭尽全力地去招募精兵良将,是为了让这些人才来解决非常棘手的技术问题的。但是,这不意味着技术管理者全知全能。在我的职业生涯中,每次我们向一些非常资深的技术管理者耐心解释诸如“NoSQL是什么”这种问题时,他们都很由衷地感激我。

现在,我一点都不觉得向其他人(不论他们是资深经理,还是新人)介绍基础技术概念及相关技术的优势对比是浪费自己的时间。

如果以平等的方式与他们进行对话,他们就会慢慢地培养起对我的建议和决策能力的信任,而这终将有利于推动我想推动的变革。花时间向别人解释自己的想法是非常重要和值得的。

《技术为径:带领公司走向卓越的工程师》–如何管理一个项目

所谓项目管理,就是将一个复杂的最终目标分成一些小目标,同时将这些目标以最高效的方式进行排序,识别出其中可以并行开展的工作,以及这些工作前后的依赖关系。

同时,还应该试图找出那些可能导致项目延期或者彻底失败的高风险环节。

项目管理的过程,就是试图将不确定的部分更确定化、将不可知的问题变成可知问题的过程。

同时,我们也应该认识到在这个过程中犯错误和漏掉一些未知风险是不可避免的。

1、将工作进行拆分

拿一张表格,或者采用甘特图的形式,抑或采用你最习惯的其他形式,尝试将你的大目标(例如,重写应收账款系统)拆分成一些具体的小目标,继续迭代。这个过程不一定要完全由你自己来完成。

如果系统的某个方面你不是很了解,就让其他人来帮助你。

你将目标拆分到一定程度的时候,就要专注于这些任务的排序。哪些任务可以马上开始?

将其拆分成一个个具体的任务进行推进。

2、持续关注细节与未知部分

做好项目管理的秘诀就是,迎难而上,永不停止。

项目管理本身就是一项非常烦琐、累人的工作。所以不管这项工作有多烦人,有多无聊,有多痛苦,你都不要停止。

一个好的上级领导会和你一起完成这项工作,帮助你指出计划的不足之处,同时向你提出自己的看法。没有人喜欢制订项目计划,但是这是一个和上级领导共同工作的好机会,同时也是一个很好的学习过程。

一定要持续跟进项目中的未知之处,一直到无可追踪为止。

3、真正落实项目计划并时刻调整

之前的周密计划制订过程的价值所在,就是帮助你了解项目的真实进度,以及与达成最终目标的距离

在项目进展过程中,如果出现延期情况,一定要向所有人及时通报信息。

有计划在手,你就不再需要盲目地猜测项目进度,而是可以清晰地知道项目的哪些目标已经达成,而哪些目标还没有达成。

4、利用计划过程中获得的知识来管理需求变更

在按照原始需求进行任务拆分的过程中,你对项目有了更深刻的了解。如果中途发生了需求变更的情况,这些知识可以帮助你更好地应对。

如果某项变更大大增加了项目风险,或者需要大量的额外工作,你就可以与他人明确这项变更的成本。

如果正在做的项目有一个非常严格的截止日期,那么了解每项细分工作所需的时间可以让你更好地排列优先级,甚至是割舍某些工作,平衡项目的整体质量、进度与交付时间。

5、随着项目的推进不断完善细节信息

当项目快要进入尾声的时候,烦琐的工作又出现了。

这正是落实项目每一项小细节的阶段。还差哪些任务没做完?测试做完了吗?交付验证做了吗?

可以考虑进行一次复盘模拟,提前将项目发布时可能出现的各种问题场景都预演一遍。

这也就是在落实“足够好”这个标准线的时候,让团队内的每个人都了解这个标准线,并且要具体落实到自己负责的部分。

排队那些无关紧要的工作,让整个团队都专注在落实真正重要的细节问题上。

制订一份发布计划,再制订一份回滚计划。

重读经典–《程序员修炼之道》

是什么造就了务实的程序员?

早期的采纳者/快速的适配者

你对技术和技巧有一种直觉,喜欢尝试。当接触到新东西时,你可以很快地掌握它们,并把它们与其他的知识结合起来。你的信心来自经验。

好奇

你倾向于问问题。这真不错–你怎么做到的?你对那个库有意见吗?总在听人说起的量子计算到底是什么?符号链接是怎么实现的?

你热衷于收集各种细微的事实,坚信它们会影响自己多年后的决策。

批判性的思考者

你在没有得到证实前很少接受既定的现实。当同事们说“因为就该怎么做”,或者供应商承诺会解决所有的问题时,你会闻到挑战的味道。

现实主义

你试图理解所面临的每个问题的本质。这种现实主义让你对事情有多困难、需要彡多长时间有一个很好的感知。一个过程应该很难,或是需要点时间才能完成,对这些深刻理解,给了你坚持下去的毅力。

多面手

你努力熟悉各种技术和环境,并努力跟上新的进展。虽然目前的工作可能要求你在某个专门领域成为行家,但你总是能够进入新的领域,迎接新的挑战。

重读经典–《程序员修炼之道》

大多数开发人员对设计正交系统的必要性都很熟悉。只不过他们可能会使用其他一些词来描述这个过程,例如模块化、基于组件和分层,等等。

正交性是什么

“正交性”是从几何学中借用来的术语。

若两条直线相交后构成直角,它们就是正交的。

例如,图表中的坐标轴就是正交的。对于向量而言,这两条线相互独立。图示中的向量1指向北,完全不影响东西朝向。向量2指向东,完全不影响南北朝向。

在计算科学中,这个术语象征着独立性或解耦性

对于两个或多个事务,其中一个的改变不影响其他任何一个,则这些事物是正交的。

在良好设计的系统中,数据库相关代码应该和用户界面保持正交:你可以变更界面但不应影响数据库,切换数据库而不必要更换界面。

正交的好处

编写正交的系统,就能获得两个主要的收益:提高生产力及降低风险

提高生产力

1、将变更限制在局部后,开发时间和测试时间都会减少。

2、正交的方法同时促进了重用。

3、组合正交组件能获得相当微妙的生产力提升。

假设一个组件能做M件独特的事情,另一个能做N件。如果它们是正交的,组合起来就能做M*N件事。如果两个组件不正交,有重叠之处,结果就少一些。

在正交组件的组合过程中,单个单元的性价比提高了。

降低风险

1、代码中病变的部分被隔离开。

如果一个模块生病了,不太可能将症状传播到系统的其他部分。把它切下来并移植一个新的健康器官进去,也更加容易

2、这样获得的系统不那么脆弱。

对特定区域进行小的变更和修复后,因此而产生的任何问题都将局限于该区域。

3、你不会被特定的供应商、产品或平台紧紧束缚。

因为这些第三方组件的接口所关联的,仅仅是整个开发中相对很小的一部分。

4、正交系统可能更利于测试,因为为其组件设计和运行测试更加容易

正交的方法

设计

系统应该由一组相互协作的模块构成,每个模块实现的功能应独立于其他模块。

有时这些模块组件被组织到不同的层次上,每一层都做了一级抽象。

这种分层的实现是设计正交系统的有力途径。

因为每一层只使用它下面一层提供的抽象,所以可以在不影响代码的情况下极其灵活地更改底层实现。

工具包和程序库

在引入第三方工具包和程序库时,请注意保持系统的正交性。技术选择要明智。

当你拿出一个工具包时,问问自己,它是否会将一些不应该有的变化强加给你代码。

编码

每当你写下代码时,就有降低软件的正交性的风险。

不仅需要盯着正做的事情,还要监控软件的大环境。

养成不断质疑代码的习惯。只要有机会就重新组织、改善其结构和正交性。这个过程被称为重构。

保持代码解耦

编写害羞的代码–模块不会向其他模块透露任何不必要的信息,也不依赖于其他模块的实现。

试试最少知识原则。如果你需要改变一个对象的状态,让该对象替你来完成。这样做能让你的代码和其他代码实现隔离,更有可能保持正交性。

避免全局数据

只要代码引用全局数据,就会将自己绑定到共享该数据的其他组件上。

即使只打算对全局数据进行读操作,也可能引发问题。

避免相似的函数

重复代码是结构问题的症状。

测试

基于正交性设计和实现的系统更容易测试。

由于系统组件之间的交互是形式化的,且交互有限,因此可以在单个模块级别上执行更多的系统测试。

文档

正交性也适用于文档。其涉及的两个独立维度是内容和呈现。

真正符合正交性的文档,应该能够在不改变内容的情况下显著地改变外观。

读完刘媛媛的《精准努力》,想起了冯唐说的“结构化思维,结构化表达”。当你再怎么焦虑、恐慌时,不只是情绪表现,而应该提出为什么焦虑,焦虑怎么消除?怎么体现结构化思维,最佳表现形式,就是绘制一张思维导图。

在《精准努力》中,作者针对我们常见的问题:目标、策略、执行、心态、学习、社交六个方面进行了结构化表达。

目标篇

制定目标,我们一般会想到SMART原则,但作者提出了独特的视角:

最主要的思想就是二八原则,找到那20%的因素,作为核心目标,进行攻克。

产品质量管理中有一个法则:重要少数法则

这个法则是由质量管理专家约瑟夫朱兰提出的。朱兰认为,大多数品质不良的问题能够形成,可以归因为重要的少数,其余的小瑕疵,才是属于不重要的多数造成的

在发生质量问题的时候,20%的问题是由基层操作人员的失误造成的,但是80%的问题是由领导者造成的;而80%的领导问题,又是在20%的重要环节上造成的,这就是质量管理当中的“二八原则”。

有用不等于值得

值不值得,要把机会成本考虑进去

为何你的努力和别的人的努力总是有差别?

如果能够抓住关键的环节,就可以更少的投入,收获更大的回报。

这不是投机取巧,我们只是追求有效。人的专注力、时间、精力都是有限的。我们必须选择一种有节制的、更专注的努力方式,去追求更高的效率,追求有用功。

当我们不知道什么问题是关键问题的时候,要把通往目标遇到的所有问题全部列举出来。问自己:

是哪些问题,导致你对现在的生活不满意?

其中的哪些问题是你认为的主要问题?

这些问题的解决,是否可以促进其他问题的解决?

通过自问自答,圈定其中关键的部分,然后在这20%的领域,投入专注的卓越努力,去得到80%的收获。

改掉只要有用就去做的思维,也可以反过来思考这个问题:如果我只做三件事,我应该做哪三件?如果只做一件呢?

策略篇

做事的正确顺序:策略,勤奋地执行策略,然后成功。

要做成一件事:

1、要有一个目标

2、思考通往目标的途径

3、最后才是执行层面的事情

每个人都应该去追求成功。在这个过程中,无论是靠天分、靠勤奋,还是靠机遇,都靠不住。最可靠的是属于自己的方法论。方法论带来的成功是必然的,只不过来得早或晚而已。

六步循环

自我定位-瞄准目标-制定策略-执行反馈-调整行动-最终完成

自我定位

什么是目标?“我想考北大”这不是目标,说得好听点,叫想法、愿望。目标要有可执行性和衡量标准。也就是要符合SMART原则。比如“我要考到240分”,这算是一个目标。

什么叫作学会解释目标?

比如要考北大,意味着两门专业课都必须要考120分;意味着这张卷子当中80%的考点我都要知道;意味着考试范围内的80%我必须得知道。如果我只知道10%的话,那么还要搞定70%,那整个复习的过程就会变成这样:找到不会的地方,变成会的,直到这个百分比达到80%。

这就是目标被解释的过程,从“我想考北大”到“我要考到240分”,到“我要搞定那此不会做的题目直到百分比达到80%”,到“我今天已经做了50%,还有30%”。

策略

策略意味着:

第一,找到通往目标的真正问题和障碍

第二,为这些问题和障碍去设计方案

第三,方案应该是一个互相促进和统一的系统

第四,集中精力去执行自己的方案,放弃其他的事情

执行

一件事情最后的成果如何,取决于以下几个要素:

效果=时间x精力x目标x策略x专注度x熟练度

执行篇

1、专注力

2、拖延症

3、精力管理

4、专注

5、坚持

最近在工作中使用一个消息队列组件。发现当系统没有完全启动成功时,就开始消费消息了,出现所依赖资源初始化工作还没有完成,造成消费失败的情况。

通过源码跟踪,发现组件开始消费是在InitializingBean#afterPropertiesSet中触发的。

怎么处理呢?

首先,InitializingBean#afterPropertiesSet是在Bean初始化完成后调用的。

而现状是想在所有资源都被初始化完成后,再开始执行。

第一种方案:SmartInitializingSingleton

spring中提供了所有Bean初始化完成后再调用的机制。实现SmartInitializingSingleton

SmartInitializingSingleton 与 InitializingBean两者有很大的区别:

1,SmartInitializingSingleton只作用于单例bean,InitializingBean无此要求。但他们都不能用于懒加载的bean。

2,SmartInitializingSingleton是在所有单例Bean都初始化完成后调用的,InitializingBean是每个bean初始化完成后就会调用。

但因为是第三方组件,也不想修改源码,所以此路不通。

第二种方案:想到了@Lazy

把触发的Bean设置成@Lazy,再所有资源初始化完成后,进行主动加载。激活具体的消息消费行为。

行动分两步:

1、在具体消费消息的Bean上增加注解@Lazy

2、spring启动完成后,再主动加载此Bean

此时,又涉及到了spring的初始化过程

spring提供了很多启动各个阶段的触发点

包括ApplicationRunner、CommandLineRunner以及各种事件:

ApplicationContextInitializedEvent : triggered after ApplicationContext is prepared and ApplicationContextInitializers are called but before bean definitions are loaded

ApplicationPreparedEvent : triggered after bean definitions are loaded

ApplicationStartedEvent : triggered after context has been refreshed but before command-line and application runners are called

ApplicationReadyEvent : triggered after any application and command-line runners are called

ApplicationFailedEvent : triggered if there is an exception on startup

结合spring初始化过程,写一个小示例,验证一下bean初始化与这些事件的先后顺序

创建一个Bean

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
public class InitFirst implements InitializingBean, SmartInitializingSingleton {

public InitFirst() {
System.out.println("InitFirst construct method");
}

public void init() {
System.out.println("InitFirst init method");
}

@PostConstruct
public void postConstruct() {
System.out.println("InitFirst @PostConstruct");
}

@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InitFirst afterPropertiesSet()");
}

@Override
public void afterSingletonsInstantiated() {
System.out.println("InitFirst afterSingletonsInstantiated()");
}

}

实现一下BeanPostProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class InitBeanPostProcessor implements BeanPostProcessor {

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof InitFirst || bean instanceof InitSecond) {
System.out.println("postProcessBeforeInitialization:" + beanName);
}
return bean;
}

@Override
@Nullable
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof InitFirst || bean instanceof InitSecond) {
System.out.println("postProcessAfterInitialization:" + beanName);
}
return bean;
}

}

监听一下各种Event

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class InitListener {


@EventListener(ApplicationPreparedEvent.class)
public void preparedEvent() {
System.out.println("Yaaah, ApplicationPreparedEvent");
}

@EventListener(ApplicationStartedEvent.class)
public void startedEvent() {
System.out.println("Yaaah, ApplicationStartedEvent");
}

@EventListener(ApplicationReadyEvent.class)
public void runAfterStartup() {
System.out.println("Yaaah, ApplicationReadyEvent");
}

}

最后执行的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
InitFirst construct method
postProcessBeforeInitialization:initFirst
InitFirst @PostConstruct
InitFirst afterPropertiesSet()
InitFirst init method
postProcessAfterInitialization:initFirst
InitSecond construct method
postProcessBeforeInitialization:initSecond
InitSecond @PostConstruct
InitSecond afterPropertiesSet()
InitSecond init method
postProcessAfterInitialization:initSecond
InitFirst afterSingletonsInstantiated()
InitSecond afterSingletonsInstantiated()
Yaaah, ApplicationPreparedEvent
Yaaah, ApplicationStartedEvent
ApplicationRunner
CommandLineRunner
Yaaah, ApplicationReadyEvent

整个初始化的顺序:

construct -> postProcessBeforeInitialization -> @PostConstruct -> InitializingBean#afterPropertiesSet -> init-method -> postProcessAfterInitialization -> ApplicationPreparedEvent -> ApplicationStartedEvent -> ApplicationRunner/CommandLineRunner -> ApplicationReadyEvent

ApplicationRunner与CommandLineRunner谁先执行?

在都没有标注order的情况下,ApplicationRunner 的优先级要高于CommandLineRunner

在方案二执行时,发现了Spring延迟加载Lazy没有生效的情况,后来追查到是xxl-job的bug。在遍历任务时,加载了所有的bean,包含了Lazy的Bean。在最近的xxl-job 2.4版本中进行了修复:

https://github.com/xuxueli/xxl-job/pull/3155/commits/b4835d40f18084e9facb9ec0d41993fdc885aca8

参考资料

Run method on Spring Boot startup