Preface
In projects, we often have scenarios where we need to use distributed locks, and Redis is the most common way to implement distributed locks, and we all hope to write the code a little simpler, so today we try to implement it in the simplest way.
The following code usesgo-redisClient andgofakeit, referenced and referencedRedis official article
Single Redis instance scenario
If you are familiar with Redis's commands, you may immediately think of using Redis's set if not exists operation to implement it, and the standard implementation method is SET resource_name my_random_value NX PX 30000 commands, where:
- resource_name indicates the resource to be locked
- NX means if it does not exist, set
- PX 30000 means that the expiration time is 30000 milliseconds, that is, 30 seconds
- The value of my_random_value must be unique to all clients, and the value of all acquirers (competitors) of the same key cannot be the same.
The value of value must be a random number. It is mainly to release the lock safely. When releasing the lock, use a script to tell Redis: Only when the key exists and the stored value is the same as the value I specified can I tell me that the deletion is successful. This can be implemented through the following Lua script:
if ("get",KEYS[1]) == ARGV[1] then return ("del",KEYS[1]) else return 0 end
For example: Client A acquires the resource lock, but is then blocked by another operation. When Client A completes other operations and wants to release the lock, the original lock has already timed out and is automatically released by Redis. During this period, the resource lock is acquired by Client B again.
Use Lua script because judgment and deletion are two operations, so it is possible that A will automatically release the lock after it expires and then B acquires the lock, and then A calls Del, causing B's lock to be released.
Add unlock example
package main import ( "context" "errors" "fmt" "/brianvoe/gofakeit/v6" "/go-redis/redis/v8" "sync" "time" ) var client * const unlockScript = ` if ("get",KEYS[1]) == ARGV[1] then return ("del",KEYS[1]) else return 0 end` func lottery(ctx ) error { // Add lock myRandomValue := () resourceName := "resource_name" ok, err := (ctx, resourceName, myRandomValue, *30).Result() if err != nil { return err } if !ok { return ("The system is busy, please try again") } // Unlock defer func() { script := (unlockScript) (ctx, client, []string{resourceName}, myRandomValue) }() // Business processing () return nil } func main() { client = (&{ Addr: "127.0.0.1:6379", }) var wg (2) go func() { defer () ctx, _ := ((), *3) err := lottery(ctx) if err != nil { (err) } }() go func() { defer () ctx, _ := ((), *3) err := lottery(ctx) if err != nil { (err) } }() () }
Let's first look at the lottery() function. Here we simulate a lottery operation. When entering the function, we first use SET resource_name my_random_value NX PX 30000 to lock, and use UUID as a random value. If the operation fails, return directly and ask the user to try again. If the unlocking logic is successfully executed in the defer, the unlocking logic is to execute the lua script mentioned above, and then perform business processing.
We execute two goroutine concurrent calls to the lottery() function in the main() function, one of which will fail directly because we cannot get the lock.
summary
- Generate random values
- Use SET resource_name my_random_value NX PX 30000 to add lock
- If the lock fails, return directly
- Defer adds unlocking logic to ensure that it will be executed when the function exits
- Execute business logic
Multiple Redis instance scenarios
In the case of a single instance, if this instance is dead, all requests will fail because we cannot get the lock. Therefore, we need multiple Redis instances distributed on different machines and get the locks of most of the nodes to successfully lock it. This is the RedLock algorithm. It is actually based on the above single instance algorithm, but we need to acquire locks for multiple Redis instances at the same time.
Add unlock example
package main import ( "context" "errors" "fmt" "/brianvoe/gofakeit/v6" "/go-redis/redis/v8" "sync" "time" ) var clients []* const unlockScript = ` if ("get",KEYS[1]) == ARGV[1] then return ("del",KEYS[1]) else return 0 end` func lottery(ctx ) error { // Add lock myRandomValue := () resourceName := "resource_name" var wg (len(clients)) // Here is the main thing to make sure that you don't lock for too long, as this will cause less time to handle business lockCtx, _ := (ctx, *5) // The client of the Redis instance that successfully obtained the lock successClients := make(chan *, len(clients)) for _, client := range clients { go func(client *) { defer () ok, err := (lockCtx, resourceName, myRandomValue, *30).Result() if err != nil { return } if !ok { return } successClients <- client }(client) } () // Wait for all lock acquisition operations to complete close(successClients) // Unlock, regardless of whether the lock is successful or not, the lock you have already obtained must be released in the end defer func() { script := (unlockScript) for client := range successClients { go func(client *) { (ctx, client, []string{resourceName}, myRandomValue) }(client) } }() // If the lock is successfully locked and the client is less than half of the number of clients + 1, it means that the locking has failed if len(successClients) < len(clients)/2+1 { return ("The system is busy, please try again") } // Business processing () return nil } func main() { clients = append(clients, (&{ Addr: "127.0.0.1:6379", DB: 0, }), (&{ Addr: "127.0.0.1:6379", DB: 1, }), (&{ Addr: "127.0.0.1:6379", DB: 2, }), (&{ Addr: "127.0.0.1:6379", DB: 3, }), (&{ Addr: "127.0.0.1:6379", DB: 4, })) var wg (2) go func() { defer () ctx, _ := ((), *3) err := lottery(ctx) if err != nil { (err) } }() go func() { defer () ctx, _ := ((), *3) err := lottery(ctx) if err != nil { (err) } }() () () }
In the above code, we use Redis's multi-database to simulate multiple Redis master instances. Generally, we will select 5 Redis instances. In the real environment, these instances should be distributed on different machines to avoid failure at the same time.
In the locking logic, we mainly perform SET resource_name my_random_value NX PX 30000 acquisition lock on each Redis instance, and then put the client that successfully acquires the lock in a channel (there may be concurrency problems when using slice here), and use it to wait at the same time, so the acquisition lock operation is over.
Then add the defer release lock logic. Release lock logic is very simple, just release the lock you successfully obtained.
Finally, determine whether the number of locks successfully obtained is greater than half. If more than half of the locks are not obtained, it means that the locking has failed.
If the lock is successfully added, the next step is to conduct business processing.
summary
- Generate random values
- Concurrently send to each Redis instance for use
SET resource_name my_random_value NX PX 30000
Add lock - Wait for all lock acquisition operations to complete
- Defer adds unlocking logic to ensure that it will be executed when the function exits. Here we first determine that it is because it is possible to obtain a lock for a part of the Redis instance, but because it does not exceed half, it will still be judged that the locking failed.
- Determine whether to get more than half of the locks of Redis instances. If there is no indication that the locking failed, return directly
- Execute business logic
Summarize
RedLock (more than 30 lines of code) can be easily implemented by using Go's goroutine, channel, context, etc.
You can encapsulate the unlocking operation into a function, so that there will be no too much unlocking logic involved in the business code.
This is the article about Go+Redis in the simplest way to implement distributed locks. For more information about Go Redis distributed locks, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!