برنامه نویسی

استراتژی حافظه پنهان ترکیبی در Spring Boot: راهنمای ادغام ردیسون و کافئین

مقدمه: نیاز به حافظه پنهان ترکیبی

در توسعه برنامه های کاربردی مدرن، عملکرد و مقیاس پذیری عوامل مهمی هستند که موفقیت یک سیستم را تعیین می کنند. حافظه پنهان با کاهش بار پایگاه داده، به حداقل رساندن تأخیر و تضمین تجربه کاربری یکپارچه، نقشی اساسی در بهبود این جنبه ها ایفا می کند. با این حال، هیچ راه حل ذخیره سازی واحدی برای هر سناریویی عالی نیست.

حافظه پنهان محلی، مانند کافئین، بازیابی بسیار سریع داده ها را فراهم می کنند زیرا در حافظه کار می کنند و به برنامه نزدیک هستند. اینها برای کاهش زمان پاسخ برای داده هایی که اغلب به آنها دسترسی دارند ایده آل هستند. از سوی دیگر، حافظه های پنهان توزیع شده، مانند آن هایی که توسط ردیسون با Redis، مقیاس پذیری و سازگاری را در چندین نمونه از یک برنامه ارائه می دهد. حافظه پنهان توزیع شده تضمین می کند که همه گره ها در یک سیستم توزیع شده به داده های به روز یکسانی دسترسی دارند، که در محیط های چند گره بسیار مهم است.

با این حال، تکیه صرفا به کش محلی یا توزیع شده با چالش هایی همراه است:

  • کش های محلی می‌تواند در محیط‌های توزیع‌شده ناسازگار شود زیرا به‌روزرسانی‌های داده در بین گره‌ها همگام‌سازی نمی‌شوند.

  • کش های توزیع شده تأخیر شبکه کمی را معرفی کنید، که ممکن است برای سناریوهای با تأخیر بسیار کم مناسب نباشد.

اینجاست که ذخیره سازی ترکیبی به یک راه حل موثر تبدیل می شود. با ترکیب نقاط قوت کش محلی و توزیع شده با استفاده از کافئین و ردیسون، می توانید با استفاده از کش توزیع شده، با حفظ ثبات و مقیاس پذیری، با سرعت ذخیره محلی به عملکرد بالایی برسید.

این مقاله نحوه پیاده‌سازی کش هیبریدی را در یک برنامه Spring Boot بررسی می‌کند و از عملکرد بهینه و سازگاری داده‌ها اطمینان می‌دهد.

توضیحات تصویر

پیاده سازی

مرحله 1: افزودن وابستگی ها

برای شروع، وابستگی های لازم را به خود اضافه کنید pom.xml:

        
            org.springframework.boot
            spring-boot-starter-cache
        
        
            com.github.ben-manes.caffeine
            caffeine
            3.2.0
        
        
            org.redisson
            redisson
            3.43.0
        
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مرحله 2: کش را پیکربندی کنید

در اینجا پیکربندی حافظه پنهان است:

@Configuration
@EnableCaching
public class CacheConfig implements CachingConfigurer {

  @Value("${cache.server.address}")
  private String cacheAddress;

  @Value("${cache.server.password}")
  private String cachePassword;

  @Value("${cache.server.expirationTime:60}")
  private Long cacheExpirationTime;

  @Bean(destroyMethod = "shutdown")
  RedissonClient redisson() {
    Config config = new Config();
    config.useSingleServer().setAddress(cacheAddress).setPassword(cachePassword.trim());
    config.setLazyInitialization(true);
    return Redisson.create(config);
  }

  @Bean
  @Override
  public CacheManager cacheManager() {
    CaffeineCacheManager cacheManager = new CaffeineCacheManager();
    cacheManager.setCaffeine(
        Caffeine.newBuilder().expireAfterWrite(cacheExpirationTime, TimeUnit.MINUTES));
    return cacheManager;
  }

