Files
attacker c68aef4444 feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)
- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全)
- AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus
- 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错
- 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder)
- 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断
- 同步未提交的前端类型安全迁移(140+ 文件)
- ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新

验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常,
锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
2026-06-26 21:13:15 +08:00

109 lines
3.0 KiB
Go

// go-redis 外部 Redis 服务器实现
// 用于 Docker Compose / 生产环境部署 // Logan.Li
package redis
import (
"context"
"fmt"
"time"
goredis "github.com/redis/go-redis/v9"
)
// goRedisCache 基于 go-redis 的缓存实现 // Logan.Li
type goRedisCache struct {
client *goredis.Client
}
func newGoRedis(cfg Config) (Cache, error) {
client := goredis.NewClient(&goredis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
Password: cfg.Password,
DB: cfg.DB,
})
// 连接测试
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := client.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("redis: 连接失败 %s:%d: %w", cfg.Host, cfg.Port, err)
}
return &goRedisCache{client: client}, nil
}
func (r *goRedisCache) Capabilities() Capabilities {
return Capabilities{SortedSet: true, PubSub: true}
}
func (r *goRedisCache) Get(ctx context.Context, key string) (string, error) {
val, err := r.client.Get(ctx, key).Result()
if err == goredis.Nil {
return "", nil
}
return val, err
}
func (r *goRedisCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
return r.client.Set(ctx, key, value, expiration).Err()
}
// SetNX 原子 SET if Not eXists — go-redis 原生支持,用于分布式锁
func (r *goRedisCache) SetNX(ctx context.Context, key string, value string, expiration time.Duration) (bool, error) {
return r.client.SetNX(ctx, key, value, expiration).Result()
}
func (r *goRedisCache) Del(ctx context.Context, keys ...string) error {
return r.client.Del(ctx, keys...).Err()
}
func (r *goRedisCache) Exists(ctx context.Context, keys ...string) (int64, error) {
return r.client.Exists(ctx, keys...).Result()
}
func (r *goRedisCache) Expire(ctx context.Context, key string, expiration time.Duration) error {
return r.client.Expire(ctx, key, expiration).Err()
}
func (r *goRedisCache) Ping(ctx context.Context) error {
return r.client.Ping(ctx).Err()
}
func (r *goRedisCache) Close() error {
return r.client.Close()
}
func (r *goRedisCache) ZAdd(ctx context.Context, key string, member SortedSetMember) error {
return r.client.ZAdd(ctx, key, goredis.Z{
Score: member.Score,
Member: member.Member,
}).Err()
}
func (r *goRedisCache) ZRangeByScore(ctx context.Context, key string, min, max string) ([]SortedSetMember, error) {
zs, err := r.client.ZRangeByScoreWithScores(ctx, key, &goredis.ZRangeBy{
Min: min,
Max: max,
}).Result()
if err != nil {
return nil, err
}
members := make([]SortedSetMember, len(zs))
for i, z := range zs {
members[i] = SortedSetMember{
Score: z.Score,
Member: z.Member.(string),
}
}
return members, nil
}
func (r *goRedisCache) ZRemRangeByScore(ctx context.Context, key string, min, max string) error {
return r.client.ZRemRangeByScore(ctx, key, min, max).Err()
}
func (r *goRedisCache) Publish(ctx context.Context, channel string, message string) error {
return r.client.Publish(ctx, channel, message).Err()
}