package loaders import ( "context" "errors" "time" "github.com/zeromicro/go-zero/core/logx" ) // cacheCleanTimeout 是 post-commit 缓存失效的默认硬超时。 // 3s 足够覆盖正常 Redis 节点的 DEL/SUNION/Pipelined 三步互动,同时给悬挂 goroutine 兜底。 const cacheCleanTimeout = 3 * time.Second // DetachCacheCleanCtx 为 post-commit 阶段的缓存失效构造一个独立 ctx(审计 L-R13-5 方案 A): // - 用 context.WithoutCancel(parent) 切断"HTTP 请求 ctx 被 client 断连 / server 超时取消 → // 紧跟在事务提交之后的 Clean / BatchDel / InvalidateProfileCache 半途被打断"的联动—— // 事务已经落盘,这几次 Redis 写属于**后置补偿**,不应该随请求生命周期一起结束; // - 套 3s 硬 timeout 兜底,防止 Redis 慢 / 挂起时后台 goroutine 悬挂不退。 // // 典型用法: // // if err := transactionBody(...); err != nil { return err } // cleanCtx, cancel := loaders.DetachCacheCleanCtx(l.ctx) // defer cancel() // l.svcCtx.UserDetailsLoader.Clean(cleanCtx, userId) // l.svcCtx.SysUserModel.InvalidateProfileCache(cleanCtx, userId, username) // // 原调用链里的 logx trace / 调用栈元信息会通过 WithoutCancel 保留,便于把后置补偿的日志与 // 原请求 trace 关联上;parent 只被剥离了 cancel/deadline,其它值(trace id / tenant id 等)仍然在。 func DetachCacheCleanCtx(parent context.Context) (context.Context, context.CancelFunc) { return context.WithTimeout(context.WithoutCancel(parent), cacheCleanTimeout) } // isCtxCanceledErr 判定错误是否源自 ctx 取消 / 超时,用于把 post-commit 缓存失效失败里 // "请求中断"与"Redis 真的挂了"拆开(审计 L-R13-5 方案 B)。 func isCtxCanceledErr(err error) bool { return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) } // logCacheInvalidationErr 统一"缓存失效失败"的日志打点。 // - ctx 取消 / 超时:打一条带 `audit=cache_invalidation_skipped_due_to_ctx_cancel` tag 的 // Errorw,运维可以按 tag 单独建看板而不污染"Redis 真挂了"的告警; // - 其它错误:审计 L-R18-8 改用 Errorw 带 `audit=cache_invalidation_failed` tag,方便 // 运维按 tag 在日志平台聚合告警(项目当前未集成 Prometheus,以日志 tag 作为告警输入面, // 未来接入 metrics 时再把 tag 聚合转成 counter 即可,不需要二次改造业务代码)。 // // scope 形如 "userDetailsLoader.BatchDel",detail 可附带 key / 业务上下文。 func logCacheInvalidationErr(ctx context.Context, scope, detail string, err error) { if err == nil { return } if isCtxCanceledErr(err) { logx.WithContext(ctx).Errorw("cache invalidation skipped: ctx canceled", logx.Field("audit", "cache_invalidation_skipped_due_to_ctx_cancel"), logx.Field("scope", scope), logx.Field("detail", detail), logx.Field("err", err.Error()), ) return } logx.WithContext(ctx).Errorw("cache invalidation failed", logx.Field("audit", "cache_invalidation_failed"), logx.Field("scope", scope), logx.Field("detail", detail), logx.Field("err", err.Error()), ) }