Distributed Locks in Spring-boot Microservice Environment
Suppose we are in an environment where only one instance is running in production. We want to do an account update and we want to synchronise the transaction where we are doing the update. This can easily be achieved with 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 which is 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 be requiring 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.
For that 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:
If you like this blog, please clap and follow my channel. I keep posting interesting topics from Java Ecosystem. Also, you can