Berkeley DB Reference Guide:
Transaction Protected Applications

PrevRefNext

Building transaction protected routines

Consider an application suite where multiple threads of control (multiple processes or threads in one or more processes) are changing the values associated with a key in one or more databases. Specifically, they are taking the current value, incrementing it, and then storing it back into the database. There are two reasons that such an application needs transactions (other than the obvious requirement of recoverability after application or system failure):

The first reason for transactions is that the application needs atomicity. Since we want to change a value in the database, we must make sure that once we read it, no other thread of control modifies it. For example, assume that both thread #1 and thread #2 are doing similar operations in the database, where thread #1 is incrementing records by 3, and thread #2 is incrementing records by 5. We want to increment the record by a total of 8. If the operations interleave in the right (well, wrong) order, that is not what will happen:

thread #1  read record: the value is 2
thread #2  read record: the value is 2
thread #2  write record + 5 back into the database (new value 7)
thread #1  write record + 3 back into the database (new value 5)

As you can see, instead of incrementing the record by a total of 8, we've only incremented it by 3, because thread #1 overwrote thread #2's change. By wrapping the operations in transactions, we ensure that this cannot happen. In a transaction, when the first thread reads the record, locks are acquired that will not be released until the transaction finishes, guaranteeing that all other readers and writers will block, waiting for the first thread's transaction to complete (or to be aborted).

The second reason for transactions is that when multiple processes or threads of control are modifying the database, there is the potential for deadlock. In Berkeley DB, deadlock is signified by an error return from the Berkeley DB function of DB_LOCK_DEADLOCK.

Here is an example function that does transaction-protected increments on database records.

void
increment(dbenv, dbp, key, increment)
	DB_ENV *dbenv;
	DB *dbp;
	DBT *key;
	int increment;
{
	DBT data;
	DB_TXN *tid;
	int ret;
	char buf[64];

/* Initialization. */ memset(&data, 0, sizeof(data));

/* Abort and retry the modification. */ if (0) { retry: if ((ret = txn_abort(tid)) != 0) dbenv->err(dbenv, ret, "txn_abort"); /* FALLTHROUGH */ }

/* Begin the transaction. */ if ((ret = txn_begin(dbenv, NULL, &tid, 0)) != 0) dbenv->err(dbenv, ret, "txn_begin");

/* Get the key. */ switch (ret = dbp->get(dbp, tid, key, &data, 0)) { case DB_LOCK_DEADLOCK: goto retry; case 0: break; case DB_NOTFOUND: if (txn_commit(tid, 0)) dbenv->err(dbenv, ret, "txn_commit"); return (0); }

/* Get the current value, add the increment. */ (void)sprintf(buf, "%d", atoi(data.data) + increment); data.data = buf; data.size = strlen(buf) + 1;

/* Store the new value. */ switch (ret = dbp->put(dbp, tid, key, &data, 0)) { case DB_LOCK_DEADLOCK: goto retry; case 0: break; default: (void)txn_abort(tid); dbenv->err(dbenv, ret, "dbp->put"); }

/* The transaction finished, commit it. */ if ((ret = txn_commit(tid, 0)) != 0) dbenv->err(dbenv, ret, "txn_commit");

return (0); }

In applications supporting transactions, all Berkeley DB functions have an additional possible error return: DB_LOCK_DEADLOCK. In the sample code, any time the Berkeley DB function returns DB_LOCK_DEADLOCK, the transaction is aborted (by calling txn_abort, which releases all of its resources), and then the transaction is retried from the beginning. There is no requirement that the transaction be attempted again, but that is the common course of action for an application.

PrevRefNext

Copyright Sleepycat Software