前言#
这一大段时间都没写文档,一方面是之前提到的在参与一个项目重构,全是坑搞得焦头烂额。另一方面是有准备跑路的打算,还有一方面是我这边在实现之前博客平台的前端部分,所以没写文章。等过段时间完成了会把实现的内容以文章的方式分享出来。
docker 安装redis#
如果你还不知道怎么安装docker,你可以看我之前的文章:坏蛋Dan:docker简单入门
直接上代码,我们这里准备使用的是redisJSON,也就是使用json格式存储。但是redisJSON的docker包已经废弃了,所以我们这里使用redis/redis-stack [1]
docker run -dp 2333:6379 --name blog_redis_stack -e REDIS_ARGS="--requirepass [your password]" redis/redis-stack--requirepass:这个是用来设置密码的,千万别忘了这一点,不然你就等着被注入病毒吧。。。。(别问我怎么知道的。。。)
没有镜像会自动下载镜像。
这个时候你可以用docker ps -a或者docker logs [container-name]的方式看下是否正常安装。
rust 接入 redis#
安装必要crate#
这里我们需要接入两个包
deadpool-redis = "0.13.0"
redis = { version = "0.23.3", features = ["tokio-comp", "json"] }- redis - crates.io: Rust Package Registry:提供连接
redis服务器的功能 - deadpool-redis - crates.io: Rust Package Registry 这个用来做
redis池
另外还需要
serde_json = "1.0"虽然是json格式,但是传输还是得序列化。。。
下载完这仨
业务接入#
然后我们来接入,咱这里是基于之前我们实现的博客平台来接入,如果你对这个项目还不了解,可以看下这个索引:坏蛋Dan:Rust + Rocket + PostgreSQL简单实现CRUD的restfulAPI后台——索引
不过GitHub上的代码好久没更新了,里面还有些问题和bug,但大体设计还是差不多的。
首先,我们的这个redis需要放到全局,这样就不需要每次都去创建。
而在rocket中,如果我们需要将某个东西放置全局,那么我们就需要用到State。
回到我们之前的state文件夹中。
use std::{ops::{DerefMut, Deref}, env};
use deadpool_redis::{Config, Runtime, Pool, Manager, Connection};
use rocket::{http::Status, State, futures::lock::Mutex};
use redis::{AsyncCommands, JsonAsyncCommands};
type DealpoolInstance = Pool;
pub struct RedisInstance(pub DealpoolInstance);
pub fn init_redis_instance() -> Result<RedisInstance, ()> {
let url = env::var("ROCKET_REDIS").expect("get ROCKET_REDIS fail");
let cfg = Config::from_url(url);
let pool = cfg.create_pool(Some(Runtime::Tokio1)).unwrap();
Ok(RedisInstance(pool))
}ROCKET_REDIS:这个是环境变量,我们需要手动在环境中添加,添加方式和我们数据库ROCKET_DB变量一样的。格式为:redis://:[password]@[host]:[port]/。比如你在powerShell中运行我们的服务器,那么你就需要设置环境变量:$ENV:ROCKET_REDIS = 'redis://:[password]@[host]:[port]/';
这里我们是创建一个redis池子,然后将它放置到全局。
接着我们去到main.rs中引入这个state。
#[launch]
fn rocket() -> _ {
rocket::custom(get_custom_figment())
// ...
.manage(init_redis_instance().unwrap())
// ...
} 那么这里就接入了,我们接着找一处地方使用下api看能否正常连上。
这里就直接去到article/route.rs文件中找到获取文章列表的route。
#[get("/get_article?<all>&<id>&<limit>&<page_no>")]
pub async fn route_article(
db: BlogDBC,
all: bool,
id: &str,
limit: i32,
page_no: i32,
redis_client: &State<RedisInstance>,
) -> Result<RtData<ArticleData>, Status> {
let limit = if limit < 0 { 20 } else { limit };
// 不存在的情况下直接插入数据
let result = get_article(db, all, id, limit, page_no).await;
match result {
Ok(v) => {
let articles: Vec<Article> = v
.iter()
.map(|row| Article {
id: row.get::<Uuid, usize>(0).to_string(),
title: row.get(1),
description: row.get(2),
modify_time: row.get::<i64, usize>(4) as u64,
head_pic: row.try_get(7).unwrap_or_else(|_str| String::new()),
likes: row.get::<i64, usize>(9) as u64,
views: row.get::<i64, usize>(10) as u64,
author_name: row.get(5),
author_desc: row.get(6),
author_id: row.get::<sqlx::types::Uuid, usize>(3).to_string(),
user_pic: row.try_get(8).unwrap_or_else(|_str| String::new()),
})
.collect();
return Ok(RtData {
success: true,
msg: String::from("get all article success!"),
rt: Rt::Success,
data: ArticleData { list: articles },
});
}
Err(err) => {
println!("query all article error, {}", err);
return Err(Status::InternalServerError);
}
}
}这里代码我就不详细讲了,就是分页查询。
然后我们来接入redis测试下能不能用。
首先我们在type文件夹下创建一个cache_type.rs的文件用于存储缓存的类型。
use redis::{FromRedisValue, from_redis_value, RedisResult};
use serde::{Serialize, Deserialize};
use super::Article;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheArticles(pub Vec<Article>);
impl FromRedisValue for Article {
fn from_redis_value(v: &redis::Value) -> RedisResult<Self> {
let value: String = from_redis_value(v)?;
dbg!(&value);
RedisResult::Ok(serde_json::from_str::<Article>(&value).expect("get article cache fail"))
}
}
impl FromRedisValue for CacheArticles {
fn from_redis_value(v: &redis::Value) -> redis::RedisResult<Self> {
let cache: String = from_redis_value(v)?;
let cache = serde_json::from_str::<CacheArticles>(&cache).expect("get cache articles fail");
dbg!(&cache);
RedisResult::Ok(cache)
}
} 这里我们创建了一个CacheArticles的 tuple struct,用来存放我们缓存文章列表。然后我们需要给它实现FromRedisValue这个trait,为什么要这么做呢?因为我们需要告知redis我们需要它返回的数据类型(当然,还是我们手动实现的,没办法自动判断)。
这里我们还得给Article也实现FromRedisValue,因为外层可以实现的原则是内部一定都实现了~
from_redis_value:这是个function,用来简化对redis::Value的处理。我们可以稍微看下相关的源码

