亚洲精品久久久中文字幕-亚洲精品久久片久久-亚洲精品久久青草-亚洲精品久久婷婷爱久久婷婷-亚洲精品久久午夜香蕉

您的位置:首頁技術(shù)文章
文章詳情頁

用 Node + MySQL 處理 100G 數(shù)據(jù)

瀏覽:32日期:2023-10-16 14:41:54

通過這個(gè) Node.js 和 MySQL 示例項(xiàng)目,我們將看看如何有效地處理 數(shù)十億行 占用 數(shù)百GB 存儲(chǔ)空間的數(shù)據(jù)。

本文的第二個(gè)目標(biāo)是幫助你確定 Node.js + MySQL 是否適合你的需求,并為實(shí)現(xiàn)此類解決方案提供幫助。

本文章使用的實(shí)際代碼 可以在 GitHub 上找到 。

為什么使用 Node.js 和 MySQL?

我們使用 MySQL 來存儲(chǔ)我們的 Node.js監(jiān)控和調(diào)試工具 用戶的分布式跟蹤數(shù)據(jù) Trace。

我們選擇了 MySQL,因?yàn)樵跊Q定的時(shí)候,Postgres 并不是很擅長更新行,而對于我們來說,更新不可變數(shù)據(jù)是不合理的。

大多數(shù)人認(rèn)為,如果有數(shù)百萬的數(shù)十億行,他們應(yīng)該使用一個(gè) NoSQL 解決方案,如 Cassandra 或 Mongo。

不幸的是,這些解決方案不 符合ACID ,當(dāng)數(shù)據(jù)一致性非常重要時(shí),這些解決方案就難以使用。

然而,通過良好的索引和適當(dāng)?shù)囊?guī)劃,MySQL 可以作為上面提到的 NoSQL 的一種替代方案,很適合這樣的任務(wù)。

MySQL 有幾個(gè)存儲(chǔ)引擎。 InnoDB 是默認(rèn)的,它功能最多。但是,應(yīng)該考慮到 InnoDB 表是不可變的,這意味著每個(gè) ALTER TABLE 語句都將所有的數(shù)據(jù)復(fù)制到一個(gè)新的表中。 當(dāng)需要遷移已經(jīng)存在的數(shù)據(jù)庫時(shí),這會(huì)更加糟糕。

如果你有名義值,每個(gè)都有很多關(guān)聯(lián)的數(shù)據(jù) —— 例如你的每個(gè)用戶都有數(shù)百萬個(gè)產(chǎn)品,并且你擁有大量用戶 —— 這可能是為每個(gè)用戶創(chuàng)建表格最簡單的方法,并給出如 <user_id>_<entity_name> 。 這樣可以顯著減少單個(gè)表的大小。

此外,在刪除帳戶的情況下,刪除用戶的數(shù)據(jù)是 O(1) 量級的操作。這是非常重要的,因?yàn)槿绻阈枰獜拇蟊碇袆h除大量的值,MySQL可能會(huì)決定使用錯(cuò)誤的索引或不使用索引。

因?yàn)椴荒苁褂盟饕崾?DELETE 會(huì)讓事情變得更復(fù)雜。你可能需要 ALTER 來刪除你的數(shù)據(jù),但這意味著將每行復(fù)制到新表。

為每個(gè)用戶創(chuàng)建表格顯然增加了復(fù)雜性,但是當(dāng)涉及到刪除具有大量相關(guān)數(shù)據(jù)的用戶或類似實(shí)體時(shí),這可能是一個(gè)有效的辦法。

但是,在進(jìn)行動(dòng)態(tài)創(chuàng)建表之前,你應(yīng)該嘗試刪除塊中的行,因?yàn)樗部赡苡袔椭梢詼p少附加復(fù)雜性。當(dāng)然,如果你的添加數(shù)據(jù)速度比你刪除的速度更快,你可能會(huì)感覺上述解決方案是個(gè)坑。

