什么是分布式锁
满足分布式系统或集群模式下多进程可见并且互斥的锁
获取锁
互斥:确保只能有一个线程获取锁,可以利用setnx
的互斥特性
非阻塞:尝试一次,成功返回true,失败返回false
释放锁
手动释放,DEL key
超时释放:获取锁时添加一个超时时间,避免服务宕机引起的死锁,EXPIRE lock 10
组合
SET lock thread1 NX EX 10 # NX是互斥、EX是设置超时时间
package cn.sticki.common.redis.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @author 阿杆
* @version 1.0
* @date 2022/6/21 21:57
*/
public class RedisSimpleLock implements ILock {
private final StringRedisTemplate stringRedisTemplate;
private final String name;
private final static String KEY_PREFIX = "lock:";
public RedisSimpleLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeout) {
// 1. 获取线程标识
long threadId = Thread.currentThread().getId();
// 2. 尝试写入redis
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeout, TimeUnit.SECONDS);
// 3. 返回写入情况,成功即获取到锁
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 1. 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
存在一种情况,按照以下顺序执行:
示意图:
这个问题,实际上就是释放了不是自己产生的锁,故我们可以通过特定的标识,在释放锁之前判断锁是否是由自己产生的,且只释放自己产生的锁。
可以将线程id存入value,在释放之前判断锁的value是否等于自己的线程id,若等于则说明该锁是当前线程产生的,可以释放。
如果我有多个服务器,组成了一个集群,那么不同的服务器有可能出现线程id相同的情况,就会导致value相同,从而错误的释放了别人的锁。
让每个启动的服务都有一个不同的标识,再拼接线程id,就可以解决这个问题。
在获取锁时存入线程标识(可以用UUID + 线程id 表示)
在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致
如果一致则释放锁
如果不一致则不释放锁
package cn.sticki.common.redis.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @author 阿杆
* @version 1.0
* @date 2022/6/21 21:57
*/
public class RedisSimpleLock implements ILock {
private final StringRedisTemplate stringRedisTemplate;
private final String name;
private final static String KEY_PREFIX = "lock:";
private final static String KEY_UUID = UUID.randomUUID() + ":";
public RedisSimpleLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeout) {
// 1. 生成key,通过拼接前缀和业务名
String key = KEY_PREFIX + name;
// 2. 生成value,用于判断该锁是不是当前线程生成的。使用随机的UUID+当前线程id,防止集群时value碰撞。
String value = KEY_UUID + Thread.currentThread().getId();
// 3. 尝试写入redis
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
// 4. 返回写入情况,成功即获取到锁
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
String key = KEY_PREFIX + name;
String value = KEY_UUID + Thread.currentThread().getId();
// 1. 获取锁的值
String lockValue = stringRedisTemplate.opsForValue().get(key);
if (value.equals(lockValue)) {
// 2. 若值相同,则当前锁是由当前线程创建的,可以删除
stringRedisTemplate.delete(key);
}
}
}
如果,我是说如果,在上面的unlock()代码中,获取到锁的值之后,删除key之前,发生了阻塞(GC阻塞),等阻塞完成后,当前线程创建的锁已经被释放了,然后发生了和上面类似的问题,也一样会导致锁的失效。
可能描述的不太清楚,看看示意图吧:
产生这个问题的原因主要在于判断锁标识和释放锁是分别执行的两个操作,解决这个问题,可以通过Lua脚本将两个操作绑定在一起。
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法可以参考网站:https://www.runoob.com/lua/lua-tutorial.html
这里重点介绍Redis提供的调用函数,语法如下:
# 执行redis命令
redis.call('命令名称', 'key', '其它参数', ...)
例如,我们要执行set name jack,则脚本是这样:
# 执行 set name jack
redis.call('set', 'name', 'jack')
例如,我们要先执行set name Rose,再执行get name,则脚本如下:
# 先执行 set name jack
redis.call('set', 'name', 'jack')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name
释放锁的业务流程是这样的:
unlock.lua(这个文件放在mian/resource下面)
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
package cn.sticki.common.redis.utils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @author 阿杆
* @version 1.0
* @date 2022/6/21 21:57
*/
public class RedisSimpleLock implements ILock {
private final static String KEY_PREFIX = "lock:";
private final static String KEY_UUID = UUID.randomUUID() + ":";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
// 加载lua文件
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
private final StringRedisTemplate stringRedisTemplate;
private final String name;
public RedisSimpleLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeout) {
// 1. 生成key,通过拼接前缀和业务名
String key = KEY_PREFIX + name;
// 2. 生成value,用于判断该锁是不是当前线程生成的。使用随机的UUID+当前线程id,防止集群时value碰撞。
String value = KEY_UUID + Thread.currentThread().getId();
// 3. 尝试写入redis
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
// 4. 返回写入情况,成功即获取到锁
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
String key = KEY_PREFIX + name;
String value = KEY_UUID + Thread.currentThread().getId();
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(key),
value);
}
}
实现思路:
可改进点:
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
官网地址: https://redisson.org
GitHub地址: https://github.com/redisson/redisson
这篇文章我是学习自黑马程序员的视频课程的时候做的总结和笔记,有兴趣的同学可以自行观看视频:https://www.bilibili.com/video/BV1cr4y1671t?p=56