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

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


相关推荐
21439652 分钟前
如何防止SQL注入利用存储过程_确保存储过程不拼字符串
jvm·数据库·python
2301_764150564 分钟前
如何统计表单中已填写的特定类名输入框数量
jvm·数据库·python
沐知全栈开发7 分钟前
Java Number & Math 类详解
开发语言
2401_897190558 分钟前
宝塔面板SSH提示连接被拒绝_检查服务器端口开关
jvm·数据库·python
浮尘笔记8 分钟前
Java Snowy 框架生产环境安全部署全流程(服务器篇)
java·运维·服务器·开发语言·后端
2401_871696528 分钟前
MySQL无法通过网络连接服务器_检查bind-address与访问权限
jvm·数据库·python
2401_8877245010 分钟前
SQL注入的安全架构设计_将数据库置于内网隔离区
jvm·数据库·python
Rsun0455111 分钟前
6、Java 适配器模式从入门到实战
java·开发语言·适配器模式
m0_6784854511 分钟前
如何配置文件描述符限制_limits.conf中Oracle用户配置
jvm·数据库·python
我科绝伦(Huanhuan Zhou)12 分钟前
Oracle BBED 工具部署全流程:Linux 64位环境实操指南
linux·数据库·oracle