但是,如果你的表在分離用戶后仍然很大,導(dǎo)致你還需要?jiǎng)h除過期的行呢?你添加數(shù)據(jù)速度仍然比你刪除的速度更快。 在這種情況下,你應(yīng)該嘗試使用 MySQL 內(nèi)置的表分區(qū)。 當(dāng)你需要通過按順序或連續(xù)遞增的值(例如創(chuàng)建的時(shí)間戳)來切割表時(shí),它很方便。

MySQL 表分區(qū)

MySQL 中一個(gè)表的表分區(qū)將像多個(gè)表一樣工作,但你可以使用與之前相同的界面,不需要更多應(yīng)用程序的附加邏輯。這也意味著你可以像刪除表一樣刪除表分區(qū)。

這個(gè) 文檔 很好,但也很繁瑣(畢竟這不是一個(gè)簡單的話題),所以讓我們快速看一下如何創(chuàng)建一個(gè)表分區(qū)。

我們處理我們的分區(qū)的方式是從 Rick James 的文章中獲取的。他還深入探討了如何規(guī)劃你的數(shù)據(jù)表。

CREATE TABLE IF NOT EXISTS tbl ( id INTEGER NOT NULL AUTO_INCREMENT, data VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id, created_at) ) PARTITION BY RANGE (TO_DAYS(created_at)) ( start VALUES LESS THAN (0), from20170514 VALUES LESS THAN (TO_DAYS(’2017-05-15’)), from20170515 VALUES LESS THAN (TO_DAYS(’2017-05-16’)), from20170516 VALUES LESS THAN (TO_DAYS(’2017-05-17’)), future VALUES LESS THAN MAXVALUE );

PARTITION BY RANGE 之后才是我們關(guān)注的焦點(diǎn)。

在 MySQL 中,你可以通過 RANGE , LIST , COLUMN , HASH 和 KEY 進(jìn)行分區(qū),你可以在 文檔 中找到它們。請注意,分區(qū)鍵必須是主鍵或任何唯一的索引。

from<date> 開始的那些語句含義應(yīng)該是不言自明的。每個(gè)分區(qū)都保存 created_at 列小于第二天的值。這也意味著從 from20120414 保留所有在 2012-04-15 以前的數(shù)據(jù),所以這是執(zhí)行清理時(shí)我們將刪除的分區(qū)。

future 和 start 分區(qū)需要一些解釋: future 持有我們尚未定義日期的數(shù)據(jù)。如果我們不能及時(shí)重新分區(qū), 2017-05-17 以后的所有數(shù)據(jù)都將儲(chǔ)存在 future ,確保我們不會(huì)丟失任何數(shù)據(jù)。 start 也是一個(gè)安全網(wǎng)。我們期望所有行都有一個(gè) DATETIME 和 created_at 值,但是我們需要為可能的錯(cuò)誤做好準(zhǔn)備。如果由于某種原因,有一行最終會(huì)出現(xiàn) NULL ,那么它將在 start 分區(qū)中,這表示我們需要進(jìn)行 debug。

當(dāng)你使用分區(qū)時(shí),MySQL 將該數(shù)據(jù)保存在磁盤的不同部分,就像它們是獨(dú)立的表一樣,并根據(jù)分區(qū)鍵自動(dòng)組織數(shù)據(jù)。

要考慮到的一些限制:

不支持查詢緩存。 分區(qū)的 InnoDB 表不支持外鍵。 分區(qū)表不支持 FULLTEXT 索引或搜索。

還有 更多的限制 ,但是在 RisingStack 采用分區(qū)表之后,我們感觸最大的一個(gè)限制是。

如果要?jiǎng)?chuàng)建新分區(qū),則需要重新組織一個(gè)現(xiàn)有分區(qū),并將其分解以滿足你的需求:

ALTER TABLE tbl REORGANIZE PARTITION future INTO ( from20170517 VALUES LESS THAN (TO_DAYS(’2017-05-18’)), from20170518 VALUES LESS THAN (TO_DAYS(’2017-05-19’)), PARTITION future VALUES LESS THAN MAXVALUE );

