Redis - 优惠卷秒杀

场景分析

为了避免对数据库造成压力,我们在新增优惠卷的时候,可以将优惠卷的信息储存在Redis中,这样用户抢购的时候访问优惠卷信息,通过Redis读取信息。

抢购流程

业务分析

既然在新增优惠卷的时候,我们只需要id和库存信息,那么这个时候我们需要说选择Redis中Stringt数据类型,以id作为键,此时我们在进行抢劵的时候,通过id可以直接查询到对应的库存信息,进行判断。
此时我们会面临一下场景:

  1. 出现超卖问题
  2. 不能保证一人一单

超卖问题

锁结构选型:


读多写少的场景:

选择乐观锁。因为读操作不会加锁,可以提高系统的并发性能。写操作通过版本号或条件更新来避免冲突。
写操作频繁且并发量高的场景

选择悲观锁。悲观锁通过加锁机制可以有效避免并发冲突,保证数据的一致性和正确性。
性能和一致性权衡:

在性能和一致性之间需要权衡。如果一致性要求非常高,且系统能够接受较低的并发性能,选择悲观锁。如果系统需要高性能,并且能够接受偶尔的冲突重试,选择乐观锁。

由于这个优惠卷抢票属于,读多写少这种场景,那么我们选型的话选择乐观锁会比较合适。

数据库乐观锁解决超卖问题
超卖问题分析

假设我们现在库存store = 1 ,那么此时线程一 判断之后 store > 0 那么这个时候线程一开始进行库存缩减,但是此时库存还没开始在数据库中扣减,此时线程二开始查询,发现 store > 0,那么线程二也认定自己抢票成功,开始扣减库存,那么此时就会出现store 减少了两次,那么库存就变成了-1,假设在线程二查询之后,线程一库存扣减之前,线程三又进行了判断,那么此时库存就又变成了-2。这是超卖问题的一个场景。
乐观锁,版本号法:

版本号法:也就是说引入一个新的字段用来检测当前我查询到的version,和我修改时的version是否是同一个,这样的话保证不会出现超卖问题,但是同样,会出现大量失败情况导致错误率极高。不推荐使用
CAS方案:

比较我查询到的库存值跟我更新后的库存值判断。但是这种情况也会出现上述的那种限制,所以我们只要判断扣减前的库存是否大于0即可,那么这种情况效率高,且不会出现大幅度失败。这个主要还是依靠mysql自身的行级锁机制进行的。属于悲观锁的一种范畴。

MySQL 中的行级锁可以确保同一行的数据在同一时间只能被一个事务修改,从而避免了并发更新导致的数据不一致问题。在代码中,当多个线程同时尝试更新同一行数据时,MySQL 会自动对该行数据加锁,以确保只有一个线程能够成功执行更新操作,其他线程会被阻塞直到锁释放。

这种方案解决较上一种方案会好一点,但依旧不是好的解决方案,因为对于这种场景下,阻塞依旧是不可避免的。

java 复制代码
 boolean voucherId1 = iSeckillVoucherService.update()
         .setSql("stock = stock - 1")
         .eq("voucher_id", voucherId)
         .gt("stock", 0)
         .update();

一人一单问题

我们只需要用户下单之后,把用户订单信息,存入Redis中,只需要到时候读取这个信息判断有多少次下单即可。

java 复制代码
// 这个位置采用优惠卷id 和 用户 id作为键,储存对应的信息,避免抢其他票时出现误判
String keyUserOrder = "user:order:" + voucherId + user.getId();
Integer count = 0;
try{
    count  = Integer.valueOf(stringRedisTemplate.opsForValue().get(keyUserOrder));
    if (count >= 1) {
        return Result.fail("用户已购买");
    }
}catch (Exception e){
    log.error("第一次抢票");
}
// 这个是抢票成功时,将订单信息保存。值自增用来判断是否是第一次购买
Long orderCount = stringRedisTemplate.opsForValue().increment(keyUserOrder, 1);
新的实现思路

