製作 Rust 語言堪比 Mybatis 的非同步 ORM 框架

daxiaxia發表於2020-06-21

因為是復刻Java系的Mybatis,因此框架暫命名 Rbatis。小部分功能還在進行中。 github連結github.com/rbatis/rbatis

首先介紹下rust語言下牛逼哄哄的產品有哪些?(最近風靡前端的原nodejs大神實現的TypeScript執行時Deno估計要替代nodejs,後端分散式raft協議實現的資料庫的Tidb,火狐瀏覽器等等….)

經過被Rust編譯器吊打和放棄一段時間之後,筆者立志要自虐寫一款沒有GC壓力,高併發且穩定安全的ORM框架。為啥?因為可以保證只要程式碼編譯通過,線上即沒bug,rust編譯器就好比一位超級大神監督著你敲程式碼,不會讓你有任何空指標異常,併發死鎖異常,GC異常…..emmm

讀者如果想閱讀原始碼,必須瞭解Rust涉及到的基本語法,Rc,Arc,RefCell,Mutex鎖,RwLock鎖,Send,Sync介面,Rust1.9之後加入的Future介面,Pin,Box。

首先寫rust的ORM框架需要解決幾個關鍵問題

  • 1 框架必須支援非同步(future), 想象一下,假設我們執行N多條慢sql,那麼很有可能耗盡執行緒池資源造成等待。因為協程或者說纖程只消耗幾kb而且可以啟動成百上千甚至上萬條,併發更高。

  • 為了節省時間,支援future網路部分拷貝sqlx-core(注意sqlx框架大量使用巨集 ,近乎偏執的使用編譯期生成程式碼,這導致程式碼智慧提示基本不起作用,這不是我想要的)部分基礎的網路實現程式碼。

  • 因為Rust語言本身中立,可以選擇例如Tokio(Actor模型),Async_Std(Actor模型),may(CSP模型和go類似,但其作者使用了固定容量的棧記憶體空間,有可能造成記憶體溢位,筆者暫時不考慮它。如果未來他能解決的話…)。 考慮到框架必須儘可能低開銷,高併發,預設支援了Tokio和AsyncStd. 目前使用Tokio系web框架的效能似乎是除了C++以外效能最高的併發框架,可以參考國外權威web框架效能評測網站

techempower權威壓測-tokio​www.techempower.com

CPS模型圖解,使用CSP模型的有Rust的may,Golang語言

  • 既然支援future,那麼框架必須支援跨協程共享,且跨協程修改(mut)。我們可以使用lazy_static 這個庫保證框架可以被任意協程使用。但是,lazy_static 包裹的變數必須實現了Rust官方介面 Send和Sync,即保證是執行緒、協程安全競爭併發的。

  • 筆者首先嚐試使用rust std庫的執行緒Mutex鎖,也就是執行緒互斥鎖(肯定不是最佳方案)

  • 1程式碼部分

struct Rbatis{

pub map:HashMap<String, Data>

}

pub fn query(&mut self, sql: &str){

//......

}

}

lazy_static! {

//全域性變數

static ref RB:Mutex<Rbatis>=Mutex::new(Rbatis::new());

}

//使用ORM框架執行Sql的時候,,,,就變成了

  • 2程式碼部分
    RB.lock().unwrap().query("select * from table");

這段程式碼看起來沒什麼問題,實際上問題很多。首先 2程式碼部分 獲得鎖的時候,我們的web服務其他的服務都必須等待當前任務釋放鎖 ,那麼對併發非常有害。

因為協程和執行緒是M:N的關係,我們使用tokio執行時,tokio中執行的協程是不能呼叫阻塞執行緒的(因為std::Mutex鎖阻塞了執行緒,那麼tokio執行時則會暫停排程),那麼理論上我們應當使用tokio提供的鎖(該鎖使用tokio執行時.await 排程來模擬鎖定和等待,是不會阻塞執行緒的)。而使用讀寫鎖也可以減少鎖定時間,但是讀寫鎖適合多讀而不是併發寫入的場景,不能保證併發寫入安全

其實我們最終目的是為了修改內部變數,多協程修改內部變數其實是不被編譯器認可的。編譯器會攔截並且 提示 不允許沒有實現 Send和Sync的結構體使用mut修改。

最終實現是使用Rust提供的RefCell(就是可以安全的修改 &self 而不是&mut self。具體資料可以自行查詢RefCell)+tokio::Mutex

編寫SyncMap

