Implementation of distributed lock
There are three common implementations of distributed locks:
- Redis implementation
- Zookeeper implementation
- Database implementation
1. Implementation based on Redis
There are three important commands in Redis that enable distributed locking
- Setnx key val: Sets a string with key val if and only if the key does not exist, returning 1; If key exists, do nothing and return 0.
- Expire key timeout: Sets a timeout period for the key. The unit is second. After this timeout period, the key will be automatically deleted.
- Delete key: deletes a key
1.1 Implementation Principles
- When obtaining the lock, use the setnx command to set a KV, where K is the name of the lock and V is a random number. If the lock is set successfully, the lock will be obtained. If the lock is not set successfully, it will fail. If the maximum time for trying to obtain a lock is set, the procedure is repeated until the lock is obtained or the maximum time is exceeded.
- Use the expire command to set a reasonable timeout period for the key created just now. This prevents the lock from being released if the lock cannot be released correctly. The timeout period should be set according to the project request.
- When releasing the lock, use V to check whether the lock is the original one. If the lock is the same, run delete to release the lock.
1.2 Implementation Method
1.2.1 Native code
public class DistributedLock implements Lock {
private static JedisPool JEDIS_POOL = null;
private static int EXPIRE_SECONDS = 60;
public static void setJedisPool(JedisPool jedisPool, int expireSecond) {
JEDIS_POOL = jedisPool;
EXPIRE_SECONDS = expireSecond;
}
private String lockKey;
private String lockValue;
private DistributedLock(String lockKey) {
this.lockKey = lockKey;
}
public static DistributedLock newLock(String lockKey) {
return new DistributedLock(lockKey);
}
@Override
public void lock(a) {
if(! tryLock()) {throw new IllegalStateException("Lock not obtained"); }}@Override
public void lockInterruptibly(a) throws InterruptedException {}@Override
public boolean tryLock(a) {
return tryLock(0.null);
}
@Override
public boolean tryLock(long time, TimeUnit unit) {
Jedis conn = null;
String retIdentifier = null;
try {
conn = JEDIS_POOL.getResource();
lockKey = UUID.randomUUID().toString();
// The timeout period for obtaining the lock. If the timeout period is exceeded, the lock is abandoned
long end = 0;
if(time ! =0) {
end = System.currentTimeMillis() + unit.toMillis(time);
}
do {
if (conn.setnx(lockKey, lockValue) == 1) {
conn.expire(lockKey, EXPIRE_SECONDS);
return true;
}
try {
Thread.sleep(10);
} catch(InterruptedException e) { Thread.currentThread().interrupt(); }}while (System.currentTimeMillis() < end);
} catch (JedisException e) {
if (lockValue.equals(conn.get(lockKey))) {
conn.del(lockKey);
}
e.printStackTrace();
} finally {
if(conn ! =null) { conn.close(); }}return false;
}
@Override
public void unlock(a) {
Jedis conn = null;
try {
conn = JEDIS_POOL.getResource();
if(lockValue.equals(conn.get(lockKey))) { conn.del(lockKey); }}catch (JedisException e) {
e.printStackTrace();
} finally {
if(conn ! =null) { conn.close(); }}}@Override
public Condition newCondition(a) {
return null; }}Copy the code
There is also a problem with the above code. Setnx and EXPIRE are done in two steps. Although handling exceptions in a catch and trying to remove locks that might occur is not a friendly approach, a good solution is to implement lua scripts. Both Spring Redis Lock and Redission are implemented through Lua scripting
local lockClientId = redis.call('GET', KEYS[1])
if lockClientId == ARGV[1] then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return true
elseif not lockClientId then
redis.call('SET', KEYS[1], ARGV[1].'PX', ARGV[2])
return true
end
return false
Copy the code
1.2.2 Spring Redis Lock implementation
1. The import library
The Spring Boot project automatically configures the version number according to Spring Boot dependency management
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Copy the code
2. Configure redis
Configure in application-xxx.yml
spring:
redis:
host: 127.0. 01.
port: 6379
timeout: 2500
password: xxxxx
Copy the code
3. Add configurations
RedisLockConfig.java
import java.util.concurrent.TimeUnit;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.integration.redis.util.RedisLockRegistry;
@Configuration
public class RedisLockConfig {
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
return new RedisLockRegistry(redisConnectionFactory, "redis-lock",
TimeUnit.MINUTES.toMillis(10)); }}Copy the code
Use 4.
@Autowired
private RedisLockRegistry lockRegistry;
Lock lock = lockRegistry.obtain(key);
boolean locked = false;
try {
locked = lock.tryLock();
if(! locked) {// No lock logic was obtained
}
// Get the lock logic
} finally {
// Be sure to unlock it
if(locked) { lock.unlock(); }}Copy the code
1.2.3 Redission implementation
Config config = new Config();
config.useSingleServer().setAddress("Redis: / / 127.0.0.1:6379").setPassword("xxxxxx").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
RLock rLock = redissonClient.getLock("lockKey");
boolean locked = false;
try {
/* * waitTimeout Specifies the maximum waiting time for the lock to be acquired. If this value is exceeded, the lock is considered to have failed
locked = rLock.tryLock((long) waitTimeout, (long) leaseTime, TimeUnit.SECONDS);
if(! locked) {// There is no logic to acquire the lock
}
// Get the lock logic
} catch (Exception e) {
throw new RuntimeException("aquire lock fail");
} finally {
if(locked)
rLock.unlock();
}
Copy the code
1.3 the advantages and disadvantages
Advantages: Redis itself has high performance, even if there are a large number of setnx commands will not be reduced
Disadvantages:
- If the timeout period set for the key is too short, the lock may be released before the business process finishes processing it, and other requests may obtain the lock
- If the timeout period set for the key is too large and the lock is not released, some requests may wait for locks for a long time
- CPU resources are wasted while the lock is constantly trying
For the second disadvantage, Redission uses the renewal mechanism to check whether the lock is still in progress at regular intervals. If the lock is still running, the corresponding key will be added for a certain period of time to ensure that the key will not be automatically deleted when the lock is running
2. Implementation based on Zookeeper
2.1 Implementation Principles
Distributed lock based on ZooKeeper temporary ordered node.
General procedure: When a client locks a method, a unique temporary ordered node is generated in the directory corresponding to the specified node on ZooKeeper. The way to determine whether to obtain the lock is very simple, just need to determine the smallest serial number in the ordered node. When the lock is released, the instantaneous node is simply removed. At the same time, it can avoid deadlock problems caused by locks that cannot be released due to service downtime.
When the first node applies for the lock xxxlock, it is as follows: under the persistent node xxxLock, create a temporary ordered node of lock, because lock is the smallest ordered node, then obtain the lock
When the first node is still processing the service logic and does not release the lock, the second node applies for xxxlock and creates a temporary ordered node of lock. At this time, because lock is not the one with the smallest serial number among the ordered nodes, it cannot obtain the lock at this time. The lock can be obtained only after lock:1 node is deleted. At this point, lock:2 will watch its previous node (i.e., lock:1) until lock:1 is deleted before acquiring the lock
When the first node is still processing the business logic and has not released the lock, the second node is still queuing. When the third node applies for the lock, a temporary ordered node of lock is created. At this time, because lock is not the one with the smallest serial number among the ordered nodes, the lock cannot be obtained. The lock can be obtained only after the upper nodes (lock:1 and lock:2) are deleted. In this case, lock:3 will watch its previous node (lock:2) until lock:2 is deleted
2.2 the use of
2.2.1 Using spring-integration-ZooKeeper
Maven.
<dependency>
<! -- spring integration -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-zookeeper</artifactId>
</dependency>
Copy the code
Gradle.
compile "Org. Springframework. Integration: spring - integration - the zookeeper: 5.1.2. RELEASE"
Copy the code
Increase the configuration
@Configuration
public class ZookeeperLockConfig {
@Value("${zookeeper.host}")
private String zkUrl;
@Bean
public CuratorFrameworkFactoryBean curatorFrameworkFactoryBean(a) {
return new CuratorFrameworkFactoryBean(zkUrl);
}
@Bean
public ZookeeperLockRegistry zookeeperLockRegistry(CuratorFramework curatorFramework) {
return new ZookeeperLockRegistry(curatorFramework, "/lock"); }}Copy the code
use
@Autowired
private ZookeeperLockRegistry lockRegistry;
Lock lock = lockRegistry.obtain(key);
boolean locked = false;
try {
locked = lock.tryLock();
if(! locked) {// No lock logic was obtained
}
// Get the lock logic
} finally {
// Be sure to unlock it
if(locked) { lock.unlock(); }}Copy the code
2.2.2 Using Apache Curator
Maven
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.1.0</version>
</dependency>
Copy the code
use
CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient(
connectString,
sessionTimeoutMs,
connectionTimeoutMs,
new RetryNTimes(retryCount, elapsedTimeMs));
InterProcessMutex mutex = new InterProcessMutex(curatorFramework, "lock name");
mutex.acquire(); / / acquiring a lock
mutex.acquire(long time, TimeUnit unit) // Get the lock and set the maximum wait time
mutex.release(); / / releases the lock
Copy the code
2.3 the advantages and disadvantages
Advantages:
- Solve the single point problem and deploy ZooKeeper in a cluster.
- Because the temporary node is used, the lock can be released in the event of an unexpected project, and the temporary node will be deleted automatically when the session is interrupted unexpectedly.
- No need to set storage expiration time, avoid Redis lock expiration issues;
Disadvantages:
- Performance is not as good as Redis implementation;
3. Database based implementation
3.1 Implementation Principle
create table distributed_lock (
id int(11) unsigned NOT NULL auto_increment primary key,
key_name varchar(30) unique NOT NULL comment 'lock name',
update_time datetime default current_timestamp on update current_timestamp comment 'Update Time'
)ENGINE=InnoDB comment 'Database lock';
Copy the code
Method 1: Implement insert and DELETE
With database unique indexes, when we want to acquire a lock, we insert a piece of data, if the insert is successful, we acquire the lock, after obtaining the lock, we delete the lock through the DELETE statement
In this way, the lock does not wait. If you want to set the maximum time for acquiring the lock, you need to implement it yourself
Method 2: Use for Update
The following operations need to be done in a transaction
select * from distributed_lock where key_name = 'lock' for update;
Copy the code
Add for UPDATE to the end of the query statement, and the database adds an exclusive lock to the database table during the query. When an exclusive lock is added to a record, other threads cannot add an exclusive lock to that record. Another feature of for Update is that it blocks, which indirectly implements a blocking queue, but the blocking time for for Update is determined by the database, not the program.
In MySQL 8, for UPDATE statements can be used with nowait to implement non-blocking usage
select * from distributed_lock where key_name = 'lock' for update nowait;
Copy the code
InnoDB uses row-level locks only for index queries, otherwise it is a table lock, and can be upgraded to a table lock if there is no data to query.
This approach needs to be used when the data already exists in the database implementation.
3.2 the advantages and disadvantages
Advantages:
If the project already uses the database in the case of no other middleware, can directly use the database, reduce the dependency directly with the database, easy to understand.
Disadvantages:
- Operating a database requires some overhead, and performance issues need to be considered.
- Using database row-level locking is not always a good idea, especially if our lock table is not large;
- There is no lock timeout mechanism, so you must delete the lock by yourself. How to delete the lock after the fault occurs
- The for Update mode must be inside the transaction, which is another problem if the business operation cannot be performed inside the transaction
- All kinds of problems will make the whole scheme more and more complicated in the process of solving them.
4. Contrast
From a performance perspective (high to low) Cache > Zookeeper >= database
From the perspective of reliability (from highest to lowest) Zookeeper > Cache > Database
Problem, implementation | Redis | Zookeeper | The database |
---|---|---|---|
performance | high | In the | low |
reliability | In the | high | low |
Overdue delete | If yes, set the expiration time or manually delete it | Manually delete the data after the service logic is executed | 1. After the transaction is complete, the database is automatically released. 2 |
Blocking queue | None. The solution requires client spin | By listening on the previous lock solution, watch mechanism | 2. Insert mode requires client spin solution |
Services are not completed within the timeout period | You need to write your own renewal mechanism to do this, which Redission implemented internally | No this problem | 1. The transaction may time out if the execution for UPDATE takes too long. 2 |
The lock is not manually deleted due to an exception of the project | Redis has an expiration date, after which it is automatically deleted | After the session is disconnected, the temporary node is automatically deleted | 1. For update mechanism database will automatically clear 2. Insert mode have to think of their own solution |