Redis从入门到精通(七)Redis实战(四)库存超卖、一人一单与Redis分布式锁

↑↑↑请在文章开头处下载测试项目源代码↑↑↑

文章目录

    • 前言
    • 4.3 优惠券秒杀
      • 4.3.4 库存超卖问题及其解决
        • 4.3.4.1 问题分析
        • 4.3.4.2 问题解决
      • 4.3.5 一人一单需求
        • 4.3.5.1 需求分析
        • 4.3.5.2 代码实现
        • 4.3.5.3 并发问题
        • 4.3.5.4 悲观锁解决并发问题
        • 4.3.5.5 集群环境下的并发问题
      • 4.3.6 分布式锁
        • 4.3.6.1 分布式锁介绍
        • 4.3.6.2 Redis分布式锁的实现核心思路
        • 4.3.6.3 代码实现分布式锁

前言

Redis实战系列文章:

Redis从入门到精通(四)Redis实战(一)短信登录
Redis从入门到精通(五)Redis实战(二)商户查询缓存
Redis从入门到精通(六)Redis实战(三)优惠券秒杀

4.3 优惠券秒杀

4.3.4 库存超卖问题及其解决

4.3.4.1 问题分析

如上图所示,线程1查询库存,判断当前库存是1,正准备扣减库存,但还没来得及扣减完成,此时线程2也过来查询库存,线程2的查询结果也必然是1,因此也去扣减库存。最终结果是,线程1和线程2都扣减库存,但总库存只有1,从而出现库存超卖问题。

库存超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁。

常见的锁分为悲观锁和乐观锁:

乐观锁一般有一个版本号,每次操作数据都会对版本号+1,提交数据后,会校验版本号是否比之前大1,如果是则说明操作成功,如果不是则说明数据还被其他线程修改过,则操作失败。如下图:

4.3.4.2 问题解决

本项目采用的是校验库存是否被修改过。 修改后的代码如下:

// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl#seckillVoucher()

// 4.扣减库存
// boolean update = seckillVoucherService.update().setSql("stock = stock - 1")
//        .eq("voucher_id", voucherId).update();

// 修改方案一
// where voucher_id = ? and stock = ?
boolean update = seckillVoucherService.update().setSql("stock = stock - 1")
    .eq("voucher_id", voucherId)
    .eq("stock", seckillVoucher.getStock())
    .update();

以上代码的含义是,在扣减库存时需要校验库存是否和查询时的库存一致,一致的话则说明没有其他人修改过库存,是安全的,可以进行扣减;否则不能进行扣减。

但以上代码还是有一点问题的,假设有100个线程同时拿到了100个库存,然后同时进行库存扣减,正常来讲所有线程都可以成功扣减,但使用以上代码时只有一个线程可以成功扣减(where voucher_id = ? and stock = 100),其余99个线程都会失败。这就导致失败率太高。

我们可以做如下修改:

// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl#seckillVoucher()

// 修改方案二
// where voucher_id = ? and stock > 0
boolean update = seckillVoucherService.update().setSql("stock = stock - 1")
        .eq("voucher_id", voucherId)
        .gt("stock", 0)
        .update();

以上代码中,不管其他线程是否扣减库存,只要判断出当前库存还大于0,就说明是安全的,当前线程就可以进行扣减。 这样也可以解决库存超卖问题。

4.3.5 一人一单需求

4.3.5.1 需求分析

现在有一个需求:同一个秒杀优惠券,一个用户只能下一单。

目前情况下,一个用户可以无限制地抢优惠券,因此要实现一人一单,就需要增加以下逻辑:在秒杀已开始、且库存充足的情况下,根据优惠券ID和用户ID查询是否已有订单,如果已有订单,则不能再下单。 如下图:

4.3.5.2 代码实现

在VoucherOrderServiceImpl实现类的seckillVoucher()方法中增加一人一单逻辑:

// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl#seckillVoucher()

// 3.判断库存是否充足...

// 增加一人一单规则
// 根据优惠券ID和用户ID查询订单是否已存在
int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();
log.info("old order count = {}", count);
if(count > 0) {
    // 该用户已下过单
    return BaseResult.setFail("每个帐号只能抢购一张优惠券!");
}

// 4.扣减库存...

简单测试下,调用/voucher/seckill/order接口:

