Configuration des chargeurs de base de données

Les chargeurs sont des plug-in de mappe de sauvegarde appelés lorsque des modifications sont apportées à la mappe de sauvegarde ou lorsque cette dernière est dans l'impossibilité de répondre à une demande de données (absence dans le cache).

Remarques sur le préchargement

Les chargeurs sont des plug-in de mappe de sauvegarde appelés lorsque des modifications sont apportées à la mappe de sauvegarde ou lorsque cette dernière est dans l'impossibilité de répondre à une demande de données (absence dans le cache). Pour une présentation de la manière dont eXtreme Scale interagit avec un chargeur, voir Cache en ligne.

Chaque mappe de sauvegarde dispose d'un attribut booléen preloadMode qui est défini pour indiquer si le préchargement d'une mappe s'exécute de manière asynchrone. Par défaut, l'attribut preloadMode est défini sur false, ce qui signifie que l'initialisation de la mappe de sauvegarde ne s'effectue que si le préchargement de la mappe est terminé. Par exemple, l'initialisation de la mappe de sauvegarde n'est pas complète tant que la méthode preloadMap s'exécute. Si la méthode preloadMap reconnaît une grande quantité de données en provenance de son programme d'arrière plan et les charge dans la mappe, son exécution peut prendre un certain temps. Dans ce cas, vous pouvez configurer l'utilisation du préchargement asynchrone pour une mappe de sauvegarde donnée en définissant l'attribut preloadMode sur true. Ce paramètre amène le code d'initialisation de la mappe de sauvegarde à démarrer une unité d'exécution qui appelle la méthode preloadMap pour permettre d'initialiser une mappe de sauvegarde lorsque le préchargement de la mappe est en cours.

Dans un scénario eXtreme Scale réparti, l'un des motifs de préchargement est le préchargement client. Dans le modèle de préchargement client, un client eXtreme Scale est responsable de l'extraction des données à partir du programme d'arrière-plan et de l'insertion des données dans le serveur de conteneur réparti à l'aide d'agents DataGrid. En outre, le préchargement client peut être exécuté dans la méthode Loader.preloadMap dans une seule et unique partition spécifique. Dans ce cas, le chargement asynchrone des données dans la grille devient crucial. Si le préchargement client était exécuté dans la même unité d'exécution, la mappe de sauvegarde ne serait jamais initialisée, de sorte que la partition dans laquelle elle réside ne passerait jamais au statut ONLINE. Par conséquent, le client eXtreme Scale ne pourrait pas envoyer la demande à la partition, ce qui provoquerait une exception au final.

Si un client eXtreme Scale est utilisé dans la méthode preloadMap, vous devez affecter la valeur true à l'attribut preloadMode. Il est également possible de lancer une unité d'exécution dans le code de préchargement du client.

Le fragment de code ci-dessous illustre comment l'attribut preloadMode est défini pour activer le préchargement asynchrone :

BackingMap bm = og.defineMap( "map1" );
bm.setPreloadMode( true );

L'attribut preloadMode peut également être défini à l'aide d'un fichier XML tel qu'illustré dans l'exemple suivant :

<backingMap name="map1" preloadMode="true" pluginCollectionRef="map1" 
	lockStrategy="OPTIMISTIC" />

Objet TxID et utilisation de l'interface TransactionCallback

Les méthodes get et batchUpdate sur l'interface Loader reçoivent un objet TxID qui représente la transaction Session nécessitant d'exécuter l'opération get ou batchUpdate. Les méthodes get et batchUpdate peuvent être appelées plusieurs fois par transaction. Par conséquent, les objets inclus dans l'étendue de la transaction nécessaires au chargeur sont généralement conservés dans un emplacement de l'objet TxID. Un chargeur JDBC (Java Database Connectivity) est utilisé pour illustrer comment un chargeur utilise l'objet TxID et les interfaces TransactionCallback.

