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

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


相关推荐
冬奇Lab5 小时前
每日一个开源项目(第134篇):Zvec - 阿里开源的嵌入式向量数据库,向量搜索界的 SQLite
数据库·人工智能·llm
ClouGence16 小时前
Oracle CDC 架构优化:从主库直连到 DataGuard 备库同步
数据库·后端·oracle
无响应de神18 小时前
三、用户与权限管理
数据库·mysql
LDR0061 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术1 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript
码云数智-园园1 天前
C++20 Modules 模块详解
java·开发语言·spring
swordbob1 天前
NIO的channel中什么是 fd(File Descriptor,文件描述符)
java·开发语言·nio
源分享1 天前
Java线程同步的多种实现方法(非常详细)
java·开发语言·jvm
Luminous.1 天前
C语言--day30
c语言·开发语言
何以解忧,唯有..1 天前
Go语言循环语句详解:for、range与循环控制
开发语言·算法·golang