Using Distributed Transactions on Couchbase in Java Application
Basically, a transaction is defined as a group of operations that should be committed to the database or they are undone from the database together. Couchbase supports transactions and ACID properties for protecting the database from undesired actions. In this article, we will implement distributed transactions on the Couchbase by using Java SDK.
Transactions on Couchbase
Since the Couchbase is a distributed system, the ACID database transactions are an important concern that should be handled as distributed. Couchbase supports distributed multi-document ACID database transactions which means transactions are distributed and working across multiple documents on multiple nodes.
Here, how they handle ACID properties in the Couchbase mentioned. It guarantees that;
- either all the changes on the documents are committed or the database is rolled back to the first state (Atomicity)
- the database moves from one consistent state to another (Consistency)
- all the changes made in the transaction scope will not be affected by another separated transaction (Isolation)
- all the changes made in a successful transaction will be kept permanently (Durability)
Open the bucket and create an index on the Couchbase
Before implementing Java SDK, we create a sample bucket and make all configurations from the Java application. Here, we create a Company bucket and create an index on the document id with the following command;
CREATE INDEX `idx_company_id` ON `Company`(META().`id`);
Connect to the bucket from the Java application
Before starting this step, the following article might be helped to understand the classes, which will be explained in this section, below and how to configure them better.
Add dependency and define configuration information
We continue with connecting to the bucket so we need to add the following library to the pom.xml
file.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-couchbase</artifactId
<version>2.5.0</version>
</dependency>
We define the Couchbase connection configurations that we defined as creating the bucket step, so we add the following lines into the application.yaml
file.
couchbase:
connection-string: localhost
user-name: admin
password: 123456
bucket-company:
name: Company
user-name: Company
password: 123456
Create Document Model class
We open the model package at the root directory and create the Company class as the Couchbase document model. As it seems, the Company modal comprises of;
- as content, the companyName, location, and size properties
- the id property will be set as
companyName#location
format
Create Configuration classes
We open the configuration package at the root directory and create the following classes to take the configuration parameters above and create the bucket connection.
CouchbaseBucketProperties: This class encounters the bucket properties (i.e. name, user-name, and password) under the bucket-company parameter in the application.yaml
file above.
CouchbaseClusterProperties: This class encounters the cluster properties (i.e. connection-string, user-name, password, and bucket-company) under the couchbase parameter in the application.yaml
file above. Also, the couchbase parameter is declared as the prefix of the ConfigurationProperties
annotation to get properties properly.
CouchbaseBucketConfiguration: This class is used to open the Couchbase connection with the configuration defined. We should be extended from the AbstractCouchbaseConfiguration
interface so there are 4 methods that should be overridden.
Besides these methods, we define a bean called transactions
that will be used during calling the transaction scope in our service class. As you see, it takes Cluster object, and durability level and failure level configurations.
Create Repository interface
We open the repository package at the root directory and create the CompanyRepository interface extended from CrudRepository. In CrudRepository, we define id as string format and content as Company format defined at previous step.
public interface CompanyRepository extends CrudRepository<Company, String> { }
Everything we need for connecting to Couchbase was done, and we are ready to implement to our application from Java SDK.
Implementing Transactions from the Java SDK
The Java SDK provides to use Couchbase’s distributed ACID transactions. After making sure we provide the following requirements, we look practically at how to initialize transactions, how to configure them, and open/close a transaction. The requirements are;
- Couchbase Server 6.6.1 or above
- Couchbase Java Client 3.1.5 or above
Firstly, we add the couchbase-transactions
library via the following command. It will automatically pull the other dependencies of this library such as Couchbase Java SDK. Therefore, there is no need to add this library into the pom.xml
file.
<dependency>
<groupId>com.couchbase.client</groupId>
<artifactId>couchbase-transactions</artifactId
<version>1.2.1</version>
</dependency>
NoSuchMethodError: …setAttribute error
This error is caused by the version of thecore-io:2.1.6
library that is automatically pulled by the couchbase-transactions:1.2.1
library. If you look at the Maven bar, the following dependency tree is shown.
But the setAttribute method is removed at thecore-io:2.1.6
library, so we need to exclude thejava-client
library from the couchbase-transactions
and install thejava-client:3.1.5
library separately via formatting the pom.xml
file as follows;
<dependency>
<groupId>com.couchbase.client</groupId>
<artifactId>couchbase-transactions</artifactId
<version>${couchbase.transactions.version}</version>
<exclusions>
<exclusion>
<groupId>com.couchbase.client</groupId>
<artifactId>java-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.couchbase.client</groupId>
<artifactId>java-client</artifactId>
<version>3.1.5</version>
</dependency>
Now, the dependency tree is created as follows;
Create Service classes (both with and without transaction)
We open the service package at the root directory and create the CouchbaseTxService class. Here, we implement a very basic service to check whether the items remaining will be saved after an exception occurs or not. There are several important things to understand transaction scoped;
- We should inject the
transactions
dependency, which is defined into the CouchbaseBucketConfiguration as bean at the configuration step. It will be used in the try/catch block to handle the CommitAmbigious and Failed situations. - The transaction logic should be defined in transaction context (in run method). The
ctx
is an AttemptContext which manage the whole transaction process (i.e. permits getting, inserting, removing the documents, performing N1QL queries, and rolling back the transaction) - At the end of the context, all operations will be committed anyway if no exception is thrown, so it is optional to add the commit method at the end of the transaction scope.
In the CouchbaseTxService file, only the insertTx
method is implemented. In this method, we would like to insert 5 Company documents, which are generated from the CompanyCreateRequest
parameter, and we will throw an exception if the isFailed
parameter is true after inserting the third document. We pretend all documents would be rolled back with throwing the exception.
On the other hand, we create the CouchbaseService class that is working very similarly to the CouchbaseTxService class above. The only difference between them on of them is not covered with the transaction scoped. Therefore, we can evaluate the effects of the transaction feature clearly in the section below.
Create Controller class
We open the controller package at the root directory and create the CouchbaseController class. Firstly, both service classes are injected here, and then 3 endpoints are implemented. Each of these endpoints is dedicated to calling one of the following methods from a service class;
- the
failed
method doesn’t use transactions and it will throw an exception - the
failedTx
method uses transaction and it will throw an exception - the
succeedTx
method uses transaction and it will not throw an exception
Check the Results
Now, we are ready for interacting with the Couchbase so we run the project, open the Swagger on the http://localhost:9292/swagger-ui.html#/
, and check these 3 endpoints below.
We execute all of them with the following body content;
{ “companyName”: “TYG”, “location”: “İstanbul”, “size”: 1800 }
The Failed method
In the failed
method, the following exception is thrown in the application after saving second documents to the bucket.
Since this method was implemented with the transaction feature, the first 3 documents, which are inserted until the exception, have been saved into the bucket and the 2 documents remaining have not been saved.
The FailedTx method
In the failedTx
method, the following exception is thrown in the application after saving second documents to the bucket.
Since this method was implemented with the transaction feature, the first 3 documents, which are inserted until the exception, have been rollbacked from the bucket and there is no document in the bucket.
The SucceedTx method
In the succeedTx
method, there is no exception to be thrown and all 5 documents are saved into the bucket successfully.
The java project, which contains all code we implemented at the steps above, is available here.