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;}@Overridepublic 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);}@Overridepublic 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;}@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标识,拼接上对应的UUIDString 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);}@Overridepublic 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);}}
所以要确保加锁和释放锁具有原子性