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.4 分布式锁
      • 4.4.1 分布式锁介绍
      • 4.4.2 Redis分布式锁的实现核心思路
      • 4.4.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.返回订单IDreturn 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.4 分布式锁

4.4.1 分布式锁介绍

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

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

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

常见的分布式锁有三种:

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

本案例使用Redis分布式锁。

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

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

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

4.4.3 代码实现分布式锁

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

// com.star.redis.dzdp.utils.ILockpublic 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;}@Overridepublic boolean tryLock(long timeout) {// 1.获取线程IDlong 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);}@Overridepublic 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为面试赋能(持续更新中…)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

最新版手机软件App下载排行网站源码/App应用商店源码

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 一款简洁蓝色的手机软件应用app下载排行&#xff0c;app下载平台&#xff0c;最新手机app发布网站响应式织梦模板。 主要有&#xff1a;主页、app列表页、app介绍详情页、新闻资讯列…

Linux中磁盘的分区,格式化,挂载和文件系统的修复

一.分区工具 1.分区工具介绍 fdisk 2t及以下分区 推荐 (分完区不保存不生效&#xff0c;有反悔的可能) gdisk 全支持 推荐 parted 全支持 不推荐 ( 即时生效&#xff0c;分完立即生效) 2.fdisk 分区,查看磁盘 格式:fdisk -l [磁盘设备] fdisk -l 查看…

网络协议——RSTP(快速生成树)与MSTP(多实例生成树)

一. RSTP 1. STP的不足 1、依靠计时器超时的方式进行收敛导致它的收敛时间需要30到50秒 2、端口状态和端口角色没有细致区分&#xff0c;指导数据转发依靠的不是端口状态而是端口所扮演角色。 3、如果拓扑频繁变化导致用户通信质量差&#xff0c;甚至通信中断&#xf…

详解cmake简单语法与使用

注意&#xff1a;这是一篇cmake入门浅显的文章&#xff0c;深入学习的话没必要阅读。 CMake的使用流程及其语法非常丰富&#xff08;其实就是过于灵活&#xff0c;一个项目一个风格&#xff0c;看上去相当麻烦&#xff09;&#xff0c;下面逐步介绍一些核心概念和常用命令&…

排序:冒泡排序,直接插入排序,简单选择排序,希尔排序,快速排序,堆排序,二路归并排序

目录 一.冒泡排序 代码如下 冒泡排序时间复杂度分析 二.直接插入排序 直接插入排序时间复杂度分析 直接插入排序优化&#xff1a;折半插入排序 三.简单选择排序 简单选择排序优化&#xff1a;双向选择排序 选择排序时间复杂度 双向选择排序时间复杂度 四.希尔排序 希…

rust学习(recursive mutex 实现)

问题&#xff1a; 编写如下代码的时候出现死锁&#xff1a; pub fn test_double_lock() {let t Arc::new(Mutex::new(1));let t1 t.clone();let t2 t.clone();let h std::thread::spawn(move || {println!("hello trace1");let l1 t1.lock().unwrap();println…

达梦的归档日志参数ARCH_RESERVE_TIME测试

达梦的参数ARCH_RESERVE_TIME测试 前面有提到和oracle相比&#xff0c;达梦的归档日志相关参数有个比较特别&#xff0c;可以通过设置它去规定归档日志的保留时间。 ARCH_RESERVE_TIME&#xff1a;归档日志保留时间&#xff0c;单位分钟&#xff0c;取值范围 0~2147483647。只…

黄金白银价格上涨是投资机会吗?

黄金和白银&#xff0c;作为历史悠久的贵重金属&#xff0c;一直以来都被投资者视为避险资产。近年来&#xff0c;随着全球经济环境的变动&#xff0c;我们观察到黄金与白银的价格不断攀升&#xff0c;这是否预示着投资机会的到来&#xff1f;今天&#xff0c;就让我们来深度探…

R+VIC模型融合实践技术应用及未来气候变化模型预测

在气候变化问题日益严重的今天&#xff0c;水文模型在防洪规划&#xff0c;未来预测等方面发挥着不可替代的重要作用。目前&#xff0c;无论是工程实践或是科学研究中都存在很多著名的水文模型如SWAT/HSPF/HEC-HMS等。虽然&#xff0c;这些软件有各自的优点&#xff1b;但是&am…

如何在 Ubuntu 14.04 上使用 Rsyslog、Logstash 和 Elasticsearch 实现日志集中管理

Elastic 的一篇文章 介绍 理解组织生成的数百万条日志行可能是一个艰巨的挑战。一方面&#xff0c;这些日志行提供了对应用程序性能、服务器性能指标和安全性的视图。另一方面&#xff0c;日志管理和分析可能非常耗时&#xff0c;这可能会阻碍对这些日益必要的服务的采用。 …

【软考】哈希表

目录 一、概念1.1 定义 二、哈希函数的构造方法2.1 说明2.2 特性 三、处理冲突的方法3.1 说明3.2 开放定址法3.2.1 说明3.2.2 线性探测 3.3 链地址法3.4 再哈希法3.5 建立公共溢出区 四、哈希表的查找4.1 查找过程4.2 查找特点4.3 装填因子 一、概念 1.1 定义 1.一般存储结构由…

Es中bool 查询中的四个(must must_not should filter)

1.must :相当于and 2.must_not :相当于not 3.should:相当于or 4. filter:过滤 gte 大于 gt大于 lte小于等于 lt小于 使用示例&#xff1a; {“bool”:{“must”:{“match”:{“title”:”how to make millons “}},“must_not”:{“match”:{“tag”:”spam“}},“should”:[{…

C++11 用户自定义字面量

在 C 中&#xff0c;operator"" 是用户定义字面量&#xff08;User-Defined Literals&#xff09;的一部分&#xff0c;它允许程序员扩展现有的字面量类型或者创建新的字面量类型。用户定义字面量是在 C11 标准中引入的特性&#xff0c;主要用于提供更易读、更具表达…

S7-1500F和S7-1200F安全PLC实现安全相关控制器与智能设备PN通信的程序示例

S7-1500F和S7-1200F安全PLC实现安全相关控制器与智能设备PN通信的程序示例 在TIA 安全系统中具有PROFINET接口的S7-1500F和S7-1200F CPU 之间可以进行安全相关的控制器与智能设备通信。 通信通过两个安全应用程序指令进行,即SENDDP指令用于发送数据,而RCVDP指令用于接收数据…

Redis+lua脚本限制ip多次输入错误密码

Redislua脚本限制ip多次输入错误密码 不能锁username&#xff0c;因为如果有人恶意保留破解密码的话。会导致用户本人无法登录。 这里我采用 以ip的方式进行锁定。利用redis 设置key&#xff1a;ip。value&#xff1a;当前ip尝试登录的次数 实现逻辑 逻辑简单&#xff0c;假设…

30个商业赚钱的思考(上)

从一名一线技术从业者&#xff0c;到如今技术创业者&#xff0c;不同角色&#xff0c;有了对技术更加深刻的认识和理解。所谓创业、所谓商业&#xff0c;本质就是打通产品、流量和变现的过程。​ 本小节精选30篇商业赚钱思考内容&#xff0c;覆盖商业、认知、营销、管理、创业…

centos7部署zabbix6.4.9

文章目录 [toc]一、环境准备1&#xff09;部署lnmp2&#xff09;修改配置文件3&#xff09;安装数据库 二、部署zabbix1&#xff09;下载zabbix2&#xff09;安装zabbix服务端3&#xff09;修改配置4&#xff09;开机启动5&#xff09;安装客户端 三、登录配置1&#xff09;访问…

在Windows电脑上上传iOS应用至App Store

引言 &#x1f4f1; 使用UniApp开发iOS应用十分便捷&#xff0c;一套代码即可兼容多个平台。然而&#xff0c;UniApp开发iOS应用需要进行证书打包和将IPA文件上传至App Store&#xff0c;这两个步骤通常需要在Mac电脑上完成。那么&#xff0c;如果我们使用的是Windows开发环境…

three.js跟着教程实现VR效果(四)

参照教程&#xff1a;https://juejin.cn/post/6973865268426571784&#xff08;作者&#xff1a;大帅老猿&#xff09; 1.WebGD3D引擎 用three.js &#xff08;1&#xff09;使用立方体6面图 camera放到 立方体的中间 like “回” 让贴图向内翻转 &#xff08;2&#xff09;使…

机器学习 —— 使用机器学习进行情感分析 详细介绍版

机器学习 —— 使用机器学习进行情感分析 详细介绍版 机器学习 —— 使用机器学习进行情感分析 演示版 目录 一、 绪论二、 数据处理1. 构建CSV文档2. 构建模型前的思考2.1. 问题2.2. 解决方法 3. 读取数据4. 用正则表达式来进行对特定符号的剔除5. 使用口袋模型进行文本处理和…