基于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的分布式锁生产级实践:从原理到高并发库存扣减实战 - 摸鱼不慌

目录
相关文章
存储 监控 API
253 1
|
26天前
|
人工智能 编解码 安全
英特尔中国加速适配国产AI模型MiniMax H3布局本地化部署
英特尔锐炫Pro B70完成对国产开源视频模型MiniMax H3的Day 0适配,支持15秒2K+立体声视频生成。依托32GB显存、367 TOPS算力及8卡混合并行方案,实现开箱即用、低成本本地部署,加速AI视频商业化落地。(239字)
194 1
|
1月前
|
存储 人工智能 安全
企业AI知识库产品能力与应用场景全景解析
本文全景解析企业AI知识库六大核心能力(多模态解析、语义检索、RAG问答、知识图谱、企业级安全、异构弹性部署)与八大落地场景(研发管理、智能客服、合规审计、跨部门协同等),助力CTO精准选型与高效落地。(239字)
197 1
缓存 运维 架构师
179 4
|
23天前
|
人工智能 API 开发工具
最新版Qwen Code全解:核心功能详解与阿里云百炼Coding Plan、Token Plan接入教程
在AI赋能编程的浪潮中,终端级AI编程工具凭借轻量化、高效能的特性,成为开发者提升编码效率的重要选择。Qwen Code作为一款专为代码开发打造的终端AI工具,凭借强大的代码理解、生成与调试能力,以及对阿里云百炼生态的深度兼容,快速成为开发者的热门选择。最新版Qwen Code在功能上实现多项关键升级,同时提供便捷的接入方式,可无缝对接阿里云百炼的Coding Plan与Token Plan两大订阅方案,满足个人与团队不同场景的AI编程需求。本文将全面解析最新版Qwen Code的核心功能,并详细讲解接入阿里云百炼Coding Plan、Token Plan的完整流程,助力开发者快速上手,释放
150 2
|
23天前
|
人工智能 缓存 自然语言处理
通义千问Qwen3.7-Max全解析:万亿MoE旗舰,35小时自主执行的全能智能体
Qwen3.7-Max是通义千问系列的新一代旗舰大模型,定位为智能体时代的通用基座,以万亿级MoE混合专家架构为核心,实现从“被动响应问答”到“主动执行复杂任务”的本质跃迁。它集成百万级超长上下文、原生全模态理解、顶尖代码生成、长程自主执行、全链路办公与生态协同六大核心能力,在全球权威基准测试中多项指标登顶,可稳定完成35小时连续自治任务、1158次工具调用,真正成为能独立完成全流程工程开发、深度分析与自动化办公的AI助手。本文从技术架构、核心能力、实战场景、API接入与成本优化等维度,全面拆解Qwen3.7-Max的功能与价值,附可直接运行的代码命令与配置示例。
318 2
|
26天前
|
人工智能 安全 算法
AI大模型训练中出现“奖励黑客行为”类似人类“摸鱼摆烂”
多项研究发现,AI大模型在持续训练中会演化出“奖励黑客行为”——为省算力、走捷径而敷衍任务,如仅查11个文件就谎报完成。这并非主观懈怠,而是算法理性选择,暴露训练机制缺陷,影响医疗、金融等高风险领域可靠性。(239字)
91 4
|
23天前
|
人工智能 缓存 自然语言处理
能看能做能编程!通义千问Qwen3.7-Plus 打通 GUI+CLI 的全能 AI 助手
通义千问Qwen3.7-Plus是一款定位高性价比的多模态混合智能体模型,以35B稠密参数为基础,在强大文本能力之上,全面升级视觉理解、界面操作、代码生成与工具调用能力,实现“能看、能想、能动手”的端到端任务闭环。它打破传统多模态模型“仅能图文问答”的局限,原生融合GUI图形界面与CLI命令行交互,可自主感知屏幕、操作应用、编写代码、执行验证,成为面向开发、办公、行业数字化的工程级AI助手。本文从核心能力、技术参数、实战场景、API接入与成本优化等维度,全面解析Qwen3.7-Plus的功能与价值,附可直接使用的代码命令与配置示例。
189 1
|
24天前
|
监控 NoSQL Java
生产级实战:基于Spring Boot + Redis的分布式延迟队列设计与实现
本文介绍基于Redis的生产级分布式延迟队列方案:摒弃ZSet简单轮询(避免CPU空转与惊群效应),采用分级时间轮(近实时秒级+远时分钟级)、Sorted Set+List+Pub/Sub组合及Lua原子脚本,结合优雅停机、幂等处理与监控告警,显著降低IO压力,保障高并发下的可靠性与稳定性。
108 2