Node.jsとRedis

Node.jsとRedisは自然な組み合わせです。このレッスンでは、Node.jsでのRedisの使用について解説します。

RedisのNode.jsクライアント

主要なNode.js Redisクライアント:

💡 推奨: 新しいプロジェクトではioredisまたはnode-redis v4+を使用してください。

ioredis

インストール

BASH
npm install ioredis

基本接続

JAVASCRIPT
const Redis = require('ioredis');

// Redisに接続
const redis = new Redis({
  host: 'localhost',
  port: 6379,
  // password: 'your_password',
  db: 0
});

// 接続をテスト
redis.ping().then(result => {
  console.log(result); // 'PONG'
});

文字列操作

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

// SETとGET
async function stringDemo() {
  // SET
  await redis.set('name', 'Alice');
  
  // GET
  const name = await redis.get('name');
  console.log(name); // 'Alice'
  
  // 有効期限付きSET
  await redis.set('session', 'data', 'EX', 3600); // 1時間後に期限切れ
  
  // MSETとMGET
  await redis.mset('key1', 'value1', 'key2', 'value2');
  const values = await redis.mget('key1', 'key2');
  console.log(values); // ['value1', 'value2']
  
  // INCR
  await redis.set('counter', 0);
  const count = await redis.incr('counter');
  console.log(count); // 1
  
  // DEL
  await redis.del('name');
}

stringDemo();

ハッシュ操作

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function hashDemo() {
  // HSETとHGET
  await redis.hset('user:1', 'name', 'Alice');
  await redis.hset('user:1', 'age', 25);
  const name = await redis.hget('user:1', 'name');
  console.log(name); // 'Alice'
  
  // HMSET
  await redis.hmset('user:2', {
    name: 'Bob',
    age: 30,
    city: 'Beijing'
  });
  
  // HGETALL
  const user = await redis.hgetall('user:2');
  console.log(user); // { name: 'Bob', age: '30', city: 'Beijing' }
  
  // HKEYSとHVALS
  const keys = await redis.hkeys('user:2');
  const values = await redis.hvals('user:2');
  
  // HDEL
  await redis.hdel('user:2', 'city');
  
  // HINCRBY
  await redis.hincrby('user:2', 'age', 1);
}

hashDemo();

リスト操作

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function listDemo() {
  // LPUSHとRPUSH
  await redis.lpush('mylist', 'value1', 'value2');
  await redis.rpush('mylist', 'value3');
  
  // LRANGE
  const list = await redis.lrange('mylist', 0, -1);
  console.log(list); // ['value2', 'value1', 'value3']
  
  // LPOPとRPOP
  const left = await redis.lpop('mylist');
  const right = await redis.rpop('mylist');
  
  // LLEN
  const length = await redis.llen('mylist');
}

listDemo();

セット操作

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function setDemo() {
  // SADD
  await redis.sadd('myset', 'a', 'b', 'c');
  
  // SMEMBERS
  const members = await redis.smembers('myset');
  console.log(members); // Set { 'a', 'b', 'c' }
  
  // SISMEMBER
  const exists = await redis.sismember('myset', 'a');
  console.log(exists); // 1 (true)
  
  // SREM
  await redis.srem('myset', 'a');
  
  // SINTER、SUNION、SDIFF
  await redis.sadd('set1', 'a', 'b', 'c');
  await redis.sadd('set2', 'b', 'c', 'd');
  const inter = await redis.sinter('set1', 'set2');
  console.log(inter); // Set { 'b', 'c' }
}

setDemo();

ソート済みセット操作

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function zsetDemo() {
  // ZADD
  await redis.zadd('leaderboard', 100, 'player1', 200, 'player2', 150, 'player3');
  
  // ZRANGE
  const top = await redis.zrange('leaderboard', 0, -1, 'WITHSCORES');
  console.log(top); // ['player1', '100', 'player3', '150', 'player2', '200']
  
  // ZREVRANGE(高い方から低い方へ)
  const topDesc = await redis.zrevrange('leaderboard', 0, 2, 'WITHSCORES');
  
  // ZSCORE
  const score = await redis.zscore('leaderboard', 'player1');
  console.log(score); // '100'
  
  // ZINCRBY
  await redis.zincrby('leaderboard', 50, 'player1');
  
  // ZREM
  await redis.zrem('leaderboard', 'player1');
}

zsetDemo();

Pub/Sub

JAVASCRIPT
const Redis = require('ioredis');

// サブスクライバー
const subscriber = new Redis();
subscriber.subscribe('news', 'sports');

subscriber.on('message', (channel, message) => {
  console.log(`Channel: ${channel}, Message: ${message}`);
});

// パブリッシャー
const publisher = new Redis();
setInterval(() => {
  publisher.publish('news', `News message ${Date.now()}`);
}, 1000);

パイプライン

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function pipelineDemo() {
  const pipeline = redis.pipeline();
  
  pipeline.set('key1', 'value1');
  pipeline.set('key2', 'value2');
  pipeline.set('key3', 'value3');
  pipeline.get('key1');
  
  const results = await pipeline.exec();
  console.log(results);
  // [ [ null, 'OK' ], [ null, 'OK' ], [ null, 'OK' ], [ null, 'value1' ] ]
}

pipelineDemo();

トランザクション

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function transactionDemo() {
  const multi = redis.multi();
  
  multi.set('key1', 'value1');
  multi.set('key2', 'value2');
  multi.incr('counter');
  
  const results = await multi.exec();
  console.log(results);
}

transactionDemo();

接続管理

ioredisは組み込みのコネクションプールを備えており、追加設定は不要です:

JAVASCRIPT
const Redis = require('ioredis');

// 単一接続(自動プール)
const redis = new Redis({
  host: 'localhost',
  port: 6379,
  maxRetriesPerRequest: 3,
  enableReadyCheck: true
});

// クラスターモード
const cluster = new Redis.Cluster([
  { host: '127.0.0.1', port: 7000 },
  { host: '127.0.0.1', port: 7001 },
  { host: '127.0.0.1', port: 7002 }
]);

node-redis(公式クライアント)

インストール

BASH
npm install redis

基本的な使用方法

JAVASCRIPT
const { createClient } = require('redis');

async function main() {
  // クライアントを作成
  const client = createClient({
    url: 'redis://localhost:6379'
  });
  
  // 接続
  await client.connect();
  
  // SETとGET
  await client.set('key', 'value');
  const value = await client.get('key');
  console.log(value); // 'value'
  
  // ハッシュ操作
  await client.hSet('user:1', 'name', 'Alice');
  const name = await client.hGet('user:1', 'name');
  
  // リスト操作
  await client.lPush('mylist', 'value1');
  const list = await client.lRange('mylist', 0, -1);
  
  // セット操作
  await client.sAdd('myset', 'a', 'b', 'c');
  const members = await client.sMembers('myset');
  
  // 接続を閉じる
  await client.quit();
}

main();

Pub/Sub

JAVASCRIPT
const { createClient } = require('redis');

async function pubsub() {
  const subscriber = createClient();
  await subscriber.connect();
  
  // サブスクライブ
  await subscriber.subscribe('news', (message) => {
    console.log(`Received: ${message}`);
  });
  
  const publisher = createClient();
  await publisher.connect();
  
  // 公開
  await publisher.publish('news', 'Hello World');
}

pubsub();

実践的な例

例1:キャッシュデコレーター

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

function cache(expire = 3600) {
  return function(target, propertyKey, descriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function(...args) {
      const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
      
      // キャッシュから取得を試みる
      const cached = await redis.get(cacheKey);
      if (cached) {
        return JSON.parse(cached);
      }
      
      // 元のメソッドを実行
      const result = await originalMethod.apply(this, args);
      
      // キャッシュに保存
      await redis.set(cacheKey, JSON.stringify(result), 'EX', expire);
      
      return result;
    };
    
    return descriptor;
  };
}

class UserService {
  @cache(300)
  async getUser(userId) {
    console.log(`Querying database: userId=${userId}`);
    return { id: userId, name: `User${userId}` };
  }
}
▶ 試してみよう

