first commit
This commit is contained in:
commit
2891687b72
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
9
.idea/consistencehash.iml
generated
Normal file
9
.idea/consistencehash.iml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/consistencehash.iml" filepath="$PROJECT_DIR$/.idea/consistencehash.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
4
.idea/vcs.xml
generated
Normal file
4
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings" defaultProject="true" />
|
||||
</project>
|
261
consistencehash.go
Normal file
261
consistencehash.go
Normal file
@ -0,0 +1,261 @@
|
||||
package consistencehash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ConsistenceHash struct {
|
||||
hashRing HashRing
|
||||
encrytor Encrytor
|
||||
migrator Migrator
|
||||
opts Options
|
||||
}
|
||||
|
||||
func NewConsistenceHash(hashRing HashRing, encrytor Encrytor, migrator Migrator, opts ...Option) *ConsistenceHash {
|
||||
chash := &ConsistenceHash{
|
||||
hashRing: hashRing,
|
||||
encrytor: encrytor,
|
||||
migrator: migrator,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&chash.opts)
|
||||
}
|
||||
|
||||
chash.opts.repire()
|
||||
|
||||
return chash
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) getVirNodeId(id string, index int) string {
|
||||
return fmt.Sprintf("%s_%d", id, index)
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) getRawNodeId(id string) string {
|
||||
index := strings.LastIndex(id, "_")
|
||||
return id[:index]
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) migratorHandle(funs []func()) {
|
||||
wg := sync.WaitGroup{}
|
||||
for _, fn := range funs {
|
||||
wg.Add(1)
|
||||
|
||||
go func(fn func()) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Println("recover")
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
fn()
|
||||
}(fn)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) wight(wight int) int {
|
||||
if wight < 1 {
|
||||
return 1
|
||||
}
|
||||
|
||||
if wight > 10 {
|
||||
return 10
|
||||
}
|
||||
|
||||
return wight
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) AddNode(ctx context.Context, nodeId string, wight int) error {
|
||||
if err := c.hashRing.Lock(ctx, c.opts.lockExpire); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = c.hashRing.UnLock(ctx)
|
||||
}()
|
||||
|
||||
nodes, err := c.hashRing.Nodes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for node := range nodes {
|
||||
if node == nodeId {
|
||||
return errors.New("node already exists")
|
||||
}
|
||||
}
|
||||
|
||||
count := c.wight(wight) * c.opts.replicas
|
||||
|
||||
if err := c.hashRing.AddNodes(ctx, nodeId, count); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var migratorFns []func()
|
||||
for i := 0; i < count; i++ {
|
||||
vNodeId := c.getVirNodeId(nodeId, i)
|
||||
sum := c.encrytor.Hash(vNodeId)
|
||||
|
||||
if err := c.hashRing.Add(ctx, sum, vNodeId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
datas, from, to, err := c.migratorIn(ctx, sum, vNodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(datas) == 0 || from == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
migratorFns = append(migratorFns, func() {
|
||||
_ = c.migrator(ctx, datas, from, to)
|
||||
})
|
||||
}
|
||||
|
||||
c.migratorHandle(migratorFns)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) getAllDatas(ctx context.Context, nodeId string, counts int) (map[string]struct{}, error) {
|
||||
var allDatas map[string]struct{}
|
||||
|
||||
for i := 0; i < counts; i++ {
|
||||
vNodeId := c.getVirNodeId(nodeId, i)
|
||||
_datas, err := c.hashRing.Datas(ctx, vNodeId)
|
||||
if err != nil {
|
||||
return allDatas, err
|
||||
}
|
||||
|
||||
if allDatas == nil {
|
||||
allDatas = make(map[string]struct{})
|
||||
}
|
||||
|
||||
for key := range _datas {
|
||||
allDatas[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return allDatas, nil
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) DelNode(ctx context.Context, nodeId string) error {
|
||||
if err := c.hashRing.Lock(ctx, c.opts.lockExpire); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = c.hashRing.UnLock(ctx)
|
||||
}()
|
||||
|
||||
nodes, err := c.hashRing.Nodes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
exists bool
|
||||
counts int
|
||||
)
|
||||
|
||||
for node, nums := range nodes {
|
||||
if node == nodeId {
|
||||
exists = true
|
||||
counts = nums
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return errors.New("node id is not exists")
|
||||
}
|
||||
|
||||
// _datas, err := c.hashRing.Datas(ctx, nodeId)
|
||||
_datas, err := c.getAllDatas(ctx, nodeId, counts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(_datas) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var migratorFns []func()
|
||||
for i := 0; i < counts; i++ {
|
||||
vNodeId := c.getVirNodeId(nodeId, i)
|
||||
sum := c.encrytor.Hash(vNodeId)
|
||||
|
||||
datas, from, to, err := c.migratorOut(ctx, sum, vNodeId, &_datas)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.hashRing.Del(ctx, sum, vNodeId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(datas) == 0 || to == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
migratorFns = append(migratorFns, func() {
|
||||
_ = c.migrator(ctx, datas, from, to)
|
||||
})
|
||||
}
|
||||
|
||||
if len(_datas) != 0 {
|
||||
fmt.Println(_datas)
|
||||
return errors.New("internal error")
|
||||
}
|
||||
|
||||
if err := c.hashRing.DelNodes(ctx, nodeId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.migratorHandle(migratorFns)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) GetNode(ctx context.Context, key string) (string, error) {
|
||||
if err := c.hashRing.Lock(ctx, c.opts.lockExpire); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = c.hashRing.UnLock(ctx)
|
||||
}()
|
||||
|
||||
keysum := c.encrytor.Hash(key)
|
||||
|
||||
nextScore, err := c.hashRing.Ceiling(ctx, keysum)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if nextScore == -1 {
|
||||
return "", errors.New("no avalible node")
|
||||
}
|
||||
|
||||
nextNode, err := c.hashRing.Node(ctx, nextScore)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(nextNode) == 0 {
|
||||
return "", errors.New("node id error")
|
||||
}
|
||||
|
||||
if err := c.hashRing.AddDatas(ctx, nextNode[0], map[string]struct{}{
|
||||
key: {},
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return c.getRawNodeId(nextNode[0]), nil
|
||||
}
|
149
consistencehash_test.go
Normal file
149
consistencehash_test.go
Normal file
@ -0,0 +1,149 @@
|
||||
package consistencehash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.zhangshuocauc.cn/redhat/goconsistencehash/local"
|
||||
"git.zhangshuocauc.cn/redhat/goconsistencehash/redis/hashring"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"hash/fnv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func migrator(ctx context.Context, datas map[string]struct{}, from, to string) error {
|
||||
fmt.Printf("from: %s, to: %s, datas: %v", from, to, datas)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestConHash_Local(t *testing.T) {
|
||||
hashRing := local.NewHashRing(&local.BlockLock{})
|
||||
encrytor := NewEncrytorImp(fnv.New32())
|
||||
|
||||
hash := NewConsistenceHash(
|
||||
hashRing,
|
||||
encrytor,
|
||||
migrator,
|
||||
WithReplicas(10),
|
||||
WithLockExpire(10),
|
||||
)
|
||||
|
||||
test(t, hash)
|
||||
}
|
||||
|
||||
func TestConHash_Redis(t *testing.T) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: "192.168.8.1:6379",
|
||||
Password: "",
|
||||
})
|
||||
|
||||
lockwap := hashring.NewRedisLockWrap(client, "redhat_redis_lock")
|
||||
hashRing := hashring.NewRedisHashRing(client, "redhat_redis_hashring", lockwap)
|
||||
encrytor := NewEncrytorImp(fnv.New32())
|
||||
|
||||
hash := NewConsistenceHash(
|
||||
hashRing,
|
||||
encrytor,
|
||||
migrator,
|
||||
WithReplicas(10),
|
||||
WithLockExpire(10),
|
||||
)
|
||||
|
||||
test(t, hash)
|
||||
}
|
||||
|
||||
func test(t *testing.T, consistentHash *ConsistenceHash) {
|
||||
ctx := context.Background()
|
||||
nodeA := "node_a"
|
||||
weightNodeA := 2
|
||||
nodeB := "node_b"
|
||||
weightNodeB := 1
|
||||
nodeC := "node_c"
|
||||
weightNodeC := 1
|
||||
|
||||
if err := consistentHash.AddNode(ctx, nodeA, weightNodeA); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := consistentHash.AddNode(ctx, nodeB, weightNodeB); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
dataKeyA := "data_a"
|
||||
dataKeyB := "data_b"
|
||||
dataKeyC := "data_c"
|
||||
dataKeyD := "data_d"
|
||||
node, err := consistentHash.GetNode(ctx, dataKeyA)
|
||||
fmt.Println(node)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyA, node)
|
||||
|
||||
if node, err = consistentHash.GetNode(ctx, dataKeyB); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyB, node)
|
||||
if node, err = consistentHash.GetNode(ctx, dataKeyC); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyC, node)
|
||||
if node, err = consistentHash.GetNode(ctx, dataKeyD); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyD, node)
|
||||
if err := consistentHash.AddNode(ctx, nodeC, weightNodeC); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if node, err = consistentHash.GetNode(ctx, dataKeyA); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyA, node)
|
||||
if node, err = consistentHash.GetNode(ctx, dataKeyB); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyB, node)
|
||||
if node, err = consistentHash.GetNode(ctx, dataKeyC); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyC, node)
|
||||
if node, err = consistentHash.GetNode(ctx, dataKeyD); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyD, node)
|
||||
if err = consistentHash.DelNode(ctx, nodeC); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if node, err = consistentHash.GetNode(ctx, dataKeyA); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyA, node)
|
||||
if node, err = consistentHash.GetNode(ctx, dataKeyB); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyB, node)
|
||||
if node, err = consistentHash.GetNode(ctx, dataKeyC); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyC, node)
|
||||
if node, err = consistentHash.GetNode(ctx, dataKeyD); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("data: %s belongs to node: %s", dataKeyD, node)
|
||||
t.Error("ok")
|
||||
}
|
32
encrytor.go
Normal file
32
encrytor.go
Normal file
@ -0,0 +1,32 @@
|
||||
package consistencehash
|
||||
|
||||
import (
|
||||
"hash"
|
||||
"math"
|
||||
|
||||
"github.com/spaolacci/murmur3"
|
||||
)
|
||||
|
||||
type Encrytor interface {
|
||||
Hash(string) int32
|
||||
}
|
||||
|
||||
type EncrytorImp struct {
|
||||
hash hash.Hash32
|
||||
}
|
||||
|
||||
func NewEncrytorImp(hash hash.Hash32) *EncrytorImp {
|
||||
return &EncrytorImp{
|
||||
hash: hash,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EncrytorImp) Hash(id string) int32 {
|
||||
e.hash = murmur3.New32()
|
||||
// e.hash.Reset()
|
||||
e.hash.Write([]byte(id))
|
||||
|
||||
return int32(e.hash.Sum32() % math.MaxInt32)
|
||||
}
|
||||
|
||||
var _ Encrytor = &EncrytorImp{}
|
13
go.mod
Normal file
13
go.mod
Normal file
@ -0,0 +1,13 @@
|
||||
module git.zhangshuocauc.cn/redhat/goconsistencehash
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/redis/go-redis/v9 v9.8.0
|
||||
github.com/spaolacci/murmur3 v1.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
)
|
12
go.sum
Normal file
12
go.sum
Normal file
@ -0,0 +1,12 @@
|
||||
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
|
||||
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
21
hashring.go
Normal file
21
hashring.go
Normal file
@ -0,0 +1,21 @@
|
||||
package consistencehash
|
||||
|
||||
import "context"
|
||||
|
||||
type HashRing interface {
|
||||
Lock(context.Context, int) error
|
||||
UnLock(context.Context) error
|
||||
Add(context.Context, int32, string) error
|
||||
Del(context.Context, int32, string) error
|
||||
Floor(context.Context, int32) (int32, error)
|
||||
Ceiling(context.Context, int32) (int32, error)
|
||||
Node(context.Context, int32) ([]string, error)
|
||||
|
||||
Nodes(context.Context) (map[string]int, error)
|
||||
AddNodes(context.Context, string, int) error
|
||||
DelNodes(context.Context, string) error
|
||||
|
||||
Datas(context.Context, string) (map[string]struct{}, error)
|
||||
AddDatas(context.Context, string, map[string]struct{}) error
|
||||
DelDatas(context.Context, string, map[string]struct{}) error
|
||||
}
|
64
local/blocklock.go
Normal file
64
local/blocklock.go
Normal file
@ -0,0 +1,64 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.zhangshuocauc.cn/redhat/goconsistencehash/local/os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BlockLock struct {
|
||||
mu sync.Mutex
|
||||
dmu sync.Mutex
|
||||
token string
|
||||
calFn context.CancelFunc
|
||||
}
|
||||
|
||||
func (b *BlockLock) Lock(ctx context.Context, expire int) error {
|
||||
b.mu.Lock()
|
||||
|
||||
b.dmu.Lock()
|
||||
defer b.dmu.Unlock()
|
||||
|
||||
token := os.GetCurrentProcessAndGoroutineIDStr()
|
||||
b.token = token
|
||||
b.calFn = nil
|
||||
|
||||
var sctx context.Context
|
||||
sctx, b.calFn = context.WithCancel(ctx)
|
||||
go func() {
|
||||
select {
|
||||
case <-sctx.Done():
|
||||
return
|
||||
case <-time.After(time.Duration(expire) * time.Second):
|
||||
_ = b.unLock(token)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BlockLock) unLock(id string) error {
|
||||
b.dmu.Lock()
|
||||
defer b.dmu.Unlock()
|
||||
|
||||
if b.token != id {
|
||||
return errors.New("not my lock")
|
||||
}
|
||||
|
||||
if b.calFn != nil {
|
||||
b.calFn()
|
||||
}
|
||||
|
||||
b.token = ""
|
||||
|
||||
b.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BlockLock) UnLock(context.Context) error {
|
||||
id := os.GetCurrentProcessAndGoroutineIDStr()
|
||||
return b.unLock(id)
|
||||
}
|
298
local/hashring.go
Normal file
298
local/hashring.go
Normal file
@ -0,0 +1,298 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HashRing struct {
|
||||
Locker
|
||||
head *node
|
||||
|
||||
nodes map[string]int
|
||||
datas map[string]map[string]struct{}
|
||||
|
||||
rand *rand.Rand
|
||||
}
|
||||
|
||||
type node struct {
|
||||
nexts []*node
|
||||
score int32
|
||||
node []string
|
||||
}
|
||||
|
||||
func NewHashRing(locker Locker) *HashRing {
|
||||
return &HashRing{
|
||||
Locker: locker,
|
||||
head: &node{},
|
||||
nodes: make(map[string]int),
|
||||
datas: make(map[string]map[string]struct{}),
|
||||
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HashRing) search(score int32) *node {
|
||||
move := h.head
|
||||
for level := len(h.head.nexts) - 1; level >= 0; level-- {
|
||||
for move.nexts[level] != nil && move.nexts[level].score < score {
|
||||
move = move.nexts[level]
|
||||
}
|
||||
|
||||
if move.nexts[level] != nil && move.nexts[level].score == score {
|
||||
return move.nexts[level]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HashRing) floor(score int32) (int32, bool) {
|
||||
if len(h.head.nexts) == 0 {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
move := h.head
|
||||
for level := len(move.nexts) - 1; level >= 0; level-- {
|
||||
for move.nexts[level] != nil && move.nexts[level].score < score {
|
||||
move = move.nexts[level]
|
||||
}
|
||||
}
|
||||
|
||||
if move.nexts[0] != nil && move.nexts[0].score == score {
|
||||
return move.nexts[0].score, true
|
||||
}
|
||||
|
||||
if move == h.head {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
return move.score, true
|
||||
}
|
||||
|
||||
func (h *HashRing) ceiling(score int32) (int32, bool) {
|
||||
if len(h.head.nexts) == 0 {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
move := h.head
|
||||
for level := len(move.nexts) - 1; level >= 0; level-- {
|
||||
for move.nexts[level] != nil && move.nexts[level].score < score {
|
||||
move = move.nexts[level]
|
||||
}
|
||||
}
|
||||
|
||||
if move.nexts[0] == nil {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
return move.nexts[0].score, true
|
||||
}
|
||||
|
||||
func (h *HashRing) first() (int32, bool) {
|
||||
if len(h.head.nexts) == 0 {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
return h.head.nexts[0].score, true
|
||||
}
|
||||
|
||||
func (h *HashRing) last() (int32, bool) {
|
||||
if len(h.head.nexts) == 0 {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
move := h.head
|
||||
for level := len(move.nexts) - 1; level >= 0; level-- {
|
||||
for move.nexts[level] != nil {
|
||||
move = move.nexts[level]
|
||||
}
|
||||
}
|
||||
|
||||
if move == h.head {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
return move.score, true
|
||||
}
|
||||
|
||||
func (h *HashRing) roll() int {
|
||||
level := 0
|
||||
for h.rand.Intn(2) > 0 {
|
||||
level++
|
||||
}
|
||||
|
||||
return level
|
||||
}
|
||||
|
||||
func (h *HashRing) Add(ctx context.Context, score int32, nodeId string) error {
|
||||
nodes := h.search(score)
|
||||
if nodes != nil {
|
||||
for _, _nodeId := range nodes.node {
|
||||
if _nodeId == nodeId {
|
||||
return errors.New("alrady inset")
|
||||
}
|
||||
}
|
||||
|
||||
nodes.node = append(nodes.node, nodeId)
|
||||
return nil
|
||||
}
|
||||
|
||||
roll := h.roll()
|
||||
if len(h.head.nexts)-1 < roll {
|
||||
diff := make([]*node, roll+1-len(h.head.nexts))
|
||||
h.head.nexts = append(h.head.nexts, diff...)
|
||||
}
|
||||
|
||||
newNode := &node{
|
||||
score: score,
|
||||
node: []string{nodeId},
|
||||
nexts: make([]*node, roll+1),
|
||||
}
|
||||
|
||||
move := h.head
|
||||
for level := len(move.nexts) - 1; level >= 0; level-- {
|
||||
for move.nexts[level] != nil && move.nexts[level].score < score {
|
||||
move = move.nexts[level]
|
||||
}
|
||||
|
||||
if level > roll {
|
||||
continue
|
||||
}
|
||||
|
||||
newNode.nexts[level] = move.nexts[level]
|
||||
move.nexts[level] = newNode
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HashRing) Del(ctx context.Context, score int32, nodeId string) error {
|
||||
nodes := h.search(score)
|
||||
if nodes == nil {
|
||||
return errors.New("no such score node")
|
||||
}
|
||||
|
||||
index := -1
|
||||
for idx, key := range nodes.node {
|
||||
if key == nodeId {
|
||||
index = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index == -1 {
|
||||
return errors.New("nodes not exists")
|
||||
}
|
||||
|
||||
if len(nodes.node) > 1 {
|
||||
nodes.node = append(nodes.node[:index], nodes.node[index+1:]...)
|
||||
return nil
|
||||
}
|
||||
|
||||
move := h.head
|
||||
for level := len(move.nexts) - 1; level >= 0; level-- {
|
||||
for move.nexts[level] != nil && move.nexts[level].score < score {
|
||||
move = move.nexts[level]
|
||||
}
|
||||
|
||||
if move.nexts[level] == nil || move.nexts[level].score > score {
|
||||
continue
|
||||
}
|
||||
|
||||
move.nexts[level] = move.nexts[level].nexts[level]
|
||||
}
|
||||
|
||||
diff := 0
|
||||
for level := len(h.head.nexts) - 1; level >= 0 && h.head.nexts[level] == nil; level-- {
|
||||
diff++
|
||||
}
|
||||
|
||||
if diff > 0 {
|
||||
h.head.nexts = h.head.nexts[:len(h.head.nexts)-diff]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HashRing) Floor(ctx context.Context, score int32) (int32, error) {
|
||||
if floorScore, ok := h.floor(score); ok {
|
||||
return floorScore, nil
|
||||
}
|
||||
|
||||
lastScore, _ := h.last()
|
||||
|
||||
return lastScore, nil
|
||||
}
|
||||
|
||||
func (h *HashRing) Ceiling(ctx context.Context, score int32) (int32, error) {
|
||||
if ceillingScore, ok := h.ceiling(score); ok {
|
||||
return ceillingScore, nil
|
||||
}
|
||||
|
||||
firstScore, _ := h.first()
|
||||
|
||||
return firstScore, nil
|
||||
}
|
||||
|
||||
func (h *HashRing) Node(ctx context.Context, score int32) ([]string, error) {
|
||||
node := h.search(score)
|
||||
if node == nil {
|
||||
return []string{}, errors.New("no such score node")
|
||||
}
|
||||
|
||||
return node.node, nil
|
||||
}
|
||||
|
||||
func (h *HashRing) Nodes(context.Context) (map[string]int, error) {
|
||||
return h.nodes, nil
|
||||
}
|
||||
|
||||
func (h *HashRing) AddNodes(ctx context.Context, nodeId string, num int) error {
|
||||
h.nodes[nodeId] = num
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HashRing) DelNodes(ctx context.Context, nodeId string) error {
|
||||
delete(h.nodes, nodeId)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HashRing) Datas(ctx context.Context, nodeId string) (map[string]struct{}, error) {
|
||||
return h.datas[nodeId], nil
|
||||
}
|
||||
|
||||
func (h *HashRing) AddDatas(ctx context.Context, nodeId string, datas map[string]struct{}) error {
|
||||
datasMap := h.datas[nodeId]
|
||||
if datasMap == nil {
|
||||
datasMap = make(map[string]struct{})
|
||||
}
|
||||
|
||||
for keys := range datas {
|
||||
datasMap[keys] = struct{}{}
|
||||
}
|
||||
|
||||
h.datas[nodeId] = datasMap
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HashRing) DelDatas(ctx context.Context, nodeId string, datas map[string]struct{}) error {
|
||||
datasMap := h.datas[nodeId]
|
||||
if datasMap == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for keys := range datas {
|
||||
delete(datasMap, keys)
|
||||
}
|
||||
|
||||
if len(datasMap) == 0 {
|
||||
delete(h.datas, nodeId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
8
local/locker.go
Normal file
8
local/locker.go
Normal file
@ -0,0 +1,8 @@
|
||||
package local
|
||||
|
||||
import "context"
|
||||
|
||||
type Locker interface{
|
||||
Lock(context.Context,int)error
|
||||
UnLock(context.Context)error
|
||||
}
|
27
local/os/os.go
Normal file
27
local/os/os.go
Normal file
@ -0,0 +1,27 @@
|
||||
package os
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetCurrentProcessId() int {
|
||||
return os.Getpid()
|
||||
}
|
||||
|
||||
func GetCurrentGoroutineId() string {
|
||||
buf := make([]byte, 128)
|
||||
buf = buf[:runtime.Stack(buf, false)]
|
||||
stackInfo := string(buf)
|
||||
|
||||
return strings.TrimSpace(strings.Split(strings.Split(stackInfo, "[running]")[0], "goroutine")[1])
|
||||
}
|
||||
|
||||
func GetCurrentProcessAndGoroutineIDStr() string {
|
||||
pid := GetCurrentProcessId()
|
||||
goroutineID := GetCurrentGoroutineId()
|
||||
|
||||
return fmt.Sprintf("%d_%s", pid, goroutineID)
|
||||
}
|
72
local/spinlock.go
Normal file
72
local/spinlock.go
Normal file
@ -0,0 +1,72 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.zhangshuocauc.cn/redhat/goconsistencehash/local/os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SpinLock struct {
|
||||
mu sync.Mutex
|
||||
token string
|
||||
locked bool
|
||||
expire time.Time
|
||||
}
|
||||
|
||||
func NewSpinLock() *SpinLock {
|
||||
return &SpinLock{}
|
||||
}
|
||||
|
||||
func (s *SpinLock) tryLock(expire int) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if !s.locked || now.After(s.expire) {
|
||||
s.locked = true
|
||||
s.token = os.GetCurrentProcessAndGoroutineIDStr()
|
||||
s.expire = now.Add(time.Duration(expire) * time.Second)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SpinLock) Lock(ctx context.Context, expire int) error {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
if s.tryLock(expire) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("try lock error")
|
||||
}
|
||||
|
||||
func (s *SpinLock) UnLock(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.token != os.GetCurrentProcessAndGoroutineIDStr() {
|
||||
return errors.New("not my lock")
|
||||
}
|
||||
|
||||
if !s.locked || time.Now().After(s.expire) {
|
||||
return errors.New("try unlock an unlocked lock")
|
||||
}
|
||||
|
||||
s.locked = false
|
||||
s.token = ""
|
||||
|
||||
return nil
|
||||
}
|
278
migrator.go
Normal file
278
migrator.go
Normal file
@ -0,0 +1,278 @@
|
||||
package consistencehash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type Migrator func(ctx context.Context, datas map[string]struct{}, from, to string) error
|
||||
|
||||
func (c *ConsistenceHash) decrScore(score int32) int32 {
|
||||
if score == 0 {
|
||||
return (math.MaxInt32 - 1)
|
||||
}
|
||||
|
||||
return (score - 1)
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) incrScore(score int32) int32 {
|
||||
if score == (math.MaxInt32 - 1) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return (score + 1)
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) migratorIn(ctx context.Context, score int32, nodeId string) (datas map[string]struct{}, from, to string, _err error) {
|
||||
if c.migrator == nil {
|
||||
return
|
||||
}
|
||||
|
||||
nodeIds, err := c.hashRing.Node(ctx, score)
|
||||
if err != nil {
|
||||
_err = err
|
||||
return
|
||||
}
|
||||
|
||||
if len(nodeIds) > 1 {
|
||||
return
|
||||
}
|
||||
|
||||
lastScore, err := c.hashRing.Floor(ctx, c.decrScore(score))
|
||||
if err != nil {
|
||||
_err = err
|
||||
return
|
||||
}
|
||||
|
||||
if lastScore == -1 || lastScore == score {
|
||||
return
|
||||
}
|
||||
|
||||
nextScore, err := c.hashRing.Ceiling(ctx, c.incrScore(score))
|
||||
if err != nil {
|
||||
_err = err
|
||||
}
|
||||
|
||||
if nextScore == -1 || nextScore == score {
|
||||
return
|
||||
}
|
||||
|
||||
patternOne := lastScore > score
|
||||
|
||||
if patternOne {
|
||||
lastScore -= math.MaxInt32
|
||||
}
|
||||
|
||||
nextNode, err := c.hashRing.Node(ctx, nextScore)
|
||||
if err != nil {
|
||||
_err = err
|
||||
return
|
||||
}
|
||||
|
||||
if len(nextNode) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if c.getRawNodeId(nextNode[0]) == c.getRawNodeId(nodeId) {
|
||||
return
|
||||
}
|
||||
|
||||
_datas, err := c.hashRing.Datas(ctx, nextNode[0])
|
||||
if err != nil {
|
||||
_err = err
|
||||
return
|
||||
}
|
||||
|
||||
if len(_datas) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for key := range _datas {
|
||||
sum := c.encrytor.Hash(key)
|
||||
if patternOne && (sum > (lastScore + math.MaxInt32)) {
|
||||
sum -= math.MaxInt32
|
||||
}
|
||||
|
||||
if sum < lastScore || sum >= score {
|
||||
continue
|
||||
}
|
||||
|
||||
if datas == nil {
|
||||
datas = make(map[string]struct{})
|
||||
}
|
||||
|
||||
datas[key] = struct{}{}
|
||||
}
|
||||
|
||||
to = c.getRawNodeId(nodeId)
|
||||
from = c.getRawNodeId(nextNode[0])
|
||||
|
||||
if err := c.hashRing.DelDatas(ctx, nextNode[0], datas); err != nil {
|
||||
_err = err
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.hashRing.AddDatas(ctx, nodeId, datas); err != nil {
|
||||
_err = err
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) migratorOut(ctx context.Context, score int32, nodeId string,
|
||||
allDatas *map[string]struct{}) (datas map[string]struct{}, from, to string, _err error) {
|
||||
|
||||
var rawTo string
|
||||
|
||||
defer func () {
|
||||
if _err !=nil{
|
||||
return
|
||||
}
|
||||
|
||||
if to == "" || len(datas)==0{
|
||||
return
|
||||
}
|
||||
|
||||
if _err = c.hashRing.DelDatas(ctx, nodeId, datas); _err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_err = c.hashRing.AddDatas(ctx, rawTo, datas)
|
||||
}()
|
||||
|
||||
if c.migrator == nil {
|
||||
return
|
||||
}
|
||||
|
||||
nodes, err := c.hashRing.Node(ctx, score)
|
||||
fmt.Println(nodes,score)
|
||||
if err != nil {
|
||||
_err = err
|
||||
return
|
||||
}
|
||||
|
||||
if len(nodes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if nodes[0] != nodeId {
|
||||
return
|
||||
}
|
||||
|
||||
if len(*allDatas) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
lastScore, err := c.hashRing.Floor(ctx, c.decrScore(score))
|
||||
fmt.Println(lastScore,err)
|
||||
if err != nil {
|
||||
_err = err
|
||||
return
|
||||
}
|
||||
|
||||
if lastScore == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("score: %d, lastscore: %d",score,lastScore)
|
||||
|
||||
if lastScore == score {
|
||||
for _, nodeIds := range nodes {
|
||||
if c.getRawNodeId(nodeIds) != c.getRawNodeId(nodeId) {
|
||||
rawTo = nodeIds
|
||||
}
|
||||
}
|
||||
|
||||
if rawTo == "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
patternOne := lastScore > score //这里必须定义模式,如果直接让sum > (lastScore + math.MaxInt32)判断,后面括号可能会溢出
|
||||
|
||||
if patternOne {
|
||||
lastScore -= math.MaxInt32
|
||||
}
|
||||
|
||||
fmt.Println("del lastscore", lastScore, "score", score)
|
||||
|
||||
for key := range *allDatas {
|
||||
sum := c.encrytor.Hash(key)
|
||||
fmt.Println("del sum", sum, "getsum", lastScore+math.MaxInt32)
|
||||
if patternOne && (sum > (lastScore + math.MaxInt32)) {
|
||||
sum -= math.MaxInt32
|
||||
}
|
||||
|
||||
if sum < lastScore || sum >= score {
|
||||
continue
|
||||
}
|
||||
|
||||
if datas == nil {
|
||||
datas = make(map[string]struct{})
|
||||
}
|
||||
|
||||
datas[key] = struct{}{}
|
||||
delete(*allDatas, key)
|
||||
}
|
||||
|
||||
from = c.getRawNodeId(nodeId)
|
||||
if rawTo != "" {
|
||||
to = c.getRawNodeId(rawTo)
|
||||
return
|
||||
}
|
||||
|
||||
rawTo, err = c.getNextNode(ctx, score, nodeId, nil)
|
||||
if err != nil {
|
||||
_err = err
|
||||
return
|
||||
}
|
||||
|
||||
to = c.getRawNodeId(rawTo)
|
||||
|
||||
if to == "" {
|
||||
_err = errors.New("unavailable to node")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ConsistenceHash) getNextNode(ctx context.Context, score int32, nodeId string, dup map[int32]struct{}) (string, error) {
|
||||
nextScore, err := c.hashRing.Ceiling(ctx, c.incrScore(score))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if nextScore == -1 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if _, ok := dup[nextScore]; ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
nodes, err := c.hashRing.Node(ctx, nextScore)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(nodes) == 0 {
|
||||
return "", errors.New("un available next node")
|
||||
}
|
||||
|
||||
for _, nodeIds := range nodes {
|
||||
if c.getRawNodeId(nodeIds) != c.getRawNodeId(nodeId) {
|
||||
return nodeIds, nil
|
||||
}
|
||||
}
|
||||
|
||||
if dup == nil {
|
||||
dup = make(map[int32]struct{})
|
||||
}
|
||||
dup[nextScore] = struct{}{}
|
||||
|
||||
return c.getNextNode(ctx, nextScore, nodeId, dup)
|
||||
}
|
26
option.go
Normal file
26
option.go
Normal file
@ -0,0 +1,26 @@
|
||||
package consistencehash
|
||||
|
||||
type Options struct {
|
||||
replicas int
|
||||
lockExpire int
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
func (o *Options) repire() {
|
||||
if o.replicas <= 0 {
|
||||
o.replicas = 5
|
||||
}
|
||||
}
|
||||
|
||||
func WithReplicas(replicas int) Option {
|
||||
return func(o *Options) {
|
||||
o.replicas = replicas
|
||||
}
|
||||
}
|
||||
|
||||
func WithLockExpire(expire int)Option{
|
||||
return func(o *Options) {
|
||||
o.lockExpire=expire
|
||||
}
|
||||
}
|
323
redis/hashring/hashring.go
Normal file
323
redis/hashring/hashring.go
Normal file
@ -0,0 +1,323 @@
|
||||
package hashring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.zhangshuocauc.cn/redhat/goconsistencehash/local"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
RingPreFix = "redis:consistence_hash:ring"
|
||||
NodePreFix = "redis:consistence_hash:virtual"
|
||||
DataKeysPreFix = "redis:consistence_hash:datakeys"
|
||||
)
|
||||
|
||||
type RedisHashRing struct {
|
||||
local.Locker
|
||||
client *redis.Client
|
||||
key string
|
||||
}
|
||||
|
||||
func NewRedisHashRing(client *redis.Client, key string, lock local.Locker) *RedisHashRing {
|
||||
return &RedisHashRing{
|
||||
Locker: lock,
|
||||
client: client,
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) getRingKey() string {
|
||||
return fmt.Sprintf("%s:%s", RingPreFix, h.key)
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) getNodeKey() string {
|
||||
return fmt.Sprintf("%s:%s", NodePreFix, h.key)
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) getDataKey(nodeId string) string {
|
||||
return fmt.Sprintf("%s:%s:%s", DataKeysPreFix, h.key, nodeId)
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) Add(ctx context.Context, score int32, nodeId string) error {
|
||||
res, err := h.client.ZRangeByScore(ctx, h.getRingKey(), &redis.ZRangeBy{
|
||||
Min: strconv.Itoa(int(score)),
|
||||
Max: strconv.Itoa(int(score)),
|
||||
}).Result()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(res) > 1 {
|
||||
return errors.New("score to many element")
|
||||
}
|
||||
|
||||
var nodeIds []string
|
||||
if len(res) == 1 {
|
||||
if err := json.Unmarshal([]byte(res[0]), &nodeIds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range nodeIds {
|
||||
if id == nodeId {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.client.ZRem(ctx, h.getRingKey(), score).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
nodeIds = append(nodeIds, nodeId)
|
||||
appendNodeIds, _ := json.Marshal(nodeIds)
|
||||
|
||||
if err := h.client.ZAdd(ctx, h.getRingKey(), redis.Z{
|
||||
Score: float64(score),
|
||||
Member: appendNodeIds,
|
||||
}).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) Del(ctx context.Context, score int32, nodeId string) error {
|
||||
res, err := h.client.ZRangeByScore(ctx, h.getRingKey(), &redis.ZRangeBy{
|
||||
Min: strconv.Itoa(int(score)),
|
||||
Max: strconv.Itoa(int(score)),
|
||||
}).Result()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(res) != 1 {
|
||||
return errors.New("score to error element")
|
||||
}
|
||||
|
||||
var nodeIds []string
|
||||
if err := json.Unmarshal([]byte(res[0]), &nodeIds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
exists bool
|
||||
index int
|
||||
)
|
||||
|
||||
for idx, num := range nodeIds {
|
||||
if num == nodeId {
|
||||
exists = true
|
||||
index = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return errors.New("the score have no such nodeid")
|
||||
}
|
||||
|
||||
if err := h.client.ZRemRangeByScore(ctx, h.getRingKey(), strconv.Itoa(int(score)), strconv.Itoa(int(score))).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(nodeIds) <= 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
nodeIds = append(nodeIds[:index], nodeIds[index+1:]...)
|
||||
|
||||
if err := h.client.ZAdd(ctx, h.getRingKey(), redis.Z{
|
||||
Score: float64(score),
|
||||
Member: nodeIds,
|
||||
}).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) Floor(ctx context.Context, score int32) (int32, error) {
|
||||
res, err := h.client.ZRevRangeByScoreWithScores(ctx, h.getRingKey(), &redis.ZRangeBy{
|
||||
Min: "-inf",
|
||||
Max: strconv.Itoa(int(score)),
|
||||
Count: 1,
|
||||
}).Result()
|
||||
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
if len(res) == 1 {
|
||||
return int32(res[0].Score), nil
|
||||
}
|
||||
|
||||
res, err = h.client.ZRevRangeByScoreWithScores(ctx, h.getRingKey(), &redis.ZRangeBy{
|
||||
Min: "-inf",
|
||||
Max: "+inf",
|
||||
Count: 1,
|
||||
}).Result()
|
||||
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
if len(res) == 1 {
|
||||
return int32(res[0].Score), nil
|
||||
}
|
||||
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) Ceiling(ctx context.Context, score int32) (int32, error) {
|
||||
res, err := h.client.ZRangeByScoreWithScores(ctx, h.getRingKey(), &redis.ZRangeBy{
|
||||
Min: strconv.Itoa(int(score)),
|
||||
Max: "+inf",
|
||||
Count: 1,
|
||||
}).Result()
|
||||
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
if len(res) == 1 {
|
||||
return int32(res[0].Score), nil
|
||||
}
|
||||
|
||||
res, err = h.client.ZRangeByScoreWithScores(ctx, h.getRingKey(), &redis.ZRangeBy{
|
||||
Min: "-inf",
|
||||
Max: "+inf",
|
||||
Count: 1,
|
||||
}).Result()
|
||||
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
if len(res) == 1 {
|
||||
return int32(res[0].Score), nil
|
||||
}
|
||||
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) Node(ctx context.Context, score int32) ([]string, error) {
|
||||
res, err := h.client.ZRangeByScore(ctx, h.getRingKey(), &redis.ZRangeBy{
|
||||
Min: strconv.Itoa(int(score)),
|
||||
Max: strconv.Itoa(int(score)),
|
||||
}).Result()
|
||||
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
if len(res) != 1 {
|
||||
return []string{}, errors.New("score to error element")
|
||||
}
|
||||
|
||||
var nodeIds []string
|
||||
if err := json.Unmarshal([]byte(res[0]), &nodeIds); err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
return nodeIds, nil
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) Nodes(ctx context.Context) (map[string]int, error) {
|
||||
res, err := h.client.HGetAll(ctx, h.getNodeKey()).Result()
|
||||
if err != nil {
|
||||
return map[string]int{}, err
|
||||
}
|
||||
|
||||
var keys map[string]int
|
||||
for key, value := range res {
|
||||
count, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if keys == nil {
|
||||
keys = make(map[string]int)
|
||||
}
|
||||
|
||||
keys[key] = count
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) AddNodes(ctx context.Context, nodeId string, num int) error {
|
||||
if err := h.client.HSet(ctx, h.getNodeKey(), map[string]string{
|
||||
nodeId: strconv.Itoa(num),
|
||||
}).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) DelNodes(ctx context.Context, nodeId string) error {
|
||||
if err := h.client.HDel(ctx, h.getNodeKey(), nodeId).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) Datas(ctx context.Context, nodeId string) (map[string]struct{}, error) {
|
||||
res, err := h.client.SMembers(ctx, h.getDataKey(nodeId)).Result()
|
||||
if err != nil {
|
||||
return map[string]struct{}{}, err
|
||||
}
|
||||
|
||||
var datas map[string]struct{}
|
||||
|
||||
for _, keys := range res {
|
||||
if datas == nil {
|
||||
datas = make(map[string]struct{})
|
||||
}
|
||||
|
||||
datas[keys] = struct{}{}
|
||||
}
|
||||
|
||||
return datas, nil
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) AddDatas(ctx context.Context, nodeId string, datas map[string]struct{}) error {
|
||||
var addkeys []string
|
||||
for key := range datas {
|
||||
addkeys = append(addkeys, key)
|
||||
}
|
||||
|
||||
if len(addkeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := h.client.SAdd(ctx, h.getDataKey(nodeId), addkeys).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RedisHashRing) DelDatas(ctx context.Context, nodeId string, datas map[string]struct{}) error {
|
||||
var addkeys []string
|
||||
for key := range datas {
|
||||
addkeys = append(addkeys, key)
|
||||
}
|
||||
|
||||
if len(addkeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := h.client.SRem(ctx, h.getDataKey(nodeId), addkeys).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
29
redis/hashring/lock.go
Normal file
29
redis/hashring/lock.go
Normal file
@ -0,0 +1,29 @@
|
||||
package hashring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.zhangshuocauc.cn/redhat/goconsistencehash/redis/lock"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type RedisLockWrap struct {
|
||||
client *redis.Client
|
||||
key string
|
||||
lock *lock.RedisLock
|
||||
}
|
||||
|
||||
func NewRedisLockWrap(client *redis.Client, key string) *RedisLockWrap {
|
||||
return &RedisLockWrap{
|
||||
client: client,
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RedisLockWrap) Lock(ctx context.Context, expire int) error {
|
||||
r.lock = lock.NewRedisLock(r.client, r.key, lock.WithExpireTime(expire))
|
||||
return r.lock.Lock(ctx)
|
||||
}
|
||||
|
||||
func (r *RedisLockWrap) UnLock(ctx context.Context) error {
|
||||
return r.lock.UnLock(ctx)
|
||||
}
|
174
redis/lock/lock.go
Normal file
174
redis/lock/lock.go
Normal file
@ -0,0 +1,174 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrorsOtherLock = errors.New("not my lock")
|
||||
|
||||
type RedisLock struct {
|
||||
client *redis.Client
|
||||
token string
|
||||
key string
|
||||
opts Options
|
||||
|
||||
isRuning int32
|
||||
calFn context.CancelFunc
|
||||
}
|
||||
|
||||
func NewRedisLock(client *redis.Client, key string, opts ...Option) *RedisLock {
|
||||
redisLock := &RedisLock{
|
||||
client: client,
|
||||
key: key,
|
||||
token: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&redisLock.opts)
|
||||
}
|
||||
|
||||
redisLock.opts.repire()
|
||||
|
||||
return redisLock
|
||||
}
|
||||
|
||||
func (r *RedisLock) tryLock(ctx context.Context) error {
|
||||
res, err := r.client.SetNX(ctx, r.key, r.token, time.Second*time.Duration(r.opts.expireTime)).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !res {
|
||||
return errors.New("not my lock")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RedisLock) reNew(ctx context.Context) error {
|
||||
ires, err := r.client.Eval(ctx, LuaReNew, []string{r.key}, r.token, r.opts.expireTime).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res, ok := ires.(int64); ok && res == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrorsOtherLock
|
||||
}
|
||||
|
||||
func (r *RedisLock) unLock(ctx context.Context) error {
|
||||
ires, err := r.client.Eval(ctx, LuaUnLock, []string{r.key}, r.token).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res, ok := ires.(int64); ok && res == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrorsOtherLock
|
||||
}
|
||||
|
||||
func (r *RedisLock) block(ctx context.Context) error {
|
||||
var after <-chan time.Time
|
||||
if r.opts.maxWaitTime > 0 {
|
||||
after = time.After(time.Duration(r.opts.maxWaitTime))
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(200 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-after:
|
||||
return errors.New("time out")
|
||||
default:
|
||||
}
|
||||
|
||||
err := r.tryLock(ctx)
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !errors.Is(err, ErrorsOtherLock) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RedisLock) watchDog(ctx context.Context) {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
_ = r.reNew(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RedisLock) watch(ctx context.Context) {
|
||||
if !r.opts.reNew {
|
||||
return
|
||||
}
|
||||
|
||||
for !atomic.CompareAndSwapInt32(&r.isRuning, 0, 1) {
|
||||
}
|
||||
|
||||
var ctxn context.Context
|
||||
ctxn, r.calFn = context.WithCancel(ctx)
|
||||
|
||||
atomic.StoreInt32(&r.isRuning, 1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
atomic.StoreInt32(&r.isRuning, 0)
|
||||
}()
|
||||
r.watchDog(ctxn)
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *RedisLock) Lock(ctx context.Context) (err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
r.calFn = nil
|
||||
r.watch(ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
err = r.tryLock(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !r.opts.block || !errors.Is(err, ErrorsOtherLock) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.block(ctx)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *RedisLock) UnLock(ctx context.Context) error {
|
||||
if r.calFn != nil {
|
||||
r.calFn()
|
||||
}
|
||||
|
||||
return r.unLock(ctx)
|
||||
}
|
17
redis/lock/lua.go
Normal file
17
redis/lock/lua.go
Normal file
@ -0,0 +1,17 @@
|
||||
package lock
|
||||
|
||||
const LuaReNew=`
|
||||
if (redis.call("get",KEYS[1]) == ARGV[1]) then
|
||||
return redis.call("expire",KEYS[1],ARGV[2])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
`
|
||||
|
||||
const LuaUnLock=`
|
||||
if (redis.call("get",KEYS[1]) == ARGV[1]) then
|
||||
return redis.call("del",KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
`
|
35
redis/lock/option.go
Normal file
35
redis/lock/option.go
Normal file
@ -0,0 +1,35 @@
|
||||
package lock
|
||||
|
||||
type Options struct {
|
||||
maxWaitTime int
|
||||
expireTime int
|
||||
block bool
|
||||
reNew bool
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
func (o *Options) repire() {
|
||||
if o.expireTime <= 0 {
|
||||
o.expireTime = 10
|
||||
o.reNew = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithExpireTime(expire int) Option {
|
||||
return func(o *Options) {
|
||||
o.expireTime = expire
|
||||
}
|
||||
}
|
||||
|
||||
func WithBlock() Option {
|
||||
return func(o *Options) {
|
||||
o.block = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithMaxWaitTime(watiTime int) Option {
|
||||
return func(o *Options) {
|
||||
o.maxWaitTime = watiTime
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user