Java में Hazelcast: Spring Boot Applications के लिए Distributed In-Memory Grid
Java में Hazelcast का व्यावहारिक, code-first tour — Spring Boot applications के लिए cache, coordination layer, और distributed compute engine का काम करने वाला in-memory data grid। IMap, EntryProcessor, MapStore, Near Cache, और कब इसे इस्तेमाल करना है इस पर चर्चा।
ज़्यादातर caches एक डिब्बा हैं जिनसे आप network पर बात करते हैं। Hazelcast ख़ुद network है। यही फ़र्क़ पूरी बात है — और इस post की पूरी pitch भी।
Hazelcast सच में क्या है
ज़्यादातर teams cache की ओर इसलिए बढ़ती हैं कि read traffic के लिए database बहुत slow है। वे Redis install करते हैं, कुछ methods पर @Cacheable लगाते हैं, और dashboard हरा हो जाता है। यह तब तक ठीक है जब तक caching से ज़्यादा कुछ ज़रूरी नहीं हो जाता। जब तक एक ऐसे counter की ज़रूरत नहीं पड़ती जिस पर दो services को सहमत होना है। एक queue की जो node restart पर नहीं खोनी चाहिए। एक तरीक़ा जिससे cluster-भर में किसी record को lock किया जा सके जब कोई long-running job उसे छू रहा हो। जिस क्षण इनमें से कोई भी ज़रूरत पड़ती है, simple cache काफ़ी नहीं रहती।
Hazelcast वही है जो आपको तब मिलता है जब आप "cache" लेकर पूछते हैं: अगर cache ही cluster हो तो? यह एक in-memory data grid (IMDG) है — JVMs का peer-to-peer network जो data साझा करते हैं, code साथ चलाते हैं, और चुपचाप partitioning, replication, और failover संभालते हैं। आपको distributed Map, distributed Queue, distributed Lock, एक ExecutorService जो उसी node पर tasks चलाता है जहाँ data पहले से है, और Spring Boot integration मिलती है जो ज़्यादातर चीज़ों को साधारण caching जैसा बना देती है।
यह एक tour है। हम "अपने Spring Boot app में embed करो और cache की तरह इस्तेमाल करो" से "distributed system के लिए coordination layer के रूप में इस्तेमाल करो" तक चलेंगे। पहले code, राय वहीं जहाँ वह काम लायक हो।
Embedded बनाम Client-Server — एक जल्दी चुनिए
Hazelcast दो आकार में चलता है, और चुनाव बाक़ी सब कुछ रंगता है।
Embedded mode. Hazelcast आपके application JVM के अंदर रहता है। आपकी service का हर instance भी एक Hazelcast member है, और वे network पर automatically cluster बना लेते हैं। साथ रहने का मतलब है data lookups अक्सर local memory access — कोई network hop नहीं। trade-off: आप अपने data grid को application से अलग scale नहीं कर सकते। आपकी service 3 से 30 nodes तक बढ़ी, तो grid भी बढ़ी।
Client-server mode. Hazelcast अपना dedicated JVMs cluster की तरह चलता है। आपका application उससे एक thin client के माध्यम से बात करता है। यह वैसा ही है जैसे teams Redis को इस्तेमाल करती हैं। data tier को अलग size कर सकते हैं, data खोए बिना application nodes restart कर सकते हैं, और एक ही grid पर polyglot stack चला सकते हैं।
एक ही Java service ship करने वाली ज़्यादातर Spring Boot teams के लिए embedded mode सबसे आसान शुरुआत है। Multi-language stacks के लिए या जब ops cache को अलग tier रखना चाहते हैं, client-server सही है।
Spring Boot में Hazelcast Setup करना
Spring Boot में built-in support है — classpath पर embedded cluster के लिए dependency जोड़ देना काफ़ी है।
<!-- pom.xml -->
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-spring</artifactId>
<version>5.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
एक न्यूनतम config — आप classpath पर एक hazelcast.yaml रख सकते हैं, या Java से config बना सकते हैं।
// src/main/resources/hazelcast.yaml
hazelcast:
cluster-name: orders-cluster
network:
port:
auto-increment: true
port: 5701
join:
multicast:
enabled: false
tcp-ip:
enabled: true
member-list:
- 10.0.0.10
- 10.0.0.11
- 10.0.0.12
map:
products:
time-to-live-seconds: 300
max-size:
policy: PER_NODE
max-size: 10000
eviction:
eviction-policy: LRU
या Java में, अगर आप पसंद करें:
@Configuration
public class HazelcastConfig {
@Bean
public Config hazelcastConfig() {
Config config = new Config();
config.setClusterName("orders-cluster");
MapConfig productsMap = new MapConfig("products")
.setTimeToLiveSeconds(300)
.setEvictionConfig(new EvictionConfig()
.setEvictionPolicy(EvictionPolicy.LRU)
.setMaxSizePolicy(MaxSizePolicy.PER_NODE)
.setSize(10_000));
config.addMapConfig(productsMap);
return config;
}
}
अपनी application class में @EnableCaching जोड़ दीजिए और Spring @Cacheable के लिए Hazelcast को automatically इस्तेमाल करेगा।
@SpringBootApplication
@EnableCaching
public class OrdersApplication {
public static void main(String[] args) {
SpringApplication.run(OrdersApplication.class, args);
}
}
आपको मिलने वाले Distributed Data Structures
Hazelcast की क़ीमत building blocks से आती है। वे जाने-पहचाने Java APIs जैसे दिखते हैं, पर उनकी state cluster भर में share होती है, scale के लिए partitioned और failover के लिए replicated होती है। ये वही हैं जिन तक आप अक्सर पहुँचेंगे।
IMap — Distributed HashMap
IMap मुख्य workhorse है। यह ConcurrentHashMap जैसा दिखता है, पर इसकी keys cluster में partitioned हैं, और हर partition एक या एक से अधिक backup nodes पर replicate होती है।
@Service
public class ProductCatalog {
private final HazelcastInstance hazelcast;
public ProductCatalog(HazelcastInstance hazelcast) {
this.hazelcast = hazelcast;
}
public Product get(String productId) {
IMap<String, Product> map = hazelcast.getMap("products");
return map.get(productId);
}
public void put(String productId, Product product) {
IMap<String, Product> map = hazelcast.getMap("products");
map.put(productId, product);
}
public Product getOrLoad(String productId) {
IMap<String, Product> map = hazelcast.getMap("products");
return map.computeIfAbsent(productId, this::loadFromDatabase);
}
private Product loadFromDatabase(String productId) {
// hits the DB
return null;
}
}
IMap आपको predicates, indexes, और entry listeners मुफ़्त में देता है। आप map.values(Predicates.equal("category", "books")) कर सकते हैं और Hazelcast हर partition पर predicate को parallel push कर देता है।
IQueue, ITopic — Cluster-Wide Messaging
एक ऐसी queue चाहिए जो node restart झेल जाए और जो भी member ज़िंदा हो वह consume कर ले? IQueue एक distributed BlockingQueue है।
IQueue<OrderEvent> queue = hazelcast.getQueue("order-events");
queue.put(new OrderEvent(orderId, "PLACED")); // producer
OrderEvent next = queue.take(); // consumer (any node)
broadcast के लिए — हर node हर message सुने — ITopic इस्तेमाल करें:
ITopic<CacheInvalidate> topic = hazelcast.getTopic("cache-invalidations");
topic.addMessageListener(message -> {
String key = message.getMessageObject().getKey();
localCache.remove(key);
});
topic.publish(new CacheInvalidate("product:123"));
IAtomicLong, FencedLock — Coordination Primitives
दो services को अगले sequence number पर सहमत होना है। या उनमें से एक को long-running operation के दौरान record पर exclusive lock चाहिए। Hazelcast आपको वही primitives देता है जो आप एक JVM में इस्तेमाल करते, पर cluster-भर में।
// Cluster-wide counter
IAtomicLong sequence = hazelcast.getCPSubsystem().getAtomicLong("invoice-seq");
long next = sequence.incrementAndGet();
// Cluster-wide lock (with timeout — never use the unbounded version in production)
FencedLock lock = hazelcast.getCPSubsystem().getLock("order:" + orderId);
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
// do the thing only one cluster member should be doing
} finally {
lock.unlock();
}
}
CPSubsystem Raft consensus algorithm पर चलता है — ये primitives network partitions के बीच भी सही हैं, इसकी क़ीमत quorum की ज़रूरत है। उसे वहाँ इस्तेमाल कीजिए जहाँ correctness मायने रखती है; इन्हीं APIs के AP versions तेज़ होते हैं पर eventually consistent।
MultiMap और ReplicatedMap
MultiMap एक key के कई values store करता है — tag indexes या one-to-many lookups के लिए उपयोगी। ReplicatedMap पूरा dataset हर node पर रखता है — बहुत छोटे, बहुत बार पढ़े जाने वाले reference data के लिए perfect, क्योंकि हर read एक local memory hit है।
MultiMap<String, String> tagsByPost = hazelcast.getMultiMap("post-tags");
tagsByPost.put("post-1", "java");
tagsByPost.put("post-1", "hazelcast");
Collection<String> tags = tagsByPost.get("post-1"); // [java, hazelcast]
ReplicatedMap<String, String> countryByCode = hazelcast.getReplicatedMap("countries");
countryByCode.put("US", "United States"); // copied to every node
Hazelcast से समर्थित Spring @Cacheable
ज़्यादातर teams पहले जो इस्तेमाल करती हैं वही सबसे आसान भी है: methods पर annotation लगाइए, काम Spring पर छोड़िए, Hazelcast को drop-in cache की तरह treat कीजिए।
@Service
public class ProductService {
private final ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
@Cacheable(value = "products", key = "#productId")
public Product getProduct(String productId) {
log.info("Loading product {} from DB", productId);
return repository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
}
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return repository.save(product);
}
@CacheEvict(value = "products", key = "#productId")
public void deleteProduct(String productId) {
repository.deleteById(productId);
}
@CacheEvict(value = "products", allEntries = true)
public void clearAll() {
// wipes the products cache cluster-wide
}
}
Redis-backed cache से क्या अलग है: जब एक node product update करता है, तो हर दूसरा node उसे तुरंत देखता है क्योंकि underlying IMap वही distributed object है। "services के बीच cache invalidation" की समस्या हल करने की ज़रूरत ही नहीं — cache स्वयं shared है।
Near Cache — जब reads ही ज़्यादातर traffic हैं
एक in-memory grid भी थोड़ी क़ीमत चुकाता है जब data किसी और partition पर हो। बहुत hot keys के लिए — एक config table, एक feature-flag map — network hop जुड़ जाता है। Near Cache इसका हल है, हाल ही में access की गई entries की एक local copy रखता है, और source बदलने पर grid invalidations push कर देता है।
NearCacheConfig nearCacheConfig = new NearCacheConfig()
.setName("default")
.setInMemoryFormat(InMemoryFormat.OBJECT)
.setMaxIdleSeconds(60)
.setEvictionConfig(new EvictionConfig()
.setSize(1000)
.setEvictionPolicy(EvictionPolicy.LRU));
MapConfig featureFlags = new MapConfig("feature-flags")
.setNearCacheConfig(nearCacheConfig);
पहले hit के बाद अब reads सीधे local memory से आते हैं। stale-data risk invalidation push और max-idle से सीमित है। "कितनी पुरानी data बहुत पुरानी है" का सही answer एक product सवाल है, technology का नहीं — framework default पर नहीं, data कैसा behave करता है इस पर चुनिए।
MapStore — Database में Read-Through और Write-Through
कभी-कभी आप चाहते हैं कि grid ही access layer हो। application map.get(id) कॉल करती है, और entry memory में नहीं हो तो Hazelcast database से fetch करता है, return करता है, और cache कर देता है। map.put(id, value) पर Hazelcast आपके लिए database में persist कर देता है। यही MapStore है।
public class ProductMapStore implements MapStore<String, Product> {
private final JdbcTemplate jdbc;
public ProductMapStore(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Override
public Product load(String key) {
return jdbc.queryForObject(
"SELECT id, name, price FROM products WHERE id = ?",
new ProductRowMapper(),
key);
}
@Override
public Map<String, Product> loadAll(Collection<String> keys) {
// batch load
return jdbc.query(
"SELECT id, name, price FROM products WHERE id IN (...)",
new ProductRowMapper())
.stream()
.collect(Collectors.toMap(Product::getId, p -> p));
}
@Override
public void store(String key, Product value) {
jdbc.update(
"INSERT INTO products(id, name, price) VALUES(?, ?, ?) " +
"ON CONFLICT (id) DO UPDATE SET name = ?, price = ?",
value.getId(), value.getName(), value.getPrice(),
value.getName(), value.getPrice());
}
@Override
public void delete(String key) {
jdbc.update("DELETE FROM products WHERE id = ?", key);
}
}
Config में wired up:
MapStoreConfig mapStoreConfig = new MapStoreConfig()
.setEnabled(true)
.setImplementation(productMapStore)
.setWriteDelaySeconds(0); // 0 = synchronous write-through; >0 = write-behind batched
config.getMapConfig("products").setMapStoreConfig(mapStoreConfig);
Write-behind (writeDelaySeconds > 0) throughput के लिए writes को batch करता है। इसका मतलब यह भी है कि node crash उन writes को खो सकता है जिन्हें अभी तक flush नहीं किया गया — स्पष्ट trade-off, इसे वहाँ document कीजिए जहाँ team देखेगी।
EntryProcessor — Updates जो Data पर ही रहते हैं
entry update करने का स्वाभाविक तरीक़ा "get, modify, put" है। distributed system में यह दो network round-trips है और एक window जिसमें दो clients एक-दूसरे के changes को मिटा सकते हैं। EntryProcessor उस partition को code भेजता है जो data का मालिक है और उसे वहीं atomically चलाता है।
public class IncrementStockProcessor implements EntryProcessor<String, Product, Integer> {
private final int delta;
public IncrementStockProcessor(int delta) { this.delta = delta; }
@Override
public Integer process(Map.Entry<String, Product> entry) {
Product product = entry.getValue();
if (product == null) return 0;
product.setStock(product.getStock() + delta);
entry.setValue(product);
return product.getStock();
}
}
// Usage
IMap<String, Product> products = hazelcast.getMap("products");
Integer newStock = (Integer) products.executeOnKey("sku-42", new IncrementStockProcessor(-1));
एक round-trip, कोई race नहीं, कोई manual lock नहीं। aggregations के लिए — "इस predicate से match करने वाली हर entry पर इस counter को increment करो" — आप executeOnEntries(processor, predicate) इस्तेमाल करते हैं और Hazelcast सभी partitions पर processor parallel चलाता है।
IExecutorService से Distributed Compute
Grid सिर्फ़ data store नहीं करता — code भी चलाता है। IExecutorService एक cluster-aware ExecutorService है जो किसी specific member को, सभी members को, या सबसे उपयोगी रूप में "इस key के owner को" task submit कर सकता है, ताकि task data पर चले, network पर नहीं।
IExecutorService executor = hazelcast.getExecutorService("default");
// Run on a specific key's owner — task arrives where the data already lives
Future<Integer> future = executor.submitToKeyOwner(new InventoryCheck("sku-42"), "sku-42");
// Or run on every member and aggregate
Map<Member, Future<Long>> results = executor.submitToAllMembers(new RowCountTask());
long total = results.values().stream()
.mapToLong(this::getQuietly)
.sum();
task class हर member की classpath पर होनी चाहिए। ad-hoc analytics या scheduled work के लिए जिसे बहुत data scan करना हो, यह pattern सब कुछ एक node पर वापस खींचकर centrally process करने से बेहतर है।
Production Considerations
Cluster discovery. Multicast laptop पर चलता है और production में लगभग कहीं नहीं। एक स्पष्ट member list के साथ TCP/IP इस्तेमाल कीजिए, या cloud deployments के लिए Kubernetes / AWS / Azure discovery plugins, जो members को platform की service discovery के माध्यम से एक-दूसरे को ढूँढने देते हैं।
Backups. Default में IMap हर partition का एक synchronous backup किसी अलग member पर रखता है। जिस data को आप खो नहीं सकते, उसके लिए backup-count 2 कर दीजिए — हर partition तब तीन nodes पर है, और आप दो खो सकते हैं बिना data loss के। क़ीमत: ज़्यादा memory और थोड़ी धीमी writes।
Split-brain. अगर network partition हो और cluster विभाजित हो जाए, तो दोनों आधे writes accept करते रहते हैं। connectivity वापस आने पर Hazelcast विभाजन का पता लगाता है और reconcile करने के लिए merge policy चलाता है — default पर निर्भर रहने के बजाय एक स्पष्ट चुनिए (LATEST_UPDATE, HIGHER_HITS, या एक custom policy)। जिस data को partitions के बीच निश्चित consistent होना चाहिए, उसके लिए CP subsystem इस्तेमाल कीजिए।
Memory management. Off-heap (HD-Memory) आपको Java heap के बाहर data store करने देता है, ताकि GC के पास scan करने को कुछ न रहे। multi-gigabyte caches के लिए यह snappy cluster और हर मिनट एक second के लिए ठहरने वाले cluster का फ़र्क़ है।
Observability. Hazelcast box से ही JMX metrics publish करता है, और Management Center cluster health के लिए एक UI देता है। दोनों को अपनी monitoring में wire कीजिए; slow partition के बारे में ग्राहक से पता न चले।
Hazelcast कब उठाएँ
Hazelcast toolbox में अपनी जगह तब कमाता है जब समस्या सिर्फ़ "database को तेज़ करो" नहीं है। आपको चाहिए जब आपको ज़रूरत हो:
- एक ऐसा cache जो ephemeral state (sessions, leader election state, in-flight workflow state) के लिए source of truth भी है।
- Cluster-wide coordination — distributed locks, atomic counters, message broadcast — एक अलग ZooKeeper या etcd खड़ा किए बिना।
- उस data पर compute जो इधर-उधर भेजने के लिए बहुत बड़ा है — function को data तक भेजना, data को function तक नहीं।
- एक embedded grid जहाँ हर application instance grid member भी है, और आप sub-millisecond reads चाहते हैं बिना अलग cache tier ऑपरेट किए।
अगर आपको सिर्फ़ key-value cache चाहिए और आपकी team पहले से Redis चला रही है, तो Hazelcast समस्या से ज़्यादा grid हो सकता है। उल्टा भी सच है: एक team जिसने सालों पहले Redis चुना और अब Redis जो नहीं है उसकी भरपाई के लिए distributed locks, queues, और Lua scripts छिड़कती है — उस team को अक्सर Hazelcast-shaped समस्या होती है, जिसे वे hard way हल कर रहे हैं।
एक आख़िरी बात
Tool वही चुनिए जो समस्या के आकार से मेल खाए। पहली बार Hazelcast इसलिए चुना क्योंकि official tutorial दोस्ताना था और embedded mode का मतलब एक कम चीज़ ऑपरेट करनी थी। दूसरी बार इसलिए चुना क्योंकि तीन services एक shared workflow पर coordinate करने की कोशिश कर रहे थे और मैं HTTP के ऊपर protocols invent करते-करते थक चुका था। ईमानदार pattern: एक cache जो coordination layer में बढ़ती है — ठीक यही IMDG को बनाने का मक़सद था।
अक्सर पूछे जाने वाले प्रश्न
क्या Hazelcast सिर्फ़ Redis का प्रतियोगी है?
caching में overlap है, पर यह एक अलग category का tool है। Redis rich data types और single-threaded core के साथ एक remote key-value server है। Hazelcast एक distributed in-memory grid है जो आपके JVMs के हिस्से के रूप में (या एक dedicated cluster की तरह) चलता है, parallel compute support करता है, और cluster-भर में Java-shaped concurrency primitives देता है। शुद्ध caching के लिए वे similar दिखते हैं; distributed coordination, compute-with-data, और embedded scenarios के लिए वे बराबर नहीं हैं।
क्या मैं Hazelcast को embedded या client-server mode में चलाऊँ?
Embedded operationally सरल है — आपकी service और grid एक साथ scale, deploy, fail होते हैं। Client-server तब सही है जब ops data tier को अलग size करना चाहता है, जब application restarts cache restarts नहीं हो सकते, या जब grid कई polyglot services के बीच साझा है। ज़्यादातर single-Java-service teams embedded से शुरू करते हैं और client-server तभी जाते हैं जब वे constraints दिखते हैं।
Hazelcast JCache या Caffeine से कैसे अलग है?
Caffeine एक तेज़ local cache है — एक JVM, कोई replication नहीं, कोई cluster awareness नहीं। JCache (JSR 107) एक standard cache API है, जिसे कई products (Hazelcast और Caffeine सहित) implement करते हैं। Hazelcast नीचे का distributed system है, cluster membership, partitioning, replication, और बाक़ी सब के साथ। अगर आपकी समस्या एक JVM में आ जाती है, Caffeine तेज़ और सरल है। अगर instances के बीच data साझा चाहिए, Hazelcast बड़े सवाल का बड़ा जवाब है।
Stream processing के लिए Hazelcast Jet?
Jet को core product में मिला दिया गया है। वही nodes जो आपका IMap रखते हैं, एक streaming pipeline भी चला सकते हैं जो Kafka topic से पढ़े, map से join करे, और दूसरी IMap में लिखे — सब कुछ grid के अंदर। low-latency state lookups के साथ event-driven enrichment के लिए, JVM ecosystem में Jet ज़्यादा elegant जवाबों में से एक है।
क्या Hazelcast मेरे पूरे cluster के restart होने पर ज़िंदा बचेगा?
Box से, data memory में है और full cluster restart उसे खो देता है। Full restart झेलने के लिए Persistence (पहले Hot Restart Store) configure कीजिए — entries हर member पर local disk पर mirror होती हैं, ताकि cluster वापस आते ही members अपने partitions reload करें और अपने data के साथ जुड़ें। ऐसे workloads के लिए जो किसी भी data loss को बर्दाश्त नहीं कर सकते, इसे एक MapStore के साथ मिलाइए जो database में write through करता है — fast recovery के लिए local persistence, durable record of truth के लिए database।