news 2026/9/2 19:29:06

多租户架构实战:从独立部署到共享表的数据隔离方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多租户架构实战:从独立部署到共享表的数据隔离方案

在“千万 QPS 架构”这个系列里,我们聊过很多高并发场景下的通用技术:缓存、分库分表、消息队列、限流熔断。但有一个问题,几乎每个从私有化部署转向 SaaS 模式的团队都会反复纠结:当一个系统要同时服务几十家甚至上千家客户时,技术上到底应该怎么隔离?

很多团队在讨论多租户时,第一反应是先回答一个“听起来很技术”的问题:租户之间是独立部署,还是共享一套系统?但真正经历过这个阶段的人会明白,这个问题背后其实是商业模式的抉择。独立部署,隔离性最好,但每接入一个大客户就要交付一套环境,运维成本、机器成本、发布成本都会成倍上涨;共享一套系统,资源利用率高,但一个租户的慢查询、热点请求、突发流量,随时可能影响隔壁租户。

如果你正在做 SaaS 平台、OpenAPI 开放平台,或者正在把企业内部系统改造成对外输出的云服务,这篇文章值得读完。我会从架构视角拆解多租户从“独占到共享”的演进路径,讲清楚三种主流隔离模式、共享模式下最容易踩的坑,以及如何用代码和配置把租户隔离真正落到工程里。

1. 多租户架构要解决的本质问题

先给多租户下一个不那么“百科”的定义。

多租户(Multi-Tenant)指的是:一套系统实例,同时服务多个客户(租户),每个租户的数据、配置、权限、资源使用都相互隔离,但底层基础设施是共享的。

它要解决的本质问题有两个:一是成本问题,二是效率问题。

成本问题很好理解。假设你是一个做零售 SaaS 的厂商,客户 A、客户 B、客户 C 都是你的付费用户。如果不采用多租户架构,每个客户都部署一套独立系统,那么客户一多,机器采购、网络规划、版本迭代就会变成灾难。更重要的是,独立部署意味着每个客户都需要单独升级、单独备份、单独监控,运维人力会随着客户数量线性增长。这种模式在项目管理软件、企业内部系统中还能接受,但放到 SaaS 领域,几乎不可能支撑规模化增长。

效率问题是成本问题的延伸。共享一套系统后,新版本上线只需要发布一次,所有租户立刻就能使用新功能;数据库也只需要维护一套集群,备份、扩容、监控都能统一做。从工程效率来说,这是巨大的提升。

但资源一旦共享,问题也来了:你用什么标准来判断一个租户可以享受多少资源?当一个租户出现异常流量时,你要不要限制它?限制以后,客户体验会不会受影响?

这就是为什么多租户架构的设计,本质上不是“要不要共享”的选择题,而是“隔离粒度如何权衡”的工程题。

更好的判断方式是看业务阶段。刚开始做 SaaS 时,用户量小,租户数量少,独立部署简单直接;租户量上来以后,如果还不做共享,就会被成本压垮;但共享程度太高,又会影响隔离性和稳定性。所以绝大多数成熟系统的路径是:先独立跑通业务,再逐步走向共享,在中间找到适合自己业务的隔离粒度。

2. 三种经典多租户隔离模式对比

多租户架构在业界经过多年发展,基本沉淀出了三种经典模式。了解这三种模式,是理解整个多租户架构的起点。

2.1 独立部署模式(隔离数据库实例)

每个租户独占一套完整的系统环境,包括独立的数据库实例、独立的应用服务、独立的中间件。

这是隔离性最强、实施成本也最高的一种模式。它的优点是:租户之间的故障完全隔离,一个租户的慢查询不会影响别人;数据安全级别最高;可以针对大客户做定制化功能。缺点是:运维成本高,机器利用率低,版本发布需要遍历所有租户环境。

这种模式适合客单价高、数量少、有定制化需求的大型企业客户。国内很多银行、政府、大型国企的私有化交付就是这种模式。

2.2 共享数据库、独立 Schema

所有租户共享同一个数据库实例,但每个租户拥有独立的 Schema(或独立的表集合)。

这种模式是在隔离性和成本之间做了一次折中。数据库实例只需要维护一套,备份和扩容成本大幅降低;Schema 之间的数据天然隔离,可以实现一定程度上的数据安全。但问题在于,数据库连接数、CPU、内存仍然是共享的,一个租户的慢查询依然可能拖垮整个实例。

这种模式适合租户数量中等、业务规模相似的场景。

2.3 共享数据库、共享表

