使用共享数据库连接和缓存编写 Rust 微服务的惯用方法是什么?
What is the idiomatic way to write Rust microservice with shared db connections and caches?
我正在使用 hyper
编写我的第一个 Rust
微服务。经过 C++
和 Go
多年的发展,我倾向于使用控制器来处理请求(如此处 - https://github.com/raycad/go-microservices/blob/master/src/user-microservice/controllers/user.go),其中控制器存储共享数据,如数据库连接池和不同类型的缓存。
我知道,有了 hyper
,我可以这样写:
use hyper::{Body, Request, Response};
pub struct Controller {
// pub cache: Cache,
// pub db: DbConnectionPool
}
impl Controller {
pub fn echo(&mut self, req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
// extensively using db and cache here...
let mut response = Response::new(Body::empty());
*response.body_mut() = req.into_body();
Ok(response)
}
}
然后使用它:
use hyper::{Server, Request, Response, Body, Error};
use hyper::service::{make_service_fn, service_fn};
use std::{convert::Infallible, net::SocketAddr, sync::Arc, sync::Mutex};
async fn route(controller: Arc<Mutex<Controller>>, req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
let mut c = controller.lock().unwrap();
c.echo(req)
}
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let controller = Arc::new(Mutex::new(Controller{}));
let make_svc = make_service_fn(move |_conn| {
let controller = Arc::clone(&controller);
async move {
Ok::<_, Infallible>(service_fn(move |req| {
let c = Arc::clone(&controller);
route(c, req)
}))
}
});
let server = Server::bind(&addr).serve(make_svc);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}
由于编译器不允许我在线程之间共享可变结构,所以我不得不使用 Arc<Mutex<T>>
习惯用法。但是我担心 let mut c = controller.lock().unwrap();
部分会在处理单个请求时阻塞整个控制器,即这里没有并发。
解决这个问题的惯用方法是什么?
&mut
始终获取对值的(编译时或运行时)独占锁。
仅在您要锁定的确切范围内获取 &mut
。
如果锁定值所拥有的值需要单独的锁定管理,
将其包装在 Mutex
.
中
假设您的 DbConnectionPool
结构如下:
struct DbConnectionPool {
conns: HashMap<ConnId, Conn>,
}
当我们 HashMap
上的 add/remove 项时,我们需要 &mut
HashMap
,
但我们不需要 &mut
Conn
中的值。
所以 Arc
允许我们 将可变性边界与其父级 分开,
并且 Mutex
允许我们 添加它自己的内部可变性 .
此外,我们的echo
方法不想成为&mut
,
因此需要在 HashMap
.
上添加另一层内部可变性
所以我们将其更改为
struct DbConnectionPool {
conns: Mutex<HashMap<ConnId, Arc<Mutex<Conn>>>,
}
然后当你想要连接时,
fn get(&self, id: ConnId) -> Arc<Mutex<Conn>> {
let mut pool = self.db.conns.lock().unwrap(); // ignore error if another thread panicked
if let Some(conn) = pool.get(id) {
Arc::clone(conn)
} else {
// here we will utilize the interior mutability of `pool`
let arc = Arc::new(Mutex::new(new_conn()));
pool.insert(id, Arc::clone(&arc));
arc
}
}
(ConnId
参数和if-exists-else逻辑用于简化代码;您可以更改逻辑)
在 returned 值上你可以做
self.get(id).lock().unwrap().query(...)
为了方便说明,我将逻辑更改为用户提供 ID。
现实中,你应该可以找到一个Conn
没有得到的return。
然后你可以 return 一个 RAII 守卫 Conn
,
类似于 MutexGuard
的工作方式,
当用户停止使用时自动释放连接。
也可以考虑使用 RwLock
而不是 Mutex
如果这可能会导致性能提升。
我正在使用 hyper
编写我的第一个 Rust
微服务。经过 C++
和 Go
多年的发展,我倾向于使用控制器来处理请求(如此处 - https://github.com/raycad/go-microservices/blob/master/src/user-microservice/controllers/user.go),其中控制器存储共享数据,如数据库连接池和不同类型的缓存。
我知道,有了 hyper
,我可以这样写:
use hyper::{Body, Request, Response};
pub struct Controller {
// pub cache: Cache,
// pub db: DbConnectionPool
}
impl Controller {
pub fn echo(&mut self, req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
// extensively using db and cache here...
let mut response = Response::new(Body::empty());
*response.body_mut() = req.into_body();
Ok(response)
}
}
然后使用它:
use hyper::{Server, Request, Response, Body, Error};
use hyper::service::{make_service_fn, service_fn};
use std::{convert::Infallible, net::SocketAddr, sync::Arc, sync::Mutex};
async fn route(controller: Arc<Mutex<Controller>>, req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
let mut c = controller.lock().unwrap();
c.echo(req)
}
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let controller = Arc::new(Mutex::new(Controller{}));
let make_svc = make_service_fn(move |_conn| {
let controller = Arc::clone(&controller);
async move {
Ok::<_, Infallible>(service_fn(move |req| {
let c = Arc::clone(&controller);
route(c, req)
}))
}
});
let server = Server::bind(&addr).serve(make_svc);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}
由于编译器不允许我在线程之间共享可变结构,所以我不得不使用 Arc<Mutex<T>>
习惯用法。但是我担心 let mut c = controller.lock().unwrap();
部分会在处理单个请求时阻塞整个控制器,即这里没有并发。
解决这个问题的惯用方法是什么?
&mut
始终获取对值的(编译时或运行时)独占锁。
仅在您要锁定的确切范围内获取 &mut
。
如果锁定值所拥有的值需要单独的锁定管理,
将其包装在 Mutex
.
假设您的 DbConnectionPool
结构如下:
struct DbConnectionPool {
conns: HashMap<ConnId, Conn>,
}
当我们 HashMap
上的 add/remove 项时,我们需要 &mut
HashMap
,
但我们不需要 &mut
Conn
中的值。
所以 Arc
允许我们 将可变性边界与其父级 分开,
并且 Mutex
允许我们 添加它自己的内部可变性 .
此外,我们的echo
方法不想成为&mut
,
因此需要在 HashMap
.
所以我们将其更改为
struct DbConnectionPool {
conns: Mutex<HashMap<ConnId, Arc<Mutex<Conn>>>,
}
然后当你想要连接时,
fn get(&self, id: ConnId) -> Arc<Mutex<Conn>> {
let mut pool = self.db.conns.lock().unwrap(); // ignore error if another thread panicked
if let Some(conn) = pool.get(id) {
Arc::clone(conn)
} else {
// here we will utilize the interior mutability of `pool`
let arc = Arc::new(Mutex::new(new_conn()));
pool.insert(id, Arc::clone(&arc));
arc
}
}
(ConnId
参数和if-exists-else逻辑用于简化代码;您可以更改逻辑)
在 returned 值上你可以做
self.get(id).lock().unwrap().query(...)
为了方便说明,我将逻辑更改为用户提供 ID。
现实中,你应该可以找到一个Conn
没有得到的return。
然后你可以 return 一个 RAII 守卫 Conn
,
类似于 MutexGuard
的工作方式,
当用户停止使用时自动释放连接。
也可以考虑使用 RwLock
而不是 Mutex
如果这可能会导致性能提升。