آلية التخزين المؤقت
التخزين المؤقت هو الخطوة الأولى في تحسين الأداء—للبيانات التي تُقرأ بكثرة لكن تُكتب نادرًا، تخزينها مرة واحدة يوفر عليك ألف استعلام.
1. ما ستتعلمه
- تعليقات
@EnableCachingو@Cacheable/@CachePut/@CacheEvict - استراتيجيات مفتاح التخزين المؤقت والتصفية الشرطية
condition/unless - تكوين التخزين المؤقت المحلي Caffeine: TTL / السعة القصوى / سياسة الإخلاء
- استراتيجيات معالجة مشكلات اختراق التخزين المؤقت واختراقه
- Alice أضافت تخزين Caffeine المؤقت لقائمة منتجات شائعة تتلقى ملايين الزيارات يوميًا
2. قصة حقيقية لمهندس أداء
(1) نقطة الألم: قاعدة البيانات مثقلة
في إطلاق المنتج، عرض Charlie ميزة البيع السريع لـ OrderFlow، مما أطلق فورًا 500 ألف طلب—كلها أغرقت قاعدة البيانات باستعلامات المنتجات الشائعة. نُفد تجمع اتصالات MySQL، مما تسبب في تعطل النظام بالكامل لمدة 10 دقائق وخسارة مباشرة بقيمة 100 ألف دولار. كشف تحليل Alice أن استعلامات تفاصيل المنتج شكلت 80% من إجمالي الطلبات، ومع ذلك ظلت هذه البيانات دون تغيير تقريبًا لساعة كاملة.
(2) حل Spring Cache
يمكنك تفعيل التخزين المؤقت بتعليق توضيحي واحد:
@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 |
تفعيل دعم التخزين المؤقت | فئة التكوين |
graph TD
A["طلب العميل"] --> B{"@Cacheable<br/>إصابة التخزين المؤقت؟"}
B -->|نعم| C["إرجاع البيانات المخزنة مؤقتًا"]
B -->|لا| D["تنفيذ الطريقة<br/>استعلام قاعدة البيانات"]
D --> E["تخزين النتيجة في التخزين المؤقت"]
E --> F["إرجاع النتيجة"]
(1) ▶ مثال:تخزين استعلام @Cacheable المؤقت
@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));
}
}
الناتج:
// تم التنفيذ بنجاح
(2) ▶ مثال:تحديث التخزين المؤقت @CachePut
@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);
}
الناتج:
// تم التنفيذ بنجاح
(3) ▶ مثال:مسح التخزين المؤقت @CacheEvict
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
@CacheEvict(value = "product-list", allEntries = true)
public void clearProductListCache() {
log.info("تم مسح جميع إدخالات تخزين قائمة المنتجات المؤقت");
}
الناتج:
// تم التنفيذ بنجاح
| التعليق | تنفيذ الطريقة | تحديث التخزين المؤقت | العمليات المطبقة |
|---|---|---|---|
@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
@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) { /* ... */ }
الناتج:
// تم التنفيذ بنجاح
(2) التصفية الشرطية
(2) ▶ مثال:condition وunless
// تخزين المنتجات ذات السعر > 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) { /* ... */ }
الناتج:
// تم التنفيذ بنجاح
| المعامل | توقيت التنفيذ | المعنى |
|---|---|---|
condition |
قبل تنفيذ الطريقة | تخزين مؤقت فقط إذا تحققت الشروط |
unless |
بعد تنفيذ الطريقة | إذا تحقق الشرط، لا تخزن مؤقتًا |
5. تكوين التخزين المؤقت المحلي Caffeine
(1) ميزات Caffeine
| الميزة | الوصف |
|---|---|
| أداء عالٍ | إنتاجية أعلى بنسبة 30% من Guava Cache |
| تحديث غير متزامن | يدعم تحديث التخزين المؤقت المنتهي في الخلفية بشكل غير متزامن |
| إحصائيات | معدل الإصابة، وقت التحميل، إلخ |
| Window TinyLFU | خوارزمية إخلاء مثالية |
(1) ▶ مثال:تكوين تخزين Caffeine المؤقت
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
الناتج:
// تم التنفيذ بنجاح
@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) مشكلات التخزين المؤقت الشائعة
| المشكلة | السبب | العاقبة | الحل |
|---|---|---|---|
| اختراق التخزين المؤقت | الاستعلام عن بيانات غير موجودة | استعلام قاعدة البيانات في كل مرة | تخزين القيم الفارغة / مرشحات بلوم |
| اختراق التخزين المؤقت | تدفق طلبات في لحظة انتهاء مفتاح ساخن | ارتفاع مفاجئ في حمل قاعدة البيانات | قفل تبادلي / عدم انتهاء أبدًا + تحديث غير متزامن |
| انهيار التخزين المؤقت | انتهاء صلاحية عدد هائل من المفاتيح في وقت واحد | زيادة مفاجئة في حمل قاعدة البيانات | إضافة إزاحة عشوائية لوقت الانتهاء |
graph TD
A["اختراق التخزين المؤقت<br/>استعلام بيانات غير موجودة"] --> B["الحل: تخزين القيمة الفارغة"]
C["اختراق التخزين المؤقت<br/>انتهاء مفتاح ساخن"] --> D["الحل: قفل تبادلي / تحديث غير متزامن"]
E["انهيار التخزين المؤقت<br/>انتهاء العديد من المفاتيح دفعة واحدة"] --> F["الحل: إزاحة عشوائية لـ TTL"]
(1) ▶ مثال:تخزين القيم الفارغة لمنع الاختراق
@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));
}};
}
الناتج:
// تم التنفيذ بنجاح
7. مثال شامل: تنفيذ كامل لتخزين منتجات OrderFlow المؤقت
// 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);
}
}
❓ أسئلة شائعة
maximumSize وmaximumWeight؟maximumSize يحد عدد الإدخالات، بينما maximumWeight يحد الوزن الإجمالي (يتطلب تحديد weigher). إذا كانت أحجام الكائنات المخزنة مؤقتًا تختلف بشكل كبير، فإن استخدام maximumWeight يكون أكثر دقة.recordStats() في Caffeine، يمكنك استرداد معدل الإصابة باستخدام Cache.stats(). ادمج هذا مع Micrometer لتسجيل المقاييس في Prometheus، وعرضها بصريًا في Grafana.📖 ملخص
@Cacheableفحص التخزين المؤقت → إذا إخفاق، استعلام قاعدة البيانات → كتابة التخزين المؤقت@CachePutينفذ الطريقة دائمًا ويحدث التخزين المؤقت؛ مناسب لعمليات التحديث@CacheEvictمسح التخزين المؤقت؛allEntries = trueمسح التخزين المؤقت بالكامل- سياسة المفتاح معبر عنها بتعبيرات SpEL؛
condition/unlessشروط تصفية التخزين المؤقت - تكوين TTL وCaffeine والسعة القصوى وسياسة الإخلاء لتخزين مؤقت محلي عالي الأداء
- ثلاث مشكلات رئيسية في التخزين المؤقت: اختراق التخزين المؤقت (تخزين القيم الفارغة)، اختراق التخزين المؤقت (أقفال تبادلية)، انهيار التخزين المؤقت (TTL عشوائي)
📝 تمارين
-
تمرين أساسي (صعوبة: ⭐): أضف تخزين
@Cacheableالمؤقت لاستعلام المنتجات في OrderFlow، وتحقق أن سجل SQL لا يُطبع في الاستعلام الثاني (إصابة التخزين المؤقت). -
تمرين متقدم (صعوبة ⭐⭐): كوّن مدير تخزين Caffeine المؤقت لتعيين TTLs وسعات قصوى مختلفة لمساحات تخزين مؤقت مختلفة. نفّذ
@CachePutلتحديث التخزين المؤقت عند تحديث منتج، و@CacheEvictلمسح التخزين المؤقت عند حذف منتج. -
تحدي (صعوبة: ⭐⭐⭐): نفِّذ نظام تخزين مؤقت ثنائي المستوى (L1: Caffeine + L2: Redis). أنشئ
CacheResolverمخصصًا يحدد استخدام التخزين المؤقت المحلي أو الموزع بناءً على اسم التخزين المؤقت. فكر في استراتيجية الاتساق للتخزين المؤقت ثنائي المستوى.



