Golang 操作 Zookeeper

Golang 操作 Zookeeper

完整的源码地址:https://github.com/xuqil/experiments/tree/main/zookeeper-client

go-zookeeper 库地址:https://pkg.go.dev/github.com/go-zookeeper/zk@v1.0.3

连接 Zookeeper

conn, _, err := zk.Connect([]string{"192.168.122.20:2181", "192.168.122.21:2181", "192.168.122.22:2181"},
		5*time.Second)

对 Zookeeper 的 CRUD

// getPath 获取 path 的 data 和 stat
// Cmd: get -s /path
func getPath(t *testing.T, conn *zk.Conn, path string) ([]byte, *zk.Stat) {
	data, stat, err := conn.Get(path)
	if err != nil {
		t.Fatal(err)
	}
	return data, stat
}

// deletePath 删除指定的 path,如果 path 下还有 children path,会删除失败
// Cmd: delete  /path
func deletePath(t *testing.T, conn *zk.Conn, path string) {
	_, stat, err := conn.Get(path)
	if err != nil {
		t.Fatal(err)
	}

	// version是用于 CAS支持,可以通过此种方式保证原子性
	if err := conn.Delete(path, stat.Version); err != nil {
		t.Fatal(err)
	}
}

// modifyData 修改节点的值
func modifyData(t *testing.T, conn *zk.Conn, path string, data []byte) {
	// 获取 path 的属性
	_, stat, err := conn.Get(path)
	if err != nil {
		t.Fatal(err)
	}
	stat, err = conn.Set(path, data, stat.Version)
	if err != nil {
		t.Fatal(err)
	}
}

// createPath 创建节点
func createPath(t *testing.T, conn *zk.Conn, path string, data []byte, flags int32) string {
	// 获取访问控制权限
	acl := zk.WorldACL(zk.PermAll)
	res, err := conn.Create(path, data, flags, acl)
	if err != nil {
		t.Fatal(err)
	}
	return res
}

CRUD 的测试示例

// TestCreatePersistentPath 创建一个持久化节点:/path1
// Cmd: create /path1
func TestCreatePersistentPath(t *testing.T) {
	conn := initConn(t)
	defer conn.Close()

	// flags有4种取值:
	// 0: 永久,除非手动删除
	// zk.FlagEphemeral = 1: 短暂,session断开则该节点也被删除
	// zk.FlagSequence  = 2: 会自动在节点后面添加序号
	// 3: Ephemeral和Sequence,即,短暂且自动添加序号
	var flags int32 = 0
	path := "/path1"
	res := createPath(t, conn, path, []byte("data"), flags)
	assert.Equal(t, path, res)

	// 持久化的 path 需要显式删除
	deletePath(t, conn, path)
}

// TestCreateEphemeralPath 创建一个临时节点:/path2
// Cmd: create -e /path2
func TestCreateEphemeralPath(t *testing.T) {
	conn := initConn(t)
	defer conn.Close()

	var flags int32 = zk.FlagEphemeral
	// 创建临时节点,连接断开后会自动删除
	path := "/path2"
	data := []byte("ephemeral")
	res := createPath(t, conn, path, []byte("ephemeral"), flags)
	assert.Equal(t, path, res)

	// 获取刚刚创建的临时节点
	resData, stat := getPath(t, conn, path)
	assert.Equal(t, data, resData)
	t.Logf("stat:%+v\n", *stat)
}

// TestCreatePersistentPathWithSequence 创建一个带序号的持久化节点:/path3
// Cmd: create -s /path3
func TestCreatePersistentPathWithSequence(t *testing.T) {
	conn := initConn(t)
	defer conn.Close()

	// flags有4种取值:
	// 0: 永久,除非手动删除
	// zk.FlagEphemeral = 1: 短暂,session断开则该节点也被删除
	// zk.FlagSequence  = 2: 会自动在节点后面添加序号
	// 3: Ephemeral和Sequence,即,短暂且自动添加序号
	var flags int32 = zk.FlagSequence
	path := "/path3"
	data := []byte("data")

	// 第一次创建
	res := createPath(t, conn, path, data, flags)
	assert.True(t, strings.HasPrefix(res, path))
	// 持久化的 path 需要显式删除
	deletePath(t, conn, res)

	// 第二次创建
	res = createPath(t, conn, path, data, flags)
	assert.True(t, strings.HasPrefix(res, path))
	// 持久化的 path 需要显式删除
	deletePath(t, conn, res)
}

// TestCreateEphemeralPathWithSequence 创建一个临时节点:/path4
// Cmd: create -e -s /path4
func TestCreateEphemeralPathWithSequence(t *testing.T) {
	conn := initConn(t)
	defer conn.Close()

	// flags有4种取值:
	// 0: 永久,除非手动删除
	// zk.FlagEphemeral = 1: 短暂,session断开则该节点也被删除
	// zk.FlagSequence  = 2: 会自动在节点后面添加序号
	// 3: Ephemeral和Sequence,即,短暂且自动添加序号
	var flags int32 = 3
	// 创建临时节点,连接断开后会自动删除
	path := "/path4"
	data := []byte("ephemeral")

	// 第一次创建
	res := createPath(t, conn, path, data, flags)
	assert.True(t, strings.HasPrefix(res, path))

	// 第二次创建
	res = createPath(t, conn, path, data, flags)
	assert.True(t, strings.HasPrefix(res, path))
}

// TestModifyPathData 修改指定 path 的值
// Cmd: set /path "new data"
func TestModifyPathData(t *testing.T) {
	conn := initConn(t)
	defer conn.Close()

	// flags有4种取值:
	// 0: 永久,除非手动删除
	// zk.FlagEphemeral = 1: 短暂,session断开则该节点也被删除
	// zk.FlagSequence  = 2: 会自动在节点后面添加序号
	// 3: Ephemeral和Sequence,即,短暂且自动添加序号
	var flags int32 = 0
	path := "/path5"
	data := []byte("data")

	_ = createPath(t, conn, path, data, flags)

	// 将 path5 的值修改为 new data
	modifyData(t, conn, path, []byte("new data"))
	afterData, _ := getPath(t, conn, path)
	assert.Equal(t, []byte("new data"), afterData)

	deletePath(t, conn, path)
}

// TestGetPathChildren 获取 path 下的所有 children
// Cmd: ls -s /path
func TestGetPathChildren(t *testing.T) {
	conn := initConn(t)
	defer conn.Close()

	var flags int32 = 0

	path := "/path6"
	data := []byte("ephemeral")
	_ = createPath(t, conn, path, data, flags)
	_ = createPath(t, conn, path+"/ch1", data, flags)
	_ = createPath(t, conn, path+"/ch2", data, flags)

	children, _, err := conn.Children(path)
	if err != nil {
		t.Fatal(err)
	}
	assert.ElementsMatch(t, []string{"ch1", "ch2"}, children)

	for _, child := range children {
		deletePath(t, conn, path+"/"+child)
	}

	deletePath(t, conn, path)
}

// TestExists 判断节点是否存在
func TestExists(t *testing.T) {
	conn := initConn(t)
	defer conn.Close()

	exists, _, err := conn.Exists("/noExist")
	if err != nil {
		t.Fatal(err)
	}
	assert.False(t, exists)
}

Zookeeper 的 watch

在 go-zookeeper 中,golang 是通过 Event 来实现 watch 机制

全局监听

// TestWatchGlobal 全局 watch
func TestWatchGlobal(t *testing.T) {
	// 创建监听的option,用于初始化zk
	eventCallbackOption := zk.WithEventCallback(callback)
	// 连接zk
	conn, _, err := zk.Connect([]string{"192.168.122.20:2181", "192.168.122.21:2181", "192.168.122.22:2181"}, time.Second*5, eventCallbackOption)
	defer conn.Close()
	if err != nil {
		t.Fatal(err)
	}
	data := []byte("hello")
	var flags int32 = zk.FlagEphemeral
	path := "/watchGlobal"

	// 开始监听path
	_, _, _, err = conn.ExistsW(path)
	if err != nil {
		t.Fatal(err)
	}

	// 触发创建操作
	createPath(t, conn, path, data, flags)

	//再次监听path
	_, _, _, err = conn.ExistsW(path)
	if err != nil {
		t.Fatal(err)
	}

	// 触发删除操作
	deletePath(t, conn, path)
}

// callback 回调函数
func callback(event zk.Event) {
	if event.Err != nil {
		log.Fatal(event.Err)
	}
	log.Println("发生 global watch 回调:")
	log.Println("path:", event.Path)
	log.Println("type:", event.Type.String())
	log.Println("state:", event.State.String())
	log.Println("---------------------------")
}

提示:

  1. 如果即设置了全局监听又设置了部分监听,那么最终是都会触发的,并且全局监听会先执行
  2. 如果设置了监听子节点,那么事件的触发是先子节点后父节点

监听节点的值

// TestWatchPathData watch 节点的值
// Cmd: get -w /path
func TestWatchPathData(t *testing.T) {
	conn := initConn(t)

	path := "/watchData"
	data := []byte("data")
	var flags int32 = zk.FlagEphemeral

	_ = createPath(t, conn, path, data, flags)
	t.Log("before data:", string(data))

	// 监听 path 的数据
	_, _, event, err := conn.GetW(path)
	if err != nil {
		t.Error(err)
		return
	}

	go watchData(event)

	modifyData(t, conn, path, []byte("new data"))
	afterData, _ := getPath(t, conn, path)
	t.Log("after data:", string(afterData))
}

func watchData(e <-chan zk.Event) {
	event := <-e
	log.Println("发生 data watch 回调:")
	log.Println("path:", event.Path)
	log.Println("type:", event.Type.String())
	log.Println("state:", event.State.String())
	log.Println("---------------------------")
}

监听节点的 Children Path

// TestWatchPathChildren watch 节点的子节点
// Cmd: ls -w /path
func TestWatchPathChildren(t *testing.T) {
	conn, _, err := zk.Connect([]string{"192.168.122.20:2181", "192.168.122.21:2181", "192.168.122.22:2181"},
		5*time.Second)
	if err != nil {
		t.Fatal(err)
	}
	defer conn.Close()

	path := "/watchData"
	data := []byte("data")
	var flags int32 = 0

	_ = createPath(t, conn, path, data, flags)

	// 监听 path 的数据
	_, _, event, err := conn.ChildrenW(path)
	if err != nil {
		t.Error(err)
		return
	}
	_ = createPath(t, conn, path+"/ch1", data, flags)

	// 监听 path 的数据
	_, _, event, err = conn.ChildrenW(path)
	if err != nil {
		t.Error(err)
		return
	}
	_ = createPath(t, conn, path+"/ch2", data, flags)

	go watchData(event)

	modifyData(t, conn, path, []byte("new data"))
	children, _, err := conn.Children(path)
	if err != nil {
		t.Fatal(err)
	}
	for _, child := range children {
		deletePath(t, conn, path+"/"+child)
	}

	deletePath(t, conn, path)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值