• Webinars
  • Docs
  • Download
  • Blogs
  • Contact Us
Try Free
Show / Hide Table of Contents

Locking with Cache Item Versioning (Optimistic locking)

While Pessimistic Locking is a very helpful approach, there is a limitation involved in of using it that an item cannot be used unless atleast one operation is performed on it completely meaning that 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 represents 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 occurrs 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 preffered over 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 User1 wants to update the account , 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 fail 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 the cache, the cache item version returns to the cache client. This value denotes the number of updates performed on particular data. With every update, the value of item version increments.

Prerequisites

  • .NET/.NET Core
  • Java
  • Scala
  • Node.js
  • Python
  • To learn about the standard prerequisites required to work with all NCache client side features please refer to the given page on Client Side API Prerequisites.
  • For API details, refer to: Add, ICache, CacheItem, CacheItemVersion, Contains, Count, GetIfNewer, Insert, Remove.
  • To learn about the standard prerequisites required to work with all NCache client side features please refer to the given page on Client Side API Prerequisites.
  • For API details, refer to: add, Cache, CacheItem, CacheItemVersion, contains, getCount, getIfNewer, insert, remove.
  • To learn about the standard prerequisites required to work with all NCache client side features please refer to the given page on Client Side API Prerequisites.
  • For API details refer to: Cache, add, CacheItem, CacheItemVersion.
  • To learn about the standard prerequisites required to work with all NCache client side features please refer to the given page on Client Side API Prerequisites.
  • For API details, refer to: add, Cache, CacheItem, CacheItemVersion, contains, getCount, getIfNewer, insert, remove.
  • To learn about the standard prerequisites required to work with all NCache client side features please refer to the given page on Client Side API Prerequisites.
  • For API details refer to: Cache, CacheItem, insert, get_cacheitem, CacheItemVersion, getValue, get_if_newer.

Retrieve and Update Item with Item Version

An Add operation returns the 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 reinserts the item in the cache which updates the item version of the item to newVersion. Application2 still has the item with version. If application2 updates the item's units in stock and reinserts the item in the cache, the insertion will fail. Application2 will have to fetch the updated version to perform operation, on that cacheItem.

Note

You can add an item in the cache using both Add or Insert methods.

  • The Add method adds a new item in the cache and saves the item version for the first time.
  • The Insert method adds an item in the cache, if it is not present already, whereas it overwrites the value of an existing time and updates the item version.