所有租户的数据都存放在同一组表中,通过tenant_id字段来区分数据归属。

这是成本最低、扩展性最好、也是实现难度最高的一种模式。它的核心挑战在于:应用层必须保证每次数据库操作都带上租户上下文,否则就很容易出现数据越权;同时,存储层难以对单个租户做资源隔离,噪声隔离能力最弱。

这种模式适合租户数量大、单租户数据量小、业务标准化程度高的场景,也是目前绝大多数互联网 SaaS 系统的选择。

三种模式的对比可以看下表:

隔离维度独立部署共享数据库、独立 Schema共享数据库、共享表
数据隔离较强弱,靠 tenant_id 区分
资源隔离一般弱,需额外配额机制
运维成本
部署成本
单租户扩展可以独立扩容受数据库实例限制整体扩容
适合租户数量少(十几个以内)中等(几十到几百)多(上千到上万)
适合业务类型大型客户定制中型客户标准化标准 SaaS 产品

需要特别说明的是,这三种模式并不是互相排斥的。成熟的大型系统往往是混合模式:大部分中小租户使用共享表模式,少数核心大客户使用独立 Schema 或者独立部署模式。

3. 从独占到共享:架构演进的核心逻辑

既然共享数据库、共享表的成本优势这么明显,为什么不是所有系统一开始就这么做?

因为共享是有代价的。从独占到共享,真正变化的不是代码,而是隔离模型。

在独立部署模式里,隔离是由部署边界天然提供的。每个客户一套环境,客户 A 的数据库和客户 B 的数据库根本不在一台机器上,数据不可能串;客户 A 的程序出 Bug,只影响它自己,不会影响客户 B。

但在共享模式下,租户之间只剩一个逻辑边界。原来由运维环境解决的隔离问题,现在全部转移到了应用代码里。你必须在每一个 DAO、每一个 Service、每一个定时任务里都意识到:当前请求属于哪个租户?这条数据能不能被这个租户看到?这个缓存 key 是否包含租户维度?这个死信队列里的消息应该路由到哪个租户?

从架构演进的角度看,从独占到共享主要经历了三个层次的改造:

第一层是数据层改造。需要在核心业务表上增加tenant_id字段,并且把所有查询 SQL 从“按主键查”改造成“按租户 + 主键查”。这一层的难点在于存量数据的迁移和历史 SQL 的改造,很多时候团队会在这一层翻车。

第二层是应用层改造。需要引入租户上下文(Tenant Context),在请求链路中传递当前租户标识,并基于租户上下文动态改写 SQL。这一层需要用 ThreadLocal、拦截器、MyBatis 插件等机制来实现。

第三层是资源层改造。需要把连接池、线程池、缓存、消息队列等资源都做成按租户隔离或按租户配额管理。这一层解决的是“资源如何分配”的问题。

很多团队做多租户改造时只关注了第一层,以为加个tenant_id字段就算完了,结果线上频繁出现数据越权、租户相互干扰的问题。原因就是没有理解:多租户改造是整个架构层的改造,不是加一个字段那么简单。

4. 共享模式下的关键设计难点

从独占到共享,最容易被低估的是以下四个设计难点。

4.1 租户上下文传递

在共享模式下,应用服务接收到的每个请求,都必须明确它属于哪个租户。这里的第一个难点是:租户标识如何从入口传到最底层的数据访问层?

常见做法是用 ThreadLocal 保存当前请求的租户 ID,在请求开始时设置,在请求结束后清除。但这里有一个非常隐蔽的坑:如果代码里使用了线程池、异步任务、消息消费,ThreadLocal 不会自动传递到子线程,数据就可能在异步场景下串租户。

解决思路一般是两种:要么在异步提交任务时显式传递租户上下文,要么使用支持上下文传递的链路标识组件(比如基于 TransmittableThreadLocal 的方案)。这块属于细节工程,但做不好就是线上事故。

4.2 数据库连接池与 SQL 改写

共享数据库、共享表模式下,所有租户共用同一个数据库连接池。为了保证租户数据隔离,必须在 SQL 执行前自动拼接tenant_id条件,而且这个拼接对业务开发应该是透明的,不能要求每个开发都记得手动写条件。

Java 生态里最常见的做法是实现 MyBatis 的 Interceptor,在 SQL 解析阶段自动注入租户条件。这种方案的好处是开发无感知,坏处是它要求所有查询都必须经过 MyBatis,如果代码里还有原生 JDBC、存储过程或者其他 ORM,SQL 改写就不一定能覆盖到。

