在 Dubbo 等非 Web 容器的微服務架構中,無法直接使用 Spring 的 request 作用域 Bean。本文展示如何利用 ThreadLocal 實現「一次請求、多次讀取、單次 IO」的請求級緩存,顯著降低重複數據庫查詢開銷。
價格遷移業務:單次遷移調用 5 個服務接口,每個接口需根據 Token 解析用戶信息(userId、userName)寫入審計字段。實測單次遷移觸發 20+ 次 getTokenInfo RPC 調用,嚴重拖慢性能。
@Service
public class ProductContext {
private static final Logger log = LoggerFactory.getLogger(ProductContext.class);
private final ThreadLocal<CiaUserInfo> userCache = new ThreadLocal<>();
public CiaUserInfo getUser(String token) {
CiaUserInfo cached = userCache.get();
if (cached != null) {
log.debug("緩存命中 getTokenInfo");
return cached;
}
return null; // 未命中,調用方負責查 DB 並 set
}
public void setUser(CiaUserInfo info) {
userCache.set(info);
log.debug("寫入緩存 getTokenInfo");
}
public void clear() {
userCache.remove();
log.debug("清理請求緩存");
}
}
@Service
public class UserServiceImpl implements UserService {
@Autowired private ProductContext ctx;
@Autowired private UserDao userDao;
@Override
public CiaUserInfo getTokenInfo(String token) {
CiaUserInfo user = ctx.getUser(token);
if (user != null) return user;
// 緩存未命中:查數據庫
user = userDao.selectByToken(token);
if (user != null) ctx.setUser(user);
return user;
}
}
自定義註解 @LocalCacheContext 標記入口方法,切面在方法結束時強制清理 ThreadLocal,防止線程池復用導致數據洩漏。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LocalCacheContext { boolean enable() default true; }
@Aspect
@Component
public class LocalCacheContextInterceptor {
@Autowired private ProductContext ctx;
private static final Logger log = LoggerFactory.getLogger(LocalCacheContextInterceptor.class);
@Pointcut("execution(* com.example.service.impl.*.*(..))")
public void servicePointcut() {}
@After("servicePointcut() && @annotation(localCacheContext)")
public void after(JoinPoint jp, LocalCacheContext localCacheContext) {
if (localCacheContext.enable()) {
ctx.clear();
log.debug("請求結束,清理 ThreadLocal 緩存");
}
}
}
@Service
public class MigrationFacade {
@Autowired private MigrationService migrationService;
@LocalCacheContext // 啟用請求級緩存
public void migratePrice(Long priceId) {
migrationService.migrate(priceId); // 內部多次調用 getTokenInfo 命中緩存
}
}
| 指標 | 優化前 | 優化後 |
|---|---|---|
| 單次遷移 DB 查詢次數 | 20+ | 1 |
| 平均耗時 | ~800ms | ~120ms |
| 數據庫 QPS 壓力 | 高 | 降低 95%+ |
ThreadLocal.remove() 在 finally 塊或 AOP 後置通知中調用,否則線程池復用導致數據污染、內存洩漏。CompletableFuture、線程池提交任務,ThreadLocal 不會自動傳遞,需用 TransmittableThreadLocal (阿里 TTL) 或手動父子線程傳值。