普通视图

发现新文章,点击刷新页面。
昨天以前首页

0基础带你精通Java对象序列化--以Hessian为例|得物技术

作者 得物技术
2025年9月11日 14:31

一、概述

在高级编程语言的世界中,开发者始终与 【object/struct】 这类高度抽象的数据结构打交道。然而在分布式架构下,任何服务进程都不是数据孤岛——跨进程数据交换是必然需求。

以Java为例,业务逻辑的输入输出都是 【object】 。但在RPC场景中,这些对象必须经由网络传输。这里出现了一个根本性矛盾:网络介质(网线/光纤)对面向对象编程(OOP)一无所知,它们只会用光和电忠实地传输扁平化的字节流(byte[] )。

软件工程经典的分层理论驱使我们去添加一个转换层

我们需要有个工具或者组件来协助进行 【object】【byte[]】 之间的双向转换。这个过程包含两个对称的流程:

  1. object】->【byte[] 】:业界一般称为序列化/serialize,但是那个单词念起来很拗口,本文我们都叫它【编码/encode】好了。
  2. byte[] 】->【object】:业界一般称为反序列化/deserialize,但是那个单词念起来很拗口,本文我们都叫它【解码/decode】好了。

Hessian作为Java生态中久经考验的对象编解码器,相较于同类产品具有以下两大核心优势:

  1. 深度Java生态适配:与JSON、Protobuf等语言中立的通用协议不同,Hessian专为Java深度优化,对泛型、多态等Java特有语言特性提供原生支持
  2. 高效二进制协议:相较JSON等文本协议,Hessian采用精心设计的二进制编码方案,在编解码效率和数据压缩率方面表现更优。

需要强调的是,软件工程没有银弹——业务场景的差异决定了编解码器的选择必然需要权衡取舍。但就Java RPC而言,Hessian应该是经过广泛实践验证的稳健选择。

本文将系统解析Hessian的编码流程,重点揭示其实现【object】->【byte[] 】转换的核心机制。

二、基础编码原理

对象编码过程主要包含如下两大核心:

  • 对象图遍历:遍历高级数据结构
    • 通过反射或元编程技术遍历对象图(Object Graph)。
    • 是同类产品的通用逻辑,不管jackson、fastjson、hessian都需要用不同的方式做类似的事情。
  • 编码格式:将高级数据结构按协议拍平放到byte[]
    • 同类产品百家争鸣,各有各的思路。
    • 是同类产品的竞技场,各个产品在这里体现差异化的竞争力
    • 设计权衡包括:
      • 二进制效率 vs 可读性(如Hessian二进制 vs JSON文本)
      • 编码紧凑性 vs 扩展灵活性
      • 跨语言支持 vs 语言特性深度优化

对象图遍历决定了编码能力的下限(能否正确处理对象结构),而编码格式决定了编码能力的上限(传输效率、兼容性等)。

对象图遍历

对象图遍历的本质是按深度优先进行对象属性导航

举个例子:

宏观来看,A类型的对象其实是一棵树(或图) ,如果脑补不出来的话,我给你画个图:

可以看到这棵树的叶子结点都一定是Java内置的基本数据类型。换句话说,Java的8种基础数据类型和他们的数组变体,支撑了Java丰富的预定义/自定义数据结构。

八股文:Java的8种基础数据类型是哪些?String算不算基础数据类型?

编码的本质就是深度优先的遍历这棵树,拍平它,然后放到byte[] 里。

我举个例子吧。

伪代码

为降低伪代码复杂度,我们假设Java只有1种基础数据类型int,也就是说Java里只有int和只包含int字段的自定义POJO。

我们定义POJO指的是用于传输、存储使用的简单Java Bean或者常说的DTO。

从某种意义上来说,Integer也是基于int封装的自定义POJO。

字节流抽象

我们使用标准库里的java.io.DataOutput来进行伪代码说理,这个类提供了一些语义化的编码function。

java.io.DataOutput

对象图遍历

字节流布局

最终呈现出来的字节流层面的数据布局会是这样:

看起来没毛病,唯一的问题就是不好解码。

当解码端收到一个16字节的字节流以后,它分不清哪块数据是A对象的,哪块数据是B对象的。甚至都分不清这到底是4个int32还是2个int64。

这个问题需要编码格式来解决。

编码格式

上面遗留的问题,聪明的你肯定想到了答案。

就是因为编码产物太太太简陋了,整个过程中只是一股脑的把树拍平,把叶子节点的值写入字节流,缺少结构元数据

最最最重要的结构元数据就是数据块的边界,上述4个数据块,最起码应该添加3个边界标识。

我们先用我们耳熟能详的JSON格式来理解下编码格式这个事情。

伪代码

JSON是这样解决这个问题的:

JSON协议在嵌套的POJO上用 {} 来作为边界,POJO内部的字段键值用 , 来做边界, : 拆分字段键值。

字节流布局

结果就变成这样:

这样在解码的时候,可以通过 {},:token来切割JSON字符串,判定数据块边界并恢复出对象图

三、Hessian编码格式

接下来我们可以开始介绍Hessian的编码魔法了。

需要强调的是:Hessian跟JSON不同,Hessian是二进制格式。如果一个字节流直接按字符集解码不能得到一个完整的、有意义的字符串,那它就是二进制编码数据。

Hessian在编码时,按数据块类型为每一个数据块添加一个前缀字节(byte) 作为结构元数据,这些元数据数据块一起,交给解码端使用。

数据块

对象图里的每一个节点,都是一个数据块。

如上图所示,以A对象为根的对象图,一共有6个数据块

数据块标签(tag)

Hessain在编码每一个数据块时,都会根据数据块的类型在字节流中写入一个前缀字节(0-255),这个字节说明了数据块的语义和结构

int32为例,其最基础的编码格式如下:

除该基础编码格式外,int32的编码还有其他变体。

上述 I 就是整数类型的tag。解码端读取tag后,按tag值来解码数据。

com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject(java.util.List<java.lang.Class<?>>)

由此延伸、拓展,其他的数据类型都是类似的模式。常见数据类型及其对应的tag值如下:

值得注意的是,NFT三个tag是自解释的,和固定值映射、绑定。

POJO编码

POJO是一种特殊的数据块,Hessian将POJO的结构拆开,分别编码。

POJO结构编码

POJO结构的tag为C,对照int32的编码格式,POJO结构的编码格式如下:

举个例子:

编码POJO时,Hessain会将POJO的类名、字段名列表写入字节流,供解码端使用。后续编码POJO字段值时,需要按照字段名列表(如上述bbcc)的顺序来编码字段值。

POJO字段值编码

POJO字段值的tag为O,对照int32的编码格式,POJO字段值的编码格式如下:

举个例子:

可以看到,编码POJO字段值的时候,在tag后面有一个POJO结构序号

这是Hessian的一个数据复用的小技巧。

POJO结构复用

JSON协议有一个缺点,那就是重复数据带来的存储/传输开销。举个例子:

如上图,B类型的字段名(ddee)在编码产物中重复出现

Hessian希望解决这个问题,同一类型的多个POJO对象在序列化时,只需要在第一次的时候编码类名、字段名等元数据,后续可以被重复的引用、使用,无需重复编码。

如果用Hessian来编码,结果会是这样:

数据布局

数据布局详解

如上图,APojo、BPojo的字段名只会编码一次。多个BPojo对象在编码时会通过结构引用序号(1) 来引用它。相对JSON,Hessian避免了多次编码BPojo字段名的开销。

为什么APojo的序号是1、BPojo的序号是2?

Hessain在编码过程中,每次遇到一个新的、没有处理过的新POJO类型时,会给它分配一个从0开始、单调递增的序号。

遥相呼应的,解码侧每次解码一个tag为C的POJO结构数据块时,也会按解码顺序维护好其索引序号。

四、Hessian编码细节

到现在,我们已经对Hessian编码有了一个的概括性的认识,接下来我们来看看一些值得注意的细节。

重复对象复用

A对象里有两个字段(d、e)指向同一个对象B。如果不做处理,会因为重复编码而带来不必要的开销。

相同的一个B对象,因为被两个字段重复引用,导致2次编码、产生2份数据空间占用!

如果只是有额外的开销,没有可用性问题那都还好。关键是在循环引用场景下,会因为引用成环导致递归进行对象图遍历时触发方法栈溢出!

循环引用是重复引用的特例,只要将重复引用处理掉,循环引用也就没问题了。

Hessian通过对象引用来解决这个问题。在对象图遍历过程中,遇到一个之前没有遇到过、处理过的POJO对象时,会给它分配一个从0开始、单调递增的序号。

后续再次需要序列化相同的对象时,直接跳过编码流程,将这个对象的序号写入字节流。

解码时,解码侧按相同的顺序来恢复出引用序号表,解码后续的对象引用。

小整数内联(direct)

很多编码类型,都需要在tag后再维护一个整数类型的字段。比如:

  • POJO的编码tag O需要一个整数来引用POJO结构引用序号

  • 类似String的变长类型需要一个整数来标识变长数据的长度

当字符串很短,就比如 "hi" 吧,短字符串编码格式的长度字段可能比实际字符数据还大(用4字节存储长度2),效率低下。

tag分段

Hessian将一些tag值的语义富化,让它既体现数据类型,也体现小数值。

因为tag是一个byte(int8) ,取值范围是0-255,每个tag标识一种特定的数据类型(int、boolean等),但是这些数据类型最多几十种,取值范围内还有很大的数值区间没有被使用,其实比较浪费。那我们就可以把这些空闲的tag值,挪作他用,提升tag数值空间利用率。

我举个例子,注意这个是参考Hessian思路的一个简单示意,具体的Tag值和Hessian无关

长度内联

对于长度≤31的字符串,Hessian用tag同时编码类型和长度

  1. 当0 <= tag <= 31 时,标识后续的数据块为字符串。
  2. tag的数值即为后续数据块的长度。

示例如下:

序号内联

当结构引用序号<=16时,Hessain用tag同时编码类型和序号

  1. 当0x60 <= tag <= 0x70 时,标识后续的数据块为POJO字段值。

  2. tag - 0x60的值,即为POJO结构(类名+字段名)引用序号。

示例如下:

相关源码如下:

com.alibaba.com.caucho.hessian.io.Hessian2Output#writeObjectBegin

字符串编码

Hessian编码字符串的关键流程是:字符串分段+不同长度的子串使用不同的tag。

  • 分段原则

字符串会被分割为若干块,每块最大长度为32768(0x8000)。前N-1块均为完整长度的子串(32768字节),使用固定tag R标识;最后一块为剩余部分,长度范围为0-32768字节,根据实际长度选择动态tag。

  • 尾段tag的选择基于尾块的长度决定
    • 长度≤31(0x1F):使用单字节tag 0x00-0x1F直接内联长度值。
    • 32≤长度≤1023(0x3FF):使用tag 0后跟1字节长度(大端序),10bit的计数空间由tag字节和长度字节共同提供。这个地方有点绕,看下代码吧。
    • 长度≥1024:使用tag S后跟2字节长度(大端序)。
  • 相关源码

com.alibaba.com.caucho.hessian.io.Hessian2Output#writeString(java.lang.String)

这种设计通过减少长字符串的冗余长度标记,在保持兼容性的同时显著提升了编码效率。

整数压缩

基础编码

整数(int32)的的取值范围很大(-23^31 - 2^31),保守的编码格式会用4个byte来编码整数。

但是日常使用中,我们会大量使用小整数,比如1、31。这时候如果还用4字节编码就很不划算啦~

变长编码

Hessian根据整数的值范围,动态的选择不同的编码方式,且不同的编码方式有不同的tag

  • 单字节整数编码类似【长度压缩】,tag中直接内联数值

适用范围:-16 到 47(共64个值)

编码方式:使用单字节,值为 value + 0x90(144)

例如:0 编码为 0x90,-1 编码为 0x8f,47 编码为 0xbf

  • 双字节整数编码

适用范围:-2048 到 2047

编码方式:首字节为 0xc8 + (value >> 8),后跟一个字节存储value剩下的bit。

这种编码可以表示12bit有符号整数

  • 三字节整数编码

适用范围:-262144 到 262143

编码方式:首字节为 0xd4 + (value >> 16),后跟两个字节存储 value 的高8位和低8位。

这种编码可以表示19bit有符号整数。

  • 五字节整数编码

适用范围:超出上述范围的所有32位整数

编码方式:以 'I' (0x49)开头,后跟4个字节表示完整的32位整数值。

  • 相关源码

com.alibaba.com.caucho.hessian.io.Hessian2Output#writeInt

收益

  • 小整数(如 0、-1)仅需 1字节 ,而传统 int32 固定4字节。
  • 大整数动态扩展,避免固定长度浪费(如 1000 仅需2字节)。

其他的数值类型比如int64也有类似的机制。

五、总结

Hessian专为Java优化,采用高效二进制协议,通过对象图遍历和编码协议实现对象与字节流的转换,利用数据块标签、重复对象复用、数据压缩等机制,提升编解码效率和数据压缩率。

本文没有去展开Hessian的代码细节,而是尽可能深入浅出的介绍了Hessain的核心编码原理,以帮助读者建立对Hessian的宏观认知,从而可以更好的去理解和使用它。

尽管不同语言/生态的序列化框架选型让人眼花缭乱,但是各自需要解决的问题和解决问题的思路都大同小异;我们对Hessain原理的认识可以迁移到其他序列化框架,甚至自己写一个领域特定的序列化框架。

相关内容均为笔者走读源码整理而来,如有疏漏,欢迎指正。

参考:

往期回顾

  1. 前端日志回捞系统的性能优化实践|得物技术

  2. 得物灵犀搜索推荐词分发平台演进3.0

  3. R8疑难杂症分析实战:外联优化设计缺陷引起的崩溃|得物技术

  4. 可扩展系统设计的黄金法则与Go语言实践|得物技术

  5. 营销会场预览直通车实践|得物技术

文 / 羊羽

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

双 Token 认证机制:从原理到实践的完整实现

2025年9月10日 17:47

双 Token 认证机制:从原理到实践的完整实现

在现代 Web 应用中,用户认证是保障系统安全的核心环节。随着前后端分离架构的普及,传统的 Session 认证方式逐渐被 Token 认证取代。其中,双 Token(Access Token + Refresh Token)机制凭借其在安全性与用户体验之间的出色平衡,成为主流的认证方案。本文将结合完整的 Express 后端与 Vue 前端代码,详细解析双 Token 机制的实现原理与实践细节。

双 Token 机制的核心原理

双 Token 机制通过两种不同特性的令牌协同工作,解决了 "安全性" 与 "用户体验" 之间的矛盾。其核心设计思想是:

  • Access Token(访问令牌) :短期有效,用于直接访问受保护资源,有效期通常设置为几分钟(示例中为 12 秒,仅用于演示)
  • Refresh Token(刷新令牌) :长期有效,仅用于获取新的 Access Token,有效期可设置为几天甚至几周(示例中为 7 天)

这种设计的优势在于:当 Access Token 被盗取时,攻击者仅有很短的时间窗口可以利用;而 Refresh Token 虽然长期有效,但通常存储在更安全的环境中,且一旦发现异常可立即吊销。

后端实现:Express 框架下的双 Token 系统

后端作为 Token 的签发者和验证者,承担着整个认证系统的核心逻辑。以下从初始化配置、核心工具函数、认证中间件到具体接口,逐步解析实现过程。

基础配置与依赖

const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const app = express();
app.use(cors({
    origin: [
        'http://localhost:5173', 'http://localhost:5174', 
        'http://localhost:5175', 'http://localhost:5176'
    ],
    credentials: true // 允许跨域请求携带Cookie
}));
app.use(express.json());
app.use(cookieParser());

这段代码完成了三件关键工作:

  1. 引入 Express 框架及必要中间件(cors 处理跨域,cookie-parser 解析 Cookie)
  1. 配置跨域规则,允许指定前端域名访问并支持跨域携带 Cookie
  1. 启用 JSON 请求体解析和 Cookie 解析功能,为后续处理打好基础

令牌存储与核心工具函数

// 内存存储(生产环境应使用Redis或数据库)
const accessTokens = new Map();
const refreshTokens = new Map();
// 获取当前时间戳(秒)
function now() {
    return Math.floor(Date.now() / 1000);
}
// 生成带前缀的随机令牌
function getRandom(prefix) {
    return `${prefix}-${Math.random().toString(26).slice(2)}${Date.now()}`;
}

为模拟令牌存储,示例使用 Map 对象临时存储令牌信息。在生产环境中,应替换为 Redis 等分布式存储系统,以支持多实例部署和令牌过期自动清理。

令牌签发函数
function getAccessToken(userId, ttlSec = 12) {
    const at = getRandom('AccessToken');
    accessTokens.set(at, { userId, expiresIn: now() + ttlSec });
    return at;
}
function getRefreshToken(userId, ttlSec = 3600 * 24 * 7) {
    const rt = getRandom('RefreshToken');
    refreshTokens.set(rt, { 
        userId, 
        expiresIn: now() + ttlSec, 
        revoked: false 
    });
    return rt;
}

这两个函数分别负责生成 Access Token 和 Refresh Token:

  • 为令牌添加前缀便于区分类型
  • 记录令牌关联的用户 ID 和过期时间
  • 为 Refresh Token 额外添加 "revoked" 状态,支持主动吊销
令牌验证与吊销函数
function verifyAccessToken(at) {
    const result = accessTokens.get(at);
    if (!result || result.expiresIn <= now()) return null;
    return result.userId;
}
function verifyRefreshToken(rt) {
    const result = refreshTokens.get(rt);
    if (!result || result.revoked || result.expiresIn <= now()) return null;
    return result.userId;
}
function revokeRefreshToken(rt) {
    const result = refreshTokens.get(rt);
    if (result) result.revoked = true;
}

验证函数通过检查令牌是否存在、是否过期、是否被吊销等状态,决定是否返回有效的用户 ID。这种设计确保了只有符合条件的令牌才能通过验证。

认证中间件:请求的第一道防线

app.use((req, res, next) => {
    // 登录、刷新、登出接口跳过验证
    if (['/auth/login', '/auth/refresh', '/auth/logout', '/login'].includes(req.path)) {
        return next();
    }
    // 从请求头获取AT
    const token = req.headers.token || '';
    const userId = verifyAccessToken(token);
    if (userId) {
        req.userId = userId; // 挂载用户ID到请求对象
        return next();
    }
    // 验证失败
    res.send({ status: 401, msg: "未登录或令牌过期" });
});

这个中间件实现了 "守门人" 功能:

  • 对登录、刷新、登出等特殊接口直接放行
  • 对其他所有请求验证 Access Token 的有效性
  • 验证通过则将用户 ID 挂载到请求对象,供后续接口使用
  • 验证失败则返回 401 错误,提示客户端进行处理

核心接口实现

登录接口:令牌的初始发放
app.post('/auth/login', (req, res) => {
    const { username } = req.body || {};
    const userId = username || 'demoUser';
    const at = getAccessToken(userId, 12);
    const rt = getRefreshToken(userId);
    // 将RT存入httpOnly Cookie
    res.cookie('rt', rt, {
        httpOnly: true, // 禁止前端JS访问,防XSS攻击
        sameSite: 'lax', // 限制跨站请求携带,防CSRF攻击
        secure: false, // 本地开发为false,生产需设为true
        path: '/',
        maxAge: 7 * 24 * 3600 * 1000
    });
    res.send({ status: 200, data: at });
});

登录接口是用户获取初始令牌的入口:

  1. 接收用户身份信息(示例简化为用户名)
  1. 生成 Access Token 和 Refresh Token
  1. 将 Access Token 直接返回给前端(通常存储在 localStorage)
  1. 将 Refresh Token 存入 httpOnly Cookie,提升安全性

特别注意 Cookie 的配置:httpOnly: true防止前端 JavaScript 访问,有效抵御 XSS 攻击;sameSite: 'lax'限制跨站请求携带 Cookie,降低 CSRF 攻击风险。

令牌刷新接口:无感续期的关键
app.post('/auth/refresh', (req, res) => {
    const rt = req.cookies.rt; // 从Cookie获取RT
    if (!rt) return res.send({ status: 401, msg: '无刷新令牌' });
    const userId = verifyRefreshToken(rt);
    if (!userId) return res.send({ status: 401, msg: '刷新令牌失效' });
    // 令牌旋转:吊销旧RT,生成新RT和新AT
    revokeRefreshToken(rt);
    const newRt = getRefreshToken(userId);
    const newAt = getAccessToken(userId);
    // 写入新RT到Cookie,返回新AT
    res.cookie('rt', newRt, { ... });
    res.send({ status: 200, data: newAt });
});

刷新接口实现了 Token 的无感续期:

  1. 从 Cookie 中获取 Refresh Token 并验证其有效性
  1. 采用 "令牌旋转" 机制:吊销旧的 Refresh Token,生成新的一对令牌
  1. 将新的 Refresh Token 存入 Cookie,新的 Access Token 返回给前端

令牌旋转机制大幅提升了安全性,即使 Refresh Token 被盗取,攻击者也只能使用一次。

登出接口:安全终止会话
app.post('/auth/logout', (req, res) => {
    const rt = req.cookies.rt;
    if (rt) revokeRefreshToken(rt); // 吊销RT
    res.clearCookie('rt', { path: '/' }); // 清除Cookie中的RT
    res.send({ status: 200, msg: '已登出' });
});

