📘 TL;DR
- まだ node_redis を使ってる人が多いけど標準で Promise 対応してなくてレガシー
-
ioredis
- 使い方はほぼいっしょ
- 標準で Promise 対応してるので async/await でそのまま書ける
- Cluster, Sentinel, LuaScripting 含めたフル機能が使える
👨🎓 開発者の Luin さん
- 元 Alibaba のエンジニア
- Redis の GUI ツール Medis も開発
🔎 使い方
$ 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
の 2 つを格納し、スコアを元に順位付けする。
一般的には member
にユーザIDだけ保存し、取得した後に RDB から最新のユーザ情報を取ってくる実装が多いと思うけど、上記コードではユーザ情報を JSON 文字列として Redis のなかにすべて格納してしまう方式。小さなデータ量で最新性が重視されないならこれで事足りる。
member
の接頭辞を <user_id>:
としておくことで zscan
使えばユーザIDで取得することもできる。
フルソースコードはこちら => GitHub
✏️ ES6 以降の数値⇒文字列変換
ちなみに TypeScript の場合は暗黙の型変換をしないので、member
も score
も明示的に文字列で渡さなければいけないけど、数値の文字列変換には Template String 使うのが一番高速。
const s1 = `${score}`; // Fast!!
const s2 = score + ''; // ↑
const s3 = String(score); // ↓
const s4 = score.toString(); // Slow
まぁ for 文で 100,000 回転とかしない限り差異ないけど。