• dp秒杀优惠券


    1、全局id生成器

    当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

    • id的规律性太明显
    • 受单表数据量的限制

    场景分析:如果我们的id具有太明显的规则,用户或者说商业对手很容易猜测出来我们的一些敏感信息,比如商城在一天时间内,卖出了多少单,这明显不合适。

    场景分析二:随着我们商城规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行拆库拆表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性

    1. @Component
    2. public class RedisIdWorker {
    3. /**
    4. * 开始时间戳
    5. */
    6. private static final long BEGIN_TIMESTAMP = 1640995200L;
    7. /**
    8. * 序列号的位数
    9. */
    10. private static final int COUNT_BITS = 32;
    11. private StringRedisTemplate stringRedisTemplate;
    12. public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
    13. this.stringRedisTemplate = stringRedisTemplate;
    14. }
    15. public long nextId(String keyPrefix) {
    16. // 1.生成时间戳
    17. LocalDateTime now = LocalDateTime.now();
    18. long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
    19. // 2.生成序列号
    20. long timestamp = nowSecond - BEGIN_TIMESTAMP;
    21. // 2.1.获取当前日期,精确到天
    22. // 2.2.自增长
    23. String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
    24. Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
    25. // 3.拼接并返回
    26. return timestamp << COUNT_BITS | count;
    27. }
    28. }

    利用线程池创建300个并发线程,每个线程生成100个id,总耗时time = 2629ms

    1. @Test
    2. void testIdWorker() throws InterruptedException {
    3. CountDownLatch latch = new CountDownLatch(300);
    4. Runnable task = () -> {
    5. for (int i = 0; i < 100; i++) {
    6. long id = redisIdWorker.nextId("order");
    7. System.out.println("id = " + id);
    8. }
    9. latch.countDown();
    10. };
    11. long begin = System.currentTimeMillis();
    12. for (int i = 0; i < 300; i++) {
    13. es.submit(task);
    14. }
    15. latch.await();
    16. long end = System.currentTimeMillis();
    17. System.out.println("time = " + (end - begin));
    18. }

    2、添加优惠券,实现秒杀下单

    下单时需要判断两点:

    • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
    • 库存是否充足,不足则无法下单

    1. @Service
    2. @Transactional
    3. public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService {
    4. @Resource
    5. private SeckillVoucherServiceImpl seckillVoucherService;
    6. @Resource
    7. private RedisIdWorker redisIdWorker;
    8. @Override
    9. public Result seckillVoucher(Long voucherId) {
    10. // 1、查询优惠券信息
    11. SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    12. // 2、判断秒杀是否开始
    13. if (voucher.getBeginTime().isAfter(LocalDateTime.now()) || voucher.getEndTime().isBefore(LocalDateTime.now())){
    14. return Result.fail("秒杀未开始");
    15. }
    16. // 3、判断库存是否充足
    17. if(voucher.getStock()<1) {
    18. return Result.fail("库存不足");
    19. }
    20. // 4、扣减库存
    21. voucher.setStock(voucher.getStock()-1);
    22. boolean success = seckillVoucherService.updateById(voucher);
    23. if(!success) {
    24. return Result.fail("库存不足");
    25. }
    26. // 5、创建订单
    27. VoucherOrder voucherOrder = new VoucherOrder();
    28. // 5.1.订单id
    29. long orderId = redisIdWorker.nextId("order");
    30. voucherOrder.setId(orderId);
    31. // 5.2.用户id
    32. Long userId = UserHolder.getUser().getId();
    33. voucherOrder.setUserId(userId);
    34. // 5.3.代金券id
    35. voucherOrder.setVoucherId(voucherId);
    36. save(voucherOrder);
    37. return Result.ok(orderId);
    38. }
    39. }

    3、解决超卖问题

    jmeter分析时记得在HTTP信息头管理器中加上token

    测试1秒200个并发量,发现会出现超卖问题,100个订单扣减,库存剩下-9个。

    乐观锁解决超卖问题

    1. // 4、扣减库存
    2. boolean success = seckillVoucherService.update()
    3. .setSql("stock= stock -1")
    4. .eq("voucher_id", voucherId)
    5. .gt("stock", 0)
    6. .update();

    4、一人一单

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

    1. // 根据用户id与优惠券id,判断订单是否存在
    2. Long useId = UserHolder.getUser().getId();
    3. Integer count = query().eq("user_id", useId).eq("voucher_id", voucherId).count();
    4. if(count > 0) {
    5. return Result.fail("用户已经购买过一次!");
    6. }
    7. // 4、扣减库存
    8. boolean success = seckillVoucherService.update()
    9. .setSql("stock= stock -1")
    10. .eq("voucher_id", voucherId)
    11. .gt("stock", 0)
    12. .update();

    但是上述代码涉及到查询与修改,因此还是会有多线程安全问题。用jmeter测试,发现还是有相同用户id与优惠券id的订单超卖,没有达到一人一单的需求。

    因此尝试加锁。由于存在较多的写操作,因此采用悲观锁。但如果对后面一大段设计增删改查的代码加锁,锁粒度太大。如下代码所示。

    1. @Transactional
    2. public synchronized Result createVoucherOrder(Long voucherId) {
    3. Long userId = UserHolder.getUser().getId();
    4. // 5.1.查询订单
    5. int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    6. // 5.2.判断是否存在
    7. if (count > 0) {
    8. // 用户已经购买过了
    9. return Result.fail("用户已经购买过一次!");
    10. }
    11. // 6.扣减库存
    12. boolean success = seckillVoucherService.update()
    13. .setSql("stock = stock - 1") // set stock = stock - 1
    14. .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
    15. .update();
    16. if (!success) {
    17. // 扣减失败
    18. return Result.fail("库存不足!");
    19. }
    20. // 7.创建订单
    21. VoucherOrder voucherOrder = new VoucherOrder();
    22. // 7.1.订单id
    23. long orderId = redisIdWorker.nextId("order");
    24. voucherOrder.setId(orderId);
    25. // 7.2.用户id
    26. voucherOrder.setUserId(userId);
    27. // 7.3.代金券id
    28. voucherOrder.setVoucherId(voucherId);
    29. save(voucherOrder);
    30. // 7.返回订单id
    31. return Result.ok(orderId);
    32. }

    存在两个问题:

    1)且由于在createVoucherOrder代码外加上事务,事务包含锁,因此会导致加锁读操作后事务还未提交,就提前将所释放,下一个线程获取锁时,读取到的数据库的值为旧值,造成数据不一致性,因此需要在事务外加锁。

    2)将synchronized加在方法上,相当于是对this加锁,因此多线程过来加的是一把锁,串行化,性能差。由于需求是一人一单,因此只需要对同一用户加锁。因此去除ThreadLocal中的userId进行加锁。但每次线程进入createVoucherOrder方法都会新建一个userId对象,所以其实本质上还是对不同的对象进行了加锁,userId.toString()的底层也是new一个string对象,但我们需要的是同一用户只有一把锁,因此需要intern() 这个方法从常量池中拿到数据。修改代码:

    1. @Override
    2. public Result seckillVoucher(Long voucherId) {
    3. // 1、查询优惠券信息
    4. SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    5. // 2、判断秒杀是否开始
    6. if (voucher.getBeginTime().isAfter(LocalDateTime.now()) || voucher.getEndTime().isBefore(LocalDateTime.now())){
    7. return Result.fail("秒杀未开始");
    8. }
    9. // 3、判断库存是否充足
    10. Integer stock = voucher.getStock();
    11. if(voucher.getStock()<1) {
    12. return Result.fail("库存不足");
    13. }
    14. Long userId = UserHolder.getUser().getId();
    15. synchronized (userId.toString().intern()) {
    16. return createVoucherOrder(voucherId);
    17. }
    18. }
    19. @Transactional
    20. public Result createVoucherOrder(Long voucherId) {
    21. Long userId = UserHolder.getUser().getId();
    22. // 根据用户id与优惠券id,判断订单是否存在
    23. int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    24. if(count > 0) {
    25. return Result.fail("用户已经购买过一次!");
    26. }
    27. // 4、扣减库存
    28. boolean success = seckillVoucherService.update()
    29. .setSql("stock= stock -1")
    30. .eq("voucher_id", voucherId)
    31. .gt("stock", 0)
    32. .update();
    33. if (!success) {
    34. //扣减库存
    35. return Result.fail("库存不足!");
    36. }
    37. // 5、创建订单
    38. VoucherOrder voucherOrder = new VoucherOrder();
    39. // 5.1.订单id
    40. long orderId = redisIdWorker.nextId("order");
    41. voucherOrder.setId(orderId);
    42. // 5.2.用户id
    43. voucherOrder.setUserId(userId);
    44. // 5.3.代金券id
    45. voucherOrder.setVoucherId(voucherId);
    46. save(voucherOrder);
    47. return Result.ok(orderId);
    48. }

    但是以上做法依然有问题,因为你调用的方法,其实是this.的方式调用的,事务想要生效,还得利用代理来生效,所以这个地方,我们需要获得原始的事务对象, 来操作事务

    1)在启动类上加上

    2)在pom.xml文件里加上依赖

    1. <dependency>
    2. <groupId>org.aspectj</groupId>
    3. <artifactId>aspectjweaver</artifactId>
    4. </dependency>

    3)修改代码

    1. synchronized (userId.toString().intern()) {
    2. IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    3. return proxy.createVoucherOrder(voucherId);
    4. }

    记得将seckillVoucher方法上的事务注解取消,否则还是会出现上述问题。

    查看数据库,成功实现一人一单

    5、集群环境下的并发问题

    通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。

    开启两份服务,同一用户下单两次(负载均衡算法采用轮询),库存扣减两次。

    有关锁失效原因分析

    由于现在我们部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是 集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。

  • 相关阅读:
    重数和众数问题——C语言实现
    单调队列优化的DP——最大子序和
    jsplumb应用场景快速开发
    基于工业智能网关的汽车充电桩安全监测方案
    centos使用root用户登录
    如何把thinkphp5的项目迁移到阿里云函数计算来应对流量洪峰?
    学习笔记【Java 虚拟机③】类加载与字节码技术
    探讨代理IP与Socks5代理在跨界电商中的网络安全应用
    在Jupyter里面安装torch的历程
    医院能耗管理平台:实现能源消耗的全面掌控
  • 原文地址:https://blog.csdn.net/m0_47536537/article/details/139255552