Go 实时批处理:让数据库少挨点打 [特殊字符]

问题场景

想象一下,你的应用每次用户访问都要更新 last_active_at 时间戳:

go 复制代码
func UpdateUserTimestamp(ctx context.Context, userID int) error {
    // UPDATE users SET last_active_at = NOW() WHERE id = userID
}

问题来了:1000 个并发请求 = 1000 次数据库更新。数据库:😫

核心思路

把单个更新变成批量更新:

sql 复制代码
-- 1000 次单独更新 ❌
UPDATE users SET last_active_at = NOW() WHERE id = 1
UPDATE users SET last_active_at = NOW() WHERE id = 2
...

-- 1 次批量更新 ✅
UPDATE users SET last_active_at = NOW() WHERE id IN (1, 2, 3, ..., 1000)

实现方案

1. 请求结构体

每个请求带一个专属回复通道:

go 复制代码
type updateUserTimestampRequest struct {
    userID  int
    ReplyTo chan error  // 专属回复通道
}

var updateUserTimestampQueue = make(chan updateUserTimestampRequest)

2. 公开 API

go 复制代码
func UpdateUserTimestamp(ctx context.Context, userID int) error {
    req := updateUserTimestampRequest{
        userID:  userID,
        ReplyTo: make(chan error, 1), // 必须缓冲!
    }

    // 发送请求(支持上下文取消)
    select {
    case <-ctx.Done():
        return ctx.Err()
    case updateUserTimestampQueue <- req:
    }

    // 等待结果
    select {
    case <-ctx.Done():
        return ctx.Err()
    case err := <-req.ReplyTo:
        return err
    }
}

3. 批处理工作流程

复制代码
┌─────────────┐
│  调用方 1    │──┐
└─────────────┘  │
                 │     ┌──────────┐     ┌─────────┐
┌─────────────┐  └────>│  Channel  │────>│ Worker  │
│  调用方 2    │──┐     └──────────┘     └─────────┘
└─────────────┘  │                          │
                 │                          ▼
┌─────────────┐  │                    ┌──────────┐
│  调用方 3    │──┘                    │ 数据库批量 │
└─────────────┘                       └──────────┘

4. Worker 实现

使用 rill 库简化批处理:

go 复制代码
func updateUserTimestampWorker(batchSize int, batchTimeout time.Duration) {
    requests := rill.FromChan(updateUserTimestampQueue, nil)
    requestBatches := rill.Batch(requests, batchSize, batchTimeout)
    
    _ = rill.ForEach(requestBatches, concurrency, func(batch []updateUserTimestampRequest) error {
        // 提取用户 ID 列表
        ids := make([]int, len(batch))
        for i, req := range batch {
            ids[i] = req.userID
        }

        // 执行批量更新
        err := sendQueryToDB(ctx, "UPDATE users SET ... WHERE id IN (?)", ids)

        // 回复所有调用方
        for _, req := range batch {
            req.ReplyTo <- err
            close(req.ReplyTo)
        }
        return nil
    })
}

关键技巧

技巧 作用
缓冲通道 防止调用方取消后 Worker 阻塞
批处理超时 低流量时也能及时更新(如 10ms)
select 语句 优雅支持上下文取消

完整示例

go 复制代码
func main() {
    // 启动 Worker
    go updateUserTimestampWorker(7, 10*time.Millisecond, 2, 10*time.Second)

    // 模拟 100 个并发请求
    var wg sync.WaitGroup
    for i := 1; i <= 100; i++ {
        wg.Add(1)
        go func(userID int) {
            defer wg.Done()
            UpdateUserTimestamp(ctx, userID)
        }(i)
    }
    wg.Wait()
}

总结

复制代码
性能提升 ✅
代码简洁 ✅
实时响应 ✅

这个模式同样适用于:

  • Redis 批量操作
  • 外部 API 批量调用
  • 日志批量写入

数据库:谢谢你,终于能喘口气了 😄


相关推荐
哥不想学算法6 小时前
【C++】字符串字面量拼接
开发语言·c++
不像程序员的程序媛7 小时前
系统cpu内存负载资源分析
运维·服务器·数据库
Jane Chiu8 小时前
Oracle DBMS_REDEFINITION(在线重定义)
数据库·oracle·分区表
gugucoding8 小时前
21. 【C语言】打包不同类型:结构体
c语言·开发语言
Brookty8 小时前
【JavaEE】线程安全(一).4:写块串行保安全、CAS
java·开发语言·java-ee·多线程·线程安全
全栈前端老曹8 小时前
【MongoDB】安全与权限管理 —— 用户认证、角色权限、SSL 加密
前端·javascript·数据库·安全·mongodb·nosql·ssl
怕孤单的草丛8 小时前
缓存管理面临的主要问题
java·数据库·缓存
F20226974869 小时前
西门子 PLC 与 C# 通信
开发语言·c#
gugucoding9 小时前
31. 【C语言】堆栈与队列的实现
c语言·开发语言·数据结构·链表