Configuración de cargadores de base de datos

Los cargadores son plug-ins de correlaciones de respaldo que se invocan cuando se realizan cambios en la correlación de respaldo o ésta no puede satisfacer una solicitud de datos (una falta de memoria caché).

Consideraciones de precarga

Los cargadores son plug-ins de correlaciones de respaldo que se invocan cuando se realizan cambios en la correlación de respaldo o ésta no puede satisfacer una solicitud de datos (una falta de memoria caché). Para obtener una visión general de cómo interactúa eXtreme Scale con un cargador, consulte Memoria caché en línea.

Cada correlación de respaldo tiene un atributo preloadMode booleano establecido para indicar si una precarga de una correlación se ejecuta asíncronamente. De manera predeterminada, el atributo preloadMode está establecido en false, que indica que la inicialización de la correlación de respaldo no se completa hasta que la precarga de la correlación haya terminado. Por ejemplo, la inicialización de la correlación de respaldo no se completa hasta que se devuelve el método preloadMap. Si el método preloadMap lee una gran cantidad de datos de su programa de fondo y los carga en la correlación, puede que tarde en completarse. En ese caso, puede configurar una correlación de respaldo de modo que use una precarga asíncrona de la correlación; para ello, establezca el atributo preloadMode en true. Este valor hace que el código de inicialización de la correlación de respaldo inicie una hebra que invoca el método preloadMap, lo que permite que se complete la inicialización de una correlación de respaldo mientras la precarga de la correlación aún está en curso.

En un caso de ejemplo de eXtreme Scale distribuido, uno de los patrones de precarga es la precarga de cliente. En el patrón de precarga de cliente, un cliente de eXtreme Scale es responsable de recuperar datos del programa de fondo y a continuación de insertar los datos en el servidor de contenedor distribuido utilizando agentes de DataGrid. Además, la precarga de cliente se puede ejecutar en el método Loader.preloadMap en una y sólo una partición específica. En este caso, es muy importante cargar asincrónicamente los datos en la cuadrícula. Si la precarga del cliente se ejecutase en la misma hebra, la correlación de respaldo nunca se inicializaría, de modo que la partición en la que reside nunca estaría ONLINE. Por lo tanto, el cliente de eXtreme Scale no podría enviar la solicitud a la partición y esto acabaría causando una excepción.

Si se utiliza un cliente de eXtreme Scale en el método preloadMap, debe establecer el atributo preloadMode en true. La alternativa debe consiste en iniciar una hebra en el código de precarga del cliente.

El fragmento de código siguiente ilustra cómo se establece el atributo preloadMode para habilitar la precarga asíncrona:

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

El atributo preloadMode también puede establecerse mediante un archivo XML, como se muestra en el ejemplo siguiente:

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

TxID y uso de la interfaz TransactionCallback

El método get y los métodos batchUpdate de la interfaz Loader se pasan a un objeto TxID que representa la transacción Session que requiere que se realice la operación get o batchUpdate. Se puede llamar a los métodos get y batchUpdate más de una vez por transacción. Por lo tanto, los objetos con ámbito de transacción que el cargador necesita se conservan normalmente en una ranura del objeto TxID. Se utiliza un cargador JDBC (Java database connectivity) para ilustrar cómo utiliza un cargador las interfaces TxID y TransactionCallback.

Se pueden almacenar varias correlaciones ObjectGrid en la misma base de datos. Cada correlación tiene su propio cargador, y cada cargador podría conectarse a la misma base de datos. Cuando los cargadores se conectan a la base de datos, deben utilizar la misma conexión JDBC. La utilización de la misma conexión confirma los cambios en cada tabla como parte de la misma transacción de base de datos. Normalmente, la misma persona que escribe la implementación Loader también escribe la implementación TransactionCallback. El mejor método es cuando la amplía la interfaz TransactionCallback para añadir métodos que el cargador necesita para obtener una conexión de base de datos y para almacenar en memoria caché sentencias preparadas. Comprenderá porqué se recomienda este procedimiento en cuanto vea cómo utiliza el cargador las interfaces TransactionCallback y TxID.

