Skip to content
Mustafa Erbay
Career · 10 min read · görüntülenme Türkçe oku

Distributed Locks in Side Projects: 4 Simpler Approaches

Learn how to implement distributed lock mechanisms in your side projects using simpler and more pragmatic methods.

100%

Introduction: The Need for Distributed Locks in Side Projects

When working on my small projects or side projects, I often stick to a single server or even a single process. But sometimes, things get a bit more complex. For instance, when I want to update a configuration file accessed from multiple places simultaneously, or ensure that multiple workers process a task atomically at the same time, I might need distributed lock mechanisms. Such scenarios become inevitable, especially as the project grows in size or the number of users increases.

Normally, Redis’s distributed lock libraries or more complex solutions like ZooKeeper come to mind. However, for those like me seeking practical solutions, resorting to such heavy systems isn’t always logical. Especially in situations where resources are limited or the project hasn’t yet become that complex, simpler and lighter approaches both facilitate development and reduce infrastructure costs. In this post, I will explain four relatively simpler but effective distributed lock approaches I’ve used in my side projects, and why I chose these methods.

1. Ensuring Safety with PostgreSQL’s Advisory Locks

PostgreSQL’s pg_advisory_lock and pg_try_advisory_lock functions offer a surprisingly effective solution for distributed locks. These functions create session-based locks at the database level. The locks are automatically released when the session ends, which reduces the risk of being locked indefinitely. Especially for projects already using PostgreSQL, leveraging this feature without creating an additional dependency is a significant advantage.

I once used this approach while developing an enterprise resource planning (ERP) system. In the inventory management module, it was necessary to prevent multiple operators from updating the same product simultaneously. We added a pg_try_advisory_lock call to each operator screen. If the lock was successfully acquired, the operation proceeded. If another operator tried to acquire the lock at the same time, pg_try_advisory_lock would return immediately, and the operator would see a message like “This product is currently being processed by another user.” This was very effective in preventing data inconsistency.

-- To acquire a lock (automatically released)
SELECT pg_advisory_lock(123456); -- 123456 is a unique lock ID

-- To try acquiring a lock (returns immediately if unsuccessful)
SELECT pg_try_advisory_lock(123456);