  @Bean
  public CacheEntryRemovedListener cacheEntryRemovedListener() {
    return new CacheEntryRemovedListener(cacheManager());
  }

  @Bean
  @Override
  public CacheResolver cacheResolver() {
    return new LocalCacheResolver(cacheManager(), redisson(), cacheEntryRemovedListener());
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مرحله 3: از حاشیه نویسی انتزاعی کش Spring در کد خود استفاده کنید


    @Cacheable(value = "products", key = "#id")
    public Product getProductById(Long id) {
        // Simulates an expensive database call
        return productRepository.findById(id);
    }

    @CacheEvict(value = "products", key = "#id")
    public void deleteProductById(Long id) {
        productRepository.deleteById(id);
    } 
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توضیح اجزای کلیدی

1. Cache Manager

را CacheManager مسئول مدیریت چرخه حیات حافظه پنهان و دسترسی به اجرای مناسب کش (مثلاً محلی یا توزیع شده) است. در این مورد استفاده می کنیم CaffeineCacheManager برای فعال کردن کش در حافظه با خط مشی انقضا که از طریق آن پیکربندی شده است Caffeine.

2. Cache Resolver

را CacheResolver تعیین می کند که از کدام کش (های) برای یک عملیات خاص به صورت پویا استفاده شود. اینجا، LocalCacheResolver کش های محلی (کافئین) و کش های توزیع شده (ردیسون) را پل می کند و تضمین می کند که استراتژی ترکیبی به طور موثر اعمال می شود.

@Component
public class LocalCacheResolver implements CacheResolver {

  private final CacheManager cacheManager;
  private final RedissonClient redisson;
  private final CacheEntryRemovedListener cacheEntryRemovedListener;

  @Value("${cache.server.expirationTime:60}")
  private Long expirationTime;

  private final Map<String, LocalCache> cacheMap = new ConcurrentHashMap<>();

  public LocalCacheResolver(
      CacheManager cacheManager,
      RedissonClient redisson,
      CacheEntryRemovedListener cacheEntryRemovedListener) {
    this.cacheManager = cacheManager;
    this.redisson = redisson;
    this.cacheEntryRemovedListener = cacheEntryRemovedListener;
  }

  @Override
  @Nonnull
  public Collection extends Cache> resolveCaches(
      @Nonnull CacheOperationInvocationContext> context) {
    Collection<Cache> caches = getCaches(cacheManager, context);
    return caches.stream().map(this::getOrCreateLocalCache).toList();
  }

  private Collection<Cache> getCaches(
      CacheManager cacheManager, CacheOperationInvocationContext> context) {
    return context.getOperation().getCacheNames().stream()
        .map(cacheManager::getCache)
        .filter(Objects::nonNull)
        .toList();
  }

  private LocalCache getOrCreateLocalCache(Cache cache) {
    return cacheMap.computeIfAbsent(
        cache.getName(),
        cacheName -> new LocalCache(cache, redisson, expirationTime, cacheEntryRemovedListener));
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

public class LocalCache implements Cache {

  private final Cache cache;
  private final Long expirationTime;
  private final RMapCache<Object, Object> distributedCache;

  public LocalCache(
      Cache cache,
      RedissonClient redisson,
      Long expirationTime,
      CacheEntryRemovedListener cacheEntryRemovedListener) {
    this.cache = cache;
    this.expirationTime = expirationTime;
    this.distributedCache = redisson.getMapCache(getName());
    this.distributedCache.addListener(cacheEntryRemovedListener);
  }

  @Override
  @Nonnull
  public String getName() {
    return cache.getName();
  }

  @Override
  @Nonnull
  public Object getNativeCache() {
    return cache.getNativeCache();
  }

  @Override
  public ValueWrapper get(@Nonnull Object key) {
    Object value = cache.get(key);

    if (value == null && (value = distributedCache.get(key)) != null) {
      cache.put(key, value);
    }

    return toValueWrapper(value);
  }

  private ValueWrapper toValueWrapper(Object value) {
    if (value == null) return null;
    return value instanceof ValueWrapper ? (ValueWrapper) value : new SimpleValueWrapper(value);
  }

  @Override
  public <T> T get(@Nonnull Object key, Class<T> type) {
    return cache.get(key, type);
  }

  @Override
  public <T> T get(@Nonnull Object key, @Nonnull Callable<T> valueLoader) {
    return cache.get(key, valueLoader);
  }

  @Override
  public void put(@Nonnull Object key, Object value) {
    distributedCache.put(key, value, expirationTime, TimeUnit.MINUTES);
    cache.put(key, value);
  }

  @Override
  public void evict(@Nonnull Object key) {
    distributedCache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

3. Cache Entry Removed Listener

را CacheEntryRemovedListener به ورودی های حذف شده از حافظه پنهان توزیع شده (Redis) گوش می دهد و اطمینان حاصل می کند که از حافظه نهان محلی در سراسر گره ها نیز حذف می شوند و یکپارچگی را حفظ می کنند.

@RequiredArgsConstructor
@Component
public class CacheEntryRemovedListener implements EntryRemovedListener<Object, Object> {

  private final CacheManager cacheManager;

  @Override
  public void onRemoved(EntryEvent event) {
    Cache cache = cacheManager.getCache(event.getSource().getName());
    if (cache != null) {
      cache.evict(event.getKey());
    }
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

گردش کار Hybrid Caching

Cache Entry Put

هنگامی که یک روش حاشیه نویسی با @Cacheable اجرا می شود، put روش فراخوانی شده است. این داده ها را هم در کش محلی (کافئین) و هم در کش توزیع شده (Redis) ذخیره می کند:

javaCopyEdit@Override
public void put(@Nonnull Object key, Object value) {
    distributedCache.put(key, value, expirationTime, TimeUnit.MINUTES);
    cache.put(key, value);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

دریافت ورودی کش

برای بازیابی داده ها، سیستم ابتدا حافظه پنهان محلی را برای کلید بررسی می کند. اگر کلید پیدا نشد، کش توزیع شده را پرس و جو می کند. اگر مقدار در حافظه پنهان توزیع شده وجود داشته باشد، برای دسترسی سریعتر بعدی به حافظه نهان محلی نیز اضافه می شود:

@Override
public ValueWrapper get(@Nonnull Object key) {
    Object value = cache.get(key);

    if (value == null && (value = distributedCache.get(key)) != null) {
      cache.put(key, value);
    }

    return toValueWrapper(value);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تخلیه ورودی حافظه پنهان

هنگامی که یک حذف حافظه پنهان رخ می دهد (به عنوان مثال، از طریق a @CacheEvict حاشیه نویسی)، کلید از حافظه پنهان توزیع شده حذف می شود. حافظه پنهان محلی دیگر گره ها از طریق اطلاع رسانی می شود CacheEntryRemovedListener برای حذف همان کلید:

@Override
public void evict(@Nonnull Object key) {
    distributedCache.remove(key);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید


نتیجه گیری

کش ترکیبی سرعت کش های محلی درون حافظه را با مقیاس پذیری و سازگاری کش های توزیع شده ترکیب می کند. این رویکرد محدودیت های استفاده از کش محلی یا توزیع شده را به تنهایی برطرف می کند. با ادغام کافئین و ردیسون در یک برنامه Spring Boot، می توانید به بهبود عملکرد قابل توجهی دست یابید و در عین حال از داده های ثابت در گره های برنامه اطمینان حاصل کنید.

با استفاده از CacheEntryRemovedListener و CacheResolver تضمین می‌کند که ورودی‌های کش در تمام لایه‌های کش همگام می‌مانند، و یک استراتژی ذخیره‌سازی کارآمد و قابل اعتماد برای برنامه‌های مدرن و مقیاس‌پذیر ارائه می‌دهد. این رویکرد ترکیبی به‌ویژه در سیستم‌های توزیع‌شده که در آن‌ها عملکرد و سازگاری بسیار مهم است، ارزشمند است.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا