文章目录
一、前言
如果服务端程序分布式部署,有两个副本,类似kubernetes的一个Deployment下面两个Pod,有一个task任务,只需要一个Pod去执行就好了。此时,开启一个redis,这个redis不用存储数据,就用来给两个Pod setnx 争抢。
方式1:setnx(“lock”, “1”) + expire(“lock”, 30) 实现,达到超时时间自动解锁,进程/线程/副本重新竞争;
方式2:setnx(“lock”, “1”) + del(“lock”) 实现,执行完逻辑之后手动解锁,进程/线程/副本重新竞争。
相同点:加锁方式相同,都是利用了 setnx 命令只能执行一次的原理,从而创造出一个互斥的条件。
二、Redis分布式锁原理
Redis分布式锁
思考:分布锁满足两个条件,一个是加有效时间的锁,一个是高性能解锁
注意1:解锁流程不能遗漏,否则导致任务执行一次就永不过期
注意2:将加锁代码和任务逻辑放在try,catch代码块,将解锁流程放在finally(保证解锁逻辑一定执行到)
采用redis命令setnx(set if not exist)、setex(set expire value)实现,如下图:
三、不使用lua脚本
3.1 setnx + 超时时间
setnx + 超时时间,代码执行如下:
单独提取到一个类 Distribution1 里面,如下:
public class Distribution1 {
public static void main(String[] args) throws Exception{
for (int i = 0; i < 2; i++) {
new Thread() {
public void run() {
try {
long exit = JedisUtil.getJedisUtil().setnx("lock", "1");
if (exit == 0) {
System.out.println("被锁住了,不能执行");
} else {
//为什么要给锁添加一个过期时间(这里设置过期时间30秒)?
JedisUtil.getJedisUtil().expire("lock", 30);
System.out.println("处理业务中");
Thread.sleep(3000); // 模拟业务处理3秒钟
}
} catch (Exception ex) {
}
}
;
}.start();
Thread.sleep(1000);
}
}
}
3.2 setnx + del
public class Distribution2 {
public static void main(String[] args) throws Exception{
for (int i = 0; i < 2; i++) {
new Thread() {
public void run() {
try {
long exit = JedisUtil.getJedisUtil().setnx("lock2", "1");
if (exit == 0) {
System.out.println("被锁住了,不能执行");
} else {
System.out.println("处理业务中");
Thread.sleep(3000); // 模拟业务处理3秒钟
// 处理完业务逻辑删除锁
JedisUtil.getJedisUtil().del("lock2");
}
} catch (Exception ex) {
}
};
}.start();
Thread.sleep(4000);
}
}
}
四、使用lua脚本
4.1 lua脚本引入
分布式锁setnx、setex的缺陷,在setnx和setex中间发生了服务down机
-
从Redis宕机讲解分布式锁执行的异常场景流程
-
从Server服务宕机讲解分布式锁执行的异常场景流程
-
在setnx和setex中间发生了服务down机 那么key将没有超时时间 会一直存在,新的请求永远进不来
解决方案:由于setnx与setex是分步进行,那么我们将两步合成一步,放在同一个原子中即可采用Lua脚本
4.2 lua脚本实现分布式锁(setnx + 超时时间)
lua脚本实现分布式锁(setnx + 超时时间),代码执行如下:
单独提取到一个类 Distribution3 里面,如下:
public class Distribution3 {
private static String script = "" +
"local lockSet = redis.call('setnx', KEYS[1], ARGV[1])\n" +
"if lockSet == 1 then\n" +
" redis.call('expire', KEYS[1], ARGV[2])\n" +
"end\n" +
"return lockSet";
public static void main(String[] args) throws Exception {
// region 分布式锁Lua脚本实现 (新建两个线程并运行,两个线程模拟两个服务端副本Pod)
// 第一个线程 setnx 成功
// 第二个线程 是隔断了 1s 去执行 setnx 命令,此时第一次 setnx 过期时间30s 还没有结束,所以第二个线程 setnx 返回为0
for (int i = 0; i < 2; i++) {
new Thread() {
public void run() {
try {
//lua脚本分布式锁
Object result = JedisUtil.getJedisUtil().eval(script, Arrays.asList("lualock"), Arrays.asList("huihui", "30"));
System.out.println("打印:" + result.toString());
} catch (Exception ex) {
ex.printStackTrace();
}
}
;
}.start();
Thread.sleep(1000);
}
//endregion
}
}
4.3 lua脚本实现分布式锁(setnx + del)
public class Distribution4 {
private static String script = "" +
"local lockSet = redis.call('setnx', KEYS[1], ARGV[1])\n" +
"if lockSet == 1 then\n" +
" redis.call('del', KEYS[1])\n" +
"end\n" +
"return lockSet";
public static void main(String[] args) throws Exception {
// region 分布式锁Lua脚本实现 (新建两个线程并运行,两个线程模拟两个服务端副本Pod)
// 第一个线程 setnx 成功
// 第二个线程 是隔断了 1s 去执行 setnx 命令,此时第一次 setnx 过期时间30s 还没有结束,所以第二个线程 setnx 返回为0
for (int i = 0; i < 2; i++) {
new Thread() {
public void run() {
try {
//lua脚本分布式锁
Object result = JedisUtil.getJedisUtil().eval(script, Arrays.asList("lualock2"), Arrays.asList("huihui"));
System.out.println("打印:" + result.toString());
} catch (Exception ex) {
ex.printStackTrace();
}
}
;
}.start();
Thread.sleep(1000);
}
//endregion
}
}
五、尾声
实现分布式锁的两种方式(两个线程模拟两个副本Pod)
1、单单redis命令:setnx + expire/del 【lock/lock2】
2、lua脚本实现: 将 setnx + expire/del 放到lua脚本中,然后去执行lua脚本 【lualock/lualock2】
方式1:单独的setnx 和 expire 的命令确实是原子的,但是无法保证两个组合起来还是原子的,无法保证执行 setnx 和 expire 的是同一个线程;
方式2:整个lua脚本是原子的,单独的setnx 和 expire 的命令确实是原子的,但是两个组合起来就不是原子的了,现在把他们放到同一个 lua 脚本里面,setnx + expire 就变成了原子的,保证执行 setnx 和 expire 的是同一个线程。
Distribution1: 不使用lua脚本(setnx + 超时时间)
Distribution2: 不使用lua脚本(setnx + del)
Distribution3: 使用lua脚本实现分布式锁(setnx + 超时时间)
Distribution4: 使用lua脚本实现分布式锁(setnx + del)
Redis实现分布式锁,完成了。