Apoyo a las transacciones se ha añadido recientemente a los SDK de Couchbase. A partir de Spring Data Couchbase 5.0.0-M5se ha añadido soporte para transacciones a Spring Data Couchbase. En este blog, describiremos el uso de Spring Data @Transactional para aprovechar Couchbase Transactions.
En spring-data-couchbase contiene un spring-data-testapp que ejerce las transacciones. Se basa en la viaje-muestra en un servidor Couchbase local. (cualquier bucket con un índice primario será suficiente).
|
1 2 |
git clone git@github.com:spring-projects/spring-data-examples.git cd spring-data-examples/couchbase/transactions |
Se han añadido a AbstractCouchbaseConfiguration. Para permitir que la clase de interceptor de transacciones incorporada sea sobrescrita con CouchbaseTransactionInterceptor, asegúrese de que la siguiente línea está en el archivo aplicación.propiedades file:
aplicación.propiedades
|
1 |
spring.main.allow-bean-definition-overriding=true |
La configuración de la transacción puede modificarse anulando la directiva configurarEntorno() en su clase que extiende AbstractCouchbaseConfiguration. Aquí fijamos el durabilityLevel a NONE para que podamos trabajar en nuestro clúster de un solo nodo.
Config.java
|
1 2 3 4 5 6 7 8 9 |
@Configuration @EnableCouchbaseRepositories({"com.example.demo"}) @EnableTransactionManagement public class Config extends AbstractCouchbaseConfiguration { … @Override public void configureEnvironment(ClusterEnvironment.Builder builder){ builder.transactionsConfig(TransactionsConfig.durabilityLevel(DurabilityLevel.NONE)); } |
En la clase que los utilizará, tenemos que definir la plantilla como de costumbre y una referencia al objeto de servicio. Es importante tener en cuenta que el @Autowired rellena una variable con un objeto proxy. Este es un detalle importante, ya que los objetos proxy realizan el procesamiento antes y después de llamar al método real. Si se crea directamente el objeto de servicio desde su constructor, la anotación @Transactional no tendrá ningún efecto.
CmdRunner.java
|
1 2 |
@Autowired CouchbaseTemplate template; @Autowired AirportGatesService airportGatesService; |
El servicio se define con un @Servicio anotación. El constructor toma argumentos que son @Bean de forma que la infraestructura pueda crear un objeto proxy para @Autowired.
AirlineGateService.java
|
1 2 3 4 5 6 |
@Service public class AirlineGatesService { CouchbaseTemplate template; public AirlineGatesService(CouchbaseTemplate template) { this.template = template; } |
AirlineGateService.java (continuación)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// The @Transactional annotation results in the method of the proxy for the service executing this in a transaction @Transactional public void transferGates(String fromId, String toId, int gatesToTransfer, RuntimeException exceptionToThrow) { // Not necessary, but may wish to include this check to confirm this is actually in a transaction. TransactionalSupport.checkForTransactionInThreadLocalStorage().map((h) -> { if (!h.isPresent()) throw new RuntimeException("not in transaction!"); return h; }); AirlineGates fromAirlineGates = template.findById(AirlineGates.class).one(fromId); AirlineGates toAirlineGates = template.findById(AirlineGates.class).one(toId); toAirlineGates.gates += gatesToTransfer; fromAirlineGates.gates -= gatesToTransfer; template.save(fromAirlineGates); // maybe simulate an error occurring after the fromAirlineGates doc has been saved if(exceptionToThrow != null){ throw exceptionToThrow; } template.save(toAirlineGates); } |
Ahora usa todo lo que hemos construido:
- salva dos AirlineGates documentos, cada uno de los cuales indica 200 puertas
- verificar que se han salvado.
- ejecutar la transferGates() método del servicio para transferir 50 puertas de embarque de una compañía aérea a otra en una transacción.
- verificar que la transferencia se ha realizado.
- intento de ejecución transferGates() de nuevo, esta vez con una excepción que se produce después de guardar el primer documento.
CmdRunner.java
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
AirlineGates airlineGates1 = new AirlineGates("1", "JFK", "American Airlines", Long.valueOf(200)); //1 AirlineGates airlineGates2 = new AirlineGates("2", "JFK", "Lufthansa", Long.valueOf(200)); AirlineGates saved1 = airlineGatesService.save(airlineGates1); AirlineGates saved2 = airlineGatesService.save(airlineGates2); AirlineGates found1 = airlineGatesService.findById(saved1.getId()); //2 AirlineGates found2 = airlineGatesService.findById(saved2.getId()); System.err.println("found before transferGates: " + found1); System.err.println("found before transferGates: " + found2); // move 50 gates from airline1 to airline2 int gatesToTransfer=50; airlineGatesService.transferGates(airlineGates1.getId(), airlineGates2.getId(), gatesToTransfer, null); //3 found1 = airlineGatesService.findById(saved1.getId()); found2 = airlineGatesService.findById(saved2.getId()); System.err.println("found after transferGates: " + found1); //4 System.err.println("found after transferGates: " + found2); Assert.isTrue(found1.getGates().equals(airlineGates1.getGates()-gatesToTransfer), "should have transferred"); Assert.isTrue(found2.getGates().equals(airlineGates1.getGates()+gatesToTransfer), "should have transferred"); // attempt to move 44 gates from airline1 to airline2, but it fails. try { // 5 airlineGatesService.transferGates(airlineGates1.getId(), airlineGates2.getId(), 44, new SimulateErrorException()); } catch (RuntimeException rte) { if (!(rte instanceof TransactionSystemUnambiguousException) && rte != null && rte.getCause() instanceof SimulateErrorException) { throw rte; } } System.err.println("found after transferGates: " + airlineGatesService.findById(airlineGates1.getId())); System.err.println("found after transferGates: " + airlineGatesService.findById(airlineGates2.getId())); Assert.isTrue(found1.getGates().equals(airlineGates1.getGates()-gatesToTransfer), "should be same as previous"); Assert.isTrue(found2.getGates().equals(airlineGates1.getGates()+gatesToTransfer), "should be same as previous"); |
Cómo funciona:
- En TransactionInterceptor @Bean in AbstractCouchbaseConfiguration anula la función TransactionInterceptor para métodos anotados @Transactional.
- La anotación @Transactional hace que el proxy de AirportGatesService para utilizar el TransactionInterceptor para llamar al método anotado utilizando el método CouchbaseCallbackTransactionManager.
- En CouchbaseCallbackTransactionManager inicializa una transacción y luego llama al método de servicio real en el contexto de la transacción.
- Si la llamada al método tiene éxito, el CouchbaseCallbackTransactionManager confirma la transacción.
- Si la consignación se realiza correctamente, se devuelve la llamada.
- Si la confirmación falla con una excepción reintentable, la función CouchbaseCallbackTransactionManger reintentará el proceso completo de inicializar una nueva transacción, ejecutar el método de servicio real y luego confirmar la transacción.
En CouchbaseCallbackTransactionManager repetirá esta operación hasta que la confirmación tenga éxito o se haya alcanzado el número máximo de reintentos.
Opciones para los próximos pasos
- Las transacciones de Couchbase pueden aprovecharse en Spring Data Couchbase utilizando el Operador transaccional - ver el Documentación de Spring Data.
- Las transacciones pueden también puede ser utilizado directamente por el SDK Java de Couchbase. Documentación sobre transacciones Java ACID.
Hola amigos - el proyecto couchbase/transactions aún no se ha fusionado con spring-data-examples. Estará disponible en breve.
- Mike
El proyecto couchbase/transactions ya está disponible en la rama boot-3
https://github.com/spring-projects/spring-data-examples/tree/boot-3/couchbase/transactions
El ejemplo de transacción necesita spring-boot-starter-data-couchbase 3.0.0-RC2 para ejecutarse debido a una incompatibilidad con reactor en otras versiones 3.0.0*.
Hola Michael, Gracias por implementar el soporte de transacciones couchbase incorporado. Funciona muy bien cuando uso "couchbase transacción" utilizando el cubo único, pero tengo un error en las transacciones utilizando con múltiples cubos.
He implementado este enlace https://github.com/spring-projects/spring-data-couchbase/issues/878. Esta implementación no funciona con las transacciones. ¿Tiene alguna idea?
Spring Data Couchbase Transactions no funcionará con multi-buckets ya que los multi-buckets en Spring Data Couchbase requieren múltiples conexiones a Couchbase y la transacción debe estar en una única conexión.
- Mike