• Redis实战——优惠券秒杀(一人一单业务)


    需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

    我们只需要在增加订单之前,拿用户id和优惠券id判断订单是否已经存在,如果存在,说明用户已经购买。

    代码实现:

    1. package com.hmdp.service.impl;
    2. import com.hmdp.dto.Result;
    3. import com.hmdp.entity.SeckillVoucher;
    4. import com.hmdp.entity.VoucherOrder;
    5. import com.hmdp.mapper.VoucherOrderMapper;
    6. import com.hmdp.service.ISeckillVoucherService;
    7. import com.hmdp.service.IVoucherOrderService;
    8. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    9. import com.hmdp.utils.RedisIdWorker;
    10. import com.hmdp.utils.UserHolder;
    11. import org.springframework.stereotype.Service;
    12. import org.springframework.transaction.annotation.Transactional;
    13. import javax.annotation.Resource;
    14. import java.time.LocalDateTime;
    15. /**
    16. *

    17. * 服务实现类
    18. *

    19. *
    20. * @author 虎哥
    21. * @since 2021-12-22
    22. */
    23. @Service
    24. public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService {
    25. @Resource
    26. private ISeckillVoucherService iSeckillVoucherService;
    27. @Resource
    28. private RedisIdWorker redisIdWorker;
    29. @Override
    30. public Result seckillVoucher(Long voucherId) {
    31. //1.获取优惠券信息
    32. SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
    33. //2.判断是否已经开始
    34. if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
    35. Result.fail("秒杀尚未开始!");
    36. }
    37. //3.判断是否已经结束
    38. if (voucher.getEndTime().isBefore(LocalDateTime.now())){
    39. Result.fail("秒杀已经结束了!");
    40. }
    41. //4.判断库存是否充足
    42. if (voucher.getStock() < 1) {
    43. Result.fail("库存不充足!");
    44. }
    45. //5.扣减库存
    46. boolean success = iSeckillVoucherService.update()
    47. .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
    48. .update();
    49. if (!success){
    50. Result.fail("库存不充足!");
    51. }
    52. Long userId = UserHolder.getUser().getId();
    53. //6.根据优惠券id和用户id判断订单是否已经存在
    54. //如果存在,则返回错误信息
    55. int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    56. if (count > 0) {
    57. return Result.fail("用户已经购买!");
    58. }
    59. //7. 创建订单
    60. VoucherOrder voucherOrder = new VoucherOrder();
    61. //7.1添加订单id
    62. Long orderId = redisIdWorker.nextId("order");
    63. voucherOrder.setId(orderId);
    64. //7.2添加用户id
    65. voucherOrder.setUserId(userId);
    66. //7.3添加优惠券id
    67. voucherOrder.setVoucherId(voucherId);
    68. save(voucherOrder);
    69. //8.返回订单id
    70. return Result.ok(orderId);
    71. }
    72. }

     但是,还没完,这种代码逻辑,在高并发的情况下还是会出现一个人购买购买多个的情况:

    就是同一时间,多个线程来查询数据,都没有查到订单,都去创建了订单(高并发的情况下)

    类似超卖问题,所以我们要进行上锁

    这次就用悲观锁。

    最简单的实现方法,就是把从查询订单是否存在到保存订单返回订单id这一段代码块进行封装成一个方法,然后在这个方法上加上synchronized关键字和spring事务。

    如下:

    1. package com.hmdp.service.impl;
    2. import com.hmdp.dto.Result;
    3. import com.hmdp.entity.SeckillVoucher;
    4. import com.hmdp.entity.VoucherOrder;
    5. import com.hmdp.mapper.VoucherOrderMapper;
    6. import com.hmdp.service.ISeckillVoucherService;
    7. import com.hmdp.service.IVoucherOrderService;
    8. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    9. import com.hmdp.utils.RedisIdWorker;
    10. import com.hmdp.utils.UserHolder;
    11. import org.springframework.stereotype.Service;
    12. import org.springframework.transaction.annotation.Transactional;
    13. import javax.annotation.Resource;
    14. import java.time.LocalDateTime;
    15. /**
    16. *

    17. * 服务实现类
    18. *

    19. *
    20. * @author 虎哥
    21. * @since 2021-12-22
    22. */
    23. @Service
    24. public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService {
    25. @Resource
    26. private ISeckillVoucherService iSeckillVoucherService;
    27. @Resource
    28. private RedisIdWorker redisIdWorker;
    29. @Override
    30. public Result seckillVoucher(Long voucherId) {
    31. //1.获取优惠券信息
    32. SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
    33. //2.判断是否已经开始
    34. if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
    35. Result.fail("秒杀尚未开始!");
    36. }
    37. //3.判断是否已经结束
    38. if (voucher.getEndTime().isBefore(LocalDateTime.now())){
    39. Result.fail("秒杀已经结束了!");
    40. }
    41. //4.判断库存是否充足
    42. if (voucher.getStock() < 1) {
    43. Result.fail("库存不充足!");
    44. }
    45. //5.扣减库存
    46. boolean success = iSeckillVoucherService.update()
    47. .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
    48. .update();
    49. if (!success){
    50. Result.fail("库存不充足!");
    51. }
    52. return createVoucherOrder(voucherId);
    53. }
    54. @Transactional
    55. public synchronized Result createVoucherOrder(Long voucherId) {
    56. Long userId = UserHolder.getUser().getId();
    57. //6.根据优惠券id和用户id判断订单是否已经存在
    58. //如果存在,则返回错误信息
    59. int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    60. if (count > 0) {
    61. return Result.fail("用户已经购买!");
    62. }
    63. //7. 创建订单
    64. VoucherOrder voucherOrder = new VoucherOrder();
    65. //7.1添加订单id
    66. Long orderId = redisIdWorker.nextId("order");
    67. voucherOrder.setId(orderId);
    68. //7.2添加用户id
    69. voucherOrder.setUserId(userId);
    70. //7.3添加优惠券id
    71. voucherOrder.setVoucherId(voucherId);
    72. save(voucherOrder);
    73. //8.返回订单id
    74. return Result.ok(orderId);
    75. }
    76. }

    但是,这个方法就是使用了悲观锁,锁的对象是整个类对象,所有用户公用一把锁,就会导致串行执行,从而性能大大降低。

    我们可以只锁上用户id,让他每个用户获得一把锁。

    1. package com.hmdp.service.impl;
    2. import com.hmdp.dto.Result;
    3. import com.hmdp.entity.SeckillVoucher;
    4. import com.hmdp.entity.VoucherOrder;
    5. import com.hmdp.mapper.VoucherOrderMapper;
    6. import com.hmdp.service.ISeckillVoucherService;
    7. import com.hmdp.service.IVoucherOrderService;
    8. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    9. import com.hmdp.utils.RedisIdWorker;
    10. import com.hmdp.utils.UserHolder;
    11. import org.springframework.stereotype.Service;
    12. import org.springframework.transaction.annotation.Transactional;
    13. import javax.annotation.Resource;
    14. import java.time.LocalDateTime;
    15. /**
    16. *

    17. * 服务实现类
    18. *

    19. *
    20. * @author 虎哥
    21. * @since 2021-12-22
    22. */
    23. @Service
    24. public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService {
    25. @Resource
    26. private ISeckillVoucherService iSeckillVoucherService;
    27. @Resource
    28. private RedisIdWorker redisIdWorker;
    29. @Override
    30. public Result seckillVoucher(Long voucherId) {
    31. //1.获取优惠券信息
    32. SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
    33. //2.判断是否已经开始
    34. if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
    35. Result.fail("秒杀尚未开始!");
    36. }
    37. //3.判断是否已经结束
    38. if (voucher.getEndTime().isBefore(LocalDateTime.now())){
    39. Result.fail("秒杀已经结束了!");
    40. }
    41. //4.判断库存是否充足
    42. if (voucher.getStock() < 1) {
    43. Result.fail("库存不充足!");
    44. }
    45. //5.扣减库存
    46. boolean success = iSeckillVoucherService.update()
    47. .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
    48. .update();
    49. if (!success){
    50. Result.fail("库存不充足!");
    51. }
    52. Long userId = UserHolder.getUser().getId();
    53. return createVoucherOrder(voucherId);
    54. }
    55. @Transactional
    56. public Result createVoucherOrder(Long voucherId) {
    57. Long userId = UserHolder.getUser().getId();
    58. //6.根据优惠券id和用户id判断订单是否已经存在
    59. synchronized (userId.toString().intern()){
    60. //如果存在,则返回错误信息
    61. int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    62. if (count > 0) {
    63. return Result.fail("用户已经购买!");
    64. }
    65. //7. 创建订单
    66. VoucherOrder voucherOrder = new VoucherOrder();
    67. //7.1添加订单id
    68. Long orderId = redisIdWorker.nextId("order");
    69. voucherOrder.setId(orderId);
    70. //7.2添加用户id
    71. voucherOrder.setUserId(userId);
    72. //7.3添加优惠券id
    73. voucherOrder.setVoucherId(voucherId);
    74. save(voucherOrder);
    75. //8.返回订单id
    76. return Result.ok(orderId);
    77. }
    78. }
    79. }

     这里锁上userid时,除了用toString方法转成字符串,还使用intern方法的原因是:

    toString方法的底层原理其实是new一个String对象,然后将其变成字符串,如果只锁上了加toString方法的userid,就有可能出现相同的userid,但是toString底层new出来的String对象不同,而多分了锁。所以使用intern方法来直接判断常量池中的string值是否一致,值一样的共用一把锁,这样就不会导致多分锁了。

    但是但是,还没完因为这里我们是加了锁和事务,但是因为这个事务时Spring进行管理的,它会在我们代码块结束后才会去执行事务,也就是我们释放锁的时候,才会执行事务。这个时候,锁放开了,就会有其他线程进来,就很有可能出现事务提交带上了其他线程

    我们可以这样进行改进:在本个方法上进行加锁。

    1. package com.hmdp.service.impl;
    2. import com.hmdp.dto.Result;
    3. import com.hmdp.entity.SeckillVoucher;
    4. import com.hmdp.entity.VoucherOrder;
    5. import com.hmdp.mapper.VoucherOrderMapper;
    6. import com.hmdp.service.ISeckillVoucherService;
    7. import com.hmdp.service.IVoucherOrderService;
    8. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    9. import com.hmdp.utils.RedisIdWorker;
    10. import com.hmdp.utils.UserHolder;
    11. import org.springframework.stereotype.Service;
    12. import org.springframework.transaction.annotation.Transactional;
    13. import javax.annotation.Resource;
    14. import java.time.LocalDateTime;
    15. /**
    16. *

    17. * 服务实现类
    18. *

    19. *
    20. * @author 虎哥
    21. * @since 2021-12-22
    22. */
    23. @Service
    24. public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService {
    25. @Resource
    26. private ISeckillVoucherService iSeckillVoucherService;
    27. @Resource
    28. private RedisIdWorker redisIdWorker;
    29. @Override
    30. public Result seckillVoucher(Long voucherId) {
    31. //1.获取优惠券信息
    32. SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
    33. //2.判断是否已经开始
    34. if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
    35. Result.fail("秒杀尚未开始!");
    36. }
    37. //3.判断是否已经结束
    38. if (voucher.getEndTime().isBefore(LocalDateTime.now())){
    39. Result.fail("秒杀已经结束了!");
    40. }
    41. //4.判断库存是否充足
    42. if (voucher.getStock() < 1) {
    43. Result.fail("库存不充足!");
    44. }
    45. //5.扣减库存
    46. boolean success = iSeckillVoucherService.update()
    47. .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
    48. .update();
    49. if (!success){
    50. Result.fail("库存不充足!");
    51. }
    52. Long userId = UserHolder.getUser().getId();
    53. synchronized (userId.toString().intern()){
    54. return createVoucherOrder(voucherId);
    55. }
    56. }
    57. @Transactional
    58. public Result createVoucherOrder(Long voucherId) {
    59. Long userId = UserHolder.getUser().getId();
    60. //6.根据优惠券id和用户id判断订单是否已经存在
    61. //如果存在,则返回错误信息
    62. int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    63. if (count > 0) {
    64. return Result.fail("用户已经购买!");
    65. }
    66. //7. 创建订单
    67. VoucherOrder voucherOrder = new VoucherOrder();
    68. //7.1添加订单id
    69. Long orderId = redisIdWorker.nextId("order");
    70. voucherOrder.setId(orderId);
    71. //7.2添加用户id
    72. voucherOrder.setUserId(userId);
    73. //7.3添加优惠券id
    74. voucherOrder.setVoucherId(voucherId);
    75. save(voucherOrder);
    76. //8.返回订单id
    77. return Result.ok(orderId);
    78. }
    79. }

    但是但是但是,还没完。哈哈

    我们只给创建订单这个方法(createVoucherOrder)加了事务,但是没给上面判断条件的方法加上事务,而我们锁代码块里执行的方法,其实是this.createVoucherOrder()方法,是没有加事务的方法调用的createVoucherOrder()方法,这个this可不是spring的事务代理对象,这就会导致事务失效。

    解决方法就是,我们只需要拿到代理对象,然后通过代理对象调用我们这个加了事务的方法,也就是createVoucherOrder()方法。

    使用 AopContext.currentProxy();方法来拿到代理对象

    温馨提示 :使用这个方法前要先做两件事~

    1. 记得在配置类似加上@EnableAspectJAutoProxy(exposeProxy = true)注解来暴露这个代理对象

    2. 加上依赖:

    1. <dependency>
    2. <groupId>org.aspectjgroupId>
    3. <artifactId>aspectjweaverartifactId>
    4. dependency>

    完整代码;: 

    1. package com.hmdp.service.impl;
    2. import com.hmdp.dto.Result;
    3. import com.hmdp.entity.SeckillVoucher;
    4. import com.hmdp.entity.VoucherOrder;
    5. import com.hmdp.mapper.VoucherOrderMapper;
    6. import com.hmdp.service.ISeckillVoucherService;
    7. import com.hmdp.service.IVoucherOrderService;
    8. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    9. import com.hmdp.utils.RedisIdWorker;
    10. import com.hmdp.utils.UserHolder;
    11. import org.springframework.aop.framework.AopContext;
    12. import org.springframework.stereotype.Service;
    13. import org.springframework.transaction.annotation.Transactional;
    14. import javax.annotation.Resource;
    15. import java.time.LocalDateTime;
    16. /**
    17. *

    18. * 服务实现类
    19. *

    20. *
    21. * @author 虎哥
    22. * @since 2021-12-22
    23. */
    24. @Service
    25. public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService {
    26. @Resource
    27. private ISeckillVoucherService iSeckillVoucherService;
    28. @Resource
    29. private RedisIdWorker redisIdWorker;
    30. @Override
    31. public Result seckillVoucher(Long voucherId) {
    32. //1.获取优惠券信息
    33. SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
    34. //2.判断是否已经开始
    35. if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
    36. Result.fail("秒杀尚未开始!");
    37. }
    38. //3.判断是否已经结束
    39. if (voucher.getEndTime().isBefore(LocalDateTime.now())){
    40. Result.fail("秒杀已经结束了!");
    41. }
    42. //4.判断库存是否充足
    43. if (voucher.getStock() < 1) {
    44. Result.fail("库存不充足!");
    45. }
    46. //5.扣减库存
    47. boolean success = iSeckillVoucherService.update()
    48. .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
    49. .update();
    50. if (!success){
    51. Result.fail("库存不充足!");
    52. }
    53. Long userId = UserHolder.getUser().getId();
    54. synchronized (userId.toString().intern()){
    55. IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    56. return proxy.createVoucherOrder(voucherId);
    57. }
    58. }
    59. @Transactional
    60. public Result createVoucherOrder(Long voucherId) {
    61. Long userId = UserHolder.getUser().getId();
    62. //6.根据优惠券id和用户id判断订单是否已经存在
    63. //如果存在,则返回错误信息
    64. int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    65. if (count > 0) {
    66. return Result.fail("用户已经购买!");
    67. }
    68. //7. 创建订单
    69. VoucherOrder voucherOrder = new VoucherOrder();
    70. //7.1添加订单id
    71. Long orderId = redisIdWorker.nextId("order");
    72. voucherOrder.setId(orderId);
    73. //7.2添加用户id
    74. voucherOrder.setUserId(userId);
    75. //7.3添加优惠券id
    76. voucherOrder.setVoucherId(voucherId);
    77. save(voucherOrder);
    78. //8.返回订单id
    79. return Result.ok(orderId);
    80. }
    81. }

  • 相关阅读:
    网络原理——HTTP
    02 请求默认值
    JAVA学习第十一课:java绘图
    基于AM5708开发板——开箱初探+环境搭建、源码编译
    pandas DataFrame内存优化技巧:让数据处理更高效
    k8s的安装部署,详细过程展示(保姆级安装教程)
    解读TCP协议和UDP协议的区别
    LightMap 设置参数的介绍
    【HarmonyOS】低代码平台组件拖拽使用技巧之页签容器
    CSDN每日一练 |『买苹果』『最长回文串』『查找整数』2023-10-20
  • 原文地址:https://blog.csdn.net/qq_59212867/article/details/128121316