feature/upgrade_redis_queue #1

Merged
zhangdeman merged 2 commits from feature/upgrade_redis_queue into master 2026-04-13 17:38:13 +08:00
6 changed files with 475 additions and 164 deletions
Showing only changes of commit c14e5f84dc - Show all commits

211
delay/redis_consumer.go Normal file
View File

@@ -0,0 +1,211 @@
// Package delay ...
//
// Description : event ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-08-22 18:55
package delay
import (
"context"
"errors"
"fmt"
"os"
"strings"
"sync"
"time"
"git.zhangdeman.cn/zhangdeman/network/util"
"git.zhangdeman.cn/zhangdeman/queue/abstract"
"git.zhangdeman.cn/zhangdeman/queue/define"
"git.zhangdeman.cn/zhangdeman/redis"
"git.zhangdeman.cn/zhangdeman/serialize"
redisV9 "github.com/redis/go-redis/v9"
"github.com/tidwall/gjson"
)
// NewRedisConsumer 启动一个消费者实例
func NewRedisConsumer(
ctx context.Context,
queueName string, queueCnt int, redisFlag string,
consumerHandler abstract.IConsumerHandler,
) abstract.IConsumer {
if nil == consumerHandler {
panic("consumer handler instance is nil")
}
if queueName == "" || redisFlag == "" {
panic("init redis producer: queue name or redis flag is empty")
}
// 验证redis实例是否存在
if _, err := redis.Client.GetRealClientWithError(redisFlag); nil != err {
panic(err.Error())
}
if queueCnt <= 0 {
queueCnt = 1 // 默认单队列
}
if nil == ctx {
ctx = context.Background()
}
c := &redisConsumer{
lock: &sync.RWMutex{},
hasStop: false,
ctx: ctx,
stopChan: make(chan bool),
redisFlag: redisFlag,
queueName: queueName,
queueCnt: queueCnt,
consumerHandler: consumerHandler,
}
return c
}
type redisConsumer struct {
lock *sync.RWMutex
hasStop bool
stopChan chan bool // 停止请求的chan
ctx context.Context // 请求处理上下文
redisFlag string // redis 标识, 必须是使用统一的 pkg/redis 进行管理的
queueName string // 队列名称,基础公共前缀,尾部后缀会自动根据 shard cnt 进行哈希
queueCnt int // 队列数量
consumerHandler abstract.IConsumerHandler // 消息处理实例
}
// Start 启动消费者
func (r *redisConsumer) Start() error {
var realQueueList []string
for i := 0; i < r.queueCnt; i++ {
realQueueList = append(realQueueList, fmt.Sprintf("%v_%v", r.queueName, i))
}
hostname, _ := os.Hostname()
serverInfo := &define.EventConsumerServerInfo{
SystemTimestamp: time.Now().UnixMilli(),
Hostname: hostname,
HostIp: util.IP.GetHostIP(),
Queue: strings.Join(realQueueList, ","),
}
for {
isFinish := false
select {
case <-r.ctx.Done(): // context被取消(对应程序系统默认行为,一般对应程序停止运行)
if nil != r.lock {
r.lock.Lock()
r.hasStop = true
r.lock.Unlock()
} else {
r.hasStop = true
}
isFinish = true
case <-r.stopChan: // 对应开发者介入处理行为, 主进程不停止运行, 但是消费者要退出
isFinish = true
default: // 60s 无数据进入下一轮等待, 或者 消费到数据正常处理
if r.hasStop {
// 已停止
return nil
}
res := redis.Wrapper.BRPop(r.ctx, r.redisFlag, realQueueList, 60)
if nil != res.Err {
if errors.Is(res.Err, redisV9.Nil) {
// 判断是否为未订阅到消息
continue
}
r.notifyMessageSubscribeFailure(serverInfo, res.Err)
time.Sleep(time.Second * 10)
continue
}
// 订阅成功, 判断是否无数据
if res.Result == "" {
// 直接进入下一轮阻塞等待
continue
}
// 订阅到数据, 数据解析
// BRPOP 返回数据是数组 0: 读取到数据的队列名 1: 读取到的数据
// 前置redis操作统一序列化, 此处读取到的是一个序列化后的字符串 `["queue_name", "value"]`
serverInfo.Queue = gjson.Get(res.Result, "0").String()
var (
err error
formatData define.EventData
)
if err = serialize.JSON.UnmarshalWithNumberForString(gjson.Get(res.Result, "1").String(), &formatData); nil != err {
// 回调数据解析失败的处理函数
r.notifyMessageUnmarshalFailure(serverInfo, res.Result, err)
continue
}
// 处理消息(同步或异步)
r.handler(serverInfo, &formatData)
}
if isFinish {
break
}
}
return nil
}
// notifyMessageSubscribeFailure 通知订阅失败
func (r *redisConsumer) notifyMessageSubscribeFailure(serverInfo *define.EventConsumerServerInfo, err error) {
defer func() {
// 防一手 panic, 订阅失败回调出现panic, 不回调panic处理方法
recover()
}()
r.consumerHandler.SubscribeFailureCallback(r.ctx, serverInfo, err)
}
// 回调消息返序列化失败的处理逻辑
func (r *redisConsumer) notifyMessageUnmarshalFailure(serverInfo *define.EventConsumerServerInfo, res string, err error) {
defer func() {
// 防一手 panic, 返回劣化失败回调出现panic, 不回调panic处理方法
recover()
}()
r.consumerHandler.UnmarshalFailureCallback(r.ctx, serverInfo, res, err)
}
// handler 处理订阅到的数据
func (r *redisConsumer) handler(serverInfo *define.EventConsumerServerInfo, data *define.EventData) {
// 处理消息(同步或异步)
if r.consumerHandler.Async(r.ctx, serverInfo, data) {
// 异步处理
go func() {
r.dealMessageData(serverInfo, data)
}()
} else {
r.dealMessageData(serverInfo, data)
}
}
// dealMessageData 处理消息数据
func (r *redisConsumer) dealMessageData(serverInfo *define.EventConsumerServerInfo, data *define.EventData) {
defer func() {
// 同步处理
if err := recover(); nil != err {
// 出现panic, 回调 panic 处理
r.consumerHandler.PanicCallback(r.ctx, serverInfo, data, err)
}
}()
// 加锁, 若果不需要加锁, 实现接口时函数体留空即可
if err := r.consumerHandler.Lock(r.ctx, serverInfo, data); nil != err {
r.consumerHandler.LockFailureCallback(r.ctx, serverInfo, data, err)
return
}
defer func() {
// 释放锁
if err := r.consumerHandler.Unlock(r.ctx, serverInfo, data); nil != err {
r.consumerHandler.UnlockFailureCallback(r.ctx, serverInfo, data, err)
return
}
}()
// 执行对消息的处理
if err := r.consumerHandler.MessageLogic(r.ctx, serverInfo, data); nil != err {
r.consumerHandler.MessageLogicFailureCallback(r.ctx, serverInfo, data, err)
}
}
// Stop 停止消费者
func (r *redisConsumer) Stop() {
r.stopChan <- true
}

129
delay/redis_delay.go Normal file
View File

@@ -0,0 +1,129 @@
// Package delay ...
//
// Description : 基于redis实现的延迟事件队列
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-08-29 09:11
package delay
import (
"context"
"time"
"git.zhangdeman.cn/zhangdeman/queue/abstract"
"git.zhangdeman.cn/zhangdeman/queue/define"
redisPkg "git.zhangdeman.cn/zhangdeman/redis"
redisPkgDefine "git.zhangdeman.cn/zhangdeman/redis/define"
"git.zhangdeman.cn/zhangdeman/serialize"
"github.com/tidwall/gjson"
)
// NewRedisDelayEvent 获取延迟队列实例
// 参数说明:
// - redisFlag: 使用那个redis实例
// - queueName: 延迟队列名称
// - pullTimeInterval: 延迟队列扫描的时间间隔, 单位秒
// - distributeProducer: 消息生产者示例, 扫描到的数据通过此实例向二级任务队列分发
func NewRedisDelayEvent(redisFlag string, queueName string, pullTimeInterval int64, distributeProducer abstract.IProducer) abstract.IDelayQueue {
if queueName == "" || redisFlag == "" {
panic("init redis delay event: queue name or redis flag is empty")
}
if nil == distributeProducer {
panic("distributeProducer is nil")
}
// 验证redis实例是否存在
if _, err := redisPkg.Client.GetRealClientWithError(redisFlag); nil != err {
panic(err.Error())
}
if pullTimeInterval <= 0 {
pullTimeInterval = 60
}
return &redisDelayEvent{
stopChan: make(chan bool, 1),
redisFlag: redisFlag,
delayQueueName: queueName,
pullTimeInterval: pullTimeInterval,
distributeProducer: distributeProducer,
}
}
type redisDelayEvent struct {
stopChan chan bool // 停止请求的chan
redisFlag string // 使用的redis实例
delayQueueName string // 延迟队列名称
pullTimeInterval int64 // 延迟队列多久拉取一次数据
distributeProducer abstract.IProducer // 事件分发的生产者
}
// Send 生成一条延时事件
// 参数说明:
// - delayTime: 延时时长, 单位: s
// - data: 要发送的数据
func (rde redisDelayEvent) Send(ctx context.Context, delayTime int64, data *define.EventData) *redisPkgDefine.RedisResult {
if delayTime <= 0 {
// 默认延迟 1min
delayTime = 60
}
if delayTime >= time.Now().Unix() {
// 指定具体的延迟时间, 重置延时时间为相对时间
delayTime = delayTime - time.Now().Unix()
}
return redisPkg.Wrapper.ZAdd(ctx, rde.redisFlag, rde.delayQueueName, time.Now().Unix()+delayTime, serialize.JSON.MarshalForStringIgnoreError(data))
}
// Distribute 到期事件事件分发
func (rde redisDelayEvent) Distribute(ctx context.Context) {
for {
hasFinish := false
select {
case <-ctx.Done():
// 收到上下文已完成事件, 退出
hasFinish = true
case <-rde.stopChan:
// 程序调用stop方法, 退出
hasFinish = true
default:
now := time.Now()
// 计时器, 固定时长扫描一次
// ZRangeAndRemByScore 此方法内部已经自动包装了删除逻辑
eventDataListRes := redisPkg.Wrapper.ZRangeAndRemByScore(ctx, rde.redisFlag, rde.delayQueueName, 0, now.Unix())
// ZRange 返回数据时二维数组 [["数据部分", "设置的数据score"]]
resList := gjson.Parse(eventDataListRes.Result).Array()
if len(resList) > 0 {
for _, itemData := range resList {
var (
resFormat define.EventData
err error
)
if err = serialize.JSON.UnmarshalWithNumberForString(itemData.Get("0").String(), &resFormat); nil != err {
// 数据解析失败, 触发失败回调
rde.distributeProducer.GetProducerHandler().FailureCallback(ctx, &define.EventData{
Type: "UNMARSHAL_FAILURE",
Data: itemData.Get("0").String(),
}, &define.EventProduceResult{
Queue: "",
Success: false,
Err: err,
Cost: 0,
})
continue
} else {
// 生产数据, 向耳机队列分发任务
rde.distributeProducer.Sync(ctx, &resFormat)
}
}
}
// 间隔指定时间拉取一次
time.Sleep(time.Second * time.Duration(rde.pullTimeInterval))
}
if hasFinish {
break
}
}
}
func (rde redisDelayEvent) Stop() {
rde.stopChan <- true
}