例2:分散ロック

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();
const { v4: uuidv4 } = require('uuid');

class DistributedLock {
  constructor(key, expire = 10) {
    this.key = `lock:${key}`;
    this.expire = expire;
    this.identifier = uuidv4();
  }
  
  async acquire() {
    const result = await redis.set(
      this.key,
      this.identifier,
      'NX',
      'EX',
      this.expire
    );
    return result === 'OK';
  }
  
  async release() {
    const script = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      else
        return 0
      end
    `;
    const result = await redis.eval(script, 1, this.key, this.identifier);
    return result === 1;
  }
}

// ロックを使用
async function withLock(key, callback) {
  const lock = new DistributedLock(key);
  
  try {
    while (!(await lock.acquire())) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    
    return await callback();
  } finally {
    await lock.release();
  }
}

// 例
await withLock('resource', async () => {
  console.log('Executing business logic');
});
▶ 試してみよう

例3:レートリミッター

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function rateLimit(key, limit = 10, period = 60) {
  const current = Math.floor(Date.now() / 1000);
  const windowStart = current - period;
  
  const results = await redis
    .multi()
    .zremrangebyscore(key, 0, windowStart)
    .zcard(key)
    .zadd(key, current, `${current}`)
    .expire(key, period)
    .exec();
  
  const count = results[1][1];
  return count < limit;
}

// レートリミッターの使用
async function apiHandler(userId) {
  const allowed = await rateLimit(`api:${userId}`, 10, 60);
  
  if (allowed) {
    console.log('Request allowed');
    return { success: true };
  } else {
    console.log('Request denied');
    return { success: false, error: 'Rate limit exceeded' };
  }
}
▶ 試してみよう

例4:メッセージキュー

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

class MessageQueue {
  constructor(name) {
    this.name = `queue:${name}`;
  }
  
  async push(message) {
    await redis.rpush(this.name, JSON.stringify(message));
  }
  
  async pop(timeout = 0) {
    const result = await redis.blpop(this.name, timeout);
    if (result) {
      return JSON.parse(result[1]);
    }
    return null;
  }
  
  async size() {
    return await redis.llen(this.name);
  }
}

// プロデューサー
const queue = new MessageQueue('tasks');
await queue.push({ task: 'send_email', to: 'user@example.com' });

// コンシューマー
while (true) {
  const task = await queue.pop(5);
  if (task) {
    console.log('Processing task:', task);
  } else {
    break;
  }
}
▶ 試してみよう

エラーハンドリング

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

redis.on('error', (error) => {
  console.error('Redis error:', error);
});

redis.on('connect', () => {
  console.log('Redis connected');
});

redis.on('ready', () => {
  console.log('Redis ready');
});

redis.on('close', () => {
  console.log('Redis connection closed');
});

❓ よくある質問

Q ioredisとnode-redisの選び方は?
A ioredisはより多機能(クラスター、センチネル)です。node-redisは公式クライアントです。ioredisが推奨されます。
Q ioredisはスレッドセーフですか?
A はい。ioredisは組み込みのコネクションプールを備えており、モジュール間で共有できます。
Q 切断を処理するにはどうすればよいですか?
A ioredisは自動的に再接続します。errorイベントとconnectイベントをリッスンできます。
Q 非同期操作はどのように機能しますか?
A すべてのioredis操作はPromiseを返します。async/awaitまたは.then()を使用します。
Q コネクションプールを実装するにはどうすればよいですか?
A ioredisは組み込みのコネクションプールを備えており、追加設定は不要です。

📖 まとめ

📝 練習問題

  1. 基本操作: ioredisを使用して文字列、ハッシュ、リストの操作をしましょう
  2. パイプライン: パイプラインを使用して1000個のキーをバッチ設定し、パフォーマンスを比較しましょう
  3. Pub/Sub: シンプルなPub/Subメッセージングシステムを実装しましょう
  4. 実践: シンプルな分散ロックまたはレートリミッターを実装しましょう

おめでとうございます!

Redisチュートリアルの全26レッスンを完了しました!次のステップ:

100%