READ_COMMITTED isolation can be sufficient and
satisfactory for most applications, but those that do
require stricter isolation should be permitted to perform
large-scale compound cache operations atomically when
necessary. The difference is that it should be a caching
option rather than a characteristic entrenched in the
implementation.
ISOLATION STRATEGIES
We just discussed transaction isolation requirements from
the user perspective, but only waved our hands about
the fact that such isolation requirements may be implemented differently. In this section we describe some of
the common strategies for handling isolation between
applications.
IMPLICIT COPY-ON-READ
One of the tried-and-true approaches to guaranteeing
data consistency and complete isolation is implicit copy-on-read, which creates a local transactional cache copy
of the object as soon as it is read in the unit of work.
This certainly guarantees that whatever happens, no
other application will see the changes to that object until
they are written to the persistent store or merged into
the shared cache. The following is a sample sequence of
events in a traditional implicit copy-on-read scenario:
1. Begin tx.
2. Read object (get object from shared cache or from
database).
3. Copy object and insert in tx cache.
4. Return copy to user.
5. User modifies copy.
6. Tx commit begins.
7. Modified copy contents get sent to persistent storage.
8. Tx commit completes.
9. Changes made to object are merged into shared cache.
A window exists between the time the transaction
completes its commit phase and the time the changes are
merged into the shared cache. This means that there is
some nonzero amount of time in which another application could get a stale copy of the object from the shared
cache, even though it has been updated in the database.
This window is of no real consequence, however, since
if the second application is only reading the object, then
it might just as well have read it before the first application committed its transaction and gotten the same stale
data. The fact that it happened to have read it within that
window of time has no relevance. If it were to have a stale
copy of the object and perform a write on it, however, it
would be bad if the changes of the first application ended
up getting overwritten or lost as a result of the staleness
of the initial state of the object being written to by the
second application. The solution to this problem lies in
optimistic locking of the entity to ensure that no changes
get overwritten or lost. 3
Because the implementation automatically performs
the copying without the user needing to do anything,
one of the advantages of an implicit copy-on-read is that
the user need not take any special action when deciding
to update the object. The user can rely on the copy that
it has been using to read from and perform the writes on
that copy. Any and all updates will be sent to the database
at the appropriate time.
A marked problem with eagerly copying-on-read is
the accumulation of objects in the transactional cache.
The cache does not necessarily distinguish between
objects that were read and those that were updated or
made transactional because an update was forthcoming.
Applications that start a transaction and do a great deal
of reading but only a little writing will see their transactional caches grow to include all of the objects, not just
those that contain changes and need to be written out.
IMPLICIT COPY-ON-WRITE
A more efficient approach to managing the transactional
cache space is to do no copying of objects as they are read
into the transaction, but only as they are modified by the
user. This implicit copy-on-write approach limits the trans-
action to containing only those objects that have been
changed, and it does not leave the transaction vulnerable
to bulging instance counts and management costs. A
typical implicit copy-on-write sequence is:
1. Begin tx.
2. Read object (get object from shared cache or from
database).
3. Return object to user.
4. User modifies object, causing copy to be created with
changes stored inside it.
5. Insert copy in tx cache.
6. Tx commit begins.
7. Modified copy contents get sent to persistent storage.
8. Tx commit completes.
9. Changes made to object are merged into shared cache.
This appears to be an elegant approach to managing
transactional data, since only dirty objects end up being
copied, and this would again be performed automatically
by the implementation. Objects that became transac-
tional solely for reading turn out not really to be transac-
tional at all and don’t take up transactional space.
The catch to this strategy shows up when the copy