如何在Golang中实现并发订单处理_Golang channel与任务分发方法

直接用 go func() 启动订单处理会导致并发不安全,因 goroutine 泛滥和缺乏协调引发连接池耗尽、库存错乱等问题;应改用带缓冲 channel 作任务队列,配合固定数量 worker 并监听 ctx.Done() 实现可控并发与优雅退出。

为什么直接用 go func() 启动订单处理会出问题

并发不等于安全。很多开发者一上来就对每个订单起一个 go func(),结果发现数据库连接被打爆、库存扣减错乱、日志混成一团。根本原因在于:没有节制的 goroutine 泛滥 + 缺乏任务协调机制。比如 1000 个订单进来,瞬间 spawn 1000 个 goroutine,而数据库连接池可能只有 20 个,panic: dial tcp: lookup xxx: no such hosttoo many connections 就是典型表现。

真正需要的是可控的并发度 + 明确的任务生命周期管理 —— 这正是 channel 的核心价值。

chan *Order 做任务队列 + 固定 worker 数量

把订单当消息塞进 channel,由一组固定数量的 worker 从 channel 中取任务执行。这样既限流又解耦,还能自然支持优雅退出。

  • workerCount 通常设为 CPU 核心数 × 1.5 ~ 2(I/O 密集型可更高),避免过度抢占调度器
  • 输入 channel 必须是带缓冲的(如 make(chan *Order, 100)),否则生产者可能被阻塞,导致上游 HTTP handler 卡住
  • 每个 worker 应监听 ctx.Done(),收到取消信号时完成当前任务后退出
func startWorkers(ctx context.Context, tasks chan *Order, workerCount int) {
    var wg sync.WaitGroup
    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for {
                select {
                case order, ok := <-tasks:
                    if !ok {
                        return
                    }
                    processOrder(order) // 实际业务逻辑
                case <-ctx.Done():
                    return
                }
            }
        }()
    }
    go func() {
        <-ctx.Done()
        close(tasks)
        wg.Wait()
    }()
}

select 配合 default 实现非阻塞投递与背压控制

上游(比如 HTTP handler)往 tasks channel 发送订单时,不能无条件 tasks ,否则在 channel 满时会阻塞整个请求处理流程。需要用 select + default 做快速失败或降级。

立即学习“go语言免费学习笔记(深入)”;

  • 投递失败时可返回 429 Too Many Requests,而不是让请求 hang 住
  • 也可改走本地内存队列(如 sync.Map)暂存,再异步批量刷入 channel,但会增加复杂度
  • 注意:不要在 default 分支里加 time.Sleep 循环重试 —— 这等于自己实现低效轮询,且容易拖垮 handler
func handleOrder(w http.ResponseWriter, r *http.Request) {
    order := parseOrder(r)
    select {
    case tasks <- order:
        json.NewEncoder(w).Encode(map[string]string{"status": "accepted"})
    default:
        http.Error(w, "system busy", http.StatusTooManyRequests)
    }
}

如何安全关闭 channel 并等待所有任务完成

直接 close(tasks) 是危险的 —— 如果有 worker 正在读取,会收到零值;如果多个 goroutine 同时 close 同一个 channel,会 panic:panic: close of closed channel。必须确保仅由单一 goroutine 关闭,且关闭前所有生产者已停止写入。

  • 推荐用 context.WithCancel 控制生命周期,关闭信号由上层统一触发
  • worker 内部用 for order := range tasks 自动退出,比手动 select 更简洁可靠
  • sync.WaitGroup 等待所有 worker 退出后再返回,否则进程可能提前结束,导致部分订单丢失

最易忽略的一点:如果你在 worker 里调用了外部服务(如支付回调、消息推送),这些操作本身可能超时或失败,必须单独设置超时和重试策略 —— channel 只管调度,不负责业务容错。