client.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. // Package redis Redis 连接装配(缓存/锁/任务表并发控制 + token 存储,ADR-008 配套)。
  2. //
  3. // 职责边界(与 infra/db 对称):
  4. // - 本包只负责"连接池建立 + 健康检查 + 提供 Client 句柄",**不实现任何 token 业务键**;
  5. // refresh 白名单 / 吊销黑名单的存储实现见同包 refresh_store.go / revocation_store.go,
  6. // 它们满足 token 包定义的 RefreshStore / RevocationStore 接口(接口在 identity 侧定义,
  7. // 实现落 infra——依赖方向 identity → infra,infra 不 import identity,见 token 包注释)。
  8. // - 敏感配置(addr 带密码时)走环境变量(config.RedisCfg 注释),不入库。
  9. //
  10. // 命名说明:本包包名是 redis,为避免与 go-redis(github.com/redis/go-redis/v9,包名亦 redis)
  11. // 冲突,本包内一律用别名 gredis 指代 go-redis。
  12. //
  13. // 典型使用(cmd/api/main.go 装配段):
  14. //
  15. // rc, err := redis.NewClient(cfg.Redis)
  16. // if err != nil { logger.Fatal("redis: %v", err) } // 启动即不可用 = fail-fast(token 链路强依赖)
  17. // defer func() { _ = rc.Close() }()
  18. //
  19. // // 构造 token 签发器/验签器时传入两个 store(满足 token.RefreshStore / token.RevocationStore 接口)
  20. // iss, err := token.NewIssuer(cfg.Auth.PrivateKeyPEM, cfg.Auth.Issuer,
  21. // accessTTL, refreshTTL, redis.NewRefreshStore(rc), redis.NewRevocationStore(rc, cfg.Auth.RevocationKeyPrefix))
  22. package redis
  23. import (
  24. "context"
  25. "fmt"
  26. "time"
  27. gredis "github.com/redis/go-redis/v9"
  28. "yuansiwei/unified-backend/internal/infra/config"
  29. )
  30. // pingTimeout NewClient 内 Ping 健康检查的超时(1.5s:本地 Redis 毫秒级响应,
  31. // 1.5s 足够覆盖慢启动/高负载;超时必须视为"不可用",不允许带病启动)。
  32. const pingTimeout = 1500 * time.Millisecond
  33. // defaultPoolSize cfg.PoolSize ≤ 0 时的连接池大小兜底(对齐 config 注释建议 ≥ 工作池并发 + 2)。
  34. const defaultPoolSize = 10
  35. // Client Redis 客户端句柄(薄封装 go-redis 的 *gredis.Client,便于未来换实现/埋观测点)。
  36. //
  37. // 嵌入 *gredis.Client——所有命令方法(Set/Get/Del/Exists/Ping/...)+ Close 直接可用,
  38. // 业务 store(refresh_store.go / revocation_store.go)只依赖这个嵌入类型,
  39. // 方法经嵌入提升直接调用(s.Client.Set(...) 等),无需逐命令转发。
  40. //
  41. // 说明:本类型额外提供 Close()(shadow 嵌入类型的 Close,加 nil 接收者保护,
  42. // 统一"进程退出关闭"入口);调用方一律 `defer func() { _ = rc.Close() }()`。
  43. type Client struct {
  44. *gredis.Client
  45. }
  46. // NewClient 建立 Redis 连接池并 Ping 探活(启动时装配一次,进程共享;探活失败应 fail-fast 阻止启动)。
  47. //
  48. // 入参:
  49. // - cfg config.RedisCfg:Redis 配置(Addr 必填;DB 逻辑库编号 0-15;PoolSize 连接池大小,≤0 用默认)。
  50. //
  51. // 出参:
  52. // - *Client:就绪的客户端。
  53. // - error:以下场景非 nil:
  54. // - cfg.Addr 为空(配置缺失,调用方应在 config.Load 层已拦截,此处双保险);
  55. // - Ping 超时/连接被拒(Redis 不可用)→ 调用方按"启动失败"处理(token 链路强依赖,
  56. // 不允许"先启动后降级"——refresh/吊销存储缺失 = 登录态安全机制失效,安全红线)。
  57. //
  58. // 异常场景:
  59. // - 双实例(ADR-015)共享同一 Redis:连接池各实例独立,键空间按前缀天然隔离(revocationKeyPrefix),
  60. // 无需 per-instance 命名空间;
  61. // - Redis 后续运行期故障:本包不重试/不重连(go-redis 内部自带连接池自愈),
  62. // 业务 store 的错误语义(fail-closed)由 store 自身保证(见 revocation_store.go 注释)。
  63. func NewClient(cfg config.RedisCfg) (*Client, error) {
  64. if cfg.Addr == "" {
  65. return nil, fmt.Errorf("redis: 配置 Addr 为空(config.Redis.Addr 必填)")
  66. }
  67. pool := cfg.PoolSize
  68. if pool <= 0 {
  69. pool = defaultPoolSize
  70. }
  71. cli := gredis.NewClient(&gredis.Options{
  72. Addr: cfg.Addr,
  73. DB: cfg.DB,
  74. PoolSize: pool,
  75. })
  76. ctx, cancel := context.WithTimeout(context.Background(), pingTimeout)
  77. defer cancel()
  78. if err := cli.Ping(ctx).Err(); err != nil {
  79. // Ping 失败:关闭已建的连接池(避免泄漏),上抛
  80. _ = cli.Close()
  81. return nil, fmt.Errorf("redis: 探活失败(%s): %w", cfg.Addr, err)
  82. }
  83. return &Client{Client: cli}, nil
  84. }
  85. // Close 释放连接池(进程退出时调用;幂等,重复调用不报错)。
  86. //
  87. // 说明:shadow 嵌入 *gredis.Client 的 Close() error——调用方只看到 Client.Close 一个入口;
  88. // 内部委托给底层 go-redis 连接池关闭。nil 接收者 / 未初始化客户端时返回 nil(便于 defer 场景)。
  89. func (c *Client) Close() error {
  90. if c == nil || c.Client == nil {
  91. return nil
  92. }
  93. return c.Client.Close()
  94. }