Initial open source release
This commit is contained in:
commit
74992e893f
377 changed files with 118084 additions and 0 deletions
112
internal/store/redisstore/user_counter_allocator.go
Normal file
112
internal/store/redisstore/user_counter_allocator.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue