「黑马点评」四、分布式锁
分布式锁
产生背景:各自单机看各自的锁监视器却无法共享锁

image.png|500|500
不用 JVM 内部的锁,使用一个,满足分布式系统或集群模式下多进程可见且互斥的锁,即分布式锁

image.png|500|500
分布式锁:多进程可见、互斥、高可用、高性能、安全性......
常见实现方案

image.png|500|500
基于 Redis 的分布式锁
最基本的两个操作
- 获取锁
- 互斥:确保只有一个线程获取到锁
SETNX lock thread1
EXPIRE lock 10
- 抢锁和设置过期时间需要有原子性
SET lock thread1 EX 10 NX
- 对于获取锁操作,分为:
- 阻塞式:复杂且浪费处理机
- 非阻塞式:尝试一次,立即返回 true OR false
- 释放锁
- 手动释放
- 超时释放
DEL lock
业务示意图

image.png|500|500
基于 Redis 实现分布式锁初级版本
背景:根据 ILock 接口,利用 Redis 实现分布式锁
public interface ILock {
public boolean tryLock(long timeoutSec);
public void unlock();
}
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
@Override
public boolean tryLock(long timeoutSec) {
long threadId = Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success); // 防止拆箱得到空指针
}
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
示例
Long userId = UserHolder.getUser().getId();
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
boolean isLock = lock.tryLock(120);
if (!isLock) {
return Result.error("不允许重复下单");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
初级分布式锁超时误删问题
极端情况:某一线程业务处理时间过长,锁超时释放,结果等线程处理完毕,将别人获取到的锁删除

image.png|500|500
解决方法:检查锁业务标志,即 value,一致才有资格释放

image.png|500|500
流程示意图

image.png|500|500
但是由于不同 JVM 会有相同递增到 PID 序列,所以考虑用 UUID 表示线程标示
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success); // 防止拆箱得到空指针
}
@Override
public void unlock() {
String value = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
String threadId = ID_PREFIX + Thread.currentThread().getId();
if (threadId.equals(value)) { // value 可能为 null,所以用 threadId 来判断
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
初级分布式锁非原子性误删
线程安全问题:检查锁是否是自身的以及删除锁并不是原子性的,所以可能出现检查通过但是突然阻塞,另一个线程抢夺锁成功后被原先的线程删除锁,导致误删锁现象再次出现

image.png|500|500
原子性方案考虑用 Redis 配合乐观锁,但是太过于复杂
此处选择 Lua 脚本确保原子性 > Redis 提供了 Lua 脚本功能,可以在一个脚本中编写多条 Redis 命令,确保多条命令执行时的原子性,Lua 是一种编程语言,基本语法
Redis 在 Lua 中提供的调用函数
redis.call('命令名称', 'key', '其他参数', ...)
Redis 为 Lua 脚本提供的调用命令 > 末尾的 0 代表之后 key 类型参数个数
EVAl "return redis.call('set', 'name', 'jack')" 0
key 类型参数会放入 KEYS 数组,其他参数会放入 ARGV 数组,在脚本中可以从 KEYS 和 ARGV 中获取
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose
当前分布式锁对应的 Lua 脚本
-- 获取锁中的线程标识 get key
local id = redis.call('get' ,KEYS[1])
-- 比较线程标识与锁中标识是否一致
if(id == ARGV[1]) then
-- 释放锁 del key
return redis.call('del',KEYS[1])
end
return 0
利用 Lua 优化原子性后的基于 Redis 的分布式锁
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success); // 防止拆箱得到空指针
}
@Override
public void unlock() {
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId()
);
}
}