Object-Relational
Mapping
Exposing
the
ORM
Cache
drome increases dramatically with each new server that
operates on the same data set. Every operation that causes
data mutation in the primary data source (the database)
also produces the consequence that every cached version
of that entity in the cluster (except for the entry cached
in the server that made the update) becomes invalid.
Furthermore, each of the caches needs to know, or at least
have the ability to figure out, that its cache entry for that
entity is stale.
A number of tactics can be used to remedy the clus-tered-cache problem, but most can be categorized or
subsumed by one of three strategies:
• The initiating cache (the one that is doing the mutating) sends messages to the other caches in the cluster to
indicate the objects that have changed. These messages
may take on any form and use various transports and
group communication schemes.
• Each cache tries to figure things out on its own, continually checking to see if objects have changed in the
primary database and refreshing the entities in memory
when necessary.
• Caches rely upon an external process to notify them of
changes in the database. A cache may react to these messages simply by evicting the objects that it knows have
changed, or it may go and eagerly refresh its copy from
the database.
The best fit for a particular application is going to
depend both upon the application itself, as well as the
ability of its environment to support a given strategy. All
will clearly perform better if the number of writes is sufficiently low than if it is high, since data mutation is the
source of cache incoherency and the cause of traffic to
render the cache coherent.
The first and third approaches would appear to be
more network intensive in the face of a growing network,
since adding n instances (or nodes) to a network is going
to cause n additional messages to be sent (by either the
initiating node or the notifier) every time an object is
changed. Even though the second approach does not
send any messages to other nodes, it may have to check
frequently with the database. It never knows if an object
has changed, so even just to do a read it must ask the
database, the source of truth, to be sure it has the most
recent state. If every node is following this same procedure, then the traffic could end up being higher than the
other two approaches, creating a database bottleneck and
essentially executing without any caching at all.
It may be that some of the objects are immutable or
that the tolerance for stale data is higher for some objects
than for others, so the database needs to be consulted
only for a select smaller group of objects or at a specific
frequency. This might make the second approach more
palatable. It might also be that only a small percentage
of the objects are ever modified or that the environment
doesn’t allow for a connection to be established from an
external database-monitoring process to the ORM system;
thus, the first approach would be well suited for the task.
TRANSACTION ISOLATION
The notion that you would even need to consider the
transaction isolation of a cache is foreign to some. The
assumption is that a cache will work, and the isolation
will be correct. The fault with this way of thinking is that
there are as many different strategies of managing, loading, merging, evicting, and consulting the caches as there
are products that use caching, and each of these factors
may have an effect on transaction isolation.
The isolation level typically expected, whether in
ignorance or by experience, from a cache is usually
READ_COMMITTED. At the low end this is reasonable since,
in general, nobody anticipates getting a query result that
includes an uncommitted change from another transaction. Most serious products, therefore, do not merge the
contents of their transactional caches into their shared
cache until the transaction has successfully committed.
At the upper end of the spectrum there is some variety in the isolation inherent in a cache. The difference
between READ_COMMITTED and REPEATABLE_READ is well
defined, yet the cost is not bounded. One vendor may
decide that cache safety is paramount and comes only
through a given isolation level, gating all cache access by
coarse-grained locks, or acquiring the locks eagerly. The
problem with this well-intentioned perspective is that a
practice such as serializing all cache access carries a nontrivial cost. Many applications don’t have data dependencies spread across their domain model, and thus do not
actually have such strict isolation requirements, but are
still forced to pay the performance price.