feat: cache for posts list
This commit is contained in:
@@ -92,7 +92,7 @@ pub async fn add_comment(
|
|||||||
rconn: RdsConn,
|
rconn: RdsConn,
|
||||||
) -> API<Value> {
|
) -> API<Value> {
|
||||||
let mut p = Post::get(&db, &rconn, ci.pid).await?;
|
let mut p = Post::get(&db, &rconn, ci.pid).await?;
|
||||||
Comment::create(
|
let c = Comment::create(
|
||||||
&db,
|
&db,
|
||||||
NewComment {
|
NewComment {
|
||||||
content: ci.text.to_string(),
|
content: ci.text.to_string(),
|
||||||
@@ -104,6 +104,7 @@ pub async fn add_comment(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
p.change_n_comments(&db, 1).await?;
|
p.change_n_comments(&db, 1).await?;
|
||||||
|
p.update_comment_time(&db, c.create_time).await?;
|
||||||
// auto attention after comment
|
// auto attention after comment
|
||||||
let mut att = Attention::init(&user.namehash, &rconn);
|
let mut att = Attention::init(&user.namehash, &rconn);
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ pub async fn delete(
|
|||||||
_ => return Err(APIError::PcError(NotAllowed)),
|
_ => return Err(APIError::PcError(NotAllowed)),
|
||||||
}
|
}
|
||||||
|
|
||||||
p.refresh_cache(&rconn, false).await;
|
// 如果是删除,需要也从0号缓存队列中去掉
|
||||||
|
p.refresh_cache(&rconn, true).await;
|
||||||
|
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"code": 0
|
"code": 0
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ async fn p2output(p: &Post, user: &CurrentUser, db: &Db, rconn: &RdsConn) -> Pos
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
// 单个洞还有查询评论的接口,这里挂了不用报错
|
// 单个洞还有查询评论的接口,这里挂了不用报错
|
||||||
p.get_comments(db, rconn).await.ok().map(|cs| c2output(p, &cs, user))
|
p.get_comments(db, rconn)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.map(|cs| c2output(p, &cs, user))
|
||||||
},
|
},
|
||||||
can_del: p.check_permission(user, "wd").is_ok(),
|
can_del: p.check_permission(user, "wd").is_ok(),
|
||||||
attention: Attention::init(&user.namehash, &rconn)
|
attention: Attention::init(&user.namehash, &rconn)
|
||||||
@@ -117,7 +120,7 @@ pub async fn get_list(
|
|||||||
let page = p.unwrap_or(1);
|
let page = p.unwrap_or(1);
|
||||||
let page_size = 25;
|
let page_size = 25;
|
||||||
let start = (page - 1) * page_size;
|
let start = (page - 1) * page_size;
|
||||||
let ps = Post::gets_by_page(&db, order_mode, start.into(), page_size.into()).await?;
|
let ps = Post::gets_by_page(&db, &rconn, order_mode, start.into(), page_size.into()).await?;
|
||||||
let ps_data = ps2outputs(&ps, &user, &db, &rconn).await;
|
let ps_data = ps2outputs(&ps, &user, &db, &rconn).await;
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"data": ps_data,
|
"data": ps_data,
|
||||||
@@ -147,6 +150,7 @@ pub async fn publish_post(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Attention::init(&user.namehash, &rconn).add(p.id).await?;
|
Attention::init(&user.namehash, &rconn).add(p.id).await?;
|
||||||
|
p.refresh_cache(&rconn, true).await;
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"code": 0
|
"code": 0
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub async fn search(
|
|||||||
} else {
|
} else {
|
||||||
Post::search(
|
Post::search(
|
||||||
&db,
|
&db,
|
||||||
|
&rconn,
|
||||||
search_mode,
|
search_mode,
|
||||||
keywords.to_string(),
|
keywords.to_string(),
|
||||||
start.into(),
|
start.into(),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::api::{CurrentUser, API};
|
use crate::api::{CurrentUser, API};
|
||||||
|
use crate::db_conn::Db;
|
||||||
use crate::random_hasher::RandomHasher;
|
use crate::random_hasher::RandomHasher;
|
||||||
use rocket::serde::json::{json, Value};
|
use rocket::serde::json::{json, Value};
|
||||||
use rocket::State;
|
use rocket::State;
|
||||||
use crate::db_conn::Db;
|
|
||||||
|
|
||||||
#[get("/systemlog")]
|
#[get("/systemlog")]
|
||||||
pub async fn get_systemlog(user: CurrentUser, rh: &State<RandomHasher>, db: Db) -> API<Value> {
|
pub async fn get_systemlog(user: CurrentUser, rh: &State<RandomHasher>, db: Db) -> API<Value> {
|
||||||
|
|||||||
108
src/cache.rs
108
src/cache.rs
@@ -1,10 +1,15 @@
|
|||||||
use crate::models::{Comment, Post, User};
|
use crate::models::{Comment, Post, User};
|
||||||
use crate::rds_conn::RdsConn;
|
use crate::rds_conn::RdsConn;
|
||||||
|
use rand::Rng;
|
||||||
use redis::AsyncCommands;
|
use redis::AsyncCommands;
|
||||||
use rocket::serde::json::serde_json;
|
use rocket::serde::json::serde_json;
|
||||||
// can use rocket::serde::json::to_string in master version
|
// can use rocket::serde::json::to_string in master version
|
||||||
|
|
||||||
const INSTANCE_EXPIRE_TIME: usize = 60 * 60;
|
const INSTANCE_EXPIRE_TIME: usize = 60 * 60;
|
||||||
|
|
||||||
|
const MIN_LENGTH: isize = 200;
|
||||||
|
const MAX_LENGTH: isize = 900;
|
||||||
|
|
||||||
macro_rules! post_cache_key {
|
macro_rules! post_cache_key {
|
||||||
($id: expr) => {
|
($id: expr) => {
|
||||||
format!("hole_v2:cache:post:{}", $id)
|
format!("hole_v2:cache:post:{}", $id)
|
||||||
@@ -150,6 +155,109 @@ impl PostCommentCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PostListCommentCache {
|
||||||
|
key: String,
|
||||||
|
mode: u8,
|
||||||
|
rconn: RdsConn,
|
||||||
|
length: isize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostListCommentCache {
|
||||||
|
pub async fn init(mode: u8, rconn: &RdsConn) -> Self {
|
||||||
|
let mut cacher = PostListCommentCache {
|
||||||
|
key: format!("hole_v2:cache:post_list:{}", &mode),
|
||||||
|
mode: mode,
|
||||||
|
rconn: rconn.clone(),
|
||||||
|
length: 0,
|
||||||
|
};
|
||||||
|
cacher.set_and_check_length().await;
|
||||||
|
cacher
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_and_check_length(&mut self) {
|
||||||
|
let mut l = self.rconn.zcard(&self.key).await.unwrap();
|
||||||
|
if l > MAX_LENGTH {
|
||||||
|
self.rconn
|
||||||
|
.zremrangebyrank::<&String, ()>(&self.key, MIN_LENGTH, -1)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
warn!("cut list cache failed, {}, {}", e, &self.key);
|
||||||
|
});
|
||||||
|
l = MIN_LENGTH;
|
||||||
|
}
|
||||||
|
self.length = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn need_fill(&self) -> bool {
|
||||||
|
self.length < MIN_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn i64_len(&self) -> i64 {
|
||||||
|
self.length.try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn i64_minlen(&self) -> i64 {
|
||||||
|
MIN_LENGTH.try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn p2pair(&self, p: &Post) -> (i64, i32) {
|
||||||
|
(
|
||||||
|
match self.mode {
|
||||||
|
0 => (-p.id).into(),
|
||||||
|
1 => -p.last_comment_time.timestamp(),
|
||||||
|
2 => (-p.hot_score).into(),
|
||||||
|
3 => rand::thread_rng().gen_range(0..i64::MAX),
|
||||||
|
_ => panic!("wrong mode"),
|
||||||
|
},
|
||||||
|
p.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fill(&mut self, ps: &Vec<Post>) {
|
||||||
|
let items: Vec<(i64, i32)> = ps.iter().map(|p| self.p2pair(p)).collect();
|
||||||
|
self.rconn
|
||||||
|
.zadd_multiple(&self.key, &items)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
warn!("fill list cache failed, {} {}", e, &self.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.set_and_check_length().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn put(&mut self, p: &Post) {
|
||||||
|
// 其他都是加到最前面的,但热榜不是。可能导致MIN_LENGTH到MAX_LENGTH之间的数据不可靠
|
||||||
|
// 影响不大,先不管了
|
||||||
|
if p.is_deleted {
|
||||||
|
self.rconn.zrem(&self.key, p.id).await.unwrap_or_else(|e| {
|
||||||
|
warn!(
|
||||||
|
"remove from list cache failed, {} {} {}",
|
||||||
|
e, &self.key, p.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let (s, m) = self.p2pair(p);
|
||||||
|
self.rconn.zadd(&self.key, m, s).await.unwrap_or_else(|e| {
|
||||||
|
warn!(
|
||||||
|
"put into list cache failed, {} {} {} {}",
|
||||||
|
e, &self.key, m, s
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_pids(&mut self, start: i64, limit: i64) -> Vec<i32> {
|
||||||
|
self.rconn
|
||||||
|
.zrange(
|
||||||
|
&self.key,
|
||||||
|
start.try_into().unwrap(),
|
||||||
|
(start + limit - 1).try_into().unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct UserCache {
|
pub struct UserCache {
|
||||||
key: String,
|
key: String,
|
||||||
rconn: RdsConn,
|
rconn: RdsConn,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
use crate::cache::{PostCache, PostCommentCache, UserCache};
|
use crate::cache::*;
|
||||||
use crate::db_conn::Db;
|
use crate::db_conn::Db;
|
||||||
use crate::libs::diesel_logger::LoggingConnection;
|
use crate::libs::diesel_logger::LoggingConnection;
|
||||||
use crate::rds_conn::RdsConn;
|
use crate::rds_conn::RdsConn;
|
||||||
@@ -11,6 +11,7 @@ use diesel::{
|
|||||||
insert_into, BoolExpressionMethods, ExpressionMethods, QueryDsl, QueryResult, RunQueryDsl,
|
insert_into, BoolExpressionMethods, ExpressionMethods, QueryDsl, QueryResult, RunQueryDsl,
|
||||||
TextExpressionMethods,
|
TextExpressionMethods,
|
||||||
};
|
};
|
||||||
|
use rocket::futures::{future, join};
|
||||||
use rocket::serde::{Deserialize, Serialize};
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::identity;
|
use std::convert::identity;
|
||||||
@@ -191,11 +192,7 @@ impl Post {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_comments(
|
pub async fn get_comments(&self, db: &Db, rconn: &RdsConn) -> QueryResult<Vec<Comment>> {
|
||||||
&self,
|
|
||||||
db: &Db,
|
|
||||||
rconn: &RdsConn,
|
|
||||||
) -> QueryResult<Vec<Comment>> {
|
|
||||||
let mut cacher = PostCommentCache::init(self.id, rconn);
|
let mut cacher = PostCommentCache::init(self.id, rconn);
|
||||||
if let Some(cs) = cacher.get().await {
|
if let Some(cs) = cacher.get().await {
|
||||||
Ok(cs)
|
Ok(cs)
|
||||||
@@ -212,12 +209,34 @@ impl Post {
|
|||||||
|
|
||||||
pub async fn gets_by_page(
|
pub async fn gets_by_page(
|
||||||
db: &Db,
|
db: &Db,
|
||||||
|
rconn: &RdsConn,
|
||||||
order_mode: u8,
|
order_mode: u8,
|
||||||
start: i64,
|
start: i64,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> QueryResult<Vec<Self>> {
|
) -> QueryResult<Vec<Self>> {
|
||||||
|
let mut cacher = PostListCommentCache::init(order_mode, &rconn).await;
|
||||||
|
if cacher.need_fill() {
|
||||||
|
let pids =
|
||||||
|
Self::_get_ids_by_page(db, order_mode.clone(), 0, cacher.i64_minlen()).await?;
|
||||||
|
let ps = Self::get_multi(db, rconn, &pids).await?;
|
||||||
|
cacher.fill(&ps).await;
|
||||||
|
}
|
||||||
|
let pids = if start + limit > cacher.i64_len() {
|
||||||
|
Self::_get_ids_by_page(db, order_mode, start, limit).await?
|
||||||
|
} else {
|
||||||
|
cacher.get_pids(start, limit).await
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::get_multi(db, rconn, &pids).await
|
||||||
|
}
|
||||||
|
async fn _get_ids_by_page(
|
||||||
|
db: &Db,
|
||||||
|
order_mode: u8,
|
||||||
|
start: i64,
|
||||||
|
limit: i64,
|
||||||
|
) -> QueryResult<Vec<i32>> {
|
||||||
db.run(move |c| {
|
db.run(move |c| {
|
||||||
let mut query = base_query!(posts);
|
let mut query = base_query!(posts).select(posts::id);
|
||||||
if order_mode > 0 {
|
if order_mode > 0 {
|
||||||
query = query.filter(posts::is_reported.eq(false))
|
query = query.filter(posts::is_reported.eq(false))
|
||||||
}
|
}
|
||||||
@@ -237,15 +256,20 @@ impl Post {
|
|||||||
|
|
||||||
pub async fn search(
|
pub async fn search(
|
||||||
db: &Db,
|
db: &Db,
|
||||||
|
rconn: &RdsConn,
|
||||||
search_mode: u8,
|
search_mode: u8,
|
||||||
search_text: String,
|
search_text: String,
|
||||||
start: i64,
|
start: i64,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> QueryResult<Vec<Self>> {
|
) -> QueryResult<Vec<Self>> {
|
||||||
let search_text2 = search_text.replace("%", "\\%");
|
let search_text2 = search_text.replace("%", "\\%");
|
||||||
db.run(move |c| {
|
let pids = db
|
||||||
|
.run(move |c| {
|
||||||
let pat;
|
let pat;
|
||||||
let mut query = base_query!(posts).distinct().left_join(comments::table);
|
let mut query = base_query!(posts)
|
||||||
|
.select(posts::id)
|
||||||
|
.distinct()
|
||||||
|
.left_join(comments::table);
|
||||||
// 先用搜索+缓存,性能有问题了再真的做tag表
|
// 先用搜索+缓存,性能有问题了再真的做tag表
|
||||||
query = match search_mode {
|
query = match search_mode {
|
||||||
0 => {
|
0 => {
|
||||||
@@ -278,7 +302,8 @@ impl Post {
|
|||||||
.limit(limit)
|
.limit(limit)
|
||||||
.load(with_log!(c))
|
.load(with_log!(c))
|
||||||
})
|
})
|
||||||
.await
|
.await?;
|
||||||
|
Self::get_multi(db, rconn, &pids).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create(db: &Db, new_post: NewPost) -> QueryResult<Self> {
|
pub async fn create(db: &Db, new_post: NewPost) -> QueryResult<Self> {
|
||||||
@@ -303,6 +328,18 @@ impl Post {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update_comment_time(&mut self, db: &Db, t: DateTime<Utc>) -> QueryResult<()> {
|
||||||
|
let pid = self.id;
|
||||||
|
*self = db
|
||||||
|
.run(move |c| {
|
||||||
|
diesel::update(posts::table.find(pid))
|
||||||
|
.set(posts::last_comment_time.eq(t))
|
||||||
|
.get_result(with_log!(c))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn change_n_comments(&mut self, db: &Db, delta: i32) -> QueryResult<()> {
|
pub async fn change_n_comments(&mut self, db: &Db, delta: i32) -> QueryResult<()> {
|
||||||
let pid = self.id;
|
let pid = self.id;
|
||||||
*self = db
|
*self = db
|
||||||
@@ -343,7 +380,15 @@ impl Post {
|
|||||||
PostCache::init(rconn).sets(&vec![self]).await;
|
PostCache::init(rconn).sets(&vec![self]).await;
|
||||||
}
|
}
|
||||||
pub async fn refresh_cache(&self, rconn: &RdsConn, is_new: bool) {
|
pub async fn refresh_cache(&self, rconn: &RdsConn, is_new: bool) {
|
||||||
self.set_instance_cache(rconn).await;
|
join!(
|
||||||
|
self.set_instance_cache(rconn),
|
||||||
|
future::join_all((if is_new { 0..4 } else { 1..4 }).map(|mode| async move {
|
||||||
|
PostListCommentCache::init(mode, &rconn.clone())
|
||||||
|
.await
|
||||||
|
.put(self)
|
||||||
|
.await
|
||||||
|
})),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user