Redis实现优惠券限一单限制详解
芝麻干 人气:0需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
我们只需要在增加订单之前,拿用户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; /** * <p> * 服务实现类 * </p> */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> 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; /** * <p> * 服务实现类 * </p> */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> 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; /** * <p> * 服务实现类 * </p> */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> 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; /** * <p> * 服务实现类 * </p> */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> 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.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </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; /** * <p> * 服务实现类 * </p> */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> 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); } }
加载全部内容