En el ejemplo siguiente verá cómo el cargador necesita que la interfaz TransactionCallback se amplíe:

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 );
}

Utilizando estos nuevos métodos,Loader y los métodos batchUpdate pueden obtener una conexión de la forma siguiente:

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;
}

En el ejemplo anterior y en los ejemplos siguientes, ivTcb y ivOcb son variables de instancia del cargador que se inicializaron como se describió en el apartado sobre las consideraciones de precarga. La variable ivTcb es una referencia a la instancia de MyTransactionCallback e ivOcb es una referencia a la instancia de MyOptimisticCallback. La variable databaseName es una variable de instancia del cargador que se estableció como propiedad de cargador durante la inicialización de la correlación de respaldo. El argumento isolationLevel es una de las constantes JDBC Connection definidas para los diversos niveles de aislamiento que JDBC admite. Si el cargador utiliza una implementación optimista, el método get suele utilizar una conexión JDBC de confirmación automática para captar los datos de la base de datos. En ese caso, el cargador podría tener un método getAutoCommitConnection que se implementase de la siguiente manera:

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;
}

Recuerde que el método batchUpdate tiene la siguiente sentencia switch:

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;
}

Cada uno de los métodos buildBatchSQL utiliza la interfaz MyTransactionCallback para obtener una sentencia preparada. A continuación se muestra un fragmento de código que ilustra cómo el método buildBatchSQLUpdate crea una sentencia update de SQL para actualizar una entrada EmployeeRecord y añadirla a la actualización de proceso por lotes:

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();
}

Una vez que el bucle batchUpdate ha credo todas las sentencias preparadas, llama al método getPreparedStatementCollection. Este método se implementa de la siguiente manera:

private Collection getPreparedStatementCollection( TxID tx, Connection conn )
{
    return ( ivTcb.getPreparedStatementCollection( tx, conn, "employee" ) );
}
Cuando la aplicación invoca el método commit en Session, el código de Session llama al método commit en el método TransactionCallback después de haber enviado al cargador todos los cambios realizados por la transacción para cada correlación que la transacción modificó. Debido a que todos los cargadores utilizaron el método MyTransactionCallback para obtener las conexiones y las sentencias preparadas que necesitan, el método TransactionCallback sabe qué conexión utilizar para solicitar que el programa de fondo confirme los cambios. Por lo tanto, ampliar la interfaz TransactionCallback con los métodos que necesite cada uno de los cargadores tiene las ventajas siguientes:
  • El objeto TransactionCallback encapsula el uso de ranuras de TxID para los datos con ámbito de transacción, y el cargador no requiere información sobre las ranuras de TxID. El cargador sólo necesita saber qué métodos se van a añadir a TransactionCallback mediante la interfaz MyTransactionCallback para las funciones que necesite el cargador.
  • El objeto TransactionCallback puede garantizar que la conexión se comparta entre cada cargador que se conecte el mismo programa de fondo, de modo que puede evitarse un protocolo de confirmación de dos fases.
  • Con el objeto TransactionCallback, la conexión al programa de fondo se completa a través de una confirmación o retrotracción que se invoca en la conexión cuando se necesita.
  • TransactionCallback garantiza que se produzca la limpieza de los recursos de base de datos cuando se completa una transacción.
  • TransactionCallback oculta si está obteniendo una conexión gestionada de un entorno gestionado como, por ejemplo, WebSphere Application Server u otro servidor de aplicaciones compatible con Java 2 Platform, Enterprise Edition (J2EE). Esta ventaja permite que se utilice el mismo código de cargador en entornos gestionados y no gestionados. Sólo debe cambiarse el plug-in TransactionCallback.
  • Para obtener más información sobre cómo la implementación TransactionCallback utiliza las ranuras de TxID para los datos con ámbito de transacción, consulte Plug-in TransactionCallback

