《黑马点评》优惠券秒杀的一点笔记

实现一人一单功能

批量获取用户token,并使用jmeter对秒杀接口进行压力测试(黑马点评)https://blog.csdn.net/qq_32792547/article/details/131969918

教我怎么用Jmeter 测试的
https://blog.csdn.net/dingd1234/article/details/124438307

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

只能怪博主大神笔记记的太全了呜呜呜我本来想自己好好总结的来着,就只能锦上添花了。

首先不建议把锁加在方法上,因为任何一个用户来了都要加这把锁,而且是同一把锁,方法之间变成串行执行,性能很差。

因此可以把锁加在用户id上,只有当id相同时才会对锁形成竞争关系。但是因为toString的内部是new了一个String字符串,每调一次toString都是生成一个全新的字符串对象,锁对象会变。

所以可以调用intern()方法,intern()方法会优先去字符串常量池里查找与目标字符串值相同的引用返回(只要字符串一样能保证返回的结果一样)。

但是因为事务是在函数执行结束之后由Spring进行提交,如果把锁加在createVoucherOrder内部其实有点小——因为如果解锁之后,其它线程可以进入,而此时事务尚未提交,仍然会导致安全性问题。

因此最终方案是把synchronized加在createVoucherOrder的方法外部,锁住的是用户id。

关于代理对象事务的问题:通常情况下,当一个使用了@Transactional注解的方法被调用时,Spring会从上下文中获取一个代理对象来管理事务。

但是如果加@Transactional方法是被同一个类中的另一个方法调用时,Spring不会使用代理对象,而是直接调用该方法,导致事务注解失效。

为避免这种情况,可以使用AopContext.currentProxy方法获取当前的代理对象,然后通过代理对象调用被@Transactional注解修饰的方法,确保事务生效。

在VoucherOrderServiceImpl中写入如下代码(注意:ctrl+alt+m可以把含有return的代码段进行提取):

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券信息
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        //2.判断秒杀是否开始
        //2.1秒杀尚未开始返回异常
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始");
        }
        //2.2秒杀已结束返回异常
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束");
        }
        voucher = seckillVoucherService.getById(voucherId);
        //3.判断库存是否充足
        if (voucher.getStock() < 1) {
            //3.1库存不足返回异常
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        /**
         * 先获取锁 再完成下单
         */
        /*
        最终方案是把synchronized加在createVoucherOrder的方法外部
        加锁的对象是用户id 所以要再外面获取用户id 然后上锁

        先获取锁 提交事务 再释放锁

        但是如果加@Transactional方法是被同一个类中的另一个方法调用时,Spring不会使用代理对象,
        而是直接调用该方法,导致事务注解失效。
        为避免这种情况,可以使用AopContext.currentProxy方法获取当前的代理对象,
        然后通过代理对象调用被@Transactional注解修饰的方法,确保事务生效。
         */
        synchronized (userId.toString().intern()) {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return createVoucherOrder(voucherId);
        }
    }

    /**
     * 先查询 再判断 判断完了再去扣减库存
     * 然后从查询出订单到判断订单到新增订单整段逻辑加上悲观锁 需要把这段逻辑做一个封装
     * 然后方法内部加上synchronized 同一个用户加一把锁 不同的用户加不同锁 根据id来加
     * 但是因为事务是在函数执行结束之后由Spring进行提交,如果把锁加在createVoucherOrder内部
     * 其实有点小——因为如果解锁之后,其它线程可以进入,而此时事务尚未提交,仍然会导致安全性问题。
     */
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        //6.一人一单
        Long userId = UserHolder.getUser().getId();
        /*
        intern()方法会优先去字符串常量池里查找与目标字符串值相同的引用返回
        (只要字符串一样能保证返回的结果一样)。
        这里用intern()就能确保当用户的id一样 则锁就一样
         */
        //6.1查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        //6.2判断是否存在
        if (count > 0) {
            //用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }
        //没买过就去创建订单
        //3.2库存充足扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") //相当于set条件 set stock = stock - 1
                .eq("voucher_id", voucherId) //相当于where条件 where id = ? and stock = ?
                .gt("stock", 0).update();
        if (!success) {
            return Result.fail("库存不足!");
        }
        //4.创建订单,返回订单id
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId("order");//订单id
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);//代金券id
        save(voucherOrder);
        return Result.ok(orderId);
    }
}

Java 8 Runnable和Callable使用Lambda表达式示例(带参数)

参考文章:https://www.concretepage.com/java/jdk-8/java-8-runnable-and-callable-lambda-example-with-argument

@Component
public class RedisIdWorker {
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    //定义序列号的位数
    private static final int COUNT_BITS=32;
    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    public  long nextId(String keyPrefix){
        //1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long timeStamp = now.toEpochSecond(ZoneOffset.UTC) - BEGIN_TIMESTAMP;
        //2.生成序列号
        //2.1获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //2.2自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
        //3.拼接并返回
        //借助到 位运算
        //让时间戳的值向左移动,多少位不要写死,定义一个常量就行
        //这里用或运算 先让右边这个向左补齐,多出来的进行或运算保留
        return timeStamp << COUNT_BITS | count;
    }
}
//注入redisIdWorker
    @Resource
    private RedisIdWorker redisIdWorker;

    //线程池
    private ExecutorService es = Executors.newFixedThreadPool(500);

    @Test
    void testIdWorker() throws InterruptedException {

        CountDownLatch latch = new CountDownLatch(300);
        //Java 8 Runnable和Callable使用Lambda表达式(带参数)
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextId("order");
                System.out.println("id = " + id);
            }
            //每当一个线程完毕就会执行一次countDown()
            latch.countDown();
        };
        /*
        等同于
        Runnable task = new Runnable() {
        for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextId("order");
                System.out.println("id = " + id);
            }
         };
         */
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 300; i++) {
            es.submit(task);
        }
        //等待countDown()结束为止
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println(end - begin);//打印时间
    }
}
  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
黑马rabbitMQ优惠卷秒杀是指利用RabbitMQ消息队列来实现优惠卷的秒杀活动。RabbitMQ是一个开源的消息队列中间件,它可以实现高效的消息传递和异步通信。在秒杀活动中,由于瞬间会有大量用户同时请求抢购,传统的同步处理方式无法满足高并发的需求,而使用RabbitMQ可以将请求异步化,提高系统的并发处理能力。 具体实现过程如下: 1. 创建一个消息队列:首先需要创建一个RabbitMQ消息队列,用于存储用户的秒杀请求。 2. 生成优惠卷:在秒杀活动开始前,需要提前生成一定数量的优惠卷,并将其存储在数据库中。 3. 用户抢购请求:用户在秒杀活动开始时,发送抢购请求到消息队列中。 4. 消费者处理请求:创建多个消费者来监听消息队列中的请求,并进行处理。当有新的请求进入队列时,消费者会从队列中获取请求,并进行相应的处理逻辑。 5. 校验优惠卷:消费者在处理请求时,会先校验用户是否有资格参与秒杀活动,并检查优惠卷的库存情况。 6. 分发优惠卷:如果用户符合条件并且优惠卷有库存,消费者会将优惠卷分发给用户,并更新数据库中的库存信息。 7. 返回结果:消费者处理完请求后,将处理结果返回给用户,告知用户是否成功抢购。 通过使用RabbitMQ消息队列,可以有效地解决高并发场景下的请求处理问题,提高系统的性能和稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值