在上一篇我们使用了乐观锁解决超卖问题,悲观锁保证了一人一单。具体可以参考下面这篇文章https://blog.youkuaiyun.com/ppp666777/article/details/140887568https://blog.youkuaiyun.com/ppp666777/article/details/140887568但是这都是只有一台jvm的情况,但是当有多台jvm时锁就会失效,每个jvm内部都有自己独立的堆栈,所以我们的以前的锁就会失效
所以我们要在jvm外部提供一个锁监视器让所有jvm都能看到,当线程一来获取锁时,获取成功后锁监视器对象会持有线程一中用户id,此时jvm中的任意线程来获取锁时会失败,会等待所释放,而线程一则正常执行业务逻辑。
我们可以基于redis来实现这个分布式锁
我们基于set nx ex 来实现获取锁的操作
当获取锁成功后如果因为执行业务超时那么会自动释放锁
封装了获取锁和释放锁的方法
public class SimleRedisLock implements ILock{
private String name;
private StringRedisTemplate redisTemplate;
public SimleRedisLock(String name, StringRedisTemplate redisTemplate) {
this.name = name;
this.redisTemplate = redisTemplate;
}
private static final String KEY_PREFIX = "lock:";
@Override
public boolean tryLock(long timeoutSec) {
//获取当前线程的id
long id = Thread.currentThread().getId();
Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, id + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
redisTemplate.delete(KEY_PREFIX + name);
}
}
初步实现
Long userId = UserHolder.getUser().getId();
//创建锁对象
SimleRedisLock lock = new SimleRedisLock("order:"+userId,redisTemplate);
//获取锁
boolean isLock = lock.tryLock(1200);
if (!isLock) {
//获取锁失败,返回错误或重试
return Result.fail("不允许重复下单");
}
try {
//只有通过spring创建的代理对象调用@Transactional注解类下的方法时
注解才会生效,所以需要通过AopContext.currentProxy()方法来获取当前对象的代理类
通过他调用creatVoucherOrder方法
IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy();
return proxy.creatVoucherOrder(voucherId, voucher);
} finally {
lock.unlock();
}
存在的缺点
线程一获取锁成功,但是由于业务阻塞导致锁超时释放,此时线程二来获取锁可以成功获取,当线程二开始执行时线程一正好完成业务了,此时它会释放锁,这时的锁已被线程二获取这样它就会把线程二的锁释放掉,线程三也可以来获取锁会导致并发安全问题
改进
在删除锁之前获取锁标识并判断是否一致
private String name;
private StringRedisTemplate redisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true);
public SimleRedisLock(String name, StringRedisTemplate redisTemplate) {
this.name = name;
this.redisTemplate = redisTemplate;
}
private static final String KEY_PREFIX = "lock:";
@Override
public boolean tryLock(long timeoutSec) {
//获取当前线程的id
String id = ID_PREFIX+Thread.currentThread().getId() ;
Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, id, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
public void unlock() {
//获取线程标识
String id = ID_PREFIX+Thread.currentThread().getId() ;
//获取锁中的标识
String lockId = redisTemplate.opsForValue().get(KEY_PREFIX + name);
if (id.equals(lockId)) {
//释放锁
redisTemplate.delete(KEY_PREFIX + name);
}
你以为这样就安全了嘛?显然不是。当jvm进行垃圾回收时,如果线程一此时刚好判断完成准备释放,那么会阻塞线程从而使锁超时释放,当线程二去获取锁使会成功,当阻塞结束,线程一会执行释放锁,由于判断已经完成,所以会将线程二的锁删除。也就是说判断锁和删除锁的操作必须是一个原子操作。那用什么来保证呢?其实是用Lua脚本来实现
--比较线程标识与锁中的是否一致
if (redis.call ('get',KEYS[1] == ARGV[1])) then
--释放锁
return redis.call ('del',KEYS[1])
end
return 0
Java中调用脚本
//定义并初始化脚本
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() {
redisTemplate.execute(UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX+Thread.currentThread().getId()
);
}