OptimisticCallback

Como se ha mencionado anteriormente, el cargador puede utilizar un acercamiento optimista para conseguir un control de simultaneidad. En ese caso, el ejemplo del método buildBatchSQLUpdate debe modificarse ligeramente para implementar un acercamiento optimista. Existen diversas formas de utilizar un acercamiento optimista. Puede tener un columna de indicación de hora o una columna de contador de número de secuencia para añadir una versión a cada actualización de la fila. Presuponga que la tabla de empleados tiene una columna de número de secuencia que aumenta cada vez que se actualiza la fila. A continuación, deberá modificar la firma del método buildBatchSQLUpdate de modo que se pase al objeto LogElement en lugar del par clave/valor. También deberá utilizar el objeto OptimisticCallback conectado a la correlación de respaldo para obtener el objeto de versión inicial y para actualizar el objeto de versión. A continuación se muestra un ejemplo de un método buildBatchSQLUpdate modificado que utiliza la variable de instancia ivOcb que se inicializó como se describió en el apartado sobre preloadMap:

Ejemplo de código de método de actualización por lotes modificado
private void  buildBatchSQLUpdate( TxID tx, LogElement le, Connection conn )
	throws SQLException, LoaderException
{
    // Obtener el objeto de versión inicial cuando esta entrada de correlación se leyó
    // o actualizó por última vez en la base de datos.
    Employee emp = (Employee) le.getCurrentValue();
    long initialVersion = ((Long) le.getVersionedValue()).longValue();
    // Obtener el objeto de versión del objeto Employee actualizado para la
    //operación update de SQL.
    Long currentVersion = (Long)ivOcb.getVersionedObjectForValue( emp );
    long nextVersion = currentVersion.longValue();
    // A continuación cree una operación update de SQL que incluya el objeto de
versión en la cláusula where
    // para la comprobación optimista.
    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();
}

El ejemplo muestra que se utiliza el objeto LogElement para obtener el valor de versión inicial. Cuando la transacción accede por primera vez a la entrada de correlación, se crea un objeto LogElement con el objeto Employee inicial que se obtiene de la correlación. Este objeto Employee inicial se pasa también al método getVersionedObjectForValue en la interfaz OptimisticCallback y el resultado se guarda en el LogElement. Este proceso se produce antes de que se dé a la aplicación una referencia al objeto Employee inicial, por lo que es posible llamar a algún método que cambie el estado del objeto Employee inicial.

El ejemplo muestra que el cargador utiliza el método getVersiondObjectForValue para obtener el objeto de versión para el objeto Employee actual y actualizado. Antes de llamar al método batchUpdate en la interfaz Loader, eXtreme Scale llama al método updateVersionedObjectForValue en la interfaz OptimisticCallback para provocar que se genere un objeto de una nueva versión para el objecto Employee actualizado. Una vez que el método batchUpdate vuelve a ObjectGrid, se actualiza el objeto LogElement con el objeto de versión actual y pasa a ser el nuevo objeto de versión inicial. Este paso es necesario porque la aplicación podría haber llamado al método flush en la correlación en lugar del método commit en Session. Es posible que una única transacción llame al cargador varias veces para la misma clave. Por dicho motivo, eXtreme Scale se asegura de que se actualice el objeto LogElement con el objeto de la nueva versión, cada vez que se actualiza la fila en la tabla de empleados.

