preface
Distributed lock is widely used in distributed applications. If you want to understand a new thing, you must first understand its origin, so that you can better understand and even draw inferences.
First of all, talking about distributed lock is naturally associated with distributed applications.
In the standalone system before we split the application into distributed applications, the need to read common resources in some concurrent scenarios, such as holding inventory and selling tickets, could be realized simply by using synchronization or locking.
But after the application of distributed system from the previous single-process multi-threading procedures into multi-process multi-threading, then the use of the above solution is obviously not enough.
Therefore, common solutions in the industry often resort to a third party component and exploit its own exclusivity to achieve multi-process mutual exclusion. Such as:
- DB – based unique index.
- Temporary ordered nodes based on ZK.
- Based on the Redis
NX EX
Parameters.
The discussion here is mainly based on Redis.
implementation
Since Redis is chosen, it must be exclusive. It should also have some of the basic features of locks:
- High performance (High performance when adding and unlocking)
- Blocking and non-blocking locks can be used.
- Deadlocks cannot occur.
- Availability (cannot lock a node after it is down).
Here, an NX parameter of the Redis set key is used to ensure that the write succeeds even if the key does not exist. In addition, the EX parameter can automatically delete the key after timeout.
So using the above two features ensures that only one process can acquire the lock at a time, and that no deadlocks occur (in the worst case, the key is automatically deleted when timeout occurs).
lock
The implementation code is as follows:
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
public boolean tryLock(String key, String request) {
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)){
return true ;
}else {
return false; }}Copy the code
Notice the jedis used here
String set(String key, String value, String nxxx, String expx, long time);
Copy the code
API.
This command guarantees the atomicity of NX EX.
Never execute the two commands (NX EX) separately, because a deadlock may occur if the program fails after NX.
Blocking locks
A blocking lock can also be implemented:
// block all the time
public void lock(String key, String request) throws InterruptedException {
for(;;) { String result =this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)){
break ;
}
// Prevent constant CPU consumptionThread.sleep(DEFAULT_SLEEP_TIME) ; }}// Customize the blocking time
public boolean lock(String key, String request,int blockTime) throws InterruptedException {
while (blockTime >= 0){
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)){
return true ;
}
blockTime -= DEFAULT_SLEEP_TIME ;
Thread.sleep(DEFAULT_SLEEP_TIME) ;
}
return false ;
}
Copy the code
unlock
Unlocking is also very simple. Simply delete the key, such as using the del key command.
But the reality is not always that easy.
If process A acquires the lock and sets the timeout period, but the lock is automatically released after the timeout period due to A long execution period. Process B then acquires the lock and quickly releases the lock. So process B releases the lock from process A.
The best way to do this is to check whether the lock is yours every time you unlock it.
This needs to be combined with the locking mechanism together.
When locking, you need to pass a parameter as the value of the key. In this way, you can check whether the values are the same each time you unlock the key.
So the unlock code can’t be a simple DEL.
public boolean unlock(String key,String request){
//lua script
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = null ;
if (jedis instanceof Jedis){
result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
}else if (jedis instanceof JedisCluster){
result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
}else {
//throw new RuntimeException("instance is error") ;
return false ;
}
if (UNLOCK_MSG.equals(result)){
return true ;
}else {
return false; }}Copy the code
Here a lua script is used to determine if values are equal, and then del is executed.
Using Lua also ensures the atomicity of these two operations.
Therefore, the four basic characteristics mentioned above can also be satisfied:
- Using Redis guarantees performance.
- Blocking and non-blocking locks are described above.
- Deadlocks are resolved using a timeout mechanism.
- Redis supports cluster deployment to improve availability.
use
I have a complete implementation of myself, and has been used for production, interested friends can be used out of the box:
Maven depends on:
<dependency>
<groupId>top.crossoverjie.opensource</groupId>
<artifactId>distributed-redis-lock</artifactId>
<version>1.0.0</version>
</dependency>
Copy the code
Configuration bean:
@Configuration
public class RedisLockConfig {
@Bean
public RedisLock build(a){
RedisLock redisLock = new RedisLock() ;
HostAndPort hostAndPort = new HostAndPort("127.0.0.1".7000); JedisCluster jedisCluster =new JedisCluster(hostAndPort) ;
// Either Jedis or JedisCluster works
redisLock.setJedisCluster(jedisCluster) ;
returnredisLock ; }}Copy the code
Use:
@Autowired
private RedisLock redisLock ;
public void use(a) {
String key = "key";
String request = UUID.randomUUID().toString();
try {
boolean locktest = redisLock.tryLock(key, request);
if(! locktest) { System.out.println("locked error");
return;
}
//do something
} finally{ redisLock.unlock(key,request) ; }}Copy the code
It’s easy to use. The main idea here is to use Spring to help us manage the RedisLock singleton bean, so we need to manually pass in the key and request when releasing the lock (because there is only one instance of RedisLock in the entire context).
You can also create a new RedisLock and pass in the key and request each time you use the lock, which is very convenient for unlocking. But you need to manage instances of RedisLock yourself. Each has its pros and cons.
Project source at:
Github.com/crossoverJi…
Welcome to the discussion.
A single measurement
When I was working on this project, I had to mention testing.
This is because the application is strongly dependent on third party components (Redis), but we need to eliminate this dependency in the single test. For example, another partner forked the project and tried to run a single test locally, but it didn’t work:
- It is possible that Redis IP, port and single test are inconsistent.
- Redis may have problems of its own.
- It is also possible that the student does not have Redis in his or her environment.
So it’s best to eliminate these external factors of instability and test only the code we’ve written.
This is where the single-sharpener Mock comes in.
The idea is simply to block out all external resources that you depend on. Such as: database, external interface, external files and so on.
The way of use is also quite simple, you can refer to the single test of the project:
@Test
public void tryLock(a) throws Exception {
String key = "test";
String request = UUID.randomUUID().toString();
Mockito.when(jedisCluster.set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyLong())).thenReturn("OK");
boolean locktest = redisLock.tryLock(key, request);
System.out.println("locktest=" + locktest);
Assert.assertTrue(locktest);
//check
Mockito.verify(jedisCluster).set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyLong());
}
Copy the code
This is just a quick demo, and we’ll look at it next time if you can.
Debug: It is easy to see how it works.
The JedisCluster we are relying on here is actually a Cglib proxy object. So it’s not hard to imagine how it might work.
For example, here we need the set function of JedisCluster and its return value.
The Mock will proxy the object and return you a custom value after actually executing the set method.
This allows us to test as much as we want, completely shielding external dependencies.
conclusion
A distributed lock based on Redis is now complete, but there are still some problems.
- For example, after a key timeout, the service is not completed but the lock is automatically released, which can cause concurrency problems.
- Even if Redis is clustered, if every node is only a master and not a slave, then when the master goes down, all keys on the node will be released at that moment, which will also cause concurrency problems. Even if there are slave nodes, the above problems can occur if the master goes down before data is synchronized to the Salve.
Those interested can also refer to Redisson’s implementation.
extra
Recently in the summary of some Java related knowledge points, interested friends can maintain together.
Address: github.com/crossoverJi…