The following code sections explain the operations performed by the application.

  • .NET/.NET Core
  • Java
  • Scala
  • Node.js
  • Python
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
}
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 = new CacheItemVersion();

    // readThruOptions set to null
    CacheItem cacheItem = cache.getCacheItem(key, version, null);

    // If result is not null
    if (cacheItem != null)
    {
        var prod = new Product();
        prod = cacheItem.getValue(Product.Class);
        prod.unitsInStock++;

        // Update the retrieved cacheitem according to requirement
        // Create a new cacheItem with updated value
        CacheItem updatedItem = new CacheItem(prod);

        // Set the itemversion. This version will be used to compare the
        // item version of cached item
        updatedItem.setCacheItemVersion(version);
        cache.insert(key, updatedItem);

        // If it matches, the insert will be successful, otherwise it will fail
    }
}
catch (OperationFailedException ex)
{
    if(ex.getErrorCode() == 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 IllegalArgumentException or NullPointerException
}
try {
    // Pre-condition: Cache is already connected
    // An item is added in the cache with itemVersion
    // Specify the key of the cacheItem
    val key = "Product:1001"

    // Initialize the cacheItemVersion
    val version = new CacheItemVersion

    // readThruOptions set to null
    val cacheItem = cache.getCacheItem(key, version, null)

    // If result is not null
    if (cacheItem != null) {
      var prod = cacheItem.getValue(classOf[Product])
      prod.setUnitsInStock(100)

      // Update the retrieved cacheItem according to requirement
      // Create a new cacheItem with updated value
      val updatedItem = new CacheItem(prod)

      // Set the itemVersion. This version will be used to compare the
      // item version of cached item
      updatedItem.setCacheItemVersion(version)
      cache.insert(key, updatedItem)

      // If it matches, the insert will be successful, otherwise it will fail
    }
}
catch {
    case exception: Exception => {
      // Handle any errors
    }
}
// This is an async method
try
{
    // Pre-condition: Cache is already connected
    // An item is added in the cache with itemVersion
    // Specify the key of the cacheItem
    var key = "Product:1001";

    // Initialize the cacheItemVersion
    var version = new ncache.CacheItemVersion();

    // readThruOptions set to null
    var cacheItem = await this.cache.getCacheItem(key, null, version);

    // If result is not null
    if (cacheItem != null)
    {
        var prod = new Product();
        prod = cacheItem.getValue(ncache.JsonDataType.Object);
        prod.unitsInStock++;

        // Update the retrieved cacheitem according to requirement
        // Create a new cacheItem with updated value
        // You also need to specify the FQN(Fully Qualified Name) of the class 
        var updatedItem = new ncache.CacheItem(prod,"FQN.Product");

        // Set the itemversion. This version will be used to compare the
        // item version of cached item
        updatedItem.setCacheItemVersion(version);

        await this.cache.insert(key, updatedItem);

        // If it matches, the insert will be successful, otherwise it will fail
    }
}
catch (error)
{
    // Handle errors
}
try:
    # Pre-condition: Cache is already connected
    # An item is added in the cache with ItemVersion

    # Specify the key of the cacheItem
    key = "Product:1001"

    # Initialize the cacheItemVersion
    version = ncache.CacheItemVersion(0)

    cache_item = cache.get_cacheitem(key, cacheitemversion=version)

    # If result is not None
    if cache_item is not None:
        prod = cache_item.get_value(Product)
        prod.set_units_in_stock(20)

        # Update the retrieved cacheitem according to requirement
        # Create a new CacheItem with updated value
        updated_item = ncache.CacheItem(prod)

        # Set the item_version. This version will be used to compare the
        # item version of cached item
        updated_item.set_cache_item_version(version)

        cache.insert(key, updated_item)

        # If it matches, the insert will be successful, otherwise it will fail
except Exception as exp:
    # Handle errors

Retrieve Item if a Newer Version Exists in Cache

The 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 does the method return 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.

  • .NET/.NET Core
  • Java
  • Scala
  • Node.js
  • Python
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
}
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.getProductID();

    // Create a new CacheItem
    CacheItem item = new CacheItem(product);

    // Add CacheItem to cache with new itemversion
    CacheItemVersion version = cache.insert(key, item);

    // Get object from cache
    Object result = cache.getIfNewer(key,version,Object.class);

    // Check if updated item is available
    if (result != null)
    {
        // Perform operations according to business logic    
    }
    else
    {
        // No new itemVersion is available       
    }
}
catch (OperationFailedException ex)
{
    if (ex.getErrorCode() == NCacheErrorCodes.ITEM_LOCKED)
    {
        // Item is already locked with a different LockHandle
    } 
    else
    {
        // NCache specific exception can occur due to:
        // Connection failures
        // Operation performed during state transfer
        // Operation timeout
    }    
}
catch (Exception ex)
{
    // Any generic exception like IllegalArgumentException or NullPointerException
}
try {
    // Pre-condition: Cache is already connected
    // Get updated product from database against given product ID
    val product = fetchProductByProductID(1001)

    // Generate a unique key for this item
    val key = "Product:" + product.getProductId

    // Create a new CacheItem
    val item = new CacheItem(product)

    // Add CacheItem to cache with new itemVersion
    val version = cache.insert(key, item)

    // Get object from cache
    val result = cache.getIfNewer(key, version, classOf[Any])

    // Check if updated item is available
    if (result != null) {
      // Perform operations according to business logic
    }
    else {
      // No new itemVersion is available
    }
}
catch {
    case exception: Exception => {
      // Handle any errors
    }
}
// This is an async method
try
{
    // Pre-condition: Cache is already connected
    // Get updated product from database against given product ID
    var product = this.fetchProductByProductID(1001);

    // Generate a unique key for this item
    var key = "Product:" + product.getProductID();

    // Create a new CacheItem
    // You also need to specifyt the FQN(Fully Qualified Name) of the class 
    var item = new ncache.CacheItem(product,"FQN.Product");

    // Add CacheItem to cache with new itemversion
    var version = await this.cache.insert(key, item);

    // Get object from cache
    var result = await this.cache.getIfNewer(key, ncache.JsonDataType.Object, version);

    // Check if updated item is available
    if (result != null)
    {
        // Perform operations according to business logic
    }
    else
    {
        // No new itemVersion is available
    }
}
catch (error)
{
    // Handle errors
}
try:
    # Pre-condition: Cache is already connected
    # Get updated product from database against given product ID
    product = fetch_product_from_db(1001)

    # Generate a unique key for this item
    key = "Product:" + product.get_product_id()

    # Create a new CacheItem
    item = ncache.CacheItem(product)

    # Add CacheItem to cache with new itemversion
    version = cache.insert(key, item)

    # Get object from cache
    result = cache.get_if_newer(key, cacheitemversion=version, objtype=Product)

    # Check if updated item is available
    if result is not None:
        # Perform operations according to business logic
        print("New version is available")
    else:
        # No new ItemVersion is available
        print("New version is not available")
