今天要说的技术方案也是有一定项目背景的。在上一个项目中,我们需要对一个redis集群中过期的key进行处理,这是一个分布式
系统,考虑到高可用性,需要具备过期处理功能的服务有多个副本,这样我们就要求在同一时间内仅有一个副本可以对过期的key>进行处理,如果该副本挂掉,系统会在其他副本中再挑选出一个来处理过期的key。
很显然,这里涉及到一个选主(leader election)的过程。每当涉及选主,很多人就会想到一些高大上的分布式一致性/共识算法,
比如: raft 、 paxos 等。当然使用这些算法自然没有问题,但是也给系统徒增了很多复杂性。能否有一些更简单直接的方案呢?我们已经有了一个redis集群,是否可>以利用redis集群的能力来完成这一点呢?
Redis原生并没有提供leader election算法,但Redis作者提供了 分布式锁的算法 ,也就是说我们可以用分布式锁来实现一个简单的选主功能,见下图:
在上图中我们看到,只有持有锁的服务才具备操作数据的资格,也就是说持有锁的服务的角色是leader,而其他服务则继续尝试去持有锁,它们是follower的角色。
1. 基于单节点redis的分布式锁
在redis官方 有关分布式锁算法的介绍页面 中,作者给出了各种编程语言的推荐实现,而Go语言的推荐实现仅 redsync 这一种。在这篇短文中,我们就来使用redsync实现基于Redis分布式锁的选主方案。
在Go生态中,连接和操作redis的主流go客户端库有 go-redis 和 redigo 。最新的redsync版本底层redis driver既支持go-redis,也支持redigo,我个人日常使用最多的是go-redis这个客户端,这里我们就用go-redis。
redsync github主页中给出的例子是基于单redis node的分布式锁示例。下面我们也先以单redis节点来看看如何通过Redis的分布式锁实现我们的业务逻辑:
// github.com/bigwhite/experiments/blob/master/redis-cluster-distributed-lock/standalone/main.go
1 package main
2
3 import (
4 "context"
5 "log"
6 "os"
7 "os/signal"
8 "sync"
9 "sync/atomic"
10 "syscall"
11 "time"
12
13 goredislib "github.com/go-redis/redis/v8"
14 "github.com/go-redsync/redsync/v4"
15 "github.com/go-redsync/redsync/v4/redis/goredis/v8"
16 )
17
18 const (
19 redisKeyExpiredEventSubj = `__keyevent@0__:expired`
20 )
21
22 var (
23 isLeader int64
24 m atomic.Value
25 id string
26 mutexName = "the-year-of-the-ox-2021"
27 )
28
29 func init() {
30 if len(os.Args) < 2 {
31 panic("args number is not correct")
32 }
33 id = os.Args[1]
34 }
35
36 func tryToBecomeLeader() (bool, func() (bool, error), error) {
37 client := goredislib.NewClient(&goredislib.Options{
38 Addr: "localhost:6379",
39 })
40 pool := goredis.NewPool(client)
41 rs := redsync.New(pool)
42
43 mutex := rs.NewMutex(mutexName)
44
45 if err := mutex.Lock(); err != nil {
46 client.Close()
47 return false, nil, err
48 }
49
50 return true, func() (bool, error) {
51 return mutex.Unlock()
52 }, nil
53 }
54
55 func doElectionAndMaintainTheStatus(quit <-chan struct{}) {
56 ticker := time.NewTicker(time.Second * 5)
57 var err error
58 var ok bool
59 var cf func() (bool, error)
60
61 c := goredislib.NewClient(&goredislib.Options{
62 Addr: "localhost:6379",
63 })
64 defer c.Close()
65 for {
66 select {
67 case <-ticker.C:
68 if atomic.LoadInt64(&isLeader) == 0 {
69 ok, cf, err = tryToBecomeLeader()
70 if ok {
71 log.Printf("prog-%s become leader successfully\n", id)
72 atomic.StoreInt64(&isLeader, 1)
73 defer cf()
74 }
75 if !ok || err != nil {
76 log.Printf("prog-%s try to become leader failed: %s\n", id, err)
77 }
78 } else {
79 log.Printf("prog-%s is the leader\n", id)
80 // update the lock live time and maintain the leader status
81 c.Expire(context.Background(), mutexName, 8*time.Second)
82 }
83 case <-quit:
84 return
85 }
86 }
87 }
88
89 func doExpire(quit <-chan struct{}) {
90 // subscribe the expire event of redis
91 c := goredislib.NewClient(&goredislib.Options{
92 Addr: "localhost:6379"})
93 defer c.Close()
94
95 ctx := context.Background()
96 pubsub := c.Subscribe(ctx, redisKeyExpiredEventSubj)
97 _, err := pubsub.Receive(ctx)
98 if err != nil {
99 log.Printf("prog-%s subscribe expire event failed: %s\n", id, err)
100 return
101 }
102 log.Printf("prog-%s subscribe expire event ok\n", id)
103
104 // Go channel which receives messages from redis db
105 ch := pubsub.Channel()
106 for {
107 select {
108 case event := <-ch:
109 key := event.Payload
110 if atomic.LoadInt64(&isLeader) == 0 {
111 break
112 }