Posted in

goroutine泄漏、channel死锁、context超时失效——Go高并发三大隐性杀手,你中了几个?

第一章:goroutine泄漏、channel死锁、context超时失效——Go高并发三大隐性杀手,你中了几个?

在生产环境中,Go程序常因看似“无害”的并发写法悄然失控:内存持续增长却无panic、接口响应缓慢但不报错、服务重启前数小时才暴露异常——这些往往是隐性并发缺陷的晚期症状。

goroutine泄漏的典型征兆

goroutine无法退出导致其栈内存与关联对象长期驻留。常见诱因包括:向已关闭channel发送数据、无限等待未关闭的channel、或忘记cancel context。检测方式:

# 运行时导出goroutine堆栈(需pprof启用)
curl "http://localhost:6060/debug/pprof/goroutine?debug=2"

重点关注select, chan receive, runtime.gopark等状态持续存在的goroutine。

channel死锁的触发场景

当所有goroutine均阻塞于channel操作且无goroutine能推进流程时,程序panic:“all goroutines are asleep – deadlock!”。经典错误模式:

  • 向无缓冲channel写入,但无其他goroutine读取;
  • 使用带缓冲channel但容量为0(make(chan int, 0))并执行同步写;
  • 在单个goroutine中同时读写同一channel(无并发协程配合)。

修复原则:确保至少一个goroutine负责读、另一个负责写;或使用select+default避免永久阻塞。

context超时失效的隐蔽原因

context.WithTimeout看似设定了截止时间,但若下游函数忽略ctx.Done()或未将context传递至I/O调用,则超时形同虚设。验证方法:

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// ✅ 正确:显式监听取消信号
select {
case <-time.After(200 * time.Millisecond):
    // 模拟慢操作
case <-ctx.Done():
    return ctx.Err() // 返回context.Canceled或DeadlineExceeded
}
隐性杀手 根本原因 推荐防御手段
goroutine泄漏 协程失去退出路径 使用pprof定期采样,结合runtime.NumGoroutine()告警阈值监控
channel死锁 同步通信链路断裂 避免单goroutine channel操作;优先使用select+timeout
context超时失效 context未贯穿调用链 所有I/O函数签名强制接收context.Context参数

第二章:goroutine泄漏:看不见的资源吞噬者

2.1 goroutine生命周期与泄漏本质:从调度器视角解析泄漏根源

goroutine 的生命周期始于 go 关键字调用,终于其函数体执行完毕并被 runtime 回收。但若其阻塞在未关闭的 channel、空 select、或无限等待锁,便进入 永久待调度状态——调度器无法感知其“已死亡”,导致内存与栈资源持续占用。

调度器视角下的泄漏判定条件

  • G 状态长期处于 _Gwait 或 _Gsyscall 且无唤醒源
  • 对应的 g.p 已释放,但 g.m 未归还至空闲队列
  • runtime·gcBgMarkWorker 无法扫描到该 G 的栈根(因栈未释放)

典型泄漏模式示例

func leakyWorker(ch <-chan int) {
    for range ch { // 若 ch 永不关闭,goroutine 永不退出
        // 处理逻辑
    }
}

此代码中,range ch 底层调用 chanrecv,当 channel 无数据且未关闭时,G 陷入 gopark 并挂入 waitq;调度器将其标记为 _Gwait,但因无 sender 唤醒或 close 事件,该 G 永远滞留——构成泄漏。

状态 可回收性 触发条件
_Grunning 否 正在 M 上执行
_Gwaiting 是(若可唤醒) 等待 channel/semaphore 等
_Gdead 是 执行结束,等待 GC 清理栈
graph TD
    A[go f()] --> B[G 创建<br/>状态: _Gidle]
    B --> C[投入 runq<br/>状态: _Grunnable]
    C --> D[被 M 调度执行<br/>状态: _Grunning]
    D --> E{f() 返回?}
    E -->|是| F[状态置 _Gdead<br/>栈延迟回收]
    E -->|否| G[阻塞操作<br/>如 ch recv]
    G --> H[状态 _Gwait<br/>挂入 waitq]
    H --> I{是否有唤醒源?}
    I -->|无| J[泄漏:G 持久驻留]
    I -->|有| K[被唤醒→继续执行]

2.2 常见泄漏模式剖析:无限等待channel、闭包捕获导致的引用滞留、defer延迟执行阻塞

无限等待 channel 导致 goroutine 泄漏

当向无缓冲 channel 发送数据,且无协程接收时,发送方将永久阻塞:

func leakByUnbufferedSend() {
    ch := make(chan int)
    go func() { ch <- 42 }() // 永远阻塞,goroutine 无法退出
}

ch <- 42 在无接收者时挂起,该 goroutine 占用栈内存且永不释放,形成泄漏。

闭包捕获引发引用滞留

闭包隐式持有外部变量引用,延长其生命周期:

func leakByClosure() {
    data := make([]byte, 1<<20) // 1MB
    handler := func() { fmt.Println("handled") }
    // 误将 data 传入闭包(即使未使用)
    go func() { _ = handler; time.Sleep(time.Second) }()
    // data 因闭包环境被间接持有,GC 无法回收
}

handler 闭包虽未访问 data,但若编译器无法证明其无关性,可能保守保留整个词法环境。

defer 阻塞资源释放

defer 在函数返回前执行,若依赖未就绪资源则卡住:

场景 风险表现 规避方式
defer db.Close() 连接池耗尽 显式 close + error 检查
defer mu.Unlock() 死锁(未加锁即 defer) 确保 lock/unlock 成对
graph TD
A[goroutine 启动] --> B[执行业务逻辑]
B --> C[遇到 defer 语句]
C --> D[等待 defer 函数可执行]
D --> E{资源是否就绪?}
E -->|否| F[永久挂起]
E -->|是| G[执行 defer 并退出]

2.3 实战检测手段:pprof goroutine profile + runtime.Stack()动态诊断

goroutine profile 快速抓取阻塞线索

启用 pprof 的 goroutine profile 可捕获当前所有 goroutine 的栈快照(含运行中、等待中、空闲状态):

import _ "net/http/pprof"

// 启动 pprof HTTP 服务
go func() { http.ListenAndServe("localhost:6060", nil) }()

访问 http://localhost:6060/debug/pprof/goroutine?debug=2 获取完整栈信息,debug=2 输出含源码行号的详细视图。

runtime.Stack() 精准触发式诊断

适合在可疑逻辑点主动采集:

buf := make([]byte, 1024*1024)
n := runtime.Stack(buf, true) // true: all goroutines; false: current only
log.Printf("Stack dump (%d bytes):\n%s", n, string(buf[:n]))

runtime.Stack() 参数说明:

  • buf: 目标缓冲区,需预先分配足够空间(建议 ≥1MB);
  • all: true 抓全量 goroutine,false 仅当前 goroutine;
  • 返回值 n 为实际写入字节数,避免越界读取。

二者协同诊断策略

场景 推荐手段 响应粒度
持续高并发阻塞 pprof/goroutine?debug=2 全局快照
条件触发死锁嫌疑 runtime.Stack(true) 精确时机
自动化巡检集成 定时调用 + 日志归档 可编程
graph TD
    A[发现goroutine数异常增长] --> B{是否可复现?}
    B -->|是| C[插入runtime.Stack(true)断点]
    B -->|否| D[启动pprof监听并压测]
    C --> E[分析栈中重复wait/chan/block调用]
    D --> F[导出profile后用go tool pprof分析]

2.4 防御性编程实践:带取消机制的worker池与goroutine守卫封装

在高并发服务中,失控的 goroutine 是常见资源泄漏根源。防御性编程要求每个并发单元具备明确生命周期控制能力。

取消感知的 Worker 池核心结构

type WorkerPool struct {
    jobs   <-chan Task
    done   <-chan struct{}
    workers int
}

func NewWorkerPool(jobs <-chan Task, done <-chan struct{}, n int) *WorkerPool {
    return &WorkerPool{jobs: jobs, done: done, workers: n}
}

func (p *WorkerPool) Start() {
    for i := 0; i < p.workers; i++ {
        go func() {
            for {
                select {
                case job, ok := <-p.jobs:
                    if !ok { return }
                    job.Process()
                case <-p.done: // ✅ 关键取消信号入口
                    return
                }
            }
        }()
    }
}

逻辑分析:select 中 <-p.done 优先响应上下文取消;jobs 通道关闭时 ok==false 安全退出;done 通常来自 context.WithCancel(),确保跨层级传播终止信号。

