分布式锁的详细解释
什么是分布式锁
分布式锁是一种用于协调分布式系统中多个进程或线程之间访问共享资源的机制。在分布式系统中,多个进程或线程可能同时竞争访问某个共享资源,为了避免并发访问导致的数据不一致或冲突,需要使用分布式锁来保证资源的独占性。
分布式锁使用互斥的方式来控制对共享资源的访问,通常通过加锁和释放锁的操作来实现。加锁操作会阻塞其他进程或线程的访问请求,直到当前进程或线程释放锁。这样可以确保在任意时刻只有一个进程或线程可以访问共享资源,从而避免并发访问导致的数据不一致或冲突。
分布式锁可以通过多种方式来实现,包括基于数据库、基于缓存、基于分布式协议等。常见的实现方式包括使用分布式缓存如Redis实现的锁、使用数据库的行级锁、使用ZooKeeper或etcd等分布式协调服务来实现的锁等。这些实现方式都具有一定的优缺点,选择合适的实现方式需要考虑系统的需求和限制。
分布式锁的解决方案
1、数据库解决方案思路:
a.数据库建一张表,字段方法名并且作为唯一性,当一个方法执行时插入,则相当于获得锁,其他线程将无法访问,方法执行完则释放锁。
但是上面这种存在问题:
1、数据库单点,出现故障则将导致系统不可用。
2、没有失效时间,一旦操作方法异常,导致一直没有解锁,也将导致其他不可用用。
b.使用select * from user u where username = '' for update
来对记录加上排他锁。操作完成后使用commit命令释放锁。
2、基于缓存实现: 通常有Memcached、Redis实现等,以下以Redis实现分布式锁为例
思路:主要用到的redis函数是setnx(),这个应该是实现分布式锁最主要的函数。首先是将某一任务标识名(这里用Lock:order作为标识名的例子)作为键存到redis里,并为其设个过期时间,如果是还有Lock:order请求过来,先是通过setnx()看看是否能将Lock:order插入到redis里,可以的话就返回true,不可以就返回false
3.Zookeeper分布式锁
大致思路:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。
判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。
当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上,所以性能上不如基于缓存实现。
分布式锁的应用场景
-
避免资源竞争:在分布式系统中,多个进程或节点可能需要同时访问共享资源。使用分布式锁可以确保同一时间只有一个进程或节点能够访问共享资源,避免资源竞争和数据不一致。
-
避免重复操作:在某些场景下,比如定时任务或消息队列处理,可能会有多个进程或节点同时执行某个操作。使用分布式锁可以保证只有一个进程或节点能够执行该操作,避免重复执行。
-
保证数据一致性:在分布式系统中,多个进程或节点可能需要同时修改某个共享数据。使用分布式锁可以确保同一时间只有一个进程或节点能够修改共享数据,避免数据不一致。
-
避免死锁:在分布式系统中,可能会出现多个进程或节点之间的循环依赖锁的情况,导致死锁。使用分布式锁可以避免这种情况的发生。
-
控制并发访问:在某些场景下,需要限制同时访问某个资源的并发数。使用分布式锁可以实现对并发访问的控制,保证系统的稳定性和性能。
实例
import org.apache.zookeeper.*;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class DistributedLock implements Watcher {
private ZooKeeper zooKeeper;
private String lockPath;
private String currentPath;
private String waitPath;
private CountDownLatch countDownLatch;
public DistributedLock(String address, String lockPath) {
try {
this.zooKeeper = new ZooKeeper(address, 5000, this);
this.lockPath = lockPath;
if (zooKeeper.exists(lockPath, false) == null) {
zooKeeper.create(lockPath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (IOException | InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
public void lock() {
try {
currentPath = zooKeeper.create(lockPath + "/lock_", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
List<String> children = zooKeeper.getChildren(lockPath, false);
Collections.sort(children);
if (currentPath.equals(lockPath + "/" + children.get(0))) {
return;
}
String currentNode = currentPath.substring(currentPath.lastIndexOf("/") + 1);
waitPath = lockPath + "/" + children.get(Collections.binarySearch(children, currentNode) - 1);
countDownLatch = new CountDownLatch(1);
zooKeeper.exists(waitPath, true);
countDownLatch.await();
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
public void unlock() {
try {
zooKeeper.delete(currentPath, -1);
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
@Override
public void process(WatchedEvent event) {
if (countDownLatch != null && event.getPath().equals(waitPath) && event.getType() == Event.EventType.NodeDeleted) {
countDownLatch.countDown();
}
}
}
在上述示例中,首先创建了一个ZooKeeper实例,连接到ZooKeeper服务。然后创建了一个指定路径的永久节点,用于保存分布式锁。在lock方法中,首先创建一个临时有序节点,然后获取锁路径下的所有子节点,并对子节点进行排序。如果当前节点是所有子节点中最小的节点,则表示获取到了锁;否则,使用二分查找找到当前节点的前一个节点,并监听其删除事件,在该节点被删除时释放锁,调用countDownLatch.countDown()方法唤醒等待的线程。在unlock方法中,删除当前节点即可释放锁。
使用示例:
public class Main {
public static void main(String[] args) {
DistributedLock lock = new DistributedLock("localhost:2181", "/distributed-lock");
try {
lock.lock();
// 执行需要同步的代码块
} finally {
lock.unlock();
}
}
}
在上述示例中,创建了一个DistributedLock实例,然后在需要同步访问的代码块前调用lock方法获取锁,在代码块执行完毕后调用unlock方法释放锁。这样就可以保证同一时刻只有一个线程能够执行该代码块,实现了分布式环境下的同步访问。
总结
-
基于数据库的分布式锁:可以通过在数据库中创建一张锁表,对需要加锁的资源在该表中插入一条记录,当其他进程或线程尝试获取锁时,通过数据库事务机制来保证原子性和互斥性;
-
基于缓存的分布式锁:使用分布式缓存,如Redis或Memcache,将锁作为一个缓存键的值进行存储。其他进程或线程在获取锁时,先尝试设置该缓存键,如果设置成功,则表示获取到锁;
-
基于ZooKeeper的分布式锁:使用ZooKeeper这样的分布式协调服务,通过创建临时有序节点来实现锁。其他进程或线程在获取锁时,需要通过比较自身创建的节点是否是最小的节点来判断是否获取到锁;
分布式锁需要考虑以下几个问题:
-
死锁问题:由于网络延迟或其他异常情况,可能会导致锁没有正确释放,从而造成死锁。可以通过设置锁的超时时间或引入心跳机制来解决;
-
锁竞争问题:多个进程或线程同时竞争同一个锁时,可能会导致频繁的锁竞争,从而影响性能。可以通过引入重试机制、公平锁等来减少锁竞争;
-
容错问题:分布式系统中,可能存在节点故障、网络分区等情况,需要确保分布式锁在这些异常情况下仍能正常工作。可以通过引入选举机制、多副本存储等来提高容错性。