1.分布式锁优化一人一单
本项目采用redis
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
采用opsForValue的setIfAbsent方法,确保每次只有一个锁存在,同时增加过期时间,防止死锁。
public class SimpleRedisLock implements ILock {
private final String name;
private final StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
// 获取锁
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示,因为我们要区分到底是哪个线程拿到了这个锁,方便之后unLock的时候进行判断
String 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() {
//通过del删除锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
注意,在返回那里,返回的是Boolean.TRUE.equals(success);而不是直接返回success,这样不会返回空指针。
VoucherOrderServiceImpl
@Override
public Result seckillVoucher(Long voucherId) {
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 判断是否在秒杀时间内
// 判断是否还有库存
Long userId = UserHolder.getUser().getId();
// 之前的:没有考虑集群模式下的锁问题
// 【完善代码】:考虑集群模式下的锁问题
//创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//获取锁对象
boolean isLock = lock.tryLock(1200);
//加锁失败
if (!isLock) {
return Result.fail("之前的下单逻辑还在处理/不允许重复下单");
}
// 这里就是为了调用createVoucherOrder方法,但是要考虑到事务的问题,所以要通过代理对象来调用
try {
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
//释放锁
lock.unlock();
}
}
关于调用代理:在类的内部直接调用另一个类内部的方法,不一定都需要使用代理。但如果你希望事务管理、AOP 切面(例如日志、权限检查、缓存等)或者其他基于代理的功能正常生效,就必须通过代理来调用这些方法。·
2.误删问题
在线程1拿到业务后,执行业务时出现了堵,从而使锁超时释放,线程2 便挤了进来,拿到了线程1的锁,然后最后线程1完成后又提前把线程2拿到的锁给提前释放了,这种问题如何解决呢?
解决思路:在获取锁时存入线程标示(可以用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);
}
}
3.分布式锁的原子性问题
这里涉及到了一个很极端的情况。我们在上一节通过标识判断解决了误删问题,但是如果我们判断标识之后,还没有来得及释放锁,线程收到了堵塞,锁提前超时释放了。这时线程2又可以拿到锁了。然后线程1继续释放锁,把线程2的锁给释放了。此处的误删根本原因是锁的标识判断和释放锁的操作不是原子性的。
解决思路: Lua脚本解决多条命令原子性问题
RedisTemplate中可以利用execute方法去执行lua脚本。
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
// 提前在静态代码块中加载lua脚本,,提高性能
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public void unlock() {
// 调用lua脚本解决误删问题,确保原子性
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
// 字符串转集合
Collections.singletonList(KEY_PREFIX + name),
// 线程标识
ID_PREFIX + Thread.currentThread().getId());
}
4.分布式锁Redission
我们基于setnx实现的锁有以下弊端:
关于主从节点
Redisson 是一个基于 Redis 的 Java 客户端,提供了丰富的功能扩展和高级功能,旨在通过分布式架构简化和增强 Redis 的使用。它在标准的 Redis 客户端基础上做了很多增强,提供了更为简洁、灵活的 API 以及支持更复杂分布式场景的能力。
Redission提供了分布式锁的多种多样的功能
4.Redission快速入门
5.Redission实现可重入锁的原理
Redission底层也是使用lua脚本实现的,此处不做过多阐述。
6.锁重试和WatchDog机制
在 Redisson 中,watchdog 是一个用于 自动续期 分布式锁和其他分布式数据结构的机制。它主要用于解决 分布式锁失效的问题,保证在锁持有过程中,锁的有效期不会因为某些原因而被过期,确保在锁持有的线程执行时间超过锁的过期时间时,锁不会被其他线程错误地释放。
自动续期:Redisson 的 watchdog 确保锁在超时之前会被自动续期,从而防止锁过期。特别是在长时间运行的操作中,避免了由于业务逻辑耗时过长,锁被其他客户端误释放的问题。
总结
7.MutiLock锁解决主从不一致问题
如果主节点宕机,会从从节点中选拔一个新的节点作为主节点。如果主从同步尚未完成,会出现锁失效的问题。
现在在所有主节点中都存放一份锁,要求一个线程必须从所有主节点中获取锁,才算真正获取锁。
假如此时有一个主节点宕机,恰好主从同步没有完成。若有其它线程趁虚而入获取到了新主节点的锁,但因为没能获取其它主节点的锁,因此也是获取锁失败的。