Object-Relational Mapping
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.
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.
References:
Archives