Nếu bộ sưu tập chứa các tài liệu có giá trị được lập chỉ mục tuần tự, Cloud Firestore giới hạn tốc độ ghi ở mức 500 lần ghi mỗi giây. Trang này mô tả cách phân chia trường tài liệu để vượt qua giới hạn này. Trước tiên, hãy xác định ý nghĩa của "các trường được lập chỉ mục tuần tự" và làm rõ khi nào giới hạn này được áp dụng.
Các trường được lập chỉ mục tuần tự
"Các trường được lập chỉ mục tuần tự" nghĩa là bất kỳ tập hợp tài liệu nào chứa trường được lập chỉ mục tăng hoặc giảm đơn điệu. Trong nhiều trường hợp, điều này có nghĩa là trường timestamp
, nhưng bất kỳ giá trị trường tăng hoặc giảm đơn điệu nào cũng có thể kích hoạt giới hạn ghi là 500 lần ghi mỗi giây.
Ví dụ: giới hạn áp dụng cho tập hợp tài liệu user
có userid
trường được lập chỉ mục nếu ứng dụng chỉ định các giá trị userid
như sau:
-
1281, 1282, 1283, 1284, 1285, ...
Mặt khác, không phải tất cả các trường timestamp
đều kích hoạt giới hạn này. Nếu trường timestamp
theo dõi các giá trị được phân phối ngẫu nhiên thì giới hạn ghi sẽ không được áp dụng. Giá trị thực tế của trường cũng không thành vấn đề, chỉ có điều trường tăng hoặc giảm đơn điệu. Ví dụ: cả hai tập hợp giá trị trường tăng đều đều sau đây đều kích hoạt giới hạn ghi:
-
100000, 100001, 100002, 100003, ...
-
0, 1, 2, 3, ...
Phân chia trường dấu thời gian
Giả sử ứng dụng của bạn sử dụng trường timestamp
tăng dần đều. Nếu ứng dụng của bạn không sử dụng trường timestamp
trong bất kỳ truy vấn nào, bạn có thể xóa giới hạn 500 lần ghi mỗi giây bằng cách không lập chỉ mục trường dấu thời gian. Nếu bạn thực sự yêu cầu trường timestamp
cho các truy vấn của mình, bạn có thể vượt qua giới hạn bằng cách sử dụng dấu thời gian được chia nhỏ :
- Thêm trường
shard
cùng với trườngtimestamp
. Sử dụng các giá trị riêng biệt1..n
cho trườngshard
. Điều này tăng giới hạn ghi cho bộ sưu tập lên500*n
, nhưng bạn phải tổng hợpn
truy vấn. - Cập nhật logic ghi của bạn để gán ngẫu nhiên giá trị
shard
cho mỗi tài liệu. - Cập nhật các truy vấn của bạn để tổng hợp các tập hợp kết quả được phân chia.
- Tắt chỉ mục một trường cho cả trường
shard
và trườngtimestamp
. Xóa các chỉ mục tổng hợp hiện có có chứa trườngtimestamp
. - Tạo các chỉ mục tổng hợp mới để hỗ trợ các truy vấn cập nhật của bạn. Thứ tự của các trường trong chỉ mục rất quan trọng và trường
shard
đoạn phải đứng trước trườngtimestamp
. Bất kỳ chỉ mục nào bao gồm trườngtimestamp
cũng phải bao gồm trườngshard
.
Bạn chỉ nên triển khai dấu thời gian được phân chia trong các trường hợp sử dụng có tốc độ ghi liên tục trên 500 lần ghi mỗi giây. Nếu không thì đây là một sự tối ưu hóa chưa hoàn thiện. Việc phân chia trường timestamp
sẽ loại bỏ hạn chế 500 lần ghi mỗi giây nhưng phải đánh đổi việc cần tổng hợp truy vấn phía máy khách.
Các ví dụ sau đây cho thấy cách phân chia trường timestamp
và cách truy vấn tập kết quả được phân chia.
Mô hình dữ liệu mẫu và truy vấn
Ví dụ: hãy tưởng tượng một ứng dụng để phân tích các công cụ tài chính gần như theo thời gian thực như tiền tệ, cổ phiếu phổ thông và quỹ ETF. Ứng dụng này ghi tài liệu vào bộ sưu tập instruments
như sau:
Node.js
async function insertData() { const instruments = [ { symbol: 'AAA', price: { currency: 'USD', micros: 34790000 }, exchange: 'EXCHG1', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.010Z')) }, { symbol: 'BBB', price: { currency: 'JPY', micros: 64272000000 }, exchange: 'EXCHG2', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.101Z')) }, { symbol: 'Index1 ETF', price: { currency: 'USD', micros: 473000000 }, exchange: 'EXCHG1', instrumentType: 'etf', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.001Z')) } ]; const batch = fs.batch(); for (const inst of instruments) { const ref = fs.collection('instruments').doc(); batch.set(ref, inst); } await batch.commit(); }
Ứng dụng này chạy các truy vấn và đơn đặt hàng sau theo trường timestamp
:
Node.js
function createQuery(fieldName, fieldOperator, fieldValue, limit = 5) { return fs.collection('instruments') .where(fieldName, fieldOperator, fieldValue) .orderBy('timestamp', 'desc') .limit(limit) .get(); } function queryCommonStock() { return createQuery('instrumentType', '==', 'commonstock'); } function queryExchange1Instruments() { return createQuery('exchange', '==', 'EXCHG1'); } function queryUSDInstruments() { return createQuery('price.currency', '==', 'USD'); }
insertData() .then(() => { const commonStock = queryCommonStock() .then( (docs) => { console.log('--- queryCommonStock: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const exchange1Instruments = queryExchange1Instruments() .then( (docs) => { console.log('--- queryExchange1Instruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const usdInstruments = queryUSDInstruments() .then( (docs) => { console.log('--- queryUSDInstruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); return Promise.all([commonStock, exchange1Instruments, usdInstruments]); });
Sau một số nghiên cứu, bạn xác định rằng ứng dụng sẽ nhận được từ 1.000 đến 1.500 cập nhật công cụ mỗi giây. Con số này vượt quá 500 lần ghi mỗi giây được phép đối với các bộ sưu tập chứa tài liệu có trường dấu thời gian được lập chỉ mục. Để tăng thông lượng ghi, bạn cần 3 giá trị phân đoạn, MAX_INSTRUMENT_UPDATES/500 = 3
. Ví dụ này sử dụng các giá trị phân đoạn x
, y
và z
. Bạn cũng có thể sử dụng số hoặc ký tự khác cho giá trị phân đoạn của mình.
Thêm trường phân đoạn
Thêm trường shard
vào tài liệu của bạn. Đặt trường shard
thành các giá trị x
, y
hoặc z
để tăng giới hạn ghi trên bộ sưu tập lên 1.500 lần ghi mỗi giây.
Node.js
// Define our 'K' shard values const shards = ['x', 'y', 'z']; // Define a function to help 'chunk' our shards for use in queries. // When using the 'in' query filter there is a max number of values that can be // included in the value. If our number of shards is higher than that limit // break down the shards into the fewest possible number of chunks. function shardChunks() { const chunks = []; let start = 0; while (start < shards.length) { const elements = Math.min(MAX_IN_VALUES, shards.length - start); const end = start + elements; chunks.push(shards.slice(start, end)); start = end; } return chunks; } // Add a convenience function to select a random shard function randomShard() { return shards[Math.floor(Math.random() * Math.floor(shards.length))]; }
async function insertData() { const instruments = [ { shard: randomShard(), // add the new shard field to the document symbol: 'AAA', price: { currency: 'USD', micros: 34790000 }, exchange: 'EXCHG1', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.010Z')) }, { shard: randomShard(), // add the new shard field to the document symbol: 'BBB', price: { currency: 'JPY', micros: 64272000000 }, exchange: 'EXCHG2', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.101Z')) }, { shard: randomShard(), // add the new shard field to the document symbol: 'Index1 ETF', price: { currency: 'USD', micros: 473000000 }, exchange: 'EXCHG1', instrumentType: 'etf', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.001Z')) } ]; const batch = fs.batch(); for (const inst of instruments) { const ref = fs.collection('instruments').doc(); batch.set(ref, inst); } await batch.commit(); }
Truy vấn dấu thời gian được phân chia
Việc thêm trường shard
yêu cầu bạn cập nhật các truy vấn của mình để tổng hợp các kết quả được phân đoạn:
Node.js
function createQuery(fieldName, fieldOperator, fieldValue, limit = 5) { // For each shard value, map it to a new query which adds an additional // where clause specifying the shard value. return Promise.all(shardChunks().map(shardChunk => { return fs.collection('instruments') .where('shard', 'in', shardChunk) // new shard condition .where(fieldName, fieldOperator, fieldValue) .orderBy('timestamp', 'desc') .limit(limit) .get(); })) // Now that we have a promise of multiple possible query results, we need // to merge the results from all of the queries into a single result set. .then((snapshots) => { // Create a new container for 'all' results const docs = []; snapshots.forEach((querySnapshot) => { querySnapshot.forEach((doc) => { // append each document to the new all container docs.push(doc); }); }); if (snapshots.length === 1) { // if only a single query was returned skip manual sorting as it is // taken care of by the backend. return docs; } else { // When multiple query results are returned we need to sort the // results after they have been concatenated. // // since we're wanting the `limit` newest values, sort the array // descending and take the first `limit` values. By returning negated // values we can easily get a descending value. docs.sort((a, b) => { const aT = a.data().timestamp; const bT = b.data().timestamp; const secondsDiff = aT.seconds - bT.seconds; if (secondsDiff === 0) { return -(aT.nanoseconds - bT.nanoseconds); } else { return -secondsDiff; } }); return docs.slice(0, limit); } }); } function queryCommonStock() { return createQuery('instrumentType', '==', 'commonstock'); } function queryExchange1Instruments() { return createQuery('exchange', '==', 'EXCHG1'); } function queryUSDInstruments() { return createQuery('price.currency', '==', 'USD'); }
insertData() .then(() => { const commonStock = queryCommonStock() .then( (docs) => { console.log('--- queryCommonStock: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const exchange1Instruments = queryExchange1Instruments() .then( (docs) => { console.log('--- queryExchange1Instruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const usdInstruments = queryUSDInstruments() .then( (docs) => { console.log('--- queryUSDInstruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); return Promise.all([commonStock, exchange1Instruments, usdInstruments]); });
Cập nhật định nghĩa chỉ mục
Để loại bỏ giới hạn 500 lần ghi mỗi giây, hãy xóa các chỉ mục trường đơn và tổng hợp hiện có sử dụng trường timestamp
.
Xóa định nghĩa chỉ mục tổng hợp
Bảng điều khiển Firebase
Mở trang Chỉ mục tổng hợp Cloud Firestore trong bảng điều khiển Firebase.
Đối với mỗi chỉ mục chứa trường
timestamp
, hãy nhấp vào nút và nhấp vào Xóa .
Bảng điều khiển GCP
Trong Google Cloud Platform Console, hãy truy cập trang Cơ sở dữ liệu .
Chọn cơ sở dữ liệu cần thiết từ danh sách cơ sở dữ liệu.
Trong menu điều hướng, nhấp vào Chỉ mục , sau đó nhấp vào tab Tổng hợp .
Sử dụng trường Bộ lọc để tìm kiếm định nghĩa chỉ mục có chứa trường
timestamp
.Đối với mỗi chỉ mục này, hãy nhấp vào nút
và nhấp vào Xóa .
Firebase CLI
- Nếu bạn chưa thiết lập Firebase CLI, hãy làm theo hướng dẫn sau để cài đặt CLI và chạy lệnh
firebase init
. Trong lệnhinit
, đảm bảo chọnFirestore: Deploy rules and create indexes for Firestore
. - Trong quá trình thiết lập, Firebase CLI tải các định nghĩa chỉ mục hiện có của bạn xuống một tệp có tên mặc định là
firestore.indexes.json
. Xóa mọi định nghĩa chỉ mục có chứa trường
timestamp
, ví dụ:{ "indexes": [ // Delete composite index definition that contain the timestamp field { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "exchange", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "instrumentType", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "price.currency", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, ] }
Triển khai các định nghĩa chỉ mục được cập nhật của bạn:
firebase deploy --only firestore:indexes
Cập nhật định nghĩa chỉ mục một trường
Bảng điều khiển Firebase
Mở trang Chỉ mục trường đơn của Cloud Firestore trong bảng điều khiển Firebase.
Nhấp vào Thêm miễn trừ .
Đối với ID Bộ sưu tập , hãy nhập
instruments
. Đối với Đường dẫn trường , hãy nhậptimestamp
.Trong phạm vi Truy vấn , chọn cả nhóm Bộ sưu tập và Bộ sưu tập.
Bấm tiếp
Chuyển tất cả cài đặt chỉ mục thành Tắt . Nhấp vào để lưu .
Lặp lại các bước tương tự cho trường
shard
.
Bảng điều khiển GCP
Trong Google Cloud Platform Console, hãy truy cập trang Cơ sở dữ liệu .
Chọn cơ sở dữ liệu cần thiết từ danh sách cơ sở dữ liệu.
Trong menu điều hướng, hãy bấm vào Chỉ mục , rồi bấm vào tab Trường Đơn .
Bấm vào tab Trường đơn .
Nhấp vào Thêm miễn trừ .
Đối với ID Bộ sưu tập , hãy nhập
instruments
. Đối với Đường dẫn trường , hãy nhậptimestamp
.Trong phạm vi Truy vấn , chọn cả nhóm Bộ sưu tập và Bộ sưu tập.
Bấm tiếp
Chuyển tất cả cài đặt chỉ mục thành Tắt . Nhấp vào để lưu .
Lặp lại các bước tương tự cho trường
shard
.
Firebase CLI
Thêm phần sau vào phần
fieldOverrides
của tệp định nghĩa chỉ mục của bạn:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Triển khai các định nghĩa chỉ mục được cập nhật của bạn:
firebase deploy --only firestore:indexes
Tạo chỉ mục tổng hợp mới
Sau khi xóa tất cả các chỉ mục trước đó có chứa timestamp
, hãy xác định các chỉ mục mới mà ứng dụng của bạn yêu cầu. Bất kỳ chỉ mục nào chứa trường timestamp
cũng phải chứa trường shard
. Ví dụ: để hỗ trợ các truy vấn trên, hãy thêm các chỉ mục sau:
Bộ sưu tập | Các trường được lập chỉ mục | Phạm vi truy vấn |
---|---|---|
dụng cụ | mũi | , tăng.tiền tệ, thời gian giảm dầnBộ sưu tập |
dụng cụ | mũi | , đổi hướng lên, thời gian hướng xuốngBộ sưu tập |
dụng cụ | mũi | , tên_loại công cụ hướng lên, thời gian hướng xuốngBộ sưu tập |
Thông báo lỗi
Bạn có thể xây dựng các chỉ mục này bằng cách chạy các truy vấn được cập nhật.
Mỗi truy vấn trả về một thông báo lỗi kèm theo liên kết để tạo chỉ mục bắt buộc trong Bảng điều khiển Firebase.
Firebase CLI
Thêm các chỉ mục sau vào tệp định nghĩa chỉ mục của bạn:
{ "indexes": [ // New indexes for sharded timestamps { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "exchange", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "instrumentType", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "price.currency", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, ] }
Triển khai các định nghĩa chỉ mục được cập nhật của bạn:
firebase deploy --only firestore:indexes
Hiểu cách viết cho các trường được lập chỉ mục tuần tự giới hạn
Giới hạn về tốc độ ghi đối với các trường được lập chỉ mục tuần tự xuất phát từ cách Cloud Firestore lưu trữ các giá trị chỉ mục và tỷ lệ ghi chỉ mục. Đối với mỗi lần ghi chỉ mục, Cloud Firestore xác định mục nhập khóa-giá trị nối tên tài liệu và giá trị của từng trường được lập chỉ mục. Cloud Firestore tổ chức các mục chỉ mục này thành các nhóm dữ liệu được gọi là máy tính bảng . Mỗi máy chủ Cloud Firestore chứa một hoặc nhiều máy tính bảng. Khi tải ghi vào một máy tính bảng cụ thể trở nên quá cao, Cloud Firestore sẽ mở rộng theo chiều ngang bằng cách chia máy tính bảng thành các máy tính bảng nhỏ hơn và trải rộng các máy tính bảng mới trên các máy chủ Cloud Firestore khác nhau.
Cloud Firestore đặt các mục chỉ mục đóng theo từ điển trên cùng một máy tính bảng. Nếu các giá trị chỉ mục trong máy tính bảng quá gần nhau, chẳng hạn như đối với các trường dấu thời gian, Cloud Firestore không thể chia máy tính bảng thành các máy tính bảng nhỏ hơn một cách hiệu quả. Điều này tạo ra một điểm nóng trong đó một máy tính bảng nhận được quá nhiều lưu lượng truy cập và các hoạt động đọc và ghi vào điểm nóng trở nên chậm hơn.
Bằng cách phân chia trường dấu thời gian, bạn có thể giúp Cloud Firestore phân chia khối lượng công việc một cách hiệu quả trên nhiều máy tính bảng. Mặc dù các giá trị của trường dấu thời gian có thể vẫn gần nhau, nhưng giá trị chỉ mục và phân đoạn được nối sẽ cung cấp cho Cloud Firestore đủ không gian giữa các mục chỉ mục để phân chia các mục giữa nhiều máy tính bảng.
Cái gì tiếp theo
- Đọc các phương pháp hay nhất để thiết kế theo quy mô
- Đối với các trường hợp có tốc độ ghi cao vào một tài liệu, hãy xem Bộ đếm phân tán
- Xem giới hạn tiêu chuẩn cho Cloud Firestore