【Redis核心知识】实现秒杀的三种方案

Redis秒杀方案

Redis性能很好,被大量使用于秒杀场景下,实现秒杀有以下几种方案:

方案一:使用商品ID作为分布式锁,加锁后扣减库存

该方案的实现流程为:

  • 用户发起秒杀请求到RedisRedis先使用商品ID作为key尝试加锁,保证只有一个用户进入之后流程,保证原子性
  • 如果加锁成功,则查询库存。如果库存充足,则扣减库存,代表秒杀成功;若库存不足,直接返回秒杀失败;

实现代码如下:

/**
 * Redis秒杀方法一:先加分布式锁,然后查询缓存,根据库存量数量进行后续操作:如果库存量大于零,则扣减库存,返回true;否则返回false;
 * @param goodId 商品ID
 * @return 成功返回true,失败返回false
 */
@Override
public Boolean secKillByRedisFun1(Integer goodId) {
    // 根据商品ID构造key
    String goodKey = "good-stock-" + goodId;
    String userId = Thread.currentThread().getName() + "-" + System.currentTimeMillis();
    // 使用商品作为锁,锁的粒度较大
    String lockId = "sec-kill-lock-" + goodId;
    return this.subStock(lockId, userId, goodKey);
}

/**
 * 使用分布式锁秒杀,加锁后再查询redis库存,最后扣减库存
 * @param lockId 锁ID
 * @param userId 用户ID
 * @param goodKey 商品ID
 * @return 秒杀成功返回 true,否则返回 false
 */
private boolean subStock(String lockId, String userId, String goodKey) {
    // 尝试先加锁,如果加锁成功再进行查询库存量,和扣减库存操作,此时只能有一个线程进入代码块
    if (redisLock.lock(lockId, userId, 4000)) {
        try {
            // 查询库存
            Integer stock = (Integer) redisTemplate.opsForValue().get(goodKey);
            if (stock == null) {
                System.out.println("商品不在缓存中");
            }
            // 如果剩余库存量大于零,则扣减库存
            if (stock > 0) {
                redisTemplate.opsForValue().decrement(goodKey);
                return true;
            } else {
                return false;
            }
        } finally {
            // 释放锁
            redisLock.unlock(lockId, userId);
        }
    }
    return false;
}

该方案存在一些缺点

  • 用户进来后都要抢锁,即便是库存量已经为零,仍然需要抢锁,这无疑带来了很多无用争抢;
  • 锁的是商品ID,锁粒度太大,并发性能可以进一步优化;

解决方案:

  • 抢锁前先查询库存,如果库存已经为零,则直接返回false,不必参与抢锁过程;
  • 使用商品ID+库存量作为锁,降低锁粒度,进一步提升并发性能;

方案二:使用商品ID+库存量作为分布式锁,加锁后扣减库存

该方案的实现流程为:

  • 用户发起秒杀请求到RedisRedis先查询库存量,然后根据商品ID+库存量作为key尝试加锁,保证只有一个用户进入之后流程,保证原子性
  • 如果加锁成功,则查询库存。如果库存充足,则扣减库存,代表秒杀成功;若库存不足,直接返回秒杀失败;

注意:第一步查询库存量后,可以添加判断库存是否为零的操作,这样就能过滤掉库存为零后的大量请求。

实现代码如下:

@Override
public Boolean secKillByRedisFun2(Integer goodId) {
    // 根据商品ID构造key
    String goodKey = "good-stock-" + goodId;
    // 查询库存,使用库存量作为锁,细化锁粒度,提高并发量
    Integer curStock = (Integer) redisTemplate.opsForValue().get(goodKey);
    if (curStock <= 0) {
        return false;
    }
    String userId = Thread.currentThread().getName() + "-" + System.currentTimeMillis();
    String lockId = "sec-kill-lock-" + goodId + "-" + curStock;
    return this.subStock(lockId, userId, goodKey);
}

