استراتژی حافظه پنهان ترکیبی در 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
تضمین میکند که ورودیهای کش در تمام لایههای کش همگام میمانند، و یک استراتژی ذخیرهسازی کارآمد و قابل اعتماد برای برنامههای مدرن و مقیاسپذیر ارائه میدهد. این رویکرد ترکیبی بهویژه در سیستمهای توزیعشده که در آنها عملکرد و سازگاری بسیار مهم است، ارزشمند است.