Files
platform/web/services/id.go

179 lines
3.8 KiB
Go
Raw 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.
package services
import (
"context"
"errors"
"fmt"
"platform/pkg/rds"
"strings"
"time"
"github.com/google/uuid"
"github.com/jxskiss/base62"
"github.com/redis/go-redis/v9"
)
var ID IdService = IdService{}
type IdService struct {
}
// region SerialID
const (
// 保留位确保最高位为0防止产生负值
reservedBits = 1
// 时间戳位数
timestampBits = 41
// 序列号位数
sequenceBits = 22
// 最大序列号掩码2^22 - 1
maxSequence = (1 << sequenceBits) - 1
// 位移计算常量
timestampShift = sequenceBits
// Redis 缓存过期时间(秒)
redisTTL = 5
)
var (
ErrSequenceOverflow = errors.New("sequence overflow")
)
func (s *IdService) GenSerial(ctx context.Context) (uint64, error) {
// 构造Redis键
now := time.Now().Unix()
key := idSerialKey(now)
// 使用Redis事务确保原子操作
var sequence int64
err := rds.Client.Watch(ctx, func(tx *redis.Tx) error {
// 获取当前序列号
currentVal, err := tx.Get(ctx, key).Int64()
if err != nil && !errors.Is(err, redis.Nil) {
return err
}
if errors.Is(err, redis.Nil) {
currentVal = 0
}
sequence = currentVal + 1
// 检查序列号是否溢出
if sequence > maxSequence {
return ErrSequenceOverflow
}
// 将更新后的序列号保存回Redis设置5秒过期时间
pipe := tx.Pipeline()
pipe.Set(ctx, key, sequence, redisTTL*time.Second)
_, err = pipe.Exec(ctx)
return err
}, key)
if err != nil {
return 0, err
}
// 组装最终ID
id := uint64((now << timestampShift) | sequence)
return id, nil
}
// ParseSerial 解析ID返回其组成部分
func (s *IdService) ParseSerial(id uint64) (timestamp int64, sequence int64) {
// 通过位运算和掩码提取各部分
timestamp = int64(id >> timestampShift)
sequence = int64(id & maxSequence)
return
}
// idSerialKey 根据时间戳生成Redis键
func idSerialKey(timestamp int64) string {
return fmt.Sprintf("global:id:serial:%d", timestamp)
}
// endregion
// region ReadableID
// GenReadable 根据给定的标签生成易读的全局唯一标识符
// tag 参数用于标识 ID 的用途,如 "usr" 表示用户ID"ord" 表示订单ID等
// 生成的 ID 格式为:<tag>_<encoded-uuid>例如usr_7NLmVLeHwqS73enFZ1i8tB
func (s *IdService) GenReadable(tag string) string {
// 生成 UUID
id := uuid.New()
// 将 UUID 编码为 Base62 字符串(更短,更易读)
encoded := base62.EncodeToString(id[:])
// 如标签为空,则直接返回编码后的字符串
if tag == "" {
return encoded
}
// 标准化标签:转换为小写并移除特殊字符
tag = normalizeTag(tag)
// 组合最终 ID
return fmt.Sprintf("%s_%s", tag, encoded)
}
// ParseReadableID 解析易读ID返回其标签和编码部分
func (s *IdService) ParseReadableID(id string) (tag string, encoded string) {
parts := strings.SplitN(id, "_", 2)
if len(parts) != 2 {
return "", id
}
return parts[0], parts[1]
}
// TryDecodeID 尝试将编码部分解码回 UUID
// 如果解码失败,返回错误
func (s *IdService) TryDecodeID(encoded string) (uuid.UUID, error) {
// 尝试解码 Base62 编码
bytes, err := base62.DecodeString(encoded)
if err != nil {
return uuid.UUID{}, err
}
// 确保长度正确
if len(bytes) != 16 {
return uuid.UUID{}, fmt.Errorf("invalid UUID length after decoding: %d", len(bytes))
}
// 转换为 UUID
var result uuid.UUID
copy(result[:], bytes)
return result, nil
}
// normalizeTag 标准化标签
// 转换为小写,移除特殊字符,最多保留 5 个字符
func normalizeTag(tag string) string {
// 转换为小写
tag = strings.ToLower(tag)
// 移除特殊字符
var sb strings.Builder
for _, c := range tag {
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
sb.WriteRune(c)
}
}
// 截取最多 5 个字符
result := sb.String()
if len(result) > 5 {
result = result[:5]
}
return result
}
// endregion