View File

@@ -1,103 +0,0 @@
// Package delay ...
//
// Description : delay ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2022-07-07 10:32
package delay
import (
"context"
"errors"
"time"
"git.zhangdeman.cn/zhangdeman/serialize"
"github.com/redis/go-redis/v9"
)
// NewRedisConsumer redis延迟队列消费者
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 14:10 2022/7/8
func NewRedisConsumer(redisInstance *redis.Client, cfg *ConsumerConfig) IConsumer {
return &redisConsumer{
redisInstance: redisInstance,
cfg: cfg,
}
}
type redisConsumer struct {
redisInstance *redis.Client
cfg *ConsumerConfig
}
func (r *redisConsumer) Consume(ctx context.Context) ([]*ProduceData, error) {
if nil == ctx {
ctx = context.Background()
}
zRangeResult := r.redisInstance.ZRange(ctx, r.cfg.QueueName, 0, time.Now().UnixNano()/1e6)
if err := zRangeResult.Err(); nil != err {
return make([]*ProduceData, 0), err
}
// 格式化数据
var (
result []*ProduceData
)
valueList := zRangeResult.Val()
if len(valueList) == 0 {
return result, nil
}
for _, item := range valueList {
d := &ProduceData{}
serialize.JSON.UnmarshalWithNumberForStringIgnoreError(item, d)
result = append(result, d)
}
return result, nil
}
func (r *redisConsumer) ConsumeWithHandler(ctx context.Context, handler IHandler) error {
if nil == handler {
return errors.New("handler instance is nil")
}
var (
msgList []*ProduceData
err error
)
if msgList, err = r.Consume(ctx); nil != err {
return err
}
// 未订阅到消息
if len(msgList) == 0 {
return nil
}
return handler.Handle(msgList)
/* wg := &sync.WaitGroup{}
wg.Add(len(msgList))
for _, item := range msgList {
go func(msgData *ZRangeData) {
defer wg.Done()
hashValue, exist := msgData.Data.Data[r.cfg.HashKey]
if !exist || hashValue == nil {
hashValue = msgData.Data.MsgID
}
shard := util.Hash.GetHashIDMod(hashValue, r.cfg.SonQueueCnt)
realQueue := fmt.Sprintf(r.cfg.QueueName+"_%d", shard)
r.redisInstance.LPush()
}(item)
}
wg.Wait()
return nil*/
}
type redisConsumerMsgHandler struct {
}
func (r redisConsumerMsgHandler) Handle(queData []*ZRangeData) error {
panic("implement me")
}

