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 批量调用
  • 日志批量写入

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


相关推荐
田甲3 分钟前
做一款 Type-C 母座读卡器:GL3224 双卡方案实测
c语言·开发语言
严肃小白兔20 分钟前
MATLAB 绘图与科研论文插图制作:图形设计、统计表达与矢量导出
开发语言·matlab·科研绘图
这个DBA有点耶22 分钟前
MySQL 8.0直方图:统计信息升级了,执行计划为什么还是选错?
数据库·mysql·代码规范
Funlang2 小时前
给机器写的 JavaScript 造个笼子
开发语言·javascript·ecmascript
普通人62 小时前
PHP文件读写完全指南
开发语言·php·1024程序员节
老纪的技术唠嗑局2 小时前
把 seekdb 塞进 iOS App:让混合检索嵌入手机
数据库
坤坤子吖3 小时前
Linux网络编程:TCP服务器的多进程与多线程实现
linux·服务器·开发语言·网络·c++·笔记·学习
ClouGence3 小时前
开源数据库管理工具 CloudDM 新版本发布,支持 OpenSearch、IoTDB 数据源
数据库·开源
徐小黑ACG3 小时前
Redis-基础
数据库·redis·缓存
一晌小贪欢4 小时前
Python基础:第 09 篇 · while 循环:重复直到不满足
开发语言·c++·python·数据可视化·面向对象·python办公