Repository: jaemk/cached (language: rust)

Issue:
Support Redis
I suggest supporting any network-based cache software, like Redis, Memcached, and ...

In the k8s environment, in which you may have several small pods working at the same time, caching locally is not helpful enough and will increase memory usage which may end in OOM Kill, having a shared place to cache the data is helpful.

If you agree, I can start trying to do it.
By adding a new store type, `RedisCache` which I hope will be similar to `TimedCache` and `UnboundCache`.

To support this, I think the return type of the function must implement `Serialize` and `Deserialize`.


Discussion / hints:
Another problem is that, the trait is returning references, for example:
```
    /// Attempt to retrieve a cached value
    fn cache_get(&mut self, k: &K) -> Option<&V>;
```

So when I fetch a value from Redis, it's initiated inside the function, then it's not possible to return the reference to it!

A solution can be to store them in a hashmap inside the struct and return the ref to that. But I don't know how to clear the memory up after it went out of scope.
Yeah the reference thing is a problem. The last time I was thinking about this, I was thinking of making a separate trait where the `cache_*` methods would return a wrapped `V`, like `-> CacheRef<V>` which impl's deref -> `V`. A big downside being that the `cached` macros need to handle both traits. I guess we _could_ just change the existing `Cached` trait to return a new wrapper type - the deref impls would make it _mostly_ compatible except in types...
@jaemk I've spent some time on this, couldn't figure it out :(
Can you lead me to the container (smart pointer) you meant?

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 90d90cb..2e438ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## [Unreleased]
 ## Added
+- Support of Redis cache store type
 ## Changed
 - Convert to github actions
 - Update build status badge to link to github actions
diff --git a/Cargo.toml b/Cargo.toml
index 369eb34..a2126a6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,9 +15,10 @@ edition = "2018"
 travis-ci = { repository = "jaemk/cached", branch = "master" }
 
 [features]
-default = ["proc_macro", "async"]
+default = ["proc_macro", "tokio"]
 proc_macro = ["async-mutex", "async-rwlock", "cached_proc_macro", "cached_proc_macro_types"]
 async = ["futures", "async-trait"]
+sync_redis = ["redis", "serde", "serde_json"]
 
 [dependencies.hashbrown]
 version = "0.11.2"
@@ -53,9 +54,22 @@ optional = true
 version = "0.1"
 optional = true
 
-[dev-dependencies.tokio]
+[dependencies.redis]
+version = "0.21"
+optional = true
+
+[dependencies.serde]
+version = "1.0"
+optional = true
+
+[dependencies.serde_json]
+version = "1.0"
+optional = true
+
+[dependencies.tokio]
 version = "1.12"
 features = ["macros", "time", "rt-multi-thread"]
+optional = true
 
 [dev-dependencies.async-std]
 version = "1.6"
@@ -66,3 +80,7 @@ version = "0.2.9"
 
 [workspace]
 members = ["cached_proc_macro"]
+
+[[example]]
+name = "redis"
+required-features = ["sync_redis", "proc_macro"]
diff --git a/README.md b/README.md
index 621fe05..ee8bcf5 100644
--- a/README.md
+++ b/README.md
@@ -330,6 +330,28 @@ pub fn main() {
 }
 ```
 
+```rust
+#[macro_use] extern crate cached;
+use std::thread::sleep;
+use std::time::Duration;
+use cached::RedisCache;
+
+cached! {
+    UNBOUND_REDIS: RedisCache<u32, u32> = RedisCache::new();
+    fn cached_redis(n: u32) -> u32 = {
+        sleep(Duration::new(3, 0));
+        n
+    }
+}
+
+cached! {
+    TIMED_REDIS: RedisCache<u32, u32> = RedisCache::with_lifespan(2);
+    fn cached_timed_redis(n: u32) -> u32 = {
+        sleep(Duration::new(3, 0));
+        n
+    }
+}
+```
 ----
 
 
@@ -340,9 +362,14 @@ computed (and still valid) value before evaluating the function body.
 
 Due to the requirements of storing arguments and return values in a global cache:
 
-- Function return types must be owned and implement `Clone`
-- Function arguments must either be owned and implement `Hash + Eq + Clone` OR the `cached_key!`
+- Function return types:
+  - In all store types, except Redis, must be owned and implement `Clone`
+  - In the Redis type, must be `Serialize + DeserializeOwned + Clone`
+- Function arguments:
+  - In all store types, except Redis, must either be owned and implement `Hash + Eq + Clone` OR the `cached_key!`
   macro must be used to convert arguments into an owned + `Hash + Eq + Clone` type.
+  - In the Redis store type, must either be owned and implement `Display` OR the `cached_key!`
+  macro must be used to convert arguments into an owned + `Display` type.
 - Arguments and return values will be `cloned` in the process of insertion and retrieval.
 - `#[cached]`/`cached!` functions should not be used to produce side-effectual results!
 - `#[cached]`/`cached!` functions cannot live directly under `impl` blocks since `cached!` expands to a
diff --git a/cached_proc_macro/src/lib.rs b/cached_proc_macro/src/lib.rs
index d8cce90..59cf07e 100644
--- a/cached_proc_macro/src/lib.rs
+++ b/cached_proc_macro/src/lib.rs
@@ -138,7 +138,7 @@ pub fn cached(args: TokenStream, input: TokenStream) -> TokenStream {
         })
         .collect::<Vec<_>>();
     let output_string = output_parts.join("::");
