使用 React.js 和 Firebase 准确递增和分配订单号(电子商务)
Accurately Incrementing and Assigning Order Numbers (eCommerce) with React.js & Firebse
我一直在使用 React.js(框架)和 Firebase(托管、Firestore、函数和分布式计数器扩展)构建一个简单的在线商店。我正在努力使用简单的电子商务功能来为每个新订单递增和分配一个唯一的订单#。我以为我解决了一个问题,如果 2 个以上的订单连续放置得太快,后端将读取当前订单 # 然后为该订单设置新订单 #,但由于两个订单放置得太近,订单 # 给出每个订单都是一样的。分布式计数器似乎仍在工作,因为它总是正确地对 'placed' 计数器求和。我最近发现,当我在本地机器上的两个网络浏览器 windows 快速下两个订单时,这个问题仍然存在。
有没有人成功使用 Firebase Distributed Counter 扩展来递增然后为订单分配订单号?我的设置与他们使用的用于“访问”到“页面”的示例有点不同,主要只是在“放置”计数器上,而不是在我的“统计”订单文档上的每个“订单”上。
阅读的轻微延迟是不可能消除的吗?我觉得必须有一个变通办法!我在想也许可以将每个订单增加 10,然后在其中添加一个 0-10 之间的随机数,所以如果有重复,它们被随机分配到相同子编号的可能性很小(即订单 #44 和订单 #48 ),但这感觉像是一个简陋的修复,因为它们仍有可能相同。
相关:我目前在后端的计数器扩展中遇到此错误,正在与 GCP 合作进行修复,但我猜这是他们的问题:https://issuetracker.google.com/194948300
distributed_counter.js:
Node.js Admin Firebase sample I used
firebase.rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Orders collection
match /orders/{orderId} {
function isAdmin() {
return (
get(/databases/$(database)/documents/users/$(request.auth.uid)/readOnly/flags).data.isAdmin == true
)
}
allow create: if true;
allow update, read, list, delete: if isAdmin();
// Allow to increment only the 'placed' field and only by 1.
match /_counter_shards_/{shardId} {
allow get;
allow write: if request.resource.data.keys() == ["placed"]
&& (resource == null || request.resource.data.placed ==
resource.data.placed + 1);
}
}
}
}
FFunctions index.ts:
"use strict";
import functions = require('firebase-functions');
import admin = require("firebase-admin");
import { DocumentSnapshot } from '@google-cloud/firestore';
import { CHECK_STATUSES, genId, RESPONSE_TYPES, UPLOAD_TYPES } from './common';
admin.initializeApp(functions.config().firebase);
const request = require("request");
const Counter = require("./distributed_counter")
const Papa = require('papaparse');
const sgMail = require('@sendgrid/mail')
sgMail.setApiKey(functions.config().sendgrid_api.key)
...
export const onOrderCreated = functions.firestore.document('orders/{orderId}')
.onCreate(async (snap: DocumentSnapshot, context: functions.EventContext) => {
console.log("Order created with ID: " + context.params.orderId);
const newValue = snap.data();
if (newValue === null || newValue === undefined) {
return;
}
try {
const allPromises: Array<Promise<any>> = [];
// Need to grab the shop public data for email info mainly
let shopData: FirebaseFirestore.DocumentData | any = null;
await admin.firestore().collection('public').doc("shop").get().then((shopDoc) => {
if (shopDoc.exists) {
const docWithMore = Object.assign({}, shopDoc.data());
docWithMore.id = shopDoc.id;
shopData = docWithMore;
} else {
console.error("Shop doc doesn't exist!")
}
}).catch((error) => {
console.log("Error getting shop document:", error);
})
// First test to make sure the adminViolation passes!
if(context.params.orderId !== 'stats'){
// Increment orderNum
const statsRef = admin.firestore().collection("orders").doc("stats");
let orderNum = -1;
const orderCounter = new Counter(statsRef, "placed")
await orderCounter.incrementBy(1);
orderNum = await orderCounter.get();
console.log("This orderNum is #" + orderNum)
// Set stats doc with current orderNum
allPromises.push(
statsRef.set({
lastOrdered: Date.now()
}, {merge: true}).then(() => {
console.log("Set order stats doc lastOrdered date.")
})
);
// Set order number on order doc
allPromises.push(
admin.firestore().collection("orders").doc(context.params.orderId).update({
number: orderNum
}).then(() => {
console.log("Set processed flag to true and orderNum on order doc.")
})
);
// Send email to user to confirm we received
// Template email
const htmlEmail =
`
<div style="width: 100%; font-family: Arial, Helvetica, sans-serif">
email sent here...
</div>
`
// Pack It
const msg = {
to: newValue.user.email,
from: `noreply@webapp.shop`,
subject: `${shopData?.name} Order #${orderNum}`,
text: `We received your order #${orderNum} successfully for ${shopData?.name}.`,
html: htmlEmail,
}
// Send it
allPromises.push(
sgMail.send(msg)
.then(() => {
console.log('Order email sent')
})
.catch((error: any) => {
console.error(error)
})
)
// Set processed flag to true when finished
allPromises.push(
admin.firestore().collection("orders").doc(context.params.orderId).update({
processed: true
}).then(() => {
console.log("Set processed flag to true and orderNum on order doc.")
})
);
} else {
console.log("Stats doc was created OR adminViolation flagged!!")
}
return Promise.all(allPromises)
} catch(error) {
console.error("Error: " + error);
return;
}
});
计数器的主要问题是提到的限制 here:
In Cloud Firestore, you can only update a single document about once per second, which might be too low for some high-traffic applications.
还有here:
You should not update a single document more than once per second. If you update a document too quickly, then your application will experience contention, including higher latency, timeouts, and other errors.
有关最佳做法,请参阅 Cloud Functions best practices。
在此 documentation 上,它解释了如何使用分布式计数器解决此限制。
这是一个例子:
function getCount(ref) {
// Sum the count of each shard in the subcollection
return ref.collection('shards').get().then((snapshot) => {
let total_count = 0;
snapshot.forEach((doc) => {
total_count += doc.data().count;
});
return total_count;
});
}
文档还提到了使用解决方法的其他限制。
- Shard count - The number of shards controls the performance of the
distributed counter. With too few shards, some transactions may have
to retry before succeeding, which will slow writes. With too many
shards, reads become slower and more expensive. You can offset the
read-expense by keeping the counter total in a separate roll-up
document which is updated at a slower cadence (e.g. once per second),
and having clients read from that document to get the total. The
tradeoff is that clients will have to wait for the roll-up document to
be updated, instead of computing the total by reading all of the
shards immediately after any update.
- Cost - The cost of reading a
counter value increases linearly with the number of shards, because
the entire shards subcollection must be loaded.
您也可以参考此 guide 了解更多信息。
我发现这个问题的答案是采用类似于 Amazon 的方法,使用散列的订单 # 应该在时间范围内足够独特。我认为从理论上讲,碰撞很可能发生在大量商店中,以至于只能将它们增加 1。我找到了一个方便的库,所以我不必在这里重新发明轮子,请查看 order-id Github page.
我一直在使用 React.js(框架)和 Firebase(托管、Firestore、函数和分布式计数器扩展)构建一个简单的在线商店。我正在努力使用简单的电子商务功能来为每个新订单递增和分配一个唯一的订单#。我以为我解决了一个问题,如果 2 个以上的订单连续放置得太快,后端将读取当前订单 # 然后为该订单设置新订单 #,但由于两个订单放置得太近,订单 # 给出每个订单都是一样的。分布式计数器似乎仍在工作,因为它总是正确地对 'placed' 计数器求和。我最近发现,当我在本地机器上的两个网络浏览器 windows 快速下两个订单时,这个问题仍然存在。
有没有人成功使用 Firebase Distributed Counter 扩展来递增然后为订单分配订单号?我的设置与他们使用的用于“访问”到“页面”的示例有点不同,主要只是在“放置”计数器上,而不是在我的“统计”订单文档上的每个“订单”上。
阅读的轻微延迟是不可能消除的吗?我觉得必须有一个变通办法!我在想也许可以将每个订单增加 10,然后在其中添加一个 0-10 之间的随机数,所以如果有重复,它们被随机分配到相同子编号的可能性很小(即订单 #44 和订单 #48 ),但这感觉像是一个简陋的修复,因为它们仍有可能相同。
相关:我目前在后端的计数器扩展中遇到此错误,正在与 GCP 合作进行修复,但我猜这是他们的问题:https://issuetracker.google.com/194948300
distributed_counter.js: Node.js Admin Firebase sample I used
firebase.rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Orders collection
match /orders/{orderId} {
function isAdmin() {
return (
get(/databases/$(database)/documents/users/$(request.auth.uid)/readOnly/flags).data.isAdmin == true
)
}
allow create: if true;
allow update, read, list, delete: if isAdmin();
// Allow to increment only the 'placed' field and only by 1.
match /_counter_shards_/{shardId} {
allow get;
allow write: if request.resource.data.keys() == ["placed"]
&& (resource == null || request.resource.data.placed ==
resource.data.placed + 1);
}
}
}
}
FFunctions index.ts:
"use strict";
import functions = require('firebase-functions');
import admin = require("firebase-admin");
import { DocumentSnapshot } from '@google-cloud/firestore';
import { CHECK_STATUSES, genId, RESPONSE_TYPES, UPLOAD_TYPES } from './common';
admin.initializeApp(functions.config().firebase);
const request = require("request");
const Counter = require("./distributed_counter")
const Papa = require('papaparse');
const sgMail = require('@sendgrid/mail')
sgMail.setApiKey(functions.config().sendgrid_api.key)
...
export const onOrderCreated = functions.firestore.document('orders/{orderId}')
.onCreate(async (snap: DocumentSnapshot, context: functions.EventContext) => {
console.log("Order created with ID: " + context.params.orderId);
const newValue = snap.data();
if (newValue === null || newValue === undefined) {
return;
}
try {
const allPromises: Array<Promise<any>> = [];
// Need to grab the shop public data for email info mainly
let shopData: FirebaseFirestore.DocumentData | any = null;
await admin.firestore().collection('public').doc("shop").get().then((shopDoc) => {
if (shopDoc.exists) {
const docWithMore = Object.assign({}, shopDoc.data());
docWithMore.id = shopDoc.id;
shopData = docWithMore;
} else {
console.error("Shop doc doesn't exist!")
}
}).catch((error) => {
console.log("Error getting shop document:", error);
})
// First test to make sure the adminViolation passes!
if(context.params.orderId !== 'stats'){
// Increment orderNum
const statsRef = admin.firestore().collection("orders").doc("stats");
let orderNum = -1;
const orderCounter = new Counter(statsRef, "placed")
await orderCounter.incrementBy(1);
orderNum = await orderCounter.get();
console.log("This orderNum is #" + orderNum)
// Set stats doc with current orderNum
allPromises.push(
statsRef.set({
lastOrdered: Date.now()
}, {merge: true}).then(() => {
console.log("Set order stats doc lastOrdered date.")
})
);
// Set order number on order doc
allPromises.push(
admin.firestore().collection("orders").doc(context.params.orderId).update({
number: orderNum
}).then(() => {
console.log("Set processed flag to true and orderNum on order doc.")
})
);
// Send email to user to confirm we received
// Template email
const htmlEmail =
`
<div style="width: 100%; font-family: Arial, Helvetica, sans-serif">
email sent here...
</div>
`
// Pack It
const msg = {
to: newValue.user.email,
from: `noreply@webapp.shop`,
subject: `${shopData?.name} Order #${orderNum}`,
text: `We received your order #${orderNum} successfully for ${shopData?.name}.`,
html: htmlEmail,
}
// Send it
allPromises.push(
sgMail.send(msg)
.then(() => {
console.log('Order email sent')
})
.catch((error: any) => {
console.error(error)
})
)
// Set processed flag to true when finished
allPromises.push(
admin.firestore().collection("orders").doc(context.params.orderId).update({
processed: true
}).then(() => {
console.log("Set processed flag to true and orderNum on order doc.")
})
);
} else {
console.log("Stats doc was created OR adminViolation flagged!!")
}
return Promise.all(allPromises)
} catch(error) {
console.error("Error: " + error);
return;
}
});
计数器的主要问题是提到的限制 here:
In Cloud Firestore, you can only update a single document about once per second, which might be too low for some high-traffic applications.
还有here:
You should not update a single document more than once per second. If you update a document too quickly, then your application will experience contention, including higher latency, timeouts, and other errors.
有关最佳做法,请参阅 Cloud Functions best practices。
在此 documentation 上,它解释了如何使用分布式计数器解决此限制。 这是一个例子:
function getCount(ref) {
// Sum the count of each shard in the subcollection
return ref.collection('shards').get().then((snapshot) => {
let total_count = 0;
snapshot.forEach((doc) => {
total_count += doc.data().count;
});
return total_count;
});
}
文档还提到了使用解决方法的其他限制。
- Shard count - The number of shards controls the performance of the distributed counter. With too few shards, some transactions may have to retry before succeeding, which will slow writes. With too many shards, reads become slower and more expensive. You can offset the read-expense by keeping the counter total in a separate roll-up document which is updated at a slower cadence (e.g. once per second), and having clients read from that document to get the total. The tradeoff is that clients will have to wait for the roll-up document to be updated, instead of computing the total by reading all of the shards immediately after any update.
- Cost - The cost of reading a counter value increases linearly with the number of shards, because the entire shards subcollection must be loaded.
您也可以参考此 guide 了解更多信息。
我发现这个问题的答案是采用类似于 Amazon 的方法,使用散列的订单 # 应该在时间范围内足够独特。我认为从理论上讲,碰撞很可能发生在大量商店中,以至于只能将它们增加 1。我找到了一个方便的库,所以我不必在这里重新发明轮子,请查看 order-id Github page.