目录
1. 基本原理
分布式锁:满足分布式系统或集群模式下多进程课件并且互斥的锁
分布式锁的核心思想就是让大家使用同一把锁,这样就可实现一把锁锁住线程,这就是分布式的核心思路。(多个jvm共用一把锁)
分布式锁的特性:
- 可见性:多个线程都看到相同的结果
- 互斥性:互斥是分布式锁的基本特性,使程序串行执行
- 高性能性:由于加锁本身就让性能降低,因此对于分布式本身需要他有较高的加锁性能和释放锁的性能。
- 安全性:安全也是程序中不可缺少的。
2. redis实现
实现分布式锁使需要实现两个基本方法获取锁和释放锁
核心思路:
利用redis的setNx方法,当多个线程进入时,第一个线程就获取到锁,此时redis中存在相应的key,返回1,其他线程要想去申请锁时,此时由于reids中已经存在了key,此时返回0,申请锁失败,进入线程等待。实现了互斥性。
2.1 申请锁实现
利用setnx方法进行加锁,同时增加过期时间,防止死锁。
private static final String KEY_PREFIX="lock:"
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = Thread.currentThread().getId()
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
2.2 释放锁实现
就是简单的删除操作
public void unlock() {
//通过del删除锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
2.3 业务修改
public Result addSeckillVoucherOrder2(Long voucherId) {
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//判断秒杀活动是否开启
if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
return Result.fail("秒杀尚未开始");
}
//判断秒杀活动是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())){
return Result.fail("秒杀已经结束");
}
//查询库存不足
if (voucher.getStock()<1){
return Result.fail("库存不足");
}
//设置分布式锁,一人一订单。
Long userId = UserHolder.getUser().getId();
//在redis中设置互斥锁。
SimpleRedisLock simpleRedisLock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
//申请锁
boolean isLock = lock.tryLock();
if (!isLock){
return Result.fail("不允许重复下单");
}
//申请成功,进行下单业务
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.seckillVoucher(voucherId);
} catch (IllegalStateException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
2.4 reids分布式锁误删问题
2.4.1 逻辑说明
在持有锁的线程(线程1)在执行过程中出现阻塞,导致锁过期释放,此时又来了其他线程,此时锁时空闲的,(线程2)申请锁成功,等到线程1业务阻塞完成,释放锁,但此时释放的确实线程2的锁,这就是误删。
2.4.2 解决方案
在加锁时,填入当前线程的标识,释放锁时,判断是否是当前线程的锁,如果是就释放,否则不做处理
2.4.3 代码修改
申请锁
线程标识由线程id+uuid组成
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);
}
释放锁
判断线程标识是否相同,相同释放。
public void unlock() {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
2.5 分布式锁的原子问题
2.5.1 逻辑说明
线程1在持有锁后,在准备释放锁并已经通过判断线程标识后,此时恰好进入阻塞。等到锁过期,线程2此时申请锁成功,此时线程1阻塞完成,继续进行释放锁(已经通过标识判断)的逻辑,这是删除的就是线程2的锁。这就是因为此时分布式锁并不满足原子性。
2.5.2 解决方案
reids提供了Lua脚本功能,在一个脚本中编写多条reids命令,确保多条命令执行时的原子性。基本语法可以参考网站:Lua 教程 | 菜鸟教程 。
这里介绍redis提供的调用函数,语法如下:
redis.call('命令名称', 'key', '其它参数', ...)
释放锁的业务逻辑:
- 获取锁中的线程标识
- 判断是否与指定的标识是否一致
- 如果一致释放锁
- 不一致什么都不做
lua脚本:
unlock.lua:
-- KEYS[1]就是锁的key值,ARGV[1]就是当前线程的标识
-- 判断标识是否一致
if(redis.call('GET',KEYS[1])==ARGV[1]) then
-- 一致删除锁
return redis.call('DEL',KEYS[1])
end
return 0;
2.5.3 代码修改
利用java代码调用Lua脚本改造分布式锁
在redisTemplate中,可以利用execute方法去执行lua脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
//初始化
UNLOCK_SCRIPT = new DefaultRedisScript<>();
//lua脚本文件位置
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
//返回类型
UNLOCK_SCRIPT.setResultType(Long.class);
}
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,//lua脚本初始化
Collections.singletonList(KEY_PREFIX + name),//keys
ID_PREFIX + Thread.currentThread().getId());//argv
}
2.6 总结
基于redis实现分布式锁:
- 利用set nx ex 获取锁,并设置过期时间,保存线程标识
- 释放锁时先判断线程标识是否与自己一致,一致则删除锁
此时,基于redis实现分布式锁已经实现,但还有一些问题时redis锁无法解决的,锁不住问题、不可重入和无法重试获取问题。要解决这些问题就需要使用redission来解决。
3. 完整代码
SimpleRedisLock
public class SimpleRedisLock implements ILock{
private final StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock";
private final String name;
private static final String ID_PREFIX = UUID.randomUUID().toString()+"-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
//脚本初始化
UNLOCK_SCRIPT = new DefaultRedisScript<Long>();
//脚本文件位置
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unLock.lua"));
//返回类型
UNLOCK_SCRIPT.setResultType(Long.class);
}
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
//分布式锁的分类
this.name = name;
}
@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 threadId = ID_PREFIX + Thread.currentThread().getId();
//执行lua脚本
stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX+name),threadId);
}
}
Lua脚本
保存在resource目录下
-- KEYS[1]就是锁的key值,ARGV[1]就是当前线程的标识
-- 判断标识是否一致
if(redis.call('GET',KEYS[1])==ARGV[1]) then
-- 一致删除锁
return redis.call('DEL',KEYS[1])
end
return 0;