1. 实现一个基础的分布式锁
1.1 定义Lock接口
public interface Lock {
boolean tryLock(long timeout);
boolean unLock();
1.2 实现接口
public class LockImpl implements Lock{
private static final String defaultNamePre = "sanjin:lock:";
private String lockName;
@Autowired
private StringRedisTemplate redisTemplate;
// 因为LockImpl不是bean不会注入redistemplate, 需要手动传
public LockImpl(String name, StringRedisTemplate stringRedisTemplate) {
this.lockName = name;
this.redisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeout) {
String key = defaultNamePre + lockName;
Boolean b = redisTemplate.opsForValue().setIfAbsent(key, "1", timeout, TimeUnit.SECONDS);
return b;
}
@Override
public boolean unLock() {
String key = defaultNamePre + lockName;
Boolean b = redisTemplate.delete(key);
return b;
}
}
1.3 测试
@SpringBootTest
public class TestA {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void test1() {
CountDownLatch countDownLatch = new CountDownLatch(2);
// 每次测试都先把锁删一下
stringRedisTemplate.delete("sanjin");
// 线程A
new Thread(() -> {
LockImpl lock = new LockImpl("sanjin", stringRedisTemplate);
boolean b = lock.tryLock(120);
while (!b) {
System.out.println("A线程再次尝试获取锁");
b = lock.tryLock(120);
try {Thread.sleep(1000);} catch (Exception e) {System.out.println(e.getMessage());}
}
System.out.println("A线程获取到锁");
System.out.println("A线程开始执行任务...");
// 模拟执行任务
try {Thread.sleep(3000);} catch (Exception e) {System.out.println(e.getMessage());}
System.out.println("A线程任务执行完毕");
boolean b1 = lock.unLock();
if (b1) System.out.println("A线程成功释放锁");
countDownLatch.countDown();
}, "threadA").start();
try {Thread.sleep(1000);} catch (Exception e) {System.out.println(e.getMessage());}
// 线程B
new Thread(() -> {
LockImpl lock = new LockImpl("sanjin", stringRedisTemplate);
boolean b = lock.tryLock(120);
while (!b) {
System.out.println("B线程再次尝试获取锁");
b = lock.tryLock(120);
try {Thread.sleep(1000);} catch (Exception e) {System.out.println(e.getMessage());}
}
System.out.println("B线程获取到锁");
System.out.println("B线程开始执行任务...");
// 模拟执行任务
try {Thread.sleep(3000);} catch (Exception e) {System.out.println(e.getMessage());}
System.out.println("B线程任务执行完毕");
boolean b1 = lock.unLock();
if (b1) System.out.println("B线程成功释放锁");
countDownLatch.countDown();
}, "threadB").start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
-------------------------------------结果----------------------------------------------------
A线程获取到锁
A线程开始执行任务...
B线程再次尝试获取锁
B线程再次尝试获取锁
B线程再次尝试获取锁
A线程任务执行完毕
A线程成功释放锁
B线程再次尝试获取锁
B线程获取到锁
B线程开始执行任务...
B线程任务执行完毕
B线程成功释放锁
2. 问题1:误删其他线程的锁
问题:如果线程A的锁超时释放了,但是A还没有删除锁,B获取到了同一个name的锁,A在删除的时候会删除掉B的锁
解决:添加线程标识,每次删锁前判断一下是不是自己的线程所获取到的锁
@Override
public boolean tryLock(long timeout) {
String key = defaultNamePre + lockName;
String value = String.valueOf(Thread.currentThread().getId());
// 这个加锁和设置过期是原子性的
Boolean b = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
return b != null ? b : false;
}
@Override
public boolean unLock() {
String key = defaultNamePre + lockName;
String value = redisTemplate.opsForValue().get(key);
String trueValue = String.valueOf(Thread.currentThread().getId());
Boolean b = null;
if (trueValue.equals(value)) {
b = redisTemplate.delete(key);
}
return b != null ? b : false;
}
3. 问题2:删锁不是原子性的

解决方案:使用lua脚本比锁、删锁
JDK17版本
public class LockImpl implements Lock{
private static final String defaultNamePre = "sanjin:lock:";
private String lockName;
@Autowired
private StringRedisTemplate redisTemplate;
// 因为LockImpl不是bean不会注入redistemplate, 需要手动传
public LockImpl(String name, StringRedisTemplate stringRedisTemplate) {
this.lockName = name;
this.redisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeout) {
String key = defaultNamePre + lockName;
String value = String.valueOf(Thread.currentThread().getId());
Boolean res = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
return res != null && res;
}
@Override
public boolean unLock() {
String key = defaultNamePre + lockName;
String trueValue = String.valueOf(Thread.currentThread().getId());
// 文本块要求jdk大于15
String luaScript = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0;
end
""";
// List.of() 要求大于jdk9, RedisScript.of也是new了一个DefaultRedisScript
Long res = redisTemplate.execute(RedisScript.of(luaScript, Long.class), List.of(key), trueValue);
return res != null && res > 0;
}
}
JDK8版本
public class LockImpl implements Lock{
private static final String defaultNamePre = "sanjin:lock:";
private String lockName;
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Autowired
private StringRedisTemplate redisTemplate;
// 因为LockImpl不是bean不会注入redistemplate, 需要手动传
public LockImpl(String name, StringRedisTemplate stringRedisTemplate) {
this.lockName = name;
this.redisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeout) {
String key = defaultNamePre + lockName;
String value = String.valueOf(Thread.currentThread().getId());
Boolean res = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
return res != null && res;
}
@Override
public boolean unLock() {
String key = defaultNamePre + lockName;
String trueValue = String.valueOf(Thread.currentThread().getId());
Long res = redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(key), trueValue);
return res != null && res > 0;
}
}
lua脚本的位置

测试结果
A线程获取到锁
A线程开始执行任务...
B线程再次尝试获取锁
B线程再次尝试获取锁
A线程任务执行完毕
B线程再次尝试获取锁
A线程成功释放锁
B线程再次尝试获取锁
B线程获取到锁
B线程开始执行任务...
B线程任务执行完毕
B线程成功释放锁
4. 其他问题
问题
- 过期时间是固定的,线程如果在过期时间内未完成任务,锁会被自动释放
- 如果 Redis 节点宕机,加锁和解锁功能将失效
- 不支持公平锁,线程之间的锁竞争是随机的
- 不支持可重入锁,如果同一线程再次尝试获取锁会失败,可能引发死锁
- 一旦需求复杂化,需要投入更多开发时间完善功能
- ....
解决:利用Redisson,跳转链接
5. 实现可重入分布式锁
public class DistributedLock {
private static final String DEFAULT_PRE = "Jesus:";
private String name;
private StringRedisTemplate template;
private int timeout;
public DistributedLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = DEFAULT_PRE + name;
this.template = stringRedisTemplate;
}
public boolean tryLock(int timeOut) {
this.timeout = timeOut;
Long threadId = Thread.currentThread().getId();
String script = """
local key = KEYS[1]
local file1 = 'filed1'
local value1 = ARGV[1]
local file2 = 'count'
local value2 = 1
local timeout = ARGV[2]
-- 1.锁不存在
if redis.call('exists', key) == 0 then
redis.call('hset', key, file1, value1, file2, value2)
redis.call('expire', key, timeout)
return nil
end
-- 2.锁存在并且是自己的锁
if redis.call('hget', key, file1) == value1 then
value2 = tonumber(redis.call('hget', key, file2)) + 1
redis.call('hset', key, file2, value2);
redis.call('expire', key, timeout)
return nil
end
return 0
""";
Object execute = template.execute(RedisScript.of(script, Long.class),
List.of(name),
String.valueOf(threadId), String.valueOf(timeOut));
if (execute == null) {
return true;
}
return false;
}
public boolean unLock() {
Long threadId = Thread.currentThread().getId();
String script = """
local key = KEYS[1]
local file1 = 'filed1'
local value1 = ARGV[1]
local file2 = 'count'
local value2 = 1
local timeout = ARGV[2]
-- 1.锁不存在
if redis.call('exists', key) == 0 then
return nil
end
-- 2.锁存在并且是自己的锁
if redis.call('hget', key, file1) == value1 then
value2 = tonumber(redis.call('hget', key, file2)) - 1
if value2 > 0 then
redis.call('hset', key, file2, value2);
redis.call('expire', key, timeout)
return nil
end
redis.call('del', key)
return nil
end
""";
Object execute = template.execute(RedisScript.of(script, Long.class),
List.of(name),
String.valueOf(threadId), String.valueOf(timeout));
if (execute == null) {
return true;
}
return false;
}
}