大家好,我是余数,这两天温习了下分布式锁,然后就顺便整理了这篇文章出来。文末附有源码链接,需要的朋友可以自取。
至于什么是分布式锁,这里不做赘述,不了解的可以自行去查阅资料。
文章目录
- 实现要点
- 项目结构
- Parent Maven依赖
- 锁的定义
- 锁的使用
- 源码地址
- 参考资料
- 如何用Redis实现分布式锁
实现要点
1. 使用 Redis 的 Setnx(SET if Not Exists) 命令加锁。
即锁不存在的时候才能加锁成功。如果锁存在了,说明其他服务已经持有该锁了,所以加锁失败。
2. 需要设置过期时间。
防止持有锁的服务意外挂掉后无法释放锁,导致其他服务永远都获取不到锁。
3. 锁的名字是固定的,但是锁的值需要保证线程唯一。
防止误删其他服务(线程)持有的锁。比如 线程A
获取到锁后被挂起了,等到锁自动过期后 线程B
又获得了锁,然后 线程B
开始执行自己的业务逻辑。
这个时候如果 线程A
被唤醒后并执行完所有的业务逻辑需要释放锁了,但这个时锁的持有者其实是 线程B
,如果只根据 key
去释放锁的话,那么 线程A
就错误的把 线程B
持有的锁给释放掉了。
所以我们需要让 线程A
释放锁的时候,先判断一下锁是不是自己持有的,是才能释放。
判断锁是不是自己持有的就是通过加锁时给锁设置的 value
来确定的。
4. 使用 Lua 脚本保证 “判断是否是自己持有的锁” 和 “释放锁” 的原子性。
因为判断和释放是两个命令,如果不保证原子性,会出现这种情况:刚判断完这个锁是自己的,然后这个锁就过期且被其他服务获取到了,你再释放岂不是把其他服务的锁给释放掉了。
5. 动态刷新锁的过期时间。
如果业务逻辑比较耗时,还没执行完锁就过期了怎么办。因为过期时间是在加锁的时候设置的,根本没有办法准确的预估到业务究竟需要多长时间。所以我们需要在业务逻辑没执行完的时候动态给锁续期,也就是更新锁的过期时间。
项目结构
- FileService: 模拟共享资源操作,以及分布式锁的实现。
- ServiceA:模拟分布式服务,读写共享资源。
- ServiceB:模拟分布式服务,读写共享资源。
- CountFile:共享资源,
服务A
和服务B
会读写该文件中的内容。
Parent Maven依赖
基于SpringBoot 3.0.6。
<?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>demo.iyushu</groupId>
<artifactId>redis-lock</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>ServiceA</module>
<module>ServiceB</module>
<module>FileService</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.6</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>19</maven.compiler.source>
<maven.compiler.target>19</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
锁的定义
锁的定义是在 FileService
模块实现的,同时该模块还实现了文件读写操作,结构如下:
- FileService:读写文件服务。
- Lock:定义一个锁。
public class Lock{
private String key;
private String value;
// unit is second
private int timeout;
// 看门狗watchDog,用于给锁续期。
private LockService.WatchDog watchDog;
}
- LockService:加锁和释放锁。
加锁:加锁时需要指定加锁的 key
,value
,和超时时间timeout
。等待20s
没有获取到锁则认为加锁失败。
加锁成功则返回锁,加锁失败返回空。
public Lock lock(String key, String value, int timeout){
boolean success = false;
long start = System.currentTimeMillis();
// 加锁等待时间 20s
while(!success && System.currentTimeMillis() - start <= 20000){
success = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
}
if(success){
Lock lock = new Lock(key, value, timeout, new WatchDog());
lock.getWatchDog().start();
return lock;
}
return null;
}
释放锁:先判断是自己的锁,然后将锁删掉,需要使用Lua
脚本确认这两步的原子性。同时停止给锁的续期。
public void unlock(Lock lock){
redisTemplate.execute(unlockScript, Arrays.asList(lock.getKey()), lock.getValue());
lock.getWatchDog().stop();
}
Lua 脚本
--- Lua脚本语言,释放锁 比较value是否相等,避免删除别人占有的锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
动态给锁续期:加锁成功后会新启一个线程,每隔1s
给锁续期一次,直到业务逻辑执行完并释放锁后,续期线程中断。
public class WatchDog implements Runnable {
private Lock lock;
public void setLock(Lock lock) {
this.lock = lock;
}
private Thread thread;
public void start(){
thread = new Thread(this);
thread.start();
}
public void stop(){
thread.interrupt();
}
@Override
public void run() {
long start = System.currentTimeMillis();
while(!thread.isInterrupted()){
long current = System.currentTimeMillis();
if(current - start >= 1000){
System.out.println("续期。。。");
redisTemplate.opsForValue().getAndExpire(lock.getKey(), lock.getTimeout(), TimeUnit.SECONDS);
start = current;
}
}
}
}
锁的使用
ServiceA
和 ServieB
主要模拟了实际业务中的多个服务,执行有资源共享的业务逻辑前先获取锁,获取成功才继续执行,执行完成后释放锁。
注意加锁的时候,value
值需要是唯一的,这里使用了服务名 + 线程名的方式。
public int getCount() throws InterruptedException {
Lock lock = null;
try{
// 获取锁
lock = lockService.lock("lock", "服务A" + Thread.currentThread().getName(), 3);
if(lock == null){
throw new RuntimeException("lock failed");
}
int count = fileService.getCount();
System.out.println("服务A获取到的计数为" + count);
// 模拟业务逻辑
Thread.sleep(2000);
fileService.setCount(count + 1);
return count;
}finally {
// 释放锁
if(lock != null){
lockService.unlock(lock);
}
}
}
我们让 服务A
和 服务B
同时各执行10
次读写文件的操作,每次写的时候将文件中的数字加一,看看结果如何。
服务A运行结果:
服务B运行结果:
可以看到加锁成功,计数是正常的。两个服务几乎是交替执行的,即一个服务执行完成后,另一个服务才能获取到锁并执行业务逻辑。
那试试不加锁会怎样呢?对比一下看看,将 服务A
和 服务B
中加锁的逻辑注释掉。
毫无悬念的,服务B
把 服务A
的计数完全覆盖了。
源码地址
以下是源码链接,仅用于理解Redis分布式锁的实现原理,代码还有很多不足,切勿用于生产环境哟,欢迎多多交流,嘿嘿~
https://github.com/justyuze/share-redis-lock/tree/main
参考资料
如何用Redis实现分布式锁
https://blog.csdn.net/fuzhongmin05/article/details/119251590