登出接口通过吊销 Refresh Token 并清除 Cookie,确保用户会话被安全终止,防止后续被恶意使用。

前端实现:Vue 中的令牌管理

前端作为令牌的持有者和使用方,需要妥善处理令牌的存储、传递和刷新逻辑。以下从路由守卫、请求拦截器到页面组件,解析前端实现细节。

路由守卫:控制页面访问权限

router.beforeEach((to, from, next) => {
    // 处理URL中的token参数
    const token = to.query.token;
    if (token) {
        localStorage.setItem("token", token);
        next({ path: to.path, query: {} });
        return;
    }
    // 检查是否需要认证
    if (to.meta.requiresAuth) {
        const currentToken = localStorage.getItem('token');
        if (!isValidToken(currentToken)) {
            // 没有有效token,跳转到登录中心
            window.open(`http://localhost:5174/login?resource=${window.location.origin}${to.path}`);
            return;
        }
    }
    next();
})

路由守卫实现了页面级别的访问控制:

  • 处理 URL 中携带的 token 参数,存储到 localStorage
  • 对标记为需要认证的路由(如/about),检查 token 有效性
  • 没有有效 token 时,引导用户到登录页面

Axios 拦截器:自动处理令牌

// 请求拦截器:添加token到请求头
request.interceptors.request.use((config) => {
    const token = localStorage.getItem("token");
    config.headers = config.headers || {}
    if (isValidToken(token)){
        config.headers.token = token;
    }else{
        localStorage.removeItem('token');
    }
    return config;
})
// 响应拦截器:处理token过期
request.interceptors.response.use(async (res) => {
    if (res.data && res.data.status === 401) {
        const original = res.config || {}
        if (original._retried) {
            // 已重试过仍失败,跳登录中心
            window.open(`http://localhost:5174/login?resource=${window.location.origin}`)
            return res;
        }
        original._retried = true
        if (!isRefreshing) {
            isRefreshing = true
            refreshPromise = request.post('/auth/refresh', {})
                .then(r => {
                    if (r.data && r.data.status === 200) {
                        const newToken = r.data.data
                        localStorage.setItem('token', newToken)
                        return newToken
                    }
                    throw new Error('refresh failed')
                })
                .catch(() => {
                    localStorage.removeItem('token')
                    throw new Error('refresh failed')
                })
                .finally(() => {
                    isRefreshing = false
                })
        }
        try {
            const newToken = await refreshPromise
            original.headers.token = newToken
            return request(original)
        } catch (e) {
            window.open(`http://localhost:5174/login?resource=${window.location.origin}`)
            return res
        }
    }
    return res;
})

拦截器是实现 "无感刷新" 的核心:

  • 请求拦截器自动为每个请求添加 Access Token
  • 响应拦截器在收到 401 错误时,自动尝试刷新令牌
  • 使用isRefreshing和refreshPromise避免并发刷新请求
  • 刷新成功则用新令牌重试原请求,失败则引导用户重新登录

登录页面组件:处理身份验证

<script setup>
import request from "../server/request";
import { useRoute } from "vue-router";
import { watch, ref } from "vue";
const route = useRoute();
const resource = ref("");
const token = localStorage.getItem("token");
function windowPostMessage(token, resource) {
  if (window.opener) {
    window.opener.postMessage({ token }, resource.value)
  }
}
watch(
  () => route.query.resource,
  (val) => {
    resource.value = val ? decodeURIComponent(val) : "";
    if (token) {
      windowPostMessage(token, resource.value)
    }
  },
  { immediate: true }
);
function login() {
  request.get("/auth/login").then((res) => {
    const apitoken = res.data.data;
    localStorage.setItem("token", apitoken);
    windowPostMessage(apitoken, resource.value)
    window.location.href = `${resource.value}?token=${apitoken}`;
    window.close()
  });
}
</script>

登录页面处理用户身份验证流程:

  • 通过 URL 参数接收跳转来源(resource)
  • 登录成功后,通过 postMessage 通知父窗口
  • 将新令牌通过 URL 参数传递给来源页面
  • 关闭登录窗口,完成登录流程

双 Token 机制操作流程演示

为了更直观地理解双 Token 机制的实际运行过程,以下是一个 GIF 演示,展示了从用户登录到令牌刷新、访问资源以及登出的完整操作流程:

双tokenPlus.gif

演示内容说明:

  1. 用户未登录时在Home页面跳转登录页面后输入信息并登录,前端获取 Access Token 并存储,Refresh Token 通过 Cookie 存储
  1. 登录后访问受保护资源/api1,请求头携带 Access Token,成功获取资源
  1. 等待 Access Token 过期后再次访问/api1,前端拦截 401 错误,自动调用刷新接口获取新令牌
  1. 使用新的 Access Token 重新请求/api1,成功获取资源,用户无感知
  1. 点击登出按钮,前端清除本地存储的 Access Token,后端吊销 Refresh Token 并清除 Cookie

通过这个演示可以清晰看到,整个过程中用户无需多次输入账号密码,在 Access Token 过期时实现了无感续期,既保证了安全性又提升了用户体验。

双 Token 机制的安全性考量

双 Token 机制的安全性建立在多个层面的防护措施上:

  1. 令牌存储安全
  • Access Token 存储在 localStorage,便于前端管理但存在 XSS 风险
  • Refresh Token 存储在 httpOnly Cookie,防止前端 JS 访问,抵御 XSS 攻击
  1. 通信安全
  • 生产环境应启用 HTTPS,防止令牌在传输过程中被窃听
  • 合理设置 Cookie 的 secure 属性,确保仅通过 HTTPS 传输
  1. 令牌生命周期
  • Access Token 短期有效,减少被盗用后的风险窗口
  • Refresh Token 长期有效但支持主动吊销,平衡安全性与用户体验
  1. 防御机制
  • 令牌旋转机制确保 Refresh Token 只能使用一次
  • sameSite Cookie 属性降低 CSRF 攻击风险
  • 严格的令牌验证逻辑防止无效令牌被使用

总结与扩展

双 Token 机制通过 Access Token 和 Refresh Token 的协同工作,在安全性和用户体验之间取得了出色的平衡。本文提供的完整代码实现了从令牌签发、验证、刷新到登出的全流程,包含了前端和后端的关键处理逻辑。

在实际应用中,还可以进一步扩展:

  • 使用 Redis 等分布式存储替换内存存储,支持集群部署
  • 实现令牌黑名单机制,处理已吊销但未过期的令牌
  • 添加令牌撤销通知,在用户修改密码等场景立即失效所有令牌
  • 结合 JWT(JSON Web Token)实现无状态令牌验证,减轻服务器负担

通过理解和实践双 Token 机制,开发者可以为 Web 应用构建更加安全、可靠的认证系统,为用户提供流畅的使用体验同时保障系统安全。

使用 HashMap 提高性能的小技巧

作者 渣哥
2025年9月10日 09:32

原文来自于:zha-ge.cn/java/44

使用 HashMap 提高性能的小技巧

最近在重构一个老项目,每次看到满屏的HashMap,我总会想起曾经遇到的那些坑。虽然HashMap用起来简单,但真要优化性能,还真有几个小技巧值得分享。

初识 HashMap 的性能问题

刚开始使用HashMap时,我们通常会直接使用默认构造器:

Map<String, Object> map = new HashMap<>();

这种方式虽然简单,但在处理大量数据时可能会遇到性能瓶颈。比如,频繁的resize操作会导致垃圾回收压力增大,进而影响应用性能。

优化 HashMap 的初步尝试

经过一些研究,我发现指定初始容量可以有效减少resize的次数:

Map<String, Object> bigMap = new HashMap<>(2048);

通过预估数据量并设置合理的初始容量,可以显著减少HashMap的扩容次数,从而提升性能。

常见性能问题及解决方案

在使用HashMap时,我们可能会遇到以下问题:

  • 问题1: 初始容量设置不合理,导致频繁扩容。
  • 问题2: 存入大量null键或值,引发NullPointerException
  • 问题3: 错误地认为HashMap是线程安全的,导致多线程环境下数据不一致。
  • 问题4: 自定义对象作为键时,hashCode()equals()方法实现不当,导致哈希冲突。

例如,如果一次性向HashMap中插入大量数据,而没有指定初始容量,可能会导致频繁的resize操作,从而影响性能:

// 不良示例:未指定初始容量,导致频繁resize
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < 100000; i++) {
    map.put("key" + i, "value" + i);
}

提升 HashMap 性能的实用技巧

为了更好地利用HashMap,我们可以参考以下建议:

  • 1. 合理设置初始容量
    根据预计的数据量,合理设置HashMap的初始容量。如果不确定数据量,可以通过以下公式估算:

    new HashMap<>((int)(targetSize / 0.75f) + 1);
    

    其中,0.75是默认的负载因子。

  • 2. 避免使用null键或值
    HashMap允许使用null键或值,但在实际使用中应尽量避免,以防止NullPointerException的发生。

  • 3. 使用不可变对象作为键
    键对象应尽量使用不可变类(如String),并确保hashCode()equals()方法实现正确。

  • 4. 多线程环境下使用ConcurrentHashMap
    如果需要在多线程环境下使用HashMap,建议使用ConcurrentHashMap以保证线程安全。

  • 5. 避免频繁调用remove方法
    remove方法可能会导致HashMap的容量调整,影响性能。如果需要频繁删除元素,可以考虑其他数据结构。

代码示例:合理设置初始容量

以下是一个合理设置HashMap初始容量的示例:

int estimatedSize = 10000;
// 根据负载因子0.75计算初始容量
Map<String, Object> optimizedMap = new HashMap<>((int)(estimatedSize / 0.75f) + 1);

总结

通过合理设置初始容量、避免使用null键或值、使用不可变对象作为键等方法,我们可以有效提升HashMap的性能。记住,HashMap并非万能,选择合适的数据结构才能事半功倍。

最后提醒: 在实际开发中,一定要根据具体场景选择合适的数据结构,并通过性能测试验证优化效果。

nestJS 使用ExcelJS 实现数据的excel导出功能

2025年9月9日 14:28

nestJS 使用ExcelJS 实现数据的excel导出功能

下面展示如何在 NestJS 中使用 ExcelJS 实现 Excel 导出功能,并设置表头和数据行的样式(包括字体、颜色等)
  1. 安装必要的依赖:
npm install exceljs
npm install @nestjs/platform-express
  1. excel.controller.ts
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';
import { ExcelService } from './excel.service';

@Controller('excel')
export class ExcelController {
  constructor(private readonly excelService: ExcelService) {}

  @Get('export')
  async exportExcel(@Res() res: Response) {
    // 模拟数据 - 实际应用中可能来自数据库
    const mockData = [
      {
        id: 1,
        name: '张三',
        email: 'zhangsan@example.com',
        age: 28,
        registerDate: new Date('2023-01-15'),
        status: '活跃'
      },
      {
        id: 2,
        name: '李四',
        email: 'lisi@example.com',
        age: 32,
        registerDate: new Date('2023-02-20'),
        status: '活跃'
      },
      {
        id: 3,
        name: '王五',
        email: 'wangwu@example.com',
        age: 45,
        registerDate: new Date('2023-03-10'),
        status: '禁用'
      },
      {
        id: 4,
        name: '赵六',
        email: 'zhaoliu@example.com',
        age: 23,
        registerDate: new Date('2023-04-05'),
        status: '活跃'
      }
    ];

    try {
      // 生成 Excel 文件
      const buffer = await this.excelService.generateExcel(mockData);
      
      // 设置响应头,告诉浏览器这是一个 Excel 文件
      // 如果框架中封装了处理文件流返回的装饰器,直接
      res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
      res.setHeader('Content-Disposition', 'attachment; filename="用户数据报表.xlsx"');
      
      // 发送文件
      res.send(buffer);
      
      // 如果框架中封装了处理文件流返回的装饰器,直接创建可读流return即可
      // const stream = new StreamableFile(excelBuffer);
      // return stream
    } catch (error) {
      res.status(500).send('导出 Excel 失败: ' + error.message);
    }
  }
}

  1. excel.module.ts
import { Module } from '@nestjs/common';
import { ExcelController } from './excel.controller';
import { ExcelService } from './excel.service';

@Module({
  controllers: [ExcelController],
  providers: [ExcelService],
  exports: [ExcelService]
})
export class ExcelModule {}

  1. excel.service.ts
import { Injectable } from '@nestjs/common';
import * as ExcelJS from 'exceljs';

@Injectable()
export class ExcelService {
  async generateExcel(data: any[]): Promise<Buffer> {
    // 创建工作簿
    const workbook = new ExcelJS.Workbook();
    workbook.creator = 'NestJS Application';
    workbook.lastModifiedBy = 'NestJS Application';
    workbook.created = new Date();
    workbook.modified = new Date();
    workbook.lastPrinted = new Date();

    // 添加工作表
    const worksheet = workbook.addWorksheet('数据报表');

    // 定义表头
    const headers = [
      { header: 'ID', key: 'id', width: 10 },
      { header: '名称', key: 'name', width: 20 },
      { header: '邮箱', key: 'email', width: 30 },
      { header: '年龄', key: 'age', width: 10 },
      { header: '注册日期', key: 'registerDate', width: 15 },
      { header: '状态', key: 'status', width: 15 }
    ];

    // 设置表头
    worksheet.columns = headers;

    // 设置表头样式
    const headerRow = worksheet.getRow(1);
    headerRow.font = {
      name: 'Arial',
      size: 12,
      bold: true,
      color: { argb: 'FFFFFF' } // 白色字体
    };
    headerRow.fill = {
      type: 'pattern',
      pattern: 'solid',
      fgColor: { argb: '4285F4' } // 谷歌蓝背景
    };
    headerRow.alignment = {
      vertical: 'middle',
      horizontal: 'center'
    };
    headerRow.height = 20;

    // 添加数据行
    data.forEach((item, index) => {
      const row = worksheet.addRow(item);
      
      // 设置奇数行和偶数行不同样式
      const isEvenRow = (index + 1) % 2 === 0;
      
      row.font = {
        name: 'Arial',
        size: 11,
        color: { argb: '000000' } // 黑色字体
      };
      
      // 隔行变色
      if (isEvenRow) {
        row.fill = {
          type: 'pattern',
          pattern: 'solid',
          fgColor: { argb: 'F8F9FA' } // 浅灰色
        };
      }
      
      // 设置对齐方式
      row.alignment = {
        vertical: 'middle',
        horizontal: 'left'
      };
      
      // 设置行高
      row.height = 18;

      // 为状态列设置特殊样式
      const statusCell = row.getCell('status');
      if (item.status === '活跃') {
        statusCell.font = {
          ...statusCell.font,
          color: { argb: '008000' } // 绿色
        };
      } else if (item.status === '禁用') {
        statusCell.font = {
          ...statusCell.font,
          color: { argb: 'FF0000' } // 红色
        };
      }
      
      // 为日期列设置格式
      const dateCell = row.getCell('registerDate');
      dateCell.numFmt = 'yyyy-mm-dd';
    });

    // 设置所有单元格边框
    worksheet.eachRow({ includeEmpty: false }, (row) => {
      row.eachCell({ includeEmpty: false }, (cell) => {
        cell.border = {
          top: { style: 'thin' },
          left: { style: 'thin' },
          bottom: { style: 'thin' },
          right: { style: 'thin' }
        };
      });
    });

    // 转换为 buffer 并返回
    return await workbook.xlsx.writeBuffer();
  }
}

样式设置说明

  • 表头样式:使用蓝色背景、白色粗体 Arial 字体,垂直和水平居中
  • 数据行样式:使用 Arial 字体,隔行使用不同背景色区分
  • 状态列:根据状态值使用不同颜色(活跃为绿色,禁用为红色)
  • 日期列:设置了日期格式
  • 边框:所有单元格都添加了细边框

要使用这个功能,只需将 ExcelModule 导入到你的 AppModule 中,然后访问 /excel/export 即可下载生成的 Excel 文件。

Vibe Coding,这种技术面试形式会成为新的趋势吗?

作者 why技术
2025年9月8日 22:05

你好呀,我是歪歪。

前几天在网上冲浪的时候,我看到一则消息:

说实话,我看到标题的时候我就猜到,这个开发岗位极有可能是美团的岗位。

因为美团今年校招的时候已经有 AI 面试官的环节了。

同时,今年 5 月的时候,美团 CEO 王兴在某个会议上还表示:

目前的新代码中有 52% 左右是由 AI 生成的,有 90% 以上的工程师团队成员广泛使用 AI 编码工具,并将继续加大投资开发大语言模型。

6 月份的时候还发布了一个名叫 NoCode 的 AI 编程产品,主打“零基础用户通过对话生成网站和软件”,看起来就是“人人都是开发者”内味儿:

歪师傅去了解了一下,这个 NoCode,其实就王兴五月份提到的美团内部在使用的 AI 编程工具。

可以说,美团在 AI 化这方面,确实步子迈得挺大。

再说回校招。

前面提到了,美团已经有了 AI 面试官,在网上已经可以看到很多同学参加了 AI 面试。

这是参与了春招、秋招面试的同学,给出的 AI 面试官提出的问题:

可以看到面试的还是很全面的。有八股文、有场景题、还有系统设计类题目。

而从大多数同学的反馈来看,这场人机对话的面试体验居然还不错。

歪师傅工作这么多年,校招和社招的技术面试官都当过。

所以,如果站在技术面试官的角度,特别是一面的技术面试官,我觉得这个 AI 面试官非常棒,至少能帮面试官分担 60% 以上的工作量,效率提升非常明显。

但是,我也看到了美中不足的一个点:

就是 AI 面试官出的题,肯定是来源于它预设的题库。

不管他的题库多么丰富,即使包含了各种八股、场景、设计、算法类问题,它出的题都不会基于面试者的简历来。

但是“从简历出发”这个点在我看来,又是一个非常重要的点。

只有从简历中的经历切入,才能更真实地判断一个人的技术深度和思考方式。

如果简历没有亮点,再考察八股文也不迟。

不过话说回来,让 AI 作为面试官,在当下这个 AI 技术大爆炸的环境下,其实不算什么新鲜的事情。

这个事情,并配不上这篇文章的标题。

真正让我想要写这篇文章的,是我前面提到的这个:

“已经允许使用 AI 甚至二面内容是 Vibe Coding”

这句话,才配的上这篇文章标题中的“新的趋势”。

新的趋势

在我之前接触的信息中,面试者在面试过程中使用 AI,会被认为是作弊。

而在我的认知中,也认为这确实是在作弊。

但美团考察 “Vibe Coding” 这个做法,像是突然推开了一扇窗,让新鲜空气吹了进来:既然 AI 已成为程序员日常开发的一部分,为什么不能大大方方让它进入面试环节?

这不仅不是作弊,反而可能正是未来面试的形态。

首先,简单解释一下什么是 Vibe Coding?

“Vibe Coding”是 OpenAI 的一个联合创始人,在 2025 年初提出的一个相对较新的术语。

它指的是一种软件开发方法,开发者主要使用自然语言提示来引导 AI 工具生成、完善和调试代码。

这种方法强调速度和迭代开发,甚至可能让非程序员通过描述他们期望的功能,让 AI 来创建软件。

做个简单的类比。

我们作为程序员,在传统概念上的编程,你要去手写一行行代码。就像是你亲手用砖块砌一堵墙。你需要知道每块砖怎么放,水泥怎么抹。

但是在“Vibe Coding”的理念中,你就是建筑师。你对 AI(相当于施工队)说:“我想在这里建一堵墙,墙上是白色的、带一扇窗、再给我画一个和平鸽的墙绘。”

AI 就会按照你的要求把墙建好。

而你要做的,就是来检查这座墙是否符合你的要求,是否需要进行微调。

而在当下,在每一个程序员都早已已经感受过 AI 威力的 2025 年,我可以非常肯定地说:

在未来工作模式中,程序员编程,肯定离不开 AI。Vibe Coding 也一定是趋势。

所以,在这个趋势之下,我们的面试形式为什么不能变一变?

就变成筛选能适应未来工作模式的开发者。

我们应该筛选的,是那些能适应甚至主导这种协作模式的开发者。

  • 面对初级开发者,面试要求是:会使用 常见的 AI 编程工具完成日常任务,具有一定的“Vibe Coding”能力。
  • 面对中级开发者,面试要求是:独立运用 AI 编程工具完成复杂功能的开发。具备优秀的 Prompt 工程能力。能够利用 AI 工具进行技术调研、方案设计和可行性分析
  • 面对高级开发者,面试要求是:能够领导并制定团队级的 AI 开发流程与规范,能评估和引入新的 AI 开发工具和实践。能够将业务需求和领域问题转化为精准的技术语言和 Prompt。

而这一切,只有在允许使用 AI 的面试中,才能真实考察到。

我认为这才是未来面试的新趋势。 美团,做了一个很好的表率和尝试。

美团这一次做了一个很好的表率和尝试。

是一次思维范式的转向,从“考你会什么”逐渐转向“考你如何与 AI 合作完成什么”。

那么问题就来了

如果以后面试都可以使用 AI 了,那八股文还要背吗,算法还是要学吗?

这是一个很现实的问题。

我的答案是:更要学,但要换一种学法。

AI 再好,它也只是工具而已,在这个工具的加持下,能极大提升你的学习效率。

但是工具代替不了学习本身,不能替代你的知识体系和思维判断。

如果你连算法基础都没有,又怎么判断 AI 生成的代码是否正确?

