Introduction à Coherence: Part II


Nous avons vu au cours de notre article précédent comment intégrer Oracle Coherence dans l'application PetClinic de SpringSource. Cependant, si l'application est à peu près utilisable, il reste de nombreux points à améliorer.
Dans cet article, nous allons présenter les topologies de caches offertes par Coherence et remplacer le cache par défaut par notre propre cache.

Commençons par regarder de plus près les options qui s'offrent à nous.

Les principales topologies de cache

Coherence fournit un certain nombre de types de caches. Les topologies les plus importantes sont:

Ce cache duplique chaque entrée qu'il contient sur chaque noeud du cluster. La taille de ce cache est donc limitée par la mémoire individuellement disponible sur chaque machine. Toute écriture de donnée dans ce cache provoque une mise à jour de l'ensemble des noeuds du cluster. Cette topologie convient donc particulièrement au stockage de données peu volumineuses et fréquemment accédées en lecture.

Ce cache répartit ses données de manière équilibrée sur l'ensemble des noeuds. Pour chaque donnée, une ou plusieurs copies de secours sont aussi stockées sur des noeuds différents pour prévenir toute perte en cas de défaillance du noeud maître. La gestion de la localisation de la donnée est totalement transparente pour les utilisateurs du cache. Il est possible d'augmenter la taille du cache simplement en ajoutant des noeuds au cluster.

Le near cache est un cache hybride typiquement utilisé pour disposer d'un cache distribué couplé à un cache local propre à chaque noeud. Si les données et l'utilisation qui en est faite s'y prêtent, la présence du cache local permet d'améliorer sensiblement les performances en lecture au prix d'un surcout en écriture ou de données éventuellement légèrement périmées (en fonction de la politique d'invalidation choisie).

Get the stuff

Pour les courageux qui ont dégainé leur IDE pour suivre l'article précédent, vous pouvez repartir avec le même projet. Pour les autres (on vous a vu !), vous pouvez récupérer le projet prêt-à-coder (collection automne-hiver):

  • sur Github:
git clone -n git://github.com/obourgain/petclinic-coherence.git
git checkout article1-end
  • sous forme de zip: zip

Vous pouvez alors importer le projet dans votre IDE préféré en suivant les étapes décrites ici.

Time to code !

Nous allons dans un premier temps créer les caches dont nous aurons besoin. Pour cela, créez un fichier petclinic-cache-config.xml dans le dossier src/main/resources avec le contenu suivant:

<?xml version="1.0"?>
<cache-config
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://xmlns.oracle.com/coherence/coherence-cache-config"
	xsi:schemaLocation="http://xmlns.oracle.com/coherence/coherence-cache-config coherence-cache-config.xsd">
</cache-config>

Définition des caches

What's the plan ?

Les Vets et les PetTypes sont des données statiques peu volumineuses et donc parfaitement adaptées au stockage dans des caches répliqués. Les Owners, Pets et Visits sont eux amenés à être créés et modifiés régulièrement. Leur nombre peut croître fortement au cours de la vie de l'application, éventuellement au point de ne plus tenir dans la mémoire d'une seule JVM. Nous utiliserons donc un cache distribué pour ces données sans toucher (pour le moment) au modèle: chaque entrée du cache sera donc constituée d'un Owner ainsi que des Pets et Visits associés.

Définition des caches

Let's do it !

Coherence stocke toutes ses données dans des backing maps. Chaque NamedCache est une vue sur une backing map et plusieurs caches peuvent utiliser la même backing map. Les données restent strictement séparées logiquement mais la configuration est factorisée.

Nous allons donc commencer par définir une backing map commune pour tous nos caches. Il est possible de paramétrer finement le comportement de cette backing map mais nous allons faire simple pour le moment.

Toujours dans le fichier petclinic-cache-config.xml, déclarons une configuration de cache local qui nous servira de backing map :

<local-scheme>
	<scheme-name>noeviction-backing-map</scheme-name>
</local-scheme>

Nous pouvons maintenant définir deux configurations de caches: une pour les caches répliqués et une pour les caches distribués:

<replicated-scheme>
	<scheme-name>replicated-scheme</scheme-name>
	<service-name>ReplicatedCache</service-name>
	<backing-map-scheme>
		<local-scheme>
			<scheme-ref>noeviction-backing-map</scheme-ref>
		</local-scheme>
	</backing-map-scheme>