Plusieurs mappes ObjectGrid peuvent être stockées dans la même base de données. Chaque mappe possède son propre chargeur, et chaque chargeur peut devoir se connecter à la même base de données. Lorsque les chargeurs se connectent à la base de données, ils doivent utiliser la même connexion JDBC pour valider les modifications dans chaque table de la même transaction de base de données. En règle générale, la personne qui écrit l'implémentation Loader écrit également l'implémentation de TransactionCallback. La meilleure méthodes est lorsque l'interface TransactionCallback est étendue pour ajouter des méthodes requises par le chargeur pour l'obtention d'une connexion à la base de données et pour mettre en cache les instructions préparées. Cette méthode devient une évidence lorsque vous constatez comment les interfaces TransactionCallback et TxID sont utilisées par le chargeur.

Par exemple, le chargeur peut nécessiter l'extension de l'interfaceTransactionCallback de la manière suivante :

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import com.ibm.websphere.objectgrid.TxID;
public interface MyTransactionCallback extends TransactionCallback
{
    Connection getAutoCommitConnection(TxID tx, String databaseName) throws SQLException;
    Connection getConnection(TxID tx, String databaseName, int isolationLevel ) throws SQLException;
    PreparedStatement getPreparedStatement(TxID tx, Connection conn, String tableName, String sql) 
			throws SQLException;
    Collection getPreparedStatementCollection( TxID tx, Connection conn, 	String tableName );
}

A l'aide de ces nouvelles méthodes, les méthodes Loader get et batchUpdate peuvent obtenir une connexion comme suit :

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import com.ibm.websphere.objectgrid.TxID;
private Connection getConnection(TxID tx, int isolationLevel)
{
    Connection conn = ivTcb.getConnection(tx, databaseName, isolationLevel );
    return conn;
}

Dans l'exemple précédent et dans les exemples qui suivent, ivTcb et ivOcb sont des variables d'instance du chargeur qui ont été initialisées tel que décrit dans la section Remarques sur le préchargement. La variable ivTcb est une référence à l'instance MyTransactionCallback et la variable ivOcb est une référence à l'instance MyOptimisticCallback. La variable databaseName est une variable d'instance du chargeur qui a été définie en tant que propriété du chargeur lors de l'initialisation de la mappe de sauvegarde. L'argument isolationLevel est l'une des constantes de la connexion JDBC définies pour les différents niveaux d'isolement pris en charge par JDBC. Si le chargeur utilise une implémentation optimiste, la méthode get utilise généralement une connexion JDBC de validation automatique pour extraire les données de la base de données. Dans ce cas, il se peut que le chargeur possède une méthode getAutoCommitConnection implémentée de la façon suivante :

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import com.ibm.websphere.objectgrid.TxID;
private Connection getAutoCommitConnection(TxID tx)
{
    Connection conn = ivTcb.getAutoCommitConnection(tx, databaseName);
    return conn;
}

Rappelez-vous que la méthode batchUpdate comporte l'instruction switch suivante :

switch ( logElement.getType().getCode() )
{
    case LogElement.CODE_INSERT:
        buildBatchSQLInsert( tx, key, value, conn );
        break;
    case LogElement.CODE_UPDATE:
        buildBatchSQLUpdate( tx, key, value, conn );
        break;
    case LogElement.CODE_DELETE:
        buildBatchSQLDelete( tx, key, conn );
        break;
}

Chacun méthode buildBatchSQL utilise l'interface MyTransactionCallback pour obtenir une instruction préparée. Le fragment de code ci-dessous présente la méthode buildBatchSQLUpdate créant une instruction SQL update pour mettre à jour une entrée EmployeeRecord et l'ajouter pour la mise à jour par lots :

private void buildBatchSQLUpdate( TxID tx, Object key, Object value, 
	Connection conn ) 
