Spring Boot Redis实战如何用Redisson解决抽奖系统的高并发库存问题抽奖这个看似简单的“点击一下”在电商大促、直播互动、游戏运营等场景下却是一个不折不扣的技术“高压锅”。想象一下当某个头部主播在直播间喊出“三、二、一上链接”的瞬间或是某个电商平台零点秒杀活动开启时成千上万的请求会像潮水般涌向你的服务器。此时最核心、也最脆弱的环节是什么库存。奖品库存的扣减必须保证绝对的准确性和一致性不能多发更不能少发。在高并发的冲击下传统的数据库行锁或乐观锁方案要么性能捉襟见肘要么在极端情况下会出现超卖或数据不一致的致命问题。作为一名经历过多次“大考”的后端开发者我深知在高并发场景下库存管理是决定抽奖系统成败的“生命线”。今天我们不谈宏观架构而是聚焦于一个具体而微、却又至关重要的技术点如何利用Spring Boot生态下的Redisson构建一个既安全又高效的分布式库存扣减方案。这篇文章将带你从零开始深入Redisson的分布式锁、原子操作等核心特性并结合真实的代码案例手把手教你搭建一个能扛住瞬时流量洪峰的抽奖库存系统。无论你是正在为即将到来的营销活动做技术储备还是希望优化现有系统的并发处理能力这里都有你需要的“硬核”解决方案。1. 高并发抽奖库存的挑战与核心思路在深入代码之前我们必须先理解在高并发抽奖场景下库存管理面临的具体挑战。这不仅仅是技术选型问题更是对业务逻辑和系统稳定性的双重考验。首先我们来拆解一下“高并发库存扣减”这个问题的本质原子性与一致性一次抽奖行为必须对应一次且仅一次成功的库存扣减。在分布式多实例部署的环境下多个请求同时读取到同一个库存值比如还剩1个然后都认为自己可以扣减最终导致库存变为负数超卖这是绝对不允许的。高性能与低延迟抽奖是强交互行为用户点击后期待毫秒级的响应。如果库存扣减操作本身耗时过长比如频繁访问数据库会直接拖垮整个系统的吞吐量导致用户体验急剧下降。高可用性用于保证一致性的锁服务或协调组件本身不能成为单点故障。如果锁服务挂了整个抽奖流程就会停滞。防止重复请求在弱网络环境下用户可能因未及时收到响应而重复点击提交系统需要有能力识别并过滤这类重复请求避免一次抽奖扣减多次库存。面对这些挑战一个成熟的技术方案通常由几个核心部分组成缓存作为主战场将库存数据从关系型数据库如MySQL前置到内存数据库如Redis中。所有扣减操作首先在内存中完成以获得极致的速度。分布式锁作为“交通警察”在关键资源如某个特定奖品的库存键上施加互斥锁确保同一时间只有一个请求能执行扣减逻辑。异步化与最终一致性将库存扣减的“写回”数据库操作异步化通过消息队列或定时任务来保证缓存与数据库的最终一致从而将同步操作的耗时降到最低。令牌桶或漏桶限流在网关或应用层对抽奖入口进行流量整形防止超出系统处理能力的请求直接压垮后端服务。其中Redisson作为Redis的Java客户端其提供的分布式锁和丰富的分布式对象正是实现前两点思路的利器。它不仅仅是一个客户端更是一个基于Redis的分布式协调服务框架。提示选择Redisson而非原生Redis命令如SETNX来实现分布式锁主要原因在于Redisson解决了锁的自动续期、可重入性、以及在高可用Redis集群下的可靠性问题避免了开发者自己实现时容易踩的“坑”。2. 环境搭建与Redisson核心配置工欲善其事必先利其器。我们先快速搭建一个基于Spring Boot的项目并集成Redisson。2.1 项目初始化与依赖引入创建一个标准的Spring Boot项目在pom.xml中引入关键依赖dependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring Data Redis -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- Redisson Starter -- dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.0/version !-- 请使用最新稳定版 -- /dependency !-- 数据库相关 (用于最终数据持久化) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- 其他工具 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies2.2 Redisson连接配置在application.yml中配置Redisson。这里以单节点模式为例生产环境建议使用集群或哨兵模式。spring: redis: host: localhost port: 6379 password: # 如果有密码则填写 database: 0 # Redisson 特定配置 redisson: config: | singleServerConfig: address: redis://${spring.redis.host}:${spring.redis.port} password: ${spring.redis.password} database: ${spring.redis.database} connectionPoolSize: 64 # 连接池大小 connectionMinimumIdleSize: 24 # 最小空闲连接数 idleConnectionTimeout: 10000 # 连接空闲超时时间 connectTimeout: 10000 # 连接超时时间 timeout: 3000 # 命令等待超时 retryAttempts: 3 # 命令失败重试次数 retryInterval: 1500 # 命令重试发送间隔这个配置定义了一个与本地Redis单节点连接的Redisson客户端。connectionPoolSize和connectionMinimumIdleSize对于高并发场景至关重要需要根据实际负载进行调整。2.3 核心BeanRedissonClient与库存服务配置完成后Spring Boot会自动为我们注入RedissonClient实例。我们创建一个库存服务类作为所有库存操作的门面。import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; Service public class InventoryService { Autowired private RedissonClient redissonClient; Autowired private RedisTemplateString, Object redisTemplate; // Spring Data Redis模板用于非锁操作 private static final String INVENTORY_KEY_PREFIX inventory:prize:; /** * 初始化或预热库存到Redis。 * 通常在活动开始前从数据库加载库存数量到缓存。 * param prizeId 奖品ID * param totalStock 总库存量 */ public void initInventory(Long prizeId, Integer totalStock) { String key INVENTORY_KEY_PREFIX prizeId; // 使用SET命令如果key不存在则设置防止覆盖已有数据比如活动进行中重启服务 redisTemplate.opsForValue().setIfAbsent(key, totalStock); // 可以同时设置一个过期时间防止缓存永久驻留例如活动结束后24小时清理 // redisTemplate.expire(key, 24, TimeUnit.HOURS); } }至此基础环境已经就绪。接下来我们将进入最核心的部分利用Redisson的分布式锁来安全地扣减库存。3. 基于Redisson分布式锁的库存安全扣减这是整个方案的心脏。我们的目标是在任意高的并发下保证每个奖品库存被准确扣减且每个中奖请求都能得到正确的“有库存”或“无库存”反馈。3.1 朴素方案的问题很多开发者首先会想到用Redis的DECR命令。它确实是原子的但存在一个问题它允许库存减为负数。在高并发下可能出现多个请求在库存为1时同时执行DECR结果库存变成了-5这意味着超卖了5个。所以我们需要在扣减前判断。一个错误的“先判断后扣减”伪代码如下// 伪代码 - 错误示范 public boolean deductStock(Long prizeId) { String key inventory: prizeId; Integer stock (Integer) redisTemplate.opsForValue().get(key); if (stock ! null stock 0) { // 问题所在在get和decr之间其他请求可能已经修改了stock值 redisTemplate.opsForValue().decrement(key); return true; } return false; }这段代码在并发下会引发超卖因为get和decr不是原子操作。3.2 使用Redisson分布式锁实现安全扣减Redisson的RLock实现了JavaLock接口使用起来非常直观。下面是改进后的安全扣减方法import org.redisson.api.RLock; import java.util.concurrent.TimeUnit; public boolean deductStockWithLock(Long prizeId) { String lockKey lock:inventory: prizeId; String stockKey INVENTORY_KEY_PREFIX prizeId; RLock lock redissonClient.getLock(lockKey); try { // 尝试获取锁最多等待100毫秒锁持有时间10秒 boolean isLocked lock.tryLock(100, 10000, TimeUnit.MILLISECONDS); if (!isLocked) { // 获取锁失败可能是系统繁忙可以记录日志或进行重试 log.warn(获取库存锁失败奖品ID: {}, prizeId); return false; // 或者抛出特定业务异常 } // 成功获取锁执行临界区代码 Integer currentStock (Integer) redisTemplate.opsForValue().get(stockKey); if (currentStock null) { log.error(奖品库存数据不存在奖品ID: {}, prizeId); // 可以选择从数据库加载这里直接返回失败 return false; } if (currentStock 0) { log.info(奖品库存已售罄奖品ID: {}, prizeId); return false; } // 扣减库存 redisTemplate.opsForValue().decrement(stockKey); log.debug(扣减库存成功奖品ID: {}剩余库存: {}, prizeId, currentStock - 1); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(获取锁过程被中断, e); return false; } finally { // 无论如何最终都要释放锁 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }代码解读与关键点lock.tryLock(100, 10000, TimeUnit.MILLISECONDS): 这是核心。第一个参数waitTime是获取锁的最大等待时间这里设为100毫秒。在高并发场景下不能让线程无限等待否则会堆积大量线程导致系统瘫痪。第二个参数leaseTime是锁的自动释放时间设为10秒防止因为业务逻辑异常或系统崩溃导致锁永远无法释放死锁。Redisson内部有看门狗机制如果业务未执行完会自动续期。lock.isHeldByCurrentThread(): 在finally块中释放锁前必须检查当前线程是否还持有该锁避免误释放其他线程的锁在可重入锁场景下尤其重要。锁的粒度我们以prizeId为维度加锁。这意味着不同奖品的库存扣减是互不影响的可以并行处理提升了吞吐量。如果对整个库存操作加一把全局锁性能会大打折扣。3.3 更优方案结合Lua脚本实现原子化虽然加锁保证了安全但获取和释放锁本身也有开销。对于库存扣减这种极其简单的“判断扣减”操作Redis的Lua脚本是更优的选择因为它能确保多个命令的原子执行无需在客户端加锁。我们可以使用Redisson执行Lua脚本public boolean deductStockWithLua(Long prizeId) { String stockKey INVENTORY_KEY_PREFIX prizeId; // Lua脚本如果库存大于0则减1并返回1否则返回0 String luaScript local current redis.call(get, KEYS[1]) if current and tonumber(current) 0 then redis.call(decr, KEYS[1]) return 1 else return 0 end; Long result redissonClient.getScript().eval( RScript.Mode.READ_WRITE, // 读写模式 luaScript, RScript.ReturnType.INTEGER, // 返回整数类型 Collections.singletonList(stockKey) // 键列表 ); return result ! null result 1L; }方案对比特性Redisson 分布式锁方案Lua 脚本原子操作方案安全性高通过锁机制保证高Redis单线程执行Lua保证原子性性能较好锁有开销极佳无需网络往返获取/释放锁复杂度中需处理锁获取、释放、异常低逻辑封装在脚本中适用场景临界区逻辑复杂需执行多个Redis或本地操作临界区逻辑简单仅为1-2个Redis命令的组合可读性高代码逻辑清晰中需要理解Lua语法对于纯粹的库存扣减Lua脚本方案是首选性能最高。Redisson分布式锁则更适合用于协调更复杂的业务逻辑例如“检查用户资格 - 扣减库存 - 生成中奖记录”这一系列需要原子性的操作。4. 构建完整的抽奖库存服务与降级策略现在我们将上述扣减逻辑整合到一个完整的、生产可用的库存服务中并考虑异常情况和降级策略。4.1 完整的InventoryService实现import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.SessionCallback; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; Service Slf4j public class PrizeInventoryService { Resource private RedissonClient redissonClient; Resource private RedisTemplateString, Integer redisTemplate; // 可指定泛型为Integer Resource private PrizeRepository prizeRepository; // JPA Repository用于数据库操作 private static final String STOCK_KEY inventory:stock:prize:%s; private static final String STOCK_LOCK_KEY lock:inventory:prize:%s; private static final String STOCK_SYNC_QUEUE queue:inventory:sync; /** * 核心抽奖扣库存方法 (使用Lua脚本) * param prizeId 奖品ID * param userId 用户ID (可用于更细粒度的锁或风控) * return 扣减是否成功 */ public DrawResult deductForLottery(Long prizeId, Long userId) { String stockKey String.format(STOCK_KEY, prizeId); // 1. 使用Lua脚本原子扣减 String luaScript local key KEYS[1] local stock redis.call(get, key) if not stock then return {-1, 库存数据未初始化} end // 代码-1: 数据不存在 stock tonumber(stock) if stock 0 then return {0, 库存不足} end // 代码0: 库存不足 redis.call(decr, key) return {1, stock - 1}; // 代码1: 成功返回剩余库存 ListObject result redisTemplate.execute( new SessionCallbackListObject() { Override public ListObject execute(RedisOperations operations) throws DataAccessException { return operations.execute( redisTemplate.getConnectionFactory().getConnection().getNativeConnection(), (connection, key) - connection.eval( luaScript.getBytes(), ReturnType.MULTI, // 返回多个值 1, stockKey.getBytes() ) ); } } ); if (result null || result.size() 2) { log.error(Lua脚本执行异常prizeId: {}, prizeId); return DrawResult.fail(系统繁忙请重试); } Long code (Long) result.get(0); String msg (String) result.get(1); if (code 1L) { // 扣减成功 Long remainingStock Long.parseLong(msg); log.info(用户 {} 抽中奖品 {} 成功剩余库存 {}, userId, prizeId, remainingStock); // 2. 异步记录扣减日志或同步数据库 (非常重要) asyncSyncToDatabase(prizeId, userId); // 3. 如果库存低于阈值触发预警 if (remainingStock 10) { triggerLowStockAlert(prizeId, remainingStock); } return DrawResult.success(prizeId, remainingStock); } else if (code 0L) { // 库存不足 log.info(用户 {} 抽奖时奖品 {} 库存不足, userId, prizeId); return DrawResult.fail(很遗憾奖品已被抢光); } else { // 数据未初始化或其他错误 log.warn(奖品 {} 库存数据异常: {}, prizeId, msg); // 降级策略尝试从数据库加载并重试一次或直接返回失败 return fallbackDeductFromDB(prizeId, userId); } } /** * 降级策略当缓存异常时尝试从数据库扣减 (需加分布式锁) */ private DrawResult fallbackDeductFromDB(Long prizeId, Long userId) { String lockKey String.format(STOCK_LOCK_KEY, prizeId); RLock lock redissonClient.getLock(lockKey); try { if (lock.tryLock(50, 5000, TimeUnit.MILLISECONDS)) { // 双重检查防止在获取锁期间缓存已被其他线程修复 Integer cacheStock redisTemplate.opsForValue().get(String.format(STOCK_KEY, prizeId)); if (cacheStock ! null) { // 缓存已恢复走正常流程 (这里可以递归调用但需注意深度) return deductForLottery(prizeId, userId); } // 从数据库查询并扣减 OptionalPrize prizeOpt prizeRepository.findById(prizeId); if (!prizeOpt.isPresent()) { return DrawResult.fail(奖品不存在); } Prize prize prizeOpt.get(); if (prize.getSurplusCount() 0) { // 更新缓存为0防止后续请求继续穿透 redisTemplate.opsForValue().set(String.format(STOCK_KEY, prizeId), 0, 5, TimeUnit.MINUTES); return DrawResult.fail(很遗憾奖品已被抢光); } // 数据库扣减 int updatedRows prizeRepository.decrementStock(prizeId); if (updatedRows 0) { int newStock prize.getSurplusCount() - 1; // 修复缓存 redisTemplate.opsForValue().set(String.format(STOCK_KEY, prizeId), newStock); log.warn(降级策略生效从数据库扣减库存成功prizeId: {}, newStock: {}, prizeId, newStock); return DrawResult.success(prizeId, (long) newStock); } else { // 扣减失败可能被其他请求抢先 return DrawResult.fail(活动太火爆了请稍后再试); } } else { return DrawResult.fail(系统繁忙请重试); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return DrawResult.fail(系统中断); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } /** * 异步同步库存变更到数据库 */ private void asyncSyncToDatabase(Long prizeId, Long userId) { // 这里可以使用消息队列 (如RabbitMQ/Kafka) 或放入一个本地内存队列由后台线程批量处理 // 示例发送到Redis Stream 或 RabbitMQ InventorySyncTask task new InventorySyncTask(prizeId, userId, -1, System.currentTimeMillis()); // redisTemplate.opsForList().leftPush(STOCK_SYNC_QUEUE, task); // 或者使用 Async 注解的Spring异步方法 log.debug(生成库存同步任务: {}, task); } /** * 库存预热活动开始前将数据库库存加载到Redis */ public void preheatInventory(Long activityId) { ListPrize prizes prizeRepository.findByActivityId(activityId); for (Prize prize : prizes) { String key String.format(STOCK_KEY, prize.getId()); // setIfAbsent 防止覆盖正在进行的活动数据 Boolean success redisTemplate.opsForValue().setIfAbsent(key, prize.getSurplusCount(), 48, TimeUnit.HOURS); if (Boolean.TRUE.equals(success)) { log.info(预热奖品库存成功: prizeId{}, stock{}, prize.getId(), prize.getSurplusCount()); } } } // 内部结果类 Data AllArgsConstructor NoArgsConstructor public static class DrawResult { private boolean success; private Long prizeId; private String message; private Long remainingStock; public static DrawResult success(Long prizeId, Long remainingStock) { return new DrawResult(true, prizeId, 恭喜中奖, remainingStock); } public static DrawResult fail(String message) { return new DrawResult(false, null, message, null); } } }4.2 关键设计解析与注意事项异步同步asyncSyncToDatabase方法是保证性能的关键。在高并发下每次扣减都写数据库是灾难。我们应该将扣减记录先放入一个高速队列如Redis List、Kafka然后由消费者异步、批量地更新数据库。这实现了缓存与数据库的最终一致性。降级策略fallbackDeductFromDB是系统的“安全气囊”。当Redis完全不可用或缓存数据异常时系统能自动降级到基于数据库的加锁扣减模式。虽然性能下降但保证了核心功能的可用性。库存预热preheatInventory必须在活动开始前执行。冷启动时缓存没有数据会导致所有请求穿透到数据库引发雪崩。缓存过期与数据一致性我们为库存Key设置了48小时过期时间。这需要有一个兜底机制比如定时任务定期从数据库同步库存到缓存防止缓存因过期而丢失导致库存不一致。5. 压力测试、监控与生产环境调优方案实现后不上线测试就是纸上谈兵。我们需要用接近真实场景的流量来验证其稳定性和性能。5.1 使用JMeter进行压力测试设计一个简单的测试计划线程组模拟5000个并发用户在10秒内启动完毕持续运行60秒。HTTP请求指向你的抽奖扣库存接口/api/lottery/draw参数包含prizeId和userId。断言检查响应中是否包含成功或库存不足的标识。监听器添加聚合报告、查看结果树、用表格查看结果。重点关注以下指标吞吐量 (Throughput)每秒能成功处理的请求数。这直接反映了系统的处理能力。平均响应时间 (Average Response Time)用户感知的延迟。在库存充足时这个时间应非常稳定且短暂毫秒级。错误率 (Error %)必须为0或极低。任何非业务逻辑错误如超时、连接拒绝都需要排查。Redis监控使用redis-cli的INFO commandstats命令观察decr、get、eval等命令的调用次数和耗时确保Redis不是瓶颈。5.2 生产环境配置调优根据压测结果你可能需要调整以下配置Redisson配置redisson: config: | singleServerConfig: address: redis://${redis.host}:${redis.port} connectionPoolSize: 128 # 增大连接池默认64根据应用实例数和QPS调整 connectionMinimumIdleSize: 32 idleConnectionTimeout: 10000 connectTimeout: 5000 # 网络状况好可以调低 timeout: 2000 # 命令超时时间不宜过长 retryAttempts: 3 retryInterval: 1000 pingConnectionInterval: 30000 # 保持连接活跃 threads: 16 # 处理Redis响应的线程数 nettyThreads: 32 # Netty工作线程数Spring Boot应用配置server: tomcat: threads: max: 1000 # 增大Tomcat工作线程数以处理更多并发请求 min-spare: 100 spring: redis: lettuce: # 如果使用Lettuce客户端 pool: max-active: 200 max-idle: 50 min-idle: 20JVM参数根据服务器内存合理设置堆大小并启用GC日志监控。5.3 监控与告警系统上线后必须建立完善的监控业务监控实时库存数量、扣减成功率、库存耗尽预警。Redis监控内存使用率、连接数、CPU使用率、慢查询。应用监控JVM内存与GC、接口QPS与RT、错误日志。告警当库存低于阈值、Redis连接数接近上限、接口错误率突增时及时通过钉钉、短信等渠道通知负责人。一个真实的踩坑经验在一次大促中我们忽略了Redis的内存监控。库存Key没有设置过期时间随着活动增多大量历史库存Key堆积最终导致Redis内存溢出整个抽奖服务瘫痪。教训就是一定要为缓存数据设置合理的过期时间并监控内存增长趋势。6. 进阶应对更高并发与更复杂场景当你的业务发展到更大规模或者遇到更特殊的需求时可以考虑以下进阶方案。6.1 库存分段 (Stock Sharding)对于库存量特别大比如10万件的热门商品将所有库存放在一个Redis Key下这个Key会成为热点所有扣减请求都集中于此。我们可以将库存分段例如分成100个段每个段1000件库存。扣减时先随机或根据用户ID哈希选择一个段进行操作。public boolean deductStockSharding(Long prizeId, int totalStock, int shardCount) { // 计算分片Key int shardIndex ThreadLocalRandom.current().nextInt(shardCount); // 随机选择分片 String shardKey String.format(inventory:prize:%s:shard:%d, prizeId, shardIndex); // 初始化分片库存 (假设平均分配) redisTemplate.opsForValue().setIfAbsent(shardKey, totalStock / shardCount); // 使用Lua脚本扣减该分片库存 // ... Lua脚本逻辑同上 ... }这种方式将热点打散提升了并发处理能力。但需要注意分片库存用尽后的处理逻辑可能需要一个全局计数器或二次路由到其他有库存的分片。6.2 使用Redis Cluster与Redisson单节点Redis有性能上限和单点故障风险。生产环境应使用Redis Cluster。Redisson完美支持集群模式只需修改配置redisson: config: | clusterServersConfig: nodeAddresses: - redis://node1:6379 - redis://node2:6379 - redis://node3:6379 scanInterval: 2000 # 集群状态扫描间隔 # ... 其他连接池配置在集群模式下Redisson的分布式锁和Lua脚本依然可以正常工作因为它能正确处理key的哈希槽分布。6.3 结合本地缓存 (Caffeine) 应对极端读场景如果“查询库存”的读请求量巨大比如每秒百万级即使Redis也可能有压力。可以考虑在应用层使用Guava Cache或Caffeine做一层本地缓存缓存库存数量并设置一个很短的过期时间如100毫秒。import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import java.util.concurrent.TimeUnit; Component public class LocalInventoryCache { // 缓存最多10000个奖品每个库存值缓存100毫秒 private CacheLong, Integer cache Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(100, TimeUnit.MILLISECONDS) // 写后过期 .build(); public Integer getStock(Long prizeId) { return cache.get(prizeId, id - { // 当缓存不存在时从Redis加载 String key String.format(STOCK_KEY, id); Integer stock redisTemplate.opsForValue().get(key); return stock ! null ? stock : 0; }); } public void invalidate(Long prizeId) { cache.invalidate(prizeId); } }注意本地缓存会导致数据短暂不一致最多100毫秒只适用于对实时性要求不苛刻的库存数量展示场景绝对不能用于扣减决策。扣减必须基于唯一可信源Redis或DB。处理高并发抽奖库存本质上是在一致性、性能、可用性三者之间寻找最佳平衡点。以Redis为核心利用其原子操作和Redisson的高级特性构建一个异步化、可降级的缓存层是目前经过大量实践验证的可靠方案。从简单的分布式锁到高效的Lua脚本从单节点到集群部署从纯缓存到多级缓存技术的选择始终要服务于业务场景和流量规模。记住没有银弹最好的方案永远是那个最适合你当前系统压力和团队技术栈的方案。在下次大流量来袭前不妨用本文的思路和代码亲手搭建并压测一遍你的库存系统做到心中有数临阵不慌。