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 回归通过
This commit is contained in:
attacker committed 2026-06-26 21:13:15 +08:00
1 parent 5ca0e6d83a
commit c68aef4444
150 files changed
+1038 -690

No files matched your search

+5
View File
@@ -49,6 +49,11 @@ func (r *goRedisCache) Set(ctx context.Context, key string, value string, expira
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()
}
+2
View File
@@ -51,6 +51,8 @@ type Cache interface {
Capabilities() Capabilities
Get(ctx context.Context, key string) (string, error)
Set(ctx context.Context, key string, value string, expiration time.Duration) error
// SetNX 原子「Set if Not eXists」— 用于分布式锁(多副本 Leader 选举)
SetNX(ctx context.Context, key string, value string, expiration time.Duration) (bool, error)
Del(ctx context.Context, keys ...string) error
Exists(ctx context.Context, keys ...string) (int64, error)
Expire(ctx context.Context, key string, expiration time.Duration) error
+21
View File
@@ -65,6 +65,27 @@ func (r *redkaCache) Set(ctx context.Context, key string, value string, expirati
return nil
}
// SetNX check-then-set — Redka v0.2.0 无原子 SetNX,使用 Exists+Set 模拟。
// 本地/redka 模式为单副本运行,不存在并发竞争,非原子实现安全。
func (r *redkaCache) SetNX(ctx context.Context, key string, value string, expiration time.Duration) (bool, error) {
ok, err := r.db.Key().Exists(key)
if err != nil {
return false, err
}
if ok {
return false, nil
}
if err := r.db.Str().Set(key, value); err != nil {
return false, err
}
if expiration > 0 {
if _, err := r.db.Key().Expire(key, expiration); err != nil {
return false, err
}
}
return true, nil
}
func (r *redkaCache) Del(ctx context.Context, keys ...string) error {
for _, key := range keys {
if _, err := r.db.Key().Delete(key); err != nil {