Distributed Locks in Spring-boot Microservice Environment

22 / Feb / 2023 by anil.gola 0 comments

Suppose we are in an environment where only one instance is running in production. We want to do an account update and synchronise the transaction where we are doing the update. This can easily be achieved with the help of Reentrant Locks api’s of java, as shown below.

@Service
@RequiredArgsConstructor
@Slf4j
public class AccountService {
private final AccountRepository accountRepository;
private ReentrantLock lock = new ReentrantLock();
public void updateAccount(Long id) throws InterruptedException {
boolean lockAquired = lock.tryLock();
if(lockAquired){
try{
log.info("lock taken");
Account account = accountRepository.findById(id).get();
account.setBalance(account.getBalance() + 100L);
Thread.sleep(20_000);
accountRepository.save(account);
}
finally {
lock.unlock();
}
}
}
}

Now consider the scenario where multiple instances of this service are running in production, which is quite common in today’s microservice environment. The above method fails as lock is applicable for each instance and we want some kind of mechanism to take a lock on something common across all instances of this microservice. What could be better than a table in our database which is common across all instances. By taking this into cue, Spring has provided distributed locks. Let see how we can configure our distributed locks in Spring Boot.

We would require the below dependencies in our pom.xml.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

In our configuration file, we need following beans to be setup

@Bean
public DefaultLockRepository DefaultLockRepository(DataSource dataSource){
return new DefaultLockRepository(dataSource);
}

@Bean
public JdbcLockRegistry jdbcLockRegistry(LockRepository lockRepository){
return new JdbcLockRegistry(lockRepository);
}

Now, coming back to our service, we need to inject first LockRegistry

private final LockRegistry lockRegistry; // this is injected automatically if we use @RequiredArgsConstructor of lombok

private final JdbcTemplate jdbcTemplate;

public void updateAccountViaDistributedLocks(Long id) throws InterruptedException {
var lock = lockRegistry.obtain(String.valueOf(id));
boolean lockAquired = lock.tryLock();
if(lockAquired){
try{
log.info("lock taken");
Account account = accountRepository.findById(id).get();
account.setBalance(account.getBalance() + 100L);
Thread.sleep(20_000);
accountRepository.save(account);
}
finally {
lock.unlock();
}
}
}

We have first got the lock on id, and this is a distributed lock which is being taken on INT_LOCK table.

We need to create a table in sql database with the following script.

CREATE TABLE `INT_LOCK` (
`LOCK_KEY` char(36) NOT NULL,
`REGION` varchar(100) NOT NULL,
`CLIENT_ID` char(36) DEFAULT NULL,
`CREATED_DATE` timestamp NOT NULL,
PRIMARY KEY (`LOCK_KEY`,`REGION`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Now let’s create the apis for demo purposes. We have created two apis one for update account via simple locks and other with distributed locks

@PostMapping("/updateAccount/{id}")
public void updateAccount(@PathVariable("id") Long id) throws InterruptedException {
accountService.updateAccount(id);
}

@PostMapping("/updateAccountViaDistributedLocks/{id}")
public void updateAccountViaDistributedLocks(@PathVariable("id") Long id) throws InterruptedException {
accountService.updateAccountViaDistributedLocks(id);
}

Let’s see our application.yaml

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
maxIdle: 1
url: jdbc:mysql://localhost:3306/distributed_locks
testWhileIdle: true
username: root
password: root1234
jpa:
database: mysql
generate-ddl: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL55Dialect
jdbc:
time_zone: UTC
hibernate:
ddl-auto: update
show-sql: true
server:
port: 0

We have server.port as 0 so that we can have multiple instances of our service running on random ports.

Testing

Run the below curls which is using simple locks.

curl --location --request POST 'localhost:57841/monolith/updateAccount/1'
curl --location --request POST 'localhost:57896/monolith/updateAccount/1'

If we check the data in our Account table we would see that our data will be updated once only even though we have hit the apis twice. Our database is in a state we do not know(inconsistent state).

Now, hit the below two curls for which are using distributed locks

curl --location --request POST 'localhost:57841/monolith/updateAccountViaDistributedLocks/1'
curl --location --request POST 'localhost:57896/monolith/updateAccountViaDistributedLocks/1'

We would see that our account has been updated twice properly. The database will be in a consistent state.

That’s all from my side. Please find the the link of this project on Github:

https://github.com/anilgola90/distributedlock

This blog is originally published on Medium.

FOUND THIS USEFUL? SHARE IT

Leave a Reply

Your email address will not be published. Required fields are marked *