</replicated-scheme>
<distributed-scheme>
	<scheme-name>distributed-scheme</scheme-name>
	<service-name>DistributedCache</service-name>
	<backing-map-scheme>
		<local-scheme>
			<scheme-ref>noeviction-backing-map</scheme-ref>
		</local-scheme>
	</backing-map-scheme>
</distributed-scheme>

Finalement, nous pouvons déclarer chaque cache avec sa configuration. Au runtime, nous accéderons aux caches par les noms définis ci-dessous.

<caching-scheme-mapping>
	<cache-mapping>
		<cache-name>pet-types-cache</cache-name>
		<scheme-name>replicated-scheme</scheme-name>
	</cache-mapping>
 
	<cache-mapping>
		<cache-name>vet-cache</cache-name>
		<scheme-name>replicated-scheme</scheme-name>
	</cache-mapping>
 
	<cache-mapping>
		<cache-name>owner-cache</cache-name>
		<scheme-name>distributed-scheme</scheme-name>
	</cache-mapping>
</caching-scheme-mapping>

Au revoir la DB !

Tous les éléments de configuration sont désormais en place pour pouvoir remplacer tous les accès à la base de données par des accès à Coherence. Retirez la référence vers la jdbcClinic du bean CoherenceClinic et supprimez le bean jdbcClinic associé dans le fichier applicationContext-coherence.xml:

<bean id="jdbcClinic" class="org.springframework.samples.petclinic.jdbc.SimpleJdbcClinic"/>
 
<bean id="clinic" class="com.zenika.petclinic.coherence.CoherenceClinic">
	<property name="jdbcClinic" ref="jdbcClinic" />
</bean>

devient donc

<bean id="clinic" class="com.zenika.petclinic.coherence.CoherenceClinic" />

Nous allons ensuite ajouter dans CoherenceClinic des méthodes getPetTypesCache() et getVetsCache() ainsi que mettre à jour getOwnersCache() pour utiliser les noms des caches que nous avons définis dans le fichier petclinic-cache-config.xml.

private NamedCache getOwnersCache() {
	return CacheFactory.getCache("owner-cache");
}
 
private NamedCache getPetTypesCache() {
	return CacheFactory.getCache("pet-types-cache");
}
 
private NamedCache getVetsCache() {
	return CacheFactory.getCache("vet-cache");
}

Les méthodes storeOwner() et loadOwner() sont simplifiées puisqu'elles n'accèdent plus à la base de données. Notez qu'auparavant, l'id était généré par la jdbcClinic. Maintenant nous devons le faire nous même.

public void storeOwner(Owner owner) throws DataAccessException {
	if (owner.getId() == null) {
		owner.setId(RandomUtils.nextInt());
	}
	getOwnersCache().put(owner.getId(), owner);
}
 
public Owner loadOwner(int id) throws DataAccessException {
	return (Owner) getOwnersCache().get(id);
}

Pour récupérer tous les Owners dont le nom correspond à celui donné, nous allons itérer sur le cache des Owners et construire une liste de tous ceux dont le nom matche le paramètre. Cette solution est loin d'être optimale puisque la méthode values() va faire transiter sur le réseau, de-sérialiser et monter dans la mémoire de la JVM qui effectue le traitement tout le contenu du cache. Dans le prochain article, nous verrons comment distribuer cette opération sur tous les noeuds du cache afin de réduire l'utilisation du réseau et de la mémoire.

public Collection<Owner> findOwners(String lastName) throws DataAccessException {
	Collection<Owner> owners = getOwnersCache().values();
	String lowerCasedLastName = lastName.toLowerCase();
 
	List<Owner> result = new ArrayList<Owner>();
	for (Owner owner : owners) {
		if (owner.getLastName().toLowerCase().startsWith(lowerCasedLastName)) {
			result.add(owner);
		}
	}
	return result;
}

Pour loadPet(), nous devons aussi parcourir le cache des Owners pour trouver celui qui porte le Pet recherché. Cette méthode pourra être optimisée avec le même mécanisme que pour findOwners().

public Pet loadPet(int id) throws DataAccessException {
	Collection<Owner> owners = getOwnersCache().values();
	for (Owner owner : owners) {
		for (Pet pet : owner.getPets()) {
			if (pet.getId().equals(id)) {
				return pet;
			}
		}
	}
	return null;
}

