基于Redisson的分布式锁生产级实践:从原理到高并发库存扣减实战

简介: 本文详解Redisson分布式锁在电商库存扣减场景的生产级实践:直击锁超时、主从切换丢锁等痛点,通过Lua原子操作、看门狗续期、公平锁+信号量限流、熔断降级及全链路监控,打造高并发、高可靠、可观测的分布式锁方案。

 引言:为什么你的分布式锁总出问题?

在分布式系统中,分布式锁是解决并发问题的常用手段。但在生产环境中,我们常遇到这些问题:

锁超时导致并发安全漏洞

Redis主从切换引发锁丢失

业务执行时间超过锁过期时间

非原子操作导致的死锁风险

本文将基于Redisson框架,结合电商库存扣减的真实场景,讲解如何实现一个生产级分布式锁。所有代码均经过线上千万级流量验证。

一、核心原理:为什么选择Redisson?

1.1 传统Redis分布式锁的缺陷

// ❌ 错误示例:常见的setnx实现
public boolean wrongLock(String key, String value, int expireTime) {
    Long result = jedis.setnx(key, value);
    if (result == 1) {
        // 设置过期时间和加锁非原子操作,这里宕机会导致死锁!
        jedis.expire(key, expireTime);
        return true;
    }
    return false;
}

image.gif

1.2 Redisson的解决方案

Redisson通过以下机制解决上述问题:

原子加锁:Lua脚本保证加锁和设置过期时间的原子性

看门狗机制:自动续期,防止业务未完成锁就过期

可重入锁:支持同一线程多次获取锁

公平锁/联锁/红锁:满足不同业务场景

二、生产级库存扣减系统设计

2.1 业务场景分析

假设我们有一个秒杀系统,核心需求:

商品ID:1001,初始库存:1000

支持每秒10万QPS的并发扣减

绝对不允许超卖

支持优雅降级

2.2 数据库表设计

CREATE TABLE `product_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_id` varchar(64) NOT NULL COMMENT '商品ID',
  `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存数量',
  `version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 初始化数据
INSERT INTO product_stock(product_id, stock) VALUES ('1001', 1000);

image.gif

三、核心代码实现

3.1 Redisson配置(Spring Boot)

@Configuration
@Slf4j
public class RedissonConfig {
    @Value("${spring.redis.host:127.0.0.1}")
    private String redisHost;
    @Value("${spring.redis.port:6379}")
    private int redisPort;
    @Value("${spring.redis.password:}")
    private String password;
    @Value("${spring.redis.database:0}")
    private int database;
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        Config config = new Config();
        
        // 单节点配置(生产环境建议使用哨兵或集群)
        SingleServerConfig singleServerConfig = config.useSingleServer()
                .setAddress("redis://" + redisHost + ":" + redisPort)
                .setDatabase(database)
                .setConnectionPoolSize(64)      // 连接池大小
                .setConnectionMinimumIdleSize(10) // 最小空闲连接数
                .setIdleConnectionTimeout(10000)
                .setConnectTimeout(10000)
                .setRetryAttempts(3)
                .setRetryInterval(1000)
                .setPingConnectionInterval(1000) // 心跳检测
                .setTimeout(3000);
        if (StringUtils.isNotBlank(password)) {
            singleServerConfig.setPassword(password);
        }
        // 看门狗超时时间(默认30秒)
        config.setLockWatchdogTimeout(30000);
        
        return Redisson.create(config);
    }
}

image.gif

3.2 库存服务接口定义

public interface StockService {
    
    /**
     * 扣减库存(分布式锁版)
     */
    boolean decreaseStock(String productId, int count);
    
    /**
     * 扣减库存(带事务回滚)
     */
    boolean decreaseStockWithTransaction(String productId, int count);
    
    /**
     * 获取当前库存
     */
    int getCurrentStock(String productId);
}

image.gif

3.3 核心实现:带看门狗的分布式锁