4.3.5.3 并发问题

假设一个线程1过来,根据优惠券ID和用户ID查询订单不存在,准备进行扣减库存和创建订单的动作,但还没来得及完成,另一个线程2也进来了,线程2根据优惠券ID和用户ID查询订单的结果也是不存在,也进行扣减库存和创建订单的动作。最终结果是,创建了同一用户的两个订单。

我们可以在创建订单处打一个断点,调用/voucher/seckill/order接口,下单id=12的优惠券。如日志显示,线程2依次查询秒杀活动是否存在及在有效期内、判断该用户是否重复下单、扣减库存,最终停在断点处:

[http-nio-8081-exec-2] 开始秒杀下单...voucherId = 12, userId = 1012
// 查秒杀活动是否存在及在有效期内
[http-nio-8081-exec-2] ==>  Preparing: SELECT voucher_id,stock,create_time,begin_time,end_time,update_time FROM tb_seckill_voucher WHERE voucher_id=?
[http-nio-8081-exec-2] ==> Parameters: 12(Long)
[http-nio-8081-exec-2] <==      Total: 1
[http-nio-8081-exec-2] SeckillVoucher(voucherId=12, stock=999, createTime=Fri Apr 05 18:57:23 CST 2024, beginTime=Fri Apr 05 14:00:00 CST 2024, endTime=Sat Apr 06 18:00:00 CST 2024, updateTime=Fri Apr 05 19:01:44 CST 2024)
// 判断该用户是否重复下单
[http-nio-8081-exec-2] ==>  Preparing: SELECT COUNT( * ) FROM tb_voucher_order WHERE (voucher_id = ? AND user_id = ?)
[http-nio-8081-exec-2] ==> Parameters: 12(Long), 1012(Long)
[http-nio-8081-exec-2] <==      Total: 1
// 扣减库存
[http-nio-8081-exec-2] ==>  Preparing: UPDATE tb_seckill_voucher SET stock = stock - 1 WHERE (voucher_id = ? AND stock > ?)
[http-nio-8081-exec-2] ==> Parameters: 12(Long), 0(Integer)
[http-nio-8081-exec-2] <==    Updates: 1
[http-nio-8081-exec-2] update result = true
[http-nio-8081-exec-2] get orderId = 7354337750083960833

此时再次调用/voucher/seckill/order接口,下单id=12的优惠券。日志限制,新线程5仍然查询订单不存在,会直接创建订单:

[http-nio-8081-exec-5] 开始秒杀下单...voucherId = 12, userId = 1012
// 查秒杀活动是否存在及在有效期内
[http-nio-8081-exec-5] ==>  Preparing: SELECT voucher_id,stock,create_time,begin_time,end_time,update_time FROM tb_seckill_voucher WHERE voucher_id=?
[http-nio-8081-exec-5] ==> Parameters: 12(Long)
[http-nio-8081-exec-5] <==      Total: 1
[http-nio-8081-exec-5] SeckillVoucher(voucherId=12, stock=999, createTime=Fri Apr 05 18:57:23 CST 2024, beginTime=Fri Apr 05 14:00:00 CST 2024, endTime=Sat Apr 06 18:00:00 CST 2024, updateTime=Fri Apr 05 19:01:44 CST 2024)
// 判断该用户是否重复下单,仍然是没有
[http-nio-8081-exec-5] ==>  Preparing: SELECT COUNT( * ) FROM tb_voucher_order WHERE (voucher_id = ? AND user_id = ?)
[http-nio-8081-exec-5] ==> Parameters: 12(Long), 1012(Long)
[http-nio-8081-exec-5] <==      Total: 1
// 扣减库存
[http-nio-8081-exec-5] ==>  Preparing: UPDATE tb_seckill_voucher SET stock = stock - 1 WHERE (voucher_id = ? AND stock > ?)
[http-nio-8081-exec-5] ==> Parameters: 12(Long), 0(Integer)
[http-nio-8081-exec-5] <==    Updates: 1
[http-nio-8081-exec-5] update result = true
// 创建订单
[http-nio-8081-exec-5] get orderId = 7354337754378928129
[http-nio-8081-exec-5] ==>  Preparing: INSERT INTO tb_voucher_order ( id, user_id, voucher_id, pay_time ) VALUES ( ?, ?, ?, ? )
[http-nio-8081-exec-5] ==> Parameters: 7354337754378928129(Long), 1012(Long), 12(Long), 2024-04-05 19:06:33.4(Timestamp)
[http-nio-8081-exec-5] <==    Updates: 1

