mirror of
https://github.com/charlienet/go-mixed.git
synced 2025-07-18 16:42:41 +08:00
Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
822932fe15 | |||
85c5a611e1 | |||
fe5c0b54b6 | |||
54fbe8eb0a | |||
2d851d4872 | |||
a83ccf7c00 | |||
bb979f5ccb | |||
330d9f78d3 | |||
5428b530b2 | |||
249d3b4682 | |||
bdbf18969e | |||
01f426c5b2 | |||
2f2af226ee | |||
5f065de145 | |||
f3918dd02b | |||
1c5a32e828 | |||
5a17236fd7 | |||
91a5a7d612 | |||
0d124c0b79 | |||
6647f96978 | |||
d1c269ed90 | |||
e83db7daee | |||
69690da6b4 | |||
1203e27c7e | |||
42b5cef555 | |||
bcfb177fd9 | |||
0df55ed551 | |||
95ad0941a8 | |||
165fc91f9b | |||
b0a97978d8 |
@ -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
20
bloom/bloom.lua
Normal 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)
|
@ -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)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
13
bytesconv/readme.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
功能列表
|
||||||
|
|
||||||
|
1. 无内存消耗的字符串转字节数组,字节数组转字符串
|
||||||
|
StringToBytes、BytesToString
|
||||||
|
|
||||||
|
2. 字节数组转换函数
|
||||||
|
ByteResult
|
||||||
|
From
|
||||||
|
To
|
||||||
|
|
||||||
|
3. 对象的二进制序列化和反序列化
|
||||||
|
Encode、Decode
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
31
cache/big_cache.go → cache/bigcache/big_cache.go
vendored
31
cache/big_cache.go → cache/bigcache/big_cache.go
vendored
@ -1,4 +1,4 @@
|
|||||||
package cache
|
package bigcache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -8,8 +8,6 @@ import (
|
|||||||
"github.com/charlienet/go-mixed/logx"
|
"github.com/charlienet/go-mixed/logx"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ MemCache = &bigCacheClient{}
|
|
||||||
|
|
||||||
type BigCacheConfig struct {
|
type BigCacheConfig struct {
|
||||||
Shards int
|
Shards int
|
||||||
LifeWindow time.Duration
|
LifeWindow time.Duration
|
||||||
@ -49,8 +47,13 @@ func NewBigCache(c BigCacheConfig) (*bigCacheClient, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *bigCacheClient) Get(key string) ([]byte, error) {
|
func (c *bigCacheClient) Get(key string) ([]byte, bool) {
|
||||||
return c.cache.Get(key)
|
b, err := c.cache.Get(key)
|
||||||
|
if err == nil {
|
||||||
|
return b, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *bigCacheClient) Set(key string, entry []byte, expire time.Duration) error {
|
func (c *bigCacheClient) Set(key string, entry []byte, expire time.Duration) error {
|
||||||
@ -68,9 +71,23 @@ func (c *bigCacheClient) Delete(keys ...string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *bigCacheClient) Exist(key string) {
|
func (c *bigCacheClient) Exist(key string) bool {
|
||||||
|
_, err := c.cache.Get(key)
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return !errors.Is(err, bigcache.ErrEntryNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *bigCacheClient) Clear() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *bigCacheClient) IsNotFound(err error) bool {
|
func (c *bigCacheClient) IsNotFound(err error) bool {
|
||||||
return errors.Is(err, bigcache.ErrEntryNotFound)
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return !errors.Is(err, bigcache.ErrEntryNotFound)
|
||||||
}
|
}
|
27
cache/bigcache/big_cache_test.go
vendored
Normal file
27
cache/bigcache/big_cache_test.go
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package bigcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBigCache(t *testing.T) {
|
||||||
|
r := require.New(t)
|
||||||
|
|
||||||
|
c, err := NewBigCache(BigCacheConfig{})
|
||||||
|
r.Nil(err)
|
||||||
|
|
||||||
|
cacheKey := "a"
|
||||||
|
cacheValue := "bbb"
|
||||||
|
|
||||||
|
c.Set(cacheKey, []byte(cacheValue), time.Second*5)
|
||||||
|
|
||||||
|
r.True(c.Exist(cacheKey))
|
||||||
|
r.False(c.Exist("abb"))
|
||||||
|
|
||||||
|
b, ok := c.Get(cacheKey)
|
||||||
|
r.True(ok)
|
||||||
|
r.Equal(cacheValue, string(b))
|
||||||
|
}
|
144
cache/cache.go
vendored
144
cache/cache.go
vendored
@ -6,62 +6,73 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charlienet/go-mixed/bytesconv"
|
|
||||||
"github.com/charlienet/go-mixed/json"
|
|
||||||
"github.com/charlienet/go-mixed/locker"
|
"github.com/charlienet/go-mixed/locker"
|
||||||
"github.com/charlienet/go-mixed/logx"
|
"github.com/charlienet/go-mixed/logx"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrNotFound = errors.New("key not found")
|
var ErrNotFound = errors.New("key not found")
|
||||||
|
|
||||||
|
// 数据加载函数定义
|
||||||
type LoadFunc func(context.Context) (any, error)
|
type LoadFunc func(context.Context) (any, error)
|
||||||
|
|
||||||
|
type ICache interface {
|
||||||
|
}
|
||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
prefix string // 键前缀
|
prefix string // 键前缀
|
||||||
retry int // 资源获取时的重试次数
|
retry int // 资源获取时的重试次数
|
||||||
mem MemCache // 内存缓存
|
mem MemCache // 内存缓存
|
||||||
distributdCache DistributdCache // 分布式缓存
|
rds DistributedCache // 远程缓存
|
||||||
publishSubscribe PublishSubscribe // 发布订阅
|
publishSubscribe PublishSubscribe // 发布订阅
|
||||||
|
group singleflight.Group // singleflight.Group
|
||||||
lock locker.ChanLocker // 资源锁
|
lock locker.ChanLocker // 资源锁
|
||||||
stats *Stats // 缓存命中计数
|
stats *Stats // 缓存命中计数
|
||||||
qps *qps // 访问计数
|
qps *qps // 访问计数
|
||||||
logger logx.Logger // 日志记录
|
logger logx.Logger // 日志记录
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCache(opts ...option) *Cache {
|
func New(opts ...option) (*Cache, error) {
|
||||||
|
|
||||||
c := acquireDefaultCache()
|
c := acquireDefaultCache()
|
||||||
for _, f := range opts {
|
for _, f := range opts {
|
||||||
if err := f(c); err != nil {
|
if err := f(c); err != nil {
|
||||||
return c
|
return c, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go c.subscribe()
|
// 未设置内存缓存时,添加默认缓存
|
||||||
|
if c.mem == nil {
|
||||||
return c
|
c.mem = NewTinyLFU(1<<12, time.Second*30)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Set(key string, value any, expiration time.Duration) error {
|
return c, nil
|
||||||
if c.mem != nil {
|
}
|
||||||
bytes, err := bytesconv.Encode(value)
|
|
||||||
|
func (c *Cache) Set(ctx context.Context, key string, value any, expiration time.Duration) error {
|
||||||
|
buf, err := Marshal(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.mem.Set(key, bytes, expiration)
|
if c.mem != nil {
|
||||||
|
c.mem.Set(key, buf, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.rds != nil {
|
||||||
|
c.rds.Set(ctx, key, buf, expiration)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Get(key string, out any) error {
|
func (c *Cache) Get(ctx context.Context, key string, out any) error {
|
||||||
if c.mem != nil {
|
if c.mem != nil {
|
||||||
c.getFromMem(key, out)
|
c.getFromMem(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.distributdCache != nil {
|
if c.rds != nil {
|
||||||
if err := c.distributdCache.Get(key, out); err != nil {
|
if err := c.rds.Get(ctx, key, out); err != nil {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,7 +81,7 @@ func (c *Cache) Get(key string, out any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) GetFn(ctx context.Context, key string, out any, fn LoadFunc, expiration time.Duration) (bool, error) {
|
func (c *Cache) GetFn(ctx context.Context, key string, out any, fn LoadFunc, expiration time.Duration) (bool, error) {
|
||||||
c.Get(key, out)
|
c.Get(ctx, key, out)
|
||||||
|
|
||||||
// 多级缓存中未找到时,放置缓存对象
|
// 多级缓存中未找到时,放置缓存对象
|
||||||
ret, err := fn(ctx)
|
ret, err := fn(ctx)
|
||||||
@ -78,7 +89,7 @@ func (c *Cache) GetFn(ctx context.Context, key string, out any, fn LoadFunc, exp
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Set(key, ret, expiration)
|
c.Set(ctx, key, ret, expiration)
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@ -87,32 +98,69 @@ func (c *Cache) Exist(key string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Delete(key ...string) error {
|
func (c *Cache) Delete(ctx context.Context, key ...string) error {
|
||||||
if c.mem != nil {
|
if c.mem != nil {
|
||||||
c.mem.Delete(key...)
|
c.mem.Delete(key...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.distributdCache != nil {
|
if c.rds != nil {
|
||||||
c.distributdCache.Delete(key...)
|
c.rds.Delete(ctx, key...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range key {
|
||||||
|
c.group.Forget(k)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) subscribe() {
|
// 清除本地缓存
|
||||||
|
func (c *Cache) ClearMem() {
|
||||||
|
if c.mem != nil {
|
||||||
|
c.mem.Clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) getFromMem(key string, out any) error {
|
func (c *Cache) Clear() {
|
||||||
bytes, err := c.mem.Get(key)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bytesconv.Decode(bytes, out); err != nil {
|
func (c *Cache) Disable() {
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
func (c *Cache) Enable() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) getOnce(ctx context.Context, key string) (b []byte, cached bool, err error) {
|
||||||
|
if c.mem != nil {
|
||||||
|
b, ok := c.mem.Get(key)
|
||||||
|
if ok {
|
||||||
|
return b, true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.group.Do(key, func() (any, error) {
|
||||||
|
if c.mem != nil {
|
||||||
|
b, ok := c.mem.Get(key)
|
||||||
|
if ok {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.rds != nil {
|
||||||
|
c.rds.Get(ctx, key, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) getFromMem(key string) ([]byte, bool) {
|
||||||
|
bytes, cached := c.mem.Get(key)
|
||||||
|
return bytes, cached
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从缓存加载数据
|
// 从缓存加载数据
|
||||||
@ -125,8 +173,6 @@ func (c *Cache) getFromCache() {
|
|||||||
// 从数据源加载数据
|
// 从数据源加载数据
|
||||||
func (c *Cache) getFromSource(ctx context.Context, key string, fn LoadFunc) error {
|
func (c *Cache) getFromSource(ctx context.Context, key string, fn LoadFunc) error {
|
||||||
|
|
||||||
// 1. 尝试获取资源锁,如成功获取到锁加载数据
|
|
||||||
// 2. 未获取到锁,等待从缓存中获取
|
|
||||||
ch, ok := c.lock.Get(key)
|
ch, ok := c.lock.Get(key)
|
||||||
if ok {
|
if ok {
|
||||||
defer c.lock.Release(key)
|
defer c.lock.Release(key)
|
||||||
@ -150,39 +196,3 @@ func (c *Cache) getFromSource(ctx context.Context, key string, fn LoadFunc) erro
|
|||||||
return c.getFromSource(ctx, key, fn)
|
return c.getFromSource(ctx, key, fn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) marshal(value any) ([]byte, error) {
|
|
||||||
switch value := value.(type) {
|
|
||||||
case nil:
|
|
||||||
return nil, nil
|
|
||||||
case []byte:
|
|
||||||
return value, nil
|
|
||||||
case string:
|
|
||||||
return []byte(value), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(value)
|
|
||||||
return b, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache) unmarshal(b []byte, value any) error {
|
|
||||||
if len(b) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch value := value.(type) {
|
|
||||||
case nil:
|
|
||||||
return nil
|
|
||||||
case *[]byte:
|
|
||||||
clone := make([]byte, len(b))
|
|
||||||
copy(clone, b)
|
|
||||||
*value = clone
|
|
||||||
return nil
|
|
||||||
case *string:
|
|
||||||
*value = string(b)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.Unmarshal(b, value)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
117
cache/cache_builder.go
vendored
117
cache/cache_builder.go
vendored
@ -1,6 +1,14 @@
|
|||||||
package cache
|
package cache
|
||||||
|
|
||||||
import "github.com/charlienet/go-mixed/logx"
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charlienet/go-mixed/cache/bigcache"
|
||||||
|
"github.com/charlienet/go-mixed/cache/freecache"
|
||||||
|
"github.com/charlienet/go-mixed/logx"
|
||||||
|
"github.com/charlienet/go-mixed/redis"
|
||||||
|
)
|
||||||
|
|
||||||
const defaultPrefix = "cache"
|
const defaultPrefix = "cache"
|
||||||
|
|
||||||
@ -10,90 +18,59 @@ type options struct {
|
|||||||
Prefix string
|
Prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func acquireDefaultCache() *Cache {
|
func WithRedis(rdb redis.Client) option {
|
||||||
return &Cache{
|
return func(c *Cache) error {
|
||||||
prefix: defaultPrefix,
|
rds := NewRedis(rdb)
|
||||||
qps: NewQps(),
|
c.rds = rds
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||||
|
defer cancel()
|
||||||
|
return rds.Ping(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type cacheBuilder struct {
|
func WithBigCache(opts bigcache.BigCacheConfig) option {
|
||||||
prefix string
|
return func(c *Cache) error {
|
||||||
redisOptions RedisConfig
|
mem, err := bigcache.NewBigCache(opts)
|
||||||
bigCacheConfig BigCacheConfig
|
|
||||||
freeSize int
|
c.mem = mem
|
||||||
publishSubscribe PublishSubscribe
|
return err
|
||||||
log logx.Logger
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCacheBuilder() *cacheBuilder {
|
func WithFreeCache(size int) option {
|
||||||
return &cacheBuilder{}
|
return func(c *Cache) error {
|
||||||
}
|
mem := freecache.NewFreeCache(size)
|
||||||
|
c.mem = mem
|
||||||
|
|
||||||
func (b *cacheBuilder) WithLogger(log logx.Logger) *cacheBuilder {
|
return nil
|
||||||
b.log = log
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *cacheBuilder) WithPrefix(prefix string) *cacheBuilder {
|
|
||||||
b.prefix = prefix
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *cacheBuilder) WithRedis(opts RedisConfig) *cacheBuilder {
|
|
||||||
b.redisOptions = opts
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *cacheBuilder) WithBigCache(opts BigCacheConfig) *cacheBuilder {
|
|
||||||
b.bigCacheConfig = opts
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *cacheBuilder) WithFreeCache(size int) *cacheBuilder {
|
|
||||||
b.freeSize = size
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用自定义分布式缓存
|
// 使用自定义分布式缓存
|
||||||
func WithDistributedCache(c DistributdCache) {
|
func WithDistributedCache(rds DistributedCache) option {
|
||||||
|
return func(c *Cache) error {
|
||||||
|
c.rds = rds
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *cacheBuilder) WithPublishSubscribe(p PublishSubscribe) *cacheBuilder {
|
func WithPublishSubscribe(p PublishSubscribe) option {
|
||||||
b.publishSubscribe = p
|
return func(c *Cache) error {
|
||||||
return b
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b cacheBuilder) Build() (*Cache, error) {
|
func WithLogger(log logx.Logger) option {
|
||||||
var err error
|
return func(c *Cache) error {
|
||||||
cache := acquireDefaultCache()
|
c.logger = log
|
||||||
if len(b.prefix) > 0 {
|
|
||||||
cache.prefix = b.prefix
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.redisOptions.Prefix = cache.prefix
|
func acquireDefaultCache() *Cache {
|
||||||
|
return &Cache{
|
||||||
redis := NewRedis(b.redisOptions)
|
qps: NewQps(),
|
||||||
if err := redis.Ping(); err != nil {
|
|
||||||
return cache, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var mem MemCache
|
|
||||||
if b.freeSize > 0 {
|
|
||||||
mem = NewFreeCache(b.freeSize)
|
|
||||||
} else {
|
|
||||||
if b.log != nil {
|
|
||||||
b.bigCacheConfig.log = b.log
|
|
||||||
}
|
|
||||||
|
|
||||||
mem, err = NewBigCache(b.bigCacheConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.distributdCache = redis
|
|
||||||
cache.mem = mem
|
|
||||||
cache.publishSubscribe = b.publishSubscribe
|
|
||||||
cache.logger = b.log
|
|
||||||
|
|
||||||
return cache, err
|
|
||||||
}
|
}
|
||||||
|
47
cache/cache_builder_test.go
vendored
47
cache/cache_builder_test.go
vendored
@ -3,27 +3,38 @@ package cache
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charlienet/go-mixed/logx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuilder(t *testing.T) {
|
func TestBuilder(t *testing.T) {
|
||||||
cache, err := NewCacheBuilder().
|
now := time.Now()
|
||||||
WithLogger(logx.NewLogrus(logx.WithFormatter(logx.NewNestedFormatter(logx.NestedFormatterOption{
|
t.Log(now)
|
||||||
Color: true,
|
t1, _ := time.ParseDuration("9h27m")
|
||||||
})))).
|
t1 += time.Hour * 24
|
||||||
WithRedis(RedisConfig{
|
t2, _ := time.ParseDuration("16h28m")
|
||||||
Addrs: []string{"192.168.2.222:6379"},
|
t.Log(t1)
|
||||||
Password: "123456",
|
t.Log(t2)
|
||||||
}).
|
|
||||||
WithBigCache(BigCacheConfig{}).
|
|
||||||
// WithFreeCache(10 * 1024 * 1024).
|
|
||||||
Build()
|
|
||||||
|
|
||||||
if err != nil {
|
f := time.Date(2022, time.December, 12, 8, 0, 0, 0, time.Local)
|
||||||
t.Fatal(err)
|
t.Log(f.Sub(time.Now()))
|
||||||
}
|
|
||||||
|
|
||||||
u := SimpleUser{FirstName: "Radomir", LastName: "Sohlich"}
|
// cache, err := New(
|
||||||
t.Log(cache.Set(defaultKey, u, time.Minute*10))
|
// WithLogger(logx.NewLogrus(logx.WithNestedFormatter(logx.NestedFormatterOption{
|
||||||
|
// Color: true,
|
||||||
|
// }))).
|
||||||
|
// UseRedis(RedisConfig{
|
||||||
|
// Addrs: []string{"192.168.2.222:6379"},
|
||||||
|
// Password: "123456",
|
||||||
|
// }).
|
||||||
|
// UseBigCache(bigcache.BigCacheConfig{}).
|
||||||
|
// Build()
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatal(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||||
|
// defer cancel()
|
||||||
|
|
||||||
|
// u := SimpleUser{FirstName: "Radomir", LastName: "Sohlich"}
|
||||||
|
// t.Log(cache.Set(ctx, defaultKey, u, time.Minute*10))
|
||||||
}
|
}
|
||||||
|
12
cache/cache_preload.go
vendored
Normal file
12
cache/cache_preload.go
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// PreLoadItem 预加载数据项
|
||||||
|
type PreLoadItem struct {
|
||||||
|
Key string
|
||||||
|
Value any
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreloadFunc 数据预加载函数定义
|
||||||
|
type PreloadFunc func(context.Context) ([]PreLoadItem, error)
|
93
cache/cache_test.go
vendored
93
cache/cache_test.go
vendored
@ -7,8 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charlienet/go-mixed/bytesconv"
|
"github.com/charlienet/go-mixed/redis"
|
||||||
"github.com/charlienet/go-mixed/logx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -16,25 +15,26 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNewCache(t *testing.T) {
|
func TestNewCache(t *testing.T) {
|
||||||
c, err := NewCacheBuilder().
|
// c, err := NewCacheBuilder().
|
||||||
WithRedis(RedisConfig{
|
// UseRedis(RedisConfig{
|
||||||
Addrs: []string{"192.168.2.222:6379"},
|
// Addrs: []string{"192.168.2.222:6379"},
|
||||||
Password: "123456",
|
// Password: "123456",
|
||||||
}).
|
// }).
|
||||||
WithPrefix("cache_test").
|
// WithPrefix("cache_test").
|
||||||
WithLogger(logx.NewLogrus()).
|
// WithLogger(logx.NewLogrus()).
|
||||||
Build()
|
// Build()
|
||||||
|
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
t.Fatal(err)
|
// t.Fatal(err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
c.Set("abc", "value", time.Minute*10)
|
// ctx := context.Background()
|
||||||
|
// c.Set(ctx, "abc", "value", time.Minute*10)
|
||||||
|
|
||||||
var s string
|
// var s string
|
||||||
c.Get("abc", &s)
|
// c.Get(ctx, "abc", &s)
|
||||||
|
|
||||||
t.Log(s)
|
// t.Log(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SimpleUser struct {
|
type SimpleUser struct {
|
||||||
@ -43,45 +43,45 @@ type SimpleUser struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMemCache(t *testing.T) {
|
func TestMemCache(t *testing.T) {
|
||||||
b, _ := NewBigCache(BigCacheConfig{})
|
// b, _ := bigcache.NewBigCache(bigcache.BigCacheConfig{})
|
||||||
var mems = []MemCache{
|
// var mems = []MemCache{
|
||||||
NewFreeCache(10 * 1024 * 1024),
|
// NewFreeCache(10 * 1024 * 1024),
|
||||||
b,
|
// b,
|
||||||
}
|
// }
|
||||||
|
|
||||||
u := SimpleUser{FirstName: "Radomir", LastName: "Sohlich"}
|
// u := SimpleUser{FirstName: "Radomir", LastName: "Sohlich"}
|
||||||
encoded, _ := bytesconv.Encode(u)
|
// encoded, _ := bytesconv.Encode(u)
|
||||||
for _, m := range mems {
|
// for _, m := range mems {
|
||||||
m.Set(defaultKey, encoded, time.Second)
|
// m.Set(defaultKey, encoded, time.Second)
|
||||||
ret, err := m.Get(defaultKey)
|
// ret, _ := m.Get(defaultKey)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var u2 SimpleUser
|
// var u2 SimpleUser
|
||||||
bytesconv.Decode(ret, &u2)
|
// bytesconv.Decode(ret, &u2)
|
||||||
t.Log(u2)
|
// t.Log(u2)
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
||||||
|
}))
|
||||||
|
|
||||||
if err := c.Ping(); err != nil {
|
ctx := context.Background()
|
||||||
|
if err := c.Ping(ctx); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Log(c.Exist(defaultKey))
|
t.Log(c.Exist(ctx, defaultKey))
|
||||||
|
|
||||||
u := SimpleUser{FirstName: "redis client"}
|
u := SimpleUser{FirstName: "redis client"}
|
||||||
|
|
||||||
var u2 SimpleUser
|
var u2 SimpleUser
|
||||||
c.Get(defaultKey, &u2)
|
c.Get(ctx, defaultKey, &u2)
|
||||||
|
|
||||||
c.Set(defaultKey, u, time.Minute*10)
|
c.Set(ctx, defaultKey, u, time.Minute*10)
|
||||||
t.Log(c.Exist(defaultKey))
|
t.Log(c.Exist(ctx, defaultKey))
|
||||||
|
|
||||||
if err := c.Get(defaultKey, &u2); err != nil {
|
if err := c.Get(ctx, defaultKey, &u2); err != nil {
|
||||||
t.Fatal("err:", err)
|
t.Fatal("err:", err)
|
||||||
}
|
}
|
||||||
t.Logf("%+v", u2)
|
t.Logf("%+v", u2)
|
||||||
@ -136,10 +136,13 @@ func load() (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildCache() *Cache {
|
func buildCache() *Cache {
|
||||||
c, err := NewCacheBuilder().
|
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{
|
||||||
Build()
|
Addrs: []string{"192.168.2.222:6379"},
|
||||||
|
DB: 6,
|
||||||
|
Password: "123456",
|
||||||
|
})))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
10
cache/distributd_cache.go
vendored
10
cache/distributd_cache.go
vendored
@ -1,10 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type DistributdCache interface {
|
|
||||||
Get(key string, out any) error
|
|
||||||
Set(key string, value any, expiration time.Duration) error
|
|
||||||
Delete(key ...string) error
|
|
||||||
Ping() error
|
|
||||||
}
|
|
14
cache/distributed_cache.go
vendored
Normal file
14
cache/distributed_cache.go
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 分布式缓存接口
|
||||||
|
type DistributedCache interface {
|
||||||
|
Get(ctx context.Context, key string, out any) error
|
||||||
|
Set(ctx context.Context, key string, value any, expiration time.Duration) error
|
||||||
|
Delete(ctx context.Context, key ...string) error
|
||||||
|
Ping(ctx context.Context) error
|
||||||
|
}
|
10
cache/empty_cache_adaper.go
vendored
Normal file
10
cache/empty_cache_adaper.go
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// var emptyCache DistributedCache = &emptyCacheAdapter{}
|
||||||
|
|
||||||
|
type emptyCacheAdapter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*emptyCacheAdapter) Delete(ctx context.Context, keys ...string) {}
|
@ -1,4 +1,4 @@
|
|||||||
package cache
|
package freecache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -10,8 +10,6 @@ import (
|
|||||||
|
|
||||||
const defaultSize = 10 * 1024 * 1024 // 10M
|
const defaultSize = 10 * 1024 * 1024 // 10M
|
||||||
|
|
||||||
var _ MemCache = &freeCache{}
|
|
||||||
|
|
||||||
type freeCache struct {
|
type freeCache struct {
|
||||||
cache *freecache.Cache
|
cache *freecache.Cache
|
||||||
}
|
}
|
||||||
@ -29,8 +27,12 @@ func NewFreeCache(size int) *freeCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *freeCache) Get(key string) ([]byte, error) {
|
func (c *freeCache) Get(key string) ([]byte, bool) {
|
||||||
return c.cache.Get([]byte(key))
|
b, err := c.cache.Get([]byte(key))
|
||||||
|
if err != nil {
|
||||||
|
return b, false
|
||||||
|
}
|
||||||
|
return b, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *freeCache) Set(key string, value []byte, d time.Duration) error {
|
func (c *freeCache) Set(key string, value []byte, d time.Duration) error {
|
||||||
@ -54,6 +56,10 @@ func (c *freeCache) Exist(key string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *freeCache) Clear() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (c *freeCache) IsNotFound(err error) bool {
|
func (c *freeCache) IsNotFound(err error) bool {
|
||||||
return errors.Is(err, freecache.ErrNotFound)
|
return errors.Is(err, freecache.ErrNotFound)
|
||||||
}
|
}
|
1
cache/freecache/free_cache_test.go
vendored
Normal file
1
cache/freecache/free_cache_test.go
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
package freecache
|
7
cache/local_cache.go
vendored
7
cache/local_cache.go
vendored
@ -1,7 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
type LocalCache interface {
|
|
||||||
Set(key string, data []byte)
|
|
||||||
Get(key string) ([]byte, bool)
|
|
||||||
Del(key string)
|
|
||||||
}
|
|
2
cache/lru.go
vendored
Normal file
2
cache/lru.go
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
package cache
|
||||||
|
|
5
cache/mem.go
vendored
5
cache/mem.go
vendored
@ -3,7 +3,8 @@ package cache
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type MemCache interface {
|
type MemCache interface {
|
||||||
Get(key string) ([]byte, error)
|
Get(key string) ([]byte, bool)
|
||||||
Set(key string, entry []byte, expire time.Duration) error
|
Set(key string, b []byte, expire time.Duration) error
|
||||||
Delete(key ...string) error
|
Delete(key ...string) error
|
||||||
|
Clear()
|
||||||
}
|
}
|
||||||
|
43
cache/msg_pack.go
vendored
Normal file
43
cache/msg_pack.go
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmihailenco/msgpack/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Marshal(v any) ([]byte, error) {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case nil:
|
||||||
|
return nil, nil
|
||||||
|
case []byte:
|
||||||
|
return v, nil
|
||||||
|
case string:
|
||||||
|
return []byte(v), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := msgpack.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unmarshal(b []byte, v any) error {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := v.(type) {
|
||||||
|
case nil:
|
||||||
|
return nil
|
||||||
|
case *[]byte:
|
||||||
|
clone := make([]byte, len(b))
|
||||||
|
copy(clone, b)
|
||||||
|
*v = clone
|
||||||
|
case *string:
|
||||||
|
*v = string(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgpack.Unmarshal(b, v)
|
||||||
|
}
|
6
cache/qps.go
vendored
6
cache/qps.go
vendored
@ -31,7 +31,9 @@ func (q *qps) statisticsTotal() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Second)
|
ticker := time.NewTicker(time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
q.all.viewTotal = atomic.SwapInt64(&q.all.total, 0)
|
q.all.viewTotal = atomic.SwapInt64(&q.all.total, 0)
|
||||||
q.memoryTotal.viewTotal = atomic.SwapInt64(&q.memoryTotal.total, 0)
|
q.memoryTotal.viewTotal = atomic.SwapInt64(&q.memoryTotal.total, 0)
|
||||||
@ -39,5 +41,7 @@ func (q *qps) statisticsTotal() {
|
|||||||
q.redisTotal.viewTotal = atomic.SwapInt64(&q.redisTotal.total, 0)
|
q.redisTotal.viewTotal = atomic.SwapInt64(&q.redisTotal.total, 0)
|
||||||
q.redisHit.viewTotal = atomic.SwapInt64(&q.redisHit.total, 0)
|
q.redisHit.viewTotal = atomic.SwapInt64(&q.redisHit.total, 0)
|
||||||
q.sourceTotal.viewTotal = atomic.SwapInt64(&q.sourceTotal.total, 0)
|
q.sourceTotal.viewTotal = atomic.SwapInt64(&q.sourceTotal.total, 0)
|
||||||
|
|
||||||
|
// percnt := 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
cache/readme.md
vendored
13
cache/readme.md
vendored
@ -1,4 +1,7 @@
|
|||||||
# 多级缓存模块
|
# 二级缓存模块
|
||||||
|
|
||||||
|
提供本地缓存和分布式缓存组合的缓存模块,可以只使用本地缓存或分布式缓存。并可全局禁用缓存。
|
||||||
|
|
||||||
|
|
||||||
1. 一级缓存可使用freecache或bigcache作为本地缓存,当数据在本地缓存不存在时,会向二级缓存请求数据
|
1. 一级缓存可使用freecache或bigcache作为本地缓存,当数据在本地缓存不存在时,会向二级缓存请求数据
|
||||||
2. 二级缓存使用redis作为缓存模块,当数据在二级缓存不存在时向资源请求数据。
|
2. 二级缓存使用redis作为缓存模块,当数据在二级缓存不存在时向资源请求数据。
|
||||||
@ -14,6 +17,14 @@
|
|||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
|
创建
|
||||||
|
|
||||||
|
```go
|
||||||
|
cache.New().UseRedis().UseBigCache().Build()
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
```go
|
```go
|
||||||
Cache.Get(key, dist, func() (bool,error){}, options func(){})
|
Cache.Get(key, dist, func() (bool,error){}, options func(){})
|
||||||
|
Cache.GetFn(context, key, dist, func() (bool, error))
|
||||||
```
|
```
|
||||||
|
47
cache/redis.go
vendored
47
cache/redis.go
vendored
@ -9,52 +9,25 @@ 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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *redisClient) Get(key string, out any) error {
|
func (c *redisClient) Get(cxt context.Context, key string, out any) error {
|
||||||
val, err := c.client.Get(context.Background(), c.getKey(key)).Result()
|
val, err := c.client.Get(context.Background(), c.getKey(key)).Result()
|
||||||
if errors.Is(err, redis.Nil) {
|
if errors.Is(err, redis.Nil) {
|
||||||
return ErrNotFound
|
return ErrNotFound
|
||||||
@ -72,17 +45,17 @@ func (c *redisClient) Get(key string, out any) error {
|
|||||||
return json.Unmarshal(bytesconv.StringToBytes(val), out)
|
return json.Unmarshal(bytesconv.StringToBytes(val), out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *redisClient) Set(key string, value any, expiration time.Duration) error {
|
func (c *redisClient) Set(ctx context.Context, key string, value any, expiration time.Duration) error {
|
||||||
j, _ := json.Marshal(value)
|
j, _ := json.Marshal(value)
|
||||||
return c.client.Set(context.Background(), c.getKey(key), j, expiration).Err()
|
return c.client.Set(context.Background(), c.getKey(key), j, expiration).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *redisClient) Exist(key string) (bool, error) {
|
func (c *redisClient) Exist(ctx context.Context, key string) (bool, error) {
|
||||||
val, err := c.client.Exists(context.Background(), c.getKey(key)).Result()
|
val, err := c.client.Exists(context.Background(), c.getKey(key)).Result()
|
||||||
return val > 0, err
|
return val > 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *redisClient) Delete(key ...string) error {
|
func (c *redisClient) Delete(ctx context.Context, key ...string) error {
|
||||||
keys := make([]string, 0, len(key))
|
keys := make([]string, 0, len(key))
|
||||||
for _, k := range key {
|
for _, k := range key {
|
||||||
keys = append(keys, c.getKey(k))
|
keys = append(keys, c.getKey(k))
|
||||||
@ -96,8 +69,8 @@ func (c *redisClient) Delete(key ...string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *redisClient) Ping() error {
|
func (c *redisClient) Ping(ctx context.Context) error {
|
||||||
_, err := c.client.Ping(context.Background()).Result()
|
_, err := c.client.Ping(ctx).Result()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
cache/stats.go
vendored
4
cache/stats.go
vendored
@ -7,11 +7,11 @@ type Stats struct {
|
|||||||
Misses uint64
|
Misses uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stats) AddHits() {
|
func (s *Stats) IncrementHits() {
|
||||||
atomic.AddUint64(&s.Hits, 1)
|
atomic.AddUint64(&s.Hits, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stats) AddMisses() {
|
func (s *Stats) IncrementMisses() {
|
||||||
atomic.AddUint64(&s.Misses, 1)
|
atomic.AddUint64(&s.Misses, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
cache/tiny_lfu.go
vendored
18
cache/tiny_lfu.go
vendored
@ -7,6 +7,8 @@ import (
|
|||||||
"github.com/vmihailenco/go-tinylfu"
|
"github.com/vmihailenco/go-tinylfu"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ MemCache = &TinyLFU{}
|
||||||
|
|
||||||
type TinyLFU struct {
|
type TinyLFU struct {
|
||||||
mu locker.Locker
|
mu locker.Locker
|
||||||
lfu *tinylfu.T
|
lfu *tinylfu.T
|
||||||
@ -21,7 +23,7 @@ func NewTinyLFU(size int, ttl time.Duration) *TinyLFU {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TinyLFU) Set(key string, b []byte, expire time.Duration) {
|
func (c *TinyLFU) Set(key string, b []byte, expire time.Duration) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -30,6 +32,8 @@ func (c *TinyLFU) Set(key string, b []byte, expire time.Duration) {
|
|||||||
Value: b,
|
Value: b,
|
||||||
ExpireAt: time.Now().Add(c.ttl),
|
ExpireAt: time.Now().Add(c.ttl),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TinyLFU) Get(key string) ([]byte, bool) {
|
func (c *TinyLFU) Get(key string) ([]byte, bool) {
|
||||||
@ -44,9 +48,17 @@ func (c *TinyLFU) Get(key string) ([]byte, bool) {
|
|||||||
return val.([]byte), true
|
return val.([]byte), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TinyLFU) Del(key string) {
|
func (c *TinyLFU) Delete(keys ...string) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
c.lfu.Del(key)
|
for _, k := range keys {
|
||||||
|
c.lfu.Del(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TinyLFU) Clear() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,3 +9,16 @@ func ParseDuration(s string) (time.Duration, error) {
|
|||||||
|
|
||||||
return time.ParseDuration(s)
|
return time.ParseDuration(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseDurationDefault(s string, d time.Duration) time.Duration {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := time.ParseDuration(s)
|
||||||
|
if err != nil {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
14
calendar/scheduled_executor.go
Normal file
14
calendar/scheduled_executor.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package calendar
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ScheduledExecutor struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScheduledExecutor() *ScheduledExecutor {
|
||||||
|
return &ScheduledExecutor{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ScheduledExecutor) Schedule(i any, duration time.Duration) {
|
||||||
|
|
||||||
|
}
|
14
calendar/scheduled_executor_test.go
Normal file
14
calendar/scheduled_executor_test.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package calendar_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charlienet/go-mixed/calendar"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExecutor(t *testing.T) {
|
||||||
|
executor := calendar.NewScheduledExecutor()
|
||||||
|
|
||||||
|
executor.Schedule(nil, time.Minute)
|
||||||
|
}
|
@ -1,9 +1,5 @@
|
|||||||
package list
|
package list
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/charlienet/go-mixed/locker"
|
|
||||||
)
|
|
||||||
|
|
||||||
const minCapacity = 16
|
const minCapacity = 16
|
||||||
|
|
||||||
type ArrayList[T any] struct {
|
type ArrayList[T any] struct {
|
||||||
@ -31,7 +27,7 @@ func NewArrayList[T any](elems ...T) *ArrayList[T] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
l := &ArrayList[T]{
|
l := &ArrayList[T]{
|
||||||
list: list[T]{size: size, locker: locker.EmptyLocker},
|
list: list[T]{size: size},
|
||||||
buf: buf,
|
buf: buf,
|
||||||
tail: tail,
|
tail: tail,
|
||||||
minCap: minCap,
|
minCap: minCap,
|
||||||
@ -45,8 +41,8 @@ func NewArrayList[T any](elems ...T) *ArrayList[T] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *ArrayList[T]) PushFront(v T) {
|
func (l *ArrayList[T]) PushFront(v T) {
|
||||||
l.locker.Lock()
|
l.mu.Lock()
|
||||||
defer l.locker.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
l.grow()
|
l.grow()
|
||||||
|
|
||||||
@ -56,8 +52,8 @@ func (l *ArrayList[T]) PushFront(v T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *ArrayList[T]) PushBack(v T) {
|
func (l *ArrayList[T]) PushBack(v T) {
|
||||||
l.locker.Lock()
|
l.mu.Lock()
|
||||||
defer l.locker.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
l.grow()
|
l.grow()
|
||||||
|
|
||||||
@ -68,8 +64,8 @@ func (l *ArrayList[T]) PushBack(v T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *ArrayList[T]) PopFront() T {
|
func (l *ArrayList[T]) PopFront() T {
|
||||||
l.locker.Lock()
|
l.mu.Lock()
|
||||||
defer l.locker.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
if l.size <= 0 {
|
if l.size <= 0 {
|
||||||
panic("list: PopFront() called on empty list")
|
panic("list: PopFront() called on empty list")
|
||||||
@ -86,8 +82,8 @@ func (l *ArrayList[T]) PopFront() T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *ArrayList[T]) PopBack() T {
|
func (l *ArrayList[T]) PopBack() T {
|
||||||
l.locker.Lock()
|
l.mu.Lock()
|
||||||
defer l.locker.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
l.tail = l.prev(l.tail)
|
l.tail = l.prev(l.tail)
|
||||||
|
|
||||||
@ -105,8 +101,8 @@ func (l *ArrayList[T]) RemoveAt(at int) T {
|
|||||||
panic(ErrorOutOffRange)
|
panic(ErrorOutOffRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.locker.Lock()
|
l.mu.Lock()
|
||||||
defer l.locker.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
rm := (l.head + at) & (len(l.buf) - 1)
|
rm := (l.head + at) & (len(l.buf) - 1)
|
||||||
if at*2 < l.size {
|
if at*2 < l.size {
|
||||||
@ -127,22 +123,22 @@ func (l *ArrayList[T]) RemoveAt(at int) T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *ArrayList[T]) Front() T {
|
func (l *ArrayList[T]) Front() T {
|
||||||
l.locker.RLock()
|
l.mu.RLock()
|
||||||
defer l.locker.RUnlock()
|
defer l.mu.RUnlock()
|
||||||
|
|
||||||
return l.buf[l.head]
|
return l.buf[l.head]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ArrayList[T]) Back() T {
|
func (l *ArrayList[T]) Back() T {
|
||||||
l.locker.RLock()
|
l.mu.RLock()
|
||||||
defer l.locker.RUnlock()
|
defer l.mu.RUnlock()
|
||||||
|
|
||||||
return l.buf[l.tail]
|
return l.buf[l.tail]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ArrayList[T]) ForEach(fn func(T)) {
|
func (l *ArrayList[T]) ForEach(fn func(T)) {
|
||||||
l.locker.RLock()
|
l.mu.RLock()
|
||||||
defer l.locker.RUnlock()
|
defer l.mu.RUnlock()
|
||||||
|
|
||||||
n := l.head
|
n := l.head
|
||||||
for i := 0; i < l.size; i++ {
|
for i := 0; i < l.size; i++ {
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
package list
|
package list
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/charlienet/go-mixed/locker"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LinkedList[T any] struct {
|
type LinkedList[T any] struct {
|
||||||
list[T]
|
list[T]
|
||||||
front, tail *LinkedNode[T]
|
front, tail *LinkedNode[T]
|
||||||
@ -16,9 +12,7 @@ type LinkedNode[T any] struct {
|
|||||||
|
|
||||||
// NewLinkedList 初始化链表
|
// NewLinkedList 初始化链表
|
||||||
func NewLinkedList[T any](elems ...T) *LinkedList[T] {
|
func NewLinkedList[T any](elems ...T) *LinkedList[T] {
|
||||||
l := &LinkedList[T]{
|
l := &LinkedList[T]{}
|
||||||
list: list[T]{locker: locker.EmptyLocker},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range elems {
|
for _, e := range elems {
|
||||||
l.pushBackNode(&LinkedNode[T]{Value: e})
|
l.pushBackNode(&LinkedNode[T]{Value: e})
|
||||||
@ -28,8 +22,8 @@ func NewLinkedList[T any](elems ...T) *LinkedList[T] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *LinkedList[T]) PushBack(v T) *LinkedList[T] {
|
func (l *LinkedList[T]) PushBack(v T) *LinkedList[T] {
|
||||||
l.locker.Lock()
|
l.mu.Lock()
|
||||||
defer l.locker.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
l.pushBackNode(&LinkedNode[T]{Value: v})
|
l.pushBackNode(&LinkedNode[T]{Value: v})
|
||||||
|
|
||||||
@ -37,8 +31,8 @@ func (l *LinkedList[T]) PushBack(v T) *LinkedList[T] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *LinkedList[T]) PushFront(v T) *LinkedList[T] {
|
func (l *LinkedList[T]) PushFront(v T) *LinkedList[T] {
|
||||||
l.locker.Lock()
|
l.mu.Lock()
|
||||||
defer l.locker.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
l.pushFrontNode(&LinkedNode[T]{Value: v})
|
l.pushFrontNode(&LinkedNode[T]{Value: v})
|
||||||
|
|
||||||
@ -65,8 +59,8 @@ func (l *LinkedList[T]) Back() T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *LinkedList[T]) ForEach(fn func(T) bool) {
|
func (l *LinkedList[T]) ForEach(fn func(T) bool) {
|
||||||
l.locker.RLock()
|
l.mu.RLock()
|
||||||
defer l.locker.RUnlock()
|
defer l.mu.RUnlock()
|
||||||
|
|
||||||
for current := l.front; current != nil; current = current.Next {
|
for current := l.front; current != nil; current = current.Next {
|
||||||
if fn(current.Value) {
|
if fn(current.Value) {
|
||||||
@ -77,12 +71,10 @@ func (l *LinkedList[T]) ForEach(fn func(T) bool) {
|
|||||||
|
|
||||||
func (l *LinkedList[T]) GetAt(i int) T {
|
func (l *LinkedList[T]) GetAt(i int) T {
|
||||||
if i <= l.Size() {
|
if i <= l.Size() {
|
||||||
var n int
|
for n, current := 0, l.front; current != nil; current, n = current.Next, n+1 {
|
||||||
for current := l.front; current != nil; current = current.Next {
|
|
||||||
if n == i {
|
if n == i {
|
||||||
return current.Value
|
return current.Value
|
||||||
}
|
}
|
||||||
n++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,8 +82,8 @@ func (l *LinkedList[T]) GetAt(i int) T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *LinkedList[T]) Remove(n *LinkedNode[T]) {
|
func (l *LinkedList[T]) Remove(n *LinkedNode[T]) {
|
||||||
l.locker.Lock()
|
l.mu.Lock()
|
||||||
defer l.locker.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
if n.Next != nil {
|
if n.Next != nil {
|
||||||
n.Next.Prev = n.Prev
|
n.Next.Prev = n.Prev
|
||||||
@ -112,8 +104,8 @@ func (l *LinkedList[T]) Remove(n *LinkedNode[T]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *LinkedList[T]) RemoveAt(index int) {
|
func (l *LinkedList[T]) RemoveAt(index int) {
|
||||||
l.locker.Lock()
|
l.mu.Lock()
|
||||||
defer l.locker.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
var i int
|
var i int
|
||||||
for current := l.front; current != nil; current = current.Next {
|
for current := l.front; current != nil; current = current.Next {
|
||||||
|
@ -13,11 +13,11 @@ type List[T any] interface {
|
|||||||
|
|
||||||
type list[T any] struct {
|
type list[T any] struct {
|
||||||
size int
|
size int
|
||||||
locker locker.RWLocker
|
mu locker.WithRWLocker
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *list[T]) Synchronize() {
|
func (l *list[T]) Synchronize() {
|
||||||
l.locker = locker.NewRWLocker()
|
l.mu.Synchronize()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *list[T]) ForEach(fn func(T) bool) { panic("Not Implemented") }
|
func (l *list[T]) ForEach(fn func(T) bool) { panic("Not Implemented") }
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package collections
|
package collections
|
||||||
|
|
||||||
import "sync"
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
var _ Queue[string] = &ArrayQueue[string]{}
|
var _ Queue[string] = &ArrayQueue[string]{}
|
||||||
|
|
||||||
|
@ -1 +1,10 @@
|
|||||||
package rbtree
|
package rbtree
|
||||||
|
|
||||||
|
type color bool
|
||||||
|
|
||||||
|
const (
|
||||||
|
black, red color = true, false
|
||||||
|
)
|
||||||
|
|
||||||
|
type TreeNode[K any, V any] struct {
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
75
concurrent/delay_queue/delay_queue.go
Normal file
75
concurrent/delay_queue/delay_queue.go
Normal 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()
|
||||||
|
}
|
48
concurrent/delay_queue/delay_queue_test.go
Normal file
48
concurrent/delay_queue/delay_queue_test.go
Normal 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):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
concurrent/delay_queue/kafka_store.go
Normal file
37
concurrent/delay_queue/kafka_store.go
Normal 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
|
||||||
|
}
|
88
concurrent/delay_queue/mem_store.go
Normal file
88
concurrent/delay_queue/mem_store.go
Normal 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
|
||||||
|
}
|
119
concurrent/delay_queue/mem_store_test.go
Normal file
119
concurrent/delay_queue/mem_store_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
154
concurrent/delay_queue/redis_store.go
Normal file
154
concurrent/delay_queue/redis_store.go
Normal 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
|
||||||
|
}
|
89
concurrent/delay_queue/redis_store_test.go
Normal file
89
concurrent/delay_queue/redis_store_test.go
Normal 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
20
concurrent/readme.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
延迟队列
|
||||||
|
|
||||||
|
包含以下实现模型
|
||||||
|
|
||||||
|
1. 内存模式。
|
||||||
|
2. Redis模式
|
||||||
|
3. MQ模式
|
||||||
|
|
||||||
|
|
||||||
|
内存模式,在队列中放入。定时检查过期时间,过期时间到达时取出并使用协程执行。
|
||||||
|
|
||||||
|
Redis模式,使用ZSET存储任务队列。定时取出规则内的任务。取出后使用ZREM删除,并放入执行队列LPUSH。放入成功后在任务执行通道发送消息,执行通道使用LPOP取出并执行。
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
queue := delayqueue.New()
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
7
configure/config.toml
Normal file
7
configure/config.toml
Normal 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
46
configure/configure.go
Normal 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
|
||||||
|
}
|
100
configure/configure_builder.go
Normal file
100
configure/configure_builder.go
Normal 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
|
||||||
|
}
|
42
configure/configure_test.go
Normal file
42
configure/configure_test.go
Normal 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
96
configure/nacos.go
Normal 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
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package crypto
|
package sm2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@ -14,7 +14,7 @@ var (
|
|||||||
C1C2C3 = 1
|
C1C2C3 = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ IAsymmetric = &sm2Instance{}
|
type option func(*sm2Instance) error
|
||||||
|
|
||||||
type sm2Instance struct {
|
type sm2Instance struct {
|
||||||
mode int
|
mode int
|
||||||
@ -22,9 +22,42 @@ type sm2Instance struct {
|
|||||||
puk *s.PublicKey
|
puk *s.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
type option func(*sm2Instance) error
|
func WithSm2PrivateKey(p []byte, pwd []byte) option {
|
||||||
|
return func(so *sm2Instance) error {
|
||||||
|
priv, err := x.ReadPrivateKeyFromPem(p, pwd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func NewSm2(opts ...option) (*sm2Instance, error) {
|
so.prk = priv
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSm2PublicKey(p []byte) option {
|
||||||
|
return func(so *sm2Instance) error {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := x.ReadPublicKeyFromPem(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
so.puk = pub
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMode(mode int) option {
|
||||||
|
return func(so *sm2Instance) error {
|
||||||
|
so.mode = mode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts ...option) (*sm2Instance, error) {
|
||||||
o := &sm2Instance{
|
o := &sm2Instance{
|
||||||
mode: defaultMode,
|
mode: defaultMode,
|
||||||
}
|
}
|
||||||
@ -51,37 +84,6 @@ func NewSm2(opts ...option) (*sm2Instance, error) {
|
|||||||
return o, nil
|
return o, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseSm2PrivateKey(p []byte, pwd []byte) option {
|
|
||||||
return func(so *sm2Instance) error {
|
|
||||||
priv, err := x.ReadPrivateKeyFromPem(p, pwd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
so.prk = priv
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseSm2PublicKey(p []byte) option {
|
|
||||||
return func(so *sm2Instance) error {
|
|
||||||
pub, err := x.ReadPublicKeyFromPem(p)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
so.puk = pub
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithMode(mode int) option {
|
|
||||||
return func(so *sm2Instance) error {
|
|
||||||
so.mode = mode
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *sm2Instance) Encrypt(msg []byte) ([]byte, error) {
|
func (o *sm2Instance) Encrypt(msg []byte) ([]byte, error) {
|
||||||
return s.Encrypt(o.puk, msg, rand.Reader, o.mode)
|
return s.Encrypt(o.puk, msg, rand.Reader, o.mode)
|
||||||
}
|
}
|
@ -1,20 +1,34 @@
|
|||||||
package crypto_test
|
package sm2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/charlienet/go-mixed/crypto"
|
"github.com/tjfoc/gmsm/sm2"
|
||||||
|
x "github.com/tjfoc/gmsm/x509"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestPem(t *testing.T) {
|
||||||
|
|
||||||
|
key, _ := sm2.GenerateKey(rand.Reader)
|
||||||
|
|
||||||
|
prv, _ := x.WritePrivateKeyToPem(key, []byte{})
|
||||||
|
pub, _ := x.WritePublicKeyToPem(key.Public().(*sm2.PublicKey))
|
||||||
|
|
||||||
|
t.Log(x.WritePublicKeyToHex(&key.PublicKey))
|
||||||
|
t.Log(string(prv))
|
||||||
|
t.Log(string(pub))
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewSm2(t *testing.T) {
|
func TestNewSm2(t *testing.T) {
|
||||||
o, err := crypto.NewSm2()
|
o, err := New()
|
||||||
t.Logf("%+v, %v", o, err)
|
t.Logf("%+v, %v", o, err)
|
||||||
|
|
||||||
t.Log(crypto.NewSm2(crypto.ParseSm2PrivateKey([]byte{}, []byte{})))
|
t.Log(New(WithSm2PrivateKey([]byte{}, []byte{})))
|
||||||
|
|
||||||
msg := []byte("123456")
|
msg := []byte("123456")
|
||||||
sign, err := o.Sign(msg)
|
sign, err := o.Sign(msg)
|
||||||
@ -49,9 +63,9 @@ hslcifiQY8173nHtaB3R6T0PwRQTwKbpdec0dwVCpvVcdzHtivndlG0mqQ==
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestPrivatePem(t *testing.T) {
|
func TestPrivatePem(t *testing.T) {
|
||||||
signer, err := crypto.NewSm2(
|
signer, err := New(
|
||||||
crypto.ParseSm2PrivateKey([]byte(privPem), []byte{}),
|
WithSm2PrivateKey([]byte(privPem), []byte{}),
|
||||||
crypto.ParseSm2PublicKey([]byte(pubPem)))
|
WithSm2PublicKey([]byte(pubPem)))
|
||||||
|
|
||||||
t.Log(signer, err)
|
t.Log(signer, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -67,9 +81,9 @@ func TestPrivatePem(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBadPublicPem(t *testing.T) {
|
func TestBadPublicPem(t *testing.T) {
|
||||||
signer, err := crypto.NewSm2(
|
signer, err := New(
|
||||||
crypto.ParseSm2PrivateKey([]byte(privPem), []byte{}),
|
WithSm2PrivateKey([]byte(privPem), []byte{}),
|
||||||
crypto.ParseSm2PublicKey([]byte(badPubPem)))
|
WithSm2PublicKey([]byte(badPubPem)))
|
||||||
|
|
||||||
t.Log(signer, err)
|
t.Log(signer, err)
|
||||||
|
|
1
distributed_locker/consul_store.go
Normal file
1
distributed_locker/consul_store.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package locker
|
166
distributed_locker/distributed_lock.go
Normal file
166
distributed_locker/distributed_lock.go
Normal 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
|
||||||
|
}
|
87
distributed_locker/distributed_lock_test.go
Normal file
87
distributed_locker/distributed_lock_test.go
Normal 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) {
|
||||||
|
}
|
@ -12,6 +12,19 @@ const (
|
|||||||
defaultErrorCode = "999999"
|
defaultErrorCode = "999999"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var NotImplemented = errors.New("Not Implemented")
|
||||||
|
|
||||||
|
type Error interface {
|
||||||
|
Wraped() []error
|
||||||
|
Code() string
|
||||||
|
|
||||||
|
error
|
||||||
|
|
||||||
|
private()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Error = &CodeError{}
|
||||||
|
|
||||||
type CodeError struct {
|
type CodeError struct {
|
||||||
cause error // 原始错误信息
|
cause error // 原始错误信息
|
||||||
code string // 错误码
|
code string // 错误码
|
||||||
@ -70,6 +83,10 @@ func (e *CodeError) WithCause(err error) *CodeError {
|
|||||||
return new(err, e.code, e.message)
|
return new(err, e.code, e.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *CodeError) Wraped() []error {
|
||||||
|
return []error{}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *CodeError) Format(s fmt.State, verb rune) {
|
func (e *CodeError) Format(s fmt.State, verb rune) {
|
||||||
switch verb {
|
switch verb {
|
||||||
case 'v':
|
case 'v':
|
||||||
@ -86,6 +103,8 @@ func (e *CodeError) Format(s fmt.State, verb rune) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*CodeError) private() {}
|
||||||
|
|
||||||
func new(err error, code string, args ...any) *CodeError {
|
func new(err error, code string, args ...any) *CodeError {
|
||||||
return &CodeError{
|
return &CodeError{
|
||||||
code: code,
|
code: code,
|
||||||
@ -102,7 +121,7 @@ func newf(err error, code string, format string, args ...any) *CodeError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Error(code string, args ...any) *CodeError {
|
func ErrorWithCode(code string, args ...any) *CodeError {
|
||||||
return new(nil, code, args...)
|
return new(nil, code, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,8 +16,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
globalError = errors.Error(defaultErrorCode, "全局错误对象")
|
globalError = errors.ErrorWithCode(defaultErrorCode, "全局错误对象")
|
||||||
errorbyCode = errors.Error(testCode, "全局错误对象")
|
errorbyCode = errors.ErrorWithCode(testCode, "全局错误对象")
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPersetError(t *testing.T) {
|
func TestPersetError(t *testing.T) {
|
||||||
@ -40,7 +40,7 @@ func TestWithMessage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewError(t *testing.T) {
|
func TestNewError(t *testing.T) {
|
||||||
var e error = errors.Error("123456", "测试")
|
var e error = errors.ErrorWithCode("123456", "测试")
|
||||||
|
|
||||||
err := e.(*errors.CodeError)
|
err := e.(*errors.CodeError)
|
||||||
t.Log(e, err.Code())
|
t.Log(e, err.Code())
|
||||||
@ -58,17 +58,17 @@ func TestWithStack(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLogMessage(t *testing.T) {
|
func TestLogMessage(t *testing.T) {
|
||||||
t.Logf("%+v", errors.Error("88888", "错误消息"))
|
t.Logf("%+v", errors.ErrorWithCode("88888", "错误消息"))
|
||||||
t.Log(errors.Error("77777"))
|
t.Log(errors.ErrorWithCode("77777"))
|
||||||
t.Log(errors.Error("77777", "测试"))
|
t.Log(errors.ErrorWithCode("77777", "测试"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIs(t *testing.T) {
|
func TestIs(t *testing.T) {
|
||||||
code1 := "000090"
|
code1 := "000090"
|
||||||
code2 := "000091"
|
code2 := "000091"
|
||||||
e1 := errors.Error(code1)
|
e1 := errors.ErrorWithCode(code1)
|
||||||
e2 := errors.Error(code1)
|
e2 := errors.ErrorWithCode(code1)
|
||||||
e3 := errors.Error(code2)
|
e3 := errors.ErrorWithCode(code2)
|
||||||
|
|
||||||
t.Log(errors.Is(e1, e2))
|
t.Log(errors.Is(e1, e2))
|
||||||
t.Log(errors.Is(e1, e3))
|
t.Log(errors.Is(e1, e3))
|
||||||
|
21
file_store/file_store.go
Normal file
21
file_store/file_store.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package filestore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() {
|
||||||
|
f, err := os.Open("")
|
||||||
|
|
||||||
|
_ = err
|
||||||
|
_ = f
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func Store(name string, reader io.Reader) error {
|
||||||
|
|
||||||
|
return errors.New("abc")
|
||||||
|
|
||||||
|
}
|
6
file_store/oss/oss.go
Normal file
6
file_store/oss/oss.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package oss
|
||||||
|
|
||||||
|
// add two int
|
||||||
|
func Add(a, b int) {
|
||||||
|
|
||||||
|
}
|
38
file_store/readme.md
Normal file
38
file_store/readme.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# 文件存储组件
|
||||||
|
|
||||||
|
1. 支持磁盘,OSS,FTP等
|
||||||
|
2. 支持自定义存储
|
||||||
|
3. 支持同时存储到不同的存储目标
|
||||||
|
|
||||||
|
创建文件存储
|
||||||
|
|
||||||
|
```GO
|
||||||
|
|
||||||
|
New().WithStore(LocalDisk("D:\abc"))
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
删除文件
|
||||||
|
|
||||||
|
```GO
|
||||||
|
|
||||||
|
filestore.Delete("filename")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
文件移动
|
||||||
|
|
||||||
|
```GO
|
||||||
|
|
||||||
|
filestore.Move("old", "new")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
文件重命名
|
||||||
|
|
||||||
|
```GO
|
||||||
|
|
||||||
|
filestore.Rename("old", "new")
|
||||||
|
|
||||||
|
```
|
58
go.mod
58
go.mod
@ -1,52 +1,52 @@
|
|||||||
module github.com/charlienet/go-mixed
|
module github.com/charlienet/go-mixed
|
||||||
|
|
||||||
go 1.18
|
go 1.21.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bits-and-blooms/bitset v1.3.3
|
github.com/alicebob/miniredis/v2 v2.31.0
|
||||||
github.com/cespare/xxhash/v2 v2.1.2
|
github.com/allegro/bigcache/v3 v3.1.0
|
||||||
github.com/go-playground/universal-translator v0.18.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/coocood/freecache v1.2.4
|
||||||
|
github.com/dlclark/regexp2 v1.10.0
|
||||||
|
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 (
|
require (
|
||||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // 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
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
|
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect
|
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect
|
||||||
github.com/jonboulle/clockwork v0.3.0 // indirect
|
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||||
github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570 // indirect
|
github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570 // indirect
|
||||||
github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect
|
github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect
|
||||||
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/tebeka/strftime v0.1.5 // indirect
|
github.com/tebeka/strftime v0.1.5 // indirect
|
||||||
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
github.com/yuin/gopher-lua v1.1.0 // indirect
|
||||||
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/alicebob/miniredis/v2 v2.23.0
|
|
||||||
github.com/allegro/bigcache/v3 v3.0.2
|
|
||||||
github.com/alphadose/haxmap v1.0.2
|
|
||||||
github.com/antonfisher/nested-logrus-formatter v1.3.1
|
|
||||||
github.com/coocood/freecache v1.2.2
|
|
||||||
github.com/dlclark/regexp2 v1.7.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.0
|
|
||||||
github.com/stretchr/testify v1.8.0
|
|
||||||
github.com/vmihailenco/go-tinylfu v0.2.2
|
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
|
|
||||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
|
||||||
|
|
||||||
)
|
|
||||||
|
103
go.sum
103
go.sum
@ -1,49 +1,53 @@
|
|||||||
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/miniredis/v2 v2.23.0 h1:+lwAJYjvvdIVg6doFHuotFjueJ/7KY10xo/vm3X3Scw=
|
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
|
||||||
github.com/alicebob/miniredis/v2 v2.23.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88=
|
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||||
github.com/allegro/bigcache/v3 v3.0.2 h1:AKZCw+5eAaVyNTBmI2fgyPVJhHkdWder3O9IrprcQfI=
|
github.com/alicebob/miniredis/v2 v2.31.0 h1:ObEFUNlJwoIiyjxdrYF0QIDE7qXcLc7D3WpSH4c22PU=
|
||||||
github.com/allegro/bigcache/v3 v3.0.2/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
|
github.com/alicebob/miniredis/v2 v2.31.0/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
|
||||||
github.com/alphadose/haxmap v1.0.2 h1:ZZwFf15DcsAz4O+SyqrpH/xeO5Plh7mNRXDM9QIcWQQ=
|
github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
|
||||||
github.com/alphadose/haxmap v1.0.2/go.mod h1:Pq2IXbl9/ytYHfrIAd7rIVtZQ2ezdIhZfvdqOizDeWY=
|
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
|
||||||
|
github.com/alphadose/haxmap v1.3.0 h1:C/2LboOnPCZP27GmmSXOcwx360st0P8N0fTJ3voefKc=
|
||||||
|
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.3.3 h1:R1XWiopGiXf66xygsiLpzLo67xEYvMkHw3w+rCOSAwg=
|
github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88=
|
||||||
github.com/bits-and-blooms/bitset v1.3.3/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
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 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
|
||||||
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=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
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.2 h1:UPkJCxhRujykq1jXuwxAPgDHnm6lKGrLZPnuHzgWRtE=
|
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
|
||||||
github.com/coocood/freecache v1.2.2/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.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
github.com/dlclark/regexp2 v1.7.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=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
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.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
|
||||||
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=
|
||||||
@ -61,8 +65,8 @@ 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.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||||
github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
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=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570 h1:0iQektZGS248WXmGIYOwRXSQhD4qn3icjMpuxwO7qlo=
|
github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570 h1:0iQektZGS248WXmGIYOwRXSQhD4qn3icjMpuxwO7qlo=
|
||||||
@ -76,43 +80,44 @@ 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.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.0/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/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.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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/tebeka/strftime v0.1.5 h1:1NQKN1NiQgkqd/2moD6ySP/5CoZQsKa1d3ZhJ44Jpmg=
|
github.com/tebeka/strftime v0.1.5 h1:1NQKN1NiQgkqd/2moD6ySP/5CoZQsKa1d3ZhJ44Jpmg=
|
||||||
github.com/tebeka/strftime v0.1.5/go.mod h1:29/OidkoWHdEKZqzyDLUyC+LmgDgdHo4WAFCDT7D/Ig=
|
github.com/tebeka/strftime v0.1.5/go.mod h1:29/OidkoWHdEKZqzyDLUyC+LmgDgdHo4WAFCDT7D/Ig=
|
||||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
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/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw=
|
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||||
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
|
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/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
|
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
|
||||||
|
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.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
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-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
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=
|
||||||
@ -122,24 +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.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
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.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||||
|
golang.org/x/sync v0.4.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.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.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.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
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=
|
||||||
@ -162,10 +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.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
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=
|
||||||
|
BIN
idGenerator/assets/双缓存号段分配.webp
Normal file
BIN
idGenerator/assets/双缓存号段分配.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
72
idGenerator/buffer.go
Normal file
72
idGenerator/buffer.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
51
idGenerator/buffer_test.go
Normal file
51
idGenerator/buffer_test.go
Normal 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
132
idGenerator/formater.go
Normal 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)
|
||||||
|
}
|
34
idGenerator/formater_test.go
Normal file
34
idGenerator/formater_test.go
Normal 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
124
idGenerator/generator.go
Normal 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
|
||||||
|
}
|
148
idGenerator/genterator_test.go
Normal file
148
idGenerator/genterator_test.go
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
114
idGenerator/readme.md
Normal file
114
idGenerator/readme.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
id生成服务
|
||||||
|
|
||||||
|
标识生成服务,可用于分布式环境的标识生成服务,本服务可以多实例部署。
|
||||||
|
|
||||||
|
要求
|
||||||
|
1. 全局唯一: 必须保证生成的 ID 是全局性唯一的;
|
||||||
|
2. 趋势递增: 生成的 ID 需要按照某种规则有序,便于数据库的写入和排序操作;
|
||||||
|
3. 单调递增: 后生成的标识大于前面生成的标识
|
||||||
|
3. 可用性: 需要保证高并发下的可用性。
|
||||||
|
4. 自主性: 分布式环境下不依赖中心认证即可自行生成 ID;
|
||||||
|
5. 安全性: 不暴露系统和业务的信息。在一些业务场景下,需要 ID 无规则或者不规则;
|
||||||
|
|
||||||
|
|
||||||
|
标识组成
|
||||||
|
1. 时间信息,从当前时间中根据模板获取数据段。
|
||||||
|
|
||||||
|
|
||||||
|
号段模式
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
批量获取标识,业务应用使用后临近剩余数量时生成新的号段,使用双标识缓冲区方式保存。在活动缓冲区使用量达到限定值时在备缓冲区放置新的号段。
|
||||||
|
|
||||||
|
号段分配器
|
||||||
|
|
||||||
|
数据结构
|
||||||
|
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)步进式持久化:增加一个缓存中间层,内存中缓存最近一个分配出现的 sequence:cur_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 次数。
|
||||||
|
|
||||||
|
|
||||||
|
二进制模式
|
||||||
|
按照二进制模式处理序列号生成,各数据段按照各自的位长度组装
|
||||||
|
|
||||||
|
十进制模式
|
||||||
|
|
||||||
|
|
||||||
|
缺陷
|
||||||
|
|
||||||
|
当发生序列回绕时,不同的时间序列有不同的处理。如时间序列精度为日,那么在发生回绕时认为此时的回绕不能发生。如果应用在当日内被重启,并且发生了回绕,当前机制无法检测此问题。
|
||||||
|
序列记录可以依赖于外部存储,如Redis,DB等。当外部存储失效时序列可能会发生冲突。此时当时间段没有前进时可能发生重复。一般认为DB为不失效存储。
|
||||||
|
|
||||||
|
|
||||||
|
需要烦精确的选择时间段和序列的相对关系,一般情况下
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ID分配器工作流程
|
||||||
|
|
||||||
|
初始化
|
||||||
|
1. 创建生成器
|
||||||
|
2. 创建外部存储
|
||||||
|
3. 指定生成器生成规则,机器码长度,序列长度,序列化格式。
|
||||||
|
4. 创建双缓存,使用外部存储分配数据段。
|
||||||
|
|
||||||
|
|
||||||
|
标识分配
|
||||||
|
1. 向序列缓冲器获取下一序列
|
||||||
|
2. 向时间段分配器获取下一时间戳(传入是否回旋)
|
||||||
|
4. 由标识组装器获取
|
||||||
|
|
||||||
|
格式化器
|
||||||
|
|
||||||
|
十进制格式化,时间段格式串
|
||||||
|
二进制格式化,起始时间,时间精度
|
11
idGenerator/store.go
Normal file
11
idGenerator/store.go
Normal 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()
|
||||||
|
}
|
60
idGenerator/store/mem_store.go
Normal file
60
idGenerator/store/mem_store.go
Normal 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() {
|
||||||
|
|
||||||
|
}
|
21
idGenerator/store/mem_store_test.go
Normal file
21
idGenerator/store/mem_store_test.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
2
idGenerator/store/mysql_store.go
Normal file
2
idGenerator/store/mysql_store.go
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
package store
|
||||||
|
|
99
idGenerator/store/redis_id_store.lua
Normal file
99
idGenerator/store/redis_id_store.lua
Normal 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)
|
145
idGenerator/store/redis_store.go
Normal file
145
idGenerator/store/redis_store.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
106
idGenerator/store/redis_store_test.go
Normal file
106
idGenerator/store/redis_store_test.go
Normal 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"})
|
||||||
|
}
|
49
idGenerator/store/segment.go
Normal file
49
idGenerator/store/segment.go
Normal 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)
|
||||||
|
}
|
38
ketama/ketama.go
Normal file
38
ketama/ketama.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package ketama
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charlienet/go-mixed/locker"
|
||||||
|
"github.com/charlienet/go-mixed/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Ketama struct {
|
||||||
|
mu locker.WithRWLocker
|
||||||
|
replicas int
|
||||||
|
m maps.Map[uint64, string]
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Ketama {
|
||||||
|
return &Ketama{
|
||||||
|
m: maps.NewHashMap[uint64, string](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Ketama) Synchronize() {
|
||||||
|
k.mu.Synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Ketama) Add(nodes ...string) {
|
||||||
|
k.mu.Lock()
|
||||||
|
defer k.mu.Unlock()
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
_ = node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Ketama) IsEmpty() bool {
|
||||||
|
k.mu.RLock()
|
||||||
|
defer k.mu.RUnlock()
|
||||||
|
|
||||||
|
return k.m.Count() == 0
|
||||||
|
}
|
32
ketama/ketama_test.go
Normal file
32
ketama/ketama_test.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package ketama_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/charlienet/go-mixed/ketama"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
k := ketama.New()
|
||||||
|
|
||||||
|
t.Logf("%+v", k)
|
||||||
|
k.Synchronize()
|
||||||
|
|
||||||
|
// k.Lock()
|
||||||
|
|
||||||
|
t.Logf("%+v", k)
|
||||||
|
|
||||||
|
t.Log(k.IsEmpty())
|
||||||
|
t.Logf("%+v", k)
|
||||||
|
t.Log(k.IsEmpty())
|
||||||
|
t.Logf("%+v", k)
|
||||||
|
k.Synchronize()
|
||||||
|
|
||||||
|
t.Log(k.IsEmpty())
|
||||||
|
t.Logf("%+v", k)
|
||||||
|
t.Log(k.IsEmpty())
|
||||||
|
t.Logf("%+v", k)
|
||||||
|
t.Log(k.IsEmpty())
|
||||||
|
|
||||||
|
t.Logf("%+v", k)
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
package locker
|
package locker
|
||||||
|
|
||||||
|
var _ RWLocker = &emptyLocker{}
|
||||||
|
var _ Locker = &emptyLocker{}
|
||||||
|
|
||||||
var EmptyLocker = &emptyLocker{}
|
var EmptyLocker = &emptyLocker{}
|
||||||
|
|
||||||
type emptyLocker struct{}
|
type emptyLocker struct{}
|
||||||
|
113
locker/synchronizeable.go
Normal file
113
locker/synchronizeable.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package locker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/charlienet/go-mixed/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
var empty = &emptyLocker{}
|
||||||
|
|
||||||
|
type Locker struct {
|
||||||
|
once sync.Once
|
||||||
|
distributedLocker DistributedLocker // 分布式锁
|
||||||
|
mu locker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Locker) WithRedis(key string, rdb redis.Client) *Locker {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Locker) Lock() {
|
||||||
|
w.ensureLocker().mu.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Locker) Unlock() {
|
||||||
|
w.ensureLocker().mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Locker) TryLock() bool {
|
||||||
|
return w.ensureLocker().mu.TryLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Locker) ensureLocker() *Locker {
|
||||||
|
w.once.Do(func() {
|
||||||
|
if w.mu == nil {
|
||||||
|
w.mu = empty
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpinLocker struct {
|
||||||
|
Locker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *SpinLocker) Synchronize() {
|
||||||
|
if w.mu == nil || w.mu == empty {
|
||||||
|
w.mu = NewSpinLocker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RWLocker struct {
|
||||||
|
once sync.Once
|
||||||
|
mu rwLocker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RWLocker) Synchronize() *RWLocker {
|
||||||
|
if w.mu == nil || w.mu == empty {
|
||||||
|
w.mu = NewRWLocker()
|
||||||
|
}
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RWLocker) Lock() {
|
||||||
|
w.ensureLocker().mu.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RWLocker) TryLock() bool {
|
||||||
|
return w.ensureLocker().mu.TryLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RWLocker) Unlock() {
|
||||||
|
w.ensureLocker().mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RWLocker) RLock() {
|
||||||
|
w.ensureLocker().mu.RLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RWLocker) TryRLock() bool {
|
||||||
|
return w.ensureLocker().mu.TryRLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RWLocker) RUnlock() {
|
||||||
|
w.ensureLocker().mu.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *RWLocker) ensureLocker() *RWLocker {
|
||||||
|
w.once.Do(func() {
|
||||||
|
if w.mu == nil {
|
||||||
|
log.Println("初始化一个空锁")
|
||||||
|
w.mu = empty
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
@ -4,7 +4,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/charlienet/go-mixed/fs"
|
"github.com/charlienet/go-mixed/fs"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -61,6 +60,12 @@ func WithOptions(o LogrusOptions) logrusOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithNestedFormatter(o NestedFormatterOption) logrusOption {
|
||||||
|
return func(logrusLogger *logrus.Logger) {
|
||||||
|
logrusLogger.Formatter = NewNestedFormatter(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithFormatter(formatter logrus.Formatter) logrusOption {
|
func WithFormatter(formatter logrus.Formatter) logrusOption {
|
||||||
return func(logrusLogger *logrus.Logger) {
|
return func(logrusLogger *logrus.Logger) {
|
||||||
logrusLogger.SetFormatter(formatter)
|
logrusLogger.SetFormatter(formatter)
|
||||||
@ -68,7 +73,6 @@ func WithFormatter(formatter logrus.Formatter) logrusOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func WithOutput(options LogrusOutputOptions) logrusOption {
|
func WithOutput(options LogrusOutputOptions) logrusOption {
|
||||||
_ = time.Now()
|
|
||||||
return func(l *logrus.Logger) {
|
return func(l *logrus.Logger) {
|
||||||
var writer io.Writer
|
var writer io.Writer
|
||||||
switch {
|
switch {
|
||||||
|
@ -3,8 +3,9 @@ package maps
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"golang.org/x/exp/slices"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/charlienet/go-mixed/expr"
|
||||||
xmaps "golang.org/x/exp/maps"
|
xmaps "golang.org/x/exp/maps"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -124,8 +125,12 @@ func (m *sorted_map[K, V]) Asc() SortedMap[K, V] {
|
|||||||
func (m *sorted_map[K, V]) Desc() SortedMap[K, V] {
|
func (m *sorted_map[K, V]) Desc() SortedMap[K, V] {
|
||||||
keys := m.keys
|
keys := m.keys
|
||||||
|
|
||||||
slices.SortFunc(keys, func(a, b K) bool {
|
slices.SortFunc(keys, func(a, b K) int {
|
||||||
return a > b
|
if a == b {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr.Ternary(a > b, -1, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
return &sorted_map[K, V]{
|
return &sorted_map[K, V]{
|
||||||
|
35
mathx/int.go
35
mathx/int.go
@ -1,35 +0,0 @@
|
|||||||
package mathx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/charlienet/go-mixed/expr"
|
|
||||||
"golang.org/x/exp/constraints"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Max returns the larger one of v1 and v2.
|
|
||||||
func Max[T constraints.Ordered](v1, v2 T) T {
|
|
||||||
return expr.Ternary(v1 > v2, v1, v2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Min returns the smaller one of v1 and v2.
|
|
||||||
func Min[T constraints.Ordered](v1, v2 T) T {
|
|
||||||
return expr.Ternary(v1 < v2, v1, v2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Abs1[T constraints.Signed](n T) T {
|
|
||||||
shift := 63
|
|
||||||
switch unsafe.Sizeof(n) {
|
|
||||||
case 1:
|
|
||||||
shift = 7
|
|
||||||
case 4:
|
|
||||||
shift = 31
|
|
||||||
}
|
|
||||||
|
|
||||||
y := n >> shift
|
|
||||||
return T((n ^ y) - y)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Abs(n int64) int64 {
|
|
||||||
y := n >> 63
|
|
||||||
return (n ^ y) - y
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package mathx_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/charlienet/go-mixed/mathx"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMin(t *testing.T) {
|
|
||||||
assert.Equal(t, 1, mathx.Min(1, 3))
|
|
||||||
assert.Equal(t, 2, mathx.Min(66, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMax(t *testing.T) {
|
|
||||||
assert.Equal(t, 3, mathx.Max(1, 3))
|
|
||||||
assert.Equal(t, 66, mathx.Max(66, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAbs(t *testing.T) {
|
|
||||||
assert.Equal(t, 23, mathx.Abs1(23))
|
|
||||||
assert.Equal(t, 23, mathx.Abs1(-23))
|
|
||||||
assert.Equal(t, 0, mathx.Abs1(0))
|
|
||||||
|
|
||||||
var u int8 = -127
|
|
||||||
var exp int8 = 127
|
|
||||||
assert.Equal(t, exp, mathx.Abs1(u))
|
|
||||||
}
|
|
46
mathx/math.go
Normal file
46
mathx/math.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package mathx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charlienet/go-mixed/expr"
|
||||||
|
"golang.org/x/exp/constraints"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Real interface {
|
||||||
|
constraints.Integer | constraints.Float
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max returns the larger one of v1 and v2.
|
||||||
|
func Max[T Real](v1, v2 T) T {
|
||||||
|
return expr.Ternary(v1 > v2, v1, v2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min returns the smaller one of v1 and v2.
|
||||||
|
func Min[T Real](v1, v2 T) T {
|
||||||
|
return expr.Ternary(v1 < v2, v1, v2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Abs[T Real](val T) T {
|
||||||
|
return expr.Ternary(val < 0, -val, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neg returns the negative of value. It does not negate value. For
|
||||||
|
// negating, simply use -value instead.
|
||||||
|
func Neg[T Real](value T) T {
|
||||||
|
return expr.Ternary(value < 0, value, -value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Clamp[T Real](value, min, max T) T {
|
||||||
|
if min > max {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
if value < min {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
if value > max {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
54
mathx/math_test.go
Normal file
54
mathx/math_test.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package mathx_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/charlienet/go-mixed/mathx"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMin(t *testing.T) {
|
||||||
|
assert.Equal(t, 1, mathx.Min(1, 3))
|
||||||
|
assert.Equal(t, 2, mathx.Min(66, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMax(t *testing.T) {
|
||||||
|
assert.Equal(t, 3, mathx.Max(1, 3))
|
||||||
|
assert.Equal(t, 66, mathx.Max(66, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAbs(t *testing.T) {
|
||||||
|
assert.Equal(t, 23, mathx.Abs(23))
|
||||||
|
assert.Equal(t, 23, mathx.Abs(-23))
|
||||||
|
assert.Equal(t, 0, mathx.Abs(0))
|
||||||
|
|
||||||
|
var u int8 = -127
|
||||||
|
var exp int8 = 127
|
||||||
|
assert.Equal(t, exp, mathx.Abs(u))
|
||||||
|
|
||||||
|
assert.Equal(t, 1.23, mathx.Abs(-1.23))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClamp(t *testing.T) {
|
||||||
|
tests := []struct{ value, min, max, want int }{
|
||||||
|
// Min.
|
||||||
|
{-1, 0, 2, 0},
|
||||||
|
{0, 0, 2, 0},
|
||||||
|
// Mid.
|
||||||
|
{1, 0, 2, 1},
|
||||||
|
{2, 0, 2, 2},
|
||||||
|
// Max.
|
||||||
|
{2, 0, 2, 2},
|
||||||
|
{3, 0, 2, 2},
|
||||||
|
// Empty range.
|
||||||
|
{-1, 0, 0, 0},
|
||||||
|
{0, 0, 0, 0},
|
||||||
|
{1, 0, 0, 0},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
got := mathx.Clamp(test.value, test.min, test.max)
|
||||||
|
if got != test.want {
|
||||||
|
t.Errorf("Clamp(%v, %v, %v) = %v, want %v", test.value, test.min, test.max, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
175
redis/redis.go
175
redis/redis.go
@ -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,
|
type Clients []Client
|
||||||
|
|
||||||
|
func (clients Clients) LoadFunction(code string) {
|
||||||
|
for _, c := range clients {
|
||||||
|
c.LoadFunction(code)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return r
|
type Client interface {
|
||||||
|
redis.UniversalClient
|
||||||
|
LoadFunction(f string) // 加载函数脚本
|
||||||
|
Prefix() string // 统一前缀
|
||||||
|
Separator() string // 分隔符
|
||||||
|
JoinKeys(keys ...string) string // 连接KEY
|
||||||
|
FormatKeys(keys ...string) []string // 格式化KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Redis) Set(ctx context.Context, key, value string) error {
|
type redisClient struct {
|
||||||
conn, err := s.getRedis()
|
redis.UniversalClient
|
||||||
if err != nil {
|
prefix string
|
||||||
return err
|
separator string
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn.Set(ctx, s.formatKey(key), value, 0).Err()
|
func New(opt *RedisOption) redisClient {
|
||||||
|
var rdb redisClient
|
||||||
|
|
||||||
|
if len(opt.Addrs) == 0 && len(opt.Addr) > 0 {
|
||||||
|
opt.Addrs = []string{opt.Addr}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Redis) Get(ctx context.Context, key string) (string, error) {
|
separator := expr.Ternary(len(opt.Separator) == 0, defaultSeparator, opt.Separator)
|
||||||
conn, err := s.getRedis()
|
prefix := expr.Ternary(len(opt.Prefix) > 0, fmt.Sprintf("%s%s", opt.Prefix, separator), "")
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn.Get(ctx, s.formatKey(key)).Result()
|
rdb = redisClient{
|
||||||
}
|
prefix: prefix,
|
||||||
|
separator: separator,
|
||||||
|
UniversalClient: redis.NewUniversalClient(&redis.UniversalOptions{
|
||||||
|
Addrs: opt.Addrs,
|
||||||
|
Password: opt.Password,
|
||||||
|
|
||||||
func (s *Redis) GetSet(ctx context.Context, key, value string) (string, error) {
|
DB: opt.DB,
|
||||||
conn, err := s.getRedis()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
val, err := conn.GetSet(ctx, s.formatKey(key), value).Result()
|
MaxRetries: opt.MaxRetries,
|
||||||
return val, err
|
MinRetryBackoff: opt.MinRetryBackoff,
|
||||||
}
|
MaxRetryBackoff: opt.MaxRetryBackoff,
|
||||||
|
|
||||||
func (s *Redis) Del(ctx context.Context, key ...string) (int, error) {
|
DialTimeout: opt.DialTimeout,
|
||||||
conn, err := s.getRedis()
|
ReadTimeout: opt.ReadTimeout,
|
||||||
if err != nil {
|
WriteTimeout: opt.WriteTimeout,
|
||||||
return 0, err
|
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
|
||||||
}
|
|
||||||
|
|
||||||
keys := s.formatKeys(key...)
|
PoolSize: opt.PoolSize,
|
||||||
v, err := conn.Del(ctx, keys...).Result()
|
PoolTimeout: opt.PoolTimeout,
|
||||||
if err != nil {
|
MinIdleConns: opt.MinIdleConns,
|
||||||
return 0, err
|
MaxIdleConns: opt.MaxIdleConns,
|
||||||
}
|
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||||
|
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||||
|
})}
|
||||||
|
|
||||||
return int(v), err
|
rdb.ConfigSet(context.Background(), "slowlog-log-slower-than", defaultSlowThreshold)
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Redis) getRedis() (redis.UniversalClient, error) {
|
if len(opt.Prefix) > 0 {
|
||||||
client := redis.NewUniversalClient(&redis.UniversalOptions{
|
rdb.AddHook(renameKey{
|
||||||
Addrs: []string{s.addr},
|
prefix: prefix,
|
||||||
})
|
})
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Redis) formatKeys(keys ...string) []string {
|
return rdb
|
||||||
// If no prefix is configured, this parameter is returned
|
}
|
||||||
if s.prefix == "" {
|
|
||||||
|
func (rdb redisClient) Prefix() string {
|
||||||
|
return rdb.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
|
|
||||||
}
|
}
|
||||||
|
@ -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
102
redis/rename_hook.go
Normal 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":
|
||||||
|
// 间隔KEY,KEY位置规则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
27
redis/rename_hook_test.go
Normal 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)
|
||||||
|
}
|
@ -28,6 +28,7 @@ type option struct {
|
|||||||
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 = &sync.RWMutex{}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user