因为分布式锁会导致请求串行化、吞吐骤降,而信号量(RSemaphore)可控制“最多N个线程并发查库”,既防数据库击穿又保留并发度;N需根据连接池大小、查询耗时、QPS综合估算,且必须配合空值缓存或布隆过滤器。

为什么不用分布式锁而选信号量?
分布式锁(如 RLock)在缓存雪崩场景下容易造成请求串行化,吞吐骤降;而信号量(RSemaphore)允许你控制「同时最多几个线程能穿透缓存查库」,既防压垮数据库,又保留一定并发度。关键区别在于:锁是 1 个,信号量是 N 个——这个 N 要根据数据库连接池大小、单次查询耗时、平均 QPS 综合估算,比如 MySQL 连接池设了 20,就别把信号量设成 50。
常见错误现象包括:RedissonTimeoutException 频发、接口 P99 响应时间突增但 CPU 不高、数据库慢日志里出现大量重复 ID 查询——这往往说明信号量阈值设得太低或没兜底逻辑。
- 信号量不解决缓存失效本身,只控制失效后的并发冲击
- 必须配合空值缓存或布隆过滤器,否则信号量会被无效请求耗尽
- 获取失败不能直接 throw,要 fallback 到延迟重试或降级返回
怎么用 Redisson 的 RSemaphore 控制并发查库?
核心是在缓存未命中、准备回源查 DB 前,先尝试 acquire 一个许可。注意不是在方法入口加,而是精准卡在「即将访问数据库」那一行之前。
示例代码片段(Spring Boot 3 + Redisson 3.23.4):
@Service
public class UserService {
@Autowired private RedissonClient redissonClient;
@Autowired private UserMapper userMapper;
<pre class="brush:php;toolbar:false;">public User getUserById(Long id) {
String key = "user:" + id;
String json = redisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
return JSONUtil.toBean(json, User.class);
}
// 1. 尝试获取信号量许可(最多 5 个并发查库)
RSemaphore semaphore = redissonClient.getSemaphore("sem:user:db:query");
try {
if (!semaphore.tryAcquire(1, 3, TimeUnit.SECONDS)) {
// 获取失败:走降级或延迟重试,避免阻塞
return fallbackUser(id);
}
// 2. 再次检查缓存(防止其他线程已写入)
json = redisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
return JSONUtil.toBean(json, User.class);
}
// 3. 查库并写缓存
User user = userMapper.selectById(id);
if (user != null) {
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(user),
3600 + ThreadLocalRandom.current().nextInt(360) - 180,
TimeUnit.SECONDS);
} else {
// 空值缓存,防穿透
redisTemplate.opsForValue().set("null:" + key, "", 120, TimeUnit.SECONDS);
}
return user;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return fallbackUser(id);
} finally {
// 必须释放,哪怕查库异常也要放
if (semaphore.isPermitAvailable()) {
semaphore.release();
}
}
}}
几个关键点:
-
tryAcquire(1, 3, TimeUnit.SECONDS)中的 3 秒超时必须设,否则线程可能永久挂起 - 获取成功后要二次检查缓存,这是 Double-Check,防止 A 获取许可期间 B 已写入
- 释放操作放在
finally,且用isPermitAvailable()判定是否真持有许可再 release,避免重复释放报错 - 信号量 key 建议带业务前缀(如
sem:user:db:query),方便监控和清理
信号量初始化与配置注意事项
Redisson 默认不会自动创建信号量,首次 getSemaphore("xxx") 只是获取引用,真正初始化发生在第一次 tryAcquire 或 availablePermits() 调用时。但生产环境建议显式初始化,并设置初始许可数。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
推荐在启动时预热:
@PostConstruct
public void initSemaphore() {
RSemaphore semaphore = redissonClient.getSemaphore("sem:user:db:query");
// 设置最大许可数为 5,初始可用数也为 5
if (semaphore.trySetPermits(5)) {
log.info("RSemaphore sem:user:db:query initialized with 5 permits");
}
}容易踩的坑:
- 没调
trySetPermits(),首次 acquire 会创建默认 1 个许可,后续扩容需手动调用addPermits(),线上不敢轻易动 - 信号量 key 没做命名空间隔离,多个服务共用同一 key,互相干扰
- 忽略 Redisson 客户端连接状态,
redissonClient为 null 或关闭时调用getSemaphore报 NPE - 没配
lockWatchdogTimeout,长时间 GC 可能导致信号量 lease 自动释放,引发许可数错乱
怎么验证信号量真的生效了?
光看代码不顶用。真实压测时重点观察三处指标:
一是 Redis 中信号量对应的 key(格式为 redisson_semaphore:{sem:user:db:query}),用 redis-cli 直接查其 hash 字段:hgetall "redisson_semaphore:{sem:user:db:query}",确认 permits 和 availablePermits 值符合预期波动;
二是应用日志里 semaphore.tryAcquire 的返回率,正常应在 80%~95% 区间,长期低于 70% 说明阈值设太小;
三是数据库连接池活跃数(如 HikariCP 的 ActiveConnections),应稳定在信号量上限附近,而不是随 QPS 暴涨暴跌。
最常被忽略的是信号量的「生命周期管理」:它不随 JVM 退出自动销毁,Redis 里会一直存在。上线新版本或调整阈值时,得主动执行 semaphore.delete() 再重建,否则旧配置残留可能引发线上事故。

















