Summary: With optimistic locking, you write your program under the assumption that an update operation has a chance of failing, if your updated object has been changed by someone else since you read it.
Work In Progress Notice: This page has not been reviewed since XAP 6.0 and might include incorrect details.
Contents:

Overview

With optimistic locking, you write your program under the assumption that an update operation has a chance of failing, if your updated object has been changed by someone else since you read it. Optimistic locking offers higher concurrency and better performance than pessimistic locking. It also avoids deadlocks.

Optimistic data locking relies on the idea that data remains unmodified while it is away from the server. As a simple example, consider how you would update client details. The customer details are stored in an object, and if a client wants to update them, it will first need to get the object from the space. The data is not locked, and other clients can have access to it simultaneously, thus ensuring a scalable system.

The problem is that while the customer details are away from the space server, they may become stale. For example, a second client can request the same customer details, update them, and commit them back to the space. The first client, unaware that it is dealing with a stale copy of the data, modifies and commits the data. Obviously, with no checking mechanism to detect this conflict, the first client's changes, which commit last, will be made permanent, thus overwriting the changes made by the second client.
For optimistic locking to work effectively, you must be able to detect these update-update conflicts and to make the client aware of them so they can be dealt with appropriately.

Optimistic locking is best suited for environments with many read-only transactions, few read-update transactions, and a relatively low volume of objects that are changed.

Pessimistic locking may be less suitable for real-time systems than optimistic locking, because the space runs best with short term transactions. Optimistic locking also has a big advantage when you may want to read a large number of objects but update only a few of them; or when it is unlikely that objects you want to work with will be updated by others. In general, optimistic locking ensures that updated objects are the most recent ones, while improving the coherency of system behavior.

Optimistic Locking Actions

To get optimistic locking features to work with unlocked objects:

  1. Enable the class to support the optimistic locking protocol.
  2. Get a space proxy using versioned mode. This can be done using one of the options listed below:
    • Javaspaces API:
      IJSpace space = (IJSpace)SpaceFinder.find("jini://*/*/mySpace?versioned=true")

      Or, call the IJSpace.setOptimisticLocking(true)

    • Map API:
      IMap map = (IMap)CacheFinder.find("jini://*/*/mySpace?versioned=true");

      Or, call the IMap.setVersioned(true)

  3. Follow an optimistic locking protocol (explained in a following section). This will ensure coherency and consistency.

By following the above procedure, you get a shorter locking duration, which improves performance and concurrency of access among multiple users. At the same time, the version ID validation performed on update, take, and multipleUpdate requests keeps the space consistent.

Making Class Optimistic Locking Protocol Enabled

Entry Based Class

An object is in optimistic locking protocol mode and its version information is updated when:

  • Its class extends MetaDataEntry.
  • Its class implements IMetaDataEntry.
  • Its class includes the _setEntryInfo and _getEntryInfo methods.
  • It is running in master-local mode.

EntryInfo Access Methods

To make a class "optimistic locking ready," add the _setEntryInfo and _getEntryInfo methods to the class.
Following is an example for a class that includes the EntryInfo access methods.

public class Employee implements Entry{

  transient private EntryInfo m_EntryInfo;// <- stores the EntryInfo data
  public String name;
  public Integer age;
  public void __setEntryInfo(EntryInfo entryInfo)
  {
      m_EntryInfo = entryInfo;
  }

  public EntryInfo __getEntryInfo()
  {
      return m_EntryInfo;
  }

  public Employee() {}

  public Employee( String name , Integer age )
  {

    this.age = age;
    this.name = name;
   }
 }

The EntryInfo class includes the following attributes:

Attribute Type Description
m_UID java.lang.string Entry unique identifier.
m_versioned int Entry version ID – the optimistic locking timestamp.
m_TimeToLive long Specifies how much time, in milliseconds, the Entry has to live. You should update this attribute after read, write or update operations.

If an object has the EntryInfo access methods, the GigaSpaces take, update, and updateMultiple automatically use the version ID to detect obsolete copies in the space, and prevent you from using the obsolete copies as the basis for changing the object.
An obsolete copy occurs if, while you are working with an unlocked copy of an object, another user updates the original object in the space. If your object has EntryInfo access methods, you will automatically be told if your update and updateMultiple method will overwrite changes made by another user since you read your object.

If you use a template for the take operation that includes EntryInfo access methods, the system checks the template's version ID. If it is not zero, and does not match the version ID of the Entry in the space, the take operation fails.
An obsolete copy will also occur if you try to update an object that another user has already taken.

