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

152 lines
3.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// redka 内嵌 Redis 兼容存储实现(v0.2.x API)
// 使用 github.com/nalgeon/redka — 基于 SQLite 的 Redis 替代方案 // Logan.Li
// 零外部依赖,数据存在本地文件中,非常适合独立二进制部署
package redis
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/nalgeon/redka"
_ "github.com/mattn/go-sqlite3" // redka.Open 需要 database/sql 驱动名 sqlite3 // Logan.Li
)
// redkaCache 基于 Redka v0.2.x 的缓存实现 // Logan.Li
type redkaCache struct {
db *redka.DB
}
func newRedka(cfg Config) (Cache, error) {
// redka.db 与独立二进制同级存放 // Logan.Li
dataDir := cfg.DataDir
if dataDir == "" {
dataDir = "."
}
if err := os.MkdirAll(dataDir, 0o755); err != nil {
return nil, fmt.Errorf("redka: 创建数据目录失败: %w", err)
}
dbPath := filepath.Join(dataDir, "redka.db")
db, err := redka.Open(dbPath, nil)
if err != nil {
return nil, err
}
return &redkaCache{db: db}, nil
}
func (r *redkaCache) Capabilities() Capabilities {
return Capabilities{SortedSet: false, PubSub: false}
}
func (r *redkaCache) Get(ctx context.Context, key string) (string, error) {
// Redka v0.2.x: Str().Get() 返回 core.Value // Logan.Li
val, err := r.db.Str().Get(key)
if err == redka.ErrNotFound {
return "", nil
}
if err != nil {
return "", err
}
return val.String(), nil
}
func (r *redkaCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
// Redka v0.2.x: Str().Set() 返回 error // Logan.Li
if err := r.db.Str().Set(key, value); err != nil {
return err
}
if expiration > 0 {
_, err := r.db.Key().Expire(key, expiration)
return err
}
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 {
return err
}
}
return nil
}
func (r *redkaCache) Exists(ctx context.Context, keys ...string) (int64, error) {
var count int64
for _, key := range keys {
ok, err := r.db.Key().Exists(key)
if err != nil {
return 0, err
}
if ok {
count++
}
}
return count, nil
}
func (r *redkaCache) Expire(ctx context.Context, key string, expiration time.Duration) error {
ok, err := r.db.Key().Expire(key, expiration)
if err != nil {
return err
}
if !ok {
return redka.ErrNotFound
}
return nil
}
func (r *redkaCache) Ping(ctx context.Context) error {
// Redka 始终可用(内嵌数据库)// Logan.Li
return nil
}
func (r *redkaCache) Close() error {
return r.db.Close()
}
var errSortedSetNotSupported = fmt.Errorf("sorted set operations are not supported by the redka backend; use go-redis (driver=redis) instead")
func (r *redkaCache) ZAdd(ctx context.Context, key string, member SortedSetMember) error {
return errSortedSetNotSupported
}
func (r *redkaCache) ZRangeByScore(ctx context.Context, key string, min, max string) ([]SortedSetMember, error) {
return nil, errSortedSetNotSupported
}
func (r *redkaCache) ZRemRangeByScore(ctx context.Context, key string, min, max string) error {
return errSortedSetNotSupported
}
var errPubSubNotSupported = fmt.Errorf("Pub/Sub is not supported by the redka backend; use go-redis (driver=redis)")
func (r *redkaCache) Publish(ctx context.Context, channel string, message string) error {
// local/redka 无 Pub/Sub,流水线日志走轮询 Get,此处 no-op // Logan.Li
return nil
}