4.3 缓存与队列的租户维度

共享模式下,缓存也是一个容易踩坑的地方。

如果使用 Redis 缓存用户数据,缓存 key 里必须包含tenant_id。如果不加,租户 A 查询的数据可能被租户 B 命中,造成数据越权。类似地,消息队列的消费逻辑也要按租户处理:如果一个死信队列里的消息来自多个租户,消费端必须根据消息内容重新定位租户上下文,再执行对应的业务逻辑。

4.4 资源配额与噪声隔离

共享模式下,一个租户的突发流量会“挤占”其他租户的资源。如果完全不限制,某个租户的全表扫描或者热点促销就可能拖垮整个数据库。

所以生产环境里,还需要做资源配额管理:比如按租户设置连接数上限、按租户限制 QPS、按租户设置缓存容量上限。在 Kubernetes 部署场景下,还可以按租户划分 Namespace,通过资源配额(ResourceQuota)和限流(LimitRange)实现更细粒度的控制。

5. 代码示例:基于 Spring Boot + MyBatis 实现多租户隔离

理论知识聊了很多,接下来用一个最小示例把多租户隔离落到代码里。这里以 Java 技术栈为例,使用 Spring Boot + MyBatis 实现共享数据库、共享表模式下的租户隔离。

5.1 创建带租户字段的业务表

首先,业务表需要增加tenant_id字段,并且建议在tenant_id和业务主键上建立联合索引,避免全表扫描。

-- 文件路径:src/main/resources/db/schema.sql CREATE TABLE `t_order` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID', `tenant_id` VARCHAR(64) NOT NULL COMMENT '租户ID', `order_no` VARCHAR(64) NOT NULL COMMENT '订单编号', `amount` DECIMAL(10, 2) NOT NULL COMMENT '订单金额', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`tenant_id`, `order_no`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '订单表';

这里的关键设计点是uk_order_no联合唯一索引。因为不同租户可能生成相同的订单号,如果只在order_no上建唯一索引,就会导致租户间数据冲突。

5.2 实现租户上下文 holder

租户上下文的本质是:在当前请求线程里保存租户 ID,并在请求结束后清理。

// 文件路径:src/main/java/com/example/tenant/TenantContext.java public class TenantContext { private static final ThreadLocal<String> TENANT_ID_HOLDER = new ThreadLocal<>(); public static void setTenantId(String tenantId) { TENANT_ID_HOLDER.set(tenantId); } public static String getTenantId() { return TENANT_ID_HOLDER.get(); } public static void clear() { TENANT_ID_HOLDER.remove(); } }

这里使用remove()而不是set(null)来清理,是为了避免线程池复用线程时,旧租户上下文泄露到下一次请求。

5.3 实现租户解析过滤器

租户 ID 一般通过请求头X-Tenant-Id传递。在 Spring Boot 中,可以用一个 Filter 在请求入口解析租户 ID,并设置到TenantContext中。

// 文件路径:src/main/java/com/example/tenant/TenantFilter.java @Component @Order(1) public class TenantFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String tenantId = httpRequest.getHeader("X-Tenant-Id"); if (StringUtils.hasText(tenantId)) { TenantContext.setTenantId(tenantId); } try { chain.doFilter(request, response); } finally { TenantContext.clear(); } } }

注意,这里使用try/finally保证无论如何都会清理租户上下文。如果忘记清理,在高并发线程池场景下很容易出现串租户的数据事故。

5.4 实现 MyBatis 多租户插件

这是整个示例的核心。我们需要在 SQL 执行前,自动为查询、更新、删除语句拼接tenant_id条件。

// 文件路径:src/main/java/com/example/tenant/TenantLineInnerInterceptor.java @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) public class TenantLineInnerInterceptor implements Interceptor { private static final String TENANT_COLUMN = "tenant_id"; @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String originalSql = boundSql.getSql(); String tenantId = TenantContext.getTenantId(); if (StringUtils.hasText(tenantId) && isNeedProcess(originalSql)) { String newSql = rewriteSql(originalSql, tenantId); // 通过反射修改 BoundSql 中的 SQL Field sqlField = BoundSql.class.getDeclaredField("sql"); sqlField.setAccessible(true); sqlField.set(boundSql, newSql); } return invocation.proceed(); } private boolean isNeedProcess(String sql) { // 只处理 DML 语句,跳过 INSERT 语句 String trimmedSql = sql.trim().toLowerCase(); return trimmedSql.startsWith("select") || trimmedSql.startsWith("update") || trimmedSql.startsWith("delete"); } private String rewriteSql(String originalSql, String tenantId) { // 简化处理:直接在 where 前拼接 tenant_id 条件 // 生产环境建议使用 JSqlParser 解析 SQL,自动识别表名和已有条件 if (originalSql.toLowerCase().contains("where")) { return originalSql + " AND " + TENANT_COLUMN + " = '" + tenantId + "'"; } return originalSql + " WHERE " + TENANT_COLUMN + " = '" + tenantId + "'"; } }

上面这段代码是演示逻辑,目的是让你理解多租户插件的工作原理。真实生产环境中,直接拼接 SQL 是非常危险的,很可能被注入或者产生语法错误。更稳妥的做法是使用 MyBatis-Plus 的TenantLineInnerInterceptor,它内部通过 JSqlParser 解析 SQL 的抽象语法树,可以自动识别表名、别名、已有 where 条件,支持多表查询时给所有相关表拼接租户条件。

使用 MyBatis-Plus 时,只需要在配置类中注册该插件。

// 文件路径:src/main/java/com/example/tenant/MybatisPlusConfig.java @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor( new TenantLineHandler() { @Override public Expression getTenantId() { String tenantId = TenantContext.getTenantId(); if (!StringUtils.hasText(tenantId)) { throw new RuntimeException("租户上下文缺失"); } return new StringValue(tenantId); } @Override public String getTenantIdColumn() { return "tenant_id"; } @Override public boolean ignoreTable(String tableName) { // 字典表、系统配置表等全局表可以忽略租户条件 return "t_dict".equalsIgnoreCase(tableName); } } ); interceptor.addInnerInterceptor(tenantInterceptor); return interceptor; } }

这段配置意味着:除了t_dict这类全局表,其他表在执行 SQL 时都会自动追加tenant_id = 当前租户ID的条件。业务开发人员不需要感知租户隔离逻辑,只需要在入口设置租户上下文。

5.5 编写业务查询示例

加了插件之后,普通业务代码不需要做任何特殊处理。

// 文件路径:src/main/java/com/example/order/OrderService.java @Service public class OrderService { @Autowired private OrderMapper orderMapper; public List<Order> listOrders() { // 直接调用 Mapper 即可,SQL 改写由插件自动完成 return orderMapper.selectList( new LambdaQueryWrapper<Order>() .eq(Order::getStatus, 1) ); } }

从开发者的视角看,这段代码和普通 Spring Boot 业务代码没有区别。但实际执行的 SQL 会自动变成:

SELECT * FROM t_order WHERE status = 1 AND tenant_id = 'tenant_a';

这就是共享模式下多租户隔离的落地方式:尽可能把隔离逻辑下沉到框架层,让业务代码保持干净。

6. 运行结果与效果验证

代码写完后,怎么验证多租户隔离是否生效?

第一步,启动 Spring Boot 应用。应用启动成功之后,使用curl模拟两个租户的请求:

# 租户 A 查询订单 curl -H "X-Tenant-Id: tenant_a" "http://localhost:8080/order/list" # 租户 B 查询订单 curl -H "X-Tenant-Id: tenant_b" "http://localhost:8080/order/list"

第二步,在数据库中预先插入两条不同租户的订单数据:

INSERT INTO t_order (tenant_id, order_no, amount, status) VALUES ('tenant_a', 'NO2024001', 100.00, 1); INSERT INTO t_order (tenant_id, order_no, amount, status) VALUES ('tenant_b', 'NO2024002', 200.00, 1);

第三步,分别请求后,预期结果是:

  • 租户 A 的请求只返回NO2024001这条订单。
  • 租户 B 的请求只返回NO2024002这条订单。

如果租户 A 的请求返回了租户 B 的数据,说明 SQL 改写没有生效,优先排查以下方向:

  1. MyBatis 插件是否被正确注册到 SqlSessionFactory 中。
  2. TenantContext中的租户 ID 是否在请求开始时被正确设置。
  3. 是否走了二级缓存或本地缓存,导致数据绕过 SQL 层。
  4. 是否使用了@Async或线程池,导致子线程中租户上下文丢失。

验证时还有一点需要特别注意:要验证 INSERT 语句是否也带上了租户字段。很多团队只关注查询隔离,结果新增数据时没有写入tenant_id,导致后续查询出来的数据租户字段为空,出现“数据凭空消失”的诡异问题。

7. 常见问题与排查思路

多租户改造过程中,常见问题非常集中,基本都出现在上下文传递、SQL 改写和缓存维度上。下面整理了一份排查清单,供参考:

