1.集群模式下的锁是每个JVM中都有个锁监视器,并不能保证线程并发的安全,也就无法实现一人一单这种要求。
2.所以引入了分布式锁,即在集群模式下公用一个锁监视器,这样保证了线程并发运行和安全问题
3.分布式锁要满足一下特征
5.基于redis的分布式锁
使用redis的setnx语句老师先分布式锁
setnx key value: 若key存在则无法设置,可以看见下图中就thread1获得了锁,其他的线程并不能
del key:可以释放锁,释放lock之后,thread55又获得锁
若在获取锁的过程中服务宕机,又只能手动释放锁,导致长时间无法释放,可以通过设置TTL过期时间来让锁自动释放
expire key time:给指定的锁定义到期时间
可以看到设置了到期时间后,redis中并没有lock这个字段
为了保证设置锁和设置国企时间的原子性,采用一句话来设计
6.实现rendis的分布式锁
业务逻辑
public class SimpleRedisLock implements ILock {
//定义不同锁的名字,不同业务使用不同的锁
private String name;
private StringRedisTemplate stringRedisTemplate;
//因为这两个参数是用户传给这个方法的,所以需要构造函数
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PRIFIX = "lock:";
@Override
public boolean tryLock(long timeoutSec) {
//value是当前线程的id
long threadId = Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PRIFIX + name,
threadId + "", timeoutSec, TimeUnit.SECONDS);
//因为success是包装型,直接返回true有一定的拆箱风险
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//释放锁
stringRedisTemplate.delete(KEY_PRIFIX + name);
}
}
实现分布式锁下的一人一单逻辑
Long userid = UserHolder.getUser().getId();
//redis的分布式锁
SimpleRedisLock lock = new SimpleRedisLock("order:"+userid,stringRedisTemplate);
//获取分布式锁
boolean isLock = lock.tryLock(1200);
//判断是否获取锁成功
if (!isLock) {
return Result.fail("请勿重新下单");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
/*//为什么要加锁?因为当多线程同一刻查询该用户id是否存在时,都会返回不存在,那么就会创建多个订单。
*//*那为什么不在下面的方法中加锁,因为锁和事务管理之间还存在并发安全问题,
* 若仅在下面5.1以后增加锁,
* 首先加锁,然后查询订单,创建订单,释放锁,这时候@Transactional将订单事务提交上去这是一个正常的流程
* 可是,若在提交事务的过程中,又有一个线程进来执行流程,这时候数据暂时还没更新,那也会造成并发安全问题,无法实现一人一单
* !!所以将返回方法进行上锁,才能确保事务提交之后才释放锁,防止并发问题*//*
synchronized(userid.toString().intern()) {
//如果直接调用方法返回还会有事务失效的风险,因为直接return的是createVoucherOrder代理对象
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}*/
}
@Transactional//这个方法将创建优惠券订单的查询用户订单和创建优惠券订单的两个动作封装起来,就需要添加事务管理,防止事务崩溃,可以轮回
public Result createVoucherOrder(Long voucherId) {
//5.一人一单(即在查询判断库存是否足够后,根据用户id查询是否已经存在秒杀券订单)
//5.1根据用户id查询秒杀全订单
Long userid = UserHolder.getUser().getId();
int count = query().eq("user_id", userid).eq("voucher_id", voucherId).count();
//5.2判断订单是否存在
if (count > 0) {
return Result.fail("您已经购买过秒杀券,请勿重新购买!");
}
//扣减库存(使用乐观锁的CAS方案,即操作时,判断库存的大小即可,不用像版本号那样还要添加额外的字段,
// 还有一个问题,就是乐观锁若时以相等作为条件,成功率很低,因为多线程中,一个操作成功时,其他的线程都判断不相等只能失败。)
boolean success = seckillVoucherService.update().setSql("stock = stock - 1")
.eq("voucher_id", voucherId).gt("stock", 0)
.update();
if (!success) {
return Result.fail("库存不足");
}
//6.没有抢购过,则创建新订单
VoucherOrder order = new VoucherOrder();
//6.1订单ID
long orderId = redisIdWorker.nextId("order");
order.setId(orderId);
//6.2用户iD
order.setUserId(userid);
//6.3优惠ID
order.setVoucherId(voucherId);
save(order);
//7.返回订单ID
return Result.ok(orderId);
}