目录
前文链接:
什么是分布式锁
修改秒杀优惠券的流程
分布式锁的实现
基于Redis的分布式锁(非阻塞实现)
ILock接口
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的过期时间,过期后自动释放
* @return true代表获取锁成功,false代表获取锁失败
*/
boolean trLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
创建一个类并实现ILock接口,用自定义的name值来区分锁,同时获取当前线程的Id作为value值存入进redis中,并设置超时时间
public class SimpleRedisLock implements ILock {
private static final String KEY_PREFIX="lock:";
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean trLock(long timeoutSec) {
//获取线程标识
long 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() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
实现
@Transactional
public Result createVoucherOrder(Long voucherId) {
//获取用户Id
UserDTO user = UserHolder.getUser();
Long userId = user.getId();
//创建锁对象
SimpleRedisLock redisLock = new SimpleRedisLock("order" + userId, stringRedisTemplate);
//尝试获取锁
boolean isLock = redisLock.trLock(5);
//判断
if(!isLock){
//获取锁失败,直接返回失败或者重试
return Result.fail("不允许重复下单");
}
try {
//一人一单
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if(count>0){
//说明用户之前买过了
return Result.fail("用户已经购买过一次");
}
VoucherOrder voucherOrder = new VoucherOrder();
//扣减库存
seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id",voucherId).gt("stock",0)
.update();
//设置订单Id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//设置优惠券Id
voucherOrder.setVoucherId(voucherId);
//设置用户Id
voucherOrder.setUserId(userId);
save(voucherOrder);
//返回订单Id
return Result.ok(orderId);
} finally {
//释放锁
redisLock.unlock();
}
}
当业务阻塞时出现的并发问题(误删Redis分布式锁)
可能会出现误删Redis分布式锁的情况
修改流程 ,需要判断锁标示是否是自己的
需要确保标示是唯一的,可以采用UUID拼接线程Id的做法
业务代码不需要改,只需要修改锁的获取与释放,增加逻辑判断
public class SimpleRedisLock implements ILock {
private static final String KEY_PREFIX="lock:";
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean trLock(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();
//获取锁中的标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if(threadId.equals(id)){
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
虽然增加了锁标示的判断,但仍然存在一些极端的问题,如下图,当判断标示与释放锁之间产生了阻塞,也有可能发生误删分布式锁的情况,所以得让判断标示与释放锁成原子性操作,要么一起成功,要么一起失败
我们可以采用Redis中的Lua脚本进行编写,然后在Java程序中进行调用
Redis的Lua脚本
Lua语言的数组第一个下标为1
业务逻辑转换为Lua脚本
再次改进Redis的分布式锁
unlock.lua脚本的编写
-- 比较线程标识是否是锁的一致
if(redis.call('get',KEYS[1]==ARGV[1])) then
-- 释放锁
return redis.call('del',KEYS[1])
end
return 0
实现
public class SimpleRedisLock implements ILock {
private static final String KEY_PREFIX="lock:";
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT=new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean trLock(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);
}
//基于Lua脚本
@Override
public void unlock() {
//调用Lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
}
总结
仍存在优化的空间