Scaling Java Microservices to Extreme Performance Using NCache
This article will teach you how to scale the Java Microservices to achieve extreme performance using different cache techniques with NCache.
Join the DZone community and get the full member experience.
Join For FreeMicroservices have emerged as a transformative architectural approach in the realm of software development, offering a paradigm shift from monolithic structures to a more modular and scalable system. At its core, microservices involve breaking down complex applications into smaller, independently deployable services that communicate seamlessly, fostering agility, flexibility, and ease of maintenance. This decentralized approach allows developers to focus on specific functionalities, enabling rapid development, continuous integration, and efficient scaling to meet the demands of modern, dynamic business environments. As organizations increasingly embrace the benefits of microservices, this article explores the key principles, advantages, and challenges associated with this architectural style, shedding light on its pivotal role in shaping the future of software design and deployment.
A fundamental characteristic of microservices applications is the ability to design, develop, and deploy each microservice independently, utilizing diverse technology stacks. Each microservice functions as a self-contained, autonomous application with its own dedicated persistent storage, whether it be a relational database, a NoSQL DB, or even a legacy file storage system. This autonomy enables individual microservices to scale independently, facilitating seamless real-time infrastructure adjustments and enhancing overall manageability.
NCache Caching Layer in Microservice Architecture
In scenarios where application transactions surge, bottlenecks may persist, especially in architectures where microservices store data in non-scalable relational databases. Simply deploying additional instances of the microservice doesn't alleviate the problem.
To address these challenges, consider integrating NCache as a distributed cache at the caching layer between microservices and datastores. NCache serves not only as a cache but also functions as a scalable in-memory publisher/subscriber messaging broker, facilitating asynchronous communication between microservices.
Microservice Java application performance optimization can be achieved by the cache techniques like Cache item locking, grouping Cache data, Hibernate Caching, SQL Query, data structure, spring data cache technique pub-sub messaging, and many more with NCache. Please check the out-of-the-box features provided by NCache.
Using NCache as Hibernate Second Level Java Cache
Hibernate First-Level Cache
The Hibernate first-level cache serves as a fundamental standalone (in-proc) cache linked to the Session object, limited to the current session. Nonetheless, a drawback of the first-level cache is its inability to share objects between different sessions. If the same object is required by multiple sessions, each triggers a database trip to load it, intensifying database traffic and exacerbating scalability issues. Furthermore, when the session concludes, all cached data is lost, necessitating a fresh fetch from the database upon the next retrieval.
Hibernate Second-Level Cache
For high-traffic Hibernate applications relying solely on the first-level cache, deployment in a web farm introduces challenges related to cache synchronization across servers. In a web farm setup, each node operates a web server—such as Apache, Oracle WebLogic, etc.—with multiple instances of httpd processes to serve requests. Each Hibernate first-level cache in these HTTP worker processes maintains a distinct version of the same data directly cached from the database, posing synchronization issues.
This is why Hibernate offers a second-level cache with a provider model. The Hibernate second-level cache enables you to integrate third-party distributed (out-proc) caching providers to cache objects across sessions and servers. Unlike the first-level cache, the second-level cache is associated with the SessionFactory object and is accessible to the entire application, extending beyond a single session.
Enabling the Hibernate second-level cache results in the coexistence of two caches: the first-level cache and the second-level cache. Hibernate endeavors to retrieve objects from the first-level cache first; if unsuccessful, it attempts to fetch them from the second-level cache. If both attempts fail, the objects are directly loaded from the database and cached. This configuration substantially reduces database traffic, as a significant portion of the data is served by the second-level distributed cache.
NCache Java has implemented a Hibernate second-level caching provider by extending org.hibernate.cache.CacheProvider. Integrating NCache Java Hibernate distributed caching provider with the Hibernate application requires no code changes. This integration enables you to scale your Hibernate application to multi-server configurations without the database becoming a bottleneck. NCache also delivers enterprise-level distributed caching features, including data size management, data synchronization across servers, and more.
To incorporate the NCache Java Hibernate caching provider, a simple modification of your hibernate.cfg.xml and ncache.xml is all that is required.
Thus, with the NCache Java Hibernate distributed cache provider, you can achieve linear scalability for your Hibernate applications seamlessly, requiring no alterations to your existing code.
Code Snippet
// Configure Hibernate properties programmatically
Properties hibernateProperties = new Properties();
hibernateProperties.put("hibernate.connection.driver_class", "org.h2.Driver");
hibernateProperties.put("hibernate.connection.url", "jdbc:h2:mem:testdb");
hibernateProperties.put("hibernate.show_sql", "false");
hibernateProperties.put("hibernate.hbm2ddl.auto", "create-drop");
hibernateProperties.put("hibernate.cache.use_query_cache", "true");
hibernateProperties.put("hibernate.cache.use_second_level_cache", "true");
hibernateProperties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.jcache.internal.JCacheRegionFactory");
hibernateProperties.put("hibernate.javax.cache.provider", "com.alachisoft.ncache.hibernate.jcache.HibernateNCacheCachingProvider");
// Set other Hibernate properties as needed
Configuration configuration = new Configuration()
.setProperties(hibernateProperties).addAnnotatedClass(Product.class);
Logger.getLogger("org.hibernate").setLevel(Level.OFF);
// Build the ServiceRegistry
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
.applySettings(configuration.getProperties()).build();
// Build the SessionFactory
SessionFactory factory = configuration.buildSessionFactory(serviceRegistry);
// Create a List of Product objects
ArrayList<Product> products = (ArrayList<Product>) getProducts();
// Open a new Hibernate session to save products to the database. This also caches it
try (Session session = factory.openSession()) {
Transaction transaction = session.beginTransaction();
// save() method saves products to the database and caches it too
System.out.println("ProductID, Name, Price, Category");
for (Product product : products) {
System.out.println("- " + product.getProductID() + ", " + product.getName() + ", " + product.getPrice() + ", " + product.getCategory());
session.save(product);
}
transaction.commit();
System.out.println();
// Now open a new session to fetch products from the DB.
// But, these products are actually fetched from the cache
try (Session session = factory.openSession()) {
List<Product> productList = (List<Product>) session.createQuery("from Product").list();
if (productList != null) {
printProductDetails(productList);
}
}
Integrate NCache with Hibernate to effortlessly cache the results of queries. When these objects are subsequently fetched by Hibernate, they are retrieved from the cache, thereby avoiding a costly trip to the database.
From the above code sample, the products are saved in the database, and it also caches; now, when the new session opens to fetch the product details, it will fetch from the Cache and avoid an unnecessary database trip.
Learn more about Hibernate Caching
Scaling With NCache Pub/Sub Messaging
NCache is a distributed in-memory caching solution designed for .NET. Its compatibility extends to Java through a native client and third-party integrations, ensuring seamless support for both platforms.
NCache serves as an in-memory distributed data store tailored for .NET and Java, offering a feature-rich, in-memory pub/sub mechanism for event-driven communication. This makes it straightforward to set up NCache as a messaging broker, employing the Pub/Sub model for seamless asynchronous communication between microservices.
Using NCache In-Memory Pub/Sub for Microservices
NCache enables Pub/Sub functionality by establishing a topic where microservices can publish and subscribe to events. These events are published to the NCache message broker outside the microservice. Within each subscribing microservice, there exists an event handler to manage the corresponding event once it has been published by the originating microservice.
In the realm of Java microservices, NCache functions as an event bus or message broker, facilitating the relay of messages to one or multiple subscribers.
In the context of Pub/Sub models that necessitate a communication channel, NCache serves as a medium for topics. This entails the publisher dispatching messages to the designated topic and subscribers receiving notifications through the same topic. Employing NCache as the medium for topics promotes loose coupling within the model, offering enhanced abstraction and additional advantages for distributed topics.
Publish
The code snippet below initializes the messageService object using NCache MessagingService package.
Initializing the Topic
// Create a Topic in NCache.
MessagingService messagingService = cache.getMessagingService();
Topic topic = messagingService.createTopic(topicName);
// Create a thread pool for publishers
ExecutorService publisherThreadPool = Executors.newFixedThreadPool(2);
The below code snippet used to define register the subscribers to this topic
Register subscribers to this Topic
MessageReceivedListener subscriptionListener1 = new MessageReceivedListener() {
@Override
public void onMessageReceived(Object o, MessageEventArgs messageEventArgs) {
messageReceivedSubscription1(messageEventArgs.getMessage());
}
};
MessageReceivedListener subscriptionListener2 = new MessageReceivedListener() {
@Override
public void onMessageReceived(Object o, MessageEventArgs messageEventArgs) {
messageReceivedSubscription2(messageEventArgs.getMessage());
}
};
TopicSubscription subscription1 = topic.createSubscription(subscriptionListener1);
TopicSubscription subscription2 = topic.createSubscription(subscriptionListener2);
NCache provides two variants of durable subscriptions to cater to the message durability needs within your Java microservices:
- Shared Durable Subscriptions: This allows multiple subscribers to connect to a single subscription. The Round Robin approach is employed to distribute messages among the various subscribers. Even if a subscriber exits the network, messages persistently flow between the active subscribers.
- Exclusive Durable Subscriptions: In this type, only one active subscriber is allowed on a subscription at any given time. No new subscriber requests are accepted for the same subscription until the existing connection is active.
Learn more Pub/Sub Messaging with NCache implementation here Pub/Sub Messaging in Cache: An Overview
SQL Query on Cache
NCache provides your microservices with the capability to perform SQL-like queries on indexed cache data. This functionality becomes particularly beneficial when the values of the keys storing the desired information are not known. It abstracts much of the lower-level cache API calls, contributing to clearer and more maintainable application code. This feature is especially advantageous for individuals who find SQL-like commands more intuitive and comfortable to work with.
NCache provides functionality for searching and removing cache data through queries similar to SQL's SELECT and DELETE statements. However, operations like INSERT and UPDATE are not available. For executing SELECT queries within the cache, NCache utilizes ExecuteReader; the ExecuteScalar function is used to carry out a query and retrieve the first row's first column from the resulting data set, disregarding any extra columns or rows.
For NCache SQL queries to function, indexes must be established on all objects undergoing search. This can be achieved through two methods: configuring the cache or utilizing code with "Custom Attributes" to annotate object fields. When objects are added to the cache, this approach automatically creates indexes on the specified fields.
Code Snippet
String cacheName = "demoCache";
// Connect to the cache and return a cache handle
Cache cache = CacheManager.getCache(cacheName);
// Adds all the products to the cache. This automatically creates indexes on various
// attributes of Product object by using "Custom Attributes".
addSampleData(cache);
// $VALUE$ keyword means the entire object instead of individual attributes that are also possible
String sql = "SELECT $VALUE$ FROM com.alachisoft.ncache.samples.Product WHERE category IN (?, ?) AND price < ?";
QueryCommand sqlCommand = new QueryCommand(sql);
List<String> catParamList = new ArrayList<>(Arrays.asList(("Electronics"), ("Stationery")));
sqlCommand.getParameters().put("category", catParamList);
sqlCommand.getParameters().put("price", 2000);
// ExecuteReader returns ICacheReader with the query resultset
CacheReader resultSet = cache.getSearchService().executeReader(sqlCommand);
List<Product> fetchedProducts = new ArrayList<>();
if (resultSet.getFieldCount() > 0) {
while (resultSet.read()) {
// getValue() with $VALUE$ keyword returns the entire object instead of just one column
fetchedProducts.add(resultSet.getValue("$VALUE$", Product.class));
}
}
printProducts(fetchedProducts);
Utilize SQL in NCache to perform queries on cached data by focusing on object attributes and Tags, rather than solely relying on keys.
In this example, we utilize "Custom Attributes" to generate an index on the Product object.
Learn more about SQL Query with NCache in Java Query Data in Cache Using SQL
Read-Thru and Write-Thru
Utilize the Data Source Providers feature of NCache to position it as the primary interface for data access within your microservices architecture. When a microservice needs data, it should first query the cache. If the data is present, the cache supplies it directly. Otherwise, the cache employs a read-thru handler to fetch the data from the datastore on behalf of the client, caches it, and then provides it to the microservice.
In a similar fashion, for write operations (such as Add, Update, Delete), a microservice can perform these actions on the cache. The cache then automatically carries out the corresponding write operation on the datastore using a write-thru handler.
Furthermore, you have the option to compel the cache to fetch data directly from the datastore, regardless of the presence of a possibly outdated version in the cache. This feature is essential when microservices require the most current information and complements the previously mentioned cache consistency strategies.
The integration of the Data Source Provider feature not only simplifies your application code but also, when combined with NCache's database synchronization capabilities, ensures that the cache is consistently updated with fresh data for processing.
ReadThruProvider
For implementing Read-Through caching, it's necessary to create an implementation of the ReadThruProvider interface in Java
Here's a code snippet to get started with implementing Read-Thru in your microservices:
ReadThruOptions readThruOptions = new ReadThruOptions(ReadMode.ReadThru, _readThruProviderName);
product = _cache.get(_productId, readThruOptions, Product.class);
Read more about Read-Thru implementation here: Read-Through Provider Configuration and Implementation
WriteThruProvider:
For implementing Write-Through caching, it's necessary to create an implementation of the WriteThruProvider interface in Java
The code snippet to get started with implementing Write-Thru in your microservices:
_product = new Product();
WriteThruOptions writeThruOptions = new WriteThruOptions(WriteMode.WriteThru, _writeThruProviderName)
CacheItem cacheItem= new CacheItem(_customer)
_cache.insert(_product.getProductID(), cacheItem, writeThruOptions);
Read more about Write-Thru implementation here: Write-Through Provider Configuration and Implementation
Summary
Microservices are designed to be autonomous, enabling independent development, testing, and deployment from other microservices. While microservices provide benefits in scalability and rapid development cycles, some components of the application stack can present challenges. One such challenge is the use of relational databases, which may not support the necessary scale-out to handle growing loads. This is where a distributed caching solution like NCache becomes valuable.
In this article, we have seen the variety of ready-to-use features like pub/sub messaging, data caching, SQL Query, Read-Thru and Write-Thru, and Hibernate second-level Java Cache techniques offered by NCache that simplify and streamline the integration of data caching into your microservices application, making it an effortless and natural extension.
Opinions expressed by DZone contributors are their own.
Comments