如果你不了解基础框架、组件功能、数据库原理,又怎能判断出 AI 给出的架构设计是不是在一本正经的胡说八道?

工具解放的是重复劳动,而不是思考本身。

未来的面试不会淘汰八股文,只会把它进化成“新八股”:一半考原理,一半考如何用 AI 更好地实践原理。

这是一种属于 AI 时代的面试方式。

它强调的不再是记忆与熟练度,而是理解、协作与创造力。

这个面试方式肯定还有很多很多值得讨论的点,而且可以预见的是,它的落地还要经过很长一段时间的探索,还要走很长一段的路程。

但是我觉得这个方向是对的。

方向对了,就不怕路远。

而我们,此刻,正站在这条路的起点。

大话设计模式——观察者模式和发布/订阅模式的区别

2025年9月8日 18:28

观察者模式和发布/订阅模式,有相当多的程序员,尤其是前端,完全分不清他们之间的区别,甚至认为这两个设计模式是同一个。

其实这两个设计模式用一句话就可以区分其差别:观察者模式观察的是被观察者本身,而发布订阅模式订阅的是主题

从API设计到数据流转

image.png

观察者模式

观察者模式通常有两个核心api:

  • observable: 将数据包装为可观察对象
  • observer:观察可观察者
const obj$ = observable({name:'a'})
observer(obj$,(snapshot?)=>{console.log('change')})
obj$.name = 'b';// change

一般情况下,observer只需要知道该对象发生变化即可,如果需要知道变化内容,也仅提供快照或部分快照

发布订阅模式

发布订阅模式通常有三个api:

  • dispath(或emit)
  • subscribe(或on)
  • unsubscribe(或off)
const unsubscribe = messageCenter.subscribe('weather',(info)=>{console.log('天气',info)})
messageCenter.dispatch('weather','Sunny');// 天气: sunny
unsubscribe()//取消订阅

订阅的内容往往是一个主题,所以响应内容中必定带上该主题的内容。虽然一般都是订阅后再接收该主题内容,但也可以订阅后立即获取该主题历史内容,甚至设置推送频率、内容分组和过滤等。

概念陷阱

这里最容易产生理解偏差的地方就是发布订阅模式中的发布者和订阅者是谁?

上文代码中,表面上看,messageCenter自己订阅,自己发布,又是订阅者,又是发布者。但实际上,我们应该将调用订阅方法的地方看成是订阅者,将调用发布方法的地方,看成发布者。比如class A的某个方法中进行了订阅,则该class A的实例就是订阅者,class B的某个方法中调用了发布,则class B的实例,就是发布者。从这个角度看,发布者与订阅者是解耦的,两者互不关心对方的存在,都和messageCenter单向联系,因此,这里的messageCenter往往也叫做事件总线(EventBus),相当于用事件/消息这根线串起多个互不关联对象。

而观察者模式,观察者对被观察者有直接依赖,对被观察者的响应或通知,并不需要进行主题区分和内容响应,因为被观察者本身就是观察的内容,并且观察者本身就拥有或能直接获取到被观察者对象

html中获取容器部署的环境变量

2025年9月5日 17:25

在云端环境下,标准产品的租户定制,可以通过登录人获取租户信息,再获取租户配置,进行项目的功能或ui调整。

如果是非云端的组件型产品——既无用户也无租户,却又有不同属地特殊要求,如何知道该项目部署于哪个属地就成为一个比较头疼的问题。

一个比较轻量级的方案是,容器部署时,传入属地信息。比如web项目,在项目中维护一个nginx配置文件模板,编写docker-entrypoint.sh文件用于容器启动:

envsubst '$TENANT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf


# 启动 nginx
exec nginx -g "daemon off;"

在启动时,通过环境变量将属地信息注入nginx配置文件中:

server {
    ...
    location / {
      ...
      sub_filter "{tenant}" "${TENANT}";
    }
}

envsubst

envsubst 是 GNU gettext 工具集中的一个命令,功能是扫描文本中的环境变量占位符(如 ${VAR_NAME}),并替换为当前系统中对应环境变量的实际值

例如,若存在文本 Hello ${NAME}!,且环境变量 NAME="Docker",执行 envsubst 后会输出 Hello Docker!

sub_filter

sub_filter 的语法是 sub_filter "搜索字符串" "替换字符串",其中 “替换字符串” 可以是:

