首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

微服務無狀態環境下實現 Per-Request 緩存:ThreadLocal 方案提升 IO 效率實戰

     編輯:Java 綜合教學

微服務無狀態環境下的 Per-Request 緩存實戰

在 Dubbo 等非 Web 容器的微服務架構中,無法直接使用 Spring 的 request 作用域 Bean。本文展示如何利用 ThreadLocal 實現「一次請求、多次讀取、單次 IO」的請求級緩存,顯著降低重複數據庫查詢開銷。

問題背景

價格遷移業務:單次遷移調用 5 個服務接口,每個接口需根據 Token 解析用戶信息(userId、userName)寫入審計字段。實測單次遷移觸發 20+ 次 getTokenInfo RPC 調用,嚴重拖慢性能。

解決方案:ThreadLocal 請求上下文

1. 緩存容器 ProductContext

@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("清理請求緩存");
    }
}

2. 業務服務改造:先查緩存,再查 DB

@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;
    }
}

3. 請求邊界清理:AOP 攔截器

自定義註解 @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 緩存");
        }
    }
}

4. 入口方法標記

@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%+

注意事項與最佳實踐

  1. 必須清理:ThreadLocal.remove() 在 finally 塊或 AOP 後置通知中調用,否則線程池復用導致數據污染、內存洩漏。
  2. 緩存粒度:僅緩存「不可變、高頻、昂貴」的數據(用戶信息、配置項、權限集合),避免緩存大對象佔用堆內存。
  3. 異步線程傳遞:若業務中使用 CompletableFuture、線程池提交任務,ThreadLocal 不會自動傳遞,需用 TransmittableThreadLocal (阿里 TTL) 或手動父子線程傳值。
  4. 非 Web 環境適用:Dubbo、gRPC、MQ 消費端等無 Servlet Request 的場景均可复用此模式。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved