您的位置:首页 > 手游攻略 > 生产级实战:基于Redisson的分布式锁高可用方案与源码级剖析

生产级实战:基于Redisson的分布式锁高可用方案与源码级剖析

作者:互联网  时间: 2026-08-20 10:04:56  

生产级实战:基于Redisson的分布式锁高可用方案与源码级剖析需要先看清适用场景和关键步骤,避免只记结论却忽略实际限制。

一、背景与痛点

在电商秒杀场景中,我们面临以下挑战:

超卖问题:多个实例同时扣减库存。死锁问题:服务宕机导致锁未释放。锁误删: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)

代码语言:javascript

复制

<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)

生产环境务必配置 连接池、超时时间 和 重试机制。

代码语言:javascript

复制

spring:redis:host: ${REDIS_HOST:127.0.0.1}port: 6379password: ${REDIS_PASSWORD}database: 0timeout: 3000ms # 命令超时lettuce:pool:max-active: 32 # 连接池最大连接数max-idle: 16min-idle: 4max-wait: 1000ms # 连接池获取连接最大等待时间

3. Redisson Config Bean (Java Config)

代码语言:javascript

复制

@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不可用 的情况。

代码语言:javascript

复制

@Service@Slf4jpublic class InventoryServiceImpl implements InventoryService {@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate 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 是否成功 */@Overridepublic 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 或 Sentinellog.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) {// 模拟发送MQlog.info("Sending MQ message: productId={}, quantity={}", productId, quantity);}}

5. 高级特性:公平锁与读写锁

在某些场景下,我们需要更精细的控制。

5.1 公平锁 (Fair Lock)

防止饥饿线程,先到先得。

代码语言:javascript

复制

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)

适用于读多写少的场景(如商品详情页)。

代码语言:javascript

复制

public ProductInfo getProductInfo(Long productId) {RReadWriteLock rwLock = redissonClient.getReadWriteLock("PRODUCT_RW_LOCK:" productId);RLock readLock = rwLock.readLock();try {readLock.lock();// 读缓存或DBreturn 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 的核心源码:

代码语言:javascript

复制

// 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() {@Overridepublic 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:01stock:product_id:10)。监控告警:监控Redis的 blocked_clientsconnected_clients 以及 lock wait time。一旦锁等待时间过长,立即告警。Lua脚本原子性:Redisson的所有锁操作都是Lua脚本,保证了 SET EXPIRE 的原子性,这是生产级应用的基石。

六、总结

生产级的分布式锁不仅仅是 SETNX。本文通过 Redisson 实现了具备 自动续期、可重入、防误删 以及 熔断降级 能力的库存扣减方案。

最新游戏

更多

Copyright©2010-2019. All rights reserved | 波波三国游戏官网|[email protected]

备案编号:湘ICP备2022015115号-4