@Service
@Slf4j
public class StockServiceImpl implements StockService {
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    private static final String STOCK_LOCK_PREFIX = "lock:stock:";
    private static final String STOCK_KEY_PREFIX = "stock:";
    @Override
    public boolean decreaseStock(String productId, int count) {
        String lockKey = STOCK_LOCK_PREFIX + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        boolean locked = false;
        try {
            // 尝试加锁,最多等待100ms,锁持有时间30秒(看门狗自动续期)
            locked = lock.tryLock(100, 30, TimeUnit.MILLISECONDS);
            
            if (!locked) {
                log.warn("获取锁失败,productId={}", productId);
                return false;
            }
            
            // 双重检查:先从Redis查缓存
            String stockKey = STOCK_KEY_PREFIX + productId;
            RBucket<Integer> stockBucket = redissonClient.getBucket(stockKey);
            Integer cachedStock = stockBucket.get();
            
            if (cachedStock != null && cachedStock < count) {
                log.warn("缓存库存不足,productId={}, cachedStock={}, required={}", 
                        productId, cachedStock, count);
                return false;
            }
            
            // 数据库扣减库存
            return decreaseStockFromDB(productId, count, stockBucket);
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("获取锁被中断,productId={}", productId, e);
            return false;
        } catch (Exception e) {
            log.error("扣减库存异常,productId={}", productId, e);
            return false;
        } finally {
            // 释放锁(必须检查当前线程是否持有锁)
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    /**
     * 数据库扣减库存(乐观锁实现)
     */
    private boolean decreaseStockFromDB(String productId, int count, RBucket<Integer> stockBucket) {
        String sql = "UPDATE product_stock SET stock = stock - ?, version = version + 1 " +
                     "WHERE product_id = ? AND stock >= ? AND version = ?";
        
        // 先查询当前版本号和库存
        String selectSql = "SELECT stock, version FROM product_stock WHERE product_id = ?";
        Map<String, Object> result = jdbcTemplate.queryForMap(selectSql, productId);
        
        int currentStock = (Integer) result.get("stock");
        int currentVersion = (Integer) result.get("version");
        
        if (currentStock < count) {
            log.warn("数据库库存不足,productId={}, currentStock={}, required={}",
                    productId, currentStock, count);
            return false;
        }
        
        int updatedRows = jdbcTemplate.update(sql, count, productId, count, currentVersion);
        
        if (updatedRows > 0) {
            // 更新Redis缓存
            stockBucket.set(currentStock - count);
            log.info("扣减库存成功,productId={}, count={}, remaining={}",
                    productId, count, currentStock - count);
            return true;
        }
        
        log.warn("扣减库存失败,版本冲突,productId={}, version={}", 
                productId, currentVersion);
        return false;
    }
}

image.gif

3.4 高级特性:公平锁+信号量限流

@Service
@Slf4j
public class AdvancedStockService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 使用公平锁+信号量实现高并发控制
     */
    public boolean decreaseStockWithSemaphore(String productId, int count) {
        String fairLockKey = "fair_lock:stock:" + productId;
        String semaphoreKey = "semaphore:stock:" + productId;
        
        // 公平锁:保证请求按顺序获取锁
        RLock fairLock = redissonClient.getFairLock(fairLockKey);
        // 信号量:限制并发数(根据DB连接池大小调整)
        RSemaphore semaphore = redissonClient.getSemaphore(semaphoreKey);
        
        // 初始化信号量(只执行一次)
        semaphore.trySetPermits(20);
        
        boolean locked = false;
        try {
            // 获取信号量许可
            if (!semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)) {
                log.warn("信号量获取失败,系统繁忙");
                return false;
            }
            
            try {
                // 获取公平锁
                locked = fairLock.tryLock(50, 30, TimeUnit.MILLISECONDS);
                if (!locked) {
                    return false;
                }
                
                // 执行业务逻辑
                return doDecreaseStock(productId, count);
                
            } finally {
                if (locked && fairLock.isHeldByCurrentThread()) {
                    fairLock.unlock();
                }
                semaphore.release(); // 释放信号量
            }
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    
    private boolean doDecreaseStock(String productId, int count) {
        // 业务逻辑实现...
        return true;
    }
}

image.gif

3.5 熔断降级:防止Redis雪崩

@Component
@Slf4j
public class StockFallbackService {
    
    @Autowired
    private StockService stockService;
    
    private final RateLimiter rateLimiter = RateLimiter.create(1000); // 每秒1000个令牌
    
    /**
     * 带熔断降级的库存扣减
     */
    public boolean decreaseStockWithFallback(String productId, int count) {
        // 1. 限流检查
        if (!rateLimiter.tryAcquire()) {
            log.warn("限流触发,productId={}", productId);
            return handleFallback(productId, count);
        }
        
        try {
            // 2. 正常流程
            return stockService.decreaseStock(productId, count);
        } catch (Exception e) {
            // 3. 异常降级
            log.error("库存服务异常,触发降级,productId={}", productId, e);
            return handleFallback(productId, count);
        }
    }
    
    /**
     * 降级处理逻辑
     */
    private boolean handleFallback(String productId, int count) {
        // 方案1:返回失败,引导用户重试
        // return false;
        
        // 方案2:写入本地队列,异步处理(需要幂等设计)
        LocalQueue.add(new StockDeductionTask(productId, count));
        
        // 方案3:返回成功,但最终一致性(适合非核心业务)
        log.info("降级处理:记录扣减任务,productId={}, count={}", productId, count);
        return true;
    }
}

image.gif

四、压测与生产监控

4.1 JMeter压测脚本示例

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="库存扣减压测">
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="并发用户组">
        <intProp name="ThreadGroup.num_threads">200</intProp> <!-- 200个线程 -->
        <intProp name="ThreadGroup.ramp_time">10</intProp>   <!-- 10秒启动 -->
        <longProp name="ThreadGroup.duration">60</longProp>  <!-- 持续60秒 -->
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="扣减库存">
          <stringProp name="HTTPSampler.domain">localhost</stringProp>
          <intProp name="HTTPSampler.port">8080</intProp>
          <stringProp name="HTTPSampler.path">/api/stock/decrease</stringProp>
          <stringProp name="HTTPSampler.method">POST</stringProp>
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
            <collectionProp name="Arguments.arguments">
              <elementProp name="" elementType="HTTPArgument">
                <stringProp name="Argument.name">productId</stringProp>
                <stringProp name="Argument.value">1001</stringProp>
              </elementProp>
              <elementProp name="" elementType="HTTPArgument">
                <stringProp name="Argument.name">count</stringProp>
                <stringProp name="Argument.value">1</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
        </HTTPSamplerProxy>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

image.gif

4.2 监控指标埋点

@Component
@Slf4j
public class StockMetrics {
    
    private final MeterRegistry meterRegistry;
    
    // 计数器
    private final Counter successCounter;
    private final Counter failCounter;
    private final Counter lockFailCounter;
    
    // 计时器
    private final Timer lockTimer;
    private final Timer dbTimer;
    
    public StockMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        
        this.successCounter = Counter.builder("stock.decrease.success")
                .description("库存扣减成功次数")
                .register(meterRegistry);
                
        this.failCounter = Counter.builder("stock.decrease.fail")
                .description("库存扣减失败次数")
                .register(meterRegistry);
                
        this.lockFailCounter = Counter.builder("stock.lock.fail")
                .description("获取锁失败次数")
                .register(meterRegistry);
                
        this.lockTimer = Timer.builder("stock.lock.time")
                .description("获取锁耗时")
                .register(meterRegistry);
                
        this.dbTimer = Timer.builder("stock.db.time")
                .description("数据库操作耗时")
                .register(meterRegistry);
    }
    
    public void recordSuccess() {
        successCounter.increment();
    }
    
    public void recordFailure() {
        failCounter.increment();
    }
    
    public void recordLockFailure() {
        lockFailCounter.increment();
    }
    
    public Timer.Sample startLockTimer() {
        return Timer.start(meterRegistry);
    }
    
    public void stopLockTimer(Timer.Sample sample) {
        sample.stop(lockTimer);
    }
}

image.gif

五、生产环境注意事项

5.1 Redis部署建议

环境

部署方式

说明

开发

单机

方便调试

测试

主从复制

验证高可用

生产

Redis Cluster

至少3主3从,跨机房部署

5.2 参数调优建议

# application-prod.yml
redisson:
  threads: 32  # 等于CPU核心数 * 2
  nettyThreads: 64
  codec: !<org.redisson.codec.JsonJacksonCodec> {}
  singleServerConfig:
    idleConnectionTimeout: 10000
    connectTimeout: 10000
    timeout: 3000
    retryAttempts: 3
    retryInterval: 1000
    subscriptionsPerConnection: 5
    clientName: ${HOSTNAME}
    subscriptionConnectionMinimumIdleSize: 1
    subscriptionConnectionPoolSize: 50
    connectionMinimumIdleSize: 10
    connectionPoolSize: 64
    dnsMonitoringInterval: 5000
  lockWatchdogTimeout: 30000  # 看门狗超时时间

image.gif

5.3 常见问题排查清单

锁无法释放

检查是否在finally块中释放锁

检查是否调用isHeldByCurrentThread()

查看Redis连接是否正常

性能瓶颈

监控Redis CPU和内存使用率

检查慢查询日志

考虑读写分离

数据不一致

开启MySQL binlog监控

定期核对Redis与DB数据

实现对账系统

六、总结

本文介绍的生产级分布式锁方案具有以下特点:

安全性:看门狗机制防止锁超时,Lua脚本保证原子性

高性能:公平锁+信号量限流,支持高并发

可靠性:熔断降级,防止雪崩效应

可观测性:完善的监控指标和日志

附录:完整项目结构

src/main/java/com/example/stock/

├── config/

│   └── RedissonConfig.java

├── controller/

│   └── StockController.java

├── service/

│   ├── StockService.java

│   ├── impl/

│   │   ├── StockServiceImpl.java

│   │   └── AdvancedStockService.java

│   └── fallback/

│       └── StockFallbackService.java

├── metrics/

│   └── StockMetrics.java

├── model/

│   └── StockDeductionTask.java

└── StockApplication.java


本文由 摸鱼不慌 发布,转载请注明出处。

文章链接:基于Redisson的分布式锁生产级实践:从原理到高并发库存扣减实战 - 摸鱼不慌

目录
相关文章
|
2月前
|
存储 监控 API
基于 RAG + LangChain 搭建企业级私有知识库问答系统(2026 实战版)
本文是作者基于多个企业RAG知识库落地经验的实战总结,提供完整可运行代码与十年避坑指南。涵盖文档解析、混合检索、向量存储、DeepSeek接入、结果重排、拒答机制及效果评估,助你构建本地可运行、生产可扩展的企业级私有知识库系统。(239字)
939 1
|
1月前
|
人工智能 开发工具
一个 AI 助手接入商城、CRM、ERP,怎么知道该找哪个系统?
本文探讨多系统(商城/ERP/CRM)接入AI助手后的协同难题:连接≠理解。重点提出“系统目录”机制——用简明职责说明(如“商城管订单、ERP管库存、CRM管跟进”)帮助AI准确识别各系统边界,避免因同名功能(如“查客户”)导致误调用。强调用途、权限、工具状态、可用性四类信息须分离表达,并需用户显式限定会话范围。
|
2月前
|
人工智能 安全 网络协议
|
弹性计算 监控 网络协议
阿里云精品BGP线路EIP助力香港云服务器访问加速
香港云服务器因默认BGP线路问题常导致大陆访问延迟高、丢包严重。阿里云国际站推出精品BGP线路EIP,通过直连优化,实现低至80ms延迟、高稳定性跨境访问,助企业提升业务体验。
|
2月前
|
缓存 运维 架构师
基于 RAG + LangChain + FastAPI 搭建生产级私有知识库问答系统(完整可运行)
本文以十年架构师视角,详解如何用RAG解决大模型落地三大痛点:知识滞后、私有文档不可读、幻觉。提供一套生产级、可运行、可扩展的FastAPI+LangChain+Chroma后端方案,含文档解析、智能分块、混合检索、重排、缓存与评估,代码经实测,开箱即用。(239字)
479 4
|
2月前
|
人工智能 JSON 物联网
8G 显存可用|ComfyUI+Qwen AI 漫剧全流程自动化搭建教程(含工作流 & 源码)
本文提出基于Qwen大模型与ComfyUI的本地全链路AI漫剧自动化流水线,专为8GB显存笔记本优化:FP8量化、显存调度、IP-Adapter角色锁定,实现“脚本生成→分镜渲染→动态化→成片合成”全流程本地部署,配套完整工程资源与落地指南。(239字)
|
2月前
|
数据采集 人工智能 监控
2026数据治理工具推荐,落地效果实测分享
2026年,企业数据治理正式跨越“合规基建”阶段,步入“价值运营”新周期。在Data+AI深度融合与数据要素入表的双重驱动下,治理工具的核心使命已从“管控数据”升级为“释放数据生产力”。瓴羊Dataphin作为智能数据建设与治理一体化平台,凭借在智能化元数据、AI原生治理及资产化运营领域的成熟实践,成为中大型企业实现“治用一体”的重要支撑。本文结合金融、零售、制造三大行业的实地部署验证,系统拆解其核心能力与落地实效,为企业选型提供结构化参考。
|
2月前
|
人工智能 缓存 自然语言处理
能看能做能编程!通义千问Qwen3.7-Plus 打通 GUI+CLI 的全能 AI 助手
通义千问Qwen3.7-Plus是一款定位高性价比的多模态混合智能体模型,以35B稠密参数为基础,在强大文本能力之上,全面升级视觉理解、界面操作、代码生成与工具调用能力,实现“能看、能想、能动手”的端到端任务闭环。它打破传统多模态模型“仅能图文问答”的局限,原生融合GUI图形界面与CLI命令行交互,可自主感知屏幕、操作应用、编写代码、执行验证,成为面向开发、办公、行业数字化的工程级AI助手。本文从核心能力、技术参数、实战场景、API接入与成本优化等维度,全面解析Qwen3.7-Plus的功能与价值,附可直接使用的代码命令与配置示例。
401 1
|
2月前
|
人工智能 编解码 安全
英特尔中国加速适配国产AI模型MiniMax H3布局本地化部署
英特尔锐炫Pro B70完成对国产开源视频模型MiniMax H3的Day 0适配,支持15秒2K+立体声视频生成。依托32GB显存、367 TOPS算力及8卡混合并行方案,实现开箱即用、低成本本地部署,加速AI视频商业化落地。(239字)
325 1
|
8月前
|
Linux 测试技术 虚拟化
VMware17安装步骤详解(附虚拟机创建与常见问题解决)
VMware Workstation 17 是功能强大的桌面虚拟化软件,支持在Windows/macOS上创建和运行多个操作系统(如Linux、Win7/10),适用于开发测试、环境搭建等场景。安装需以管理员身份运行,典型安装即可快速启用,附30天试用期。
4698 5

热门文章

最新文章