404 Not Found

404 Not Found


nginx

Spring Data Redis

Redis هي سكين الجيش السويسري للتخزين المؤقت—بخمس هياكل بيانات—String وHash وList وSet وZSet—يغطي كل شيء من التخزين المؤقت إلى الجلسات إلى المراسلة.

1. ما ستتعلمه


2. قصة حقيقية لمهندس مجموعة

(1) نقطة الألم: تخزين مؤقت عبر مثيلات متعددة غير مشترك

بعد أن وسّعت Alice مثيل OrderFlow من مثيل واحد إلى ثلاثة مثيلات، ظهرت مشكلات مع التخزين المؤقت المحلي Caffeine: المثيل A حدّث أسعار المنتجات وأعاد تحميل التخزين المؤقت المحلي، لكن التخزين المؤقت المحلي على المثيلين B وC لا يزال يحتوي على الأسعار القديمة، مما أدى إلى عرض أسعار غير متسقة للمستخدمين. حاول Bob استخدام Redis لمشاركة الجلسات، لكنه لم يعرف كيف يدمج Spring Cache مع Redis.

(2) حل Spring Data Redis

كتخزين مؤقت موزع، Redis يشارك عبر جميع المثيلات:

JAVA
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
    return productRepository.findById(id).orElseThrow();
}
// التخزين المؤقت مخزن في Redis، مشترك عبر جميع المثيلات

(3) العائد

بعد أن استبدل Bob تخزين Caffeine بـ Redis كتخزين مؤقت موزع، حافظت المثيلات الثلاثة على تخزين مؤقت متسق، وتزامنت تحديثات أسعار المنتجات في الوقت الفعلي. في نفس الوقت، استُخدم Redis Pub/Sub لتنفيذ إشعارات أحداث الطلبات، واستُخدم Redis لتخزين القائمة السوداء لـ JWT، ومشاركة حالة تسجيل الدخول عبر مثيلات متعددة.


3. العمل مع هياكل بيانات Redis

(1) هياكل البيانات الخمسة الرئيسية

النوع الغرض السيناريوهات النموذجية
String أزواج مفتاح-قيمة بسيطة تخزين مؤقت، عدادات، جلسات
Hash خصائص الكائن تفاصيل المنتج، معلومات المستخدم
List قائمة مرتبة طابور رسائل، قائمة الأحدث
Set مجموعة غير مرتبة وسوم، أصدقاء مشتركون
ZSet مجموعة مرتبة لوحات المتصدرين، طوابير التأخير

(1) ▶ مثال:عمليات RedisTemplate الأساسية

JAVA
@Service
public class RedisOperationService {

    private final StringRedisTemplate redis;

    public RedisOperationService(StringRedisTemplate redis) {
        this.redis = redis;
    }

    // عمليات String
    public void cacheProduct(Long id, String json) {
        redis.opsForValue().set("product:" + id, json, Duration.ofHours(1));
    }

    public String getProduct(Long id) {
        return redis.opsForValue().get("product:" + id);
    }

    // عداد
    public Long incrementOrderCount() {
        return redis.opsForValue().increment("stats:daily-orders");
    }

    // عمليات Hash
    public void saveProductDetail(Long id, Map<String, String> details) {
        redis.opsForHash().putAll("product:detail:" + id, details);
    }

    public Map<Object, Object> getProductDetail(Long id) {
        return redis.opsForHash().entries("product:detail:" + id);
    }

    // عمليات List - الطلبات الأخيرة
    public void addRecentOrder(Long orderId) {
        redis.opsForList().leftPush("recent:orders", orderId.toString());
        redis.opsForList().trim("recent:orders", 0, 99);  // الاحتفاظ بآخر 100
    }

    public List<String> getRecentOrders() {
        return redis.opsForList().range("recent:orders", 0, -1);
    }

    // عمليات Set - وسوم المنتج
    public void addProductTag(Long productId, String tag) {
        redis.opsForSet().add("product:tags:" + productId, tag);
    }

    public Set<String> getProductTags(Long productId) {
        return redis.opsForSet().members("product:tags:" + productId);
    }

    // عمليات ZSet - ترتيب المبيعات
    public void updateSalesRank(Long productId, double salesCount) {
        redis.opsForZSet().add("sales:ranking", productId.toString(), salesCount);
    }

    public List<String> getTopSellingProducts(int limit) {
        return redis.opsForZSet()
            .reverseRange("sales:ranking", 0, limit - 1)
            .stream().toList();
    }
}

الناتج:

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

4. تكامل @Cacheable مع Redis

(1) تكوين تخزين Redis المؤقت

(1) ▶ مثال:تكوين RedisCacheManager

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

الناتج:

TEXT
// تم التنفيذ بنجاح
YAML
spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}
      lettuce:
        pool:
          max-active: 20
          max-idle: 10
          min-idle: 5
JAVA
@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .disableCachingNullValues();

        Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
            "products", config.entryTtl(Duration.ofHours(1)),
            "product-list", config.entryTtl(Duration.ofMinutes(10)),
            "order-stats", config.entryTtl(Duration.ofMinutes(5))
        );

        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .withInitialCacheConfigurations(cacheConfigs)
            .transactionAware()
            .build();
    }
}
Lettuce مقابل Jedis Lettuce Jedis
نموذج الاتصال غير متزامن غير حظر (Netty) متزامن حظر
آمن للخيوط نعم (اتصال واحد مشترك بين خيوط متعددة) لا (يتطلب تجمع اتصالات)
الأداء أفضل للسيناريوهات عالية التزامن كافٍ للسيناريوهات البسيطة
الافتراضي في Spring Boot ✅ الافتراضي يجب التبديل يدويًا

5. نشر/اشتراك Redis

(1) نموذج رسائل Pub/Sub

100%
sequenceDiagram
    participant P as الناشر<br/>(OrderService)
    participant R as Redis<br/>Pub/Sub
    participant S1 as المشترك 1<br/>(NotificationService)
    participant S2 as المشترك 2<br/>(AnalyticsService)

    P->>R: PUBLISH order-events "ORDER_CREATED:123"
    R->>S1: رسالة: "ORDER_CREATED:123"
    R->>S2: رسالة: "ORDER_CREATED:123"

(1) ▶ مثال:حدث تقديم طلب

JAVA
@Service
public class OrderEventPublisher {

    private final StringRedisTemplate redis;

    public OrderEventPublisher(StringRedisTemplate redis) {
        this.redis = redis;
    }

    public void publishOrderCreated(Long orderId) {
        redis.convertAndSend("order-events",
            "ORDER_CREATED:" + orderId);
    }

    public void publishOrderCancelled(Long orderId) {
        redis.convertAndSend("order-events",
            "ORDER_CANCELLED:" + orderId);
    }
}

الناتج:

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

(2) ▶ مثال:الاشتراك في أحداث الطلبات

JAVA
@Component
public class OrderEventSubscriber {

    private static final Logger log = LoggerFactory.getLogger(OrderEventSubscriber.class);

    @RedisListener(topic = "order-events")
    public void handleOrderEvent(String message) {
        String[] parts = message.split(":");
        String eventType = parts[0];
        Long orderId = Long.parseLong(parts[1]);

        switch (eventType) {
            case "ORDER_CREATED" -> log.info("معالجة طلب تم إنشاؤه: {}", orderId);
            case "ORDER_CANCELLED" -> log.info("معالجة طلب تم إلغاؤه: {}", orderId);
        }
    }
}

@Configuration
public class RedisPubSubConfig {

    @Bean
    public RedisMessageListenerContainer container(
            RedisConnectionFactory factory,
            MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        container.addMessageListener(listenerAdapter,
            new ChannelTopic("order-events"));
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(OrderEventSubscriber subscriber) {
        return new MessageListenerAdapter(subscriber, "handleOrderEvent");
    }
}

الناتج:

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

6. جلسات Redis المشتركة والقوائم السوداء لـ JWT

(1) ▶ مثال:تنفيذ القائمة السوداء لـ JWT

JAVA
@Service
public class TokenBlacklistService {

    private final StringRedisTemplate redis;

    public TokenBlacklistService(StringRedisTemplate redis) {
        this.redis = redis;
    }

    public void blacklistToken(String tokenId, Duration remainingTtl) {
        redis.opsForValue().set(
            "token:blacklist:" + tokenId, "1", remainingTtl);
    }

    public boolean isBlacklisted(String tokenId) {
        return Boolean.TRUE.equals(
            redis.hasKey("token:blacklist:" + tokenId));
    }
}

الناتج:

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

(2) ▶ مثال:تحديد المعدل الموزع

JAVA
@Service
public class RateLimitService {

    private final StringRedisTemplate redis;

    public RateLimitService(StringRedisTemplate redis) {
        this.redis = redis;
    }

    public boolean isAllowed(String clientId, int maxRequests, Duration window) {
        String key = "rate-limit:" + clientId;
        Long count = redis.opsForValue().increment(key);
        if (count != null && count == 1) {
            redis.expire(key, window);
        }
        return count != null && count <= maxRequests;
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح
حالات الاستخدام هياكل بيانات Redis تصميم المفتاح
التخزين المؤقت الموزع String / Hash cache:products:123
القائمة السوداء لـ JWT String + TTL token:blacklist:tokenId
تحديد المعدل الموزع String + INCR + TTL rate-limit:clientId
الطلبات الأخيرة List + LPUSH + TRIM recent:orders
الأكثر مبيعًا ZSet sales:ranking
إشعار الأحداث Pub/Sub order-events

7. مثال شامل: تكامل كامل لـ OrderFlow مع Redis

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

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.builder(factory)
            .cacheDefaults(defaults)
            .withInitialCacheConfigurations(Map.of(
                "products", defaults.entryTtl(Duration.ofHours(1)),
                "product-list", defaults.entryTtl(Duration.ofMinutes(10))
            ))
            .build();
    }
}

// ProductService مع تخزين Redis المؤقت
@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));
    }

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