L'enregistrement des Pets est particulier. En effet, ils appartiennent à la grappe d'objets liée à un Owner dans le cache. Il faudra donc récupérer ledit Owner, ajouter/modifier le Pet puis remettre le Owner dans le cache (et donc son graphe complet). Cette façon de faire n'est pas transactionnelle: si une modification du Owner par un autre accès au cache survient entre le get() et le put(), cette modification disparaîtra. Les opérations get() et put() sont par contre garanties atomiques. L'approche grossière ci-dessous suffira pour l'instant mais nous verrons dans un prochain article comment faire les choses proprement.

// FIXME not transactional !!!
public void storePet(Pet pet) throws DataAccessException {
	Owner owner = (Owner) getOwnersCache().get(pet.getOwner().getId());
 
	if (owner == null) {
		throw new IllegalStateException("Owner with id: " + pet.getOwner().getId() + " not found");
	}
 
	if (pet.getId() == null) {
		pet.setId(RandomUtils.nextInt());
		owner.addPet(pet);
	} else {
		Pet existingPet = owner.getPet(pet.getName());
		existingPet.setBirthDate(pet.getBirthDate());
		existingPet.setName(pet.getName());
		existingPet.setType(pet.getType());
	}
	getOwnersCache().put(owner.getId(), owner);
}

storeVisit() pose un problème similaire: il faut retrouver le Pet puis le Owner associé. Ici non plus, la modification n'est pas transactionnelle et il faudra attendre un futur article pour faire mieux (et avoir un code plus élégant :).

public void storeVisit(Visit visit) throws DataAccessException {
	for (Owner owner : (Collection<Owner>) getOwnersCache().values()) {
		for (Pet pet : owner.getPets()) {
			if (pet.getId().equals(visit.getPet().getId())) {
				if (visit.getId() == null) {
					visit.setId(RandomUtils.nextInt());
				}
				pet.addVisit(visit);
				getOwnersCache().put(owner.getId(), owner);
				break;
			}
		}
	}
}

Les méthodes getVets() et getPetTypes() ne posent pas particulièrement de problèmes, nous utilisons l'API Map pour récupérer toutes les valeurs. Notez que la collection renvoyée par Coherence est immuable, nous ne pourrions donc pas y ajouter ou supprimer de données. Pour notre application, ces données sont en read only, nous n'avons donc pas de problème.

public Collection<Vet> getVets() throws DataAccessException {
	return getVetsCache().values();
}
 
public Collection<PetType> getPetTypes() throws DataAccessException {
	return getPetTypesCache().values();
}

Pour limiter la quantité de code moche (et surtout non transactionnel) dans cet article, nous traiterons deletePet() plus tard, avec des fonctionnalité plus avancées de Coherence.

public void deletePet(int id) throws DataAccessException {
	throw new UnsupportedOperationException("Pet with id " + id + " is not allowed to die");
}

Ready to test

Nous avons maintenant du joli code tout neuf, mais comment le tester puisque nous n'accédons plus à la base de donnée ? Et bien nous avons préparé pour vous une petite classe qui va injecter des données dans le cache au démarrage de Tomcat (oui, notre générosité n'a pas de limites...) : DataInitializer.java. Il faut ajouter dans le web.xml :

<listener>
	<listener-class>com.zenika.petclinic.coherence.DataInitializer</listener-class>
</listener>

Vous pouvez tester l'application et tout devrait fonctionner à part, évidement, la suppression de Pets.

Le résultat final est récupérable sur Github :

git clone -n git://github.com/obourgain/petclinic-coherence.git
git checkout article2-end

et en zip

Au programme du prochain article: comment faire proprement des recherches dans un cache Coherence !

Article co-écrit par Guillaume Tinon et Olivier Bourgain.

Index des articles de la série Coherence :


Commentaires

1. Le mercredi 11 avril 2012, 17:38 par nomadeous

Bonjour,
bonne série d'articles pour entrer dans Coherence, merci.
Par contre, un truc que j'ai dû loupé.
Comment Coherence va loader le fichier petclinic-cache-config.xml ? Je ne vois pas d'options ou de déclarations positionnées ?

merci d'avance.

Fil des commentaires de ce billet

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.