Locking with Cache Item Versioning (Optimistic locking)
While Pessimistic Locking is a very helpful approach, there is a limitation of using it that an item cannot be used unless one operation is performed on it completely that means the item remains locked till the completion of one task performed on the item. This can cause thread starvation if an item remains locked for a long period of time.
This is where optimistic locking comes in handy since NCache uses cache item versioning. CacheItemVersion
is a property associated with every cache item. It is basically a numeric
value that is used to represent the version of the cached item which increments
itself by one with every update to an item. This property allows you to track whether any
change occurred in an item or not. When you fetch an item from cache, you
also fetch its current version in the cache.
For read-intensive applications optimistic locking is a recommended approach than pessimistic locking.
When to Use Optimistic Locking
In the previous example, we had a single bank account used by two users simultaneously. Let's suppose one of the users acquired the lock for performing a deposit transaction on the bank account. User2 is waiting for User1 to free the lock for him to make the withdrawal transaction. Consider that User1 goes to an unstable state due to network connectivity issues without setting the lock free. The User2 keeps waiting for him to release the lock without the knowledge of any connectivity issues so he goes to the state of starvation till the first user releases the lock.
In order to avoid this kind of problem, optimistic locking is a useful solution. Using this kind of locking, if the User1 wants to update the account according to his changes, he can update the account without locking it and the item version will be updated accordingly. Now when User2 wants to update the data, he will get the updated version based on the item version and this would make sure no data integrity issues occur. If any user performs the operation on the data with the old item version, the operation will be failed considering it has outdated item version.
CacheItemVersion
adds an additional dimension to the development of application using NCache.
Optimistic concurrency can be achieved in applications by NCache Item
Versioning.
When any item is added in cache, cache item version is returned to the cache client. This value denotes the number of updates performed on a particular data. With every update, the value of item version is incremented.
Note
- To use Maven packages for NCache Professional Edition, change the
<artifactId>
as shown below:<artifactId>ncache-professional-client</artifactId>
- To use Node.js API in NCache Professional Edition, install and include the
ncache-professional-client
npm package in your Node.js application.
Pre-Requisites
- Install the following NuGet packages:
- Include the following namespace in your application:
Alachisoft.NCache.Client
Alachisoft.NCache.Runtime.Caching
Alachisoft.NCache.Runtime.Exceptions
- The application must be connected to cache before performing the operation.
- Cache must be running.
- For API details, refer to: Add, ICache, CacheItem, CacheItemVersion, Contains(), Count, GetIfNewer, Insert, Remove.
- Make sure that the data being added is serializable.
- To ensure the operation is fail safe, it is recommended to handle any potential exceptions within your application, as explained in Handling Failures.
- To handle any unseen exceptions, refer to the Troubleshooting section.
Retrieve and Update Item with Item Version
An Add operation returns CacheItemVersion
. If an item is added for the first
time, a long value containing the timestamp of its creation is returned. This
version will be incremented by "1" upon performing operations on this key in future.
Optimistic locking makes sure that the user always gets the most updated copy of the item from the cache. If the user keeps performing functions on the outdated version, NCache throws an exception so that the user gets the updated item from the cache.
In the example below a cache is used by multiple applications. The cache contains the data of products. A CacheItem
is added in the cache. Both the applications fetch the item with the current version let's say version. Application1 modifies the productName and then re-inserts the item in the cache which updates the item version of the item to newVersion. Application2 still has the item with version. If the application2 updates the item's units in stock and re-inserts the item in the cache, item insertion will fail. Application2 will thus have to fetch the updated version in order to perform operation on that cacheItem.
Note
You can add an item in the cache using both Add or Insert methods.
Add
method adds a new item in the cache and saves the item version for the first time.Insert
method adds an item in the cache if it is not present already, whereas overwrites the value of an existing time and updates the item version.
The following code sections explain the operations performed by the application.
try
{
// Pre-condition: Cache is already connected
// An item is added in the cache with itemVersion
// Specify the key of the cacheItem
string key = "Product:1001";
// Initialize the cacheItemVersion
CacheItemVersion version = null;
// Get the cacheItem previously added in the cache with the version
CacheItem cacheItem = cache.GetCacheItem(key, ref version);
// If result is not null
if (cacheitem != null)
{
// CacheItem is retrieved successfully with the version
// If result is Product type
var prod = new Product();
prod = cacheItem.GetValue<Product>();
prod.UnitsInStock++;
// Create a new cacheItem with updated value
var updateItem = new CacheItem(prod);
//Set the itemversion. This version will be used to compare the
// item version of cached item
updateItem.Version = version;
cache.Insert(key, updateItem);
// If it matches, the insert will be successful, otherwise it will fail
}
else
{
// Item could not be retrieved due to outdated CacheItemVersion
}
}
catch (OperationFailedException ex)
{
// NCache specific exception
if (ex.ErrorCode == NCacheErrorCodes.ITEM_WITH_VERSION_DOESNT_EXIST)
{
// If the itemversion mismatches with the already added itemversion
}
else
{
// Exception can occur due to:
// Connection Failures
// Operation Timeout
// Operation performed during state transfer
}
}
catch (Exception ex)
{
// Any generic exception like ArgumentNullException or ArgumentException
}
Recommendation: To ensure the operation is fail safe, it is recommended to handle any potential exceptions within your application, as explained in Handling Failures.
Retrieve Item if a Newer Version Exists in Cache
GetIfNewer
method can be used to fetch the existing item if a newer version is
available in cache. By specifying the current version as an argument of the
method call, the cache returns appropriate result.
If the version specified is less than the one in cache, only then the method returns a new Item else null will be returned.
The following example adds an item in the cache with the key Product:1001
and item version, and then retrieves it if any newer version is available using the GetIfNewer
method which fetches an item using the cache item version.
Note
You can add an item in the cache using both Add or Insert methods.
Add
method adds a new item in the cache and saves the item version for the first time.Insert
method adds an item in the cache if it is not present already, whereas overwrites the value of an existing time and updates the item version.
try
{
// Pre-condition: Cache is already connected
// Get updated product from database against given product ID
Product product = FetchProductByProductID(1001);
// Generate a unique key for this item
string key = $"Product:{product.ProductID}";
// Create a new CacheItem
var item = new CacheItem(product);
// Add CacheItem to cache with new itemversion
CacheItemVersion version = cache.Insert(key, item);
// Get object from cache
var result = cache.GetIfNewer<Product>(key, ref version);
// Check if updated item is available
if (result != null)
{
// An item with newer version is available
if (result is Product)
{
// Perform operations according to business logic
}
}
else
{
// No new itemVersion is available
}
}
catch (OperationFailedException ex)
{
// NCache specific exception
if (ex.ErrorCode == NCacheErrorCodes.ITEM_WITH_VERSION_DOESNT_EXIST)
{
// If the itemversion mismatches with the already added itemversion
}
else
{
// Exception can occur due to:
// Connection Failures
// Operation Timeout
// Operation performed during state transfer
}
}
catch (Exception ex)
{
// Any generic exception like ArgumentNullException or ArgumentException
}
Recommendation: To ensure the operation is fail safe, it is recommended to handle any potential exceptions within your application, as explained in Handling Failures.
Remove Item with Item Version
An item can be removed from the cache using either overload of Remove, based on the item version.
If the item version is different from the one in the cache, you get an exception specifying so.
The following example shows how to remove an item from the cache by specifying the Item Version using the Remove method.
Tip
You can monitor/verify removal:
- "Cache Count" Counter in NCache Web Monitor or PerfMon Counters
- Using
cache.Contains()
after expiration interval has elapsed - Using
cache.Count
before and after specifying expiration.
try
{
// Pre-condition: Cache is already connected
// Get updated product from database against given product ID
Product product = FetchProductByProductID(1001);
// Cache key remains the same for this product
string key = $"Product:{product.ProductID}";
// Create a new CacheItem
var item = new CacheItem(product);
// Insert CacheItem to cache with new itemversion
CacheItemVersion version = cache.Insert(key, item);
// Remove the item from the cache using the itemVersion
cache.Remove(key, null, version);
}
catch (OperationFailedException ex)
{
// NCache specific exception
if (ex.ErrorCode == NCacheErrorCodes.ITEM_WITH_VERSION_DOESNT_EXIST)
{
// If the itemversion mismatches with the already added itemversion
}
else
{
// Exception can occur due to:
// Connection Failures
// Operation Timeout
// Operation performed during state transfer
}
}
catch (Exception ex)
{
// Any generic exception like ArgumentNullException or ArgumentException
}
Recommendation: To ensure the operation is fail safe, it is recommended to handle any potential exceptions within your application, as explained in Handling Failures.
Topology Wise Behavior
- For Mirror and Replicated Cache
In mirror topology when an item is added or updated its version is generated at Active node and same version is then replicated to Passive node along with the item so that when active node becomes passive item version remains same.
In replicated topology client is connected to one node and item version is generated at node which receives client update/add operation and then same item version along with the item will be replicated to all other nodes for data consistency.
- For Partitioned and Partitioned Replica Cache
In partitioned topology item version is generated and exists on the same node which contains the item and during state transfer version is also transferred along with the item in case item move to another node.
In partitioned-replica topology version is generated on the active node which contains the item and same version along with the item is then replicated to its replica for data consistency and during state transfer version is also transferred along with the item in case item moves to another node.
- Client Cache
In Client Cache, all version related information is maintained at clustered cache and whenever version related API is called the user gets the version from clustered cache.
Additional Resources
NCache provides sample application for item locking on GitHub.
See Also
Cache Data Dependency on Database
Lock Items with Cache Item Versioning (Optimistic Locking)