第一章:Go语言Web接口幂等性设计全景概览
幂等性是构建高可靠、可重试 Web 服务的基石。在分布式系统中,网络超时、客户端重发、负载均衡重试等场景极易导致同一业务请求被多次执行,若后端未做幂等防护,将引发重复扣款、订单裂变、库存超卖等严重数据一致性问题。Go 语言凭借其轻量协程、强类型约束与丰富中间件生态,为实现细粒度、高性能的幂等控制提供了理想载体。
幂等性的核心判定维度
一个请求是否幂等,取决于三要素的联合唯一性:
- 业务标识(Business ID):如订单号、支付流水号;
- 操作语义(Operation Type):如
CREATE_ORDER、REFUND_PAYMENT; - 请求指纹(Request Fingerprint):基于请求体(Body)、关键查询参数(Query)及头部(如
X-Request-ID)生成的 SHA-256 哈希值,排除时间戳、随机数等非幂等字段。
常见实现策略对比
| 策略 | 适用场景 | Go 实现要点 |
|---|---|---|
| Token + Redis | 创建类操作(如下单) | 服务端预发 token,客户端提交时携带,Redis SETNX 校验并设置过期 |
| 数据库唯一约束 | 具备天然唯一键的业务(如用户注册) | 在 users(email) 上建唯一索引,捕获 ErrDuplicateEntry |
| 状态机 + 版本号 | 多阶段更新(如订单状态流转) | UPDATE orders SET status=?, version=? WHERE id=? AND version=? |
快速接入示例:基于 Redis 的通用幂等中间件
func IdempotentMiddleware(redisClient *redis.Client, expire time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
// 提取客户端传入的幂等键(建议由前端生成并传递)
idempotencyKey := c.GetHeader("X-Idempotency-Key")
if idempotencyKey == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "missing X-Idempotency-Key"})
return
}
// 尝试原子写入:仅当 key 不存在时才设置成功
status := redisClient.SetNX(c, "idempotent:"+idempotencyKey, "1", expire).Val()
if !status {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": "request already processed"})
return
}
c.Next() // 继续处理业务逻辑
}
}
该中间件可无缝集成 Gin 框架,在路由注册时链式调用:r.POST("/order", IdempotentMiddleware(rdb, 10*time.Minute), createOrderHandler)。注意:X-Idempotency-Key 应由客户端在首次请求时生成(如 UUID),并在重试时严格复用。
第二章:基于Token机制的幂等性实现
2.1 Token生成策略与JWT签名验证实践
JWT结构与签名原理
JWT由Header、Payload、Signature三部分组成,通过base64url(Header).base64url(Payload)拼接后,用密钥对结果进行HMAC-SHA256签名。
安全Token生成实践
- 使用强随机数生成器生成
exp(建议15–30分钟) - 必填标准声明:
iss(服务标识)、sub(用户ID)、iat(签发时间) - 禁止在Payload中存放敏感数据(即使签名有效,内容仍可解码)
签名验证核心逻辑
import jwt
from datetime import datetime, timedelta
SECRET_KEY = "prod-secret-2024" # 生产环境应从KMS或环境变量加载
def generate_token(user_id: str) -> str:
payload = {
"sub": user_id,
"iss": "auth-service",
"iat": int(datetime.utcnow().timestamp()),
"exp": int((datetime.utcnow() + timedelta(minutes=20)).timestamp())
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
逻辑分析:
jwt.encode()先序列化payload为JSON,UTF-8编码后与header一起base64url编码,再用HS256算法与SECRET_KEY生成签名。exp和iat必须为整型时间戳(秒级),否则验证将失败。
验证流程状态机
graph TD
A[接收JWT] --> B{格式校验<br>是否含3段?}
B -->|否| C[拒绝:Malformed Token]
B -->|是| D[Base64解码头部获取alg]
D --> E[查表确认alg是否白名单]
E -->|否| C
E -->|是| F[用密钥重算Signature]
F --> G{匹配原始Signature?}
G -->|否| H[拒绝:Invalid Signature]
G -->|是| I[解析Payload并校验exp/iat/nbf]
常见签名算法对比
| 算法 | 密钥类型 | 是否支持密钥轮换 | 推荐场景 |
|---|---|---|---|
| HS256 | 对称密钥 | ❌(需全局同步更新) | 内部微服务间通信 |
| RS256 | RSA非对称 | ✅(公钥分发,私钥保密) | 开放平台/OAuth2授权服务器 |
2.2 分布式环境下Token分发与防重放设计
核心挑战
跨服务、多节点场景下,Token需满足全局唯一性、时效可控性与不可重放性。单纯依赖本地时间戳或UUID易引发时钟漂移冲突或碰撞风险。
时间窗口+随机熵防重放
import time, secrets
from hashlib import sha256
def generate_nonce_token(user_id: str) -> str:
ts = int(time.time() * 1000) // 30000 # 30s滑动窗口(毫秒级对齐)
entropy = secrets.token_hex(8) # 16字节强随机熵
return sha256(f"{user_id}:{ts}:{entropy}".encode()).hexdigest()
逻辑说明:
ts按30秒切片实现时间窗口归一化,规避NTP偏差;entropy确保同一窗口内Token不重复;SHA256输出固定长度哈希,天然抗碰撞。服务端校验时仅需验证窗口±1范围内是否存在已用Nonce(Redis Set去重)。
防重放校验流程
graph TD
A[客户端请求] --> B[携带Token+Timestamp]
B --> C{网关解析Token}
C --> D[提取ts片段 → 计算合法窗口范围]
D --> E[查Redis: token_set:{ts-1}..{ts+1}]
E -->|存在| F[拒绝:重放攻击]
E -->|不存在| G[写入token_set:ts并放行]
关键参数对比
| 参数 | 值 | 说明 |
|---|---|---|
| 窗口粒度 | 30s | 平衡安全性与时钟容错 |
| Entropy长度 | 16 bytes | 满足128位熵值,抗暴力枚举 |
| Redis TTL | 90s | 覆盖窗口±1且留缓冲期 |
2.3 Go标准库net/http中间件封装Token校验逻辑
中间件设计原则
遵循单一职责与无侵入性:不修改原 handler,仅在请求链路中注入校验逻辑。
Token校验中间件实现
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
http.Error(w, "missing or malformed Bearer token", http.StatusUnauthorized)
return
}
tokenStr := strings.TrimPrefix(auth, "Bearer ")
if !isValidToken(tokenStr) { // 实际应解析JWT并验证签名/过期
http.Error(w, "invalid token", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
逻辑分析:
AuthMiddleware接收原始http.Handler,返回新 handler。提取Authorization头,校验前缀与令牌有效性;失败则立即响应错误,成功才调用next。isValidToken应对接 JWT 解析库(如golang-jwt/jwt/v5),验证签名、exp、iss等声明。
校验流程示意
graph TD
A[Request] --> B{Has Authorization header?}
B -->|No| C[401 Unauthorized]
B -->|Yes| D{Valid Bearer format?}
D -->|No| C
D -->|Yes| E[Parse & Validate JWT]
E -->|Invalid| F[403 Forbidden]
E -->|Valid| G[Call next Handler]
常见校验参数对照
| 参数 | 说明 | 是否必需 |
|---|---|---|
Authorization |
Bearer <token> 格式头 |
是 |
exp |
过期时间戳(Unix秒) | 是 |
iss |
签发者标识 | 推荐 |
2.4 基于gin框架的Token幂等拦截器实战开发
在高并发场景下,重复提交(如网络重试、按钮连点)易导致订单重复创建、库存超扣等问题。Token幂等拦截器通过“预发令牌 + 校验删除”双阶段机制保障接口幂等性。
核心流程设计
func IdempotentMiddleware(store redis.Cmdable) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("X-Idempotent-Token")
if token == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "missing X-Idempotent-Token"})
return
}
// 原子性校验并删除:若存在则通过,否则拒绝
n, _ := store.Del(context.Background(), "idempotent:"+token).Result()
if n == 0 {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": "duplicate request"})
return
}
c.Next()
}
}
逻辑分析:
store.Del()原子执行校验与清理,避免竞态;"idempotent:"+token构成唯一键,TTL需在业务侧预设(如30s)。参数store支持任意兼容 redis.Cmdable 的客户端(如 redis-go、rueidis)。
关键约束说明
- Token 必须由服务端生成(如
/api/v1/token接口返回 UUIDv4) - 客户端需在每次请求中携带且仅使用一次
- 后端需确保幂等Key的TTL ≥ 单次请求最大处理时长
| 组件 | 职责 |
|---|---|
| 前端 | 获取Token → 携带Header → 禁用提交按钮 |
| Gin中间件 | 校验+删除Token,阻断重复请求 |
| Redis | 提供原子性、高性能的Token存储 |
2.5 Token过期、刷新与并发请求冲突的边界处理
并发刷新导致的Token覆盖问题
当多个请求几乎同时检测到Token过期,可能触发多次刷新请求,最终后返回的refresh_token覆盖先返回的有效凭证。
// 使用互斥锁确保单次刷新
let refreshPromise = null;
function getValidToken() {
if (!isExpired()) return localStorage.getItem('access_token');
if (refreshPromise) return refreshPromise;
refreshPromise = fetch('/auth/refresh', { method: 'POST' })
.then(r => r.json())
.then(data => {
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
return data.access_token;
})
.finally(() => refreshPromise = null);
return refreshPromise;
}
逻辑分析:refreshPromise缓存未决的刷新Promise,避免重复发起请求;.finally()确保锁释放,支持下一轮刷新。关键参数:access_token(短期有效)和refresh_token(长期但单次有效)。
状态同步策略对比
| 策略 | 原子性 | 客户端复杂度 | 适用场景 |
|---|---|---|---|
| 全局锁 + Promise缓存 | 强 | 低 | 中低并发SPA |
| 时间戳+版本号校验 | 中 | 高 | 多端协同系统 |
graph TD
A[请求发出] --> B{Token是否过期?}
B -- 是 --> C[检查refreshPromise是否存在]
C -- 存在 --> D[等待并复用现有Promise]
C -- 不存在 --> E[发起刷新请求并缓存Promise]
E --> F[持久化新Token并释放锁]
B -- 否 --> G[直接携带Token请求]
第三章:Redis原子操作驱动的幂等控制
3.1 Redis SETNX+EXPIRE组合实现幂等令牌池
在高并发场景下,需确保同一业务请求仅被处理一次。SETNX(SET if Not eXists)配合EXPIRE可构建轻量级幂等令牌池。
核心原子性保障
Redis 不支持 SETNX + EXPIRE 原子执行,需规避竞态:
# 非原子操作(❌风险)
SETNX order:token:abc123 1
EXPIRE order:token:abc123 60
⚠️ 若
SETNX成功但EXPIRE失败,令牌将永不过期,导致池泄露。应改用SET key value EX seconds NX(Redis 2.6.12+):
# 原子写入(✅推荐)
SET order:token:abc123 1 EX 60 NX
EX 60指定60秒过期;NX确保仅当key不存在时设置。失败返回nil,成功返回OK,天然支持幂等校验。
令牌生命周期管理
| 阶段 | 操作 | 说明 |
|---|---|---|
| 申请 | SET ... NX EX |
获取唯一、限时令牌 |
| 校验 | GET key |
存在即表示已申请,拒绝重复提交 |
| 清理 | 自动过期(无需手动删) | 避免人工干预与残留风险 |
graph TD
A[客户端请求] --> B{SET token EX 60 NX}
B -- OK --> C[接受请求,进入业务流程]
B -- nil --> D[拒绝重复提交]
3.2 Lua脚本保障Redis操作的原子性与一致性
Redis 单命令天然原子,但多步操作(如“读-改-写”)需靠 Lua 脚本实现事务级一致性。
为什么需要 Lua?
- Redis 执行 Lua 脚本时全程阻塞单线程,确保脚本内所有命令串行、不可中断、无竞态;
EVAL/EVALSHA将逻辑封装为原子单元,规避WATCH+MULTI/EXEC的乐观锁复杂性与失败重试开销。
典型场景:库存扣减
-- KEYS[1] = "inventory:1001", ARGV[1] = "1"
local stock = redis.call("GET", KEYS[1])
if not stock or tonumber(stock) < tonumber(ARGV[1]) then
return -1 -- 库存不足
end
redis.call("DECRBY", KEYS[1], ARGV[1])
return tonumber(stock) - tonumber(ARGV[1])
逻辑分析:脚本一次性完成“读取→校验→扣减”,避免并发下单导致超卖。
KEYS和ARGV分离键与参数,提升复用性与安全性。
Lua 原子性保障对比
| 方式 | 原子性 | 一致性 | 网络往返 | 失败处理 |
|---|---|---|---|---|
| 原生命令 | ✅ 单命令 | ❌ 多步不保 | 多次 | 无自动回滚 |
| MULTI/EXEC | ⚠️ 伪事务(无回滚) | ⚠️ 依赖 WATCH | 多次 | 需客户端重试 |
| Lua 脚本 | ✅ 全脚本 | ✅ 强一致 | 一次 | 内置逻辑控制 |
graph TD
A[客户端发送 EVAL] --> B[Redis 解析并加载脚本]
B --> C[锁定当前执行线程]
C --> D[逐行执行 Lua 命令]
D --> E[返回结果,释放线程]
3.3 Go redis-go客户端幂等写入封装与错误重试机制
幂等写入设计原理
利用 Redis 的 SET key value NX PX timeout 原子指令,确保同一业务ID(如 order:123)仅首次写入成功,后续重复请求返回 nil。
重试策略配置
- 指数退避:初始延迟 10ms,最大 5 次尝试
- 错误过滤:仅对
redis.Nil(键已存在)以外的网络/超时错误重试
封装示例代码
func IdempotentSet(ctx context.Context, client *redis.Client, key, value string, ttl time.Duration) error {
status := client.SetNX(ctx, key, value, ttl)
if err := status.Err(); err != nil {
return fmt.Errorf("redis setnx failed: %w", err)
}
ok, _ := status.Result()
if !ok {
return redis.Nil // 幂等成功,逻辑上非错误
}
return nil
}
SetNX返回bool表示是否设置成功;redis.Nil是预期结果(键已存在),不视为异常;ctx支持超时与取消传播。
重试流程图
graph TD
A[调用IdempotentSet] --> B{成功?}
B -->|是| C[返回nil]
B -->|否| D{是否可重试?}
D -->|是| E[指数退避后重试]
D -->|否| F[返回原始错误]
E --> B
第四章:数据库唯一约束与状态机协同方案
4.1 基于业务主键+唯一索引的强一致性幂等落库
核心思想:利用数据库唯一约束拦截重复插入,确保同一业务实体(如订单号、支付流水号)仅成功写入一次。
数据同步机制
业务系统在插入前不查库,直接 INSERT ... ON CONFLICT DO NOTHING(PostgreSQL)或 INSERT IGNORE(MySQL),依赖唯一索引(如 UNIQUE (biz_order_id))触发冲突拒绝。
-- 假设订单表已建唯一索引
CREATE UNIQUE INDEX uk_order_biz_id ON t_order(biz_order_id);
INSERT INTO t_order (biz_order_id, amount, status, created_at)
VALUES ('ORD20240001', 99.9, 'PAID', NOW())
ON CONFLICT (biz_order_id) DO NOTHING; -- 冲突时静默忽略
逻辑分析:
ON CONFLICT (biz_order_id)显式指定冲突列,避免全表扫描;DO NOTHING保证原子性,无需额外事务控制。参数biz_order_id即业务主键,天然具备全局唯一性和业务语义。
关键保障要素
- ✅ 唯一索引必须覆盖完整业务主键(不可为前缀索引)
- ✅ 应用层需统一生成/传递业务主键,禁止数据库自增ID替代
- ❌ 不可依赖
SELECT + INSERT(存在竞态窗口)
| 组件 | 职责 |
|---|---|
| 业务服务 | 生成并透传 biz_order_id |
| 数据库 | 唯一索引校验与原子拦截 |
| 监控告警 | 捕获 unique_violation 异常量突增 |
graph TD
A[请求到达] --> B{携带 biz_order_id?}
B -->|是| C[执行带冲突处理的INSERT]
B -->|否| D[拒绝:缺失幂等凭证]
C --> E{DB返回成功?}
E -->|是| F[落库完成]
E -->|否 unique_violation| G[视为已存在,幂等成功]
4.2 幂等记录表设计与GORM迁移脚本编写
数据同步机制
为保障分布式任务重试不重复消费,需持久化操作指纹。核心字段包括唯一业务键、操作类型、状态及时间戳。
表结构设计
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, auto_increment | 主键 |
| biz_key | VARCHAR(128) | NOT NULL, UNIQUE | 业务唯一标识(如 order_123) |
| operation_type | VARCHAR(32) | NOT NULL | CREATE/UPDATE/DELETE |
| status | TINYINT | DEFAULT 1 | 1=success, 0=failed |
| created_at | DATETIME | NOT NULL | 记录首次插入时间 |
GORM 迁移脚本
func (IDempotentRecord) Migrate(db *gorm.DB) error {
return db.AutoMigrate(&IDempotentRecord{})
}
type IDempotentRecord struct {
ID uint64 `gorm:"primaryKey"`
BizKey string `gorm:"size:128;not null;uniqueIndex"`
OperationType string `gorm:"size:32;not null"`
Status int8 `gorm:"default:1"`
CreatedAt time.Time `gorm:"not null"`
}
AutoMigrate 自动创建表并添加唯一索引 biz_key;Status 使用 int8 节省空间;CreatedAt 由 GORM 自动写入,避免应用层时钟偏差。
4.3 状态机建模:从“待处理”到“成功/失败”的幂等跃迁
状态机是保障异步任务幂等性的核心抽象。关键在于禁止非法跃迁,仅允许 PENDING → SUCCESS、PENDING → FAILURE,且禁止 SUCCESS → FAILURE 等反向或覆盖操作。
状态跃迁约束表
| 当前状态 | 允许目标状态 | 是否幂等 |
|---|---|---|
PENDING |
SUCCESS |
✅(首次写入) |
PENDING |
FAILURE |
✅(首次写入) |
SUCCESS |
SUCCESS |
✅(重复提交) |
FAILURE |
FAILURE |
✅(重复提交) |
SUCCESS |
FAILURE |
❌(拒绝) |
状态更新原子操作(PostgreSQL)
UPDATE task_state
SET status = 'SUCCESS',
updated_at = NOW()
WHERE id = $1
AND status IN ('PENDING') -- 关键守卫:仅允许从 PENDING 跃迁
AND version = $2; -- 乐观锁防并发覆盖
逻辑分析:
status IN ('PENDING')强制跃迁起点约束;version字段确保单次成功更新,避免多实例竞态导致的双写。返回affected_rows > 0即表示跃迁生效,否则视为幂等跳过。
状态流转图谱
graph TD
PENDING -->|commit| SUCCESS
PENDING -->|fail| FAILURE
SUCCESS -->|retry| SUCCESS
FAILURE -->|retry| FAILURE
PENDING -.->|invalid| FAILURE
SUCCESS -.->|forbidden| FAILURE
4.4 多阶段事务中状态机与补偿机制联动实践
在分布式订单履约场景中,状态机驱动各阶段生命周期,补偿动作则按失败点动态触发。
状态迁移与补偿注册
状态机每进入 PROCESSING、SHIPPING 等关键节点时,自动注册对应补偿操作(如 cancelPayment()、rollbackInventory()),并持久化至 compensation_log 表:
| step_id | state | compensator | timeout_s | created_at |
|---|---|---|---|---|
| S1002 | SHIPPING | rollbackInventory | 300 | 2024-05-22 14:22 |
补偿执行逻辑示例
// 根据当前状态回溯最近未执行的补偿项
public void triggerCompensation(String orderId) {
CompensationLog log = compensationRepo.findLatestUnexecuted(orderId);
if (log != null && !log.isExecuted()) {
log.getCompensator().execute(); // 如:inventoryService.releaseHold(itemId, qty)
log.markExecuted();
}
}
该方法确保幂等性:markExecuted() 基于数据库 UPDATE ... WHERE executed = false 实现原子更新;findLatestUnexecuted 按 created_at DESC 排序,保障逆序补偿顺序。
整体协作流程
graph TD
A[Order Created] --> B{State: CONFIRMED}
B --> C[Start Payment]
C --> D{Success?}
D -- Yes --> E[State: PAID]
D -- No --> F[Trigger cancelPayment]
F --> G[Update State to FAILED]
第五章:四种方案对比选型与生产环境落地建议
方案核心能力横向对比
下表汇总了在金融级日志审计场景中实测的四项关键指标(基于Kubernetes 1.26集群、日均5TB结构化日志、P99延迟要求≤200ms):
| 方案 | 吞吐量(EPS) | 内存占用(GB/节点) | 索引延迟(P95) | 运维复杂度 | 扩容响应时间 |
|---|---|---|---|---|---|
| Elasticsearch 8.x + ILM | 128,000 | 32 | 142ms | 高(需调优JVM/分片) | 8–12分钟 |
| Loki + Promtail + Grafana | 95,000 | 8 | 89ms | 中(依赖对象存储一致性) | |
| ClickHouse + Vector | 210,000 | 16 | 47ms | 中高(Schema变更需停机) | 3–5分钟 |
| OpenSearch 2.11 + AD | 112,000 | 28 | 118ms | 高(插件兼容性风险) | 6–10分钟 |
生产环境拓扑适配策略
某省级医保平台采用混合部署模式:核心交易链路日志(含PCI-DSS敏感字段)强制走ClickHouse方案,通过Vector的remap处理器实时脱敏;而运维审计日志采用Loki方案,利用其labels机制按team=infra、env=prod自动路由至不同S3存储桶。实际运行中,ClickHouse集群通过ReplicatedReplacingMergeTree引擎实现双AZ数据强一致,故障切换RTO控制在17秒内。
成本与弹性权衡实践
在AWS环境实测显示:Elasticsearch方案单节点月成本为$1,240(r6i.4xlarge + 2TB gp3),而Loki方案仅需$380(c6i.2xlarge + S3标准存储)。但当突发流量达日常300%时,Loki因S3 LIST操作瓶颈导致标签索引延迟飙升至3.2秒,此时需提前配置chunk_pool_max_cached_chunks: 10000并启用boltdb-shipper加速元数据加载。
# ClickHouse生产环境关键配置节选(config.xml)
<max_threads>32</max_threads>
<merge_tree>
<max_bytes_to_merge_at_max_space_in_pool>20000000000</max_bytes_to_merge_at_max_space_in_pool>
<replicated_can_become_leader>1</replicated_can_become_leader>
</merge_tree>
安全合规强化要点
所有方案均需启用传输层加密,但Loki必须配合auth_enabled: true与basic_auth中间件拦截未授权/loki/api/v1/push请求;Elasticsearch则需禁用_cat API并开启xpack.security.audit.enabled: true,审计日志单独写入独立ES集群防篡改。某银行POC中发现OpenSearch的AD插件在TLS 1.3环境下存在证书链验证绕过漏洞(CVE-2023-25189),已通过升级至2.11.1修复。
graph LR
A[日志采集端] -->|Vector TLS 1.3| B{分流网关}
B -->|敏感业务| C[ClickHouse集群<br/>AES-256静态加密]
B -->|审计日志| D[Loki集群<br/>S3 SSE-KMS]
B -->|告警日志| E[Elasticsearch集群<br/>FIPS 140-2认证]
C --> F[BI看板直连]
D --> G[Grafana Loki datasource]
E --> H[SIEM系统API集成]
故障恢复真实案例
2023年Q4某电商大促期间,Elasticsearch集群因分片未均衡触发CIRCUIT_BREAKING_EXCEPTION,导致订单日志丢失11分钟。复盘后将ILM策略从rollover改为delete,并增加shard_allocation.enable: primaries临时保护。同期Loki集群遭遇S3 SlowDown错误,通过将chunk_target_size: 2MB下调至1.2MB并启用table_manager.retention_deletes_enabled: true成功规避。