-    let output_type_display = output_ts.to_string().replace(" ", "");
+    let output_type_display = output_ts.to_string().replace(' ', "");
 
     // if `with_cached_flag = true`, then enforce that the return type
     // is something wrapped in `Return`. Either `Return<T>` or the
@@ -576,7 +576,7 @@ pub fn once(args: TokenStream, input: TokenStream) -> TokenStream {
         })
         .collect::<Vec<_>>();
     let output_string = output_parts.join("::");
-    let output_type_display = output_ts.to_string().replace(" ", "");
+    let output_type_display = output_ts.to_string().replace(' ', "");
 
     // if `with_cached_flag = true`, then enforce that the return type
     // is something wrapped in `Return`. Either `Return<T>` or the
diff --git a/examples/redis.rs b/examples/redis.rs
new file mode 100644
index 0000000..5a643fe
--- /dev/null
+++ b/examples/redis.rs
@@ -0,0 +1,51 @@
+/*
+Have Redis or run it locally via docker with the following command: `docker run --rm --name cached-tests -p 6379:6379 -d redis`
+Set the required env variable and run this example. Similar to the following command:
+`REDIS_CS=redis://127.0.0.1/ cargo run --example redis --features=redis`
+ */
+
+#[macro_use]
+extern crate cached;
+
+use cached::proc_macro::cached;
+use cached::RedisCache;
+use std::io;
+use std::io::Write;
+use std::time::Duration;
+
+cached! {
+    SLOW_FN: RedisCache<u64, ()> = RedisCache::with_lifespan(30);
+    fn cached_sleep_secs(secs: u64) -> () = {
+        std::thread::sleep(Duration::from_secs(secs));
+    }
+}
+
+cached! {
+    SLOW_ASYNC_FN: RedisCache<u64, ()> = RedisCache::with_lifespan(30);
+    async fn cached_async_sleep_secs(secs: u64) -> () = {
+        tokio::time::sleep(Duration::from_secs(secs)).await;
+    }
+}
+
+#[tokio::main]
+async fn main() {
+    if cfg!(feature = "redis") {
+        print!("first async call with a 2 seconds sleep...");
+        io::stdout().flush().unwrap();
+        cached_async_sleep_secs(2).await;
+        println!("done");
+        print!("second async call with a 2 seconds sleep (it should be fast)...");
+        io::stdout().flush().unwrap();
+        cached_async_sleep_secs(2).await;
+        println!("done");
+
+        print!("first sync call with a 2 seconds sleep...");
+        io::stdout().flush().unwrap();
+        cached_sleep_secs(2);
+        println!("done");
+        print!("second sync call with a 2 seconds sleep (it should be fast)...");
+        io::stdout().flush().unwrap();
+        cached_sleep_secs(2);
+        println!("done");
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index cfc9535..a11a44c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -341,6 +341,29 @@ pub fn main() {
 }
 ```
 
+```rust,ignore
+#[macro_use] extern crate cached;
+use std::thread::sleep;
+use std::time::Duration;
+use cached::RedisCache;
+
+cached! {
+    UNBOUND_REDIS: RedisCache<u32, u32> = RedisCache::new();
+    fn cached_redis(n: u32) -> u32 = {
+        sleep(Duration::new(3, 0));
+        n
+    }
+}
+
+cached! {
+    TIMED_REDIS: RedisCache<u32, u32> = RedisCache::with_lifespan(2);
+    fn cached_timed_redis(n: u32) -> u32 = {
+        sleep(Duration::new(3, 0));
+        n
+    }
+}
+# pub fn main() { }
+```
 ----
 
 
@@ -351,9 +374,14 @@ computed (and still valid) value before evaluating the function body.
 
 Due to the requirements of storing arguments and return values in a global cache:
 
-- Function return types must be owned and implement `Clone`
-- Function arguments must either be owned and implement `Hash + Eq + Clone` OR the `cached_key!`
+- Function return types:
+  - In all store types, except Redis, must be owned and implement `Clone`
+  - In the Redis type, must be `Serialize + DeserializeOwned + Clone`
+- Function arguments:
+  - In all store types, except Redis, must either be owned and implement `Hash + Eq + Clone` OR the `cached_key!`
   macro must be used to convert arguments into an owned + `Hash + Eq + Clone` type.
+  - In the Redis store type, must either be owned and implement `Display` OR the `cached_key!`
+  macro must be used to convert arguments into an owned + `Display` type.
 - Arguments and return values will be `cloned` in the process of insertion and retrieval.
 - `#[cached]`/`cached!` functions should not be used to produce side-effectual results!
 - `#[cached]`/`cached!` functions cannot live directly under `impl` blocks since `cached!` expands to a
@@ -564,6 +592,8 @@ mod lru_list;
 pub mod macros;
 pub mod stores;
 
+#[cfg(feature = "redis")]
+pub use stores::RedisCache;
 pub use stores::{SizedCache, TimedCache, TimedSizedCache, UnboundCache};
 
 #[cfg(feature = "proc_macro")]
diff --git a/src/lru_list.rs b/src/lru_list.rs
index fc77555..13fd110 100644
--- a/src/lru_list.rs
+++ b/src/lru_list.rs
@@ -1,5 +1,6 @@
 /// Limited functionality doubly linked list using Vec as storage.
 #[derive(Clone, Debug)]
+
 pub struct LRUList<T> {
     values: Vec<ListEntry<T>>,
 }
diff --git a/src/macros.rs b/src/macros.rs
index 4984ef9..144bf0e 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -33,6 +33,29 @@ macro_rules! cached {
             val
         }
     };
+
+    ($cachename:ident : $cachetype:ty = $cacheinstance:expr ;
+     async fn $name:ident ($($arg:ident : $argtype:ty),*) -> $ret:ty = $body:block) => {
+        static $cachename: $crate::once_cell::sync::Lazy<::std::sync::Mutex<$cachetype>>
+            = $crate::once_cell::sync::Lazy::new(|| ::std::sync::Mutex::new($cacheinstance));
+
+        #[allow(unused_parens)]
+        pub async fn $name($($arg: $argtype),*) -> $ret {
+            let key = ($($arg.clone()),*);
+            {
+                let mut cache = $cachename.lock().unwrap();
+                let res = $crate::Cached::cache_get(&mut *cache, &key);
+                if let Some(res) = res { return res.clone(); }
+            }
+            // run the function and cache the result
+            async fn inner($($arg: $argtype),*) -> $ret $body
+            let val = inner($($arg),*).await;
+
+            let mut cache = $cachename.lock().unwrap();
+            $crate::Cached::cache_set(&mut *cache, key, val.clone());
+            val
+        }
+    };
 }
 
 #[macro_export]
