├── springcache ├── springcache_test.go ├── springcache.go ├── byteview.go ├── cache.go ├── group.go └── server.go ├── pkg └── docker │ └── Dockerfile ├── connect ├── peers.go ├── discover.go ├── client.go └── register.go ├── springcachepb ├── springcachepb.proto ├── springcachepb_grpc.pb.go └── springcachepb.pb.go ├── singleflight └── singleflight.go ├── go.mod ├── LICENSE ├── lru ├── lru_test.go └── lru.go ├── consistenthash └── consistenthash.go ├── go.sum └── README.md /springcache/springcache_test.go: -------------------------------------------------------------------------------- 1 | package springcache 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestGetter(t *testing.T) { 9 | var f Getter = GetterFunc(func(key string) ([]byte, error) { 10 | return []byte(key), nil 11 | }) 12 | 13 | expect := []byte("key") 14 | if v, _ := f.Get("key"); !reflect.DeepEqual(v, expect) { 15 | t.Errorf("callback failed") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pkg/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | ENV IP_ADDRESS=127.0.0.1 \ 4 | GOPROXY='https://goproxy.cn,direct' \ 5 | GOOS=linux 6 | 7 | WORKDIR /build 8 | 9 | COPY . . 10 | 11 | RUN go build . 12 | 13 | FROM alpine 14 | 15 | COPY --from=builder /build/SpringCache / 16 | 17 | EXPOSE 9999 18 | EXPOSE 8888 19 | 20 | WORKDIR / 21 | 22 | CMD ["/bin/sh","-c","/SpringCache --name svc --peer svc --etcd 127.0.0.1:2379"] -------------------------------------------------------------------------------- /springcache/springcache.go: -------------------------------------------------------------------------------- 1 | package springcache 2 | 3 | // A Getter loads data for a key. 4 | // 设计一个回调函数,缓存未命中时会调用这个函数,去获取源数据 5 | type Getter interface { 6 | Get(key string) ([]byte, error) 7 | } 8 | 9 | // GetterFunc implements Getter 10 | // 函数类型实现某一个接口,称之为接口型函数,方便使用者在调用时既能够传入函数作为参数, 11 | // 也能够传入实现了该接口的结构体作为参数。 12 | type GetterFunc func(key string) ([]byte, error) 13 | 14 | func (f GetterFunc) Get(key string) ([]byte, error) { 15 | return f(key) 16 | } 17 | -------------------------------------------------------------------------------- /connect/peers.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import "time" 4 | 5 | // peers 是用于rpc交流的模块 6 | 7 | // PeerPicker 定义了获取分布式节点的能力,Server 8 | type PeerPicker interface { 9 | PickPeer(key string) (peer PeerGetter, ok bool) 10 | } 11 | 12 | // PeerGetter 定义了从远端获取缓存的能力,Client 13 | // 在connect.client 包中, 定义了结构体Client, 它有下面的Get方法和Set方法,满足了下面的接口,所以可以作为PeerGetter被使用 14 | type PeerGetter interface { 15 | Get(group string, key string) ([]byte, error) 16 | Set(group string, key string, value []byte, expire time.Time, ishot bool) error 17 | } 18 | -------------------------------------------------------------------------------- /springcachepb/springcachepb.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // protoc --go-grpc_out=. --go_out=. ./springcachepb/springcachepb.proto 4 | package springcachepb; 5 | 6 | option go_package = "./springcachepb"; 7 | 8 | message GetRequest{ 9 | string group = 1; 10 | string key = 2; 11 | } 12 | 13 | message GetResponse { 14 | bytes value =1 ; 15 | } 16 | 17 | message SetRequest{ 18 | string group = 1; 19 | string key = 2; 20 | bytes value = 3; 21 | int64 expire = 4; 22 | bool ishot = 5; 23 | } 24 | 25 | message SetResponse{ 26 | bool ok = 1; 27 | } 28 | 29 | service SpringCache { 30 | rpc Get(GetRequest) returns (GetResponse); 31 | rpc Set(SetRequest) returns (SetResponse); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /springcache/byteview.go: -------------------------------------------------------------------------------- 1 | package springcache 2 | 3 | import "time" 4 | 5 | // A ByteView holds an immutable view of bytes. 6 | // ByteView 用来表示缓存值,是SpringCache的存储单元,它实现了lru的Value接口,所以可以直接在lru里面进行存储 7 | type ByteView struct { 8 | b []byte 9 | e time.Time 10 | } 11 | 12 | func (v *ByteView) Len() int { 13 | return len(v.b) 14 | } 15 | 16 | // Returns the expire time associated with this view 17 | func (v ByteView) Expire() time.Time { 18 | return v.e 19 | } 20 | 21 | func (v *ByteView) ByteSlice() []byte { 22 | return cloneBytes(v.b) 23 | } 24 | 25 | func (v *ByteView) String() string { 26 | return string(v.b) 27 | } 28 | 29 | func cloneBytes(b []byte) []byte { 30 | c := make([]byte, len(b)) 31 | copy(c, b) 32 | return c 33 | } 34 | 35 | func NewByteView(b []byte, e time.Time) *ByteView { 36 | return &ByteView{b: b, e: e} 37 | } 38 | -------------------------------------------------------------------------------- /singleflight/singleflight.go: -------------------------------------------------------------------------------- 1 | package singleflight 2 | 3 | import "sync" 4 | 5 | type call struct { 6 | wg sync.WaitGroup 7 | val interface{} 8 | err error 9 | } 10 | 11 | type Group struct { 12 | mu sync.Mutex 13 | m map[string]*call 14 | } 15 | 16 | func (g *Group) DoOnce(key string, fn func() (interface{}, error)) (interface{}, error) { 17 | g.mu.Lock() 18 | if g.m == nil { 19 | g.m = make(map[string]*call) 20 | } 21 | if c, ok := g.m[key]; ok { 22 | // 如果g.m[key]不为空,则说明已经有其他线程在请求该key,则等待其他请求结束后一起返回 23 | g.mu.Unlock() 24 | c.wg.Wait() 25 | return c.val, c.err 26 | } 27 | c := new(call) 28 | c.wg.Add(1) // 发起请求前加锁 29 | g.m[key] = c // 加到map中,表示该key已经在请求 30 | g.mu.Unlock() 31 | 32 | c.val, c.err = fn() // 执行请求 33 | c.wg.Done() // 请求结束 34 | 35 | g.mu.Lock() 36 | delete(g.m, key) // 删除该key,已经执行完毕 37 | g.mu.Unlock() 38 | 39 | return c.val, c.err 40 | } 41 | -------------------------------------------------------------------------------- /springcache/cache.go: -------------------------------------------------------------------------------- 1 | package springcache 2 | 3 | import ( 4 | "SpringCache/lru" 5 | "sync" 6 | ) 7 | 8 | // 设计一个并发缓存 9 | type cache struct { 10 | mu sync.Mutex 11 | lru *lru.Cache 12 | cacheBytes int64 13 | } 14 | 15 | // add 使用锁保证数据的一致性,底层调用lru的Add方法调整lru结构 16 | func (c *cache) add(key string, value *ByteView) { 17 | c.mu.Lock() 18 | defer c.mu.Unlock() 19 | if c.lru == nil { 20 | if lru.DefaultMaxBytes > c.cacheBytes { 21 | c.lru = lru.New(lru.DefaultMaxBytes, nil) 22 | } else { 23 | c.lru = lru.New(c.cacheBytes, nil) 24 | } 25 | } 26 | c.lru.Add(key, value, value.Expire()) 27 | } 28 | 29 | // get 加锁,调用底层的Get 30 | func (c *cache) get(key string) (value *ByteView, ok bool) { 31 | c.mu.Lock() 32 | defer c.mu.Unlock() 33 | if c.lru == nil { 34 | return 35 | } 36 | if v, ok := c.lru.Get(key); ok { 37 | return v.(*ByteView), ok 38 | } 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module SpringCache 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/pkg/errors v0.8.1 7 | github.com/segmentio/fasthash v1.0.3 8 | go.etcd.io/etcd/client/v3 v3.5.17 9 | golang.org/x/net v0.23.0 10 | google.golang.org/grpc v1.59.0 11 | google.golang.org/protobuf v1.36.1 12 | ) 13 | 14 | require ( 15 | github.com/coreos/go-semver v0.3.0 // indirect 16 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 17 | github.com/gogo/protobuf v1.3.2 // indirect 18 | github.com/golang/protobuf v1.5.4 // indirect 19 | go.etcd.io/etcd/api/v3 v3.5.17 // indirect 20 | go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect 21 | go.uber.org/atomic v1.7.0 // indirect 22 | go.uber.org/multierr v1.6.0 // indirect 23 | go.uber.org/zap v1.17.0 // indirect 24 | golang.org/x/sys v0.18.0 // indirect 25 | golang.org/x/text v0.14.0 // indirect 26 | google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect 27 | google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect 28 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Spr1n9 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /connect/discover.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "context" 5 | clientv3 "go.etcd.io/etcd/client/v3" 6 | "go.etcd.io/etcd/client/v3/naming/resolver" 7 | "google.golang.org/grpc" 8 | "google.golang.org/grpc/credentials/insecure" 9 | "time" 10 | ) 11 | 12 | // DialPeer 传入etcd客户端和节点名,获取与其的grpc连接 13 | func DialPeer(c *clientv3.Client, service string) (conn *grpc.ClientConn, err error) { 14 | PeerResolver, err := resolver.NewBuilder(c) 15 | if err != nil { 16 | return 17 | } 18 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 19 | defer cancel() 20 | //log.Println("In discover.DialPeer, try to get conn, service :", service) 21 | 22 | return grpc.DialContext(ctx, 23 | "etcd:///"+service, 24 | grpc.WithResolvers(PeerResolver), 25 | grpc.WithTransportCredentials(insecure.NewCredentials()), 26 | grpc.WithBlock(), 27 | ) 28 | } 29 | 30 | // GetAddrByName 是根据服务名在etcd中进行服务发现并返回对应的ip地址 31 | func GetAddrByName(c *clientv3.Client, name string) (addr string, err error) { 32 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 33 | defer cancel() 34 | //log.Printf("debug, In discover.GetAddrByName, after get ctx") 35 | resp, err := c.Get(ctx, name) 36 | if err != nil { 37 | return "", err 38 | } 39 | return string(resp.Kvs[0].Value), nil 40 | } 41 | 42 | //func CheckIf 43 | -------------------------------------------------------------------------------- /lru/lru_test.go: -------------------------------------------------------------------------------- 1 | package lru 2 | 3 | import ( 4 | "container/list" 5 | "testing" 6 | ) 7 | 8 | type String string 9 | 10 | func (d String) Len() int { 11 | return len(d) 12 | } 13 | func TestGet(t *testing.T) { 14 | lru := New(int64(0), nil) 15 | lru.Add( 16 | "key1", String("1234"), 17 | ) 18 | if v, ok := lru.Get("key1"); !ok || string(v.(String)) != "1234" { 19 | t.Fatalf("cache hit key1=1234 failed") 20 | } 21 | if _, ok := lru.Get("key2"); ok { 22 | t.Fatalf("cache miss key2 failed") 23 | } 24 | } 25 | 26 | func TestCache_RemoveOldest(t *testing.T) { 27 | type fields struct { 28 | maxBytes int64 29 | nbytes int64 30 | ll *list.List 31 | cache map[string]*list.Element 32 | OnEvicted func(key string, value Value) 33 | } 34 | tests := []struct { 35 | name string 36 | fields fields 37 | }{ 38 | // TODO: Add test cases. 39 | { 40 | "1", fields{ 41 | maxBytes: 3, 42 | nbytes: 0, 43 | ll: new(list.List), 44 | cache: make(map[string]*list.Element), 45 | OnEvicted: nil, 46 | }, 47 | }, 48 | } 49 | for _, tt := range tests { 50 | t.Run(tt.name, func(t *testing.T) { 51 | c := &Cache{ 52 | maxBytes: tt.fields.maxBytes, 53 | nbytes: tt.fields.nbytes, 54 | ll: tt.fields.ll, 55 | cache: tt.fields.cache, 56 | OnEvicted: tt.fields.OnEvicted, 57 | } 58 | c.RemoveOldest() 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /connect/client.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | pb "SpringCache/springcachepb" 5 | "context" 6 | "fmt" 7 | "log" 8 | "time" 9 | ) 10 | 11 | // client 包是去调用远端的方法,封装了用grpc调用远端节点的Get和Set 方法 12 | 13 | type Client struct { 14 | Name string 15 | Etcd *Etcd 16 | } 17 | 18 | func newClient(name string, etcd *Etcd) *Client { 19 | return &Client{name, etcd} 20 | } 21 | 22 | func (c *Client) Get(group string, key string) ([]byte, error) { 23 | 24 | // 用etcd进行服务发现, 获得grpc的连接 25 | conn, err := DialPeer(c.Etcd.EtcdCli, c.Name) 26 | if err != nil { 27 | return nil, err 28 | } 29 | defer conn.Close() 30 | 31 | // 创建grpc客户端,调用远程peer的get方法 32 | grpcClient := pb.NewSpringCacheClient(conn) 33 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 34 | defer cancel() 35 | resp, err := grpcClient.Get(ctx, &pb.GetRequest{ 36 | Group: group, 37 | Key: key, 38 | }) 39 | if err != nil { 40 | return nil, fmt.Errorf("could not get %s/%s from peer %s", group, key, c.Name) 41 | } 42 | log.Println("In client.Get, grpcClient.Get Done, resp :", resp) 43 | return resp.GetValue(), nil 44 | } 45 | 46 | func (c *Client) Set(group string, key string, value []byte, expire time.Time, ishot bool) error { 47 | 48 | // 用etcd进行服务发现, 获得grpc的连接 49 | conn, err := DialPeer(c.Etcd.EtcdCli, c.Name) 50 | if err != nil { 51 | return err 52 | } 53 | defer conn.Close() 54 | 55 | // 创建grpc客户端,调用远程peer的get方法 56 | grpcClient := pb.NewSpringCacheClient(conn) 57 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 58 | defer cancel() 59 | resp, err := grpcClient.Set(ctx, &pb.SetRequest{ 60 | Group: group, 61 | Key: key, 62 | Value: value, 63 | Expire: expire.Unix(), 64 | Ishot: ishot, 65 | }) 66 | if err != nil { 67 | log.Println("grpcClient.Set Error:", err) 68 | return err 69 | } 70 | if !resp.GetOk() { 71 | return fmt.Errorf("grpcClient.Set Failed !") 72 | } 73 | return nil 74 | } 75 | 76 | // 验证是否实现接口 77 | var _ PeerGetter = (*Client)(nil) 78 | -------------------------------------------------------------------------------- /consistenthash/consistenthash.go: -------------------------------------------------------------------------------- 1 | package consistenthash 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "github.com/segmentio/fasthash/fnv1" 7 | "sort" 8 | "strconv" 9 | "sync" 10 | ) 11 | 12 | type Hash func(data []byte) uint64 13 | 14 | type Map struct { 15 | sync.Mutex 16 | hash Hash // 所使用的哈希算法 17 | replicas int // 虚拟节点倍数 18 | keys []int // 存储一致性哈希的真实和虚拟节点的数组的哈希环 19 | hashMap map[int]string // 虚拟节点与真实节点的映射表 20 | } 21 | 22 | func New(replicas int, fn Hash) *Map { 23 | m := &Map{ 24 | replicas: replicas, 25 | hash: fn, 26 | hashMap: make(map[int]string), 27 | } 28 | // 如果没有选择哈希算法,则使用默认的fnv1算法 29 | if m.hash == nil { 30 | m.hash = fnv1.HashBytes64 31 | // m.hash = crc32.ChecksumIEEE 32 | } 33 | return m 34 | } 35 | 36 | // AddNodes add some nodes to hash 37 | func (m *Map) AddNodes(keys ...string) { 38 | m.Lock() 39 | defer m.Unlock() 40 | //log.Println("In consistenthash.AddNodes, keys =", keys) 41 | for _, key := range keys { 42 | // 对每个新加的真实节点都创建一些虚拟节点,用于解决一致性哈希中数据倾斜的问题 43 | for i := 0; i < m.replicas; i++ { 44 | //log.Printf("当前处理节点是%v,正在添加它的第%v个虚拟节点", key, i) 45 | hash := int(m.hash([]byte(fmt.Sprintf("%x", md5.Sum([]byte(strconv.Itoa(i)+key)))))) // 通过哈希算法得到虚拟节点的哈希值 46 | m.keys = append(m.keys, hash) 47 | m.hashMap[hash] = key // 把映射记录到表中 hashMap[虚拟节点哈希]真实节点key 48 | } 49 | } 50 | sort.Ints(m.keys) // 将哈希环的节点排序 51 | //fmt.Printf("In consistenthash.AddNodes, m.keys = %v, m.hashMap = %v \n", m.keys, m.hashMap) 52 | } 53 | 54 | // Get 会根据传入的键去找到存储该键值对的真实节点,并返回真实节点的ip地址 55 | func (m *Map) Get(key string) string { 56 | 57 | if len(m.keys) == 0 { 58 | return "" 59 | } 60 | hash := int(m.hash([]byte(key))) 61 | // 顺时针匹配第一个虚拟节点 62 | //idx := sort.Search(len(m.keys), func(i int) bool { 63 | // return m.keys[i] >= hash 64 | //}) 65 | 66 | // 通过二分查找找到节点 67 | // 注意这里的j不是 len(m.keys)-1 ,因为我们要让他在查找的哈希值大于环上最大值时 68 | // 返回结果为len(m.keys) 69 | i, j := 0, len(m.keys) 70 | for i < j { 71 | mid := uint((i + j) >> 1) 72 | if m.keys[mid] >= hash { 73 | j = int(mid) 74 | } else if m.keys[mid] < hash { 75 | i = int(mid) + 1 76 | } 77 | } 78 | idx := i 79 | // 有一种特殊情况是 当加入的hash值超过环上最大节点的哈希值时,需要将其指向第一个节点 80 | // 当这种情况发生时, 如果当前哈希表中有9个节点,那么 idx 会等于 9 81 | // 所以对idx与9取余, 使得当idx=9 的时候能归并为 0 ,也就是第一个节点 82 | //log.Println("here, consistenthash.Get, idx: ", idx, ", m.keys[idx%len(m.keys)]:", m.keys[idx%len(m.keys)], ", m.hashMap[m.keys[idx%len(m.keys)]]:", m.hashMap[m.keys[idx%len(m.keys)]]) 83 | return m.hashMap[m.keys[idx%len(m.keys)]] 84 | } 85 | 86 | func (m *Map) Remove(key string) { 87 | m.Lock() 88 | defer m.Unlock() 89 | for i := 0; i < m.replicas; i++ { 90 | hash := int(m.hash([]byte(fmt.Sprintf("%x", md5.Sum([]byte(strconv.Itoa(i)+key)))))) 91 | idx := sort.SearchInts(m.keys, hash) 92 | m.keys = append(m.keys[:idx], m.keys[idx+1:]...) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lru/lru.go: -------------------------------------------------------------------------------- 1 | // 这里实现lru 算法 2 | package lru 3 | 4 | import ( 5 | "container/list" 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | var DefaultMaxBytes int64 = 10 11 | var DefaultExpireRandom time.Duration = 3 * time.Minute 12 | 13 | type NowFunc func() time.Time 14 | 15 | var nowFunc NowFunc = time.Now 16 | 17 | type Cache struct { 18 | maxBytes int64 // 允许使用的最大内存 19 | nbytes int64 // 当前已经使用的内存 20 | ll *list.List // 双向队列,用于lru算法 21 | cache map[string]*list.Element // 实际保存键值的缓存 22 | OnEvicted func(key string, value Value) // 当节点被删除时可以选择性调用回调函数 23 | 24 | // Now is the Now() function the cache will use to determine 25 | // the current time which is used to calculate expired values 26 | // Defaults to time.Now() 27 | Now NowFunc 28 | // 29 | ExpireRandom time.Duration 30 | } 31 | 32 | type entry struct { 33 | key string 34 | value Value 35 | expire time.Time // 过期时间 36 | addTime time.Time 37 | } 38 | 39 | type Value interface { 40 | Len() int 41 | } 42 | 43 | func New(maxBytes int64, onEvicted func(string, Value)) *Cache { 44 | return &Cache{ 45 | maxBytes: maxBytes, 46 | ll: list.New(), 47 | cache: make(map[string]*list.Element), 48 | OnEvicted: onEvicted, 49 | Now: nowFunc, 50 | ExpireRandom: DefaultExpireRandom, 51 | } 52 | } 53 | 54 | func (c *Cache) Len() int { 55 | return c.ll.Len() 56 | } 57 | 58 | // Get 是用于处理lru逻辑的函数,当请求到某个key就会把他置为队尾 59 | func (c *Cache) Get(key string) (value Value, ok bool) { 60 | // 如果在缓存中找到对应的节点,则把他移动到队尾 61 | if ele, ok := c.cache[key]; ok { 62 | // ll.Value 是一个interface{}类型,可以存储任何类型的数据 63 | // 所以如果之前链表里存储的是*entry类型,这里就可以断言为*entry类型 64 | kv := ele.Value.(*entry) 65 | // 如果kv过期了,将它们移除缓存 66 | if kv.expire.Before(time.Now()) { 67 | c.removeElement(ele) 68 | return nil, false 69 | } 70 | // 如果没有过期,更新键值对的添加实现为现在 71 | expireTime := kv.expire.Sub(kv.addTime) 72 | kv.expire = time.Now().Add(expireTime) 73 | kv.addTime = time.Now() 74 | // 双向链表作为队列,队首队尾是相对的,在这里约定 front 为队尾 75 | c.ll.MoveToFront(ele) 76 | return kv.value, true 77 | } 78 | return nil, false 79 | } 80 | 81 | func (c *Cache) RemoveOldest() { 82 | ele := c.ll.Back() 83 | if ele != nil { 84 | // 在lru队列中删除这个节点 85 | c.ll.Remove(ele) 86 | kv := ele.Value.(*entry) 87 | // 在缓存中删除这个节点 88 | delete(c.cache, kv.key) 89 | c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) 90 | if c.OnEvicted != nil { 91 | c.OnEvicted(kv.key, kv.value) 92 | } 93 | } 94 | } 95 | 96 | func (c *Cache) Remove(key string) { 97 | if ele, ok := c.cache[key]; ok { 98 | c.removeElement(ele) 99 | } 100 | } 101 | 102 | func (c *Cache) removeElement(ele *list.Element) { 103 | kv := ele.Value.(*entry) 104 | delete(c.cache, kv.key) 105 | c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) 106 | if c.OnEvicted != nil { 107 | c.OnEvicted(kv.key, kv.value) 108 | } 109 | } 110 | 111 | func (c *Cache) Add(key string, value Value, expire time.Time) { 112 | // randDuration 是用户添加的过期时间进行一定范围的随机,用于防止大量缓存同一时间过期而发生缓存雪崩 113 | randDuration := time.Duration(rand.Int63n(int64(c.ExpireRandom))) 114 | 115 | if ele, ok := c.cache[key]; ok { 116 | // 如果key已经存在则将value替换 117 | c.ll.MoveToFront(ele) 118 | kv := ele.Value.(*entry) 119 | c.nbytes += int64(value.Len()) - int64(kv.value.Len()) 120 | kv.value = value 121 | kv.expire = expire.Add(randDuration) 122 | } else { 123 | newEle := c.ll.PushFront(&entry{key, value, expire.Add(randDuration), time.Now()}) 124 | c.cache[key] = newEle 125 | c.nbytes += int64(len(key)) + int64(value.Len()) 126 | } 127 | for c.maxBytes != 0 && c.maxBytes < c.nbytes { 128 | c.RemoveOldest() 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /connect/register.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "fmt" 5 | clientv3 "go.etcd.io/etcd/client/v3" 6 | "go.etcd.io/etcd/client/v3/naming/endpoints" 7 | "golang.org/x/net/context" 8 | "log" 9 | "time" 10 | ) 11 | 12 | var ( 13 | //defaultEndpoints = []string{"10.0.0.166:2379"} 14 | defaultTimeout = 3 * time.Second 15 | defaultLeaseExpTime = 10 16 | ) 17 | 18 | type Etcd struct { 19 | EtcdCli *clientv3.Client 20 | leaseId clientv3.LeaseID // 租约ID 21 | ctx context.Context 22 | cancel context.CancelFunc 23 | } 24 | 25 | func NewEtcd(endpoints []string) (*Etcd, error) { 26 | client, err := clientv3.New(clientv3.Config{ 27 | Endpoints: endpoints, 28 | DialTimeout: defaultTimeout, 29 | }) 30 | if err != nil { 31 | log.Println("create etcd register err:", err) 32 | return nil, err 33 | } 34 | // defer client.Close() 35 | ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) 36 | svr := &Etcd{ 37 | EtcdCli: client, 38 | ctx: ctx, 39 | cancel: cancel, 40 | } 41 | return svr, nil 42 | } 43 | 44 | // CreateLease 为注册在etcd上的节点创建租约。 由于服务端无法保证自身是一直可用的,可能会宕机,所以与etcd的租约是有时间期限的,租约一旦过期,服务端存储在etcd上的服务地址信息就会消失。 45 | // 另一方面,如果服务端是正常运行的,etcd中的地址信息又必须存在,因此发送心跳检测,一旦发现etcd上没有自己的服务地址时,请求重新添加(续租)。 46 | func (s *Etcd) CreateLease(expireTime int) error { 47 | 48 | res, err := s.EtcdCli.Grant(s.ctx, int64(expireTime)) 49 | if err != nil { 50 | return err 51 | } 52 | s.leaseId = res.ID 53 | log.Println("create lease success:", s.leaseId) 54 | return nil 55 | 56 | } 57 | 58 | // BindLease 把服务和对应的租约绑定 59 | func (s *Etcd) BindLease(server string, addr string) error { 60 | 61 | _, err := s.EtcdCli.Put(s.ctx, server, addr, clientv3.WithLease(s.leaseId)) 62 | if err != nil { 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | func (s *Etcd) KeepAlive() error { 69 | log.Println("keep alive start") 70 | log.Println("s.leaseId:", s.leaseId) 71 | KeepRespChan, err := s.EtcdCli.KeepAlive(context.Background(), s.leaseId) 72 | if err != nil { 73 | log.Println("keep alive err:", err) 74 | return err 75 | } 76 | go func() { 77 | for { 78 | for KeepResp := range KeepRespChan { 79 | if KeepResp == nil { 80 | fmt.Println("keep alive is stop") 81 | return 82 | } else { 83 | fmt.Println("keep alive is ok") 84 | } 85 | } 86 | time.Sleep(5 * time.Second) 87 | } 88 | }() 89 | return nil 90 | } 91 | 92 | //func (s *Etcd) KeepAlive() error { 93 | // log.Println("keep alive start") 94 | // log.Println("s.leaseId:", s.leaseId) 95 | // KeepRespChan, err := s.EtcdCli.KeepAlive(context.Background(), s.leaseId) 96 | // if err != nil { 97 | // log.Println("keep alive err:", err) 98 | // return err 99 | // } 100 | // go func() { 101 | // for { 102 | // select { 103 | // case KeepResp := <-KeepRespChan: 104 | // if KeepResp == nil { 105 | // fmt.Println("keep alive is stop") 106 | // return 107 | // } else { 108 | // fmt.Println("keep alive is ok") 109 | // } 110 | // } 111 | // time.Sleep(7 * time.Second) 112 | // } 113 | // }() 114 | // return nil 115 | //} 116 | 117 | // RegisterServer 会把serviceName作为key,addr作为value 存储到etcd中 118 | func (s *Etcd) RegisterServer(serviceName, addr string) error { 119 | // 创建租约 120 | err := s.CreateLease(defaultLeaseExpTime) 121 | if err != nil { 122 | log.Println("create etcd register err:", err) 123 | return err 124 | } 125 | // 绑定租约 126 | err = s.BindLease(serviceName, addr) 127 | if err != nil { 128 | log.Println("bind etcd register err:", err) 129 | return err 130 | } 131 | // 心跳检测 132 | err = s.KeepAlive() 133 | if err != nil { 134 | log.Println("keep alive register err:", err) 135 | return err 136 | } 137 | // 注册服务用于服务发现 138 | em, err := endpoints.NewManager(s.EtcdCli, serviceName) 139 | if err != nil { 140 | log.Println("create etcd register err:", err) 141 | return err 142 | } 143 | return em.AddEndpoint(s.ctx, serviceName+"/"+addr, endpoints.Endpoint{Addr: addr}, clientv3.WithLease(s.leaseId)) 144 | } 145 | -------------------------------------------------------------------------------- /springcache/group.go: -------------------------------------------------------------------------------- 1 | package springcache 2 | 3 | import ( 4 | "SpringCache/connect" 5 | "SpringCache/singleflight" 6 | "fmt" 7 | "github.com/pkg/errors" 8 | "log" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | var DefaultExpireTime = 30 * time.Second // 设置短过期时间用于测试 14 | 15 | // Group 是 SpringCache 最核心的数据结构,负责与用户的交互,并且控制缓存值存储和获取的流程。 16 | type Group struct { 17 | name string 18 | getter Getter // 获取源数据的接口 19 | mainCache cache // 哈希算法本地存储的键值对 20 | hotCache cache // 热点数据 21 | peers connect.PeerPicker 22 | // use singleflight.Group to make sure that 23 | // each key is only fetched once 24 | loader *singleflight.Group // 用于控制并发问题 25 | } 26 | 27 | var ( 28 | mu sync.RWMutex 29 | groups = make(map[string]*Group) // 全局变量groups,里面记录了已经创建的group 30 | ) 31 | 32 | func NewGroup(name string, cacheBytes int64, hotcacheBytes int64, getter Getter) *Group { 33 | if getter == nil { 34 | panic("springcache: getter is nil") 35 | } 36 | mu.Lock() 37 | defer mu.Unlock() 38 | g := &Group{ 39 | name: name, 40 | getter: getter, 41 | mainCache: cache{cacheBytes: cacheBytes}, 42 | hotCache: cache{cacheBytes: hotcacheBytes}, 43 | loader: &singleflight.Group{}, 44 | } 45 | groups[name] = g 46 | return g 47 | } 48 | 49 | func (g *Group) RegisterPeers(peers connect.PeerPicker) { 50 | if g.peers != nil { 51 | panic("springcache: peer already registered") 52 | } 53 | g.peers = peers 54 | } 55 | 56 | func GetGroup(name string) *Group { 57 | mu.RLock() 58 | g := groups[name] 59 | mu.RUnlock() 60 | return g 61 | } 62 | 63 | func (g *Group) Get(key string) (*ByteView, error) { 64 | if key == "" { 65 | return &ByteView{}, fmt.Errorf("springcache: key is empty") 66 | } 67 | if v, ok := g.lookupCache(key); ok { 68 | log.Println("SpringCache hit") 69 | return v, nil 70 | } 71 | log.Println("SpringCache miss, try to add it") 72 | return g.Load(key) 73 | } 74 | 75 | // 如果缓存没有命中,则将去远程节点进行查询或查询数据库 76 | // Load loads key either by invoking the getter locally or by sending it to another machine. 77 | func (g *Group) Load(key string) (value *ByteView, err error) { 78 | // 用Do函数封装实际的load操作,保证并发性 79 | view, err := g.loader.DoOnce(key, func() (interface{}, error) { 80 | if g.peers != nil { 81 | log.Println("try to search from peers") 82 | if peer, ok := g.peers.PickPeer(key); ok { 83 | if value, err = g.getFromPeer(peer, key); err != nil { 84 | log.Println("springcache: get from peer error:", err) 85 | return nil, err 86 | } 87 | return value, nil 88 | } 89 | } 90 | return g.getLocally(key) 91 | }) 92 | if err == nil { 93 | return view.(*ByteView), nil 94 | } 95 | return 96 | } 97 | 98 | func (g *Group) getFromPeer(peer connect.PeerGetter, key string) (*ByteView, error) { 99 | bytes, err := peer.Get(g.name, key) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return &ByteView{b: bytes}, nil 104 | } 105 | 106 | // 在数据库中查到数据后,添加到缓存中 107 | func (g *Group) getLocally(key string) (*ByteView, error) { 108 | // 这里调用的是创建Group时存储的getter函数 109 | bytes, err := g.getter.Get(key) 110 | if err != nil { 111 | return &ByteView{}, err 112 | } 113 | value := &ByteView{b: cloneBytes(bytes), e: time.Now().Add(DefaultExpireTime)} 114 | g.populateCache(key, value) 115 | return value, nil 116 | } 117 | 118 | // populateCache 将源数据添加到缓存 mainCache 119 | func (g *Group) populateCache(key string, value *ByteView) { 120 | g.mainCache.add(key, value) 121 | } 122 | 123 | func (g *Group) lookupCache(key string) (value *ByteView, ok bool) { 124 | value, ok = g.mainCache.get(key) 125 | if ok { 126 | return 127 | } 128 | value, ok = g.hotCache.get(key) 129 | return 130 | } 131 | 132 | func (g *Group) Set(key string, value *ByteView, ishot bool) error { 133 | if key == "" { 134 | return errors.New("key is empty") 135 | } 136 | if ishot { 137 | return g.setHotCache(key, value) 138 | } 139 | _, err := g.loader.DoOnce(key, func() (interface{}, error) { 140 | if peer, ok := g.peers.PickPeer(key); ok { 141 | err := g.setFromPeer(peer, key, value, ishot) 142 | if err != nil { 143 | log.Println("springcache: set from peer error:", err) 144 | return nil, err 145 | } 146 | return value, nil 147 | } 148 | // 如果 !ok,则说明选择到当前节点 149 | g.mainCache.add(key, value) 150 | return value, nil 151 | }) 152 | return err 153 | } 154 | 155 | func (g *Group) setFromPeer(peer connect.PeerGetter, key string, value *ByteView, ishot bool) error { 156 | return peer.Set(g.name, key, value.ByteSlice(), value.Expire(), ishot) 157 | } 158 | 159 | // setHotCache 设置热点缓存 160 | func (g *Group) setHotCache(key string, value *ByteView) error { 161 | if key == "" { 162 | return errors.New("key is empty") 163 | } 164 | g.loader.DoOnce(key, func() (interface{}, error) { 165 | g.hotCache.add(key, value) 166 | log.Printf("SpringCache set hot cache %v \n", value.ByteSlice()) 167 | return nil, nil 168 | }) 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /springcache/server.go: -------------------------------------------------------------------------------- 1 | package springcache 2 | 3 | import ( 4 | "SpringCache/connect" 5 | "SpringCache/consistenthash" 6 | pb "SpringCache/springcachepb" 7 | "fmt" 8 | "github.com/pkg/errors" 9 | "golang.org/x/net/context" 10 | "google.golang.org/grpc" 11 | "log" 12 | "net" 13 | "strings" 14 | "sync" 15 | "time" 16 | ) 17 | 18 | // server 处理别人发来的请求 19 | var ( 20 | ErrorServerHasStarted = errors.New("server has started, err:") 21 | ErrorTcpListen = errors.New("tcp listen error") 22 | ErrorRegisterEtcd = errors.New("register etcd error") 23 | ErrorGrpcServerStart = errors.New("start grpc server error") 24 | ) 25 | 26 | var ( 27 | defaultListenAddr = "0.0.0.0:8888" 28 | ) 29 | 30 | const ( 31 | defaultReplicas = 50 32 | ) 33 | 34 | type Server struct { 35 | pb.UnimplementedSpringCacheServer 36 | 37 | status bool // 标记服务是否正在运行 38 | self string // 标记自己的ip地址 39 | mu sync.Mutex 40 | peers *consistenthash.Map // 哈希相关 41 | etcd *connect.Etcd 42 | name string 43 | clients map[string]*connect.Client // 【节点名】客户端 44 | } 45 | 46 | // NewServer 会创建一个grpc服务端,并绑定与etcd进行绑定 47 | func NewServer(serverName, selfAddr string, etcd *connect.Etcd) *Server { 48 | 49 | return &Server{ 50 | self: selfAddr, 51 | status: false, 52 | peers: consistenthash.New(defaultReplicas, nil), 53 | etcd: etcd, 54 | clients: make(map[string]*connect.Client), 55 | name: serverName, 56 | } 57 | } 58 | 59 | // 实现grpc定义的接口Get,即当远端调用该节点时查找缓存时,返回对应的值 60 | func (s *Server) Get(ctx context.Context, in *pb.GetRequest) (out *pb.GetResponse, err error) { 61 | groupName, key := in.GetGroup(), in.GetKey() 62 | group := GetGroup(groupName) 63 | bytes, err := group.Get(key) 64 | if err != nil { 65 | return nil, err 66 | } 67 | out = &pb.GetResponse{ 68 | Value: bytes.ByteSlice(), 69 | } 70 | return out, nil 71 | } 72 | 73 | // 实现grpc定义的接口Set,远端调用该节点设置缓存 74 | func (s *Server) Set(ctx context.Context, in *pb.SetRequest) (out *pb.SetResponse, err error) { 75 | groupName, key, value, expire := in.GetGroup(), in.GetKey(), in.GetValue(), in.GetExpire() 76 | ishot := in.GetIshot() 77 | group := GetGroup(groupName) 78 | bytes := NewByteView(value, time.Unix(expire, 0)) 79 | out = &pb.SetResponse{ 80 | Ok: false, 81 | } 82 | err = group.Set(key, bytes, ishot) 83 | if err != nil { 84 | return out, err 85 | } 86 | return &pb.SetResponse{Ok: true}, nil 87 | } 88 | 89 | func (s *Server) Log(format string, v ...interface{}) { 90 | log.Printf("[Server %s] %s", s.self, fmt.Sprintf(format, v...)) 91 | } 92 | 93 | // SetPeers 会把节点名在etcd中进行服务发现,并把获取的ip地址加入到哈希环中,并且把客户端保存到clients这个map中方便后面调用 94 | func (s *Server) SetPeers(names ...string) { 95 | 96 | for _, name := range names { 97 | //log.Printf("debug, In server.SetPeers, name:", name) 98 | ip, err := connect.GetAddrByName(s.etcd.EtcdCli, name) 99 | if err != nil { 100 | log.Printf("SetPeers err : %v", err) 101 | return 102 | } 103 | //log.Printf("debug, In server.SetPeers, ip:", ip) 104 | addr := strings.Split(ip, ":")[0] 105 | s.peers.AddNodes(addr) 106 | s.clients[addr] = &connect.Client{name, s.etcd} 107 | } 108 | //log.Println("SetPeers success, s.clients =", s.clients) 109 | } 110 | 111 | // PickPeer 包装了一致性哈希算法的 Get() 方法,根据具体的 key,选择节点, 112 | // 返回节点对应的 rpc服务器。 113 | func (s *Server) PickPeer(key string) (connect.PeerGetter, bool) { 114 | //s.mu.Lock() 115 | //defer s.mu.Unlock() 116 | if peer := s.peers.Get(key); peer != "" { 117 | ip := strings.Split(s.self, ":")[0] 118 | if peer == ip { 119 | log.Println("ops! peek my self! , i am :", peer) 120 | return nil, false 121 | } 122 | s.Log("Pick peer %s", peer) 123 | return s.clients[peer], true 124 | } 125 | return nil, false 126 | } 127 | 128 | // 根据key找出移除哈希环上存储该键值对的节点,并移除这个节点 129 | func (s *Server) RemovePeerByKey(key string) { 130 | peer := s.peers.Get(key) 131 | s.peers.Remove(peer) 132 | log.Printf("RemovePeer %s", peer) 133 | } 134 | 135 | // StartServer 开启grpc服务,并在etcd上注册 136 | func (s *Server) StartServer() error { 137 | // -----------------启动服务---------------------- 138 | // 1. 设置status为true 表示服务器已在运行 139 | // 2. 初始化tcp socket并开始监听 140 | // 3. 注册rpc服务至grpc 这样grpc收到request可以分发给server处理 141 | // ---------------------------------------------- 142 | s.mu.Lock() 143 | if s.status { 144 | s.mu.Unlock() 145 | return ErrorServerHasStarted 146 | } 147 | // 开启grpc 148 | lis, err := net.Listen("tcp", defaultListenAddr) 149 | if err != nil { 150 | log.Println("listen server error:", err) 151 | return ErrorTcpListen 152 | } 153 | grpcServer := grpc.NewServer() 154 | pb.RegisterSpringCacheServer(grpcServer, s) 155 | 156 | log.Println("start grpc server:", s.self) 157 | err = grpcServer.Serve(lis) 158 | if err != nil { 159 | log.Println(ErrorGrpcServerStart, "err: ", err) 160 | return ErrorGrpcServerStart 161 | } 162 | s.status = true 163 | s.mu.Unlock() 164 | return nil 165 | } 166 | 167 | var _ connect.PeerPicker = (*Server)(nil) 168 | -------------------------------------------------------------------------------- /springcachepb/springcachepb_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc v5.29.1 5 | // source: springcachepb/springcachepb.proto 6 | 7 | package springcachepb 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // SpringCacheClient is the client API for SpringCache service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type SpringCacheClient interface { 25 | Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) 26 | Set(ctx context.Context, in *SetRequest, opts ...grpc.CallOption) (*SetResponse, error) 27 | } 28 | 29 | type springCacheClient struct { 30 | cc grpc.ClientConnInterface 31 | } 32 | 33 | func NewSpringCacheClient(cc grpc.ClientConnInterface) SpringCacheClient { 34 | return &springCacheClient{cc} 35 | } 36 | 37 | func (c *springCacheClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) { 38 | out := new(GetResponse) 39 | err := c.cc.Invoke(ctx, "/springcachepb.SpringCache/Get", in, out, opts...) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return out, nil 44 | } 45 | 46 | func (c *springCacheClient) Set(ctx context.Context, in *SetRequest, opts ...grpc.CallOption) (*SetResponse, error) { 47 | out := new(SetResponse) 48 | err := c.cc.Invoke(ctx, "/springcachepb.SpringCache/Set", in, out, opts...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return out, nil 53 | } 54 | 55 | // SpringCacheServer is the server API for SpringCache service. 56 | // All implementations must embed UnimplementedSpringCacheServer 57 | // for forward compatibility 58 | type SpringCacheServer interface { 59 | Get(context.Context, *GetRequest) (*GetResponse, error) 60 | Set(context.Context, *SetRequest) (*SetResponse, error) 61 | mustEmbedUnimplementedSpringCacheServer() 62 | } 63 | 64 | // UnimplementedSpringCacheServer must be embedded to have forward compatible implementations. 65 | type UnimplementedSpringCacheServer struct { 66 | } 67 | 68 | func (UnimplementedSpringCacheServer) Get(context.Context, *GetRequest) (*GetResponse, error) { 69 | return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") 70 | } 71 | func (UnimplementedSpringCacheServer) Set(context.Context, *SetRequest) (*SetResponse, error) { 72 | return nil, status.Errorf(codes.Unimplemented, "method Set not implemented") 73 | } 74 | func (UnimplementedSpringCacheServer) mustEmbedUnimplementedSpringCacheServer() {} 75 | 76 | // UnsafeSpringCacheServer may be embedded to opt out of forward compatibility for this service. 77 | // Use of this interface is not recommended, as added methods to SpringCacheServer will 78 | // result in compilation errors. 79 | type UnsafeSpringCacheServer interface { 80 | mustEmbedUnimplementedSpringCacheServer() 81 | } 82 | 83 | func RegisterSpringCacheServer(s grpc.ServiceRegistrar, srv SpringCacheServer) { 84 | s.RegisterService(&SpringCache_ServiceDesc, srv) 85 | } 86 | 87 | func _SpringCache_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 88 | in := new(GetRequest) 89 | if err := dec(in); err != nil { 90 | return nil, err 91 | } 92 | if interceptor == nil { 93 | return srv.(SpringCacheServer).Get(ctx, in) 94 | } 95 | info := &grpc.UnaryServerInfo{ 96 | Server: srv, 97 | FullMethod: "/springcachepb.SpringCache/Get", 98 | } 99 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 100 | return srv.(SpringCacheServer).Get(ctx, req.(*GetRequest)) 101 | } 102 | return interceptor(ctx, in, info, handler) 103 | } 104 | 105 | func _SpringCache_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 106 | in := new(SetRequest) 107 | if err := dec(in); err != nil { 108 | return nil, err 109 | } 110 | if interceptor == nil { 111 | return srv.(SpringCacheServer).Set(ctx, in) 112 | } 113 | info := &grpc.UnaryServerInfo{ 114 | Server: srv, 115 | FullMethod: "/springcachepb.SpringCache/Set", 116 | } 117 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 118 | return srv.(SpringCacheServer).Set(ctx, req.(*SetRequest)) 119 | } 120 | return interceptor(ctx, in, info, handler) 121 | } 122 | 123 | // SpringCache_ServiceDesc is the grpc.ServiceDesc for SpringCache service. 124 | // It's only intended for direct use with grpc.RegisterService, 125 | // and not to be introspected or modified (even as a copy) 126 | var SpringCache_ServiceDesc = grpc.ServiceDesc{ 127 | ServiceName: "springcachepb.SpringCache", 128 | HandlerType: (*SpringCacheServer)(nil), 129 | Methods: []grpc.MethodDesc{ 130 | { 131 | MethodName: "Get", 132 | Handler: _SpringCache_Get_Handler, 133 | }, 134 | { 135 | MethodName: "Set", 136 | Handler: _SpringCache_Set_Handler, 137 | }, 138 | }, 139 | Streams: []grpc.StreamDesc{}, 140 | Metadata: "springcachepb/springcachepb.proto", 141 | } 142 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= 2 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 3 | github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= 4 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 9 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 10 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 11 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 12 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 13 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 14 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 16 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 17 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 18 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= 22 | github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= 23 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 24 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 25 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 26 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 27 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 28 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 29 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 30 | go.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w= 31 | go.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4= 32 | go.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw= 33 | go.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w= 34 | go.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY= 35 | go.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo= 36 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 37 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 38 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 39 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 40 | go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= 41 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 42 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 43 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 44 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 45 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 46 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 47 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 48 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 49 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 50 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 51 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 52 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 53 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 54 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 56 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 57 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 60 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 61 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 62 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 63 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 64 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 65 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 66 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 67 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 68 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 69 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 70 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 71 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 72 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= 74 | google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= 75 | google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= 76 | google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= 77 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= 78 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= 79 | google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= 80 | google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= 81 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 82 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 85 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 86 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 89 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 90 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # springcache 2 | 3 | ## 简介 4 | 5 | 🙋这是一个由go语言编写的分布式缓存项目,类似groupcache,实现了缓存过期,热点数据等,并引入了etcd和grpc进行节点管理和服务通信。 6 | 7 | 8 | 9 | *A distributed caching system ,like groupcache, but enhanced with etcd and gRPC .* 10 | 11 | 12 | 13 | 这个项目由极客兔兔的GeeCache启发,并在原先的基础上引入了etcd 进行服务发现和节点存活检测,并参考groupcache的设计,加入了缓存过期机制,热点数据等。本人才疏学浅尚属新人,开源项目仅抛砖引玉,供大家参考学习指正。在此感谢GeeCache和groupcache,在我的学习路上帮助了很多。 14 | 15 | ## 安装方法 16 | 除了在github进行下载之外,你还可以通过docker进行下载,使用 `docker pull 1712617575/springcache` 进行拉取镜像并启动 17 | 详见 https://hub.docker.com/repository/docker/1712617575/springcache/general 18 | 19 | ## 使用方法 20 | 21 | 这里给出一个例子main.go 22 | 23 | ```go 24 | package main 25 | 26 | import ( 27 | "SpringCache/connect" 28 | "SpringCache/springcache" 29 | "flag" 30 | "fmt" 31 | "golang.org/x/net/context" 32 | "log" 33 | "net/http" 34 | "os" 35 | "strconv" 36 | "strings" 37 | "time" 38 | ) 39 | 40 | var store = map[string]string{ 41 | "Tom": "630", 42 | "Jack": "589", 43 | "Sam": "567", 44 | } 45 | 46 | // api服务 47 | func startAPIServer(apiAddr string, group *springcache.Group, svr *springcache.Server) { 48 | 49 | getHandle := func(w http.ResponseWriter, r *http.Request) { 50 | key := r.URL.Query().Get("key") 51 | view, err := group.Get(key) 52 | if err != nil { 53 | if err == context.DeadlineExceeded { 54 | // 如果发现超时,则说明远方节点不可用 55 | // 删除该节点在哈希环上的记录,并且向数据库请求数据 56 | svr.RemovePeerByKey(key) 57 | view, err = group.Load(key) 58 | if err != nil { 59 | http.Error(w, err.Error(), http.StatusInternalServerError) 60 | return 61 | } 62 | w.Header().Set("Content-Type", "application/octet-stream") 63 | value := fmt.Sprintf("value=%v\n", string(view.ByteSlice())) 64 | w.Write([]byte(value)) 65 | return 66 | } 67 | http.Error(w, err.Error(), http.StatusInternalServerError) 68 | return 69 | } 70 | w.Header().Set("Content-Type", "application/octet-stream") 71 | value := fmt.Sprintf("value=%v\n", string(view.ByteSlice())) 72 | w.Write([]byte(value)) 73 | } 74 | setPeerHandle := func(w http.ResponseWriter, r *http.Request) { 75 | err := r.ParseForm() 76 | if err != nil { 77 | http.Error(w, err.Error(), http.StatusInternalServerError) 78 | return 79 | } 80 | peer := r.FormValue("peer") 81 | if peer == "" { 82 | http.Error(w, "peer is not allow empty!", http.StatusInternalServerError) 83 | return 84 | } 85 | //log.Println("debug : In main.setPeerHandler, get peer:", peer) 86 | svr.SetPeers(peer) 87 | 88 | w.Header().Set("Content-Type", "application/octet-stream") 89 | w.Write([]byte(fmt.Sprintf("set peer %v successful\n", peer))) 90 | } 91 | setHandle := func(w http.ResponseWriter, r *http.Request) { 92 | err := r.ParseForm() 93 | if err != nil { 94 | http.Error(w, "Error ParseForm", http.StatusInternalServerError) 95 | return 96 | } 97 | key := r.FormValue("key") 98 | value := r.FormValue("value") 99 | expire := r.FormValue("expire") 100 | hot := r.FormValue("hot") 101 | expireTime, err := strconv.Atoi(expire) 102 | if err != nil { 103 | w.Write([]byte("请正确设置超时时间\"expire\",单位:分钟")) 104 | http.Error(w, "", http.StatusBadRequest) 105 | return 106 | } 107 | if expireTime < 0 || expireTime > 4321 { 108 | w.Write([]byte("过期时间设置有误,单位是分钟,最长过期时间不超过4320分钟(3天)")) 109 | http.Error(w, "", http.StatusBadRequest) 110 | return 111 | } 112 | ishot := false 113 | if hot == "true" { 114 | ishot = true 115 | } 116 | if hot != "true" && hot != "false" && hot != "" { 117 | w.Write([]byte("Invaild Param \"hot\" ")) 118 | http.Error(w, "", http.StatusBadRequest) 119 | return 120 | } 121 | exp := time.Duration(expireTime) * time.Minute 122 | exptime := time.Now().Add(exp) 123 | byteView := springcache.NewByteView([]byte(value), exptime) 124 | //log.Printf("debug, In main.sethandler, key=%v, value=%v, expire=%v, hot=%v\n", key, value, expire, hot) 125 | if err := group.Set(key, byteView, ishot); err != nil { 126 | log.Println(err) 127 | http.Error(w, err.Error(), http.StatusInternalServerError) 128 | return 129 | } 130 | w.Write([]byte("done\n")) 131 | } 132 | http.HandleFunc("/api/get", getHandle) 133 | http.HandleFunc("/setpeer", setPeerHandle) 134 | http.HandleFunc("/api/set", setHandle) 135 | log.Println("fontend server is running at", apiAddr[7:]) 136 | log.Fatal(http.ListenAndServe(apiAddr[7:], nil)) 137 | } 138 | 139 | func main() { 140 | 141 | var ( 142 | addr = os.Getenv("IP_ADDRESS") 143 | svrName = flag.String("name", "", "server name") 144 | port = flag.String("port", "8888", "server port") 145 | peers = flag.String("peer", "", "peers name") 146 | etcdAddr = flag.String("etcd", "127.0.0.1:2379", "etcd address") 147 | defaultApiAddr = "http://0.0.0.0:9999" 148 | ) 149 | flag.Parse() 150 | if *svrName == "" { 151 | log.Fatal("--name is required") 152 | } 153 | if *peers == "" { 154 | log.Fatal("--peer is required") 155 | } 156 | if !strings.Contains(*peers, *svrName) { 157 | log.Fatal("--peers must contains " + *svrName) 158 | } 159 | if addr == "" { 160 | log.Fatal("please set env IP_ADDRESS") 161 | } 162 | 163 | // 开启代码时要记得 --name,--peer, 设置好 env IP_ADDRESS 164 | 165 | // 新建cache实例 166 | group := springcache.NewGroup("scores", 2<<10, 2<<7, springcache.GetterFunc( 167 | func(key string) ([]byte, error) { 168 | log.Printf("Searching the \"%v\" from databases", key) 169 | if v, ok := store[key]; ok { 170 | return []byte(v), nil 171 | } 172 | return nil, fmt.Errorf("%s not exist", key) 173 | })) 174 | 175 | // 新建一个etcd的客户端 176 | etcd, err := connect.NewEtcd([]string{*etcdAddr}) 177 | if err != nil { 178 | log.Println("etcd connect err:", err) 179 | panic(err) 180 | } 181 | 182 | log.Println("server name: ", *svrName) 183 | address := fmt.Sprintf("%s:%s", addr, *port) 184 | err = etcd.RegisterServer(*svrName, address) 185 | if err != nil { 186 | log.Fatal("register server error:", err) 187 | } 188 | log.Println("register server is Done") 189 | 190 | log.Println("grpc server address :", address) 191 | // 新建grpc Server 192 | svr := springcache.NewServer(*svrName, address, etcd) 193 | 194 | // 把节点加入到哈希环中 195 | // 检查其余节点是否在etcd中注册,如果没有则等待 196 | peer := strings.Split(*peers, ",") 197 | if len(peer) != 1 { 198 | timer := 0 199 | log.Println("wait other server to register") 200 | done := make(chan bool, 1) 201 | go func() { 202 | for { 203 | if IfAllRegistered(etcd, peer) { 204 | break 205 | } 206 | time.Sleep(2 * time.Second) 207 | timer++ 208 | if timer > 15 { 209 | log.Fatal("other svc doesn't register, please check and try agian later") 210 | } 211 | } 212 | done <- true 213 | }() 214 | <-done 215 | } 216 | log.Println("other servers are registered") 217 | svr.SetPeers(peer...) 218 | // 把服务与group绑定 219 | group.RegisterPeers(svr) 220 | // 开启api服务 221 | go startAPIServer(defaultApiAddr, group, svr) 222 | 223 | // 开启服务 224 | err = svr.StartServer() 225 | if err != nil { 226 | log.Println("grpc server start err:", err) 227 | panic(err) 228 | } 229 | 230 | } 231 | 232 | func IfAllRegistered(etcd *connect.Etcd, peer []string) bool { 233 | for _, v := range peer { 234 | resp, err := etcd.EtcdCli.Get(context.Background(), v) 235 | if err != nil || len(resp.Kvs) == 0 { 236 | return false 237 | } 238 | } 239 | return true 240 | } 241 | 242 | ``` 243 | 244 | 245 | 246 | 程序启动前,需要你 247 | 248 | - 有一个运行etcd 的节点 249 | - 设置环境变量IP_ADDRESS=[server_ip] 250 | 251 | 准备好后就可以`go build .` 252 | 253 | 254 | 255 | 然后使用 `./SpringCache --name [svr_name] --peer [svr_name] --etcd [etcd_ip:port]`进行启动。 256 | 257 | 258 | 259 | 其中, `IP_ADDRESS` 是当前节点的ip地址,需要你预先执行 `export IP_ADDRESS=[your_serverIP]` , 260 | `--name [svr_name]` 当前节点的名字, 261 | `--peer [svr_name]` 其他缓存节点的名字(注意这里要包含当前节点名,多个节点用","分割开。), 262 | `--etcd [etcd_ip:port]` 运行etcd组件的ip地址和端口号,用于服务发现和服务节点存活探测, 263 | 264 | ```shell 265 | root@node2:/home/spring/springcache$ go build . 266 | root@node2:/home/spring/springcache$ ./SpringCache --name svc1 --peer svc1,svc2 --etcd 10.0.0.166:2379 267 | 2025/01/12 19:04:33 server name: svc1 268 | 2025/01/12 19:04:33 create lease success: 7587884038862411724 269 | 2025/01/12 19:04:33 keep alive start 270 | 2025/01/12 19:04:33 s.leaseId: 7587884038862411724 271 | keep alive is ok 272 | 2025/01/12 19:04:33 register server is Done 273 | 2025/01/12 19:04:33 grpc server address : 10.0.0.165:8888 274 | 2025/01/12 19:04:33 wait other server to register 275 | keep alive is ok 276 | ... 277 | ``` 278 | 279 | 节点会向etcd进行注册并且在此阻塞,直到peer 指定的其他节点都成功启动才会继续运行。 280 | 281 | 282 | 283 | 如果其他节点成功启动,程序会打印`xxxxxxxxxx: other servers are registered` 284 | 285 | 286 | 287 | `main.go` 中设计了三个对外接口,分别是 288 | 289 | - `/api/get` 290 | 291 | 用于请求,例如`curl http://xxx.xxx.xxx.xxx:9999/api/get?key=Tom` 292 | 293 | - `/api/set` 294 | 295 | 用于手动设置缓存,例如`curl -X POST http://xxx.xxx.xxx.xxx:9999/api/set -d "key=lpc&value=185&expire=5&hot=false"` 296 | 297 | 其中: 298 | 299 | key,value 为你要设置的键值对 300 | 301 | expire 为过期时间,单位为分钟,最多不超过三天 302 | 303 | hot 为该数据是否为热点数据 304 | 305 | - `/setpeer` 306 | 307 | 当一个缓存存储在节点B上,节点A如果收到get 请求,发现连接节点B失败,则会自动删除节点B的记录,防止某一部分缓存值不可使用。setpeer 用于节点B恢复后,可以重新让节点A添加节点B的记录。例如`curl -X POST http://xxx.xxx.xxx.xxx:9999/setpeer -d "peer=svcB"` 308 | 309 | 310 | 311 | 感谢你看到这里,有任何疑问欢迎随时提问,有任何修改建议请随意提出! 312 | 313 | 314 | -------------------------------------------------------------------------------- /springcachepb/springcachepb.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc v5.29.1 5 | // source: springcachepb/springcachepb.proto 6 | 7 | // protoc --go-grpc_out=. --go_out=. ./springcachepb/springcachepb.proto 8 | 9 | package springcachepb 10 | 11 | import ( 12 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 13 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 14 | reflect "reflect" 15 | sync "sync" 16 | ) 17 | 18 | const ( 19 | // Verify that this generated code is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 21 | // Verify that runtime/protoimpl is sufficiently up-to-date. 22 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 23 | ) 24 | 25 | type GetRequest struct { 26 | state protoimpl.MessageState 27 | sizeCache protoimpl.SizeCache 28 | unknownFields protoimpl.UnknownFields 29 | 30 | Group string `protobuf:"bytes,1,opt,name=group,proto3" json:"group,omitempty"` 31 | Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` 32 | } 33 | 34 | func (x *GetRequest) Reset() { 35 | *x = GetRequest{} 36 | if protoimpl.UnsafeEnabled { 37 | mi := &file_springcachepb_springcachepb_proto_msgTypes[0] 38 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 39 | ms.StoreMessageInfo(mi) 40 | } 41 | } 42 | 43 | func (x *GetRequest) String() string { 44 | return protoimpl.X.MessageStringOf(x) 45 | } 46 | 47 | func (*GetRequest) ProtoMessage() {} 48 | 49 | func (x *GetRequest) ProtoReflect() protoreflect.Message { 50 | mi := &file_springcachepb_springcachepb_proto_msgTypes[0] 51 | if protoimpl.UnsafeEnabled && x != nil { 52 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 53 | if ms.LoadMessageInfo() == nil { 54 | ms.StoreMessageInfo(mi) 55 | } 56 | return ms 57 | } 58 | return mi.MessageOf(x) 59 | } 60 | 61 | // Deprecated: Use GetRequest.ProtoReflect.Descriptor instead. 62 | func (*GetRequest) Descriptor() ([]byte, []int) { 63 | return file_springcachepb_springcachepb_proto_rawDescGZIP(), []int{0} 64 | } 65 | 66 | func (x *GetRequest) GetGroup() string { 67 | if x != nil { 68 | return x.Group 69 | } 70 | return "" 71 | } 72 | 73 | func (x *GetRequest) GetKey() string { 74 | if x != nil { 75 | return x.Key 76 | } 77 | return "" 78 | } 79 | 80 | type GetResponse struct { 81 | state protoimpl.MessageState 82 | sizeCache protoimpl.SizeCache 83 | unknownFields protoimpl.UnknownFields 84 | 85 | Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` 86 | } 87 | 88 | func (x *GetResponse) Reset() { 89 | *x = GetResponse{} 90 | if protoimpl.UnsafeEnabled { 91 | mi := &file_springcachepb_springcachepb_proto_msgTypes[1] 92 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 93 | ms.StoreMessageInfo(mi) 94 | } 95 | } 96 | 97 | func (x *GetResponse) String() string { 98 | return protoimpl.X.MessageStringOf(x) 99 | } 100 | 101 | func (*GetResponse) ProtoMessage() {} 102 | 103 | func (x *GetResponse) ProtoReflect() protoreflect.Message { 104 | mi := &file_springcachepb_springcachepb_proto_msgTypes[1] 105 | if protoimpl.UnsafeEnabled && x != nil { 106 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 107 | if ms.LoadMessageInfo() == nil { 108 | ms.StoreMessageInfo(mi) 109 | } 110 | return ms 111 | } 112 | return mi.MessageOf(x) 113 | } 114 | 115 | // Deprecated: Use GetResponse.ProtoReflect.Descriptor instead. 116 | func (*GetResponse) Descriptor() ([]byte, []int) { 117 | return file_springcachepb_springcachepb_proto_rawDescGZIP(), []int{1} 118 | } 119 | 120 | func (x *GetResponse) GetValue() []byte { 121 | if x != nil { 122 | return x.Value 123 | } 124 | return nil 125 | } 126 | 127 | type SetRequest struct { 128 | state protoimpl.MessageState 129 | sizeCache protoimpl.SizeCache 130 | unknownFields protoimpl.UnknownFields 131 | 132 | Group string `protobuf:"bytes,1,opt,name=group,proto3" json:"group,omitempty"` 133 | Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` 134 | Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` 135 | Expire int64 `protobuf:"varint,4,opt,name=expire,proto3" json:"expire,omitempty"` 136 | Ishot bool `protobuf:"varint,5,opt,name=ishot,proto3" json:"ishot,omitempty"` 137 | } 138 | 139 | func (x *SetRequest) Reset() { 140 | *x = SetRequest{} 141 | if protoimpl.UnsafeEnabled { 142 | mi := &file_springcachepb_springcachepb_proto_msgTypes[2] 143 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 144 | ms.StoreMessageInfo(mi) 145 | } 146 | } 147 | 148 | func (x *SetRequest) String() string { 149 | return protoimpl.X.MessageStringOf(x) 150 | } 151 | 152 | func (*SetRequest) ProtoMessage() {} 153 | 154 | func (x *SetRequest) ProtoReflect() protoreflect.Message { 155 | mi := &file_springcachepb_springcachepb_proto_msgTypes[2] 156 | if protoimpl.UnsafeEnabled && x != nil { 157 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 158 | if ms.LoadMessageInfo() == nil { 159 | ms.StoreMessageInfo(mi) 160 | } 161 | return ms 162 | } 163 | return mi.MessageOf(x) 164 | } 165 | 166 | // Deprecated: Use SetRequest.ProtoReflect.Descriptor instead. 167 | func (*SetRequest) Descriptor() ([]byte, []int) { 168 | return file_springcachepb_springcachepb_proto_rawDescGZIP(), []int{2} 169 | } 170 | 171 | func (x *SetRequest) GetGroup() string { 172 | if x != nil { 173 | return x.Group 174 | } 175 | return "" 176 | } 177 | 178 | func (x *SetRequest) GetKey() string { 179 | if x != nil { 180 | return x.Key 181 | } 182 | return "" 183 | } 184 | 185 | func (x *SetRequest) GetValue() []byte { 186 | if x != nil { 187 | return x.Value 188 | } 189 | return nil 190 | } 191 | 192 | func (x *SetRequest) GetExpire() int64 { 193 | if x != nil { 194 | return x.Expire 195 | } 196 | return 0 197 | } 198 | 199 | func (x *SetRequest) GetIshot() bool { 200 | if x != nil { 201 | return x.Ishot 202 | } 203 | return false 204 | } 205 | 206 | type SetResponse struct { 207 | state protoimpl.MessageState 208 | sizeCache protoimpl.SizeCache 209 | unknownFields protoimpl.UnknownFields 210 | 211 | Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` 212 | } 213 | 214 | func (x *SetResponse) Reset() { 215 | *x = SetResponse{} 216 | if protoimpl.UnsafeEnabled { 217 | mi := &file_springcachepb_springcachepb_proto_msgTypes[3] 218 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 219 | ms.StoreMessageInfo(mi) 220 | } 221 | } 222 | 223 | func (x *SetResponse) String() string { 224 | return protoimpl.X.MessageStringOf(x) 225 | } 226 | 227 | func (*SetResponse) ProtoMessage() {} 228 | 229 | func (x *SetResponse) ProtoReflect() protoreflect.Message { 230 | mi := &file_springcachepb_springcachepb_proto_msgTypes[3] 231 | if protoimpl.UnsafeEnabled && x != nil { 232 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 233 | if ms.LoadMessageInfo() == nil { 234 | ms.StoreMessageInfo(mi) 235 | } 236 | return ms 237 | } 238 | return mi.MessageOf(x) 239 | } 240 | 241 | // Deprecated: Use SetResponse.ProtoReflect.Descriptor instead. 242 | func (*SetResponse) Descriptor() ([]byte, []int) { 243 | return file_springcachepb_springcachepb_proto_rawDescGZIP(), []int{3} 244 | } 245 | 246 | func (x *SetResponse) GetOk() bool { 247 | if x != nil { 248 | return x.Ok 249 | } 250 | return false 251 | } 252 | 253 | var File_springcachepb_springcachepb_proto protoreflect.FileDescriptor 254 | 255 | var file_springcachepb_springcachepb_proto_rawDesc = []byte{ 256 | 0x0a, 0x21, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x63, 0x61, 0x63, 0x68, 0x65, 0x70, 0x62, 0x2f, 257 | 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x63, 0x61, 0x63, 0x68, 0x65, 0x70, 0x62, 0x2e, 0x70, 0x72, 258 | 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x63, 0x61, 0x63, 0x68, 0x65, 259 | 0x70, 0x62, 0x22, 0x34, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 260 | 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 261 | 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 262 | 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x23, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 263 | 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 264 | 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x78, 0x0a, 265 | 0x0a, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x67, 266 | 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 267 | 0x70, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 268 | 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 269 | 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, 270 | 0x69, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 271 | 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 272 | 0x52, 0x05, 0x69, 0x73, 0x68, 0x6f, 0x74, 0x22, 0x1d, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x52, 0x65, 273 | 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 274 | 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x32, 0x89, 0x01, 0x0a, 0x0b, 0x53, 0x70, 0x72, 0x69, 0x6e, 275 | 0x67, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x3c, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x19, 0x2e, 276 | 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x63, 0x61, 0x63, 0x68, 0x65, 0x70, 0x62, 0x2e, 0x47, 0x65, 277 | 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 278 | 0x67, 0x63, 0x61, 0x63, 0x68, 0x65, 0x70, 0x62, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 279 | 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x03, 0x53, 0x65, 0x74, 0x12, 0x19, 0x2e, 0x73, 0x70, 280 | 0x72, 0x69, 0x6e, 0x67, 0x63, 0x61, 0x63, 0x68, 0x65, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x74, 0x52, 281 | 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x63, 282 | 0x61, 0x63, 0x68, 0x65, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 283 | 0x73, 0x65, 0x42, 0x11, 0x5a, 0x0f, 0x2e, 0x2f, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x63, 0x61, 284 | 0x63, 0x68, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 285 | } 286 | 287 | var ( 288 | file_springcachepb_springcachepb_proto_rawDescOnce sync.Once 289 | file_springcachepb_springcachepb_proto_rawDescData = file_springcachepb_springcachepb_proto_rawDesc 290 | ) 291 | 292 | func file_springcachepb_springcachepb_proto_rawDescGZIP() []byte { 293 | file_springcachepb_springcachepb_proto_rawDescOnce.Do(func() { 294 | file_springcachepb_springcachepb_proto_rawDescData = protoimpl.X.CompressGZIP(file_springcachepb_springcachepb_proto_rawDescData) 295 | }) 296 | return file_springcachepb_springcachepb_proto_rawDescData 297 | } 298 | 299 | var file_springcachepb_springcachepb_proto_msgTypes = make([]protoimpl.MessageInfo, 4) 300 | var file_springcachepb_springcachepb_proto_goTypes = []interface{}{ 301 | (*GetRequest)(nil), // 0: springcachepb.GetRequest 302 | (*GetResponse)(nil), // 1: springcachepb.GetResponse 303 | (*SetRequest)(nil), // 2: springcachepb.SetRequest 304 | (*SetResponse)(nil), // 3: springcachepb.SetResponse 305 | } 306 | var file_springcachepb_springcachepb_proto_depIdxs = []int32{ 307 | 0, // 0: springcachepb.SpringCache.Get:input_type -> springcachepb.GetRequest 308 | 2, // 1: springcachepb.SpringCache.Set:input_type -> springcachepb.SetRequest 309 | 1, // 2: springcachepb.SpringCache.Get:output_type -> springcachepb.GetResponse 310 | 3, // 3: springcachepb.SpringCache.Set:output_type -> springcachepb.SetResponse 311 | 2, // [2:4] is the sub-list for method output_type 312 | 0, // [0:2] is the sub-list for method input_type 313 | 0, // [0:0] is the sub-list for extension type_name 314 | 0, // [0:0] is the sub-list for extension extendee 315 | 0, // [0:0] is the sub-list for field type_name 316 | } 317 | 318 | func init() { file_springcachepb_springcachepb_proto_init() } 319 | func file_springcachepb_springcachepb_proto_init() { 320 | if File_springcachepb_springcachepb_proto != nil { 321 | return 322 | } 323 | if !protoimpl.UnsafeEnabled { 324 | file_springcachepb_springcachepb_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 325 | switch v := v.(*GetRequest); i { 326 | case 0: 327 | return &v.state 328 | case 1: 329 | return &v.sizeCache 330 | case 2: 331 | return &v.unknownFields 332 | default: 333 | return nil 334 | } 335 | } 336 | file_springcachepb_springcachepb_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 337 | switch v := v.(*GetResponse); i { 338 | case 0: 339 | return &v.state 340 | case 1: 341 | return &v.sizeCache 342 | case 2: 343 | return &v.unknownFields 344 | default: 345 | return nil 346 | } 347 | } 348 | file_springcachepb_springcachepb_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 349 | switch v := v.(*SetRequest); i { 350 | case 0: 351 | return &v.state 352 | case 1: 353 | return &v.sizeCache 354 | case 2: 355 | return &v.unknownFields 356 | default: 357 | return nil 358 | } 359 | } 360 | file_springcachepb_springcachepb_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 361 | switch v := v.(*SetResponse); i { 362 | case 0: 363 | return &v.state 364 | case 1: 365 | return &v.sizeCache 366 | case 2: 367 | return &v.unknownFields 368 | default: 369 | return nil 370 | } 371 | } 372 | } 373 | type x struct{} 374 | out := protoimpl.TypeBuilder{ 375 | File: protoimpl.DescBuilder{ 376 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 377 | RawDescriptor: file_springcachepb_springcachepb_proto_rawDesc, 378 | NumEnums: 0, 379 | NumMessages: 4, 380 | NumExtensions: 0, 381 | NumServices: 1, 382 | }, 383 | GoTypes: file_springcachepb_springcachepb_proto_goTypes, 384 | DependencyIndexes: file_springcachepb_springcachepb_proto_depIdxs, 385 | MessageInfos: file_springcachepb_springcachepb_proto_msgTypes, 386 | }.Build() 387 | File_springcachepb_springcachepb_proto = out.File 388 | file_springcachepb_springcachepb_proto_rawDesc = nil 389 | file_springcachepb_springcachepb_proto_goTypes = nil 390 | file_springcachepb_springcachepb_proto_depIdxs = nil 391 | } 392 | --------------------------------------------------------------------------------