1
0
mirror of https://github.com/charlienet/go-mixed.git synced 2025-07-18 08:32:40 +08:00

32 Commits

Author SHA1 Message Date
38f7cc75c9 set 2024-05-28 04:25:17 +08:00
b4ac1cc449 locker 2024-05-28 04:23:30 +08:00
1abde30d8f locker 2024-05-28 04:17:26 +08:00
822932fe15 use base locker 2024-05-28 04:14:08 +08:00
85c5a611e1 bbb 2024-05-28 04:12:50 +08:00
fe5c0b54b6 locker 2024-05-28 04:08:50 +08:00
54fbe8eb0a locker 2024-05-28 04:05:37 +08:00
2d851d4872 fix error 2024-04-11 10:45:33 +08:00
a83ccf7c00 config 2023-12-13 17:26:23 +08:00
bb979f5ccb config 2023-12-13 17:08:56 +08:00
330d9f78d3 添加锁 2023-11-06 10:09:40 +08:00
5428b530b2 hash function 2023-11-03 16:27:14 +08:00
249d3b4682 delay 2023-11-03 15:48:33 +08:00
bdbf18969e update 2023-11-03 15:48:14 +08:00
01f426c5b2 use redis function(Version 7 and above is required
)
2023-11-03 15:46:45 +08:00
2f2af226ee update redis 2023-11-03 15:04:40 +08:00
5f065de145 redis rename key 2023-11-01 16:32:58 +08:00
f3918dd02b use redis function 2023-10-27 17:33:11 +08:00
1c5a32e828 添加redis function 2023-10-27 16:35:28 +08:00
5a17236fd7 优化redis eval的key重命名 2023-10-26 15:47:33 +08:00
91a5a7d612 update 2023-10-26 14:42:56 +08:00
0d124c0b79 更新ID生成器 2023-10-26 14:42:23 +08:00
6647f96978 rename 2023-10-13 14:38:42 +08:00
d1c269ed90 更新redis 2023-10-13 10:52:36 +08:00
e83db7daee 添加分布式锁实现-使用Redis 2023-10-12 15:25:13 +08:00
69690da6b4 更新库 2023-10-12 15:02:04 +08:00
1203e27c7e add DB 2023-10-12 15:01:16 +08:00
42b5cef555 u 2023-10-12 15:00:23 +08:00
bcfb177fd9 use redis 2023-10-12 14:46:30 +08:00
0df55ed551 使用接口返回 2023-10-12 14:45:50 +08:00
95ad0941a8 update redis client 2023-10-12 14:37:10 +08:00
165fc91f9b 添加订阅 2023-10-12 14:29:42 +08:00
81 changed files with 3542 additions and 658 deletions

View File

@ -1,21 +1,21 @@
package bloom package bloom
import ( import (
"context"
"math" "math"
"github.com/charlienet/go-mixed/bytesconv" "github.com/charlienet/go-mixed/bytesconv"
"github.com/charlienet/go-mixed/expr"
"github.com/charlienet/go-mixed/hash" "github.com/charlienet/go-mixed/hash"
"github.com/go-redis/redis/v8" "github.com/charlienet/go-mixed/redis"
) )
const DEFAULT_SIZE = 2 << 24 const DEFAULT_SIZE = 2 << 24
var seeds = []uint{7, 11, 13, 31, 37, 61} var seeds = []uint{7, 11, 13, 31, 37, 61, 79, 97}
type bitStore interface { type bitStore interface {
Clear() Clear()
Set(pos ...uint) error Set(ctx context.Context, pos ...uint) error
Test(pos ...uint) (bool, error) Test(pos ...uint) (bool, error)
} }
@ -26,13 +26,13 @@ type BloomFilter struct {
} }
type bloomOptions struct { type bloomOptions struct {
redisClient *redis.Client redisClient redis.Client
redisKey string redisKey string
} }
type option func(*bloomOptions) type option func(*bloomOptions)
func WithRedis(redis *redis.Client, key string) option { func WithRedis(redis redis.Client, key string) option {
return func(bo *bloomOptions) { return func(bo *bloomOptions) {
bo.redisClient = redis bo.redisClient = redis
bo.redisKey = key bo.redisKey = key
@ -50,22 +50,22 @@ func New(expectedInsertions uint, fpp float64, opts ...option) *BloomFilter {
bits := optimalNumOfBits(expectedInsertions, fpp) bits := optimalNumOfBits(expectedInsertions, fpp)
k := optimalNumOfHashFunctions(bits, expectedInsertions) k := optimalNumOfHashFunctions(bits, expectedInsertions)
if k > uint(len(seeds)) {
k = uint(len(seeds))
}
bf := &BloomFilter{ bf := &BloomFilter{
bits: bits, bits: bits,
funcs: k, funcs: k,
store: expr.Ternary[bitStore]( store: createBitStore(opt, bits),
opt.redisClient == nil,
newMemStore(bits),
newRedisStore(opt.redisClient, opt.redisKey, bits)),
} }
return bf return bf
} }
func (bf *BloomFilter) Add(data string) { func (bf *BloomFilter) Add(ctx context.Context, data string) {
offsets := bf.geOffsets([]byte(data)) offsets := bf.geOffsets([]byte(data))
bf.store.Set(offsets...) bf.store.Set(ctx, offsets...)
} }
func (bf *BloomFilter) ExistString(data string) (bool, error) { func (bf *BloomFilter) ExistString(data string) (bool, error) {
@ -73,7 +73,7 @@ func (bf *BloomFilter) ExistString(data string) (bool, error) {
} }
func (bf *BloomFilter) Exists(data []byte) (bool, error) { func (bf *BloomFilter) Exists(data []byte) (bool, error) {
if data == nil || len(data) == 0 { if len(data) == 0 {
return false, nil return false, nil
} }
@ -89,7 +89,7 @@ func (bf *BloomFilter) Exists(data []byte) (bool, error) {
func (bf *BloomFilter) geOffsets(data []byte) []uint { func (bf *BloomFilter) geOffsets(data []byte) []uint {
offsets := make([]uint, bf.funcs) offsets := make([]uint, bf.funcs)
for i := uint(0); i < bf.funcs; i++ { for i := uint(0); i < bf.funcs; i++ {
offsets[i] = uint(hash.Murmur3(append(data, byte(i))) % uint64(bf.bits)) offsets[i] = uint(hash.Murmur3(append(data, byte(seeds[i]))) % uint64(bf.bits))
} }
return offsets return offsets
@ -100,6 +100,14 @@ func (bf *BloomFilter) Clear() {
bf.store.Clear() bf.store.Clear()
} }
func createBitStore(opt *bloomOptions, bits uint) bitStore {
if opt.redisClient != nil {
return newRedisStore(opt.redisClient, opt.redisKey, bits)
}
return newMemStore(bits)
}
// 计算优化的位图长度, // 计算优化的位图长度,
// n 期望放置元素数量, // n 期望放置元素数量,
// p 预期的误判概率 // p 预期的误判概率

20
bloom/bloom.lua Normal file
View File

@ -0,0 +1,20 @@
#!lua name=charlie_bloom
local function set_bit(keys, args)
for _, offset in ipairs(args) do
redis.call("setbit", keys[1], offset, 1)
end
end
local function test_bit(keys, args)
for _, offset in ipairs(args) do
if tonumber(redis.call("getbit", keys[1], offset)) == 0 then
return false
end
end
return true
end
redis.register_function('set_bit',set_bit)
redis.register_function('test_bit',test_bit)

View File

@ -1,6 +1,7 @@
package bloom_test package bloom_test
import ( import (
"context"
"fmt" "fmt"
"math" "math"
"strconv" "strconv"
@ -8,8 +9,8 @@ import (
"github.com/charlienet/go-mixed/bloom" "github.com/charlienet/go-mixed/bloom"
"github.com/charlienet/go-mixed/rand" "github.com/charlienet/go-mixed/rand"
"github.com/charlienet/go-mixed/redis"
"github.com/charlienet/go-mixed/sys" "github.com/charlienet/go-mixed/sys"
"github.com/go-redis/redis/v8"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -19,13 +20,13 @@ func TestBloom(t *testing.T) {
b := bloom.New(1000, 0.03) b := bloom.New(1000, 0.03)
for i := 0; i < 1000000; i++ { for i := 0; i < 1000000; i++ {
b.Add(strconv.Itoa(i)) b.Add(context.Background(), strconv.Itoa(i))
} }
v := "6943553521463296-1635402930" v := "6943553521463296-1635402930"
t.Log(b.ExistString(v)) t.Log(b.ExistString(v))
b.Add(v) b.Add(context.Background(), v)
t.Log(b.ExistString(v)) t.Log(b.ExistString(v))
isSet, err := b.ExistString(strconv.Itoa(9999)) isSet, err := b.ExistString(strconv.Itoa(9999))
@ -49,15 +50,16 @@ func TestOptimize(t *testing.T) {
} }
func TestRedis(t *testing.T) { func TestRedis(t *testing.T) {
client := redis.NewClient(&redis.Options{
Addr: "192.168.2.222:6379", client := redis.New(&redis.RedisOption{
Addrs: []string{"192.168.2.222:6379"},
Password: "123456", Password: "123456",
}) })
bf := bloom.New(10000, 0.03, bloom.WithRedis(client, "bloom:test")) bf := bloom.New(10000, 0.03, bloom.WithRedis(client, "bloom:test"))
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
bf.Add(strconv.Itoa(i)) bf.Add(context.Background(), strconv.Itoa(i))
} }
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
@ -81,7 +83,7 @@ func TestClear(t *testing.T) {
bf := bloom.New(1000, 0.03) bf := bloom.New(1000, 0.03)
v := "abc" v := "abc"
bf.Add(v) bf.Add(context.Background(), v)
isSet, _ := bf.ExistString(v) isSet, _ := bf.ExistString(v)
assert.True(t, isSet) assert.True(t, isSet)
@ -96,7 +98,7 @@ func TestParallel(t *testing.T) {
for i := 0; i < 10000; i++ { for i := 0; i < 10000; i++ {
v := rand.Hex.Generate(10) v := rand.Hex.Generate(10)
f.Add(v) f.Add(context.Background(), v)
isSet, _ := f.ExistString(v) isSet, _ := f.ExistString(v)
assert.True(t, isSet) assert.True(t, isSet)
@ -108,8 +110,9 @@ func BenchmarkFilter(b *testing.B) {
b.RunParallel(func(p *testing.PB) { b.RunParallel(func(p *testing.PB) {
for p.Next() { for p.Next() {
v := rand.Hex.Generate(10) v := rand.Hex.Generate(10)
f.Add(v) f.Add(context.Background(), v)
f.ExistString(v) f.ExistString(v)

View File

@ -1,21 +1,22 @@
package bloom package bloom
import ( import (
"context"
"sync"
"github.com/bits-and-blooms/bitset" "github.com/bits-and-blooms/bitset"
"github.com/charlienet/go-mixed/locker"
) )
type memStore struct { type memStore struct {
size uint size uint
set *bitset.BitSet // 内存位图 set *bitset.BitSet // 内存位图
lock locker.RWLocker // 同步锁 lock sync.RWMutex // 同步锁
} }
func newMemStore(size uint) *memStore { func newMemStore(size uint) *memStore {
return &memStore{ return &memStore{
size: size, size: size,
set: bitset.New(size), set: bitset.New(size),
lock: locker.NewRWLocker(),
} }
} }
@ -26,7 +27,7 @@ func (s *memStore) Clear() {
s.set.ClearAll() s.set.ClearAll()
} }
func (s *memStore) Set(offsets ...uint) error { func (s *memStore) Set(ctx context.Context, offsets ...uint) error {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()

View File

@ -4,33 +4,18 @@ import (
"context" "context"
"errors" "errors"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/go-redis/redis/v8" _ "embed"
"github.com/charlienet/go-mixed/redis"
) )
const ( //go:embed bloom.lua
// ARGV:偏移量offset数组 var redis_bloom_function string
// KYES[1]: setbit操作的key
// 全部设置为1
setScript = `
for _, offset in ipairs(ARGV) do
redis.call("setbit", KEYS[1], offset, 1)
end
`
//ARGV:偏移量offset数组 var once sync.Once
//KYES[1]: setbit操作的key
//检查是否全部为1
testScript = `
for _, offset in ipairs(ARGV) do
if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then
return false
end
end
return true
`
)
var ErrTooLargeOffset = errors.New("超出最大偏移量") var ErrTooLargeOffset = errors.New("超出最大偏移量")
@ -38,12 +23,14 @@ var _ bitStore = &redisBitSet{}
// 使用Redis存储位图 // 使用Redis存储位图
type redisBitSet struct { type redisBitSet struct {
store *redis.Client store redis.Client
key string key string
bits uint bits uint
} }
func newRedisStore(store *redis.Client, key string, bits uint) *redisBitSet { func newRedisStore(store redis.Client, key string, bits uint) *redisBitSet {
once.Do(func() { store.LoadFunction(redis_bloom_function) })
return &redisBitSet{ return &redisBitSet{
store: store, store: store,
key: key, key: key,
@ -51,16 +38,13 @@ func newRedisStore(store *redis.Client, key string, bits uint) *redisBitSet {
} }
} }
func (s *redisBitSet) Set(offsets ...uint) error { func (s *redisBitSet) Set(ctx context.Context, offsets ...uint) error {
args, err := s.buildOffsetArgs(offsets) args, err := s.buildOffsetArgs(offsets)
if err != nil { if err != nil {
return err return err
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) _, err = s.store.FCall(ctx, "set_bit", []string{s.key}, args...).Result()
defer cancel()
_, err = s.store.Eval(ctx, setScript, []string{s.key}, args).Result()
//底层使用的是go-redis,redis.Nil表示操作的key不存在 //底层使用的是go-redis,redis.Nil表示操作的key不存在
//需要针对key不存在的情况特殊判断 //需要针对key不存在的情况特殊判断
@ -82,7 +66,7 @@ func (s *redisBitSet) Test(offsets ...uint) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
defer cancel() defer cancel()
resp, err := s.store.Eval(ctx, testScript, []string{s.key}, args).Result() resp, err := s.store.FCall(ctx, "test_bit", []string{s.key}, args...).Result()
// key 不存在,表示还未存放任何数据 // key 不存在,表示还未存放任何数据
if err == redis.Nil { if err == redis.Nil {
@ -103,8 +87,8 @@ func (s *redisBitSet) Clear() {
} }
func (r *redisBitSet) buildOffsetArgs(offsets []uint) ([]string, error) { func (r *redisBitSet) buildOffsetArgs(offsets []uint) ([]any, error) {
args := make([]string, 0, len(offsets)) args := make([]any, 0, len(offsets))
for _, offset := range offsets { for _, offset := range offsets {
if offset >= r.bits { if offset >= r.bits {
return nil, ErrTooLargeOffset return nil, ErrTooLargeOffset

View File

@ -1,19 +1,22 @@
package bloom package bloom
import ( import (
"context"
"testing" "testing"
"time"
"github.com/go-redis/redis/v8" "github.com/charlienet/go-mixed/redis"
"github.com/charlienet/go-mixed/tests"
) )
func TestRedisStore(t *testing.T) { func TestRedisStore(t *testing.T) {
client := redis.NewClient(&redis.Options{ tests.RunOnDefaultRedis(t, func(client redis.Client) {
Addr: "192.168.2.222:6379",
Password: "123456",
})
store := newRedisStore(client, "abcdef", 10000) store := newRedisStore(client, "abcdef", 10000)
err := store.Set(1, 2, 3, 9, 1223)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
err := store.Set(ctx, 1, 2, 3, 9, 1223)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -21,4 +24,5 @@ func TestRedisStore(t *testing.T) {
t.Log(store.Test(1)) t.Log(store.Test(1))
t.Log(store.Test(1, 2, 3)) t.Log(store.Test(1, 2, 3))
t.Log(store.Test(4, 5, 8)) t.Log(store.Test(4, 5, 8))
})
} }

View File

@ -9,6 +9,14 @@ const hexTable = "0123456789ABCDEF"
type BytesResult []byte type BytesResult []byte
func FromString(s string) BytesResult {
return (BytesResult)([]byte(s))
}
func FromBytes(b []byte) BytesResult {
return BytesResult(b)
}
// FromHexString 从十六进制获取 // FromHexString 从十六进制获取
func FromHexString(s string) (BytesResult, error) { func FromHexString(s string) (BytesResult, error) {
b, err := hex.DecodeString(s) b, err := hex.DecodeString(s)

View File

@ -5,6 +5,8 @@ import (
"github.com/charlienet/go-mixed/bytesconv" "github.com/charlienet/go-mixed/bytesconv"
"github.com/charlienet/go-mixed/rand" "github.com/charlienet/go-mixed/rand"
"golang.org/x/net/html/charset"
"golang.org/x/text/encoding/unicode"
) )
func TestHexUppercase(t *testing.T) { func TestHexUppercase(t *testing.T) {
@ -16,3 +18,20 @@ func TestHexUppercase(t *testing.T) {
u := bytesconv.BytesResult(b).UppercaseHex() u := bytesconv.BytesResult(b).UppercaseHex()
t.Log(u) t.Log(u)
} }
func TestHexToBase64(t *testing.T) {
v := "abc"
unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM)
t.Log(bytesconv.FromString(v).Base64())
b, _ := rand.RandBytes(43)
t.Log(bytesconv.FromBytes(b).Base64())
c, n := charset.Lookup("utf8")
t.Log(c, n)
}

13
bytesconv/readme.md Normal file
View File

@ -0,0 +1,13 @@
功能列表
1. 无内存消耗的字符串转字节数组,字节数组转字符串
StringToBytes、BytesToString
2. 字节数组转换函数
ByteResult
From
To
3. 对象的二进制序列化和反序列化
Encode、Decode

View File

@ -1,8 +1,10 @@
package bytesconv package bytesconv
import ( import (
"encoding/hex"
"encoding/json" "encoding/json"
"testing" "testing"
"time"
) )
type SimpleUser struct { type SimpleUser struct {
@ -25,3 +27,18 @@ func TestGob(t *testing.T) {
t.Logf("%+v", u2) t.Logf("%+v", u2)
} }
type delayTask struct {
message string
delay time.Time
execute func()
}
func TestMarshal(t *testing.T) {
d := delayTask{
message: "sssssssss",
}
b, err := Encode(d)
t.Log(hex.EncodeToString(b), err)
}

View File

@ -7,25 +7,20 @@ import (
"github.com/charlienet/go-mixed/cache/bigcache" "github.com/charlienet/go-mixed/cache/bigcache"
"github.com/charlienet/go-mixed/cache/freecache" "github.com/charlienet/go-mixed/cache/freecache"
"github.com/charlienet/go-mixed/logx" "github.com/charlienet/go-mixed/logx"
"github.com/charlienet/go-mixed/redis"
) )
const defaultPrefix = "cache" const defaultPrefix = "cache"
type option func(*Cache) error type option func(*Cache) error
type options struct { type options struct {
Prefix string Prefix string
} }
func WithRedis(opts RedisConfig) option { func WithRedis(rdb redis.Client) option {
return func(c *Cache) error { return func(c *Cache) error {
if len(opts.Prefix) == 0 { rds := NewRedis(rdb)
opts.Prefix = defaultPrefix
}
rds := NewRedis(opts)
c.rds = rds c.rds = rds
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)

12
cache/cache_test.go vendored
View File

@ -6,6 +6,8 @@ import (
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
"github.com/charlienet/go-mixed/redis"
) )
var ( var (
@ -60,7 +62,9 @@ func TestMemCache(t *testing.T) {
} }
func TestDistributedCache(t *testing.T) { func TestDistributedCache(t *testing.T) {
c := NewRedis(RedisConfig{Addrs: []string{"192.168.2.222:6379"}, DB: 6, Password: "123456", Prefix: "abcdef"}) c := NewRedis(redis.New(&redis.ReidsOption{
Addrs: []string{"192.168.2.222:6379"}, DB: 6, Password: "123456", Prefix: "abcdef",
}))
ctx := context.Background() ctx := context.Background()
if err := c.Ping(ctx); err != nil { if err := c.Ping(ctx); err != nil {
@ -134,7 +138,11 @@ func load() (any, error) {
func buildCache() *Cache { func buildCache() *Cache {
c, err := New( c, err := New(
WithFreeCache(10*1024*1024), WithFreeCache(10*1024*1024),
WithRedis(RedisConfig{Addrs: []string{"192.168.2.222:6379"}, DB: 6, Password: "123456"})) WithRedis(redis.New(&redis.ReidsOption{
Addrs: []string{"192.168.2.222:6379"},
DB: 6,
Password: "123456",
})))
if err != nil { if err != nil {
panic(err) panic(err)

5
cache/readme.md vendored
View File

@ -1,4 +1,7 @@
# 级缓存模块 # 级缓存模块
提供本地缓存和分布式缓存组合的缓存模块,可以只使用本地缓存或分布式缓存。并可全局禁用缓存。
1. 一级缓存可使用freecache或bigcache作为本地缓存当数据在本地缓存不存在时会向二级缓存请求数据 1. 一级缓存可使用freecache或bigcache作为本地缓存当数据在本地缓存不存在时会向二级缓存请求数据
2. 二级缓存使用redis作为缓存模块当数据在二级缓存不存在时向资源请求数据。 2. 二级缓存使用redis作为缓存模块当数据在二级缓存不存在时向资源请求数据。

35
cache/redis.go vendored
View File

@ -9,48 +9,21 @@ import (
"github.com/charlienet/go-mixed/bytesconv" "github.com/charlienet/go-mixed/bytesconv"
"github.com/charlienet/go-mixed/json" "github.com/charlienet/go-mixed/json"
"github.com/charlienet/go-mixed/rand" "github.com/charlienet/go-mixed/rand"
"github.com/go-redis/redis/v8" "github.com/charlienet/go-mixed/redis"
) )
const redisEmptyObject = "redis object not exist" const redisEmptyObject = "redis object not exist"
type RedisConfig struct {
Prefix string // key perfix
Addrs []string
// Database to be selected after connecting to the server.
// Only single-node and failover clients.
DB int
Username string
Password string
MaxRetries int
MinRetryBackoff time.Duration
MaxRetryBackoff time.Duration
DialTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
}
type redisClient struct { type redisClient struct {
client redis.UniversalClient client redis.Client
emptyStamp string // 空对象标识,每个实例隔离 emptyStamp string // 空对象标识,每个实例隔离
prefix string // 缓存键前缀 prefix string // 缓存键前缀
} }
func NewRedis(c RedisConfig) *redisClient { func NewRedis(c redis.Client) *redisClient {
client := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: c.Addrs,
DB: c.DB,
Username: c.Username,
Password: c.Password,
})
return &redisClient{ return &redisClient{
emptyStamp: fmt.Sprintf("redis-empty-%d-%s", time.Now().Unix(), rand.Hex.Generate(6)), emptyStamp: fmt.Sprintf("redis-empty-%d-%s", time.Now().Unix(), rand.Hex.Generate(6)),
prefix: c.Prefix, client: c,
client: client,
} }
} }

View File

@ -53,6 +53,30 @@ const (
ShortTimeNanoLayout = "150405.999999999" ShortTimeNanoLayout = "150405.999999999"
) )
func String(t time.Time) string {
return ToDateTimeString(t)
}
func ToDateTimeString(t time.Time) string {
return Create(t).ToDateTimeString()
}
func Format(t time.Time, layout string) string {
return Create(t).Format(layout)
}
func ToDateTimeInt(t time.Time) int {
return Create(t).ToDateTimeInt()
}
func ToShortDateInt(t time.Time) int {
return Create(t).ToShortDateInt()
}
func ToMonthInt(t time.Time) int {
return Create(t).ToMonthInt()
}
func (c Calendar) String() string { func (c Calendar) String() string {
return c.ToDateTimeString() return c.ToDateTimeString()
} }

View File

@ -10,12 +10,16 @@ import (
var s = "^aaa^" var s = "^aaa^"
func TestPutGet(t *testing.T) { func TestPutGet(t *testing.T) {
b := NewCompiledBuffer(func(s string) (*regexp.Regexp, error) { return regexp.Compile(s) }) b := NewCompiledBuffer(func(s string) (*regexp.Regexp, error) {
t.Log("init")
return regexp.Compile(s)
})
t.Log(b.Put(s)) t.Log(b.Put(s))
r, _ := b.Get(s) r, _ := b.Get(s)
t.Log(r.Match([]byte("abc"))) t.Log(r.Match([]byte("abc")))
t.Log(r.Match([]byte("abc")))
} }
func BenchmarkGet(b *testing.B) { func BenchmarkGet(b *testing.B) {
@ -28,7 +32,7 @@ func BenchmarkGet(b *testing.B) {
} }
}) })
b.Run("buf", func(b *testing.B) { b.Run("buf-nocom", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
regexp.Compile(s) regexp.Compile(s)
} }

View File

@ -2,7 +2,6 @@ package compress
import ( import (
"archive/zip" "archive/zip"
"io/ioutil"
"os" "os"
) )
@ -49,7 +48,7 @@ func (z *zipPackage) Write(out *os.File) error {
return err return err
} }
in, err := ioutil.ReadFile(f.filename) in, err := os.ReadFile(f.filename)
if err != nil { if err != nil {
return err return err
} }

View File

@ -0,0 +1,75 @@
package delayqueue
import (
"context"
"time"
"github.com/charlienet/go-mixed/locker"
)
type store[T Delayed] interface {
Push(context.Context, T) error
Pop() (T, error)
Peek() (T, bool)
IsEmpty() bool // 队列是否为空
}
type delayQueue[T Delayed] struct {
mu locker.RWLocker
store store[T]
}
type Delayed interface {
Delay() time.Time
}
func New[T Delayed]() *delayQueue[T] {
return &delayQueue[T]{
mu: locker.NewRWLocker(),
store: newMemStore[T](),
}
}
func (q *delayQueue[T]) UseStore(s store[T]) *delayQueue[T] {
q.store = s
return q
}
func (q *delayQueue[T]) Push(task T) error {
q.mu.Lock()
defer q.mu.Unlock()
return q.store.Push(context.Background(), task)
}
func (q *delayQueue[T]) Peek() (T, bool) {
q.mu.RLock()
defer q.mu.RUnlock()
return q.store.Peek()
}
func (q *delayQueue[T]) Pop() (T, error) {
q.mu.Lock()
defer q.mu.Unlock()
return q.store.Pop()
}
func (q *delayQueue[T]) Channel(size int) <-chan T {
out := make(chan T, size)
go func() {
for {
entry, _ := q.Pop()
out <- entry
}
}()
return out
}
func (q *delayQueue[T]) IsEmpty() bool {
q.mu.RLock()
defer q.mu.RUnlock()
return q.store.IsEmpty()
}

View File

@ -0,0 +1,48 @@
package delayqueue_test
import (
"testing"
"time"
delayqueue "github.com/charlienet/go-mixed/concurrent/delay_queue"
)
type delayTask struct {
message string
delay time.Time
}
func (t delayTask) Delay() time.Time {
return t.delay
}
func TestDelayQueue(t *testing.T) {
queue := delayqueue.New[delayTask]()
queue.Push(delayTask{})
}
func TestDelayedFunc(t *testing.T) {
q := delayqueue.New[delayTask]()
q.Push(delayTask{})
}
func TestDelayedChannel(t *testing.T) {
q := delayqueue.New[delayTask]()
c := q.Channel(10)
q.Push(delayTask{message: "abc", delay: time.Now().Add(time.Second)})
q.Push(delayTask{message: "abcaaa", delay: time.Now().Add(time.Second * 3)})
for {
if q.IsEmpty() {
t.Log("队列为空,退出")
break
}
select {
case task := <-c:
t.Log(task)
case <-time.After(time.Second * 2):
}
}
}

View File

@ -0,0 +1,37 @@
package delayqueue
import (
"context"
"github.com/charlienet/go-mixed/errors"
)
type kafkaStore[T Delayed] struct {
}
func (s *delayQueue[T]) UseKafka() *delayQueue[T] {
s.UseStore(newKafka[T]())
panic(errors.NotImplemented)
// return s.UseStore(newKafka[T]())
}
func newKafka[T Delayed]() *kafkaStore[T] {
return &kafkaStore[T]{}
}
func (*kafkaStore[T]) Push(context.Context, T) error {
return nil
}
func (*kafkaStore[T]) Pop() (T, error) {
return *new(T), nil
}
func (*kafkaStore[T]) Peek() (T, bool) {
return *new(T), false
}
func (*kafkaStore[T]) IsEmpty() bool {
return false
}

View File

@ -0,0 +1,88 @@
package delayqueue
import (
"container/heap"
"context"
"sync"
"time"
)
type delayedQueue []Delayed
type memStore[T Delayed] struct {
mu sync.Mutex
h *delayedQueue
wakeup chan struct{}
}
func (q delayedQueue) Len() int {
return len(q)
}
func (q delayedQueue) Less(i, j int) bool {
return q[i].Delay().Before(q[j].Delay())
}
func (q delayedQueue) Swap(i, j int) {
q[i], q[j] = q[j], q[i]
}
func (q *delayedQueue) Push(x any) {
*q = append(*q, x.(Delayed))
}
func (h *delayedQueue) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
func newMemStore[T Delayed]() *memStore[T] {
store := &memStore[T]{
h: new(delayedQueue),
wakeup: make(chan struct{}, 1),
}
heap.Init(store.h)
return store
}
func (s *memStore[T]) Push(ctx context.Context, v T) error {
s.mu.Lock()
defer s.mu.Unlock()
heap.Push(s.h, v)
return nil
}
func (s *memStore[T]) Pop() (T, error) {
for {
_, exist := s.Peek()
if exist {
return s.h.Pop().(T), nil
}
time.Sleep(time.Millisecond * 10)
}
}
func (s *memStore[T]) Peek() (T, bool) {
s.mu.Lock()
defer s.mu.Unlock()
if s.h.Len() > 0 {
return (*s.h)[0].(T), true
}
return *new(T), false
}
func (s *memStore[T]) Len() int {
return s.h.Len()
}
func (s *memStore[T]) IsEmpty() bool {
return s.Len() == 0
}

View File

@ -0,0 +1,119 @@
package delayqueue
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/charlienet/go-mixed/calendar"
"github.com/charlienet/go-mixed/rand"
)
type delayTask struct {
Message string
At time.Time
}
func (t delayTask) Delay() time.Time {
return t.At
}
func (t delayTask) execute() {
println(t.Message)
}
// var _ encoding.BinaryMarshaler = new(myStruct)
// var _ encoding.BinaryUnmarshaler = new(myStruct)
func (t delayTask) BinaryUnmarshaler(data []byte, v any) {
json.Unmarshal(data, v)
}
func (t delayTask) MarshalBinary() (data []byte, err error) {
return json.Marshal(t)
}
func TestMemStore(t *testing.T) {
s := newMemStore[delayTask]()
for i := 0; i < 10; i++ {
s.Push(
context.Background(),
delayTask{
Message: "tesss",
At: time.Now().Add(-time.Minute * time.Duration(rand.Intn(20))),
})
}
t.Log("count:", s.Len())
v, exists := s.Peek()
t.Logf("Peek %v:%v %v", exists, v.Message, calendar.Create(v.Delay()).ToDateTimeString())
for i := 0; i < 10; i++ {
v, _ := s.Pop()
t.Logf("POP:%v %v", v.Message, calendar.Create(v.Delay()).ToDateTimeString())
}
v, exists = s.Peek()
t.Logf("Peek %v:%v %v", exists, v.Message, calendar.Create(v.At).ToDateTimeString())
}
func TestMemPush(t *testing.T) {
s := newMemStore[delayTask]()
for i := 0; i < 10; i++ {
s.Push(
context.Background(),
delayTask{
Message: fmt.Sprintf("abc:%d", i),
At: time.Now().Add(time.Second * time.Duration(rand.IntRange(5, 30))),
})
}
now := time.Now()
delay, _ := s.Pop()
after := delay.Delay().Sub(now)
t.Log("after:", calendar.String(now), calendar.String(delay.Delay()), after)
}
func TestExecute(t *testing.T) {
s := newMemStore[delayTask]()
s.Push(context.Background(),
delayTask{
Message: "这是消息",
At: time.Now().Add(time.Second * 2),
})
s.Push(context.Background(),
delayTask{
Message: "这是消息",
At: time.Now().Add(time.Second * 4),
})
t.Log("start:", calendar.String(time.Now()))
for {
if s.IsEmpty() {
break
}
task, _ := s.Pop()
for {
if task.Delay().Before(time.Now()) {
task.execute()
t.Log("end:", calendar.String(time.Now()))
break
}
time.Sleep(time.Millisecond * 20)
}
}
}

View File

@ -0,0 +1,154 @@
package delayqueue
import (
"context"
"encoding"
"encoding/json"
"strconv"
"time"
goredis "github.com/redis/go-redis/v9"
"github.com/charlienet/go-mixed/hash"
"github.com/charlienet/go-mixed/redis"
)
// 使用Redis存储队列
type redisStore[T Delayed] struct {
rdb redis.Client
delayQueue string
executeQueue string
delayTaskSet string
}
func (q *delayQueue[T]) UseRedis(delayQueueName, executeQueueName, delayTaskName string, rdb redis.Client) *delayQueue[T] {
q.store = newRedisStroe[T](delayQueueName, executeQueueName, delayTaskName, rdb)
return q
}
func newRedisStroe[T Delayed](delayQueueName, executeQueueName, delayTaskName string, rdb redis.Client) *redisStore[T] {
store := &redisStore[T]{
delayQueue: delayQueueName,
executeQueue: executeQueueName,
delayTaskSet: delayTaskName,
rdb: rdb,
}
go func() {
for {
store.pushToExecute()
time.Sleep(time.Millisecond * 100)
}
}()
return store
}
func (s *redisStore[T]) Push(ctx context.Context, v T) error {
o := any(v).(encoding.BinaryMarshaler)
bytes, err := o.MarshalBinary()
if err != nil {
return err
}
tx := s.rdb.TxPipeline()
tx.HSet(context.Background(), s.delayTaskSet, hash.Sha1(bytes).Hex(), bytes)
tx.Exec(context.Background())
tx.HSet(context.Background(), s.delayTaskSet)
// tx.Exec()
ret := s.rdb.ZAdd(ctx, s.delayQueue, goredis.Z{
Score: float64(v.Delay().Unix()),
Member: v,
})
return ret.Err()
}
func (s *redisStore[T]) pushToExecute() error {
now := time.Now().Unix()
ret, err := s.rdb.ZRangeByScore(
context.Background(),
s.delayQueue,
&goredis.ZRangeBy{
Min: "-inf",
Max: strconv.FormatInt(now, 10),
}).Result()
if err != nil {
return err
}
if len(ret) > 0 {
pipe := s.rdb.TxPipeline()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
pipe.LPush(ctx, s.executeQueue, ret)
pipe.ZRem(ctx, s.delayQueue, ret)
if _, err := pipe.Exec(ctx); err != nil {
return err
}
}
return nil
}
func (s *redisStore[T]) Pop() (T, error) {
for {
v, err := s.rdb.RPop(context.Background(), s.executeQueue).Result()
if err != nil {
if err == redis.Nil {
time.Sleep(time.Millisecond * 10)
continue
}
return *new(T), err
}
if len(v) > 0 {
var task T
if err := json.Unmarshal([]byte(v), &task); err != nil {
return *new(T), err
}
return task, nil
}
}
}
func (s *redisStore[T]) Peek() (t T, r bool) {
m, err := s.rdb.ZRange(context.Background(), s.delayQueue, 0, 0).Result()
if err != nil {
return *new(T), false
}
if len(m) == 1 {
var t T
s := m[0]
if err := json.Unmarshal([]byte(s), &t); err != nil {
return *new(T), false
}
return t, true
}
return *new(T), false
}
func (s *redisStore[T]) Clear() {
s.rdb.Del(context.Background(), s.delayQueue)
s.rdb.Del(context.Background(), s.executeQueue)
}
func (s *redisStore[T]) IsEmpty() bool {
n, _ := s.rdb.LLen(context.Background(), s.executeQueue).Result()
m, _ := s.rdb.ZCard(context.Background(), s.delayQueue).Result()
return (m + n) == 0
}

View File

@ -0,0 +1,89 @@
package delayqueue
import (
"context"
"fmt"
"testing"
"time"
"github.com/charlienet/go-mixed/redis"
"github.com/charlienet/go-mixed/tests"
"github.com/stretchr/testify/assert"
)
const (
redisAddr = "192.168.123.100:6379"
delay_queue = "delay_queue"
execute_queue = "execute_queue"
delay_task_set = "task_set"
)
func TestRedis(t *testing.T) {
tests.RunOnRedis(t, func(client redis.Client) {
defer client.Close()
q := New[delayTask]().UseRedis(delay_queue, execute_queue, delay_task_set, client)
err := q.Push(delayTask{
Message: "abc1111111111111",
At: time.Now().Add(time.Second * 2)})
if err != nil {
t.Fatal(err)
}
t.Log(time.Now())
task, _ := q.Pop()
t.Logf("%+v", task)
t.Log(time.Now())
task.execute()
}, redis.RedisOption{Addr: redisAddr, Prefix: "redis_test"})
}
func TestMutiTask(t *testing.T) {
tests.RunOnRedis(t, func(client redis.Client) {
defer client.Close()
timer := time.NewTimer(time.Second)
ticker := time.NewTicker(time.Second)
timer.Reset(time.Microsecond)
ticker.Reset(time.Millisecond)
store := newRedisStroe[delayTask](delay_queue, execute_queue, delay_task_set, client)
for i := 1; i <= 5; i++ {
store.Push(context.Background(), delayTask{
Message: fmt.Sprintf("abc:%d", i),
At: time.Now().Add(time.Second * time.Duration(i)),
})
}
for !store.IsEmpty() {
v, err := store.Pop()
assert.Nil(t, err)
t.Log(time.Now(), v)
}
})
}
func TestIsEmpty(t *testing.T) {
tests.RunOnRedis(t, func(client redis.Client) {
defer client.Close()
store := newRedisStroe[delayTask](delay_queue, execute_queue, delay_task_set, client)
store.Clear()
assert.True(t, store.IsEmpty())
store.Push(context.Background(), delayTask{Message: "bbb", At: time.Now().Add(time.Second)})
assert.False(t, store.IsEmpty())
}, redis.RedisOption{
Addrs: []string{"redis-10448.c90.us-east-1-3.ec2.cloud.redislabs.com:10448"},
Password: "E7HFwvENEqimiB1EG4IjJSa2IUi0B22o",
})
}

20
concurrent/readme.md Normal file
View File

@ -0,0 +1,20 @@
延迟队列
包含以下实现模型
1. 内存模式。
2. Redis模式
3. MQ模式
内存模式,在队列中放入。定时检查过期时间,过期时间到达时取出并使用协程执行。
Redis模式使用ZSET存储任务队列。定时取出规则内的任务。取出后使用ZREM删除并放入执行队列LPUSH。放入成功后在任务执行通道发送消息执行通道使用LPOP取出并执行。
```
queue := delayqueue.New()
```

7
configure/config.toml Normal file
View File

@ -0,0 +1,7 @@
Mode = "dev"
[Nacos]
Address = "192.168.2.121"
Port = 8848
Namespace = "8560b58d-87d0-4b85-8ac5-f2d308c6669e"
Group = "dev"

46
configure/configure.go Normal file
View File

@ -0,0 +1,46 @@
package configure
import (
"github.com/charlienet/go-mixed/expr"
"github.com/spf13/viper"
)
type NotifyFunc func(Configure) error
type Configure interface {
Load(dataId string, v any, onChanged ...NotifyFunc) error
GetString(string, string) string
GetInt(key string, defaultValue int) int
}
type conf struct {
viper *viper.Viper //
nacos *nacos //
onChangeNotifies map[string][]NotifyFunc // 已经注册的配置变更通知
nacosOptions *NacosOptions //
useNacos bool //
}
func (c *conf) GetString(key string, defaultValue string) string {
if c.viper.IsSet(key) {
return c.viper.GetString(key)
}
return defaultValue
}
func (c *conf) GetInt(key string, defaultValue int) int {
return expr.Ternary(c.viper.IsSet(key), c.viper.GetInt(key), defaultValue)
}
func (c *conf) Load(dataId string, v any, onChanged ...NotifyFunc) error {
if err := c.nacos.Load(dataId, v); err != nil {
return err
}
if len(onChanged) > 0 {
c.onChangeNotifies[dataId] = onChanged
}
return nil
}

View File

@ -0,0 +1,100 @@
package configure
import (
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
func New() *conf { return &conf{viper: viper.New(), onChangeNotifies: make(map[string][]NotifyFunc)} }
func (c *conf) AddConfigPath(in ...string) *conf {
for _, v := range in {
c.viper.AddConfigPath(v)
}
return c
}
func (c *conf) SetConfigName(in string) *conf {
c.viper.SetConfigName(in)
return c
}
func (c *conf) SetConfigFile(f string) *conf {
c.viper.SetConfigFile(f)
return c
}
func (c *conf) SetDefault(key string, value any) *conf {
c.viper.SetDefault(key, value)
return c
}
func (c *conf) AutomaticEnv() *conf {
c.viper.AutomaticEnv()
return c
}
func (c *conf) Read() (*conf, error) {
// 从本地配置读取
if err := c.viper.ReadInConfig(); err != nil {
return nil, err
}
c.viper.WatchConfig()
c.viper.OnConfigChange(c.OnViperChanged)
// 初始化Nacos客户端
if err := c.createNacosClient(); err != nil {
return nil, err
}
return c, nil
}
func (c *conf) OnViperChanged(in fsnotify.Event) {
}
func (c *conf) createNacosClient() error {
opt := c.getNacosOptions()
if opt == nil {
return nil
}
nc, err := createNacosClient(opt.Address, opt.Port, opt.Namespace, opt.Group)
if err != nil {
return err
}
c.nacos = &nacos{client: nc, group: opt.Group, onChanged: c.onNacosChanged}
return nil
}
func (c *conf) onNacosChanged(dataId, data string) {
if fs, ok := c.onChangeNotifies[dataId]; ok {
for _, f := range fs {
if f != nil {
f(c)
}
}
}
}
func (c *conf) getNacosOptions() *NacosOptions {
if c.nacosOptions != nil {
return c.nacosOptions
}
if c.useNacos {
return &NacosOptions{
Address: c.GetString(AddressKey, "127.0.0.1"),
Port: c.GetInt(PortKey, 8848),
Namespace: c.GetString(Namespace, ""),
Group: c.GetString(Group, ""),
}
}
return nil
}

View File

@ -0,0 +1,42 @@
package configure_test
import (
"testing"
"github.com/charlienet/go-mixed/configure"
"github.com/charlienet/go-mixed/json"
"github.com/stretchr/testify/assert"
)
func TestLoadSpecifiedFile(t *testing.T) {
conf, err := configure.New().SetConfigFile("config.toml").Read()
t.Log(err)
assert.Equal(t, "192.168.2.121", conf.GetString("nacos.address", ""))
_ = conf
}
func TestNewConfigure(t *testing.T) {
}
func TestNacos(t *testing.T) {
conf, err := configure.
New().
AddConfigPath(".").
WithNacos().
Read()
assert.Nil(t, err)
t.Log(conf.GetString("nacos.address", ""))
type redis struct {
Addrs string
}
r := &redis{}
t.Log(conf.Load("redis", r))
t.Log(json.StructToJsonIndent(r))
}

96
configure/nacos.go Normal file
View File

@ -0,0 +1,96 @@
package configure
import (
"encoding/json"
"fmt"
"github.com/nacos-group/nacos-sdk-go/v2/clients"
"github.com/nacos-group/nacos-sdk-go/v2/clients/config_client"
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
)
const (
AddressKey = "Nacos.Address"
PortKey = "Nacos.Port"
Namespace = "Nacos.Namespace"
Group = "Nacos.Group"
)
type nacos struct {
client config_client.IConfigClient
onChanged func(string, string)
group string
}
type NacosOptions struct {
Address string
Port int
Namespace string
Group string
}
func (c *conf) WithNacosOptions(options *NacosOptions) *conf {
c.nacosOptions = options
return c
}
func (c *conf) WithNacos() *conf {
c.useNacos = true
return c
}
func (n *nacos) Load(dataId string, v any) error {
voParam := vo.ConfigParam{
DataId: dataId,
Group: n.group,
OnChange: n.onChange,
}
content, err := n.client.GetConfig(voParam)
if err != nil {
return err
}
if len(content) == 0 {
return fmt.Errorf("parameters not configured:%s", dataId)
}
if err := json.Unmarshal([]byte(content), v); err != nil {
return err
}
n.client.ListenConfig(voParam)
return nil
}
func (n *nacos) onChange(namespace, group, dataId, data string) {
n.onChanged(dataId, data)
}
func createNacosClient(addr string, port int, namespace, group string) (config_client.IConfigClient, error) {
sc := []constant.ServerConfig{{
IpAddr: addr,
Port: uint64(port),
}}
cc := constant.ClientConfig{
NamespaceId: namespace,
TimeoutMs: 5000,
LogDir: "logs",
CacheDir: "cache",
LogLevel: "info",
NotLoadCacheAtStart: true,
}
configClient, err := clients.CreateConfigClient(map[string]any{
"serverConfigs": sc,
"clientConfig": cc,
})
if err != nil {
return nil, err
}
return configClient, nil
}

View File

@ -0,0 +1 @@
package locker

View File

@ -0,0 +1,166 @@
package locker
import (
"context"
"errors"
"strings"
"sync"
"time"
"github.com/charlienet/go-mixed/rand"
"github.com/charlienet/go-mixed/redis"
goredis "github.com/redis/go-redis/v9"
)
const (
// 加锁(可重入)
lockCmd = `if redis.call("GET", KEYS[1]) == ARGV[1] then
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
return "OK"
else
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end`
// 解锁
delCmd = `if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return '0'
end`
// 延期
incrCmd = `
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('expire', KEYS[1], ARGV[2])
else
return '0'
end`
)
const (
defaultExpire = time.Second * 10
retryInterval = time.Millisecond * 10
)
var (
once sync.Once
ErrContextCancel = errors.New("context cancel")
)
type distributedlock struct {
clients []redis.Client // redis 客户端
ctx context.Context //
key string // 资源键
rand string // 随机值
unlocked bool // 是否已解锁
expire time.Duration // 过期时间
}
func NewDistributedLocker(ctx context.Context, key string, clients ...redis.Client) *distributedlock {
expire := defaultExpire
if deadline, ok := ctx.Deadline(); ok {
expire = time.Until(deadline)
}
locker := &distributedlock{
ctx: ctx,
clients: clients,
key: key,
rand: rand.Hex.Generate(24),
expire: expire,
}
return locker
}
func (locker *distributedlock) Lock() error {
for {
select {
case <-locker.ctx.Done():
return ErrContextCancel
default:
if locker.TryLock() {
return nil
}
}
time.Sleep(retryInterval)
}
}
func (locker *distributedlock) TryLock() bool {
results := locker.Eval(locker.ctx, lockCmd, []string{locker.key}, locker.rand, locker.expire.Milliseconds())
if !isSuccess(results) {
locker.Unlock()
return false
}
locker.expandLockTime()
return true
}
func (locker *distributedlock) Unlock() {
locker.Eval(locker.ctx, delCmd, []string{locker.key}, locker.rand)
locker.unlocked = true
}
func (l *distributedlock) expandLockTime() {
once.Do(func() {
go func() {
for {
time.Sleep(l.expire / 3)
if l.unlocked {
break
}
l.resetExpire()
}
}()
})
}
func (locker *distributedlock) resetExpire() {
locker.Eval(locker.ctx,
incrCmd,
[]string{locker.key},
locker.rand,
locker.expire.Seconds())
}
func (locker *distributedlock) Eval(ctx context.Context, cmd string, keys []string, args ...any) []*goredis.Cmd {
results := make([]*goredis.Cmd, 0, len(locker.clients))
var wg sync.WaitGroup
wg.Add(len(locker.clients))
for _, rdb := range locker.clients {
go func(rdb redis.Client) {
defer wg.Done()
results = append(results, rdb.Eval(ctx, cmd, keys, args...))
}(rdb)
}
wg.Wait()
return results
}
func isSuccess(results []*goredis.Cmd) bool {
successCount := 0
for _, ret := range results {
resp, err := ret.Result()
if err != nil || resp == nil {
return false
}
reply, ok := resp.(string)
if ok && strings.EqualFold(reply, "OK") {
successCount++
}
}
return successCount >= len(results)/2+1
}

View File

@ -0,0 +1,87 @@
package locker
import (
"context"
"log"
"sync"
"testing"
"time"
"github.com/charlienet/go-mixed/redis"
"github.com/charlienet/go-mixed/tests"
)
func TestDistributedLock(t *testing.T) {
tests.RunOnRedis(t, func(rdb redis.Client) {
lock := NewDistributedLocker(context.Background(), "lock_test", rdb)
lock.Lock()
lock.Unlock()
})
}
func TestConcurrence(t *testing.T) {
tests.RunOnRedis(t, func(rdb redis.Client) {
count := 5
var wg sync.WaitGroup
wg.Add(count)
for i := 0; i < count; i++ {
go func(i int) {
defer wg.Done()
locker := NewDistributedLocker(context.Background(), "lock_test", rdb)
for n := 0; n < 5; n++ {
locker.Lock()
t.Logf("协程%d获取到锁", i)
time.Sleep(time.Second)
t.Logf("协程%d释放锁", i)
locker.Unlock()
}
}(i)
}
wg.Wait()
log.Println("所有任务完成")
})
}
func TestTwoLocker(t *testing.T) {
tests.RunOnRedis(t, func(rdb redis.Client) {
l1 := NewDistributedLocker(context.Background(), "lock_test", rdb)
l2 := NewDistributedLocker(context.Background(), "lock_test", rdb)
go func() {
l1.Lock()
println("l1 获取锁")
}()
go func() {
l2.Lock()
println("l2 获取锁")
}()
time.Sleep(time.Second * 20)
l1.Unlock()
l2.Unlock()
})
}
func TestDistributediTryLock(t *testing.T) {
tests.RunOnRedis(t, func(client redis.Client) {
lock := NewDistributedLocker(context.Background(), "lock_test", client)
l := lock.TryLock()
t.Log("尝试加锁结果:", l)
time.Sleep(time.Second * 20)
lock.Unlock()
})
}
func TestLocker(t *testing.T) {
}

View File

@ -1,19 +0,0 @@
package encode
import "encoding/hex"
type BytesResult []byte
func FromString(s string) BytesResult {
return BytesResult([]byte(s))
}
func FromHexString(s string) BytesResult {
b, _ := hex.DecodeString(s)
return BytesResult(b)
}
func FromBytes(b []byte) BytesResult {
return BytesResult(b)
}

View File

@ -12,6 +12,8 @@ const (
defaultErrorCode = "999999" defaultErrorCode = "999999"
) )
var NotImplemented = errors.New("Not Implemented")
type Error interface { type Error interface {
Wraped() []error Wraped() []error
Code() string Code() string

53
go.mod
View File

@ -1,19 +1,36 @@
module github.com/charlienet/go-mixed module github.com/charlienet/go-mixed
go 1.21 go 1.21.1
require ( require (
github.com/bits-and-blooms/bitset v1.8.0 github.com/alicebob/miniredis/v2 v2.31.0
github.com/allegro/bigcache/v3 v3.1.0
github.com/alphadose/haxmap v1.3.0
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/bits-and-blooms/bitset v1.10.0
github.com/cespare/xxhash/v2 v2.2.0 github.com/cespare/xxhash/v2 v2.2.0
github.com/coocood/freecache v1.2.4
github.com/dlclark/regexp2 v1.10.0
github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/universal-translator v0.18.1
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f
github.com/pkg/errors v0.9.1
github.com/redis/go-redis/v9 v9.3.0
github.com/shopspring/decimal v1.3.1 github.com/shopspring/decimal v1.3.1
github.com/sirupsen/logrus v1.9.3
github.com/spaolacci/murmur3 v1.1.0 github.com/spaolacci/murmur3 v1.1.0
github.com/stretchr/testify v1.8.4
github.com/tjfoc/gmsm v1.4.1 github.com/tjfoc/gmsm v1.4.1
github.com/vmihailenco/go-tinylfu v0.2.2
github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/crypto v0.14.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
golang.org/x/net v0.17.0
golang.org/x/sync v0.4.0
golang.org/x/text v0.13.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
require github.com/alphadose/haxmap v1.3.0
require ( require (
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
@ -27,35 +44,9 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.1 // indirect
github.com/tebeka/strftime v0.1.5 // indirect github.com/tebeka/strftime v0.1.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/yuin/gopher-lua v1.1.0 // indirect github.com/yuin/gopher-lua v1.1.0 // indirect
golang.org/x/net v0.14.0 // indirect golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.12.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
require (
github.com/allegro/bigcache/v3 v3.1.0
github.com/coocood/freecache v1.2.3
github.com/vmihailenco/go-tinylfu v0.2.2
)
require (
github.com/alicebob/miniredis/v2 v2.30.5
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/dlclark/regexp2 v1.10.0
github.com/go-redis/redis/v8 v8.11.5
github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
github.com/vmihailenco/msgpack/v5 v5.3.5
golang.org/x/crypto v0.12.0
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
golang.org/x/sync v0.3.0
golang.org/x/sys v0.11.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)

119
go.sum
View File

@ -1,30 +1,23 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.23.1 h1:jR6wZggBxwWygeXcdNyguCOCIjPsZyNUNlAkTx2fu0U= github.com/alicebob/miniredis/v2 v2.31.0 h1:ObEFUNlJwoIiyjxdrYF0QIDE7qXcLc7D3WpSH4c22PU=
github.com/alicebob/miniredis/v2 v2.23.1/go.mod h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q= github.com/alicebob/miniredis/v2 v2.31.0/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
github.com/alicebob/miniredis/v2 v2.30.0 h1:uA3uhDbCxfO9+DI/DuGeAMr9qI+noVWwGPNTFuKID5M=
github.com/alicebob/miniredis/v2 v2.30.0/go.mod h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q=
github.com/alicebob/miniredis/v2 v2.30.5 h1:3r6kTHdKnuP4fkS8k2IrvSfxpxUTcW1SOL0wN7b7Dt0=
github.com/alicebob/miniredis/v2 v2.30.5/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg=
github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk= github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
github.com/alphadose/haxmap v1.2.0 h1:noGrAmCE+gNheZ4KpW+sYj9W5uMcO1UAjbAq9XBOAfM=
github.com/alphadose/haxmap v1.2.0/go.mod h1:rjHw1IAqbxm0S3U5tD16GoKsiAd8FWx5BJ2IYqXwgmM=
github.com/alphadose/haxmap v1.3.0 h1:C/2LboOnPCZP27GmmSXOcwx360st0P8N0fTJ3voefKc= github.com/alphadose/haxmap v1.3.0 h1:C/2LboOnPCZP27GmmSXOcwx360st0P8N0fTJ3voefKc=
github.com/alphadose/haxmap v1.3.0/go.mod h1:rjHw1IAqbxm0S3U5tD16GoKsiAd8FWx5BJ2IYqXwgmM= github.com/alphadose/haxmap v1.3.0/go.mod h1:rjHw1IAqbxm0S3U5tD16GoKsiAd8FWx5BJ2IYqXwgmM=
github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UMEoHck02Q9L0FP13b/xSbQ= github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UMEoHck02Q9L0FP13b/xSbQ=
github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA= github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA=
github.com/bits-and-blooms/bitset v1.4.0 h1:+YZ8ePm+He2pU3dZlIZiOeAKfrBkXi1lSrXJ/Xzgbu8= github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88=
github.com/bits-and-blooms/bitset v1.4.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD6ZIpQT8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@ -35,17 +28,13 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coocood/freecache v1.2.3 h1:lcBwpZrwBZRZyLk/8EMyQVXRiFl663cCuMOrjCALeto= github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
github.com/coocood/freecache v1.2.3/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk= github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -53,18 +42,12 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU=
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -82,8 +65,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4=
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=
github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg=
github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -99,37 +80,22 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tebeka/strftime v0.1.5 h1:1NQKN1NiQgkqd/2moD6ySP/5CoZQsKa1d3ZhJ44Jpmg= github.com/tebeka/strftime v0.1.5 h1:1NQKN1NiQgkqd/2moD6ySP/5CoZQsKa1d3ZhJ44Jpmg=
@ -138,30 +104,20 @@ github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI=
github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI=
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@ -171,41 +127,26 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -228,12 +169,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

72
idGenerator/buffer.go Normal file
View File

@ -0,0 +1,72 @@
package idgenerator
import (
"sync"
"time"
"github.com/charlienet/go-mixed/idGenerator/store"
)
type obtainFunc func() (*store.Segment, error)
type doubleBuffer struct {
current *store.Segment // 当前
backup *store.Segment // 备用
obtain obtainFunc // 数据段获取函数
inFill bool // 备用缓冲填充中
isReadly bool // 备用缓冲区填充完成
mu sync.Mutex
}
func newDoubleBuffer(obtainFunc obtainFunc) *doubleBuffer {
b := &doubleBuffer{obtain: obtainFunc}
b.current, _ = b.obtain()
return b
}
func (b *doubleBuffer) allot() (int64, bool) {
if !b.inFill && b.current.IsEnding() {
go b.full() // 填充备用缓冲
}
// 缓冲区耗尽时切换
if b.current.IsEmpty() {
// 检查备用缓冲是否已经填充完成,已完成时切换,否则等待
for !b.isReadly {
time.Sleep(time.Microsecond * 100)
}
b.switchBuf()
}
return b.current.Allot(), b.current.Reback()
}
func (b *doubleBuffer) full() {
b.mu.Lock()
defer b.mu.Unlock()
if !b.inFill {
var err error
b.inFill = true
b.backup, err = b.obtain()
if err != nil {
println("填充失败:", err.Error())
panic(err)
}
b.isReadly = true
}
}
func (b *doubleBuffer) switchBuf() {
b.mu.Lock()
defer b.mu.Unlock()
if b.isReadly {
b.current, b.backup = b.backup, b.current
b.inFill = false
b.isReadly = false
}
}

View File

@ -0,0 +1,51 @@
package idgenerator
import (
"context"
"sync"
"testing"
"time"
"github.com/charlienet/go-mixed/idGenerator/store"
"github.com/charlienet/go-mixed/redis"
"github.com/charlienet/go-mixed/tests"
)
func TestBufferAlloc(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
f := func() (*store.Segment, error) {
return store.NewRedisStore("sss", rdb).Assign(3, 99, 10)
}
b := newDoubleBuffer(f)
for i := 0; i < 80; i++ {
t.Log(b.allot())
}
})
}
func TestTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
select {
case <-ctx.Done():
println("协程退出", ctx.Err().Error())
case <-time.After(time.Second * 100):
println("协程超时")
}
}()
wg.Wait()
println("应用退出")
}

132
idGenerator/formater.go Normal file
View File

@ -0,0 +1,132 @@
package idgenerator
import (
"errors"
"math"
"strconv"
"time"
)
const (
YYYYMMDDHHmmss = "20060102150405"
YYYYMMDDHHmm = "200601021504"
YYYYMMDDHH = "2006010215"
YYYYMMDD = "20060102"
)
type Layout int
const (
Binary Layout = iota
Decimal
)
type formater interface {
MaxinalMachineCode() int64
MaximalSequence() int64
Format(machine, serial int64, reback bool) Id
}
type Id int64
func (i Id) String() string {
if i == 0 {
return ""
}
return strconv.FormatInt(int64(i), 10)
}
func (i Id) Int64() int64 {
return int64(i)
}
// 标识生成器
type generator struct {
maximalSequence int64 // 序列段的最大值
maxinalMachineCode int64 // 机器码的最大值
sequenceLength int64 // 序列段长度
machineCodeLength int64 // 机器码长度
lastTimestamp int64 // 最后回绕时间
getTimestampFunc func() int64 // 获取时间段的方法
}
func (g generator) MaxinalMachineCode() int64 {
return g.maxinalMachineCode
}
func (g generator) MaximalSequence() int64 {
return g.maximalSequence
}
func (g *generator) getTimestamp(reback bool) int64 {
newTimestamp := g.getTimestampFunc()
for reback && g.lastTimestamp == newTimestamp {
time.Sleep(time.Microsecond * 10)
newTimestamp = g.getTimestampFunc()
}
g.lastTimestamp = newTimestamp
return newTimestamp
}
type binaryFormater struct {
generator
}
func newBinaryFormatter(start int64, sequenceLength, machineCodeLength int64) (*binaryFormater, error) {
return &binaryFormater{
generator: generator{
maximalSequence: int64(-1 ^ (-1 << sequenceLength)),
maxinalMachineCode: int64(-1 ^ (-1 << machineCodeLength)),
sequenceLength: sequenceLength,
machineCodeLength: machineCodeLength,
getTimestampFunc: func() int64 { return time.Now().Unix() - start },
},
}, nil
}
func (f *binaryFormater) Format(machine, serial int64, reback bool) Id {
timestamp := f.getTimestamp(reback)
return Id(timestamp<<(f.sequenceLength+f.machineCodeLength) | machine<<f.sequenceLength | serial)
}
type decimalFormater struct {
generator
supportReback bool
}
const (
decimalMaxLength = 19
)
func newDecimalFormater(format string, serialLength, machineLength int) (*decimalFormater, error) {
if len(format)+serialLength+machineLength > decimalMaxLength {
return nil, errors.New("the data length is out of limit")
}
serialShift := int64(math.Pow10(serialLength))
machineShift := int64(math.Pow10(machineLength))
return &decimalFormater{
generator: generator{
sequenceLength: serialShift,
maximalSequence: serialShift - 1,
machineCodeLength: machineShift,
maxinalMachineCode: machineShift - 1,
getTimestampFunc: func() int64 {
now := time.Now()
v := now.Format(format)
r, _ := strconv.ParseInt(v, 10, 64)
return r
},
},
supportReback: len(format) == 14,
}, nil
}
func (f *decimalFormater) Format(machine, serial int64, reback bool) Id {
timestamp := f.getTimestamp(reback)
return Id(timestamp*f.sequenceLength*f.machineCodeLength + machine*f.sequenceLength + serial)
}

View File

@ -0,0 +1,34 @@
package idgenerator
import (
"testing"
)
func TestBinary(t *testing.T) {
f, _ := newBinaryFormatter(DefaultStartTimeStamp, 16, 12)
t.Log(f.maxinalMachineCode, f.maximalSequence)
}
func TestDecimal(t *testing.T) {
f, _ := newDecimalFormater(YYYYMMDDHHmmss, 4, 1)
t.Log(f.maxinalMachineCode, f.maxinalMachineCode)
t.Log(f.Format(22333, 9, false))
}
func TestDecimalMonth111(t *testing.T) {
f, _ := newDecimalFormater(YYYYMMDD, 4, 1)
t.Log(f.maxinalMachineCode, f.maxinalMachineCode)
t.Log(f.Format(233, 9, false))
}
func TestBinaryTimestamp(t *testing.T) {
f, _ := newBinaryFormatter(DefaultStartTimeStamp, 10, 4)
for i := 0; i < 100; i++ {
if i%7 == 0 {
t.Log(f.Format(int64(i), 0xF, true))
} else {
t.Log(f.Format(int64(i), 0xF, false))
}
}
}

124
idGenerator/generator.go Normal file
View File

@ -0,0 +1,124 @@
package idgenerator
import (
"time"
"github.com/charlienet/go-mixed/idGenerator/store"
"github.com/charlienet/go-mixed/mathx"
"github.com/charlienet/go-mixed/redis"
)
const (
defaultDoubleBufferStep int64 = 50
)
var DefaultStartTimeStamp = time.Date(2023, 1, 1, 0, 0, 0, 0, time.Local).Unix()
type opt func(*idGenerator) error
type Option struct {
TimeFormat string
SerialLength int
MachineLength int
}
type idGenerator struct {
store storage // 外部存储
formater formater // 格式化器
buffer *doubleBuffer // 序列缓冲
}
func WithMem(machineCode int64) opt {
return WithStore(store.NewMemStore(machineCode))
}
func WithRedis(key string, rdb redis.Client) opt {
return WithStore(store.NewRedisStore(key, rdb))
}
func WithStore(s storage) opt {
return func(ig *idGenerator) error {
ig.store = s
return nil
}
}
func WithDecimalFormater(format string, serialLength, machineLength int) opt {
return func(ig *idGenerator) error {
f, err := newDecimalFormater(format, serialLength, machineLength)
if err != nil {
return err
}
ig.formater = f
return nil
}
}
func WithBinaryFormatter(start int64, serialLength, machineLength int64) opt {
return func(ig *idGenerator) error {
f, err := newBinaryFormatter(start, serialLength, machineLength)
if err != nil {
return err
}
ig.formater = f
return nil
}
}
func New(opts ...opt) (*idGenerator, error) {
g := &idGenerator{}
for _, o := range opts {
err := o(g)
if err != nil {
return nil, err
}
}
if g.store == nil {
g.store = store.NewMemStore(0)
}
_, err := g.store.UpdateMachineCode(g.formater.MaxinalMachineCode()) // 初始化机器码
if err != nil {
return nil, err
}
g.buffer = newDoubleBuffer(g.obtain) // 初始化序列缓冲
return g, nil
}
func (g *idGenerator) WithRedis(key string, rdb redis.Client) *idGenerator {
return g.WithStore(store.NewRedisStore(key, rdb))
}
func (g *idGenerator) WithStore(s storage) *idGenerator {
g.store = s
return g
}
func (g *idGenerator) Next() Id {
serial, reback := g.buffer.allot()
id := g.formater.Format(g.store.MachineCode(), serial, reback)
return id
}
func (g *idGenerator) Close() {
if g.store != nil {
g.store.Close()
}
}
func (g *idGenerator) obtain() (*store.Segment, error) {
step := mathx.Min(defaultDoubleBufferStep, g.formater.MaximalSequence())
s, err := g.store.Assign(0, g.formater.MaximalSequence(), step)
if err != nil {
println("分配失败", err.Error())
}
return s, err
}

View File

@ -0,0 +1,148 @@
package idgenerator_test
import (
"sync"
"testing"
idgenerator "github.com/charlienet/go-mixed/idGenerator"
"github.com/charlienet/go-mixed/redis"
"github.com/charlienet/go-mixed/sets"
"github.com/charlienet/go-mixed/tests"
)
func TestGenerator(t *testing.T) {
tests.RunOnRedis(t, func(rdb redis.Client) {
generator, err := idgenerator.New(
idgenerator.WithDecimalFormater(idgenerator.YYYYMMDDHHmmss, 1, 1),
idgenerator.WithRedis("idgen_test", rdb))
if err != nil {
t.Fatal(err)
}
for i := 0; i < 20; i++ {
t.Log(generator.Next())
}
})
}
func TestDecimalGenerator(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
generator, err := idgenerator.New(
idgenerator.WithDecimalFormater(idgenerator.YYYYMMDDHHmmss, 3, 1),
idgenerator.WithRedis("idgen_test", rdb))
if err != nil {
t.Fatal(err)
}
for i := 0; i < 200; i++ {
t.Log(generator.Next())
}
})
}
func TestDecimalMonth(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
generator, err := idgenerator.New(
idgenerator.WithDecimalFormater(idgenerator.YYYYMMDD, 2, 1),
idgenerator.WithRedis("idgen_test", rdb))
if err != nil {
t.Fatal(err)
}
for i := 0; i < 105; i++ {
t.Log(generator.Next())
}
})
}
func TestParallelCreate(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
g1, err := idgenerator.New(
idgenerator.WithDecimalFormater(idgenerator.YYYYMMDDHHmmss, 3, 1),
idgenerator.WithRedis("idgen_testcccc", rdb))
if err != nil {
panic(err)
}
_ = g1.Next().Int64()
}()
go func() {
defer wg.Done()
g2, err := idgenerator.New(
idgenerator.WithDecimalFormater(idgenerator.YYYYMMDDHHmmss, 3, 1),
idgenerator.WithRedis("idgen_testcccc", rdb))
if err != nil {
panic(err)
}
_ = g2.Next().Int64()
}()
wg.Wait()
})
}
func TestParallel(t *testing.T) {
set := sets.NewHashSet[int64]().Sync()
_ = set
f := func() {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
generator, err := idgenerator.New(
idgenerator.WithDecimalFormater(idgenerator.YYYYMMDDHHmmss, 3, 1),
idgenerator.WithRedis("idgen_testcccc", rdb))
if err != nil {
t.Fatal(err)
}
defer generator.Close()
generator.Next()
for i := 0; i < 50000; i++ {
id := generator.Next().Int64()
if set.Contains(id) {
panic("生成重复")
}
set.Add(id)
}
})
}
var wg sync.WaitGroup
for i := 0; i < 6; i++ {
wg.Add(1)
go func() {
defer wg.Done()
f()
}()
}
wg.Wait()
}
func BenchmarkGenerator(b *testing.B) {
tests.RunOnDefaultRedis(b, func(rdb redis.Client) {
b.Run("bbb", func(b *testing.B) {
generator, err := idgenerator.New(
idgenerator.WithDecimalFormater(idgenerator.YYYYMMDDHHmmss, 3, 1),
idgenerator.WithRedis("idgen_test", rdb))
if err != nil {
b.Fatal(err)
}
for i := 0; i < 999; i++ {
generator.Next()
}
})
})
}

View File

@ -1,118 +0,0 @@
package idgenerator
import (
"fmt"
"math"
"time"
_ "unsafe"
)
// 时间段开始时间 2022-01-01
const startTimeStamp = 1640966400
const (
MachineIdBits = uint(8) //机器id所占的位数
SequenceBits = uint(12) //序列所占的位数
MachineIdMax = int64(-1 ^ (-1 << MachineIdBits)) //支持的最大机器id数量
SequenceMask = uint64(-1 ^ (-1 << SequenceBits)) //
MachineIdShift = uint64(SequenceBits) //机器id左移位数
TimestampShift = uint64(SequenceBits + MachineIdBits) //时间戳左移位数
)
type TimePrecision int
const (
Second TimePrecision = iota // 秒
Minute // 分
Day // 日
)
type Config struct {
Machine int
TimeScope TimePrecision
}
type Generator struct {
machine uint64
timeScope TimePrecision // 时间段精度
timestamp uint64 // 上次生成时间
sequence uint64 // 上次使用序列
}
type Id struct {
machine uint64 // 机器标识
Scope int // 时间精度
Timestamp uint64 // 生成时间
Sequence uint64 // 标识序列
}
func New(cfg Config) *Generator {
return &Generator{
machine: uint64(cfg.Machine),
timeScope: cfg.TimeScope,
}
}
func (g *Generator) Next() Id {
now := currentTimestamp()
if g.timestamp == now && g.sequence == 0 {
fmt.Println(time.Now().Format("2006-01-02 15:04:05.000"), "下一个时间点")
for now <= g.timestamp {
// runtime.Gosched()
now = currentTimestamp()
}
}
g.timestamp = now // 标识生成时间
g.sequence = (g.sequence + 1) & SequenceMask // 生成下一序列,超过最大值时返回零
return Id{
machine: g.machine,
Timestamp: g.timestamp,
Sequence: g.sequence,
}
}
func (i Id) Id() uint64 {
return i.Timestamp<<TimestampShift | i.machine<<MachineIdShift | i.Sequence
}
func (i Id) genCheck() uint64 {
return math.MaxUint64
}
func (i Id) String() string {
return fmt.Sprintf("Time:%d scope: %d Machine: %d Ser:%06d id:%d", i.Timestamp, i.Scope, i.machine, i.Sequence, i.Id())
}
func (g *Generator) NextId() uint64 {
return g.Next().Id()
}
func (g *Generator) Batch(num int) []uint64 {
ret := make([]uint64, num)
for i := 0; i < num; i++ {
ret[i] = g.NextId()
}
return ret
}
func Decode(id uint64) Id {
return Id{}
}
func Verify(id uint64) bool {
return false
}
func currentTimestamp() uint64 {
return uint64(time.Now().Unix() - startTimeStamp)
}
//go:linkname runtimeNano runtime.nanotime
func runtimeNano() int64

View File

@ -1,32 +0,0 @@
package idgenerator_test
import (
"testing"
idgenerator "github.com/charlienet/go-mixed/idGenerator"
)
func TestNextId(t *testing.T) {
generator := idgenerator.New(idgenerator.Config{})
for i := 0; i < 10; i++ {
t.Log(generator.Next().Id())
}
}
func TestBatch(t *testing.T) {
generator := idgenerator.New(idgenerator.Config{})
b := generator.Batch(20)
for _, i := range b {
t.Log(i)
}
}
func TestNext(t *testing.T) {
generator := idgenerator.New(idgenerator.Config{})
for i := 0; i < 10; i++ {
t.Log(generator.Next())
}
}

114
idGenerator/readme.md Normal file
View File

@ -0,0 +1,114 @@
id生成服务
标识生成服务,可用于分布式环境的标识生成服务,本服务可以多实例部署。
要求
1. 全局唯一: 必须保证生成的 ID 是全局性唯一的;
2. 趋势递增: 生成的 ID 需要按照某种规则有序,便于数据库的写入和排序操作;
3. 单调递增: 后生成的标识大于前面生成的标识
3. 可用性: 需要保证高并发下的可用性。
4. 自主性: 分布式环境下不依赖中心认证即可自行生成 ID
5. 安全性: 不暴露系统和业务的信息。在一些业务场景下,需要 ID 无规则或者不规则;
标识组成
1. 时间信息,从当前时间中根据模板获取数据段。
号段模式
![号段模式](assets/双缓存号段分配.webp)
批量获取标识,业务应用使用后临近剩余数量时生成新的号段,使用双标识缓冲区方式保存。在活动缓冲区使用量达到限定值时在备缓冲区放置新的号段。
号段分配器
数据结构
1. 最大序列
2. 步长
3. 版本号
分配步骤
1. 服务端按照KEY存储当前分配的最大值
2. 获取是
序列回绕当序列递增到最大值时恢复为起始值重新开始。在使用中按照业务场景同一时间段中不能耗尽序列值。当序列发生回绕时分配器返回已回绕backward
操作接口
1. 客户端注册标识,客户端使用应用名、规则(前缀、步长、阈值、初始值)进行注册。注册成功后返回标识
标识生成规则,时间段、数字序列
1. 前缀: 使用模板方式生成,生成参数包括当前时间,时间戳。
2. 位数: 表示数据序列包含的位数如4位表示序列
3. 初始值:序列初始值,序列到达最大值时返回到初始值。
标识生成统一包含时间信息,由于在分布式环境中各应用节点时间可能并不同步,可能造成应用节点生成重复。
1. 应用端获取片段时记录当前时间(T1)
2. 请求服务端分配片段
3. 服务端记录报文到达时间(T2)
4. 服务端处理完成后记录离开时间(T3)
5. 应用端接收到响应时,记录时间(T4)
时间周期延时
1. Delay = ( T4 - T1 ) - ( T3 - T2 )
2. Offset = (( T2- T1 ) + ( T3 T4 )) / 2
应用端使用延时和偏移计算当前时间
标识服务服务器多活
微信序列号生成方案
微信序列号跟用户 uin 绑定,具有以下性质:递增的 64 位整形;使用每个用户独立的 64 位 sequence 的体系,而不是用一个全局的 64 位(或更高位) sequence ,很大原因是全局唯一的 sequence 会有非常严重的申请互斥问题,不容易去实现一个高性能高可靠的架构。其实现方式包含如下两个关键点:
1步进式持久化增加一个缓存中间层内存中缓存最近一个分配出现的 sequencecur_seq以及分配上限max_seq分配 sequence 时,将 cur_seq++,与分配上限 max_seq 比较,如果 cur_seq > max_seq将分配上限提升一个步长 max_seq += step并持久化 max_seq重启时读出持久化的 max_seq赋值给 cur_seq。此种处理方式可以降低持久化的硬盘 IO 次数,可以系统的整体吞吐量。
2分号段共享存储引入号段 section 的概念uin 相邻的一段用户属于一个号段,共享一个 max_seq。该处理方式可以大幅减少 max_seq 数据的大小,同时可以进一步地降低 IO 次数。
二进制模式
按照二进制模式处理序列号生成,各数据段按照各自的位长度组装
十进制模式
缺陷
当发生序列回绕时,不同的时间序列有不同的处理。如时间序列精度为日,那么在发生回绕时认为此时的回绕不能发生。如果应用在当日内被重启,并且发生了回绕,当前机制无法检测此问题。
序列记录可以依赖于外部存储如RedisDB等。当外部存储失效时序列可能会发生冲突。此时当时间段没有前进时可能发生重复。一般认为DB为不失效存储。
需要烦精确的选择时间段和序列的相对关系,一般情况下
ID分配器工作流程
初始化
1. 创建生成器
2. 创建外部存储
3. 指定生成器生成规则,机器码长度,序列长度,序列化格式。
4. 创建双缓存,使用外部存储分配数据段。
标识分配
1. 向序列缓冲器获取下一序列
2. 向时间段分配器获取下一时间戳(传入是否回旋)
4. 由标识组装器获取
格式化器
十进制格式化,时间段格式串
二进制格式化,起始时间,时间精度

11
idGenerator/store.go Normal file
View File

@ -0,0 +1,11 @@
package idgenerator
import "github.com/charlienet/go-mixed/idGenerator/store"
// 序列存储分配器
type storage interface {
MachineCode() int64 // 当前机器码
UpdateMachineCode(max int64) (int64, error) // 更新机器标识
Assign(min, max, step int64) (*store.Segment, error) // 分配号段
Close()
}

View File

@ -0,0 +1,60 @@
package store
import (
"sync"
"github.com/charlienet/go-mixed/mathx"
)
type memStore struct {
mu sync.Mutex
machine int64
current int64
}
func NewMemStore(machineCode int64) *memStore {
return &memStore{machine: machineCode}
}
func (s *memStore) UpdateMachineCode(max int64) (int64, error) {
return s.machine, nil
}
func (s *memStore) MachineCode() int64 {
return s.machine
}
func (s *memStore) Assign(min, max, step int64) (*Segment, error) {
s.mu.Lock()
defer s.mu.Unlock()
step = mathx.Min(step, max)
start := mathx.Max(s.current, min)
end := start + step
reback := false
if start >= max {
start = min
end = step
s.current = end
reback = true
}
if end > max {
end = max
s.current = end
}
s.current = end
return &Segment{
start: start,
current: start,
end: end,
reback: reback,
}, nil
}
func (s *memStore) Close() {
}

View File

@ -0,0 +1,21 @@
package store_test
import (
"testing"
"github.com/charlienet/go-mixed/idGenerator/store"
)
func TestMemSmall(t *testing.T) {
s := store.NewMemStore(2)
for i := 0; i < 10; i++ {
t.Log(s.Assign(1, 9, 20))
}
}
func TestMemBig(t *testing.T) {
s := store.NewMemStore(2)
for i := 0; i < 10; i++ {
t.Log(s.Assign(0, 99, 18))
}
}

View File

@ -0,0 +1,2 @@
package store

View File

@ -0,0 +1,99 @@
#!lua name=charlie_id_generator
-- 安装命令
-- cat redis_id_store.lua | redis-cli -x --cluster-only-masters --cluster call 192.168.123.30:6379 FUNCTION LOAD REPLACE
-- 标识分配的redis函数
-- updateMachineCode更新客户端机器码
-- allocateSerial分配序列段。在分配序列段之前更新机器码
-- 默认机器码有效期秒数
local machineExpires = 60
local function _updateMachineCode(key, code, token, max)
local machineKey = key..":allocated:"..tostring(code)
if redis.call("GET", machineKey) == token then
redis.call("EXPIRE", machineKey, machineExpires)
return code
end
for i = 0, tostring(max), 1 do
machineKey = key..":allocated:"..tostring(i)
if redis.call("EXISTS", machineKey) == 0 then
redis.call("SET", machineKey, token, "EX", machineExpires)
return i
end
end
return -1
end
-- 请求参数:机器码当前值,机器标识,机器码最大值
-- 响应参数:分配的机器码。分配失败时返回-1
-- 机器标识在客户端创建时生成,分辨不同的客户。
-- FCALL updateMachineCode 1 "bbcc" -1 "aaaaa" 9
local function updateMachineCode(keys, args)
local key = keys[1]
local code = tonumber(args[1])
local token = args[2]
local max = tonumber(args[3])
return _updateMachineCode(key, code, token, max)
end
-- 请求参数:机器码,机器标识,步长,序列最小值,序列最大值,机器码最大值
-- 响应参数:分配成功的机器码,序列起始值和结束值{machineCode, begin, finish}
local function allocateSerial(keys, args)
local key = keys[1]
local code = tonumber(args[1])
local token = args[2]
local step = tonumber(args[3])
local min = tonumber(args[4])
local max = tonumber(args[5])
local maxCode = tonumber(args[6])
code = _updateMachineCode(key, code, token, maxCode)
if code == -1 then
-- 刷新机器码失败,响应错误信息。
return {code, 0, 0, 0, "machine code allocation failed"}
end
if step > max then
step = max
end
key = key..":sequence"
if redis.call("HEXISTS", key, code) == 0 then
redis.call("HSET", key, code, step)
return {code, min, step, 0, "success"}
end
local begin = tonumber(redis.call("HGET", key, code))
local finish = redis.call("HINCRBY", key, code, step)
local reback = 0
-- 计算后的起始值超过最大值,从序列段起点重新开始
if begin >= max then
begin = min
finish = step
redis.call("HSET", key, code, step)
-- 检查上次绕回时间,判断是否需要检查时间段冲突
reback = 1
end
-- 计算后结束值超过最大值
if finish > max then
finish = max
redis.call("HSET", key, code, finish)
end
return {code, begin, finish, reback, "success"}
end
redis.register_function('updateMachineCode',updateMachineCode)
redis.register_function('allocateSerial',allocateSerial)

View File

@ -0,0 +1,145 @@
package store
import (
"context"
"errors"
"fmt"
"sync"
"time"
_ "embed"
"github.com/charlienet/go-mixed/rand"
"github.com/charlienet/go-mixed/redis"
)
//go:embed redis_id_store.lua
var redis_id_function string
var once sync.Once
type redisStore struct {
rdb redis.Client
key string // 缓存键
machine string // 随机键值(机器标识)
machineCode int64 // 机器码
max int64 // 机器码的最大值
close chan struct{} // 关闭保活协程
isRunning bool // 是否已经关闭
mu sync.Mutex
}
func NewRedisStore(key string, rdb redis.Client) *redisStore {
once.Do(func() { rdb.LoadFunction(redis_id_function) })
return &redisStore{
rdb: rdb,
key: key,
machineCode: -1,
machine: rand.Hex.Generate(24),
close: make(chan struct{}),
}
}
// 分配机器标识,分配值为-1时表示分配失败
func (s *redisStore) UpdateMachineCode(max int64) (int64, error) {
s.max = max
err := s.updateMachine(max)
if err != nil {
return -1, err
}
// 关闭原协程,开启新的保活协程
// if s.isRunning {
// s.close <- struct{}{}
// }
// if !s.isRunning {
// s.close <- struct{}{}
go s.keepAlive(max)
// }
return s.machineCode, nil
}
func (s *redisStore) MachineCode() int64 {
return s.machineCode
}
func (s *redisStore) Assign(min, max, step int64) (*Segment, error) {
s.mu.Lock()
defer s.mu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
// 序列段分配 机器码,机器标识,步长,序列最小值,序列最大值,机器码最大值
r, err := s.rdb.FCall(ctx, "allocateSerial", []string{s.key}, s.machineCode, s.machine, step, min, max, s.max).Result()
if err != nil {
return &Segment{}, err
}
machineCode, start, end, reback := split(r)
s.machineCode = machineCode
return &Segment{start: start, end: end, current: start, reback: reback}, err
}
func split(r any) (machineCode, start, end int64, reback bool) {
if result, ok := r.([]any); ok {
machineCode = result[0].(int64)
start = result[1].(int64)
end = result[2].(int64)
reback = result[3].(int64) == 1
}
return
}
func (s *redisStore) updateMachine(max int64) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
//机器码当前值,机器标识,机器码最大值
r, err := s.rdb.FCall(ctx, "updateMachineCode", []string{s.key}, s.machineCode, s.machine, s.max).Result()
if err != nil {
return err
}
if r == nil {
return errors.New("failed to obtain machine code")
}
s.machineCode = r.(int64)
if s.machineCode == -1 {
return errors.New("machine code allocation failed")
}
return nil
}
func (s *redisStore) Close() {
s.close <- struct{}{}
s.isRunning = false
}
func (s *redisStore) keepAlive(max int64) {
t := time.NewTicker(time.Second * 2)
defer t.Stop()
for {
select {
case <-t.C:
// println("当前机器码:", s.machineCode)
err := s.updateMachine(max)
if err != nil {
fmt.Println("err:", err.Error())
}
case <-s.close:
println("保活停止")
return
}
}
}

View File

@ -0,0 +1,106 @@
package store_test
import (
"testing"
"time"
"github.com/charlienet/go-mixed/idGenerator/store"
"github.com/charlienet/go-mixed/redis"
"github.com/charlienet/go-mixed/tests"
)
func TestSmallSerail(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
s := store.NewRedisStore("sss", rdb)
for i := 0; i < 5; i++ {
t.Log(s.Assign(0, 9, 20))
}
})
}
func TestSmallAssign(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
s := store.NewRedisStore("sss", rdb)
for i := 0; i < 10; i++ {
t.Log(s.Assign(0, 9, 30))
}
})
}
func TestBigAssign(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
s := store.NewRedisStore("sss", rdb)
for i := 0; i < 102; i++ {
t.Log(s.Assign(0, 99, 10))
}
})
}
func TestRedisAssign(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
s := store.NewRedisStore("sss", rdb)
for i := 0; i < 10; i++ {
t.Log(s.Assign(21, 99, 30))
}
})
}
func TestFullRedisAssign(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
s := store.NewRedisStore("sss", rdb)
for i := 0; i < 10; i++ {
t.Log(s.Assign(0, 999, 99))
}
})
}
func TestUpdateMachineCode(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
for i := 0; i < 20; i++ {
s := store.NewRedisStore("id", rdb)
code, err := s.UpdateMachineCode(99)
t.Log("获取到机器标识:", code, err)
if err != nil {
return
}
time.Sleep(time.Millisecond * 100)
// s.Close()
}
time.Sleep(time.Second * 10)
})
}
func TestUpdate(t *testing.T) {
tests.RunOnRedis(t, func(rdb redis.Client) {
s := store.NewRedisStore("id", rdb)
s.UpdateMachineCode(99)
t.Log(s.MachineCode())
s.UpdateMachineCode(99)
t.Log(s.MachineCode())
s2 := store.NewRedisStore("id", rdb)
s2.UpdateMachineCode(99)
t.Log(s2.MachineCode())
}, redis.RedisOption{Addr: "192.168.123.50:6379", Password: "123456", Prefix: "cacc"})
}

View File

@ -0,0 +1,49 @@
package store
import (
"fmt"
"sync"
)
// 号段
type Segment struct {
start int64
end int64
current int64
reback bool
mu sync.RWMutex
}
func (s *Segment) Allot() int64 {
s.mu.Lock()
defer s.mu.Unlock()
s.current++
return s.current
}
func (s *Segment) IsEnding() bool {
s.mu.Lock()
defer s.mu.Unlock()
return (s.current - s.start) > (s.end - s.current)
}
func (s *Segment) IsEmpty() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.current == s.end
}
func (s *Segment) Reback() bool {
s.mu.Lock()
defer s.mu.Unlock()
// println("回旋确认:", s.reback, s.current == (s.start+1))
return s.reback && s.current == (s.start+1)
}
func (s *Segment) String() string {
return fmt.Sprintf("start:%d-%d(%v)", s.start, s.end, s.reback)
}

View File

@ -6,10 +6,17 @@ type ChanLocker interface {
} }
type chanSourceLock struct { type chanSourceLock struct {
m RWLocker m rwLocker
content map[string]chan int content map[string]chan int
} }
func NewChanSourceLocker() *chanSourceLock {
return &chanSourceLock{
m: NewRWLocker(),
content: make(map[string]chan int),
}
}
func (s *chanSourceLock) Get(key string) (ch <-chan int, ok bool) { func (s *chanSourceLock) Get(key string) (ch <-chan int, ok bool) {
s.m.RLock() s.m.RLock()
ch, ok = s.content[key] ch, ok = s.content[key]

View File

@ -0,0 +1,19 @@
package locker_test
import (
"testing"
"github.com/charlienet/go-mixed/locker"
)
func TestChanSourceLocker(t *testing.T) {
l := locker.NewChanSourceLocker()
c, ok := l.Get("aaaa")
if ok {
<-c
println("ok")
}
println("fail")
}

View File

@ -0,0 +1,7 @@
package locker
import "context"
type DistributedLocker interface {
Unlock(context.Context, string)
}

View File

@ -0,0 +1,13 @@
package locker_test
import (
"testing"
"github.com/charlienet/go-mixed/redis"
"github.com/charlienet/go-mixed/tests"
)
func TestRedisDistrbutedLocker(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
})
}

View File

@ -1,14 +1,9 @@
package locker package locker
var _ RWLocker = &emptyLocker{} var _ rwLocker = &emptyLocker{}
var _ Locker = &emptyLocker{} var _ locker = &emptyLocker{}
var EmptyLocker = &emptyLocker{} type emptyLocker struct {
type emptyLocker struct{}
func NewEmptyLocker() *emptyLocker {
return &emptyLocker{}
} }
func (l *emptyLocker) RLock() {} func (l *emptyLocker) RLock() {}

View File

@ -2,14 +2,16 @@ package locker
import "sync" import "sync"
type Locker interface { type locker interface {
Lock() Lock()
Unlock() Unlock()
TryLock() bool TryLock() bool
} }
type RWLocker interface { type rwLocker interface {
Locker Lock()
Unlock()
TryLock() bool
RLock() RLock()
RUnlock() RUnlock()
TryRLock() bool TryRLock() bool
@ -18,3 +20,7 @@ type RWLocker interface {
func NewLocker() *sync.Mutex { func NewLocker() *sync.Mutex {
return &sync.Mutex{} return &sync.Mutex{}
} }
func NewRWLocker() *sync.RWMutex {
return &sync.RWMutex{}
}

22
locker/readme.md Normal file
View File

@ -0,0 +1,22 @@
同步锁
EmptyLocker 空锁
RWLocker, 读写锁
SpinLocker, 旋转锁
锁可以添加一个外部存储成为分布式锁。WithRedis, WithZookeeper
单例锁
资源锁
分布式锁
在锁的基础上添加分布式存储升级为分布式锁
locker.WithRedis()
locker.WithZookeeper()

View File

@ -0,0 +1,33 @@
#!lua name=charlie_locker
-- 安装命令
-- cat redis_locker.lua | redis-cli -x --cluster-only-masters --cluster call 192.168.123.30:6379 FUNCTION LOAD REPLACE
local function lock(keys, args)
if redis.call("GET", keys[1]) == args[1] then
redis.call("SET", keys[1], args[1], "PX", args[2])
return "OK"
else
return redis.call("SET", keys[1], args[1], "NX", "PX", args[2])
end
end
local function del(keys, args)
if redis.call("GET", keys[1]) == args[1] then
return redis.call("DEL", keys[1])
else
return '0'
end
end
local function expire(keys, args)
if redis.call('get', keys[1]) == args[1] then
return redis.call('expire', keys[1], args[2])
else
return '0'
end
end
redis.register_function('locker_lock',lock)
redis.register_function('locker_unlock',del)
redis.register_function('locker_expire',expire)

164
locker/redis/redis_store.go Normal file
View File

@ -0,0 +1,164 @@
package redis
import (
"context"
_ "embed"
"maps"
"strings"
"sync"
"time"
"github.com/charlienet/go-mixed/rand"
"github.com/charlienet/go-mixed/redis"
goredis "github.com/redis/go-redis/v9"
)
//go:embed redis_locker.lua
var redis_locker_function string
const (
defaultExpire = time.Second * 20
retryInterval = time.Millisecond * 10
)
var once sync.Once
type redis_locker_store struct {
key string
sources map[string]string
expire time.Duration // 过期时间
mu sync.RWMutex
clients []redis.Client
}
func NewRedisStore(key string, clients ...redis.Client) *redis_locker_store {
once.Do(func() { redis.Clients(clients).LoadFunction(redis_locker_function) })
locker := &redis_locker_store{
key: key,
sources: make(map[string]string),
clients: clients,
expire: defaultExpire,
}
go locker.expandLockTime()
return locker
}
func (l *redis_locker_store) Lock(ctx context.Context, sourceName string) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
if l.TryLock(ctx, sourceName) {
return nil
}
}
time.Sleep(retryInterval)
}
}
func (l *redis_locker_store) TryLock(ctx context.Context, sourceName string) bool {
value := l.getSourceValue(sourceName)
results := l.fCall(ctx, "locker_lock", sourceName, value, l.expire.Milliseconds())
if !isSuccess(results) {
for _, r := range results {
if r.Err() != nil {
println("err:", r.Err().Error())
}
}
l.Unlock(ctx, sourceName)
return false
}
return true
}
func (locker *redis_locker_store) Unlock(ctx context.Context, sourceName string) {
value := locker.getSourceValue(sourceName)
locker.fCall(ctx, "locker_unlock", sourceName, value)
locker.mu.Lock()
defer locker.mu.Unlock()
delete(locker.sources, sourceName)
}
func (l *redis_locker_store) expandLockTime() {
for {
time.Sleep(l.expire / 3)
if len(l.sources) == 0 {
continue
}
l.mu.RLock()
cloned := maps.Clone(l.sources)
l.mu.RUnlock()
for k, v := range cloned {
results := l.fCall(context.Background(), "locker_expire", k, v, l.expire.Seconds())
for _, r := range results {
if r.Err() != nil {
println("键延期失败:", r.Err().Error())
}
}
}
}
}
func (l *redis_locker_store) getSourceValue(name string) string {
l.mu.Lock()
defer l.mu.Unlock()
if v, ok := l.sources[name]; ok {
return v
}
v := rand.Hex.Generate(36)
l.sources[name] = v
return v
}
func (locker *redis_locker_store) fCall(ctx context.Context, cmd string, key string, args ...any) []*goredis.Cmd {
results := make([]*goredis.Cmd, 0, len(locker.clients))
var wg sync.WaitGroup
wg.Add(len(locker.clients))
for _, rdb := range locker.clients {
go func(rdb redis.Client) {
defer wg.Done()
newKey := rdb.JoinKeys(locker.key, key)
results = append(results, rdb.FCall(ctx, cmd, []string{newKey}, args...))
}(rdb)
}
wg.Wait()
return results
}
func isSuccess(results []*goredis.Cmd) bool {
successCount := 0
for _, ret := range results {
resp, err := ret.Result()
if err != nil || resp == nil {
return false
}
reply, ok := resp.(string)
if ok && strings.EqualFold(reply, "OK") {
successCount++
}
}
return successCount >= len(results)/2+1
}

View File

@ -0,0 +1,31 @@
package redis
import (
"context"
"testing"
"time"
"github.com/charlienet/go-mixed/redis"
"github.com/charlienet/go-mixed/tests"
)
func TestCreateRedisStore(t *testing.T) {
tests.RunOnDefaultRedis(t, func(rdb redis.Client) {
keyName := "source"
l := NewRedisStore("locker_key", rdb)
ret := l.TryLock(context.Background(), keyName)
if !ret {
t.Log("加锁失败")
}
l.Lock(context.Background(), keyName)
t.Log("锁重入完成")
l.Unlock(context.Background(), keyName)
time.Sleep(time.Second * 15)
// l.Unlock(context.Background())
})
}

View File

@ -1,7 +0,0 @@
package locker
import "sync"
func NewRWLocker() *sync.RWMutex {
return &sync.RWMutex{}
}

View File

@ -1,12 +0,0 @@
package locker
import "testing"
func TestRWLokcer(t *testing.T) {
l := NewRWLocker()
l.RLock()
t.Log(l.TryRLock())
l.RUnlock()
}

View File

@ -1,27 +1,45 @@
package locker package locker
import ( import (
"context"
"fmt" "fmt"
"sync/atomic" "sync/atomic"
redis_store "github.com/charlienet/go-mixed/locker/redis"
"github.com/charlienet/go-mixed/redis"
) )
// 带计数器锁 // 带计数器锁
type countLocker struct { type countLocker struct {
Locker rw rwLocker
Count int32 Count int32
} }
// SourceLocker 资源锁 // SourceLocker 资源锁
type SourceLocker struct { type SourceLocker struct {
m RWLocker m RWLocker
distributedLocker DistributedLocker
locks map[string]*countLocker locks map[string]*countLocker
err error
} }
func NewSourceLocker() *SourceLocker { func NewSourceLocker() *SourceLocker {
return &SourceLocker{ l := &SourceLocker{
m: NewRWLocker(),
locks: make(map[string]*countLocker), locks: make(map[string]*countLocker),
} }
l.m.Synchronize()
return l
}
func (s *SourceLocker) WithRedis(key string, clients ...redis.Client) *SourceLocker {
redisStore := redis_store.NewRedisStore(key, clients...)
return s.WithDistributedLocker(redisStore)
}
func (s *SourceLocker) WithDistributedLocker(distributed DistributedLocker) *SourceLocker {
s.distributedLocker = distributed
return s
} }
func (s *SourceLocker) Lock(key string) { func (s *SourceLocker) Lock(key string) {
@ -31,7 +49,7 @@ func (s *SourceLocker) Lock(key string) {
if ok { if ok {
atomic.AddInt32(&l.Count, 1) atomic.AddInt32(&l.Count, 1)
l.Lock() l.rw.Lock()
fmt.Println("加锁") fmt.Println("加锁")
} else { } else {
@ -40,18 +58,15 @@ func (s *SourceLocker) Lock(key string) {
if l2, ok := s.locks[key]; ok { if l2, ok := s.locks[key]; ok {
s.m.Unlock() s.m.Unlock()
l2.Lock() l2.rw.Lock()
fmt.Println("二次检查加锁") fmt.Println("二次检查加锁")
} else { } else {
n := NewLocker() n := NewRWLocker()
s.locks[key] = &countLocker{Locker: n, Count: 1} s.locks[key] = &countLocker{rw: n, Count: 1}
s.m.Unlock() s.m.Unlock()
fmt.Printf("新锁准备加锁:%p\n", n)
n.Lock() n.Lock()
fmt.Println("初始加锁")
} }
} }
} }
@ -61,8 +76,11 @@ func (s *SourceLocker) Unlock(key string) {
if l, ok := s.locks[key]; ok { if l, ok := s.locks[key]; ok {
atomic.AddInt32(&l.Count, -1) atomic.AddInt32(&l.Count, -1)
fmt.Printf("解锁%p\n", l) l.rw.Unlock()
l.Unlock()
if s.distributedLocker != nil {
s.distributedLocker.Unlock(context.Background(), key)
}
if l.Count == 0 { if l.Count == 0 {
delete(s.locks, key) delete(s.locks, key)
@ -75,18 +93,14 @@ func (s *SourceLocker) TryLock(key string) bool {
// 加读锁 // 加读锁
s.m.RLock() s.m.RLock()
l, ok := s.locks[key] l, ok := s.locks[key]
if ok {
ret := l.TryLock()
s.m.RUnlock() s.m.RUnlock()
if ok {
ret := l.rw.TryLock()
return ret return ret
} else { } else {
s.m.RUnlock()
s.m.Lock() s.m.Lock()
n := NewLocker() n := NewRWLocker()
s.locks[key] = &countLocker{Locker: n, Count: 1} s.locks[key] = &countLocker{rw: n, Count: 1}
s.m.Unlock() s.m.Unlock()
return n.TryLock() return n.TryLock()

View File

@ -1,19 +1,39 @@
package locker package locker_test
import ( import (
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/charlienet/go-mixed/locker"
"github.com/stretchr/testify/assert"
) )
var sourcekey = "u-0001" var sourcekey = "u-0001"
func TestTryLock(t *testing.T) { func TestTryLock(t *testing.T) {
l := locker.NewSourceLocker()
l.Lock("aa")
assert.False(t, l.TryLock("aa"))
assert.True(t, l.TryLock("bb"))
defer l.Unlock("aa")
}
func TestM(t *testing.T) {
l := locker.NewSourceLocker()
for i := 0; i < 10000000; i++ {
l.Lock("aaa")
l.Unlock("aaa")
}
t.Logf("%+v", l)
} }
func TestSourceLocker(t *testing.T) { func TestSourceLocker(t *testing.T) {
l := NewSourceLocker() l := locker.NewSourceLocker()
c := 5 c := 5
n := 0 n := 0
@ -41,7 +61,7 @@ func TestSourceTryLock(t *testing.T) {
wg := new(sync.WaitGroup) wg := new(sync.WaitGroup)
wg.Add(c) wg.Add(c)
l := NewSourceLocker() l := locker.NewSourceLocker()
for i := 0; i < c; i++ { for i := 0; i < c; i++ {
go func() { go func() {
@ -61,7 +81,7 @@ func TestSourceTryLock(t *testing.T) {
} }
func BenchmarkSourceLocker(b *testing.B) { func BenchmarkSourceLocker(b *testing.B) {
l := NewSourceLocker() l := locker.NewSourceLocker()
b.RunParallel(func(p *testing.PB) { b.RunParallel(func(p *testing.PB) {
for p.Next() { for p.Next() {

View File

@ -1,12 +1,14 @@
package locker package locker_test
import ( import (
"sync" "sync"
"testing" "testing"
"github.com/charlienet/go-mixed/locker"
) )
func TestSpinLock(t *testing.T) { func TestSpinLock(t *testing.T) {
l := NewSpinLocker() l := locker.NewSpinLocker()
n := 10 n := 10
c := 0 c := 0

View File

@ -3,94 +3,111 @@ package locker
import ( import (
"log" "log"
"sync" "sync"
"github.com/charlienet/go-mixed/redis"
) )
type WithLocker struct { var empty = &emptyLocker{}
type Locker struct {
once sync.Once once sync.Once
mu Locker distributedLocker DistributedLocker // 分布式锁
mu locker
} }
func (w *WithLocker) Synchronize() { func (w *Locker) WithRedis(key string, rdb redis.Client) *Locker {
if w.mu == nil || w.mu == EmptyLocker { return w
}
func (w *Locker) WithDistributedLocker(d DistributedLocker) *Locker {
return w
}
func (w *Locker) Synchronize() *Locker {
if w.mu == nil || w.mu == empty {
w.mu = NewLocker() w.mu = NewLocker()
} }
return w
} }
func (w *WithLocker) Lock() { func (w *Locker) Lock() {
w.ensureLocker().Lock() w.ensureLocker().mu.Lock()
} }
func (w *WithLocker) Unlock() { func (w *Locker) Unlock() {
w.ensureLocker().Unlock() w.ensureLocker().mu.Unlock()
} }
func (w *WithLocker) TryLock() bool { func (w *Locker) TryLock() bool {
return w.ensureLocker().TryLock() return w.ensureLocker().mu.TryLock()
} }
func (w *WithLocker) ensureLocker() Locker { func (w *Locker) ensureLocker() *Locker {
w.once.Do(func() { w.once.Do(func() {
if w.mu == nil { if w.mu == nil {
w.mu = EmptyLocker w.mu = empty
} }
}) })
return w.mu return w
} }
type WithSpinLocker struct { type SpinLocker struct {
WithLocker Locker
} }
func (w *WithSpinLocker) Synchronize() { func (w *SpinLocker) Synchronize() {
if w.mu == nil || w.mu == EmptyLocker { if w.mu == nil || w.mu == empty {
w.mu = NewSpinLocker() w.mu = NewSpinLocker()
} }
} }
type WithRWLocker struct { type RWLocker struct {
once sync.Once once sync.Once
mu RWLocker mu rwLocker
} }
func (w *WithRWLocker) Synchronize() { func (w *RWLocker) Synchronize() *RWLocker {
if w.mu == nil || w.mu == EmptyLocker { if w.mu == nil || w.mu == empty {
log.Println("初始化有效锁")
w.mu = NewRWLocker() w.mu = NewRWLocker()
} }
return w
} }
func (w *WithRWLocker) Lock() { func (w *RWLocker) Lock() {
w.ensureLocker().Lock() w.ensureLocker().mu.Lock()
} }
func (w *WithRWLocker) TryLock() bool { func (w *RWLocker) TryLock() bool {
return w.ensureLocker().TryLock() return w.ensureLocker().mu.TryLock()
} }
func (w *WithRWLocker) Unlock() { func (w *RWLocker) Unlock() {
w.ensureLocker().Unlock() w.ensureLocker().mu.Unlock()
} }
func (w *WithRWLocker) RLock() { func (w *RWLocker) RLock() {
w.ensureLocker().RLock() w.ensureLocker().mu.RLock()
} }
func (w *WithRWLocker) TryRLock() bool { func (w *RWLocker) TryRLock() bool {
return w.ensureLocker().TryRLock() return w.ensureLocker().mu.TryRLock()
} }
func (w *WithRWLocker) RUnlock() { func (w *RWLocker) RUnlock() {
w.ensureLocker().RUnlock() w.ensureLocker().mu.RUnlock()
} }
func (w *WithRWLocker) ensureLocker() RWLocker { func (w *RWLocker) ensureLocker() *RWLocker {
w.once.Do(func() { w.once.Do(func() {
if w.mu == nil { if w.mu == nil {
log.Println("初始化一个空锁") log.Println("初始化一个空锁")
w.mu = EmptyLocker w.mu = empty
} }
}) })
return w.mu return w
} }

View File

@ -0,0 +1,44 @@
package locker_test
import (
"testing"
"github.com/charlienet/go-mixed/locker"
)
func TestLocker(t *testing.T) {
var l locker.Locker
l.Synchronize()
l.Lock()
defer l.Unlock()
}
func TestNew(t *testing.T) {
var a locker.RWLocker
a.Synchronize()
}
func TestSpinLocker(t *testing.T) {
var l locker.SpinLocker
l.Synchronize()
l.Lock()
defer l.Unlock()
}
func TestRWLocker(t *testing.T) {
var l locker.RWLocker
l.Lock()
}
func TestPointLocker(t *testing.T) {
l := locker.NewLocker()
l.Lock()
l.Lock()
defer l.Unlock()
}

View File

@ -2,9 +2,8 @@ package rand
import ( import (
mrnd "math/rand" mrnd "math/rand"
"sync"
"time" "time"
"github.com/charlienet/go-mixed/locker"
) )
// 随机数生成器接口 // 随机数生成器接口
@ -47,13 +46,12 @@ var (
type mathRandGenerator struct { type mathRandGenerator struct {
source mrnd.Source source mrnd.Source
r locker.Locker r *sync.Mutex
} }
func NewRandGenerator() *mathRandGenerator { func NewRandGenerator() *mathRandGenerator {
return &mathRandGenerator{ return &mathRandGenerator{
source: mrnd.NewSource(getSeed()), source: mrnd.NewSource(getSeed()),
r: locker.NewSpinLocker(),
} }
} }

View File

@ -1,3 +1,3 @@
# 杂物工具库 # 杂物工具库
go >= 1.18 常用库

View File

@ -2,9 +2,12 @@ package redis
import ( import (
"context" "context"
"fmt"
"strings"
"time" "time"
"github.com/go-redis/redis/v8" "github.com/charlienet/go-mixed/expr"
"github.com/redis/go-redis/v9"
) )
const ( const (
@ -12,94 +15,142 @@ const (
blockingQueryTimeout = 5 * time.Second blockingQueryTimeout = 5 * time.Second
readWriteTimeout = 2 * time.Second readWriteTimeout = 2 * time.Second
defaultSlowThreshold = time.Millisecond * 100 // 慢查询 defaultSlowThreshold = "5000" // 慢查询(单位微秒)
) )
type Option func(r *Redis) var Nil = redis.Nil
type Redis struct { type RedisOption struct {
addr string // 服务器地址 Addr string
prefix string // 键值前缀 Addrs []string
separator string // 分隔符 Password string // 密码
Prefix string
Separator string
// Database to be selected after connecting to the server.
// Only single-node and failover clients.
DB int
MaxRetries int
MinRetryBackoff time.Duration
MaxRetryBackoff time.Duration
DialTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
ContextTimeoutEnabled bool
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
PoolFIFO bool
PoolSize int
PoolTimeout time.Duration
MinIdleConns int
MaxIdleConns int
ConnMaxIdleTime time.Duration
ConnMaxLifetime time.Duration
} }
func New(addr string, opts ...Option) *Redis { var _ Client = redisClient{}
r := &Redis{
addr: addr,
}
return r type Clients []Client
func (clients Clients) LoadFunction(code string) {
for _, c := range clients {
c.LoadFunction(code)
}
} }
func (s *Redis) Set(ctx context.Context, key, value string) error { type Client interface {
conn, err := s.getRedis() redis.UniversalClient
if err != nil { LoadFunction(f string) // 加载函数脚本
return err Prefix() string // 统一前缀
} Separator() string // 分隔符
JoinKeys(keys ...string) string // 连接KEY
return conn.Set(ctx, s.formatKey(key), value, 0).Err() FormatKeys(keys ...string) []string // 格式化KEY
} }
func (s *Redis) Get(ctx context.Context, key string) (string, error) { type redisClient struct {
conn, err := s.getRedis() redis.UniversalClient
if err != nil { prefix string
return "", err separator string
}
return conn.Get(ctx, s.formatKey(key)).Result()
} }
func (s *Redis) GetSet(ctx context.Context, key, value string) (string, error) { func New(opt *RedisOption) redisClient {
conn, err := s.getRedis() var rdb redisClient
if err != nil {
return "", err if len(opt.Addrs) == 0 && len(opt.Addr) > 0 {
opt.Addrs = []string{opt.Addr}
} }
val, err := conn.GetSet(ctx, s.formatKey(key), value).Result() separator := expr.Ternary(len(opt.Separator) == 0, defaultSeparator, opt.Separator)
return val, err prefix := expr.Ternary(len(opt.Prefix) > 0, fmt.Sprintf("%s%s", opt.Prefix, separator), "")
}
func (s *Redis) Del(ctx context.Context, key ...string) (int, error) { rdb = redisClient{
conn, err := s.getRedis() prefix: prefix,
if err != nil { separator: separator,
return 0, err UniversalClient: redis.NewUniversalClient(&redis.UniversalOptions{
} Addrs: opt.Addrs,
Password: opt.Password,
keys := s.formatKeys(key...) DB: opt.DB,
v, err := conn.Del(ctx, keys...).Result()
if err != nil {
return 0, err
}
return int(v), err MaxRetries: opt.MaxRetries,
} MinRetryBackoff: opt.MinRetryBackoff,
MaxRetryBackoff: opt.MaxRetryBackoff,
func (s *Redis) getRedis() (redis.UniversalClient, error) { DialTimeout: opt.DialTimeout,
client := redis.NewUniversalClient(&redis.UniversalOptions{ ReadTimeout: opt.ReadTimeout,
Addrs: []string{s.addr}, WriteTimeout: opt.WriteTimeout,
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
PoolSize: opt.PoolSize,
PoolTimeout: opt.PoolTimeout,
MinIdleConns: opt.MinIdleConns,
MaxIdleConns: opt.MaxIdleConns,
ConnMaxIdleTime: opt.ConnMaxIdleTime,
ConnMaxLifetime: opt.ConnMaxLifetime,
})}
rdb.ConfigSet(context.Background(), "slowlog-log-slower-than", defaultSlowThreshold)
if len(opt.Prefix) > 0 {
rdb.AddHook(renameKey{
prefix: prefix,
}) })
}
return client, nil return rdb
} }
func (s *Redis) formatKeys(keys ...string) []string { func (rdb redisClient) Prefix() string {
// If no prefix is configured, this parameter is returned return rdb.prefix
if s.prefix == "" { }
func (rdb redisClient) LoadFunction(code string) {
_, err := rdb.FunctionLoadReplace(context.Background(), code).Result()
if err != nil {
panic(err)
}
}
func (rdb redisClient) Separator() string {
return rdb.separator
}
func (rdb redisClient) JoinKeys(keys ...string) string {
return strings.Join(keys, rdb.separator)
}
func (rdb redisClient) FormatKeys(keys ...string) []string {
if len(rdb.prefix) == 0 {
return keys return keys
} }
ret := make([]string, 0, len(keys)) re := make([]string, 0, len(keys))
for _, k := range keys { for _, k := range keys {
ret = append(ret, s.formatKey(k)) re = append(re, fmt.Sprintf("%s%s", rdb.prefix, k))
} }
return ret return re
}
func (s *Redis) formatKey(key string) string {
if s.prefix == "" {
return key
}
return s.prefix + s.separator + key
} }

View File

@ -1,86 +1,152 @@
package redis package redis_test
import ( import (
"context" "context"
"log" "fmt"
"strconv"
"sync"
"testing" "testing"
"time" "time"
"github.com/alicebob/miniredis/v2" "github.com/charlienet/go-mixed/redis"
"github.com/charlienet/go-mixed/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestGetSet(t *testing.T) { func TestGetSet(t *testing.T) {
runOnRedis(t, func(client *Redis) { tests.RunOnRedis(t, func(client redis.Client) {
ctx := context.Background() ctx := context.Background()
val, err := client.GetSet(ctx, "hello", "world") val, err := client.GetSet(ctx, "hello", "world").Result()
assert.NotNil(t, err) assert.NotNil(t, err)
assert.Equal(t, "", val) assert.Equal(t, "", val)
val, err = client.Get(ctx, "hello") val, err = client.Get(ctx, "hello").Result()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "world", val) assert.Equal(t, "world", val)
val, err = client.GetSet(ctx, "hello", "newworld") val, err = client.GetSet(ctx, "hello", "newworld").Result()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "world", val) assert.Equal(t, "world", val)
val, err = client.Get(ctx, "hello") val, err = client.Get(ctx, "hello").Result()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "newworld", val) assert.Equal(t, "newworld", val)
ret, err := client.Del(ctx, "hello") ret, err := client.Del(ctx, "hello").Result()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, ret) assert.Equal(t, 1, ret)
}) })
} }
func TestRedis_SetGetDel(t *testing.T) { func TestRedis_SetGetDel(t *testing.T) {
runOnRedis(t, func(client *Redis) { tests.RunOnRedis(t, func(client redis.Client) {
ctx := context.Background() ctx := context.Background()
err := client.Set(ctx, "hello", "world") _, err := client.Set(ctx, "hello", "world", 0).Result()
assert.Nil(t, err) assert.Nil(t, err)
val, err := client.Get(ctx, "hello") val, err := client.Get(ctx, "hello").Result()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "world", val) assert.Equal(t, "world", val)
ret, err := client.Del(ctx, "hello") ret, err := client.Del(ctx, "hello").Result()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, ret) assert.Equal(t, int64(1), ret)
}) })
} }
func runOnRedis(t *testing.T, fn func(client *Redis)) { func TestPubSub(t *testing.T) {
redis, clean, err := CreateMiniRedis() tests.RunOnRedis(t, func(client redis.Client) {
assert.Nil(t, err) ctx := context.Background()
defer clean() c := "chat"
quit := false
fn(redis) total := 0
} mu := &sync.Mutex{}
f := func(wg *sync.WaitGroup) {
wg.Add(1)
var receivedCount int = 0
func CreateMiniRedis() (r *Redis, clean func(), err error) { sub := client.Subscribe(ctx, c)
mr, err := miniredis.Run() defer sub.Close()
if err != nil {
return nil, nil, err
}
addr := mr.Addr()
log.Println("mini redis run at:", addr)
return New(addr), func() {
ch := make(chan struct{})
go func() {
mr.Close()
close(ch)
}()
for {
select { select {
case <-ch: case <-sub.Channel():
case <-time.After(time.Second): receivedCount++
// case <-quit:
default:
if quit {
mu.Lock()
total += receivedCount
mu.Unlock()
t.Logf("Subscriber received %d message %d", receivedCount, total)
wg.Done()
return
} }
}, nil }
}
// for msg := range sub.Channel() {
// if strings.EqualFold(msg.Payload, "quit") {
// break
// }
// receivedCount++
// }
}
var wg = &sync.WaitGroup{}
go f(wg)
go f(wg)
go f(wg)
for i := 0; i < 20000; i++ {
n, err := client.Publish(ctx, c, fmt.Sprintf("hello %d", i)).Result()
if err != nil {
t.Log(err)
}
_ = n
// t.Logf("%d clients received the message\n", n)
}
// for i := 0; i < 20; i++ {
// client.Publish(ctx, c, "quit")
// }
t.Log("finished send message")
time.Sleep(time.Second * 5)
quit = true
wg.Wait()
time.Sleep(time.Second * 2)
t.Logf("total received %d message", total)
})
}
func TestRedisPool(t *testing.T) {
tests.RunOnRedis(t, func(client redis.Client) {
defer client.Close()
err := client.ConfigSet(context.Background(), "slowlog-log-slower-than", strconv.FormatInt(int64(time.Microsecond)*5, 10)).Err()
assert.Nil(t, err, err)
t.Log(client.ConfigGet(context.Background(), "slowlog-log-slower-than").Result())
// client.FunctionLoadReplace(context.Background(), "")
}, redis.RedisOption{
Addr: "192.168.123.100:6379",
PoolSize: 100,
PoolFIFO: true,
})
} }

102
redis/rename_hook.go Normal file
View File

@ -0,0 +1,102 @@
package redis
import (
"context"
"net"
"strings"
"github.com/redis/go-redis/v9"
)
type renameKey struct {
prefix string
separator string
}
func (r renameKey) DialHook(next redis.DialHook) redis.DialHook {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
return next(ctx, network, addr)
}
}
func (r renameKey) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
return func(ctx context.Context, cmds []redis.Cmder) error {
// 对多个KEY进行更名操作
for i := 0; i < len(cmds); i++ {
r.renameKey(cmds[i])
}
return next(ctx, cmds)
}
}
func (r renameKey) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
r.renameKey(cmd)
return next(ctx, cmd)
}
}
func (r renameKey) renameKey(cmd redis.Cmder) {
if len(r.prefix) == 0 {
return
}
args := cmd.Args()
if len(args) == 1 {
return
}
switch strings.ToUpper(cmd.Name()) {
case "SELECT", "FUNCTION":
// 无KEY指令
case
"RENAME", "RENAMENX",
"MGET",
"RPOPLPUSH",
"SDIFF", "SDIFFSTORE", "SINTER", "SINTERSTORE",
"SUNION", "SUNIONSTORE",
"WATCH":
// 连续KEY
r.rename(args, createSepuence(1, len(args), 1)...)
case
"BLPOP", "BRPOP",
"BRPOPLPUSH ",
"SMOVE":
// 除最后一个外连续键
r.rename(args, createSepuence(1, len(args)-1, 1)...)
case "MSET", "MSETNX":
// 间隔KEYKEY位置规则1,3,5,7
r.rename(args, createSepuence(1, len(args), 2)...)
case "EVAL", "EVALSHA", "EVALSHA_RO", "FCALL", "FCALL_RO":
// 命令中包含键数量
if n, ok := args[2].(int); ok && n > 0 {
r.rename(args, createSepuence(3, 3+n, 1)...)
}
default:
// 默认第一个参数为键值
r.rename(args, 1)
}
}
func (r renameKey) rename(args []any, indexes ...int) {
for _, i := range indexes {
if key, ok := args[i].(string); ok {
var builder strings.Builder
builder.WriteString(r.prefix)
builder.WriteString(r.separator)
builder.WriteString(key)
args[i] = builder.String()
}
}
}
func createSepuence(start, end, step int) []int {
ret := make([]int, 0, (end-start)/step+1)
for i := start; i < end; i += step {
ret = append(ret, i)
}
return ret
}

27
redis/rename_hook_test.go Normal file
View File

@ -0,0 +1,27 @@
package redis
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRename(t *testing.T) {
New(&RedisOption{
Addrs: []string{"192.168.123.100:6379"},
})
}
func TestEvalName(t *testing.T) {
rdb := New(&RedisOption{
Addrs: []string{"192.168.123.100:6379"},
Prefix: "aabbcc",
})
_, err := rdb.Eval(context.Background(), "return 1", []string{"a1", "a2", "a3"}, "b1", "b2", "b3").Result()
assert.Nil(t, err, err)
v, err := rdb.FunctionLoadReplace(context.Background(), "#!lua name=mylib\nredis.register_function('myfunc1', function() return 'hello world' end)").Result()
t.Log(v, err)
}

View File

@ -14,13 +14,12 @@ var _ Set[string] = &hash_set[string]{}
type hash_set[T constraints.Ordered] struct { type hash_set[T constraints.Ordered] struct {
m map[T]struct{} m map[T]struct{}
lock locker.RWLocker locker locker.RWLocker
} }
func NewHashSet[T constraints.Ordered](values ...T) *hash_set[T] { func NewHashSet[T constraints.Ordered](values ...T) *hash_set[T] {
set := hash_set[T]{ set := hash_set[T]{
m: make(map[T]struct{}, len(values)), m: make(map[T]struct{}, len(values)),
lock: locker.EmptyLocker,
} }
set.Add(values...) set.Add(values...)
@ -28,37 +27,41 @@ func NewHashSet[T constraints.Ordered](values ...T) *hash_set[T] {
} }
func (s *hash_set[T]) Sync() *hash_set[T] { func (s *hash_set[T]) Sync() *hash_set[T] {
s.lock = locker.NewRWLocker() s.locker.Synchronize()
return s return s
} }
func (s hash_set[T]) Add(values ...T) { func (s *hash_set[T]) Add(values ...T) Set[T] {
s.lock.Lock() s.locker.Lock()
defer s.lock.Unlock() defer s.locker.Unlock()
for _, v := range values { for _, v := range values {
s.m[v] = struct{}{} s.m[v] = struct{}{}
} }
return s
} }
func (s hash_set[T]) Remove(v T) { func (s *hash_set[T]) Remove(v T) Set[T] {
s.lock.Lock() s.locker.Lock()
defer s.lock.Unlock() defer s.locker.Unlock()
delete(s.m, v) delete(s.m, v)
return s
} }
func (s hash_set[T]) Contains(value T) bool { func (s *hash_set[T]) Contains(value T) bool {
s.lock.RLock() s.locker.RLock()
defer s.lock.RUnlock() defer s.locker.RUnlock()
_, ok := s.m[value] _, ok := s.m[value]
return ok return ok
} }
func (s hash_set[T]) ContainsAny(values ...T) bool { func (s *hash_set[T]) ContainsAny(values ...T) bool {
s.lock.RLock() s.locker.RLock()
defer s.lock.RUnlock() defer s.locker.RUnlock()
for _, v := range values { for _, v := range values {
if _, ok := s.m[v]; ok { if _, ok := s.m[v]; ok {
@ -69,9 +72,9 @@ func (s hash_set[T]) ContainsAny(values ...T) bool {
return false return false
} }
func (s hash_set[T]) ContainsAll(values ...T) bool { func (s *hash_set[T]) ContainsAll(values ...T) bool {
s.lock.RLock() s.locker.RLock()
defer s.lock.RUnlock() defer s.locker.RUnlock()
for _, v := range values { for _, v := range values {
if _, ok := s.m[v]; !ok { if _, ok := s.m[v]; !ok {
@ -82,15 +85,15 @@ func (s hash_set[T]) ContainsAll(values ...T) bool {
return true return true
} }
func (s hash_set[T]) Asc() Set[T] { func (s *hash_set[T]) Asc() Set[T] {
return s.copyToSorted().Asc() return s.copyToSorted().Asc()
} }
func (s hash_set[T]) Desc() Set[T] { func (s *hash_set[T]) Desc() Set[T] {
return s.copyToSorted().Desc() return s.copyToSorted().Desc()
} }
func (s hash_set[T]) copyToSorted() Set[T] { func (s *hash_set[T]) copyToSorted() Set[T] {
orderd := NewSortedSet[T]() orderd := NewSortedSet[T]()
for k := range s.m { for k := range s.m {
orderd.Add(k) orderd.Add(k)
@ -109,13 +112,13 @@ func (s *hash_set[T]) Clone() *hash_set[T] {
return set return set
} }
func (s hash_set[T]) Iterate(fn func(value T)) { func (s *hash_set[T]) Iterate(fn func(value T)) {
for v := range s.m { for v := range s.m {
fn(v) fn(v)
} }
} }
func (s hash_set[T]) ToSlice() []T { func (s *hash_set[T]) ToSlice() []T {
values := make([]T, 0, s.Size()) values := make([]T, 0, s.Size())
s.Iterate(func(value T) { s.Iterate(func(value T) {
values = append(values, value) values = append(values, value)
@ -124,15 +127,15 @@ func (s hash_set[T]) ToSlice() []T {
return values return values
} }
func (s hash_set[T]) IsEmpty() bool { func (s *hash_set[T]) IsEmpty() bool {
return len(s.m) == 0 return len(s.m) == 0
} }
func (s hash_set[T]) Size() int { func (s *hash_set[T]) Size() int {
return len(s.m) return len(s.m)
} }
func (s hash_set[T]) MarshalJSON() ([]byte, error) { func (s *hash_set[T]) MarshalJSON() ([]byte, error) {
items := make([]string, 0, s.Size()) items := make([]string, 0, s.Size())
for ele := range s.m { for ele := range s.m {
@ -147,7 +150,7 @@ func (s hash_set[T]) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("[%s]", strings.Join(items, ", "))), nil return []byte(fmt.Sprintf("[%s]", strings.Join(items, ", "))), nil
} }
func (s hash_set[T]) UnmarshalJSON(b []byte) error { func (s *hash_set[T]) UnmarshalJSON(b []byte) error {
var i []any var i []any
d := json.NewDecoder(bytes.NewReader(b)) d := json.NewDecoder(bytes.NewReader(b))
@ -166,7 +169,7 @@ func (s hash_set[T]) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (s hash_set[T]) String() string { func (s *hash_set[T]) String() string {
l := make([]string, 0, len(s.m)) l := make([]string, 0, len(s.m))
for k := range s.m { for k := range s.m {
l = append(l, fmt.Sprint(k)) l = append(l, fmt.Sprint(k))

View File

@ -1,15 +1,13 @@
package sets package sets
import ( import (
"sync"
"github.com/charlienet/go-mixed/locker" "github.com/charlienet/go-mixed/locker"
"golang.org/x/exp/constraints" "golang.org/x/exp/constraints"
) )
type Set[T comparable] interface { type Set[T comparable] interface {
Add(...T) Add(...T) Set[T]
Remove(v T) Remove(v T) Set[T]
Asc() Set[T] Asc() Set[T]
Desc() Set[T] Desc() Set[T]
Contains(T) bool Contains(T) bool
@ -19,18 +17,16 @@ type Set[T comparable] interface {
ToSlice() []T // 转换为切片 ToSlice() []T // 转换为切片
} }
var defaultOptions = option{locker: locker.NewEmptyLocker()}
type option struct { type option struct {
locker sync.Locker locker locker.Locker
} }
type setFunc func(option) type setFunc func(*option)
func WithSync() setFunc { func WithSync() setFunc {
return func(o option) { return func(o *option) {
o.locker = &sync.RWMutex{} o.locker.Synchronize()
} }
} }

79
tests/redis.go Normal file
View File

@ -0,0 +1,79 @@
package tests
import (
"context"
"log"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/charlienet/go-mixed/redis"
"github.com/stretchr/testify/assert"
)
var DefaultRedis = redis.RedisOption{
Addr: "redis:6379",
Password: "123456",
}
func RunOnDefaultRedis(t assert.TestingT, fn func(rdb redis.Client)) {
RunOnRedis(t, fn, DefaultRedis)
}
func RunOnRedis(t assert.TestingT, fn func(rdb redis.Client), opt ...redis.RedisOption) {
var redis redis.Client
var clean func()
var err error
redis, clean, err = CreateRedis(opt...)
assert.Nil(t, err, err)
defer clean()
fn(redis)
}
func CreateRedis(opt ...redis.RedisOption) (r redis.Client, clean func(), err error) {
if len(opt) > 0 {
return createRedisClient(opt[0])
} else {
return createMiniRedis()
}
}
func createRedisClient(opt redis.RedisOption) (r redis.Client, clean func(), err error) {
rdb := redis.New(&opt)
if err := rdb.Ping(context.Background()).Err(); err != nil {
return nil, nil, err
}
return rdb, func() { rdb.Close() }, nil
}
func createMiniRedis() (r redis.Client, clean func(), err error) {
mr, err := miniredis.Run()
if err != nil {
return nil, nil, err
}
addr := mr.Addr()
log.Println("mini redis run at:", addr)
rdb := redis.New(&redis.RedisOption{
Addrs: []string{addr},
})
return rdb, func() {
ch := make(chan struct{})
go func() {
rdb.Close()
mr.Close()
close(ch)
}()
select {
case <-ch:
case <-time.After(time.Second * 5):
}
}, nil
}