- 作者简介:一名后端开发人员,每天分享后端开发以及人工智能相关技术,行业前沿信息,面试宝典。
- 座右铭:未来是不可确定的,慢慢来是最快的。
- 个人主页:极客李华-CSDN博客
- 合作方式:私聊+
- 这个专栏内容:BAT等大厂常见后端java开发面试题详细讲解,更新数目100道常见大厂java后端开发面试题。
- 我的CSDN社区:https://bbs.csdn.net/forums/99eb3042821a4432868bb5bfc4d513a8
- 微信公众号,抖音,b站等平台统一叫做:极客李华,加入微信公众号领取各种编程资料,加入抖音,b站学习面试技巧,职业规划
分布式锁,Redission,其它实现问题讲解,以及面试题回答案例
什么是分布式锁?
分布式锁是一种同步机制,用于控制多个进程或节点对共享资源的访问。其目标是在分布式系统中防止并发访问引起的数据不一致或竞争条件问题。当一个节点获得了分布式锁后,其他节点必须等待或被阻塞,直到锁被释放。
分布式锁的需求
在分布式环境中,有几个方面需要考虑:
-
原子性: 获取锁和释放锁的操作应该是原子的,以防止竞态条件。
-
可靠性: 即使在节点故障或网络分区的情况下,分布式锁也应该是可靠的,能够正确地保持锁的状态。
-
性能: 分布式锁应该是高性能的,以确保不会成为系统的瓶颈。
常见的分布式锁实现方式
-
基于数据库的实现: 使用数据库的事务特性来实现分布式锁,通过在数据库中创建一个锁表,将锁状态存储在数据库中。
-
基于ZooKeeper的实现: ZooKeeper是一个分布式协调服务,可以用来实现分布式锁。通过创建ZooKeeper节点来表示锁的状态,可以实现简单而可靠的分布式锁。
-
基于Redis的实现: Redis是一种内存数据库,提供了原子操作和分布式特性,因此可以用作分布式锁的存储介质。
Redis作为分布式锁的存储介质
Redis是一个快速且具有分布式特性的内存数据库,常被用作分布式锁的存储介质。在Redis中,我们可以使用两种方式实现分布式锁:
- 基于SETNX和EXPIRE指令的简单实现: 使用
SETNX
(Set if Not eXists)指令来尝试获取锁,如果成功则获得锁,然后通过EXPIRE
指令设置锁的过期时间。释放锁时,通过删除对应的键来释放锁。
SET resource_name my_unique_identifier NX EX 10
- 基于Lua脚本的复杂实现: 使用Lua脚本在一次请求中执行多个指令,从而实现获取锁和设置过期时间的原子操作。
Redission作为分布式锁解决方案
Redission是一个基于Redis的Java驱动,提供了丰富的分布式锁功能。它封装了分布式锁的复杂性,提供了简单而强大的API。
使用Redission获取锁
RRedissionClient redisson = Redisson.create();
RLock lock = redisson.getLock("myLock");
lock.lock(); // 尝试获取锁
try {
// 在这里执行需要加锁的代码
} finally {
lock.unlock(); // 释放锁
}
锁的可重入性
Redission支持锁的可重入性,同一个线程可以多次获取同一把锁,每次获取都需要相应的释放。
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// 执行加锁代码
lock.lock(); // 可以再次获取锁
try {
// 执行嵌套的加锁代码
} finally {
lock.unlock(); // 释放嵌套的锁
}
} finally {
lock.unlock(); // 释放外层锁
}
基于数据库的分布式锁
使用关系型数据库实现分布式锁是一种常见的方式。通过在数据库中创建锁表,并使用事务来确保获取锁和释放锁的原子性,可以实现基于数据库的分布式锁。
-- 创建锁表
CREATE TABLE distributed_lock (
lock_name VARCHAR(255) PRIMARY KEY,
locked_by VARCHAR(255),
expiration_time TIMESTAMP
);
-- 获取锁
INSERT INTO distributed_lock (lock_name, locked_by, expiration_time)
VALUES ('myLock', 'myIdentifier', NOW() + INTERVAL 10 SECOND)
ON DUPLICATE KEY UPDATE locked_by = VALUES(locked_by), expiration_time = VALUES(expiration_time);
-- 释放锁
DELETE FROM distributed_lock WHERE lock_name = 'myLock' AND locked_by = 'myIdentifier';
ZooKeeper分布式锁的实现
-
创建锁节点: 每个参与锁的节点在ZooKeeper上创建一个独特的顺序临时节点,表示它想要获取锁。
-
获取锁: 节点通过获取锁的过程,即检查是否是所有子节点中最小的节点。如果是最小节点,则它成功获取了锁。
-
监听前一个节点: 如果节点没有成功获取锁,它会监听它前面一个节点的删除事件。一旦前一个节点被删除,表示锁可用,当前节点再次尝试获取锁。
-
释放锁: 节点在使用锁完成后,将自己创建的节点删除,释放锁。
下面是一个使用ZooKeeper进行分布式锁实现的简单示例:
import org.apache.zookeeper.*;
public class ZooKeeperLock implements Watcher {
private ZooKeeper zooKeeper;
private String lockPath;
public ZooKeeperLock(String connectionString, String lockPath) throws Exception {
this.zooKeeper = new ZooKeeper(connectionString, 5000, this);
this.lockPath = lockPath;
}
public void acquireLock() throws KeeperException, InterruptedException {
String lockNode = zooKeeper.create(lockPath + "/lock_", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取所有子节点
var children = zooKeeper.getChildren(lockPath, false);
// 排序子节点
children.sort(String::compareTo);
// 判断当前节点是否是最小节点
if (lockNode.endsWith(children.get(0))) {
System.out.println("Lock acquired!");
return;
}
// 如果不是最小节点,则监听前一个节点
String predecessor = lockPath + "/" + children.get(children.indexOf(lockNode.substring(lockPath.length() + 1)) - 1);
zooKeeper.exists(predecessor, true);
}
public void releaseLock() throws KeeperException, InterruptedException {
zooKeeper.close();
}
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted) {
// 前一个节点被删除,重新尝试获取锁
try {
acquireLock();
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
考虑因素和注意事项
-
锁的粒度: 锁的粒度应该尽量小,以减小锁的争用,提高系统的并发性能。
-
死锁: 在设计分布式锁时,要注意死锁的可能性。一些系统引入超时机制,以防止因故障或其他原因导致的死锁。
-
锁的持有时间: 锁的持有时间应该尽量短,以减小锁的争用时间,提高系统的响应性能。
-
锁的可重入性: 考虑是否需要支持锁的可重入性,以便同一个线程可以多次获取同一把锁。
-
容错性: 分布式锁的实现应该考虑系统的容错性,即使在节点故障或网络分区的情况下,锁仍然能够正确地工作。
实际应用中的分布式锁
在实际应用中,分布式锁常用于以下场景:
-
防止重复操作: 通过锁来确保某个操作只能被执行一次,防止重复操作。
-
控制资源访问: 限制对共享资源的并发访问,确保数据一致性。
-
分布式事务: 在分布式事务中使用分布式锁来确保事务的一致性。
面试回答演示
分布式锁是一种同步机制,简单的说就是在分布式系统中当多个进程或节点共享资源的时候,用于解决他们因为并发访问引起的数据不一致问题,当一个节点获得分布式锁之后,其他节点必须等待直到说释放为止。一般情况下分布式锁的实现需要考虑三个方面,也就是原子性,可靠性和性能,要在高性能情况下,让分布式锁在获得和释放的过程中操作是原子的,同时在节点故障或者网络分区情况,分布式锁还是可靠的。
常见的实现方式有基于数据库的实现,基于zookeeper的实现,基于redis的实现,数据库实现的话就是通过数据库创建锁表,并且利用数据库的事物来确保锁的获取与释放是原子性的,redis的话就是可以使用set,setnx和expire指令来实现,setnx是用来获取锁的,如果成功获取锁,再用expire指令来设置锁的过期时间,再复杂的锁的实现可以使用lua脚本进行实现,lua脚本可以在一次请求的过程中执行过个指令。
还有的方法是使用redission这个基于redis的java驱动来实现,它里面提供了丰富的api来实现这个复杂的过程,比如最开始创建一个redisson对象,然后通过这个redisson对象可以获取锁,然后再使用try…finally在try中加入需要加锁的代码,在finally中进行锁的释放,redisson还支持锁的可重入性,也就是允许同一个线程多次获取同一把锁,也就是使用try…finally嵌套,这种方式可以解决一些死锁的情况,比如在我正常的业务没有问题,但是存在数据库发生异常的情况而导致的错误,这个时候我们使用try…catch嵌套来实现,里面的try…catch来处理正常业务,外面的try…catch来处理数据库异常的情况。
zookeeper也可以实现分布式锁,这个实现的话,一般是每个参与锁的节点在zookeeper上创建一个独特的顺序临时节点,然后节点获取锁的过程,当该节点是所有子节点中最小的节点他就可以获取锁,一个节点没有获取锁成功,他会监听前面一个节点是否发生删除事件,因为一个锁他的锁使用完了之后,他会把自己创建的节点删了,释放锁。