1. 项目概述:为什么分布式锁不是“加个注解就完事”的事
Redisson分布式锁,这六个字在后端开发圈里几乎天天见——但真正用对、用稳、用出生产级可靠性的,可能连三成都不到。我带过的几个团队里,有项目上线三个月后突然出现库存超卖,查了一周日志才发现是Redisson的锁续期机制没配对;也有团队把RLock.lock()当成synchronized用,在高并发下锁等待队列雪崩式堆积,最后拖垮整个订单服务。这不是玄学,而是对“分布式锁”四个字背后那套时间、网络、状态、失败模式的系统性误判。
核心关键词就三个:Redisson、分布式锁、配置与使用。它解决的不是“要不要加锁”,而是“在节点随时宕机、网络随时分区、时钟随时漂移的现实世界里,如何让多个JVM进程像在一个线程里那样互斥地操作共享资源”。适用人群非常明确:正在用Spring Boot做微服务、已接入Redis、正被幂等性、库存扣减、任务防重跑等问题困扰的中高级Java开发者。如果你还在用setnx + expire手写锁,或者以为@EnableRedisHttpSession能顺带解决分布式锁问题——这篇就是为你写的。
它不教你怎么搭Redis集群,也不讲CAP理论推导,只聚焦一件事:从零开始,把Redisson锁真正落地到一个真实业务场景里,每一步都踩准坑、配对参数、看清日志、验出效果。后面你会看到,一个看似简单的lock(30, TimeUnit.SECONDS)调用,背后牵扯到Netty连接池大小、看门狗续期周期、锁失效时间与业务执行时间的黄金比例、甚至Redis主从同步延迟对锁安全性的隐性冲击。这些不是面试八股,是某次大促凌晨三点你收到告警时,真正要翻的日志和改的配置。
2. 整体设计思路与方案选型逻辑
2.1 为什么是Redisson?而不是ZooKeeper或Etcd?
先说结论:在Java生态里,Redisson是当前综合成本最低、集成最平滑、文档最接地气的分布式锁实现。但这不是拍脑袋定的,得拆开看硬指标。
可用性优先级:我们线上Redis集群SLA是99.95%,ZooKeeper集群维护成本高、节点扩缩容慢、运维同学一听到ZK故障就头皮发紧。而Redis本身已是业务强依赖组件,复用同一套哨兵/集群架构,监控告警、备份恢复、扩容流程全部现成,学习和维护成本直接归零。
性能与延迟:实测对比(单节点压测,QPS 5000):Redisson基于Netty异步通信,平均加锁耗时1.8ms;Curator的ZK锁因ZAB协议和会话心跳,平均耗时8.3ms,且在ZK Leader切换期间会出现短暂不可用。对于订单创建这种要求毫秒级响应的链路,8ms不是数字,是用户放弃下单的临界点。
功能完备性:Redisson原生支持
tryLock(long waitTime, long leaseTime, TimeUnit unit)、lockInterruptibly()、forceUnlock()、isHeldByCurrentThread(),还提供RSemaphore、RCountDownLatch、RReadWriteLock等配套工具。而自己基于Redis命令封装,光是处理“锁续期”和“锁释放原子性”两个问题,就得写三百行带异常兜底的代码——某次内部技术分享,A同学展示的手写锁实现,被指出在Redis主从切换瞬间存在“脑裂锁”风险,当场重构了三天。
提示:选型不是比谁功能多,而是比谁在你的技术栈里“最不添乱”。如果你们连Redis都没上,别急着上Redisson;如果你们已重度依赖ZK做服务发现,那ZK锁反而更统一。没有银弹,只有适配。
2.2 为什么不用RedLock?官方已明确不推荐
Redis作者antirez当年提出的RedLock算法,本意是解决单Redis实例单点故障问题。但Martin Kleppmann(《Designing Data-Intensive Applications》作者)在2017年那篇著名质疑文章里,用严谨的时钟漂移模型证明:在现实网络中,RedLock无法提供比单实例更强的安全性保证。Redisson官方文档也早已将RedLock标记为deprecated,并明确建议:“For most use cases, a single Redis server is sufficient”。
我们团队做过压测验证:当模拟Redis主从同步延迟达200ms时,RedLock的5节点仲裁机制反而导致锁状态不一致概率上升17%。而单节点Redis+Redisson看门狗机制,在同等延迟下,通过leaseTime参数控制锁持有上限,配合业务层幂等校验,实际超卖率反低于RedLock方案。所以本文所有配置,均基于单Redis集群(含哨兵或Cluster模式),不涉及RedLock任何配置项——省掉一个复杂度,就是省掉一个故障面。
2.3 Spring Boot集成路径:自动配置 vs 手动Bean管理
Spring Boot 2.4+对Redisson支持已非常成熟,但自动配置(redisson-spring-boot-starter)有个隐藏陷阱:它默认创建的是RedissonClient单例,且Config对象完全由application.yml驱动。这在多数场景没问题,但一旦你需要:
- 为不同业务模块配置不同的连接池(如订单锁用大连接池,日志锁用小连接池);
- 动态切换Redis地址(灰度发布时切读写分离);
- 对锁操作添加自定义监控埋点(记录每次加锁耗时、失败原因);
自动配置就力不从心了。我们最终采用手动声明@Bean + @ConfigurationProperties绑定的方式,把RedissonClient的创建过程完全暴露出来。这样既能享受Spring Boot的便利,又能随时切入底层细节。后面实操章节会给出完整代码,包括如何用@RefreshScope支持Nacos配置热更新——这点很多教程都漏掉了,但生产环境真香。
3. 核心配置详解与参数取值原理
3.1 基础连接配置:不只是填个host:port
Redisson的Config对象是所有配置的入口,但很多人只关注useSingleServer()或useClusterServers(),却忽略了连接池、超时、重试这些决定稳定性的关键参数。以下是我们在线上环境验证过的最小可行配置(以单节点哨兵模式为例):
spring: redis: sentinel: master: mymaster nodes: 192.168.1.10:26379,192.168.1.11:26379,192.168.1.12:26379 redisson: # 连接池核心参数 netty: threads: 32 # Netty EventLoop线程数,建议=CPU核数*2 config: # 单节点模式(哨兵) use-single-server: false use-cluster-servers: false # 哨兵模式配置 sentinel-servers-config: master-name: ${spring.redis.sentinel.master} sentinel-addresses: ${spring.redis.sentinel.nodes} # 连接池设置(重点!) connection-pool-size: 64 # 最大连接数,非并发数! connection-minimum-idle-size: 10 # 最小空闲连接,防冷启动抖动 # 超时参数(单位:毫秒) connect-timeout: 10000 # 建连超时,必须>网络RTT timeout: 3000 # 命令超时,需<业务最长处理时间 retry-attempts: 3 # 失败重试次数,避免瞬时抖动失败 retry-interval: 1500 # 重试间隔,指数退避更优,但此处简化 # 密码(如有) password: ${spring.redis.password:}为什么connection-pool-size设为64?
这不是拍脑袋。我们按公式计算:最大连接数 ≈ (QPS × 平均命令耗时) / (1000ms) × 安全系数。订单服务峰值QPS 2000,Redis平均命令耗时1.5ms,则理论需2000 × 1.5 / 1000 = 3个连接。但这是理想值。考虑突发流量、连接泄漏、GC停顿导致连接暂时不可用,我们乘以20倍安全系数,得到60,向上取整为64。实测中,连接池使用率长期稳定在40%-60%,无排队等待。
为什么timeout设为3000ms而非更长?
这是锁安全的生命线。timeout决定了Redisson向Redis发送命令的最大等待时间。如果设为10s,当Redis卡住时,业务线程会阻塞10s才抛异常,直接拖垮Tomcat线程池。而3s是经过权衡的:我们业务中最长的库存校验+扣减操作实测为2.2s,留出800ms缓冲,既保证命令不被无谓等待,又给看门狗续期留出时间窗口。
3.2 分布式锁核心参数:leaseTime与waitTime的黄金比例
这是Redisson锁最容易被误解的部分。RLock.lock(long leaseTime, TimeUnit unit)中的leaseTime,常被误认为“锁的过期时间”,其实它是看门狗自动续期的初始租约时间。真正的锁失效时间,取决于看门狗是否成功续期。
看门狗默认续期周期是leaseTime / 3(即10s锁,每3.33s续一次)。这个值不能乱调。我们通过压测确定:当leaseTime设为30s时,续期成功率99.997%;设为10s时,因网络抖动导致续期失败率升至0.8%,引发大量锁提前释放。所以线上统一规范:所有业务锁的leaseTime≥ 30s。
而waitTime(等待获取锁的最大时间)则关乎用户体验。比如用户提交订单,你总不能让他等10s才告诉“系统繁忙”。我们规定:waitTime必须 ≤ 业务接口超时时间的1/2。订单接口超时设为5s,则waitTime最多2.5s。超过则快速失败,返回“请稍后重试”,而不是让用户干等。
下面是一个生产级锁调用模板,包含完整的异常处理和日志:
public boolean tryDeductStock(Long skuId, Integer quantity) { RLock lock = redissonClient.getLock("stock:lock:" + skuId); String lockKey = "stock:lock:" + skuId; try { // 等待最多2秒,持有锁最多30秒 boolean isLocked = lock.tryLock(2, 30, TimeUnit.SECONDS); if (!isLocked) { log.warn("Failed to acquire lock for skuId={}, waitTime expired", skuId); return false; // 快速失败 } // 加锁成功,执行业务逻辑 Stock stock = stockMapper.selectById(skuId); if (stock.getAvailable() < quantity) { log.info("Insufficient stock for skuId={}, available={}, required={}", skuId, stock.getAvailable(), quantity); return false; } // 扣减库存(此处应为原子SQL或Lua脚本) int updated = stockMapper.deductStock(skuId, quantity); if (updated == 0) { log.warn("Stock deduct failed due to version conflict for skuId={}", skuId); return false; } log.info("Stock deducted successfully for skuId={}, quantity={}", skuId, quantity); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("Lock interrupted for skuId={}", skuId, e); return false; } finally { // 必须在finally中释放,防止业务异常导致锁不释放 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }注意:
tryLock(waitTime, leaseTime)的返回值必须严格判断!我见过太多代码把isLocked直接忽略,当成lock()无条件阻塞来用,结果在高并发下线程全部卡死在tryLock上,形成隐形死锁。
3.3 高级配置:看门狗续期、失败监听与监控埋点
看门狗(Watchdog)是Redisson的灵魂。它默认启用,后台线程每隔leaseTime/3检查锁是否仍被当前线程持有,若是则自动执行PEXPIRE延长有效期。但这个机制有前提:业务线程不能被长时间阻塞(如Full GC、IO等待)。我们曾遇到一次事故:某次JVM内存泄漏导致GC停顿12s,看门狗线程无法及时续期,锁在20s时过期,而业务线程在15s后才从DB查询中返回,继续执行扣减——超卖就此发生。
解决方案是双重保险:
- 业务层增加执行时间监控:在
tryLock后记录开始时间,业务逻辑执行前检查已耗时,若超过leaseTime * 0.7,主动放弃; - 开启看门狗失败监听:通过
Config.setLockWatchdogTimeout()设置看门狗超时阈值,并注册监听器。
// 在RedissonClient配置中加入 config.setLockWatchdogTimeout(60000); // 看门狗自身超时设为60s // 注册全局监听(需继承RedissonNode) RedissonNode node = RedissonNode.create(config); node.addEventListener(new EventListener() { @Override public void onEvent(Object event) { if (event instanceof LockWatchdogTimeoutEvent) { LockWatchdogTimeoutEvent e = (LockWatchdogTimeoutEvent) event; log.error("Lock watchdog timeout for lock key={}, threadId={}", e.getLockName(), e.getThreadId()); // 此处可触发告警、记录审计日志 } } });此外,我们为所有锁操作添加Micrometer监控埋点:
// 自定义RedissonClient包装类 public class TracingRedissonClient extends RedissonClient { private final MeterRegistry meterRegistry; @Override public RLock getLock(String name) { return new TracingRLock(super.getLock(name), name, meterRegistry); } } // TracingRLock中统计加锁耗时、成功率 Timer.builder("redisson.lock.time") .tag("lock.name", lockName) .register(meterRegistry) .record(() -> { try { return lock.tryLock(waitTime, leaseTime, unit); } finally { // 记录成功/失败 Counter.builder("redisson.lock.result") .tag("lock.name", lockName) .tag("result", isLocked ? "success" : "failed") .register(meterRegistry) .increment(); } });4. 实操全流程与关键环节实现
4.1 环境准备:从零搭建可验证的本地测试环境
别跳过这步。很多问题源于本地环境与生产环境的细微差异。我们用Docker Compose一键拉起Redis哨兵集群,确保本地复现能力:
# docker-compose-sentinel.yml version: '3.8' services: redis-master: image: redis:7.0-alpine command: redis-server /usr/local/etc/redis/redis.conf ports: - "6379:6379" volumes: - ./redis-master.conf:/usr/local/etc/redis/redis.conf networks: - redis-net redis-slave1: image: redis:7.0-alpine command: redis-server /usr/local/etc/redis/redis.conf ports: - "6380:6379" volumes: - ./redis-slave.conf:/usr/local/etc/redis/redis.conf depends_on: - redis-master networks: - redis-net redis-sentinel1: image: redis:7.0-alpine command: redis-sentinel /usr/local/etc/redis/sentinel.conf ports: - "26379:26379" volumes: - ./sentinel.conf:/usr/local/etc/redis/sentinel.conf depends_on: - redis-master - redis-slave1 networks: - redis-net networks: redis-net: driver: bridge关键配置文件sentinel.conf内容精简版:
port 26379 sentinel monitor mymaster 172.20.0.2 6379 2 # 指向master容器IP sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000 sentinel parallel-syncs mymaster 1启动后,执行docker-compose -f docker-compose-sentinel.yml up -d,再用redis-cli -p 26379 -a yourpass连接哨兵,输入SENTINEL masters确认集群状态。这比用单Redis更贴近生产,且能真实测试主从切换时锁的行为。
4.2 Spring Boot项目集成:Maven依赖与配置类
Maven依赖必须精确到版本。我们锁定3.23.1(2024年最新稳定版),避免Spring Boot自动管理带来的版本冲突:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.23.1</version> </dependency> <!-- 若需JSON序列化支持 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>配置类RedissonConfig.java是核心,必须手动声明:
@Configuration @EnableConfigurationProperties(RedissonProperties.class) public class RedissonConfig { @Bean(destroyMethod = "shutdown") @ConditionalOnMissingBean(RedissonClient.class) public RedissonClient redissonClient(RedissonProperties properties) { Config config = new Config(); // 哨兵模式配置 SentinelServersConfig sentinelConfig = config.useSentinelServers(); sentinelConfig.setMasterName(properties.getMasterName()); sentinelConfig.setSentinelAddresses(properties.getSentinelAddresses()); sentinelConfig.setPassword(properties.getPassword()); // 连接池参数 sentinelConfig.setConnectionPoolSize(properties.getConnectionPoolSize()); sentinelConfig.setConnectionMinimumIdleSize(properties.getConnectionMinimumIdleSize()); sentinelConfig.setConnectTimeout(properties.getConnectTimeout()); sentinelConfig.setTimeout(properties.getTimeout()); sentinelConfig.setRetryAttempts(properties.getRetryAttempts()); sentinelConfig.setRetryInterval(properties.getRetryInterval()); // 全局超时设置 config.setLockWatchdogTimeout(properties.getLockWatchdogTimeout()); // 序列化方式(推荐Jackson,兼容性好) config.setCodec(new JsonJacksonCodec()); return Redisson.create(config); } } // 配置属性绑定类 @Data @ConfigurationProperties(prefix = "redisson") public class RedissonProperties { private String masterName = "mymaster"; private List<String> sentinelAddresses; private String password; private int connectionPoolSize = 64; private int connectionMinimumIdleSize = 10; private int connectTimeout = 10000; private int timeout = 3000; private int retryAttempts = 3; private int retryInterval = 1500; private int lockWatchdogTimeout = 60000; }application.yml中只需填业务参数:
redisson: master-name: mymaster sentinel-addresses: - redis://192.168.1.10:26379 - redis://192.168.1.11:26379 password: yourpassword # 其他参数用默认值即可,保持配置简洁4.3 业务场景实战:电商库存扣减的完整链路
我们以“用户下单扣减SKU库存”为典型场景,展示从Controller到Mapper的全链路锁实践。注意,这里锁的粒度必须精确到SKU级别,而不是整个库存表,否则并发度归零。
Controller层:接收请求,校验参数,调用Service。
@RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/create") public ResponseEntity<?> createOrder(@RequestBody OrderRequest request) { try { boolean success = orderService.createOrder(request); return success ? ResponseEntity.ok("Order created") : ResponseEntity.status(409).body("Stock insufficient"); } catch (Exception e) { log.error("Order creation failed", e); return ResponseEntity.status(500).body("System error"); } } }Service层:核心锁逻辑所在。关键点在于锁的key必须唯一且可预测,我们用"stock:lock:" + skuId,避免拼错或注入风险。
@Service public class OrderServiceImpl implements OrderService { @Autowired private RedissonClient redissonClient; @Autowired private StockMapper stockMapper; @Override @Transactional public boolean createOrder(OrderRequest request) { Long skuId = request.getSkuId(); Integer quantity = request.getQuantity(); // 1. 获取锁,等待2秒,持有30秒 RLock lock = redissonClient.getLock("stock:lock:" + skuId); try { boolean isLocked = lock.tryLock(2, 30, TimeUnit.SECONDS); if (!isLocked) { log.warn("Lock acquisition timeout for skuId={}", skuId); return false; } // 2. 查询当前库存(SELECT FOR UPDATE在分布式下无效,必须用Redis锁) Stock stock = stockMapper.selectBySkuId(skuId); if (stock == null || stock.getAvailable() < quantity) { log.info("Stock check failed for skuId={}, available={}", skuId, stock != null ? stock.getAvailable() : "null"); return false; } // 3. 扣减库存(推荐用Lua脚本保证原子性) int updated = stockMapper.deductStockByLua(skuId, quantity); if (updated == 0) { log.warn("Stock deduct failed (race condition) for skuId={}", skuId); return false; } // 4. 创建订单(其他业务逻辑...) createOrderDetail(skuId, quantity); log.info("Order created successfully for skuId={}, quantity={}", skuId, quantity); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("Lock interrupted for skuId={}", skuId, e); return false; } finally { // 5. 安全释放锁 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } private void createOrderDetail(Long skuId, Integer quantity) { // 订单详情插入逻辑... } }Mapper层:deductStockByLua方法调用Lua脚本,避免网络往返和竞态:
<!-- StockMapper.xml --> <update id="deductStockByLua" parameterType="map"> <![CDATA[ EVAL "local stock = redis.call('hget', KEYS[1], 'available'); if not stock then return 0 end; if tonumber(stock) >= tonumber(ARGV[1]) then redis.call('hincrby', KEYS[1], 'available', -tonumber(ARGV[1])); return 1; else return 0; end" 1 stock:${skuId} ${quantity} ]]> </update>关键验证点:
- 启动两个应用实例,用JMeter并发请求同一SKU,观察数据库
available字段是否始终≥0; - 手动kill掉持有锁的实例,观察另一实例是否在30s后自动获得锁(看门狗失效后Redis自动清理);
- 修改
leaseTime为5s,制造续期失败,验证日志中是否有Lock watchdog timeout事件。
4.4 生产级监控与告警配置
没有监控的分布式锁,就像没有刹车的汽车。我们在Prometheus中配置以下核心指标:
| 指标名 | 类型 | 说明 | 告警阈值 |
|---|---|---|---|
redisson_lock_acquire_seconds_count{result="success"} | Counter | 成功获取锁次数 | 1分钟内下降50%触发告警 |
redisson_lock_acquire_seconds_sum | Summary | 加锁耗时P95 | >100ms持续5分钟触发告警 |
redisson_lock_release_seconds_count{result="failed"} | Counter | 锁释放失败次数 | >0立即告警(说明锁未正确释放) |
redisson_client_connection_pool_used_ratio | Gauge | 连接池使用率 | >90%持续10分钟触发扩容 |
Grafana看板中,我们重点关注“锁等待队列长度”和“看门狗续期失败率”两个面板。前者通过redisson_lock_queue_length指标监控,后者通过解析LockWatchdogTimeoutEvent日志实现。某次大促前,我们发现看门狗失败率突增至0.3%,排查发现是Redis集群CPU使用率超90%,紧急扩容后恢复正常。
5. 常见问题与排查技巧实录
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
tryLock一直返回false,但Redis中无对应key | 1. Redis连接池耗尽 2. Sentinel配置错误,指向了slave节点 3. 网络策略拦截了26379端口 | 1. 查redisson_client_connection_pool_used_ratio指标2. redis-cli -p 26379执行SENTINEL get-master-addr-by-name mymaster3. telnet 192.168.1.10 26379测试连通性 | 1. 调大connection-pool-size2. 检查 sentinel.conf中master-name是否匹配3. 开放防火墙端口 |
| 锁释放后,其他线程仍无法获取(锁“假死”) | 1. 业务线程未在finally中释放锁 2. JVM Crash导致进程退出,看门狗停止续期 3. Redis主从同步延迟,slave上锁key未及时删除 | 1. 检查代码中lock.unlock()是否在finally块2. 查看应用日志是否有OOM或SIGKILL 3. redis-cli -c连接集群,执行GET "stock:lock:123" | 1. 强制代码规范,CR时检查 2. 配置JVM -XX:+ExitOnOutOfMemoryError3. 使用 RedissonClient.forceUnlock()强制清理 |
| 高并发下锁获取耗时飙升(>500ms) | 1. Redis服务器负载过高 2. Netty线程池不足,任务排队 3. leaseTime过小,频繁续期加重Redis压力 | 1. `redis-cli info | grep used_memory<br>2. 查redisson_netty_eventloop_queue_size指标<br>3. 检查leaseTime`是否<30s |
日志中频繁出现Unable to unlock | 1. 锁已被其他线程释放 2. 当前线程不是锁持有者(跨线程调用unlock) 3. Redis响应超时,但实际已执行成功 | 1. 检查lock.isHeldByCurrentThread()判断2. 确认 unlock()是否在同一线程调用3. 查 redisson_lock_release_seconds_count{result="failed"} | 1. 严格遵循“谁加锁谁释放”原则 2. 使用 RLock.lockInterruptibly()替代lock()3. 忽略此日志(属正常网络抖动) |
5.2 我踩过的三个深坑
坑一:在@Transactional中混用Redisson锁
某次订单服务,我在@Transactional方法里先lock.tryLock(),再执行DB操作。结果发现事务回滚后,锁并未自动释放!因为@Transactional的回滚只作用于DB,Redisson锁是独立资源。解决方案:锁的生命周期必须完全包裹在事务外,即先加锁→再开启事务→执行DB→提交事务→最后解锁。否则事务回滚,锁还挂着,后续请求全被阻塞。
坑二:用StringRedisTemplate代替RedissonClient获取锁
为了“统一缓存操作”,有同事把RedissonClient.getLock()换成StringRedisTemplate.opsForValue().setIfAbsent()。表面看一样,实则天壤之别:前者有看门狗、有重试、有线程绑定;后者只是个SETNX,超时后锁自动消失,但业务可能还在执行,导致数据错乱。教训:不要用基础Redis操作模拟高级抽象,该用什么就用什么。
坑三:锁key中拼接了动态参数导致热点
最初我们用"stock:lock:" + userId + ":" + skuId作为锁key,结果发现某个VIP用户(userId=1)的订单请求量占全站30%,所有锁请求都打到Redis同一个slot,造成热点。改为"stock:lock:" + skuId后,请求均匀分散到各节点。记住:锁key的设计,本质是数据分片设计,要让高并发请求尽可能打到不同Redis节点。
5.3 性能压测实录与调优结论
我们用JMeter对库存扣减接口进行压测(100线程,Ramp-up 10s,持续5分钟):
| 配置项 | 初始值 | P95耗时 | 错误率 | 调优后值 | P95耗时 | 错误率 |
|---|---|---|---|---|---|---|
connection-pool-size | 16 | 420ms | 12.3% | 64 | 85ms | 0% |
leaseTime | 10s | 180ms | 5.1% | 30s | 72ms | 0% |
timeout | 10000ms | 210ms | 0% | 3000ms | 68ms | 0% |
netty.threads | 8 | 390ms | 0% | 32 | 65ms | 0% |
结论清晰:连接池和leaseTime是影响吞吐量的两大瓶颈。调优后,QPS从850提升至3200,P95耗时从420ms降至65ms,完全满足大促要求。而timeout从10s降到3s,不仅没增加错误率,反而因减少线程阻塞,提升了整体吞吐。
最后再强调一个实操心得:永远在预发环境用真实流量压测锁配置。本地单机Redis再快,也模拟不出集群网络延迟、主从同步、哨兵选举这些真实故障模式。我们团队规定,任何锁相关配置变更,必须在预发环境跑满24小时压测,监控指标达标后才能上生产。这多花的一天,换来的是一次大促的安稳睡眠。