throws SQLException, LoaderException
{
    String sql = "update EMPLOYEE set LASTNAME = ?, FIRSTNAME = ?, DEPTNO = ?,
    SEQNO = ?, MGRNO = ? where EMPNO = ?";
    PreparedStatement sqlUpdate = ivTcb.getPreparedStatement( tx, conn, 
			"employee", sql );
    EmployeeRecord emp = (EmployeeRecord) value;
    sqlUpdate.setString(1, emp.getLastName());
    sqlUpdate.setString(2, emp.getFirstName());
    sqlUpdate.setString(3, emp.getDepartmentName());
    sqlUpdate.setLong(4, emp.getSequenceNumber());
    sqlUpdate.setInt(5, emp.getManagerNumber());
    sqlUpdate.setInt(6, key);
    sqlUpdate.addBatch();
}

Une fois que la boucle batchUpdate a créé toutes les instructions préparées, elle appelle la méthode getPreparedStatementCollection. Cette dernière est implémentée comme suit :

private Collection getPreparedStatementCollection( TxID tx, Connection conn )
{
    return ( ivTcb.getPreparedStatementCollection( tx, conn, "employee" ) );
}
Lorsque l'application appelle la méthode commit sur la session, le code de session appelle la méthode commit sur la méthode TransactionCallback après avoir envoyé toutes les modifications apportées par la transaction vers le chargeur pour chaque mappe qui a été modifiée par la transaction. Etant donné que tous les chargeurs ont utilisé la méthode MyTransactionCallback pour établir la connexion et les instructions préparées requises, la méthode TransactionCallback identifie la connexion à utiliser pour demander la validation des modifications par la programme d'arrière plan. L'extension de l'interface TransactionCallback avec des méthodes requises par chaque chargeur présente donc les avantages suivants :
  • L'objet TransactionCallback incorpore l'utilisation des emplacements TxID pour les données incluses dans l'étendue de la transaction et le chargeur n'a pas besoin des informations sur les emplacements TxID. Le chargeur doit uniquement reconnaître les méthodes ajoutées à l'objet TransactionCallback utilisant l'interface MyTransactionCallback pour les fonctions de prise en charge requises par le chargeur.
  • L'objet TransactionCallback peut permettre le partage de la connexion entre chaque chargeur qui établit la connexion avec le même programme d'arrière plan pour éviter un protocole commit à deux phases.
  • L'objet TransactionCallback peut faire en sorte que la connexion au programme d'arrière plan est terminée via une instruction commit ou rollback appelée lors de la connexion si nécessaire.
  • TransactionCallback garantit le nettoyage des ressources de la base de données à la fin d'une transaction.
  • TransactionCallback se masque s'il établit une connexion gérée à partir d'un environnement géré tel que WebSphere Application Server ou autre serveur d'applications compatible Java 2 Platform, Enterprise Edition (J2EE). Cet avantage permet l'utilisation du même code de chargeur dans un environnement géré et dans un environnement non géré. Seul le plug-in TransactionCallback doit être modifié.
  • Pour plus de détails sur l'utilisation des emplacements TxID par l'implémentation TransactionCallback pour les données incluses dans l'étendue de la transaction, reportez-vous à la rubrique Plug-in TransactionCallback.

OptimisticCallback

Comme mentionné précédemment, le chargeur peut utiliser une approche optimiste pour le contrôle des accès simultanés. Dans ce cas, l'exemple de la méthode buildBatchSQLUpdate doit être légèrement modifié pour l'implémentation d'une approche optimiste. Il existe différentes façons pour mettre en oeuvre une approche optimiste. La façon la plus courante consiste à disposer d'une colonne d'horodatage ou d'une colonne de compteur de numéros de séquence pour la gestion des versions de mise à jour de la ligne. Supposons que la table employee comporte une colonne de numéros de séquence qui s'incrémentent à chaque mise à jour de la ligne. Vous modifiez ensuite la signature de la méthode buildBatchSQLUpdate pour qu'elle soit transmise à l'objet LogElement et non à la paire clé-valeur. Elle doit également utiliser l'objet OptimisticCallback relié à la mappe de sauvegarde pour l'obtention de l'objet de version initiale et la mise à jour de l'objet de version. L'exemple suivant est celui d'une méthode buildBatchSQLUpdate modifiée qui utilise la variable d'instance ivOcb initialisée tel que décrit dans la section preloadMap :

