使用Spring對Postgres實(shí)現(xiàn)可擴(kuò)展寫入
譯文?譯者 | 布加迪
審校 | 孫淑娟
每個(gè)與客戶產(chǎn)生共鳴的技術(shù)型組織最終都會(huì)遇到擴(kuò)展問題。擴(kuò)展產(chǎn)品和組織對您的流程和基礎(chǔ)架構(gòu)提出了新的要求。本文著重介紹了我們公司如何應(yīng)對基礎(chǔ)架構(gòu)擴(kuò)展方面的諸多挑戰(zhàn)之一:使用Spring和Spring Data對Postgres數(shù)據(jù)庫實(shí)現(xiàn)可擴(kuò)展寫入。
隨著用戶群越來越龐大,我們開始遇到一些性能問題,主要是受到我們的上游Postgres 數(shù)據(jù)庫的制約。我們的RPS(每秒請求)在短短幾個(gè)月內(nèi)就從<50增加到了超過180,我們開始遇到SQL連接超時(shí)、連接斷開和延遲顯著增加等問題。這導(dǎo)致客戶體驗(yàn)下降,這是不可接受的。
因此,我們著手研究如何消除這些Postgres瓶頸。我們很快意識(shí)到耗費(fèi)太多的周期進(jìn)行數(shù)據(jù)庫寫入,這阻塞了系統(tǒng)。對Postgres的每次寫入都是一次調(diào)用,這意味著如果我們想將50行保存到數(shù)據(jù)庫中,每行將調(diào)用1次,而不是執(zhí)行一次SQL調(diào)用來保存所有這50行!
根本原因:在Hibernate中使用IDENTITY生成ID值
為什么我們無法進(jìn)行批量更新?事實(shí)證明,問題與我們?nèi)绾问褂肏ibernate為數(shù)據(jù)庫中的實(shí)體生成標(biāo)識(shí)符值(即主鍵)有關(guān)。
我們使用的方法需要從IDENTITY列檢索值,新實(shí)體插入數(shù)據(jù)庫時(shí)??,Hibernate動(dòng)態(tài)維護(hù)這些列。我們針對新資源寫入數(shù)據(jù)庫是在沒有指定id(主鍵)的情況下完成的,改而使用GenerationType.IDENTITY。
這是我們的Spring實(shí)體的樣子:
Kotlin
@Entity
@Table(name = "entity")
data class Entity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val metadata: String,
) : TenantEntity()
采用這種策略后,使用ORM來創(chuàng)建和更新現(xiàn)有資源顯得非常簡單:
- 如果沒有傳遞id,會(huì)創(chuàng)建一個(gè)新行。
- 如果傳遞了id,會(huì)更新現(xiàn)有行。
是不是聽起來很簡單?我們也是這么想的!而且似乎效果良好,直到后來我們意識(shí)到使用IDENTITY帶來了嚴(yán)重的性能問題。這種策略的缺點(diǎn)是批量更新不起作用。
這給我們帶來了一個(gè)大問題,因?yàn)槲覀兊乃袑?shí)體都使用IDENTITY標(biāo)識(shí)符值生成。對于每個(gè)現(xiàn)有的表及對應(yīng)的實(shí)體,我們必須將策略從IDENTITY換成支持批量插入語句的不同策略。
從IDENTITY遷移到基于序列的ID生成
我們研究可用于支持批處理的實(shí)體的其他生成類型后,遇到了Hibernate基于序列的標(biāo)識(shí)符值生成。這個(gè)策略得到底層數(shù)據(jù)庫序列的支持。Hibernate從序列中請求下一個(gè)可用的id,為資源獲取新的id。
雖然該策略的底層機(jī)制超出了本文的討論范圍,但結(jié)論是,這種基于序列的策略將為我們實(shí)現(xiàn)批量插入。
現(xiàn)在我們需要弄清楚如何從現(xiàn)有的IDENTITY策略遷移到基于序列的新方法。
進(jìn)一步調(diào)查后,我們意識(shí)到現(xiàn)有的表已經(jīng)有一個(gè)Postgres序列。所以如果我們有一個(gè)這樣定義的表:
SQL
CREATE TABLE IF NOT EXISTS entity (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
...
)
將創(chuàng)建一個(gè)名為entity_id_seq的序列!
您可以運(yùn)行以下SQL命令來檢查序列是否存在:
SELECT
*
FROM
pg_sequence
WHERE
seqrelid = 'entity_id_seq'::regclass;
由于我們能夠輕松訪問Postgres表的序列,因此可以進(jìn)行非常本地化的更改,改而使用基于序列的策略來生成id。
對于每個(gè)實(shí)體,我們只需更改幾行代碼即可解決性能瓶頸。更新后的實(shí)體如下所示:
Kotlin
private const val TABLE = "entity"
private const val SEQUENCE = "${TABLE}_id_seq"
(name = TABLE)
data class Entity(
(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 50)
(name = "id")
val id: Long? = null,
val metadata: String,
) : TenantEntity()
AllocationSize和序列增量大小
這里需要說明的一點(diǎn)是,Hibernate中的allocationSize屬性需要與Postgres中底層序列的增量大小相同。
這是為了讓Hibernate和底層序列在它們擁有的id方面“同步”。這還可以防止多臺(tái)服務(wù)器寫入到同一個(gè)表的分布式架構(gòu)出現(xiàn)任何問題。
默認(rèn)情況下,Postgres序列的增量大小為1。我們寫了一個(gè)非??焖俚倪w移來更改它,以便與我們的allocationSize匹配:
ALTER SEQUENCE entity_id_seq INCREMENT 50;
現(xiàn)在,Hibernate只需要進(jìn)行1次調(diào)用,即可獲取每50次插入的id列表。
它也只需要1次調(diào)用即可插入這50行。
以下是我們從這個(gè)問題中得出的總結(jié):
- 如使用Hibernate,盡快開始使用基于數(shù)據(jù)庫序列的身份值生成,尤其是在您預(yù)見到寫入次數(shù)會(huì)增加的情況下。
- 保持allocationSize和底層Postgres序列增量大小參數(shù)相同,避免id沖突,并支持分布式系統(tǒng)。
最后,這是我們實(shí)施該更改后RPS從近180變成約90的屏幕截圖。
原文標(biāo)題:??Scalable Writes to Postgres With Spring??,作者:Aditya Bansal?