首页 > 解决方案 > 使用共享数据库连接和缓存编写 Rust 微服务的惯用方式是什么?

问题描述

我正在编写我的第一个Rust微服务hyper。经过多年的发展,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();分会在处理单个请求时阻塞整个控制器,即这里没有并发。解决这个问题的惯用方法是什么?

标签: concurrencyrustmutexshared-ptr

解决方案


&mut总是获取值的(编译时或运行时)排他锁。仅&mut在您想要锁定的确切范围内获取 a。如果锁定值所拥有的值需要单独的锁定管理,请将其包装在Mutex.

假设您DbConnectionPool的结构如下:

struct DbConnectionPool {
    conns: HashMap<ConnId, Conn>,
}

&mut当我们在HashMap上添加/删除项目时我们需要,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 逻辑用于简化代码;您可以更改逻辑)

在返回值上你可以做

self.get(id).lock().unwrap().query(...)

为了方便说明,我将逻辑更改为用户提供 ID。实际上,您应该能够找到Conn尚未获得的并将其退回。然后,您可以返回一个 RAII 保护Conn,类似于MutexGuard工作原理,以在用户停止使用连接时自动释放连接。

如果这可能会导致性能提升,还可以考虑使用RwLock而不是。Mutex


推荐阅读