#[cfg(feature = "redis")]
use async_trait::async_trait;
use redis::{aio::ConnectionManager, AsyncCommands, Client};
use secrecy::{ExposeSecret, Secret};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
use tracing::{debug, error, info, trace, warn};
use crate::error::{ConsumeResult, StorageError};
use crate::lua_scripts::{LuaScriptManager, LuaScriptType};
use crate::storage::{BanRecord, BanStorage, BanTarget, QuotaInfo, QuotaStorage, Storage};
const MAX_KEY_COMPONENT_LENGTH: usize = 255;
const MAX_KEY_LENGTH: usize = 1024;
fn sanitize_key_component(input: &str) -> String {
input
.chars()
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_' || *c == '.')
.take(MAX_KEY_COMPONENT_LENGTH)
.collect()
}
fn validate_key(key: &str) -> Result<(), StorageError> {
if key.is_empty() {
return Err(StorageError::QueryError("键不能为空".to_string()));
}
if key.len() > MAX_KEY_LENGTH {
return Err(StorageError::QueryError(format!(
"键长度超过限制(最大 {} 字符)",
MAX_KEY_LENGTH
)));
}
if key.contains('\0') {
return Err(StorageError::QueryError("键包含非法字符".to_string()));
}
Ok(())
}
#[cfg(feature = "redis")]
#[derive(Clone)]
pub struct RedisConfig {
pub url: String,
pub db: i64,
pub password: Option<Secret<String>>,
pub connection_timeout: Duration,
pub io_timeout: Duration,
pub max_retries: u32,
pub retry_initial_backoff: Duration,
pub cluster_mode: bool,
pub pool_size: usize,
pub enable_lua: bool,
}
impl std::fmt::Debug for RedisConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RedisConfig")
.field("url", &self.url)
.field("db", &self.db)
.field("password", &self.password.as_ref().map(|_| "***"))
.field("connection_timeout", &self.connection_timeout)
.field("io_timeout", &self.io_timeout)
.field("max_retries", &self.max_retries)
.field("retry_initial_backoff", &self.retry_initial_backoff)
.field("cluster_mode", &self.cluster_mode)
.field("pool_size", &self.pool_size)
.field("enable_lua", &self.enable_lua)
.finish()
}
}
impl Default for RedisConfig {
fn default() -> Self {
Self {
url: "redis://127.0.0.1:6379".to_string(),
db: 0,
password: None,
connection_timeout: Duration::from_secs(5),
io_timeout: Duration::from_secs(5),
max_retries: 3,
retry_initial_backoff: Duration::from_millis(100),
cluster_mode: false,
pool_size: 10,
enable_lua: true,
}
}
}
impl RedisConfig {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
..Default::default()
}
}
pub fn db(mut self, db: i64) -> Self {
self.db = db;
self
}
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(Secret::new(password.into()));
self
}
pub fn password_secret(mut self, password: Secret<String>) -> Self {
self.password = Some(password);
self
}
pub fn connection_timeout(mut self, timeout: Duration) -> Self {
self.connection_timeout = timeout;
self
}
pub fn io_timeout(mut self, timeout: Duration) -> Self {
self.io_timeout = timeout;
self
}
pub fn max_retries(mut self, retries: u32) -> Self {
self.max_retries = retries;
self
}
pub fn retry_initial_backoff(mut self, backoff: Duration) -> Self {
self.retry_initial_backoff = backoff;
self
}
pub fn cluster_mode(mut self, cluster: bool) -> Self {
self.cluster_mode = cluster;
self
}
pub fn pool_size(mut self, size: usize) -> Self {
self.pool_size = size;
self
}
pub fn enable_lua(mut self, enable: bool) -> Self {
self.enable_lua = enable;
self
}
}
#[cfg(feature = "redis")]
#[derive(Debug, Default, Clone)]
pub struct RetryStats {
pub total_retries: Arc<std::sync::atomic::AtomicU64>,
pub successful_retries: Arc<std::sync::atomic::AtomicU64>,
pub failed_retries: Arc<std::sync::atomic::AtomicU64>,
}
impl RetryStats {
pub fn total_retries(&self) -> u64 {
self.total_retries
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn successful_retries(&self) -> u64 {
self.successful_retries
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn failed_retries(&self) -> u64 {
self.failed_retries
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn record_success(&self) {
self.total_retries
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.successful_retries
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub fn record_failure(&self) {
self.total_retries
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.failed_retries
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub fn reset(&self) {
self.total_retries
.store(0, std::sync::atomic::Ordering::Relaxed);
self.successful_retries
.store(0, std::sync::atomic::Ordering::Relaxed);
self.failed_retries
.store(0, std::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(feature = "redis")]
#[derive(Clone)]
pub struct RedisStorage {
conn_manager: Arc<Mutex<Option<ConnectionManager>>>,
config: RedisConfig,
lua_manager: Option<Arc<LuaScriptManager>>,
retry_stats: RetryStats,
degraded: Arc<Mutex<bool>>,
last_degraded_at: Arc<Mutex<Option<Instant>>>,
}
impl RedisStorage {
pub async fn new(config: RedisConfig) -> Result<Self, StorageError> {
info!("创建Redis存储, URL: {}", config.url);
let lua_manager = if config.enable_lua {
Some(Arc::new(LuaScriptManager::new()))
} else {
None
};
let storage = Self {
conn_manager: Arc::new(Mutex::new(None)),
config,
lua_manager,
retry_stats: RetryStats::default(),
degraded: Arc::new(Mutex::new(false)),
last_degraded_at: Arc::new(Mutex::new(None)),
};
storage.connect().await?;
if let Some(lua_manager) = &storage.lua_manager {
if let Some(conn_manager) = storage.conn_manager.lock().await.as_ref() {
let mut conn = conn_manager.clone();
lua_manager.preload_all_scripts(&mut conn).await?;
}
}
info!("Redis存储创建成功");
Ok(storage)
}
pub async fn ping(&self) -> Result<(), StorageError> {
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
let _: String = redis::cmd("PING")
.query_async(&mut conn)
.await
.map_err(|e| {
error!("Redis PING失败: {}", e);
StorageError::QueryError(format!("PING失败: {}", e))
})?;
Ok(())
})
.await
}
async fn connect(&self) -> Result<(), StorageError> {
debug!("建立Redis连接");
let client_info = if let Some(password) = &self.config.password {
let url = self.config.url.trim_start_matches("redis://");
let url = url.trim_start_matches("rediss://");
let url = if let Some(at_pos) = url.find('@') {
&url[at_pos + 1..]
} else {
url
};
let (host, port) = if let Some(colon_pos) = url.rfind(':') {
let host = &url[..colon_pos];
let port = url[colon_pos + 1..].parse::<u16>().unwrap_or(6379);
(host.to_string(), port)
} else {
(url.to_string(), 6379)
};
let addr = redis::ConnectionAddr::Tcp(host, port);
redis::ConnectionInfo {
addr,
redis: redis::RedisConnectionInfo {
db: self.config.db,
username: None,
password: Some(password.expose_secret().clone()),
},
}
} else {
let url = self.config.url.trim_start_matches("redis://");
let url = url.trim_start_matches("rediss://");
let url = if let Some(at_pos) = url.find('@') {
&url[at_pos + 1..]
} else {
url
};
let (host, port) = if let Some(colon_pos) = url.rfind(':') {
let host = &url[..colon_pos];
let port = url[colon_pos + 1..].parse::<u16>().unwrap_or(6379);
(host.to_string(), port)
} else {
(url.to_string(), 6379)
};
let addr = redis::ConnectionAddr::Tcp(host, port);
redis::ConnectionInfo {
addr,
redis: redis::RedisConnectionInfo {
db: self.config.db,
username: None,
password: None,
},
}
};
let client = Client::open(client_info).map_err(|e| {
error!("创建Redis客户端失败: {}", e);
StorageError::ConnectionError(format!("创建Redis客户端失败: {}", e))
})?;
let conn_manager = ConnectionManager::new(client).await.map_err(|e| {
error!("创建Redis连接管理器失败: {}", e);
StorageError::ConnectionError(format!("创建Redis连接管理器失败: {}", e))
})?;
*self.conn_manager.lock().await = Some(conn_manager);
*self.degraded.lock().await = false;
info!("Redis连接建立成功");
Ok(())
}
async fn execute_with_retry<F, Fut, T>(&self, f: F) -> Result<T, StorageError>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<T, StorageError>>,
{
let mut last_error = None;
let mut backoff = self.config.retry_initial_backoff;
for attempt in 0..=self.config.max_retries {
match f().await {
Ok(result) => {
if attempt > 0 {
self.retry_stats.record_success();
debug!("重试成功,尝试次数: {}", attempt);
}
return Ok(result);
}
Err(e) => {
last_error = Some(e.clone());
if attempt < self.config.max_retries {
warn!(
"操作失败,将在 {:?} 后重试 (尝试 {}/{}): {}",
backoff,
attempt + 1,
self.config.max_retries,
e
);
tokio::time::sleep(backoff).await;
backoff = backoff.mul_f32(2.0);
if matches!(e, StorageError::ConnectionError(_)) {
if let Err(reconnect_err) = self.reconnect().await {
error!("重新连接失败: {}", reconnect_err);
}
}
}
}
}
}
self.retry_stats.record_failure();
error!("操作失败,已达最大重试次数: {:?}", last_error);
if matches!(last_error, Some(StorageError::ConnectionError(_))) {
self.set_degraded(true).await;
}
Err(last_error.unwrap_or(StorageError::TimeoutError("操作超时".to_string())))
}
async fn reconnect(&self) -> Result<(), StorageError> {
debug!("尝试重新连接Redis");
*self.conn_manager.lock().await = None;
self.connect().await
}
async fn set_degraded(&self, degraded: bool) {
let current = *self.degraded.lock().await;
if current != degraded {
*self.degraded.lock().await = degraded;
if degraded {
*self.last_degraded_at.lock().await = Some(Instant::now());
warn!("Redis存储已降级,将使用备用存储");
} else {
info!("Redis存储已恢复正常");
}
}
}
pub async fn is_degraded(&self) -> bool {
*self.degraded.lock().await
}
pub fn retry_stats(&self) -> &RetryStats {
&self.retry_stats
}
pub fn lua_manager(&self) -> Option<&Arc<LuaScriptManager>> {
self.lua_manager.as_ref()
}
pub async fn sliding_window(
&self,
key: &str,
window_size: Duration,
max_requests: u64,
) -> Result<(bool, u64, i64), StorageError> {
let lua_manager = self
.lua_manager
.as_ref()
.ok_or_else(|| StorageError::QueryError("Lua脚本未启用".to_string()))?;
let current_timestamp = chrono::Utc::now().timestamp_millis();
let window_size_ms = window_size.as_millis() as i64;
let result: (i32, i64, i64) = self
.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
lua_manager
.execute_script(
&mut conn,
LuaScriptType::SlidingWindow,
&[key],
&[
&window_size_ms.to_string(),
&max_requests.to_string(),
¤t_timestamp.to_string(),
],
)
.await
})
.await?;
let allowed = result.0 == 1;
let current_count = result.1 as u64;
let reset_time = result.2;
Ok((allowed, current_count, reset_time))
}
pub async fn fixed_window(
&self,
key: &str,
window_size: Duration,
max_requests: u64,
) -> Result<(bool, u64, i64), StorageError> {
let lua_manager = self
.lua_manager
.as_ref()
.ok_or_else(|| StorageError::QueryError("Lua脚本未启用".to_string()))?;
let current_timestamp = chrono::Utc::now().timestamp_millis();
let window_size_ms = window_size.as_millis() as i64;
let result: (i32, i64, i64) = self
.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
lua_manager
.execute_script(
&mut conn,
LuaScriptType::FixedWindow,
&[key],
&[
&window_size_ms.to_string(),
&max_requests.to_string(),
¤t_timestamp.to_string(),
],
)
.await
})
.await?;
let allowed = result.0 == 1;
let current_count = result.1 as u64;
let reset_time = result.2;
Ok((allowed, current_count, reset_time))
}
pub async fn token_bucket(
&self,
key: &str,
capacity: u64,
refill_rate: u64, tokens_requested: u64,
) -> Result<(bool, u64, i64), StorageError> {
let lua_manager = self
.lua_manager
.as_ref()
.ok_or_else(|| StorageError::QueryError("Lua脚本未启用".to_string()))?;
let current_timestamp = chrono::Utc::now().timestamp_millis();
let refill_rate_ms = refill_rate as f64 / 1000.0;
let result: (i32, i64, i64) = self
.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
lua_manager
.execute_script(
&mut conn,
LuaScriptType::TokenBucket,
&[key],
&[
&capacity.to_string(),
&refill_rate_ms.to_string(),
¤t_timestamp.to_string(),
&tokens_requested.to_string(),
],
)
.await
})
.await?;
let allowed = result.0 == 1;
let tokens_remaining = result.1 as u64;
let refill_time = result.2;
Ok((allowed, tokens_remaining, refill_time))
}
fn quota_key(user_id: &str, _resource: &str) -> String {
format!("quota:{}", user_id)
}
fn quota_field(resource: &str, field: &str) -> String {
format!("{}_{}", resource, field)
}
fn ban_key(target: &BanTarget) -> String {
let key = match target {
BanTarget::Ip(ip) => {
let sanitized_ip = sanitize_key_component(ip);
format!("ban:ip:{}", sanitized_ip)
}
BanTarget::UserId(user_id) => {
let sanitized_user_id = sanitize_key_component(user_id);
format!("ban:user:{}", sanitized_user_id)
}
BanTarget::Mac(mac) => {
let sanitized_mac = sanitize_key_component(mac);
format!("ban:mac:{}", sanitized_mac)
}
};
if let Err(e) = validate_key(&key) {
error!("无效的封禁键: {}", e);
return "ban:invalid".to_string();
}
key
}
fn ban_history_key(target: &BanTarget) -> String {
let base_key = Self::ban_key(target);
let key = format!("{}:history", base_key);
if let Err(e) = validate_key(&key) {
error!("无效的封禁历史键: {}", e);
return "ban:invalid:history".to_string();
}
key
}
}
#[async_trait]
impl Storage for RedisStorage {
async fn get(&self, key: &str) -> Result<Option<String>, StorageError> {
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
let result: Option<String> = conn.get(key).await.map_err(|e| {
error!("Redis GET失败: {}", e);
StorageError::QueryError(format!("GET失败: {}", e))
})?;
trace!("GET key={}, result={:?}", key, result);
Ok(result)
})
.await
}
async fn set(&self, key: &str, value: &str, ttl: Option<u64>) -> Result<(), StorageError> {
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
if let Some(ttl) = ttl {
let _: () = conn.set_ex(key, value, ttl).await.map_err(|e| {
error!("Redis SETEX失败: {}", e);
StorageError::QueryError(format!("SETEX失败: {}", e))
})?;
} else {
let _: () = conn.set(key, value).await.map_err(|e| {
error!("Redis SET失败: {}", e);
StorageError::QueryError(format!("SET失败: {}", e))
})?;
}
trace!("SET key={}, value={:?}, ttl={:?}", key, value, ttl);
Ok(())
})
.await
}
async fn delete(&self, key: &str) -> Result<(), StorageError> {
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
let _: () = conn.del(key).await.map_err(|e| {
error!("Redis DEL失败: {}", e);
StorageError::QueryError(format!("DEL失败: {}", e))
})?;
trace!("DEL key={}", key);
Ok(())
})
.await
}
}
#[async_trait]
impl QuotaStorage for RedisStorage {
async fn get_quota(
&self,
user_id: &str,
resource: &str,
) -> Result<Option<QuotaInfo>, StorageError> {
let key = Self::quota_key(user_id, resource);
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
let consumed_field = Self::quota_field(resource, "consumed");
let limit_field = Self::quota_field(resource, "limit");
let window_start_field = Self::quota_field(resource, "window_start");
let window_end_field = Self::quota_field(resource, "window_end");
let result: Vec<Option<String>> = redis::cmd("HMGET")
.arg(&key)
.arg(&consumed_field)
.arg(&limit_field)
.arg(&window_start_field)
.arg(&window_end_field)
.query_async(&mut conn)
.await
.map_err(|e| {
error!("Redis HMGET失败: {}", e);
StorageError::QueryError(format!("HMGET失败: {}", e))
})?;
let consumed = result[0].as_ref().and_then(|s| s.parse::<u64>().ok());
let limit = result[1].as_ref().and_then(|s| s.parse::<u64>().ok());
let window_start = result[2].as_ref().and_then(|s| s.parse::<i64>().ok());
let window_end = result[3].as_ref().and_then(|s| s.parse::<i64>().ok());
if let (Some(consumed), Some(limit), Some(window_start), Some(window_end)) =
(consumed, limit, window_start, window_end)
{
let quota_info = QuotaInfo {
consumed,
limit,
window_start: chrono::DateTime::from_timestamp(window_start / 1000, 0)
.unwrap_or_else(chrono::Utc::now),
window_end: chrono::DateTime::from_timestamp(window_end / 1000, 0)
.unwrap_or_else(chrono::Utc::now),
};
Ok(Some(quota_info))
} else {
Ok(None)
}
})
.await
}
async fn consume(
&self,
user_id: &str,
resource: &str,
cost: u64,
limit: u64,
window: std::time::Duration,
) -> Result<ConsumeResult, StorageError> {
let lua_manager = self
.lua_manager
.as_ref()
.ok_or_else(|| StorageError::QueryError("Lua脚本未启用".to_string()))?;
let key = Self::quota_key(user_id, resource);
let overdraft_limit = 0u64;
let now = chrono::Utc::now();
let window_start = now.timestamp_millis();
let window_end = window_start
+ i64::try_from(window.as_millis())
.map_err(|_| StorageError::QueryError("window duration overflow".to_string()))?;
let consumed_field = Self::quota_field(resource, "consumed");
let limit_field = Self::quota_field(resource, "limit");
let window_start_field = Self::quota_field(resource, "window_start");
let window_end_field = Self::quota_field(resource, "window_end");
let result: (i32, i64, i64) = self
.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
lua_manager
.execute_script(
&mut conn,
LuaScriptType::QuotaConsume,
&[&key],
&[
&cost.to_string(),
&limit.to_string(),
&overdraft_limit.to_string(),
&window_start.to_string(),
&window_end.to_string(),
&consumed_field,
&limit_field,
&window_start_field,
&window_end_field,
],
)
.await
})
.await?;
let allowed = result.0 == 1;
let remaining = result.1 as u64;
let consumed = result.2 as u64;
let alert_triggered = remaining < (limit / 10);
if alert_triggered {
warn!(
"配额告警: user_id={}, resource={}, remaining={}, consumed={}",
user_id, resource, remaining, consumed
);
}
Ok(ConsumeResult {
allowed,
remaining,
alert_triggered,
})
}
async fn reset(
&self,
user_id: &str,
resource: &str,
_limit: u64,
_window: std::time::Duration,
) -> Result<(), StorageError> {
let key = Self::quota_key(user_id, resource);
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
let _: () = conn.del(&key).await.map_err(|e| {
error!("Redis DEL失败: {}", e);
StorageError::QueryError(format!("DEL失败: {}", e))
})?;
debug!("配额已重置: user_id={}, resource={}", user_id, resource);
Ok(())
})
.await
}
}
#[async_trait]
impl BanStorage for RedisStorage {
async fn is_banned(&self, target: &BanTarget) -> Result<Option<BanRecord>, StorageError> {
let key = Self::ban_key(target);
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
let exists: bool = conn.exists(&key).await.map_err(|e| {
error!("Redis EXISTS失败: {}", e);
StorageError::QueryError(format!("EXISTS失败: {}", e))
})?;
if !exists {
return Ok(None);
}
let ban_times: u32 = conn.hget(&key, "ban_times").await.unwrap_or(0);
let duration_ms: i64 = conn.hget(&key, "duration").await.unwrap_or(0);
let banned_at: i64 = conn.hget(&key, "banned_at").await.unwrap_or(0);
let expires_at: i64 = conn.hget(&key, "expires_at").await.unwrap_or(0);
let is_manual: bool = conn.hget(&key, "is_manual").await.unwrap_or(false);
let reason: String = conn.hget(&key, "reason").await.unwrap_or_default();
let now = chrono::Utc::now().timestamp_millis();
if now > expires_at {
let _: () = conn.del(&key).await.map_err(|e| {
error!("Redis DEL失败: {}", e);
StorageError::QueryError(format!("DEL失败: {}", e))
})?;
return Ok(None);
}
let record = BanRecord {
target: target.clone(),
ban_times,
duration: Duration::from_millis(duration_ms as u64),
banned_at: chrono::DateTime::from_timestamp(banned_at / 1000, 0)
.unwrap_or_else(chrono::Utc::now),
expires_at: chrono::DateTime::from_timestamp(expires_at / 1000, 0)
.unwrap_or_else(chrono::Utc::now),
is_manual,
reason,
};
debug!(
"检查封禁: target={}, is_banned=true",
format!("{:?}", target)
);
Ok(Some(record))
})
.await
}
async fn save(&self, record: &BanRecord) -> Result<(), StorageError> {
let key = Self::ban_key(&record.target);
let history_key = Self::ban_history_key(&record.target);
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
let _: () = conn
.hset(&key, "ban_times", record.ban_times)
.await
.map_err(|e| {
error!("Redis HSET失败: {}", e);
StorageError::QueryError(format!("HSET失败: {}", e))
})?;
let _: () = conn
.hset(&key, "duration", record.duration.as_millis() as i64)
.await
.map_err(|e| {
error!("Redis HSET失败: {}", e);
StorageError::QueryError(format!("HSET失败: {}", e))
})?;
let _: () = conn
.hset(&key, "banned_at", record.banned_at.timestamp_millis())
.await
.map_err(|e| {
error!("Redis HSET失败: {}", e);
StorageError::QueryError(format!("HSET失败: {}", e))
})?;
let _: () = conn
.hset(&key, "expires_at", record.expires_at.timestamp_millis())
.await
.map_err(|e| {
error!("Redis HSET失败: {}", e);
StorageError::QueryError(format!("HSET失败: {}", e))
})?;
let _: () = conn
.hset(&key, "is_manual", record.is_manual)
.await
.map_err(|e| {
error!("Redis HSET失败: {}", e);
StorageError::QueryError(format!("HSET失败: {}", e))
})?;
let _: () = conn
.hset(&key, "reason", &record.reason)
.await
.map_err(|e| {
error!("Redis HSET失败: {}", e);
StorageError::QueryError(format!("HSET失败: {}", e))
})?;
let ttl = (record.expires_at - chrono::Utc::now()).num_seconds();
if ttl > 0 {
let _: () = conn.expire(&key, ttl).await.map_err(|e| {
error!("Redis EXPIRE失败: {}", e);
StorageError::QueryError(format!("EXPIRE失败: {}", e))
})?;
}
let _: () = conn
.hset(&history_key, "ban_times", record.ban_times)
.await
.map_err(|e| {
error!("Redis HSET失败: {}", e);
StorageError::QueryError(format!("HSET失败: {}", e))
})?;
let _: () = conn
.hset(
&history_key,
"last_banned_at",
record.banned_at.timestamp_millis(),
)
.await
.map_err(|e| {
error!("Redis HSET失败: {}", e);
StorageError::QueryError(format!("HSET失败: {}", e))
})?;
debug!("保存封禁记录: target={:?}", record.target);
Ok(())
})
.await
}
async fn get_history(
&self,
target: &BanTarget,
) -> Result<Option<crate::storage::BanHistory>, StorageError> {
let history_key = Self::ban_history_key(target);
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
let exists: bool = conn.exists(&history_key).await.map_err(|e| {
error!("Redis EXISTS失败: {}", e);
StorageError::QueryError(format!("EXISTS失败: {}", e))
})?;
if !exists {
return Ok(None);
}
let ban_times: u32 = conn.hget(&history_key, "ban_times").await.unwrap_or(0);
let last_banned_at: i64 = conn.hget(&history_key, "last_banned_at").await.unwrap_or(0);
let history = crate::storage::BanHistory {
ban_times,
last_banned_at: chrono::DateTime::from_timestamp(last_banned_at / 1000, 0)
.unwrap_or_else(chrono::Utc::now),
};
Ok(Some(history))
})
.await
}
async fn increment_ban_times(&self, target: &BanTarget) -> Result<u64, StorageError> {
let key = Self::ban_history_key(target);
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
let ban_times: u64 = conn.hincr(&key, "ban_times", 1).await.map_err(|e| {
error!("Redis HINCR失败: {}", e);
StorageError::QueryError(format!("HINCR失败: {}", e))
})?;
let _: () = conn
.hset(
&key,
"last_banned_at",
chrono::Utc::now().timestamp_millis(),
)
.await
.map_err(|e| {
error!("Redis HSET失败: {}", e);
StorageError::QueryError(format!("HSET失败: {}", e))
})?;
Ok(ban_times)
})
.await
}
async fn get_ban_times(&self, target: &BanTarget) -> Result<u64, StorageError> {
let key = Self::ban_history_key(target);
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
let ban_times: u64 = conn.hget(&key, "ban_times").await.unwrap_or(0);
Ok(ban_times)
})
.await
}
async fn remove_ban(&self, target: &BanTarget) -> Result<(), StorageError> {
let key = Self::ban_key(target);
self.execute_with_retry(|| async {
let conn_manager = self.conn_manager.lock().await;
let conn_manager = conn_manager
.as_ref()
.ok_or_else(|| StorageError::ConnectionError("连接未初始化".to_string()))?;
let mut conn = conn_manager.clone();
let _: i64 = conn.del(&key).await.map_err(|e| {
error!("Redis DEL失败: {}", e);
StorageError::QueryError(format!("DEL失败: {}", e))
})?;
Ok(())
})
.await
}
async fn cleanup_expired_bans(&self) -> Result<u64, StorageError> {
Ok(0)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redis_config_default() {
let config = RedisConfig::default();
assert_eq!(config.url, "redis://127.0.0.1:6379");
assert_eq!(config.db, 0);
assert_eq!(config.max_retries, 3);
assert!(!config.cluster_mode);
assert!(config.enable_lua);
}
#[test]
fn test_redis_config_builder() {
let config = RedisConfig::new("redis://localhost:6379")
.db(1)
.password("password")
.connection_timeout(Duration::from_secs(10))
.io_timeout(Duration::from_secs(10))
.max_retries(5)
.cluster_mode(true)
.pool_size(20)
.enable_lua(false);
assert_eq!(config.url, "redis://localhost:6379");
assert_eq!(config.db, 1);
assert_eq!(
config.password.as_ref().map(|p| p.expose_secret()),
Some(&"password".to_string())
);
assert_eq!(config.connection_timeout, Duration::from_secs(10));
assert_eq!(config.io_timeout, Duration::from_secs(10));
assert_eq!(config.max_retries, 5);
assert!(config.cluster_mode);
assert_eq!(config.pool_size, 20);
assert!(!config.enable_lua);
}
#[test]
fn test_quota_key() {
let key = RedisStorage::quota_key("user1", "api");
assert_eq!(key, "quota:user1");
}
#[test]
fn test_ban_key() {
let key = RedisStorage::ban_key(&BanTarget::Ip("192.168.1.1".to_string()));
assert_eq!(key, "ban:ip:192.168.1.1");
let key = RedisStorage::ban_key(&BanTarget::UserId("user1".to_string()));
assert_eq!(key, "ban:user:user1");
let key = RedisStorage::ban_key(&BanTarget::Mac("00:11:22:33:44:55".to_string()));
assert_eq!(key, "ban:mac:001122334455");
}
#[test]
fn test_ban_history_key() {
let key = RedisStorage::ban_history_key(&BanTarget::UserId("user1".to_string()));
assert_eq!(key, "ban:user:user1:history");
}
#[test]
fn test_retry_stats() {
let stats = RetryStats::default();
assert_eq!(stats.total_retries(), 0);
assert_eq!(stats.successful_retries(), 0);
assert_eq!(stats.failed_retries(), 0);
stats.record_success();
assert_eq!(stats.total_retries(), 1);
assert_eq!(stats.successful_retries(), 1);
stats.record_failure();
assert_eq!(stats.total_retries(), 2);
assert_eq!(stats.failed_retries(), 1);
stats.reset();
assert_eq!(stats.total_retries(), 0);
}
#[tokio::test]
async fn test_degraded_state() {
let config = RedisConfig::new("redis://invalid:6379");
let storage = RedisStorage {
conn_manager: Arc::new(Mutex::new(None)),
config,
lua_manager: None,
retry_stats: RetryStats::default(),
degraded: Arc::new(Mutex::new(false)),
last_degraded_at: Arc::new(Mutex::new(None)),
};
assert!(!storage.is_degraded().await);
storage.set_degraded(true).await;
assert!(storage.is_degraded().await);
storage.set_degraded(false).await;
assert!(!storage.is_degraded().await);
}
}