@@ -58,6 +81,29 @@ macro_rules! cached_key {
             val
         }
     };
+
+    ($cachename:ident : $cachetype:ty = $cacheinstance:expr ;
+     Key = $key:expr;
+     async fn $name:ident ($($arg:ident : $argtype:ty),*) -> $ret:ty = $body:expr) => {
+        static $cachename: $crate::once_cell::sync::Lazy<::std::sync::Mutex<$cachetype>>
+            = $crate::once_cell::sync::Lazy::new(|| ::std::sync::Mutex::new($cacheinstance));
+
+        #[allow(unused_parens)]
+        pub async fn $name($($arg: $argtype),*) -> $ret {
+            let key = $key;
+            {
+                let mut cache = $cachename.lock().unwrap();
+                let res = $crate::Cached::cache_get(&mut *cache, &key);
+                if let Some(res) = res { return res.clone(); }
+            }
+            // run the function and cache the result
+            async fn inner($($arg: $argtype),*) -> $ret $body
+            let val = inner($($arg),*).await;
+            let mut cache = $cachename.lock().unwrap();
+            $crate::Cached::cache_set(&mut *cache, key, val.clone());
+            val
+        }
+    };
 }
 
 #[macro_export]
@@ -86,6 +132,30 @@ macro_rules! cached_result {
             Ok(val)
         }
     };