既然是抢劵,那我确实无法避免这个内容。我可以说实现一个优惠卷池,将这个内容存入Redis中,抢到的话就生成订单,这样的话也没有超卖问题。只不过还得解决一人一单。效率较上面这种会更好一点。

实现步骤
  • 初始化优惠券池:将每张优惠券的信息预先存入Redis。
  • 抢券逻辑:用户请求时,从Redis中获取一张优惠券。
  • 生成订单:抢到优惠券后,生成订单并将订单信息和优惠券信息关联起来。
  • 一人一单检查:确保同一用户不能重复抢券。
代码示例

以下是一个简单的代码实现示例:

初始化优惠券池:

java 复制代码
// 初始化优惠券池,将100张优惠券信息存入Redis
String voucherPoolKey = "voucher:pool";
for (int i = 1; i <= 100; i++) {
    // 假设优惠券信息是一个简单的字符串,这里可以是更复杂的对象
    stringRedisTemplate.opsForList().leftPush(voucherPoolKey, "voucher_" + i);
}

抢券逻辑

java 复制代码
public Result grabVoucher(Long voucherId, Long userId) {
    // Redis中的键
    String voucherPoolKey = "voucher:pool";
    String userOrderKey = "user:order:" + voucherId + ":" + userId;

    // 检查用户是否已经抢过
    if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(userOrderKey))) {
        return Result.fail("用户已购买");
    }

    // 从Redis中弹出一张优惠券
    String voucher = stringRedisTemplate.opsForList().rightPop(voucherPoolKey);
    if (voucher == null) {
        return Result.fail("优惠券已抢完");
    }

    // 抢券成功,生成订单
    // 这里假设订单生成成功,实际应用中需要加入订单生成逻辑
    String orderInfo = "order_for_" + voucher + "_by_user_" + userId;

    // 记录用户抢券信息
    stringRedisTemplate.opsForValue().set(userOrderKey, orderInfo);

    return Result.ok("抢券成功", orderInfo);
}

订单生成

实际的订单生成过程可能涉及复杂的业务逻辑和数据库操作,这里简化为成功后记录用户抢券信息。

一人一单的实现

在用户抢券前,检查用户是否已经抢过,通过Redis键的存在性判断。

java 复制代码
// 检查用户是否已经抢过
if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(userOrderKey))) {
    return Result.fail("用户已购买");
}
优点
  • 高效处理并发:Redis的高吞吐量和低延迟使得这种方案在高并发场景下表现出色。
  • 避免超卖:优惠券从Redis中弹出后即减少,确保不会超卖。
  • 简单实现一人一单:通过Redis键值对存储和检查用户抢券信息,轻松实现一人一单的限制。
  • 简化订单生成:在抢券成功后,立即生成订单并将信息存储在Redis中,减少了数据库操作的复杂性。

或者我感觉这个并发场景比较大的情况下,使用RabbitMQ削峰填谷,做限流也可以。

相关推荐
森林猿16 分钟前
mongodb-数据备份和恢复
数据库·mongodb
oscube1 小时前
Apache AGE中的图
数据库·apache
科学的发展-只不过是读大自然写的代码1 小时前
qt播放视频
数据库·qt·音视频
激昂~逐流1 小时前
Qt使用sqlite数据库及项目实战
数据库·qt·sqlite·学生信息管理系统
svygh1232 小时前
数据库性能优化系统设计
数据库·性能优化·软件设计·系统设计·设计文档
云烟成雨TD2 小时前
Redis 7.x 系列【19】管道
redis·缓存·高性能
wilsonzane3 小时前
Mongodb性能优化方法
数据库·mongodb
InterestingFigure3 小时前
Java 使用sql查询mongodb
java·开发语言·数据库·sql·mongodb
吹吹晚风-3 小时前
深入Django(三)
数据库·django·sqlite
xyh20043 小时前
python 10个自动化脚本
数据库·python·自动化