» Rust: Build a REST API with Rocket » 2. Development » 2.9 Cache: Redis

Cache: Redis

Big queries in MySQL or large aggregations in MongoDB may take seconds or even minutes to finish. You definitely don't want to trigger these operations frequently for each user request.

Caching the results in memory is a great way to alleviate this issue. If your API server is running on a single machine or node, simply putting these results in in-memory HashMaps or Dictionaries should solve the problem. If you have multiple machines or nodes running the API server and sharing common memory, Redis is your best choice here.

Try Redis

  1. Install Redis on your machine and start it.

  2. Add redis dependency.

cargo add redis

Diff in 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. Update code.

Add 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>>;
}

Use redis in 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)
    }
}

Export symbols in infrastructure/cache/mod.rs:

mod redis;
pub use redis::RedisCache;

mod helper;
pub use helper::Helper;

Add related config items in 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,

Put in new values in 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"

Wire in redis connection in 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>
+    }
 }

Suppose listing all books is a resource-intensive query in your database. In such cases, you may choose to store its query result in Redis for faster retrieval in the future.

Changes in 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(

Tune a little bit in 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()),
     }
 }

Those are all the changes you need to incorporate redis. Let's now try the endpoint powered by the new cache system.

Try with curl

List all books:

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

The result is just as before, but the performance improves a lot here. You can see that from the logs of 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

Use redis-cli to check the values in Redis:

redis-cli

Play with keys in the redis client 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

Awesome! Redis is at your service now! 💐