Initial open source release

This commit is contained in:
A 2026-06-04 01:37:39 +08:00
commit 74992e893f
377 changed files with 118084 additions and 0 deletions

View file

@ -0,0 +1,112 @@
package redisstore
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"telesrv/internal/store"
)
// UserCounterAllocator 在一次 Lua 往返内同时分配某 user 的下一个 pts 与 box_id。
//
// counter:pts:{userID} 与 counter:box_id:{userID} 共享 hash-tag {userID},处于同一
// Redis Cluster slot,可被单个 Lua 脚本原子操作——把发送热路径上「pts + box 两次往返」
// 合并成一次。语义与 PtsAllocator/BoxIDAllocator 各自调用等价:pts 账号级无洞、
// Redis 冷 miss 时从 PG durable log 恢复基线。
type UserCounterAllocator struct {
c *redis.Client
ptsSource store.CounterSource
boxSource store.CounterSource
}
// NewUserCounterAllocator 创建合并 allocator。ptsSource 恢复 pts(MAX(user_update_events.pts)),
// boxSource 恢复 box_id(MAX(message_boxes.box_id))。
func NewUserCounterAllocator(c *redis.Client, ptsSource, boxSource store.CounterSource) *UserCounterAllocator {
return &UserCounterAllocator{c: c, ptsSource: ptsSource, boxSource: boxSource}
}
// userCountersNextScript 热路径:两 key 都存在时各自 INCR 并返回;任一缺失返回 {-1,-1} 触发恢复。
var userCountersNextScript = redis.NewScript(`
local pts = redis.call("GET", KEYS[1])
local box = redis.call("GET", KEYS[2])
if pts and box then
return {redis.call("INCR", KEYS[1]), redis.call("INCR", KEYS[2])}
end
return {-1, -1}
`)
// userCountersRecoverScript 冷路径:每个 key「缺失才用 PG 基线 SET,再 INCR」。
// 对已存在的 key 跳过 SET 只 INCR,故对「一存一缺」也安全;Redis 单线程串行化保证无重复无洞。
var userCountersRecoverScript = redis.NewScript(`
local function recnext(key, base)
if not redis.call("GET", key) then
redis.call("SET", key, base)
end
return redis.call("INCR", key)
end
return {recnext(KEYS[1], ARGV[1]), recnext(KEYS[2], ARGV[2])}
`)
// NextUserCounters 返回该 user 的下一个 (pts, boxID)。
func (a *UserCounterAllocator) NextUserCounters(ctx context.Context, userID int64) (int, int, error) {
if userID == 0 {
return 0, 0, fmt.Errorf("redis user counters: missing user id")
}
if a.c == nil {
return 0, 0, fmt.Errorf("redis user counters: nil client")
}
keys := []string{ptsKey(userID), boxIDKey(userID)}
pts, box, err := runTwoCounters(ctx, a.c, userCountersNextScript, keys)
if err != nil {
return 0, 0, fmt.Errorf("redis next user counters: %w", err)
}
if pts != missingCounterSentinel && box != missingCounterSentinel {
return int(pts), int(box), nil
}
// 冷路径:至少一个 key 缺失,从 PG durable log 恢复两个基线,再 recover-next。
ptsBase, err := recoverBase(ctx, a.ptsSource, userID, "pts")
if err != nil {
return 0, 0, err
}
boxBase, err := recoverBase(ctx, a.boxSource, userID, "box_id")
if err != nil {
return 0, 0, err
}
pts, box, err = runTwoCounters(ctx, a.c, userCountersRecoverScript, keys, ptsBase, boxBase)
if err != nil {
return 0, 0, fmt.Errorf("redis recover user counters: %w", err)
}
return int(pts), int(box), nil
}
// runTwoCounters 执行返回二元整数表的 Lua 脚本,用 .Slice() 手动解析(不依赖 Int64Slice)。
func runTwoCounters(ctx context.Context, c *redis.Client, script *redis.Script, keys []string, args ...any) (int64, int64, error) {
raw, err := script.Run(ctx, c, keys, args...).Slice()
if err != nil {
return 0, 0, err
}
if len(raw) != 2 {
return 0, 0, fmt.Errorf("unexpected reply len %d, want 2", len(raw))
}
a, okA := raw[0].(int64)
b, okB := raw[1].(int64)
if !okA || !okB {
return 0, 0, fmt.Errorf("unexpected reply element types %T,%T, want int64", raw[0], raw[1])
}
return a, b, nil
}
func recoverBase(ctx context.Context, source store.CounterSource, userID int64, name string) (int, error) {
if source == nil {
return 0, nil
}
v, err := source.Current(ctx, userID)
if err != nil {
return 0, fmt.Errorf("recover %s counter: %w", name, err)
}
return v, nil
}