You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 01:15:31