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.
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.
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
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.
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.
Générer une valeur aléatoire
et l'envoyer à chaque instance Redis pour utilisationSET resource_name my_random_value NX PX 30000
Lock
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!