文章目录
- Redis实现分布式锁
- 一、分布式锁实现对比
- 二、基于Redis实现
- 2.1 原理
- 2.2 细节
- 2.2.1 加锁
- 2.2.2 解锁
- 2.3 实现代码
- 2.4 测试
- 三、小结
Redis实现分布式锁
- 分布式锁应用场景
1.多任务环境
2.多任务对共享资源访问
3.共享资源访问是互斥的(比如读就不是互斥的,写是互斥的)
一、分布式锁实现对比
- 分布式锁常用的方案如下:
方案 | 实现思路 | 优点 | 缺点 |
基于mysql | 利用数据库的行锁机制 | 简单 | 性能差,容易死锁 |
基于redis | 基于redis的setnx命令,lua保证原子性 | 性能好 | 实现相对复杂 |
基于zk | 基于zk节点的原子特性和watch机制 | 性能好,稳定可靠,可较好的实现阻塞锁 | 实现较复杂 |
二、基于Redis实现
2.1 原理
- 使用setnx这个命令(不存在才设置),命令是原子的。设置值成功代表加锁成功,后面的线程设置就会失败。
- 使用lua脚本,脚本内的执行逻辑是原子性的,和一条命令一样,因此可以将多个命令组合成一个原子命令
- 另外用到了key的自动过期
2.2 细节
2.2.1 加锁
- 使用setnx向特定的key写入一个随机值并设置失效时间,成功代表加锁成功
必须设置失效时间,避免死锁。比如服务突然宕机,没有失效时间的话,节点永远不会被删除,那么其他线程都获取不到锁,死锁。
写入随机值,避免锁误删。比如失效时间是3S,业务一般处理只需要几十毫秒,某一次业务发生异常耗费了5S,再去解锁的时候,实际上此时自己设置的值已经过期删除了,此时的key是另一个线程加锁设置的,那么自己肯定不能把另一个线程加锁的值给删除,通过随机值value的匹配来避免。
写入值和设置失效时间必须是一个原子操作,保证加锁是原子的
set key value nx px 10000 (不能分几次命令操作,那样不是原子性的)
2.2.2 解锁
- 获取指定key数据,判断和自己加锁时的随机值是否一致,匹配一致就删除节点。保证获取数据,判断,删除这三个动作是原子的
//因为key是不变的,每个线程加锁的时候,设置的值不一样,因此值在不断变化,如果步骤1和2不是原子的,可能获取的时候是一致
//的,判断的时候已经更改了。使用lua脚本实现:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1]);
else
return 0;
end
2.3 实现代码
public class MyRedisLock {
//使用threadLocal来保存每个线程加锁的时候生成的随机数
private static ThreadLocal<String> local = new ThreadLocal<>();
private static final String KEY = "KEY";
//简单实现了阻塞锁,自旋直到获取锁成功
public static boolean lock() {
for (; ; ) {
if (tryLock()) {
return true;
}
}
}
public static boolean tryLock() {
String uuid = UUID.randomUUID().toString();
String ret = JedisFactory.getJedis().set(KEY, uuid, "NX", "PX", 3000);
if ("OK".equals(ret)) {
local.set(uuid);
return true;
}
return false;
}
public static void unLock() {
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1]);\n" +
"else\n" +
" return 0;\n" +
"end";
//从threadLocal中获取本线程加锁的时候设置的随机数
String value = local.get();
JedisFactory.getJedis().eval(script, Arrays.asList(KEY), Arrays.asList(value));
}
}
2.4 测试
- 测试类:
- 实际上应该使用5个进程来测试,因为分布式锁实际上是对于跨进程而言的,如果仅仅只有线程之间可以使用JDK中的锁,但是跨线程也可以理解为跨进程的
一种特殊情况,如果分布式锁生效,那么跨进程可以生效,那么夸线程也是肯定可以生效的,这个锁实际上是在远程"锁"住的
public class MyRedisLockTest {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
//创建5个执行线程
new MyThread("Thread-" + i).start();
}
}
static class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
MyRedisLock.lock();
try {
System.out.println("Thread " + Thread.currentThread().getName() +
" do something thing..." + new Date());
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
MyRedisLock.unLock();
}
}
}
}
- 输出(5个线程排队执行,锁生效了):
Thread Thread-0 do something thing...Sat Jun 15 23:14:52 CST 2019
Thread Thread-2 do something thing...Sat Jun 15 23:14:54 CST 2019
Thread Thread-1 do something thing...Sat Jun 15 23:14:56 CST 2019
Thread Thread-4 do something thing...Sat Jun 15 23:14:58 CST 2019
Thread Thread-3 do something thing...Sat Jun 15 23:15:00 CST 2019
三、小结
- 本文主要梳理了基于redis实现分布式锁,从原理到细节,到代码,测试。
- redis实现分布式锁,注意事项如下
操作 | 注意事项 |
加锁 | 必须设置失效时间避免死锁。 |
加锁 | 写入随机值,避免锁误删。 |
加锁 | 写入值和设置失效时间必须是一个原子操作,保证加锁是原子的 |
解锁 | 保证获取数据,判断,删除节点这三个动作是原子的 |