Maison  >  Article  >  base de données  >  Comment implémenter des verrous distribués dans Go combinés avec Redis

Comment implémenter des verrous distribués dans Go combinés avec Redis

PHPz
PHPzavant
2023-05-27 21:55:241193parcourir

    Scénario d'instance Redis unique

    Si vous êtes familier avec les commandes Redis, vous pouvez immédiatement penser à utiliser l'opération set if not exist de Redis pour l'implémenter, et la méthode d'implémentation standard actuelle est le SET nom_ressource my_random_value NX PX 30000 séries de commandes, où :

    • resource_name signifie la ressource à verrouiller

    • NX signifie la définir si elle n'existe pas

    • PX 30000 signifie que le délai d'expiration est de 30000 millisecondes, soit 30 secondes

    • my_random_value Cette valeur est utilisée par tous les clients. La fin doit être unique, et tous les acquéreurs (concurrents) d'une même clé ne peuvent pas avoir la même valeur.

    La valeur de value doit être un nombre aléatoire principalement pour déverrouiller le verrou de manière plus sûre, utilisez un script pour indiquer à Redis : Uniquement lorsque la clé existe et que la valeur stockée est la même que la valeur que j'ai spécifiée. puis-je être informé que la suppression a réussi. Ceci peut être réalisé grâce au script Lua suivant :

    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end

    Par exemple : le client A obtient un verrou de ressource, mais est immédiatement bloqué par une autre opération. Lorsque le client A souhaite libérer le verrou après avoir exécuté d'autres opérations, le verrou d'origine a déjà été chronométré. out. Et il a été automatiquement libéré par Redis, et pendant cette période, le verrouillage des ressources a été à nouveau acquis par le client B.

    Le script Lua est utilisé car le jugement et la suppression sont deux opérations, il est donc possible que A libère automatiquement le verrou après son expiration dès qu'il a jugé, puis B a acquis le verrou, puis A appelle Del, provoquant le déverrouillage de B.

    Ajouter un exemple de déverrouillage

    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()
    }

    Regardons d'abord la fonction lottery() Ici, lors de la saisie de la fonction, utilisez d'abord SET resource_name my_random_value NX PX 30000 pour verrouiller. valeur. Si l'opération échoue, retournez directement et laissez l'utilisateur réessayer. Si la logique de déverrouillage est exécutée avec succès en différé, la logique de déverrouillage consiste à exécuter le script Lua mentionné ci-dessus, puis à effectuer le traitement métier.

    Nous avons exécuté deux goroutines dans la fonction main() pour appeler simultanément la fonction lottery(). L'une des opérations échouera directement car le verrou ne peut pas être obtenu.

    Résumé

    • Générer une valeur aléatoire

    • Utilisez SET nom_ressource my_random_value NX PX 30000 pour verrouiller

    • Si le verrouillage échoue, revenez directement

    • defer pour ajouter une logique de déverrouillage pour vous en assurer sera débloqué lorsque la fonction se termine, Exécuter

    • Exécuter la logique métier

    Scénarios d'instances Redis multiples

    Dans le cas d'une seule instance, si cette instance se bloque, toutes les requêtes échoueront car le verrou ne peut pas être obtenu, nous avons donc besoin de plusieurs Instances Redis distribuées dans différents L'instance Redis sur la machine et le verrouillage de la plupart des nœuds peuvent être verrouillés avec succès. Il s'agit de l'algorithme RedLock. Nous devons acquérir des verrous sur plusieurs instances Redis en même temps, mais cela est en fait basé sur un algorithme à instance unique.

    Exemple de déverrouillage ajouté

    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) 
    }

    Dans le code ci-dessus, nous utilisons la multi-base de données de Redis pour simuler plusieurs instances maîtres Redis. Généralement, nous choisirons 5 instances Redis. Dans l'environnement réel, ces instances doivent être distribuées sur différentes machines. échecs simultanés.
    Dans la logique de verrouillage, nous exécutons principalement SET resource_name my_random_value NX PX 30000 sur chaque instance Redis pour obtenir le verrou, puis plaçons le client qui a réussi à obtenir le verrou dans un canal (il peut y avoir des problèmes de concurrence lors de l'utilisation de slice ici), et utilisez sync.WaitGroup attend la fin de l’opération d’acquisition du verrou.
    Ensuite, ajoutez un délai pour libérer la logique de verrouillage. La logique de déverrouillage est très simple, il suffit de libérer le verrou obtenu avec succès.
    Enfin, jugez si le nombre de verrous acquis avec succès est supérieur à la moitié. Si plus de la moitié des verrous ne sont pas acquis, cela signifie que le verrouillage a échoué.
    Si le verrouillage réussit, l'étape suivante consiste à effectuer le traitement commercial.

    Résumé

    • Générer une valeur aléatoire

    • et l'envoyer à chaque instance Redis pour utilisationSET resource_name my_random_value NX PX 30000Lock

    • Attendez que toutes les opérations d'acquisition de verrou soient terminées

    • defer ajoute une logique de déverrouillage pour garantir que il sera déverrouillé à la sortie de la fonction Exécution, ici différer d'abord puis juger car il est possible d'obtenir le verrou d'une partie de l'instance Redis, mais comme il ne dépasse pas la moitié, il sera quand même jugé comme un échec de verrouillage

    • Déterminez si le verrouillage de plus de la moitié de l'instance Redis a été obtenu, s'il n'y a pas d'explication. Si le verrouillage échoue, revenez directement à

    • pour exécuter la logique métier

    Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

    Déclaration:
    Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer