Wenn Sie mit Redis-Befehlen vertraut sind, denken Sie möglicherweise sofort daran, die Set-If-Not-Existenz-Operation von Redis zu verwenden, um sie zu implementieren. Die aktuelle Standardimplementierungsmethode ist SET resources_name my_random_value NX PX 30000 Befehlsreihen, wobei:
Ressourcenname die zu sperrende Ressource bedeutet
NX bedeutet, sie festzulegen, wenn sie nicht vorhanden ist
PX 30000 bedeutet, dass die Ablaufzeit 30000 Millisekunden beträgt, also 30 Sekunden
my_random_value Dieser Wert wird von allen Kunden verwendet. Das Ende muss eindeutig sein und alle Erwerber (Konkurrenten) desselben Schlüssels können nicht denselben Wert haben.
Der Wert von value muss eine Zufallszahl sein, hauptsächlich um die Sperre sicherer aufzuheben. Verwenden Sie beim Aufheben der Sperre ein Skript, um Redis Folgendes mitzuteilen: Nur wenn der Schlüssel vorhanden ist und der gespeicherte Wert mit dem von mir angegebenen Wert übereinstimmt Kann mir mitgeteilt werden, dass die Löschung erfolgreich war? Dies kann durch das folgende Lua-Skript erreicht werden:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
Beispiel: Client A erhält eine Ressourcensperre, wird aber sofort durch einen anderen Vorgang blockiert. Wenn Client A die Sperre nach der Ausführung anderer Vorgänge aufheben möchte, ist die ursprüngliche Sperre bereits festgelegt Und es wurde automatisch von Redis freigegeben, und während dieser Zeit wurde die Ressourcensperre erneut von Client B erworben.
Lua-Skript wird verwendet, da Beurteilung und Löschung zwei Vorgänge sind. Daher ist es möglich, dass A die Sperre automatisch aufhebt, nachdem sie abgelaufen ist, sobald sie beurteilt hat, und B dann die Sperre erworben hat und A dann Del aufruft. Dadurch wird die Sperre von B aufgehoben.
package main import ( "context" "errors" "fmt" "github.com/brianvoe/gofakeit/v6" "github.com/go-redis/redis/v8" "sync" "time" ) var client *redis.Client const unlockScript = ` if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end` func lottery(ctx context.Context) error { // 加锁 myRandomValue := gofakeit.UUID() resourceName := "resource_name" ok, err := client.SetNX(ctx, resourceName, myRandomValue, time.Second*30).Result() if err != nil { return err } if !ok { return errors.New("系统繁忙,请重试") } // 解锁 defer func() { script := redis.NewScript(unlockScript) script.Run(ctx, client, []string{resourceName}, myRandomValue) }() // 业务处理 time.Sleep(time.Second) return nil } func main() { client = redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() ctx, _ := context.WithTimeout(context.Background(), time.Second*3) err := lottery(ctx) if err != nil { fmt.Println(err) } }() go func() { defer wg.Done() ctx, _ := context.WithTimeout(context.Background(), time.Second*3) err := lottery(ctx) if err != nil { fmt.Println(err) } }() wg.Wait() }
Sehen wir uns zunächst die Funktion lottery() an. Bei der Eingabe der Funktion wird zunächst SET resources_name my_random_value NX PX 30000 zum Sperren verwendet Wenn der Vorgang fehlschlägt, kehren Sie direkt zurück und lassen Sie den Benutzer es erneut versuchen. Wenn die Entsperrlogik erfolgreich ausgeführt wird, besteht die Entsperrlogik darin, das oben erwähnte Lua-Skript auszuführen und dann die Geschäftsverarbeitung durchzuführen.
Wir haben zwei Goroutinen in der Funktion main() ausgeführt, um gleichzeitig die Funktion lottery() aufzurufen. Eine der Operationen schlägt direkt fehl, da die Sperre nicht erhalten werden kann.
Zufälligen Wert generieren
Verwenden Sie SET resources_name my_random_value NX PX 30000 zum Sperren
Wenn die Sperre fehlschlägt, kehren Sie direkt zurück
Verzögern Sie, um die Entsperrlogik hinzuzufügen um sicherzustellen, dass es entsperrt wird Wenn die Funktion beendet wird, führen Sie die Geschäftslogik aus Redis-Instanzen sind auf verschiedenen Rechnern verteilt und die meisten Knoten können erfolgreich gesperrt werden. Dies ist der RedLock-Algorithmus. Wir müssen Sperren für mehrere Redis-Instanzen gleichzeitig erwerben, aber dies basiert tatsächlich auf einem einzelnen Instanzalgorithmus.
package main import ( "context" "errors" "fmt" "github.com/brianvoe/gofakeit/v6" "github.com/go-redis/redis/v8" "sync" "time" ) var clients []*redis.Client const unlockScript = ` if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end` func lottery(ctx context.Context) error { // 加锁 myRandomValue := gofakeit.UUID() resourceName := "resource_name" var wg sync.WaitGroup wg.Add(len(clients)) // 这里主要是确保不要加锁太久,这样会导致业务处理的时间变少 lockCtx, _ := context.WithTimeout(ctx, time.Millisecond*5) // 成功获得锁的Redis实例的客户端 successClients := make(chan *redis.Client, len(clients)) for _, client := range clients { go func(client *redis.Client) { defer wg.Done() ok, err := client.SetNX(lockCtx, resourceName, myRandomValue, time.Second*30).Result() if err != nil { return } if !ok { return } successClients <- client }(client) } wg.Wait() // 等待所有获取锁操作完成 close(successClients) // 解锁,不管加锁是否成功,最后都要把已经获得的锁给释放掉 defer func() { script := redis.NewScript(unlockScript) for client := range successClients { go func(client *redis.Client) { script.Run(ctx, client, []string{resourceName}, myRandomValue) }(client) } }() // 如果成功加锁得客户端少于客户端数量的一半+1,表示加锁失败 if len(successClients) < len(clients)/2+1 { return errors.New("系统繁忙,请重试") } // 业务处理 time.Sleep(time.Second) return nil } func main() { clients = append(clients, redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", DB: 0, }), redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", DB: 1, }), redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", DB: 2, }), redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", DB: 3, }), redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", DB: 4, })) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() ctx, _ := context.WithTimeout(context.Background(), time.Second*3) err := lottery(ctx) if err != nil { fmt.Println(err) } }() go func() { defer wg.Done() ctx, _ := context.WithTimeout(context.Background(), time.Second*3) err := lottery(ctx) if err != nil { fmt.Println(err) } }() wg.Wait() time.Sleep(time.Second) }
Im obigen Code verwenden wir die Multi-Datenbank von Redis, um mehrere Redis-Masterinstanzen zu simulieren. In der realen Umgebung sollten diese Instanzen vermieden werden gleichzeitige Ausfälle.
In der Sperrlogik führen wir hauptsächlich SET resources_name my_random_value NX PX 30000 auf jeder Redis-Instanz aus, um die Sperre zu erhalten, und stellen dann den Client, der die Sperre erfolgreich erhalten hat, in einen Kanal (die Verwendung von Slice kann hier Probleme mit der Parallelität haben) und verwenden die Synchronisierung .WaitGroup wartet auf das Ende des Sperrenerfassungsvorgangs. Dann fügen Sie „defer“ hinzu, um die Sperrlogik freizugeben. Die Sperrfreigabelogik ist sehr einfach. Geben Sie einfach die erfolgreich erhaltene Sperre frei. Beurteilen Sie abschließend, ob die Anzahl der erfolgreich erworbenen Sperren mehr als die Hälfte beträgt. Wenn mehr als die Hälfte der Sperren nicht erworben wurden, bedeutet dies, dass die Sperrung fehlgeschlagen ist.Zusammenfassung
Generieren Sie einen Zufallswert
und senden Sie ihn zur Verwendung an jede Redis-Instanz.
SET resource_name my_random_value NX PX 30000
Das obige ist der detaillierte Inhalt vonSo implementieren Sie verteilte Sperren in Go in Kombination mit Redis. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!