// OrderEventPublisher.java
@Service
public class OrderEventPublisher {
    private final StringRedisTemplate redis;
    public OrderEventPublisher(StringRedisTemplate redis) { this.redis = redis; }

    public void publishOrderCreated(Long orderId) {
        redis.convertAndSend("order-events", "ORDER_CREATED:" + orderId);
    }
}
YAML
# application.yml
spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}
      lettuce:
        pool:
          max-active: 20
          max-idle: 10

❓ أسئلة شائعة

س ما الفرق بين RedisTemplate وStringRedisTemplate؟
ج StringRedisTemplate يرث من RedisTemplate<String, String>؛ المفاتيح والقيم كلاهما Strings، ويستخدم StringRedisSerializer. RedisTemplate يستخدم تسلسل JDK افتراضيًا، وهو أقل قابلية للقراءة. نوصي باستخدام StringRedisTemplate أو مسلسل مخصص.
س كيف أختار بين تخزين Redis المؤقت وتخزين Caffeine المؤقت؟
ج استخدم Caffeium لمثيل واحد (الأسرع)، وRedis لمثيلات متعددة (مشترك). أفضل ممارسة: نظام تخزين مؤقت ثنائي المستوى مع Caffeine كـ L1 وRedis كـ L2، حيث يكون التخزين المؤقت المحلي قصير الأمد (1-5 دقائق) والتخزين المؤقت Redis طويل الأمد (30-60 دقيقة).
س هل يمكن فقدان رسائل Redis Pub/Sub؟
ج نعم. Pub/Sub يعمل بنموذج "أطلق وانسَ"، فالرسائل تُفقد إذا كان المشتركون غير متصلين. إذا كنت بحاجة إلى مراسلة موثوقة، استخدم Redis Streams أو طابور رسائل مخصص (مثل RabbitMQ أو Kafka).
س هل يتطلب Lettuce تجمع اتصالات؟
ج Lettuce يستخدم تعدد إرسال اتصال واحد، لذا تجمع اتصالات غير مطلوب عمومًا. ومع ذلك، في السيناريوهات التي تنطوي على عمليات حظر عالية التزامن، يمكن لتكوين تجمع اتصالات تحسين الإنتاجية. يجب تكوين تجمع اتصالات لـ Jedis.
س كيف يجب أن أختار طريقة التسلسل لتخزين Redis المؤقت؟
ج نوصي بتسلسل JSON (GenericJackson2JsonRedisSerializer) لقابليته للقراءة والتوافق بين اللغات. تسلسل JDK غير قابل للقراءة وغير موصى به. يمكنك أيضًا استخدام StringRedisSerializer مع تحويل JSON يدوي.
س كيف أتعامل مع فشل اتصال Redis؟
ج 1) افتراضيًا، لا يرمي Spring Cache استثناءً عند الفشل (يعود لاستعلام قاعدة البيانات)؛ 2) كوّن CacheErrorHandler لتنفيذ معالجة فشل مخصصة؛ 3) استخدم Redis Sentinel أو Cluster لضمان التوافر العالي.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (صعوبة ⭐): كوّن Spring Data Redis لـ OrderFlow، واستخدم @Cacheable لتخزين تخزين مؤقت للمنتجات في Redis، وتحقق من مشاركة التخزين المؤقت عبر مثيلات متعددة.

  2. تمرين متقدم (صعوبة: ⭐⭐): نفّذ إشعارات أحداث الطلبات عبر Redis Pub/Sub—انشر حدث ORDER_CREATED عند تقديم طلب، واشترك فيه خدمة الإشعارات وأرسل بريدًا إلكترونيًا. نفّذ ميزة إبطال رمز القائمة السوداء لـ JWT.

  3. تحدي (صعوبة: ⭐⭐⭐): نفِّذ نظام تخزين مؤقت ثنائي المستوى يتكون من L1 Caffeine وL2 Redis. عند تحديث تخزين Redis المؤقت، استخدم Pub/Sub لإخطار جميع المثيلات بمسح تخزين Caffeine المحلي المؤقت، مما يضمن الاتساق عبر المستويين.

Web-Tutorial.com

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

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

100%