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