一、背景与痛点
在电商秒杀场景中,我们面临以下挑战:
- 超卖问题:多个实例同时扣减库存。
- 死锁问题:服务宕机导致锁未释放。
- 锁误删:A线程删掉了B线程的锁。
- 业务超时:业务执行时间超过锁过期时间。
- Redis抖动:Redis超时导致大面积锁失效。
二、为什么选择 Redisson 而非原生 Jedis?
原生的 SET key random_value NX PX 30000 虽然能实现互斥,但我们需要自己处理:
- 锁续期(Watchdog):业务没执行完,锁过期了怎么办?
- 可重入性:同一个线程内递归调用怎么办?
- 集群容错:Redis Master宕机,锁信息未同步到Slave怎么办?(虽然Redisson也无法完全解决Redlock争议,但在CAP中做了很好的折中)。
Redisson 底层封装了 Netty,实现了 Lua脚本原子操作 和 看门狗机制,是Java分布式锁的事实标准。
三、生产级代码实战
1. 依赖配置 (POM)
<dependencies> <!-- Redisson Starter --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.23.4</version> </dependency> <!-- 连接池 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency></dependencies>
2. Redisson 生产级配置 (YAML)
生产环境务必配置 连接池、超时时间 和 重试机制。
spring: redis: host: ${REDIS_HOST:127.0.0.1} port: 6379 password: ${REDIS_PASSWORD} database: 0 timeout: 3000ms # 命令超时 lettuce: pool: max-active: 32 # 连接池最大连接数 max-idle: 16 min-idle: 4 max-wait: 1000ms # 连接池获取连接最大等待时间
3. Redisson Config Bean (Java Config)
@Configurationpublic class RedissonConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private String port; @Value("${spring.redis.password}") private String password; @Bean(destroyMethod = "shutdown") public RedissonClient redissonClient() { Config config = new Config(); // 单节点模式(生产环境建议哨兵或集群模式) SingleServerConfig singleServerConfig = config.useSingleServer() .setAddress("redis://" + host + ":" + port) .setPassword(password) .setDatabase(0); // 生产级关键参数 singleServerConfig.setConnectionPoolSize(64); // 连接池大小 singleServerConfig.setConnectionMinimumIdleSize(10); // 最小空闲连接 singleServerConfig.setIdleConnectionTimeout(10000); // 空闲连接超时 singleServerConfig.setConnectTimeout(3000); // 连接超时 singleServerConfig.setTimeout(3000); // 命令等待超时 singleServerConfig.setRetryAttempts(3); // 命令重试次数 singleServerConfig.setRetryInterval(1500); // 重试间隔 // 看门狗超时时间(默认30秒,如果未自定义,锁过期时间会以此为准) config.setLockWatchdogTimeout(30 * 1000); return Redisson.create(config); } }
4. 核心业务:库存扣减 Service (含熔断与降级)
这是本文的重点。我们不仅要加锁,还要处理 业务异常 和 Redis不可用 的情况。
@Service@Slf4jpublic class InventoryServiceImpl implements InventoryService { @Autowired private RedissonClient redissonClient; @Autowired private StringRedisTemplate redisTemplate; private static final String STOCK_KEY = "seckill:stock:%s"; private static final String LOCK_KEY = "seckill:lock:%s"; /** * 扣减库存(生产级实现) * @param productId 商品ID * @param quantity 数量 * @return 是否成功 */ @Override public boolean decreaseStock(Long productId, Integer quantity) { String lockKey = String.format(LOCK_KEY, productId); RLock lock = redissonClient.getLock(lockKey); boolean locked = false; try { // 1. 尝试加锁,最多等待100ms,锁自动释放时间30s(看门狗会自动续期,除非服务宕机) // 参数说明:waitTime, leaseTime, unit // 如果leaseTime不设置(-1),则启用看门狗机制 locked = lock.tryLock(100, -1, TimeUnit.MILLISECONDS); if (!locked) { // 2. 获取锁失败,快速失败(防止线程堆积) log.warn("Product {} lock acquisition failed, system busy.", productId); return false; } // 3. 双重检查,防止Redis锁失效后的极端情况(虽然Redisson已处理,但作为防御性编程) if (!lock.isHeldByCurrentThread()) { log.error("Lock is not held by current thread! Thread: {}", Thread.currentThread().getName()); return false; } // 4. 执行核心业务逻辑 String stockKey = String.format(STOCK_KEY, productId); Long remainStock = redisTemplate.opsForValue().decrement(stockKey, quantity); if (remainStock == null) { // Redis中无此Key,初始化库存(通常从DB加载,此处简化) // 生产环境此处应有缓存预热逻辑 initStock(productId); remainStock = redisTemplate.opsForValue().decrement(stockKey, quantity); } if (remainStock < 0) { // 5. 库存不足,回滚(Lua脚本保证原子性) log.warn("Product {} stock not enough, remain: {}", productId, remainStock); redisTemplate.opsForValue().increment(stockKey, quantity); return false; } // 6. 扣减成功,发送MQ异步写数据库(最终一致性) sendDeductionMessage(productId, quantity); log.info("Product {} stock decreased successfully. Remain: {}", productId, remainStock); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("Thread interrupted while acquiring lock for product {}", productId, e); return false; } catch (RedisTimeoutException | RedisCommandExecutionException e) { // 7. 熔断降级:Redis超时或执行异常 // 生产环境此处应接入 Hystrix 或 Sentinel log.error("Redis error occurred while processing product {}. Entering degrade mode.", productId, e); // 降级策略:记录日志,返回失败,或者根据业务允许读本地缓存(视业务而定) return handleFallback(productId, quantity); } catch (Exception e) { log.error("Unexpected error while decreasing stock for product {}", productId, e); return false; } finally { // 8. 释放锁(必须确保是当前线程持有的锁) if (locked && lock.isHeldByCurrentThread()) { try { lock.unlock(); } catch (IllegalMonitorStateException e) { // 防止锁已过期自动释放导致的误删异常 log.warn("Attempted to unlock a lock that was not held by current thread or already expired."); } } } } /** * 降级处理逻辑 */ private boolean handleFallback(Long productId, Integer quantity) { // 方案A:返回失败,让用户重试 // 方案B:将请求放入本地队列,待Redis恢复后执行(复杂度高) // 方案C:限流,直接返回“系统繁忙” log.info("Executing fallback logic for product {}", productId); return false; } private void initStock(Long productId) { // 从数据库加载库存 int dbStock = 1000; // 模拟DB查询 String stockKey = String.format(STOCK_KEY, productId); redisTemplate.opsForValue().setIfAbsent(stockKey, String.valueOf(dbStock)); } private void sendDeductionMessage(Long productId, Integer quantity) { // 模拟发送MQ log.info("Sending MQ message: productId={}, quantity={}", productId, quantity); } }
5. 高级特性:公平锁与读写锁
在某些场景下,我们需要更精细的控制。
5.1 公平锁 (Fair Lock)
防止饥饿线程,先到先得。
public boolean processFairly(Long orderId) { RLock fairLock = redissonClient.getFairLock("ORDER_FAIR_LOCK:" + orderId); try { fairLock.lock(); // 业务逻辑 return true; } finally { fairLock.unlock(); } }
5.2 读写锁 (ReadWrite Lock)
适用于读多写少的场景(如商品详情页)。
public ProductInfo getProductInfo(Long productId) { RReadWriteLock rwLock = redissonClient.getReadWriteLock("PRODUCT_RW_LOCK:" + productId); RLock readLock = rwLock.readLock(); try { readLock.lock(); // 读缓存或DB return fetchFromCacheOrDb(productId); } finally { readLock.unlock(); } }public void updateProductInfo(Long productId, ProductInfo info) { RReadWriteLock rwLock = redissonClient.getReadWriteLock("PRODUCT_RW_LOCK:" + productId); RLock writeLock = rwLock.writeLock(); try { writeLock.lock(); // 更新DB和缓存 updateDbAndCache(productId, info); } finally { writeLock.unlock(); } }
四、源码级深度剖析:看门狗机制 (Watchdog)
很多同学只知道看门狗会自动续期,但不知道原理。我们来看 RedissonLock 的核心源码:
// RedissonLock.javaprivate void scheduleExpirationRenewal(long threadId) { ExpirationEntry entry = new ExpirationEntry(); ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry); if (oldEntry != null) { oldEntry.addThreadId(threadId); } else { entry.addThreadId(threadId); // 核心:开启定时任务 renewExpiration(); } }private void renewExpiration() { Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { // 执行Lua脚本,刷新过期时间 if (renewInternal()) { // 递归调用,直到锁释放或线程中断 renewExpiration(); } } }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 默认10秒续期一次}
原理总结:
- 如果我们在加锁时未指定
leaseTime,Redisson会启用看门狗。 - 看门狗会在锁过期前 1/3 的时间(默认10秒)执行一次
renew操作,重置过期时间。 - 如果服务宕机,Netty的
TimerTask停止,锁会在30秒后自动释放,避免死锁。
五、生产环境避坑指南
- 时钟同步:确保集群内所有服务器NTP时间同步,虽然Redisson主要依赖Redis时间,但业务逻辑可能依赖本地时间。
- Key设计:锁的Key必须唯一且具有业务含义(如
order_id,product_id),避免锁范围过大(全局锁)或过小(无效锁)。 - 避免热Key:如果某个商品是超级热点(如iPhone首发),单个Redis Key可能会成为瓶颈。解决方案:Key分片(将库存拆分成10份,Key为
stock:product_id:01到stock:product_id:10)。 - 监控告警:监控Redis的
blocked_clients、connected_clients以及lock wait time。一旦锁等待时间过长,立即告警。 - Lua脚本原子性:Redisson的所有锁操作都是Lua脚本,保证了
SET + EXPIRE的原子性,这是生产级应用的基石。
六、总结
生产级的分布式锁不仅仅是 SETNX。本文通过 Redisson 实现了具备 自动续期、可重入、防误删 以及 熔断降级 能力的库存扣减方案。
本文由 摸鱼不慌 发布,转载请注明出处。