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

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


相关推荐
caimouse11 分钟前
ReactOS 图形系统分析(16):字符串对象 — STROBJ(string.c)
c语言·开发语言
Aphelios38030 分钟前
一次锁内网络IO引发的Tomcat线程池“饿死”事故
java·开发语言·spring boot·elasticsearch·tomcat·网络io阻塞·线程池耗尽
不可求~1 小时前
C++ std::string_view 不是字符串:从悬空引用到安全用法
java·开发语言·c++
90后小陈老师2 小时前
PHP 配置默认值失效排查实录:isset 兜底遇上表单空串回填,首页文案是怎么集体消失的
开发语言·php
何以解忧,唯有..2 小时前
数据库索引失效的常见情况与优化策略
数据库·sql·oracle
JJJennie7772 小时前
Gemini 3.7 Flash 上线,Google 想让 AI 多干活
开发语言·javascript·ecmascript
这个DBA有点耶3 小时前
分布式数据库到底该不该上?从判断标准到架构选型的实战思考
数据库·架构·dba
01_ice3 小时前
MySQL库和表的操作
数据库·mysql
wuyk5553 小时前
Python 第三章:列表 List
服务器·开发语言·python
oradh3 小时前
Oracle UNDO表空间管理维护总结
数据库·oracle·undo表空间·undo表空间管理·undo表空间管理维护