Sirix.io是如何基於Vert.x和Kotlin協程構建非同步RESTful API
Sirix是一個儲存系統,它的核心是日誌結構,讀取可以是隨機的,並且在事務提交期間將寫入批處理並同步到磁碟。資料永遠不會寫回到同一個地方,因此不會就地修改,相反,Sirix在記錄級別使用寫時複製(COW)(因此,它建立頁面片段並且通常不復制整個頁面),每次必須修改頁面時,已更改的記錄都會寫入新位置,確切複製哪些記錄取決於所使用的版本控制演算法。
對資料庫/資源的更改發生在資源繫結事務中。因此,必須開啟ResourceManager才能建立寫入事務。在任何時候,只允許與N讀取事務同時進行一次寫入事務。每個事務都繫結到一個修訂版,而它們可以在任何修訂版上開啟,無論哪個修訂版都無關緊要。
Vert.x在Node.js和JVM之後進行了嚴格的建模。Vert.x中的所有內容都應該是非阻塞的。因此,稱為事件迴圈的單個執行緒可以處理大量請求。阻止呼叫必須在特殊的執行緒池上處理。預設值是每個CPU兩個事件迴圈(多反應器模式)。
我們正在使用Kotlin,因為它簡單而簡潔。其中一個非常有趣的功能是協同程式。從概念上講,它們就像非常輕量級的執行緒 另一方面,建立執行緒非常昂貴。關於協同程式的一個很酷的事情是,它們允許編寫幾乎像順序的非同步程式碼。每當一個協程將被掛起時,底層執行緒不會被阻塞並且可以被重用。在引擎蓋下,每個掛起函式透過Kotlin編譯器獲得另一個引數,這是一個延續,它儲存恢復函式的位置(正常恢復,恢復異常)。
Keycloak用作OAuth2授權伺服器(密碼憑據流量),因為我們決定不自己實現授權。
為了獲得訪問令牌,首先必須針對POST / login進行請求- 使用身份中作為JSON物件傳送的使用者名稱/密碼憑證進行路由。實現程式碼:
post("/login").produces("application/json").coroutineHandler { rc -> val userJson = rc.bodyAsJson val user = keycloak.authenticateAwait(userJson) rc.response().end(user.principal().toString()) } |
coroutine-handler是一個簡單的擴充套件函式:
/* An extension method for simplifying coroutines usage with Vert.x Web routers. */ private fun Route.coroutineHandler(fn: suspend (RoutingContext) -> Unit) { handler { ctx -> launch(ctx.vertx().dispatcher()) { try { fn(ctx) } catch (e: Exception) { ctx.fail(e) } } } } |
協程式在Vert.x事件迴圈(排程程式)上啟動。
這是為了執行更長的執行處理程式:
vertxContext.executeBlockingAwait(Handler < Future < Nothing >> { //更長時間執行任務 }) |
Vert.x為這類任務使用不同的執行緒池。因此,該任務在另一個執行緒中執行。請注意當協程被暫停,事件迴圈不會被阻止。
現在我們再次將焦點轉移到我們的API,並展示它是如何設計的。我們首先需要設定我們的伺服器和Keycloak。
一旦兩個伺服器都啟動並執行,我們就能夠編寫一個簡單的HTTP客戶端。我們首先必須讓/login使用指定的“使用者名稱/密碼”JSON-Object 從端點獲取令牌。在Kotlin中使用非同步HTTP客戶端(來自Vert.x),它看起來像這樣:
val server = "https://localhost:9443" val credentials = json { obj("username" to "testUser", "password" to "testPass") } val response = client.postAbs("$server/login").sendJsonAwait(credentials) if (200 == response.statusCode()) { val user = response.bodyAsJsonObject() val accessToken = user.getString("access_token") } |
然後,必須在Authorization HTTP-Header中為每個後續請求傳送此訪問令牌。儲存第一個資源看起來像這樣(簡單的HTTP PUT-Request):
val xml = """ <xml> foo <bar/> </xml> """.trimIndent() var httpResponse = client.putAbs("$server/database/resource1").putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer $accessToken").sendBufferAwait(Buffer.buffer(xml)) if (200 == response.statusCode()) { println("Stored document.") } else { println("Something went wrong ${response.message}") } |
首先,建立一個名稱database帶有一些後設資料的空資料庫,然後使用名稱儲存XML片段resource1。PUT HTTP-Request是冪等的。具有相同URL端點的另一個PUT-Request將刪除以前的資料庫和資源並重新建立資料庫/資源
HTTP-Response應為200,產生HTTP-body:
<rest:sequence xmlns:rest="https://sirix.io/rest"> <rest:item> <xml rest:id="1"> foo <bar rest:id="3"/> </xml> </rest:item> </rest:sequence> |
以上是從儲存系統為元素節點序列化生成ID。
然後透過GET HTTP-Request,https://localhost:9443/database/resource1我們還可以再次檢索儲存的資源。
然而,到目前為止,這並不是很有趣。我們可以更新資源POST-Request。假設我們像以前一樣檢索了訪問令牌,我們可以簡單地執行POST-Request並使用我們之前收集的有關node-ID的資訊:
val xml = """ <test> yikes <bar/> </test> """.trimIndent() val url = "$server/database/resource1?nodeId=3&insert=asFirstChild" val httpResponse = client.postAbs(url).putHeader(HttpHeaders.AUTHORIZATION .toString(), "Bearer $accessToken").sendBufferAwait(Buffer.buffer(xml)) |
有趣的部分是URL,我們用作端點。我們簡單地說,選擇ID為3的節點,然後將給定的XML片段作為第一個子片段插入。這將生成以下序列化XML文件:
<rest:sequence xmlns:rest="https://sirix.io/rest"> <rest:item> <xml rest:id="1"> foo <bar rest:id="3"> <test rest:id="4"> yikes <bar rest:id="6"/> </test> </bar> </xml> </rest:item> </rest:sequence> |
每個PUT-以及POST請求都隱含commits了底層事務。因此,我們現在能夠再次傳送第一個GET請求來檢索整個資源的內容,例如透過指定一個簡單的XPath查詢,在所有版本中選擇根節點GET https://localhost:9443/database/resource1?query=/xml/all-time::*並獲得以下XPath結果:
<rest:sequence xmlns:rest="https://sirix.io/rest"> <rest:item rest:revision="1" rest:revisionTimestamp="2018-12-20T18:44:39.464Z"> <xml rest:id="1"> foo <bar rest:id="3"/> </xml> </rest:item> <rest:item rest:revision="2" rest:revisionTimestamp="2018-12-20T18:44:39.518Z"> <xml rest:id="1"> foo <bar rest:id="3"> <xml rest:id="4"> foo <bar rest:id="6"/> </xml> </bar> </xml> </rest:item> </rest:sequence> |
一般來說,我們支援幾個額外的時間XPath軸:future ::,future-or-self ::,past ::,past-or-self ::,previous ::,previous-or-self ::,next ::, next-or-self ::,first ::,last ::,all-time ::
透過在GET請求中指定序列化(開始和結束脩訂引數)的一系列修訂,可以實現相同的目的:
GET https://localhost:9443/database/resource1?start-revision=1&end-revision=2
或透過時間戳:
GET https://localhost:9443/database/resource1?start-revision-timestamp=2018-12-20T18:00:00&end-revision-timestamp=2018-12-20T19:00:00
我們肯定也能夠透過更新XQuery表示式(不是非常RESTful)或使用簡單的DELETEHTTP請求來刪除資源或其任何子樹:
val url = "$server/database/resource1?nodeId=3" val httpResponse = client.deleteAbs(url).putHeader(HttpHeaders.AUTHORIZATION .toString(), "Bearer $accessToken").sendAwait() if (200 == httpResponse.statusCode()) { ... } |
這將刪除ID為3的節點,在我們的例子中,因為它是整個子樹的元素節點。肯定它已作為修訂版3提交,因此所有舊版本仍然可以查詢整個子樹(或者在第一個修訂版中,它只是名稱為“bar”而沒有任何子樹的元素)。
如果我們想得到一個差異,目前以XQuery Update語句的形式(但我們可以以任何格式序列化它們),只需呼叫XQuery函式sdb:diff,該函式定義為:
sdb:diff($coll as xs:string, $res as xs:string, $rev1 as xs:int, $rev2 as xs:int) as xs:string
例如,透過我們上面建立的資料庫/ resource1這樣的GET請求,我們可以發出以下請求:
GET https://localhost:9443/?query=sdb%3Adiff%28%27database%27%2C%27resource1%27%2C1%2C2%29
請注意,query-String必須進行URL編碼,然後對其進行解碼
sdb:diff('database','resource1',1,2)
在我們的示例中,diff的輸出是包含在封閉sequence-element中的XQuery-Update語句:
<rest:sequence xmlns:rest="https://sirix.io/rest"> let $doc := sdb:doc('database','resource1', 1) return ( insert nodes <xml>foo<bar/></xml> as first into sdb:select-node($doc, 3) ) </rest:sequence> |
這意味著resource1從database第一次修訂中開啟,然後將子樹<xml>foo<bar/></xml>附加到具有穩定節點ID 3作為第一子節點的節點。
相關文章
- 使用Golang和MongoDB構建 RESTful APIGolangMongoDBRESTAPI
- [beego新手入門]基於web框架-beego的RESTful API的構建之旅GoWeb框架RESTAPI
- 構建api gateway之 基於etcd實現動態配置同步APIGateway
- 對執行緒、協程和同步非同步、阻塞非阻塞的理解執行緒非同步
- 基於 Spring Boot 2.0 構建一個 RESTful WebServiceSpring BootRESTWeb
- Kotlin coroutine之協程基礎Kotlin
- 如何使用NodeJS構建基於RPC的API系統NodeJSRPCAPI
- 在Kotlin中如何利用協程進行非同步程式設計Kotlin非同步程式設計
- 資源混淆是如何影響到Kotlin協程的Kotlin
- 【Kotlin】協程Kotlin
- 【譯】kotlin 協程官方文件(1)-協程基礎(Coroutine Basics)Kotlin
- 教你如何構建非同步伺服器和客戶端的 Kotlin 框架 Ktor非同步伺服器客戶端Kotlin框架
- 「譯」如何使用 NodeJS 構建基於 RPC 的 API 系統NodeJSRPCAPI
- 使用 .NET Core 3.x 構建 RESTFUL ApiRESTAPI
- Swagger 文件工具 設計、構建、文件化和使用您的 RESTful APISwaggerRESTAPI
- 使用 .NET Core 3.x 構建 RESTFUL Api (續)RESTAPI
- 使用 Python 構建一個簡單的 RESTful APIPythonRESTAPI
- Kotlin Coroutine(協程): 二、初識協程Kotlin
- Kotlin Coroutine(協程): 三、瞭解協程Kotlin
- Ionic 3和Angular 4:使用基於令牌的Restful API插入和刪除AngularRESTAPI
- Android Kotlin 協程初探AndroidKotlin
- Kotlin 協程一 —— CoroutineKotlin
- 基於MySql和Sails.js的RESTful風格的api實現MySqlAIJSRESTAPI
- 在 Android 開發中使用 Kotlin 協程 (一) -- 初識 Kotlin 協程AndroidKotlin
- Android版kotlin協程入門(四):kotlin協程開發實戰AndroidKotlin
- API架構的選擇,RESTful、GraphQL還是gRPCAPI架構RESTRPC
- Android版kotlin協程入門(三):kotlin協程的異常處理AndroidKotlin
- [實戰] 使用 MongoDB Go 驅動與 Iris 構建 RESTful APIMongoDBRESTAPI
- Kotlin Coroutine(協程)簡介Kotlin
- Kotlin Coroutines(協程)講解Kotlin
- Kotlin協程快速進階Kotlin
- Kotlin協程快速入門Kotlin
- Android Kotlin協程入門AndroidKotlin
- 基於多執行緒+協程的非同步增量式爬蟲執行緒非同步爬蟲
- 11、協程和io教程01 -- 併發 並行 同步 非同步 阻塞 非阻塞 以及 IO多路複用並行非同步
- 微服務雲架構-Swagger2構建強大的RESTful API文件微服務架構SwaggerRESTAPI
- 基於Guava API實現非同步通知和事件回撥GuavaAPI非同步事件
- 教你 10 分鐘構建一套 RESTful API 服務 ( 上 )RESTAPI