43
35

More than 5 years have passed since last update.

Node.jsでRedis使うならioredisがおすすめ

Last updated at Posted at 2018-10-16

📘 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 の場合は暗黙の型変換をしないので、memberscore も明示的に文字列で渡さなければいけないけど、数値の文字列変換には Template String 使うのが一番高速。

const s1 = `${score}`;       // Fast!!
const s2 = score + '';       //  ↑
const s3 = String(score);    //  ↓
const s4 = score.toString(); // Slow

まぁ for 文で 100,000 回転とかしない限り差異ないけど。

43
35
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
35