+
+    ($cachename:ident : $cachetype:ty = $cacheinstance:expr ;
+     async fn $name:ident ($($arg:ident : $argtype:ty),*) -> $ret:ty = $body:expr) => {
+        static $cachename: $crate::once_cell::sync::Lazy<::std::sync::Mutex<$cachetype>>
+            = $crate::once_cell::sync::Lazy::new(|| ::std::sync::Mutex::new($cacheinstance));
+
+        #[allow(unused_parens)]
+        pub async fn $name($($arg: $argtype),*) -> $ret {
+            let key = ($($arg.clone()),*);
+            {
+                let mut cache = $cachename.lock().unwrap();
+                let res = $crate::Cached::cache_get(&mut *cache, &key);
+                if let Some(res) = res { return Ok(res.clone()); }
+            }
+
+            // run the function and cache the result
+            async fn inner($($arg: $argtype),*) -> $ret $body
+            let val = inner($($arg),*).await?;
+
+            let mut cache = $cachename.lock().unwrap();
+            $crate::Cached::cache_set(&mut *cache, key, val.clone());
+            Ok(val)
+        }
+    };
 }
 
 #[macro_export]
@@ -115,6 +185,31 @@ macro_rules! cached_key_result {
             Ok(val)
         }
     };
+
+    ($cachename:ident : $cachetype:ty = $cacheinstance:expr ;
+     Key = $key:expr;
+     async fn $name:ident ($($arg:ident : $argtype:ty),*) -> $ret:ty = $body:expr) => {
+        static $cachename: $crate::once_cell::sync::Lazy<::std::sync::Mutex<$cachetype>>
+            = $crate::once_cell::sync::Lazy::new(|| ::std::sync::Mutex::new($cacheinstance));
+
+        #[allow(unused_parens)]
+        pub async fn $name($($arg: $argtype),*) -> $ret {
+            let key = $key;
+            {
+                let mut cache = $cachename.lock().unwrap();
+                let res = $crate::Cached::cache_get(&mut *cache, &key);
+                if let Some(res) = res { return Ok(res.clone()); }
+            }
+
+            // run the function and cache the result
+            async fn inner($($arg: $argtype),*) -> $ret $body
+            let val = inner($($arg),*).await?;
+
+            let mut cache = $cachename.lock().unwrap();
+            $crate::Cached::cache_set(&mut *cache, key, val.clone());
+            Ok(val)
+        }
+    };
 }
 
 #[macro_export]
@@ -148,4 +243,35 @@ macro_rules! cached_control {
             $return
         }
     };
+
+    ($cachename:ident : $cachetype:ty = $cacheinstance:expr ;
+     Key = $key:expr;
+     PostGet($cached_value:ident) = $post_get:expr;
+     PostExec($body_value:ident) = $post_exec:expr;
+     Set($set_value:ident) = $pre_set:expr;
+     Return($ret_value:ident) = $return:expr;
+     async fn $name:ident ($($arg:ident : $argtype:ty),*) -> $ret:ty = $body:expr) => {
+        static $cachename: $crate::once_cell::sync::Lazy<::std::sync::Mutex<$cachetype>>
+            = $crate::once_cell::sync::Lazy::new(|| ::std::sync::Mutex::new($cacheinstance));
+
+        #[allow(unused_parens)]
+        pub async fn $name($($arg: $argtype),*) -> $ret {
+            let key = $key;
+            {
+                let mut cache = $cachename.lock().unwrap();
+                let res = $crate::Cached::cache_get(&mut *cache, &key);
+                if let Some($cached_value) = res {
+                    $post_get
+                }
+            }
+             // run the function and cache the result
+            async fn inner($($arg: $argtype),*) -> $ret $body
+            let $body_value = inner($($arg),*).await?;
+            let $set_value = $post_exec;
+            let mut cache = $cachename.lock().unwrap();
+            $crate::Cached::cache_set(&mut *cache, key, $pre_set);
+            let $ret_value = $set_value;
+            $return
+        }
+    };
 }
diff --git a/src/stores/mod.rs b/src/stores/mod.rs
index 81cb6c4..ea94412 100644
--- a/src/stores/mod.rs
+++ b/src/stores/mod.rs
@@ -1,5 +1,6 @@
-use super::Cached;
+use crate::Cached;
 use std::cmp::Eq;
+#[cfg(feature = "async")]
 use std::collections::hash_map::Entry;
 use std::collections::HashMap;
 use std::hash::Hash;
@@ -7,11 +8,15 @@ use std::hash::Hash;
 #[cfg(feature = "async")]
 use {super::CachedAsync, async_trait::async_trait, futures::Future};
 
+#[cfg(feature = "redis")]
+mod redis;
 mod sized;
 mod timed;
 mod timed_sized;
 mod unbound;
 
+#[cfg(feature = "redis")]
+pub use crate::stores::redis::RedisCache;
 pub use sized::SizedCache;
 pub use timed::TimedCache;
 pub use timed_sized::TimedSizedCache;
