cacheCleanCtx.go 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  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. // - 其它错误:审计 L-R18-8 改用 Errorw 带 `audit=cache_invalidation_failed` tag,方便
  39. // 运维按 tag 在日志平台聚合告警(项目当前未集成 Prometheus,以日志 tag 作为告警输入面,
  40. // 未来接入 metrics 时再把 tag 聚合转成 counter 即可,不需要二次改造业务代码)。
  41. //
  42. // scope 形如 "userDetailsLoader.BatchDel",detail 可附带 key / 业务上下文。
  43. func logCacheInvalidationErr(ctx context.Context, scope, detail string, err error) {
  44. if err == nil {
  45. return
  46. }
  47. if isCtxCanceledErr(err) {
  48. logx.WithContext(ctx).Errorw("cache invalidation skipped: ctx canceled",
  49. logx.Field("audit", "cache_invalidation_skipped_due_to_ctx_cancel"),
  50. logx.Field("scope", scope),
  51. logx.Field("detail", detail),
  52. logx.Field("err", err.Error()),
  53. )
  54. return
  55. }
  56. logx.WithContext(ctx).Errorw("cache invalidation failed",
  57. logx.Field("audit", "cache_invalidation_failed"),
  58. logx.Field("scope", scope),
  59. logx.Field("detail", detail),
  60. logx.Field("err", err.Error()),
  61. )
  62. }