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

目录
相关文章
|
8天前
|
云安全 人工智能 运维
阿里云联动百位企业安全专家,共识Agent防御最佳实践
当Agent成为新员工,你的安全边界在哪里?
1930 8
阿里云联动百位企业安全专家,共识Agent防御最佳实践
|
2天前
|
编解码 人工智能 安全
2核4G/4核8G/8核16G阿里云服务器如何选择实例?经济型e、通用算力型u2i与计算型c9i选哪个?
本文介绍了阿里云2核4G、4核8G、8核16G三档主流配置下经济型e、通用算力型u2i和计算型c9i三种实例的最新活动价格与适用场景。同配置下三者价差显著,以2核4G为例,经济型e低至599.93元/年,计算型c9i则高达1742.08元/年。文章详细解析了各实例的性能定位:经济型e适合轻负载入门场景,u2i兼顾稳定算力与性价比,c9i凭借第9代至强处理器与芯片级安全能力支撑高性能业务。同时提示用户可叠加满减优惠券享受折上折,建议根据业务负载与预算综合决策。
501 111
|
6天前
|
存储 人工智能 关系型数据库
阿里云AI产品与云产品最新组合套餐:Token Plan、AI coding及云服务器和建站等组合优惠价
阿里云推出全新“算力+模型+应用”一站式云与AI组合套餐活动,覆盖从个人开发者到中大型企业的全场景需求。核心亮点为分三档定价的Token Plan订阅服务,支持Qwen3.8-Max-Preview大模型调用,错峰时段最低可享0.2折优惠。活动同步推出AI Coding、智能体部署、云电脑托管、0代码建站等十余类场景化组合,搭配99元/年的普惠云服务器、88元/年的入门数据库等经典特惠产品,还为企业提供1V1定制化AI转型方案,大幅降低了不同用户群体拥抱AI的技术门槛与采购成本。
678 111
|
16天前
|
人工智能 JSON 安全
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
阿里云AI安全产品联动防御Fastjson攻击
2601 13
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
|
14天前
|
人工智能 前端开发 Linux
Codex 桌面版安装 + CC Switch 接入第三方 API 完整教程(2026 最新)
2026最新教程:手把手教你安装Codex桌面版,通过CC Switch v3.17.0一键接入Fenno等国产API(兼容OpenAI Responses格式),跳过账号登录,完整启用代码审查、多步任务与上下文感知功能。零基础友好,全程图文实操。(239字)
1872 2
|
2天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
|
16天前
|
人工智能 自然语言处理 数据挖掘
Qwen3.8-Max-Preview深度全解析:2.4万亿参数旗舰MoE模型+Token Plan限时优惠完整落地指南
2026年7月,全新旗舰级混合专家大模型Qwen3.8-Max-Preview正式开放抢先体验,作为通义千问Qwen3系列规格最高、综合推理能力顶尖的新一代模型,该模型总参数量达到2.4万亿(2.4T),是当前线上可调用的原生多模态旗舰模型,综合推理水准对标海外顶级Fable 5模型,在复杂工程开发、长文档深度分析、多步骤智能体自治、跨境多语言创作、海量数据挖掘五大高难度业务场景实现跨越式性能提升。
1456 2
|
3天前
Qoder 一周年 × Qwen3.8-Max 正式上线,多重好礼限时领
8月3日,Qwen3.8-Max 正式上线Qoder,迎来Qoder一周年。新老用户可领800次免费调用,下单再赠2000次;夜间(22:00–08:00)调用5折;邀请好友双方得积分与调用额度。
294 0

热门文章

最新文章