-   固定文本(如 `"123"``"固定内容"`-   Nginx 内置变量(如 `$remote_addr``$time_local`-   变量与文本的组合(如 `"时间:$time_local-123"`

这种替换对所有匹配 sub_filter_types 配置的响应类型生效(默认仅 text/html

在html中接收环境变量

html中可以通过{tenant}模板字符接收nginx替换后的变量,再将其存入localStorage中

<!doctype html>
<html lang="en">
 <head>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

   <title>demo</title>
   <script>
     window.localStorage.setItem('tenant', '{tenant}');
   </script>
 </head>
 <body>
  
 </body>
</html>

这种方式在一定程度上能够防止变量篡改——每次访问该html,都会被nginx重新注入变量

得物灵犀搜索推荐词分发平台演进3.0

作者 得物技术
2025年9月4日 11:19

一、背景

导购是指在购物过程中为消费者提供指引和帮助的人或系统,旨在协助用户做出更优的购买决策。在电商平台中,导购通过推荐热卖商品、促销活动或个性化内容,显著提升用户的购物体验,同时推动销售额的增长。其核心目标是通过精准的引导,满足用户需求并促进商业价值最大化。

词分发:导购的重要组成部分

在电商导购体系中,词分发作为关键环节,主要聚焦于与关键词推荐相关的功能。这些功能包括但不限于下拉词、底纹词、热搜榜单、锦囊词以及风向标等。这些推荐词能够帮助用户快速定位感兴趣的商品或服务,降低搜索门槛,提高购物效率。例如,下拉词可以在用户输入搜索内容时提供智能提示,而热搜榜单则能引导用户关注平台上的热门趋势。

词分发平台的价值与功能

为了进一步优化词推荐的效率与一致性,词分发平台应运而生。该平台致力于打造一个通用、高效的词推荐生态系统,通过集成多种算法、工具和通用服务接口,为公司内不同业务域提供灵活的词推荐支持。其主要优势包括以下几点:

  • 统一开发,降低成本:词分发平台通过提供标准化的服务和接口,避免了各业务域重复开发和维护词推荐功能的成本。不同团队无需从零开始构建推荐系统,只需调用平台提供的接口即可快速实现定制化的词推荐功能,大幅节省开发时间和资源。
  • 高灵活性,适应多场景:平台的模块化设计使其能够根据不同业务场景和需求进行快速调整。例如,针对促销活动、节假日特辑或特定品类推荐,平台可以动态调整推荐算法和词库,确保推荐内容的精准性和时效性。
  • 支持业务扩展,提升效率:通过统一的词分发平台,各业务域能够更专注于核心业务逻辑的开发,而无需过多关注底层推荐系统的技术细节。这不仅提升了运营效率,还为业务的快速扩展提供了技术保障。
  • 优化用户体验: 词分发平台通过整合先进的推荐算法和数据分析能力,能够为用户提供更精准、更个性化的搜索建议。例如,基于用户历史行为和实时趋势生成的推荐词,可以帮助用户更快找到目标商品,从而提升整体购物体验。

二、已支持场景

已支持社区、交易、营销30+导购场景。

个别场景示例

三、整体架构

业务架构

平台架构

整体平台架构

平台+脚本化架构

脚本热部署功能在词分发搜索推荐引擎中发挥了重要作用,其主要目标是通过动态加载机制处理策略频繁变更的链路。实现这一功能的核心在于定义统一的抽象方法(具备相同出入参),将具体逻辑下放到 SDK 中,并通过后台打包、配置和推送流程,在线服务通过反射机制快速加载实现代码,再结合 AB 配置选择适用脚本。这种方法显著提升了策略调整的灵活性,同时减少了服务器重启的成本和时间。

在具体实施中,首先需要设计并实现统一的抽象方法,确保接口标准一致。随后,将具体的实现逻辑封装到 SDK 中,方便服务器端动态接收和加载。后台则负责提供打包、配置和推送功能,将实现代码整理为统一的包形式。当链路策略需要更新时,开发人员只需将新的实现代码上传至后台,完成打包、配置和推送操作。

在线服务在检测到新推送后,利用反射机制加载具体实现,并根据 AB 配置选择适用的脚本运行。这种动态加载方式无需重启服务,即可实现策略的即时切换和优化。整体而言,这一方法不仅提高了系统对策略变更的响应速度,还降低了维护成本,同时增强了系统的可靠性和稳定性,为词分发搜索推荐引擎的持续优化提供了有力支持。

主工程底座和脚本工程

在业务迭代的代码编写中,通常分为两种类型:主工程底座和脚本工程。

  • 主工程底座主要负责实现抽象和通用层的代码逻辑,注重提供稳定的基础框架和通用功能,确保系统的整体架构和扩展性。
  • 相比之下,脚本工程更贴近具体业务需求和定制化场景,专注于实现与业务逻辑密切相关的功能模块。通过这种分工,主工程提供通用的技术支持,而脚本工程则灵活应对多样化的业务需求,从而实现开发效率与业务适配性的平衡。

脚本热部署架构的存在原因

脚本热部署架构的存在主要出于以下原因:

  • 灵活应对策略变更:通过动态加载脚本,系统能快速适应频繁更新的业务需求,无需重启服务。
  • 降低维护成本:统一抽象方法和 SDK 实现减少重复开发,后台打包推送简化更新流程。
  • 提升效率:反射机制和 AB 配置实现即时脚本切换,节省时间并优化资源使用。
  • 增强稳定性:动态调整策略而不中断服务,确保系统持续稳定运行。

四、架构演进3.0之图化

串行架构

之前词分发业务一般都可以抽象为“预处理->召回->融合->粗排->精排->结果封装”等固定的几个阶段,每个阶段通常是有不同的算法或工程同学进行开发和维护。为了提升迭代效率,通过对推荐流程的抽象,将各阶段的逻辑抽象为“组件"+"配置”,整体的流程同样是一个配置,统一由“编排引擎”进行调度,同时提供统一的埋点/日志等。让工程或算法同学可以关注在自己的业务模块和对应的逻辑,而框架侧也可以做统一的优化和升级。

图化引擎架构演进

那为什么要去做“图化”/“DAG”呢?其实要真正要回答的是:  如何应对上面看到的挑战?如何解决目前发展碰到的问题?

从业界搜推领域可以看到不约而同地在推进“图化”/“DAG”。 从TensorFlow广泛采用之后,我们已经习惯把计算和数据通过采用算子(Operation)和数据(Tensor)的方式来表达,可以很好的表达搜索推荐的“召回/融合/粗排/精排/过滤”等逻辑,图化使得大家可以使用一套“模型”语言去描述业务逻辑。DAG引擎也可以在不同的系统有具体不同的实现,处理业务定制支持或者性能优化等。

通过图(DAG)来描述我们的业务逻辑,也带来这些好处:为算法的开发提供统一的接口,采用算子级别的复用,减少相似算子的重复开发;通过图化的架构,达到流程的灵活定制;算子执行的并行化和异步化可降低RT,提升性能。

图化是一种将业务逻辑抽象为有向无环图(DAG)的技术,其中节点代表算子,边表示数据流。不同的算子可以组合成子图,起到逻辑更高层封装的作用,子图的输出可供其他子图或算子引用。通过图化,策略同学的开发任务得以简化,转变为开发算子并抽象业务数据模型,而无需关注“并行化”或“异步化”等复杂逻辑,这些由 DAG 引擎负责调度。算子设计要求以较小粒度支持,通过数据流定义节点间的依赖关系。

图化引入了全新的业务编排框架,为策略同学提供了“新的开发模式”,可分为三部分:一是定义算子、图和子图的标准接口与协议,策略同学通过实现这些接口来构建业务逻辑图;二是 DAG 引擎,负责解析逻辑图、调度算子,确保系统的性能和稳定性;三是产品化支持,包括 DAG Debug 助手协助算子、图和子图的开发与调试,以及后台提供的可视化管理功能,用于管理算子、子图和图。整体架构可参考相关设计图。

图化核心设计和协议

节点‘算子’抽象封装——面向框架测

算子接口定义IDagTaskNodeExecutor

/**
 * dag 主节点注解
 */
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DagNodeMetaProcessor {
    
    /**
     * 算子名字
     * @return
     */
    String name();
    
    /**
     * 算子描述
     * @return
     */
    String desc() default "";
}
/**
 * 主工程节点任务-执行器
 *
 * @param <T>
 */
public interface IDagTaskNodeExecutor<T> {
    
    execute(DagStrategyContext dagStrategyContext);

图配置文件——面向框架测(使用者无需关心)

图分为图图,一个场景可以有多个图,可按实验制定不同的图;图定位为业务逻辑模版,可以将若干个独立算子组装为具有特定业务含义的“图”,图和算子一样可在场景大“图”中进行配置,即运行时可有多个“实例”,实现逻辑的复用和配置化。

面向业务使用者—如何配置

  • 节点自动注册:面向使用者无需关心JSON复杂的配置化,完全可视化操作。节点有俩种类型分主节点和脚本节点(可视化区分),节点注册完成框架测实现。
  • 业务关心编排关系:业务只需要关心节点之间编排关系即可。编排关系也是完全可视化拖拽实现。
  • 线程池隔离:一个服务内,不同场景线程池是隔离的,一个场景内,不同并行节点线程池也可以做到隔离,来区分强弱依赖关系。
  • 关联实验:一个场景有基础场景图,和实验图,实验图可以基于某个实验发布不同于场景的复杂实验图。

五、配套工具利器

脚本化开发&灰度发布CICD

自迭代流程图

去脚本化后台执行配置,首先选择对应环境的对应集群服务 (先预发验证,验证没问题,提merge给工程cr,合并后操作线上集群)。

脚本配置

如果是新加的脚步,选择配置,然后在配置页面对应类型的脚步后面选择新增,然后添加对应脚本类型的配置(一定要按类型添加,否则加载会失败),然后点击添加。

脚本构建

  • 配置完成后,选择cicd,进入cicd页面,首先选择新增cicd,然后会弹框,在弹框中选择你开发的分支,然后选择构建,这个时候构建记录会是打包中状态,然后等1到3分钟,刷新当前页面,查看状态是否为打包成功,如果为打包失败需要检查代码问题,如果是打包成功,操作栏会有同步操作;
  • 此次新增:在构建页面新增了构建日志和操作人两列信息。

    • 构建日志:点击详情会跳转到gitlab cicd日志详情(此次新增功能)
    • 操作人:会记录此次操作的具体人员,有问题及时联系相应同学(此次新增功能)

脚本发布

一次性全量发布(已有能力)

  • 当打包成功后,操作栏会有同步操作,点击同步,将当前打包的版本同步到集群。
  • cicd同步成功后,回到集群管理页面,这时点击操作里的发布操作,发布成功后,发布会变成同步,然后点击同步,同步成功后,这是集群中就已经加载到集群中,这就需要去ab实验配置具体的脚本然后验证。

灰度发布

  1. 通过cicd页面,构建完jar包后,点击右侧【灰度发布】按钮。

  1. 跳转到灰度发布页面
  • 基本信息如图显示,看图。
  • 发布间隔:第一批次5%,二批次30%,三批次60%,四批次100%;  当前流量xxx%(白名单验证)
  • 发布时,可以填写第一批次灰度IP机器,可选。
  • 当发布到第几批的时候,页面显示高亮
  • 系统一共默认四批次,首次点击发布是第一批,默认第一批暂停,再次点击发布,后面三批自动发布(间隔30s)
  • 如果发现异常变多或者RT变高,可马上回滚,点击回滚即可回滚上个版本
  • 如果一切正常,第四批就是全部推全操作,灰度jar包覆盖基础jar包。
  • 发布过程中,灰度的流量可以进行观察相应的QPS、RT、ERROR、和各个阶段召回、排序、打散等核心模块的性能和调用量。

  1. 灰度中的jar包,列表表格状态显示灰度流量

  1. 在集群维度,有俩个jar,一个是灰度中的jar, 另外一个是基础base的jar。 表格显示如下:

DIFF评估平台

社区搜索评测平台是面向于内部算法、产品、研发同学使用的评测系统,主要用于建设完善得物社区搜索badcase评估标准体系,致力于提升用户搜索体验和搜索算法问题发现及优化两方面,提供完善的评测解决方案。

核心功能包含:query数据抽取、快照数据抓取、评测数据导出和评测标注结果效果统计分析。

干预平台

搜索底纹词、猜搜词、下拉词在搜索链路的前置环节出现,在用户没有明确的搜索需求时,对激发用户搜索需求有较大的作用,因此,这些场景既是资源位也需要严格把控出词质量。本需求计划在上述场景支持干预能力,支持在高热事件时干预强插,也支持干预下线某些不合适的词。

召回配置平台

在现代的搜索引擎系统中,多路召回是一个非常重要的组件,其决定了搜索引擎的性能和准确性。因此,多路召回的配置和管理,对于搜索引擎系统的性能、稳定性和可维护性来说是至关重要的。

在以前的词分发系统中,多路召回的配置是以JSON字符串的形式存在的。每次修改配置都需要对这个JSON进行手动的编辑,该过程非常耗费时间,随着召回路的增多,配置效率也会越来越低,而且这种方式容易出错。因此,我们需要一种更加高效、可视化的方法来管理和配置多路召回。

为了提高多路召回的配置效率和准确性,我们需要一种可视化的后台工具来替代手动修改JSON字符串的方式。这样的后台工具可以将多路召回的配置以更加直观和可视化的方式展示出来,让配置人员能够直接在页面上进行配置和修改,从而减少手动编辑JSON字符串的错误和繁琐性。

通过使用可视化的后台工具,我们可以方便地管理和配置各种算法和策略,从而大大提高搜索引擎系统的性能和可维护性。可视化的后台工具对于提高搜索引擎的性能和可维护性非常重要,它可以大大简化配置人员的操作难度和减少错误,进一步提高搜索引擎系统的效率、可靠性和灵活性。

单路配置

多路配置

当然还有其他基建和配套工具和基建服务支撑,这里不一一展开了。

六、未来规划

词分发平台作为搜索引擎系统中的核心组成部分,负责管理和分配搜索词汇的处理与召回流程。其架构以灵活性和扩展性为核心,参考图示所示,平台通过模块化设计(如 Java 框架 Spring 容器、词分发平台主工程、依赖注入 Spring 容器、日志调试能力等)支持高效运行。为了适应市场需求的不断变化,未来词分发平台需从以下几个方面持续优化:

  • 平台建设:进一步完善灵犀平台功能,包括继承监控大盘,监控维度扩展,召回配置和脚本cicd建设,发布流水线接入等等。
  • 基座框架代码和工具完善:脚本框架改造2.0,无缝对接spring容器;构建可维护完善算字库。通过优化现有流程和算法,加速词汇处理与召回的速度,确保平台性能的持续提升。
  • 扩展场景:快速接入更多新场景,如商详触达,小蓝词等等。

此外,未来平台将联合算法团队,打破词圈品与品圈词之间的数据孤岛,打通相关链路,从而全面提升词分发平台的智能化与功能性。这一战略将推动平台更好地服务多样化业务需求,为用户提供更精准、高效的搜索体验。

往期回顾

1. R8疑难杂症分析实战:外联优化设计缺陷引起的崩溃|得物技术

2. 可扩展系统设计的黄金法则与Go语言实践|得物技术 

3. 得物新商品审核链路建设分享

4. 营销会场预览直通车实践|得物技术

5. 基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术

文 / 子房

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

5个技巧让文心快码成为你的后端开发搭子

作者 百度Geek说
2025年8月28日 16:49

本期内容4年开发经验的 Java 大佬执墨为我们带来了包括规则配置在内的5个文心使用技巧分享。助你增加 AI Coding 技能点!一起来看看!

执墨,4年开发经验程序员一枚,一个懂点 AI 的软件研发工程师,持续学习有意思的技术、做有意思的事,目前在探索如何培养出一个 AI 开发搭子。

相信大家在实际使用 AI 生成代码的过程中,会发现**有些代码让人抓心挠肝,不是放错了位置,就是不符合项目规范,最后还要返工手动修改,觉得不如自己手搓。**但别担心,自己学写代码怎么说也学了三四年,玩游戏角色释放技能也有前摇,AI 写好代码当然也需要慢慢培养。从 Zulu 开始公测我就在使用文心快码,也算是小有经验。那么结合个人在 IntelliJ IDEA 中的使用体验,我总结了 Zulu 和 Chat 各自比较实用的技巧,希望能对大家有所帮助。

▎Zulu 调教指南:生成代码更可用

1.用 # 提供上下文

大语言模型本质上是基于前文内容来预测下一个最可能的词或代码片段。没有上下文,模型就无法准确判断下一步生成内容,因此对上下文的理解和利用是生成高质量代码的基础。在开发场景中,上下文不仅包含当前文件或代码片段,还涵盖项目结构、依赖关系、函数和变量作用域等。通过理解这些上下文,AI 能够更准确补全代码或生成符合项目风格的内容。

在文心快码中为 Zulu 添加上下文:Zulu 目前支持文件、文件夹和项目级别的上下文。开发者可以通过 # 操作符来唤起当前被索引的所有的文件,并添加为当前会话的上下文。

Step1:# 操作符唤起上下文菜单

e813a4977abd98ff6c65386cfa26ca06.jpg

Step 2:选择对应的文件或者目录。可以添加多个,如果没有选择上下文,则会默认使用当前项目为上下文环境。

b54a4078c4a7c2ca41d622c6e3e74aca.jpg

Step 3:上下文配置好以后,可以通过自然语言的方式来描述你需要让 AI 干的事情。

在这个例子中,我选择了 Cache.java 这个文件,然后让 Zulu“实现一个查询缓存时,根据类型做反序列化的函数,目标为 Redis 序列化”。这时 Zulu 就开始阅读上下文,立刻理解了我的需求,在 Cache 接口中进行修改,添加了目标功能。

a28bd1d6169f8fc9e066b70508afe7e5.jpg

2.善用命令自动执行

**Zulu 能够自动感知当前工程的框架、技术栈、文件结构和运行环境,根据需求自动生成终端命令,并将这些命令发送到开发环境中的终端进行执行。**这种能力在脚本语言的开发过程中,非常具有优势,比如 Python、JS 等。你无需关注需要使用什么框架,AI 自行去进行使用和安装,开发者能够更专注于业务逻辑的实现。

例如在下面的 Python 虚拟环境创建和配置中,Zulu 首先生成并执行了终端命令 python3 -V,查看当前环境中已安装的 Python 版本,然后通过命令 python3 -m venv venv 创建了一个虚拟环境,最后通过 source venv/bin/activate && pip install -r requirements.txt 激活虚拟环境并安装项目依赖。整个过程完全没有跳出 IDE,极好地维护了开发过程中的心流体验。

c6aadce7cb5220fd4cc6e357273d06cf.jpg

3.规则约束

当没有提供规则文件去约束 Zulu 的生成行为时,在进行一个新的项目开发过程中,很容易会生成一些不达预期的代码,也会魔改代码。了解到文心快码支持自定义 Rules,因此我为 Zulu 编写了执行的上下文约束,控制其代码的生成。下面将具体展开我撰写规则的思路。

3.1编码环境

**介绍当前的编码环境,说明当前项目的所使用的技术栈。这一步至关重要,就好比给 Zulu 描绘了一幅项目的蓝图,让它清楚自己所处的 “战场” 环境。**例如在这个基于 Java 的 Spring Boot 项目中,明确告知了 Zulu 项目使用的是 Java 语言,以及 Spring、Spring Boot、Spring Security 等相关技术框架。这样,Zulu 在生成代码时,就能遵循这些技术栈的规范和特点,生成与之适配的代码。

## 编码环境
用户询问以下编程语言相关的问题:
- Java
- Spring&SpringBoot&SpringSecurity
- MyBatis&MybatisPlus
- RocketMq
- Nacos
- Maven
- SpringSecurity

3.2代码实现指南

**这个部分说明当前项目具体的代码如何实现,比如数据库表的创建规范、用户上下文怎么获取、项目结构的含义等。这相当于给 Zulu 制定了一套详细的工作 SOP,让它生成的代码符合项目的特定要求。**在采用 DDD(领域驱动设计)方式实现代码的项目中,我为 Zulu 提供了如下的 SOP。

1. 项目使用 DDD 的方式来实现代码,你需要注意如下几点:
  1. 领域层和仓库层的入参都要使用 DO、仓库层的实体对象需要添加 PO 的后缀
  2. Application或者Service层的输出必须的DTO,接口层的返回可以是DTO 也可以自己定义 VO
  3. 每一层对应的对象都需要添加对应的后缀,并且后缀要全大写。如仓库层的实体 UserPO,领域层领域UserDO,应用层的DTOUserDTO
  4. 项目的类之间的转换需要使用 MapStruct 来完成
2. 在使用三方依赖的时候,需要将对应的依赖内容先添加到 Maven 依赖中
3. 所有的接口都按照 RestFul 的风格定义,并且你需要区分接口的使用场景,如:前端使用、OpenApi、小程序端使用。
  1. 如果你无法通过用户的上下文知道需要你生成的接口的使用场景,你可以再次询问用户
  2. 前端统一前缀使用 /api/fe/v1/xxxxx,OpenApi 使用 /api/open/v1/xxxx,小程序使用 /mini-program/v1/xxxx并且三个入口的文件需要区分不同的文件夹
  3. 对于批量查询接口,你需要涉及分页的能力,不能使用内存分页,只能在 DB 层面做分页,并且要考虑深分页的问题
  4.  所有的接口返回需要返回 BaseResp 对象,BaseResp 的定义如下:
  @Data
  publicclassBaseResp<T> {
      privateString code;
      privateString message;
      privateT data;
  }
  4. 对于应用层,需要注意如下几点:
      1. 函数的输入和输出都是 DTO

3.3总结历史记录

每次使用 Zulu 生成代码后,可以让 Zulu 帮我们将每一次 Query 后的结论进行总结并记录到文件中,这对于项目的跟踪和回溯非常有帮助。提示词如下:

## 历史记录
1. 针对你回答用户问题的答案,你需要将本次回答的内容记录到项目的根路径下的 .cursor-history 文件里,格式如下:
2025-11-11 10:10:10
变更内容如下:
1. 增加用户模块
2. 修改用户管理内容
3. 增加用户内容
涉及文件为:
xxxx.java
xxxx.java
2. 你需要按照倒序的方式记录这个历史纪录

这种详细的记录格式,能够清晰地展示每次代码生成的时间、变更内容以及涉及的文件,方便开发者随时查看和追溯项目的开发记录。而倒序记录使得最新的变更记录排在前面,开发者能够快速获取到最新的项目动态,提高了信息查找的效率。

▎Chat 隐藏技巧:编码交互更灵活

在研究 Zulu 的同时,我也发现了 Chat 功能比较好用的地方,下面想继续分享一些让编码过程更方便快捷的能力。

1.Inline Chat 行间会话

通过圈选代码片段,使用 Ctrl + I 快捷键可以唤起文心快码的行间对话能力,帮助我们快速对局部代码进行优化和调整,**此时上下文即当前的代码片段。

在对话框中输入需要 AI 完成的任务,Comate 会自动扫描代码并编辑,编辑完成后可以自行决定是否采纳或者忽略。

764962263d0a9b9055b561ae6e63332f.jpg2.Git Commit 快捷键提交代码

在完成一个功能模块的开发后,需要提交代码到版本库。一般来说需要手动梳理本次代码的修改内容,编写提交信息。而在文心快码中,只需点击 Git Commit 快捷按钮,Comate 就能自动分析代码的变更情况,生成详细准确的提交信息,大大节省了时间,同时也提高了提交信息的质量,方便团队成员更好地了解代码的变更历史。

在 Git Commit 的时候点击快捷按钮,可以快速总结本次代码的变更内容。

1bb9df02a5f599f068e308d1c8530356.jpg

▎案例实操

接下来就把这些小技巧应用在实际案例中,检验一下是否对开发流程有提效。

1. 实现一个社区自动签到脚本

文心快码在编写脚本方面具有显著优势,准确率极高。在实际工作中,我现在用到的脚本几乎都由文心快码完成,并且几乎都能一次性运行通过。**以实现一个社区自动签到脚本为例,具体操作步骤如下:

Step 1: 书写提示词,直接用自然语言描述即可,需要给出其接口定义信息和执行规则。

“给我写一个 python 脚本,实现接口签到和抽奖能力。

以下是签到接口的定义:

GET 接口:……(为保护隐私,此处略去)

以下是抽奖接口的定义:……

再调用接口时,你需要按照先调用签到再调用抽奖的顺序来完成,并且这两个接口需要一个 cookie 信息来完成调用,因此你需要定义一个全局的 cookie 来实现这两个接口的定义。”

6b7c3b1aac40f4c79124eff2ef98479d.jpg

Step 2: Zulu 后自行生成对应的文件

be98a802b5973c9b2510642593a6cd70.png

Step 3: 按照 Zulu 的提示执行对应的命令,然后这个脚本就成功实现完成了。

2. 实现一个约束之下的意图识别服务

这个案例的重点在于,在新的项目中如何将自己业务项目中的一些编码规则告知 AI,使生成的代码更符合团队的开发规范。

Step 1: 书写提示词,在提示词部分添加了“实用技巧”中的第3点规则约束部分提到的规则。

“我需要你实现一个意图识别的工程能力,它的主要功能是对外提供一个 OpenApi 的接口来根据用户的输入返回一个固定的意图。这个 OpenApi 的实现思路是:

1.查询本地的规则列表;然后进行匹配;

2.如果本地的规则都无法匹配,则调用第三方的 LLM 接口进行意图识别;

3.返回结果。

你的代码实现需要按照这个规则来:#.zulurules”

658c635c05d8d0e4e517bbff6864c6d1.png

完整规则见附录一

Step 2: Zulu 生成代码与总结

997e833b4718f7461461d30e6edf8b85.jpg

22d137547651552721300fb2fa8934ca.png

▎写在最后

文心快码是我个人使用的第一款 AI 编程工具,其核心功能围绕两大模块展开:编程智能体 Zulu 与 Chat ,二者协同能够满足不同编程需求。Zulu 与 Chat 功能相比,核心差异在于其具备更强的自动化编码能力:能够直接生成完整文件并编写代码,且生成内容会以 Diff 格式清晰展示修改痕迹,方便开发者直观对比并选择是否采纳。文心快码插件实现了与 JetBrains 系列 IDE 的深度集成——开发者无需离开熟悉的 IDE 环境,即可调用 AI 编程能力。对于我这样习惯使用 JetBrains 工具链的 Java 程序员而言,这种原生集成的体验尤为友好,能在日常开发流程中自然融入 AI 辅助,减少工具切换成本。

附录:Rules 示例

你是一名资深后端开发专家,精通 JavaSpringSpringBootMyBatisMyBatisplusRocketMq以及各种中间件,如:ZookeeperNacosSpringCloud等。你思维缜密,能够提供细致入微的答案,并擅长逻辑推理。你会仔细提供准确、事实性、深思熟虑的答案,并且在推理方面堪称天才
- 严格按照用户的需求执行。
- 首先逐步思考——用伪代码详细描述你的构建计划。
- 确认后,再编写代码!
- 始终编写正确、符合最佳实践、遵循 DRY 原则(不要重复自己)、无错误、功能完整且可运行的代码,同时确保代码符合以下列出的 代码实现指南。
- 优先考虑代码的易读性和简洁性,而不是性能。
- 完全实现所有请求的功能。
- 不要留下任何待办事项、占位符或缺失的部分。
- 确保代码完整!彻底验证最终结果。
- 简洁明了,尽量减少其他描述。
- 如果你认为可能没有正确答案,请明确说明。
- 如果你不知道答案,请直接说明,而不是猜测。
- **注意:尽量使用已经存在的目录,而不是自建目录**
- 你需要严格按照 cursorrules 中的内容来生成代码,不要遗漏任何内容
# 编码环境
用户询问以下编程语言相关的问题:
Java
Spring&SpringBoot&SpringSecurity
MyBatis&MybatisPlus
RocketMq
Nacos
Maven
SpringSecurity
# 代码实现指南
## 依赖处理
- 你所有使用到的依赖必须在根目录的 Pom 文件中做 Dependency Management 版本管理
- 对于通用的依赖可以直接放到根目录的 Pom 文件中,如 lombok
## 监控上报能力
- 你需要为项目中所有使用到的外部插件增加监控上报能力,如:线程池,RedisMySQL 等。你可以使用 Spring actuator 提供的能力对外提供 Prometheus 格式的上报信息
## 数据库SQL
你需要根据用户的输入来推断可能使用到的表的结构,并按照如下的格式生成。
- 其中 create_timeupdate_timecreate_userupdate_user 是必须拥有的字段。
extis_deleted可以根据用户的需求来选择添加
- 对于唯一索引,其需要同一个前缀为 ux_,如:ux_business_key_type;对于非唯一索引,需要同一个前缀为 idx_,如:idx_business_key_type
```CREATE TABLE `audit_log` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '账户日志ID',
`business_key` varchar(100) NOT NULL DEFAULT '' COMMENT '业务实体ID或索引,如账号名',
`business_type` smallint(6) unsigned NOT NULL DEFAULT '0' COMMENT '10000-账号,20000-邮箱,30000-ADKeeper,40000-远程账号',
`operate_desc` varchar(500) NOT NULL DEFAULT '' COMMENT '操作描述',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号',
`ext` json DEFAULT NULL COMMENT '扩展属性',
`is_deleted` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '是否删除0为未删除,1为删除',
`create_time` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间',
`update_time` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间',
`create_user` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人',
`update_user` varchar(32) NOT NULL DEFAULT '' COMMENT '更新人',
PRIMARY KEY (`id`),
KEY `idx_business_key_type` (`business_key`,`operate_type`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COMMENT='账户审计表'
```#编写代码时遵循以下规则:
- 你不能直接在根目录上创建 src 文件夹,而是要创建一个当前项目的子模块来完成代码生成,模块的名字默认为 当前项目名-api
- 项目使用 DDD 的方式来实现代码,你需要注意如下几点:
  - 使用充血模式和工厂模式的方式来完成项目代码的实现
  - 领域层和仓库层的入参都要使用 DO、仓库层的实体对象需要添加 PO 的后缀
  - Application或者Service层的输出必须的 DTO,接口层的返回可以是 DTO 也可以自己定义 VO
  - 每一层对应的对象都需要添加对应的后缀,并且后缀要全大写。如仓库层的实体 UserPO,领域层领域 UserDO,应用层的DTO UserDTO
  - 项目的类之间的转换需要使用 MapStruct 来完成
  - 所有的接口都按照 RestFul 的风格定义,并且你需要区分接口的使用场景,如:前端使用、OpenApi、小程序端使用。
  - 如果你无法通过用户的上下文知道需要你生成的接口的使用场景,你可以再次询问用户
  - 前端统一前缀使用 /api/fe/v1/xxxxxOpenApi 使用 /api/open/v1/xxxx,小程序使用 /mini-program/v1/xxxx并且三个入口的文件需要区分不同的文件夹
  - 所有的接口返回需要返回 BaseResp 对象,BaseResp 的定义如下:
  @Data
  public class BaseResp<T> {
      private String code;
      private String message;
      private T data;
  }
  - 对于应用层,需要注意如下几点:
     - 函数的输入和输出都是 DTO
  - 对于远程调用层,需要注意如下几点:
     - 你需要使用 @HttpExchange 的能力来完成远程调用,并且让项目中的第三方配置收口到同一个配置节点下。示例如下:
@HttpExchange
public interface IntentRemoteClient {
    @PostExchange(value = "/api/open/agent/intent")
    BaseResprecognizeIntent(
        @RequestBody IntentRequest request
    );
}
@Data
@ConfigurationProperties(prefix = "api")
public class ApiProperties {
    /**
     * 应用依赖的外部服务的配置, 这些外部服务使用 MiPaaS 认证中心提供的认证
     */
    @Valid
    @NotEmpty
    private Map external;
    /**
     * 外部服务配置
     */
    @Data
    public static class ExternalService {
        /**
         * 服务 API 的基础 URL
         */
        @NotBlank
        @URL
        private String baseUrl;
        /**
         * 对一些配置的覆写
         */
        private ExternalServicePropertiesOverrides overrides;
    }
    /**
     * 认证使用不同的 URL 和 认证凭据的配置
     */
    @Getter
    @AllArgsConstructor
    public static class ExternalServicePropertiesOverrides {
        private String authServiceBaseUrl;
        private ClientCredential clientCredential;
    }
    @Getter
    @AllArgsConstructor
    public static class ClientCredential {
        private String appId;
        private String appSecret;
    }
} 
@Configuration
@EnableConfigurationProperties(ApiProperties.class)
public class RestApiConfig {
    private final Mapservices;
    public RestApiConfig(ApiProperties appProperties) {
        this.services = appProperties.getExternal();
    }
    @Bean
    public IntentRemoteClient intentRemoteClient() {
        // 1. 获取服务对应的配置
        var svc = findServiceConfiguration("intent");
        // 2. 构建 Client
        var httpClient = HttpClient.create()
          .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
          .wiretap(true)
          .responseTimeout(Duration.ofSeconds(10));
        var client = WebClient.builder()
          .baseUrl(svc.getBaseUrl())
          .codecs(clientCodecConfigurer -> clientCodecConfigurer.defaultCodecs().maxInMemorySize(50 * 1024 * 1024))
          .clientConnector(new ReactorClientHttpConnector(httpClient))
          .filter(new AuthorizationAuthFilter(svc.getOverrides().getClientCredential().getAppId(),
            svc.getOverrides().getClientCredential().getAppSecret(), svc.getBaseUrl()))
          .build();
        var factory = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(client)).build();
        return factory.createClient(IntentRemoteClient.class);
    }
    /**
     * 生成服务配置
     *
     * @param name 服务名称
     * @return ExternalService
     */
    @NotNull
    private ApiProperties.ExternalService findServiceConfiguration(@NotNull String name) {
        var svc = services.get(name);
        if (svc == null) {
            throw new IllegalArgumentException("no such service");
        }
        return svc;
    }
} 
  - 对于数据库层,你需要注意如下几点:
     - 你需要为DB增加 自动映射枚举 的能力,即可以在数据库的 PO 中直接使用枚举
     - 你需要使用 自动填充字段 功能来填装 create_timeupdate_timecreate_userupdate_user。其中创建人和更新人可以通过 SpringSecurity 获取。
# 历史记录
针对你回答用户问题的答案,你需要将本次回答的内容记录到项目的根路径下的 .cursor-history 文件里,格式如下:
2025-11-11 10:10:10
变更内容如下:
1. 增加用户模块
2. 修改用户管理内容
3. 增加用户内容
涉及文件为:
xxxx.java
xxxx.java
你需要按照倒序的方式记录这个历史纪录

可扩展系统设计的黄金法则与Go语言实践|得物技术

作者 得物技术
2025年8月28日 14:02

一、 引言:为什么需要可扩展的系统?

在软件开发领域,需求变更如同家常便饭。一个缺乏扩展性的系统,往往在面对新功能需求或业务调整时,陷入“改一行代码,崩整个系统”的困境。可扩展性设计的核心目标是:让系统能够以最小的修改成本,适应未来的变化。对于Go语言开发者而言,利用其接口、并发、组合等特性,可以高效构建出适应业务演进的系统。

本文将从架构设计原则、编码实践、架构实现模式、验证指标到演进路线,系统讲解如何设计一个“生长型”系统。

二、可扩展系统的核心设计原则

2.1  开闭原则: 对扩展开放,对修改关闭

理论补充:

开闭原则是面向对象设计的基石之一。它要求系统中的模块、类或函数,应该对扩展新功能保持开放,而对修改现有代码保持关闭。这意味着,当需求变更时,我们应通过添加新代码(如新增实现类)来满足需求,而不是修改已有的代码逻辑。

Go语言的实现方式:

Go语言通过接口(Interface)和组合(Composition)特性,天然支持开闭原则。接口定义了稳定的契约,具体实现可以独立变化;组合则允许通过“搭积木”的方式扩展功能,而无需修改原有结构。

示例:数据源扩展

假设我们需要支持从不同数据源(如MySQL、S3)读取数据,核心逻辑是“读取数据”,而具体数据源的实现可能频繁变化。此时,我们可以通过接口定义稳定的读取契约:

// DataSource 定义数据读取的稳定接口(契约)
type DataSource interface {
    Read(p []byte) (n int, err error)  // 读取数据到缓冲区
    Close() error                      // 关闭数据源
}


// MySQLDataSource 具体实现:MySQL数据源
type MySQLDataSource struct {
    db *sql.DB  // 依赖MySQL连接
}


func (m *MySQLDataSource) Read(p []byte) (interror) {
    // 实现MySQL数据读取逻辑(如执行查询、填充缓冲区)
    return m.db.QueryRow("SELECT data FROM table").Scan(&p)
}


func (m *MySQLDataSource) Close() error {
    return m.db.Close()  // 关闭数据库连接
}


// S3DataSource 新增实现:S3数据源(无需修改原有代码)
type S3DataSource struct {
    client *s3.Client  // 依赖AWS S3客户端
    bucket string      // S3存储桶名
}


func (s *S3DataSource) Read(p []byte) (interror) {
    // 实现S3数据读取逻辑(如下载对象到缓冲区)
    obj, err := s.client.GetObject(context.Background(), &s3.GetObjectInput{
        Bucket: aws.String(s.bucket),
        Key:    aws.String("data.txt"),
    })
    if err != nil {
        return 0, err
    }
    defer obj.Body.Close()
    return obj.Body.Read(p)  // 读取数据到缓冲区
}


func (s *S3DataSource) Close() error {
    // S3客户端通常无需显式关闭,可根据需要实现
    return nil
}

设计说明:

  • DataSource接口定义了所有数据源必须实现的方法(Read和 Close),这是系统的“稳定契约”。
  • 当需要新增数据源(如S3)时,只需实现该接口,无需修改现有的MySQL数据源或其他依赖DataSource的代码。
  • 这一设计符合开闭原则:系统对扩展(新增S3数据源)开放,对修改(无需改动现有代码)关闭。

2.2 模块化设计:低耦合、高内聚

理论补充:

模块化设计的核心是将系统拆分为独立的功能模块,模块之间通过明确的接口交互。衡量模块化质量的关键指标是:

  • 耦合度:模块之间的依赖程度(越低越好)。
  • 内聚度:模块内部功能的相关性(越高越好)。

理想情况下,模块应满足“高内聚、低耦合”:模块内部功能高度相关(如订单处理模块仅处理订单相关逻辑),模块之间通过接口通信(如订单模块通过接口调用支付模块,而非直接依赖支付模块的实现)。

Go语言的实现方式:

Go语言通过包(Package)管理模块边界,通过接口隔离依赖。开发者可以通过以下方式提升模块化质量:

  • 单一职责原则:每个模块/包仅负责单一功能(如order包处理订单逻辑,payment包处理支付逻辑)。
  • 接口隔离:模块间通过小而精的接口交互,避免暴露内部实现细节。

示例:订单模块的模块化设计

// order/order.go:订单核心逻辑(高内聚)
package order


// Order 表示一个订单(核心数据结构)
type Order struct {
    ID     string
    Items  []Item
    Status OrderStatus
}


// Item 表示订单中的商品项
type Item struct {
    ProductID string
    Quantity  int
    Price     float64
}


// OrderStatus 订单状态枚举
type OrderStatus string


const (
    OrderStatusCreated  OrderStatus = "created"
    OrderStatusPaid     OrderStatus = "paid"
    OrderStatusShipped  OrderStatus = "shipped"
)


// CalculateTotal 计算订单总金额(核心业务逻辑,无外部依赖)
func (o *Order) CalculateTotal() float64 {
    total := 0.0
    for _, item := range o.Items {
        total += item.Price * float64(item.Quantity)
    }
    return total
}


// payment/payment.go:支付模块(独立模块)
package payment


// PaymentService 定义支付接口(与订单模块解耦)
type PaymentService interface {
    Charge(orderID string, amount float64error  // 支付操作
}


// AlipayService 支付宝支付实现
type AlipayService struct {
    client *alipay.Client  // 支付宝SDK客户端
}


func (a *AlipayService) Charge(orderID string, amount float64error {
    // 调用支付宝API完成支付
    return a.client.TradeAppPay(orderID, amount)
}

设计说明:

  • order包专注于订单的核心逻辑(如计算总金额),不依赖任何外部支付实现。
  • payment包定义支付接口,具体实现(如支付宝、微信支付)独立存在。
  • 订单模块通过PaymentService接口调用支付功能,与具体支付实现解耦。当需要更换支付方式时,只需新增支付实现(如WechatPayService),无需修改订单模块。

三、Go语言的扩展性编码实践

3.1 策略模式:动态切换算法

理论补充:

策略模式(Strategy Pattern)属于行为型设计模式,用于定义一系列算法(策略),并将每个算法封装起来,使它们可以相互替换。策略模式让算法的变化独立于使用它的客户端。

Go语言的实现方式:

Go语言通过接口实现策略的抽象,通过上下文(Context)管理策略的切换。这种模式适用于需要动态选择不同算法的场景(如缓存策略、路由策略)。

示例:缓存策略的动态切换

假设系统需要支持多种缓存(Redis、Memcached),且可以根据业务场景动态切换。通过策略模式,可以将缓存的Get和Set操作抽象为接口,具体实现由不同缓存提供。

// cache/cache.go:缓存策略接口
package cache


// CacheStrategy 定义缓存操作的接口
type CacheStrategy interface {
    Get(key string) (interface{}, error)       // 从缓存获取数据
    Set(key string, value interface{}, ttl time.Duration) error  // 向缓存写入数据
}
// redis_cache.go:Redis缓存实现


type RedisCache struct {
    client *redis.Client  // Redis客户端
    ttl    time.Duration  // 默认过期时间
}


func NewRedisCache(client *redis.Client, ttl time.Duration) *RedisCache {
    return &RedisCache{client: client, ttl: ttl}
}


func (r *RedisCache) Get(key string) (interface{}, error) {
    return r.client.Get(context.Background(), key).Result()
}


func (r *RedisCache) Set(key string, value interface{}, ttl time.Duration) error {
    return r.client.Set(context.Background(), key, value, ttl).Err()
}


// memcached_cache.go:Memcached缓存实现
type MemcachedCache struct {
    client *memcache.Client  // Memcached客户端
}


func NewMemcachedCache(client *memcache.Client) *MemcachedCache {
    return &MemcachedCache{client: client}
}


func (m *MemcachedCache) Get(key string) (interface{}, error) {
    item, err := m.client.Get(key)
    if err != nil {
        return nil, err
    }
    var value interface{}
    if err := json.Unmarshal(item.Value, &value); err != nil {
        return nil, err
    }
    return value, nil
}


func (m *MemcachedCache) Set(key string, value interface{}, ttl time.Duration) error {
    data, err := json.Marshal(value)
    if err != nil {
        return err
    }
    return m.client.Set(&memcache.Item{
        Key:        key,
        Value:      data,
        Expiration: int32(ttl.Seconds()),
    }).Err()
}


// cache_context.go:缓存上下文(管理策略切换)
type CacheContext struct {
    strategy CacheStrategy  // 当前使用的缓存策略
}


func NewCacheContext(strategy CacheStrategy) *CacheContext {
    return &CacheContext{strategy: strategy}
}


// SwitchStrategy 动态切换缓存策略
func (c *CacheContext) SwitchStrategy(strategy CacheStrategy) {
    c.strategy = strategy
}


// Get 使用当前策略获取缓存
func (c *CacheContext) Get(key string) (interface{}, error) {
    return c.strategy.Get(key)
}


// Set 使用当前策略写入缓存
func (c *CacheContext) Set(key string, value interface{}, ttl time.Duration) error {
    return c.strategy.Set(key, value, ttl)
}

设计说明:

  • CacheStrategy接口定义了缓存的核心操作(Get和Set),所有具体缓存实现必须实现该接口。
  • RedisCache和MemcachedCache是具体的策略实现,分别封装了Redis和Memcached的底层逻辑。
  • CacheContext作为上下文,持有当前使用的缓存策略,并提供SwitchStrategy方法动态切换策略。客户端只需与CacheContext交互,无需关心具体使用的是哪种缓存。

优势: 当需要新增缓存类型(如本地内存缓存)时,只需实现CacheStrategy接口,无需修改现有代码;切换缓存策略时,只需调用SwitchStrategy方法,客户端无感知。

3.2 中间件链:可插拔的请求处理流程

理论补充:

中间件(Middleware)是位于请求处理链中的组件,用于实现横切关注点(如日志记录、限流、鉴权)。中间件链模式允许将多个中间件按顺序组合,形成处理流水线,每个中间件可以处理请求、传递请求或终止请求。

Go语言的实现方式:

Go语言通过函数类型(func(http.HandlerFunc) http.HandlerFunc)定义中间件,通过组合多个中间件形成处理链。这种模式灵活且易于扩展,适用于HTTP服务的请求处理。

示例:HTTP中间件链的实现

假设需要为Web服务添加日志记录、限流和鉴权功能,通过中间件链可以将这些功能解耦,按需组合。

// middleware/middleware.go:中间件定义
package middleware


import (
    "net/http"
    "time"
    "golang.org/x/time/rate"
)


// Middleware 定义中间件类型:接收http.HandlerFunc,返回新的http.HandlerFunc
type Middleware func(http.HandlerFunc) http.HandlerFunc


// LoggingMiddleware 日志中间件:记录请求信息
func LoggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        // 记录请求方法和路径
        println("Request received:", r.Method, r.URL.Path)
        // 调用下一个中间件或处理函数
        next(w, r)
        // 记录请求耗时
        println("Request completed in:", time.Since(start))
    }
}


// RateLimitMiddleware 限流中间件:限制请求频率
func RateLimitMiddleware(next http.HandlerFunc, limiter *rate.Limiter) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next(w, r)
    }
}


// AuthMiddleware 鉴权中间件:验证请求令牌
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token != "valid-token" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next(w, r)
    }
}


// chain.go:中间件链组合
func Chain(middlewares ...Middleware) Middleware {
    return func(final http.HandlerFunc) http.HandlerFunc {
        // 反向组合中间件(确保执行顺序正确)
        for i := len(middlewares) - 1; i >= 0; i-- {
            final = middlewares[i](final)
        }
        return final
    }
}

使用示例:

// main.go:Web服务入口
package main


import (
    "net/http"
    "middleware"
    "golang.org/x/time/rate"
)


func main() {
    // 创建限流器:每秒允许100个请求,突发10个
    limiter := rate.NewLimiter(10010)
    
    // 定义业务处理函数
    handleRequest := func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World"))
    }
    
    // 组合中间件链:日志 → 限流 → 鉴权
    middlewareChain := middleware.Chain(
        middleware.LoggingMiddleware,
        middleware.RateLimitMiddlewareWithLimiter(limiter),
        middleware.AuthMiddleware,
    )
    
    // 应用中间件链到处理函数
    http.HandleFunc("/", middlewareChain(handleRequest))
    
    // 启动服务
    http.ListenAndServe(":8080"nil)
}

设计说明:

  • 每个中间件(如LoggingMiddleware、RateLimitMiddleware)专注于单一功能,通过Middleware类型定义,确保接口统一。
  • Chain函数将多个中间件按顺序组合,形成一个处理链。请求会依次经过日志记录、限流、鉴权,最后到达业务处理函数。
  • 新增中间件(如CORS跨域中间件)时,只需实现Middleware类型,即可通过Chain函数轻松加入处理链,无需修改现有中间件或业务逻辑。

四、可扩展架构的实现模式

4.1 插件化架构:热插拔的功能扩展

理论补充:

插件化架构允许系统在运行时动态加载、卸载插件,从而实现功能的灵活扩展。这种架构适用于需要支持第三方扩展或多租户定制的场景(如IDE插件、电商平台应用市场)。

Go语言的实现方式:

Go语言通过plugin包支持动态库加载,结合接口定义插件契约,可以实现安全的插件化架构。插件需实现统一的接口,主程序通过接口调用插件功能。

示例:插件化系统的实现

假设需要开发一个支持插件的数据处理系统,主程序可以动态加载处理数据的插件(如csv_parser、json_parser)。

// plugin/interface.go:插件接口定义(主程序与插件共享)
package plugin


// DataProcessor 定义数据处理插件的接口
type DataProcessor interface {
    Name() string                      // 插件名称(如"csv_parser")
    Process(input []byte) (output []byte, err error)  // 处理数据
}


// plugin/csv_parser/csv_processor.go:CSV处理插件(动态库)
package main


import (
    "encoding/csv"
    "io"
    "os"
    "plugin"
)


// CSVProcessor 实现DataProcessor接口
type CSVProcessor struct{}


func (c *CSVProcessor) Name() string {
    return "csv_parser"
}


func (c *CSVProcessor) Process(input []byte) ([]byteerror) {
    // 解析CSV数据
    r := csv.NewReader(bytes.NewReader(input))
    records, err := r.ReadAll()
    if err != nil {
        return nil, err
    }
    // 转换为JSON格式输出
    var result []map[string]string
    for _, record := range records {
        row := make(map[string]string)
        for i, field := range record {
            row[fmt.Sprintf("col_%d", i)] = field
        }
        result = append(result, row)
    }
    jsonData, err := json.Marshal(result)
    if err != nil {
        return nil, err
    }
    return jsonData, nil
}


// 插件的入口函数(必须命名为"Plugin",主程序通过此函数获取插件实例)
var Plugin plugin.DataProcessor = &CSVProcessor{}
// main.go:主程序(加载插件并调用)
package main


import (
    "fmt"
    "plugin"
    "path/filepath"
)


func main() {
    // 插件路径(假设编译为so文件)
    pluginPath := filepath.Join("plugins""csv_parser.so")
    
    // 加载插件
    p, err := plugin.Open(pluginPath)
    if err != nil {
        panic(err)
    }


        // 获取插件实例(通过接口类型断言)
    sym, err := p.Lookup("Plugin")
    if err != nil {
        panic(err)
    }
    processor, ok := sym.(plugin.DataProcessor)
    if !ok {
        panic("插件未实现DataProcessor接口")
    }


        // 使用插件处理数据
    inputData := []byte("name,age
张三,20
李四,25")
    output, err := processor.Process(inputData)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(output))  // 输出JSON格式数据
}

设计说明:

  • 接口定义:主程序定义DataProcessor接口,规定插件必须实现的方法(Name和Process)。
  • 插件实现:插件(如csv_parser)实现DataProcessor接口,并导出名为Plugin的全局变量(主程序通过此变量获取插件实例)。
  • 动态加载:主程序通过plugin.Open加载插件,通过Lookup获取插件实例,并转换为DataProcessor接口调用。

优势:

  • 主程序与插件解耦,插件的添加、删除或升级不影响主程序运行。
  • 支持热插拔:插件可以在运行时动态加载(需注意Go插件的局限性,如版本兼容性)。

4.2 配置驱动架构:外部化的灵活配置

理论补充:

配置驱动架构(Configuration-Driven Architecture)通过将系统行为参数化,使系统可以通过修改配置(而非代码)来适应不同的运行环境或业务需求。这种架构适用于需要支持多环境(开发、测试、生产)、多租户定制或多场景适配的系统。

Go语言的实现方式:

Go语言通过encoding/json、encoding/yaml等包支持配置文件的解析,结合viper等第三方库可以实现更复杂的配置管理(如环境变量覆盖、热更新)。

示例:配置驱动的数据库连接

假设系统需要支持不同环境(开发、生产)的数据库配置,通过配置文件动态加载数据库连接参数。

// config/config.go:配置结构体定义
package config


// DBConfig 数据库配置
type DBConfig struct {
    DSN         string `json:"dsn"`          // 数据库连接字符串
    MaxOpenConn int    `json:"max_open_conn"` // 最大打开连接数
    MaxIdleConn int    `json:"max_idle_conn"` // 最大空闲连接数
    ConnTimeout int    `json:"conn_timeout"`  // 连接超时时间(秒)
}


// AppConfig 应用全局配置
type AppConfig struct {
    Env  string   `json:"env"`   // 环境(dev/test/prod)
    DB   DBConfig `json:"db"`    // 数据库配置
    Log  LogConfig `json:"log"`   // 日志配置
}


// LogConfig 日志配置
type LogConfig struct {
    Level string `json:"level"` // 日志级别(debug/info/warn/error)
    Path  string `json:"path"`  // 日志文件路径
}
// config/loader.go:配置加载器(支持热更新)
package config


import (
    "encoding/json"
    "os"
    "path/filepath"
    "time"


        "github.com/fsnotify/fsnotify"
)


// LoadConfig 加载配置文件
func LoadConfig(path string) (*AppConfig, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close()
    
    var cfg AppConfig
    decoder := json.NewDecoder(file)
    if err := decoder.Decode(&cfg); err != nil {
        return nil, err
    }
    return &cfg, nil
}