except Exception as exp:
    # Handle errors

Remove Item with Item Version

An item can be removed from the cache using overload of Remove, based on the item version.However,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.
  • .NET/.NET Core
  • Java
  • Scala
  • Node.js
  • Python
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
}
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.getProductID();

    // Create a new CacheItem
    CacheItem item = new CacheItem(product);

    // Add CacheItem to cache with new itemversion
    CacheItemVersion version = cache.insert(key, item);

    // Remove the item from the cache using the itemVersion
    //lockHandle and writeThruOptions set to null
    cache.remove(key,null,version,null,Object.class);


    Object value = cache.get(key,Object.class);

   // Checking if item still exists in cache or not 
    if(value == null)
    {
        // Item has been removed from the cache
    }
    else 
    {
        // Item is still in Cache    
    }
}
catch (OperationFailedException ex)
{
    if (ex.getErrorCode() == NCacheErrorCodes.ITEM_LOCKED)
    {
        // Item is already locked with a different LockHandle
    } 
    else 
    {
        // NCache specific exception can occur due to:
        // Connection failures
        // Operation performed during state transfer
        // Operation timeout
    }
}
catch (Exception ex)
{
    // Any generic exception like IllegalArgumentException or NullPointerException          
}
try {
    // Pre-condition: Cache is already connected
    // Get updated product from database against given product ID
    val product = fetchProductByProductID(1001)

    // Cache key remains the same for this product
    val key = "Product:" + product.getProductId

    // Create a new CacheItem
    val item = new CacheItem(product)

    // Add CacheItem to cache with new itemVersion
    val version = cache.insert(key, item)

    // Remove the item from the cache using the itemVersion
    //lockHandle and writeThruOptions set to null
    cache.remove(key, null, version, null, classOf[Any])

    val value = cache.get(key, classOf[Any])

    // Checking if item still exists in cache or not
    if (value == null) {
      // Item has been removed from the cache
    }
    else {
      // Item is still in Cache
    }
}
catch {
    case exception: Exception => {
      // Handle any errors
    }
}
// This is an async method
try
{
    // Pre-condition: Cache is already connected
    // Get updated product from database against given product ID
    var product = this.fetchProductByProductID(1001);

    // Cache key remains the same for this product        
    var key = "Product:" + product.getProductID();

    // Create a new CacheItem
    // You also need to specify the FQN(Fully Qualified Name) of the class
    var item = new ncache.CacheItem(product,"FQN.Product");

    // Add CacheItem to cache with new itemversion
    var version = await this.cache.insert(key, item);

    // Remove the item from the cache using the itemVersion
    // lockHandle and writeThruOptions set to null
    await this.cache.remove(key, ncache.JsonDataType.Object, null, version, null);

    var value = await this.cache.get(key, ncache.JsonDataType.Object);

    if (value == null)
    {
        // Item has been removed from cache
    }
    else
    {
        // Item is still  in cache
    }
}
catch (error)
{
    // Handle errors
}
try:
    # Pre-condition: Cache is already connected
    # Get updated product from database against given product ID
    product = fetch_product_from_db(1001)

    # Cache key remains the same for this product
    key = "Product:" + product.get_product_id()

    # Create a new CacheItem
    item = ncache.CacheItem(product)

    # Add CacheItem to cache with new itemversion
    version = cache.insert(key, item)

    # Remove the item from the cache using the ItemVersion
    cache.remove(key, version=version, objtype=Product)

    value = cache.get(key, Product)

    if value is None:
        # Item has been removed from cache
        print("Remove successful")
    else:
        # Item is still in cache
        print("Remove failed")
except Exception as exp:
    # Handle errors

Topology Wise Behavior

  • For the Mirror and the Replicated Cache

In the 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 the replicated topology the client is connected to one node and the item version generates at node which receives client update/add operation, then same item version along with the item replicates to all other nodes for data consistency.

  • For the Partitioned and the Partitioned Replica Cache

In the partitioned topology, the 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 the partitioned-replica topology, the 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 the Client Cache, all version related information is maintained at clustered cache and whenever version related API is called the user gets its 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)

Back to top Copyright © 2017 Alachisoft