Node.jsでRedis使うならioredisがおすすめ
TL;DR
- まだ node_redis を使ってる人が多いけど標準で Promise 対応してなくてレガシー
- ioredis
- 使い方はほぼいっしょ
- 標準で Promise 対応してるので async/await でそのまま書ける
- Cluster, Sentinel, LuaScripting 含めたフル機能が使える
開発者の Luin さん
使い方
$ yarn install ioredis
async/await
もちろん従来の callback 方式でも書けるんだけど、async/await 覚えちゃうと callback には戻りたくない。
const Redis = require('ioredis'); (async () => { const redis = new Redis(); const pong = await redis.ping(); console.log(pong); // => PONG redis.disconnect(); })();
ちなみに node_redis で async/await する場合
公式から引用。promisify
をかまさないといけないのが非常にだるい。
const {promisify} = require('util'); const redis = require("redis"); const client = redis.createClient(); const getAsync = promisify(client.get).bind(client); (async () => { const res = await getAsync('foo'); console.log(res); })();
TypeScript で使う場合
$ yarn install ioredis $ yarn install -D @types/ioredis
型情報が必要になるので IORedis
として import したほうがわかりやすいと思う。
import * as IORedis from 'ioredis'; export class Sample { private readonly redis: IORedis.Redis; constructor(options?: IORedis.RedisOptions) { this.redis = new IORedis(options); } async echo(message: string): string { return await this.redis.echo(message); } }
ランキングを実装してみる
ioredis を使ったデイリーランキングの実装例。
import * as IORedis from 'ioredis'; import {DateTime} from 'luxon'; import {RankingUser} from './ranking-user'; import {UserDto} from './user-dto'; import {RankingUtil} from './ranking-util'; export class DailyRanking { private readonly redis: IORedis.Redis; constructor(options?: IORedis.RedisOptions) { this.redis = new IORedis(options); } // e.g. RANKING_DAILY_20181016 static createKey(): string { return DateTime.utc().toFormat("'RANKING_DAILY_'yyyyMMdd"); } update(user: RankingUser, score: number): void { const key = DailyRanking.createKey(); const dto: UserDto = {name: user.name, grade: user.grade}; // ignore userId, score const json = JSON.stringify(dto); this.redis.zadd(key, `${score}`, `${user.userId}:${json}`); } async listByHighScore(limit: number): Promise<RankingUser[]> { const key = DailyRanking.createKey(); const max = '+inf'; const min = '-inf'; const args = ['LIMIT', '0', `${limit}`, 'WITHSCORES']; const result = await this.redis.zrevrangebyscore(key, max, min, ...args); const users: RankingUser[] = []; for (let i = 0, len = result.length; i < len; i++) { if (i % 2 === 1) { const member = result[i - 1]; const score = result[i]; const user = RankingUtil.createRankingUser(member, score); users.push(user) } } return users; } async getByUserId(userId: number): Promise<RankingUser> { const key = DailyRanking.createKey(); const args = ['MATCH', `${userId}:*`]; const [cursor, result] = await this.redis.zscan(key, 0, ...args); const [member, score] = result; return RankingUtil.createRankingUser(member, score); } close(): void { this.redis.disconnect(); } }
Redis の SortedSet では key
, member
, score
の 3 つを格納してスコアを元に順位付けする。
一般的には member
にユーザIDだけ入れて取得した後で RDB から最新データを取ってくる実装が多いと思うけど、上記コードではユーザ情報を JSON 文字列として Redis のなかに格納してしまう方式。
member
の接頭辞を <user_id>:
としておくことで zscan
使えばユーザIDで取得することもできる。
ES6 以降の数値⇒文字列変換
ちなみに member
も score
も文字列で格納しなければいけないけど、数値変換する場合は Template String 使うのが一番高速。
const s1 = `${score}`; // Fast!! const s2 = score + ''; const s3 = String(score); const s4 = score.toString(); // 暗黙の型変換が行われるので一番遅い
まぁ for 文で 10,000 回転とかしなければ差異ないけど。
ioredisで謎のエラー
ioredis で謎のエラーがでてハマった。
[ioredis] Unhandled error event: ParserError: Protocol error, got "J" as reply type byte. Please report this. at handleError (/Users/jnst/go/src/github.com/Translimit/park-server/node_modules/redis-parser/lib/parser.js:190:15) at parseType (/Users/jnst/go/src/github.com/Translimit/park-server/node_modules/redis-parser/lib/parser.js:304:14)
エラーがでないコード
空のコンストラクタを使うとデフォルトで 127.0.0.1
に接続する。
import * as Redis from 'ioredis'; const redis = new Redis();
エラーがでるコード
host
と port
を指定するとエラー。
import * as Redis from 'ioredis'; const redis = new Redis(3306, '127.0.0.1');
原因
Redis の 6379 番ポートではなく、MySQL の 3306 番ポートにアクセスしてた。
このエラーだけならすぐ気づいたかもだけど、別のエラーとの複合技だったので解決に時間がかかってしまった。疲れてるようだ…。
TypeScriptとioredisのコードをJestでテストしたらコンストラクタじゃないと言われた件
TypeScript と ioredis のソースコード
TypeScript で Redis 使いたい場合は ioredis で書いてこんな感じのコードになる。
sample.ts
import * as Redis from 'ioredis'; (async () => { const redis = new Redis(); const pong = await redis.ping(); // => PONG })();
Jest のテストコード
ところが Jest でテスト書いたら実行できなくてハマってしまった。
sample.spec.ts
import * as Redis from 'ioredis'; describe('Redis', () => { it('should be return pong', async () => { const redis = new Redis(); const pong = await redis.ping(); redis.disconnect(); expect(pong).toBe('PONG'); }) });
.ts
ファイルを直接実行するので ts-jest を入れている。
$ jest TypeError: Redis is not a constructor 4 | 5 | it('should be return pong', async () => { > 6 | const redis = new Redis(); | ^ 7 | const pong = await redis.ping(); 8 | redis.disconnect(); 9 | expect(pong).toBe('PONG'); at Object.it (src/sample.spec.ts:6:19)
TypeError: Redis is not a constructor
なんでやねん
import * as Redis from 'ioredis'; const redis = new Redis();
のところでコンストラクタじゃないと言われる。書き方は正しいのに。
import Redis from 'ioredis';
に直したら動いた。間違ってるのに。
まぁそんなこともあるのかなと思ったりもしたけど、テストファイルをコンパイルして .js
ファイルにしてテスト実行してみるとエラーになる。
TypeError: ioredis_1.default is not a constructor 4 | describe('Redis', () => { 5 | it('should be return pong', async () => { > 6 | const redis = new ioredis_1.default(); | ^ 7 | const pong = await redis.ping(); 8 | redis.disconnect(); 9 | expect(pong).toBe('PONG'); at Object.it (dist/sample.spec.js:6:23)
TypeError: ioredis_1.default is not a constructor
やっぱり書き方が間違ってるいるわけだ。
原因
ts-jest
にバグがあったようだ。
"devDependencies": { "@types/ioredis": "^4.0.3", "@types/jest": "^23.3.5", "jest": "^23.6.0", "ts-jest": "^21.2.4", "typescript": "^3.1.3" }
yarn upgrade --latest ts-jest
して 23.10.4
にしたら治った。