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

Android中如何异步执行Firestore多批次写入并实现失败全回滚?

Firestore Multi-Batch Writes: Async Execution with Atomic Rollback

Great question! Let's start by clearing up a key misunderstanding and then walk through practical solutions that fit your requirements.

First: The Critical Limitation of Your Current Code

Your existing implementation submits batches sequentially (waiting for one to finish before starting the next), which is slow. But even bigger issue: it doesn't actually roll back already completed batches if one fails. Firestore WriteBatch operations are independent atomic transactions—once a batch commits successfully, its changes are permanent and can't be undone. Your code only stops future batches, but data from successful batches stays in the database.

To get true "all-or-nothing" behavior (one failure = everything rolls back), we need to work around Firestore's lack of cross-batch atomicity. Here are your best options:


Option 1: Optimized Sequential Submission (Faster Code, No Rollback)

If you don't need strict atomicity (just want faster, cleaner sequential execution), we can use Google's Tasks API to simplify the chain of batch commits. This keeps the sequential flow but is more readable and maintainable than recursion.

btn_batch.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        CollectionReference facultyRef = db.collection("Faculty");
        List<WriteBatch> writeBatches = new ArrayList<>();
        WriteBatch currentBatch = db.batch();
        int counter = 0;

        // Prepare all batches (max 499 operations per batch)
        for(int i=1; i<=10000; i++){
            String username = "user"+i;
            String password = UUID.randomUUID().toString().substring(0, 7);
            User user = new User(username, password);
            currentBatch.set(facultyRef.document(), user);
            counter++;

            if(counter == 499){
                writeBatches.add(currentBatch);
                currentBatch = db.batch();
                counter = 0;
            }
        }
        // Add the final partial batch if needed
        if(counter > 0){
            writeBatches.add(currentBatch);
        }

        // Execute batches sequentially
        commitBatchesSequentially(writeBatches)
                .addOnCompleteListener(task -> {
                    if(task.isSuccessful()){
                        Toast.makeText(MainActivity.this, "Batched Write Success", Toast.LENGTH_SHORT).show();
                    }else{
                        Toast.makeText(MainActivity.this, task.getException().getMessage(), Toast.LENGTH_SHORT).show();
                    }
                });
    }
});

private Task<Void> commitBatchesSequentially(List<WriteBatch> batches) {
    Task<Void> chain = Tasks.forResult(null);
    for (WriteBatch batch : batches) {
        chain = chain.continueWithTask(ignored -> batch.commit());
    }
    return chain;
}

Note: This still doesn't roll back completed batches if one fails—it just stops future ones. Use this only if partial success is acceptable.


Option 2: Status Marker + Compensation Rollback (Near-Atomic All-or-Nothing)

For true "all-or-nothing" behavior with large datasets, we need to implement a two-phase approach:

  1. Track the batch job with a status document
  2. Submit all batches in parallel
  3. If all succeed, mark the job as complete
  4. If any fail, delete all created documents to "roll back"

This isn't 100% atomic (there's a tiny window where partial data exists), but it works for most real-world use cases.

