DartからRedisを操作できる「redis」パッケージ

DartやFlutterでアプリケーションを開発していると、キャッシュやデータストア、Pub/SubなどでRedisを利用したいケースがあります。

Redisは高速なインメモリデータベースとして広く利用されていますが、DartからRedisを操作するにはRedisプロトコルに対応したクライアントが必要です。

今回紹介するredisは、DartからRedisサーバーへ接続して操作できるRedisクライアントです。

Redisプロトコルのパーサーとクライアントを提供しており、シンプルさとパフォーマンスを重視して設計されているのが特徴です。また、外部パッケージへの依存なしで動作します。

この記事では、redisの基本的な使い方から、データの保存・取得、トランザクション、CAS、Pub/Sub、TLS、バイナリデータなどについて解説します。

redisとは?

redisは、Dart向けのRedisプロトコルパーサー兼クライアントです。

DartアプリケーションからRedisサーバーへ接続し、Redisコマンドを送信できます。

基本的な構成は以下のようになります。

Dartアプリケーション
        ↓
redisパッケージ
        ↓
Redis Protocol
        ↓
Redis Server

特徴的なのは、Redisの各コマンド専用のAPIを大量に用意するのではなく、Redisコマンドを配列として渡せるシンプルな設計になっていることです。

例えばSETコマンドなら、以下のように記述できます。

Dart

final response = await command.send_object([
  "SET",
  "key",
  "value",
]);

print(response);

このような仕組みにより、さまざまなRedisコマンドをDartから直接実行できます。

redisの特徴

主な特徴は以下の通りです。

機能対応
Redis接続
Redisプロトコル解析
Redisコマンド送信
トランザクション
CAS
Pub/Sub
Unicode
バイナリデータ
TLS
パイプライン処理
外部パッケージ依存なし

現在の安定版は4.1.0です。

対応プラットフォームはAndroid、iOS、Linux、macOS、Windowsです。

インストール

pubspec.yamlに追加します。

YAML

dependencies:
  redis: ^4.1.0

追加したら、以下を実行します。

Bash

flutter pub get

Dartから利用する場合はインポートします。

Dart

import 'package:redis/redis.dart';

Redisサーバーへ接続する

Redisサーバーへ接続するにはRedisConnectionを利用します。

Dart

final conn = RedisConnection();

final command = await conn.connect(
  'localhost',
  6379,
);

Redisの標準ポートは6379です。

接続が成功するとCommandが返されます。

このCommandを使ってRedisへコマンドを送信できます。

SETでデータを保存する

Redisに値を保存するにはSETコマンドを送信します。

Dart

final conn = RedisConnection();

final command = await conn.connect(
  'localhost',
  6379,
);

final response = await command.send_object([
  "SET",
  "name",
  "Dart",
]);

print(response);

これはRedisで以下のコマンドを実行するのと同じです。

SET name Dart

正常に処理されるとOKが返されます。

GETでデータを取得する

保存したデータはGETコマンドで取得できます。

Dart

final response = await command.send_object([
  "GET",
  "name",
]);

print(response);

nameDartを保存していた場合、取得結果はDartになります。

INCRで数値を増加させる

RedisのINCRコマンドも利用できます。

Dart

await command.send_object([
  "SET",
  "count",
  "0",
]);

final result = await command.send_object([
  "INCR",
  "count",
]);

print(result);

count0から1へ増加します。

さらに実行すると値を連続して増加させることができます。

Dart

await command.send_object([
  "INCR",
  "count",
]);

await command.send_object([
  "INCR",
  "count",
]);

コマンドを順番に実行する

send_object()を利用すると、複数のRedisコマンドを順番に実行できます。

Dart

final conn = RedisConnection();

final command = await conn.connect(
  'localhost',
  6379,
);

await command.send_object([
  "SET",
  "count",
  "0",
]);

await command.send_object([
  "INCR",
  "count",
]);

await command.send_object([
  "INCR",
  "count",
]);

final response = await command.send_object([
  "GET",
  "count",
]);

print(response);

この例では、SETINCRINCRGETの順番で処理しています。

コマンドのレスポンス

RedisのレスポンスはDartの型へ変換されます。

RedisDart
StringString
IntegerInteger
ArrayList
ErrorRedisError
BulkString / Binary

Redisの配列はネストすることもできます。

例えば複数階層の配列を含むレスポンスは、DartではネストされたListとして扱えます。

また、文字列は基本的にUTF-8でエンコード・デコードされます。

Unicodeを扱う

redisはUTF-8による文字列のエンコード・デコードに対応しています。

そのため、日本語などのUnicode文字列も扱えます。

Dart

await command.send_object([
  "SET",
  "message",
  "こんにちは",
]);

final response = await command.send_object([
  "GET",
  "message",
]);

print(response);

Redisとの間で文字列を扱う場合はUTF-8が利用されます。

バイナリデータを扱う

通常、Redisから取得したBulkデータはUTF-8文字列として処理されます。

バイナリデータとして扱いたい場合はRedisParserBulkBinaryを利用できます。