Goroutine 守卫封装模式

守卫类型 触发条件 资源回收动作
Panic 守卫 recover() 捕获 记录错误 + 主动退出
Timeout 守卫 time.AfterFunc 发送 cancel signal
Context 守卫 清理资源 + return

数据同步机制

使用 sync.WaitGroup 确保所有 worker 优雅退出后才释放池资源,避免竞态访问已关闭通道。

2.5 真实案例复盘:HTTP长连接管理器中的goroutine雪崩修复

问题现象

某网关服务在突发流量下,net/http 连接池耗尽,监控显示 goroutine 数量在 30 秒内从 2k 暴增至 15k,P99 延迟飙升至 8s+,触发 OOM kill。

根因定位

// ❌ 错误模式:每个连接独立启 goroutine 处理读写,无并发控制
go func(conn net.Conn) {
    for {
        conn.Read(buf) // 阻塞读,但无超时/取消机制
        handleRequest(buf)
    }
}(c)
  • conn.Read 无读超时,网络抖动或客户端异常断连时 goroutine 永久阻塞;
  • 连接复用未绑定上下文,无法随请求生命周期优雅终止。

修复方案

  • 引入 context.WithTimeout 控制单次 I/O;
  • 使用 sync.Pool 复用 bufio.Reader/Writer;
  • 改为工作协程池模型(固定 32 个 worker),通过 channel 分发连接。

效果对比

指标 修复前 修复后
峰值 goroutine 15,248 3,106
P99 延迟 8.2s 127ms
graph TD
    A[新连接接入] --> B{是否空闲worker?}
    B -->|是| C[分配给worker]
    B -->|否| D[加入等待队列]
    C --> E[带context.ReadTimeout读取]
    E --> F[处理并归还连接]

第三章:channel死锁:同步逻辑的静默崩溃点

3.1 死锁判定原理:Go runtime的deadlock detector工作机制与局限

Go runtime 的死锁检测器在程序退出前触发,仅检查 所有 goroutine 均处于阻塞状态且无 goroutine 可被唤醒 这一全局条件。

检测触发时机

  • 仅在 runtime.main 函数末尾、main goroutine 结束时执行
  • 不实时监控,不介入调度循环

核心判定逻辑

// 简化示意:实际位于 src/runtime/proc.go 中 deadlock() 函数
func checkDeadlock() {
    for _, gp := range allgs { // 遍历所有 goroutine
        if gp.status != _Gwaiting && gp.status != _Gblocked {
            return // 存在可运行或就绪的 G,非死锁
        }
    }
    throw("all goroutines are asleep - deadlock!")
}

逻辑说明:_Gwaiting(等待 channel / timer / network)和 _Gblocked(系统调用中)被视为“不可运行”;若全部 G 均属这两类,且无 netpoll 事件、无定时器到期、无 channel 可唤醒,则判定为死锁。参数 allgs 是全局 goroutine 列表快照,非实时视图。

局限性对比

场景 是否能检测 原因说明
单 goroutine 无限循环 ❌ 状态为 _Grunning,不满足阻塞条件
channel 缓冲区满 + 无接收者 ✅ 发送方 _Gwaiting,无其他 G 可唤醒
定时器未触发但有空闲 G ❌ 存在 _Grunnable G,跳过检测

检测流程(简化)

graph TD
    A[main goroutine exit] --> B{遍历 allgs}
    B --> C[是否存在 _Grunning/_Grunnable?]
    C -->|是| D[退出,不报错]
    C -->|否| E[检查 netpoll 是否有 pending I/O?]
    E -->|是| D
    E -->|否| F[触发 panic: “all goroutines are asleep”]

3.2 典型死锁场景还原:无缓冲channel单向发送、select默认分支缺失、循环依赖goroutine链

无缓冲 channel 单向发送阻塞

当向无缓冲 channel 发送数据,且无协程同时接收时,发送方永久阻塞:

ch := make(chan int)
ch <- 42 // 死锁:无人接收,goroutine 永久挂起

逻辑分析:ch 容量为 0,<- 和 -> 必须同步配对;此处仅执行发送,运行时检测到所有 goroutine 阻塞,触发 panic: “deadlock”。

select 默认分支缺失

以下代码在无就绪 channel 时陷入等待:

ch := make(chan int, 1)
select {
case ch <- 1:
    fmt.Println("sent")
// 缺失 default → 无匹配 case 时阻塞
}

循环依赖 goroutine 链

三 goroutine 形成 A→B→C→A 的接收依赖环,无启动信号则全部挂起。

场景 触发条件 检测时机
无缓冲发送 单向 send,无 receiver 运行时 deadlock 检查
select 无 default 所有 channel 均不可读/写 进入 select 即阻塞
循环依赖链 goroutine 互相等待对方发送 启动后立即死锁
graph TD
    A[goroutine A] -->|等待从B收| B
    B -->|等待从C收| C
    C -->|等待从A收| A

3.3 安全channel使用范式:超时控制、非阻塞操作、结构化channel拓扑设计

超时控制:避免goroutine永久阻塞

使用 select + time.After 实现带超时的channel接收:

select {
case msg := <-ch:
    fmt.Println("received:", msg)
case <-time.After(5 * time.Second):
    log.Println("channel read timeout")
}

逻辑分析:time.After 返回一个只读 <-chan Time,若 ch 在5秒内无数据,time.After 分支被选中,避免协程挂起。关键参数:5 * time.Second 可根据业务SLA动态配置(如API调用≤2s,后台任务≤30s)。

非阻塞操作:default 分支实现零等待尝试

select {
case ch <- data:
    log.Println("sent successfully")
default:
    log.Println("channel full or closed, dropping message")
}

逻辑分析:default 使 select 立即返回,适用于日志采集、指标上报等允许丢弃的场景;需配合缓冲channel(如 make(chan int, 100))提升吞吐。

结构化拓扑:扇入/扇出模式保障可扩展性

graph TD
    A[Producer] --> B[Channel Pool]
    B --> C[Worker-1]
    B --> D[Worker-2]
    B --> E[Worker-N]
    C --> F[Aggregator]
    D --> F
    E --> F
模式 适用场景 安全要点
扇入 多源数据聚合 需统一关闭信号(done channel)
扇出 并行处理提升吞吐 Worker需监听done并清理资源
管道链式 流式ETL处理 每级channel应设合理缓冲容量

第四章:context超时失效:分布式超时传递的断链陷阱

4.1 context超时失效的深层原因:父子ctx继承断裂、WithCancel/WithTimeout误用、value-only ctx污染

父子ctx继承断裂:cancel信号无法传递

当通过 context.WithValue(parent, key, val) 创建子ctx时,该ctx不携带取消能力——它既无done通道,也不响应父ctx的cancel。若后续错误地在此value-only ctx上调用WithTimeout,新ctx的Done()将独立于原始取消链。

parent, cancel := context.WithCancel(context.Background())
defer cancel()

valCtx := context.WithValue(parent, "k", "v") // ❌ 无cancel能力
timeoutCtx, _ := context.WithTimeout(valCtx, 100*time.Millisecond) // ✅ 但timeoutCtx的done仅依赖自身计时器
// 若parent被cancel,timeoutCtx仍存活至超时!

逻辑分析:WithValue返回的ctx类型为valueCtx,其Done()方法始终返回nil;WithTimeout内部调用withCancel时,因父ctx无canceler,会新建独立canceler,导致继承链断裂。

三类典型误用对比

场景 是否继承父cancel 超时是否联动父ctx 风险
WithValue(parent, k, v) 否 否 “静默”阻塞
WithTimeout(parent, d) 是 是 ✅ 安全
WithTimeout(WithValue(...), d) 否 否 ⚠️ 超时独立,父cancel失效

根本修复路径

  • ✅ 始终在可取消ctx(WithCancel/WithTimeout返回值)上叠加WithValue
  • ❌ 禁止在WithValue结果上调用WithCancel/WithTimeout
  • 🔍 使用ctx.Value()前,可通过ctx.Deadline()或ctx.Err()验证ctx活性
graph TD
    A[context.Background] -->|WithCancel| B[Cancelable ctx]
    B -->|WithValue| C[value-only ctx]
    C -->|WithTimeout| D[独立timeout ctx] --> E[超时后才结束,无视B.cancel]
    B -->|WithTimeout| F[联动ctx] --> G[cancel或timeout任一触发即结束]

4.2 超时穿透性验证:HTTP中间件、数据库查询、gRPC调用链中timeout的实际传播效果分析