You can avoid obsolete copies by first refreshing (reading) every object you want to change before performing an Update operation. This is costly, because objects must be fetched and because of the time required to make detailed evaluations of every attribute in every object.

Once you add EntryInfo access methods to an object, the version ID is updated, no matter who does the updating. When an entry with EntryInfo access methods is created, its VersionID attribute is set to 1. Thereafter, each update increments the VersionID attribute by 1 (during the update or the multipleUpdate operation at the space side).

POJO Based Class

The POJO Class should have the @SpaceVersion field level decoration to accommodate the version ID.
See below example for a POJO with the @SpaceVersion decoration.

@SpaceClass(replicate=true,persist=false,fifo=false)
public class Employee
		
{
	private Integer	employeeID;
	private String	lastName;
	private String	firstName;
	private int versionID;
	
	public Employee()
	{
	}

	public Employee(Integer employeeID)
	{
		this.employeeID = employeeID;
	}

	public Employee(String lastName, Integer employeeID)
	{
		this.employeeID = employeeID;
		setLastName(lastName);
	}

	public Employee(String lastName,String firstName ,Integer employeeID)
	{
		this.employeeID = employeeID;
		setFirstName(firstName);
		setLastName(lastName);
	}
	
	@SpaceId
	@SpaceProperty(index=IndexType.BASIC)
	public Integer getEmployeeID()
	{
		return employeeID;
	}

	public  void setEmployeeID(Integer employeeID)
	{
		this.employeeID = employeeID;
	}

	@SpaceProperty(index=IndexType.BASIC)
	private String getFirstName()
	{
		return firstName;
	}

	public void setFirstName(String firstName)
	{
		this.firstName = firstName;
	}

	private String getLastName()
	{
		return lastName;
	}

	public void setLastName(String name)
	{
		this.lastName = name;
	}

	public void setVersionID(int versionID)
	{
		this.versionID= versionID;
	}

	@SpaceVersion
	public int getVersionID()
	{
		return versionID;
	}
	public boolean equals(Object other)
	{
		if (!(other instanceof Employee))
			return false;
		else
		{
			Employee otherBean = (Employee) other;
			return ((otherBean.getEmployeeID() != null
							&& otherBean.getEmployeeID().equals(getEmployeeID()) || 
otherBean.getEmployeeID() == getEmployeeID()));
		}
	}

	public String toString()
	{
		return " \tlastName: " + lastName + ", \tfirstName: "
				+ firstName + ", \temployeeID: "+ employeeID;
	}
}

Here is a simple example illustrating EntryVersionConflictException usage:

IJSpace space = (IJSpace) SpaceFinder.find("jini://localhost/*/mySpace?versioned=true");
Employee employ1= new Employee("Last Name1" , "First name1" , new Integer(1));
space.write(employ1, null, Lease.FOREVER);
Employee temp = new Employee("Last Name1" , "First name1" , new Integer(1));

try
{
	Employee result1 = (Employee) space.read(temp , null, 0);
	Employee result2 = (Employee) space.read(temp , null, 0);
	System.out.println("About to Write Version:"  + result1.getVersionID());
	space.write(result1, null, Lease.FOREVER);
	System.out.println("Employee ID "  + result1.getEmployeeID() + " - Entry version in Space:"  + result2.getVersionID());
	System.out.println("About to Write Version:"  + result2.getVersionID());
	space.update(result2, null, 0,0);
}
catch (com.j_spaces.core.client.EntryVersionConflictException evce)
{
	System.out.println("Got  " + evce );
	System.out.println("re-read Object again");
	Employee result3 = (Employee) space.read(temp , null, 0);
	System.out.println("perform Update again.. Client Entry version:" + result3.getVersionID());
	Employee prevValue= (Employee)space.update(result3, null, 0,0);
	if (prevValue != null)
		System.out.println("Update Successful! Entry version in Space:"  + result3.getVersionID());
}

Here is the expected output:

About to Write Version:1
Employee ID 1 Entry Version in Space:1
About to Write Version1
Got EntryVersionConflictException com.j_spaces.core.client.EntryVersionConflictException: Entry Version ID conflict, Operation rej
ected. Operation=Update UID=-1786435842^45^1^0^0 space entry version=2 client version=1
Update sucessful

When you detect an obsolete copy, you can decide what to do. You may want to refresh your copy of the object by re-reading it, inspecting it, and then reapplying some or all of your modifications; or you may simply want to abandon your update by calling the abort () method.

