引言:为什么你的分布式锁总出问题?
在分布式系统中,分布式锁是解决并发问题的常用手段。但在生产环境中,我们常遇到这些问题:
锁超时导致并发安全漏洞
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; }
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);
三、核心代码实现
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); } }
3.2 库存服务接口定义
public interface StockService { /** * 扣减库存(分布式锁版) */ boolean decreaseStock(String productId, int count); /** * 扣减库存(带事务回滚) */ boolean decreaseStockWithTransaction(String productId, int count); /** * 获取当前库存 */ int getCurrentStock(String productId); }
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; } }
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; } }
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; } }
四、压测与生产监控
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>
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); } }
五、生产环境注意事项
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 # 看门狗超时时间
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
本文由 摸鱼不慌 发布,转载请注明出处。