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:
@@ -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{},
|
||||
|
||||
Reference in New Issue
Block a user