TP14.1: التخزين المؤقت باستخدام Redis
أهداف التمرين
- تكوين Redis مع Spring Boot
- تنفيذ التخزين المؤقت على API REST
- قياس التأثير على الأداء
- إدارة إبطال الذاكرة المؤقتة
المدة المقدرة: 1 ساعة
الهندسة المعمارية
Client --> API --> Cache (Redis) --> Database
|
+--> Cache Hit: عودة فورية
|
+--> Cache Miss: استعلام قاعدة البيانات، تخزين النتيجة
الجزء 1: التكوين (15 دقيقة)
1.1 بدء Redis باستخدام Docker
docker run -d --name redis -p 6379:6379 redis:7-alpine
1.2 تبعيات Maven
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
1.3 التكوين
# application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
data:
redis:
host: localhost
port: 6379
cache:
type: redis
redis:
time-to-live: 300000 # 5 دقائق
1.4 تكوين Java
package com.example.cache.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.time.Duration;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
)
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
الجزء 2: الكيان والمستودع (10 دقائق)
2.1 كيان المنتج
package com.example.cache.model;
import jakarta.persistence.*;
import java.io.Serializable;
@Entity
@Table(name = "products")
public class Product implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private Double price;
private Integer stock;
public Product() {}
public Product(String name, String description, Double price, Integer stock) {
this.name = name;
this.description = description;
this.price = price;
this.stock = stock;
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public Integer getStock() { return stock; }
public void setStock(Integer stock) { this.stock = stock; }
}
2.2 المستودع
package com.example.cache.repository;
import com.example.cache.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContaining(String name);
}
الجزء 3: الخدمة مع الذاكرة المؤقتة (20 دقيقة)
3.1 الخدمة
package com.example.cache.service;
import com.example.cache.model.Product;
import com.example.cache.repository.ProductRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
private static final Logger log = LoggerFactory.getLogger(ProductService.class);
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable(value = "products", key = "#id")
public Product getProductById(Long id) {
log.info("Fetching product {} from DATABASE", id);
simulateSlowQuery();
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found: " + id));
}
@Cacheable(value = "productList")
public List<Product> getAllProducts() {
log.info("Fetching ALL products from DATABASE");
simulateSlowQuery();
return productRepository.findAll();
}
@CachePut(value = "products", key = "#result.id")
@CacheEvict(value = "productList", allEntries = true)
public Product createProduct(Product product) {
log.info("Creating product: {}", product.getName());
return productRepository.save(product);
}
@CachePut(value = "products", key = "#id")
@CacheEvict(value = "productList", allEntries = true)
public Product updateProduct(Long id, Product productDetails) {
log.info("Updating product: {}", id);
Product product = productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found: " + id));
product.setName(productDetails.getName());
product.setDescription(productDetails.getDescription());
product.setPrice(productDetails.getPrice());
product.setStock(productDetails.getStock());
return productRepository.save(product);
}
@CacheEvict(value = {"products", "productList"}, allEntries = true)
public void deleteProduct(Long id) {
log.info("Deleting product: {}", id);
productRepository.deleteById(id);
}
@CacheEvict(value = {"products", "productList"}, allEntries = true)
public void clearCache() {
log.info("Cache cleared");
}
private void simulateSlowQuery() {
try {
Thread.sleep(1000); // محاكاة استعلام بطيء
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
الجزء 4: وحدة التحكم (10 دقائق)
package com.example.cache.controller;
import com.example.cache.model.Product;
import com.example.cache.service.ProductService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
long start = System.currentTimeMillis();
List<Product> products = productService.getAllProducts();
long duration = System.currentTimeMillis() - start;
return ResponseEntity.ok()
.header("X-Response-Time", duration + "ms")
.body(products);
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
long start = System.currentTimeMillis();
Product product = productService.getProductById(id);
long duration = System.currentTimeMillis() - start;
return ResponseEntity.ok()
.header("X-Response-Time", duration + "ms")
.body(product);
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(productService.createProduct(product));
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@RequestBody Product product) {
return ResponseEntity.ok(productService.updateProduct(id, product));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
@PostMapping("/cache/clear")
public ResponseEntity<Map<String, String>> clearCache() {
productService.clearCache();
return ResponseEntity.ok(Map.of("message", "تم مسح الذاكرة المؤقتة"));
}
}
الجزء 5: بيانات الاختبار (5 دقائق)
package com.example.cache;
import com.example.cache.model.Product;
import com.example.cache.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class DataInitializer implements CommandLineRunner {
private final ProductRepository productRepository;
public DataInitializer(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public void run(String... args) {
productRepository.save(new Product("Laptop", "High-performance laptop", 999.99, 50));
productRepository.save(new Product("Phone", "Smartphone 5G", 699.99, 100));
productRepository.save(new Product("Tablet", "10-inch tablet", 499.99, 75));
productRepository.save(new Product("Headphones", "Wireless headphones", 199.99, 200));
productRepository.save(new Product("Watch", "Smart watch", 299.99, 150));
}
}
الجزء 6: الاختبارات (15 دقيقة)
6.1 اختبار الذاكرة المؤقتة
# أول استدعاء (Cache Miss - ~1 ثانية)
curl -i http://localhost:8080/api/products/1
# X-Response-Time: 1002ms
# ثاني استدعاء (Cache Hit - ~5ms)
curl -i http://localhost:8080/api/products/1
# X-Response-Time: 5ms
# ثالث استدعاء (Cache Hit)
curl -i http://localhost:8080/api/products/1
# X-Response-Time: 3ms
6.2 سكريبت الاختبار
#!/bin/bash
echo "=== Test Cache Performance ==="
echo "First call (Cache Miss):"
time curl -s http://localhost:8080/api/products/1 > /dev/null
echo "Second call (Cache Hit):"
time curl -s http://localhost:8080/api/products/1 > /dev/null
echo "Third call (Cache Hit):"
time curl -s http://localhost:8080/api/products/1 > /dev/null
echo ""
echo "=== Clear Cache ==="
curl -X POST http://localhost:8080/api/products/cache/clear
echo ""
echo "After clear (Cache Miss):"
time curl -s http://localhost:8080/api/products/1 > /dev/null
6.3 التحقق من Redis
# الاتصال بـ Redis CLI
docker exec -it redis redis-cli
# قائمة المفاتيح
KEYS *
# عرض قيمة
GET "products::1"
# TTL لمفتاح
TTL "products::1"
# حذف الكل
FLUSHALL
قائمة التحقق من التحقق
- Redis يعمل على المنفذ 6379
- أول استدعاء: ~1 ثانية (Cache Miss)
- ثاني استدعاء: ~5ms (Cache Hit)
- التحديث يبطل الذاكرة المؤقتة
- المفاتيح مرئية في Redis