当前位置:

首页 > 编程开发 > 如何解决Springboot整合Redis中的超卖问题?

如何解决Springboot整合Redis中的超卖问题?

Redis Assistant
Redis Assistant

Redis Assistant 是一款为Redis设计的高效可视化管理与监控工具,提供多种操作模式,高效处理服务器数据。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

立即下载
¥19
Windows/macOS/Linux 2026-08-22
正版软件 Windows macOS软件 Linux 数据库管理工具

超卖简单代码写一段简单正常的超卖逻辑代码,多个用户同时操作同一段数据,探究出现的问题。Redis中存储一项数据信息,请求对应接口,获取商品数量信息;商品数量信息如果大于0,则扣减1,重新存储Redis中;运行代码测试问题。/***Redis数据库操作,超卖问题模拟*@author**/@RestControllerpublicclassRedisController{//引入String类型redis操作模板@AutowiredprivateStringRedisTemplatestringRedisTe

本文目录

      超卖简单代码

      写一段简单正常的超卖逻辑代码,多个用户同时操作同一段数据,探究出现的问题。

      Redis中存储一项数据信息,请求对应接口,获取商品数量信息;
      商品数量信息如果大于0,则扣减1,重新存储Redis中;
      运行代码测试问题。

      /**
       * Redis数据库操作,超卖问题模拟
       * @author 
       *
       */
      @RestController
      public class RedisController {
      	
      	// 引入String类型redis操作模板
      	@Autowired
      	private StringRedisTemplate stringRedisTemplate;
       
       
      	// 测试数据设置接口
      	@RequestMapping("/setStock")
      	public String setStock() {
      		stringRedisTemplate.opsForValue().set("stock", "100");
      		return "ok";
      	}
      	
      	// 模拟商品超卖代码
      	@RequestMapping("/deductStock")
      	public String deductStock() {
      		// 获取Redis数据库中的商品数量
      		Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
      		// 减库存
      		if(stock > 0) {
      			int realStock = stock -1;
      			stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
      			System.out.println("商品扣减成功,剩余商品:"+realStock);
      		}else {
      			System.out.println("库存不足.....");
      		}
      		return "end";
      	}
      }

      超卖问题

      单服务器单应用情况下

      在单应用模式下,使用jmeter压测。

      如何解决Springboot整合Redis中的超卖问题?

      如何解决Springboot整合Redis中的超卖问题?

       测试结果:

      如何解决Springboot整合Redis中的超卖问题?

      每个请求相当于一个线程,当几个线程同时拿到数据时,线程A拿到库存为84,这个时候线程B也进入程序,并且抢占了CPU,访问库存为84,最后两个线程都对库存减一,导致最后修改为83,实际上多卖出去了一件

      既然线程和线程之间,数据处理不一致,能否使用synchronized加锁测试?

      设置synchronized

      依旧还是先测试单服务器

      // 模拟商品超卖代码,
      	// 设置synchronized同步锁
      	@RequestMapping("/deductStock1")
      	public String deductStock1() {
      		synchronized (this) {
      			// 获取Redis数据库中的商品数量
      			Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
      			// 减库存
      			if(stock > 0) {
      				int realStock = stock -1;
      				stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
      				System.out.println("商品扣减成功,剩余商品:"+realStock);
      			}else {
      				System.out.println("库存不足.....");
      			}
      		}
      		return "end";
      	}

      数量100

      如何解决Springboot整合Redis中的超卖问题?

      重新压测,得到的日志信息如下所示: 

      如何解决Springboot整合Redis中的超卖问题?

       在单机模式下,添加synchronized关键字,的确能够避免商品的超卖现象!

      但是在分布式微服务中,针对该服务设置了集群,synchronized依旧还能保证数据的正确性吗?

      假设多个请求,被注册中心负载均衡,每个微服务中的该处理接口,都添加有synchronized,

      如何解决Springboot整合Redis中的超卖问题?

       依然会出现类似的超卖问题:

      synchronized只是针对单一服务器的JVM进行加锁,但是分布式是很多个不同的服务器,导致两个线程或多个在不同服务器上共同对商品数量信息做了操作!


      Redis实现分布式锁 

      在Redis中存在一条命令setnx (set if not exists)

      setnx key value
      如果不存在key,则可以设置成功;否则设置失败。

      修改处理接口,增加key

      // 模拟商品超卖代码
      	@RequestMapping("/deductStock2")
      	public String deductStock2() {
      		// 创建一个key,保存至redis
      		String key = "lock";
      		// setnx
      		// 由于redis是一个单线程,执行命令采取“队列”形式排队!
      		// 优先进入队列的命令先执行,由于是setnx,第一个执行后,其他操作执行失败。
      		boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "this is lock");
      		// 当不存在key时,可以设置成功,回执true;如果存在key,则无法设置,返回false
      		if (!result) {
      			// 前端监测,redis中存在,则不能让这个抢购操作执行,予以提示!
      			return "err";
      		}
      		
      		// 获取Redis数据库中的商品数量
      		Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
      		// 减库存
      		if(stock > 0) {
      			int realStock = stock -1;
      			stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
      			System.out.println("商品扣减成功,剩余商品:"+realStock);
      		}else {
      			System.out.println("库存不足.....");
      		}
       
              // 程序执行完成,则删除这个key
      		stringRedisTemplate.delete(key);
       
      		return "end";
      	}

      1、请求进入接口中,如果redis中不存在key,则会新建一个setnx;如果存在,则不会新建,同时返回错误编码,不会继续执行抢购逻辑。
      2、当创建成功后,执行抢购逻辑。
      3、抢购逻辑执行完成后,删除数据库中对应的setnx的key。让其他请求能够设置并操作。

      这种逻辑来说比之前单一使用syn合理的多,但是如果执行抢购操作中出现了异常,导致这个key无法被删除。以至于其他处理请求,一直无法拿到key,程序逻辑死锁!

      可以采取try … finally进行操作 

      /**
      	 * 模拟商品超卖代码 设置
      	 *
      	 * @return
      	 */
      	@RequestMapping("/deductStock3")
      	public String deductStock3() {
      		// 创建一个key,保存至redis
      		String key = "lock";
      		// setnx
      		// 由于redis是一个单线程,执行命令采取队列形式排队!优先进入队列的命令先执行,由于是setnx,第一个执行后,其他操作执行失败
      		boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "this is lock");
      		// 当不存在key时,可以设置成功,回执true;如果存在key,则无法设置,返回false
      		if (!result) {
      			// 前端监测,redis中存在,则不能让这个抢购操作执行,予以提示!
      			return "err";
      		}
       
      		try {
      			// 获取Redis数据库中的商品数量
      			Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
      			// 减库存
      			if (stock > 0) {
      				int realStock = stock - 1;
      				stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
      				System.out.println("商品扣减成功,剩余商品:" + realStock);
      			} else {
      				System.out.println("库存不足.....");
      			}
      		} finally {
      			// 程序执行完成,则删除这个key
      			// 放置于finally中,保证即使上述逻辑出问题,也能del掉
      			stringRedisTemplate.delete(key);
      		}
       
      		return "end";
      	}

      这个逻辑相比上面其他的逻辑来说,显得更加的严谨。

      但是,如果一套服务器,因为断电、系统崩溃等原因出现宕机,导致本该执行finally中的语句未成功执行完成!!同样出现key一直存在,导致死锁!

      通过超时间解决上述问题

      在设置成功setnx后,以及抢购代码逻辑执行前,增加key的限时。

      /**
      	 * 模拟商品超卖代码 设置setnx保证分布式环境下,数据处理安全行问题;
       * 但如果某个代码段执行异常,导致key无法清理,出现死锁,添加try...finally;
       * 如果某个服务因某些问题导致释放key不能执行,导致死锁,此时解决思路为:增加key的有效时间;
       * 为了保证设置key的值和设置key的有效时间,两条命令构成同一条原子命令,将下列逻辑换成其他代码。  *  * @return  */ @RequestMapping("/deductStock4") public String deductStock4() { // 创建一个key,保存至redis String key = "lock"; // setnx // 由于redis是一个单线程,执行命令采取队列形式排队!优先进入队列的命令先执行,由于是setnx,第一个执行后,其他操作执行失败 //boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "this is lock");   //让设置key和设置key的有效时间都可以同时执行 boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "this is lock", 10, TimeUnit.SECONDS);   // 当不存在key时,可以设置成功,回执true;如果存在key,则无法设置,返回false if (!result) { // 前端监测,redis中存在,则不能让这个抢购操作执行,予以提示! return "err"; } // 设置key有效时间 //stringRedisTemplate.expire(key, 10, TimeUnit.SECONDS);   try { // 获取Redis数据库中的商品数量 Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // 减库存 if (stock > 0) { int realStock = stock - 1; stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock)); System.out.println("商品扣减成功,剩余商品:" + realStock); } else { System.out.println("库存不足....."); } } finally { // 程序执行完成,则删除这个key // 放置于finally中,保证即使上述逻辑出问题,也能del掉 stringRedisTemplate.delete(key); }   return "end"; }

      但是上述代码的逻辑中依旧会有问题:

      如果处理逻辑中,出现超时问题。
      当逻辑执行时,时间超过设定key有效时间,此时会出现什么问题?

      如何解决Springboot整合Redis中的超卖问题?

       从上图可以清楚的发现问题:
      如果一个请求执行时间超过了key的有效时间。
      新的请求执行过来时,必然可以拿到key并设置时间;
      此时的redis中保存的key并不是请求1的key,而是别的请求设置的。
      当请求1执行完成后,此处删除key,删除的是别的请求设置的key!

      依然出现了key形同虚设的问题!如果失效一直存在,超卖问题依旧不会解决。

      通过key设置值匹配的方式解决形同虚设问题 

      既然出现key形同虚设的现象,是否可以增加条件,当finally中需要执行删除操作时,获取数据判断值是否是该请求中对应的,如果是则删除,不是则不管!

      修改上述代码如下所示:

      /**
      	 * 模拟商品超卖代码 
       * 解决`deductStock6`中,key形同虚设的问题。  *  * @return  */ @RequestMapping("/deductStock5") public String deductStock5() { // 创建一个key,保存至redis String key = "lock"; String lock_value = UUID.randomUUID().toString(); // setnx //让设置key和设置key的有效时间都可以同时执行 boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, lock_value, 10, TimeUnit.SECONDS); // 当不存在key时,可以设置成功,回执true;如果存在key,则无法设置,返回false if (!result) { // 前端监测,redis中存在,则不能让这个抢购操作执行,予以提示! return "err"; } try { // 获取Redis数据库中的商品数量 Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // 减库存 if (stock > 0) { int realStock = stock - 1; stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock)); System.out.println("商品扣减成功,剩余商品:" + realStock); } else { System.out.println("库存不足....."); } } finally { // 程序执行完成,则删除这个key // 放置于finally中,保证即使上述逻辑出问题,也能del掉   // 判断redis中该数据是否是这个接口处理时的设置的,如果是则删除 if(lock_value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) { stringRedisTemplate.delete(key); } } return "end"; }

      由于获得锁的线程必须执行完减库存逻辑才能释放锁,所以在此期间所有其他的线程都会由于没获得锁,而直接结束程序,导致有很多库存根本没有卖出去,所以这里应该可以优化,让没获得锁的线程等待,或者循环检查锁 

      如何解决Springboot整合Redis中的超卖问题?


      最终版

      我们将锁封装到一个实体类中,然后加入两个方法,加锁和解锁

      @Component
      public class RedisLock {
          private final Logger log = LoggerFactory.getLogger(this.getClass());
       
          private final long acquireTimeout = 10*1000;    // 获取锁之前的超时时间(获取锁的等待重试时间)
          private final int timeOut = 20;   // 获取锁之后的超时时间(防止死锁)
       
          @Autowired
          private StringRedisTemplate stringRedisTemplate;  // 引入String类型redis操作模板
       
          /**
           * 获取分布式锁
           * @return 锁标识
           */
          public boolean getRedisLock(String lockName,String lockValue) {
              // 1.计算获取锁的时间
              Long endTime = System.currentTimeMillis() + acquireTimeout;
              // 2.尝试获取锁
              while (System.currentTimeMillis() < endTime) {
                  //3. 获取锁成功就设置过期时间 让设置key和设置key的有效时间都可以同时执行
                  boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockName, lockValue, timeOut, TimeUnit.SECONDS);
                  if (result) {
                      return true;
                  }
              }
              return false;
          }
       
       
          /**
           * 释放分布式锁
           * @param lockName 锁名称
           * @param lockValue 锁值
           */
          public void unRedisLock(String lockName,String lockValue) {
              if(lockValue.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(lockName))) {
                  stringRedisTemplate.delete(lockName);
              }
          }
      }
      @RestController
      public class RedisController {
      	
      	// 引入String类型redis操作模板
      	@Autowired
      	private StringRedisTemplate stringRedisTemplate;
      	@Autowired
      	private RedisLock redisLock;
       
       
      	@RequestMapping("/setStock")
      	public String setStock() {
      		stringRedisTemplate.opsForValue().set("stock", "100");
      		return "ok";
      	}
       
      	@RequestMapping("/deductStock")
      	public String deductStock() {
      		// 创建一个key,保存至redis
      		String key = "lock";
      		String lock_value = UUID.randomUUID().toString();
      		try {
      			boolean redisLock = this.redisLock.getRedisLock(key, lock_value);//获取锁
      			if (redisLock)
      			{
      				// 获取Redis数据库中的商品数量
      				Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
      				// 减库存
      				if (stock > 0) {
      					int realStock = stock - 1;
      					stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
      					System.out.println("商品扣减成功,剩余商品:" + realStock);
      				} else {
      					System.out.println("库存不足.....");
      				}
      			}
      		} finally {
      			redisLock.unRedisLock(key,lock_value);   //释放锁
      		}
      		return "end";
      	}
      }

      可以看到失败的线程不会直接结束,而是会尝试重试,一直到重试结束时间,才会结束

      如何解决Springboot整合Redis中的超卖问题?


      实际上这个最终版依然存在3个问题

      1、在finally流程中,由于是先判断在处理。如果判断条件结束后,获取到的结果为true。但是在执行del操作前,此时jvm在执行GC操作(为了保证GC操作获取GC roots根完全,会暂停java程序),导致程序暂停。GC操作执行完成后(暂停恢复后),执行del操作,但是此时的key还在当前加锁的key么?

      2、问题如图所示

      如何解决Springboot整合Redis中的超卖问题?

      本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
      作者最新文章
      编程开发
      相关文章 更多
      php72w redis,docker php7安装php-redis
      php72w redis,docker php7安装php-redis

      在Docker环境下为PHP安装Redis扩展:手动编译需下载源码、安装phpize、配置编译并安装,生成redis.so并在php.d目录新建redis.ini加载,重启PHP-FPM;也可直接yum安装php72w-pecl-redis,免手动编译。

      php 如何配置 redis,php 如何设置redis
      php 如何配置 redis,php 如何设置redis

      配置PHP的Redis扩展:先安装Redis服务,从GitHub下载phpredis源码,用phpize编译安装,修改php.ini添加extension=redis.so,重启php-fpm或Apache,通过phpinfo验证并连接Redis。

      c++如何将结构体序列化为Redis兼容的RESP协议格式【深度】
      c++如何将结构体序列化为Redis兼容的RESP协议格式【深度】

      C++结构体无法直接序列化为RESP协议,需手动将字段映射为*N数组嵌套$lenbulkstring形式。整数需转换为字符串并加:前缀,空字符串用$0\r\n,null用$-1\r\n。长度计算需精确到字节数,特别注意二进制安全,同时确保字段顺序与结构体定义一致。

      如何在Golang架构中部署Redict作为高能数据临时存储中转站
      如何在Golang架构中部署Redict作为高能数据临时存储中转站

      Redis作为跨服务临时状态枢纽,需按消息队列、缓存和原子状态三重角色设计。选型用List实现任务分发及延迟重试,Stream用于事件广播与位点回溯。go-redis/v8需配置MinIdleConns、MaxRetries及超时。数据写入应JSON序列化并带type标签、设合理TTL、将关键字段提至顶层。消费通过原子GET+DEL或processedkey

      TP8.0 高并发下库存超卖?Redis 原子递减结合数据库乐观锁控制
      TP8.0 高并发下库存超卖?Redis 原子递减结合数据库乐观锁控制

      在高并发秒杀场景下,利用Redis的减库存原子操作预扣减库存拦截超卖请求,再通过数据库乐观锁(版本号与库存校验)确保最终一致性,异常时回滚Redis库存并借助Lua脚本保证原子性,从而有效防止库存超卖。

      如何在 Go 中利用 go-redis 执行 Lua 脚本优化逻辑
      如何在 Go 中利用 go-redis 执行 Lua 脚本优化逻辑

      在Go中使用go-redis执行Lua脚本可借助原子性解决竞态问题,通过redis.Script封装脚本并自动处理SHA1缓存与重试。需注意KEYS必须为非空切片且所有键在Cluster模式下落在同一slot,ARGV以字符串传入并由tonumber转换。返回值需先检查错误再调用Val(),避免panic。

      TP6.0 基于 Redis SetNX 实现的简单互斥锁机制【并发】
      TP6.0 基于 Redis SetNX 实现的简单互斥锁机制【并发】

      先看一个典型场景:电商秒杀、库存扣减、定时任务重复调度——这些并发冲突在 TP6 应用里屡见不鲜。用 Redis 的 SETNX 做互斥锁,确实是轻量又直接的方案,但“能用”和“稳妥”之间,隔着三层硬功夫:原子加锁、身份校验、合理超时。这三条,差一条就翻车。 很多人第一次上手,直接写 SETNX 加

      TP6.0 如何处理高并发抢购?Redis 原子锁与队列削峰详解【秒杀】
      TP6.0 如何处理高并发抢购?Redis 原子锁与队列削峰详解【秒杀】

      高并发抢购中,Redis的DECR原子操作可安全扣库存;配合think-queue异步削峰,接口仅扣库存和推消息;分布式锁需用SETNXEX加唯一标识,释放时用Lua脚本校验;成功资格后须以数据库事务双写为准,失败时补回Redis库存。

      Golang 实现基于 Redis 的延时任务队列
      Golang 实现基于 Redis 的延时任务队列

      基于Redis的ZSET实现延时队列需解决并发重复消费、失败重试混乱、精度不足及内存爆炸等问题。通过Lua脚本保证取删原子性,使用毫秒级时间戳,对临时错误采用指数退避重试,永久错误记入独立哈希表,并限制最大延迟、定期清理过期任务、使用紧凑序列化格式。

      RedisInsight怎么修改Key的过期时间 Redis操作方法【步骤】
      RedisInsight怎么修改Key的过期时间 Redis操作方法【步骤】

      操作 Redis 的时候,改个 Key 的过期时间其实是个高频场景。特别是做运维排查或者临时调试那会儿,谁也不想反复切命令行窗口、背那些命令参数。好在 RedisInsight 这个图形工具把这事儿做得很顺手——三种方式任选,总有一种合你胃口。 在动手前,先确认一下 Key 当前的 TTL 状态是个

      查看更多
      精品专题 更多
      装机必备
      装机必备

      正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

      Windows
      Windows

      正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

      PDF教程
      PDF教程

      正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

      Mac软件 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      Aperty macOS版
      Aperty macOS版
      macOS

      Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

      WINDOWS 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      PixTV 网页版
      PixTV 网页版
      Windows

      PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。

      即将离开本站
      您即将前往第三方网站,请确认是否继续?