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

## 📘 TL;DR

* まだ [node_redis](https://github.com/NodeRedis/node_redis) を使ってる人が多いけど標準で Promise 対応してなくてレガシー
* [ioredis](https://github.com/luin/ioredis)
  * 使い方はほぼいっしょ
  * 標準で Promise 対応してるので async/await でそのまま書ける
  * Cluster, Sentinel, LuaScripting 含めたフル機能が使える

## 👨‍🎓 開発者の Luin さん

* 元 Alibaba のエンジニア
* Redis の GUI ツール [Medis](https://github.com/luin/medis) も開発

## 🔎 使い方

```bash
$ yarn install ioredis
```

### async/await

もちろん従来の callback 方式でも書けるんだけど、async/await 覚えちゃうと callback には戻りたくない。

```javascript
const Redis = require('ioredis');

(async () => {
  const redis = new Redis();
  const pong = await redis.ping();
  console.log(pong); // => PONG

  redis.disconnect();
})();
```

### ちなみに node_redis で async/await する場合

[公式](https://github.com/NodeRedis/node_redis#promises)から引用。`promisify` をかまさないといけないのが非常にだるい。非常にだるい（大事なことなので）。

```javascript
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 で使う場合

```bash
$ yarn install ioredis
$ yarn install -D @types/ioredis
```

型情報が必要になるので `IORedis` として import したほうがわかりやすいと思う。

```typescript
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 を使ったデイリーランキングの実装例。

```typescript
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](https://github.com/jnst/x-typescript/tree/master/ranking)

## ✏️ ES6 以降の数値⇒文字列変換

ちなみに TypeScript の場合は暗黙の型変換をしないので、`member` も `score` も明示的に文字列で渡さなければいけないけど、数値の文字列変換には Template String 使うのが一番高速。

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

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