< 返回版块

zhuxiujia 发表于 2020-06-26 12:19

Tags:制作 Rust 语言堪比 Mybatis 的异步 ORM 框架

因为是复刻Java系的Mybatis,因此框架暂命名 Rbatis。小部分功能还在进行中。 github链接https://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抽象语法树,可以参考其他博客 https://blog.csdn.net/weixin_39408343/article/details/95984062

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服务实现进行压测。

启动服务端口http://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 后续文章补上实现

  • 逻辑删除插件

  • 乐观锁插件

  • 版本号控制插件


Ext Link: https://github.com/rbatis/rbatis

评论区

写评论
作者 zhuxiujia 2020-07-01 23:59

go消耗15MB左右,rust高分期在8MB,请求完成之后回落到5MB。总的来说2者消耗其实都不大,区别在于rust遵循0开销的思想,没有GC暂停而且用完后会把内存返回操作系统。灰常省内存

--
👇
Mike Tang: 少好几个数量级是多少?

作者 zhuxiujia 2020-07-01 23:55

gorm是golang的吧

--
👇
pinylin: 希望对比下gorm, 毕竟gorm才是主流

作者 zhuxiujia 2020-07-01 23:54

感谢支持

--
👇
phper-chen: tokio是reactor模型,类似redis服务的网络模型 erlang的OTP、akka的网络模型是actor模型,actor之间通过mailbox的短消息队列通信实现无锁并发 不过题主的实践精神我很钦佩,rust需要你这样的贡献者、实干家

作者 zhuxiujia 2020-07-01 23:54

已经在准备编写 QueryWrapper了, 目前已支持 select_page(),IPage,PageRequest 基本上 写分页只需要写select一份就行。不需要写count。 另外,框架未来会 默认自带save,save_batch,remove_by_id,remove_batch_by_id,update_by_id,update_batch_by_id,get_by_id,list,list_by_ids 等等常用默认方法

--
👇
pader: 最好弄个 QueryBuilder 出来,不要让人写查询语句,常见查询都要通过 QueryBuilder 写出来,什么 SELECT、JOIN 之类的。

pinylin 2020-06-29 18:35

希望对比下gorm, 毕竟gorm才是主流

Mike Tang 2020-06-29 16:00

预约一个唠嗑 topic 分享哇。

phper-chen 2020-06-29 15:00

tokio是reactor模型,类似redis服务的网络模型 erlang的OTP、akka的网络模型是actor模型,actor之间通过mailbox的短消息队列通信实现无锁并发 不过题主的实践精神我很钦佩,rust需要你这样的贡献者、实干家

pader 2020-06-27 15:23

最好弄个 QueryBuilder 出来,不要让人写查询语句,常见查询都要通过 QueryBuilder 写出来,什么 SELECT、JOIN 之类的。

Mike Tang 2020-06-26 23:10

少好几个数量级是多少?

Mike Tang 2020-06-26 14:11

我去~

1 共 10 条评论, 1 页