diff --git a/src/stores/redis.rs b/src/stores/redis.rs
new file mode 100644
index 0000000..38e9ea9
--- /dev/null
+++ b/src/stores/redis.rs
@@ -0,0 +1,452 @@
+use super::Cached;
+use redis::{Client, Commands, RedisResult};
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use std::env;
+use std::fmt::Display;
+use std::marker::PhantomData;
+use std::time::{SystemTime, UNIX_EPOCH};
+#[cfg(feature = "async")]
+use {super::CachedAsync, async_trait::async_trait, futures::Future};
+
+/// Cache store optionally bound by time
+///
+/// Values can be timestamped when inserted,
+/// then will be evicted if expired at time of retrieval.
+#[derive(Debug)]
+pub struct RedisCache<K, V> {
+    pub(super) store: Vec<V>,
+    pub(super) seconds: Option<u64>,
+    pub(super) hits: u64,
+    pub(super) misses: u64,
+    client: Client,
+    prefix: String,
+    _phantom: PhantomData<K>,
+}
+
+const ENV_KEY: &str = "REDIS_CS";
+const PREFIX: &str = "cached_key_prefix-";
+
+impl<K, V> RedisCache<K, V>
+where
+    K: Display,
+    V: Serialize + DeserializeOwned + Clone,
+{
+    /// Creates an empty `RedisCache` definition
+    pub fn new() -> Self {
+        Self {
+            store: vec![],
+            seconds: None,
+            hits: 0,
+            misses: 0,
+            client: Self::get_client(None),
+            prefix: Self::generate_prefix(),
+            _phantom: Default::default(),
+        }
+    }
+
+    /// Creates a new `RedisCache` with a specified lifespan
+    pub fn with_lifespan(seconds: u64) -> Self {
+        Self {
+            store: vec![],
+            seconds: Some(seconds),
+            hits: 0,
+            misses: 0,
+            client: Self::get_client(None),
+            prefix: Self::generate_prefix(),
+            _phantom: Default::default(),
+        }
+    }
+
+    /// Set the prefix for the keys
+    pub fn set_prefix(mut self, prefix: &str) -> Self {
+        self.prefix = prefix.to_string();
+        self
+    }
+
+    /// Set the connection string for redis
+    pub fn set_connection_string(mut self, cs: &str) -> Self {
+        self.client = Self::get_client(cs.to_string());
+        self
+    }
+
+    fn get_client(cs: impl Into<Option<String>>) -> Client {
+        let cs = cs.into().unwrap_or_else(|| {
+            env::var(ENV_KEY).unwrap_or_else(|_| {
+                panic!(
+                    "Environment variable for Redis connection string is missing, please set {} env var.",
+                    ENV_KEY
+                )
+            })
+        }
+        );
+
+        redis::Client::open(cs).expect("Cannot connect to Redis")
+    }
+
+    fn generate_prefix() -> String {
+        let timestamp = SystemTime::now()
+            .duration_since(UNIX_EPOCH)
+            .unwrap()
+            .as_nanos()
+            .to_string();
+        format!("{}{}-", PREFIX, timestamp)
+    }
+
+    fn generate_key(&self, key: &K) -> String {
+        format!("{}{}", self.prefix, key)
+    }
+
+    fn clean_up(&mut self) {
+        self.store.clear();
+    }
+
+    fn base_vec_fill(&mut self, val: &str) -> &mut V {
+        let index = self.store.len();
+        self.store.push(serde_json::from_str(val).unwrap());
+        &mut self.store[index]
+    }
+
+    fn base_get(&self, key: K) -> Option<String> {
+        let mut con = self.client.get_connection().unwrap();
+        let val: RedisResult<String> = con.get(self.generate_key(&key));
+        val.ok()
+    }
+
+    fn base_set(&self, key: K, val: V) {
+        let mut con = self.client.get_connection().unwrap();
+        let generated_key = self.generate_key(&key);
+
+        match self.seconds {
+            None => con
+                .set::<String, String, String>(generated_key, serde_json::to_string(&val).unwrap())
+                .unwrap(),
+            Some(s) => con
+                .set_ex::<String, String, String>(
+                    generated_key,
+                    serde_json::to_string(&val).unwrap(),
+                    s as usize,
+                )
+                .unwrap(),
+        };
+    }
+}
+
+impl<K, V> Default for RedisCache<K, V>
+where
+    K: Display,
+    V: Serialize + DeserializeOwned + Clone,
+{
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl<'de, K, V> Cached<K, V> for RedisCache<K, V>
+where
+    K: Display + Clone,
+    V: Serialize + DeserializeOwned + Clone,
+{
+    fn cache_get(&mut self, key: &K) -> Option<&V> {
+        self.clean_up();
+
+        let val = self.base_get(key.clone());
+
+        match val {
+            Some(val) => {
+                self.hits += 1;
+                Some(self.base_vec_fill(&val))
+            }
+            None => {
+                self.misses += 1;
+                None
+            }
+        }
+    }
+
+    fn cache_get_mut(&mut self, key: &K) -> Option<&mut V> {
+        self.clean_up();
+
+        let val = self.base_get(key.clone());
+
+        match val {
+            Some(val) => {
+                self.hits += 1;
+                Some(self.base_vec_fill(&val))
+            }
+            None => {
+                self.misses += 1;
+                None
+            }
+        }
+    }
+
+    fn cache_set(&mut self, key: K, val: V) -> Option<V> {
+        self.clean_up();
+        let old_val = self
+            .base_get(key.clone())
+            .map(|val| serde_json::from_str(&val).unwrap());
+        self.base_set(key, val);
+        old_val
+    }
+
+    fn cache_get_or_set_with<F: FnOnce() -> V>(&mut self, key: K, f: F) -> &mut V {
+        self.clean_up();
+
+        let val = self.base_get(key.clone());
+
+        match val {
+            Some(val) => {
+                self.hits += 1;
+                self.base_vec_fill(&val)
+            }
+            None => {
+                self.misses += 1;
+                let val = f();
+                self.base_set(key, val.clone());
+                let index = self.store.len();
+                self.store.push(val);
+                &mut self.store[index]
+            }
+        }
+    }
+
+    fn cache_remove(&mut self, key: &K) -> Option<V> {
+        self.clean_up();
+        let mut con = self.client.get_connection().unwrap();
+        self.base_get(key.clone()).map(|val| {
+            con.del::<String, ()>(self.generate_key(key)).unwrap();
+            serde_json::from_str(&val).unwrap()
+        })
+    }
+
+    fn cache_clear(&mut self) {
+        // copied from https://stackoverflow.com/questions/4006324/how-to-atomically-delete-keys-matching-a-pattern-using-redis#comment39607023_16974060
+        const REDIS_LUA_BATCH_DELETE_CMD: &str = "local keys = redis.call('keys', ARGV[1]) \n for i=1,#keys,5000 do \n redis.call('del', unpack(keys, i, math.min(i+4999, #keys))) \n end \n return keys";
+
+        self.clean_up();
+        let mut con = self.client.get_connection().unwrap();
+        redis::cmd("EVAL")
+            .arg(REDIS_LUA_BATCH_DELETE_CMD)
+            .arg(0)
+            .arg(format!("{}*", self.prefix))
+            .query(&mut con)
+            .unwrap()
+    }
+
+    /// In `RedisCache`, it's an alias to `cache_clear`
+    fn cache_reset(&mut self) {
+        self.cache_clear();
+    }
+
+    fn cache_size(&self) -> usize {
+        let mut con = self.client.get_connection().unwrap();
+        redis::cmd("EVAL")
+            .arg(format!("return #redis.call('keys', '{}*')", self.prefix))
+            .arg(0)
+            .query(&mut con)
+            .unwrap()
+    }
+
+    fn cache_hits(&self) -> Option<u64> {
+        Some(self.hits)
+    }
+
+    fn cache_misses(&self) -> Option<u64> {
+        Some(self.misses)
+    }
+
+    fn cache_lifespan(&self) -> Option<u64> {
+        self.seconds
+    }
+
+    fn cache_set_lifespan(&mut self, seconds: u64) -> Option<u64> {
+        self.clean_up();
+        let old = self.seconds;
+        self.seconds = Some(seconds);
+        old
+    }
+}
+
+#[cfg(feature = "async")]
+#[async_trait]
+impl<K, V> CachedAsync<K, V> for RedisCache<K, V>
+where
+    K: Send + Display,
+    V: Serialize + DeserializeOwned + Clone,
+{
+    async fn get_or_set_with<F, Fut>(&mut self, key: K, f: F) -> &mut V
+    where
+        V: Send,
+        F: FnOnce() -> Fut + Send,
+        Fut: Future<Output = V> + Send,
+    {
+        let mut con = self.client.get_connection().unwrap();
+        let generated_key = self.generate_key(&key);
+        let val: RedisResult<String> = con.get(generated_key.clone());
+        match val {
+            Ok(val) => {
+                self.hits += 1;
+                self.base_vec_fill(&val)
+            }
+            Err(_) => {
+                let val = f().await;
+                self.base_set(key, val.clone());
+                let index = self.store.len();
+                self.store.push(val);
+                &mut self.store[index]
+            }
+        }
+    }
+
+    async fn try_get_or_set_with<F, Fut, E>(&mut self, key: K, f: F) -> Result<&mut V, E>
+    where
+        V: Send,
+        F: FnOnce() -> Fut + Send,
+        Fut: Future<Output = Result<V, E>> + Send,
+    {
+        let mut con = self.client.get_connection().unwrap();
+        let generated_key = self.generate_key(&key);
+        let val: RedisResult<String> = con.get(generated_key.clone());
+        let v = match val {
+            Ok(val) => {
+                self.hits += 1;
+                self.base_vec_fill(&val)
+            }
+            Err(_) => {
+                let val = f().await?;
+                self.base_set(key, val.clone());
+                let index = self.store.len();
+                self.store.push(val);
+                &mut self.store[index]
+            }
+        };
+
+        Ok(v)
+    }
+}
+
+#[cfg(test)]
+/// Cache store tests
+mod tests {
+    use std::thread::sleep;
+    use std::time::Duration;
+
+    use super::*;
+
+    #[test]
+    fn redis_cache() {
+        let mut c = RedisCache::with_lifespan(2);
+
+        assert!(c.cache_get(&1).is_none());
+        let misses = c.cache_misses().unwrap();
+        assert_eq!(1, misses);
+
+        assert_eq!(c.cache_set(1, 100), None);
+        assert!(c.cache_get(&1).is_some());
+        let hits = c.cache_hits().unwrap();
+        let misses = c.cache_misses().unwrap();
+        assert_eq!(1, hits);
+        assert_eq!(1, misses);
+
+        sleep(Duration::new(2, 0));
+        assert!(c.cache_get(&1).is_none());
+        let misses = c.cache_misses().unwrap();
+        assert_eq!(2, misses);
+
+        let old = c.cache_set_lifespan(1).unwrap();
+        assert_eq!(2, old);
+        assert_eq!(c.cache_set(1, 100), None);
+        assert!(c.cache_get(&1).is_some());
+        let hits = c.cache_hits().unwrap();
+        let misses = c.cache_misses().unwrap();
+        assert_eq!(2, hits);
+        assert_eq!(2, misses);
+
+        sleep(Duration::new(1, 0));
+        assert!(c.cache_get(&1).is_none());
+        let misses = c.cache_misses().unwrap();
+        assert_eq!(3, misses);
+
+        c.cache_clear();
+        c.cache_set_lifespan(10).unwrap();
+        assert_eq!(c.cache_set(1, 100), None);
+        assert_eq!(c.cache_set(2, 100), None);
+        assert_eq!(c.store.len(), 0);
+        assert_eq!(c.cache_get(&1), Some(&100));
+        assert_eq!(c.store.len(), 1);
+        assert_eq!(c.cache_get(&1), Some(&100));
+        assert_eq!(c.store.len(), 1);
+        c.cache_clear();
+    }
+
+    #[test]
+    fn clear() {
+        let mut c = RedisCache::with_lifespan(3600);
+
+        assert_eq!(c.cache_set(1, 100), None);
+        assert_eq!(c.cache_set(2, 200), None);
+        assert_eq!(c.cache_set(3, 300), None);
+        c.cache_clear();
+
+        assert_eq!(0, c.cache_size());
+        c.cache_clear();
+    }
+
+    #[test]
+    fn reset() {
+        let mut c = RedisCache::with_lifespan(100);
+
+        assert_eq!(c.cache_set(1, 100), None);
+        assert_eq!(c.cache_set(2, 200), None);
+        assert_eq!(c.cache_set(3, 300), None);
+        assert_eq!(0, c.store.capacity());
+
+        c.cache_reset();
+
+        assert_eq!(0, c.store.capacity());
+        c.cache_clear();
+    }
+
+    #[test]
+    fn remove() {
+        let mut c = RedisCache::with_lifespan(3600);
+
+        assert_eq!(c.cache_set(1, 100), None);
+        assert_eq!(c.cache_set(2, 200), None);
+        assert_eq!(c.cache_set(3, 300), None);
+
+        assert_eq!(Some(100), c.cache_remove(&1));
+        assert_eq!(2, c.cache_size());
+        c.cache_clear();
+    }
+
+    #[test]
+    fn get_or_set_with() {
+        let mut c = RedisCache::with_lifespan(2);
+
+        assert_eq!(c.cache_get_or_set_with(0, || 0), &0);
+        assert_eq!(c.cache_get_or_set_with(1, || 1), &1);
+        assert_eq!(c.cache_get_or_set_with(2, || 2), &2);
+        assert_eq!(c.cache_get_or_set_with(3, || 3), &3);
+        assert_eq!(c.cache_get_or_set_with(4, || 4), &4);
+        assert_eq!(c.cache_get_or_set_with(5, || 5), &5);
+
+        assert_eq!(c.cache_misses(), Some(6));
+
+        assert_eq!(c.cache_get_or_set_with(0, || 0), &0);
+
+        assert_eq!(c.cache_misses(), Some(6));
+
+        assert_eq!(c.cache_get_or_set_with(0, || 42), &0);
+
+        assert_eq!(c.cache_misses(), Some(6));
+
+        sleep(Duration::new(2, 0));
+
+        assert_eq!(c.cache_get_or_set_with(1, || 42), &42);
+
+        assert_eq!(c.cache_misses(), Some(7));
+        c.cache_clear();
+    }
+}


Test patch:
diff --git a/lint-test.sh b/lint-test.sh
index f996ca3..41d5280 100755
--- a/lint-test.sh
+++ b/lint-test.sh
@@ -8,11 +8,19 @@ cargo fmt -- --check
 ./readme.sh check
 
 cargo clippy --all-features --all-targets --examples --tests
-cargo test --all-features
+cargo test
+
+# setup redis and env variable and run redis tests
+docker rm -f cached-tests || true
+docker run --rm --name cached-tests -p 6379:6379 -d redis
+export REDIS_CS=redis://127.0.0.1/
+cargo test --features="sync_redis" -- --test-threads=1 --nocapture
 
 for ex in examples/*; do
     base=$(basename $ex)
     exname=$(echo $base | cut -d . -f 1)
-    cargo run --example $exname
+    cargo run --example $exname --all-features
 done
 
+# clean up
+docker rm -f cached-tests || true
diff --git a/tests/cached.rs b/tests/cached.rs
index 8c1e259..82f9c8a 100644
--- a/tests/cached.rs
+++ b/tests/cached.rs
@@ -4,6 +4,8 @@ Full tests of macro-defined functions
 #[macro_use]
 extern crate cached;
 
+#[cfg(feature = "redis")]
+use cached::RedisCache;
 use cached::{
     proc_macro::cached, proc_macro::once, Cached, SizedCache, TimedCache, TimedSizedCache,
     UnboundCache,
@@ -1183,3 +1185,72 @@ fn test_mutable_args_once() {
     assert_eq!((2, 2), mutable_args_once(1, 1));
     assert_eq!((2, 2), mutable_args_once(5, 6));
 }
+
+#[cfg(feature = "redis")]
+cached_result! {
+    UNBOUND_REDIS: RedisCache<u32, u32> = RedisCache::new();
+    fn cached_unbound_redis(n: u32) -> Result<u32, ()> = {
+        if n < 5 { Ok(n) } else { Err(()) }
+    }
+}
+
+#[cfg(feature = "redis")]
+#[test]
+fn test_cached_unbound_redis() {
+    assert!(cached_unbound_redis(2).is_ok());
+    assert!(cached_unbound_redis(4).is_ok());
+    assert!(cached_unbound_redis(6).is_err());
+    assert!(cached_unbound_redis(6).is_err());
+    assert!(cached_unbound_redis(2).is_ok());
+    assert!(cached_unbound_redis(4).is_ok());
+    {
+        let mut cache = UNBOUND_REDIS.lock().unwrap();
+        assert_eq!(2, cache.cache_size());
+        assert_eq!(2, cache.cache_hits().unwrap());
+        assert_eq!(4, cache.cache_misses().unwrap());
+        cache.cache_clear();
+    }
+}
+
+#[cfg(feature = "redis")]
+cached! {
+    TIMED_REDIS: RedisCache<u32, u32> = RedisCache::with_lifespan(2);
+    fn cached_timed_redis(n: u32) -> u32 = {
+        sleep(Duration::new(3, 0));
+        n
+    }
+}
+
+#[cfg(feature = "redis")]
+#[test]
+fn test_cached_timed_redis() {
+    cached_timed_redis(1);
+    cached_timed_redis(1);
+    {
+        let cache = TIMED_REDIS.lock().unwrap();
+        assert_eq!(1, cache.cache_misses().unwrap());
+        assert_eq!(1, cache.cache_hits().unwrap());
+    }
+    sleep(Duration::new(3, 0));
+    cached_timed_redis(1);
+    {
+        let cache = TIMED_REDIS.lock().unwrap();
+        assert_eq!(2, cache.cache_misses().unwrap());
+        assert_eq!(1, cache.cache_hits().unwrap());
+    }
+    {
+        let mut cache = TIMED_REDIS.lock().unwrap();
+        assert_eq!(2, cache.cache_set_lifespan(1).unwrap());
+        cache.cache_clear();
+    }
+    cached_timed_redis(1);
+    cached_timed_redis(1);
+    sleep(Duration::new(1, 0));
+    cached_timed_redis(1);
+    {
+        let mut cache = TIMED_REDIS.lock().unwrap();
+        assert_eq!(4, cache.cache_misses().unwrap());
+        assert_eq!(2, cache.cache_hits().unwrap());
+        cache.cache_clear();
+    }
+}