| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- // 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()
- }
|