FromRedisValue

macro
这里有两个宏,用来自动实现FromRedisValue,但是很不巧,它没有被暴露出来,我们不能使用它。
而from_redis_value则是FromRedisValue::from_redis_value的简写,能快速返回一个处理好的Value。

而我们还需要将数据从字符串转换回我们的struct类型,所以我们需要手动去实现。即:
serde_json::from_str::<CacheArticles>(&cache)当然,也是需要申明类型的。
然后回到我们的route里。
我们先来实现cache部分。
const CACHE_KEY: &'static str = "__articles_cache";
let pool = &(*redis_client).0;
let mut con = pool
.get()
.await
.expect("get redis connection from deal pool fail when record likes");
match result {
Ok(v) => {
let articles: Vec<Article> = v
.iter()
.map(|row| Article {
id: row.get::<Uuid, usize>(0).to_string(),
title: row.get(1),
description: row.get(2),
modify_time: row.get::<i64, usize>(4) as u64,
head_pic: row.try_get(7).unwrap_or_else(|_str| String::new()),
likes: row.get::<i64, usize>(9) as u64,
views: row.get::<i64, usize>(10) as u64,
author_name: row.get(5),
author_desc: row.get(6),
author_id: row.get::<sqlx::types::Uuid, usize>(3).to_string(),
user_pic: row.try_get(8).unwrap_or_else(|_str| String::new()),
})
.collect();
let cache = CacheArticles(articles.clone());
let cache_res = con
.json_set::<&'static str, &str, CacheArticles, ()>(CACHE_KEY, ".", &cache)
.await;
dbg!(cache_res);
return Ok(RtData {
success: true,
msg: String::from("get all article success!"),
rt: Rt::Success,
data: ArticleData { list: articles },
});
} 这里我们在返回数据之前,将数据放到了redis里。
使用json_set的方法,这个方法会自动序列化,另外trait bound需要我们实现Serialize和Deserialize这俩trait,我们前面定义的时候已经通过派生宏的方式实现了。
这个方法会自动将我们的struct序列化。

这里它实际上是调用的redis::cmd("JSON.SET")的方式来实现的,这种方案也可以,不过繁琐一些。
不过你现在应该很疑惑,为啥你的代码提示没有提示这个api,因为你没有引入几个重要的trait。
use redis::{AsyncCommands, JsonAsyncCommands, JsonCommands, RedisError, Value}; 这里我们需要手动引入AsyncCommands, JsonAsyncCommands, JsonCommands这仨,不然就没办法使用json相关的method。
接着我们可以运行下代码看是否正常set。
可以直接看下log,或者我们也可以通过redis-cli去查看是否set进去了。
回到我们的docker。
docker exec -it [container-name] redis-cli 然后我们使用AUTH [your password]的方式验证权限。
接着我们可以使用KEYS *的方式查看

然后如果需要看具体的,redis提供了六种类型。

我这里就不演示了(其实我遇到了问题,老是说类型有误。。。但是我存储的确实是字符串)
然后回到我们的代码中,我们接着来使用get来看下缓存的数据。
let pool = &(*redis_client).0;
let mut con = pool
.get()
.await
.expect("get redis connection from deal pool fail when record likes");
let is_exist_id = con.exists(CACHE_KEY).await.unwrap_or_else(|e| false);
let cache_list = con
.json_get::<&'static str, &str, CacheArticles>(CACHE_KEY, ".")
.await; 这里使用的是json_get的method,注意这里需要声明类型CacheArticles。
然后我们重新运行下我们的代码

可以看到正常拿到并且转换成我们的enum struct类型了。
总结#
rust中接入redis的学习成本还是较高的。。。 我这边还是在摸索中,要完全接入还得思考很多问题。。
补充#
实现FromRedisValue的注意事项#
注意:实现FromRedisValue的目的是为了转换成我们json_get时需要的类型,所以这里不需要和json_set是同一个类型。
比如我们有这么一个struct People { name: String; age: u8 }。它将作为json_set的类型,即你存储到redis里的是{ name: 'xxx', age: xxx }。
而在某个场景中,你只需要name这个字段,这个时候你就不能使用People这个类型了,而是应该使用String作为它返回的类型,比如这样
let cache_name = con
.json_get::<&'static str, &str, String>(CACHE_KEY, ".name").await;而非这样
let cache_name = con
.json_get::<&'static str, &str, People>(CACHE_KEY, ".name").await;FromRedisValue是用来处理返回数据的,所以如果你get的数据结构和set的不同,此时如果还使用同一个类型,就会导致报错。
如何获取某个path下的数组切片#
由于一次性从缓存中将整个数组拿出来会临时占用大量的内存,所以最好的方式是拿必要的切片,这个时候我们也是可以使用json_get来实现的,比如:
`.json_get::<&'static str, &str, CacheArticlesSlice>(CACHE_KEY, "$.list[3:7]").await;`, 注意这里的$.list[start:end:steps],$不可少,不然只会返回索引为start的元素。
不过这里还有个问题,那就是这里会导致不走我们的CacheArticleSlice的from_redis_value,而是走的Article::from_redis_value,问题的原因可能是我们这里使用的是json_get的method,所以它会认为只有一个。
所以这个时候我们需要修改下代码,避免涉及到Article类型
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheArticlesSlice(pub Vec<Article>);
impl FromRedisValue for CacheArticlesSlice {
fn from_redis_value(v: &redis::Value) -> RedisResult<Self> {
let cache: String = from_redis_value(v)?;
dbg!(&cache);
RedisResult::Ok(CacheArticlesSlice(serde_json::from_str(&cache).unwrap()))
}
} 这里实现FromRedisValue的时候没有涉及Article,则不会走Article::from_redis_value。
参考#
- ^redis/redis-stack https://hub.docker.com/r/redis/redis-stack
发布于 2023-10-17 16:48・IP 属地广东