刪除分區(qū)需要一個(gè) alter table,盡管它會(huì)讓你感覺你是在刪除一個(gè)表:

ALTER TABLE tbl DROP PARTITION from20170517, from20170518;

你可以看到,你必須在語句中包括分區(qū)的實(shí)際名稱和描述。 它們不能由 MySQL 動(dòng)態(tài)生成,所以你必須在應(yīng)用程序邏輯中處理它。這就是我們接下來的內(nèi)容。

Node.js 和 MySQL 的表分區(qū)示例

我們來看看實(shí)際的解決方案。對于這里的示例,我們將使用 knex ,它是為 JavaScript 而生的查詢構(gòu)建器。如果你熟悉 SQL,應(yīng)該對代碼感覺很熟悉。

首先,我們創(chuàng)建表:

const dedent = require(’dedent’) const _ = require(’lodash’) const moment = require(’moment’) const MAX_DATA_RETENTION = 7 const PARTITION_NAME_DATE_FORMAT = ’YYYYMMDD’ Table.create = function () { return knex.raw(dedent CREATE TABLE IF NOT EXISTS ${tableName} ( id INTEGER NOT NULL AUTO_INCREMENT, data VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id, created_at) ) PARTITION BY RANGE ( TO_DAYS(created_at)) ( PARTITION start VALUES LESS THAN (0), ${Table.getPartitionStrings()} PARTITION future VALUES LESS THAN MAXVALUE ); ) } Table.getPartitionStrings = function () { const days = _.range(MAX_DATA_RETENTION - 2, -2, -1) const partitions = days.map((day) => { const tomorrow = moment().subtract(day, ’day’).format(’YYYY-MM-DD’) const today = moment().subtract(day + 1, ’day’).format(PARTITION_NAME_DATE_FORMAT) return PARTITION from${today} VALUES LESS THAN (TO_DAYS(’${tomorrow}’)), }) return partitions.join(’n’) }

它實(shí)際上是我們前面看到的相同的語句,但是我們必須動(dòng)態(tài)地創(chuàng)建分區(qū)的名稱和描述。這就是為什么我們創(chuàng)建了 getPartitionStrings 方法。

第一行是:

const days = _.range(MAX_DATA_RETENTION - 2, -2, -1)

MAX_DATA_RETENTION - 2 = 5 創(chuàng)建從 5 到 -2(最后一個(gè)值排除)-> [ 5, 4, 3, 2, 1, 0, -1 ] 的序列,然后從當(dāng)前時(shí)間中減去這些值,并創(chuàng)建分區(qū)名稱的( today )及其限制( tomorrow )。順序是至關(guān)重要的,因?yàn)樵谡Z句中分區(qū)值不會(huì)增長時(shí) MySQL 會(huì)拋出錯(cuò)誤。

MySQL 和 Node.js 大規(guī)模數(shù)據(jù)刪除示例

現(xiàn)在我們來看一下數(shù)據(jù)刪除。你可以 在這里 看到整個(gè)代碼。

第一種方法, removeExpired 獲取當(dāng)前分區(qū)的列表,然后將其傳遞給 repartition 。

