Caching Strategies + Redis: अपनी Application को Lightning Fast बनाएँ
Java developers के लिए caching strategies और Redis के लिए एक व्यापक गाइड — Spring Cache annotations, Redis data structures, multi-tier caching (Caffeine + Redis + Database), cache patterns को कवर करते हुए (
Caching क्यों मायने रखती है
कल्पना करें कि आप एक test के लिए पढ़ाई कर रहे हैं। हर बार जब आपको एक तथ्य देखने की ज़रूरत होती है, तो आप पूरे रास्ते library तक चलते हैं, सही shelf ढूँढते हैं, book निकालते हैं, सही page पर flip करते हैं, जवाब पढ़ते हैं, फिर सारा रास्ता वापस चलते हैं। आपकी application हर single request के लिए database hit करने पर यही करती है।
अब कल्पना करें कि आप अपनी favorite book अपनी desk पर रखते हैं। जवाब चाहिए? बस नीचे देखें। यह caching है। यह frequently used data को पास रखता है ताकि आपकी application इसे बार-बार लाने में समय बर्बाद न करे।
Cache एक exam के दौरान एक cheat sheet की तरह है — पूरी textbook देखे बिना quick जवाब।
Caching के बिना, एक popular product page database को प्रति मिनट 10,000 बार query कर सकता है। Caching के साथ, यह एक बार query करता है, result store करता है, और अगले 9,999 requests को memory से serve करता है। अंतर? Database query: ~5-50 milliseconds। Cache read: ~0.1-1 millisecond। यानी 50x से 500x तेज़।
Spring Cache Annotations
Spring Boot चार annotations के साथ caching को आसान बनाता है। उन्हें simple labels के रूप में सोचें जिन्हें आप अपने methods पर चिपकाते हैं Spring को बताने के लिए: "Hey, इस result को याद रखो।"
@Cacheable — "इस जवाब को याद रखो"
पहली बार जब कोई किसी product के लिए पूछता है, तो Spring इसे database से लाता है। उसके बाद हर बार, Spring database को छुए बिना saved जवाब लौटाता है। जैसे एक sticky note पर जवाब लिखना।
@Service
public class ProductService {
@Cacheable(value = "products", key = "#productId")
public Product getProductById(Long productId) {
// This only runs the FIRST time for each productId.
// After that, Spring returns the cached result.
log.info("Fetching product {} from database...", productId);
return productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
}
// Conditional caching — only cache if price > 10
@Cacheable(value = "products", key = "#productId",
condition = "#productId > 0",
unless = "#result.price < 10")
public Product getExpensiveProduct(Long productId) {
return productRepository.findById(productId).orElseThrow();
}
}
@CachePut — "Sticky Note Update करें"
जब आप एक product update करते हैं, तो आप चाहते हैं कि cache में latest version हो। @CachePut हमेशा method चलाता है AND cache update करता है। इसे पुराना sticky note मिटाकर नया लिखने के रूप में सोचें।
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
// This ALWAYS runs — and the result replaces what is in the cache.
log.info("Updating product {} in database and cache", product.getId());
return productRepository.save(product);
}
@CacheEvict — "Sticky Note को फेंक दें"
जब data delete हो जाता है या आप sure नहीं हैं कि क्या बदला, तो बस इसे cache से remove करें। अगली बार जब कोई पूछेगा, Spring database से fresh data लाएगा।
// Remove one product from cache
@CacheEvict(value = "products", key = "#productId")
public void deleteProduct(Long productId) {
productRepository.deleteById(productId);
}
// Nuclear option: clear the ENTIRE cache
@CacheEvict(value = "products", allEntries = true)
public void refreshAllProducts() {
log.info("Cleared entire products cache");
}
@Caching — "एक साथ कई Cache काम करें"
कभी-कभी एक method कई caches को affect करता है। @Caching आपको कई cache operations को combine करने देता है।
@Caching(
put = { @CachePut(value = "products", key = "#product.id") },
evict = {
@CacheEvict(value = "productList", allEntries = true),
@CacheEvict(value = "productSearch", allEntries = true)
}
)
public Product saveProduct(Product product) {
return productRepository.save(product);
}
Redis Basics — आपकी Super-Fast Notebook
Redis का मतलब है REmote DIctionary Server। इसे एक विशाल notebook के रूप में सोचें जो आपके computer की memory (RAM) में रहती है, hard drive पर नहीं। RAM से पढ़ना आपके सामने एक sign पढ़ने जैसा है। Hard drive से पढ़ना उसी sign को पढ़ने के लिए दूसरे शहर तक गाड़ी चलाने जैसा है।
Redis एक in-memory key-value store है। आप इसे एक name (key) और एक value देते हैं, और यह याद रखता है। एक dictionary की तरह simple: word देखें, meaning प्राप्त करें।
# Basic Redis commands — like a super-fast dictionary
SET product:101 "{"name": "Wireless Mouse", "price": 29.99}"
GET product:101
# Returns: {"name": "Wireless Mouse", "price": 29.99}
DEL product:101
# Poof — gone from the notebook
# Set with expiration (TTL) — auto-delete after 300 seconds
SET weather:nyc "72°F, Sunny" EX 300
# Check how long until it expires
TTL weather:nyc
# Returns: 295 (seconds remaining)
Redis Data Structures — केवल Key-Value से अधिक
Redis केवल एक simple dictionary नहीं है। इसके पास पाँच शक्तिशाली data structures हैं, हर एक अलग कामों के लिए perfect है। उन्हें अपनी kitchen में पाँच अलग-अलग प्रकार के containers के रूप में सोचें।
1. String — Simple Jar
एक single value store करता है। Counters, simple values, या serialized objects के लिए perfect।
# Simple value
SET user:profile:42 "{"name": "Alice", "email": "alice@example.com"}"
# Counter — like a tally counter
INCR page:views:homepage # 1
INCR page:views:homepage # 2
INCRBY page:views:homepage 10 # 12
2. Hash — Filing Cabinet
एक key के अंदर एक mini-dictionary की तरह। JSON में serialize किए बिना कई fields के साथ objects store करने के लिए perfect।
# Store a user profile as a hash
HSET user:42 name "Alice" email "alice@example.com" age "30"
# Get one field
HGET user:42 name # "Alice"
# Get all fields
HGETALL user:42 # name, Alice, email, alice@example.com, age, 30
# Update just one field — no need to rewrite the whole object
HSET user:42 age "31"
3. List — Queue Line
Values की एक ordered list। Message queues, activity feeds, और recent items के लिए perfect।
# Add to the left (newest first) — like people joining a line
LPUSH recent:searches "wireless mouse"
LPUSH recent:searches "bluetooth keyboard"
LPUSH recent:searches "usb hub"
# Get the 5 most recent searches
LRANGE recent:searches 0 4
# Returns: usb hub, bluetooth keyboard, wireless mouse
# Remove from the right (oldest first) — process the queue
RPOP recent:searches # "wireless mouse"
4. Set — Unique Stamp Collection
एक collection जहाँ हर item unique है। कोई duplicates allowed नहीं। Tags, unique visitors, और "who liked this" के लिए perfect।
# Track unique visitors to a page
SADD visitors:homepage "user:1" "user:2" "user:3"
SADD visitors:homepage "user:1" # ignored — already there!
# How many unique visitors?
SCARD visitors:homepage # 3
# Find users who visited BOTH pages (intersection)
SADD visitors:about "user:2" "user:3" "user:4"
SINTER visitors:homepage visitors:about
# Returns: user:2, user:3
5. Sorted Set — Leaderboard
एक Set की तरह, लेकिन हर item का एक score होता है। Items ऑटोमैटिकली score द्वारा sort होते हैं। Leaderboards, ranking, और top-N queries के लिए perfect।
# Movie ratings leaderboard
ZADD movie:ratings 9.3 "The Shawshank Redemption"
ZADD movie:ratings 9.2 "The Godfather"
ZADD movie:ratings 8.8 "The Dark Knight"
ZADD movie:ratings 9.0 "Pulp Fiction"
# Top 3 movies (highest score first)
ZREVRANGE movie:ratings 0 2 WITHSCORES
# 1) The Shawshank Redemption — 9.3
# 2) The Godfather — 9.2
# 3) Pulp Fiction — 9.0
# What rank is "The Dark Knight"?
ZREVRANK movie:ratings "The Dark Knight" # 3 (0-indexed)
Multi-Tier Caching — Three-Level Defense
इसे इस तरह सोचें: L1 आपकी जीभ की नोक पर जवाब है (सबसे तेज़)। L2 आपकी जेब में cheat sheet है (तेज़)। L3 आपके backpack में textbook है (धीमा लेकिन सब कुछ है)।
| Level | Technology | Speed | Capacity | Where |
|---|---|---|---|---|
| L1 | Caffeine (JVM Heap) | ~1 ns | Small (100s of MBs) | आपके ऐप के अंदर |
| L2 | Redis | ~0.5 ms | Medium (GBs) | अलग server |
| L3 | Database (PostgreSQL) | ~5-50 ms | Large (TBs) | Disk storage |
जब आपकी ऐप को data चाहिए, तो यह पहले L1 चेक करती है। वहाँ नहीं है? L2 चेक करें। अभी भी वहाँ नहीं है? L3 (database) पर जाएँ, फिर अगली बार के लिए result को L2 और L1 में store करें।
@Service
public class MultiTierCacheService {
private final Cache<String, Product> localCache; // L1: Caffeine
private final RedisTemplate<String, Product> redisTemplate; // L2: Redis
private final ProductRepository productRepository; // L3: Database
public MultiTierCacheService(RedisTemplate<String, Product> redisTemplate,
ProductRepository productRepository) {
this.redisTemplate = redisTemplate;
this.productRepository = productRepository;
// L1: Caffeine — holds 1,000 items, expires after 5 minutes
this.localCache = Caffeine.newBuilder()
.maximumSize(1_000)
.expireAfterWrite(Duration.ofMinutes(5))
.build();
}
public Product getProduct(String productId) {
String cacheKey = "product:" + productId;
// Step 1: Check L1 (Caffeine — in JVM memory)
Product product = localCache.getIfPresent(cacheKey);
if (product != null) {
log.debug("L1 HIT for {}", productId);
return product;
}
// Step 2: Check L2 (Redis — separate server)
product = redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
log.debug("L2 HIT for {}", productId);
localCache.put(cacheKey, product); // Store in L1 for next time
return product;
}
// Step 3: Fetch from L3 (Database — the source of truth)
log.debug("L3 FETCH for {}", productId);
product = productRepository.findById(productId).orElseThrow();
// Store in both L2 and L1
redisTemplate.opsForValue().set(cacheKey, product,
Duration.ofMinutes(30));
localCache.put(cacheKey, product);
return product;
}
}
Cache Patterns — अपने Cache का उपयोग करने के चार तरीके
आपकी application cache और database से कैसे बात करती है, इसके लिए चार मुख्य strategies हैं। हर एक के trade-offs हैं। अपनी ज़रूरतों के आधार पर सही चुनें।
1. Cache-Aside (Lazy Loading) — "केवल वही cache करें जो लोग वास्तव में माँगते हैं"
Application स्वयं cache को manage करती है। एक read पर, पहले cache चेक करें। अगर यह miss है, तो database से लाएँ और cache में store करें। यह सबसे common pattern है।
Analogy: आप एक sticky note पर phone number केवल तब लिखते हैं जब कोई call करता है और आपको इसे देखना पड़ा। अगर कोई कभी Uncle Bob के number के लिए नहीं पूछता, तो यह कभी sticky note पर नहीं जाता।
@Service
public class UserProfileService {
private final RedisTemplate<String, UserProfile> redis;
private final UserRepository userRepository;
// Cache-Aside: Read
public UserProfile getUserProfile(Long userId) {
String key = "user:profile:" + userId;
// 1. Check cache
UserProfile cached = redis.opsForValue().get(key);
if (cached != null) return cached;
// 2. Cache miss — fetch from DB
UserProfile profile = userRepository.findById(userId).orElseThrow();
// 3. Store in cache for next time (TTL: 1 hour)
redis.opsForValue().set(key, profile, Duration.ofHours(1));
return profile;
}
// Cache-Aside: Write (invalidate, not update)
public UserProfile updateProfile(Long userId, UserProfile updated) {
UserProfile saved = userRepository.save(updated);
redis.delete("user:profile:" + userId); // Invalidate — next read will re-cache
return saved;
}
}
Pros: केवल उस data को cache करता है जो वास्तव में request किया गया है। समझने में simple। Cache failure ऐप को नहीं तोड़ता।
Cons: हर item के लिए पहला request हमेशा slow होता है (cache miss)। अगर database आपकी ऐप के बाहर update होता है तो संभावित stale data।
2. Write-Through — "हमेशा cache और database को एक साथ update करें"
हर write एक ही समय में cache AND database पर जाता है। Cache हमेशा database के साथ sync में रहता है।
Analogy: हर बार जब आप अपना address update करते हैं, तो आप एक ही समय में अपने phone contacts AND paper address book दोनों को update करते हैं।
@Service
public class WriteThroughProductService {
public Product saveProduct(Product product) {
// Write to database FIRST
Product saved = productRepository.save(product);
// Then write to cache — cache is always up to date
String key = "product:" + saved.getId();
redis.opsForValue().set(key, saved, Duration.ofHours(2));
return saved;
}
public Product getProduct(String productId) {
String key = "product:" + productId;
// Cache is always fresh because every write updates it
Product cached = redis.opsForValue().get(key);
if (cached != null) return cached;
// Fallback to DB (only needed for first load or after cache eviction)
Product product = productRepository.findById(productId).orElseThrow();
redis.opsForValue().set(key, product, Duration.ofHours(2));
return product;
}
}
Pros: Cache हमेशा database के साथ consistent रहता है। कोई stale data नहीं।
Cons: Writes slower हैं (एक के बजाय दो writes)। ऐसे data को caches करता है जो कभी नहीं पढ़ा जा सकता।
3. Write-Behind (Write-Back) — "Cache को अब update करें, database को बाद में"
Writes cache पर तुरंत जाते हैं, और cache background में database पर writes करता है (batched)। Ultra-fast writes, लेकिन अधिक complex।
Analogy: आप पूरे दिन एक whiteboard पर notes लिखते हैं, फिर दिन के अंत में सब कुछ अपनी notebook पर copy करते हैं।
@Service
public class WriteBehindService {
private final Queue<Product> writeQueue = new ConcurrentLinkedQueue<>();
// Write goes to cache immediately — super fast
public Product saveProduct(Product product) {
String key = "product:" + product.getId();
redis.opsForValue().set(key, product);
// Queue the database write for later
writeQueue.add(product);
return product;
}
// Background job flushes the queue to the database every 5 seconds
@Scheduled(fixedRate = 5000)
public void flushToDatabase() {
List<Product> batch = new ArrayList<>();
Product item;
while ((item = writeQueue.poll()) != null) {
batch.add(item);
}
if (!batch.isEmpty()) {
productRepository.saveAll(batch);
log.info("Flushed {} products to database", batch.size());
}
}
}
Pros: अत्यंत तेज़ writes। Batching database load को कम करती है।
Cons: Flushing से पहले cache crash होने पर data loss का जोखिम। Implement करने में अधिक complex।
4. Read-Through — "Cache आपके लिए database से fetches करता है"
Application केवल cache से बात करती है। अगर cache में data नहीं है, तो cache स्वयं database में जाता है। Application कभी सीधे database को नहीं छूती।
Analogy: आप अपने assistant से एक file के लिए पूछते हैं। अगर assistant के पास यह नहीं है, तो वे filing cabinet में जाते हैं, इसे प्राप्त करते हैं, एक copy रखते हैं, और आपको देते हैं। आप कभी खुद filing cabinet में नहीं जाते।
// With Caffeine's LoadingCache — it fetches on miss automatically
@Configuration
public class ReadThroughConfig {
@Bean
public LoadingCache<Long, Product> productCache(ProductRepository repo) {
return Caffeine.newBuilder()
.maximumSize(5_000)
.expireAfterWrite(Duration.ofMinutes(15))
.build(productId -> repo.findById(productId).orElse(null));
// ^ This loader runs automatically on cache miss
}
}
@Service
public class ReadThroughProductService {
private final LoadingCache<Long, Product> productCache;
public Product getProduct(Long productId) {
// Just ask the cache — it handles everything
return productCache.get(productId);
// Cache HIT? Returns from memory.
// Cache MISS? Calls the loader, stores result, returns it.
}
}
Pros: Application code super simple है। Cache सभी fetching logic को handle करता है।
Cons: Cache को data source के साथ tightly couples करता है। चीज़ें ग़लत होने पर debug करना कठिन।
TTL और Eviction Policies — Old Data को कब फेंकें
Caches हमेशा के लिए नहीं बढ़ सकते। आपको पुराने data को हटाने के लिए rules चाहिए। अपनी desk के बारे में सोचें: अगर आप हमेशा के लिए papers ढेर करते रहते हैं, तो आप कुछ नहीं ढूँढ सकते। आपको यह तय करने के लिए rules चाहिए कि क्या रखना है और क्या फेंकना है।
TTL (Time To Live) — "यह sticky note 10 मिनट में expire होता है"
Set करें कि data कितने समय तक cache में रहता है। समय समाप्त होने के बाद, यह ऑटोमैटिकली गायब हो जाता है।
# Redis: key expires after 3600 seconds (1 hour)
SET weather:london "15°C, Cloudy" EX 3600
# Check remaining time
TTL weather:london # 3542 (seconds left)
// Java: set TTL with RedisTemplate
redis.opsForValue().set("weather:london", weatherData,
Duration.ofHours(1));
// Caffeine: expire after write
Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(10))
.build();
Eviction Policies — "Desk भरी है। हम क्या remove करते हैं?"
जब cache भर जाता है, तो Redis को यह तय करने की ज़रूरत होती है कि क्या remove करना है। Redis इन eviction policies को support करता है:
| Policy | यह क्या करती है | Best For |
|---|---|---|
allkeys-lru | Least recently used key को remove करें | General purpose (अनुशंसित) |
allkeys-lfu | Least frequently used key को remove करें | जब कुछ keys हमेशा popular होते हैं |
volatile-lru | LRU, लेकिन केवल TTL set वाले keys | Persistent और cached data का मिश्रण |
volatile-ttl | Expire होने के सबसे करीब keys को remove करें | जब TTL importance को दर्शाता है |
noeviction | Memory full होने पर errors return करें | जब data loss अस्वीकार्य है |
# Set eviction policy in redis.conf
maxmemory 256mb
maxmemory-policy allkeys-lru
Cache Invalidation Strategies — Computer Science में सबसे कठिन समस्या
Computer Science में केवल दो कठिन चीज़ें हैं: cache invalidation और चीज़ों को नाम देना। यहाँ बताया गया है कि पहले को कैसे handle किया जाए।
1. Time-Based (TTL)
सबसे simple दृष्टिकोण। एक set समय के बाद data expire हो जाता है। जब थोड़ा stale data स्वीकार्य है तो बढ़िया काम करता है (weather, product listings, blog posts)।
@Cacheable(value = "weatherForecast", key = "#city")
public Weather getWeather(String city) {
return weatherApi.fetchForecast(city);
}
// In application.yml — set TTL per cache
spring:
cache:
redis:
time-to-live: 600000 # 10 minutes in milliseconds
2. Event-Based (Publish/Subscribe)
जब data बदलता है, एक event publish करें। सभी interested services सुनते हैं और अपने caches को invalidate करते हैं।
// When a product is updated, publish an event
@Service
public class ProductUpdatePublisher {
private final RedisTemplate<String, String> redis;
public void publishProductUpdate(Long productId) {
redis.convertAndSend("product:updates",
String.valueOf(productId));
}
}
// Listener in another service — clears its local cache
@Component
public class ProductCacheListener {
private final Cache<Long, Product> localCache;
@EventListener
public void onProductUpdate(ProductUpdateEvent event) {
localCache.invalidate(event.getProductId());
log.info("Cache invalidated for product {}", event.getProductId());
}
}
3. Version-Based
Cache key में एक version number शामिल करें। जब data बदलता है, version bump करें। TTL के माध्यम से पुराने keys naturally expire होते हैं।
@Service
public class VersionedCacheService {
public Product getProduct(Long productId) {
int version = getCurrentVersion(productId);
String key = "product:" + productId + ":v" + version;
Product cached = redis.opsForValue().get(key);
if (cached != null) return cached;
Product product = productRepository.findById(productId).orElseThrow();
redis.opsForValue().set(key, product, Duration.ofHours(1));
return product;
}
public void updateProduct(Long productId, Product updated) {
productRepository.save(updated);
incrementVersion(productId); // Old cache key is now orphaned — TTL cleans it up
}
}
Spring Boot + Redis Configuration
यहाँ Redis caching के साथ Spring Boot के लिए एक complete, production-ready setup है।
Step 1: Dependencies जोड़ें
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
Step 2: application.yml Configure करें
spring:
data:
redis:
host: localhost
port: 6379
password: ${REDIS_PASSWORD:}
timeout: 2000ms
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 2
max-wait: 500ms
shutdown-timeout: 200ms
cache:
type: redis
redis:
time-to-live: 3600000 # 1 hour default TTL
cache-null-values: false # Do not cache null results
key-prefix: "myapp:" # Prefix all keys
use-key-prefix: true
logging:
level:
org.springframework.cache: DEBUG # See cache HIT/MISS in logs
Step 3: Redis Configuration Class
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
// Default config: 1 hour TTL, JSON serialization
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(
SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
// Custom TTLs per cache name
Map<String, RedisCacheConfiguration> perCacheConfig = Map.of(
"products", defaultConfig.entryTtl(Duration.ofMinutes(30)),
"userProfiles", defaultConfig.entryTtl(Duration.ofHours(2)),
"weatherForecast", defaultConfig.entryTtl(Duration.ofMinutes(10)),
"movieRatings", defaultConfig.entryTtl(Duration.ofHours(6))
);
return RedisCacheManager.builder(factory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(perCacheConfig)
.transactionAware()
.build();
}
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
Step 4: Complete Service Example
@Service
@CacheConfig(cacheNames = "products") // Default cache name for this class
public class ProductCatalogService {
private final ProductRepository productRepository;
@Cacheable(key = "#productId")
public Product findById(Long productId) {
return productRepository.findById(productId)
.orElseThrow(() -> new NotFoundException("Product not found"));
}
@Cacheable(key = "'all:' + #pageable.pageNumber")
public Page<Product> findAll(Pageable pageable) {
return productRepository.findAll(pageable);
}
@CachePut(key = "#result.id")
public Product create(CreateProductRequest request) {
Product product = new Product(request.name(), request.price());
return productRepository.save(product);
}
@CachePut(key = "#productId")
public Product update(Long productId, UpdateProductRequest request) {
Product product = productRepository.findById(productId).orElseThrow();
product.setName(request.name());
product.setPrice(request.price());
return productRepository.save(product);
}
@Caching(evict = {
@CacheEvict(key = "#productId"),
@CacheEvict(cacheNames = "products", key = "'all:*'",
allEntries = true)
})
public void delete(Long productId) {
productRepository.deleteById(productId);
}
}
Key Takeaways
Caching आवश्यक है: यह read-heavy workloads के लिए आपकी application को 50x-500x तेज़ बना सकती है। अपने सबसे अधिक queried methods पर @Cacheable से शुरू करें।
Redis आपका सबसे अच्छा दोस्त है: rich data structures (String, Hash, List, Set, Sorted Set) के साथ एक in-memory key-value store। यह तेज़, simple, और battle-tested है।
Multi-tier caching: सबसे hot data के लिए L1 (Caffeine), instances के बीच shared data के लिए L2 (Redis), source of truth के रूप में L3 (Database) का उपयोग करें।
सही pattern चुनें: simplicity के लिए Cache-Aside, consistency के लिए Write-Through, speed के लिए Write-Behind, clean code के लिए Read-Through।
Invalidation मायने रखती है: अपनी baseline के रूप में TTL का उपयोग करें, real-time accuracy के लिए event-based, और complex scenarios के लिए version-based। हमेशा plan करें कि stale data कैसे cleaned up होता है।
Thoughtfully configure करें: प्रति cache अलग TTLs set करें, debugging के लिए JSON serialization का उपयोग करें, HIT/MISS ratios देखने के लिए logging enable करें, और Redis में maxmemory-policy set करें।
अक्सर पूछे जाने वाले प्रश्न
1. मुझे caching का उपयोग कब करना चाहिए और कब टालना चाहिए?
Caching का उपयोग करें जब आपका data लिखे जाने की तुलना में बहुत अधिक बार पढ़ा जाता है (एक product catalog की तरह — 10,000 बार पढ़ा जाता है, दिन में एक बार updated होता है)। लगातार बदलते data (एक live stock ticker की तरह) या ऐसे data के लिए caching से बचें जो real-time में 100% accurate होना चाहिए (एक transaction के दौरान account balances की तरह)। एक अच्छा नियम: अगर एक ही query एक ही result के साथ प्रति मिनट 10 से अधिक बार चलती है, तो इसे cache करें।
2. अगर Redis down हो जाता है तो क्या होगा? क्या मेरी application crash हो जाएगी?
अगर आप इसे सही तरीके से design करते हैं तो नहीं। हमेशा cache को optional मानें। अगर Redis down है, तो आपकी application सीधे database को query करने पर fall back होनी चाहिए — यह slower होगी, लेकिन यह अभी भी काम करेगी। Spring Boot का @Cacheable डिफ़ॉल्ट रूप से इसे gracefully handle करता है। Production में, high availability के लिए Redis Sentinel (automatic failover) या Redis Cluster (कई nodes में data) का उपयोग करें।
3. मैं अपने cache के लिए सही TTL कैसे तय करूँ?
यह इस पर निर्भर करता है कि आपका data कितना stale हो सकता है। Weather data? 10 मिनट ठीक है। User profile? 1-2 घंटे। Product listings? 30 मिनट। Blog posts? 6-24 घंटे। एक conservative (shorter) TTL से शुरू करें, अपने cache hit ratio की निगरानी करें, और अगर आपका data शायद ही कभी बदलता है तो इसे बढ़ाएँ। एक अच्छा target 90% या उच्च cache hit ratio है।
4. Caffeine और Redis caching के बीच क्या अंतर है?
Caffeine आपकी application की JVM memory के अंदर चलता है — यह अविश्वसनीय रूप से तेज़ है (nanoseconds) लेकिन हर server की अपनी copy होती है, और ऐप restart होने पर data खो जाता है। Redis एक अलग server के रूप में चलता है — यह थोड़ा slower है (sub-millisecond network call) लेकिन आपकी सभी ऐप instances एक ही cache साझा करती हैं, और data restarts से बचता है। ऐसे ultra-hot data के लिए Caffeine का उपयोग करें जो शायद ही कभी बदलता है, instances के बीच shared data के लिए Redis।
5. क्या मैं microservices architecture के साथ caching का उपयोग कर सकता हूँ?
बिल्कुल। Redis microservices के लिए perfect है क्योंकि यह एक shared cache के रूप में काम करता है जिसे सभी services access कर सकती हैं। हर service को collisions से बचने के लिए अपनी keys को namespace करना चाहिए (जैसे product-service:product:42, user-service:profile:99)। event-based invalidation (Redis Pub/Sub या Kafka जैसा message broker) का उपयोग करें ताकि जब एक service data update करे, तो अन्य services जिन्होंने इसे cache किया है, अपनी copies को invalidate कर सकें।