etcd
etcd is a strongly consistent, distributed key-value store that provides a reliable way to store data that needs to be accessed by a distributed system or cluster of machines. It gracefully handles leader elections during network partitions and can tolerate machine failure, even in the leader node.
etcd 是一个用于配置共享和服务发现的键值存储系统。
官网:https://etcd.io/
github:https://github.com/etcd-io/etcd/
安装
下载,解压,运行即可
这里我已经将etcd的解压路径加入了全局PATH
测试
etcdctl --endpoints=localhost:2379 put foo bar
etcdctl --endpoints=localhost:2379 get foo
ophelia@Ophelia:~$ etcdctl --endpoints=localhost:2379 put foo bar
OK
ophelia@Ophelia:~$ etcdctl --endpoints=localhost:2379 get foo
foo
bar
Go客户端
https://github.com/etcd-io/etcd/tree/main/client/v3
put & get
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second, //超时时间
})
if err != nil {
panic(err)
}
defer cli.Close()
// put
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
resp, err := cli.Put(ctx, "sample_key", "sample_value")
defer cancel()
if err != nil {
panic(err)
}
// use the response
fmt.Println(resp)
// get
resp1, err := cli.Get(ctx, "sample_key")
if err != nil {
panic(err)
}
for k, v := range resp1.Kvs {
fmt.Println(k, v)
}
}
watch 监控某个值
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second, //超时时间
})
if err != nil {
panic(err)
}
defer cli.Close()
watcher := clientv3.NewWatcher(cli)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
//wch :=cli.Watch(ctx,"zhagnsna")
wch := watcher.Watch(ctx, "zhangsan")
for w := range wch {
for idx, val := range w.Events {
fmt.Println(idx, val)
}
}
}
运行代码,然后在客户端修改zhangsan的值
etcdctl --endpoints=localhost:2379 put zhangsan ok
代码端输出:
0 &{PUT key:“zhangsan” create_revision:15 mod_revision:19 version:5 value:“ok” {} [] 0}
CAP原理
Consistency(一致性)
Availability(可用性)
Partition tolerance(分区容错性)
以上三点最多只能同时实现两者。
etcd中的一些概念
应用场景
1.键值对存储
2.服务注册与发现
3.消息发布与订阅
4.分布式锁
架构图
etcd Server:对外接受和处理客户端的请求
gRPC Server:etcd 与其他 etcd 节点之间通信和信息同步
MVCC:多版本并发控制,etcd 的存储模块,键值对的每一次操作行为都会被记录存储,这些数据底层存储在 BoltDB 数据库中
WAL:预写式日志,etcd中的数据提交前都会记录到日志
Snapshot:快照,防止WAL日志过多,用于存储某一时刻etcd的所有数据
以上二者相结合,etcd可以有效地进行数据存储和节点故障恢复等操作
Raft:共识算法模块,保证数据的一致性
服务发现
service.go
package main
import (
"context"
clientv3 "go.etcd.io/etcd/client/v3"
"log"
"time"
)
// ServiceReg 创建租约注册服务
type ServiceReg struct {
client *clientv3.Client // etcd 客户端
lease clientv3.Lease // 租约
leaseResp *clientv3.LeaseGrantResponse
cancelFunc func()
keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
key string
}
func NewServiceReg(addr []string, timeNum int64) (*ServiceReg, error) {
conf := clientv3.Config{
Endpoints: addr,
DialTimeout: 5 * time.Second,
}
var client = new(clientv3.Client)
if clientTem, err := clientv3.New(conf); err == nil {
client = clientTem
} else {
return nil, err
}
ser := &ServiceReg{
client: client,
lease: nil,
leaseResp: nil,
cancelFunc: nil,
keepAliveChan: nil,
key: "",
}
if err := ser.setLease(timeNum); err != nil {
return nil, err
}
go ser.ListenLeaseRespChan()
return ser, nil
}
// 设置租约
func (s *ServiceReg) setLease(timeNum int64) error {
lease := clientv3.NewLease(s.client)
// 设置租约时间
leaseResp, err := lease.Grant(context.TODO(), timeNum)
if err != nil {
return err
}
// 设置续租
ctx, cancelFunc := context.WithCancel(context.TODO())
leaseRespChan, err := lease.KeepAlive(ctx, leaseResp.ID)
if err != nil {
return err
}
s.lease = lease
s.leaseResp = leaseResp
s.cancelFunc = cancelFunc
s.keepAliveChan = leaseRespChan
return nil
}
// ListenLeaseRespChan 监听续租情况
func (s *ServiceReg) ListenLeaseRespChan() {
for {
select {
case leaseKeepRest := <-s.keepAliveChan:
if leaseKeepRest == nil {
log.Println("已经关闭续租功能")
return
} else {
log.Println("续租成功")
}
}
}
}
// PutService 通过租约注册服务
func (s *ServiceReg) PutService(key, val string) error {
kv := clientv3.NewKV(s.client)
log.Printf("register user server for %s\n", val)
_, err := kv.Put(context.TODO(), key, val, clientv3.WithLease(s.leaseResp.ID))
return err
}
// RevokeLease 撤销租约
func (s *ServiceReg) RevokeLease() error {
s.cancelFunc()
time.Sleep(time.Second * 2)
_, err := s.lease.Revoke(context.TODO(), s.leaseResp.ID)
return err
}
func main() {
ser, _ := NewServiceReg([]string{"localhost:2379"}, 5) // 服务心跳TTL 5
defer ser.RevokeLease()
if err := ser.PutService("/user", "http://localhost:8000"); err != nil {
log.Panic(err)
}
// 阻塞,持续运行
select {}
}
client.go
package main
import (
"context"
"fmt"
"go.etcd.io/etcd/api/v3/mvccpb"
clientv3 "go.etcd.io/etcd/client/v3"
"log"
"sync"
"time"
)
// ClientInfo 客户端连接的结构体
type ClientInfo struct {
client *clientv3.Client
serverList map[string]string
lock sync.Mutex
}
// NewClientInfo 初始化 etcd 客户端连接
func NewClientInfo(addr []string) (*ClientInfo, error) {
conf := clientv3.Config{
Endpoints: addr,
DialTimeout: 5 * time.Second,
}
if client, err := clientv3.New(conf); err == nil {
return &ClientInfo{
client: client,
serverList: make(map[string]string),
}, nil
} else {
return nil, err
}
}
// GetService 获取服务实例信息
func (s *ClientInfo) GetService(prefix string) ([]string, error) {
if addrs, err := s.getServiceByName(prefix); err != nil {
panic(err)
} else {
log.Println("get service", prefix, "for instance list:", addrs)
go s.watcher(prefix)
return addrs, nil
}
}
// 监控指定键值对的变更
func (s *ClientInfo) watcher(prefix string) {
rch := s.client.Watch(context.Background(), prefix, clientv3.WithPrefix())
for wresp := range rch {
for _, ev := range wresp.Events {
switch ev.Type {
case mvccpb.PUT: // 写入的事件
s.SetServiceList(string(ev.Kv.Key), string(ev.Kv.Value))
case mvccpb.DELETE: // 删除的事件
s.DelServiceList(string(ev.Kv.Key))
}
}
}
}
// 根据服务名,获取服务实例信息
func (s *ClientInfo) getServiceByName(prefix string) ([]string, error) {
resp, err := s.client.Get(context.Background(), prefix, clientv3.WithPrefix())
if err != nil {
return nil, err
}
addrs := s.extractAddrs(resp)
return addrs, nil
}
// 根据etcd的响应,提取服务实例的数组
func (s *ClientInfo) extractAddrs(resp *clientv3.GetResponse) []string {
addrs := make([]string, 0)
if resp == nil || resp.Kvs == nil {
return addrs
}
for i := range resp.Kvs {
if v := resp.Kvs[i].Value; v != nil {
s.SetServiceList(string(resp.Kvs[i].Key), string(resp.Kvs[i].Value))
addrs = append(addrs, string(v))
}
}
return addrs
}
func (s *ClientInfo) SetServiceList(key, val string) {
s.lock.Lock()
defer s.lock.Unlock()
s.serverList[key] = val
log.Println("set data key:", key, "val:", val)
}
func (s *ClientInfo) DelServiceList(key string) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.serverList, key)
log.Println("del data key:", key)
}
// SerList2Array 工具方法,转换数组
func (s *ClientInfo) SerList2Array() []string {
s.lock.Lock()
defer s.lock.Unlock()
addrs := make([]string, 0)
for _, v := range s.serverList {
addrs = append(addrs, v)
}
return addrs
}
func main() {
cli, _ := NewClientInfo([]string{"localhost:2379"})
fmt.Println(cli.GetService("/user"))
select {}
}
分布式锁
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
/*
基于缓存实现分布式锁
流程:
开始-->客户端配置-->建立连接-->创建租约-->
自动续租-->续约应答-->创建事务-->
在租约时间内去抢占锁-->
获取到锁-->执行业务逻辑-->释放锁-->结束
没有获取到锁--> 直接结束
*/
var (
client *clientv3.Client
lease clientv3.Lease
leaseId clientv3.LeaseID
leaseGrantResp *clientv3.LeaseGrantResponse
keepRespChan <-chan *clientv3.LeaseKeepAliveResponse = make(<-chan *clientv3.LeaseKeepAliveResponse)
txnResp *clientv3.TxnResponse
)
func main() {
// 客户端配置
config := clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
}
var err error
// 建立连接
if client, err = clientv3.New(config); err != nil {
panic(err)
}
// 上锁并创建租约
lease = clientv3.NewLease(client)
if leaseGrantResp, err = lease.Grant(context.TODO(), 5); err != nil {
panic(err)
}
leaseId = leaseGrantResp.ID
// 创建一个可取消的租约,主要是为了退出的时候能够释放
ctx, cancelFunc := context.WithCancel(context.TODO())
//释放租约
defer cancelFunc()
defer lease.Revoke(context.TODO(), leaseId)
if keepRespChan, err = lease.KeepAlive(ctx, leaseId); err != nil {
panic(err)
}
// 续约应答
go func() {
for {
select {
case keepResp := <-keepRespChan:
if keepResp == nil {
fmt.Println("租约已经失效了")
return
} else { //每秒会续约一次,所以就会收到一次应答
fmt.Println("收到自动续约应答:", keepResp.ID)
}
}
}
}()
// 在租约时间内去抢占锁,(etcd里面的锁就是一个key)
kv := clientv3.NewKV(client)
// 创建事务
txn := kv.Txn(context.TODO())
// if 不存在key,Then设置它,Else抢占锁失败
txn.If(clientv3.Compare(clientv3.CreateRevision("lock"), "=", 0)).
Then(clientv3.OpPut("lock", "g", clientv3.WithLease(leaseId))).
Else(clientv3.OpGet("lock"))
// 提交事务
if txnResp, err = txn.Commit(); err != nil {
panic(err)
}
if !txnResp.Succeeded {
fmt.Println("锁被占用:", string(txnResp.Responses[0].GetResponseRange().Kvs[0].Value))
return
}
// 抢占到锁后执行业务逻辑,没有抢占到则推虎
fmt.Println("处理任务")
time.Sleep(time.Second * 5)
}
Reference
拉钩教育《etcd原理与实践》