これは、なにをしたくて書いたもの?
Node.jsからRedisにアクセスする際に、以前node-redisを使ってみました。
Node.jsからRedisにアクセスしてみる - CLOVER🍀
これもだいぶ前の話ですが、今回はioredisを試してみたいと思います。
ioredis
GitHub - redis/ioredis: 🚀 A robust, performance-focused, and full-featured Redis client for Node.js.
ioredisは、RedisのClientsとして「Recommended」なリポジトリーとして掲載されています。
ClusterやSentinel含む多くの機能をサポートしており、ハイパフォーマンスやPromiseをネイティブにサポートしていることを
特徴として謳っています。
数が多いので、詳しくは以下を参照。
また、TypeScriptで書かれており、型宣言も含まれています。
対応しているRedisのバージョンは、2.6.12以降です。ioredisの最新のバージョン5系だと、最新のRedisのバージョンに追従するようです。
ドキュメントは、README.md
とAPIリファレンスです。
今回はこちらを試してみます。
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.json
のscripts
は、以下のようにしました。
"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からの明示的な切断は、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
、待つのが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
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のように、途中で処理が失敗してもロールバックさせて全体を取り消すことはできません。
事前にある程度の確認は行われますが、途中で失敗した場合はそこまでの処理は行われます。
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とはまったく異なるものだということには注意ですね。
ひとまず、こんなところでしょうか。
内容としては、過去に書いたこちらをなぞったものになっています。