Firestore:写入一致性与恢复
验证:静态检查 · 核验于 2026-09-28
- 适用版本
- 云环境版本未实测;适用条件见正文
- 验证范围
- 已检查文档构建、链接与脚本语法;未执行云端实验。
文档数据库;Console:Firestore → 目标 Database。
推荐做法#
- 先确定 Database ID 和访问模型:服务端 SDK / Google OAuth 用 IAM;Firebase Web/mobile 客户端用 Firebase Authentication + Security Rules。
- 并发写使用事务或 updateTime 前置条件;事务重试代码避免外部副作用。
- 数据库恢复到独立目标后核对内容、索引和 IAM;持续写入需设计一致恢复点。
常用操作#
替换为实际资源;本段不依赖 lab.env。
PROJECT_ID='REPLACE_PROJECT_ID'
DATABASE='REPLACE_DATABASE'
gcloud firestore databases describe \
--project="$PROJECT_ID" --database="$DATABASE"容易踩的坑#
- 重复 PATCH 同一 ID 不新增文档,但不代表并发更新安全。
- 普通 export 不是严格的起始时间快照,也不包含全部索引和 IAM 配置。
配置说明与完整示例(按需展开)
与 DynamoDB 部分场景相近,但查询、索引、事务和客户端安全模型不同。适合文档型应用状态,不是 PostgreSQL 的透明替代品。
前置:环境篇,需要创建数据库权限及数据读写权限;仅做数据操作的服务端身份通常使用 roles/datastore.user。使用独立命名数据库,不占用已有 (default)。
source lab.env
gcloud services enable firestore.googleapis.com
gcloud firestore databases create --database=lab-docs --location="$REGION" --type=firestore-native
export DOC_URL="https://firestore.googleapis.com/v1/projects/$PROJECT_ID/databases/lab-docs/documents/orders/demo-1"
TOKEN=$(gcloud auth print-access-token)
curl --fail-with-body -X PATCH "$DOC_URL" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"fields":{"status":{"stringValue":"new"},"amount":{"integerValue":"1990"}}}'
curl --fail-with-body -H "Authorization: Bearer $TOKEN" "$DOC_URL"
curl --fail-with-body -X PATCH "$DOC_URL?updateMask.fieldPaths=status" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"fields":{"status":{"stringValue":"processed"}}}'
curl --fail-with-body -H "Authorization: Bearer $TOKEN" "$DOC_URL"
unset TOKEN验收:status 变更,amount 仍为 1990;更新字段时使用 update mask。重复 PATCH 固定文档路径不会像每次创建随机 ID 那样新增文档,但并发写仍需事务或 updateTime 前置条件。REST API
服务端 SDK / Google OAuth 调用按 IAM 授权,不受 Firebase Security Rules 同样约束。 Web/mobile 客户端的 Rules 不能充当服务端租户隔离;服务端仍需应用授权。访问控制
并发更新:防止状态被旧请求覆盖#
实际订单会收到重复/乱序更新。下面使用 updateTime 做乐观并发控制:两个操作者读到同一版本,只有第一个更新可成功。需要 Python 3、短期 gcloud 登录;无第三方依赖,紧接上面的 demo-1 执行。
cat > firestore-conflict.py <<'PY'
import json, os, subprocess, urllib.request, urllib.parse, urllib.error
url = os.environ['DOC_URL']
token = subprocess.check_output(['gcloud','auth','print-access-token'], text=True).strip()
def call(method, target, body=None):
req = urllib.request.Request(target, method=method, data=None if body is None else json.dumps(body).encode(), headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'})
try:
with urllib.request.urlopen(req, timeout=15) as r: return r.status, json.load(r)
except urllib.error.HTTPError as e: return e.code, json.load(e)
status, before = call('GET', url)
assert status == 200, before
query = urllib.parse.urlencode({'updateMask.fieldPaths':'status','currentDocument.updateTime':before['updateTime']})
status, first = call('PATCH', url+'?'+query, {'fields':{'status':{'stringValue':'shipped'}}})
assert status == 200, first
status, stale = call('PATCH', url+'?'+query, {'fields':{'status':{'stringValue':'cancelled'}}})
assert status >= 400 and stale['error']['status'] == 'FAILED_PRECONDITION', stale
status, after = call('GET', url)
assert status == 200 and after['fields']['status']['stringValue'] == 'shipped'
assert after['fields']['amount'] == before['fields']['amount']
print('PASS: stale write rejected; status and amount preserved')
PY
python3 firestore-conflict.py失败后重新读取并执行业务状态机判断,不能无限重试把取消覆盖成发货。涉及多个文档的一致更新需事务;事务重试函数里不要发邮件/扣款。订单使用随机 ID,避免以顺序 ID 或单个全局计数器制造写热点。更新前置条件
恢复演练:导出后恢复到独立数据库#
使用同项目、同区域的专用桶。操作者需 Import Export Admin 及桶管理权限;Firestore service agent 需访问该桶。先停止本实验写入,记录 demo-1 的 status=shipped、amount=1990。导出涉及按文档计费,不包含索引定义及 IAM 配置。
export BACKUP_BUCKET="${PROJECT_ID}-firestore-lab-backup"
export EXPORT_PREFIX="gs://$BACKUP_BUCKET/run-$(date -u +%Y%m%dT%H%M%SZ)"
gcloud storage buckets create "gs://$BACKUP_BUCKET" --location="$REGION" --uniform-bucket-level-access --public-access-prevention
gcloud firestore export "$EXPORT_PREFIX" --database=lab-docs --collection-ids=orders
gcloud firestore databases create --database=lab-docs-restore --location="$REGION" --type=firestore-native
gcloud firestore import "$EXPORT_PREFIX" --database=lab-docs-restore --collection-ids=orders
curl --fail-with-body -H "Authorization: Bearer $(gcloud auth print-access-token)" "https://firestore.googleapis.com/v1/projects/$PROJECT_ID/databases/lab-docs-restore/documents/orders/demo-1"验收:恢复库中的状态、金额与原库一致;原库未被修改。记录导出/导入耗时,作为恢复时间目标的实测依据。普通导出不是严格的起始时刻快照;持续写入业务需要独立设计 PITR(Point-In-Time Recovery,时间点恢复)与一致性验证。导出与恢复边界
清理#
TOKEN=$(gcloud auth print-access-token)
curl --fail-with-body -X DELETE -H "Authorization: Bearer $TOKEN" "$DOC_URL"
unset TOKEN
gcloud firestore databases delete --database=lab-docs
gcloud firestore databases delete --database=lab-docs-restore
# 仅本实验专用桶;先确认不需要保留恢复证据。
gcloud storage rm --recursive "gs://$BACKUP_BUCKET"
rm firestore-conflict.py删除整个命名数据库前确认没有其他应用使用;若开启删除保护,先审查再解除。数据库管理