-- To release a lock (usually not necessary as it's released when the session closes)
SELECT pg_advisory_unlock(123456);

The biggest advantage of this method is that it leverages PostgreSQL’s inherent reliability. Also, we can easily generate lock IDs; for example, using a combination of a table ID and the relevant record ID. This makes managing locks easier and ensures that the correct resource is locked. However, this solution won’t work in an environment without PostgreSQL.

When using this method, it’s crucial that lock IDs are unique and meaningful. If IDs are chosen randomly, you might accidentally try to acquire the same lock for different resources. Typically, unique IDs can be generated using a formula like table_id * 1000000 + record_id. This preserves the uniqueness of the table and record, ensuring the locks are also unique.

2. File Locking Mechanisms: A Simple Yet Effective Alternative

On Linux-based systems, file locking mechanisms provide a very basic and effective method for distributed locks. You can create a lock on a file using the flock command or programmatically via the fcntl system call. These locks are valid as long as the file is open and are automatically released when the file is closed. This is great for protecting specific resources, especially configuration files.

Once, while developing the backend for my own blog, I was using a queue system to process files uploaded by users. Multiple worker processes were running simultaneously, and each worker needed to pick up and process a task from the queue. To ensure that a task was picked up by only one worker, each worker would try to acquire a lock on a fixed “lock.file” using flock before starting its work. If the lock was successfully acquired, it would pick up the task from the queue and proceed with processing. If the lock could not be acquired, it would understand that another worker had taken the task and would move on to another task.

# Using flock in a script
exec 9>/path/to/your/lock.file
if flock -n 9; then
  echo "Lock acquired, proceeding with operation..."
  # Do your actual work here
  sleep 5
  echo "Operation completed."
  # Lock will be released automatically (as the file opened with exec will close)
else
  echo "Lock could not be acquired, another process is running."
fi

The biggest advantage of this method is that it doesn’t require any additional services or databases. Only the file system is sufficient. This is a great option for minimalist projects. However, managing file locks requires a bit more care. If a worker crashes unexpectedly and leaves the file open, the lock might persist indefinitely. To prevent such situations, using the -w (timeout) option of the flock command or programmatically managing locks with fcntl can be beneficial.

Furthermore, the scalability of this method is limited. If your project is to be distributed across multiple different servers, this simple file locking method will not be sufficient. However, it is still a valid and simple solution for providing synchronization between multiple processes on a single server.

3. Simple Lock Mechanism with Redis SETNX (SET if Not Exists)

Redis, as an in-memory data structure store, is a popular tool for creating lock mechanisms in distributed systems. The SETNX command checks if a key exists, and if not, sets the key to your specified value. The atomic nature of this command makes it ideal for simple distributed locks. To acquire a lock, you use a unique key name and attempt to set a value (typically a process ID or timestamp) with SETNX. If the command returns 1, you have successfully acquired the lock.

While developing the backend for a mobile application, I used this method to prevent users from winning a unique gift under the same campaign simultaneously. A unique Redis key was created for each campaign (e.g., campaign:123:lock). When a user requested a gift, the backend first executed the SETNX campaign:123:lock user_id_timestamp command. If SETNX was successful, the user was given the gift, and the lock was released. If it failed, it was understood that another user had taken the gift, and the user was shown a message like “All gifts have been claimed.”

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

lock_key = "my_resource_lock"
lock_value = "process_id_12345" # Must be a unique value
timeout_seconds = 30 # Time for the lock to automatically expire

# Try to acquire the lock
if r.set(lock_key, lock_value, nx=True, ex=timeout_seconds):
    print("Lock acquired, proceeding with operation...")
    try:
        # Do your actual work here
        pass
    finally:
        # Release the lock
        # Caution: We should only release our own lock
        if r.get(lock_key) == lock_value.encode():
            r.delete(lock_key)
        print("Lock released.")
else:
    print("Lock could not be acquired, another process is running.")

The biggest advantage of this approach is Redis’s speed and the reliability of the SETNX command, which provides a robust locking mechanism. By using the EX (expire) parameter to automatically release the lock after a certain period, you can largely solve the problem of persistent locks. However, the EX parameter is available in Redis 2.6.12 and later versions, so it’s important to check your Redis version.

An important point to note with this method is to ensure that when releasing the lock (the delete operation), you only release the lock you acquired. Otherwise, if another process acquired the lock before you released yours, you might accidentally delete its lock along with yours. Therefore, the correct approach is to check the lock’s value with GET and only DELETE if it matches your own value.

4. Lock Management with a Simple Database Table

One of the most basic solutions is to create a dedicated lock table in your database. This table might contain fields such as the lock name, the lock owner (e.g., process ID or server name), and a timestamp. To acquire a lock, you attempt to insert a record into this table with a unique lock name. If the insertion is successful, you have acquired the lock. To release locks, you simply delete the corresponding record from the table.

Once, while developing a very simple message queue application, I used this method. Each worker would check if a record named message_queue_lock existed in a table called locks before adding a new message to the queue. If no record existed, it would insert a new one. If a record existed, it understood that another worker was adding a message.

-- Example lock table
CREATE TABLE locks (
    lock_name VARCHAR(255) PRIMARY KEY,
    owner_id VARCHAR(255) NOT NULL,
    acquired_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- To acquire a lock (with INSERT IGNORE or ON CONFLICT)
-- PostgreSQL example
INSERT INTO locks (lock_name, owner_id)
VALUES ('message_queue_lock', 'worker_id_abc')
ON CONFLICT (lock_name) DO NOTHING;

-- If INSERT IGNORE (MySQL) or ON CONFLICT DO NOTHING (PostgreSQL)
-- affects 0 rows, the lock was not acquired. If 1 row is affected, the lock was acquired.

-- To release a lock
DELETE FROM locks WHERE lock_name = 'message_queue_lock' AND owner_id = 'worker_id_abc';

The biggest advantage of this approach is that it uses your existing database infrastructure. You don’t need to set up or manage an extra service. However, you need to be careful about automatically releasing locks. If a worker crashes and doesn’t delete the lock, it can leave a persistent record in the lock table. To prevent this, you can add an “expiration timestamp” field to the lock table and clean up old locks with a periodically running garbage collection process.

Another disadvantage of this method is that if the locking mechanism becomes complex (e.g., extending lock duration, transferring lock ownership), managing the database table can become complicated. Therefore, this method is generally suitable for very simple scenarios and situations where setting up an additional service is not practical. It can be a pragmatic solution, especially for a few processes running on a single server.

Conclusion: The Power of Pragmatic Solutions

When I need distributed lock mechanisms in my side projects, I don’t always opt for the most complex or popular solution. The approaches I mentioned above, such as PostgreSQL advisory locks, file locking, Redis SETNX, and a simple database table, can be quite effective depending on the project’s requirements and existing infrastructure. The important thing is to understand the trade-offs and choose the solution that best fits your project’s needs.

These simple methods, especially in situations where resources are limited or the project is in its early stages, provide both development ease and infrastructural efficiency. Before moving to complex solutions, it’s always beneficial to evaluate whether these lighter approaches can get the job done. Remember, the best solution is not always the most complex one; sometimes, the simplest is the most pragmatic.

In future posts, we can explore more detailed use cases and performance comparisons of these approaches.

Paylaş:

Bu yazı faydalı oldu mu?

Yükleniyor...

How was this post?

Frequently Asked Questions

Common questions readers have about this article.

I want to try PostgreSQL Advisory Lock in my side project, what should I install and configure as a first step?
First, I connected to an existing PostgreSQL server and made sure that the `pg_advisory_lock` functions were available. You need to create a separate `client` for each operation, as your connection pool should not share the same database session; otherwise, conflicts can occur since locks are session-based. Then, in the code block where I want to acquire a lock, I run the query `SELECT pg_try_advisory_lock(hashtext('my-resource'))` and check its return value; if it's `true`, the lock has been acquired. Don't forget to release it with `SELECT pg_advisory_unlock(hashtext('my-resource'))` when you're done. Packaging these steps into a helper function reduces code repetition.
What are the advantages and disadvantages of using PostgreSQL Advisory Lock instead of Redis's RedLock algorithm?
In my experience, PostgreSQL Advisory Lock can be run in projects already using a database without adding an extra service; this is a huge advantage in terms of maintenance and cost. Also, the lock is automatically released when the session ends, which reduces the risk of 'zombie locks'. On the other hand, Redis's RedLock offers very low latency and high scalability; Redis performs better when thousands of clients request locks simultaneously. PostgreSQL, if running on a single node, carries the risk of a single point of failure, and lock waiting times can increase with high concurrency. Therefore, PostgreSQL is sufficient for low-traffic side projects, while Redis may be preferred in high-traffic microservice environments.
What should I do if a lock cannot be acquired when using Advisory Lock? How should error handling and retry strategy be implemented?
When a lock cannot be acquired, I first check if `pg_try_advisory_lock` returns `false`, and then immediately retry after a short delay (e.g., 100-200ms) using a `setTimeout`. Limiting the number of retries to 5-7 prevents infinite loops. If these attempts also fail, a good approach is to log the operation to a queue (e.g., RabbitMQ) or a 'retry' table, allowing another worker to handle it later. Additionally, if lock acquisition takes a long time, I monitor the operation duration and add a timeout mechanism to prevent the system from getting stuck.
Is it true that PostgreSQL Advisory Locks 'only work on one node'? Can I use this method with multiple database instances?
This is a common myth; in projects where I used multiple PostgreSQL instances (e.g., read-replicas), I solved the problem by keeping Advisory Locks only on the primary node. Since locks are session-based, the same lock ID creates separate sessions on different replicas and does not block each other. Therefore, it is essential to direct all operations that need to acquire and release the lock to the same primary database. If your application also writes to replicas, you would need to add a distributed coordination layer (e.g., etcd). If you direct operations to a single primary, Advisory Lock becomes a secure distributed locking solution.
ME

Mustafa Erbay

Sistem Mimarisi · Network Uzmanı · Altyapı, Güvenlik ve Yazılım

2006'dan bu yana sistem mimarisi, network, sunucu altyapıları, büyük yapıların kurulumu, yazılım ve sistem güvenliği ekseninde çalışıyorum. Bu blogda sahada karşılığı olan teknik deneyimlerimi paylaşıyorum.

Kişisel Notlar

Bu notlar sadece sizde saklanır. Tarayıcınızda yerel olarak tutulur.

Hazır 0 karakter

Comments

Server-side AI Moderation

Comments are AI-moderated server-side and stored permanently.

?
0/2000

Server-side AI moderation

✉️ Free · No spam · Unsubscribe anytime

Get notified about new posts

New content and technical notes — straight to your inbox.

  • 📌
    Best of the week Single most-worth-reading post
  • 🔧
    Toolbox notes Real tools I used this week
  • 🧠
    Behind-the-scenes Notes that don't make it to blog

We don't spam. Unsubscribe anytime. · Tracked only by Umami (self-hosted, no Google).

Your Reading Stats

0

Posts Read

0m

Reading Time

0

Day Streak

-

Favorite Category

Related Posts