使用redis实现多实例选主功能:
为了保证稳定性,后端会部署多实例,在读的场景下其实是不需要选主的。但是也有需要单实例运行的人文,这就需要选主。
问题描述
像主从模式在创建是声明实例master实例直接处理写请求,但是单主写会存在单机风险。像redis就会启用哨兵模式,支持重新选主模式。哨兵模式为了保证一致性和高性能而建立的选主模式,对一些低频交互,低资源共享,低多用户并发的场景下,哨兵模式太重了。
原因分析:
在我们的场景下,资源共享需求较低,很多用户的资源是独占的,所以对用户写失效其实可以重试,没有很高的一致危险。使用一个有时效的互斥锁,可以实现选主功能,这个原理跟K8s选主的有点像,原理参照k8s选主机制
解决方案:
定义一个选主对象,选主锁使用redis时效互斥锁,只有得到该锁lock才能运行master程序OnLeading
type LeaderSelector struct {
lock *RedisExistLock
OnLeading []func(context.Context)
ctx context.Context
isLeader bool
}
把需要在leader状态下运行的方法进行注册,该方法会在处于leader的时候被runOnLeading运行
func (l *LeaderSelector)RegisterFunc(f func(context.Context)) {
l.OnLeading = append(l.OnLeading, f)
}
func (l *LeaderSelector) runOnLeading() {
if !l.isLeader{
return
}
for _, f := range l.OnLeading {
select {
case <-l.ctx.Done():
default:
go f(l.ctx)
}
}
}
尝试使用TryLock来获取选主,要是选主失败,那就sleep到下个选主周期。
获取选主锁成功后,运行选主后运行runOnLeading,需要进行计时,超时就要返回
func (l *LeaderSelector) StartedLeading(waitt time.Duration) {
l.isLeader = false
if ok, err := l.lock.TryLock(); ok {
l.isLeader = true
defer func() {
l.isLeader = false
}()
ctx, cancel := context.WithTimeout(n*time.Second)
defer cancel()
succ := make(chan bool)
defer close(succ)
go func() {
l.runOnLeading()
succ <- true
}()
select {
case <-ctx.Done():
case <-succ:
}
return
} else if err != nil {
time.Sleep(waitt + time.Second)
}
}
初始化和启动选主功能
func InitLeaderSelector(ctx context.Context, f func(context.Context)) *LeaderSelector {
return &LeaderSelector{
lock: NewRedisExistLock(),
ctx: ctx,
}
}
func (l *LeaderSelector)RunLeaderSelector() {
for {
select {
case <-l.ctx.Done():
return
default:
l.StartedLeading(time.Second * n) // 周期选主
}
}
}