cacheCleanCtx.go 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
  1. package loaders
  2. import (
  3. "context"
  4. "errors"
  5. "time"
  6. "github.com/zeromicro/go-zero/core/logx"
  7. )
  8. // cacheCleanTimeout 是 post-commit 缓存失效的默认硬超时。
  9. // 3s 足够覆盖正常 Redis 节点的 DEL/SUNION/Pipelined 三步互动,同时给悬挂 goroutine 兜底。
  10. const cacheCleanTimeout = 3 * time.Second
  11. // DetachCacheCleanCtx 为 post-commit 阶段的缓存失效构造一个独立 ctx(审计 L-R13-5 方案 A):
  12. // - 用 context.WithoutCancel(parent) 切断"HTTP 请求 ctx 被 client 断连 / server 超时取消 →
  13. // 紧跟在事务提交之后的 Clean / BatchDel / InvalidateProfileCache 半途被打断"的联动——
  14. // 事务已经落盘,这几次 Redis 写属于**后置补偿**,不应该随请求生命周期一起结束;
  15. // - 套 3s 硬 timeout 兜底,防止 Redis 慢 / 挂起时后台 goroutine 悬挂不退。
  16. //
  17. // 典型用法:
  18. //
  19. // if err := transactionBody(...); err != nil { return err }
  20. // cleanCtx, cancel := loaders.DetachCacheCleanCtx(l.ctx)
  21. // defer cancel()
  22. // l.svcCtx.UserDetailsLoader.Clean(cleanCtx, userId)
  23. // l.svcCtx.SysUserModel.InvalidateProfileCache(cleanCtx, userId, username)
  24. //
  25. // 原调用链里的 logx trace / 调用栈元信息会通过 WithoutCancel 保留,便于把后置补偿的日志与
  26. // 原请求 trace 关联上;parent 只被剥离了 cancel/deadline,其它值(trace id / tenant id 等)仍然在。
  27. func DetachCacheCleanCtx(parent context.Context) (context.Context, context.CancelFunc) {
  28. return context.WithTimeout(context.WithoutCancel(parent), cacheCleanTimeout)
  29. }
  30. // isCtxCanceledErr 判定错误是否源自 ctx 取消 / 超时,用于把 post-commit 缓存失效失败里
  31. // "请求中断"与"Redis 真的挂了"拆开(审计 L-R13-5 方案 B)。
  32. func isCtxCanceledErr(err error) bool {
  33. return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
  34. }
  35. // logCacheInvalidationErr 统一"缓存失效失败"的日志打点。
  36. // - ctx 取消 / 超时:打一条带 `audit=cache_invalidation_skipped_due_to_ctx_cancel` tag 的
  37. // Errorw,运维可以按 tag 单独建看板而不污染"Redis 真挂了"的告警;
  38. // - 其它错误:保持原 Errorf 的串行格式,兼容既有日志解析管线。
  39. //
  40. // scope 形如 "userDetailsLoader.BatchDel",detail 可附带 key / 业务上下文。
  41. func logCacheInvalidationErr(ctx context.Context, scope, detail string, err error) {
  42. if err == nil {
  43. return
  44. }
  45. if isCtxCanceledErr(err) {
  46. logx.WithContext(ctx).Errorw("cache invalidation skipped: ctx canceled",
  47. logx.Field("audit", "cache_invalidation_skipped_due_to_ctx_cancel"),
  48. logx.Field("scope", scope),
  49. logx.Field("detail", detail),
  50. logx.Field("err", err.Error()),
  51. )
  52. return
  53. }
  54. logx.WithContext(ctx).Errorf("%s failed: detail=%s err=%v", scope, detail, err)
  55. }