Locking with redis

We had been looking at using zookeeper (or consul) for locking, rather than postgres rowlocks; but once we realised we didn’t really fancy running a quorum, it seemed just as simple to use redis (which we already had as a session store), instead of a single node of either.

While there is a full-blown lock protocol, all you really need (for our use case) is a key, with a ttl. And some sort of CAS operation.

If you are using the latest redis version (i.e. 6, or 7), then the SET command has some extra options to make this simple:

    async function withLock(id, cb) {
        const key = `/lock/${id}`;
        const val = await redis.set(key, "true", {
            EX: 60,
            KEEPTTL: true,
            GET: true,
        });
        if (val !== null) {
            throw new Error("already locked");
        }
        try {
            const res = await cb();
            return res;
        } finally {
            await redis.del(key);
        }
    };

First, you attempt to acquire the lock (i.e. set some value for a specific key). If the key already exists, you bail; otherwise, you do your business and then free the lock. If the process dies, the lock will be released, when the ttl expires.

If you are using an older redis version (e.g. 4), and don’t fancy upgrading; it is still possible, you just need to explicitly set the TTL (after acquiring the lock):

async function withLock(id, cb) {
        const key = `/lock/${id}`;
        const val = await redis.getset(key, "true");
        if (val !== null) {
            throw new Error("already locked");
        }
        await redis.expire(key, 5 * 60);
        try {
        ...
    };