DartやFlutterでアプリケーションを開発していると、キャッシュやデータストア、Pub/SubなどでRedisを利用したいケースがあります。
Redisは高速なインメモリデータベースとして広く利用されていますが、DartからRedisを操作するにはRedisプロトコルに対応したクライアントが必要です。
今回紹介するredisは、DartからRedisサーバーへ接続して操作できるRedisクライアントです。
Redisプロトコルのパーサーとクライアントを提供しており、シンプルさとパフォーマンスを重視して設計されているのが特徴です。また、外部パッケージへの依存なしで動作します。
この記事では、redisの基本的な使い方から、データの保存・取得、トランザクション、CAS、Pub/Sub、TLS、バイナリデータなどについて解説します。
- redisとは?
- redisの特徴
- インストール
- Redisサーバーへ接続する
- SETでデータを保存する
- GETでデータを取得する
- INCRで数値を増加させる
- コマンドを順番に実行する
- コマンドのレスポンス
- Unicodeを扱う
- バイナリデータを扱う
- TLSでRedisへ接続する
- トランザクションを利用する
- CASを利用する
- CASで値を更新する
- Pub/Subを利用する
- チャンネルをSubscribeする
- Pub/Subのメッセージを受信する
- パターン購読を利用する
- Pub/Subの購読を解除する
- Pub/Subでメッセージを送信する
- パイプライン処理
- パフォーマンス
- 接続を閉じる
- キャッシュとして利用する
- Pub/Subを使った通知処理
- 対応プラットフォーム
- 外部パッケージへの依存がない
- 注意点
- まとめ
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);
nameにDartを保存していた場合、取得結果はDartになります。
INCRで数値を増加させる
RedisのINCRコマンドも利用できます。
Dart
await command.send_object([
"SET",
"count",
"0",
]);
final result = await command.send_object([
"INCR",
"count",
]);
print(result);
countが0から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);
この例では、SET、INCR、INCR、GETの順番で処理しています。
コマンドのレスポンス
RedisのレスポンスはDartの型へ変換されます。
| Redis | Dart |
|---|---|
| String | String |
| Integer | Integer |
| Array | List |
| Error | RedisError |
| Bulk | String / 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のトランザクションを利用する場合は、Commandのmulti()を使用します。
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には主に以下のメソッドがあります。
watchmultiAndExec
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クライアントを探している場合に候補となるパッケージです。
