» Rust:使用Rocket构建REST API » 2. 开发 » 2.9 缓存:Redis

缓存:Redis

在 MySQL 中的大查询或 MongoDB 中的大聚合可能需要几秒甚至几分钟才能完成。你绝对不希望频繁地触发这些操作。

将查询或聚合结果缓存到内存中是缓解这个问题的绝佳方法。如果你的 API 服务器在单个机器或节点上运行,只需将这些结果放入内存中的 HashMapsDictionaries 中即可解决问题。 但是,如果你有多台机器或节点运行 API 服务器并共享其公共内存的话,则 Redis 才是你的最佳选择。

尝试 Redis

  1. 在你的机器上安装 Redis 并启动它。

  2. 添加 redis 依赖。

cargo add redis

Cargo.toml 中变化:

@@ -10,6 +10,7 @@ chrono = { version = "0.4.35", features = ["serde"] }
 lazy_static = "1.4.0"
 mongodb = { version = "2.8.2", default-features = false, features = ["sync"] }
 mysql = "24.0.0"
+redis = "0.25.2"
 rocket = { version = "0.5.0", features = ["json"] }
 rusqlite = "0.31.0"
 serde = { version = "1.0.197", features = ["derive"] }
  1. 更新代码。

添加 infrastructure/cache/helper.rs:

use std::error::Error;

pub trait Helper: Send + Sync {
    fn save(&self, key: &str, value: &str) -> Result<(), Box<dyn Error>>;
    fn load(&self, key: &str) -> Result<Option<String>, Box<dyn Error>>;
}

使用 redis,infrastructure/cache/redis.rs:

use std::error::Error;
use std::sync::RwLock;

use redis::{Client, Commands, Connection};

use crate::infrastructure::cache::Helper;

const DEFAULT_TTL: u64 = 3600; // seconds

pub struct RedisCache {
    conn: RwLock<Connection>,
}

impl RedisCache {
    pub fn new(redis_uri: &str) -> Result<Self, Box<dyn Error>> {
        let client = Client::open(redis_uri)?;
        let conn = client.get_connection()?;
        Ok(Self {
            conn: RwLock::new(conn),
        })
    }
}

impl Helper for RedisCache {
    fn save(&self, key: &str, value: &str) -> Result<(), Box<dyn Error>> {
        let mut conn = self.conn.write().unwrap();
        conn.set_ex(key, value, DEFAULT_TTL)?;
        Ok(())
    }

    fn load(&self, key: &str) -> Result<Option<String>, Box<dyn Error>> {
        // Caution: `conn.read()` doesn't work here
        let mut conn = self.conn.write().unwrap();
        let result: Option<String> = conn.get(key)?;
        Ok(result)
    }
}

导出符号,infrastructure/cache/mod.rs:

mod redis;
pub use redis::RedisCache;

mod helper;
pub use helper::Helper;

添加相应配置项,infrastructure/config/mod.rs:

@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
 #[derive(Debug, Deserialize, Serialize)]
 pub struct Config {
     pub app: ApplicationConfig,
+    pub cache: CacheConfig,
     pub db: DBConfig,
 }
 
@@ -16,6 +17,11 @@ pub struct DBConfig {
     pub mongo_db_name: String,
 }
 
+#[derive(Debug, Deserialize, Serialize)]
+pub struct CacheConfig {
+    pub redis_uri: String,
+}
+
 #[derive(Debug, Deserialize, Serialize)]
 pub struct ApplicationConfig {
     pub port: i32,

放入对应配置值,config.toml:

@@ -6,3 +6,6 @@ file_name = "test.db"
 dsn = "mysql://test_user:test_pass@127.0.0.1:3306/lr_book"
 mongo_uri = "mongodb://localhost:27017"
 mongo_db_name = "lr_book"
+
+[cache]
+redis_uri = "redis://:test_pass@localhost:6379/0"

合入 redis 连接,application/wire_helper.rs:

@@ -1,12 +1,14 @@
 use std::sync::Arc;
 
 use crate::domain::gateway;
+use crate::infrastructure::cache;
 use crate::infrastructure::database;
 use crate::infrastructure::Config;
 
 pub struct WireHelper {
     sql_persistence: Arc<database::MySQLPersistence>,
     no_sql_persistence: Arc<database::MongoPersistence>,
+    kv_store: Arc<cache::RedisCache>,
 }
 
 impl WireHelper {
@@ -16,9 +18,11 @@ impl WireHelper {
             &c.db.mongo_uri,
             &c.db.mongo_db_name,
         )?);
+        let kv_store = Arc::new(cache::RedisCache::new(&c.cache.redis_uri)?);
         Ok(WireHelper {
             sql_persistence,
             no_sql_persistence,
+            kv_store,
         })
     }
 