use tokio::sync::Mutex;
#[derive(Debug)]
pub struct SyncMap<T> {
    pub cell: Mutex<RefCell<HashMap<String, T>>>
}

impl<T> SyncMap<T> {
    pub fn new() -> SyncMap<T> {
        SyncMap {
            cell: Mutex::new(RefCell::new(HashMap::new()))
        }
    }

    /// put an value,this value will move lifetime into SyncMap
    pub async fn put(&self, key: &str, value: T) {
        let lock = self.cell.lock().await;
        let mut b = lock.borrow_mut();
        b.insert(key.to_string(), value);
      //函式結尾 lock鎖即可釋放,因此不管是put還是pop,鎖定的時間都是比較小的。而且鎖定是依賴tokio執行時排程,而不是執行緒阻塞
    }

    /// pop value,lifetime will move to caller
    pub async fn pop(&self, key: &str) -> Option<T> {
        let lock = self.cell.lock().await;
        let mut b = lock.borrow_mut();
        return b.remove(key);
    }
}

使用SyncMap虛擬碼

pub struct Rbatis<'r> { context_tx: SyncMap<Transaction<PoolConnection<MySqlConnection>>>, }

impl Rbatis{

//這裡我們可以看到,使用SyncMap既可以修改context上下文,又不必吧&self改為&mut self。即保證了併發安全和效能

pub async fn begin(&self, tx_id: &str) -> Result<u64, rbatis_core::Error> { if tx_id.is_empty() { return Err(rbatis_core::Error::from("[rbatis] tx_id can not be empty")); } let conn = self.get_pool()?.begin().await?; self.context_tx.put(tx_id, conn).await; return Ok(1); }

}

2 實現AST(抽象語法樹)來模擬Mybatis中的ognl表示式以及 解析各種xml節點. 這部分基本上就是使用二叉樹結構+演算法 模擬。AST抽象語法樹,可以參考其他部落格 blog.csdn.net/weixin_39408343/arti...

3 改寫sqlx-core的程式碼以支援serde_json傳參和解碼結構體,使用json結構當然會大大簡化我們的序列化操作~~

任何Orm框架基本上都是使用TCP協議 使用流 例如mysql的協議返回資料行Row,也就是根據協議返回一堆行資料,需要改寫sqlx-core裡面的cursor.rs檔案增加函式

fn decode_json<T>(&mut self) -> BoxFuture<Result<T, crate::Error>>
    where T: DeserializeOwned {
    Box::pin(async move {
        let mut arr = vec![];
        while let Some(row) = self.next().await? as Option<MySqlRow<'_>> {
            let mut m = serde_json::Map::new();
            let keys = row.names.keys();
            for x in keys {
                let key = x.to_string();
                let key_str=key.as_str();
                let v:serde_json::Value = row.json_decode_impl(key_str)?;
                m.insert(key, v);
            }
            arr.push(serde_json::Value::Object(m));
        }
        let r = json_decode(arr)?;
        return Ok(r);
    })
}

完成以上的任務後,後續剩下的都是愉快的業務程式碼啦。基本可以完成大部分業務了。舉個例子

let rb = Rbatis::new(MYSQL_URL).await.unwrap();
    let py = r#"
SELECT * FROM biz_activity
WHERE delete_flag = #{delete_flag}
if name != null:
  AND name like #{name+'%'}
if ids != null:
  AND id in (
  trim ',':
     for item in ids:
       #{item},
  )"#;
    let data: serde_json::Value = rb.py_fetch("", py, &json!({   "delete_flag": 1 })).await.unwrap();
    println!("{}", data);

筆者還使用了web框架hyper配合Rbatis使用wrk壓測對比了Go語言壓測。

環境:本地win10系統,mysql使用docker啟動1核心1G記憶體。啟動Rbatis的hyper服務(使用release編譯)對比 go標準庫+GoMbatis服務實現進行壓測。

啟動服務埠0.0.0.0:8080/test 對mysql執行單條sql “select count(1) from biz_activity;”


-   go語言版本(標準庫http+GoMybatis) 65 Qps/s

-   rust語言版本(rbatis+hyper) 132Qps/s

最後看到rust效能是go的2倍,記憶體消耗也比go少好幾個數量級,且Rust版本的實現記憶體 死死的穩定在 8MB(不增長,穩如老狗。你垂涎的無GC,微服務的話可以省下非常大的開銷)

TODO 後續文章補上實現

  • 邏輯刪除外掛

  • 樂觀鎖外掛

  • 版本號控制外掛

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章