黑马点评-秒杀优惠券优化

秒杀优惠券优化点

可以看到,业务逻辑当中的每一个步骤按顺序执行,基本都是对数据库的增删改查,而数据库的反应能力本身就比较慢,极大的增加了运行压力

 所以可以将业务逻辑分为两部分,判断秒杀库存是否足够和校验一人一单的资格放在redis中,redis对于用户的请求交互有较快的响应,进行完两个资格的判断,若若都通过,则创建出订单id,但这时候不算创建订单,只是相当于入场券,在进行后续的操作。大大提升了速度

这其中有几个关键的问题,如何在redis中完成资格的判断?

业务流程

首先,对于库存的判断,我们采取String结构来存储

其次,对于一人一单的资格判断,采取set结构来存储,因为set结构里面的元素不可重复,就保证了存储的用户id不能重复,其次set集合查询快,方便判断

因为同时做了很多的判断,为了保证原子性,采取lua脚本的方式来编写代码,由于在redis中只是做了简单的判断,并且保存了符合资格的优惠券id,用户id,订单id,将来在进行创建订单写操作时,只需从rendis中读取这三个id信息就能创建订单,有点像有了抢购资格,给你锁单15分钟的时间,期间只要付款就可以,极大的增加了业务的并发性。

案例:改进秒杀业务,提高并发性能 

1.首先,将优惠券库存信息保存在redis中

@Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
        // 保存优惠券
        save(voucher);
        // 保存秒杀信息,往数据库里写
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
        // 保存优惠券id和优惠券库存到Redis中
        stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
    }

2.基于lua脚本,判断库存和一人一单,决定是否锁单成功

判断一人一单,可以用set集合中的SISMEMBER,判断该用户id是否在key中存在

下图可以看出创建了一个set集合,存入了key2中三个用户,用户存在1,不存在返回0

--1.参数列表
--1.1优惠券id
local voucherId = ARGV[1]
--1.2用户id
local userId = ARGV[2]

--2.数据key
--2.1库存key
local stockKey = 'seckill:stock:' .. voucherId
--2.2.订单key
local orderKey = 'seckill:order:' .. voucherId

--3.脚本业务
--3.1判断库存是否足够 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    --3.2 库存不足,返回错误1
    return 1
end
--3.2判断用户是否之前下过单 SISMEMBER orderKey userid
if(redis.call('sismember', orderKey,userId) == 1)then
    --3.3存在则说明重复下单
    return 2
end
--3.4扣库存
redis.call('incrby', stockKey,-1)
--3.5下单(保存用户id)
redis.call('sadd',orderKey,userId)

3.抢单成功,将优惠券id和用户id封装到队列当中

    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;

    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }    
    //创建阻塞队列,用于存储封装的信息,后续交给下单方法处理
    private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        // 1.执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString()
        );
        int r = result.intValue();
        // 2.判断结果是否为0
        if (r != 0) {
            // 2.1.不为0 ,代表没有购买资格
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        // 2.2.为0 ,有购买资格,把下单信息保存到阻塞队列,也就是封装起来
        VoucherOrder voucherOrder = new VoucherOrder();
        // 2.3.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 2.4.用户id
        voucherOrder.setUserId(userId);
        // 2.5.代金券id
        voucherOrder.setVoucherId(voucherId);
        // 2.6.放入阻塞队列
        orderTasks.add(voucherOrder);

        // 3.返回订单id
        return Result.ok(orderId);
    }

4. 创造线程池,不断地从阻塞队列中拿到消息,实现异步下单

测试,发现平均值、吞吐量都得到了很大的提升,说明从redis中进行判断可以提高并发性1.

总结 

秒杀业务的优化思路:原本,应该先判断抢购是否开始,库存是否足够,是否一人一单,最后才创建订单,同时还要为怎么多的业务添加各种事务管理和锁,同时请求数据库的操作也比较慢,导致消耗了大量的时间。所以我们就将业务进行简化,异步完成,分为两部分,一部分是在redis中对抢购资格的判断,包括库存是否足够,是否符合一人一单,另一部分时下单,从组测队列中拿到符合抢购资格的用户id,优惠券id,订单id,完成下单操作。

同时,上述思路还存着一定的问题

### 黑马点评秒杀优惠券空指针异常解决方案 在处理黑马点评秒杀优惠券过程中可能出现的空指针异常问题时,可以通过优化代码逻辑以及合理利用 Redis 的特性来规避此类风险。以下是具体的分析与解决方法: #### 1. **Redis 数据初始化** 为了防止因 Redis 中不存在特定 key 而引发的潜在空指针异常,在存储优惠券库存数据之前应确保其存在性。如果 key 不存在,则需动态创建该 key 并赋予默认值[^2]。 ```java String couponKey = "coupon_stock:" + couponId; Long stock = redisTemplate.opsForValue().get(couponKey); if (stock == null) { // 初始化库存,默认值可以根据业务需求设定 redisTemplate.opsForValue().set(couponKey, defaultStockCount); } ``` #### 2. **分布式锁机制** 为了避免多线程并发操作导致的数据不一致或者空指针异常,引入分布式锁是一种有效的方式。通过实现 `ILock` 接口并结合 Redis 来完成加解锁功能[^4]。 ```java public class RedisDistributedLock implements ILock { private final String lockKey; public RedisDistributedLock(String lockKey) { this.lockKey = lockKey; } @Override public boolean tryLock(long timeoutSec) { long expireTimeMillis = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(timeoutSec); Boolean result = redisTemplate.opsForValue() .setIfAbsent(lockKey, String.valueOf(expireTimeMillis), Duration.ofSeconds(timeoutSec)); return Boolean.TRUE.equals(result); } @Override public void unlock() { redisTemplate.delete(lockKey); } } ``` 当尝试获取锁失败时,当前请求可以直接返回提示信息而无需继续执行后续可能触发异常的操作。 #### 3. **事务控制与回滚策略** 对于复杂的业务场景,建议采用 Redis 事务配合 Lua 脚本来原子化更新多个字段或记录状态变化过程。这不仅能够提升性能还能减少错误发生概率[^3]。 示例脚本如下所示: ```lua local coupon_key = KEYS[1] local user_coupon_record_key = KEYS[2] -- 获取剩余库存量 local current_stock = tonumber(redis.call(&#39;GET&#39;, coupon_key)) if not current_stock or current_stock <= 0 then -- 库存不足情况下直接退出 return &#39;OUT_OF_STOCK&#39; end -- 扣减库存 redis.call(&#39;DECRBY&#39;, coupon_key, 1) -- 记录用户领取情况 redis.call(&#39;SADD&#39;, user_coupon_record_key, ARGV[1]) return &#39;SUCCESS&#39; ``` 调用上述脚本的方法可参照下面代码片段: ```java DefaultRedisScript<String> script = new DefaultRedisScript<>(); script.setScriptSource(new ResourceScriptSource(new ClassPathResource("seckill.lua"))); script.setResultType(String.class); List<String> keys = Arrays.asList( "coupon_stock:" + couponId, "user_seckill_records:" + userId ); Object result = redisTemplate.execute(script, keys, userId.toString()); if ("OUT_OF_STOCK".equals(result)) { throw new RuntimeException("优惠券已抢光!"); } else if (!"SUCCESS".equals(result)) { throw new RuntimeException("未知错误"); } ``` 以上措施综合运用后可以显著降低甚至完全消除由于未考虑到边界条件所引起的空指针异常现象。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值