btn_batch.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        CollectionReference facultyRef = db.collection("Faculty");
        // Create a unique status document to track the batch job
        DocumentReference batchStatusDoc = db.collection("BatchJobs").document(UUID.randomUUID().toString());
        List<WriteBatch> writeBatches = new ArrayList<>();
        List<DocumentReference> createdDocs = new ArrayList<>();
        WriteBatch currentBatch = db.batch();
        int counter = 0;

        // Step 1: Prepare batches and track all new document references
        for(int i=1; i<=10000; i++){
            DocumentReference newUserDoc = facultyRef.document();
            createdDocs.add(newUserDoc);
            
            String username = "user"+i;
            String password = UUID.randomUUID().toString().substring(0, 7);
            User user = new User(username, password);
            currentBatch.set(newUserDoc, user);
            counter++;

            if(counter == 499){
                writeBatches.add(currentBatch);
                currentBatch = db.batch();
                counter = 0;
            }
        }
        if(counter > 0){
            writeBatches.add(currentBatch);
        }

        // Step 2: Initialize job status as "in progress"
        batchStatusDoc.set(new BatchJobStatus("IN_PROGRESS"))
                .addOnSuccessListener(aVoid -> {
                    // Step 3: Submit all batches in parallel
                    List<Task<Void>> batchTasks = new ArrayList<>();
                    for (WriteBatch batch : writeBatches) {
                        batchTasks.add(batch.commit());
                    }

                    // Step 4: Wait for all batches to finish
                    Tasks.whenAll(batchTasks)
                            .addOnSuccessListener(ignored -> {
                                // All batches succeeded: mark job as complete
                                batchStatusDoc.update("status", "COMPLETED")
                                        .addOnSuccessListener(__ -> {
                                            Toast.makeText(MainActivity.this, "All Batches Completed Successfully!", Toast.LENGTH_SHORT).show();
                                        });
                            })
                            .addOnFailureListener(e -> {
                                // Batch failure: roll back all created documents
                                rollbackCreatedDocuments(createdDocs)
                                        .addOnCompleteListener(rollbackTask -> {
                                            String statusMsg = rollbackTask.isSuccessful() 
                                                ? "Batch failed, rolled back all changes" 
                                                : "Batch failed, partial rollback: " + rollbackTask.getException().getMessage();
                                            // Update status with failure details
                                            batchStatusDoc.update(
                                                    "status", "FAILED",
                                                    "errorMessage", e.getMessage()
                                            ).addOnSuccessListener(__ -> {
                                                Toast.makeText(MainActivity.this, statusMsg, Toast.LENGTH_LONG).show();
                                            });
                                        });
                            });
                })
                .addOnFailureListener(e -> {
                    Toast.makeText(MainActivity.this, "Failed to start batch job: " + e.getMessage(), Toast.LENGTH_SHORT).show();
                });
    }
});

// Helper method to roll back created documents
private Task<Void> rollbackCreatedDocuments(List<DocumentReference> docs) {
    List<Task<Void>> rollbackTasks = new ArrayList<>();
    WriteBatch rollbackBatch = db.batch();
    int counter = 0;

    for (DocumentReference doc : docs) {
        rollbackBatch.delete(doc);
        counter++;
        if (counter == 499) {
            rollbackTasks.add(rollbackBatch.commit());
            rollbackBatch = db.batch();
            counter = 0;
        }
    }
    if (counter > 0) {
        rollbackTasks.add(rollbackBatch.commit());
    }

    return Tasks.whenAll(rollbackTasks);
}

// Helper class for batch job status
public static class BatchJobStatus {
    public String status;
    public String errorMessage;

    // Required empty constructor for Firestore serialization
    public BatchJobStatus() {}

    public BatchJobStatus(String status) {
        this.status = status;
    }
}

Option 3: Firestore Transaction (Strict Atomicity for Small Datasets)

If your total number of write operations is ≤500, use a single Firestore Transaction. Transactions are strictly atomic—either all operations succeed, or none do, with automatic rollback.

btn_batch.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        CollectionReference facultyRef = db.collection("Faculty");

        db.runTransaction(transaction -> {
            // Limit: max 500 operations per transaction
            for(int i=1; i<=500; i++){
                String username = "user"+i;
                String password = UUID.randomUUID().toString().substring(0, 7);
                User user = new User(username, password);
                transaction.set(facultyRef.document(), user);
            }
            return null;
        })
        .addOnSuccessListener(aVoid -> {
            Toast.makeText(MainActivity.this, "Transaction Completed Successfully!", Toast.LENGTH_SHORT).show();
        })
        .addOnFailureListener(e -> {
            Toast.makeText(MainActivity.this, "Transaction Failed: " + e.getMessage(), Toast.LENGTH_SHORT).show();
        });
    }
});

Final Recommendations

  • Small datasets (<=500 operations): Use Option 3 (Transaction) for strict atomicity and simplicity.
  • Large datasets + all-or-nothing requirement: Use Option 2 (Status Marker + Rollback) — it's the closest you can get to atomicity with Firestore's current limitations.
  • Partial success acceptable: Use Option 1 (Optimized Sequential) or even parallel batch submission for faster execution.

内容的提问来源于stack exchange,提问作者Vince Ybañez

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 15:42:37