在集群中,有些业务逻辑只需要1个实例去执行,例如定时通知、任务调度器等。本文通过redis实现了在集群中选举一个master实例。
package redisutil
import (
"context"
"log"
"time"
)
const expiration = 60 * time.Second
var globalInstanceID = strutil.RandomStr("master-", 16)
// AcquireAsMaster 选举master。如果当前节点选中为master,则向后执行;如果没选中,则阻塞等待下一次选举
func AcquireAsMaster(ctx context.Context, masterKey string, redisEval RedisEval) (instanceID string, release func(), err error) {
instanceID = globalInstanceID
for wait := 5; ; wait = (wait << 1) % 75 { // 5, 10, 20, 40, 5, 10, ...
select {
case <-ctx.Done():
return instanceID, func() {}, ctx.Err()
default:
}
beElected, err := redisEval(`if redis.call("SET", KEYS[1], ARGV[1], "EX", tonumber(ARGV[2]), "NX") then return 1 else return 0 end`,
[]string{masterKey}, instanceID, int(expiration.Seconds()))
if be, ok := beElected.(int64); err != nil || !ok || be == 0 {
time.Sleep(time.Duration(wait) * time.Second) // 选举出错了 or 选举落选,指数退避重新选举
}
if beElected.(int64) == 1 {
// 启动看门狗无限续期,防止master身份丢失
go func() {
ticker := time.NewTicker(expiration / 3)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
success, er := redisEval(`
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("EXPIRE", KEYS[1], ARGV[2])
end
return 0`, []string{masterKey}, instanceID, int(expiration.Seconds()))
if er != nil || success == 0 {
return
}
}
}
}()
// 返回释放函数
releaseFunc := func() {
released, e := redisEval(`
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
end
return 0`, []string{masterKey}, instanceID)
if e != nil {
log.Printf("failed to release as master: %+v\n", e)
} else if released == 0 {
log.Println("failed to release as master: not master")
}
}
return instanceID, releaseFunc, nil
}
}
}
使用示例:
masterKey := fmt.Sprintf("%s:this-is-the-master-instance", ps.config.Topic)
instanceID, release, err := redisutil.AcquireAsMaster(ctx, masterKey, ps.redisEval)
defer release()
if err != nil {
log.Printf("acquire master lock error. masterKey=%s err=%+v", masterKey, err)
return
}
log.Printf("acquire master lock success. masterKey=%s instanceID=%s", masterKey, instanceID)