Chapter 3. Transaction Basics

Table of Contents

Committing a Transaction
Non-Durable Transactions
Aborting a Transaction
Auto Commit
Transactional Cursors
Secondary Indices with Transaction Applications
Configuring the Transaction Subsystem

Once you have enabled transactions for your environment and your databases, you can use them to protect your database operations. You do this by acquiring a transaction handle and then using that handle for any database operation that you want to participate in that transaction.

You obtain a transaction handle using the Environment.beginTransaction() method.

Once you have completed all of the operations that you want to include in the transaction, you must commit the transaction using the Transaction.commit() method.

If, for any reason, you want to abandon the transaction, you abort it using Transaction.abort().

Any transaction handle that has been committed or aborted can no longer be used by your application.

Finally, you must make sure that all transaction handles are either committed or aborted before closing your databases and environment.

Note

If you only want to transaction protect a single database write operation, you can use auto commit to perform the transaction administration. When you use auto commit, you do not need an explicit transaction handle. See Auto Commit for more information.

For example, the following example opens a transactional-enabled environment and database, obtains a transaction handle, and then performs a write operation under its protection. In the event of any failure in the write operation, the transaction is aborted and the database is left in a state as if no operations had ever been attempted in the first place.

package je.txn;
                                                                                                                                     
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;

import java.io.File;
                                                                                                                                     
...
                                                                                                                                     
Database myDatabase = null;
Environment myEnv = null;
try {
    EnvironmentConfig myEnvConfig = new EnvironmentConfig();
    myEnvConfig.setTransactional(true);
    myEnv = new Environment(new File("/my/env/home"),
                              myEnvConfig);

    // Open the database. Create it if it does not already exist.
    DatabaseConfig dbConfig = new DatabaseConfig();
    dbConfig.setTransactional(true);
    myDatabase = myEnv.openDatabase(null,
                                    "sampleDatabase",
                                    dbConfig);

    String keyString = "thekey";
    String dataString = "thedata";
    DatabaseEntry key = 
        new DatabaseEntry(keyString.getBytes("UTF-8"));
    DatabaseEntry data = 
        new DatabaseEntry(dataString.getBytes("UTF-8"));

    Transaction txn = myEnv.beginTransaction(null, null);
        
    try {
        myDatabase.put(txn, key, data);
        txn.commit();
    } catch (Exception e) {
        if (txn != null) {
            txn.abort();
            txn = null;
        }
    }

} catch (DatabaseException de) {
    // Exception handling goes here
} 

Committing a Transaction

In order to fully understand what is happening when you commit a transaction, you must first understand a little about what JE is doing with its log files. Logging causes all database write operations to be identified in log files (remember that in JE, your log files are your database files; there is no difference between the two). Enough information is written to restore your entire BTree in the event of a system or application failure, so by performing logging, JE ensures the integrity of your data.

Remember that all write activity made to your database is identified in JE's logs as the writes are performed by your application. However, JE maintains logs in memory. Eventually this information is written to disk, but especially in the case of a transactional application this data may be held in memory until the transaction is committed, or JE runs out of buffer space for the logging information.

When you commit a transaction, the following occurs:

  • A commit record is written to the log. This indicates that the modifications made by the transaction are now permanent. By default, this write is performed synchronously to disk so the commit record arrives in the log files before any other actions are taken.

  • Any log information held in memory is (by default) synchronously written to disk. Note that this requirement can be relaxed, depending on the type of commit you perform. See Non-Durable Transactions for more information.

    Note that a transaction commit only writes the BTree's leaf nodes to JE's log files. All other internal BTree structures are left unwritten.

  • All locks held by the transaction are released. This means that read operations performed by other transactions or threads of control can now see the modifications without resorting to uncommitted reads (see Reading Uncommitted Data for more information).

To commit a transaction, you simply call Transaction.commit().

Remember that transaction commit causes only the BTree leaf nodes to be written to JE's log files. Any other modifications made to the the BTree as a result of the transaction's activities are not written to the log file. This means that over time JE's normal recovery time can greatly increase (remember that JE always runs normal recovery when it opens an environment).

For this reason, JE by default runs the checkpointer thread. This background thread runs a checkpoint on a periodic interval so as to ensure that the amount of data that needs to be recovered upon environment open is minimized. In addition, you can also run a checkpoint manually. For more information, see Checkpoints.

Note that once you have committed a transaction, the transaction handle that you used for the transaction is no longer valid. To perform database activities under the control of a new transaction, you must obtain a fresh transaction handle.

Non-Durable Transactions

As previously noted, by default transaction commits are durable because they cause the modifications performed under the transaction to be synchronously recorded in your on-disk log files. However, it is possible to use non-durable transactions.

You may want non-durable transactions for performance reasons. For example, you might be using transactions simply for the isolation guarantee. In this case, you might want to relax the synchronized write to disk that JE normally performs as part of a transaction commit. Doing so means that your data will still make it to disk; however, your application will not necessarily have to wait for the disk I/O to complete before it can perform another database operation. This can greatly improve throughput for some workloads.

There are several ways to relax the synchronized write requirement for your transactions:

  • Specify true to the EnvironmentMutableConfig.setTxnNoSync() method. This causes JE to not synchronously force any data to disk upon transaction commit. That is, the modifications are held entirely inside the JVM and the modifications are not forced to the file system for long-term storage. Note, however, that the data will eventually make it to the filesystem (assuming no application or OS crashes) as a part of JE's management of its logging buffers and/or cache.

    This form of a commit provides a weak durability guarantee because data loss can occur due to an application, JVM, or OS crash.

    This behavior is specified on a per-environment handle basis. In order for your application to exhibit consistent behavior, you need to specify this for all of the environment handles used in your application.

    You can achieve this behavior on a transaction by transaction basis by using Transaction.commitNoSync() to commit your transaction, or by specifying true to the TransactionConfig.setNoSync() method when starting the transaction.

  • Specify true to the EnvironmentConfig.setTxnWriteNoSync() method. This causes data to be synchronously written to the OS's file system buffers upon transaction commit. The data will eventually be written to disk, but this occurs when the operating system chooses to schedule the activity; the transaction commit can complete successfully before this disk I/O is performed by the OS.

    This form of commit protects you against application and JVM crashes, but not against OS crashes. This method offers less room for the possibility of data loss than does EnvironmentConfig.setTxnNoSync().

    This behavior is specified on a per-environment handle basis. In order for your application to exhibit consistent behavior, you need to specify this for all of the environment handles used in your application.

    You can achieve this behavior on a transaction by transaction basis by using Transaction.commitWriteNoSync() to commit your transaction, or by specifying true to TransactionConfig.setWriteNoSync() method when starting the transaction.