@@ -29,4 +33,8 @@ impl WireHelper {
     pub fn review_manager(&self) -> Arc<dyn gateway::ReviewManager> {
         Arc::clone(&self.no_sql_persistence) as Arc<dyn gateway::ReviewManager>
     }
+
+    pub fn cache_helper(&self) -> Arc<dyn cache::Helper> {
+        Arc::clone(&self.kv_store) as Arc<dyn cache::Helper>
+    }
 }

假设列出所有图书操作需要在数据库中执行一个大查询,你需要将查询结果存入 Redis 以便下次可快速访问。

更改 application/executor/book_operator.rs:

@@ -2,14 +2,21 @@ use std::sync::Arc;
 
 use crate::domain::gateway;
 use crate::domain::model;
+use crate::infrastructure::cache;
+
+const BOOKS_KEY: &str = "lr-books";
 
 pub struct BookOperator {
     book_manager: Arc<dyn gateway::BookManager>,
+    cache_helper: Arc<dyn cache::Helper>,
 }
 
 impl BookOperator {
-    pub fn new(b: Arc<dyn gateway::BookManager>) -> Self {
-        BookOperator { book_manager: b }
+    pub fn new(b: Arc<dyn gateway::BookManager>, c: Arc<dyn cache::Helper>) -> Self {
+        BookOperator {
+            book_manager: b,
+            cache_helper: c,
+        }
     }
 
     pub fn create_book(&self, b: model::Book) -> Result<model::Book, Box<dyn std::error::Error>> {
@@ -24,7 +31,16 @@ impl BookOperator {
     }
 
     pub fn get_books(&self) -> Result<Vec<model::Book>, Box<dyn std::error::Error>> {
-        self.book_manager.get_books()
+        let raw_value = self.cache_helper.load(BOOKS_KEY)?;
+        if let Some(v) = raw_value {
+            let cached_books = serde_json::from_str(&v)?;
+            Ok(cached_books)
+        } else {
+            let fetched_books = self.book_manager.get_books()?;
+            let v = serde_json::to_string(&fetched_books)?;
+            self.cache_helper.save(BOOKS_KEY, &v)?;
+            Ok(fetched_books)
+        }
     }
 
     pub fn update_book(

微调 adapter/router.rs:

@@ -212,7 +212,10 @@ pub fn delete_review(
 
 pub fn make_router(wire_helper: &application::WireHelper) -> RestHandler {
     RestHandler {
-        book_operator: executor::BookOperator::new(wire_helper.book_manager()),
+        book_operator: executor::BookOperator::new(
+            wire_helper.book_manager(),
+            wire_helper.cache_helper(),
+        ),
         review_operator: executor::ReviewOperator::new(wire_helper.review_manager()),
     }
 }

这些就是引入 redis 所需的调整。现在让我们试下缓存驱动的新端点。

用 curl 进行测试

列出所有图书:

curl -X GET -w "Total time: %{time_total}s\n" http://localhost:8000/books

结果与之前相似,但是性能显著提升。你可以通过 curl 的日志看出迹象。

# Rocket logs
GET /books:
   >> Matched: (get_books) GET /books
   >> Outcome: Success(200 OK)
   >> Response succeeded.
GET /books:
   >> Matched: (get_books) GET /books
   >> Outcome: Success(200 OK)
   >> Response succeeded.
GET /books:
   >> Matched: (get_books) GET /books
   >> Outcome: Success(200 OK)
   >> Response succeeded.
...
# curl logs
Total time: 0.014904s
Total time: 0.007797s
Total time: 0.009242s
Total time: 0.008859s
Total time: 0.008415s

使用 redis-cli 查看 Redis 中的值:

redis-cli

在 redis 客户端 shell 中调试这些键值:

127.0.0.1:6379> keys *
1) "lr-books"
127.0.0.1:6379> get lr-books
"[{\"id\":3,\"title\":\"Sample Book\",\"author\":\"John Doe\",\"published_at\":\"2023-01-01\",\"description\":\"A sample book description\",\"isbn\":\"1234567890\",\"total_pages\":200,\"created_at\":\"2024-03-01 12:40:16\",\"updated_at\":\"2024-03-01 12:40:16\"},{\"id\":4,\"title\":\"The Great Gatsby\",\"author\":\"F. Scott Fitzgerald\",\"published_at\":\"1925-04-10\",\"description\":\"A novel depicting the opulent lives of wealthy Long Island residents during the Jazz Age.\",\"isbn\":\"9780743273565\",\"total_pages\":218,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":5,\"title\":\"To Kill a Mockingbird\",\"author\":\"Harper Lee\",\"published_at\":\"1960-07-11\",\"description\":\"A novel set in the American South during the 1930s, dealing with themes of racial injustice and moral growth.\",\"isbn\":\"9780061120084\",\"total_pages\":281,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":6,\"title\":\"1984\",\"author\":\"George Orwell\",\"published_at\":\"1949-06-08\",\"description\":\"A dystopian novel depicting a totalitarian regime, surveillance, and propaganda.\",\"isbn\":\"9780451524935\",\"total_pages\":328,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":7,\"title\":\"Pride and Prejudice\",\"author\":\"Jane Austen\",\"published_at\":\"1813-01-28\",\"description\":\"A classic novel exploring the themes of love, reputation, and social class in Georgian England.\",\"isbn\":\"9780486284736\",\"total_pages\":279,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":8,\"title\":\"The Catcher in the Rye\",\"author\":\"J.D. Salinger\",\"published_at\":\"1951-07-16\",\"description\":\"A novel narrated by a disaffected teenager, exploring themes of alienation and identity.\",\"isbn\":\"9780316769488\",\"total_pages\":277,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":9,\"title\":\"The Lord of the Rings\",\"author\":\"J.R.R. Tolkien\",\"published_at\":\"1954-07-29\",\"description\":\"A high fantasy epic following the quest to destroy the One Ring and defeat the Dark Lord Sauron.\",\"isbn\":\"9780544003415\",\"total_pages\":1178,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":10,\"title\":\"Moby-Dick\",\"author\":\"Herman Melville\",\"published_at\":\"1851-10-18\",\"description\":\"A novel exploring themes of obsession, revenge, and the nature of good and evil.\",\"isbn\":\"9780142000083\",\"total_pages\":624,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":11,\"title\":\"The Hobbit\",\"author\":\"J.R.R. Tolkien\",\"published_at\":\"1937-09-21\",\"description\":\"A fantasy novel set in Middle-earth, following the adventure of Bilbo Baggins and the quest for treasure.\",\"isbn\":\"9780345339683\",\"total_pages\":310,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":12,\"title\":\"The Adventures of Huckleberry Finn\",\"author\":\"Mark Twain\",\"published_at\":\"1884-12-10\",\"description\":\"A novel depicting the journey of a young boy and an escaped slave along the Mississippi River.\",\"isbn\":\"9780486280615\",\"total_pages\":366,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":13,\"title\":\"War and Peace\",\"author\":\"Leo Tolstoy\",\"published_at\":\"1869-01-01\",\"description\":\"A novel depicting the Napoleonic era in Russia, exploring themes of love, war, and historical determinism.\",\"isbn\":\"9781400079988\",\"total_pages\":1392,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":14,\"title\":\"Alice\xe2\x80\x99s Adventures in Wonderland\",\"author\":\"Lewis Carroll\",\"published_at\":\"1865-11-26\",\"description\":\"A children\xe2\x80\x99s novel featuring a young girl named Alice who falls into a fantastical world populated by peculiar creatures.\",\"isbn\":\"9780141439761\",\"total_pages\":192,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":15,\"title\":\"The Odyssey\",\"author\":\"Homer\",\"published_at\":\"8th Century BC\",\"description\":\"An ancient Greek epic poem attributed to Homer, detailing the journey of Odysseus after the Trojan War.\",\"isbn\":\"9780140268867\",\"total_pages\":541,\"created_at\":\"2024-03-02 05:48:25\",\"updated_at\":\"2024-03-02 05:48:25\"},{\"id\":16,\"title\":\"Sample Book\",\"author\":\"John Doe\",\"published_at\":\"2023-01-01\",\"description\":\"A sample book description\",\"isbn\":\"1234567890\",\"total_pages\":200,\"created_at\":\"2024-03-07 10:17:59\",\"updated_at\":\"2024-03-07 10:17:59\"},{\"id\":17,\"title\":\"Sample Book\",\"author\":\"John Doe\",\"published_at\":\"2023-01-01\",\"description\":\"A sample book description\",\"isbn\":\"1234567890\",\"total_pages\":200,\"created_at\":\"2024-03-07 10:18:20\",\"updated_at\":\"2024-03-07 10:18:20\"},{\"id\":18,\"title\":\"Test Book\",\"author\":\"John Doe\",\"published_at\":\"2003-01-01\",\"description\":\"A sample book description\",\"isbn\":\"1234567890\",\"total_pages\":100,\"created_at\":\"2024-03-08 13:10:49\",\"updated_at\":\"2024-03-08 13:10:49\"},{\"id\":20,\"title\":\"Updated Book Title\",\"author\":\"Jane Smith\",\"published_at\":\"2023-01-01\",\"description\":\"A sample book description\",\"isbn\":\"1234567890\",\"total_pages\":200,\"created_at\":\"2024-03-16 08:27:41\",\"updated_at\":\"2024-03-16 08:29:37\"}]"
127.0.0.1:6379> del lr-books
(integer) 1

赞!Redis 已可以供君驱使了!💐