GigaSpaces Optimistic Locking Protocol

When using optimistic locking, you must take responsibility for coherence and consistency of the space Entry objects by following an optimistic locking protocol.
The protocol procedure below assumes that the objects involved have the EntryInfo access methods.

Step 1 – Get Space Proxy

You should first get the space proxy using the SpaceFinder. You may get remote or embedded space proxies. Make sure the proxy is in optimistic locking mode.

Step 2 – Read Objects with null Transaction

Read objects from the space with a null transaction. You may use the readMultiple () method to get several objects in one call.
Reading objects with a null transaction allows multiple users to get the same objects at the same time, and allows them to be updated. (If objects are read over a transaction by multiple users, no user can update the objects until all other users commit or rollback their transaction.)
Steps 1, 2 constitute a "Version ID validation sequence."

Step 3 – Open Transaction (optional)

Use the usual TransactionManager () method to start a transaction.
You should only use a transaction in optimistic locking when you need to update multiple Entries in the same context.

Step 4 – Browse, Modify, and Update

Modify the objects you read from the space and call the update or the updateMultiple () space operation.
When the update is requested, the space does the following:

  • For each updated object, the Version ID in the updated object is compared to the Version ID in the source object on the space.
  • If the Version ID of the updated object is the same as the Version ID of the source object, it is incremented by 1, and the object is updated on the space.
  • If the Version ID of the updated object is different from the Version ID of the source object, the new object is not written to the space (i.e., the operation fails). In this case, if there is a version conflict, the array returns with exceptions, and if the update was successful, the array returns with Entries.
It is recommended that you call the update operation just before the commit operation, to minimize the time update operations are blocked.

Step 5 – Update Failure

If you use optimistic locking and your update operation fails, you should get the EntryVersionConflictException.

This exception is thrown when you try to write an Entry to the space during an update operation, and its version ID does not match the version of the existing Entry. In other words, when you are not using the latest version.

If some objects have outdated version IDs or have been deleted by another user, you can either roll back or refresh the failed objects and try again.

Rolling back the transaction restores the objects to their values before they were modified at the space side. Refreshing the failed objects reads the latest committed objects from the server to the client side.

For fast refresh, you may read the objects using their UIDs.

When your Entry class includes the following UID access methods:
  • Has the (__getEntryInfo & __setEntryInfo or __setEntryUID & __getEntryUID ) Implemented.
  • Extends the MetaDataEntry.
  • Implements the IMetaDataEntry interface.

and you are using the TakeIfExists (), ReadIfExists () or update () operations, an EntryNotInSpaceException will be thrown when an Entry with identical UID does not exists within the space.

null will be returned when using regular Entry (Implements Entry interface) – i.e. does not include UID access methods, and an identical UID does not exists within the space.

The same behavior (null return value) will happen when such an Entry with identical UID does exist, but is locked under a transaction.

In other words, the only way to identify the existence or inexistence of a specific Entry with a specific UID within the space is to use the ReadIfExists operation with an Entry class that includes UID access methods.

Step 6 – Commit or Rollback Changes

At any time, you can save or roll back your updates with a transaction commit or rollback.

Optimistic Locking with a Single Object

With optimistic locking, you write your program under the assumption that an update operation has a chance of failing, if your updated object was changed by someone else since you read it.
The optimistic locking involves a read and update call.

With optimistic locking set, an update operation has a chance of failing (throwing an EntryVersionConflictException ) if it was performed on an obsolete copy.

For an example that demonstrates several threads concurrently trying to update Entries in the space, see the Quick Start Guide.

Optimistic Locking with Multiple Objects

The Quick Start Guide contains an example that demonstrates an optimistic locking protocol with the updateMultiple() method. In comparison to the previous example, an update is considered successful if and only if all Entries were updated. Otherwise, the transaction should be aborted. In general, when calling updateMultiple() you should check every returned object from the array returned from the updateMultiple() method.

If one of the returned objects indicates that the update failed (eg. EntryVersionConflictException ), you need to abort the transaction, re-read, and update again. After a successful update, the Entry will include both the updated Entry information and the updated version ID.
In a multi-threaded process, before re-reading the Entries, you should add some random sleep. This permits one of the threads to be able to update all objects successfully. If you fail to perform this step, the two threads will contend to update all Entries.

Versioning