const _ = require(’lodash’) Table.removeExpired = function (dataRetention) { return Table.getPartitions() .then((currentPartitions) => Table.repartition(dataRetention, currentPartitions)) } Table.getPartitions = function () { return knex(’information_schema.partitions’) .select(knex.raw(’partition_name as name’), knex.raw(’partition_description as description’)) // description holds the day of partition in mysql days .where(’table_schema’, dbName) .andWhere(’partition_name’, ’not in’, [ ’start’, ’future’ ]) .then((partitions) => partitions.map((partition) => ({ name: partition.name, description: partition.description === ’MAX_VALUE’ ? ’MAX_VALUE’ : parseInt(partition.description) }))) } Table.repartition = function (dataRetention, currentPartitions) { const partitionsThatShouldExist = Table.getPartitionsThatShouldExist(dataRetention, currentPartitions) const partitionsToBeCreated = _.differenceWith(partitionsThatShouldExist, currentPartitions, (a, b) => a.description === b.description) const partitionsToBeDropped = _.differenceWith(currentPartitions, partitionsThatShouldExist, (a, b) => a.description === b.description) const statement = dedent ${Table.reorganizeFuturePartition(partitionsToBeCreated)} ${Table.dropOldPartitions(partitionsToBeDropped)} return knex.raw(statement) }

首先,我們從 MySQL 維護(hù)的 information_schema.partitions 表中選擇所有當(dāng)前存在的分區(qū)。

然后我們創(chuàng)建該表應(yīng)該存在的所有分區(qū)。如果 A 是存在的分區(qū)集合, B 是應(yīng)該存在的分區(qū)集合

partitionsToBeCreated = B A

partitionsToBeDropped = A B

getPartitionsThatShouldExist 創(chuàng)建集合 B

Table.getPartitionsThatShouldExist = function (dataRetention, currentPartitions) { const days = _.range(dataRetention - 2, -2, -1) const oldestPartition = Math.min(...currentPartitions.map((partition) => partition.description)) return days.map((day) => { const tomorrow = moment().subtract(day, ’day’) const today = moment().subtract(day + 1, ’day’) if (Table.getMysqlDay(today) < oldestPartition) { return null } return { name: from${today.format(PARTITION_NAME_DATE_FORMAT)}, description: Table.getMysqlDay(tomorrow) } }).filter((partition) => !!partition) } Table.getMysqlDay = function (momentDate) { return momentDate.diff(moment([ 0, 0, 1 ]), ’days’) // mysql dates are counted since 0 Jan 1 00:00:00 }

分區(qū)對象的創(chuàng)建與 CREATE TABLE ... PARTITION BY RANGE 非常相似。檢查我們即將創(chuàng)建的分區(qū)是否比當(dāng)前最舊的分區(qū)更舊,這一點(diǎn)至關(guān)重要:可能需要隨時(shí)間更改 dataRetention 。

以下情況為例:

假設(shè)你的用戶開始保留 7 天的數(shù)據(jù),但可以選擇將其升級到 10 天。開始時(shí),用戶用以下順序覆蓋分區(qū)天數(shù): [ start, -7, -6, -5, -4, -3, -2, -1, future ] 。一個(gè)月左右,用戶決定升級。在這種情況下,丟失的分區(qū)是 [ -10, -9, -8, 0 ] 。

在清理時(shí),當(dāng)前的腳本會(huì)嘗試重新組織 future 分區(qū),使其在當(dāng)前腳本 之后 附加它們。

在最開始時(shí)創(chuàng)建比 -7 天更老的分區(qū)是沒有意義的,因?yàn)槟切?shù)據(jù)注定是被拋棄的,并且還會(huì)導(dǎo)致如下的一個(gè)分區(qū)列表 [ start, -7, -6, -5, -4, -3, -2, -1, -10, -9, -8, 0, future ] ,由于不是單調(diào)增加,因此 MySQL 會(huì)拋出錯(cuò)誤,清理將失敗。

MySQL的 TO_DAYS(date) 函數(shù)計(jì)算從公元元年( 0 年)1 月 1 日以來的天數(shù),所以我們用 JavaScript 計(jì)算這個(gè)天數(shù)。

Table.getMysqlDay = function (momentDate) { return momentDate.diff(moment([ 0, 0, 1 ]), ’days’) }

現(xiàn)在我們有必須刪除的分區(qū)和必須創(chuàng)建的分區(qū),我們先為新的一天創(chuàng)建我們的新分區(qū)。

