مقدمة: لماذا التخزين المؤقت في Rust هو تحدٍ وميزة
\nفي الأنظمة عالية التحميل بلغة Rust، لم يعد التخزين المؤقت مجرد إضافة مكتبة. بسبب نظام الملكية الصارم، وغياب جامع القمامة، ومتطلبات أمان الخيوط، تتطلب الأساليب التقليدية إعادة تفكير. ومع ذلك، تمنح Rust إمكانيات فريدة: وقت استجابة يمكن التنبؤ به، وتكاليف إضافية صفرية لـ GC، وتحكم كامل في الذاكرة. في هذه المقالة، سنستعرض تقنيات التخزين المؤقت المتقدمة: من الحلول الموزعة (Redis، Memcached) إلى التخزين المؤقت المدمج (moka، lru) ومجموعاتها.
\n\n1. العمل غير المتزامن مع Redis عبر deadpool-redis
\n1.1. لماذا ليس redis-rs مباشرة؟
\nعميل redis-rs متزامن. للتطبيقات غير المتزامنة على tokio، يلزم وجود مجموعة اتصالات. يوفر deadpool-redis مجموعة اتصالات غير متزامنة تدير الاتصالات وتعيد الاتصال تلقائيًا عند الأعطال.
1.2. مثال: التخزين المؤقت مع TTL والتسلسل
\nuse deadpool_redis::{Config, Runtime, Pool};\nuse serde::{Serialize, Deserialize};\nuse tokio::sync::OnceCell;\n\n#[derive(Serialize, Deserialize, Clone)]\nstruct User {\n id: u64,\n name: String,\n email: String,\n}\n\nasync fn get_pool() -> Pool {\n static POOL: OnceCell<Pool> = OnceCell::const_new();\n POOL.get_or_init(|| async {\n let cfg = Config::from_url("redis://127.0.0.1/");\n cfg.create_pool(Some(Runtime::Tokio1)).unwrap()\n }).await.clone()\n}\n\nasync fn get_user_cached(user_id: u64) -> Option<User> {\n let pool = get_pool().await;\n let mut conn = pool.get().await.unwrap();\n let key = format!("user:{}", user_id);\n\n // محاولة القراءة من التخزين المؤقت\n if let Ok(Some(data)) = redis::cmd("GET").arg(&key).query_async::<Option<Vec<u8>>>(&mut conn).await {\n if let Ok(user) = bincode::deserialize::<User>(&data) {\n return Some(user);\n }\n }\n None\n}\n\nasync fn set_user_cache(user: &User) {\n let pool = get_pool().await;\n let mut conn = pool.get().await.unwrap();\n let key = format!("user:{}", user.id);\n let data = bincode::serialize(user).unwrap();\n\n // TTL 300 ثانية\n let _: () = redis::cmd("SETEX")\n .arg(&key)\n .arg(300)\n .arg(&data)\n .query_async(&mut conn)\n .await\n .unwrap();\n}\n\n1.3. استراتيجية متقدمة: Cache-Aside مع الأقفال
\nعند حدوث cache stampede (عندما تحاول طلبات متعددة إعادة ملء التخزين المؤقت في وقت واحد)، استخدم كائن المزامنة (mutex) على مستوى التطبيق:
\nuse tokio::sync::Mutex;\nuse std::collections::HashMap;\n\nstruct CacheLock {\n locks: Mutex<HashMap<String, tokio::sync::Semaphore>>,\n}\n\nimpl CacheLock {\n async fn get_or_compute<F, Fut, T>(&self, key: &str, f: F) -> T\n where\n F: FnOnce() -> Fut,\n Fut: Future<Output = T>,\n {\n let sem = {\n let mut map = self.locks.lock().await;\n map.entry(key.to_string())\n .or_insert_with(|| tokio::sync::Semaphore::new(1))\n .clone()\n };\n let _permit = sem.acquire().await.unwrap();\n f().await\n }\n}\n\n2. Memcached: الخفة والسرعة مع memcache-async
\n2.1. ميزات Memcached لـ Rust
\nMemcached أبسط من Redis: لا يوجد استمرارية أو هياكل بيانات معقدة، ولكنه أسرع لعمليات key-value البسيطة. يوفر كريت memcache-async عميلاً غير متزامن على tokio.
2.2. مثال: التحميل المجمع وخط الأنابيب
\nuse memcache_async::{Client, Protocol, Value};\nuse bytes::Bytes;\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n let client = Client::connect_async("memcache://127.0.0.1:11211").await?;\n\n // P