modified batch-update method code example
private void buildBatchSQLUpdate( TxID tx, LogElement le, Connection conn )
	throws SQLException, LoaderException
{
    // Obtenir l'objet de version initiale lors de la dernière lecture ou mise à jour de cette entrée
    // de mappe dans la base de données.
    Employee emp = (Employee) le.getCurrentValue();
    long initialVersion = ((Long) le.getVersionedValue()).longValue();
    // Obtenir l'objet de version de l'objet Employee mis à jour pour l'opération SQL
    //update.
    Long currentVersion = (Long)ivOcb.getVersionedObjectForValue( emp );
    long nextVersion = currentVersion.longValue();
    // Créer maintenant l'instruction SQL update qui inclut l'objet de version dans la clause where
    // pour la vérification optimiste.
    String sql = "update EMPLOYEE set LASTNAME = ?, FIRSTNAME = ?,
    DEPTNO = ?,SEQNO = ?, MGRNO = ? where EMPNO = ? and SEQNO = ?";
    PreparedStatement sqlUpdate = ivTcb.getPreparedStatement( tx, conn, 
			"employee", sql );
    sqlUpdate.setString(1, emp.getLastName());
    sqlUpdate.setString(2, emp.getFirstName());
    sqlUpdate.setString(3, emp.getDepartmentName());
    sqlUpdate.setLong(4, nextVersion );
    sqlUpdate.setInt(5, emp.getManagerNumber());
    sqlUpdate.setInt(6, key);
    sqlUpdate.setLong(7, initialVersion);
    sqlUpdate.addBatch();
}

L'exemple montre que l'objet LogElement est utilisé pour obtenir la valeur de la version initiale. Lorsque la transaction accède pour la première fois à l'entrée de mappe, un objet LogElement est créé avec l'objet Employee initial obtenu à partir de la mappe. L'objet Employee initial est également transmis à la méthode getVersionedObjectForValue dans l'interface OptimisticCallback et le résultat est enregistré dans l'objet LogElement. Ce traitement a lieu avant qu'une application soit référencée à l'objet Employee initial et qu'elle appelle une méthode modifiant l'état de l'objet Employee initial.

L'exemple montre que le chargeur utilise la méthode getVersiondObjectForValue pour obtenir l'objet de version pour l'objet Employee mis à jour. Avant d'appeler la méthode batchUpdate dans l'interface Loader, eXtreme Scale appelle la méthode updateVersionedObjectForValue dans l'interface OptimisticCallback pour provoquer la génération d'un nouvel objet de version pour l'objet Employee mis à jour. Une fois que la méthode batchUpdate est renvoyée à l'ObjectGrid, l'objet LogElement est mis à jour avec l'objet de version actuelle et devient le nouvel objet de version initiale. Cette étape est nécessaire parce qu'il se peut que l'application ait appelé la méthode flush sur la mappe et non la méthode commit sur la session. Il est possible que le chargeur soit appelé plusieurs fois par une seule transaction pour la même clé. C'est pourquoi, eXtreme Scale fait en sorte que l'objet LogElement soit mis à jour avec l'objet de nouvelle version à chaque mise à jour de la ligne dans la table employee.

Désormais que le chargeur détient l'objet de version initiale et l'objet de prochaine version, il peut exécuter une instruction SQL update qui définit la colonne SEQNO sur la valeur de l'objet de prochaine version et utilise cette dernière dans la clause where. Cette approche est parfois désignée comme étant une instruction update sur-qualifiée. L'utilisation d'une instruction update sur-qualifiée permet à la base de données relationnelle de vérifier que la ligne n'a pas été modifiée par une autre transaction entre la lecture des données de la base de données par cette transaction et la mise à jour de la base de données par cette transaction. Si une autre transaction a modifié la ligne, le tableau de comptage renvoyé par la mise à jour par lots indique qu'aucune ligne n'a été mise à jour pour cette clé. Le chargeur est chargé de vérifier que l'opération SQL update n'a pas mis à jour la ligne. Si c'est le cas, il affiche une exception com.ibm.websphere.objectgrid.plugins.OptimisticCollisionException pour informer la session que la méthode batchUpdate a échoué en raison de la tentative de plusieurs transactions simultanées de mettre à jour la même ligne dans la table de base de données. Cette exception provoque l'annulation de la session et l'application doit relancer la transaction entière. La justification réside dans le fait que la nouvelle tentative aboutira, ce qui explique pourquoi cette approche est appelée optimiste. L'approche optimiste est plus performante si les données sont peu modifiées ou si les transactions simultanées tentent rarement de mettre à jour la même ligne.

