需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
我们只需要在增加订单之前,拿用户id和优惠券id判断订单是否已经存在,如果存在,说明用户已经购买。
代码实现:
- package com.hmdp.service.impl;
-
- import com.hmdp.dto.Result;
- import com.hmdp.entity.SeckillVoucher;
- import com.hmdp.entity.VoucherOrder;
- import com.hmdp.mapper.VoucherOrderMapper;
- import com.hmdp.service.ISeckillVoucherService;
- import com.hmdp.service.IVoucherOrderService;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.hmdp.utils.RedisIdWorker;
- import com.hmdp.utils.UserHolder;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
-
- import javax.annotation.Resource;
- import java.time.LocalDateTime;
-
- /**
- *
- * 服务实现类
- *
- *
- * @author 虎哥
- * @since 2021-12-22
- */
- @Service
- public class VoucherOrderServiceImpl extends ServiceImpl
implements IVoucherOrderService { -
- @Resource
- private ISeckillVoucherService iSeckillVoucherService;
-
- @Resource
- private RedisIdWorker redisIdWorker;
-
- @Override
- public Result seckillVoucher(Long voucherId) {
- //1.获取优惠券信息
- SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
-
- //2.判断是否已经开始
- if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
- Result.fail("秒杀尚未开始!");
- }
- //3.判断是否已经结束
- if (voucher.getEndTime().isBefore(LocalDateTime.now())){
- Result.fail("秒杀已经结束了!");
- }
- //4.判断库存是否充足
- if (voucher.getStock() < 1) {
- Result.fail("库存不充足!");
- }
- //5.扣减库存
- boolean success = iSeckillVoucherService.update()
- .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
- .update();
- if (!success){
- Result.fail("库存不充足!");
- }
- Long userId = UserHolder.getUser().getId();
- //6.根据优惠券id和用户id判断订单是否已经存在
- //如果存在,则返回错误信息
- int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
- if (count > 0) {
- 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);
- //8.返回订单id
- return Result.ok(orderId);
- }
- }
但是,还没完,这种代码逻辑,在高并发的情况下还是会出现一个人购买购买多个的情况:
就是同一时间,多个线程来查询数据,都没有查到订单,都去创建了订单(高并发的情况下)
类似超卖问题,所以我们要进行上锁。
这次就用悲观锁。
最简单的实现方法,就是把从查询订单是否存在到保存订单返回订单id这一段代码块进行封装成一个方法,然后在这个方法上加上synchronized关键字和spring事务。
如下:
- package com.hmdp.service.impl;
-
- import com.hmdp.dto.Result;
- import com.hmdp.entity.SeckillVoucher;
- import com.hmdp.entity.VoucherOrder;
- import com.hmdp.mapper.VoucherOrderMapper;
- import com.hmdp.service.ISeckillVoucherService;
- import com.hmdp.service.IVoucherOrderService;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.hmdp.utils.RedisIdWorker;
- import com.hmdp.utils.UserHolder;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
-
- import javax.annotation.Resource;
- import java.time.LocalDateTime;
-
- /**
- *
- * 服务实现类
- *
- *
- * @author 虎哥
- * @since 2021-12-22
- */
- @Service
- public class VoucherOrderServiceImpl extends ServiceImpl
implements IVoucherOrderService { -
- @Resource
- private ISeckillVoucherService iSeckillVoucherService;
-
- @Resource
- private RedisIdWorker redisIdWorker;
-
- @Override
- public Result seckillVoucher(Long voucherId) {
- //1.获取优惠券信息
- SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
-
- //2.判断是否已经开始
- if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
- Result.fail("秒杀尚未开始!");
- }
- //3.判断是否已经结束
- if (voucher.getEndTime().isBefore(LocalDateTime.now())){
- Result.fail("秒杀已经结束了!");
- }
- //4.判断库存是否充足
- if (voucher.getStock() < 1) {
- Result.fail("库存不充足!");
- }
- //5.扣减库存
- boolean success = iSeckillVoucherService.update()
- .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
- .update();
- if (!success){
- Result.fail("库存不充足!");
- }
-
- return createVoucherOrder(voucherId);
-
- }
-
- @Transactional
- public synchronized Result createVoucherOrder(Long voucherId) {
- Long userId = UserHolder.getUser().getId();
- //6.根据优惠券id和用户id判断订单是否已经存在
- //如果存在,则返回错误信息
- int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
- if (count > 0) {
- 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);
- //8.返回订单id
- return Result.ok(orderId);
- }
- }
但是,这个方法就是使用了悲观锁,锁的对象是整个类对象,所有用户公用一把锁,就会导致串行执行,从而性能大大降低。
我们可以只锁上用户id,让他每个用户获得一把锁。
- package com.hmdp.service.impl;
-
- import com.hmdp.dto.Result;
- import com.hmdp.entity.SeckillVoucher;
- import com.hmdp.entity.VoucherOrder;
- import com.hmdp.mapper.VoucherOrderMapper;
- import com.hmdp.service.ISeckillVoucherService;
- import com.hmdp.service.IVoucherOrderService;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.hmdp.utils.RedisIdWorker;
- import com.hmdp.utils.UserHolder;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
-
- import javax.annotation.Resource;
- import java.time.LocalDateTime;
-
- /**
- *
- * 服务实现类
- *
- *
- * @author 虎哥
- * @since 2021-12-22
- */
- @Service
- public class VoucherOrderServiceImpl extends ServiceImpl
implements IVoucherOrderService { -
- @Resource
- private ISeckillVoucherService iSeckillVoucherService;
-
- @Resource
- private RedisIdWorker redisIdWorker;
-
- @Override
- public Result seckillVoucher(Long voucherId) {
- //1.获取优惠券信息
- SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
-
- //2.判断是否已经开始
- if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
- Result.fail("秒杀尚未开始!");
- }
- //3.判断是否已经结束
- if (voucher.getEndTime().isBefore(LocalDateTime.now())){
- Result.fail("秒杀已经结束了!");
- }
- //4.判断库存是否充足
- if (voucher.getStock() < 1) {
- Result.fail("库存不充足!");
- }
- //5.扣减库存
- boolean success = iSeckillVoucherService.update()
- .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
- .update();
- if (!success){
- Result.fail("库存不充足!");
- }
- Long userId = UserHolder.getUser().getId();
-
- return createVoucherOrder(voucherId);
-
- }
-
- @Transactional
- public Result createVoucherOrder(Long voucherId) {
- Long userId = UserHolder.getUser().getId();
- //6.根据优惠券id和用户id判断订单是否已经存在
- synchronized (userId.toString().intern()){
- //如果存在,则返回错误信息
- int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
- if (count > 0) {
- 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);
- //8.返回订单id
- return Result.ok(orderId);
- }
- }
- }
这里锁上userid时,除了用toString方法转成字符串,还使用intern方法的原因是:
toString方法的底层原理其实是new一个String对象,然后将其变成字符串,如果只锁上了加toString方法的userid,就有可能出现相同的userid,但是toString底层new出来的String对象不同,而多分了锁。所以使用intern方法来直接判断常量池中的string值是否一致,值一样的共用一把锁,这样就不会导致多分锁了。
但是但是,还没完因为这里我们是加了锁和事务,但是因为这个事务时Spring进行管理的,它会在我们代码块结束后才会去执行事务,也就是我们释放锁的时候,才会执行事务。这个时候,锁放开了,就会有其他线程进来,就很有可能出现事务提交带上了其他线程。
我们可以这样进行改进:在本个方法上进行加锁。
- package com.hmdp.service.impl;
-
- import com.hmdp.dto.Result;
- import com.hmdp.entity.SeckillVoucher;
- import com.hmdp.entity.VoucherOrder;
- import com.hmdp.mapper.VoucherOrderMapper;
- import com.hmdp.service.ISeckillVoucherService;
- import com.hmdp.service.IVoucherOrderService;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.hmdp.utils.RedisIdWorker;
- import com.hmdp.utils.UserHolder;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
-
- import javax.annotation.Resource;
- import java.time.LocalDateTime;
-
- /**
- *
- * 服务实现类
- *
- *
- * @author 虎哥
- * @since 2021-12-22
- */
- @Service
- public class VoucherOrderServiceImpl extends ServiceImpl
implements IVoucherOrderService { -
- @Resource
- private ISeckillVoucherService iSeckillVoucherService;
-
- @Resource
- private RedisIdWorker redisIdWorker;
-
- @Override
- public Result seckillVoucher(Long voucherId) {
- //1.获取优惠券信息
- SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
-
- //2.判断是否已经开始
- if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
- Result.fail("秒杀尚未开始!");
- }
- //3.判断是否已经结束
- if (voucher.getEndTime().isBefore(LocalDateTime.now())){
- Result.fail("秒杀已经结束了!");
- }
- //4.判断库存是否充足
- if (voucher.getStock() < 1) {
- Result.fail("库存不充足!");
- }
- //5.扣减库存
- boolean success = iSeckillVoucherService.update()
- .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
- .update();
- if (!success){
- Result.fail("库存不充足!");
- }
- Long userId = UserHolder.getUser().getId();
- synchronized (userId.toString().intern()){
- return createVoucherOrder(voucherId);
- }
- }
-
- @Transactional
- public Result createVoucherOrder(Long voucherId) {
- Long userId = UserHolder.getUser().getId();
- //6.根据优惠券id和用户id判断订单是否已经存在
- //如果存在,则返回错误信息
- int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
- if (count > 0) {
- 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);
- //8.返回订单id
- return Result.ok(orderId);
- }
- }
但是但是但是,还没完。哈哈
我们只给创建订单这个方法(createVoucherOrder)加了事务,但是没给上面判断条件的方法加上事务,而我们锁代码块里执行的方法,其实是this.createVoucherOrder()方法,是没有加事务的方法调用的createVoucherOrder()方法,这个this可不是spring的事务代理对象,这就会导致事务失效。
解决方法就是,我们只需要拿到代理对象,然后通过代理对象调用我们这个加了事务的方法,也就是createVoucherOrder()方法。
使用 AopContext.currentProxy();方法来拿到代理对象
温馨提示 :使用这个方法前要先做两件事~
1. 记得在配置类似加上@EnableAspectJAutoProxy(exposeProxy = true)注解来暴露这个代理对象
2. 加上依赖:
- <dependency>
- <groupId>org.aspectjgroupId>
- <artifactId>aspectjweaverartifactId>
- dependency>
完整代码;:
- package com.hmdp.service.impl;
-
- import com.hmdp.dto.Result;
- import com.hmdp.entity.SeckillVoucher;
- import com.hmdp.entity.VoucherOrder;
- import com.hmdp.mapper.VoucherOrderMapper;
- import com.hmdp.service.ISeckillVoucherService;
- import com.hmdp.service.IVoucherOrderService;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.hmdp.utils.RedisIdWorker;
- import com.hmdp.utils.UserHolder;
- import org.springframework.aop.framework.AopContext;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
-
- import javax.annotation.Resource;
- import java.time.LocalDateTime;
-
- /**
- *
- * 服务实现类
- *
- *
- * @author 虎哥
- * @since 2021-12-22
- */
- @Service
- public class VoucherOrderServiceImpl extends ServiceImpl
implements IVoucherOrderService { -
- @Resource
- private ISeckillVoucherService iSeckillVoucherService;
-
- @Resource
- private RedisIdWorker redisIdWorker;
-
- @Override
- public Result seckillVoucher(Long voucherId) {
- //1.获取优惠券信息
- SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
-
- //2.判断是否已经开始
- if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
- Result.fail("秒杀尚未开始!");
- }
- //3.判断是否已经结束
- if (voucher.getEndTime().isBefore(LocalDateTime.now())){
- Result.fail("秒杀已经结束了!");
- }
- //4.判断库存是否充足
- if (voucher.getStock() < 1) {
- Result.fail("库存不充足!");
- }
- //5.扣减库存
- boolean success = iSeckillVoucherService.update()
- .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
- .update();
- if (!success){
- Result.fail("库存不充足!");
- }
- Long userId = UserHolder.getUser().getId();
- synchronized (userId.toString().intern()){
- IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
- return proxy.createVoucherOrder(voucherId);
- }
- }
-
- @Transactional
- public Result createVoucherOrder(Long voucherId) {
- Long userId = UserHolder.getUser().getId();
- //6.根据优惠券id和用户id判断订单是否已经存在
- //如果存在,则返回错误信息
- int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
- if (count > 0) {
- 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);
- //8.返回订单id
- return Result.ok(orderId);
- }
- }