404 Not Found

404 Not Found


nginx

آلية التخزين المؤقت

التخزين المؤقت هو الخطوة الأولى في تحسين الأداء—للبيانات التي تُقرأ بكثرة لكن تُكتب نادرًا، تخزينها مرة واحدة يوفر عليك ألف استعلام.

1. ما ستتعلمه


2. قصة حقيقية لمهندس أداء

(1) نقطة الألم: قاعدة البيانات مثقلة

في إطلاق المنتج، عرض Charlie ميزة البيع السريع لـ OrderFlow، مما أطلق فورًا 500 ألف طلب—كلها أغرقت قاعدة البيانات باستعلامات المنتجات الشائعة. نُفد تجمع اتصالات MySQL، مما تسبب في تعطل النظام بالكامل لمدة 10 دقائق وخسارة مباشرة بقيمة 100 ألف دولار. كشف تحليل Alice أن استعلامات تفاصيل المنتج شكلت 80% من إجمالي الطلبات، ومع ذلك ظلت هذه البيانات دون تغيير تقريبًا لساعة كاملة.

(2) حل Spring Cache

يمكنك تفعيل التخزين المؤقت بتعليق توضيحي واحد:

JAVA
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
    return productRepository.findById(id).orElseThrow();
}

الاستعلام الأول يصل قاعدة البيانات؛ الاستعلامات اللاحقة تقرأ مباشرة من التخزين المؤقت.

(3) العائد

بعد أن أضافت Alice تخزين Caffeine المؤقت للمنتجات الشائعة، 80% من الاستعلامات أصابت التخزين المؤقت، وانخفض حمل قاعدة البيانات بمعامل 5، وانخفض تأخير P99 من 500 مللي ثانية إلى 5 مللي ثانية، مما دعم بنجاح البيع السريع التالي.


3. نظام تعليقات Spring Cache

(1) التعليقات الأساسية

الوصف الوظيفة السيناريوهات النموذجية
@Cacheable فحص التخزين المؤقت أولاً عند الاستعلام؛ إذا وُجد تطابق، أعد النتيجة فورًا عمليات الاستعلام
@CachePut تنفيذ الطريقة وتحديث التخزين المؤقت عمليات التحديث
@CacheEvict مسح التخزين المؤقت الحذف
@Caching دمج عمليات تخزين مؤقت متعددة سيناريوهات معقدة
@EnableCaching تفعيل دعم التخزين المؤقت فئة التكوين
100%
graph TD
    A["طلب العميل"] --> B{"@Cacheable<br/>إصابة التخزين المؤقت؟"}
    B -->|نعم| C["إرجاع البيانات المخزنة مؤقتًا"]
    B -->|لا| D["تنفيذ الطريقة<br/>استعلام قاعدة البيانات"]
    D --> E["تخزين النتيجة في التخزين المؤقت"]
    E --> F["إرجاع النتيجة"]

(1) ▶ مثال:تخزين استعلام @Cacheable المؤقت

JAVA
@Service
@EnableCaching
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Cacheable(value = "products", key = "#id")
    public Product getProduct(Long id) {
        log.info("إخفاق التخزين المؤقت، استعلام قاعدة البيانات عن المنتج {}", id);
        return productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product", id));
    }

    @Cacheable(value = "product-list", key = "#keyword + '-' + #page + '-' + #size")
    public Page<Product> searchProducts(String keyword, int page, int size) {
        return productRepository.findByNameContaining(keyword,
            PageRequest.of(page, size));
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

(2) ▶ مثال:تحديث التخزين المؤقت @CachePut

JAVA
@CachePut(value = "products", key = "#result.id")
public Product updateProduct(Long id, UpdateProductRequest request) {
    Product product = getProduct(id);
    product.setName(request.name());
    product.setPrice(request.price());
    product.setStock(request.stock());
    return productRepository.save(product);
}

الناتج:

TEXT
// تم التنفيذ بنجاح

(3) ▶ مثال:مسح التخزين المؤقت @CacheEvict

JAVA
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
    productRepository.deleteById(id);
}

