基础业务逻辑
初步实现
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀尚未开始!");
}
// 3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀已经结束!");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail("库存不足!");
}
//5,扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update();
if (!success) {
//扣减库存
return Result.fail("库存不足!");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 6.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 6.2.用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
// 6.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
当多个线程同时进行时可能出现超卖问题,同一个用户可能出现重复下单问题
可以使用乐观锁或者悲观锁解决
乐观锁又分为两种版本号法和CAS法
在每次扣减库存时对版本号也进行修改,假设线程1来查询库存和版本号,都为1,接着判断库存是否大于0,判断成立,执行扣减语句,set stock = stock - 1 ,version = version + 1 where id= 10 and version = 1;如果在执行这条sql语句时version依旧 = 1也就说明和查询到的version一样,没有人修改过版本号,也就没有人扣减库存,执行完后 stock = 0,version = 2. 假设线程2在线程1查询完之后也来查询库存和版本号此时stock = 1,version = 1.此时线程1 还未执行扣减库存操作,所以stock和version都没变,线程2进行stock判断 大于0执行扣减库存操作,,set stock = stock - 1 ,version = version + 1 where id= 10 and version = 1.此时这条sql语句执行失败,因为version已经被线程1修改为2所以执行失败,不会扣减库存 。
用stock修改变化代替version,因为每次查询version时也会查询stock,每次更新version时也会修改stock。只需要将版本号法中的version变为stock就可以
只需要将上面扣减库存代码做以下修改就可,如果使用方案一会发现失败率很大,从业务逻辑上想,不需要使查询的stock完全一样,只需要满足stock>0就可以
//扣减库存
//修改方案一 失败率太大
boolean updated = seckillVoucherService.update().
setSql("stock = stock - 1") //set stock = stock - 1
.eq("voucher_id", voucherId).eq("stock", voucher.getStock()) //where voucher_id = ? and stock = ?
.update();
//修改方案二
boolean update = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0
一人一单业务逻辑
初步实现
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀尚未开始!");
}
// 3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀已经结束!");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail("库存不足!");
}
// 5.一人一单逻辑
// 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("用户已经购买过一次!");
}
//6,扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update();
if (!success) {
//扣减库存
return Result.fail("库存不足!");
}
//7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
我们将一人一单和创建订单的代码统一抽取成一个方法createVoucherOrder并且加锁,这样就可以初步实现一人一单。
@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId();
// 5.1.查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
return Result.fail("用户已经购买过一次!");
}
// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣减失败
return Result.fail("库存不足!");
}
// 7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 7.2.用户id
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderId);
}
但是将锁加在方法上锁的粒度太大,会导致效率低,我们的目的是一个用户只能下一单,所以我们的锁对象是userid就可以。如果在方法内部加锁的话还有一个问题,就是锁释放了,但是事务还没提交新创建的订单可能还未写入数据库这就导致其它线程又可以进来执行查询订单操作可能没有查询到又会创建订单,导致线程安全问题,所以我们要将整个函数锁起来 ,确保事务提交后锁才会释放。
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀尚未开始!");
}
// 3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀已经结束!");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail("库存不足!");
}
Long userId = UserHolder.getUser().getId();
synchronized(userId.toString().intern()){
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
return proxy.createVoucherOrder(orderId);
}
}
toString底层是会创建一个新的对象返回,所以我们要调用inter()方法拿到线程池中的那个原始对象,否则即使是同一个userid每次也会返回一个新的对象。
如果直接调用creatVoucherOrder方法时本质是this在调用,spring只有通过代理对象调用带有@Transactional的方法时事务才会生效,所以我们需要通过上下文来获取当前对象的代理对象Spring 中只有通过代理对象调用带有 @Transactional 注解的方法时,事务才会生效。这是因为 Spring 实现声明式事务管理主要依赖于面向切面编程(AOP)机制,特别是动态代理技术。当一个类的方法被标记为 @Transactional,Spring 不会在原始类实例上直接添加事务行为,而是创建一个代理对象来包裹原始类实例。这个代理对象负责在方法调用前后插入必要的事务管理逻辑,如开启事务、提交事务或回滚事务。
以下是几个关键点说明为何必须通过代理对象调用:
代理对象的作用:
代理对象是对原始对象(即实现了 @Transactional 方法的服务类实例)的包装,它继承或实现与原始对象相同的接口或类。
当外部代码通过代理对象调用方法时,实际上是调用了代理对象上的方法。代理对象的方法内部会先执行与事务相关的前置处理(如开启事务),然后调用原始对象对应方法的实际逻辑,最后执行与事务相关的后置处理(如根据方法执行结果决定提交或回滚事务)。
直接调用的问题:
如果在服务类内部(即同一个类中)的一个方法直接调用另一个被 @Transactional 注解的方法,由于这种调用不经过代理对象,所以不会触发事务管理逻辑。Spring AOP 是基于方法调用的切面编织,同一类内部方法调用属于“自我调用”,不会触发代理方法的介入。
若要解决此类问题,可以采用以下策略之一:
将事务方法移动到另一个类中,使得方法间调用成为不同类之间的调用,从而可以通过代理对象调用。
如代码所示,主动获取当前代理对象并使用代理对象来调用事务方法,确保事务逻辑得以执行。
因此,为了确保 @Transactional 注解的有效性,应确保对事务方法的调用是通过 Spring 创建的代理对象进行的。这样,Spring 才能正确地应用事务边界管理和相应的事务行为。
要想获取当前类的代理对象需要在启动类中添加下面的注解让这个代理类暴露,我们才能获取到
@EnableAspectJAutoProxy(exposeProxy = true)
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>