如何在Kotlin中实现Firestore集合文档的读取删除原子操作
问题描述
我有一个Firestore集合Queue,想在Kotlin里把读取文档+删除文档做成原子操作,确保其他用户没法在我删文档前读到它。目前的代码有竞态问题——先在事务外查文档,再开事务删,中间可能被其他用户读取或操作,不知道怎么用锁机制或Firebase安全规则实现原子性。
现有代码:
private val db: FirebaseFirestore = FirebaseFirestore.getInstance() private val collRef: CollectionReference = db.collection("Queue") runBlocking { val querySnapshot = collRef.limit(1).get() .addOnSuccessListener { querySnapshot -> if (!querySnapshot.isEmpty) { val documentSnapshot = querySnapshot.documents[0] if (documentSnapshot.data?.isNotEmpty()!!) { db.runTransaction { transaction -> val snapshot = transaction.get(documentSnapshot.reference) if (snapshot.data?.isNotEmpty() == true) { transaction.delete(documentSnapshot.reference) } null }.addOnSuccessListener { Utils.data["from"] = documentSnapshot.data!!["from"] Utils.data["to"] = documentSnapshot.data!!["to"] Utils.data["sender"] = documentSnapshot.data!!["sender"] Utils.data["time"] = documentSnapshot.data!!["time"] } .addOnFailureListener { e -> } } } } .addOnFailureListener { } }
解决方案:事务+安全规则双管齐下实现原子操作
1. 重写Kotlin代码:把全流程塞进事务里
原代码的核心问题是事务外先查文档,这就给其他用户留了空子——他们可能在你开事务前就把文档读走或删了。正确的做法是把查询、读取、删除全放进事务,靠Firestore事务的原子性保证只有一个用户能成功执行,其他用户的事务会自动重试或失败。
另外,加个claimedBy字段做“认领”机制,确保只有认领文档的用户能碰它,彻底堵死并发读取的漏洞:
private val db: FirebaseFirestore = FirebaseFirestore.getInstance() private val collRef: CollectionReference = db.collection("Queue") // 获取当前登录用户ID(如果业务不需要登录,可调整逻辑) private val currentUserId = FirebaseAuth.getInstance().currentUser?.uid fun processQueueDoc() { db.runTransaction { transaction -> // 1. 查询第一个未被认领的文档 val unclaimedQuery = collRef.whereEqualTo("claimedBy", null).limit(1) val querySnapshot = transaction.get(unclaimedQuery) if (querySnapshot.isEmpty) { // 没找到可处理的文档,直接终止事务 return@runTransaction null } val docRef = querySnapshot.documents[0].reference val snapshot = transaction.get(docRef) val docData = snapshot.data ?: return@runTransaction null // 2. 先把文档标记为当前用户认领 transaction.update(docRef, "claimedBy", currentUserId) // 3. 删除文档(如果业务需要保留文档,可跳过这步) transaction.delete(docRef) // 返回读取到的数据,后续在成功回调里处理 docData }.addOnSuccessListener { docData -> docData?.let { Utils.data["from"] = it["from"] Utils.data["to"] = it["to"] Utils.data["sender"] = it["sender"] Utils.data["time"] = it["time"] // 这里添加你的业务逻辑 } ?: run { // 没有可处理的文档,可做提示或重试逻辑 } }.addOnFailureListener { e -> // 事务失败,大概率是并发竞争导致,可根据需求重试 e.printStackTrace() } }
2. 配置Firebase安全规则:锁死文档权限
通过安全规则限制:
- 未被认领的文档可以被任何用户认领,但一旦被认领,只有认领者能读、能删
- 禁止其他用户读取正在处理的文档
在Firebase控制台的Firestore规则页面,把规则改成这样:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /Queue/{docId} { // 允许创建文档(根据业务需求调整,不需要可删除) allow create: if true; // 允许读取:要么文档没被认领,要么认领者是当前用户 allow read: if !exists(resource.data.claimedBy) || resource.data.claimedBy == request.auth.uid; // 允许认领:用户已登录,且文档还没被认领,只能标记为自己的ID allow update: if request.auth != null && !exists(resource.data.claimedBy) && request.resource.data.claimedBy == request.auth.uid; // 允许删除:要么文档没被认领,要么认领者是当前用户 allow delete: if !exists(resource.data.claimedBy) || resource.data.claimedBy == request.auth.uid; } } }
关键细节
- 事务原子性:Firestore会保证事务里的所有操作要么全成,要么全败。如果多个用户抢同一个文档,只有一个能成功,其他会自动重试(默认5次)。
- 认领机制:
claimedBy字段相当于临时锁,结合安全规则直接把其他用户的读取权限封死。 - 别在事务外查文档:所有涉及文档状态的操作都放进事务,彻底消除竞态条件。
内容的提问来源于stack exchange,提问作者Yonatan
相关产品推荐
相关产品推荐