@CacheEvict(value = "product-list", allEntries = true)
public void clearProductListCache() {
    log.info("تم مسح جميع إدخالات تخزين قائمة المنتجات المؤقت");
}

الناتج:

TEXT
// تم التنفيذ بنجاح
التعليق تنفيذ الطريقة تحديث التخزين المؤقت العمليات المطبقة
@Cacheable تنفيذ عند إخفاق التخزين المؤقت تحديث عند إخفاق التخزين المؤقت الاستعلام
@CachePut تنفيذ دائمًا تحديث دائمًا التحديث
@CacheEvict تنفيذ دائمًا مسح التخزين المؤقت المحدد الحذف

4. استراتيجية مفتاح التخزين المؤقت

(1) قواعد توليد المفتاح

استراتيجية المفتاح تعبير SpEL مثال مُولّد
معامل واحد key = "#id" products::123
تركيبة متعددة المعاملات key = "#keyword + '-' + #page" product-list::laptop-0
خاصية كائن key = "#request.category" products::electronics
الافتراضي غير محدد hashCode لتركيبة جميع المعاملات

(1) ▶ مثال:تخصيص KeyGenerator

JAVA
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean("customKeyGenerator")
    public KeyGenerator customKeyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getSimpleName()).append(":");
            sb.append(method.getName()).append(":");
            for (Object param : params) {
                sb.append(param != null ? param.toString() : "null").append(".");
            }
            return sb.toString();
        };
    }
}

// الاستخدام
@Cacheable(value = "products", keyGenerator = "customKeyGenerator")
public Product getProduct(Long id) { /* ... */ }

الناتج:

TEXT
// تم التنفيذ بنجاح

(2) التصفية الشرطية

(2) ▶ مثال:condition وunless

JAVA
// تخزين المنتجات ذات السعر > 100 فقط مؤقتًا
@Cacheable(value = "expensive-products", key = "#id",
    condition = "#id != null")
public Product getExpensiveProduct(Long id) { /* ... */ }

// عدم التخزين مؤقتًا إذا كانت النتيجة مخزونها == 0
@Cacheable(value = "products", key = "#id",
    unless = "#result.stock == 0")
public Product getProduct(Long id) { /* ... */ }

الناتج:

TEXT
// تم التنفيذ بنجاح
المعامل توقيت التنفيذ المعنى
condition قبل تنفيذ الطريقة تخزين مؤقت فقط إذا تحققت الشروط
unless بعد تنفيذ الطريقة إذا تحقق الشرط، لا تخزن مؤقتًا

5. تكوين التخزين المؤقت المحلي Caffeine

(1) ميزات Caffeine

الميزة الوصف
أداء عالٍ إنتاجية أعلى بنسبة 30% من Guava Cache
تحديث غير متزامن يدعم تحديث التخزين المؤقت المنتهي في الخلفية بشكل غير متزامن
إحصائيات معدل الإصابة، وقت التحميل، إلخ
Window TinyLFU خوارزمية إخلاء مثالية

(1) ▶ مثال:تكوين تخزين Caffeine المؤقت

XML
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

الناتج:

