做開發的人都知道流水號這個概念,有業務流水號,交易流水號,請求流水號等等,各種流水號。
無論是啥名字的流水號,目的都是爲了在某個維度,讓一系列動作有一個唯一的標識。後面方便查日誌,查問題。系統間交互可以防止扯皮。
比如交易流水號,唯一標識一筆交易,這邊所說的交易可以是無業務含義的請求,也可以是賬務交易。如果是標識無業務含義的請求。一般會在交易開始時生成一個32位或者64位的唯一編碼。這個編碼會在交易的縱向流程中傳遞,可以用參數傳遞,也可以放到上下文 Context 中,還可以放到 ThreadLocal 中。
在單體應用 Singleton Application 中,對流水號的處理比較簡單,無論是參數傳遞還是 ThreadLocal 都沒有什麼難度。
在微服務環境下,一筆交易會經過多次微服務之間的交互,並且需要把流水號傳遞下去,它的傳遞鏈路可能是這樣的(服務A) --> (服務B --> )(服務C) --> (服務D) -->(服務E),使用 dubbo 作爲 RPC 框架。有2種傳遞流水號的方式(當然這只是我自己覺得,可能還有更好的處理方式),下面詳細說說。
參數傳遞法
這種方式很容易理解,就是把流水號作爲接口的一個參數 (String jnl) 。這種方式結構簡單,實現起來也很簡單。但是有諸多問題。
1.幾乎所有暴露的接口服務都會有這麼個參數。不好看。
2.交易流水號,實際與業務本身相關性比較低。微服務之間的參數應該儘量與業務強相關,而不是佔用單獨的參數位置來傳遞與業務無關的東西。
3.這樣的方式,維護起來麻煩,寫啥服務都得加這麼個參數,移植性也很差。
我個人不喜歡這樣的做法。
利用dubbo的Attachment做隱式傳遞
還是模擬剛剛的傳遞過程,把一個32位的Jnl在服務間傳遞。(服務A) --> (服務B --> )(服務C)
1.A服務是入口,生成32位流水號 Jnl
2.A服務把Jnl放到 ThreadLocal 中
3.在調用B系統之前,把 Jnl 從 ThreadLocal 取出並且放入 Invocation Attachment 中(這個過程由 Dubbo Filter 做)
4.B系統接收 Invocation 信息,並且反序列化。從 Attacment 中取出 Jnl 放入 ThreadLocal中(這個過程由 Dubbo Filter 做)
5.B系統調用C系統時重複剛剛的幾步操作。
簡單理解:在運行過程中, Jnl 始終從一個系統的 ThreadLocal 轉移到另一個系統的 ThreadLocal 中。轉移過程通過取和放都通過 Dubbo Filter 來完成。
這個過程涉及到3個角色。
① ThreadLocal
② Consumer 一方的 Filter ,作用是讀取 ThreadLocal 中的Jnl放入 Invocation Attachment中
③ Privoder 一方 Filter ,作用是讀取 Invocation Attachment 中的Jnl放入 ThreadLocal 中
下面看代碼實現
<pre language="typescript" code_block="true">public class AccessJnlUtil {
// 獲取
public static String getAccessJnl() {
return (String) RpcContext.getContext().get("AccessJnl");
}
// 放入
public static void setAccessJnl(String accessJnl) {
RpcContext.getContext().set("AccessJnl", accessJnl);
}
public static void deleteAccessJnl() {
RpcContext.getContext().remove("AccessJnl");
}
}
複製代碼</pre>
AccessJnlUtil 是用來對AccessJnl做put和set操作的。利用了 Dubbo RpcContext , RpcContext 本身就是 ThreadLocal 的封裝,不熟悉的話可以追蹤下源碼,這邊不再贅述。
<pre language="typescript" code_block="true">public class AccessJnlConsumerFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
invocation.getAttachments().put("AccessJnl", AccessJnlUtil.getAccessJnl());
return invoker.invoke(invocation);
}
}
AccessJnlConsumerFilter 是消費方的 Filter ,在實際調用 invoke 之前,把 AccessJnl 從 ThreadLocal 中取出,放到 Invocation Attachment 中。
<pre language="typescript" code_block="true">public class AccessJnlProviderFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
AccessJnlUtil.setAccessJnl(RpcContext.getContext().getAttachment("AccessJnl"));
return invoker.invoke(invocation);
}
}
AccessJnlProviderFilter 是消費方的 Filter ,在實際調用 invoke 之前,把 AccessJnl 從 Invocation Attachment 中取出,放到 ThreadLocal 中。
然後就是把兩個 Filter 配置到系統中,具體配置方式可以參考 Dubbo文檔
總結
利用 dubbo 的 Attachment 做隱式傳遞。我個人比較喜歡這樣的處理方式。相對優雅。