Implementation of distributed locks

Distributed Lock Solution - Toutiao

Three implementations of distributed locks - CSDN

Distributed lock JAVA common solution 1 - code

Distributed lock solution

 

Distributed lock usage scenarios

In many scenarios, in order to ensure the eventual consistency of data, we need a lot of technical solutions, such as distributed transactions, distributed locks, etc. Sometimes, we need to ensure that a method can only be executed by the same thread at the same time. In a stand-alone environment , Java actually provides many APIs related to concurrent processing ( such as ReentrantLcok or synchronized), but these APIs are powerless in distributed scenarios . That is to say, pure Java Api cannot provide the ability of distributed lock. Therefore, there are currently a variety of solutions for the implementation of distributed locks:

 

There are generally three implementations of distributed locks: 1. Database locks; 2. Redis-based distributed locks; 3. ZooKeeper-based distributed locks .

 

What should a distributed lock look like

 

Mutual exclusion  : It can be guaranteed that in a distributed deployment application cluster, the same method can only be executed by one thread on one machine at the same time .

The lock must be a reentrant lock (avoid deadlock)

No deadlock : if a client crashes while holding the lock without unlocking, it is also guaranteed that other clients can lock

This lock is preferably a blocking lock (consider whether or not to use this according to business needs)

There are highly available lock acquisition and release lock functions

Better performance in acquiring and releasing locks

 

database lock

 

Based on database table

To implement distributed locks, the easiest way may be to create a lock table directly , and then do so by manipulating the data in the table.

When we want to lock a method or resource , we add a record to the table, and delete this record when we want to release the lock .

CREATE TABLE `methodLock` (  
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',  
  `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT 'Locked method name',  
  `desc` varchar(1024) NOT NULL DEFAULT 'Remarks',  
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Save data time, automatically generated',  

  PRIMARY KEY (`id`),  
  UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE  
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Method in lock';  

 

When we want to lock a method, execute the following SQL:

insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

 

Because we have made a unique constraint on method_name , if there are multiple requests submitted to the database at the same time, the database will ensure that only one operation can be successful, then we can consider that the thread that succeeded in the operation has obtained the lock of the method and can execute Method body content.

 

After the method is executed, if you want to release the lock, you need to execute the following Sql:

delete from methodLock where method_name ='method_name'

 

The above simple implementation has the following problems :

 

1. This lock is strongly dependent on the availability of the database. The database is a single point. Once the database hangs, the business system will be unavailable .

2. This lock has no expiration time . Once the unlock operation fails, the lock record will always be in the database, and other threads can no longer obtain the lock.

3. This lock can be non-blocking , because the insert operation of the data will report an error directly once the insertion fails. Threads that have not acquired the lock will not enter the queue. To acquire the lock again, the lock acquisition operation is triggered again.

4. This lock is non-reentrant, and the same thread cannot acquire the lock again until the lock is released . Because the data already exists in the data.

 

solution:

 

Is the database a single point? Engage in two databases , and the data is synchronized in both directions before. Once hung up, quickly switch to the standby database.

No expiration time? Just do a scheduled task, and clean up the timeout data in the database at regular intervals .

non-blocking? Engage in a while loop until the insert is successful and then return to success.

non-reentrant? Add a field to the database table to record the host information and thread information of the machine that currently obtains the lock , then query the database first when acquiring the lock next time. If the host information and thread information of the current machine can be found in the database, directly Just assign the lock to him.

 

Database based exclusive lock

In addition to adding and deleting records in the data table, distributed locks can also be implemented with the help of the locks that come with the database .

We also use the database table we just created. Distributed locks can be implemented through database exclusive locks.

 

Add for update after the query statement , and the database will add an exclusive lock to the database table during the query process. When an exclusive lock is added to a record, other threads cannot add an exclusive lock to the row.

 

We can think that the thread that obtains the exclusive lock can obtain the distributed lock. When the lock is obtained, the business logic of the method can be executed. After the method is executed, it can be unlocked by the following methods:

public void unlock(){  
    connection.commit();  
}

 Release the lock through the connection.commit() operation.

 

This method can effectively solve the above-mentioned problems of inability to release locks and blocking locks.

 

blocking lock? The for update statement will return immediately after the execution is successful, and will be blocked when the execution fails until it succeeds.

After the lock, the service is down and cannot be released? In this way, the database will release the lock itself after the service goes down.

However, it still cannot directly solve the problem of database single point and reentrancy.

 

Summarize:

 

Summarize the way to use the database to implement distributed locks, both of which are dependent on a table of the database , one is to determine whether there is currently a lock by the existence of records in the table, and the other is to determine whether there is a current lock through the database. Distributed locks are implemented using exclusive locks .

 

数据库实现分布式锁的优点: 直接借助数据库,容易理解。

数据库实现分布式锁的缺点: 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。

操作数据库需要一定的开销,性能问题需要考虑。

 

乐观锁

乐观锁假设认为数据一般情况下不会造成冲突,只有在进行数据的提交更新时,才会检测数据的冲突情况,如果发现冲突了,则返回错误信息

 

实现方式:

时间戳(timestamp)记录机制实现:给数据库表增加一个时间戳字段类型的字段,当读取数据时,将timestamp字段的值一同读出,数据每更新一次,timestamp也同步更新。当对数据做提交更新操作时,检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,若相等,则更新,否则认为是失效数据。

 

若出现更新冲突,则需要上层逻辑修改,启动重试机制

同样也可以使用version的方式。

 

性能对比

 

(1) 悲观锁实现方式是独占数据,其它线程需要等待,不会出现修改的冲突,能够保证数据的一致性,但是依赖数据库的实现,且在线程较多时出现等待造成效率降低的问题。一般情况下,对于数据很敏感且读取频率较低的场景,可以采用悲观锁的方式

 

(2) 乐观锁可以多线程同时读取数据,若出现冲突,也可以依赖上层逻辑修改,能够保证高并发下的读取,适用于读取频率很高而修改频率较少的场景

 

(3) 由于库存回写数据属于敏感数据且读取频率适中,所以建议使用悲观锁优化

 

基于redis的分布式锁

相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。而且很多缓存是可以集群部署,可以解决单点问题。

 

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

 

互斥性。在任意时刻,只有一个客户端能持有锁。

不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。

解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

 

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

 

第一个为key,我们使用key来当锁,因为key是唯一的。

 

第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

 

第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

 

第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

第五个为time,与第四个参数相呼应,代表key的过期时间

 

总的来说,执行上面的set()方法就只会导致两种结果:

1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。

2. 已有锁存在,不做任何操作。

 

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

 

 

错误实例:

 

使用jedis.setnx()jedis.expire()组合实现加锁

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {  
  
    Long result = jedis.setnx(lockKey, requestId);  
    if (result == 1) {  
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁  
        jedis.expire(lockKey, expireTime);  
    }    
}  

 

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

 

解锁:

首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)

 

总结:

 

可以使用缓存来代替数据库来实现分布式锁,这个可以提供更好的性能,同时,很多缓存服务都是集群部署的,可以避免单点问题。并且很多缓存服务都提供了可以用来实现分布式锁的方法,比如redis的setnx方法等。并且,这些缓存服务也都提供了对数据的过期自动删除的支持,可以直接设置超时时间来控制锁的释放。

 

使用缓存实现分布式锁的优点

性能好,实现起来较为方便。

 

使用缓存实现分布式锁的缺点

通过超时时间来控制锁的失效时间并不是十分的靠谱。

 

基于Zookeeper实现分布式锁

 

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

 

1、创建一个目录mylock;

2、线程A想获取锁就在mylock目录下创建临时顺序节点;

3、获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;

4、线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;

5、线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁

 

基于zookeeper临时有序节点可以实现的分布式锁。大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

 

来看下Zookeeper能不能解决前面提到的问题。

 

锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(

Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

 

非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户

端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。

 

不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的

时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。

 

单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。

可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。

 

 

Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高

 

因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。

 

使用Zookeeper实现分布式锁的优点: 有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。

使用Zookeeper实现分布式锁的缺点 : 性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。

 

 

三种方案的比较

从理解的难易程度角度(从低到高): 数据库 > 缓存 > Zookeeper

 

从实现的复杂性角度(从低到高): Zookeeper >= 缓存 > 数据库

 

从性能角度(从高到低): 缓存 > Zookeeper >= 数据库

 

从可靠性角度(从高到低): Zookeeper > 缓存 > 数据库

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326010999&siteId=291194637