Heim  >  Artikel  >  Datenbank  >  So implementieren Sie verteilte Sperren in Go in Kombination mit Redis

So implementieren Sie verteilte Sperren in Go in Kombination mit Redis

PHPz
PHPznach vorne
2023-05-27 21:55:241193Durchsuche

    Szenario für eine einzelne Redis-Instanz

    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.

    Entsperrungsbeispiel hinzufügen

    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.

    Zusammenfassung

    • 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.

    • Entsperrungsbeispiel hinzugefügt
    • 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.

    Wenn die Sperrung erfolgreich ist, erfolgt im nächsten Schritt die Geschäftsabwicklung.

    Zusammenfassung

    Generieren Sie einen Zufallswert



    und senden Sie ihn zur Verwendung an jede Redis-Instanz.

    Sperren

    • Warten Sie, bis alle Sperrenerfassungsvorgänge abgeschlossen sind Es wird entsperrt, wenn die Funktion die Ausführung verlässt. Hier wird es zuerst verzögert und dann beurteilt, da es möglich ist, die Sperre eines Teils der Redis-Instanz zu erhalten. Da es jedoch nicht mehr als die Hälfte beträgt, wird es dennoch als Sperrfehler beurteilt

    • Beurteilen Sie, ob die Sperre von mehr als der Hälfte der Redis-Instanz erhalten wurde. Wenn keine Erklärung vorliegt. Wenn die Sperre fehlschlägt, kehren Sie direkt zu
    • SET resource_name my_random_value NX PX 30000

      zurück, um die Geschäftslogik auszuführen

    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!

    Stellungnahme:
    Dieser Artikel ist reproduziert unter:yisu.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen