What is cache?
A cache is a storage layer that holds temporary data to speed up retrieval. It stores frequently used or recently accessed data to avoid repeatedly fetching it from a slower source, improving performance.
What is caching in Liferay?
Caching keeps specific data readily accessible in memory, trading memory usage for better performance. There are multiple caching strategies to find what works best for your system. For instance, if your website delivers many articles, you might consider increasing the cache limit for those.
Liferay employs a cache configuration framework built on Ehcache. This framework plays a crucial role in enhancing the performance of Liferay DXP.
Liferay's caching configuration manages the cache using two types of pools:
- MultiVM - As the name suggests, this pool is used when there are multiple nodes in Liferay cluster as data must be synchronized across multiple nodes.
- SingleVM - In this pool data is managed uniquely per VM and not replicated among nodes.
Cache types:
Liferay can cache any type of class you want. By default, Liferay DXP caches service entities and service entities finder results.
For example: Liferay’s service builder persistence layer generates caching for entity by default.
- EntityCache - It holds service entities by their primary keys, mapping them to implementation objects. The caching code uses the entity’s primary keys to retrieve these objects. An entity's PersistenceImpl.fetchByPrimaryKey method utilizes the EntityCache.
- FinderCache - It caches the result sets of finder methods, which are typically used to retrieve multiple entities based on specific query conditions.
There are two types of cache configurations available in Liferay:
- Overriding Cache: When you want to modify the existing cache configurations provided by liferay.
- Caching Data: When you want to implement the caching on external data within liferay cache.
In this blog, we will focus on configuring Caching Data. We will use MultiVM to persist external data, which will be synchronized across multiple Liferay nodes in our cluster.
Caching Data with MultiVMPool
When using MultiVMPool for caching in Liferay, both the cache key and the cache value need to be serializable. This means that any object you store in the cache must implement the Serializable interface to ensure it can be properly stored and retrieved across different JVMs in a clustered environment.
Serialization is crucial in caching because it allows data to be converted into a format that can be transferred over the network or saved in memory, enabling faster access to frequently used data while ensuring consistency across instances.
Steps to Implement Caching with MultiVMPool
Create a Metadata Class for the Cache Key
The metadata class should implement the Serializable interface, which ensures that instances of this class can be stored in the distributed cache.
public class RequestsCacheMetadata implements Serializable {
private static final long serialVersionUID = -2690397126768952119L;
private String cacheKey;
public String getCacheKey() {
return cacheKey;
}
public void setCacheKey(String cacheKey) {
this.cacheKey = cacheKey;
}
public RequestsCacheMetadata(String cacheKey) {
super();
this.cacheKey = cacheKey;
}
}
Implement the Component Class
In this class, the caching logic will be implemented. The component interacts with the MultiVMPool to store and retrieve data.
@Component(immediate = true, service = RequestsCacheApi.class)
public class RequestsCacheImpl implements RequestsCacheApi {
}
Declare Required Dependencies
Inject the MultiVMPool and declare the PortalCache to enable caching in the component class.
@Reference
private MultiVMPool _multiVMPool;
private static PortalCache<Serializable, Serializable> _portalCache;
protected static final String CACHE_NAME = RequestsCacheMetadata.class.getName();
Cache Initialization
The cache name can be derived from the metadata class, ensuring uniqueness.
This method is called when the component is activated. It initializes the _portalCache by obtaining a cache instance from the MultiVMPool.
@Activate
public void activate() {
_portalCache = (PortalCache<Serializable, Serializable>) _multiVMPool.getPortalCache(CACHE_NAME);
}
Implement Cache Methods
getRequestsFromCache(): This method is responsible for retrieving cached data. It takes an object of type RequestsCacheMetadata, which contains the cache key used to fetch the corresponding cached object.
/**
* This method is used to get the request list from cache
*
* @param RequestsCacheMetadata
*
*/
public Serializable getRequestsFromCache(RequestsCacheMetadata myRequestsMetadata) {
if (Validator.isNull(myRequestsMetadata) || Validator.isNull(myRequestsMetadata.getCacheKey())) {
return null;
}
Serializable requestCacheObject = _portalCache.get(myRequestsMetadata.getCacheKey());
return requestCacheObject;
}
PutInCache() Method (With Time To Live) : This overloaded version of the putInCache() method allows you to store an object in the cache with a specified Time-To-Live (TTL) (In Seconds), which determines how long the object will remain in the cache before expiring.
/**
* This method is used to put the request list in cache
*
* @param RequestsCacheMetadata
* @param objectToCache
* @param timeToLive
*
*/
public void putInCache(RequestsCacheMetadata myRequestsMetadata, Serializable objectToCache, int timeToLive) {
if (Validator.isNull(myRequestsMetadata) || Validator.isNull(myRequestsMetadata.getCacheKey())
|| Validator.isNull(objectToCache) || timeToLive lt: BigDecimal.ONE.intValue()) {
return;
}
_portalCache.put(myRequestsMetadata.getCacheKey(), objectToCache, timeToLive);
}
PutInCache() Method (Without Time To Live) :This version of the method stores the object in the cache without Time-To-Live. The object will stay in the cache until explicitly removed or until the cache is cleared.
/**
* This method is used to put the request list in cache
*
* @param RequestsCacheMetadata
* @param objectToCache
*
*/
public void putInCache(RequestsCacheMetadata myRequestsMetadata, Serializable objectToCache) {
if (Validator.isNull(myRequestsMetadata) || Validator.isNull(myRequestsMetadata.getCacheKey())
|| Validator.isNull(objectToCache)) {
return;
}
_portalCache.put(myRequestsMetadata.getCacheKey(), objectToCache);
}
removeFromCache() Method :This method removes an object from the cache. It takes RequestsCacheMetadata as input, checks its validity, and if valid, it removes the corresponding entry from the cache.
/**
* This method is used to remove requests list from cache
*
* @param RequestsCacheMetadata
*
*/
public void removeFromCache(RequestsCacheMetadata myRequestsMetadata) {
if (Validator.isNull(myRequestsMetadata) || Validator.isNull(myRequestsMetadata.getCacheKey())) {
return;
}
_portalCache.remove(myRequestsMetadata.getCacheKey());
}
Activate the Cache :This method is called when the component is activated. It initializes the _portalCache by obtaining a cache instance from the MultiVMPool.
@Activate
public void activate() {
_portalCache = (PortalCache<Serializable, Serializable>) _multiVMPool.getPortalCache(CACHE_NAME);
}
Deactivate the cache :This method is called when the component is deactivated. It removes the cache instance from the MultiVMPool to free up resources.
@Deactivate
public void deactivate() {
_multiVMPool.removePortalCache(CACHE_NAME);
}
Usage In Business Logic
Here's the use case for external data caching.
Create the Model class with getters and setters to extract the cached data as cached data will be serialized.
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class Request {
@JsonProperty("id")
private String id;
@JsonProperty("requestNumber")
private String requestNumber;
@JsonProperty("requestUrl")
private String requestUrl;
@JsonProperty("sourceSystem")
private String sourceSystem;
@JsonProperty("processName")
private String processName;
@JsonProperty("userId")
private String userId;
@JsonProperty("userName")
private String userName;
@JsonProperty("submittedDate")
private String submittedDate;
}
getMyRequests Method
The getMyRequests method retrieves user requests and is designed to first check if data is available in the cache. If cached data is available, it is immediately returned. If not, the method fetches data from an external service, stores it in the cache for future use, and returns it. This helps to improve performance by minimizing redundant calls to the external service.
Here’s how the getMyRequests method works in detail:
- Expiry Time Setup:
Retrieves an expiry time using getExpiryTimeOut(). This sets the cache expiration time in seconds.
- Cache Check:
Calls getRequestsFromCache(userId), which checks if there is cached data for the specified user. If data is found, it is immediately returned.
- Fetch Data from External Service:
If no cache data exists and requestsCount is greater than 0, it directly calls _webServicesApi.getMyRequests(userId, requestsCount) to retrieve the data.
If requestsCount is 0, it creates a RequestsCacheMetadata object to serve as a cache key. It then calls _webServicesApi.getMyRequests(userId, BigDecimal.ZERO.intValue()) to retrieve the default set of requests.
- Store in Cache and Return:
If the response is not null, it is stored in the cache using _requestCacheApi.putInCache with the specified expiry time.
Finally, the JSON response string is parsed and converted into a MyRequestsResponse object using getMyRequestResponseFromResponseString, which is then returned.
public MyRequestsResponse getMyRequests(long userId, int requestsCount) {
String expiryTime = getExpiryTimeOut();
MyRequestsResponse myRequestsResponse = getRequestsFromCache(userId);
if (Validator.isNotNull(myRequestsResponse)) {
return myRequestsResponse;
}
String myRequestsResponseString = StringPool.BLANK;
if (requestsCount > BigDecimal.ZERO.intValue()) {
myRequestsResponseString = _webServicesApi.getMyRequests(userId, requestsCount);
} else {
RequestsCacheMetadata requestCacheMetadata = new RequestsCacheMetadata(String.valueOf(userId));
myRequestsResponseString = _webServicesApi.getMyRequests(userId, BigDecimal.ZERO.intValue());
if (Validator.isNotNull(myRequestsResponseString)) {
_requestCacheApi.putInCache(requestCacheMetadata, (Serializable) myRequestsResponseString,
Integer.parseInt(expiryTime) * 60);
}
}
myRequestsResponse = getMyRequestResponseFromResponseString(myRequestsResponseString);
return myRequestsResponse;
}
getRequestsFromCache Method
This method attempts to retrieve the cached data for the specified userId:
- Create Cache Metadata:
It creates a RequestsCacheMetadata object for the userId as a unique cache key.
- Retrieve Data from Cache:
Calls _requestApi.getRequestsFromCache(requestMetadata) to retrieve data for this key.
- Parse and Return Response:
Converts the cached JSON string into a MyRequestsResponse object using the getMyRequestResponseFromResponseString method.
private MyRequestsResponse getRequestsFromCache(long userId) {
RequestsCacheMetadata requestMetadata = new RequestsCacheMetadata(String.valueOf(userId));
String requestsResponseString = (String) _kuRequestApi.getRequestsFromCache(requestMetadata);
MyRequestsResponse requestsResponse = getMyRequestResponseFromKUReponseString(requestsResponseString);
return requestsResponse;
}
getMyRequestResponseFromResponseString Method
This method is responsible for converting a JSON string response from the external API into a MyRequestsResponse object.
- Check for Null:
It first verifies that myRequestsResponseString is not null.
- Parse JSON:
Using the Jackson ObjectMapper, it reads the JSON response string and maps it to a list of Request objects.
- Build Response:
If the list of Request objects is successfully populated, it constructs a new MyRequestsResponse object, sets the requestList, and returns the populated response object.
If parsing fails or if the response string is null, it catches the exception and returns null.
private MyRequestsResponse getMyRequestResponseFromResponseString(String myRequestsResponseString) {
try {
if (Validator.isNotNull(myRequestsResponseString)) {
ObjectMapper objectMapper = new ObjectMapper();
List<Request> requestList = objectMapper.readValue(myRequestsResponseString, new TypeReference<List<Request>>() {});
if (Validator.isNotNull(requestList)) {
MyRequestsResponse myRequestsResponse = new MyRequestsResponse();
myRequestsResponse.setRequests(requestList);
return myRequestsResponse;
}
}
} catch (JsonProcessingException jsonException) {
jsonException.printStackTrace();
}
return null;
}
Injected Dependencies
- _requestCacheApi: Provides the caching functionalities (put, get, and remove) for the user requests data. It interacts with the MultiVMPool in Liferay for managing cached data in a distributed environment.
- _webServicesApi: Used to fetch requests data from an external web service. The data returned from this service is then either cached (if not already cached) or directly returned to the client.
Conclusion
In Liferay development, implementing caching with MultiVMPool in Liferay enhances application performance by storing frequently accessed data in memory, significantly reducing the need for repeated calls to slower data sources. By caching external data with MultiVMPool, we leverage a distributed approach where cached data is synchronized across multiple nodes in a clustered environment. This not only speeds up data retrieval but also ensures data consistency and resilience, especially beneficial for high-traffic applications.
When both cache keys and values are made serializable, the caching mechanism becomes robust enough to handle complex data types, making it ideal for use cases like frequently accessed user requests, session data, or other external service results. This approach to caching is flexible and efficient, allowing Liferay to scale smoothly while maintaining optimal response times and user experience.