Dart

final conn = RedisConnection();

final command = await conn.connect(
  'localhost',
  6379,
);

final binaryCommand = Command
    .from(command)
    .setParser(RedisParserBulkBinary());

バイナリデータをRedisBulkとして送信できます。

Dart

final data = [
  1,
  2,
  3,
  4,
  5,
];

await binaryCommand.send_object([
  "SET",
  "binary-key",
  RedisBulk(data),
]);

取得する場合もバイナリデータとして扱えます。

Dart

final result = await binaryCommand.send_object([
  "GET",
  "binary-key",
]);

print(result);

画像やファイルなど、文字列として扱う必要がないデータをRedisへ保存するケースでも利用できます。

TLSでRedisへ接続する

RedisサーバーへTLSを利用して接続することもできます。

connectSecure()を利用します。

Dart

final conn = RedisConnection();

final command = await conn.connectSecure(
  'localhost',
  6379,
);

接続後は通常のCommandと同じようにRedisコマンドを実行できます。

Redisの認証を行う場合は、AUTHコマンドを送信します。

Dart

await command.send_object([
  "AUTH",
  "username",
  "password",
]);

また、既存のSocketをconnectWithSocket()へ渡して接続することもできます。

トランザクションを利用する

Redisのトランザクションを利用する場合は、Commandmulti()を使用します。

Dart

final transaction = await command.multi();

取得したTransactionに対してコマンドを追加します。

Dart

transaction.send_object([
  "SET",
  "count",
  "0",
]);

transaction.send_object([
  "INCR",
  "count",
]);

transaction.send_object([
  "INCR",
  "count",
]);

最後にexec()を実行します。

Dart

transaction.exec();

multi()exec()を利用することで、Redisのトランザクション処理をDartから扱えます。

CASを利用する

redisにはCAS(Check-and-Set)パターンを扱うためのCasクラスも用意されています。

Redisのトランザクションでは、すべてのコマンドがまとめて実行されるため、前のコマンドの結果を利用して次の処理を判断するような処理には別の仕組みが必要になる場合があります。

そのような場合にCasを利用できます。

Dart

final cas = Cas(command);

Casには主に以下のメソッドがあります。

  • watch
  • multiAndExec

watch()では監視するキーを指定し、そのキーが変更された場合にCAS処理を再実行できます。

Dart

cas.watch(
  ["key"],
  () {
    // CAS処理
  },
);

CASで値を更新する

例えばキーの値を取得して1増加させる処理は以下のように実装できます。

Dart

final cas = Cas(command);

cas.watch(
  ["key"],
  () {
    command.send_object([
      "GET",
      "key",
    ]).then((String value) {
      var number = int.parse(value);
      number++;

      cas.multiAndExec(
        (Transaction transaction) {
          transaction.send_object([
            "SET",
            "key",
            number.toString(),
          ]);
        },
      );
    });
  },
);

監視対象のキーがトランザクション外から変更された場合には、CASの処理が再試行されます。

Pub/Subを利用する

RedisのPub/Subにも対応しています。

まず既存のCommandからPubSubを作成します。

Dart

final pubsub = PubSub(command);

PubSubを作成した後は、その接続上で元のCommandを利用しないようにする必要があります。

チャンネルをSubscribeする

チャンネルを購読するにはsubscribe()を利用します。

Dart

pubsub.subscribe([
  "notifications",
]);

複数のチャンネルを指定することもできます。

Dart

pubsub.subscribe([
  "notifications",
  "messages",
]);

Pub/Subのメッセージを受信する

getStream()を利用すると、Redisから受信したメッセージをDartのStreamとして取得できます。

Dart

final stream = pubsub.getStream();

await for (final message in stream) {
  print(message);
}

これによって、Redis Pub/SubのメッセージをDartの非同期処理として扱えます。

パターン購読を利用する

subscribe()だけでなく、psubscribe()も利用できます。

Dart

pubsub.psubscribe([
  "notifications:*",
]);

パターンに一致する複数のチャンネルをまとめて購読できます。

Pub/Subの購読を解除する

通常の購読を解除する場合はunsubscribe()を利用します。

Dart

pubsub.unsubscribe([
  "notifications",
]);

パターン購読を解除する場合はpunsubscribe()を利用します。

Dart

pubsub.punsubscribe([
  "notifications:*",
]);

Pub/Subでメッセージを送信する

メッセージの送信にはRedisのPUBLISHコマンドを利用できます。

Dart

await command.send_object([
  "PUBLISH",
  "notifications",
  "Hello",
]);

notificationsというチャンネルへHelloというメッセージを送信しています。

パイプライン処理

複数のRedisコマンドを効率よく処理したい場合は、パイプライン処理を利用できます。

pipe_start()を呼び出してから複数のコマンドを送信し、最後にpipe_end()を呼び出します。

Dart

command.pipe_start();

command.send_object([
  "SET",
  "key1",
  "value1",
]);

command.send_object([
  "SET",
  "key2",
  "value2",
]);

