cache_aside.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. package cache
  2. import (
  3. "context"
  4. "errors"
  5. "time"
  6. "fwh-toolbox-server/cache/redisx"
  7. "go.uber.org/zap"
  8. )
  9. // LoaderFunc 定义
  10. type LoaderFunc[T any] func(ctx context.Context) (*T, error)
  11. // GetOrSet: cache-aside 模式,带分布式锁防击穿与异步写缓存
  12. func GetOrSet[T any](
  13. ctx context.Context,
  14. key string,
  15. loader LoaderFunc[T],
  16. opts ...Option,
  17. ) (*T, error) {
  18. o := defaultOptions()
  19. for _, op := range opts {
  20. op(o)
  21. }
  22. full := fullKey(o.prefix, key)
  23. // 1. 尝试缓存读取(JSON)
  24. var out T
  25. if err := redisx.GetJSON(ctx, full, &out); err == nil {
  26. onHit(full)
  27. return &out, nil
  28. } else {
  29. onMiss(full)
  30. }
  31. // 2. 未命中:尝试加分布式锁(如果启用)
  32. if o.lockEnabled {
  33. lockKey := full + ":lock"
  34. lock, _, err := redisx.TryLock(ctx, lockKey, o.lockTTL)
  35. if err == nil && lock != nil {
  36. // acquired lock
  37. defer func() {
  38. if err := lock.Release(ctx); err != nil {
  39. // log but don't break business
  40. zap.L().Error("cachex: failed to release lock", zap.String("key", lockKey), zap.String("err", err.Error()))
  41. }
  42. }()
  43. // double check cache (防止其它线程已写入)
  44. if err := redisx.GetJSON(ctx, full, &out); err == nil {
  45. onHit(full)
  46. return &out, nil
  47. }
  48. // 调用 loader 加载真实数据
  49. data, err := loader(ctx)
  50. if err != nil {
  51. return nil, err
  52. }
  53. if data == nil {
  54. // 业务决定:我们不写空值,直接返回 not found
  55. return nil, errors.New("cachex: loader returned nil")
  56. }
  57. // 写入缓存(可异步)
  58. if o.async {
  59. go func() {
  60. if err := redisx.SetJSON(context.Background(), full, data, o.ttl); err != nil {
  61. zap.L().Error("cachex: async set failed", zap.String("full", full), zap.String("err", err.Error()))
  62. }
  63. }()
  64. } else {
  65. if err := redisx.SetJSON(ctx, full, data, o.ttl); err != nil {
  66. zap.L().Error("cachex: set failed", zap.String("full", full), zap.String("err", err.Error()))
  67. }
  68. }
  69. return data, nil
  70. }
  71. // 未能获得锁:改为短轮询等待缓存(其他 worker 可能正在写)
  72. start := time.Now()
  73. for {
  74. // 尝试从缓存读取
  75. var r T
  76. if err := redisx.GetJSON(ctx, full, &r); err == nil {
  77. onHit(full)
  78. return &r, nil
  79. }
  80. if time.Since(start) >= o.waitForCache {
  81. // 超时则 fallback 直接调用 loader(避免长时间阻塞)
  82. break
  83. }
  84. time.Sleep(50 * time.Millisecond)
  85. }
  86. }
  87. // 最后兜底:直接调用 loader(不持锁)
  88. data, err := loader(ctx)
  89. if err != nil {
  90. return nil, err
  91. }
  92. if data == nil {
  93. return nil, errors.New("cachex: loader returned nil")
  94. }
  95. // 写入缓存(异步)
  96. if o.async {
  97. go func() {
  98. if err := redisx.SetJSON(context.Background(), full, data, o.ttl); err != nil {
  99. zap.L().Error("cachex: async set fallback failed", zap.String("full", full), zap.String("err", err.Error()))
  100. }
  101. }()
  102. } else {
  103. _ = redisx.SetJSON(ctx, full, data, o.ttl)
  104. }
  105. return data, nil
  106. }
  107. // GetOrSetMany: 批量模式,keys 与 loaderFunc 的约定:loader 需要知道哪些 keys 要加载
  108. // 这里提供一个通用版本:loader 接收 keys 并返回 map[key]*T
  109. type BatchLoaderFunc[T any] func(ctx context.Context, keys []string) (map[string]*T, error)
  110. // GetOrSetMany: 先 MGET 缓存,找出缺失 keys,调用 loader 批量加载并回写缓存
  111. func GetOrSetMany[T any](
  112. ctx context.Context,
  113. keys []string,
  114. loader BatchLoaderFunc[T],
  115. opts ...Option,
  116. ) (map[string]*T, error) {
  117. o := defaultOptions()
  118. for _, op := range opts {
  119. op(o)
  120. }
  121. // prefix + full keys
  122. fullKeys := make([]string, len(keys))
  123. for i, k := range keys {
  124. fullKeys[i] = fullKey(o.prefix, k)
  125. }
  126. // MGET via redisx.getClient().MGet
  127. out, missing, err := redisx.MGetJSON[T](ctx, fullKeys...)
  128. if err != nil {
  129. return nil, err
  130. }
  131. if len(missing) == 0 {
  132. // 全命中
  133. return out, nil
  134. }
  135. // 批量 loader
  136. loaded, err := loader(ctx, missing)
  137. if err != nil {
  138. return nil, err
  139. }
  140. fullKeysLoaded := make(map[string]*T)
  141. for k, v := range loaded {
  142. // prefix + full keys
  143. fullKeysLoaded[fullKey(o.prefix, k)] = v
  144. out[k] = v
  145. }
  146. // 回写缓存(批量 MSET)
  147. if err := redisx.MSetJSON[T](ctx, fullKeysLoaded, o.ttl); err != nil {
  148. zap.L().Error("cachex: batch mset exec failed: %v", zap.String("err", err.Error()))
  149. }
  150. return out, nil
  151. }