Redis Transactions in Node

09.15.2021

Intro

Transactions are a common database requirement for when you need to make multiple insert or update operations together. That is, if one operation fails, you don't want to execute either operation; they must be completed together. In this article, we will learn how to use transaction in Redis with Nodejs.

Let's take an example of caching orders in redis. In our applicaiton, we would like to store the user's purchase and increment the number of purchase they have. This is a good case for a transaction, because if the user purchase insert fails, we don't want the counter to increment.

Setting up Redis

For setting up Redis, I would recommend using a service for you in prod. Azure for example, has a great redis service that scales easily. However, you will want to learn redis and eventually how to scale it yourself. This will help with debugging cloud services or eventually, saving money and not using them.

We will start our intro to redis via using docker compose. Create a docker-compose.yml file and add the following.

version: "3.2"
services:
  redis:
    image: "redis:alpine"
    command: redis-server
    ports:
      - "6379:6379"
    volumes:
      - $PWD/redis-data:/var/lib/redis
      - $PWD/redis.conf:/usr/local/etc/redis/redis.conf
    environment:
      - REDIS_REPLICATION_MODE=master

Ensure you have docker installed and run

docker-compose up

Installing Redis Modules

There are two modules I see often used in nodejs. I will tend towards ioredis as it has built in support for promises and many other features in redis.

npm install ioredis

Writing the Code

Let's open up a new file, index.js. To use pipelining, we first a pipeline instance.

We start by connecting to our redis instance.

const Redis = require("ioredis")

const redis = new Redis()

To run transactions, we can start by using the multi function. This by default, will automatically create a pipeline as well.

await redis.multi()

Next, we can append our commands by calling each method on the previous multi command. Following from the example above, we want to add a user purchase and increment the count. Each one of these commands will wait until the exec method is called before sending. If one of the commands fails, neither will be executed.

await redis.multi()
  .hset("user-purchase","customer", "keith")
  .hset("user-purchase", "amount", 100)
  .incr("customer-keith-purchase-count")

Finally, to send all commands, we can use the exec function on the pipeline. Redis will execute all the commands and returns the results in an array.

const results = await redis.multi()
    .hset("user-purchase","customer", "keith")
    .hset("user-purchase", "amount", 100)
    .incr("customer-keith-purchase-count")
    .exec()
console.log(results) // [ [ null, 0 ], [ null, 0 ], [ null, 9 ] ]

Below is the full code for context.

async function main() {
    const results = await redis.multi()
        .hset("user-purchase","customer", "keith")
        .hset("user-purchase", "amount", 100)
        .incr("customer-keith-purchase-count")
        .exec()
    console.log(results) // [ [ null, 0 ], [ null, 0 ], [ null, 9 ] ]
}

(async () => {
    await main()
})()

For future reference, if you would like to disable transactions, you can set the named parameter transaction=False.

await redis.multi({ pipeline: false })