从零学习Redis07,用Redis实现秒杀功能

全局ID生成器

当用户抢购时,就会生成订单并保存到tb voucher orderi这张表中,而订单表如果使用数据库自增ID就存在一些问题:
●id的规律性太明显
●受单表数据量的限制

 

完整ID生成器代码:

@Component
public class IDWorker {
    private static final long BEGIN_TIMESTAMP=1640995200L;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    public long nextId(String prefix){
        //生成时间戳

        LocalDateTime now = LocalDateTime.now();
        long l = now.toEpochSecond(ZoneOffset.UTC);
         long timestamp=l - BEGIN_TIMESTAMP;
        System.out.println(timestamp);
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        Long count = stringRedisTemplate.opsForValue().increment("icr:" + prefix + ":" + date);
        System.out.println(count);


        return timestamp <<32 | count;
    }

}

结果:生成的订单ID:81466540238569473

 下单流程:

 代码实现

    @Transactional(rollbackFor = Exception.class)
    public Result order(Long voucherId) {
        SeckillVoucher seckillVoucher = iSeckillVoucherService.getById(voucherId);
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        //现在时间在秒杀活动之前
        if (beginTime.isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀活动还没开始!");
        }
        //现在时间在秒杀活动之后
        if (endTime.isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀活动已经结束!");
        }
        //判断库存是否充足
        if (seckillVoucher.getStock()<1){
            return Result.fail("库存不足");
        }
        //TODO 库存减1
      
        iSeckillVoucherService.update().setSql("stock=
        stock- 1").eq("voucher_id",voucherId).update();
        //TODO 创建订单
        VoucherOrder order=new VoucherOrder();
        long orderID = idWorker.nextId(RedisConstants.SECKILL_STOCK_KEY);
        order.setVoucherId(voucherId);
        order.setId(orderID);
        order.setUserId(UserHolder.getUser().getId());
        save(order);

        //TODO 返回订单ID
        return Result.ok(orderID);
    }

 

在使用乐观锁实现模式中出现了问题,由于判断库存为当前获得的库存,所以导致失败率非常高,所以将其修改为判断条件为库存>0时,就可以下单。

  @Transactional(rollbackFor = Exception.class)
    public Result order(Long voucherId) {
        SeckillVoucher seckillVoucher = iSeckillVoucherService.getById(voucherId);
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        //现在时间在秒杀活动之前
        if (beginTime.isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀活动还没开始!");
        }
        //现在时间在秒杀活动之后
        if (endTime.isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀活动已经结束!");
        }
        //判断库存是否充足
        if (seckillVoucher.getStock()<1){
            return Result.fail("库存不足");
        }
        //TODO 库存减1
        // 在执行减库存操作时,判断是否和查询时的库存一致,一致才可修改

        boolean success = iSeckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).
                gt("stock", 0).update();
        if (!success){
            return Result.fail("库存不足,秒杀失败!");
        }
        //TODO 创建订单
        VoucherOrder order=new VoucherOrder();
        long orderID = idWorker.nextId(RedisConstants.SECKILL_STOCK_KEY);
        order.setVoucherId(voucherId);
        order.setId(orderID);
        order.setUserId(UserHolder.getUser().getId());
        save(order);

        //TODO 返回订单ID
        return Result.ok(orderID);
    }

查看数据库,成功解决超卖问题。

 一人一单问题。

下面为实现代码

  public Result order(Long voucherId) {
        SeckillVoucher seckillVoucher = iSeckillVoucherService.getById(voucherId);
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        //现在时间在秒杀活动之前
        if (beginTime.isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀活动还没开始!");
        }
        //现在时间在秒杀活动之后
        if (endTime.isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀活动已经结束!");
        }
        //判断库存是否充足
        if (seckillVoucher.getStock()<1){
            return Result.fail("库存不足");
        }

        Long userId = UserHolder.getUser().getId();
        System.out.println("====>"+userId);
        //TODO 先释放锁,然后再提交事务
        //和 spring代理对象不同

        synchronized (userId.toString().intern()) {
            System.out.println("得到锁了!!!!!!!!!!!!!!");
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.getResult(voucherId);
        }
    }


    @Transactional(rollbackFor = Exception.class)
    public  Result getResult(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        Integer count = query().eq("user_id", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();
        System.out.println(count);
        //判断用户是否下过单
        if (count > 0) {
            return Result.fail("您已经下过订单");
        }
        //TODO 库存减1
        // 在执行减库存操作时,判断是否和查询时的库存一致,一致才可修改

            boolean success = iSeckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).
                    gt("stock", 0).update();
            if (!success) {
                return Result.fail("库存不足,秒杀失败!");
            }
            //TODO 创建订单
            VoucherOrder order = new VoucherOrder();
            long orderID = idWorker.nextId(RedisConstants.SECKILL_STOCK_KEY);
            order.setVoucherId(voucherId);
            order.setId(orderID);
            order.setUserId(userId);
            save(order);

            //TODO 返回订单ID
            return Result.ok(orderID);
        }

}

难点为,锁加在哪,以及springboot代理事务机制,springboot通过代理对象来进行事务管理,

所以需要自己去手动代理

但是这只能保证单体架构下一人一单,在做了集群之后,由于synchronized 是由jvm虚拟机维护的锁监视器常量池 ,集群之后jvm不同,所以依然会导致线程安全问题,通过jmeter依然发现一人能下两单。

 

 

必须保证setnx和expire具备原子性,要么都成功,要么都失败。

 

public interface ILock {
    //获取锁
    boolean tryLock(long timeoutSec);
    //释放锁
    void unlock();
}



public class Lock implements ILock{
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX="lock:";

    public Lock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        long threadId = Thread.currentThread().getId();
        //获取值
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent
                (KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(success);
    }

    @Override
    public void unlock() {
      stringRedisTemplate.delete(KEY_PREFIX+name);
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值