/**
 * 使用分布式锁秒杀,加锁后再查询redis库存,最后扣减库存
 * @param lockId 锁ID
 * @param userId 用户ID
 * @param goodKey 商品ID
 * @return 秒杀成功返回 true,否则返回 false
 */
private boolean subStock(String lockId, String userId, String goodKey) {
    // 尝试先加锁,如果加锁成功再进行查询库存量,和扣减库存操作,此时只能有一个线程进入代码块
    if (redisLock.lock(lockId, userId, 4000)) {
        try {
            // 查询库存
            Integer stock = (Integer) redisTemplate.opsForValue().get(goodKey);
            if (stock == null) {
                System.out.println("商品不在缓存中");
            }
            // 如果剩余库存量大于零,则扣减库存
            if (stock > 0) {
                redisTemplate.opsForValue().decrement(goodKey);
                return true;
            } else {
                return false;
            }
        } finally {
            // 释放锁
            redisLock.unlock(lockId, userId);
        }
    }
    return false;
}

以上两种先加锁再查询库存量扣减库存的方案,是为了保证查询库存扣减库存操作的原子性,也可以使用lua脚本实现这两个操作的原子性,这样就不需要额外维护分布式锁的开销。

方案三:使用INCRDECR原子操作扣减库存

该方案直接使用DECR操作扣减库存,不需要提前查询缓存,代码简洁:

  • 如果返回值大于零,说明库存充足,表示秒杀成功;
  • 如果返回值小于零,说明库存不足,需要使用INCR操作恢复库存,秒杀失败;

实现代码如下:

/**
 * Redis 秒杀方案三:使用原子操作DECR和INCR扣减库存
 * @param goodId 商品ID
 * @return
 */
@Override
public Boolean secKillByRedisFun3(Integer goodId) {
    // 根据商品ID构造key
    String goodKey = "good-stock-" + goodId;
    Long stockCount = redisTemplate.opsForValue().decrement(goodKey);
    if (stockCount >= 0) {
        return true;
    } else {
        // 如果库存不够,则恢复库存
        redisTemplate.opsForValue().increment(goodKey);
        return false;
    }
}

不足:后期库存为零后,大量请求扣减库存后需要恢复库存,这是一个无用操作。

解决方案:可以提前查询库存,如果库存为零,直接返回false

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Redis、RabbitMQ和MyBatis可以结合使用来实现秒杀功能。 首先,Redis可以用作秒杀缓存层。当用户请求秒杀商品时,可以先从Redis中查询商品的库存信息。由于Redis的高性能和擅长处理高并发的特性,可以快速返回库存信息,减轻数据库的压力。同时,为了防止超卖现象的发生,在Redis中可以设置一个计数器,记录已经被抢购的商品数量,每次秒杀成功后即使库存减一,确保不会超过实际库存数量。 其次,RabbitMQ可以用来处理秒杀请求的异步处理。当用户发起秒杀请求后,可以将请求消息发送至RabbitMQ中的秒杀队列。然后,消费者可以异步地从队列中获取消息,进行处理。这样可以有效地削峰填谷,降低系统的压力,提高并发处理能力。另外,通过RabbitMQ还可以实现消息的延迟投递功能,可以设置一个定时任务,定时将未处理完的请求重新放入队列中进行处理。 最后,MyBatis可以用来操作数据库,处理秒杀请求的商品库存信息。当消费者从队列中获取到秒杀请求消息后,可以通过MyBatis来更新商品库存信息。MyBatis提供了强大的数据库操作功能,可以方便地将秒杀请求与数据库进行交互。同时,为了防止超卖现象的发生,可以在更新库存信息之前进行乐观锁的检查,确保库存足够的情况下才进行更新操作。 综上所述,使用Redis、RabbitMQ和MyBatis可以实现一个高效、可靠的秒杀系统。Redis用于缓存商品库存信息,RabbitMQ用于异步处理秒杀请求,MyBatis用于操作数据库。通过这三个工具的结合利用,可以满足高并发场景下的秒杀需求,提高系统的性能和可伸缩性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值