这个方案也有问题,因为获取锁和设置过期时间分成两步了,不是原子性操作,有可能获取锁成功但设置时间失败,那样不就白干了吗。
不过也不用急,这种事情 Redis 官方早为我们考虑到了,所以就引出了下面这个命令
2、SETEX,用法SETEX key seconds value
将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。如果 key 已经存在,SETEX 命令将覆写旧值。
这个命令类似于以下两个命令:
SET key value
EXPIRE key seconds # 设置生存时间
这两步动作是原子性的,会在同一时间完成。
3、PSETEX ,用法PSETEX key milliseconds value
这个命令和SETEX命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像SETEX命令那样,以秒为单位。
不过,从 Redis 2.6.12 版本开始,SET命令可以通过参数来实现和SETNX、SETEX、PSETEX 三个命令的效果。
就比如这条命令
SET key value NX EX seconds
加上 NX、EX 参数后,效果就相当于 SETEX,这也是 Redis 获取锁写法里面最常见的。
怎么释放锁
=====
释放锁的命令就简单了,直接删除 key 就行,但我们前面说了,因为分布式锁必须由锁的持有者自己释放,所以我们必须先确保当前释放锁的线程是持有者,没问题了再删除,这样一来,就变成两个步骤了,似乎又违背了原子性了,怎么办呢?
不慌,我们可以用 lua 脚本把两步操作做拼装,就好像这样:
if Redis.call(“get”,KEYS[1]) == ARGV[1]
then
return Redis.call(“del”,KEYS[1])
else
return 0
end
KEYS[1]是当前 key 的名称,ARGV[1]可以是当前线程的 ID(或者其他不固定的值,能识别所属线程即可),这样就可以防止持有过期锁的线程,或者其他线程误删现有锁的情况出现。
代码实现
====
知道了原理后,我们就可以手写代码来实现 Redis 分布式锁的功能了,因为本文的目的主要是为了讲解原理,不是为了教大家怎么写分布式锁,所以我就用伪代码实现了。
首先是 Redis 锁的工具类,包含了加锁和解锁的基础方法:
public class RedisLockUtil {
private String LOCK_KEY = “redis_lock”;
// key的持有时间,5ms
private long EXPIRE_TIME = 5;
// 等待超时时间,1s
private long TIME_OUT = 1000;
// Redis命令参数,相当于nx和px的命令合集
private SetParams params = SetParams.setParams().nx().px(EXPIRE_TIME);
// Redis连接池,连的是本地的Redis客户端
JedisPool jedisPool = new JedisPool(“127.0.0.1”, 6379);
/**
* 加锁
* @param id
* 线程的id,或者其他可识别当前线程且不重复的字段
* @return
*/
public boolean lock(String id) {
Long start = System.currentTimeMillis();
Jedis jedis = jedisPool.getResource();
try {
for (;😉 {
// SET命令返回OK ,则证明获取锁成功
String lock = jedis.set(LOCK_KEY, id, params);
if (“OK”.equals(lock)) {
return true;
}
// 否则循环等待,在TIME_OUT时间内仍未获取到锁,则获取失败
long l = System.currentTimeMillis() - start;
if (l >= TIME_OUT) {
return false;
}
try {
// 休眠一会,不然反复执行循环会一直失败
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
jedis.close();
}
}
/**
* 解锁
* @param id
* 线程的id,或者其他可识别当前线程且不重复的字段
* @return
*/
public boolean unlock(String id) {
Jedis jedis = jedisPool.getResource();
// 删除key的lua脚本
String script = “if Redis.call(‘get’,KEYS[1]) == ARGV[1] then” + " return Redis.call(‘del’,KEYS[1]) " + “else”
+ " return 0 " + “end”;
try {
String result =
jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString();
return “1”.equals(result);
} finally {
jedis.close();
}
}
}
具体的代码作用注释已经写得很清楚了,然后我们就可以写一个 demo 类来测试一下效果:
public class RedisLockTest {
private static RedisLockUtil demo = new RedisLockUtil();
private static Integer NUM = 101;
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
String id = Thread.currentThread().getId() + “”;
boolean isLock = demo.lock(id);
try {
// 拿到锁的话,就对共享参数减一
if (isLock) {
NUM–;
System.out.println(NUM);
}
} finally {
// 释放锁一定要注意放在finally
demo.unlock(id);
}
}).start();
}
}
}
我们创建 100 个线程来模拟并发的情况,执行后的结果是这样的:
最后
给大家送一个小福利
附高清脑图,高清知识点讲解教程,以及一些面试真题及答案解析。送给需要的提升技术、准备面试跳槽、自身职业规划迷茫的朋友们。
}
我们创建 100 个线程来模拟并发的情况,执行后的结果是这样的:
最后
给大家送一个小福利
[外链图片转存中…(img-4RBwfxMX-1714800987379)]
附高清脑图,高清知识点讲解教程,以及一些面试真题及答案解析。送给需要的提升技术、准备面试跳槽、自身职业规划迷茫的朋友们。
[外链图片转存中…(img-nwBHjzcz-1714800987380)]