1. 初级版本
注意自动拆箱时的空指针异常
public class SimpleRedisLock implements ILock{
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private static final String KEY_PREFIX = "lock:";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String lockName) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
}
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
long threadId = Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + lockName, threadId + "", timeoutSec, TimeUnit.SECONDS);
// 自动拆箱存在null异常,不要直接返回success,做一下判断
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + lockName);
}
}
之前synchronized方法修改为
// 一人一单
// 用户id
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
// 获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
// 一人一单
// 用户id
Long userId = UserHolder.getUser().getId();
// 创建锁对象(注意要拼接用户id,实现单个用户的一人一单,只有同一个id的请求打来时才要进行锁定)
SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
// 获取锁(稍微设置久一点,避免测试时锁失效)
boolean isLock = lock.tryLock(1200);
if(!isLock){
return Result.fail("一人只能抢一次哦~");
}
try {
// 获取IVoucherOrderService的代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
2. 初级版本存在的问题(重点)
当线程1获取锁时,发生业务阻塞,阻塞时间甚至超过了锁的释放时间,这时线程1会失去锁资源;
此时,线程2可以获取锁资源,并执行自己的业务。此时线程1获得了某些资源之后不再阻塞,开始执行自己的业务,并且执行完之后会进行锁的释放操作;
上述情况出现时,就会触发安全问题,线程1会把线程2的锁给释放掉,线程3也可以拿到锁了,锁机制也就相当于是失效了;
解决办法:给锁加标识(有点像乐观锁),线程在释放锁时多进行一次判断
流程图进行相应修改
3. 代码实现优化版本
采用UUID是因为每个JVM都会维护线程id的递增数字,如果直接采用线程id可能出现线程id冲突,使用UUID在集群环境下更合适;
public class SimpleRedisLock implements ILock{
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private static final String KEY_PREFIX = "lock:";
// 参数true去掉UUID自动给的下划线
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String lockName) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
}
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识,拼接上对应的UUID
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + lockName, threadId + "", timeoutSec, TimeUnit.SECONDS);
// 自动拆箱存在null异常,不要直接返回success,做一下判断
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的线程标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + lockName);
// 判断标识是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + lockName);
}
}
}
4. 上述代码的原子性问题
在使用分布式锁的情况下,当一个线程持有锁时,其他线程需要等待该锁被释放才能获取到它。
然而,垃圾回收的发生可能会导致线程在释放锁之前出现延迟或暂停。具体来说,当垃圾回收器运行时,它会检查和清理不再被引用的对象,回收内存资源。在这个过程中,所有线程的执行都会被暂停,称为“停顿时间”。从而影响其他线程的等待时间;
如果一个线程在持有锁的过程中发生了垃圾回收的停顿,它可能无法及时释放锁,从而延长了其他线程等待锁的时间。并且可能存在锁到期自己释放了的情况;
例如:当上述代码进行释放锁的操作时,已经做完判断标识是否一致的if语句之后,在准备释放锁之前发送了阻塞(GC回收阻塞)。此时锁到期自动释放,线程2从而获取到锁资源,而恰好线程1恢复开始执行业务,注意,此时的线程1是已经判断完了标识一致操作之后才阻塞的,所以线程1会直接进行释放锁的操作,这样线程2获取的锁会被线程1释放掉;
// 判断标识是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + lockName);
}
}
所以要确保加锁和释放锁具有原子性