为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用并发处理相关的功能进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
分布式锁具备的条件
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
分布式锁的实现
缓存实现分布式锁
1.基于Redis本身的set nx命令实现
原理
setNX是Redis提供的一个原子操作,如果指定key存在,那么setNX失败,如果不存在会进行Set操作并返回成功。我们可以利用这个来实现一个分布式的锁,主要思路就是,set成功表示获取锁,set失败表示获取失败,失败后需要重试
实现代码
import redis.clients.jedis.Jedis;
public class RedisLockExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
long result = jedis.setnx("lock_key", "lock_value");
if (result == 1) {
System.out.println("获取锁成功");
// 执行业务逻辑
jedis.del("lock_key"); // 释放锁
} else {
System.out.println("获取锁失败");
}
}
}
2.基于Redisson客户端实现
原理:
- 锁请求:当一个客户端(比如一个Java应用)需要获取锁时,它会向Redis服务器发送一个锁请求。
- 加锁尝试:Redisson客户端会尝试通过执行Redis命令(如 SETNX 或 Lua脚本)在Redis中创建一个唯一的锁。这个锁是通过一个特定的键来表示的,通常与一个随机的值或者线程标识符结合,以保证锁的唯一性。
- 获取锁成功:如果锁键不存在,这意味着没有其他客户端持有锁,当前客户端会成功设置锁键,并继续执行其业务逻辑。
- 获取锁失败:如果锁键已存在,表明锁已被另一个客户端持有。在这种情况下,客户端可能会进入等待,定期重试获取锁。
- 锁续期(Watch Dog):一旦客户端成功获取锁,Redisson内部的“看门狗”服务会启动。这个服务会定时(如每隔10秒)检查锁的持有时间,并在锁即将到期时自动对其进行续期,确保客户端在执行业务逻辑的过程中锁不会失效。
- 业务逻辑执行:客户端执行它的业务逻辑。在此期间,如果业务逻辑执行的时间超过了锁的初始过期时间,由于“看门狗”服务的存在,锁仍然会被保持。
- 释放锁:完成业务逻辑后,客户端会向Redis发送命令以释放锁。这通常是通过删除Redis中对应的键来实现的
实现代码
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.5</version>
</dependency>
# application.yml
spring:
redis:
host: your_redis_host
port: 6379
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean(destroyMethod="shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort)
.setPassword(redisPassword)
.setDatabase(0);
return Redisson.create(config);
}
}
@Service
public class DistributedLockService {
@Autowired
private RedissonClient redissonClient;
public void performTaskWithLock() {
RLock lock = redissonClient.getLock("myLock");
try {
if (lock.tryLock(100, 10, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
} finally {
lock.unlock();
}
} else {
// 无法获取锁
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Zookeeper实现分布式锁
Zookeeper的结构
Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。
Znode分为四种类型:
1.持久节点 (PERSISTENT)
默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。
2.持久节点顺序节点(PERSISTENT_SEQUENTIAL)
所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:
3.临时节点(EPHEMERAL)
和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:
4.临时顺序节点(EPHEMERAL_SEQUENTIAL)
顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。
原理:
获取锁
首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。
于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。
这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。
于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。
这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的
释放锁
释放锁分为两种情况:
1.任务完成,客户端显示释放
当任务完成时,Client1会显示调用删除节点Lock1的指令。
2.任务执行过程中,客户端崩溃
获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。
同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。
最终,Client3成功得到了锁。
实现代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>RedLock</groupId>
<artifactId>RedLock</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<zookeeper.version>3.8.1</zookeeper.version>
<slf4j.version>1.7.36</slf4j.version>
<junit.version>5.9.2</junit.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!-- ZooKeeper Client -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>${zookeeper.version}</version>
<exclusions>
<!-- 排除冲突的日志依赖 -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- 使用兼容的Logback版本 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class ZooKeeperLock implements Watcher {
private final ZooKeeper zk;
private final String lockPath;
private String currentLockNode;
private String watchedNode;
private CountDownLatch latch;
public ZooKeeperLock(ZooKeeper zk, String lockPath) {
this.zk = zk;
this.lockPath = lockPath;
ensureLockRootExists();
}
// 确保父节点存在
private void ensureLockRootExists() {
try {
Stat stat = zk.exists(lockPath, false);
if (stat == null) {
zk.create(lockPath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (KeeperException | InterruptedException e) {
// 处理异常,实际生产环境需要更完善的错误处理
throw new RuntimeException("Failed to initialize lock root", e);
}
}
// 获取锁
public void lock() throws InterruptedException {
try {
// 创建临时顺序节点
currentLockNode = zk.create(lockPath + "/lock_", new byte[0],
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 循环尝试获取锁
while (!tryAcquireLock()) {
// 等待前一个节点释放
synchronized (this) {
wait();
}
}
} catch (KeeperException e) {
throw new RuntimeException("Failed to acquire lock", e);
}
}
// 尝试获取锁
private boolean tryAcquireLock() throws KeeperException, InterruptedException {
List<String> children = zk.getChildren(lockPath, false);
Collections.sort(children); // 按自然顺序排序
String currentNode = currentLockNode.substring(lockPath.length() + 1);
int currentIndex = children.indexOf(currentNode);
if (currentIndex == 0) {
// 当前是第一个节点,获得锁
return true;
} else {
// 监听前一个节点
String prevNode = children.get(currentIndex - 1);
watchedNode = lockPath + "/" + prevNode;
// 检查前节点是否存在,并设置Watcher
Stat stat = zk.exists(watchedNode, this);
if (stat == null) {
// 前节点已不存在,重新尝试
return tryAcquireLock();
}
return false;
}
}
// Watcher回调
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted
&& event.getPath().equals(watchedNode)) {
// 前节点被删除,唤醒等待线程
synchronized (this) {
notifyAll();
}
}
}
// 释放锁
public void unlock() {
try {
if (currentLockNode != null) {
zk.delete(currentLockNode, -1);
currentLockNode = null;
}
} catch (InterruptedException | KeeperException e) {
throw new RuntimeException("Failed to release lock", e);
}
}
// 关闭ZooKeeper连接(通常由外部管理)
public void close() throws InterruptedException {
zk.close();
}
}