etcd的事务

etcd的事务

当需要批量操作etcd多个key的时候,常常需要让多次操作形成一个原子性的效果,要么同时成功,要么同时失败。
etcd提供了事务的机制,可以实现多个key的原子性操作。
这里使用etcd提供的go clientV3进行操作

etcd的Op对象

在事务中使用到了Op对象来执行操作,所以先介绍一下Op。
Op对象是为了简化用户操作而开发的,可以根据ClientV3提供的函数来创建对应类型的Op对象、

func OpDelete(key string, opts ...OpOption) Op //执行删除动作
func OpGet(key string, opts ...OpOption) Op //执行查询动作
func OpPut(key, val string, opts ...OpOption) Op //执行新增、修改动作
func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op //执行事务

KV接口下的Do方法可以接收一个Op并执行对象定义的操作,返回结果OpResponse是各种类型返回结果的抽象,可以根据请求的类型获取对应的响应结果。

type KV interface {
	Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
	Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
	Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)
	Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)
	Do(ctx context.Context, op Op) (OpResponse, error)
	Txn(ctx context.Context) Txn
}

下面看一下使用Op操作etcd的示例

package main

import (
	"log"
	"time"
	"github.com/coreos/etcd/clientv3"
	"context"
	"fmt"

)

func main()  {
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"127.0.0.1:2379"},
		DialTimeout: time.Second * 5,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()

	ops := []clientv3.Op{
		clientv3.OpPut("aaa", "123"),
		clientv3.OpGet("aaa"),
	}

	for _, op := range ops {
		resp, err := cli.Do(context.TODO(), op)
		if  err != nil {
			log.Fatal(err)
		}
		if op.IsPut() {
			fmt.Println(resp.Put())
		}
		if op.IsGet()  {
			fmt.Println(resp.Get())
		}
	}
}

事务的API

etcd v3 为了解决多 key 的原子操作问题,提供的事务API是下面的格式:

client.Txn(ctx).If(cmp1, cmp2, ...).Then(op1, op2, ...,).Else(op1, op2, …)

事务 API 由 If 语句、Then 语句、Else 语句组成。使用的时候在If里面添加条件表达式,如果条件表达式全部通过,就执行Then中的操作,如果不通过则执行Else中的操作。
Then和Else中的操作就是上面介绍的Op对象,可以传入多个。
支持的条件判断有:

  • func CreateRevision(key string) Cmp 根据创建版本进行判断
  • func LeaseValue(key string) Cmp 根据租约Id进行判断
  • func ModRevision(key string) Cmp 根据最后修改版本进行判断
  • func Value(key string) Cmp 根据值进行判断
  • func Version(key string) Cmp 根据当前key本身的版本进行判断

事务的demo

下面案例模仿userA向userB转账,先查询出各自资金和版本号,根据金额计算出转账后的各自资金,然后使用事务写回etcd,
事务中通过版本号判断如果双方账户没有被修改过事务就执行成功,否则不做修改。

直接使用命令行在etcd中造两条数据

$ etcdctl put userA 100
OK

$ etcdctl put userB 200
OK

$ etcdctl get --prefix user
userA
100
userB
200

然后使用事务将userA值减100,userB增加100,根据版本号做判断,如果中途有别的线程操作则事务会失败。

package main

import (
	"log"
	"strconv"
	"time"
	"github.com/coreos/etcd/clientv3"
	"context"
	"fmt"

)

func main()  {
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"127.0.0.1:2379"},
		DialTimeout: time.Second * 5,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()
	type user struct {
		key string
		account int
		modRevision int64
	}
	userA := user{key:"userA"}
	userB := user{key:"userB"}

	ctx, cancel := context.WithTimeout(context.Background(), 10* time.Second)
	respA, err := cli.Get(ctx, userA.key)
	cancel()
	if err != nil {
		fmt.Printf("获取数据失败 err:%v\n", err)
		return
	}
	if len(respA.Kvs) == 1 {
		userA.modRevision =  respA.Kvs[0].ModRevision
		userA.account,_ = strconv.Atoi(string(respA.Kvs[0].Value))
	}

	ctx, cancel = context.WithTimeout(context.Background(), 10* time.Second)
	respB, err := cli.Get(ctx, userB.key)
	cancel()
	if err != nil {
		fmt.Printf("获取数据失败 err:%v\n", err)
		return
	}
	if len(respB.Kvs) == 1 {
		userB.modRevision =  respB.Kvs[0].ModRevision
		userB.account,_ = strconv.Atoi(string(respB.Kvs[0].Value))
	}
	userA.account -= 100
	userB.account += 100

	txnReps,err := cli.Txn(context.TODO()).If(
		clientv3.Compare(clientv3.ModRevision(userA.key), "=", userA.modRevision),
		clientv3.Compare(clientv3.ModRevision(userB.key), "=", userB.modRevision),
	).Then(
		clientv3.OpPut(userA.key,strconv.Itoa(userA.account)),
		clientv3.OpPut(userB.key,strconv.Itoa(userB.account)),
	).Else(
		//do something
	).Commit()
	if err != nil {
		log.Printf("事务执行失败 err:%v\n", err)
		return
	}
	if !txnReps.Succeeded {
		log.Println("事务失败")
		return
	}
	log.Println("事务成功")
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
etcd 中实现争抢 key 的方式是通过使用租约(lease)和事务(transaction)的组合来实现的。下面是一个使用 Go 客户端的示例代码: ```go package main import ( "context" "log" "time" "go.etcd.io/etcd/clientv3" ) func main() { // 创建 etcd 客户端 cli, err := clientv3.New(clientv3.Config{ Endpoints: []string{"http://localhost:2379"}, // etcd 服务器的地址 DialTimeout: 5 * time.Second, }) if err != nil { log.Fatal(err) } defer cli.Close() // 创建一个租约 resp, err := cli.Grant(context.Background(), 10) // 租约的过期时间为 10 秒 if err != nil { log.Fatal(err) } // 使用租约创建一个争抢的 key key := "mykey" value := "myvalue" for { txn := cli.Txn(context.Background()) txn.If(clientv3.Compare(clientv3.CreateRevision(key), "=", 0)). Then(clientv3.OpPut(key, value, clientv3.WithLease(resp.ID))). Else() txnResp, err := txn.Commit() if err != nil { log.Fatal(err) } if txnResp.Succeeded { log.Println("Successfully acquired the key") break } else { log.Println("Failed to acquire the key, retrying...") time.Sleep(1 * time.Second) } } // 等待租约过期 time.Sleep(15 * time.Second) log.Println("Lease expired") // 删除 key _, err = cli.Delete(context.Background(), key) if err != nil { log.Fatal(err) } } ``` 上面的代码示例中,首先创建了一个 etcd 客户端。然后使用 `cli.Grant` 创建了一个租约,设置了租约的过期时间为 10 秒。接下来使用 `cli.Txn` 进行事务操作,通过比较 key 的创建修订版本是否为 0 来判断该 key 是否已经被其他节点争抢。如果 key 还未被争抢,则使用 `cli.OpPut` 来设置 key 的值,并将租约 ID 与其关联。如果 key 已经被争抢,则等待一秒后重试。当成功争抢到 key 时,输出日志并退出循环。最后,等待租约过期(15秒后),然后删除 key。 需要注意的是,上述示例只是一个简单的示例,并没有处理多个节点同时争抢同一个 key 的情况。在实际应用中,你可能需要使用分布式锁等机制来保证只有一个节点能够成功争抢到 key。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值