放开断点,原线程2继续创建订单:

// 线程2继续创建订单
[http-nio-8081-exec-2] ==>  Preparing: INSERT INTO tb_voucher_order ( id, user_id, voucher_id, pay_time ) VALUES ( ?, ?, ?, ? )
[http-nio-8081-exec-2] ==> Parameters: 7354337750083960833(Long), 1012(Long), 12(Long), 2024-04-05 19:06:32.351(Timestamp)
[http-nio-8081-exec-2] <==    Updates: 1

此时数据库订单表有两条订单记录:

4.3.5.4 悲观锁解决并发问题

乐观锁比较适合更新数据,此处是插入数据问题,因此可以使用悲观锁来处理。*我们可以把查询订单、扣减库存、创建订单这三步封装为一个方法,并在该方法上添加一把synchronized锁。

// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl

// 方法上添加synchronized锁
public synchronized BaseResult<Long> checkAndCreateVoucherOrder(Long voucherId, Long userId) {
    log.info("begin checkAndCreateVoucherOrder... voucherId = {}, userId = {}",
            voucherId, userId);
    // 1.增加一人一单规则
    int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();
    log.info("old order count = {}", count);
    if(count > 0) {
        // 该用户已下过单
        return BaseResult.setFail("每个帐号只能抢购一张优惠券!");
    }
    // 2.扣减库存
    boolean update = seckillVoucherService.update().setSql("stock = stock - 1")
            .eq("voucher_id", voucherId)
            .gt("stock", 0)
            .update();

    log.info("update result = {}", update);
    if(!update) {
        // 扣减库存失败,返回抢券失败
        return BaseResult.setFail("库存不足,抢券失败!");
    }
    // 3.创建订单
    VoucherOrder voucherOrder = new VoucherOrder();
    Long orderId = RedisIdWorker.nextId(stringRedisTemplate, "voucher_order");
    log.info("get orderId = {}", orderId);
    voucherOrder.setId(orderId);
    voucherOrder.setUserId(userId);
    voucherOrder.setVoucherId(voucherId);
    voucherOrder.setPayTime(new Date());
    voucherOrderService.save(voucherOrder);
    // 4.返回订单ID
    return BaseResult.setOkWithData(orderId);
}

再次以相同的步骤进行测试,日志打印如下:

[http-nio-8081-exec-5] 开始秒杀下单...voucherId = 13, userId = 1012
[http-nio-8081-exec-5] ==>  Preparing: SELECT voucher_id,stock,create_time,begin_time,end_time,update_time FROM tb_seckill_voucher WHERE voucher_id=?
[http-nio-8081-exec-5] ==> Parameters: 13(Long)
[http-nio-8081-exec-5] <==      Total: 1
[http-nio-8081-exec-5] SeckillVoucher(voucherId=13, stock=996, createTime=Fri Apr 05 19:30:37 CST 2024, beginTime=Fri Apr 05 14:00:00 CST 2024, endTime=Sat Apr 06 18:00:00 CST 2024, updateTime=Fri Apr 05 19:38:06 CST 2024)
// 线程5进入锁方法
[http-nio-8081-exec-5] begin checkAndCreateVoucherOrder... voucherId = 13, userId = 1012
[http-nio-8081-exec-5] ==>  Preparing: SELECT COUNT( * ) FROM tb_voucher_order WHERE (voucher_id = ? AND user_id = ?)
[http-nio-8081-exec-5] ==> Parameters: 13(Long), 1012(Long)
[http-nio-8081-exec-5] <==      Total: 1
[http-nio-8081-exec-5] old order count = 0
[http-nio-8081-exec-5] ==>  Preparing: UPDATE tb_seckill_voucher SET stock = stock - 1 WHERE (voucher_id = ? AND stock > ?)
[http-nio-8081-exec-5] ==> Parameters: 13(Long), 0(Integer)
[http-nio-8081-exec-5] <==    Updates: 1
[http-nio-8081-exec-5] update result = true
[http-nio-8081-exec-5] get orderId = 7354346232644370433
[http-nio-8081-exec-5] ==>  Preparing: INSERT INTO tb_voucher_order ( id, user_id, voucher_id, pay_time ) VALUES ( ?, ?, ?, ? )
[http-nio-8081-exec-5] ==> Parameters: 7354346232644370433(Long), 1012(Long), 13(Long), 2024-04-05 19:39:27.61(Timestamp)
[http-nio-8081-exec-5] <==    Updates: 1
// 线程5结束
// 线程6开始
[http-nio-8081-exec-6] 开始秒杀下单...voucherId = 13, userId = 1012
[http-nio-8081-exec-6] ==>  Preparing: SELECT voucher_id,stock,create_time,begin_time,end_time,update_time FROM tb_seckill_voucher WHERE voucher_id=?
[http-nio-8081-exec-6] ==> Parameters: 13(Long)
[http-nio-8081-exec-6] <==      Total: 1
[http-nio-8081-exec-6] SeckillVoucher(voucherId=13, stock=995, createTime=Fri Apr 05 19:30:37 CST 2024, beginTime=Fri Apr 05 14:00:00 CST 2024, endTime=Sat Apr 06 18:00:00 CST 2024, updateTime=Fri Apr 05 19:39:27 CST 2024)
// 线程6进入锁方法
[http-nio-8081-exec-6] begin checkAndCreateVoucherOrder... voucherId = 13, userId = 1012
[http-nio-8081-exec-6] ==>  Preparing: SELECT COUNT( * ) FROM tb_voucher_order WHERE (voucher_id = ? AND user_id = ?)
[http-nio-8081-exec-6] ==> Parameters: 13(Long), 1012(Long)
[http-nio-8081-exec-6] <==      Total: 1
// 线程6查询发现订单已存在,不再继续往下执行
[http-nio-8081-exec-6] old order count = 1