Le chargeur doit impérativement utiliser le paramètre de clé du constructeur OptimisticCollisionException pour identifier quelle clé ou quel ensemble de clés a provoqué l'échec de la méthode optimiste batchUpdate. Le paramètre de clé peut être l'objet clé proprement dit ou un tableau d'objets clé si plusieurs clés ont abouti à l'échec de la mise à jour optimiste. eXtreme Scale utilise la méthode getKey du constructeur OptimisticCollisionException pour déterminer quelles entrées de mappe contiennent les données périmées et ont provoqué l'exception. Un aspect du processus d'annulation consiste à expulser les entrées de mappe périmées de la mappe. Cette expulsion s'avère nécessaire pour que toutes les transactions suivantes accédant aux mêmes clés déclenchent la méthode get de l'interface Loader appelée pour actualiser les entrées de mappe avec les données actuelles de la base de données.

Autres modes d'implémentation d'une approche optimiste par un chargeur :
  • Il n'existe aucune colonne d'horodatage ou de numéro de séquence. Dans ce cas, la méthode getVersionObjectForValue de l'interface OptimisticCallback renvoie simplement l'objet de valeur en tant que version. Avec cette approche, le chargeur doit créer une clause WHERE comprenant tous les zones l'objet version initial. Cette approche n'est pas efficace et tous les types de colonnes ne sont pas admissibles pour une utilisation dans la clause WHERE d'une instruction SQL update sur-qualifiée. Cette approche n'est généralement pas utilisée.
  • Il n'existe aucune colonne d'horodatage ou de numéro de séquence. Cependant, contrairement à l'approche précédente, la clause WHERE contient uniquement les zones de valeur modifiées par la transaction. L'une des méthodes permettant de détecter les zones qui ont été modifiées consiste à donner au mode de copie de la mappe de sauvegarde la valeur CopyMode.COPY_ON_WRITE. Ce mode de copie exige la transmission d'une interface de valeur à la méthode setCopyMode dans l'interface de la mappe de sauvegarde. La mappe de sauvegarde crée des objets proxy dynamiques qui implémentent l'interface de valeur fournie. Avec ce mode de copie, le chargeur peut transtyper chaque valeur en objet com.ibm.websphere.objectgrid.plugins.ValueProxyInfo. L'interface ValueProxyInfo comporte une méthode qui permet au chargeur d'obtenir la liste des noms d'attributs modifiés par la transaction. Cette méthode permet au chargeur d'appeler les méthodes get dans l'interface de valeur pour les noms d'attributs afin de consulter les données modifiées et créer une instruction SQL update qui définit uniquement les attributs modifiés. La clause where peut désormais être créée en incluent la colonne de clé primaire plus chacune des colonnes d'attributs modifiés. Cette approche est plus efficace que la précédente mais elle exige l'écriture de plus de code dans le chargeur et un cache de l'instruction préparée potentiellement plus important pour le traitement des différentes permutations. Toutefois, si les transactions modifient généralement que quelques attributs, cette limitation peut ne pas être problématique.
  • Certaines bases de données relationnelle peuvent comporter une API pour la maintenance automatique des données de colonne particulièrement utiles pour la gestion optimiste des versions. Pour plus d'informations sur l'existence de ces possibilités, consultez la documentation de votre base de données.