command.send_object([
  "SET",
  "key3",
  "value3",
]);

command.pipe_end();

pipe_start()pipe_end()は、ソケットのNagleアルゴリズムを有効・無効にするための処理として実装されています。

デフォルトでは短いレイテンシーを優先してNagleアルゴリズムが無効になっており、パイプライン処理ではスループットや通信オーバーヘッドの改善が期待できます。

パフォーマンス

パッケージのREADMEでは、ノートPC上で毎秒18万回のINCR操作を実行・処理できたというベンチマーク結果が紹介されています。

ただし、これは特定の環境で行われたテスト結果なので、実際のパフォーマンスはRedisサーバー、ネットワーク、実行環境などによって変わります。

接続を閉じる

Redisへの接続が不要になったら、接続を閉じます。

Dart

command.get_connection().close();

例えば以下のように利用できます。

Dart

import 'package:redis/redis.dart';

Future<void> main() async {
  final conn = RedisConnection();

  final command = await conn.connect(
    'localhost',
    6379,
  );

  await command.send_object([
    "SET",
    "name",
    "Dart",
  ]);

  final value = await command.send_object([
    "GET",
    "name",
  ]);

  print(value);

  command.get_connection().close();
}

キャッシュとして利用する

Redisの代表的な用途の一つがキャッシュです。

例えばデータベースから取得したデータをRedisへ保存しておき、次回以降のアクセスではRedisから取得する構成を作れます。

アプリケーション
       ↓
Redisにデータがある?
    ↙       ↘
  Yes        No
   ↓          ↓
Redisから   DBなどから取得
取得          ↓
   ↓       Redisへ保存
   └────┬─────┘
        ↓
    アプリへ返す

DartからRedisの値を確認する場合はGETを利用できます。

Dart

final cached = await command.send_object([
  "GET",
  "user:1",
]);

if (cached != null) {
  print("Cache hit: $cached");
}

Redisをキャッシュとして利用することで、データベースへのアクセス回数を減らす構成を作れます。

Pub/Subを使った通知処理

RedisのPub/Subを利用すると、複数のアプリケーション間でメッセージをやり取りできます。

例えば以下のような構成です。

Producer
   ↓
Redis
   ↓
notifications
   ↓
Consumer A
Consumer B
Consumer C

リアルタイム通知やイベント配信など、Redis Pub/Subを利用したシステムをDartから構築できます。

対応プラットフォーム

redisは以下のプラットフォームに対応しています。

  • Android
  • iOS
  • Linux
  • macOS
  • Windows

Webは対応プラットフォームとして掲載されていません。

そのため、Flutter Webから直接Redisへ接続する用途ではなく、サーバー側のDartアプリケーションなどで利用する構成を検討する必要があります。

外部パッケージへの依存がない

redisは外部パッケージへの依存なしで動作することも特徴です。

Redisクライアントを導入する際の依存関係をシンプルにしたい場合にも利用しやすいパッケージです。

注意点

Redisサーバーが必要

redisはRedisサーバーそのものを提供するパッケージではありません。

DartからRedisへ接続するためのクライアントなので、別途Redisサーバーが必要です。

Redisコマンドの知識が必要

redisではRedisコマンドを配列として直接送信する設計になっています。

例えば、

Dart

await command.send_object([
  "SET",
  "key",
  "value",
]);

のように記述します。

そのため、Redisの基本的なコマンドを理解していると扱いやすいでしょう。

Webには対応していない

対応プラットフォームにWebは含まれていません。

Flutter WebからRedisを利用する場合は、ブラウザからRedisへ直接接続するのではなく、サーバー側にRedisクライアントを配置してAPI経由で利用する構成などを検討する必要があります。

Pub/Sub利用時はCommandに注意

PubSubを作成すると、同じ接続上では元のCommandを利用できなくなります。

Pub/Sub専用の接続として扱う必要があります。

まとめ

redisは、DartからRedisサーバーへ接続してRedisコマンドを実行できるシンプルなRedisクライアントです。

基本的には以下のようなコードでRedisを操作できます。

Dart

import 'package:redis/redis.dart';

Future<void> main() async {
  final conn = RedisConnection();

  final command = await conn.connect(
    'localhost',
    6379,
  );

  await command.send_object([
    "SET",
    "key",
    "value",
  ]);

  final value = await command.send_object([
    "GET",
    "key",
  ]);

  print(value);

  command.get_connection().close();
}

主な機能として、

  • Redisプロトコルの解析
  • Redisコマンドの送信
  • トランザクション
  • CAS
  • Pub/Sub
  • Unicode
  • バイナリデータ
  • TLS
  • パイプライン処理
  • 外部パッケージへの依存なし

などに対応しています。

特に、Redisのコマンドを柔軟に直接送信したいDartアプリケーションとの相性が良いパッケージです。

キャッシュ、データストア、Pub/SubなどでRedisを利用するDartプロジェクトで、シンプルなRedisクライアントを探している場合に候補となるパッケージです。

タイトルとURLをコピーしました