Java Caching Strategies With NCache
Explore the open-source library NCache and discover how it can help implement various caching strategies in Java applications.
Join the DZone community and get the full member experience.
Join For FreeCaching is a technique to store frequently accessed data in a temporary storage area. This helps to reduce the load on the primary data source and improves the performance of the application. There are various caching strategies available, and choosing the right one is crucial.
In this article, we'll explore the open-source library, NCache, and see how it can help implement different caching strategies in Java applications.
Introduction to NCache
NCache is an open-source distributed caching solution that helps improve the performance and scalability of applications. It provides features like data caching, session caching, and object caching to store data in memory and reduce the load on the primary data source.
NCache supports various technologies like Java, .NET, and Node.js, making it a versatile choice for caching in different types of applications.
Installing NCache
Before installing NCache, it's important to understand the system prerequisites.
Let's go over a few ways to install NCache on a machine running Java:
- Windows — NCache provides a Windows installer that helps us install and configure NCache using a GUI. Alternatively, we can use a CLI to install NCache.
- Linux — For Linux, NCache provides a tar.gz file-based installation.
Docker Image
NCache also provides a docker image to enable easy installation across systems. We can use the below commands to install NCache with Docker.
Pulling an Image
docker pull alachisoft/ncache:latest-java
Starting NCache
docker run --name ncache -itd --network host alachisoft/ncache:latest-java
- -itd starts the container in detached mode and opens it for interaction through a terminal.
- --network host tells the host that the container will connect directly with the host's network.
Interacting With the Container
To interact with the application using a bash terminal, we can use:
docker exec -it ncache bash
Similarly, to use Powershell on Windows, we can use:
docker exec -it ncache PowerShell
Using NCache in Java
NCache provides a Java client library that can be used to interact with the NCache server and perform caching operations. Let's look at how we can use NCache in Java applications.
Before we start coding on the client side, we'll need to set up a cache using NCache.
Adding NCache Dependency
We'll start by adding the NCache dependency to our project. Here's an example of how to add the NCache dependency using Maven:
<dependency>
<groupId>com.alachisoft.ncache</groupId>
<artifactId>ncache-client</artifactId>
<version>5.3.3</version>
</dependency>
Please note: The version number may vary based on the latest release.
Code Setup
We'll set up a simple Java application with a data layer, service layer, and caching layer to demonstrate the usage of NCache. Using this example, we'll explore different caching patterns and how they can be implemented using NCache.
Let's define a simple POJO class to represent a user:
public class User {
private int id;
private String name;
// getters, setters, and constructors
}
Next, we'll create a database layer that simulates interactions with a database:
public class DatabaseService {
public User getUser(int id) {
// Simulate fetching user data from the database
return new User(id, "John Doe");
}
// other database operations
}
We'll not focus on the actual database operations, as the focus of this article is on caching. To demonstrate the caching patterns, we'll create a service in the coming sections that interacts with the caching layer and the data layer(if needed).
Connecting to NCache
To connect to the NCache server, we can follow one of the below methods:
Using Config File
We can define connection properties in a configuration file and use it to connect to the NCache server. Here's an example of how to define the connection properties in a client.ncconf
file:
<configuration>
<ncache-server connection-retries="5" retry-connection-delay="0" retry-interval="1" command-retries="3" command-retry-interval="0.1" client-request-timeout="90" connection-timeout="5" port="9800" local-server-ip="20.200.20.38" enable-keep-alive="true" keep-alive-interval="30" />
<cache id="demoCache" client-cache-id="" client-cache-syncmode="optimistic" default-readthru-provider="" default-writethru-provider="" load-balance="True" enable-client-logs="False" log-level="error">
<server name="20.200.20.38"/>
<server name="20.200.20.23"/>
</cache>
</configuration>
Let's look at a few important attributes in the configuration file:
ncache-server
: Defines the connection properties like connection retries, retry interval, and timeouts.cache
: Defines the cache properties like cache ID, synchronization mode, and server details.cache/server
: Specifies the server IP or hostname where the cache is running.
This file should be placed either in the application folder or at %NCHOME%\config
in Windows or /opt/ncache/config
in Linux.
Using Code
Alternatively, we can connect to the NCache server programmatically by specifying the connection properties in the code:
public class NCacheService {
private final Cache cache;
public NCacheService() throws Exception {
CacheConnectionOptions cacheConnectionOptions = new CacheConnectionOptions();
cacheConnectionOptions.UserCredentials = new Credentials("domain\\user-id", "password");
cacheConnectionOptions.ServerList = new List<ServerInfo>() {
new ServerInfo("remoteServer",9800);
};
cache = CacheManager.GetCache("demoCache", cacheConnectionOptions);
}
}
Here, we have a small code snippet that connects to the NCache server using the CacheConnectionOptions
class. We specify the server details and user credentials to connect to the cache. Similarly, we can provide all properties that are available in the configuration file programmatically.
Please note that if both the configuration file and code-based connection properties are provided, the code-based properties take precedence.
Code-based connection is useful when we need to dynamically change the connection properties based on the environment or other factors. Configuration files are more suitable when the connection properties are static and do not change frequently.
Caching Patterns
Caching patterns define how data is stored and retrieved from the cache. Let's explore some common caching patterns and how they can be implemented using NCache.
Cache-Aside
One of the simplest caching patterns is the Cache-Aside pattern. In this pattern, the application code is responsible for checking the cache before accessing the primary data source. If the data is not found in the cache, it is fetched from the primary data source and stored in the cache for future access.
To demonstrate this, let's define a service class that implements the Cache-Aside pattern using NCache:
public class CacheAsideService {
private final DatabaseService databaseService;
private final CacheService cacheService;
public CacheAsideService() {
databaseService = new DatabaseService();
cacheService = new NCacheService();
}
public User getUser(int id) {
User user = cacheService.getUser(id);
if (user == null) {
user = databaseService.getUser(id);
cacheService.addUser(user);
}
return user;
}
public void addUser(User user) {
databaseService.addUser(user);
cacheService.addUser(user); //optional
}
// other service methods
}
To demonstrate read operations, we have the getUser
method:
- It checks the cache for the user data.
- If the data is found in the cache, it returns the data.
- If the data is not found in the cache, it fetches the data from the database and stores it in the cache. It then returns the data.
Next, we have the addUser
method:
- It adds the user data to the database.
- Next, it adds the user data to the cache. This is optional and depends on the use case. Since caches have limited capacity, it's important to decide whether it's essential to write data to the cache immediately or let it be populated on demand.
Read-Through
The Read-Through pattern is a caching pattern where the primary data source resides behind the cache. When a cache miss occurs, the cache fetches the data from the primary data source and stores it in the cache for future access. In this pattern, the code does not interact directly with the primary data source.
To enable this behavior in NCache, the cache server needs to be configured to fetch the data from the primary data source when a cache miss occurs. This is handled by configuring a Read-Through provider that specifies how to fetch the data from the primary data source.
From the code perspective, the Read-Through pattern is transparent to the application code. The cache handles the data fetching and storing operations. Let's see how the code changes for the Read-Through pattern:
public class ReadThruCacheService {
private final DatabaseService databaseService;
private final Cache cache;
private final ReadThruOptions readThruOptions = new ReadThruOptions(ReadMode.ReadThru, "provider-name");
public ReadThruCacheService() throws Exception {
databaseService = new DatabaseService();
cache = CacheManager.getCache("mycache");
}
public User getUser(String id) throws CacheException {
User user = cache.get(id, readThruOptions, User.class);
if (user == null) {
user = databaseService.getUser(id);
cache.add(id, new CacheItem(user));
}
return user;
}
// other service methods
}
In this code snippet, we define a ReadThruCacheService
class that interacts with the cache using the Read-Through pattern.
- We specify the Read-Through options that define the behavior of the cache when a cache miss occurs. It requires a provider name that identifies the Read-Through provider configured in the cache server.
- When fetching the user data, we use the
cache.get
method with the Read-Through options. If the data is not found in the cache, the cache fetches the data from the primary data source using the configured Read-Through provider.
Write-Through/Write-Behind
The Write-Through pattern is used to synchronize the cache with the primary data source when data is updated. When a write operation occurs:
- the code calls the cache to update the data,
- the cache updates the primary data source first,
- if the primary data source update is successful, the cache updates the data in the cache.
Since this process is synchronous, it can impact the application's performance. It is recommended to use the Write-Through pattern for critical data that needs to be consistent across the cache and the primary data source. If performance is a concern, the Write-Behind pattern can be used. In this pattern, the cache updates the primary data source asynchronously, reducing the impact on the application's performance.
To implement the Write-Through pattern in NCache, we need to configure a Write-Through provider that specifies how to update the primary data source when a write operation occurs. Similarly, to use asynchronous updates, we can configure a Write-Behind provider that enables asynchronous updates.
Once we have the providers configured, let's look at the code changes for the Write-Through pattern:
public class WriteThruCacheService {
private final DatabaseService databaseService;
private final Cache cache;
private final WriteThruOptions writeThruOptions = new WriteThruOptions(WriteMode.WriteThru, "provider-name");
public WriteThruCacheService() throws Exception {
databaseService = new DatabaseService();
cache = CacheManager.getCache("mycache");
}
public void addUser(User user) throws CacheException {
cache.add(user.getId(), new CacheItem(user), writeThruOptions);
}
// other service methods
}
We provide WriteThruOptions to the cache when adding/updating data. The cache then updates the primary data source using the configured Write-Through provider. Here, we have used the WriteMode.WriteThru option, which ensures that the cache updates the primary data source synchronously. Similarly, we can use the WriteMode.WriteBehind option for asynchronous updates.
Comparing Caching Patterns
Now that we have explored different caching patterns and how they can be implemented using NCache, let's compare them based on their use cases, pros, and cons:
Pattern | Use Case | Pros | Cons |
---|---|---|---|
Cache-Aside | Frequently accessed data | Simple to implement | Cache updates and eviction needs to be handled through the client code |
Read-Through | Data that is read more often than written | Transparent to application code | Performance impact on cache misses |
Write-Through | Critical data that needs to be consistent | Ensures data consistency across cache and primary data source | Synchronous updates can impact performance |
Write-Behind | Performance-critical applications | Asynchronous updates reducthe e impact on performance | Data may be inconsistent temporarily |
Conclusion
Choosing the right caching pattern is crucial for the performance and scalability of applications. NCache provides a robust caching solution that supports various caching patterns like Cache-Aside, Read-Through, Write-Through, and Write-Behind.
In this article, we explored how these caching patterns can be implemented using NCache in Java applications. By understanding the use cases, pros, and cons of each pattern, developers can make informed decisions on selecting the appropriate caching strategy for their applications.
Opinions expressed by DZone contributors are their own.
Comments