第一章:Go语言多租户系统落地难题全景透视
构建高可用、可扩展的多租户系统是现代SaaS平台的核心挑战,而Go语言虽以并发高效、部署轻量见长,在实际落地中却暴露出一系列结构性矛盾。这些矛盾并非源于语法缺陷,而是由租户隔离性、资源调度粒度、数据模型弹性与Go原生生态设计哲学之间的张力所引发。
租户上下文传递的隐式断裂
Go惯用context.Context实现请求生命周期管理,但多租户场景下需将tenantID安全、不可篡改地贯穿HTTP层、业务逻辑层至数据访问层。若仅依赖中间件注入context.WithValue(),极易因协程复用或中间件顺序错位导致租户信息污染。正确做法是封装强类型上下文:
type TenantContext struct {
tenantID string
role string
}
func WithTenant(ctx context.Context, t *TenantContext) context.Context {
return context.WithValue(ctx, tenantCtxKey{}, t) // 使用私有未导出类型作key,避免冲突
}
数据隔离策略的权衡困境
常见方案包括:
- 共享数据库+共享表(行级隔离):依赖SQL
WHERE tenant_id = ?,易因ORM误用或SQL注入失效; - 共享数据库+独立表(前缀/后缀命名):需运行时动态注册GORM模型,增加迁移复杂度;
- 独立数据库实例:启动时通过
sql.Open("mysql", dsn)按租户动态连接,但连接池无法复用,需自建连接池路由层。
中间件与依赖注入的耦合风险
使用wire等DI框架时,若将*sql.DB作为全局单例注入,所有租户共享同一连接池,违背隔离原则。必须将数据访问层抽象为接口,并按租户ID构造独立实例:
type TenantDBFactory interface {
GetDB(tenantID string) (*sql.DB, error)
}
// 实现需缓存已初始化的*sql.DB,避免重复Open
配置热加载与租户个性化能力缺失
租户专属配置(如限流阈值、Webhook地址)若硬编码于config.yaml,则每次新增租户需重启服务。应采用分层配置模型:基础配置由文件加载,租户配置从Redis或etcd按需拉取,并监听变更事件触发局部重载。
第二章:租户隔离失效的根因分析与工程修复
2.1 基于Context与Middleware的请求级租户上下文透传实践
在多租户系统中,需将租户标识(如 tenant_id)从入口(HTTP Header、JWT Claim 或子域名)安全、无损地透传至业务层及数据访问层。
租户信息提取策略
- 优先级:
X-Tenant-IDHeader > JWTtenantclaim >Host子域名解析 - 拒绝未声明租户的请求(400 Bad Request)
中间件注入租户上下文
func TenantContextMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tenantID := extractTenantID(c.Request) // 见下文逻辑分析
if tenantID == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, "missing tenant context")
return
}
// 将租户ID注入context,供下游使用
ctx := context.WithValue(c.Request.Context(), "tenant_id", tenantID)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
逻辑分析:extractTenantID 按预设顺序读取来源;context.WithValue 创建不可变子上下文,确保请求生命周期内租户信息隔离。c.Request.WithContext() 替换原请求上下文,使后续 handler 可通过 c.Request.Context().Value("tenant_id") 安全获取。
上下文透传验证表
| 层级 | 是否自动继承 | 获取方式 |
|---|---|---|
| HTTP Handler | ✅ | c.Request.Context().Value("tenant_id") |
| Service | ✅ | 显式传递 ctx 参数 |
| DAO/Repository | ✅ | 依赖 service 层透传的 ctx |
graph TD
A[HTTP Request] --> B[X-Tenant-ID / JWT / Host]
B --> C[TenantContextMiddleware]
C --> D[ctx.WithValue tenant_id]
D --> E[Handler → Service → DAO]
2.2 数据库Schema/Row-Level隔离策略选型与GORM动态TenantID注入实现
多租户隔离需在性能、扩展性与安全性间权衡。Schema级隔离强隔离但运维成本高;行级隔离(tenant_id 列)轻量灵活,更适配云原生微服务。
隔离策略对比
| 维度 | Schema 级 | Row 级 |
|---|---|---|
| 隔离强度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 迁移/备份粒度 | 按租户独立 | 全库统一,需逻辑过滤 |
| GORM适配难度 | 需动态切换 db.Table() |
依赖全局 BeforeQuery 钩子 |
GORM 动态 TenantID 注入
func InjectTenantID(db *gorm.DB) *gorm.DB {
tenantID := GetTenantFromContext(db.Statement.Context) // 从 HTTP middleware 或 context.Value 提取
if tenantID != "" {
return db.Where("tenant_id = ?", tenantID)
}
return db
}
// 注册全局回调(自动触发)
db.Callback().Query().Before("gorm:query").Register("tenant:inject", InjectTenantID)
该钩子在每次 Find/First 前注入 WHERE tenant_id = ?,确保无感知行级过滤。tenant_id 必须为索引字段,否则引发全表扫描。
租户上下文传递链路
graph TD
A[HTTP Request] --> B[MiddleWare 解析 TenantID]
B --> C[ctx.WithValue(ctx, tenantKey, id)]
C --> D[GORM Query Hook]
D --> E[自动追加 WHERE 条件]
2.3 并发场景下全局变量与sync.Pool引发的租户上下文污染复现与规避
复现场景:共享Pool导致租户ID错乱
当多个 goroutine 复用 sync.Pool 中预分配的上下文对象,而该对象含未重置的 tenantID 字段时,即发生污染:
var ctxPool = sync.Pool{
New: func() interface{} {
return &TenantContext{TenantID: ""} // ❌ 缺少初始化关键字段
},
}
func HandleRequest(tenantID string) {
ctx := ctxPool.Get().(*TenantContext)
ctx.TenantID = tenantID // ✅ 赋值
// ... 业务逻辑
ctxPool.Put(ctx) // ❌ 未清空,下次Get可能携带旧tenantID
}
逻辑分析:
sync.Pool不保证对象复用前状态归零;Put后对象仍保留在池中,下次Get可能返回含残留TenantID的实例,造成跨租户数据泄漏。
根本规避策略
- ✅ 每次
Get后强制重置敏感字段(如ctx.Reset()) - ✅ 改用
context.WithValue+context.Context隔离租户态(不可变) - ✅ 禁止在
sync.Pool对象中存储租户标识等有状态字段
| 方案 | 线程安全 | 租户隔离性 | 内存开销 |
|---|---|---|---|
| 全局变量+Mutex | ✅ | ❌(易锁遗漏) | 低 |
| sync.Pool+Reset | ✅ | ✅(需严格重置) | 中 |
| context.Context | ✅ | ✅(天然不可变) | 略高 |
graph TD
A[HTTP Request] --> B{Goroutine}
B --> C[Get from sync.Pool]
C --> D[Reset TenantContext]
D --> E[Set tenantID]
E --> F[Business Logic]
F --> G[Put back to Pool]
2.4 分布式缓存(Redis)中租户Key空间隔离与自动前缀注入方案
多租户场景下,Redis 原生无租户概念,需通过 Key 命名空间隔离避免数据越界。核心策略是运行时自动注入租户标识前缀,而非由业务层硬编码拼接。
自动前缀注入中间件(Spring Boot 示例)
@Component
public class TenantKeyPrefixInterceptor implements RedisTemplateCustomizer<RedisTemplate> {
@Override
public void customize(RedisTemplate template) {
template.setKeySerializer(new RedisSerializer<String>() {
@Override
public byte[] serialize(String key) throws SerializationException {
String tenantId = TenantContext.getCurrentTenant(); // 从 ThreadLocal 获取
String prefixedKey = tenantId + ":" + key; // 如 "t_123:user:1001"
return prefixedKey.getBytes(StandardCharsets.UTF_8);
}
// deserialize 略(反向剥离前缀需谨慎,通常仅用于写入)
});
}
}
逻辑分析:该拦截器在
RedisTemplate序列化阶段动态注入tenantId:前缀,确保所有set("user:1001", ...)实际写入t_123:user:1001。参数TenantContext.getCurrentTenant()依赖请求上下文(如网关透传或 JWT 解析),要求调用链全程保持租户上下文传递。
隔离效果对比表
| 方式 | 租户可见性 | 冲突风险 | 运维复杂度 | 是否支持租户级 TTL/驱逐 |
|---|---|---|---|---|
| 无前缀(共享 Key 空间) | 全部可见 | 高 | 低 | 否 |
| 手动拼接(业务层) | 可控 | 中(易遗漏) | 高(侵入性强) | 否 |
| 自动前缀注入(中间件) | 完全隔离 | 低 | 低(一次配置) | 是(通过前缀批量操作) |
数据同步机制
当需跨租户迁移或备份时,可基于前缀扫描:
# Redis CLI 按租户前缀批量导出
KEYS "t_123:*" # ⚠️ 生产禁用,应改用 SCAN
SCAN 0 MATCH "t_123:*" COUNT 1000
SCAN替代KEYS避免阻塞,配合COUNT控制单次扫描粒度,保障线上稳定性。
graph TD
A[业务调用 set\(\"user:1001\"\)] --> B{RedisTemplate.serialize}
B --> C[获取当前租户ID]
C --> D[拼接 t_123:user:1001]
D --> E[写入 Redis]
2.5 微服务间gRPC调用链中租户标识的跨进程传递与校验机制
在多租户微服务架构中,租户上下文(如 tenant-id)必须在 gRPC 调用链中透明透传且不可篡改,否则将引发数据越权访问。
租户标识注入与提取
gRPC 拦截器统一处理元数据:客户端注入 tenant-id 到 metadata.MD,服务端从 ctx 中解析并校验有效性。
// 客户端拦截器:注入租户ID(需从当前上下文获取)
func tenantInjectInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
md, _ := metadata.FromOutgoingContext(ctx)
md = md.Copy()
md.Set("tenant-id", "t-8a9b") // 实际应从 ctx.Value(TenantKey) 获取
ctx = metadata.NewOutgoingContext(ctx, md)
return invoker(ctx, method, req, reply, cc, opts...)
}
逻辑说明:
metadata.NewOutgoingContext将租户标识写入 HTTP/2 HEADERS 帧;tenant-id必须经上游鉴权服务签发,禁止前端直传。
服务端校验流程
graph TD
A[接收gRPC请求] --> B{解析metadata.tenant-id}
B -->|缺失/格式非法| C[返回400]
B -->|存在| D[查租户白名单缓存]
D -->|未命中| E[同步调用TenantService校验]
D -->|有效| F[注入ctx.WithValue(TenantKey, id)]
校验策略对比
| 策略 | 延迟 | 安全性 | 适用场景 |
|---|---|---|---|
| 缓存校验 | 中(需防缓存污染) | 高频读场景 | |
| 强一致性校验 | ~15ms | 高(实时鉴权) | 支付/权限变更 |
- 所有业务方法必须通过
ctx.Value(TenantKey)获取租户ID,禁止解析原始HTTP头或参数; - 租户ID在日志、链路追踪(如 Jaeger)中自动脱敏(如
t-8a9b→t-****)。
第三章:计费模块错乱的技术归因与精准治理
3.1 计费精度丢失:time.Time时区漂移与纳秒级计费窗口对齐实践
在分布式计费系统中,time.Now() 默认返回本地时区时间,跨节点时区不一致会导致纳秒级窗口错位。例如,上海节点(CST, UTC+8)与法兰克福节点(CET, UTC+1)同一物理时刻的 UnixNano() 值相差达25.2亿纳秒——足以跨越多个100ms计费桶。
核心问题定位
time.Time序列化未强制归一化到UTC- 计费窗口边界(如每分钟整点)依赖
t.Truncate(time.Minute),但若t含本地时区,截断逻辑随系统时区偏移而漂移
UTC归一化实践
// ✅ 强制转UTC再截断,确保窗口对齐
func alignedBillingWindow(t time.Time) time.Time {
return t.UTC().Truncate(100 * time.Millisecond) // 纳秒级对齐基线
}
逻辑分析:
t.UTC()消除时区元数据,返回等效UTC时间点;Truncate在纳秒维度向下取整至最近100ms边界(如10:00:00.123456789→10:00:00.100000000),避免因浮点舍入或时区转换引入亚毫秒偏差。
窗口对齐效果对比
| 输入时间(本地) | 未归一化截断结果 | UTC归一化截断结果 |
|---|---|---|
| 2024-05-01 10:00:00.999999999 CST | 10:00:00.900000000 CST | 02:00:00.900000000 UTC |
| 2024-05-01 10:00:00.999999999 CET | 10:00:00.900000000 CET | 09:00:00.900000000 UTC |
graph TD
A[原始time.Time] --> B{含本地时区?}
B -->|是| C[调用 .UTC()]
B -->|否| D[直接Truncate]
C --> D
D --> E[纳秒级对齐窗口]
3.2 多租户资源用量聚合中的竞态条件复现与atomic.Value+shard map优化方案
竞态复现场景
当100+租户并发上报CPU/内存用量(每秒千级写入),共享map[string]int64统计结构时,fatal error: concurrent map writes高频触发。
原始代码缺陷
var usageMap = make(map[string]int64) // 非线程安全
func AddUsage(tenantID string, delta int64) {
usageMap[tenantID] += delta // 竞态点:读-改-写非原子
}
usageMap[tenantID] += delta展开为三步:读旧值→计算新值→写入。多goroutine交叉执行导致数据丢失或panic。
优化方案对比
| 方案 | 吞吐量(QPS) | 内存放大 | GC压力 |
|---|---|---|---|
sync.RWMutex + map |
~8k | 1.0x | 中 |
atomic.Value + shard map |
~42k | 1.3x | 低 |
分片+原子替换实现
type Shard struct {
m map[string]int64
}
type UsageAggregator struct {
shards [16]atomic.Value // 16个分片,按tenantID哈希路由
}
func (a *UsageAggregator) Add(tenantID string, delta int64) {
idx := int(uint32(hash(tenantID)) % 16)
shard := a.shards[idx].Load().(*Shard)
shard.m[tenantID] += delta // 单分片内无竞争
}
atomic.Value确保分片指针更新的原子性;hash(tenantID) % 16将租户均匀分散至16个独立map,消除全局锁瓶颈。
3.3 账单生成任务在Cron调度器中的租户维度分片执行与幂等性保障
租户分片策略设计
采用哈希取模 + 动态租户白名单机制,确保每个 Cron 实例仅处理归属租户子集:
def get_tenant_shard(tenant_id: str, total_instances: int, instance_index: int) -> bool:
# tenant_id 经 SHA256 后取前8字节转为整数,避免长ID分布不均
shard_key = int(hashlib.sha256(tenant_id.encode()).hexdigest()[:8], 16)
return (shard_key % total_instances) == instance_index
逻辑分析:total_instances 对应部署的 Worker 数量(如4),instance_index 由环境变量注入(0~3);哈希确保租户分配稳定可预测,避免分片漂移。
幂等性核心保障
- 每次账单生成前校验
bill_date + tenant_id唯一索引是否存在 - 使用数据库
INSERT ... ON CONFLICT DO NOTHING原子写入
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
tenant_id |
VARCHAR(32) | PK | 租户唯一标识 |
bill_month |
DATE | PK | 账期起始日(如 ‘2024-05-01’) |
status |
TEXT | NOT NULL | ‘generated’, ‘failed’, ‘skipped’ |
执行流程概览
graph TD
A[Cron触发] --> B{按实例索引筛选租户}
B --> C[加载待生成账期租户列表]
C --> D[逐租户执行生成+DB幂等插入]
D --> E[记录执行摘要至审计表]
第四章:可观测性缺失导致的排障黑洞与体系化建设
4.1 OpenTelemetry SDK定制:自动注入tenant_id与workspace_id至Span与Metric标签
在多租户SaaS系统中,需将上下文标识透传至所有遥测数据。OpenTelemetry SDK 提供 SpanProcessor 与 MeterProviderSdk 扩展点实现无侵入注入。
自定义 SpanProcessor 注入上下文标签
class TenantContextSpanProcessor(SpanProcessor):
def on_start(self, span: Span, parent_context=None):
# 从当前上下文提取租户与工作区ID(如通过 contextvars 或 WSGI environ)
tenant_id = get_current_tenant_id() # 如从 request.headers.get("X-Tenant-ID")
workspace_id = get_current_workspace_id()
if tenant_id:
span.set_attribute("tenant_id", tenant_id)
if workspace_id:
span.set_attribute("workspace_id", workspace_id)
逻辑说明:该处理器在 Span 创建时触发,利用 OpenTelemetry 的
contextvars或框架钩子(如 Flaskg、Djangorequest) 获取运行时租户上下文,并以标准属性写入 Span —— 确保所有 trace 数据天然携带租户维度,无需业务代码显式调用set_attribute。
Metric 标签注入策略对比
| 方式 | 适用场景 | 是否支持动态值 | 备注 |
|---|---|---|---|
Attributes(手动传入) |
单点指标打点 | ✅ | 需业务层感知,易遗漏 |
View + AttributeFilter |
全局聚合控制 | ❌(静态) | 适用于固定标签 |
MeterProvider 包装器 |
全量指标自动增强 | ✅ | 推荐:统一拦截 create_counter() 等方法 |
数据同步机制
graph TD
A[HTTP Request] --> B[Middleware: 解析 X-Tenant-ID/X-Workspace-ID]
B --> C[contextvars.set_context(...)]
C --> D[SpanProcessor / MeterWrapper]
D --> E[Span/Metric with tenant_id & workspace_id]
4.2 日志结构化增强:Zap Hook实现租户上下文字段零侵入注入
在多租户系统中,日志需天然携带 tenant_id、workspace_id 等上下文字段,但传统方式需手动在每处 logger.Info() 中传入 zap.String("tenant_id", tid),违反关注点分离。
零侵入核心机制
Zap Hook 在日志写入前拦截 *zapcore.Entry 和 []zapcore.Field,动态注入租户字段:
func TenantHook(tenantGetter func() string) zapcore.Hook {
return zapcore.HookFunc(func(entry zapcore.Entry, fields []zapcore.Field) error {
if tid := tenantGetter(); tid != "" {
fields = append(fields, zap.String("tenant_id", tid))
}
return nil
})
}
逻辑分析:该 Hook 不修改 entry 本身(不可变),而是直接扩展现有
fields切片;tenantGetter通过context.WithValue()或 goroutine-local storage(如gctx.GetTenantID())安全获取当前租户标识,确保异步/并发安全。
上下文绑定策略对比
| 方式 | 侵入性 | 动态性 | 线程安全 |
|---|---|---|---|
| 手动传参 | 高 | 强 | 依赖调用方 |
| Context.Value + Hook | 低 | 强 | ✅(需正确传递 context) |
| 全局变量 | 无 | 弱 | ❌(goroutine 冲突) |
注入时序流程
graph TD
A[log.Info\\\"user login\\\"] --> B{Zap Core Write}
B --> C[Zap Hook 触发]
C --> D[调用 tenantGetter]
D --> E[追加 tenant_id 字段]
E --> F[序列化为 JSON 输出]
4.3 多租户Prometheus指标熔断:基于label_values动态限流与异常租户自动降级
为防止单个租户指标暴增拖垮全局采集,需在Prometheus联邦层前置熔断逻辑。
动态限流策略
通过label_values(instance, tenant_id)实时获取活跃租户列表,结合rate(metrics_total{job="federate"}[5m])计算各租户QPS:
# 每租户5分钟指标写入速率(单位:条/秒)
sum by (tenant_id) (
rate(federated_metrics_total{job="federate"}[5m])
)
该查询按tenant_id聚合速率,作为限流决策依据;federated_metrics_total为自定义计数器,记录每次联邦拉取的样本数。
自动降级流程
graph TD
A[采集入口] --> B{tenant_id QPS > 阈值?}
B -- 是 --> C[暂停该tenant_id联邦目标]
B -- 否 --> D[正常采集]
C --> E[触发告警并标记“降级中”]
关键配置参数
| 参数 | 默认值 | 说明 |
|---|---|---|
max_tenant_qps |
500 | 单租户每秒最大允许指标量 |
degrade_window |
10m | 降级持续时间,超时后自动恢复 |
check_interval |
30s | 熔断状态检查周期 |
- 降级动作由Prometheus Operator的
PrometheusRule驱动; - 恢复依赖
tenant_health{status="ok"}指标重置状态。
4.4 分布式追踪火焰图租户着色:Jaeger UI插件开发与租户性能热点定位实战
在多租户SaaS系统中,需快速区分各租户调用链的性能瓶颈。Jaeger原生UI不支持火焰图按租户着色,需通过自定义UI插件扩展。
租户上下文注入机制
服务端需在Span标签中注入tenant_id(如tag: tenant_id=acme-prod),确保所有Span携带租户标识。
Jaeger UI插件核心逻辑
// src/plugins/tenant-flame-colorer.ts
export const TenantFlameColorer = (span: Span): string => {
const tenantId = span.tags?.find(t => t.key === 'tenant_id')?.value as string;
return tenantId ? d3.interpolateRainbow(hashString(tenantId) % 1) : '#ccc';
};
hashString()将租户ID映射为稳定整数,d3.interpolateRainbow生成高对比度色值,确保同租户Span在火焰图中呈现统一色调。
租户热点识别流程
graph TD
A[Jaeger Query API] --> B[获取Trace数据]
B --> C[按tenant_id分组Span]
C --> D[聚合耗时/错误率]
D --> E[火焰图着色渲染]
| 租户ID | P95延迟(ms) | 错误率 | 热点服务 |
|---|---|---|---|
| acme-prod | 842 | 0.3% | payment-service |
| demo-staging | 127 | 0.0% | auth-service |
第五章:从排障到架构演进的思考闭环
在某电商平台大促压测期间,订单服务突发 30% 的超时率上升,监控显示 MySQL 主库 CPU 持续 95%+,但慢查询日志中并无新增慢 SQL。团队按传统路径排查:检查连接池、确认索引有效性、分析执行计划——均无异常。直到通过 pt-query-digest 对全量 binlog 进行回放分析,发现高频执行的 UPDATE order_status SET status = ? WHERE order_id = ? 语句虽走主键索引,却因二级索引 idx_user_id_created_at 的频繁更新引发大量 Buffer Pool 写放大与脏页刷盘竞争。
根因定位的思维跃迁
真正瓶颈不在 SQL 本身,而在索引维护成本与写负载的耦合关系。该索引服务于后台运营报表,但被强制部署在核心交易库,导致每笔订单状态变更需同步更新 3 个索引页(主键 + 2 个二级索引)。我们将此链路拆解为以下因果环:
graph LR
A[订单状态变更] --> B[主键索引页修改]
B --> C[二级索引 idx_user_id_created_at 更新]
C --> D[Buffer Pool 脏页激增]
D --> E[Checkpoint 频率升高]
E --> F[IO 竞争加剧 & 主库响应延迟]
F --> A
数据分层策略落地
立即行动包括:
- 将
idx_user_id_created_at迁移至只读从库(应用层路由开关控制); - 在交易库中创建物化视图
mv_order_summary(按天聚合),替代原报表的实时 JOIN 查询; - 引入 Kafka 订阅 binlog,由 Flink 实时计算用户维度订单统计,写入独立 OLAP 库。
架构决策的量化验证
迁移前后关键指标对比:
| 指标 | 迁移前 | 迁移后 | 变化 |
|---|---|---|---|
| 主库平均写延迟 | 42ms | 8ms | ↓81% |
| 大促峰值 QPS 容量 | 12,500 | 28,300 | ↑126% |
| 报表查询 P95 延迟 | 3.2s | 1.7s | ↓47% |
| DBA 日均告警次数 | 17 | 2 | ↓88% |
组织协同机制固化
建立「故障复盘→模式识别→架构提案→灰度验证→反哺文档」五步闭环:
- 所有 P1 级故障必须输出《技术债映射表》,明确关联的架构缺陷项;
- 架构委员会每月评审高危模式(如“跨域索引共置”“强一致性报表直连交易库”);
- 新服务上线前需通过《数据契约检查清单》,含读写分离拓扑、索引生命周期、降级开关三类必填项。
某次支付回调失败事件中,团队发现重复回调重试机制未做幂等 key 分片,导致 Redis 热点 Key 集中于单分片,而该问题早在三个月前另一服务的缓存雪崩事故中已暴露同类模式——此次直接调用历史方案模板,2 小时内完成分片策略改造并上线。
当运维工程师在 Grafana 中看到主库 CPU 回落至 40% 并持续稳定,他打开 Confluence 文档库,在「索引治理规范」页新增了一条加粗备注:“禁止在交易核心库中部署非事务强相关且更新频次 >1000TPS 的二级索引”。
