Rust 中单例模式的最佳实践

后端 / 2023-01-10

前言

为什么需要用到单例模式?

因为在平时开发过程中,很多模块我们只需要初始化一次。 例如连接池,各种 client等。


本文就以连接池为例,探索 单例模式在 Rust 中的实现。

  • lazy_static!
  • unsafe
  • Once 闭包

lazy_static!

这个库想必大家都知道,用于延迟计算静态变量。
那么基于这个特性我们很轻松就可以实现单例模式。

添加 cargo 依赖

lazy_static = "1.4.0"

示例代码


#[cfg(test)]
mod test {
    use std::sync::{Arc, Mutex};

    use lazy_static::lazy_static;
    struct Singleton;

    lazy_static! {
        static ref INS: Arc<Mutex<Singleton>> = Arc::new(Mutex::new(Singleton {}));
    }

    impl Singleton {
        pub fn get_instance() -> Arc<Mutex<Singleton>> {
            INS.clone()
        }
    }
    #[test]
    pub fn test_singleton() {
        let ins1 = Singleton::get_instance();
        let ins2 = Singleton::get_instance();
        assert_eq!(Arc::ptr_eq(&ins1, &ins2), true);
    }
}

running 1 test
test test::test_singleton ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 17 filtered out; finished in 0.00s

unsafe 实现

这种解决方案比较暴力,大家都知道 Rust中,静态全局变量是不可变的。如果我们需要改变它就需要使用 unsafe。 这时候就有小伙伴要说了,不是说 unsafe 不安全吗。


理论上 确实是这样,但是并不是说 使用 unsafe 就一定不安全。 只要我们合理的遵循 Rust心智模型,即便是用 unsafe 也能写出 安全的代码。

use std::sync::{Arc, Mutex};

use crate::{config::Config, global::CFG};

#[derive(Clone)]
pub struct Provider {
    config: Arc<Config>,
    redis_pool: deadpool_redis::Pool,
    lapin_pool: deadpool_lapin::Pool,
}

impl Provider {
    pub fn get_instance() -> Arc<Mutex<Provider>> {
        static mut POINTER: Option<Arc<Mutex<Provider>>> = None;

        unsafe {
            POINTER
                .get_or_insert_with(|| {
                    let cfg = CFG.clone();
                    Arc::new(Mutex::new(Provider::new(cfg)))
                })
                .clone()
        }
    }
    pub fn new(config: Arc<Config>) -> Self {
        let redis_pool = {
            let cfg = deadpool_redis::Config::from_url(config.redis.dsn.clone());
            let pool = cfg
                .create_pool(Some(deadpool_redis::Runtime::Tokio1))
                .unwrap();
            pool
        };

        let lapin_pool = {
            let mut cfg = deadpool_lapin::Config::default();
            let url = config.lapin.dsn.clone();
            cfg.url = Some(url.into());
            let pool = cfg
                .create_pool(Some(deadpool_lapin::Runtime::Tokio1))
                .unwrap();
            pool
        };

        Self {
            config: config,
            redis_pool: redis_pool,
            lapin_pool: lapin_pool,
        }
    }

    pub async fn lapin_conn(&self) -> Result<deadpool_lapin::Object, deadpool_lapin::PoolError> {
        self.lapin_pool.get().await
    }

    pub async fn redis_conn(
        &self,
    ) -> Result<deadpool_redis::Connection, deadpool_redis::PoolError> {
        self.redis_pool.get().await
    }
}

#[cfg(test)]
mod test {

    use super::Provider;

    #[tokio::test]
    pub async fn test() {
        let ins = Provider::get_instance();
        let conn = ins.lock().unwrap().redis_conn().await;
    }
}

once 闭包

这种方案和上面类似,只是用了 once 只执行一次的特性,这里就不详细概述了。