CLOVER🍀

That was when it all began.

ioredisを使って、Node.jsからRedisへアクセスしてみる

これは、なにをしたくて書いたもの?

Node.jsからRedisにアクセスする際に、以前node-redisを使ってみました。

Node.jsからRedisにアクセスしてみる - CLOVER🍀

これもだいぶ前の話ですが、今回はioredisを試してみたいと思います。

ioredis

ioredisのGitHubリポジトリーはこちら。

GitHub - redis/ioredis: 🚀 A robust, performance-focused, and full-featured Redis client for Node.js.

ioredisは、RedisのClientsとして「Recommended」なリポジトリーとして掲載されています。

Clients / Node.js

ClusterやSentinel含む多くの機能をサポートしており、ハイパフォーマンスやPromiseをネイティブにサポートしていることを
特徴として謳っています。

数が多いので、詳しくは以下を参照。

ioredis / Features

また、TypeScriptで書かれており、型宣言も含まれています。

対応しているRedisのバージョンは、2.6.12以降です。ioredisの最新のバージョン5系だと、最新のRedisのバージョンに追従するようです。

ドキュメントは、README.mdAPIリファレンスです。

ioredis

今回はこちらを試してみます。

node-redis

ところで、Node.jsにおけるRedisクライアントで「Official」扱いされているのがnode-redisです。

GitHub - redis/node-redis: Redis Node.js client

こちらはバージョン3まではPromiseに対応していませんでしたが、現在のバージョン4では対応しています。

TypeScriptにも対応していそうです。

現時点ではioredisの方が人気のようですが、Promiseに対応した今となっては両者の差も縮まっていそうですね。

環境

今回の環境は、こちら。

$ node --version
v18.17.1


$ npm --version
9.6.7

Redisのバージョン。

$ bin/redis-server -v
Redis server v=7.0.12 sha=00000000:0 malloc=jemalloc-5.2.1 bits=64 build=8125aaa6ef13d89b

Redisは172.17.0.2で動作し、以下のコマンドで起動しているものとします。

$ bin/redis-server --bind '0.0.0.0' --requirepass redispass

Node.jsプロジェクトを作成する

では、Node.jsプロジェクトを作成していきます。動作確認は、テストコードで行うことにしましょう。

プロジェクトの作成と、ざっくり依存関係のインストール。

$ npm init -y
$ npm i -D typescript
$ npm i -D prettier
$ npm i -D @types/node@v18
$ npm i -D jest @types/jest
$ npm i -D esbuild esbuild-jest
$ mkdir src test

package.jsonscriptsは、以下のようにしました。

  "scripts": {
    "build": "tsc --project .",
    "build:watch": "tsc --project . --watch",
    "typecheck": "tsc --project ./tsconfig.typecheck.json",
    "typecheck:watch": "tsc --project ./tsconfig.typecheck.json --watch",
    "test": "jest",
    "format": "prettier --write src test"
  },

各種設定ファイル。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

型チェック用のファイル。

tsconfig.typecheck.json

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
    "noEmit": true
  },
  "include": [
    "src", "test"
  ]
}

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 120
}

jest.config.js

module.exports = {
  testEnvironment: 'node',
  transform: {
    "^.+\\.tsx?$": "esbuild-jest"
  }
};

ioredisを使う

ioredisを使うので、npmパッケージのインストールから。

$ npm i ioredis

今回の依存関係は、こうなりました。

  "devDependencies": {
    "@types/jest": "^29.5.3",
    "@types/node": "^18.17.5",
    "esbuild": "^0.19.2",
    "esbuild-jest": "^0.5.0",
    "jest": "^29.6.2",
    "prettier": "^3.0.1",
    "typescript": "^5.1.6"
  },
  "dependencies": {
    "ioredis": "^5.3.2"
  }

テストコードの雛形は、こんな感じで用意。

test/redis.test.ts

import Redis from 'ioredis';

// ここに、テストを書く!!

set/get

単純なset/getから。

test('simple put and get', async () => {
  const redis = new Redis({
    host: '172.17.0.2',
    port: 6379,
    password: 'redispass',
    db: 0,
  });

  try {
    await redis.set('key1', 'value1');
    expect(await redis.get('key1')).toBe('value1');

    await redis.del('key1');
    expect(await redis.get('key1')).toBeNull();

    await redis.set('key2', 'value2');
    expect(await redis.get('key2')).toBe('value2');

    await redis.del('key2');
    expect(await redis.get('key2')).toBeNull();
  } finally {
    await redis.quit();
    // または
    // redis.disconnect();
  }
});

Redisへの接続は、README.mdのこちらを参照します。

ioredis / Quick Start / Connect to Redis

今回は、こうしました。

  const redis = new Redis({
    host: '172.17.0.2',
    port: 6379,
    password: 'redispass',
    db: 0,
  });

Redisの各種コマンドは、Redisのメソッドとして定義されています。

    await redis.set('key1', 'value1');
    expect(await redis.get('key1')).toBe('value1');

    await redis.del('key1');
    expect(await redis.get('key1')).toBeNull();

    await redis.set('key2', 'value2');
    expect(await redis.get('key2')).toBe('value2');

    await redis.del('key2');
    expect(await redis.get('key2')).toBeNull();

ioredis / Quick Start / Basic Usage

Redis | ioredis

Redisからの明示的な切断は、Redis#disconnectまたはRedis#quitです。

    await redis.quit();
    // または
    // redis.disconnect();

両者の違いは、Redis#disconnectに書かれています。