问题现象可能原因排查方式解决方案
租户 A 查询到了租户 B 的数据SQL 改写未生效,或查询走了缓存查看 MyBatis 打印的 SQL 是否包含 tenant_id检查插件注册顺序和 ignoreTable 配置
新增数据后 tenant_id 为空INSERT 语句未写入租户字段查看数据库记录和 INSERT SQL使用 MyBatis-Plus 自动填充或插件处理 INSERT
使用线程池后出现串租户ThreadLocal 未传递到子线程在子线程中打印租户 ID 检查是否为空使用 TransmittableThreadLocal 或在提交任务时显式绑定租户
多表 join 查询报错或数据异常未给所有相关表拼接租户条件查看实际执行的 SQL 和表别名使用 JSqlParser 解析并给所有业务表追加条件
字典表、公共配置表查询不到数据租户条件被错误追加到全局表检查 ignoreTable 配置将全局表加入忽略名单
迁移数据后出现重复数据原有唯一索引不含 tenant_id检查表索引和迁移脚本增加联合唯一索引
高峰期一个租户拖垮整个数据库数据库资源未做租户级限制查看数据库连接数和慢查询耗时增加连接池/线程池按租户隔离或 QPS 配额
缓存中查到了其他租户的数据缓存 key 未包含租户维度检查 Redis key 是否一致缓存 key 增加 tenant_id 前缀

第一个问题还需要补充说明:SQL 改写不生效,不一定全是插件的问题,也有可能是应用里使用了@TableName配置错误,导致 MyBatis-Plus 解析的实体名和真实表名不一致。排查时建议先开启 MyBatis SQL 日志,确认最终执行的 SQL 长什么样,再顺藤摸瓜。

关于慢查询问题,还有一种常见场景:某个租户使用了LIKE '%关键词%'查询,无法走索引,扫描了整个表,导致共享数据库 CPU 飙升。这类问题只能在产品层面限制模糊查询范围,或者把大租户迁移到独立 Schema,运维上很难用一套规则覆盖所有场景。

8. 最佳实践与工程建议

多租户架构不是写完代码就结束的,它需要一整套工程规范来支撑。以下是最值得关注的几条建议。

8.1 一切全局表都要显式配置忽略名单

共享表模式下,不是所有表都是租户维度。像系统字典表、地区表、国家表这类基础数据,是全局共享的。如果插件错误地给这些表追加了tenant_id条件,会导致公共数据查询不到。所以,ignoreTable名单必须和表结构评审一起做,新增表时要明确它是“租户表”还是“全局表”。

8.2 缓存 key 一定要带租户维度

Redis 缓存是串数据的高发区。建议从设计层面规定:所有涉及租户数据的缓存 key,必须使用tenant_id:业务维度:业务ID的格式。比如:

order:tenant_a:NO2024001

这样做的好处是,即使两个租户的订单号相同,也不会互相覆盖缓存。如果觉得 key 太长,可以先对tenant_id做短编码,但决不能不包含租户维度。

8.3 租户上下文必须全链路传递

只解决 Web 请求的租户上下文是不够的,只要系统里有异步任务,就要考虑上下文传递。生产环境中比较稳妥的方式是:

  • Web 层用 Filter 解析租户 ID 并设置上下文。
  • 线程池提交任务时,显式把租户 ID 传入任务对象。
  • 消息队列消费时,从消息头里读取租户 ID。
  • 定时任务如果按租户遍历执行,需要在循环体内重置租户上下文。

8.4 按租户做好资源配额

共享模式不代表无限制共享。建议在接入层和数据库层都做租户维度的配额控制:

  • 网关层:给每个租户设置限流阈值,超过阈值的请求快速失败。
  • 数据库层:给核心租户设置连接数上限,或者使用数据库代理的租户限流能力。
  • 应用层:给异步任务设置租户维度信号量限制,避免某个租户的批量任务占满线程池。

在 Kubernetes 部署场景下,也可以按租户拆分 Namespace,通过 ResourceQuota 限制 CPU、内存和存储。这种方式比应用层限制更硬,也更可靠。

8.5 审计日志必须保留租户维度

线上排查数据问题时,如果审计日志里没有租户 ID,去重和定位会非常困难。建议所有关键操作日志、慢查询日志、异常日志,都统一打印租户 ID。这里推荐在日志框架中直接接入租户上下文的 MDC 值,让租户 ID 自动进入所有日志。