Ahora que el cargador tiene el objeto de versión inicial y el objeto de versión siguiente, puede ejecutar una sentencia update de SQL que establezca la columna SEQNO en el valor del objeto de versión siguiente y utilice el valor del objeto de versión inicial en la cláusula where. Este procedimiento suele denominarse sentencia de actualización sobrecualificada. El uso de la sentencia de actualización sobrecualificada permite que la base de datos relacional verifique que ninguna otra transacción modificó la fila entre el tiempo en que esta transacción lee los datos de la base de datos y el tiempo en que actualiza la base de datos. Si otra transacción ha modificado la fila, la matriz de números devuelta por la actualización de proceso por lotes indica que ninguna fila se actualizó para esta clave. El cargador debe verificar que la operación update de SQL ha actualizado verdaderamente la fila. Si no se ha actualizado, el cargador muestra una excepción com.ibm.websphere.objectgrid.plugins.OptimisticCollisionException para informar a Session de que se ha producido una anomalía en el método batchUpdate debido a la existencia de más de una transacción simultánea intentando actualizar la misma fila de la tabla de la base de datos. Al recibir esta excepción, Session se retrotrae y la aplicación debe volver a repetir la transacción entera. La explicación es que esta repetición será correcta, de ahí que este procedimiento se llame optimista. El procedimiento optimista funciona mejor si los datos no se modifican con frecuencia o si transacciones simultáneas rara vez intentan actualizar la misma fila.

Es importante que el cargador utilice el parámetro clave del constructor OptimisticCollisionException para identificar qué clave o conjunto de claves provocó la anomalía en el método batchUpdate optimista. El parámetro clave puede ser el propio objeto clave o una matriz de objetos clave si se obtuvo más de una clave en la anomalía de actualización optimista. eXtreme Scale utiliza el método getKey del constructor OptimisticCollisionException para determinar qué entradas de correlación contienen datos obsoletos y han provocado la excepción. Parte del proceso de retrotracción consiste en desalojar de la correlación cada entrada de correlación obsoleta. El desalojo de las entradas obsoletas es necesario para que las transacciones subsiguientes que accedan a la misma clave o claves llamen al método get de la interfaz Loader para renovar las entradas de correlación con los datos actuales de la base de datos.

Otras maneras que tiene un cargador de implementar un procedimiento optimista son:
  • No existe columna de indicación de hora ni columna de número de secuencia. En ese caso, el método getVersionObjectForValue de la interfaz OptimisticCallback devuelve simplemente el objeto de valor como versión. Con este procedimiento, el cargador necesita crear una cláusula where que incluya cada uno de los campos del objeto de versión inicial. Este procedimiento no es eficaz, y no todos los tipos de columna se pueden utilizar en la cláusula where de una sentencia update de SQL sobrecualificada. No se suele utilizar este procedimiento.
  • No existe columna de indicación de hora ni columna de número de secuencia. No obstante, a diferencia del procedimiento anterior, la cláusula where sólo contiene los campos de valor que ha modificado la transacción. Otro método para detectar qué campos se han modificado consiste en establecer la modalidad de copia en la correlación de respaldo como modalidad CopyMode.COPY_ON_WRITE. Esta modalidad de copia requiere que una interfaz de valor se pase al método setCopyMode en la interfaz BackingMap. BackingMap crea objetos de proxy dinámicos que implementan la interfaz de valor proporcionada. Con esta modalidad de copia, el cargador puede difundir cada valor a un objeto com.ibm.websphere.objectgrid.plugins.ValueProxyInfo. La interfaz ValueProxyInfo tiene un método que permite al cargador obtener la lista de los nombres de atributos modificados por la transacción. Este método permite que el cargador llame a los métodos get en la interfaz de valor de los nombres de atributos para obtener los datos modificados y para crear una sentencia update de SQL que sólo establezca los atributos modificados. A continuación, puede crearse la cláusula where de modo que tenga la columna de claves primarias más cada una de las columnas de atributos modificados. Este procedimiento es mucho más eficaz que el anterior, pero requiere escribir más código en el cargador y puede que la memoria caché de las sentencias preparadas necesite ser de gran tamaños para poder manejar las diferentes permutaciones. Sin embargo, si las transacciones sólo modifican unos pocos atributos, esta limitación no supondría un problema.
  • Puede que algunas bases de datos relacionales tengan una API que sirva de ayuda en el mantenimiento automático de los datos de columnas que resulten útiles en la creación optimista de versiones. Consulte la documentación de la base de datos para determinar si existe esta posibilidad.