“This is the sixth day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021.”
An introduction to distributed locks
A lock is a tool used to solve the problem of multiple threads of execution accessing a shared resource incorrectly or with inconsistent data.
The essence of locking: Only one user can operate on shared data at a time.
Why do we need distributed locks
In general, we use distributed locks in two main scenarios:
- For example, a user needs to enter a verification code when performing an operation. Nodes that do not communicate with each other send multiple verification codes.
- Avoid damaging data correctness: If two nodes operate on the same data, data errors or inconsistency may occur.
A common way to implement distributed locks in Java
-
MySQL has its own pessimistic lock for update keyword, can also implement pessimistic/optimistic lock to achieve the purpose;
-
Ordered nodes based on Zookeeper: Zookeeper allows you to temporarily create ordered child nodes, so that when the client obtains the node list, it can determine whether the lock can be obtained based on the serial number of the current child node list.
-
Single threading based on Redis: Since Redis is single-threaded, commands are executed in serial fashion and provide instructions such as SETNX(Set if not exists) themselves and extend the set command;
Each scheme has its own advantages and disadvantages. For example, although MySQL is easy to understand intuitively, it needs to consider lock timeout, transaction and other problems when it is implemented, and its performance is limited to the database. Here, we mainly analyze with Redis.
Features distributed locks should have
- atomic
- Mutual exclusivity
- Exclusivity: One’s own lock can only be unlocked by oneself
- reentrancy
- Timeout and renewal
Scenario analysis of distributed lock feature
-
Timeout scenario description:
Suppose there are two services A and B, where service A is down due to force majeure after acquiring the lock (for example: Machine room power outage), because the lock is held by service A, service B will never be able to obtain the lock, which is obviously unreasonable, so we need to set an extra timeout time to avoid this situation.
-
Description of an exclusive scenario
Continuing with the above scenario, we are considering a scenario where the logic between locking and releasing the lock is complex and takes so long to execute that the timeout limit for the lock is exceeded. At this time, thread A’s lock has expired, and the critical section of logic has not been completed. Because the lock has expired, Redis will automatically delete the key corresponding to the lock. At this point thread B can acquire the distributed lock. When thread B has just acquired its own lock, thread A, which had timed out, executes the code to release its own lock. In fact, A’s lock has expired and now its key is B’s lock. In fact, B has just obtained the lock and has not executed its own logic, so the lock should be exclusive and its own lock should only be unlocked by itself.
Implementation: Set the value of the lock to a random string. When releasing the lock, check whether the random string is consistent, and then delete the key.
-
Renewal scenario Description
In order to avoid the thread didn’t finish his business is expired, locking, first set an expiration time, and then start a daemon thread, timing to detect the failure of the lock time, if the lock is expired, Shared resource processing business logic operation is not yet complete, then automatic renewal of lock operation, reset the expiration time. Redisson uses this mechanism, which he calls a watchdog.
-
Atomicity
Matching a value and deleting a key are not atomic operations in Redis, and there are no similar atomic-guarantee instructions, so you might need to use a script like Lua because Lua scripts can guarantee atomicity for multiple instructions.
-
Mutual exclusion
Once a key is fetched by one instance, it cannot be fetched by any other instance.
-
Reentrancy
If the same thread method A calls method B, both A and B need to acquire the lock. Once A has acquired the lock, it performs its own logic to call B. If it is not A re-entrant lock, A deadlock will occur.
Synchronized and ReentrantLock are both reentrant locks in Java programming
Use Redis to implement distributed locks
Redis command parsing
SETEX
:
Syntax: SETEX KEY_NAME TIMEOUT VALUE Version: Redis version >= 2.0.0 Function: SETEX command sets the specified key VALUE and expiration time. If the key already exists, the SETEX command replaces the old value.Copy the code
SETNX: (SET if Not eXists)
Syntax: SETNX KEY_NAME VALUE Version: redis version >= 2.0.0 Function: SETNX sets the specified VALUE for the key if the specified key does not exist. Return value: set successfully, return 1; Setting failed, return 0.Copy the code
We have noticed that setnx can meet the requirements of setting a timeout when there is no value and setting a timeout when there is a value, but we need to set a timeout when there is a value, so we extended the set command after Redis 2.6.12:
SET
:
# a command to ensure atomicity On the basis of setnx set timeout set key value [EX seconds | PX milliseconds] [NX | XX] - [the EX seconds] set expiration time unit of seconds - [PX Milliseconds] - [XX] key does not exist, return OK, nil - [XX] key does exist, return OK, Nil set key value EX expiration time NX 127.0.0.1:6379> set lock 1 EX 10 NX OKCopy the code
Custom Redis distributed lock
As mentioned above, SETNX is an atomic operation, but there is no way to complete the EXPIRE operation at the same time, and the atomicity of SETNX and EXPIRE cannot be guaranteed. This can be done using the SET command and atomicity is guaranteed.
package com.aha.train.test.lock.distributed;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/** * Use redis to customize distributed locks **@author WT
* @date2021/10/25 * /
@Slf4j
@Service
public class MyRedisLock {
// Release lock execution LUA script
public static final String RELEASE_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
// The default lock key expiration time is 10s
private static final int DEFAULT_LOCK_EXPIRE_TIME_MILLIS = 100 * 1000;
// Node client
private RedisTemplate<Object,Object> redisTemplate;
public MyRedisLock(RedisTemplate<Object,Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/** ** Try to lock **@paramAcquireTimeout Attempts to lock wait time *@returnWhether the lock is successful *@throwsInterruptedException Thread interruption exception */
public String acquire(Integer acquireTimeout, String lockKey) throws InterruptedException {
if (acquireTimeout <= 0) {
throw new IllegalArgumentException("Lock timeout must be greater than 0");
}
try {
// The timeout period for obtaining the lock. If the timeout period is exceeded, the lock is abandoned
long end = System.currentTimeMillis() + acquireTimeout;
// Randomly generate a value
String value = UUID.randomUUID().toString();
while (System.currentTimeMillis() < end) {
SetNX does not guarantee the atomicity of SET and expire
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, value , DEFAULT_LOCK_EXPIRE_TIME_MILLIS, TimeUnit.MILLISECONDS);
if(result ! =null && result) {
log.info("Lock successful :{}",value);
return value;
}
// Delay for 100ms and continue to try to lock until the lock timeout period is reached
Thread.sleep(100);
log.info("Failed to acquire lock, try again to acquire lock"); }}catch (Exception e) {
log.error("acquire lock due to error", e);
}
return null;
}
/** * Release the lock@paramLockValue Specifies the value * of the active lock release@returnWhether the lock was successfully released */
public boolean release(String lockKey, String lockValue) {
// We need to use long to receive data from LUA scripts because int in Redis corresponds to Java long
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_LUA_SCRIPT, Long.class);
/* * The first argument is the content of the script to execute, the second argument is the set of keys in the lua script, The third argument is the collection of args in the Lua script * result = jedis.eval(script, Collections.singletonList(lockKey),Collections.singletonList(identify)); If using jedis client is this form of execution script */
Long result = redisTemplate.execute(redisScript, Arrays.asList(lockKey, lockValue));
returnresult ! =null && result > 0L; }}Copy the code
Test custom distributed locks
package com.aha.train.test.lock.distributed;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.*;
@RestController
@Slf4j
public class TestLock {
@Autowired
private MyRedisLock myRedisLock;
@GetMapping("/acquire")
public void acquire (a) throws InterruptedException {
String lockValue = myRedisLock.acquire(200."TEST_LOCK");
Thread.sleep(1000);
boolean release = myRedisLock.release("TEST_LOCK", lockValue);
log.info("Lock application successful: {}, unlock: {}",lockValue,release);
}
@GetMapping("/multiple/thread")
public void testMultipleThread (a) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
(100.200.10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
for (int i = 0; i < 100; i++) {
threadPoolExecutor.execute(() -> {
try {
String key = "aha_key";
String value = myRedisLock.acquire(20000, key);
if(value ! =null) {
Thread.sleep(10);
log.info("Execute business logic");
if (myRedisLock.release(key, value))
log.info("Unlocked successfully"); }}catch(InterruptedException e) { e.printStackTrace(); }}); }}}Copy the code
Redisson implements distributed locking
The importRedisson
The dependence of
<! -- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.1</version>
</dependency>
Copy the code
Writing configuration files
server:
port: 8002
spring:
# redis cache
redis:
# Use redisson configuration
redisson:
New versions of the Redisson configuration file can no longer use this form
# config: classpath:config/redisson-single.yaml
config: | # stand-alone mode singleServerConfig: # connection idle timeout, unit: ms idleConnectionTimeout: 10000 # connection timeout, unit: ms connectTimeout: 10000 # Command wait timeout, in milliseconds timeout: 3000 # Number of command attempts. If the retryAttempts cannot be sent to a specified node, an error will be thrown. # If the attempt to send within this limit succeeds, start the timeout timer. RetryAttempts: 3 # command retryInterval, in milliseconds retryInterval: 1000 # password password: Aha @ 3166 # a single connection to subscribe to largest quantity subscriptionsPerConnection: 5 # client name clientName: # null node address address: Redis: / / 10.8.18.115:30379 # the minimum number of idle connections subscriptionConnectionMinimumIdleSize publish and subscribe to connect: 1 # to publish and subscribe to the connection pool size subscriptionConnectionPoolSize: 50 # minimum number of idle connections connectionMinimumIdleSize: 32 # connectionPoolSize connection pool size: DnsMonitoringInterval: 5000 # Number of thread pools, Default: Number of current processing cores * 2 # Threads: 0 # Netty number of thread pools, default: number of current processing cores * 2 # nettyThreads: 0 # Codec:! < org redisson. Codec. JsonJacksonCodec > {} # transfer mode transportMode: "NIO"Copy the code
Testing distributed locks
package com.aha.distributedlock.redisson;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/** * Test using redisson to create a distributed lock **@author WT
* date 2021/11/6
*/
@Slf4j
@RestController
@RequestMapping("/redisson")
public class TestMyRedisLock {
private final RedissonClient redissonClient;
public TestMyRedisLock(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@GetMapping("/acquire")
public void acquire (a) {
// RLock reentrant lock
// Obtain the lock object (can be "reentrant lock "," fair lock ", or "red lock" if redis is clustered mode)
/ / fair lock
// RLock redissonLock = redissonClient.getFairLock("TEST_REDISSON_LOCK");
// Unfair lock
RLock redissonLock = redissonClient.getLock("TEST_REDISSON_LOCK");
try {
// Try locking. Wait 100 seconds at most. After locking, it will be unlocked automatically 10 seconds later
// We can see that the value set in redis is a hash key
boolean res = redissonLock.tryLock(100.10, TimeUnit.SECONDS);
if (res) {
log.info("Application for lock successful");
log.info("Execute the corresponding business logic.."); redissonLock.unlock(); }}catch(InterruptedException e) { e.printStackTrace(); }}@GetMapping("/multiple/thread")
public void testMultipleThread (a) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
(100.200.10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
for (int i = 0; i < 100; i++) {
threadPoolExecutor.execute(() -> {
RLock ahaLock = redissonClient.getLock("aha_key");
try {
boolean res = ahaLock.tryLock(100.10, TimeUnit.SECONDS);
if (res) {
Thread.sleep(10);
log.info("Execute business logic"); ahaLock.unlock(); }}catch(InterruptedException e) { e.printStackTrace(); }}); }}}Copy the code
Consider: Is the distributed lock implemented by Redis still secure when switching from master to slave