Versioning addresses the issue of concurrent updates on the same object among multiple cache instances. When running in optimistic locking mode, the space uses a versioning mechanism to ensure the coherency of the updated data and to synchronize concurrent updates.

How it Works

The following diagram illustrates versioned cache flow. In this diagram, we use two cache instances: cache1 and cache2. Initially, both cache instances contain the same object instance with the same version.

Cache1 updates the object in the cache successfully. As a result of this operation, the version of the object is updated to 1.

Cache2 attempts to update the same object with an older version of this object. The cache detects this conflict and throws a runtime exception that indicates a version conflict. In order for Cache2 to succeed, it needs to read the updated version from the master cache, and then repeat the update operation.

Versioned Cache Restrictions

Some restrictions apply when using the Map interface:

  • In the versioned mode, the value must not be a null value.
  • The com.j_spaces.map.IMap assigns a UID based on the Entry key of an internal class object. This object is called an envelope and holds the map's Entry key and value. The key object toString () method must return a unique value to ensure a unique UID. The UID is assigned when the Entry is written to the master space at the client side. This ensures that there aren't two or more clients creating the same map Entry (a new Entry) with the same data at the same time. If that happens, one of the clients fails and attempts to update the Entry. This generates an updated version of the Entry into the space with an incremented version ID.

Scenario Example

Suppose that you have two applications, Application_1 and Application_2, which are both working with the same Object A. The following sequence of events describes a simple optimistic locking scenario.

Time Action Application_1 Application_2
T=1 Initial conditions: Both applications Read the object from space. Object A VersionID=1
Value1=X
Value2=Y
Object A
VersionID=1
Value1=X
Value2=Y
T=2 Both modify same objects. Object A VersionID=1
value1=X
Value2=Y_1
Object A
VersionID=1
value1=X_2
Value2=Y
T=3 Application_2 updates Object A commits. Commit is successful. Object A
VersionID=1
Value1=X
Value2=Y_1
Object A
VersionID=2
Value1=X_2
Value2=Y
T=4 Application_1 tries to update object A but fails due to invalid Version ID. Object A VersionID=1
Value1=X
Value2=Y_1
 
T=5 Application_1 refreshes object A (re-reads it). Object A
VersionID=2
Value1=X_2
Value2=Y
 
T=6 Application_1 updates Object A again and commits. Commit is successful. Object A VersionID=3
value1=X_2
Value2=Y_1
 

Code Example

This example demonstrates the usage of optimistic lock protocol. One Entry is updated by many concurrent workers. Every worker updates the salary field by 10. The internal versionID is used to keep consistency.

A running example can be found at:
<GigaSpaces Root>\examples\Advanced\Data_Grid\Optimistic-Locking\simple-ol.

Steps to run the example:

  1. Run the GigaSpace instance:
    gsInstance
  2. Run the application:
    java -classpath %JARS%;..\classes com.j_spaces.examples.data_grid.optimistic_lock.
    OptimisticLock jini://*/*/mySpace?versioned 10

Application Source Code

The Entry class – extends the MetaDataEntry:

package com.j_spaces.examples.data_grid.optimistic_lock;
import com.j_spaces.core.client.MetaDataEntry;
public class myEntry extends MetaDataEntry {
	public String name;
		public Integer salary;

		public myEntry()  {
		}

		public myEntry( String name, Integer salary )  {
  		this.name   = name;
			this.salary = salary;
		}

		public void increaseSalary()  {
  		int updatedSalary = salary.intValue() + 10;
  		salary = new Integer( updatedSalary );
		}
}

The application:

package com.j_spaces.examples.data_grid.optimistic_lock;

import java.util.GregorianCalendar;

import net.jini.core.lease.Lease;
import net.jini.space.JavaSpace;
import com.j_spaces.core.IJSpace;
import com.j_spaces.core.client.EntryVersionConflictException;
import com.j_spaces.core.client.EntryNotInSpaceException;
import com.j_spaces.core.client.FinderException;
import com.j_spaces.core.client.MetaDataEntry;
import com.j_spaces.core.client.SpaceFinder;

public class OptimisticLock {
	int numOfWorkers;
	IJSpace spaceProxy;

	class Worker extends Thread	{
		myEntry workerEntry;

	public void say (String msg) {
		java.util.GregorianCalendar now = new java.util.GregorianCalendar();
		String t = now.get(GregorianCalendar.HOUR_OF_DAY)+":"+
					now.get(GregorianCalendar.MINUTE)+":"+
					now.get(GregorianCalendar.SECOND)+":"+
					now.get(GregorianCalendar.MILLISECOND) ;

		System.out.println( getName() +  " (" + t + ")" + msg);
	}

