fix(v2): 超退守卫事务化 CreateRefundGuarded(锁单行+求和+插入单事务)+ sqlite DSN 补 _txlock=immediate

Task 3 计划的 RefundSum→比较→CreateRefund 两段式裸读写在并发下会超退(两个请求都读到
reserved=0、都通过、都插入)。仿 store/order.go::MarkAttemptPaid 的模式,把锁订单行
(clause.Locking FOR UPDATE,MySQL 真锁/SQLite 由 glebarez 静默丢弃)+ 求和 + 校验 + 插入
收进单个 s.db.Transaction。SQLite 侧真正的串行化靠 DSN _txlock=immediate(BEGIN IMMEDIATE
在事务开始就抢写锁)+ busy_timeout,补进 testdb.go 与 main.go 的 sqlite DSN 构造。
This commit is contained in:
wangjia
2026-07-10 17:08:25 +08:00
parent 338e28d386
commit aa14204640
4 changed files with 201 additions and 2 deletions
+15 -1
View File
@@ -3,6 +3,7 @@ package main
import (
"errors"
"log"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -84,7 +85,7 @@ func initDB() *gorm.DB {
switch config.C.Database.Driver {
case "sqlite", "":
db, err := gorm.Open(sqlite.Open(config.C.Database.DSN), gormCfg)
db, err := gorm.Open(sqlite.Open(sqliteDSN(config.C.Database.DSN)), gormCfg)
if err != nil {
log.Fatalf("连接 sqlite 失败: %v", err)
}
@@ -98,6 +99,19 @@ func initDB() *gorm.DB {
}
}
// sqliteDSN 补 _txlock=immediate(BEGIN IMMEDIATE 立即抢库级写锁,串行化并发事务的
// 读阶段——RefundStore.CreateRefundGuarded 等守卫类事务依赖此语义)+
// busy_timeout(5000)(抢不到锁时等待重试而非立即报 SQLITE_BUSY)。呼应 pangolin
// 的 SQLite DSN 约定(server/internal/db/db.go)。config.yaml 里的 dsn 是纯文件路径
// (如 "pay.db"),这里补上查询串;若用户已经自带 "?",追加而不是覆盖。
func sqliteDSN(raw string) string {
extra := "_txlock=immediate&_pragma=busy_timeout(5000)"
if strings.Contains(raw, "?") {
return raw + "&" + extra
}
return raw + "?" + extra
}
func autoMigrate(db *gorm.DB) {
if err := db.AutoMigrate(
&model.Merchant{},