在分布式调用链中,超时并非静态配置,而是需沿请求路径逐层透传与衰减的动态契约。

HTTP中间件中的超时透传

使用 context.WithTimeout 封装入参 context,确保下游服务可感知上游时限:

func timeoutMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx, cancel := context.WithTimeout(r.Context(), 800*time.Millisecond)
        defer cancel()
        r = r.WithContext(ctx)
        next.ServeHTTP(w, r)
    })
}

逻辑分析:中间件将原始 r.Context() 包裹为带 800ms 截止时间的新 context;cancel() 防止 goroutine 泄漏;下游 handler 通过 r.Context().Done() 可同步响应超时信号。

gRPC 与数据库调用链对比

组件 是否自动继承父 context 超时是否向下衰减 典型问题
gRPC Client ✅ 是 ❌ 否(需显式设置) ctx 未传入导致忽略
PostgreSQL ✅ 是(经 pgx/v5) ✅ 是(受父 deadline 约束) 长事务阻塞 cancel 通道

调用链超时传播示意

graph TD
    A[HTTP Handler] -->|ctx.WithTimeout 1s| B[gRPC Client]
    B -->|ctx passed| C[gRPC Server]
    C -->|ctx.WithTimeout 600ms| D[DB Query]

4.3 上下文治理最佳实践:ctx.WithTimeout嵌套策略、cancel显式调用契约、测试驱动的timeout覆盖率验证

ctx.WithTimeout嵌套策略:避免“超时覆盖”陷阱

// ❌ 错误:外层timeout被内层覆盖,实际生效的是200ms
parent, _ := context.WithTimeout(context.Background(), 5*time.Second)
child, _ := context.WithTimeout(parent, 200*time.Millisecond) // 覆盖父上下文deadline

// ✅ 正确:按业务阶段分层设限,保留语义可追溯性
apiCtx, apiCancel := context.WithTimeout(ctx, 3*time.Second)
dbCtx, dbCancel := context.WithTimeout(apiCtx, 800*time.Millisecond)

apiCtx承载端到端SLA约束,dbCtx体现下游依赖的独立超时预算;嵌套时子上下文不可延长父上下文 deadline,但可提前终止。

cancel显式调用契约

  • 必须在所有控制路径(含defer、error分支)调用 cancel()
  • 禁止传递未封装的 context.CancelFunc 给不可信第三方

测试驱动的timeout覆盖率验证

场景 预期行为 覆盖方式
正常响应 不触发cancel 断言 ctx.Err() == nil
超时路径 ctx.Err() == context.DeadlineExceeded 使用 testhelper.Timeout 注入延迟
graph TD
    A[启动请求] --> B{是否命中db超时?}
    B -- 是 --> C[dbCtx.Err() == DeadlineExceeded]
    B -- 否 --> D[继续执行]
    C --> E[主动调用dbCancel]

4.4 生产级context增强库设计:支持可观测性埋点、超时分级(soft/hard)、自动panic注入检测

核心能力分层抽象

  • 可观测性埋点:在 Context 创建/传递/取消时自动注入 trace ID、span ID 及操作标签(如 rpc, db, cache)
  • 超时分级机制:SoftTimeout 触发降级逻辑(如返回缓存),HardTimeout 强制 cancel 并记录 fatal error
  • panic 检测:通过 runtime.Stack() 在 defer 中捕获 goroutine panic,并关联 context value 中的请求 ID

超时策略对比

策略 触发时机 行为 适用场景
SoftTimeout 距 deadline 剩余 200ms 执行 fallback,保留 context 高可用读请求
HardTimeout 到达 deadline cancel(), log.Panic() 支付/事务链路

自动 panic 注入检测示例

func WithPanicDetection(ctx context.Context) context.Context {
    ctx = context.WithValue(ctx, panicKey, make(chan struct{}))
    go func() {
        defer func() {
            if r := recover(); r != nil {
                reqID := ctx.Value("req_id").(string)
                log.Error("panic detected", "req_id", reqID, "stack", debug.Stack())
                // 上报至 tracing backend
            }
        }()
        <-ctx.Done() // 等待 context 结束触发 defer
    }()
    return ctx
}

该函数在 context 生命周期结束时启动 panic 监听协程;recover() 捕获本 goroutine panic,结合 req_id 实现故障精准归因。chan struct{} 作为轻量信号载体,避免内存泄漏。

