黑马点评 -- 优惠券秒杀

为什么需要全局唯一id?

采用自增长的id具有规律性,容易被猜出来,不具有安全性

全局ID生成器

这是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足以下特性:

唯一性、高性能、安全性、递增性、高可用

为了增加ID的安全性,我们可以不直接使用Redis自增的数据,而是拼接一些其他信息:

符号位(1bit)+时间戳(31bit)+序列号(32bit)

生成全局唯一Id代码

@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 nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        //2. 生成序列号
        //2.1 获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        //2.2 自增长
        Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        //3. 拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

测试代码

 @Test
    void testIdWorker() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(300);
        Runnable task = () ->{
            for (int i=0;i<100;i++){
                long id = redisIdWorker.nextId("order");
                System.out.println("id  = " + id);
            }
            latch.countDown();
        };
        long begin = System.currentTimeMillis();
        for (int i = 0;i<300;i++){
            es.submit(task);
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("time = " + (end - begin));
    }

实现订单下单

/**
     * 实现秒杀下单
     * @param voucherId
     * @return
     */
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        //1. 查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        //2. 判断秒杀是否开始
        if(voucher.getBeginTime().isAfter(LocalDateTime.now())){
            //尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        //3. 判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())){
            //已经结束
            return Result.fail("秒杀已经结束!");
        }
        //4. 判断库存是否充足
        if (voucher.getStock() < 1) {
            //库存不足
            return Result.fail("库存不足!");
        }
        //5. 扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).update();
        if (!success){
            //扣减失败
            return Result.fail("库存不足!");
        }
        //6. 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //6.1 订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //6.2 用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //6.3 代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //7. 返回订单id
        return Result.ok(orderId);
    }

执行这个代码发现出现了超卖问题,出现超卖的原因是多线程查询过程出现了问题

库存超卖问题使用乐观锁或悲观锁解决

乐观锁的关键是判断之间查询得到的数据是否有被修改过,常见的方式有两种:

1. 版本号法(每次进行更新操作时都要判断版本是否被修改过) 2. CAS法(直接比较数据是否变化)

乐观锁解决超卖

乐观锁的部分的代码

//5. 扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") //set stock = stock -1
                .eq("voucher_id", voucherId).eq("stock",voucher.getStock()) //where id = ? and stock = ?
                .update();

虽然这样实现之后解决了超卖问题,但是也导致了很多票并没有卖出去,乐观锁成功率太低

对乐观锁进行改进

//5. 扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") //set stock = stock -1
                .eq("voucher_id", voucherId).gt("stock",0) //where id = ? and stock > 0
                .update();

实现一人一单功能

多线程并发操作时,由于线程穿插执行,每个线程都查询到为0,这时就会出现一个用户可以下多单,初步改造的代码:还是会出现非预期想要的结果:

//5. 一人一单
        Long userId = UserHolder.getUser().getId();
        //5.1 订单id
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        //5.2 判断是否存在
        if (count > 0){
            //用户已经购买过了
            return Result.fail("用户已经购买过一次");
        }
synchronized关键字,用于确保同一时间只有一个线程能够执行这个方法
public synchronized Result createVoucherOrder(Long voucherId)

单机模式下解决一人一单的核心代码

Long userId = UserHolder.getUser().getId();
        //先释放锁 再提交事务
        synchronized (userId.toString().intern()) {
            //8. 返回订单id
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
/**
synchronized(userId. toString() .intern ()) 是一种同步锁的实现方式,用于在多线程环境下防止多个线程同时执行同一段代码,确保线程安全。
synchronized 关键字:表示同步块,确保同一时间只有一个线程可以执行同步块中的代码。其作用是避免多个线程同时访问共享资源时出现不一致或数据冲突的情况。
intern()方法:String.intern()是Java 中的一个方法,用于将字符串添加到字符串常量池(JVM 中的一个字符串共享区域),并返回该字符串的引用。对于相同内容的字符串,intern()会返回相同的引用。换句话说,如果两个线程尝试使用相同的 userId,intern()保证了它们会锁定相同的字符串对象。
**/
IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);

1. Spring AOP 和代理对象:
Spring 使用AOP(面向切面编程)来实现某些功能,比如事务管理。
当你在类中定义了一个方法(例如createVoucherOrder) 并加上了@Transactional 注解、Spring 会使用代理对象去管理这个方法,确保在方法执行时开启事务,方法结束时提交或回滚事务。

2. AopContext.currentProxy () 的作用: AopContext.currentProxy () 是Spring 提供的一个方法,作用是获取当前正在执行的代理对象。
为什么需要这个?因为如果你在类内部直接调用另一个方法(不通过代理对象),Spring 的AOP机制可能无法正确拦截方法,事务等增强功能不会生效。

3. 代理对象与直接调用的区别:如果你直接调用createVoucherOrder (voucherId),它不会触发 Spring的 AOP 逻辑(比如事务管理),因此事务可能无法生效。而通过
proxy.createVoucherOrder (voucherId),你实际上是通过 Spring的代理对象来调用这个方法,这样AOP功能(如事务管理)才能正确工作。

获取代理对象首先要引入依赖

<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

然后在启动类添加注解

@EnableAspectJAutoProxy(exposeProxy = true)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值