	Worker(String workerName ) throws Exception {
		 this.setName( workerName );
	}

	public void run() {
	try {
		while(true)  {
			try	{
				workerEntry = (myEntry)spaceProxy.read(new myEntry(),null,
						JavaSpace.NO_WAIT );
				workerEntry.increaseSalary();
				say(" Before update: UID " +workerEntry.__getEntryInfo().m_UID+
						" VersionID " + workerEntry.__getEntryInfo().m_VersionID);
				myEntry oldWorkerEntry = (myEntry)spaceProxy.update( workerEntry,
						null, Lease.FOREVER, JavaSpace.NO_WAIT );
				say(" After update:  UID " + workerEntry.__getEntryInfo().m_UID+
						" VersionID " + workerEntry.__getEntryInfo().m_VersionID +
						" - Update OK!" );

				/*
				* Update returned object could be:
				* The old Entry - Update Ok
				* Null - In case the entry is blocked by other user - Update failed
				*
				* If there was problem with the update operation the following
				* Exception might be thrown:
				*
				* EntryNotInSpaceException - in case the entry does not exists in
				* space - Update failed
				*
				* EntryVersionConflictException - In case updating non-latest
				* version - Update failed
				*
				* After successful update the entry will include updated Entry
				* info with updated version ID.
				*/

			}catch(Exception ex)
			{
				if ( ex instanceof EntryNotInSpaceException ) {
					EntryNotInSpaceException enise = (EntryNotInSpaceException)ex;
				  	say(" Update Failed - entry Not In Space - " +enise);
				}
				else if ( ex instanceof EntryVersionConflictException )	{
					EntryVersionConflictException evce =
						(EntryVersionConflictException)ex;
				  	say(" Update Failed - using old worker info - " +evce+
				  			"- re-read entry and update..." );
					continue;
				}
				else
					say(" failed: " + ex.getMessage() );
			}

			return;
		  }// while..
		 }catch(Exception ex) {
		   say(" failed: " + ex.getMessage() );
		 }
	   }
	}// end class

	public OptimisticLock(String spaceUrl, int numOfWorkers) throws Exception {
	  this.numOfWorkers = numOfWorkers;
	  spaceProxy = (IJSpace)SpaceFinder.find( spaceUrl );
	  spaceProxy.clear( null, null );
	}

	public void go() throws Exception {
		System.out.println("\nWelcome to GigaSpaces Optimistic Locking example");
		System.out.println("This will update an entry by multiple threads "+
				"using optimistic locking protocol");

		myEntry we = new myEntry( "Dave", new Integer(3000));
		System.out.println("\nEntryname: " + we.name +
						   "\nSalary: " + we.salary +
						   "\nNumber of Workers: " + numOfWorkers);
		System.out.println("Each worker is increasing Dave's salary by 10...\n");
		spaceProxy.write( we, null, Lease.FOREVER );

		// initilize workers
		Worker[] workers = new Worker[numOfWorkers];
		for (int i = 0; i < workers.length; i++)
		 workers[i] = new Worker("Worker" + i);

		// start workers
		for (int i = 0; i < workers.length; i++)
		 workers[i].start();

		// wait while all workers finished
		for (int i = 0; i < workers.length; i++)
		  workers[i].join();

		// check salary
	    we = (myEntry)spaceProxy.read( new myEntry(),null,JavaSpace.NO_WAIT );
	    System.out.println("\nEntryname: "+we.name+"\nSalary: "+we.salary);
	}

	public static void main(String[] args) throws Exception	{
	 if ( args.length != 2 ) {
		System.out.println("Usage: <spaceURL> <Num_of_Workers>");
		System.out.println("<protocol>://host:port/containername/spacename");
		System.exit(1);
	 }

	 try {
	 	OptimisticLock ol = new OptimisticLock(args[0],Integer.parseInt(args[Look And Feel - ServiceGrid]));
	 	ol.go();
	 	System.exit(0);
	 }
	 catch( FinderException ex ) {
		ex.printStackTrace();
		System.out.println("Could not find space: " + args[0]);
		System.out.println("Please check that GigaSpaces Server is running.");
	 }
	}
}
GigaSpaces.com - Legal Notice - 3rd Party Licenses - Site Map - API Docs - Forum - Downloads - Blog - White Papers - Contact Tech Writing - Gen. by Atlassian Confluence