Flutter Firestoreで分散カウンタを実装する

June 27, 2019

分散カウンタを簡単なカウントアップアプリで試してみます。

分散カウンタ

多くのアプリでは、いいね数やフォロワー数のためにカウントアップが必要です。
しかし Firestore では、制限上 1 つのドキュメントにつき 1 秒に 1 回しか更新することができません。

そこで分散カウンタを利用します。

1 つのドキュメントにつき 1 秒に 1 回しかできないなら、ドキュメントをたくさん作ってしまえばいいのです。

具体的な方法

  1. カウントアップ用のコレクションを作成します。
  2. カウントアップ時はランダムにコレクション内のドキュメントを 1 つ選び、フィールドの値をインクリメントします。
  3. カウント取得時は、コレクション内のドキュメントの値を合計すれば OK です。

簡単なサンプル

Shards 数 100 で 100 回インクリメントしたところ、96 まで上がりました。
4 回ほど失敗しているようです。

Shards 数 500 でも試したら 100 まで到達しました。

スループットは Shards 数に比例しますが、
あまりに Shards 数を増やすとそれだけ Firestore のコストがかかるので難しいところです。

今回のコレクションとドキュメントの構成はこれです。

20190628005307

20190628005329

今回の実験コードはこちらです。

class _CounterPageState extends State<CounterPage> {
final _random = Random();
final _db = Firestore.instance;
// Shardsをすべて削除し、counter1に記載されたShards数で初期化
void _initFirestore() async {
// Shardsサブコレクションがすでに存在しているかチェック
QuerySnapshot shardsSnapshot = await _db
.collection('counters')
.document('counter1')
.collection('shards')
.getDocuments();
if (shardsSnapshot.documents[0].exists) {
// クライアントからのdelete()は非推奨
for (DocumentSnapshot document in shardsSnapshot.documents) {
document.reference.delete();
}
}
// 作成するShards数を取得
var numShardsSnapshot =
await _db.collection('counters').document('counter1').get();
var numShards = numShardsSnapshot['num_shards'];
// batchで一括初期化
var batch = _db.batch();
final counter1 = _db.collection('counters').document('counter1');
for (int i = 0; i < numShards; i++) {
batch.setData(
counter1.collection('shards').document(i.toString()), {'count': 0});
}
batch.commit();
}
// 分散カウンタ
// counter1のサブコレクションであるShardをランダムに1つ選択し、カウントアップ
void _incrementShard() async {
// Shardsの数を取得する
var numShardsSnapshot =
await _db.collection('counters').document('counter1').get();
var numShards = numShardsSnapshot['num_shards'];
// トランザクションでカウントアップ
_db.runTransaction((Transaction tx) async {
final sharedId = _random.nextInt(numShards);
final DocumentReference postRef =
_db.document('/counters/counter1/shards/' + sharedId.toString());
DocumentSnapshot postSnapshot = await tx.get(postRef);
if (postSnapshot.exists) {
await tx.update(
postRef, <String, int>{'count': postSnapshot.data['count'] + 1});
}
});
}
// Shardsサブコレクションに変更があったらShardsサブコレクションを合計値を再計算
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(20.0),
child: StreamBuilder<QuerySnapshot>(
stream: _db
.collection('counters')
.document('counter1')
.collection('shards')
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return new Text('Loading...');
default:
var count = 0;
for (DocumentSnapshot document in snapshot.data.documents) {
count += document['count'];
}
return Center(child: new Text(count.toString()));
}
},
),
),
Padding(
padding: const EdgeInsets.all(20.0),
child: RaisedButton(
child: Text('初期化'),
color: Colors.lightBlue,
onPressed: () {
_initFirestore();
},
),
),
],
),
floatingActionButton: FloatingActionButton(
// 1タップで100回インクリメントする
onPressed: () {
for (int i = 0; i < 100; i++) {
_incrementShard();
}
},
child: Icon(Icons.thumb_up),
backgroundColor: Colors.pink,
),
);
}
}
view raw Counter.dart hosted with ❤ by GitHub

参考にさせていただいたサイト

分散カウンタ  |  Firebase Documentation

Firebase Cloud Firestore の Transaction について考える - Qiita


Profile picture

Twitter GitHub