// WatchConfig 监听配置文件变化(热更新)
func WatchConfig(path string, callback func(*AppConfig)error {
    watcher, err := fsnotify.NewWatcher()
    if err != nil {
        return err
    }
    defer watcher.Close()
    
    // 监听配置文件所在目录
    dir := filepath.Dir(path)
    if err := watcher.Add(dir); err != nil {
        return err
    }
    
    go func() {
        for {
            select {
            case event, ok := <-watcher.Events:
                if !ok {
                    return
                }
                // 仅处理写事件
                if event.Op&fsnotify.Write == fsnotify.Write {
                    // 重新加载配置
                    newCfg, err := LoadConfig(path)
                    if err != nil {
                        println("加载配置失败:", err.Error())
                        continue
                    }
                    // 触发回调(通知其他模块配置已更新)
                    callback(newCfg)
                }
            case err, ok := <-watcher.Errors:
                if !ok {
                    return
                }
                println("配置监听错误:", err.Error())
            }
        }
    }()
    
    // 保持程序运行
    select {}
}
// main.go:使用配置驱动的数据库连接
package main


import (
    "database/sql"
    "fmt"
    "config"
    _ "github.com/go-sql-driver/mysql"
)


func main() {
    // 加载初始配置
    cfg, err := config.LoadConfig("config.json")
    if err != nil {
        panic(err)
    }
    
    // 初始化数据库连接
    db, err := sql.Open("mysql", cfg.DB.DSN)
    if err != nil {
        panic(err)
    }
    defer db.Close()
    
    // 设置连接池参数(从配置中读取)
    db.SetMaxOpenConns(cfg.DB.MaxOpenConn)
    db.SetMaxIdleConns(cfg.DB.MaxIdleConn)
    db.SetConnMaxLifetime(time.Duration(cfg.DB.ConnTimeout) * time.Second)
    
    // 启动配置监听(热更新)
    go func() {
        err := config.WatchConfig("config.json"func(newCfg *config.AppConfig) {
            // 配置更新时,重新设置数据库连接池参数
            db.SetMaxOpenConns(newCfg.DB.MaxOpenConn)
            db.SetMaxIdleConns(newCfg.DB.MaxIdleConn)
            db.SetConnMaxLifetime(time.Duration(newCfg.DB.ConnTimeout) * time.Second)
            fmt.Println("配置已更新,数据库连接池参数调整")
        })
        if err != nil {
            panic(err)
        }
    }()
    
    // 业务逻辑...
}

设计说明:

  • 配置结构化:通过AppConfig、DBConfig等结构体定义配置的层次结构,确保配置的清晰性和可维护性。
  • 热更新支持:通过fsnotify监听配置文件变化,触发回调函数重新加载配置,并更新系统状态(如数据库连接池参数)。
  • 多环境适配:通过不同的配置文件(如config-dev.json、config-prod.json)或环境变量覆盖,实现不同环境的配置隔离。

优势:

  • 系统行为的调整无需修改代码,只需修改配置文件,降低了维护成本。
  • 支持动态调整关键参数(如数据库连接池大小、日志级别),提升了系统的灵活性和可观测性。

五、可扩展性的验证与演进

5.1 扩展性验证指标

为了确保系统具备良好的扩展性,需要从多个维度进行验证。以下是关键指标及测量方法:

指标 测量方法 目标值
新功能开发周期 统计新增一个中等复杂度功能所需的时间(包括设计、编码、测试) < 2人日
修改影响范围 统计修改一个功能时,需要修改的模块数量和代码行数 < 5个模块,< 500行代码
配置生效延迟 测量配置变更到系统完全应用新配置的时间 < 100ms
并发扩展能力 测量系统在增加CPU核数时,吞吐量的增长比例(理想为线性增长) 吞吐量增长 ≥ 核数增长 × 80%
插件加载时间 测量动态加载一个插件的时间 < 1秒

5.2 扩展性演进路线

系统的扩展性不是一蹴而就的,需要随着业务的发展逐步演进。以下是一个典型的演进路线:

graph TD
    A[单体架构] -->|垂直拆分| B[核心服务+支撑服务]
    B -->|接口抽象| C[模块化架构]
    C -->|策略模式/中间件| D[可扩展的分布式架构]
    D -->|插件化/配置驱动| E[云原生可扩展架构]
  • 阶段1单体架构:初期业务简单,系统以单体形式存在。此时应注重代码的可读性和可维护性,为后续扩展打下基础。
  • 阶段2核心服务+支撑服务:随着业务增长,将核心功能(如订单、用户)与非核心功能(如日志、监控)拆分,降低耦合。
  • 阶段3模块化架构:通过接口抽象和依赖倒置,将系统拆分为高内聚、低耦合的模块,支持独立开发和部署。
  • 阶段4可扩展的分布式架构:引入策略模式、中间件链等模式,支持动态切换算法和处理流程,适应多样化的业务需求。
  • 阶段5云原生可扩展架构:结合容器化(Docker)、编排(Kubernetes)和Serverless技术,实现资源的弹性扩展和自动伸缩。

六、结 语

可扩展性设计是软件系统的“生命力”所在。通过遵循开闭原则、模块化设计等核心原则,结合策略模式、中间件链、插件化架构等Go语言友好的编码模式,开发者可以构建出适应业务变化的“生长型”系统。

需要注意的是,扩展性设计并非追求“过度设计”,而是在当前需求和未来变化之间找到平衡。建议定期进行架构评审,通过压力测试和代码分析(如go mod graph查看模块依赖)评估系统的扩展性健康度,及时调整设计策略。

最后,记住:优秀的系统不是完美的,而是能够持续进化的。保持开放的心态,拥抱变化,才能在快速发展的技术领域中立于不败之地。

往期回顾

1. 得物新商品审核链路建设分享

2. 营销会场预览直通车实践|得物技术

3. 基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术

4. AI质量专项报告自动分析生成|得物技术

5. Rust 性能提升“最后一公里”:详解 Profiling 瓶颈定位与优化|得物技术

文 / 悟

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

得物新商品审核链路建设分享

作者 得物技术
2025年8月26日 14:20

一、 前言

得物近年来发展迅猛,平台商品类目覆盖越来越广,商品量级越来越大。而以往得物的上新动作更多依赖于传统方式,效率较低,无法满足现有的上新诉求。那么如何能实现更加快速的上新、更加高效的上新,就成为了一个至关重要的命题。

近两年AI大模型技术的发展,使得发布和审核逐渐向AI驱动的方式转变成为可能。因此,我们可以探索利用算法能力和大模型能力,结合业务自身规则,构建更加全面和精准的规则审核点,以实现更高效的工作流程,最终达到我们的目标。

本文围绕AI审核,介绍机审链路建设思想、规则审核点实现快速接入等核心逻辑。

二、如何实现高效审核

对于高效审核的理解,主要可以拆解成“高质量”、“高效率”。目前对于“高质量”的动作包括,基于不同的类目建设对应的机审规则、机审能力,再通过人工抽查、问题Case分析的方式,优化算法能力,逐步推进“高质量”的效果。

而“高效率”,核心又可以分成业务高效与技术高效。

业务高效

  • 逐步通过机器审核能力优化审核流程,以解决资源不足导致上新审核时出现进展阻碍的问题。
  • 通过建设机审配置业务,产品、业务可以直观的维护类目-机审规则-白名单配置,从而高效的调整机审策略。

技术高效

  • 通过建设动态配置能力,实现快速接入新的机审规则、调整机审规则等,无需代码发布,即配即生效。

Q2在搭建了动态配置能力之后,算法相关的机审规则接入效率提升了70%左右。

三、动态配置实现思路

建设新版机审链路前的调研中,我们对于老机审链路的规则以及新机审规则进行了分析,发现算法类机审规则占比超过70%以上,而算法类的机审规则接入的流程比较固化,核心分成三步:

  1. 与算法同学沟通定义好接口协议
  2. 基于商品信息构建请求参数,通过HTTP请求算法提供的URL,从而获取到算法结果。
  3. 解析算法返回的结果,与自身商品信息结合,输出最终的机审结果。

而算法协议所需要的信息通常都可以从商品中获取到,因此通过引入“反射机制”、“HTTP泛化调用”、“规则引擎”等能力,实现算法规则通过JSON配置即可实现算法接入。

四、商品审核方式演进介绍

商品审核方式的演进

人审

依赖商管、运营,对商品上架各字段是否符合得物上新标准进行人工核查。

机审

对于部分明确的业务规则,比如白底图、图片清晰度、是否重复品、是否同质品等,机审做前置校验并输出机审结果,辅助人工审核,降低审核成本,提升审核效率。

AI审核

通过丰富算法能力、强化AI大模型能力、雷达技术等,建设越来越多的商品审核点,并推动召回率、准确率的提升,达标的审核点可通过自动驳回、自动修改等action接管商品审核,降低人工审核的占比,降低人工成本。

五、现状问题分析

产品层面

  • 机审能力不足,部分字段没覆盖,部分规则不合理:
    • 机审字段覆盖度待提升
    • 机审规则采纳率不足
    • 部分机审规则不合理
  • 缺少产品配置化能力,配置黑盒化,需求迭代费力度较高:
    • 规则配置黑盒
    • 规则执行结果缺乏trace和透传
    • 调整规则依赖开发和发布
    • 缺少规则执行数据埋点

技术层面

  • 系统可扩展性不足,研发效率低:
    • 业务链路(AI发品、审核、预检等)不支持配置化和复用
    • 规则节点不支持配置化和复用

六、流程介绍

搭建机审配置后台,可以通过配置应用场景+业务身份+商品维度配置来确定所需执行的全量规则,规则可复用。

其中应用场景代表业务场景,如商品上新审核、商家发品预检、AI发品预检等;业务身份则表示不同业务场景下不同方式,如常规渠道商品上新的业务场景下,AI发布、常规商品上新(商家后台、交易后台等)、FSPU同款发布品等。

当商品变更,通过Binlog日志触发机审,根据当前的应用场景+业务身份+商品信息,构建对应的机审执行链(ProcessChain)完成机审执行,不同的机审规则不通过支持不同的action,如自动修正、自动驳回、自动通过等。

链路执行流程图如下:

七、详细设计

整体架构图

业务实体

ER图

含义解释

※ 业务场景

触发机审的应用场景,如新品发布、商家新品预检等。

※ 业务身份

对于某个应用场景,进一步区分业务场景,如新品发布的场景下,又有AI发品、常规发品、FSPU同款发品等。

※ 业务规则

各行业线对于商品的审核规则,如校验图片是否是白底图、结构化标题中的类目需与商品类目一致、发售日期不能超过60天等。同一个业务规则可以因为业务线不同,配置不同的机审规则。

※ 规则组

对规则的分类,通常是商品字段模块的名称,一个规则组下可以有多个业务规则,如商品轮播图作为规则组,可以有校验图片是否白底图、校验图片是否清晰、校验模特姿势是否合规等。

※ 机审规则

对商品某个商品字段模块的识别并给出审核结果,数据依赖机审能力以及spu本身

※ 机审能力

商品信息(一个或多个商品字段模块)的审核数据获取,通常需要调用外部接口,用于机审规则审核识别。

※ 业务&机审规则关联关系

描述业务规则和机审规则的关联关系,同一个业务规则可以根据不同业务线,给予不同的机审规则,如轮播图校验正背面,部分业务线要求校验全量轮播图,部分业务线只需要校验轮播图首图/规格首图。

机审执行流程框架

流程框架

通过责任链、策略模式等设计模式实现流程框架。

触发机审后会根据当前的业务场景、业务身份、商品信息等,获取到对应的业务身份执行链(不同业务身份绑定不同的执行节点,最终构建出来一个执行链)并启动机审流程执行。

由于机审规则中存在数据获取rt较长的情况,如部分依赖大模型的算法能力、雷达获取三方数据等,我们通过异步回调的方式解决这种场景,也因此衍生出了“异步结果更新机审触发”。

※ 完整机审触发

完整机审触发是指商品变更后,通过Binlog日志校验当前商品是否满足触发机审,命中的机审规则中如果依赖异步回调的能力,则会生成pendingId,并记录对应的机审结果为“pending”(其他规则不受该pending结果的影响),并监听对应的topic。

※ 异步结果更新机审触发

部分pending规则产出结果后发送消息到机审场景,通过pendingId以及对应的商品信息确认业务身份,获取异步结果更新责任链(与完整机审的责任链不同)再次执行机审执行责任链。

动态配置能力建设

调研

新机审链路建设不仅要支持机审规则复用,支持不同业务身份配置接入,还要支持新机审规则快速接入,降低开发投入的同时,还能快速响应业务的诉求。

经过分析,机审规则绝大部分下游为算法链路,并且算法的接入方式较为固化,即“构建请求参数” -> “发起请求” -> “结果解析”,并且数据模型通常较为简单。因此技术调研之后,通过HTTP泛化调用实现构建请求参数发起请求,利用规则引擎(规则表达式) 实现结果解析。

规则引擎技术选型

调研市面上的几种常用规则引擎,基于历史使用经验、上手难度、文档阅读难度、性能等方面综合考虑,最终决定选用QLExpress

HTTP泛化调用能力建设

※ 实现逻辑

  • 定义MachineAuditAbilityEnum统一的动态配置枚举,并基于MachineAuditAbilityProcess实现其实现类。
  • 统一入参为Map结构,通过反射机制、动态Function等方式,实现商品信息映射成算法请求参数;另外为了提升反射的效率,利用预编译缓存的方式,将字段转成MethodHandle,后续对同一个字段做反射时,可直接获取对应的MethodHandle,提升效率。
/**
 * 缓存类字段的MethodHandle(Key: Class+FieldName, Value: MethodHandle)
  */
private static final Map<StringMethodHandleFIELD_HANDLE_CACHE = new ConcurrentHashMap<>();


/**
 * 根据配置从对象中提取字段值到Map
 * @return 提取后的Map
 */
public Map<StringObjectfieldValueMapping(AutoMachineAlgoRequestConfig requestConfig, Object spuResDTO) {
    AutoMachineAlgoRequestConfig.RequestMappingConfig requestMappingConfig = requestConfig.getRequestMappingConfig();
    Map<StringObject> targetMap = Maps.newHashMap();
    //1.简单映射关系,直接将obj里的信息映射到resultMap当中


    //2.遍历复杂映射关系,value是基础类型
    //3.遍历复杂映射关系,value是对象


  
    return targetMap;
}


/**
 *  预编译FieldMapping
  */
private List<AutoMachineAlgoRequestConfig.FieldMappingcompileConfig(List<AutoMachineAlgoRequestConfig.FieldMapping> fieldMappingList, Object obj) {
 
    List<AutoMachineAlgoRequestConfig.FieldMapping> mappings = new ArrayList<>(fieldMappingList.size());
    //缓存反射mapping
    return mappings;
}


private Object getFieldValue(Object request, String fieldName) throws Throwable {
    String cacheKey = request.getClass().getName() + "#" + fieldName;
    MethodHandle handle = FIELD_HANDLE_CACHE.get(cacheKey);
    return handle != null ? handle.invoke(request) : null;
}
  • 基于实现@FeignClient注解,实现HTTP调用的执行器,其中@FeignClient中的URL表示域名,autoMachineAuditAlgo方法中的path表示具体的URL,requestBody是请求体,另外还包含headers,不同算法需要不同headers也可动态配置。
  • 返回结果均为String,而后解析成Map<String,Object>用于规则解析。
@FeignClient(
        name = "xxx",
        url = "${}"
)
public interface GenericAlgoFeignClient {


    @PostMapping(value = "/{path}")
    String autoMachineAuditAlgo(
            @PathVariable("path") String path,
            @RequestBody Object requestBody,
            @RequestHeader Map<String, String> headers
    );
   
    @GetMapping("/{path}")
    String autoMachineAuditAlgoGet(
            @PathVariable("path") String path,
            @RequestParam Map<String, Object> queryParams,
            @RequestHeader Map<String, String> headers
    );


}
  • 动态配置JSON。
{
    "url": "/ai-check/demo1",
    "requestMappingConfig": {
        "fieldMappingList": [
            {
                "sourceFieldName": "categoryId",
                "targetKey": "categoryId"
            },
            {
                "sourceFieldName": "brandId",
                "targetKey": "brandId"
            }
        ],
        "perItemMapping": {
            "mappingFunctionCode": "firstAndFirstGroundPic",
            "fieldMappingList": [
                {
                    "sourceFieldName": "imgId",
                    "targetKey": "imgId"
                },
                {
                    "sourceFieldName": "imgUrl",
                    "targetKey": "imgUrl"
                }
            ]
        }
    }
}

机审规则动态解析建设

※ 实现逻辑

  • 定义MachineAuditRuleEnum统一的动态配置枚举,并基于MachineAuditRuleProcess实现其统一实现类。
  • 搭建QLExpress规则引擎,为了提升QLExpress规则引擎的效率,同样引入了缓存机制,在机审规则配置表达式时,则触发loadRuleFromJson,将表达式转换成规则引擎并注入到缓存当中,真正机审流程执行时会直接从缓存里获取规则引擎并执行,效率上有很大提升。
// 规则引擎实例缓存
private static final Map<StringExpressRunner> runnerCache = new ConcurrentHashMap<>();


// 规则配置缓存
private static final Map<StringGenericEngineRule> ruleConfigCache = new ConcurrentHashMap<>();


// 规则版本信息
private static final Map<StringInteger> ruleVersionCache = new ConcurrentHashMap<>();


/**
 * 加载JSON规则配置
 * @param jsonConfig 规则JSON配置
 */
public GenericEngineRule loadRuleFromJson(String ruleCode, String jsonConfig) {


    //如果缓存里已经有并且是最新版本,则直接返回
    if(machineAuditCache.isSameRuleConfigVersion(ruleCode) && machineAuditCache.getRuleConfigCache(ruleCode) != null) {
        return machineAuditCache.getRuleConfigCache(ruleCode);
    }
    // 如果是可缓存的规则,预加载


  
    return rule;
}
  • 机审规则执行时,通过配置中的规则名称,获取对应的规则引擎进行执行。
/**
 * 根据规则名称执行规则
 * @param ruleCode 规则名称
 * @param context 上下文数据
 * @return 规则执行结果
 */
public MachineAuditRuleResult executeRuleByCode(String ruleCode, Map<StringObject> context, MachineAuditRuleProcessData ruleProcessData) {
    if (StringUtils.isBlank(ruleCode)) {
        throw new IllegalArgumentException("机审-通用协议-规则-规则名称不能为空");
    }


        //从缓存中获取规则引擎


    //基于规则引擎执行condition


    //统一日志
}

※ 配置demo

  • 动态配置JSON。
{
    "ruleCode": "demo1",
    "name": "规则demo1",
    "ruleType": 1,
    "priority": 100,
    "functions": [
    ],
    "conditions": [
        {
            "expression": "result.code == null || result.code != 0",
            "action": {
                "type": "NO_RESULT",
                "messageExpression": "'无结果'"
            }
        },
        {
            "expression": "result.data == 0",
            "action": {
                "type": "PASS",
                "messageExpression": "'机审通过"
            }
        },
        {
            "expression": "result.data == 1",
            "action": {
                "type": "REJECT",
                "messageExpression": "'异常结果1'",
                "suggestType": 2,
                "suggestKey": "imgId",
                "preAuditSuggestKey": "imgUrl"
            }
        },
        {
            "expression": "result.data == 2",
            "action": {
                "type": "REJECT",
                "messageExpression": "'异常结果2'",
                "suggestType": 2,
                "suggestKey": "imgId",
                "preAuditSuggestKey": "imgUrl"
            }
        }
    ],
    "defaultAction": {
        "type": "PASS"
    }
}

八、关于数据分析&指标提升

在经历了2-3个版本搭建完新机审链路 + 数据埋点之后,指标一直没有得到很好的提升,曾经一度只是维持在20%以内,甚至有部分时间降低到了10%以下;经过大量的数据分析之后,识别出了部分规则产品逻辑存在漏洞、算法存在误识别等情况,并较为有效的通过数据推动了产品优化逻辑、部分类目规则调整、算法迭代优化等,在一系列的动作做完之后,指标提升了50%+。

在持续了比较长的一段时间的50%+覆盖率之后,对数据进行了进一步的剖析,发现这50%+在那个时间点应该是到了瓶颈,原因是像“标题描述包含颜色相关字样”、“标题存在重复文案”以及部分轮播图规则,实际就是会存在不符合预期的情况,因此紧急与产品沟通,后续的非紧急需求停止,先考虑将这部分天然不符合预期的情况进行处理。

之后指标提升的动作主要围绕:

  • 算法侧产出各算法能力的召回率、准确率,达标的算法由产品与业务拉齐,是否配置自动驳回的能力。
  • 部分缺乏自动修改能力的机审规则,补充临时需求建设对应的能力。

经过产研业务各方的配合,以最快速度将这些动作进行落地,指标也得到了较大的提升。

往期回顾

1.营销会场预览直通车实践|得物技术

2.基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术

3.AI质量专项报告自动分析生成|得物技术

4.社区搜索离线回溯系统设计:架构、挑战与性能优化|得物技术

5.eBPF 助力 NAS 分钟级别 Pod 实例溯源|得物技术

文 / 沃克

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

营销会场预览直通车实践|得物技术

作者 得物技术
2025年8月21日 11:15

一、背景:活动会场的配置走查之痛

在电商营销中,会场是承载活动流量的核心阵地。得物的营销会场不仅覆盖520、七夕等活动节点,也支撑日常的"天天领券"、"疯狂周末"等高频运营场景。数据显示,会场的UV占比、GMV贡献、订单量均占平台重要比重。

然而,随着业务复杂度提升,会场配置面临三大挑战。

1.1 三大挑战

※ 多目标耦合

同一会场需同时满足不同运营GMV提升、拉新、促活等不同目标,导致配置策略叠加,复杂度激增。

※ 验证滞后性

传统方式需活动生效后才能验证效果,配置错误可能导致资损,修复成本高昂。

※ 跨团队协作低效

涉及搭建、招商、优惠、资产等6大系统,联调成本高,走查覆盖率仅60%。

1.2 会场的配置举例

二、  解决方案:全链路"痛点穿越"

2.1 痛点梳理

2.2 核心思路

通过模拟未来时间、指定用户人群、强制命中AB实验,实现**"上线未对外先验证"**,让运营和技术在配置完成后即可预览真实效果。

分层架构设计

方案选型

某一线电商大厂穿越  VS  得物-时间穿越 VS 其他。

成本范围可控性,以及业务特性使用效率考量;原理即定义预览模式,传参即为true来消费。

关键改造点

  • 搭建系统: 低成本高便捷自查和走查。
  • 投放系统 :新增travel_mode参数,透传至下游。
  • 招商系统 :各类型招商活动查询逻辑,支持未来时间过滤。
  • 优惠试算 :兼容"虚拟资产"参与计算,确保价格准确性。
  • 风险管控 :限制仅白名单用户可触发,禁止真实下单。

三、 落地效果

3.1 应用姿势

活动预演

模拟不同人群用户不同时间点的价格计算及会场效果及稳定性。

优惠叠加校验

验证"跨店满减+品类券+平台补贴+商家自建优惠+商家代金券"的组合逻辑。

人群定向测试

人群定向测试 :对比新老用户、成熟非成熟及特殊类目新等的价格分层效果。

3.2 效率提升

不需要重新复制相同活动模拟提前开始,加之商家自建活动和平台活动较多,模拟相同时间的各类活动成本较大,且不可能做到完全相同,使运营配合测试线上验证配置工作量下降50%(少配置一套)

 提前穿越预览可提前感知活动期间各类价格、价格标签及各类活动叠加的优惠试算,检查配置问题,让活动走查场景覆盖度从历史60%覆盖度提升到80%以上(历史走查只能走查商品流、活动开始后的价格、标签、资源位无法走查到,活动叠加类型不够全),也方便运营预览预期实际效果并时调整策略,同时减少配置风险。

一个账号即可实现所有人群、实验、组件会场的预览,资产与走查更高效

线上风险规避:避免如过往活动生效才能感知效果,风险前置;如有问题只能下线活动及资源位的止损;减少资损风险,避免多类型活动叠加破价M类事件。

快速check不同排期下不同人群、不同实验组用户在不同时间段的活动下的商品优惠价、营销标签以页面组件呈现。

3.3 落地效果分析

做得好的

我们的"穿越"方案通过轻量级改造,实现了全链路验证能力 ,为复杂营销系统的配置管理提供了标准化解法。其核心价值在于:

※ 风险前置化

将问题发现节点从"上线后"提前至"配置阶段"。

※ 效率最大化

一个二维码即可验证所有人群、实验、时间组合。

※ 成本最优

仅需接口参数改造,无需搭建完整灰度环境。

有待提升

  • 权益投放的咨询和领取暂未实现穿越。
  • 会场存在与商品详情页的价格试算、标签不一致问题。

四、 未来规划

扩展可应用的穿越场景:

  1. 频道穿越:承载产品化运营的频道同活动会场实现痛点穿越,提效自查走查。
  2. 商详页一致性 :建立价格版本号机制,解决会场与商详页价标不一致问题。
  3. 活动资源位:建立活动核心资源位排期可监听,可自动穿越预览。
  4. 权益投放 :在沙箱环境实现"领取→使用"全流程验证。

绿色部分是已经具备的基础能力,红色边框是未来规划去实现的业务线,如下方案非最终方案,基于改动范围和成本考量:

4.1频道

频道穿越概述:

  1. 痛点:较多频道偏产品线运营,每周末都会提前招商提前配置。
  2. 穿越实现方式:同会场,通过sence区分。
  3. 价值:频道实现后,可同理无成本拓展新品频道、补贴频道、打牌低价等。

App入口管控

测试包安装有名单管控,天然支持了白名单。

资源位

资源位穿越:

  1. 痛点:活动c端引流入口、重体验,对外前的配置走查费力。
  2. 范围:首页弹窗、活动tab、活动中通、购买feeds商卡、我的tab、穹顶。
  3. 价值:时间+人群+实验穿越减少运营流量计划重复配置,提前预览活动氛围和投放效果。

商详

商详穿越:

  1.  商详:商详价格与会场一致性、氛围、标签、导购自身商详样式实验等。
  2. 价值:时间+人群+实验穿越减少运营流量计划重复配置,提前预览活动氛围和投放效果。

五、  总结

穿越类型

  • 仅传时间:即业务处理上假定到了某一时间,uid由App自动获取,是否命中人群、实验,按真实查询星云、AB。
  • 仅传人群:即业务处理上按照当前时间处理,假定用户属于入参人群,去定位计划或招商活动。
  • 仅传实验:即业务处理上按照当前时间,用户实际人群,时间为入参实验value处理。
  • 都设定:即业务处理上按照目标时间、假定命中目标入参人群和目标AB实验value来处理业务。
  • 消费穿越入参方:严格按照接收什么,即命中什么,未接收的走实际业务查询来处理。

风险管控

  • App测试包的安装现有管控:加入测试白名单的得物账号才可以下载测试包,默认可安装测试包的机器都可穿越。
  • 穿越目的是检验个业务配置正确性、素材效果、全链路验证等,供咨询查询,避免写操作:比如创单支付、核销。

能力沉淀

  • 从客户端上developer工具的透传穿越(时间、人群、实验),基础能力沉淀后,各业务域拓展性强,对于新增业务穿越工作量大大降低,接入成本也相对较低。

往期回顾

1.基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术

2.AI质量专项报告自动分析生成|得物技术

3.Rust 性能提升“最后一公里”:详解 Profiling 瓶颈定位与优化|得物技术

4.Java volatile 关键字到底是什么|得物技术

5.eBPF 助力 NAS 分钟级别 Pod 实例溯源|得物技术

文 / 东陌

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

市面上有多少智能体平台

一、大模型智能体平台:扣子(coze)

字节跳动的扣子平台提供了丰富的功能,包括插件、系统、记忆库、工作流等,支持知识库和自定义的插件构建的机器人可以轻松的部署到多个平台,几乎不需要具备编程的基础模型插件。还有知识库等核心技术都已经进行了非常好的封装,支持多agent模式,允许用户创建多个专注于特定任务的单agent,并且可以统一管理。

二、大模型智能体平台:腾讯元器

腾讯元器的开发者可以通过插件、知识库、工作流等方式,快速的、低门槛的来打造高质量的智能体,支持发布到QQ、微信等平台的同时,也支持API的调用。

三、大模型智能体平台:智谱清言

中国版的对话语言模型与GLM大模型进行对话,基于ChatGLM中英双语大模型进行开发,通过万亿字符的文本与代码的预训练,结合有监督的微调技术,具备了通用问答、多轮对话、创意写作、代码生成、虚拟对话、AI画图、文档和图片解读等等的能力。

四、大模型智能体平台:百度灵境

百度灵境智能体支持低代码的开发模式,灵活度相对更高,可以一键分发到微信客服、微信公众号、Web端、H5以及百度灵境矩阵等主流渠道之上。基于这些渠道应用,还能够在百度搜索、百度信息流等主流场景下进行分发与挂载。

AI质量专项报告自动分析生成|得物技术

作者 得物技术
2025年8月14日 15:14

一、背景

在日常工作中,常需要通过各项数据指标,确保驱动版本项目进展正常推进,并通过各种形式报表数据,日常总结日报、周会进展、季度进行总结输出归因,分析数据变化原因,做出对应决策变化,优化运营方式,目前在梳理整理校准分析数据需要大量的时间投入、结合整体目标及当前进展,分析问题优化的后续规划。

常见形式

人工收集

数据来源依赖于各系统平台页面,通过人工收集校准后填写再通过表格公式计算,或者可以通过多维表格工作流触发通知等功能。

图片

quickbi报表

通过ODPS搭建自定义报表,实现快速收集数据,复制报表到飞书文档内进行异动分析。

图片

平台能力开发

通过代码开发文档导出能力,根据固定模板生成数据分析,该能力开发人力成本较高,需要针对不同平台数据源定制化开发。

图片图片

AI Studio智能体平台

研发效能团队基于开源Dify项目社区部署,可以根据需求自定义sop,多模型的可选项,选择最适合业务的模型。每个工作流节点可自定义流程的判断分析,轻松上线可投产的AI Agents。

Dify是一个支持工作流编排的AI智能体平台,轻松构建并部署生产级 AI 应用。其核心功能包含:

  1. 以工作流的方式编排AI应用,在工作流中可以添加LLM、知识库、Agent工具、MCP服务等节点,工作流支持分支流转、节点循环、自定义节点等高级能力项。

  2. 支持在工作流中调用公司内部的Dubbo/gRPC服务。(插件实现)

  3. 知识库管理,通过构建私有知识库以增强 LLM 的上下文。

  4. 与内部平台集成,支持H5页面嵌入、API的方式与内部平台集成。

  5. 主流模型集成,支持使用多种主流模型如DeepSeek、OpenAI等,支持多模态模型。

对标的业界产品有:

✅ 多模型选择(适配不同业务场景)

✅ 可视化工作流搭建(支持自定义SOP)

✅ 全链路可观测性(实时调试优化)

综上本期实践利用AI工作流平台针对报告进行生成分析输出,让使用方回归到聚焦数据归因分析上,减少数据收集分析、文档编写成本。

图片

二、应用实践

实践效果

整体分析数据从哪来->需要输出什么样的格式->优化模型输出结果,三步骤针对输出结果进行调优。

图片

自动化成熟度分析工作流搭建案例

图片

运用效果

图片图片

报告效果

图片

飞书机器人通知归因分析

图片图片

数据处理

图片

LLM:通过用户输入分析获取数据源请求格式,配置好对应数据的映射关系模型自行获取对应数据。

提示词输入

图片

格式化输出配置

图片

http请求:通过用户输入分析后的参数构造请求参数,通过固定接口拉取数据,支持curl导入功能。

图片图片

代码执行:支持python、js代码对结果数据进行处理过滤,提升分析结果准确性。

图片

模型提示词

如文档整体分为不同模块可设定不同模型节点处理,每个模块增加特定提示词处理节点内容,模型并行分析处理,提升输出稳定性和输出效率,再通过LLM输出整合进行整体输出。

图片

在模型输入上下文及用户输入,通过获取的数据指定输出格式,设定提示词,提供AI结合模板输出对应形式。

图片

通过衔接上下节点返回内容最终整合报表输出结果,统一输出样式格式。

图片

优化输出

切换可用模型

遇到模型输出不稳定或者未达到预期效果,可切换可用模型,寻找适配模型。

图片

设定模型预载参数

设定模型预载参数,提升模型输出准确度。

图片

优化增加提示词

优化增加提示词提升输出形式稳定性:角色定义 ->  字段映射 -> 模板说明 -> 实际数据填充 -> 输出格式定义。

`## 角色定义 你是一位接口自动化测试专家以及报告生成专家,负责将接口返回的数据映射字段结合模板输出一份有效的自动化成熟度报告-稳定性部分。

接口返回数据字段映射关系:

基础字段: bu_name:业务域名称。 parent_bu_id:业务域。

稳定性指标字段: total_auto_stability_score:稳定性评分 iter_case_success_rate: 迭代自动化成功率 iter_case_success_rate_cpp: 迭代自动化成功率环比 auto_case_failed_rate: 自动化失败率 auto_case_failed_rate_cpp: 自动化失败率环比 case_aigc_avg_score: 用例健壮有效性评分 case_aigc_avg_score_cpp: 有效性评分环比

模板:

2.2 自动化稳定性 用表格展示自动化稳定性,表格内容包含所有一级业务域、二级业务域。 表头按照顺序输出: 1、业务域 2、自动化稳定性评分 3、迭代自动化成功率 4、迭代自动化成功率环比 5、自动化失败率 6、失败率环比 7、用例健壮有效性评分 8、有效性评分环比

重点关注项:xxx --仅分析二级业务域的稳定性性指标字段,列出需重点关注指标。

模板说明:

1、以html格式输出,增加内容丰富度,不输出任何多余内容。 2、表格说明:表格需要包含所有业务域数据。不要省略或者缺少任何业务域数据,将所有业务域展示在同一个表格内。 3、表格行排序:根据评分从高到低排序。 4、环比字段说明:指标环比下降环比字段标记红色,环比提升字段标记绿色,不标记背景色。

任务说明

1、用户将提供接口返回的JSON数据。 2、根据接口数据和匹配字段映射关系。 3、结合模板以及模板说明html形式输出,不输出任何多余内容。 请你根据以上内容,回复用户,不需要输出示例。`

模板转换

输出的表格形式通过模板转化固定输出html表格形式,提升模型输出稳定性。

图片

输出形式

以markdown形式或以html形式输出,复制到飞书文档上进行输出。

html最终效果

图片

markdown最终效果

图片

飞书机器人通知归因分析

图片

生成飞书文档

支持飞书应用直接新建飞书文档,markdown形式输出。

图片

对话返回生成后的飞书文档地址及分析:

图片

三、总结

在日常工作中如何有效利用数据指标驱动项目进展,现有数据收集和分析流程中面临的挑战。通过手动收集数据、生成报表、平台开发等传统方式,需要投入大量时间和人力资源,导致工作效率低下。

为此,引入了研发效能AI 智能体平台,AI工作流平台不仅改进了数据处理方式,还提升了报告生成的效率和准确性,从而增强了业务洞察力。进一步丰富工作流和知识库,提高对核心数据指标的分析能力,并针对异常数据指标进行细致剖析,为团队提供更深入的指导和支持。

此外,相似场景的处理也可以借助AI工作流进行优化,有望在多个业务领域推广应用。

四、后续规划

  • 丰富工作流:丰富结合知识库,针对每项核心数据指标提升建议以及业务域现状给予业务域具体指导建议。

  • 明细下钻分析:获取对应数据指标异常后,结合明细数据进行分析,具体到用例、人员级别。

  • 类似场景可通过AI工作流处理:固定模板数据源报告类、周会均可使用该方法减少人工投入成本。

往期回顾

1.Rust 性能提升“最后一公里”:详解 Profiling 瓶颈定位与优化|得物技术

2.Valkey 单点性能比肩 Redis 集群了?Valkey8.0 新特性分析|得物技术

3.社区搜索离线回溯系统设计:架构、挑战与性能优化|得物技术

4.正品库拍照PWA应用的实现与性能优化|得物技术

5.得物社区活动:组件化的演进与实践

文 / 笠

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

腾讯元器的优点是什么

元器本身的优点有三个:

1、团队功能,一个团队可以有50人,支持创建5个。方便小企业和团队,在元器上以团队方式创建智能体、工作流、插件、知识库,并共享这些资产。也方便大家共同协助,相互学习,我创立了一个”浪洋洋和朋友们”的团队,欢迎大家加入。

2、有简单的官方说明文档,能在元器平台内直接加入官方群,有负责人解答。

3、免费TOKEN额度高,估计是用的人少,现在单个账号免费额度有1个亿,轻松实现人生小目标,用不完真的用不完。扣子的免费TOKEN额度很少了,初学者练手可以试试元器。

PaddleMIX推出扩散模型推理加速Fast-Diffusers:自研蒸馏加速方法FLUX-Lightning实现4步图像生成

作者 百度Geek说
2025年8月12日 16:00

背景:扩散模型推理成本亟待优化

扩散模型(Diffusion Models)近年来在高保真图像和视频生成上取得了令人瞩目的成果。然而,这类模型在推理阶段需要经过数十步乃至上百步的迭代去噪,每一步都要运行庞大的 U-Net 或 Transformer 模型,导致推理耗时巨大。对于高分辨率生成或视频生成等应用,迭代推理的开销更是呈指数级上涨,使实时应用变得非常困难。如何在不牺牲生成质量的前提加速扩散模型的推理,已成为学界和工业界共同关注的课题。

01

扩散模型推理优化的总体方案

基于上述需求,PaddleMIX 从模型蒸馏、模型推理缓存(Training-Free)以及深度学习框架编译优化等多个技术维度出发,打造了 Fast-Diffusers 扩散模型推理加速工具箱,便于开发者根据实际场景灵活组合运用,从而有效提升扩散模型的推理速度。在第一期中我们介绍了动态跳过冗余计算(SortBlock)、智能缓存复用特征(TeaBlockCache)和数学近似预测(FirstBlock-Taylor)等 Training-Free 加速算法,在保持与原始模型几乎一致的生成质量的同时,将扩散模型的推理速度提升了2倍以上。本期稿件将从蒸馏加速和框架高性能优化两个方面介绍 Fast-Diffusers 工具箱中扩散模型的加速策略。

image.png 图1 推理加速工具箱

▎蒸馏加速方案和框架性能优化

主流的扩散模型蒸馏加速方法包括有一致性模型(Consistency Models),渐进式蒸馏(Progressive Distillation)以及分布匹配蒸馏(Distribution Matching Distillation)等。一致性模型建立在概率流常微分方程(PF-ODE)上,使用一致性函数将 PF-ODE 轨迹上任何时间步的点映射到轨迹的起点,支持一步生成高质量样本,同时保留多步采样能力以平衡计算成本与生成质量。分布匹配蒸馏通过分布级对齐(Distribution Matching)而非路径级模仿,在保持图像质量的同时实现数量级的速度提升,通过要求学生模型生成的图像分布应与教师模型生成的分布的一致性,完成一步生成图像的过程。

PaddleMIX 最新发布的扩散模型工具箱 PPDiffusers 中,集成了一致性模型 PCM(Phased Consistency Distillation)和 DMD2(Improved Distribution Matching Distillation for Fast Image Synthesis)算法,同时 **PaddleMIX 推出自研蒸馏加速模型 FLUX-Lightning,实现4步快速的高质量高分辨率图像生成,生成效果超越业界开源和闭源模型,达到业界 SOTA 水平。**另外使用飞桨深度学习编译器 CINN 进一步优化推理性能,对比 torch compile、Onediff、TensorRT 等主流推理优化框架,推理性能取得了显著的性能提升。

02

FLUX-Lightning 简介

PPDiffusers 提出了基于 FLUX 的蒸馏加速模型 FLUX-Lightning,可以在4步极少步数下,生成高分辨率高质量的图像,定量指标和定性指标均超越业界开源和闭源模型,达到了业界 SOTA 水平。

ed12344b2bd438b105caa295d66253a0.png

图2 FLUX-Lightning 4步推理结果

我们提出的 FLUX-Lightning 模型主要包含4个部分,区间一致性蒸馏(Phased Consistency Distillation),对抗学习(Adversarial Learning),分布匹配蒸馏(Distribution Matching Distillation),矫正流损失(reflow loss),完整框架如下图所示。

image.png

图3 FLUX-Lightning 框架

▎区间一致性蒸馏

image.png

image.png

▎对抗学习

为了进一步提升少步数下的图像生成质量,FLUX-Lightning 模型引入了对抗学习(Adversarial Learning),使用 discriminator 在 latent space 判别真实样本和虚假样本。

discriminator 模型由冻结的 teacher denoiser 和多个可训练的 discriminator heads 组成,前者负责提取图像特征,后者负责进行判别工作。图3展示了以 FLUX 为 teacher denoiser 的 discriminator 模型结构,FLUX 包含19个 FluxTransformerBlock 和38个 FluxSignleTransformerBlock,共计57个 TransformerBlock,将每个 TransformerBlock 的输出的图像特征 hidden states 输入到可训练的 discriminator heads 中,discriminator heads 由多个卷积层和残差结构组成,判别输入样本为真实样本还是虚假样本。

image.png

图4 discriminator 网络架构

image.png

▎分布匹配蒸馏

image.png

▎矫正流损失

image.png

▎算法流程

算法完整流程如下所示

image.png

03

飞桨编译器高性能推理

深度学习编译器是一种专门为深度学习模型优化和部署而设计的工具,用于提高模型的计算效率、降低内存占用、加速训练推理过程,核心价值在于弥合高层算法描述与底层硬件指令集之间的语义鸿沟。编译器功能上是将高层次的深度学习模型转换为低层次的、高效的、底层硬件可执行的代码。编译器通过将框架输出的初始计算图转化为具有严格语义定义的中间表示层,保留计算图的完整结构,随后在中间表示层实施多轮迭代优化,最终通过目标硬件感知的代码生成模块,将优化后的中间表示转化为高度特化的机器指令序列。简单来说,深度学习编译器在深度学习框架和底层硬件之间充当了“翻译”的角色,能够将用户定义的神经网络模型描述转化为底层硬件能够理解和执行的指令。编译器在实现这种转换的过程中,应用了一系列优化技术,以提高模型在各种硬件平台上(如 CPU、GPU)的执行效率。以下是飞桨框架编译器(CINN, Compiler Infrastructure for Neural Networks)整体流程图。

image.png

▎生成模型结合 CINN 推理性能优化

针对多模态生成模型推理时间长的问题,基于飞桨深度学习编译器 CINN,我们对于 FLUX 模型在 A800单卡推理情况下进行了飞桨框架推理性能优化实验,对比基于 xDiT 优化框架提供的 Torch Compile、Onediff 和 TensorRT 推理优化性能指标作为竞品。通过编译器优化所带来的性能加速,飞桨在 FLUX.1-dev 和 FLUX.1-schnell 这两个官方模型配置的推理中都取得了显著的性能提升,并且实现对比竞品的性能优势。

飞桨单卡推理性能测速和性能优化提升如下表所示。

image.png

FLUX 模型动态图编译器推理性能

通过表格中的性能测速对比可以发现,对于 FLUX.1-dev 模型的推理性能,输出图像维度为1024p 和512p 的情况下,使用飞桨编译器优化对比原生动态图推理性能提升分别达到31.8%和36.7%,而对于 FLUX.1-schnell 模型的推理性能,使用编译器优化对比原生动态图推理性能提升分别达到30.8%和34.6%,对于不同配置下 FLUX 系列模型都表现出了显著性能提升。

飞桨单卡推理性能测速和性能竞品对比如下表所示。

image.png

▎FLUX 模型推理性能竞品对比

我们对于市场上文生图大模型推理性能优化策略进行了性能分析,包括 torch compile、Onediff、TensorRT 等主流推理优化框架。通过对比可以发现基于飞桨编译器优化实现的 FLUX 推理在各个配置下都体现出了领先的推理性能。对于 FLUX.1-dev 模型,输出图像维度为1024p 和512p 的情况下,飞桨编译器推理性能对比竞品中性能最优的 Torch compile 推理性能提升分别达到1.4%和6.5%, 对于 FLUX.1-schnell 模型,飞桨编译器推理性能对比竞品中性能最优的 Onediff 推理性能提升分别达到1.4%和6.5%, 体现出了飞桨框架在市场中的推理性能方面的领先性,以及在 FLUX 模型各不同配置和参数设置情况下稳定的性能优势。同时我们也将该技术应用到自研蒸馏加速模型 FLUX-Lightning 中,开启 CINN 后在 A800上单卡推理时延能从2.21s 进一步降低到1.66s。

04

实验结果

▎实验设置

数据方面,我们基于 laion-aesv2数据集筛选45w 数据,筛选条件为:图像长宽都大于1024,美学指标 aes>6,水印概率分数<0.5。使用 COCO-10k 作为评测数据集。

模型方面,选用目前文生图领域最新的 FLUX 模型作为基础模型,由于 FLUX 模型自带 CFG 蒸馏,将 guidance scale 进行 embedding 后作为模型输入,所以训练时默认使用 CFG-augmented ODE solver。

评测指标,方面选择 CLIP 指标和 FID-FLUX,其中 FID-FLUX 指标参考 PCM 模型的 FID-SD 指标,使用原始 FLUX dev 模型的生成结果(50 step)计算 FID 分数。CLIP 指标用于评价生成结果与 prompt 的符合程度。

模型训练方面,使用 lora 训练的方式(lora rank=32),有效节省计算资源消耗。模型总 loss 如下所示,其中

image.png

▎定量结果

消融实验定量结果显示,我们使用的 Adversarial Learning,Distribution Matching Distillation 以及 reflow loss 都获得了模型效果的提升,证明了 FLUX-Lightning 优化点的有效性。

image.png

表1 消融实验

为了进一步验证 FLUX-Lightning 模型的效果,我们和目前 SOTA 的基于 FLUX 的蒸馏加速模型进行了全面的对比,包括 FLUX schnell,TDD (Target-Driven Distillation: Consistency Distillation with Target Timestep Selection and Decoupled Guidance),SwD (Scale-wise Distillation of Diffusion Models)以及 hyper-flux,其中 flux schnell 和 Hyper-FLUX 是闭源模型,TDD 和 Swd 为开源模型,且所有模型均基于 FLUX 蒸馏得到。对比结果如下所示,在 FID-FLUX 指标上 FLUX-Lightning 模型获得了最好的效果(8.0182),CLIP 指标上也展现出了具有竞争力的分数。

image.png

表2 定量实验结果

备注:消融实验使用28w 数据实验,完整 FLUX-Lightning 模型使用全量45w 数据训练

▎定性结果

下面展示了我们的 FLUX-Lightning 模型和其他竞品之间的图像生成效果对比,可以看到 FLUX-Lightning 模型在图像质量、prompt 一致性、生成准确性方面都超过了其他竞品。具体来说:

FLUX-Lightning 在人体部位的生成上更加准确。例如第一行大部分竞品生成的脚部都很怪异,FLUX-Lightning 生成了正确的脚部同时更加符合“没有被毯子盖住”的含义。第二行和第三行中大部分竞品生成的手指数量不对或者形状不对,FLUX-Lightning 的手指数量性状则完全正确。

FLUX-Lightning 具有更好的文字生成的能力。在第4行中需要生成“New York City, 100 miles”的文字,TDD 生成了模糊不清的文字,SwD 缺少“miles”,Hyper-FLUX 的“100”很模糊,FLUX schnell 生成了不需要的“ew caft”的乱码,只有 FLUX-Lightning 生成了清晰的“New York City, 100 miles”文字。

FLUX-Lightning 可以生成更合理的人体姿态。第5行展示了抛棒球的运动员,TDD 和 Hyper-FLUX 的手臂部分出现明显扭曲,SwD 的手部和棒球合在了一起,FLUX-Lightning 生成的整体动作以及局部特征更加合理准确;第6行展示了跑步的运动员,SwD 生成的腿部和 FLUX schnell 生成的手臂都有明显问题,TDD 和 Hyper-FLUX 则是生成了不合理的背部文字,只有 FLUX-Lightning 生成了正确的跑步姿势以及背部“8”的文字。

FLUX-Lightning 生成内容和 prompt 更加契合。第7行要求生成“一家三口”,SwD 和 flux schnell 仅仅生成了两个人,Hyper-FLUX 则是生成了2个男人,TDD 生成了一家三口但是人物形态扭曲,FLUX-Lightning 正确生成了一家三口,同时人物形态正常。第8行中,TDD 和 FLUX schnell 没有体现出“大象扬起鼻子”的样子,SwD 和 Hyper-FLUX 的图像细节和背景丰富度较差,FLUX-Lightning 在大象形态和背景丰富程度上更加优秀。最后一行中,TDD 和 SwD 的手部细节扭曲,Hyper-FLUX 没有展现出“正在梳头”的状态,flux schnell 则是生成了奇怪的梳子,FLUX-Lightning 在人物细节、物体细节和动作上都更胜一筹。

image.png

▎人工评测

为了更加全面地评测 FLUX-Lightning 的效果,我们进行了图像生成效果的人工评测。具体来说,我们生成了50个富有挑战性的 prompt,对 TDD,SwD,Hyper-FLUX,FLUX schnell 及 FLUX-Lightning 共计5个模型的生成结果进行排序,4位评审员采样盲评的方式,按照结果好坏从高到低分别得到10分,7分,5分,3分,1分,最终取平均分。部分 prompt 示例如下所示,第一行中,需要考察生成结果是否包含“3个女性”,“医院”,“病床,医疗设备”等元素。第二行中,考察生成模型是否包含“蒙古包”,“马头琴”以及“墙上的乐器”等元素,同时还要依据人物是否扭曲、图像质量等多个维度进行评判。

image.png

image.png

图5 人工评测 prompt 示例

人工评测结果如下所示,其中 FLUX-Lightning 获得了最高分7.37分,表明 FLUX-Lightning 可以生成更符合人类审美的图像,体现了模型的优异效果。

image.png

表3 人工评测结果

05

使用教程

PaddleMIX 已将 FLUX-Lightning 模型开源集成到其扩散模型推理库(PPDiffusers)中,源码和使用说明都可以在 PaddleMIX 的 GitHub 仓库中获取,代码链接为:

github.com/PaddlePaddl…

感兴趣的开发者可以查阅开源代码,了解各模块的实现细节和参数配置,并对自己的扩散模型进行蒸馏加速。

▎训练

数据准备:下载 laion 训练数据和数据列表

wget https://dataset.bj.bcebos.com/PaddleMIX/flux-lightning/laion-45w.tar.gz
wget https://dataset.bj.bcebos.com/PaddleMIX/flux-lightning/filelist_hwge1024_pwatermarkle0.5.txt

数据解压之后,文件结构如下所示

|-- your_path
   |-- laion-45wlaion-45w
      |-- 0000000.txt
      |-- 0000001.txt
      |-- 0000002.txt
      ....
   |-- filelist_hwge1024_pwatermarkle0.5.txt

模型训练命令:

python -u -m paddle.distributed.launch --gpus "0,1,2,3,4,5,6,7" train_flux_lightning_lora.py \
    --data_path "your_path/laion-45w" \
    --file_list_path "your_path/filelist_hwge1024_pwatermarkle0.5.txt" \
    --pretrained_teacher_model "black-forest-labs/FLUX.1-dev" \
    --output_dir outputs/lora_flux_lightning \
    --tracker_project_name lora_flux_lightning \
    --mixed_precision "bf16" \
    --fp16_opt_level "O2" \
    --resolution "1024" \
    --lora_rank "32" \
    --learning_rate "5e-6" \
    --loss_type "huber" \
    --adam_weight_decay "1e-3" \
    --max_train_steps "28652" \
    --dataloader_num_workers "32" \
    --guidance_scale "3.5" \
    --validation_steps "20000" \
    --checkpointing_steps "1000" \
    --checkpoints_total_limit "30" \
    --train_batch_size "1" \
    --gradient_accumulation_steps "1" \
    --resume_from_checkpoint "latest" \
    --seed "453645634" \
    --num_euler_timesteps "100" \
    --multiphase "4" \
    --gradient_checkpointing \
    --adv_weight=0.1 \
    --adv_lr=1e-5 \
    --pre_alloc_memory 76 \
    --use_dmd_loss \
    --dmd_weight 0.01 \
    --apply_reflow_loss \
    --reflow_loss_weight 0.01

▎推理

下载模型权重

wget https://dataset.bj.bcebos.com/PaddleMIX/flux-lightning/paddle_lora_weights.safetensors

推理命令

python text_to_image_generation_flux_lightning.py --path_to_lora your_path/paddle_lora_weights.safetensors --prompt "a beautiful girl" --output_dir ./

text_to_image_generation_flux_lightning.py 中的内容为

# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import os
os.environ["USE_PEFT_BACKEND"] = "True"
import paddle
from ppdiffusers import FluxPipeline
parser = argparse.ArgumentParser(description="Simple example of a training script.")
parser.add_argument(
    "--path_to_lora",
    type=str,
    required=True,
    help="Path to paddle_lora_weights.safetensors",
)
parser.add_argument(
    "--prompt",
    type=str,
    required=True,
    default="a beautiful girl",
)
parser.add_argument(
    "--guidance_scale",
    type=float,
    required=False,
    default=3.5,
)
parser.add_argument(
    "--height",
    type=int,
    required=False,
    default=1024,
)
parser.add_argument(
    "--width",
    type=int,
    required=False,
    default=1024,
)
parser.add_argument(
    "--lora_scale",
    type=float,
    required=False,
    default=0.25,
)
parser.add_argument(
    "--step",
    type=int,
    required=False,
    default=4,
)
parser.add_argument(
    "--seed",
    type=int,
    required=False,
    default=42,
)
parser.add_argument(
    "--output_dir",
    type=str,
    required=False,
    default="./",
)
args = parser.parse_args()
pipe = FluxPipeline.from_pretrained("black-forest-labs/FLUX.1-dev", map_location="cpu", paddle_dtype=paddle.bfloat16)
pipe.load_lora_weights(args.path_to_lora)
with paddle.no_grad():
    result_image = pipe(
        prompt=args.prompt,
        negative_prompt="",
        height=args.height,
        width=args.width,
        num_inference_steps=args.step,
        guidance_scale=args.guidance_scale,
        generator=paddle.Generator().manual_seed(args.seed),
        joint_attention_kwargs={"scale": args.lora_scale},
    ).images[0]
result_image.save(os.path.join(args.output_dir, "test_flux_lightning.png"))

使用 CINN 技术加速推理 FLUX-Lightning 方法如下:

export FLAGS_use_cuda_managed_memory=true
export FLAGS_prim_enable_dynamic=true
export FLAGS_prim_all=true
export FLAGS_use_cinn=1
python text_to_image_generation_flux_lightning_cinn.py --path_to_lora your_path/paddle_lora_weights.safetensors --prompt "a beautiful girl" --output_dir ./ --inference_optimize

06

总结与展望

本文介绍了 PaddleMIX 最新推出的 FLUX-Lightning 模型,通过区间一致性蒸馏(Phased Consistency Distillation),对抗学习(Adversarial Learning),分布匹配蒸馏(Distribution Matching Distillation),矫正流损失(reflow loss)等技术,在保持图像生成质量的前提下,可以做到4步快速生成,大幅提升了图像生成的性能,叠加上 CINN 推理优化,单图推理仅需1.66s(A800)。模型效果也达到了业界 SOTA 的水平,定量和定性结果显示超越了目前市面上基于 FLUX 的各种开源和闭源的蒸馏加速模型,开发者可以根据需求简单地对自己的扩散模型进行蒸馏加速。

展望未来,随着扩散模型在更大规模数据和更多应用领域的发展,此类推理高效化的需求将更加迫切。我们有理由相信,蒸馏加速方法还有很大潜力可挖——例如使用 TrigFlow 消除 CM 模型中的量化误差、更加高效的对抗损失设计等,都有望在保持图像生成质量的前提下进一步提升生成效率。PaddleMIX 也将持续完善多模态模型的工具链,在提供强大模型能力的同时兼顾实际部署效率。希望这些加速方法能够帮助开发者更快地落地扩散模型应用,激发出更丰富的创意,实现高质量生成与高效推理的双赢。

▎开源代码链接:

PaddleMIX 扩散模型加速插件相关代码已在 GitHub 开源 。欢迎大家访问仓库获取源码并提出宝贵意见,共同推进扩散模型技术的发展与应用!

github.com/PaddlePaddl…

Rust 性能提升“最后一公里”:详解 Profiling 瓶颈定位与优化|得物技术

作者 得物技术
2025年8月12日 13:49

一、Profiling:揭示性能瓶颈的“照妖镜”

在过去的一年里,我们团队完成了一项壮举:将近万核的 Java 服务成功迁移到 Rust,并收获了令人瞩目的性能提升。我们的实践经验已在《RUST练习生如何在生产环境构建万亿流量》一文中与大家分享。然而,在这次大规模迁移中,我们观察到一个有趣的现象:大多数服务在迁移后性能都得到了显著提升,但有那么一小部分服务,性能提升却不尽如人意,仅仅在 10% 左右徘徊。

这让我们感到疑惑。明明已经用上了性能“王者”Rust,为什么还会遇到瓶颈?为了解开这个谜团,我们决定深入剖析这些“低提升”服务。今天,我就来和大家分享,我们是如何利用 Profiling 工具,找到并解决写入过程中的性能瓶颈,最终实现更高性能飞跃的!

在性能优化领域,盲目猜测是最大的禁忌。你需要一把锋利的“手术刀”,精准地找到问题的根源。在 Rust 生态中,虽然不像 Java 社区那样拥有 VisualVM 或 JProfiler 这类功能强大的成熟工具,但我们依然可以搭建一套高效的性能分析体系。

为了在生产环境中实现高效的性能监控,我们引入了 Jemalloc 内存分配器和 pprof CPU 分析器。这套方案不仅支持定时自动生成 Profile 文件,还可以在运行时动态触发,极大地提升了我们定位问题的能力。

二、配置项目:让Profiling“武装到牙齿”

首先,我们需要在 Cargo.toml 文件中添加必要的依赖,让我们的 Rust 服务具备 Profiling 的能力。以下是我们的配置,Rust 版本为 1.87.0。

[target.'cfg(all(not(target_env = "msvc"), not(target_os = "windows")))'.dependencies]
# 使用 tikv-jemallocator 作为内存分配器,并启用性能分析功能
tikv-jemallocator = { version = "0.6", features = ["profiling""unprefixed_malloc_on_supported_platforms"] }
# 用于在运行时控制和获取 jemalloc 的统计信息
tikv-jemalloc-ctl = { version = "0.6", features = ["use_std""stats"] }
# tikv-jemallocator 的底层绑定,同样启用性能分析
tikv-jemalloc-sys = { version = "0.6", features = ["profiling"] }
# 用于生成与 pprof 兼容的内存剖析数据,并支持符号化和火焰图
jemalloc_pprof = { version = "0.7", features = ["symbolize","flamegraph"] }
# 用于生成 CPU 性能剖析数据和火焰图
pprof = { version = "0.14", features = ["flamegraph""protobuf-codec"] }

简单来说,这几个依赖各司其职:

※ tikv-jemallocator

基于 jemalloc 的 Rust 实现,以其高效的内存管理闻名。

※ jemalloc_pprof

负责将 jemalloc 的内存剖析数据转换成标准的 pprof 格式。

※ pprof

用于 CPU 性能分析,可以生成 pprof 格式的 Profile 文件。

三、  全局配置:启动Profiling开关

接下来,在 main.rs 中进行全局配置,指定 Jemalloc 的 Profiling 参数,并将其设置为默认的全局内存分配器。

// 配置 Jemalloc 内存分析参数
#[export_name = "malloc_conf"]
pub static malloc_conf: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:16\0";


#[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc;


// 将 Jemalloc 设置为全局内存分配器
#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

这段配置中的 lg_prof_sample:16 是一个关键参数。

它表示 jemalloc 会对大约每 2^16 字节(即 64KB)的内存分配进行一次采样。这个值越大,采样频率越低,内存开销越小,但精度也越低;反之则精度越高,开销越大。在生产环境中,我们需要根据实际情况进行权衡。

四、实现Profile生成函数:打造你的“数据采集器”

我们将 Profile 文件的生成逻辑封装成异步函数,这样就可以在服务的任意时刻按需调用,非常灵活。

内存Profile生成函数

#[cfg(not(target_env = "msvc"))]
async fn dump_memory_profile() -> Result<StringString> {
    // 获取 jemalloc 的 profiling 控制器
    let prof_ctl = jemalloc_pprof::PROF_CTL.as_ref()
        .ok_or_else(|| "Profiling controller not available".to_string())?;


    let mut prof_ctl = prof_ctl.lock().await;
    
    // 检查 profiling 是否已激活
    if !prof_ctl.activated() {
        return Err("Jemalloc profiling is not activated".to_string());
    }
   
    // 调用 dump_pprof() 方法生成 pprof 数据
    let pprof_data = prof_ctl.dump_pprof()
        .map_err(|e| format!("Failed to dump pprof: {}", e))?;


    // 使用时间戳生成唯一文件名
    let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
    let filename = format!("memory_profile_{}.pb", timestamp);


    // 将 pprof 数据写入本地文件
    std::fs::write(&filename, pprof_data)
        .map_err(|e| format!("Failed to write profile file: {}", e))?;


    info!("Memory profile dumped to: {}", filename);
    Ok(filename)
}

CPU Profile生成函数

类似地,我们使用 pprof 库来实现 CPU Profile 的生成。

#[cfg(not(target_env = "msvc"))]
async fn dump_cpu_profile() -> Result<String, String> {
    use pprof::ProfilerGuard;
    use pprof::protos::Message;


    info!("Starting CPU profiling for 60 seconds...");


    // 创建 CPU profiler,设置采样频率为 100 Hz
    let guard = ProfilerGuard::new(100).map_err(|e| format!("Failed to create profiler: {}", e))?;


    // 持续采样 60 秒
    tokio::time::sleep(std::time::Duration::from_secs(60)).await;


    // 生成报告
    let report = guard.report().build().map_err(|e| format!("Failed to build report: {}", e))?;


    // 使用时间戳生成文件名
    let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
    let filenameformat!("cpu_profile_{}.pb", timestamp);


    // 创建文件并写入 pprof 数据
    let mut file = std::fs::File::create(&filename)
        .map_err(|e| format!("Failed to create file: {}", e))?;


    report.pprof()
        .map_err(|e| format!("Failed to convert to pprof: {}", e))?
        .write_to_writer(&mut file)
        .map_err(|e| format!("Failed to write profile: {}", e))?;


    info!("CPU profile dumped to: {}", filename);
    Ok(filename)
}
  •  ProfilerGuard::new()   100  Hz 意味着每秒钟会随机中断程序 100 次,以记录当前正在执行的函数调用栈
  • tokio::time::sleep(std::time::Duration::from_secs(60)).await 表示 pprof 将会持续采样 60 秒钟
  •  guard.report().build() 这个方法用于将收集到的所有采样数据进行处理和聚合,最终生成一个 Report 对象。这个 Report 对象包含了所有调用栈的统计信息,但还没有转换成特定的文件格式
  •  report.pprof() 这是 Report 对象的一个方法,用于将报告数据转换成 pprof 格式

五、 触发和使用 Profiling:随时随地捕捉性能数据

有了上述函数,我们实现了两种灵活的触发方式。

※ 定时自动生成

通过异步定时任务,每隔一段时间自动调用 dump_memory_profile() 和  dump_cpu_profile() 。

fn start_profilers() {
    // Memory profiler
    tokio::spawn(async {
        let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
        loop {
            interval.tick().await;
            #[cfg(not(target_env = "msvc"))]
            {
                info!("Starting memory profiler...");
                match dump_memory_profile().await {
                    Ok(profile_path) => info!("Memory profile dumped successfully: {}", profile_path),
                    Err(e) => info!("Failed to dump memory profile: {}", e),
                }
            }
        }
    });
    // 同理可以实现CPU profiler
}

※ 手动 HTTP 触发

通过提供 /profile/memory 和 /profile/cpu 两个 HTTP 接口,可以随时按需触发 Profile 文件的生成。

async fn trigger_memory_profile() -> Result<impl warp::Reply, std::convert::Infallible> {
    #[cfg(not(target_env = "msvc"))]
    {
        info!("HTTP triggered memory profile dump...");
        match dump_memory_profile().await {
            Ok(profile_path) => Ok(warp::reply::with_status(
                format!("Memory profile dumped successfully: {}", profile_path),
                warp::http::StatusCode::OK,
            )),
            Err(e) => Ok(warp::reply::with_status(
                format!("Failed to dump memory profile: {}", e),
                warp::http::StatusCode::INTERNAL_SERVER_ERROR,
            )),
        }
    }
}
//同理也可实现trigger_cpu_profile()函数
fn profile_routes() -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
    let memory_profile = warp::post()
        .and(warp::path("profile"))
        .and(warp::path("memory"))
        .and(warp::path::end())
        .and_then(trigger_memory_profile);
    
    
    let cpu_profile = warp::post()
        .and(warp::path("profile"))
        .and(warp::path("cpu"))
        .and(warp::path::end())
        .and_then(trigger_cpu_profile);
    memory_profile.or(cpu_profile)
}