其实这类“看起来是小问题、线上是大问题”的细节,在多租户架构里特别多。团队改造初期最好提前约定好规范,比事后靠运维救火要省力得多。

9. 总结与继续深入的方向

多租户架构从独占到共享,本质上是把“部署隔离”转变为“逻辑隔离”,把“资源独占”转变为“资源按配额共享”。它真正考验的不是某一个数据库功能或者某一个框架插件,而是整个团队对隔离边界、上下文传递、资源调度的理解是否一致。

这篇文章里,我们先梳理了多租户要解决的成本与效率问题,然后对比了三种经典隔离模式,并重点拆解了共享数据库、共享表模式下的四个关键设计难点。随后用 Spring Boot + MyBatis 生态给出了一个可落地的租户隔离实现,包括租户上下文、请求过滤器和 SQL 改写插件的完整示例。最后补充了常见问题排查清单和几条经过实践检验的工程规范。

如果你的项目刚刚开始做 SaaS 化改造,建议按照这个顺序推进:先梳理业务表,区分租户表和全局表;再引入租户上下文和 SQL 改写插件,跑通单租户隔离;然后逐步排查异步任务、缓存、消息队列中的租户传递;最后再做资源配额和监控告警。

多租户架构还有几个值得深入的方向:租户级别的数据迁移策略、按租户灰度发布、租户级配额与计费系统的联动、大规模租户下的成本分摊模型。这些内容每一块都可以单独展开。如果这篇文章对你有帮助,可以收藏备用,后续实践中有具体问题,欢迎在评论区一起讨论。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 19:28:57

从MySQL分库分表到TiDB:外贸系统数据架构弹性改造实践

外贸业务数据系统有一个很典型的尴尬期&#xff1a;订单量还在涨&#xff0c;数据库却先撑不住了&#xff1b;查询接口为了避开单表数据量&#xff0c;硬生生改成了按月份拆库&#xff1b;报表和在线事务争抢同一个实例的 CPU&#xff0c;一到月底结算&#xff0c;客服和财务同…

作者头像 李华
网站建设 2026/9/2 19:28:36

歌切不只是剪切,而是重混:虚拟主播音频精修全流程解析

第一次听到阿萨Aza演唱《Simon》的那份歌切时&#xff0c;我的第一反应不是这首歌好不好听&#xff0c;而是这个切片为什么比很多现场版听感更稳。歌切&#xff0c;也就是把直播或演唱视频里的歌曲部分单独剪出来&#xff0c;再经过音频修整、画面处理和字幕包装后重新发布的内…

作者头像 李华
网站建设 2026/9/2 19:28:15

中文BERT全词掩码模型chinese-bert-wwm-ext加载与微调实战

简介&#xff1a;面向中文自然语言处理学习与研发的预训练模型资源&#xff0c;基于哈工大讯飞联合实验室发布的Chinese-BERT-wwm-ext版本&#xff0c;专为PyTorch框架封装。该模型采用全词掩码策略&#xff0c;对中文词汇表做了针对性优化&#xff0c;相比原版BERT更注重语义完…

作者头像 李华
网站建设 2026/9/2 19:19:37

华硕弘道AI笔记本:搭建课堂编程工作流的实践指南

用华硕弘道AI笔记本搭建课堂编程工作流&#xff0c;最直观的感受是&#xff1a;它把“AI能帮你写代码”这种零散体验&#xff0c;变成了一条从环境准备、代码编写、自动测试到作业提交的稳定通道。课堂编程真正的难点不在于教某个语法&#xff0c;而在于让机房里几十台电脑保持…

作者头像 李华
网站建设 2026/9/2 19:19:07

个人微信API接口如何融入现有项目?

接口封装这事&#xff0c;很多人理解成"写个函数包一下"——把 sendText 包成 sendMessage 就完事。真做过的都知道没那么简单。好的封装要把不同关注点分离&#xff1a;鉴权怎么管、参数怎么组装、错误怎么处理、日志怎么记&#xff0c;每个关注点独立封装互不纠缠。…

作者头像 李华
网站建设 2026/9/2 19:17:46

VS2019下protobuf 3.8.0 C++静态库编译与集成实战

简介&#xff1a;protobuf-3.8.0是Google开发的跨语言数据序列化协议&#xff0c;能够将结构化的数据高效编码为二进制流&#xff0c;广泛用于网络通信、数据存储与跨平台项目。这份基于Visual Studio 2019的C使用案例包&#xff0c;面向希望在实际工程中快速上手protobuf的开发…

作者头像 李华