Table.reorganizeFuturePartition = function (partitionsToBeCreated) { if (!partitionsToBeCreated.length) return ’’ // there should be only one every day, and it is run hourly, so ideally 23 times a day it should be a noop const partitionsString = partitionsToBeCreated.map((partitionDescriptor) => { return PARTITION ${partitionDescriptor.name} VALUES LESS THAN (${partitionDescriptor.description}), }).join(’n’) return dedent ALTER TABLE ${tableName} REORGANIZE PARTITION future INTO ( ${partitionsString} PARTITION future VALUES LESS THAN MAXVALUE ); }

我們只需準(zhǔn)備一個(gè)創(chuàng)建新分區(qū)的語句。

我們每小時(shí)運(yùn)行這個(gè)腳本,以確保沒有任何遺漏,我們能夠每天至少執(zhí)行一次清理。

所以首先檢查一下是否有一個(gè)要?jiǎng)?chuàng)建的分區(qū)。這只應(yīng)該在第一次運(yùn)行時(shí)發(fā)生,然后剩余 23 次都不會(huì)發(fā)生。

我們還必須刪除過時(shí)的分區(qū)。

Table.dropOldPartitions = function (partitionsToBeDropped) { if (!partitionsToBeDropped.length) return ’’ let statement = ALTER TABLE ${tableName}nDROP PARTITIONn statement += partitionsToBeDropped.map((partition) => { return partition.name }).join(’,n’) return statement + ’;’ }

此方法創(chuàng)建了我們之前看到的 ALTER TABLE ... DROP PARTITION 語句。

最后,為重組做好了一切的準(zhǔn)備。

const statement = dedent ${Table.reorganizeFuturePartition(partitionsToBeCreated)} ${Table.dropOldPartitions(partitionsToBeDropped)} return knex.raw(statement) 總結(jié)

如你所見,與流行的觀點(diǎn)相反,當(dāng)你處理大量數(shù)據(jù)時(shí),可以使用符合 ACID 的 DBMS 解決方案(如MySQL),因此你不一定需要放棄事務(wù)數(shù)據(jù)庫的功能。

符合 ACID 的 DBMS 解決方案(如 MySQL)可用于處理大量數(shù)據(jù)。

但是,表分區(qū)有很多限制,這意味著你將無法使用 InnoDB 提供的所有功能來保持?jǐn)?shù)據(jù)的一致性。你可能還無法使用外鍵和 FULLTEXT 搜索來處理應(yīng)用程序邏輯。

我希望這篇文章可以幫助你確定 MySQL 是否適合你的需求,并幫助你實(shí)現(xiàn)解決方案。

來自:http://www.zcfy.cc/article/node-js-mysql-example-handling-100-x27-s-of-gigabytes-of-data-risingstack-3130.html

標(biāo)簽: MySQL 數(shù)據(jù)庫
相關(guān)文章:
主站蜘蛛池模板: 国产一区二区三区免费视频 | 久久人人青草97香蕉 | 国产精品揄拍100视频 | 亚洲一级视频在线观看 | 国产一区二区三区丶四区 | 加勒比久草| 久草福利免费 | 色婷婷综合久久久久中文一区二区 | 国产一级做a爰片在线 | 日韩欧美~中文字幕 | 欧美三级欧美做a爱 | aaa级毛片 | 最近最新中文字幕在线第一页 | jyzzjyzz国产免费观看 | 亚洲和欧美毛片久久久久 | 九九免费高清在线观看视频 | 久在线 | 午夜影视水蜜桃网站 | 国产成人精品福利色多多 | 香蕉一区二区 | 日韩精品一| 欧美高清视频www夜色资源网 | 日本大片免a费观看视频+播放器 | 免费久福利视频在线观看 | 亚洲国产精品第一区二区三区 | www.婷婷.com| 正在播放国产无套露脸 | 欧美国产亚洲一区二区三区 | 亚洲福利 | 99九九精品视频 | 国产香蕉免费精品视频 | 国产在线观看美女福利精 | 在线观看日韩视频 | 性生活视频黄色 | 在线成人a毛片免费播放 | 在线播放国产精品 | 黄色国产免费观看 | 麻豆传媒最新网址 | 又爽又刺激的欧美毛片 | 一级做a爰片久久毛片人呢 一级做a爰片久久毛片毛片 | 久久国产精品亚洲一区二区 |