TEXT
// تم التنفيذ بنجاح
JAVA
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(caffeineCacheBuilder());
        return manager;
    }

    @Bean
    public Caffeine<Object, Object> caffeineCacheBuilder() {
        return Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(30))   // TTL: 30 دقيقة بعد الكتابة
            .expireAfterAccess(Duration.ofHours(1))      // إخلاء بعد ساعة من الخمول
            .maximumSize(10_000)                         // حد أقصى 10,000 إدخال
            .recordStats();                              // تفعيل الإحصائيات
    }

    // تكوين لكل تخزين مؤقت
    @Bean
    public CacheManager multiCacheManager() {
        Map<String, CaffeineCache> caches = Map.of(
            "products", buildCache(Caffeine.newBuilder()
                .expireAfterWrite(Duration.ofHours(1))
                .maximumSize(5_000)),
            "product-list", buildCache(Caffeine.newBuilder()
                .expireAfterWrite(Duration.ofMinutes(10))
                .maximumSize(1_000))
        );
        return new SimpleCacheManager() {{
            setCaches(caches.values());
        }};
    }

    private CaffeineCache buildCache(Caffeine<Object, Object> builder) {
        return new CaffeineCache("cache", builder.build());
    }
}
خيار التكوين الوصف القيمة الموصى بها
expireAfterWrite وقت الانتهاء بعد الكتابة 30 دقيقة - 1 ساعة
expireAfterAccess وقت الانتهاء بعد آخر وصول 1 ساعة - 24 ساعة
maximumSize الحد الأقصى لإدخالات التخزين المؤقت تعديل حسب الذاكرة
recordStats تفعيل الإحصائيات تفعيل في بيئة التطوير

6. اختراق التخزين المؤقت واختراقه

(1) مشكلات التخزين المؤقت الشائعة

المشكلة السبب العاقبة الحل
اختراق التخزين المؤقت الاستعلام عن بيانات غير موجودة استعلام قاعدة البيانات في كل مرة تخزين القيم الفارغة / مرشحات بلوم
اختراق التخزين المؤقت تدفق طلبات في لحظة انتهاء مفتاح ساخن ارتفاع مفاجئ في حمل قاعدة البيانات قفل تبادلي / عدم انتهاء أبدًا + تحديث غير متزامن
انهيار التخزين المؤقت انتهاء صلاحية عدد هائل من المفاتيح في وقت واحد زيادة مفاجئة في حمل قاعدة البيانات إضافة إزاحة عشوائية لوقت الانتهاء
100%
graph TD
    A["اختراق التخزين المؤقت<br/>استعلام بيانات غير موجودة"] --> B["الحل: تخزين القيمة الفارغة"]
    C["اختراق التخزين المؤقت<br/>انتهاء مفتاح ساخن"] --> D["الحل: قفل تبادلي / تحديث غير متزامن"]
    E["انهيار التخزين المؤقت<br/>انتهاء العديد من المفاتيح دفعة واحدة"] --> F["الحل: إزاحة عشوائية لـ TTL"]

(1) ▶ مثال:تخزين القيم الفارغة لمنع الاختراق

JAVA
@Cacheable(value = "products", key = "#id", unless = "#result == null && #result != ''")
public Product getProduct(Long id) {
    return productRepository.findById(id).orElse(null);
}

// بديل: تخزين قيمة فارغة مع TTL قصير
@Bean
public CacheManager cacheManager() {
    return new CaffeineCacheManager() {{
        setCaffeine(Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(5))
            .maximumSize(10_000));
    }};
}

الناتج:

TEXT
// تم التنفيذ بنجاح

7. مثال شامل: تنفيذ كامل لتخزين منتجات OrderFlow المؤقت

JAVA
// CacheConfig.java
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        Map<String, Cache> caches = new HashMap<>();
        caches.put("products", Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofHours(1))
            .maximumSize(5_000)
            .recordStats()
            .build());
        caches.put("product-list", Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(10))
            .maximumSize(500)
            .build());
        caches.put("product-stats", Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(5))
            .maximumSize(100)
            .build());

        SimpleCacheManager manager = new SimpleCacheManager();
        manager.setCaches(caches.values().stream()
            .map(c -> new CaffeineCache(c.getClass().getName(), c))
            .toList());
        return manager;
    }
}

// ProductService.java مع التخزين المؤقت
@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Cacheable(value = "products", key = "#id")
    public Product getProduct(Long id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product", id));
    }

    @Cacheable(value = "product-list", key = "#keyword + '-' + #pageable.pageNumber + '-' + #pageable.pageSize")
    public Page<Product> searchProducts(String keyword, Pageable pageable) {
        return productRepository.findByNameContaining(keyword, pageable);
    }

    @CachePut(value = "products", key = "#result.id")
    @CacheEvict(value = "product-list", allEntries = true)
    public Product updateProduct(Long id, String name, BigDecimal price, Integer stock) {
        Product product = getProduct(id);
        product.setName(name);
        product.setPrice(price);
        product.setStock(stock);
        return productRepository.save(product);
    }

    @CacheEvict(value = {"products", "product-list"}, allEntries = true)
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }
}

