.NET?Core分布式鏈路追蹤框架的基本實(shí)現(xiàn)原理
分布式追蹤
什么是分布式追蹤
分布式系統(tǒng)
當(dāng)我們使用 Google 或者 百度搜索時(shí),查詢服務(wù)會(huì)將關(guān)鍵字分發(fā)到多臺(tái)查詢服務(wù)器,每臺(tái)服務(wù)器在自己的索引范圍內(nèi)進(jìn)行搜索,搜索引擎可以在短時(shí)間內(nèi)獲得大量準(zhǔn)確的搜索結(jié)果;同時(shí),根據(jù)關(guān)鍵字,廣告子系統(tǒng)會(huì)推送合適的相關(guān)廣告,還會(huì)從競(jìng)價(jià)排名子系統(tǒng)獲得網(wǎng)站權(quán)重。通常一個(gè)搜索可能需要成千上萬(wàn)臺(tái)服務(wù)器參與,需要經(jīng)過(guò)許多不同的系統(tǒng)提供服務(wù)。
多臺(tái)計(jì)算機(jī)通過(guò)網(wǎng)絡(luò)組成了一個(gè)龐大的系統(tǒng),這個(gè)系統(tǒng)即是分布式系統(tǒng)。
在微服務(wù)或者云原生開發(fā)中,一般認(rèn)為分布式系統(tǒng)是通過(guò)各種中間件/服務(wù)網(wǎng)格連接的,這些中間件提供了共享資源、功能(API等)、文件等,使得整個(gè)網(wǎng)絡(luò)可以當(dāng)作一臺(tái)計(jì)算機(jī)進(jìn)行工作。
分布式追蹤
在分布式系統(tǒng)中,用戶的一個(gè)請(qǐng)求會(huì)被分發(fā)到多個(gè)子系統(tǒng)中,被不同的服務(wù)處理,最后將結(jié)果返回給用戶。用戶發(fā)出請(qǐng)求和獲得結(jié)果這段時(shí)間是一個(gè)請(qǐng)求周期。
當(dāng)我們購(gòu)物時(shí),只需要一個(gè)很簡(jiǎn)單的過(guò)程:
獲取優(yōu)惠劵 -> 下單 -> 付款 -> 等待收貨
然而在后臺(tái)系統(tǒng)中,每一個(gè)環(huán)節(jié)都需要經(jīng)過(guò)多個(gè)子系統(tǒng)進(jìn)行協(xié)作,并且有嚴(yán)格的流程。例如在下單時(shí),需要檢查是否有優(yōu)惠卷、優(yōu)惠劵能不能用于當(dāng)前商品、當(dāng)前訂單是否符合使用優(yōu)惠劵條件等。
下圖是一個(gè)用戶請(qǐng)求后,系統(tǒng)處理請(qǐng)求的流程。
圖中出現(xiàn)了很多箭頭,這些箭頭指向了下一步要流經(jīng)的服務(wù)/子系統(tǒng),這些箭頭組成了鏈路網(wǎng)絡(luò)。
在一個(gè)復(fù)雜的分布式系統(tǒng)中,任何子系統(tǒng)出現(xiàn)性能不佳的情況,都會(huì)影響整個(gè)請(qǐng)求周期。根據(jù)上圖,我們?cè)O(shè)想:
1.系統(tǒng)中有可能每天都在增加新服務(wù)或刪除舊服務(wù),也可能進(jìn)行升級(jí),當(dāng)系統(tǒng)出現(xiàn)錯(cuò)誤,我們?nèi)绾味ㄎ粏栴}?
2.當(dāng)用戶請(qǐng)求時(shí),響應(yīng)緩慢,怎么定位問題?
3.服務(wù)可能由不同的編程語(yǔ)言開發(fā),1、2 定位問題的方式,是否適合所有編程語(yǔ)言?
分布式追蹤有什么用呢
隨著微服務(wù)和云原生開發(fā)的興起,越來(lái)越多應(yīng)用基于分布式進(jìn)行開發(fā),但是大型應(yīng)用拆分為微服務(wù)后,服務(wù)之間的依賴和調(diào)用變得越來(lái)越復(fù)雜,這些服務(wù)是不同團(tuán)隊(duì)、使用不同語(yǔ)言開發(fā)的,部署在不同機(jī)器上,他們之間提供的接口可能不同(gRPC、Restful api等)。
為了維護(hù)這些服務(wù),軟件領(lǐng)域出現(xiàn)了 Observability 思想,在這個(gè)思想中,對(duì)微服務(wù)的維護(hù)分為三個(gè)部分:
- 度量(
Metrics
):用于監(jiān)控和報(bào)警; - 分布式追蹤(Tracing):用于記錄系統(tǒng)中所有的跟蹤信息;
- 日志(Logging):記錄每個(gè)服務(wù)只能中離散的信息;
這三部分并不是獨(dú)立開來(lái)的,例如 Metrics 可以監(jiān)控 Tracing 、Logging 服務(wù)是否正常運(yùn)行。Tacing 和 Metrics 服務(wù)在運(yùn)行過(guò)程中會(huì)產(chǎn)生日志。
近年來(lái),出現(xiàn)了 APM 系統(tǒng),APM 稱為 應(yīng)用程序性能管理系統(tǒng),可以進(jìn)行 軟件性能監(jiān)視和性能分析。APM 是一種 Metrics,但是現(xiàn)在有融合 Tracing 的趨勢(shì)。
回歸正題,分布式追蹤系統(tǒng)(Tracing)有什么用呢?這里可以以 Jaeger 舉例,它可以:
- 分布式跟蹤信息傳遞
- 分布式事務(wù)監(jiān)控
- 服務(wù)依賴性分析
- 展示跨進(jìn)程調(diào)用鏈
- 定位問題
- 性能優(yōu)化
Jaeger 需要結(jié)合后端進(jìn)行結(jié)果分析,jaeger 有個(gè) Jaeger UI,但是功能并不多,因此還需要依賴 Metrics 框架從結(jié)果呈現(xiàn)中可視化,以及自定義監(jiān)控、告警規(guī)則,所以很自然 Metrics 可能會(huì)把 Tracing 的事情也做了。
Dapper
Dapper 是 Google 內(nèi)部使用的分布式鏈路追蹤系統(tǒng),并沒有開源。
Dapper 用戶接口:
分布式追蹤系統(tǒng)的實(shí)現(xiàn)
下圖是一個(gè)由用戶 X 請(qǐng)求發(fā)起的,穿過(guò)多個(gè)服務(wù)的分布式系統(tǒng),A、B、C、D、E 表示不同的子系統(tǒng)或處理過(guò)程。
在這個(gè)圖中, A 是前端,B、C 是中間層、D、E 是 C 的后端。這些子系統(tǒng)通過(guò) rpc 協(xié)議連接,例如 gRPC。
一個(gè)簡(jiǎn)單實(shí)用的分布式鏈路追蹤系統(tǒng)的實(shí)現(xiàn),就是對(duì)服務(wù)器上每一次請(qǐng)求以及響應(yīng)收集跟蹤標(biāo)識(shí)符(message identifiers)和時(shí)間戳(timestamped events)。
分布式服務(wù)的跟蹤系統(tǒng)需要記錄在一次特定的請(qǐng)求后系統(tǒng)中完成的所有工作的信息。用戶請(qǐng)求可以是并行的,同一時(shí)間可能有大量的動(dòng)作要處理,一個(gè)請(qǐng)求也會(huì)經(jīng)過(guò)系統(tǒng)中的多個(gè)服務(wù),系統(tǒng)中時(shí)時(shí)刻刻都在產(chǎn)生各種跟蹤信息,必須將一個(gè)請(qǐng)求在不同服務(wù)中產(chǎn)生的追蹤信息關(guān)聯(lián)起來(lái)。
為了將所有記錄條目與一個(gè)給定的發(fā)起者X關(guān)聯(lián)上并記錄所有信息,現(xiàn)在有兩種解決方案,黑盒(black-box)和基于標(biāo)注(annotation-based)的監(jiān)控方案。
黑盒方案:
假定需要跟蹤的除了上述信息之外沒有額外的信息,這樣使用統(tǒng)計(jì)回歸技術(shù)來(lái)推斷兩者之間的關(guān)系。
基于標(biāo)注的方案:
依賴于應(yīng)用程序或中間件明確地標(biāo)記一個(gè)全局ID,從而連接每一條記錄和發(fā)起者的請(qǐng)求。
優(yōu)缺點(diǎn):
雖然黑盒方案比標(biāo)注方案更輕便,他們需要更多的數(shù)據(jù),以獲得足夠的精度,因?yàn)樗麄円蕾囉诮y(tǒng)計(jì)推論?;跇?biāo)注的方案最主要的缺點(diǎn)是,很明顯,需要代碼植入。在我們的生產(chǎn)環(huán)境中,因?yàn)樗械膽?yīng)用程序都使用相同的線程模型,控制流和 RPC 系統(tǒng),我們發(fā)現(xiàn),可以把代碼植入限制在一個(gè)很小的通用組件庫(kù)中,從而實(shí)現(xiàn)了監(jiān)測(cè)系統(tǒng)的應(yīng)用對(duì)開發(fā)人員是有效地透明。
Dapper 基于標(biāo)注的方案,接下來(lái)我們將介紹 Dapper 中的一些概念知識(shí)。
跟蹤樹和 span
從形式上看,Dapper 跟蹤模型使用的是樹形結(jié)構(gòu),Span 以及 Annotation。
在前面的圖片中,我們可以看到,整個(gè)請(qǐng)求網(wǎng)絡(luò)是一個(gè)樹形結(jié)構(gòu),用戶請(qǐng)求是樹的根節(jié)點(diǎn)。在 Dapper 的跟蹤樹結(jié)構(gòu)中,樹節(jié)點(diǎn)是整個(gè)架構(gòu)的基本單元。
span 稱為跨度,一個(gè)節(jié)點(diǎn)在收到請(qǐng)求以及完成請(qǐng)求的過(guò)程是一個(gè) span,span 記錄了在這個(gè)過(guò)程中產(chǎn)生的各種信息。每個(gè)節(jié)點(diǎn)處理每個(gè)請(qǐng)求時(shí)都會(huì)生成一個(gè)獨(dú)一無(wú)二的的 span id,當(dāng) A -> C -> D 時(shí),多個(gè)連續(xù)的 span 會(huì)產(chǎn)生父子關(guān)系,那么一個(gè) span 除了保存自己的 span id,也需要關(guān)聯(lián)父、子 span id。生成 span id 必須是高性能的,并且能夠明確表示時(shí)間順序,這點(diǎn)在后面介紹 Jaeger 時(shí)會(huì)介紹。
Annotation 譯為注釋,在一個(gè) span 中,可以為 span 添加更多的跟蹤細(xì)節(jié),這些額外的信息可以幫助我們監(jiān)控系統(tǒng)的行為或者幫助調(diào)試問題。Annotation 可以添加任意內(nèi)容。
到此為止,簡(jiǎn)單介紹了一些分布式追蹤以及 Dapper 的知識(shí),但是這些不足以嚴(yán)謹(jǐn)?shù)恼f(shuō)明分布式追蹤的知識(shí)和概念,建議讀者有空時(shí)閱讀 Dapper 論文。
要實(shí)現(xiàn) Dapper,還需要代碼埋點(diǎn)、采樣、跟蹤收集等,這里就不再細(xì)談了,后面會(huì)介紹到,讀者也可以看看論文。
Jaeger 和 OpenTracing
OpenTracing
OpenTracing 是與分布式系統(tǒng)無(wú)關(guān)的API和用于分布式跟蹤的工具,它不僅提供了統(tǒng)一標(biāo)準(zhǔn)的 API,還致力于各種工具,幫助開發(fā)者或服務(wù)提供者開發(fā)程序。
OpenTracing 為標(biāo)準(zhǔn) API 提供了接入 SDK,支持這些語(yǔ)言:Go, JavaScript, Java, Python, Ruby, PHP, Objective-C, C++, C#。
當(dāng)然,我們也可以自行根據(jù)通訊協(xié)議,自己封裝 SDK。
接下來(lái)我們要一點(diǎn)點(diǎn)弄清楚 OpenTracing 中的一些概念和知識(shí)點(diǎn)。由于 jaeger 是 OpenTracing 最好的實(shí)現(xiàn),因此后面講 Jaeger 就是 Opentracing ,不需要將兩者嚴(yán)格區(qū)分。
Jaeger 結(jié)構(gòu)
首先是 JAEGER 部分,這部分是代碼埋點(diǎn)等流程,在分布式系統(tǒng)中處理,當(dāng)一個(gè)跟蹤完成后,通過(guò) jaeger-agent 將數(shù)據(jù)推送到 jaeger-collector。jaeger-collector 負(fù)責(zé)處理四面八方推送來(lái)的跟蹤信息,然后存儲(chǔ)到后端,可以存儲(chǔ)到 ES、數(shù)據(jù)庫(kù)等。Jaeger-UI 可以將讓用戶在界面上看到這些被分析出來(lái)的跟蹤信息。
OpenTracing API 被封裝成編程語(yǔ)言的 SDK(jaeger-client),例如在 C# 中是 .dll ,Java 是 .jar,應(yīng)用程序代碼通過(guò)調(diào)用 API 實(shí)現(xiàn)代碼埋點(diǎn)。
jaeger-Agent 是一個(gè)監(jiān)聽在 UDP 端口上接收 span 數(shù)據(jù)的網(wǎng)絡(luò)守護(hù)進(jìn)程,它會(huì)將數(shù)據(jù)批量發(fā)送給 collector。
OpenTracing 數(shù)據(jù)模型
在 OpenTracing 中,跟蹤信息被分為 Trace、Span 兩個(gè)核心,它們按照一定的結(jié)構(gòu)存儲(chǔ)跟蹤信息,所以它們是 OpenTracing 中數(shù)據(jù)模型的核心。
Trace 是一次完整的跟蹤,Trace 由多個(gè) Span 組成。下圖是一個(gè) Trace 示例,由 8 個(gè) Span 組成。
Tracing:
a Trace can be thought of as a directed acyclic graph (DAG) of Spans。
有點(diǎn)難翻譯,大概意思是 Trace 是多個(gè) Span 組成的有向非循環(huán)圖。
在上面的示例中,一個(gè) Trace 經(jīng)過(guò)了 8 個(gè)服務(wù),A -> C -> F -> G 是有嚴(yán)格順序的,但是從時(shí)間上來(lái)看,B 、C 是可以并行的。為了準(zhǔn)確表示這些 Span 在時(shí)間上的關(guān)系,我們可以用下圖表示:
有個(gè)要注意的地方, 并不是 A -> C -> F 表示 A 執(zhí)行結(jié)束,然后 C 開始執(zhí)行,而是 A 執(zhí)行過(guò)程中,依賴 C,而 C 依賴 F。因此,當(dāng) A 依賴 C 的過(guò)程完成后,最終回到 A 繼續(xù)執(zhí)行。所以上圖中 A 的跨度最大。
Span 格式
要深入學(xué)習(xí),就必須先了解 Span,請(qǐng)讀者認(rèn)真對(duì)照下面的圖片和 Json:
json 地址:https://github.com/whuanle/DistributedTracing/issues/1
后續(xù)將圍繞這張圖片和 Json 來(lái)舉例講述 Span 相關(guān)知識(shí)。
Trace
一個(gè)簡(jiǎn)化的 Trace 如下:
注:不同編程語(yǔ)言的字段名稱有所差異,gRPC 和 Restful API 的格式也有所差異。
"traceID": "790e003e22209ca4", "spans":[...], "processes":{...}
前面說(shuō)到,在 OpenTracing 中,Trace 是一個(gè)有向非循環(huán)圖,那么 Trace 必定有且只有一個(gè)起點(diǎn)。
這個(gè)起點(diǎn)會(huì)創(chuàng)建一個(gè) Trace 對(duì)象,這個(gè)對(duì)象一開始初始化了 trace id 和 process,trace id 是一個(gè) 32 個(gè)長(zhǎng)度的字符串組成,它是一個(gè)時(shí)間戳,而 process 是起點(diǎn)進(jìn)程所在主機(jī)的信息。
下面筆者來(lái)說(shuō)一些一下 trace id 是怎么生成的。trace id 是 32個(gè)字符串組成,而實(shí)際上只使用了 16 個(gè),因此,下面請(qǐng)以 16 個(gè)字符長(zhǎng)度去理解這個(gè)過(guò)程。
首先獲取當(dāng)前時(shí)間戳,例如獲得 1611467737781059
共 16 個(gè)數(shù)字,單位是微秒,表示時(shí)間 2021-01-24 13:55:37,秒以下的單位這里就不給出了,明白表示時(shí)間就行。
在 C# 中,將當(dāng)前時(shí)間轉(zhuǎn)為這種時(shí)間戳的代碼:
public static long ToTimestamp(DateTime dateTime) { DateTime dt1970 = new DateTime(1970, 1, 1, 0, 0, 0, 0); return (dateTime.Ticks - dt1970.Ticks)/10; } // 結(jié)果:1611467737781059
如果我們直接使用 Guid 生成或者 string 存儲(chǔ),都會(huì)消耗一些性能和內(nèi)存,而使用 long,剛剛好可以表示時(shí)間戳,還可以節(jié)約內(nèi)存。
獲得這個(gè)時(shí)間戳后,要傳輸?shù)?Jaeger Collector,要轉(zhuǎn)為 byet 數(shù)據(jù),為什么要這樣不太清楚,按照要求傳輸就是了。
將 long 轉(zhuǎn)為一個(gè) byte 數(shù)組:
var bytes = BitConverter.GetBytes(time); // 大小端 if (BitConverter.IsLittleEndian) { Array.Reverse(bytes); }
long 占 8 個(gè)字節(jié),每個(gè) byte 值如下:
0x00 0x05 0xb9 0x9f 0x12 0x13 0xd3 0x43
然后傳輸?shù)?Jaeger Collector 中,那么獲得的是一串二進(jìn)制,怎么表示為字符串的 trace id?
可以先還原成 long,然后將 long 輸出為 16 進(jìn)制的字符串:
轉(zhuǎn)為字符串(這是C#):
Console.WriteLine(time.ToString("x016"));
結(jié)果:
0005b99f1213d343
Span id 也是這樣轉(zhuǎn)的,每個(gè) id 因?yàn)榕c時(shí)間戳相關(guān),所以在時(shí)間上是唯一的,生成的字符串也是唯一的。
這就是 trace 中的 trace id 了,而 trace process 是發(fā)起請(qǐng)求的機(jī)器的信息,用 Key-Value 的形式存儲(chǔ)信息,其格式如下:
{ "key": "hostname", "type": "string", "value": "Your-PC" }, { "key": "ip", "type": "string", "value": "172.6.6.6" }, { "key": "jaeger.version", "type": "string", "value": "CSharp-0.4.2.0" }
Ttace 中的 trace id 和 process 這里說(shuō)完了,接下來(lái)說(shuō) trace 的 span。
Span
Span 由以下信息組成:
- An operation name:操作名稱,必有;
- A start timestamp:開始時(shí)間戳,必有;
- A finish timestamp:結(jié)束時(shí)間戳,必有;
- Span Tags.:Key-Value 形式表示請(qǐng)求的標(biāo)簽,可選;
- Span Logs:Key-Value 形式表示,記錄簡(jiǎn)單的、結(jié)構(gòu)化的日志,必須是字符串類型,可選;
- SpanContext :跨度上下文,在不同的 span 中傳遞,建立關(guān)系;
- Referencest:引用的其它 Span;
span 之間如果是父子關(guān)系,則可以使用 SpanContext 綁定這種關(guān)系。父子關(guān)系有 ChildOf
、FollowsFrom
兩種表示,ChildOf
表示 父 Span 在一定程度上依賴子 Span,而 FollowsFrom
表示父 Span 完全不依賴其子Span 的結(jié)果。
一個(gè) Span 的簡(jiǎn)化信息如下(不用理會(huì)字段名稱大小寫):
{ "traceID": "790e003e22209ca4", "spanID": "4b73f8e8e77fe9dc", "flags": 1, "operationName": "print-hello", "references": [], "startTime": 1611318628515966, "duration": 259, "tags": [ { "key": "internal.span.format", "type": "string", "value": "proto" } ], "logs": [ { "timestamp": 1611318628516206, "fields": [ { "key": "event", "type": "string", "value": "WriteLine" } ] } ] }
OpenTracing API
在 OpenTracing API 中,有三個(gè)主要對(duì)象:
- Tracer
- Span
- SpanContext
Tracer
可以創(chuàng)建Spans
并了解如何跨流程邊界對(duì)它們的元數(shù)據(jù)進(jìn)行Inject
(序列化)和Extract
(反序列化)。它具有以下功能:
- 開始一個(gè)新的
Span
Inject
一個(gè)SpanContext
到一個(gè)載體Extract
一個(gè)SpanContext
從載體
由起點(diǎn)進(jìn)程創(chuàng)建一個(gè) Tracer,然后啟動(dòng)進(jìn)程發(fā)起請(qǐng)求,每個(gè)動(dòng)作產(chǎn)生一個(gè) Span,如果有父子關(guān)系,Tracer 可以將它們關(guān)聯(lián)起來(lái)。當(dāng)請(qǐng)求完成后, Tracer 將跟蹤信息推送到 Jaeger-Collector中。
SpanContext 是在不同的 Span 中傳遞信息的,SpanContext 包含了簡(jiǎn)單的 Trace id、Span id 等信息。
我們繼續(xù)以下圖作為示例講解。
A 創(chuàng)建一個(gè) Tracer,然后創(chuàng)建一個(gè) Span,代表自己 (A),再創(chuàng)建兩個(gè) Span,分別代表 B、C,然后通過(guò) SpanContext 傳遞一些信息到 B、C;B 和 C 收到 A 的消息后,也創(chuàng)建一個(gè) Tracer ,用來(lái) Tracer.extract(...)
;其中 B 沒有后續(xù),可以直接返回結(jié)果;而 C 的 Tracer 繼續(xù)創(chuàng)建兩個(gè) Span,往 D、E 傳遞 SpanContext。
這個(gè)過(guò)程比較復(fù)雜,筆者講不好,建議讀者參與 OpenTracing 的官方文檔。
詳細(xì)的 OpenTracing API,可以通過(guò)編程語(yǔ)言編寫相應(yīng)服務(wù)時(shí),去學(xué)習(xí)各種 API 的使用。
到此這篇關(guān)于.NET Core分布式鏈路追蹤框架的基本實(shí)現(xiàn)原理的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
VS2012下QT creator登錄對(duì)話框設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了VS2012下QT creator登錄對(duì)話框的設(shè)計(jì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06.NET Core之微信支付之公眾號(hào)、H5支付詳解
這篇文章主要介紹了.NET Core之微信支付之公眾號(hào)、H5支付篇,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03.NET微服務(wù)架構(gòu)CI/CD鏡像自動(dòng)分發(fā)
這篇文章介紹了.NET微服務(wù)架構(gòu)CI/CD實(shí)現(xiàn)鏡像自動(dòng)分發(fā)的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-01-01Excel、記事本數(shù)據(jù)導(dǎo)入到數(shù)據(jù)庫(kù)的實(shí)現(xiàn)方法
將手機(jī)號(hào)批量導(dǎo)入數(shù)據(jù)庫(kù)。思路:先將要導(dǎo)入的文件傳上項(xiàng)目里,然后讀取文件的每行數(shù)據(jù)并插入數(shù)據(jù)庫(kù),操作完后再將上傳的文件刪除2013-10-10創(chuàng)建基于ASP.NET的SMTP郵件服務(wù)的具體方法
Asp.net在System.Web.Mail名稱空間中有一個(gè)發(fā)送email的內(nèi)建類,但這僅是cdosys的一個(gè)假象。開發(fā)者能使用一個(gè)替代的它smtp郵件服務(wù)。在這篇文章里面,我將會(huì)展示如何創(chuàng)建一個(gè)用于asp.net的功能齊全的smtp郵件服務(wù)2013-11-11不可或缺的ASP.NET內(nèi)置對(duì)象小結(jié)
這篇文章主要介紹了不可或缺的ASP.NET內(nèi)置對(duì)象小結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04ASP.net(c#)用類的思想實(shí)現(xiàn)插入數(shù)據(jù)到ACCESS例子
ASP.net(c#)用類的思想實(shí)現(xiàn)插入數(shù)據(jù)到ACCESS例子...2007-07-07