Simple Mutual Exclusion
When we design services for high availability, we often deploy more than one instance of the same application. It might be two servers behind a load balancer or a few nodes sharing the same workload. This setup is great for redundancy and performance, but it introduces a simple question: how can we make sure that only one instance executes a specific piece of code at a time?
That is where mutual exclusion comes in. It ensures that a critical operation runs only once, even when multiple nodes are active.
Imagine a scheduled task that sends daily reports or reminder emails. You certainly do not want every node to send the same email twice. In Spring Framework, a typical implementation might look like this:
@Scheduled(cron = "59 59 8 * * *" /* Every day at 8:59:59am */)
public void sendEmails() {
List emails = emailDAO.getEmails();
emails.forEach(email -> sendEmail(email));
}
Now, the challenge is ensuring that only one node runs this method. Ideally, we could decorate it with something like this:
@Scheduled(cron = "59 59 8 * * *" /* Every day at 8:59:59am */)
@TryLock(name = "emailLock", owner = NODE_NAME, lockFor = TEN_MINUTE)
public void sendEmails() {
List emails = emailDAO.getEmails();
emails.forEach(email -> sendEmail(email));
}
To solve this, I built a small library called dlock. It provides a simple mutual exclusion mechanism using relational databases such as PostgreSQL and MySQL. You can extend it to other coordination systems like ZooKeeper, Consul, or etcd if needed.
Although distributed locking can be done in many ways, my goal is to keep it lightweight and practical. Many production systems don’t have a full cluster management solution, but almost all of them have a relational database. That made the database an ideal place to coordinate small-scale distributed locks.
This approach is not limited to scheduled jobs. You can use it anywhere you need to guarantee single execution, such as:
- Message processing
- File imports
- Cleanup or migration jobs
- Periodic billing tasks
Here’s another simple example:
@Component
class MessageProcessor{
...
@TryLock(name = "messageProcessor", owner = NODE_NAME, lockFor = TEN_MINUTE)
public void processMessage(Message message) {
...
}
}
@Component
class MessageService{
...
@PostConstruct
public void initProcessor(){
while (true) {
Message message = pollMessage();
messageProcessor.processMessage(message);
}
}
}
The principle is straightforward. Each node attempts to acquire the same lock before executing the critical section. The first one succeeds, the others wait or skip execution depending on your configuration.
Even a simple lock with an expiration time can prevent accidental duplication, inconsistent state, or race conditions in multi-node environments. In consequence, mutual exclusion remains one of the easiest and most reliable techniques to keep distributed systems consistent. In consequence, you can implement a simple lock with a timeout with dlock.