查看此时的数据库,只有1条voucher_id=13的优惠券订单:

可见,加synchronized锁之后,只有一个线程可以进入checkAndCreateVoucherOrder()方法,也就是只有一个线程可以顺利地创建订单。等锁释放后,其他线程会发现订单已创建,而直接返回错误信息。

4.3.5.5 集群环境下的并发问题

通过加synchronized锁可以解决在单机情况下的“一人一单”安全问题,但是在集群模式下就不行了。 如下图:

集群模式下,由于们部署了多个tomcat,每个tomcat都有一个属于自己的jvm。假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,它们的锁对象是同一个,是可以实现互斥的。

但是如果服务器B的tomcat内部,又有两个线程,它们的锁对象写的内容虽然和服务器A一样,但是由于是不同的jvm所以锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥。

这就是集群环境下,synchronized锁失效的原因,在这种情况下,就需要使用分布式锁来解决这个问题。

4.3.6 分布式锁

4.3.6.1 分布式锁介绍

分布式锁即满足分布式系统或集群模式下多进程可见并且互斥的锁。它的核心思想就是,让所有线程都使用同一把锁,从而让线程串行执行。 如图:

分布式锁一般需要满足以下条件:

  • 可见性:多个线程能看到相同的结果。
  • 互斥性:互斥是分布式锁的最基本的条件,使得程序串行执行。
  • 高可用:程序不易崩溃,时时刻刻都保证较高的可用性。
  • 高性能:由于加锁本身就让性能降低,所有对于分布式锁则要求较高的加锁性能和释放锁性能。
  • 安全性:保证数据安全。

常见的分布式锁有三种:

  • MySQL:MySQL本身就带有锁机制,但是由于MySQL性能本身一般,所以使用MySQL作为分布式锁比较少见。
  • Redis:Redis作为分布式锁是非常常见的一种使用方式,利用其SETNX方法,如果插入Key成功,则表示获得到了锁,其他线程则无法获得到锁。
  • Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案。

本案例使用Redis分布式锁。

4.3.6.2 Redis分布式锁的实现核心思路

如上图所示,利用Redis的SETNX方法。当第一个线程进入时,Redis中没有"lock"这个Key,则SETNX方法返回true,表示成功获取到了锁,该线程继续执行其他业务逻辑,最后释放锁。

在释放锁之前,如果有第二个线程进来,由于Redis中已经存在"lock"这个Key,所以SETNX方法返回false,表示没有获取到锁,则等待一段时间后继续重试。

4.3.6.3 代码实现分布式锁

首先创建一个ILock接口,定义加锁和解锁的两个基本方法:

// com.star.redis.dzdp.utils.ILock

public interface ILock {

    /**
     * 尝试获取锁
     * @author hsgx
     * @since 2024/4/5 21:07
     * @param timeout 超时时间
     * @return boolean
     */
    boolean tryLock(long timeout);

    /**
     * 释放锁
     * @author hsgx
     * @since 2024/4/5 21:07
     * @param
     * @return void
     */
    void unlock();
    
}

然后创建一个SimpleRedisLock类实现ILock接口,重写基本方法:

// com.star.redis.dzdp.utils.SimpleRedisLock

@Slf4j
public class SimpleRedisLock implements ILock {
    
    private String key;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String key, StringRedisTemplate stringRedisTemplate) {
        this.key = key;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeout) {
        // 1.获取线程ID
        long threadId = Thread.currentThread().getId();
        // 2.获取锁,并设置超时时间
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock:" + key, threadId + "", timeout, TimeUnit.SECONDS);
        log.info("set to Redis : Key = {}, Value = {}. set result = {}", "lock:" + key, threadId, flag);
        // 3.返回
        return BooleanUtil.isTrue(flag);
    }

    @Override
    public void unlock() {
        // 1.释放锁
        Boolean flag = stringRedisTemplate.delete("lock:" + key);
        log.info("del from to Redis : Key = {}. del result = {}", "lock:" + key, flag);
    }

}

最后修改业务代码:

// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl

@Override
public BaseResult<Long> seckillVoucher(Long voucherId, Long userId) {
    log.info("开始秒杀下单...voucherId = {}, userId = {}", voucherId, userId);
    // 1.查询秒杀优惠券信息
    SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
    // 2.判断秒杀活动是否开启或结束
    if(seckillVoucher == null) {
        // 秒杀活动不存在
        return BaseResult.setFail("秒杀活动不存在!");
    } else if(seckillVoucher.getBeginTime().after(new Date())) {
        // 秒杀活动未开始
        log.info("beginTime = {}", seckillVoucher.getBeginTime());
        return BaseResult.setFail("秒杀尚未开始!");
    } else if(seckillVoucher.getEndTime().before(new Date())) {
        // 秒杀活动已结束
        log.info("endTime = {}", seckillVoucher.getEndTime());
        return BaseResult.setFail("秒杀已结束!");
    }
    log.info("{}", seckillVoucher.toString());
    // 3.判断库存是否充足
    if(seckillVoucher.getStock() < 1) {
        // 库存不足
        return BaseResult.setFail("库存不足,抢券失败!");
    }

    // 创建锁对象
    SimpleRedisLock simpleRedisLock = new SimpleRedisLock("voucher_order:" + userId, stringRedisTemplate);
    // 尝试获取锁
    boolean lock = simpleRedisLock.tryLock(1200);
    // 加锁失败,则说明该用户已有一条线程
    if(!lock) {
        return BaseResult.setFail("每个帐号只能抢购一张优惠券!");
    }
    // 加锁成功,则执行业务代码
    try {
        return checkAndCreateVoucherOrder(voucherId, userId);
    } finally {
        // 释放锁
        simpleRedisLock.unlock();
    }
}

下面模拟有3个线程同时到达,其日志打印如下:

// 用户ID=1012的线程10
[http-nio-8081-exec-10] token from client => ccedd4ea-c73e-42cd-b9f2-3a637cbcca9b
[http-nio-8081-exec-10] user from redis => {"id":1012,"phone":"18922102124","password":"","nickName":"18922102124","icon":"","createTime":1712041918000,"updateTime":1712041918000}
[http-nio-8081-exec-10] 开始秒杀下单...voucherId = 14, userId = 1012
[http-nio-8081-exec-10] ==>  Preparing: SELECT voucher_id,stock,create_time,begin_time,end_time,update_time FROM tb_seckill_voucher WHERE voucher_id=?
// 用户ID=1012的线程1
[http-nio-8081-exec-1] token from client => ccedd4ea-c73e-42cd-b9f2-3a637cbcca9b
[http-nio-8081-exec-10] ==> Parameters: 14(Long)
// 用户ID=1012的线程9
[http-nio-8081-exec-9] token from client => ccedd4ea-c73e-42cd-b9f2-3a637cbcca9b
[http-nio-8081-exec-9] user from redis => {"id":1012,"phone":"18922102124","password":"","nickName":"18922102124","icon":"","createTime":1712041918000,"updateTime":1712041918000}
[http-nio-8081-exec-1] user from redis => {"id":1012,"phone":"18922102124","password":"","nickName":"18922102124","icon":"","createTime":1712041918000,"updateTime":1712041918000}
[http-nio-8081-exec-10] <==      Total: 1
[http-nio-8081-exec-10] SeckillVoucher(voucherId=14, stock=999, createTime=Fri Apr 05 19:36:15 CST 2024, beginTime=Fri Apr 05 14:00:00 CST 2024, endTime=Sat Apr 06 18:00:00 CST 2024, updateTime=Fri Apr 05 21:27:47 CST 2024)
[http-nio-8081-exec-1] 开始秒杀下单...voucherId = 14, userId = 1012
// 线程10拿到了互斥锁
[http-nio-8081-exec-10] set to Redis : Key = lock:voucher_order:1012, Value = 42. set result = true
[http-nio-8081-exec-9] 开始秒杀下单...voucherId = 14, userId = 1012
[http-nio-8081-exec-10] begin checkAndCreateVoucherOrder... voucherId = 14, userId = 1012
[http-nio-8081-exec-1] ==>  Preparing: SELECT voucher_id,stock,create_time,begin_time,end_time,update_time FROM tb_seckill_voucher WHERE voucher_id=?
[http-nio-8081-exec-9] ==>  Preparing: SELECT voucher_id,stock,create_time,begin_time,end_time,update_time FROM tb_seckill_voucher WHERE voucher_id=?
[http-nio-8081-exec-1] ==> Parameters: 14(Long)
[http-nio-8081-exec-9] ==> Parameters: 14(Long)
[http-nio-8081-exec-10] ==>  Preparing: SELECT COUNT( * ) FROM tb_voucher_order WHERE (voucher_id = ? AND user_id = ?)
[http-nio-8081-exec-10] ==> Parameters: 14(Long), 1012(Long)
[http-nio-8081-exec-1] <==      Total: 1
[http-nio-8081-exec-1] SeckillVoucher(voucherId=14, stock=999, createTime=Fri Apr 05 19:36:15 CST 2024, beginTime=Fri Apr 05 14:00:00 CST 2024, endTime=Sat Apr 06 18:00:00 CST 2024, updateTime=Fri Apr 05 21:27:47 CST 2024)
[http-nio-8081-exec-10] <==      Total: 1
[http-nio-8081-exec-9] <==      Total: 1
[http-nio-8081-exec-10] old order count = 0
[http-nio-8081-exec-9] SeckillVoucher(voucherId=14, stock=999, createTime=Fri Apr 05 19:36:15 CST 2024, beginTime=Fri Apr 05 14:00:00 CST 2024, endTime=Sat Apr 06 18:00:00 CST 2024, updateTime=Fri Apr 05 21:27:47 CST 2024)
// 线程1没有拿到互斥锁
[http-nio-8081-exec-1] set to Redis : Key = lock:voucher_order:1012, Value = 33. set result = false
// 线程9没有拿到互斥锁
[http-nio-8081-exec-9] set to Redis : Key = lock:voucher_order:1012, Value = 41. set result = false
// 最终只有线程10进行扣减库存和创建订单
[http-nio-8081-exec-10] ==>  Preparing: UPDATE tb_seckill_voucher SET stock = stock - 1 WHERE (voucher_id = ? AND stock > ?)
[http-nio-8081-exec-10] ==> Parameters: 14(Long), 0(Integer)
[http-nio-8081-exec-10] <==    Updates: 1
[http-nio-8081-exec-10] update result = true
[http-nio-8081-exec-10] get orderId = 7354374484939243521
[http-nio-8081-exec-10] ==>  Preparing: INSERT INTO tb_voucher_order ( id, user_id, voucher_id, pay_time ) VALUES ( ?, ?, ?, ? )
[http-nio-8081-exec-10] ==> Parameters: 7354374484939243521(Long), 1012(Long), 14(Long), 2024-04-05 21:29:05.933(Timestamp)
[http-nio-8081-exec-10] <==    Updates: 1
[http-nio-8081-exec-10] del from to Redis : Key = lock:voucher_order:1012. del result = true

