b396638438
Motivation: 持仓与已处理信号的状态此前仅存于进程内存,服务重启或多实例部署后会丢失,导致加仓均价、仓位大小等计算失真,重复信号也无法跨实例幂等。通过将状态持久化到 Redis,保证持仓跟踪跨重启、跨实例连续一致,提升通知内容的准确性与可靠性。 Changes: * 新增持仓存储抽象,支持内存与 Redis 两种实现,持仓状态与已处理信号快照按 TTL 持久化 * 持仓变更通过 Redis 事务管道原子提交,保证状态更新与幂等记录一致写入 * 存储写入失败时消息进入重试而非直接确认,避免状态丢失导致通知失真 * 缓存层新增原始值读取与批量事务写入能力,并在订阅器初始化时注入 Redis 依赖 * 补充跨实例持久化、幂等去重与存储失败场景的测试覆盖
146 lines
3.4 KiB
Go
146 lines
3.4 KiB
Go
package tradesignal
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
"time"
|
|
|
|
"aiaa-notification-service/internal/cache"
|
|
)
|
|
|
|
const (
|
|
positionKeyPrefix = "notify:position:"
|
|
appliedKeyPrefix = "notify:position:applied:"
|
|
positionTTL = 30 * 24 * time.Hour
|
|
appliedTTL = 7 * 24 * time.Hour
|
|
storeTimeout = 2 * time.Second
|
|
)
|
|
|
|
type persistedState struct {
|
|
Avg float64 `json:"avg"`
|
|
Size float64 `json:"size"`
|
|
Mode int `json:"mode"`
|
|
}
|
|
|
|
type positionStore interface {
|
|
load(key string) (*state, error)
|
|
commit(key string, st *state, del bool, signalID string, snap Snapshot) error
|
|
loadApplied(signalID string) (Snapshot, bool, error)
|
|
}
|
|
|
|
type memoryStore struct {
|
|
mu sync.Mutex
|
|
positions map[string]*state
|
|
applied map[string]Snapshot
|
|
}
|
|
|
|
func newMemoryStore() *memoryStore {
|
|
return &memoryStore{
|
|
positions: make(map[string]*state),
|
|
applied: make(map[string]Snapshot),
|
|
}
|
|
}
|
|
|
|
func (m *memoryStore) load(key string) (*state, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return m.positions[key], nil
|
|
}
|
|
|
|
func (m *memoryStore) loadApplied(signalID string) (Snapshot, bool, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
snap, ok := m.applied[signalID]
|
|
return snap, ok, nil
|
|
}
|
|
|
|
func (m *memoryStore) commit(key string, st *state, del bool, signalID string, snap Snapshot) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if del {
|
|
delete(m.positions, key)
|
|
} else if st != nil {
|
|
m.positions[key] = st
|
|
}
|
|
if signalID != "" {
|
|
m.applied[signalID] = snap
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type kvClient interface {
|
|
GetRaw(ctx context.Context, key string) ([]byte, error)
|
|
TxWrite(ctx context.Context, writes []cache.KVWrite) error
|
|
}
|
|
|
|
type redisStore struct {
|
|
c kvClient
|
|
}
|
|
|
|
func newRedisStore(c *cache.Cache) positionStore {
|
|
if c == nil {
|
|
return newMemoryStore()
|
|
}
|
|
return &redisStore{c: c}
|
|
}
|
|
|
|
func (s *redisStore) ctx() (context.Context, context.CancelFunc) {
|
|
return context.WithTimeout(context.Background(), storeTimeout)
|
|
}
|
|
|
|
func (s *redisStore) load(key string) (*state, error) {
|
|
ctx, cancel := s.ctx()
|
|
defer cancel()
|
|
b, err := s.c.GetRaw(ctx, positionKeyPrefix+key)
|
|
if err != nil || len(b) == 0 {
|
|
return nil, err
|
|
}
|
|
var p persistedState
|
|
if err := json.Unmarshal(b, &p); err != nil {
|
|
return nil, err
|
|
}
|
|
return &state{avg: p.Avg, size: p.Size, mode: mode(p.Mode)}, nil
|
|
}
|
|
|
|
func (s *redisStore) loadApplied(signalID string) (Snapshot, bool, error) {
|
|
if signalID == "" {
|
|
return Snapshot{}, false, nil
|
|
}
|
|
ctx, cancel := s.ctx()
|
|
defer cancel()
|
|
b, err := s.c.GetRaw(ctx, appliedKeyPrefix+signalID)
|
|
if err != nil || len(b) == 0 {
|
|
return Snapshot{}, false, err
|
|
}
|
|
var snap Snapshot
|
|
if err := json.Unmarshal(b, &snap); err != nil {
|
|
return Snapshot{}, false, err
|
|
}
|
|
return snap, true, nil
|
|
}
|
|
|
|
func (s *redisStore) commit(key string, st *state, del bool, signalID string, snap Snapshot) error {
|
|
ctx, cancel := s.ctx()
|
|
defer cancel()
|
|
writes := make([]cache.KVWrite, 0, 2)
|
|
posKey := positionKeyPrefix + key
|
|
if del {
|
|
writes = append(writes, cache.KVWrite{Key: posKey, Delete: true})
|
|
} else if st != nil {
|
|
b, err := json.Marshal(persistedState{Avg: st.avg, Size: st.size, Mode: int(st.mode)})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writes = append(writes, cache.KVWrite{Key: posKey, Val: b, TTL: positionTTL})
|
|
}
|
|
if signalID != "" {
|
|
b, err := json.Marshal(snap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writes = append(writes, cache.KVWrite{Key: appliedKeyPrefix + signalID, Val: b, TTL: appliedTTL})
|
|
}
|
|
return s.c.TxWrite(ctx, writes)
|
|
}
|