webMVC和webFlux
這是spring framework提供的兩種不同的Web編程模型
應用場景:
-
用 WebMvc:
項目依賴 Servlet 生態、需要簡單同步代碼,或使用阻塞式數據庫(如 MySQL + JDBC)。 -
用 WebFlux:
需要高并發(如每秒萬級請求)、低資源消耗,或已使用響應式技術棧(如 MongoDB Reactive、Kafka)。
springcloud -gateway
在 Spring Cloud Gateway 中,請求處理默認是基于 異步非阻塞 模型的,這是由其底層使用的 Reactor Netty 和 WebFlux 框架決定的。
你說是gateway是異步的,但是一個請求不還是要等待下游服務器的響應嗎?這不還是同步嗎?
nonono~
[EventLoop線程] 接收請求 → 發起網絡調用 → 立即釋放 → 處理其他請求
↓
[IO完成回調] → 收到響應 → 繼續處理
實際上:
- 同一個線程可以交替處理多個請求的不同階段
- 等待IO時不占用線程(線程可以去處理其他請求)
- 基于回調/事件機制,IO完成后會通知系統
gateway是基于WebFlux的,理由也很合理:
webMVC 最高只能有200并發,而已經到微服務級別的應用,很明顯并發量肯定不會少,網關層面肯定不能用同步去做,異步是最好最確切的選擇。
ContextHolder在同步/異步模式中的實現
contextHolder 個人理解,就是一個全局變量,但是這"全局"是‘一個線程’的范圍
專業的說: 基于 線程/上下文隔離 的臨時數據存儲,生命周期與請求或線程綁定。
必要性:
-
解耦業務代碼:
避免在方法參數中層層傳遞上下文(如用戶身份、跟蹤ID)。 -
在攔截器、Service 層、日志工具等地方都能訪問統一上下文。
原生Servlet–request.setAttribute
- Servlet規范本身并未直接定義線程級別的共享變量接口,只定義了會話范圍的HttpSession
- 但是可以通過ServletRequest獲取,因為在Servlet規范中,每個請求都是由容器分配的獨立線程處理,ServletRequest 和 ServletResponse 對象天然與線程綁定
tomcat 容器內部 則通過ThreadLocal關聯了請求和線程。如果開發者使用ThreadLocal,需要自行清理(存在風險)
WebMvc–ThreadLocal
不同于原生Servlet,這個ThreadLocal不是Tomcat容器給的,而是Springmvc框架本身提供的。
此時: Tomcat 不直接使用 ThreadLocal 存儲 Spring MVC 的請求信息,它只是提供了線程模型(每個請求一個線程),而 Spring MVC 在此基礎上利用 ThreadLocal 存儲請求相關的上下文。
如果自定義使用:
- 定義ThreadLocal工具類
public class MyThreadLocalContext {private static final ThreadLocal<String> currentTenantId = new ThreadLocal<>();public static void setTenantId(String tenantId) {currentTenantId.set(tenantId);}public static String getTenantId() {return currentTenantId.get();}public static void clear() {currentTenantId.remove(); // 防止內存泄漏}
}
- 在 Filter/Interceptor 中設置和清理
ThreadLocal的隱式線程綁定
在工具類中你會發現,其實set/get過程并沒有關聯任何的線程ID
這是因為,ThreadLocal內部實現了自動關聯線程ID
WebFlux
ThreadLocal已經無法處理異步請求的問題了,因為異步請求一般都存在線程的切換
springcloud-gateway提供了幾種機制來處理這個問題
1. Reactor Context
2. TransmittableThreadLocal
簡稱 (TTL) 是阿里開源的一個線程本地變量工具,它是對 Java 標準 ThreadLocal 的增強,主要解決了線程池等異步執行場景下的線程變量傳遞問題。
使用:
- 定義TransmittableThreadLocal工具類
package com.ruoyi.common.core.context;import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.core.utils.StringUtils;/*** 獲取當前線程變量中的 用戶id、用戶名稱、Token等信息 * 注意: 必須在網關通過請求頭的方法傳入,同時在HeaderInterceptor攔截器設置值。 否則這里無法獲取** @author ruoyi*/
public class SecurityContextHolder
{/*** 既然springboot中默認用的是同步請求,必須等待響應完成,為什么這里用異步的threadlocal去存儲?** 1. 防御性編程,為未來的異步拓展預留空間** 2. 即時外部請求是重構的,框架內部可能使用線程池處理一些異步任務* 比如日志記錄:* @GetMapping("/user/info")* public UserInfo getUserInfo() {* // 同步設置用戶上下文* SecurityContextHolder.setUserId("123");** // 同步操作* UserInfo info = userService.getInfo();** // 異步記錄日志(很常見的需求)* logAsync("User info accessed"); // 內部使用線程池,內部會用contextHolder獲取到用戶信息,** return info;* }* 3. springcloud-gateway中請求默認是異步非阻塞的,原因是它是webflux編程*/private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL = new TransmittableThreadLocal<>();public static void set(String key, Object value){Map<String, Object> map = getLocalMap();map.put(key, value == null ? StringUtils.EMPTY : value);}public static String get(String key){Map<String, Object> map = getLocalMap();return Convert.toStr(map.getOrDefault(key, StringUtils.EMPTY));}public static <T> T get(String key, Class<T> clazz){Map<String, Object> map = getLocalMap();return StringUtils.cast(map.getOrDefault(key, null));}public static Map<String, Object> getLocalMap(){Map<String, Object> map = THREAD_LOCAL.get();if (map == null){map = new ConcurrentHashMap<String, Object>();THREAD_LOCAL.set(map);}return map;}public static void setLocalMap(Map<String, Object> threadLocalMap){THREAD_LOCAL.set(threadLocalMap);}public static Long getUserId(){return Convert.toLong(get(SecurityConstants.DETAILS_USER_ID), 0L);}public static void setUserId(String account){set(SecurityConstants.DETAILS_USER_ID, account);}public static String getUserName(){return get(SecurityConstants.DETAILS_USERNAME);}public static void setUserName(String username){set(SecurityConstants.DETAILS_USERNAME, username);}public static String getUserKey(){return get(SecurityConstants.USER_KEY);}public static void setUserKey(String userKey){set(SecurityConstants.USER_KEY, userKey);}public static String getPermission(){return get(SecurityConstants.ROLE_PERMISSION);}public static void setPermission(String permissions){set(SecurityConstants.ROLE_PERMISSION, permissions);}public static void remove(){THREAD_LOCAL.remove();}
}
- 在攔截器中添加和刪除
/*** 自定義請求頭攔截器,將Header數據封裝到線程變量中方便獲取* 注意:此攔截器會同時驗證當前用戶有效期自動刷新有效期** @author ruoyi*/
public class HeaderInterceptor implements AsyncHandlerInterceptor
{@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{if (!(handler instanceof HandlerMethod)){return true;}SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID));SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME));SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY));String token = SecurityUtils.getToken();if (StringUtils.isNotEmpty(token)){LoginUser loginUser = AuthUtil.getLoginUser(token);if (StringUtils.isNotNull(loginUser)){AuthUtil.verifyLoginUserExpire(loginUser);SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);}}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception{SecurityContextHolder.remove();}
}
TTL底層依賴ThreadLocal,但是它的核心是 跨線程傳遞 ThreadLocal 值。
具體怎么做到的:父子線程之間的值傳遞