可见,三条线程只有一条线程可以拿到锁,并执行扣减库存和创建订单的逻辑,其余两条线程均拿不到锁,也就无法扣减库存和创建订单。

本节完,更多内容请查阅分类专栏:Redis从入门到精通

感兴趣的读者还可以查阅我的另外几个专栏:

  • SpringBoot源码解读与原理分析(已完结)
  • MyBatis3源码深度解析(已完结)
  • 再探Java为面试赋能(持续更新中…)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/519514.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

状态优先级

文章目录 状态优先级1. 进程状态1.1 进程状态查看1.2 僵尸进程1.3 孤儿进程 2.进程优先级2.1 基本概念2.2 查看系统进程2.3 PRI and NI2.4 PRI vs NI 3. 查看进程优先级的命令3.1 top命令更改nice3.2 其他概念 状态优先级 1. 进程状态 看看Linux内核源代码怎么说 为了弄明白…

爬虫 新闻网站 并存储到CSV文件 以红网为例 V2.0 (控制台版)升级自定义查询关键词、时间段,详细注释

爬虫&#xff1a;红网网站&#xff0c; 获取指定关键词与指定时间范围内的新闻&#xff0c;并存储到CSV文件 V2.0&#xff08;控制台版&#xff09; 爬取目的&#xff1a;为了获取某一地区更全面的在红网已发布的宣传新闻稿&#xff0c;同时也让自己的工作更便捷 对比V1.0升级的…

设计模式总结-原型设计模式

原型设计模式 模式动机模式定义模式结构模式分析深拷贝和浅拷贝原型模式实例与解析实例一&#xff1a;邮件复制&#xff08;浅克隆&#xff09;实例二&#xff1a;邮件复制&#xff08;深克隆&#xff09; 模式动机 在面向对象系统中&#xff0c;使用原型模式来复制一个对象自…

人脸识别:Arcface--loss+code

之前只接触过传统方法的人脸识别算法&#xff0c;本以为基于深度学习的方法会使用对比损失之类的函数进行训练&#xff0c;但是Arcface算法基于softmax进行了创新&#xff0c;本文未深究其详细的loss公式原理&#xff0c;在大致明白其方向下&#xff0c;运行了代码&#xff0c;…

2024.4.2-[作业记录]-day07-CSS 盒子模型(显示模式、盒子模型)

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 作业 2024.4.2 学习笔记CSS标签元素显示模式1 块元素2 行内元素3 行内块元素4…

计算机组成结构—存储器概述

目录 一、存储器的分类 1.按存储介质分类 半导体存储器 磁性材料存储器 光盘存储器 2.按存取方式分类 随机存储器&#xff08;Random Access Memory&#xff0c; RAM&#xff09; 只读存储器&#xff08;Read Only Memory&#xff0c; ROM&#xff09; 串行访问存储器…

如何在iPhone上恢复永久删除的照片?

2007 年&#xff0c;Apple Inc. 推出了这款震撼人心的智能手机&#xff0c;后来被称为 iPhone。您会惊讶地发现&#xff0c;迄今为止&#xff0c;Apple Inc. 已售罄 7 亿台 iPhone 设备。根据 2023 年 8 月的一项调查数据&#xff0c;95% 的智能手机利润都落入了苹果公司的口袋…

下载页面上的视频

引言&#xff1a;有些页面上的视频可以直接右键另存为或者F12检索元素找到视频地址打开后保存&#xff0c;但有些视频页面是转码后的视频&#xff0c;不能直接另存为视频格式&#xff0c;可以参考下本方法 以该页面视频为例&#xff1a;加载中...点击查看详情https://wx.vzan.c…

#SOP#-如何使用AI辅助论文创作

#SOP#-如何使用AI辅助论文创作 ——2024.4.6 “在使用工具的时候&#xff0c;要做工具的主人” 最终交付物&#xff1a; 一份可执行的AI辅助创作论文的指导手册 交付物质量要求&#xff1a; 不为任何AI大模型付费&#xff01;不为任何降重网站付费&#xff01;通过知网检查论…

苍穹外卖08(地址簿功能,用户下单功能,订单支付全过程,内网穿透Cpolar)

目录 一、导入地址簿功能代码 1. 需求分析和设计 1 产品原型 2 接口设计 2. 代码导入 3. 功能测试 二、用户下单 1. 需求分析和设计 1 产品原型 2 接口设计 3 表设计 2. 代码开发 1 DTO设计 2 VO设计 3 开发代码 3. 功能测试 三、订单支付 1 微信支付介绍 1 …

Java 学习和实践笔记(51):二分法查找(折半检索)

二分法查找&#xff08;折半检索&#xff09;又叫binary search. 要在一堆数据中查找是否存在某一个已知数&#xff0c;二分法查找的步骤&#xff1a; 第一步&#xff0c;对数据实现排序 第二步&#xff0c;将该数与排序后的数据集的中间一个数进行比较 第三步&#xff0c;…

非关系型数据库(缓存数据库)redis的性能管理

目录 一.Redis性能管理 1.Info Memory——查看Redis内存使用 2.内存碎片率 3. 内存使用率 4.内存回收key 二.缓存的穿透&#xff0c;击穿和雪崩 1.缓存的穿透 1.1 问题描述 1.2 缓存穿透发生的条件 1.3 缓存穿透发生的原因 1.4 解决方案 2 缓存的击穿 2.1 问题描…

使用SVD将图像压缩四分之一(MATLAB)

SVD压缩前后数据量减少的原因在于&#xff0c;通过奇异值分解&#xff08;SVD&#xff09;&#xff0c;我们将原始数据&#xff08;如图像&#xff09;转换成了一种更加紧凑的表示形式。这种转换依赖于数据内部的结构和相关性&#xff0c;以及数据中信息的不均匀分布。 让我们…

以 2021inCTF-DeadlyFastGraph 入门 JSC利用

前言 最近一直在入门浏览器的利用&#xff0c;然后一直都在搞 V8&#xff0c;然后接触的比较多的都是一些混淆、越界的洞&#xff0c;希望后面可以入门 jit 然后在今年的阿里云 CTF 中看到了一道 jsc 相关的题目&#xff0c;当时本来想做一做的&#xff0c;但是环境一直没有搭…

vLLM介绍

vLLM是伯克利大学LMSYS组织开源的大语言模型高速推理框架&#xff0c;旨在极大地提升实时场景下的语言模型服务的吞吐与内存使用效率。vLLM是一个快速且易于使用的库&#xff0c;用于 LLM 推理和服务&#xff0c;可以和HuggingFace 无缝集成。vLLM利用了全新的注意力算法「Page…

ZKP价值链路的垂直整合

1. ZKP proof生命周期 从ZKP&#xff08;zero-knowledge proof&#xff09;生命周期&#xff0c;先看围绕ZKP的价值链路形成&#xff1a; 1&#xff09;User intent用户意图&#xff1a;以某用户意图为起点&#xff0c;如想要在某zk-rollup上swap某token、证明其身份、执行某…

java数据结构与算法刷题-----LeetCode405. 数字转换为十六进制数

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 文章目录 分组位运算 分组位运算 这道题正常来说可以用转换7进制的思想来&…

加速度:电子元器件营销网站的功能和开发周期

据工信部预计&#xff0c;到2023年&#xff0c;我国电子元器件销售总额将达到2.1万亿元。随着资本的涌入&#xff0c;在这个万亿级赛道&#xff0c;市场竞争变得更加激烈的同时&#xff0c;行业数字化发展已是大势所趋。电子元器件B2B商城平台提升数据化驱动能力&#xff0c;扩…

算法学习18:动态规划

算法学习18&#xff1a;动态规划 文章目录 算法学习18&#xff1a;动态规划前言一、线性DP1.数字三角形&#xff1a;f[i][j] max(f[i - 1][j - 1] a[i][j], f[i - 1][j] a[i][j]);2.1最长上升子序列&#xff1a;f[i] max(f[i], f[j] 1);2.2 打印出最长子序列3.最长公共子序…

[从零开始学习Redis | 第九篇] 深入了解Redis数据类型

前言&#xff1a; 在现代软件开发中&#xff0c;数据存储和处理是至关重要的一环。为了高效地管理数据&#xff0c;并实现快速的读写操作&#xff0c;各种数据库技术应运而生。其中&#xff0c;Redis作为一种高性能的内存数据库&#xff0c;广泛应用于缓存、会话存储、消息队列…