Introduction
Caching is a technique to store the data in the memory of the server. It will improve the performance of the application by reducing the database trips. Indirectly it will boost the database performance by delivering high throughput and lower latency than most databases can offer. In this blog, we will go through the caching techniques/patterns like full Cache, cache-aside, and read-through with NCache as a cache service provider.
What is NCache?
NCache is an in-memory distributed cache for .NET, Java, and Node.js, and it is also open-source. NCache is super-fast and scalable and caches application data to reduce database trips. NCache is used to overcome the performance issues related to data storage, databases, and scaling the .NET, Java, and Node.js applications.
Get started with NCache here. Please go through the installation process of NCache.
Full Cache
A full cache is a technique that loads all the datasets during the application startup. This Technique is commonly used for applications where the data is static/has no frequent changes. It improves the application performance by reducing the data source trips. NCache provides a feature called Cache Startup loader to implement the Full Cache. In other words, it pre-populates all the data in it.
Cache Startup Loader can easily integrate into our existing or new applications by implementing the ICacheLoader interface. The next step is enabling the Cache Loader/Refresher from NCache Web Manager. This feature is available only in the enterprise edition.
Launch NCache Web Manager and select the existing Clustered Cache or create a new clustered cache with minimum nodes to configure the full Cache and click on view details.
It will take you to the Cache Settings page, where you can enable Cache Loader/ Refresher.
Configure Cache Startup Loader and Refresher | NCache Docs (alachisoft.com)
Please go through the above link and complete the CacheLoader / Refresher setup.
For example, assume we have a web application that loads hundreds of images on the first page. For this case, we should go with a cache startup loader which will pre-load the Cache with all images on startup of the application so that all the images will load from the Cache, which is very fast and also avoids the data source call.
Any changes in the images from the data source will outdate the Cache. To prevent this invalidation, NCache has another feature called Cache Refresher.
Create a new class to implement IReadThruProvider.
public class LoaderRefresher: ICacheLoader {
private string _connectionString;
private static ICache _cache;
public void Init(IDictionary < string, string > parameters, string cacheName) {
if (parameters != null && parameters.Count > 0)
_connectionString = parameters.ContainsKey("ConnectionString") ?
parameters["ConnectionString"] as string : string.Empty;
_cache = CacheManager.GetCache(cacheName);
}
public object LoadDatasetOnStartup(string dataSet) {
IList < object > loadDatasetAtStartup;
if (string.IsNullOrEmpty(dataSet))
throw new InvalidOperationException("Invalid dataset.");
switch (dataSet.ToLower()) {
//call FetchProductsFromDataSouce based on dataset value
}
string[] keys = GetKeys(loadDatasetAtStartup);
IDictionary < string, CacheItem > cacheData = GetCacheItemDictionary(keys, loadDatasetAtStartup);
_cache.InsertBulk(cacheData);
object userContext = DateTime.Now;
return userContext;
}
public object RefreshDataset(string dataSet, object userContext) {
if (string.IsNullOrEmpty(dataSet))
throw new InvalidOperationException("Invalid dataset.");
DateTime ? lastRefreshTime;
switch (dataSet.ToLower()) {
//call FetchUpdatedSuppliers based on dataset value and insert the update value in cache.
}
userContext = DateTime.Now;
return userContext;
}
public IDictionary < string, RefreshPreference > GetDatasetsToRefresh(IDictionary < string, object > userContexts) {
IDictionary < string, RefreshPreference > DatasetsNeedToRefresh = new Dictionary < string, RefreshPreference > ();
DateTime ? lastRefreshTime;
bool datasetHasUpdated;
foreach(var dataSet in userContexts.Keys) {
switch (dataSet.ToLower()) {
// Based on switch input check the dataset to be updated, if it returns true call invoke DatasetsNeedToRefresh method for the dataset.
}
}
return DatasetsNeedToRefresh;
}
public void Dispose() {
// clean your unmanaged resources
}
}
The above sample code is available in GitHub – NCacheSample
Init Function will set the Local/Clustered Cache information and Database connection string information
The loadDatasetOnStartup function loads the dataset from the data source during Program Startup.
RefreshDataset
This function is triggered based on the Refresh Frequency configured during CacheLoader / Refresher setup.
GetDatasetsToRefresh
This function is used to get a new dataset in real time through polling if the refresh-on-event is enabled, as shown in the below figure. You can enable this setting from the NCache web manager cache details page under Cache Loader/Refresher section.
The Cache keeps calling this method to update the data set based on the frequency/interval set for “Refresh-Interval”. By default, it is 900 sec.
You can refresh the cached dataset manually using NCache Web Manager or Manager.
- NCache web Manager: The refresh now option for the dataset is available in the cache details page under Cache Loader/Refresher section from advanced settings.
- PowerShell command: Use the below command to refresh the dataset.
Invoke-RefresherDataset -CacheName [your clustered cache name] -Server [server IP address] -Dataset [your dataset name] -RefreshPreference RefreshNow
Cache-Aside
In the Cache-aside technique, the database and Cache won’t interact. Let me elaborate on the step-by-step process involved in this caching Technique.
Step 1. The application tries to get data from the Cache.
Step 2. In this step, the flow will check whether the request data(Key) is available in the cache server.
Step 3. If the Key is unavailable, the application will directly talk to the database.
Step 4. The data from the database is transferred to the application.
Step 5. Add the data into the Cache.
NCache, one of the leading caching service providers, to implement the cache-aside functionality in our application, the below code snippet.
try {
// Pre-condition: Cache is already connected
// Get key to fetch from cache
string key = $ "Product:{product.ProductID}";
// Get item against key from cache in template type
var retrievedItem = cache.Get < Product > (key);
if (retrievedItem != null) {
if (retrievedItem is Product) {
// Perform operations according to business logic
}
} else {
// block to query the datasource and write it to the Cache server
}
}
So, in the process, if the data is not cached, the application will directly connect with the database to get new data. Each Technique in caching has its pros and cons.
Advantages
If the caching layer is unavailable, the application still works. Instead of querying the caching, you will have complete control of the database every time.
Disadvantages
The very first time, the access will be slower. Because the keys may not be available in the Cache, so initially, the application will update the Cache by fetching the data from the database. This process may slow down the performance of the application. On the other hand, you will have inconsistent data. A separate service is required to update the data in the Cache whenever there is an update in the database for a table.
Read-through technique
Read-thru is another technique where the cache layer is always synced with the database layer. The application will query the Cache directly to get the data. If the requested Key is not available in the Cache, the cache layer will directly talk with the database layer and update the Cache. The step-by-step process involved in this caching Technique.
Step 1. The application tries to get data from the Cache.
Step 2. Check the requested key availability in Cache.
Step 3. If the Key is not available, the cache layer will directly talk with the database layer.
Step 4. The new data will be added to the Cache.
Step 5. The cache layer will return the data to the application.
So, in the process, if the data is not cached, the application will not talk with the database directly. The cache layer will take the responsibility to read the new data from the database and add it to it. In this way, the Cache can directly query the database.
NCache is one of the leading caching service providers, allowing you to implement the functionality in our application. So, with NCache, we can create a class that implements IReadThruProvider and access the database directly from the caching layer.
public class SqlReadThruProvider: Runtime.DatasourceProviders.IReadThruProvider {
private SqlDatasource sqlDatasource;
public void Init(IDictionary parameters, string cacheId) {
object connString = parameters["connstring"];
sqlDatasource = new SqlDatasource();
sqlDatasource.Connect(connString == null ? "" : connString.ToString());
}
public void Dispose() {
sqlDatasource.DisConnect();
}
public ProviderCacheItem LoadFromSource(string key) {
ProviderCacheItem cacheItem = new ProviderCacheItem(sqlDatasource.LoadCustomer(key));
cacheItem.ResyncOptions.ResyncOnExpiration = true;
return cacheItem;
}
public IDictionary < string, ProviderCacheItem > LoadFromSource(ICollection < string > keys) {
IDictionary < string, ProviderCacheItem > providerItems = new Dictionary < string, ProviderCacheItem > ();
foreach(string key in keys) {
object data = sqlDatasource.LoadCustomer(key);
ProviderCacheItem item = new ProviderCacheItem(data);
item.Expiration = new Expiration(ExpirationType.DefaultAbsolute);
item.Group = "customers";
providerItems.Add(key, item);
}
return providerItems;
}
public ProviderDataTypeItem < IEnumerable > LoadDataTypeFromSource(string key, DistributedDataType dataType) {
ProviderDataTypeItem < IEnumerable > providerItem = null;
switch (dataType) {
case DistributedDataType.Counter:
providerItem = new ProviderDataTypeItem < IEnumerable > (sqlDatasource.GetCustomerCountByCompanyName(key));
break;
case DistributedDataType.Dictionary:
providerItem = new ProviderDataTypeItem < IEnumerable > (sqlDatasource.LoadCustomersByCity(key));
break;
case DistributedDataType.List:
providerItem = new ProviderDataTypeItem < IEnumerable > (sqlDatasource.LoadCustomersFromCountry(key));
break;
case DistributedDataType.Queue:
providerItem = new ProviderDataTypeItem < IEnumerable > (sqlDatasource.LoadCustomersByOrder(key));
break;
case DistributedDataType.Set:
providerItem = new ProviderDataTypeItem < IEnumerable > (sqlDatasource.LoadOrderIDsByCustomer(key));
break;
}
return providerItem;
}
}
The above sample code is available in GitHub – NCacheSample
We have five methods implemented from the IReadThruProvider interface.
- Init(): establish the connection to the data source.
- LoadFromSource(string key): This method is used to load the data to the Cache from the source based on the Key.
- LoadFromSource(ICollection<string> keys): This method is used to load multiple objects from the data source and returns the dictionary of keys with respective data contained in ProviderCacheItem
- LoadDataTypeFromSource(string key, DistributedDataType dataType): This method load data structure from the external data source and returns the data Structure contained in ProviderCacheItem, which is enumerated.
- Dispose(): This function will release the resource.
var ReadThruProviderName = "SqlReadThruProvider";
var customer = _cache.Get<Customer>(CustomerID, new ReadThruOptions(ReadMode.ReadThru, ReadThruProviderName));
The above statements are used to get data from the Cache with the read mode as ReadThru.
Pros
The data is always up to data in the Cache, the application code doesn’t query the database, and everything will take care of by caching layer.
Cons
Single point of failure -If the caching layer is not available, the application can’t retrieve data because it’s not connected to the database.
Summary
We have seen what is full Cache, Cache-aside, and Read-thru Cache and how NCache as a distributed cache provider, used to implement all these features or techniques. All these techniques have pros and cons, so based on the requirement, we need to understand which is the right choice to solve our business problem.