Transaction support has been recently added to the Couchbase SDKs. Starting in Spring Data Couchbase 5.0.0-M5, support for transactions has been added to Spring 데이터 카우치베이스. In this blog, we will describe using the Spring Data @Transactional annotation to leverage Couchbase Transactions.
그리고 스프링 데이터 카우치베이스 repository contains a spring-data-testapp which exercises transactions. It relies on the 여행 샘플 bucket on a local Couchbase server. (any bucket with a primary index will suffice).
1 2 |
git 복제 git@github.com:봄-프로젝트/봄-데이터-예제.git cd 봄-데이터-예제/카우치베이스/거래 |
Beans supporting transaction management have been added to 추상 카우치베이스 구성. To allow the built-in transaction interceptor class to be overridden with the CouchbaseTransactionInterceptor, ensure the following line is in the application.properties file:
application.properties
1 |
봄.메인.allow-bean-정의-overriding=true |
The transaction configuration can be modified by overriding the configureEnvironment() method in your class which extends 추상 카우치베이스 구성. Here we set the durabilityLevel 에 없음 so we can work in our single-node cluster.
Config.java
1 2 3 4 5 6 7 8 9 |
@구성 @EnableCouchbaseRepositories({"com.example.demo"}) @EnableTransactionManagement public 클래스 구성 확장 추상 카우치베이스 구성 { ... @오버라이드 public void configureEnvironment(ClusterEnvironment.빌더 빌더){ 빌더.transactionsConfig(TransactionsConfig.durabilityLevel(내구성 수준.없음)); } |
In the class which will use them, we need to define the template as usual and a reference to the service object. It’s important to note that the 오토와이어드 annotation populates a variable with a proxy object. This is an important detail as the proxy objects perform processing before and after calling the actual method. Directly creating the service object from its constructor will result in the @Transactional annotation having no effect.
CmdRunner.java
1 2 |
@자동 유선 카우치베이스 템플릿 템플릿; @자동 유선 AirportGatesService airportGatesService; |
The service is defined with an 서비스 annotation. The constructor takes arguments which are @Bean objects, such that the infrastructure can create a proxy object for 오토와이어드.
AirlineGateService.java
1 2 3 4 5 6 |
@서비스 public 클래스 AirlineGatesService { 카우치베이스 템플릿 템플릿; public AirlineGatesService(카우치베이스 템플릿 템플릿) { 이.템플릿 = 템플릿; } |
AirlineGateService.java (cont’d)
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(문자열 fromId, 문자열 toId, int gatesToTransfer, 런타임 예외 exceptionToThrow) { // Not necessary, but may wish to include this check to confirm this is actually in a transaction. TransactionalSupport.checkForTransactionInThreadLocalStorage().지도((h) -> { 만약 (!h.isPresent()) throw new 런타임 예외("not in transaction!"); 반환 h; }); AirlineGates fromAirlineGates = 템플릿.findById(AirlineGates.클래스).하나(fromId); AirlineGates toAirlineGates = 템플릿.findById(AirlineGates.클래스).하나(toId); toAirlineGates.gates += gatesToTransfer; fromAirlineGates.gates -= gatesToTransfer; 템플릿.저장(fromAirlineGates); // maybe simulate an error occurring after the fromAirlineGates doc has been saved 만약(exceptionToThrow != null){ throw exceptionToThrow; } 템플릿.저장(toAirlineGates); } |
Now use everything we have constructed:
- save two AirlineGates documents, each of which indicates 200 gates
- verify they have been saved.
- execute the transferGates() method of the service to transfer 50 gates from one airline to the other in a transaction.
- verify that the transfer has taken place.
- attempt to execute transferGates() again, this time with an exception occurring after the first document has been saved.
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.저장(airlineGates1); AirlineGates saved2 = airlineGatesService.저장(airlineGates2); AirlineGates found1 = airlineGatesService.findById(saved1.getId()); //2 AirlineGates found2 = airlineGatesService.findById(saved2.getId()); 시스템.err.println("found before transferGates: " + found1); 시스템.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()); 시스템.err.println("found after transferGates: " + found1); //4 시스템.err.println("found after transferGates: " + found2); Assert.isTrue(found1.getGates().같음(airlineGates1.getGates()-gatesToTransfer), "should have transferred"); Assert.isTrue(found2.getGates().같음(airlineGates1.getGates()+gatesToTransfer), "should have transferred"); // attempt to move 44 gates from airline1 to airline2, but it fails. 시도 { // 5 airlineGatesService.transferGates(airlineGates1.getId(), airlineGates2.getId(), 44, new SimulateErrorException()); } catch (런타임 예외 rte) { 만약 (!(rte 인스턴스 오브 TransactionSystemUnambiguousException) && rte != null && rte.getCause() 인스턴스 오브 SimulateErrorException) { throw rte; } } 시스템.err.println("found after transferGates: " + airlineGatesService.findById(airlineGates1.getId())); 시스템.err.println("found after transferGates: " + airlineGatesService.findById(airlineGates2.getId())); Assert.isTrue(found1.getGates().같음(airlineGates1.getGates()-gatesToTransfer), "should be same as previous"); Assert.isTrue(found2.getGates().같음(airlineGates1.getGates()+gatesToTransfer), "should be same as previous"); |
How it works:
- 그리고 TransactionInterceptor @Bean in 추상 카우치베이스 구성 overrides the built-in TransactionInterceptor for @Transactional annotated methods.
- The @Transactional annotation causes the proxy for AirportGatesService 를 사용하여 TransactionInterceptor to call the annotated method using the CouchbaseCallbackTransactionManager.
- 그리고 CouchbaseCallbackTransactionManager initializes a transaction and then calls the actual service method in the context of the transaction.
- If the method call is successful, the CouchbaseCallbackTransactionManager commits the transaction.
- If the commit is successful, the call returns.
- If the commit fails with a retryable exception, the CouchbaseCallbackTransactionManger will retry the complete process of initializing a new transaction, executing the actual service method and then committing the transaction.
그리고 CouchbaseCallbackTransactionManager will repeat this until either the commit succeeds, or the maximum number of retries has been reached.
Options for next steps
- Couchbase transactions can be leveraged in Spring 데이터 카우치베이스 by using the TransactionalOperator – see the Spring Data docs here.
- Transactions can also be directly used by the Couchbase Java SDK – see the Java ACID transaction docs here.
Hi Folks – the couchbase/transactions project has not yet been merged into spring-data-examples. It will be available shortly.
– Mike
The couchbase/transactions project is now available in the boot-3 branch
https://github.com/spring-projects/spring-data-examples/tree/boot-3/couchbase/transactions
The transaction sample needs spring-boot-starter-data-couchbase 3.0.0-RC2 to run due to an incompatibility with reactor in other 3.0.0* versions.
Hi Michael, Thanks for implementing built-in couchbase transaction support. It works very well when I use “couchbase transaction” using the single bucket, but I have an error in transactions using with multiple buckets.
I implemented this link https://github.com/spring-projects/spring-data-couchbase/issues/878. This implementation is not working with transactions. Do you have any idea?
Spring Data Couchbase Transactions will not work with multi-buckets as multi-buckets in Spring Data Couchbase requires multiple connections to Couchbase and the transaction must be on a single connection.
– Mike