分散カウンタを簡単なカウントアップアプリで試してみます。
分散カウンタ
多くのアプリでは、いいね数やフォロワー数のためにカウントアップが必要です。
しかし Firestore では、制限上 1 つのドキュメントにつき 1 秒に 1 回しか更新することができません。
そこで分散カウンタを利用します。
1 つのドキュメントにつき 1 秒に 1 回しかできないなら、ドキュメントをたくさん作ってしまえばいいのです。
具体的な方法
- カウントアップ用のコレクションを作成します。
- カウントアップ時はランダムにコレクション内のドキュメントを 1 つ選び、フィールドの値をインクリメントします。
- カウント取得時は、コレクション内のドキュメントの値を合計すれば OK です。
簡単なサンプル
Shards 数 100 で 100 回インクリメントしたところ、96 まで上がりました。
4 回ほど失敗しているようです。
Shards 数 500 でも試したら 100 まで到達しました。
スループットは Shards 数に比例しますが、
あまりに Shards 数を増やすとそれだけ Firestore のコストがかかるので難しいところです。
今回のコレクションとドキュメントの構成はこれです。
今回の実験コードはこちらです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
), | |
); | |
} | |
} |