Transactions and Concurrency

In this part of the tutorial we will introduce you to the transaction processing capabilities of GigaSpaces.

The Spring Framework provides a transaction manager abstraction using the PlatformTransactionManager interface with several different built-in implementations, such as JDBCClosed Java DataBase Connectivity. This is an application programming interface (API) for the Java programming language, which defines how a client may access a database. Data Source and JTA. GigaSpaces provides several implementations for Spring's PlatformTransactionManager, allowing you to use the GigaSpaces Distributed Transaction Manager and the Jini Distributed Transaction Manager. GigaSpaces can also be used within an XA transaction manager using JTA.

Transaction Management

Data grid provides several transaction managers, and changing the implementation you work with is just a matter of changing the configuration. In this part of the tutorial will use data grid's Distributed Transaction Manager to demonstrate some of the features and capabilities.

Here is an example how you define a distributed transaction manager via the Spring configuration:

<!-- A bean representing a space (an IJSpace implementation) -->
<os-core:embedded-space id="space" space-name="xapTutorialSpace" />

<!-- Defines a distributed transaction manager. -->
<os-core:distributed-tx-manager id="transactionManager" />

<!-- Define the GigaSpace instance that the application will use to access the space -->
<os-core:giga-space id="xapTutorialSpace" space="space" tx-manager="transactionManager" />

<!-- enable the configuration of transactional behavior based on annotations -->
<tx:annotation-driven transaction-manager="transactionManager" />

In order to make the data grid Interface transactional, the transaction manager must be provided to it when constructing the GigaSpace bean. You also need to add <tx:annotation-driven transaction-manager="transactionManager" /> to enable the configuration of transactional behavior based on annotations.

Transaction Demarcation

In your Java code you can annotate your class or methods with the Spring @Transactional annotation and configure Spring to process the annotation such that every call to the annotated methods will be automatically performed under a transaction. The transaction starts before the method is called and commits when the method call returns. If an exception is thrown from the method the transaction is rolled back automatically. Note that you can control various aspects of the transaction like propagation by using the attributes of the @Transactional annotation.

Here is an example how to use the transactions in your code:

@Transactional
public void createPayment() {
    Payment payment = new Payment();
    payment.setCreatedDate(new Date(System.currentTimeMillis()));
    payment.setPayingAccountId(new Integer(1));
    payment.setStatus(ETransactionStatus.PROCESSED);

    space.write(payment);
}

If an exception is thrown in this example, the object that was written into the space will be rolled back.

Transaction propagation

Normally all code executed within a transaction runs in the same transaction scope. However, there are several options specifying behavior if a transactional method is executed when a transaction context already exists. For example, simply running in the existing transaction (the most common case); or suspending the existing transaction and creating a new transaction.

Here is the example how you can define a new transaction to be started for a method:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createNewPayment() {
    Payment payment = new Payment();
    payment.setCreatedDate(new Date(System.currentTimeMillis()));
    payment.setPayingAccountId(new Integer(1));
    payment.setStatus(ETransactionStatus.PROCESSED);

    space.write(payment);
}

Event Processing

All event containers support transactions.

Polling container

Both the receive operation and the actual event action can be configured to be performed under a transaction. Transaction support is required when, for example, an exception occurs in the event listener, and the receive operation needs to be rolled back (and the actual data event is returned to the space). Adding transaction support is very simple in the polling container, and can be done by injecting a transaction manager into it. Let's take our payment polling container and make it transactional.

You make a polling container transactional by adding the @TransactionalEvent annotation:

@EventDriven
@Notify
@TransactionalEvent
public class PaymentListener {
    @EventTemplate
    Payment unprocessedData() {
        Payment template = new Payment();
        template.setStatus(ETransactionStatus.CANCELLED);
        return template;
    }

    @SpaceDataEvent
    public Payment eventListener(Payment event) {
        // process Payment
        System.out.println("Notifier Received a payment");
        return null;
    }
}

Notify Container

Just like the Polling container, both the receive operation and the actual event action can be configured to be performed under a transaction. However, in case an error occurs (rollback), the notification is lost and not sent again.

Task Execution

Executors fully support transactions similar to other data grid operations. Once an execute operation is executed within a declarative transaction, it will automatically join it. The transaction itself is then passed to the node the task executed on and added declaratively to it. This means that any data grid operation performed within the task execute operation will automatically join the transaction started on the client side.

Remoting Service

Executor remoting supports transactional execution of services. On the client side, if there is an ongoing declarative transaction during the service invocation (a SpaceClosed Where GigaSpaces data is stored. It is the logical cache that holds data objects in memory and might also hold them in layered in tiering. Data is hosted from multiple SoRs, consolidated as a unified data model. based transaction), the service will be executed under the same transaction. The transaction itself is passed to the server and any Space related operations (performed using data grid) will be executed under the same transaction.

Concurrency

Locking of objects occurs in multi-user systems to preserve the integrity of changes, so that changes made by one process do not accidentally overwrite changes made by another process. data grid provides two strategies for locking objects: Optimistic and pessimistic. The focus is on optimistic locking, the preferred strategy in the data grid context.

Optimistic Locking

With optimistic locking, you write your business logic allowing multiple users to read the same object at the same time, but allow only one user to update the object successfully. The assumption is that there will be a relatively large number of users trying to read the same object, but a low probability of having a small number of users trying to update the same object at the same time. In the case of multiple users trying to update the same object at the same time, the one(s) that try to update a non-recent object version fail.

Data grid uses a version number for an object to accomplish optimistic locking. When a client makes updates to an object an additional where clause is added to the update operation where the client version number of the object is compared against the version number of the object in space. If the the version number is not the same, the operation is rejected, indicating that the object has been modified by some other client or process.

Here is an example of how optimistic locking is enabled in data grid. First we need to indicate to the space that it will hold versioned objects.

GigaSpace space = new GigaSpaceConfigurer( new SpaceProxyConfigurer("xapTutorialSpace").versioned(true)).gigaSpace();
<os-core:space-proxy id="space" space-name="xapTutorialSpace"  versioned="true" />

You should enable the Space class to support the optimistic locking, by including the @SpaceVersion decoration on an int getter field. This field stores the current object version and is maintained by data grid. See below for an example:

@SpaceClass
public class Account {
    private Long id;
    private String number;
    private Double receipts;
    private Double feeAmount;
    private ECategoryType category;
    private EAccountStatus status;
    private int version;

    @SpaceId
    @SpaceRouting
    public Long getId() {
        return id;
    }

    @SpaceVersion
    public int getVersion() {
        return version;
    }
}

Pessimistic Locking

The pessimistic locking protocol provides data consistency in a multi user transactional environment. It should be used when there might be a large number of clients trying to read and update the same object(s) at the same time. This protocol utilize the system resources (CPU, network) in a very efficient manner both at the client and space server side. This scenario is different from the optimistic locking protocol since we assume with the pessimistic locking protocol, that every object that is read and retrieved from the space will eventually be updated where the transaction duration is relatively very short.

Here is an example of pessimistic locking that uses the exclusive read lock ReadModifier:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void executePayment(Integer orderIDs[]) throws Exception {
    // Read and lock the payment object
    Payment payment = space.readById(Payment.class, 1L,
        ReadModifiers.EXCLUSIVE_READ_LOCK);

    payment.setStatus(ETransactionStatus.CANCELLED);
    space.write(payment);
}

data grid provides additional read modifiers to denote the isolation level:

  • REPEATABLE_READ - default modifier
  • DIRTY_READ
  • READ_COMMITTED
  • EXCLUSIVE_READ_LOCK