事务并发超限需在入口层限流:控QPS用rate.Limiter(如rate.NewLimiter(rate.Every(50ms), 40)),控瞬时并发数用semaphore.NewWeighted(8),二者语义不同、不可混用,且必须复用实例、匹配DB连接池配置。

事务并发超限不是GORM的问题,而是你没对事务入口做限流——GORM本身不控制并发,它只忠实地执行你发下去的SQL。 直接在事务外加 rate.Limiter 或 semaphore.Weighted 才是正解,别指望 db.Transaction() 自带排队功能。
为什么 db.Transaction() 不能防并发超限
GORM 的 Transaction() 只是封装了 *sql.Tx 的 Begin/Commit/Rollback 流程,它不感知外部调用频率,也不限制 goroutine 数量。哪怕你并发起 1000 个 db.Transaction() 调用,只要连接池撑得住,它们就全会进数据库——然后大概率触发 PostgreSQL 的 too many clients already 或 MySQL 的 Too many connections。
-
db.Transaction()内部 panic 会被捕获并回滚,但这是错误处理,不是并发控制 - 事务持续时间越长(比如含 HTTP 调用、文件读写),连接被占住的时间就越久,放大超限风险
- 嵌套事务或保存点(
SavePoint)只影响事务逻辑分层,不改变连接占用行为
该用 rate.Limiter 还是 semaphore.Weighted?
取决于你要控的是「单位时间请求数」还是「当前正在跑的事务数」:
- 控 QPS(如「每秒最多 20 个支付事务」)→ 用
rate.NewLimiter(rate.Every(50*time.Millisecond), 40):burst 设为 rate×2,允许短时毛刺 - 控并发数(如「同时最多 8 个库存扣减事务」)→ 用
semaphore.NewWeighted(8):硬性阻塞,context 取消自动释放,panic 安全 - 别混用:
rate.Limiter.Wait()不会减少 goroutine 数,只是排队;semaphore.Acquire()才真能压住并发水位
限流器怎么平滑接入事务逻辑
不要把限流塞进事务回调里,要在调用 db.Transaction() 前拦截:
- HTTP handler 中先
sem.Acquire(ctx, 1),成功后再进db.Transaction();失败直接返回 429 - Acquire 必须传带 timeout 的
ctx,否则客户端断连后 permit 永远卡住 - 事务函数内部仍要设 DB 查询超时:
tx.Session(&gorm.Session{Context: ctx}).First(&u) - 别在事务里启动新 goroutine 并漏传
ctx——这是 goroutine 泄漏高发区
容易被忽略的三个关键点
限流器生效的前提是它真的被所有路径共用,而不仅是“写了代码”:
- 全局单例的
*semaphore.Weighted或*rate.Limiter必须复用,不能每次请求 new 一个 - 如果按用户 ID 或订单号做细粒度并发控制,要用
sync.Map存 key → limiter 映射,并配过期清理(比如空闲 5 分钟删) -
db.DB().SetMaxOpenConns(100)和semaphore.NewWeighted(8)要数值匹配:前者是连接上限,后者是事务并发上限,后者必须 ≤ 前者,否则 Acquire 成功后db.Transaction()仍可能因拿不到连接而卡住