This method closes the connection immediately, and may lose some pending replies that haven't written to client. If you want to wait for the pending replies, use Redis#quit instead.

Redis#disconnect

Redisの応答を待っていたり、クライアントの書き込みが終わっていなくても接続を切ってしまうのがdisconnect、待つのがquitという
ことになりますね。

hmset/hgetall/hmget

ハッシュ的な使い方をするコマンドですね。

test('hmset and get', async () => {
  const redis = new Redis({
    host: '172.17.0.2',
    port: 6379,
    password: 'redispass',
    db: 0,
  });

  try {
    await redis.hmset('hkey1', { member1: 'value1-1', member2: 'value1-2' });
    expect(await redis.hgetall('hkey1')).toStrictEqual({ member1: 'value1-1', member2: 'value1-2' });
    expect(await redis.hmget('hkey1', 'member1', 'member2')).toStrictEqual(['value1-1', 'value1-2']);

    await redis.del('hkey1');
    expect(await redis.hgetall('hkey1')).toStrictEqual({}); // not null
  } finally {
    await redis.quit();
    // または
    // redis.disconnect();
  }
});

Pipeline

Pipeline。複数のコマンドをまとめてRedisに送信することで、パフォーマンスを向上させる方法です。

ioredis / Quick Start / Pipelining

Redis pipelining | Redis

test('use pipeline', async () => {
  const redis = new Redis({
    host: '172.17.0.2',
    port: 6379,
    password: 'redispass',
    db: 0,
  });

  try {
    const pipeline = redis.pipeline();
    pipeline.set('key1', 'value1').hmset('hkey1', { member1: 'value1-1', member2: 'value1-2' });

    await pipeline.exec();

    const result = await redis.pipeline().get('key1').hgetall('hkey1').exec();

    expect(result[0]).toStrictEqual([null, 'value1']);
    expect(result[1]).toStrictEqual([null, { member1: 'value1-1', member2: 'value1-2' }]);

    await redis.pipeline().del('key1').del('hkey1').exec();

    const deletedResult = await redis.pipeline().get('key1').hgetall('hkey1').exec();
    expect(deletedResult[0]).toStrictEqual([null, null]);
    expect(deletedResult[1]).toStrictEqual([null, {}]);

    redis.pipeline().set('key2', 'value2').hmset('hkey2', { member1: 'value2-1', member2: 'value2-2' }).discard();

    const discardedResult = await redis.pipeline().get('key2').hgetall('hkey2').exec();
    expect(discardedResult[0]).toStrictEqual([null, null]);
    expect(discardedResult[1]).toStrictEqual([null, {}]);
  } finally {
    await redis.quit();
    // または
    // redis.disconnect();
  }
});

Redis#pipelineで始めて、最後にexecします。

    const pipeline = redis.pipeline();
    pipeline.set('key1', 'value1').hmset('hkey1', { member1: 'value1-1', member2: 'value1-2' });

    await pipeline.exec();

execするまでは実行されないので、やめる場合はdiscardで。

    redis.pipeline().set('key2', 'value2').hmset('hkey2', { member1: 'value2-1', member2: 'value2-2' }).discard();

Transaction

複数のコマンドをまとめて、アトミックに実行するのがトランザクションです。

ioredis / Quick Start / Transaction

RDBMSのように、途中で処理が失敗してもロールバックさせて全体を取り消すことはできません。
事前にある程度の確認は行われますが、途中で失敗した場合はそこまでの処理は行われます。

Transactions | Redis

test('use transaction', async () => {
  const redis = new Redis({
    host: '172.17.0.2',
    port: 6379,
    password: 'redispass',
    db: 0,
  });

  try {
    const multi = redis.multi();
    multi.set('key1', 'value1').hmset('hkey1', { member1: 'value1-1', member2: 'value1-2' });

    await multi.exec();

    const result = await redis.multi().get('key1').hgetall('hkey1').exec();

    expect(result[0]).toStrictEqual([null, 'value1']);
    expect(result[1]).toStrictEqual([null, { member1: 'value1-1', member2: 'value1-2' }]);

    await redis.multi().del('key1').del('hkey1').exec();

    const deletedResult = await redis.multi().get('key1').hgetall('hkey1').exec();
    expect(deletedResult[0]).toStrictEqual([null, null]);
    expect(deletedResult[1]).toStrictEqual([null, {}]);

    redis.multi().set('key2', 'value2').hmset('hkey2', { member1: 'value2-1', member2: 'value2-2' }).discard();

    const discardedResult = await redis.multi().get('key2').hgetall('hkey2').exec();
    expect(discardedResult[0]).toStrictEqual([null, null]);
    expect(discardedResult[1]).toStrictEqual([null, {}]);
  } finally {
    await redis.quit();
    // または
    // redis.disconnect();
  }
});

使い方は、Redis#multiで始めてexecを実行します。Pipelineとよく似ています。

    const multi = redis.multi();
    multi.set('key1', 'value1').hmset('hkey1', { member1: 'value1-1', member2: 'value1-2' });

    await multi.exec();

discardで破棄できるのも同じです。

    redis.multi().set('key2', 'value2').hmset('hkey2', { member1: 'value2-1', member2: 'value2-2' }).discard();

よく似ていますが、Pipelineとはまったく異なるものだということには注意ですね。

ひとまず、こんなところでしょうか。

内容としては、過去に書いたこちらをなぞったものになっています。

Node.jsからRedisにアクセスしてみる - CLOVER🍀