❓ أسئلة شائعة

س هل يعمل @Cacheable عند استدعاء طرق نفس الفئة لبعضها؟
ج لا، لا يعمل. مثل @Transactional، يعتمد @Cacheable على وكيل AOP، والاستدعاءات بين طرق نفس الفئة لا تمر عبر الوكيل. تحتاج إلى فصل الفئات أو حقن وكيل.
س أيهما أختار بين Caffeine وRedis للتخزين المؤقت؟
ج Caffeine تخزين مؤقت محلي (عملية واحدة)؛ سريع للغاية لكن لا يدعم مشاركة المجموعة. Redis تخزين مؤقت موزع؛ جميع المثيلات تتشارك البيانات، لكنه يتكبد عبء الشبكة. عادةً، يُستخدم الاثنان معًا (L1 Caffeine + L2 Redis).
س كيف يُضمن الاتساق بين التخزين المؤقت وقاعدة البيانات؟
ج استراتيجيات شائعة: 1) Cache Aside (تحديث قاعدة البيانات أولاً، ثم حذف التخزين المؤقت)؛ 2) Write Through (الكتابة للتخزين المؤقت وقاعدة البيانات بشكل متزامن)؛ 3) Write Behind (الكتابة للتخزين المؤقت أولاً، ثم الكتابة لقاعدة البيانات بشكل غير متزامن). نوصي بـ Cache Aside مع الحذف المزدوج المؤجل.
س ما الفرق بين maximumSize وmaximumWeight؟
ج maximumSize يحد عدد الإدخالات، بينما maximumWeight يحد الوزن الإجمالي (يتطلب تحديد weigher). إذا كانت أحجام الكائنات المخزنة مؤقتًا تختلف بشكل كبير، فإن استخدام maximumWeight يكون أكثر دقة.
س كيف أراقب معدل إصابة التخزين المؤقت؟
ج بعد تفعيل recordStats() في Caffeine، يمكنك استرداد معدل الإصابة باستخدام Cache.stats(). ادمج هذا مع Micrometer لتسجيل المقاييس في Prometheus، وعرضها بصريًا في Grafana.
س هل يمكن استخدام @CachePut و@Cacheable في نفس الطريقة؟
ج لا يُوصى بهذا. قد يتخطى @Cacheable تنفيذ الطريقة (عند وجود إصابة تخزين مؤقت)، بينما @CachePut ينفذ الطريقة دائمًا ويحدث التخزين المؤقت؛ دلالات الاثنين متعارضة.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (صعوبة: ⭐): أضف تخزين @Cacheable المؤقت لاستعلام المنتجات في OrderFlow، وتحقق أن سجل SQL لا يُطبع في الاستعلام الثاني (إصابة التخزين المؤقت).

  2. تمرين متقدم (صعوبة ⭐⭐): كوّن مدير تخزين Caffeine المؤقت لتعيين TTLs وسعات قصوى مختلفة لمساحات تخزين مؤقت مختلفة. نفّذ @CachePut لتحديث التخزين المؤقت عند تحديث منتج، و@CacheEvict لمسح التخزين المؤقت عند حذف منتج.

  3. تحدي (صعوبة: ⭐⭐⭐): نفِّذ نظام تخزين مؤقت ثنائي المستوى (L1: Caffeine + L2: Redis). أنشئ CacheResolver مخصصًا يحدد استخدام التخزين المؤقت المحلي أو الموزع بناءً على اسم التخزين المؤقت. فكر في استراتيجية الاتساق للتخزين المؤقت ثنائي المستوى.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%