现在,我们就可以通过 curl 命令,随时在生产环境中采集性能数据了:

curl -X POST http://localhost:8080/profile/memory
curl -X POST http://localhost:8080/profile/cpu

生成的 .pb 文件,我们就可以通过 go tool pprof 工具,启动一个交互式 Web UI,在浏览器中直观查看调用图、火焰图等。

go tool pprof -http=localhost:8080 ./target/debug/otel-storage ./otel_storage_cpu_profile_20250806_032509.pb

六、性能剖析:火焰图下的“真相”

通过 go tool pprof 启动的 Web UI,我们可以看到程序的火焰图

如何阅读火焰图

※ 顶部: 代表程序的根函数。

※ 向下延伸; 子函数调用关系。

※ 火焰条的宽度: 代表该函数在 CPU 上消耗的时间。宽度越宽,消耗的时间越多,越可能存在性能瓶颈

CPU Profile

Memory Profile

在我们的 CPU 火焰图中,一个令人意外的瓶颈浮出水面:OSS::new 占用了约 19.1% 的 CPU 时间。深入分析后发现, OSS::new 内部的 TlsConnector 在每次新建连接时都会进行 TLS 握手,这是导致 CPU 占用过高的根本原因。

原来,我们的代码在每次写入 OSS 时,都会新建一个 OSS 实例,随之而来的是一个全新的 HTTP 客户端和一次耗时的 TLS 握手。尽管 oss-rust-sdk 内部有连接池机制,但由于我们每次都创建了新实例,这个连接池根本无法发挥作用!

