Spring Webflux共享资源并发访问:ReentrantLock使用合理性及优化方案
问题
我正在实现一个调用外部API的服务,包含两种请求类型:client.authorize()用于获取有效期1小时的新令牌,client.performRequest(..)需要使用前者获取的授权令牌从API拉取数据。Client类基于Spring Reactive的WebClient实现。请问用ReentrantLock同步检查并刷新过期令牌的代码块是否可行?会不会破坏响应式模型?响应式编程里有没有更便捷的共享资源访问方式?
@Service @RequiredArgsConstructor public class Service { private final Client client; @Setter(AccessLevel.NONE) private Lock authLock = new ReentrantLock(); @Setter(AccessLevel.NONE) private String authToken; @Setter(AccessLevel.NONE) private LocalDateTime tokenExpirationDatetime; public Mono<Response> performRequest(Request request) { return preAuthorize() .flatMap(token -> client.performRequest(request, token)); } private Mono<String> preAuthorize() { authLock.lock(); try { if (existsActualToken()) { return Mono.just(authToken); } return refreshToken(); } finally { authLock.unlock(); } } private boolean existsActualToken() { return authToken != null && tokenExpirationDatetime.isAfter(LocalDateTime.now()); } private Mono<String> refreshToken() { return client.authorize() .doOnNext(response -> { this.authToken = response.token(); this.tokenExpirationDatetime = response.expirationDatetime(); }) .map(Response::token); } }
回答
1. ReentrantLock是否可行?
可行,但存在严重缺陷:
ReentrantLock是阻塞式锁,高并发场景下会导致大量线程阻塞等待,违背响应式非阻塞的核心原则,甚至可能引发线程池耗尽问题。- 当前代码逻辑有漏洞:
refreshToken()返回的是异步Mono,但锁在return refreshToken()后就会释放,其他线程可能在令牌还未刷新完成时进入,重复触发授权请求,导致多次调用client.authorize()。
2. 是否破坏响应式模型?
是的,完全破坏。响应式编程的核心是异步非阻塞、事件驱动,而ReentrantLock会强制线程阻塞,打破Reactor的线程调度策略(比如弹性线程池的非阻塞设计)。混合阻塞与非阻塞代码会让服务失去响应式架构的优势,无法高效处理高并发请求,也无法利用背压机制保护系统稳定性。
3. 响应式编程中更合适的共享资源访问方式
推荐使用Reactor原生的Mono.cache()结合Mono.defer()实现令牌的自动刷新与共享,完全遵循非阻塞原则:
优化方案一:带过期预判的缓存实现
@Service @RequiredArgsConstructor public class Service { private final Client client; private volatile Mono<String> cachedTokenMono; private String authToken; private LocalDateTime tokenExpirationDatetime; public Mono<Response> performRequest(Request request) { return getValidToken() .flatMap(token -> client.performRequest(request, token)); } private Mono<String> getValidToken() { if (cachedTokenMono == null || isTokenExpired()) { // 用defer包装,确保每次刷新都是新的授权请求 cachedTokenMono = Mono.defer(client::authorize) .doOnNext(response -> { this.authToken = response.token(); this.tokenExpirationDatetime = response.expirationDatetime(); }) .map(Response::token) // 提前30秒刷新令牌,避免令牌刚好过期时请求失败 .cache(_ -> Duration.between(LocalDateTime.now(), tokenExpirationDatetime.minusSeconds(30))); } return cachedTokenMono; } private boolean isTokenExpired() { return tokenExpirationDatetime == null || tokenExpirationDatetime.isBefore(LocalDateTime.now()); } }
优化方案二:简洁的自动刷新缓存
@Service @RequiredArgsConstructor public class Service { private final Client client; private Mono<String> tokenMono; public Mono<Response> performRequest(Request request) { return getToken() .flatMap(token -> client.performRequest(request, token)); } private Mono<String> getToken() { if (tokenMono == null) { tokenMono = client.authorize() .map(AuthResponse::token) // 缓存55分钟(比令牌有效期少5分钟),自动触发刷新 .cache(Duration.ofMinutes(55)) // 授权失败时重置缓存,重新发起请求 .onErrorResume(e -> { tokenMono = null; return getToken(); }); } return tokenMono; } }
方案优势
- 完全非阻塞:所有操作都在响应式流中处理,无线程阻塞
- 自动去重:高并发下只会触发一次授权请求,避免重复调用外部API
- 自动刷新:利用
cache()的超时机制,在令牌即将过期时自动刷新 - 符合响应式模型:遵循Reactor异步非阻塞原则,充分发挥Spring Reactive的性能优势
内容的提问来源于stack exchange,提问作者Andrei Yusupau
相关产品推荐
相关产品推荐

