Files
service-common/utils/replay.go

60 lines
1.5 KiB
Go

package utils
import (
"context"
"fmt"
"git.hlsq.asia/alex/service-common/db/redis"
"git.hlsq.asia/alex/service-common/net/http/httpresp"
"google.golang.org/grpc/metadata"
"time"
)
const (
replayTTL = 2 * time.Minute // 防重放缓存有效期
lockTTL = 10 * time.Second // 分布式锁有效期
)
// WrapPreventReplay 防止重放请求
func WrapPreventReplay[T any](ctx context.Context, usn int64, fun func() (T, error)) (T, error) {
var zero T
key := ""
if md, ok := metadata.FromIncomingContext(ctx); ok {
ridArr := md.Get("Request-Id")
if len(ridArr) == 0 || ridArr[0] == "" {
return fun()
}
key = fmt.Sprintf("cache:replay:%v:%v", usn, ridArr[0])
} else {
return fun()
}
// 先查缓存,命中直接返回
if val := getReplayCache[T](ctx, key); val != nil {
return *val, nil
}
// 尝试抢分布式锁,抢到就执行,没抢到报错
lockKey := fmt.Sprintf("%s:lock", key)
locked, err := redis.GetClient().SetNX(ctx, lockKey, "1", lockTTL).Result()
if err == nil && locked {
defer redis.GetClient().Del(ctx, lockKey)
result, err := fun()
// 这里报错了是没有缓存的
if err != nil {
return zero, err
}
redis.GetCacheClient().Set(ctx, key, result, replayTTL)
return result, nil
}
return zero, httpresp.RequestTooFast
}
// getReplayCache 从 Redis 读取重放缓存
func getReplayCache[T any](ctx context.Context, key string) *T {
var dst T
if redis.GetCacheClient().Get(ctx, key, &dst) {
return &dst
}
return nil
}