Redid:黑马点评项目之优惠券秒杀

一、优惠券秒杀:全局唯一ID

每个店铺都可以发布优惠券;
当用户抢购,就会生成订单并保存在tb_voucher_order这张表中,订单表如果使用数据库自增id会让
1、id的规律性太明显;
2、受单表数据量的限制;

全局id生成器:全局唯一id。利用redis的incre自增属性
为了增加redis的安全性,也可以不直接使用redis自增的数值,而是拼接一下其他信息
> !](https://img-blog.csdnimg.cn/a7e84b76f6e74c74899c0113456a55f6.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbTBfNDk1Njk1NjQ=,size_20,color_FFFFFF,t_70,g_se,x_16)

    public long generateOnlyId(String prefix){
        // 1.生成时间戳(返回long,可以利用当前时间戳-开始时间戳,得到一个插值)觉得也可以利用当前时间戳
        long nowSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);

        long timeStamp = nowSecond - BEGIN_TIME_STAMP;

        // 2、生成序列号(利用当前时间-精确到天生成,可以方便以后统计按时间日期统计,而且一天的订单量往往不会超过2的32次方)
        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 这里的number永远不会为null值
        long number = redisTemplate.opsForValue().increment("icr:" + prefix + ":" + date);
        // 3、拼接 时间戳 + 序列号 (利用位运算)1的时间戳往左移动32位,然后用 | 运算,因为 | 运算只要有一个为真返回真;
        // 1的时间戳往左移动32位之后,后面32都填充为0,0 | date 返回为date;
        return timeStamp << BIT_NUMBER | number;
    }

在这里插入图片描述

二、实现优惠券的秒杀下单

在这里插入图片描述

三、库存超卖问题

高并发情况,多个线程操作同一个优惠券,在线程A执行更新操作的过程中,线程B、C、D对查询优惠券,并同样进行的扣减库存的操作;导致商品超卖;
> 在这里插入图片描述

解决方法:
在这里插入图片描述
因为需要一个个的操作,所以悲观锁的性能较差。大部分是采用乐观锁。

乐观锁的方法

  1. 版本号法:利用表中的版本字段version,只要有一个线程对其进行操作,version + 1;其他线程会去查询version,对比version是否发生变化,如果version改变了,不让其他线程进行修改操作;
    在这里插入图片描述
    2.CAS法:Compare and Switch:比较修改。在版本号的基础上,既然用version字段前后可以比较得出这条数据是否发生变化,那同样,直接用stock库存本身来比较,stock前后是否发生了变化;
    在这里插入图片描述

乐观锁的缺点: 成功率低,由于多个线程同时对优惠券进行操作,如果有一个线程拿到了锁,其他线程可能就会直接取消抢购,没有不断的重试,造成优惠券大量富余,库存大量富余,最后库存没有卖完。
在这里插入图片描述

四、优惠券秒杀:一人一单

在这里插入图片描述
不对用户限制,最后造成优惠券被同一个人全部买走的情况;

/**
* 单体项目下,悲观锁锁用户id 实现一人一单;
* 集群模式下无效
*/
@Transactional
    public Result createVoucherOrder(Long userId, Long voucherId) {
        // 5、扣减库存
        // 5.1 实现一人一单功能 查询当前用户id和优惠券id是否存在
        // todo 在高并发的情况下,100个线程同时查,查询到的都是0,所以依然存在同一个用户抢了多个优惠券的情况;
        // 由于是插入操作,无法比较,所以最好加悲观锁
        // 要保证锁的是一个对象,userId.toString()每次会是一个全新的对象,
        // .intern()返回字符串的规范表示,去常量池寻找是否有这个值,确保用户id一样,值就一样
        synchronized (userId.toString().intern()) {
            int count = iSeckillVoucherService
                    .query()
                    .eq("user_id", userId)
                    .eq("voucher_id", voucherId).count();
            if (count > 0) {
                return Result.fail("你已下过单");
            }
            boolean sucess = iSeckillVoucherService.update()
                    // CAS 法 Compare and Switch**:比较修改。在版本号的基础上,
                    // 既然用version字段前后可以比较得出这条数据是否发生变化,那同样,
                    // 直接用stock库存本身来比较,stock前后是否发生了变化;
                    .setSql("stock = stock -1") // set stock = stock -1
                    .eq("voucher_id", voucherId).gt("stock", 0)
                    // 乐观锁的缺点:**   成功率低,由于多个线程同时对优惠券进行操作,如果有一个线程拿到了锁,
                    // 其他线程可能就会直接取消抢购,没有不断的重试,造成优惠券大量富余,库存大量富余,最后库存没有卖完。
                    // 所以这里这样判断stock > 0即可
                    .update();
            if (!sucess) {
                return Result.fail("库存不足");
            }
            // 6、将数据存入优惠券订单表
            VoucherOrder voucherOrder = new VoucherOrder();
            voucherOrder.setId(redisIdWorker.generateOnlyId("order"));
            voucherOrder.setUserId(userId);
            voucherOrder.setVoucherId(voucherId);
            save(voucherOrder);
            return Result.ok(voucherOrder.getId());
        }
    }

五、一人一单在集群模式/分布式环境下的并发问题(如何让多个jvm获取同一把锁?)

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值