Cache Replicator

Summary

It can be common for multiple server-side DLLs to each have a local cache of
the same data. For example, in a scaled up application, server A and server B
each runs an instance of the ProductManager
component. This component implements and maintains a local cache of all
available products. Because client requests are stateless and can go to either
server A or server B, the cache maintained by the ProductManager
component must always be synchronized with the database; for example, If ProductManager
on server A updates its cache and consequently the underlying physical data, then ProductManager
on server B should reflect these changes in its own cache. In other words,
changes to one instance of the cache should be replicated to all other cache
instances. This is the same exact concept as database replication except that we
dealing here with caches rather than with databases.

Here are some terms that are useful when describing cache replication
strategies:

Replicated Cache
A cache instance that notifies others when its contents change.

Notification
A mechanism that caches use to replicate changes. Notification can take the
form of events, messaging, or queuing mechanisms.

Topology
A layout that describes how multiple caches connect with and notify each
other.

Which topology to use often depends on system configuration. Some common
topologies include:

Peer cache replicator
Each replicated cache notified each and every other cache instance when its
contents change. This topology requires n-1 notifications per update
where n is the number of caches in the system. It does not readily
adapt when other cache instances are dynamically added or removed from the
system, because all cache instances must know about each other. The
following shows a peer cache replication topology that involves five cache
instances. A single update operation to cache instance A requires four
update notifications to cache instances B, C, D, and E:

Centralized cache replicator
Each replicated cache notifies a central cache instance when its contents
change. The central cache is dedicated to dispatching update notifications
to all other cache instances in the system. A central cache is more amenable
to topology changes (i.e., adding/removing cache instances), but it still
requires n-1 update operations where n is the number of cache
instances. The
following shows a peer cache replication topology that involves five cache
instances:

Irrespective of the used cache topology, it is beneficial to manage cache
replication in a generic fashion outside application and data access code. This
allows you to use a versatile and reusable system that is not tied to a
particular application of topology. Cache replicator allows you to manage cache
replication in a generic fashion outside application and data access code. It is
entirely contained and can be used to coordinate replication across heterogeneous
cache implementations.

The following figure illustrates the static structure for the Cache
Replicator :

ICache interface is implemented by both ConcreteCache
and ReplicatedCache. In this design, ReplicatedCache
acts as a Decorator since it implements ICache
and delegates all its operations to ConcreteCache.
In addition, ReplicatedCache maintains a list of ICacheUpdateObservers
that it notifies whenever it delegates a Put or Remove
operation. CacheReplicator is a special
implementation of ICacheUpdateObserver that updates
another Cache instance when a ReplicatedCache
notifies it. In other words, CacheReplicator
serves as the notification channel from a source cache to a target cache

You can also implement ICacheUpdateObserver to do anything related to cache
update operations. For example, if you want to log all cache operations, you can
implement ICacheUpdateObserver in such a way (not shown above) that it writes to
the log whenever notified by ReplicatedCache.

The following figure illustrates the sequence diagram when a client issues a ReplicatedCache.Put
operation.

The ReplicatedCache instance delegates the call
to its local ConcreteCache implementation. Next, ReplicatedCache
notifies each registered CacheReplicator by calling
its EntryAdded method. Each CacheReplicator
responds by invoking the corresponding operation on another Cache
instance.

Recall that CacheReplicator's role in this
pattern is to serve as the notification channel from a source cache to a target
cache. If the participating caches exist entirely within the same process, then
you can implement notifications using .NET events or asynchronous delegates. If CacheReplicator
needs to sends notifications to other cache instances that live outside the
process or on another machine, then cross-process (.NET Remoting) or
cross-machine (.NET Remoting/messaging/queueing) communication is required. It
also feasible for replicated caches to exist on heterogeneous platforms (Unix,
Linux, etc) provided that the CacheReplicator
implementation takes care of the required conversions for the cached data.

The source cache must always be a ReplicatedCache
instance that issues update notifications, but the target can be any ICache
implementation. If, according to the cache topology, a target cache is also
responsible for delegating cache changes to other caches (like the central
cache topology), attach the CacheReplicator to
the target ReplicatedCache. If the target is not
responsible for delegating changes to other caches, then you can attach the CacheReplicator
directly to the appropriate ConcreteCache.

Has configurable cache replication topologies
Different replication topologies are more effective than others depending on
the nature of cached data and the systems that share it. Cache Replicator
accommodates the dynamic integration of multiple cache topologies because
you can wire its participants together in nearly any combination.

Isolates cache replication strategies
Cache replication details are encapsulated within their own components. They
remain isolated from general caching logic, client logic and other code.

Chatty Protocol
The system notifies every participating cache instance whenever a cache
entry changes. If these notification occur across the network and cache
updates are quite often, then the replication overhead can subsume the
performance benefit of caching data in the first place. Buffering cache
operations can help solve this problem at the expense of code complexity.

Potential for redundant notifications
In a centralized cache replication topology, cache instances that initiate
change notifications will also be notified, unless you build a mechanism to
explicitly prevent this. This is not a significant drawback unless cache
updates are frequent and notification is expensive. You must also consider
this behavior when setting up cache topologies to avoid infinite loops where
two or more replicated caches continuously notify each other regarding the
same cache operation.

Potential for inconsistent data
Timing scenarios, race conditions, reliability constraints, and concurrent
updates to the same data can surface as inconsistent data across multiple
cache instances.