View File

@@ -1,78 +1,116 @@
// Package delay ...
// Package event ...
//
// Description : delay ...
// Description : 基于redis实现事件生产 + 消费
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2022-07-06 17:59
// Date : 2025-08-22 16:34
package delay
import (
"context"
"sync"
"fmt"
"os"
"time"
"git.zhangdeman.cn/zhangdeman/network/util"
"git.zhangdeman.cn/zhangdeman/queue/abstract"
"git.zhangdeman.cn/zhangdeman/queue/define"
"git.zhangdeman.cn/zhangdeman/redis"
"git.zhangdeman.cn/zhangdeman/serialize"
"git.zhangdeman.cn/zhangdeman/wrapper/op_string"
"github.com/redis/go-redis/v9"
)
// NewRedisQueue 获取redis队列实例
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:09 2022/7/6
func NewRedisQueue(redisInstance *redis.Client) IProduce {
return &redisProduce{client: redisInstance}
}
// withRedis 使用redis实现延迟队列
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 17:59 2022/7/6
type redisProduce struct {
client *redis.Client
}
// Produce 生产数据
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:03 2022/7/6
func (rp *redisProduce) Produce(ctx context.Context, data ...*Queue) error {
if len(data) == 0 {
return nil
// NewRedisProducer 获取基于redis的生产者实例
func NewRedisProducer(
queueName string, queueCnt int, redisFlag string,
producerHandler abstract.IProducerHandler,
) abstract.IProducer {
if queueName == "" || redisFlag == "" {
panic("init redis producer: queue name or redis flag is empty")
}
if nil == ctx {
ctx = context.Background()
// 验证redis实例是否存在
if _, err := redis.Client.GetRealClientWithError(redisFlag); nil != err {
panic(err.Error())
}
wg := &sync.WaitGroup{}
wg.Add(len(data))
for _, queueData := range data {
go func(inputQueueData *Queue) {
defer wg.Done()
inputQueueData.err = rp.client.ZAdd(ctx, inputQueueData.Name, rp.buildAddMember(inputQueueData)).Err()
}(queueData)
if queueCnt <= 0 {
queueCnt = 1 // 默认单队列
}
wg.Wait()
return nil
}
// buildAddMember ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:22 2022/7/6
func (rp *redisProduce) buildAddMember(queueData *Queue) redis.Z {
return redis.Z{
Score: float64(time.Now().Unix() + queueData.DelayTime),
Member: serialize.JSON.MarshalForStringIgnoreError(&ProduceData{
MsgID: op_string.RandomMd5().Value,
Timestamp: time.Now().UnixNano() / 1e6,
Host: "",
Data: queueData.Data,
}),
if nil == producerHandler {
producerHandler = abstract.DefaultProducerHandler{}
}
return &redisProducer{
queueCnt: queueCnt,
queueName: queueName,
redisFlag: redisFlag,
producerHandler: producerHandler,
}
}
type redisProducer struct {
redisFlag string // redis 标识, 必须是使用统一的 pkg/redis 进行管理的
queueName string // 队列名称,基础公共前缀,尾部后缀会自动根据 shard cnt 进行哈希
queueCnt int // 队列数量
producerHandler abstract.IProducerHandler // 生产数据的处理逻辑
}
// fillEventData 填充一些系统数据数据
func (r *redisProducer) fillEventData(data *define.EventData) {
if len(data.Key) == 0 {
data.Key = data.MsgID // 不设置和MsgID一致
}
if len(data.TraceID) == 0 {
data.TraceID = data.Key
}
data.SystemTimestamp = time.Now().UnixMilli() // 系统时间
if data.Timestamp <= 0 {
data.Timestamp = data.SystemTimestamp
}
data.Hostname, _ = os.Hostname() // 服务器 hostname
data.HostIp = util.IP.GetHostIP() // 服务器IP
}
// Sync 同步发送
func (r *redisProducer) Sync(ctx context.Context, data *define.EventData) {
if nil == data {
return
}
r.fillEventData(data)
realQueue := r.getRealQueue(data)
sendRedisRes := redis.Wrapper.LPush(ctx, r.redisFlag, realQueue, serialize.JSON.MarshalForStringIgnoreError(data))
res := &define.EventProduceResult{
Queue: realQueue,
Success: sendRedisRes.Err == nil,
Err: sendRedisRes.Err,
Cost: sendRedisRes.UsedTime,
}
if res.Success {
r.producerHandler.SuccessCallback(ctx, data, res)
} else {
r.producerHandler.FailureCallback(ctx, data, res)
}
}
// Async 异步发送
func (r *redisProducer) Async(ctx context.Context, data *define.EventData) {
go func() {
defer func() {
// 如果出现panic, 则触发失败回调
if err := recover(); nil != err {
r.producerHandler.PanicCallback(ctx, data, err)
}
}()
r.Sync(ctx, data)
}()
}
// GetProducerHandler 获取producer处理器
func (r *redisProducer) GetProducerHandler() abstract.IProducerHandler {
return r.producerHandler
}
// getRealQueue 获取真实的队列key
func (r *redisProducer) getRealQueue(data *define.EventData) string {
queueIdx := op_string.HashNumber(data.Key).Value % uint64(r.queueCnt)
return fmt.Sprintf("%v_%v", r.queueName, queueIdx)
}

14
go.mod
View File

@@ -1,12 +1,14 @@
module git.zhangdeman.cn/zhangdeman/queue
go 1.24.0
go 1.25.0
require (
git.zhangdeman.cn/zhangdeman/redis v0.0.0-20260413082618-2adbe2bcd3a5
git.zhangdeman.cn/zhangdeman/network v0.0.0-20260406142525-48e87d040519
git.zhangdeman.cn/zhangdeman/redis v0.0.0-20260413092650-825ae95ecb29
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20260112135254-c9ba29f9f674
git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20260321023345-6c6e467e3a14
github.com/redis/go-redis/v9 v9.18.0
github.com/tidwall/gjson v1.18.0
)
require (
@@ -15,9 +17,17 @@ require (
git.zhangdeman.cn/zhangdeman/util v0.0.0-20260105024213-3d76b1bcde5a // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-redis/redismock/v9 v9.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mozillazg/go-pinyin v0.21.0 // indirect
github.com/mssola/user_agent v0.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sbabiv/xml2map v1.2.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect

26
go.sum
View File

@@ -1,9 +1,13 @@
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260413082525-fb90982b1256 h1:zYkRoH86j+4BoVGCWUY6zA+FIQNmul/c8whmmYK3bT4=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260413082525-fb90982b1256/go.mod h1:5p8CEKGBxi7qPtTXDI3HDmqKAfIm5i/aBWdrbkbdNjc=
git.zhangdeman.cn/zhangdeman/network v0.0.0-20260406142525-48e87d040519 h1:+895a/Efz/rrYSCXP95VQlIWN2rE2d2PFAevTT0D5To=
git.zhangdeman.cn/zhangdeman/network v0.0.0-20260406142525-48e87d040519/go.mod h1:0UcxxT2AoxJwR3SwsPAqA8Od+qY40kIdD8WU+TT0wYg=
git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20251013024601-da007da2fb42 h1:VjYrb4adud7FHeiYS9XA0B/tOaJjfRejzQAlwimrrDc=
git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20251013024601-da007da2fb42/go.mod h1:VHb9qmhaPDAQDcS6vUiDCamYjZ4R5lD1XtVsh55KsMI=
git.zhangdeman.cn/zhangdeman/redis v0.0.0-20260413082618-2adbe2bcd3a5 h1:WjPlJ6CoXi2yFl4NGyyCWWpB2KbBP5LH2Hia6Xeecsc=
git.zhangdeman.cn/zhangdeman/redis v0.0.0-20260413082618-2adbe2bcd3a5/go.mod h1:y/0RBrC0CipiYJlBZdkvNRU4ROw9wZt+UqzZJBxC7Ng=
git.zhangdeman.cn/zhangdeman/redis v0.0.0-20260413092650-825ae95ecb29 h1:o+B0oeJkY2JAiw/zGbavwhrd0KZ6ZpG4LAxMLWaUOwg=
git.zhangdeman.cn/zhangdeman/redis v0.0.0-20260413092650-825ae95ecb29/go.mod h1:y/0RBrC0CipiYJlBZdkvNRU4ROw9wZt+UqzZJBxC7Ng=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20260112135254-c9ba29f9f674 h1:75JJ09HPqWi9qm7XD+vV6p5TaCMQgDsae/EbsLiE1t4=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20260112135254-c9ba29f9f674/go.mod h1:EXrvDs830GzqhDNTR5TgKVbT3ADRgyUb2pFerwF4rLc=
git.zhangdeman.cn/zhangdeman/util v0.0.0-20260105024213-3d76b1bcde5a h1:IGUsWz204BTQlD2l4kenlwJQS4Av2RS2kfUHZ5QVrmw=
@@ -21,8 +25,12 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
@@ -31,6 +39,14 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mozillazg/go-pinyin v0.21.0 h1:Wo8/NT45z7P3er/9YSLHA3/kjZzbLz5hR7i+jGeIGao=
github.com/mozillazg/go-pinyin v0.21.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4=
github.com/mssola/user_agent v0.6.0/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
@@ -52,6 +68,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -62,8 +86,10 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=