流程协同视图

graph TD
    A[WithTimeout] --> B{Soft vs Hard?}
    B -->|Soft| C[触发 fallback]
    B -->|Hard| D[Cancel + Panic Report]
    C --> E[继续返回部分结果]
    D --> F[终止链路 + 告警]

第五章:构建高可靠Go并发系统的终极心法

并发安全的原子化思维

在真实微服务场景中,某支付对账系统曾因 counter++ 非原子操作导致日终差错率飙升至 0.3%。修复后采用 sync/atomic 替代互斥锁保护计数器,QPS 提升 22%,GC 压力下降 37%。关键代码如下:

var pendingJobs int64

func RegisterJob() {
    atomic.AddInt64(&pendingJobs, 1)
}

func CompleteJob() {
    atomic.AddInt64(&pendingJobs, -1)
}

func GetPending() int64 {
    return atomic.LoadInt64(&pendingJobs)
}

Channel 设计的反模式规避

以下表格对比了常见 channel 使用误用与优化方案:

场景 反模式 生产级实践
跨 goroutine 传递错误 chan error 无缓冲且未关闭 使用带超时的 select + context.WithTimeout
状态广播 多个 goroutine 同时向同一 channel 发送 改用 sync.Map + chan struct{} 单点通知
资源释放 defer close(ch) 在 goroutine 中执行 主 goroutine 显式控制 channel 生命周期

Context 传播的深度实践

某风控网关在压测中发现 15% 请求因 context 泄漏卡死。根因是中间件层未将父 context 透传至下游 HTTP client。修正后强制所有协程启动时绑定 context:

go func(ctx context.Context, req *http.Request) {
    req = req.WithContext(ctx) // 必须重赋值
    resp, err := http.DefaultClient.Do(req)
    // ... 处理逻辑
}(parentCtx, clonedReq)

panic 恢复的分级策略

生产环境必须区分 panic 类型:

  • 可恢复:JSON 解析失败、空指针解引用(recover() 后记录结构化错误日志)
  • 不可恢复:runtime.Goexit() 调用、内存溢出(直接 os.Exit(1) 终止进程)

使用 runtime.Stack() 捕获栈信息并写入独立 error 日志文件,避免污染主日志流。

连接池的动态调优机制

某消息推送服务在流量突增时出现连接耗尽。通过 sync.Pool 缓存 *bytes.Buffer,配合 net/http 的 Transport.MaxIdleConnsPerHost 动态调整:

flowchart TD
    A[每分钟采集 P99 延迟] --> B{延迟 > 200ms?}
    B -->|是| C[Increase MaxIdleConnsPerHost by 20%]
    B -->|否| D[Decrease by 10% if > idle threshold]
    C --> E[更新 Transport 配置]
    D --> E

死锁检测的自动化植入

在 CI 流程中集成 go run -gcflags="-l" -ldflags="-s -w" 编译后,使用 golang.org/x/tools/go/analysis/passes/ctrlflow 分析 goroutine 图谱。当检测到环形等待链(如 goroutine A 等待 channel X,B 等待 channel Y,X/Y 互相依赖),自动触发告警并阻断发布。

内存泄漏的精准定位

通过 pprof 的 goroutine 和 heap profile 结合分析:

  1. curl "http://localhost:6060/debug/pprof/goroutine?debug=2" 获取完整栈
  2. 对比 runtime.GC() 前后 runtime.ReadMemStats() 的 HeapInuse 差值
  3. 定位到 time.AfterFunc 引用闭包中持有大对象,改用 time.After + 显式 cancel()

分布式锁的幂等性保障

Redis 分布式锁在节点故障时出现双写。采用 SET key value NX PX 30000 原子指令,value 固定为 UUID + goroutine ID,并在业务逻辑前校验锁所有权:

lockValue := fmt.Sprintf("%s:%d", uuid.New().String(), goroutineID())
ok, _ := redisClient.SetNX(ctx, "order:lock", lockValue, 30*time.Second).Result()
if !ok { return errors.New("acquire lock failed") }
// 执行业务...
// 解锁前再次校验 value 匹配
redisClient.Eval(ctx, "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", []string{"order:lock"}, lockValue)

记录 Go 学习与使用中的点滴,温故而知新。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注