七、优化方案:从“每次新建”到“共享复用”

问题的核心在于重复创建 OSS 实例。我们的优化思路非常清晰:复用 OSS 客户端实例,避免不必要的 TLS 握手开销

优化前

每次写入都新建 OSS 客户端。

fn write_oss() {
    // 每次写入都新建一个OSS实例
    let oss_instancecreate_oss_client(oss_config.clone());
    tokio::spawn(async move {
        // 获取写入偏移量、文件名
        // 构造OSS写入所需资源和头信息
        // 写入OSS
        let result = oss_instance
            .append_object(data, file_name, headers, resources)
            .await;
}
fn create_oss_client(config: OssWriteConfig) -> OSS {
    OSS::new(
    ……
    )
}

这种方案在流量较小时可能问题不大,但在万亿流量的生产环境中,频繁的实例创建会造成巨大的性能浪费。

优化前

※ 共享实例

让每个处理任务( DecodeTask )持有 Arc 共享智能指针,确保所有写入操作都使用同一个 OSS 实例。

let oss_client = Arc::new(create_oss_client(oss_config.clone()));
let oss_instance = self.oss_client.clone()
// ...
let result = oss_instance
    .append_object(data, file_name, headers, resources)
    .await;

※ 自动重建机制

为了应对连接失效或网络问题,我们引入了自动重建机制。当写入次数达到阈值或发生写入失败时,我们会自动创建一个新的 OSS 实例来替换旧实例,从而保证服务的健壮性。

// 使用原子操作确保多线程环境下的计数安全
let write_countself.oss_write_count.load(std::sync::atomic::Ordering::SeqCst);
let failure_countself.oss_failure_count.load(std::sync::atomic::Ordering::SeqCst);


// 检查是否需要重建实例...
fn recreate_oss_client(&mut self) {
 
    let new_oss_client = Arc::new(create_oss_client(self.oss_config.clone()));
    self.oss_client = new_oss_client;
    self.oss_write_count.store(0, std::sync::atomic::Ordering::SeqCst);
    self.oss_failure_count.store(0, std::sync::atomic::Ordering::SeqCst);
    // 记录OSS客户端重建次数指标
    OSS_CLIENT_RECREATE_COUNT
        .with_label_values(&[])
        .inc();
    info!("OSS client recreated");
}

八、优化效果:性能数据“一飞冲天”

优化后的服务上线后,我们观察到了显著的性能提升。

CPU 资源使用率

同比下降约 20%

OSS 写入耗时

同比下降约 17.2% ,成为集群中最短的写入耗时。

※ OSS写入耗时

※ OSS相关资源只占千分之一

内存使用率

平均下降 8.77% ,这部分下降可能也得益于我们将内存分配器从 mimalloc 替换为 jemalloc 的综合效果。

这次优化不仅解决了特定服务的性能问题,更重要的是,它验证了在 Rust 中通过 Profiling 工具进行深度性能分析的可行性。即使在已经实现了初步性能提升的 Rust 服务中,仍然存在巨大的优化空间。

未来,我们将继续探索更高效的 Profiling 方案,并深入挖掘其他潜在的性能瓶颈,以在万亿流量的生产环境中实现极致的性能和资源利用率。

引用

往期回顾

1.Valkey 单点性能比肩 Redis 集群了?Valkey8.0 新特性分析|得物技术

2.Java volatile 关键字到底是什么|得物技术

3.社区搜索离线回溯系统设计:架构、挑战与性能优化|得物技术

4.正品库拍照PWA应用的实现与性能优化|得物技术

5.得物社区活动:组件化的演进与实践

文 / 炯帆 南风

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

❌
❌