Database
我們已經(jīng)想不起來(lái)為什么需要在項(xiàng)目中使用數(shù)據(jù)庫(kù)了蜕乡,基本上做后端開(kāi)發(fā)是無(wú)法離開(kāi)的數(shù)據(jù)庫(kù)的凯砍,從 RDBMS 到 NoSQL 再到最近火熱的 NewSQL见剩,我們一直在尋找更好更快的數(shù)據(jù)存儲(chǔ)方案剃法,不論硬盤(pán)是從磁盤(pán)變成了 SSD唇礁,但是依舊在用來(lái)自 1970 年代的 SQL勾栗。的確,使用現(xiàn)代的 web 框架配合 MySQL 能夠很快的完成一個(gè) RESTful 或者 MVC 式的應(yīng)用垒迂,但是這是有限的械姻,對(duì)于 RDBMS 來(lái)說(shuō),最大的問(wèn)題是按照行列的方式進(jìn)行數(shù)據(jù)存儲(chǔ)机断,而我們的業(yè)務(wù)邏輯算法(可能只是一個(gè)簡(jiǎn)單的 flatmap 之類(lèi)的東西)楷拳,往往是有多種數(shù)據(jù)結(jié)構(gòu)需求的。
現(xiàn)代數(shù)據(jù)庫(kù)系統(tǒng)都提供了企業(yè)級(jí)的功能吏奸,比如自動(dòng)備份欢揖、落盤(pán)加密、分布式與性能保證奋蔚,對(duì)于架構(gòu)師來(lái)說(shuō)她混,這些寫(xiě)在白皮書(shū)上的功能很有誘惑力,很多時(shí)候我們必須要通過(guò)這些功能點(diǎn)進(jìn)行技術(shù)選型泊碑,但是這是 low-level details坤按,對(duì)于架構(gòu)的設(shè)計(jì)來(lái)說(shuō),我們不應(yīng)該限定某種數(shù)據(jù)庫(kù)馒过,或者并不希望數(shù)據(jù)庫(kù)束縛住我們臭脓。其實(shí)這在項(xiàng)目中還是比較常見(jiàn)的,經(jīng)常會(huì)有人使用 h2 做 memory database 支持開(kāi)發(fā)腹忽,線上則用 MySQL 或者 MariaDB来累,換 JDBC Driver 就可以做到這一點(diǎn)砚作,但是本文是想討論在 JDBC 之外的代碼。
我不止一次的聽(tīng)過(guò)有人說(shuō)嘹锁,使用 MongoDB 是因?yàn)槠涮峁┑男阅芎迹珜?shí)際上優(yōu)化與重構(gòu)代碼所帶來(lái)的性能提升遠(yuǎn)遠(yuǎn)大于使用某種數(shù)據(jù)庫(kù),或者說(shuō)起性能的要求并沒(méi)有極端到必須要使用某種存儲(chǔ)技術(shù)领猾。這并不是說(shuō)性能不重要米同,而是說(shuō)我們的系統(tǒng)應(yīng)該有足夠的靈活性。往往在新項(xiàng)目開(kāi)始時(shí)瘤运,我們所面對(duì)的性能壓力并不是很大窍霞,可能第一個(gè)版本只是需要一個(gè)恰好能夠表達(dá)業(yè)務(wù)就行,這時(shí)候?qū)⒋鎯?chǔ)技術(shù)與實(shí)際業(yè)務(wù)進(jìn)行解耦就非常關(guān)鍵了拯坟,因?yàn)樵诓痪玫膶?lái)可能你會(huì)使用 MySQL Proxy 進(jìn)行分庫(kù)分表但金,或者直接使用 DynamoDB 這種 NoSQL。
有前輩曾向我展示過(guò)某銀行使用 MySQL Binlog 來(lái)進(jìn)行某種數(shù)據(jù)同步郁季,并圍繞該實(shí)現(xiàn)進(jìn)行了一系列的定制化開(kāi)發(fā)冷溃,隨著人員的更迭與技術(shù)的革新,這部分的系統(tǒng)已經(jīng)沒(méi)有人能進(jìn)行修改與升級(jí)了梦裂,誠(chéng)然他們也想換掉這種不是很專(zhuān)業(yè)的實(shí)現(xiàn)似枕,使用專(zhuān)業(yè)的中間件實(shí)現(xiàn)數(shù)據(jù)遷移同步并不是很難,但是已有的代碼與細(xì)節(jié)綁定的太深年柠,以至于沒(méi)人敢停掉現(xiàn)在的實(shí)現(xiàn)凿歼,雖然這些代碼依舊在工作,但是從架構(gòu)的角度上來(lái)說(shuō)是失敗的冗恨。
我們?cè)趯W(xué)習(xí)算法與數(shù)據(jù)結(jié)構(gòu)時(shí)答憔,使用過(guò)很多例如列表、樹(shù)掀抹、鏈表虐拓、隊(duì)列、堆棧傲武、Map 等等很多數(shù)據(jù)架構(gòu)蓉驹,而 RDBMS 只能提供行列,你有沒(méi)有將一個(gè)樹(shù)存放在數(shù)據(jù)庫(kù)中的經(jīng)驗(yàn)揪利?不管使用哪種方式實(shí)現(xiàn)态兴,你都會(huì)覺(jué)得比較別扭,比如下面的例子:
Option 1: 使用 Parent Id 存儲(chǔ)樹(shù)
id | parent_id | data
---+-----------+----------
1 | NULL | root
2 | 1 | Child 1
3 | 2 | Child 1.1
4 | 2 | Child 1.2
5 | 1 | Child 2
6 | 5 | Child 2.1
7 | 5 | Child 2.2
Option 2: 存儲(chǔ)左右子樹(shù)的關(guān)系
id | parent_id | lft | rgt | data
---+-----------+-----+-----+----------
1 | 0 | 1 | 14 | root
2 | 1 | 2 | 7 | Child 1
3 | 2 | 3 | 4 | Child 1.1
4 | 2 | 5 | 6 | Child 1.2
5 | 1 | 8 | 13 | Child 2
6 | 5 | 9 | 10 | Child 2.1
7 | 5 | 11 | 12 | Child 2.2
我個(gè)人曾經(jīng)實(shí)踐過(guò)使用 Option 2 的方式疟位,比如使用類(lèi)似的 SQL 進(jìn)行獲取節(jié)點(diǎn)計(jì)算:
SELECT * FROM nodes WHERE lft >= 2 AND rgt < 7 AND id != 2
這種模式很好的表達(dá)了樹(shù)的結(jié)構(gòu)诗茎,但是在我進(jìn)行節(jié)點(diǎn)的新建與更新時(shí),需要更新的數(shù)據(jù)行要遠(yuǎn)遠(yuǎn)多于 Option 1 了,這種 case 只適用于讀遠(yuǎn)遠(yuǎn)大于寫(xiě)的場(chǎng)景敢订。
(Closure Table Pattern 應(yīng)該是最流行的解決方案了,常見(jiàn)在各種 ORM 中會(huì)有實(shí)現(xiàn)罢吃,這里就不展開(kāi)講了楚午。)
這種別扭還會(huì)表達(dá)在其他地方,在代碼中尿招,插入數(shù)據(jù)我們希望是一個(gè) LinkedList矾柜,存儲(chǔ) K-V 我們希望有一個(gè) map,計(jì)算多級(jí)關(guān)系我們希望用樹(shù)就谜,計(jì)算路徑我們需要 graph怪蔑,做 workflow 我們更想要一個(gè) DAG,這些方便的數(shù)據(jù)結(jié)構(gòu)往往在存儲(chǔ)時(shí)會(huì)造成麻煩丧荐,這是我們不得不面對(duì)的缆瓣。
關(guān)系型數(shù)據(jù)庫(kù)的優(yōu)勢(shì)是 relation,使用 relation 你可以很方便的表達(dá)類(lèi)似于我有幾臺(tái)電腦這種關(guān)系虹统,使用 JOIN 類(lèi)似的查詢(xún)也很簡(jiǎn)單弓坞,但這個(gè)功能不一定是必要的。一旦沒(méi)有使用好 JOIN 或者其他的多表查詢(xún)會(huì)造成性能上的問(wèn)題车荔,而且多表查詢(xún)的語(yǔ)句也很難寫(xiě)渡冻、難以理解與改動(dòng)。在你使用 DynamoDB 或者 MongoDB 這種沒(méi)有 relation 的數(shù)據(jù)庫(kù)時(shí)忧便,很多時(shí)候你不得不自己編寫(xiě)程序來(lái)應(yīng)對(duì) JOIN 的需求族吻,對(duì)于 NoSQL 類(lèi)型的數(shù)據(jù)庫(kù),你必須要根據(jù)查詢(xún)需求進(jìn)行性能優(yōu)化珠增,加上分頁(yè)與排序就更糟糕了超歌。
這里面必須提到微服務(wù)的一些特性,在微服務(wù)世界中切平,我們提倡每個(gè)服務(wù)管理自己的數(shù)據(jù)握础,某種程度上降低了多表聯(lián)查的出現(xiàn)(我已經(jīng)很少看到三個(gè)表 JOIN 在一起的語(yǔ)句了,也有規(guī)范認(rèn)為三表 JOIN 就是反模式)悴品,但是并不代表這種 JOIN 的邏輯消失了禀综,當(dāng)你的 BFF 組織后端幾個(gè)服務(wù)的返回時(shí),那其實(shí)就是做 JOIN苔严。
所以定枷,不論你的數(shù)據(jù)庫(kù)是否支持 JOIN 或者類(lèi)似的功能,你都要將查詢(xún)的方法與過(guò)程遠(yuǎn)遠(yuǎn)的放到業(yè)務(wù)邏輯之外届氢,就是上文中我們畫(huà)過(guò)的邊界欠窒。你的 service 希望 repository 提供什么樣的數(shù)據(jù),這在接口上一定要定義清楚,有時(shí)候你甚至需要再引入 converter 將格式化的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)為適合計(jì)算的數(shù)據(jù)結(jié)構(gòu)岖妄⌒徒看起來(lái)是麻煩了一些,但是邊界清晰荐虐,即使存儲(chǔ)層有巨大的改動(dòng)需求七兜,也不需要破壞與改動(dòng) service。
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface BookRepository extends JpaRepository<Book, UUID> {
Page<Book> findByBookIdOrderByUpdatedSeqDesc(String userId, Pageable pageable);
Optional<Book> findByIdAndUserId(UUID bookId, String userId);
Optional<Book> findByNameAndUserId(String bookName, String userId);
}
ORM 也是現(xiàn)代框架中提供的殺手級(jí)功能福扬,的確能夠滿(mǎn)足大多數(shù)人的開(kāi)發(fā)需求腕铸,但是我們也要警惕 ORM 過(guò)多侵入代碼的問(wèn)題。Uncle Bob 不提倡我們將 ORM 的 Model 與業(yè)務(wù)的 Entity 混在一起铛碑,但是對(duì)于 Spring 開(kāi)發(fā)的程序員來(lái)說(shuō)很難做到狠裹,因?yàn)?Annotation Entity 幾乎隨處可見(jiàn),特別是 relation 的注解可以讓我們不需要直面 JOIN 語(yǔ)句了汽烦,還有其他的注解例如 org.hibernate.annotations.Type 這種直接幫我們使用數(shù)據(jù)庫(kù)的功能的涛菠。但是其實(shí)也沒(méi)那么糟糕,對(duì)于 Java Annotation 來(lái)說(shuō)刹缝,你不使用它就可以忽略它碗暗,只是不太好看,但是其他的編程語(yǔ)言與框架就不一定了梢夯。
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"type", "value"})})
public class FakeProductExample {
@Id
@Type(type = "pg-uuid")
private UUID id;
@Column
private String type;
@Column
private String value;
@CreationTimestamp
@Column(updatable = false)
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
...
所以我的感受是言疗,在 ORM 層與數(shù)據(jù)庫(kù)細(xì)節(jié)決裂是還是可以實(shí)現(xiàn)的,從 Service 層來(lái)看颂砸,是不需要知道 Entity 的 Annotation噪奄,把 Entity 做 POJO 來(lái)使用就可以了,同時(shí)再直接使用 Repository 獲取 Entity人乓,因?yàn)?Repository 是接口勤篮,對(duì)于 Service 來(lái)說(shuō)就無(wú)須考慮存儲(chǔ)細(xì)節(jié)了。在其他語(yǔ)言或者框架下色罚,這也是可以做到的碰缔,比如下面這個(gè) scala 的例子:
trait ProfilesDAO extends BasicDAO {
def createProfile(profileUid: String, ...): Profile
def updateProfileEmail(uid: String, ...): Int
def profileEmailExists(email: String): Boolean
...
}
object ProfileDAOImpl extends ProfilesDAO with DBPreferenceSupport with PrimitiveTypeMode {
def trans[A](action: => A): A = transaction(action)
override createProfile(profileUid: String, ...): Profile = {
...
inTransaction {
DB.profile.insert(profile)
...
}
profile
}
...
}
import SquerylImplicits._
object DB extends Schema {
case class Profile(
@Column("profile_uid") id: String,
...
@Column("profile_name") name: Option[String],
...
@Column("updated_at") updatedAt: Timestamp = new Timestamp(System.currentTimeMillis())
) extends KeyedEntity[String] {
def uid: String = id
}
...
}
trait ProfilesDAO 是外部代碼使用的邊界,就相當(dāng)于我們常說(shuō)的接口戳护,ProfileDAOImpl 負(fù)責(zé)實(shí)現(xiàn)金抡,調(diào)用了 DB 的成員( object DB 很輕量,寫(xiě)了一些映射就足夠了)并混入了其他功能腌且,我個(gè)人還是比較喜歡這種做法的梗肝,沒(méi)有引入過(guò)重的依賴(lài),邊界也很清晰铺董,ProfileDAOImpl 的實(shí)現(xiàn)也比較符合語(yǔ)言特性巫击。
Web
在我剛開(kāi)始工作的時(shí)候,我接觸了 jQuery 并很開(kāi)心的使用 $.ajax 去調(diào)用一些 endpoint,幾年后我很驚訝的發(fā)現(xiàn)我自己竟然都沒(méi)意識(shí)到我是在進(jìn)行 web service 調(diào)用坝锰,因?yàn)?web service 并不只是笨重的 WSDL 與 SOAP 按饫痢!隨著時(shí)代的發(fā)展什黑,MVC 式的應(yīng)用已經(jīng)逐漸沒(méi)落崎淳,大家已經(jīng)不喜歡做一個(gè)大而笨重的后端應(yīng)用了,也不喜歡既有后端渲染頁(yè)面愕把,也有異步的 ajax 去實(shí)現(xiàn)的復(fù)雜前端。HTML5 的發(fā)展伴隨著單頁(yè)應(yīng)用的崛起森爽,而我們的后端從有狀態(tài)恨豁、有 session 的巨大單體應(yīng)用也逐漸解體,被微服務(wù)替代爬迟,我覺(jué)得挺好橘蜜,很高興我們不是在瀏覽器里跑 Java Applet 或者 Microsoft Silverlight 了。
Framework
按照 Uncle Bob 的說(shuō)法 Framework 也是一種 detail付呕,我們要避免自己的應(yīng)用被框架綁架的情況计福。在現(xiàn)代軟件開(kāi)發(fā)的過(guò)程中,我們無(wú)法不依賴(lài)前人進(jìn)行開(kāi)發(fā)徽职,往往的節(jié)奏是象颖,選定一個(gè)框架,搞清楚基本功能姆钉,然后在上實(shí)現(xiàn)我們想要的功能说订。我們會(huì)跟進(jìn)應(yīng)用程序的需求來(lái)選擇框架,比如說(shuō)在 web 領(lǐng)域潮瓶,如果是一個(gè) MVC 式的應(yīng)用可能會(huì)使用 Spring MVC 或者 Ruby on Rails陶冷,如果我們要寫(xiě) Restful Service,可能會(huì)用 Ruby grape毯辅、Spring Boot埂伦、Scala Unfiltered 等等。因?yàn)榭蚣艿拇_幫助我們提供了通用的功能思恐,幫助我們可以專(zhuān)注于業(yè)務(wù)邏輯沾谜。但是往往的問(wèn)題是,我們對(duì)框架的依賴(lài)過(guò)重了壁袄,導(dǎo)致我們的代碼與框架沒(méi)有清晰的邊界类早,從而失去擴(kuò)展的機(jī)會(huì)。
import unfiltered.netty.future.Plan._
import unfiltered.request._
class AppRoutes(myController: MyController) {
val routes: Intent = {
case req@GET(Path(Seg("schema" :: Nil))) => myController.schema()
case req@GET(Path(Seg("search" :: Nil))) => myController.search(req)
...
case req@OPTIONS(Path(Seg("search" :: Nil))) => myController.options(req)
}
}
上面的例子描述了嗜逻,在路由中我們調(diào)用了 myController 來(lái)處理業(yè)務(wù)涩僻,而 myController 是注入進(jìn)來(lái)的,我們沒(méi)有在路由中直接編寫(xiě)業(yè)務(wù)邏輯。但是逆日,myController.search(req) 方法中的 req 是框架所定義的 HttpRequest嵌巷,所以 MyController 的必須要依賴(lài) unfiltered.request.HttpRequest,那么你就無(wú)法輕松的換掉 unfiltered 了室抽。
這種被綁架的情況被稱(chēng)為“不對(duì)稱(chēng)的婚姻”搪哪,對(duì)于 framework 的發(fā)明者來(lái)說(shuō),這樣的方式的確易于控制坪圾,但是對(duì)你來(lái)說(shuō)晓折,你必須適應(yīng)框架的規(guī)則,并且持續(xù)更新兽泄,你必須要做出很大的改動(dòng)才能適應(yīng)和使用框架漓概。我們?cè)谑褂?play framework 時(shí)就遇見(jiàn)了類(lèi)似的問(wèn)題,該項(xiàng)目是接手一個(gè)無(wú)人管理的代碼倉(cāng)庫(kù)病梢,為了升級(jí) framework 我們必須做出上千行的代碼改動(dòng)胃珍,危險(xiǎn)的是,因?yàn)闆](méi)有足夠的上下文蜓陌,沒(méi)人能確定改動(dòng)的正確性觅彰,更悲觀的是,因?yàn)槭褂昧撕芏嗵匦裕ㄈ罩九ト取⒙酚商钐А⒆⑷氲龋覀儙缀鯚o(wú)法選擇新的框架來(lái)替代霉旗。
另一個(gè)例子是痴奏,某公司的新一代交易引擎是跑在 Flink 這種流式處理框架之上的,幾個(gè)運(yùn)算模塊按照拓?fù)浔灰来握{(diào)用厌秒,對(duì)于這個(gè)場(chǎng)景他們使用 Flink 只是為了先驗(yàn)證計(jì)算方式的正確性读拆,而未來(lái)到底使用哪種技術(shù)去調(diào)用引擎還不是很確定(可以理解為這是一個(gè)從消息中間件中獲取事件,再調(diào)用引擎計(jì)算鸵闪,再將計(jì)算結(jié)果放回中間件檐晕,這種模式非常常見(jiàn))。所以在架構(gòu)的角度上來(lái)說(shuō)蚌讼,F(xiàn)link 與計(jì)算引擎之間的邊界很清晰辟灰,所以在嘗試使用其他消息消費(fèi)者獲取事件、驅(qū)動(dòng)計(jì)算的過(guò)程中篡石,計(jì)算引擎的部分一行代碼都沒(méi)有修改芥喇,整個(gè)改動(dòng)也非常小,在實(shí)際的工作中凰萨,其他忙于引擎開(kāi)發(fā)的同事并沒(méi)有感受到任何變化继控。
上面那個(gè)項(xiàng)目最后使用了 Spring Boot 來(lái)進(jìn)行依賴(lài)注入械馆、組裝 bean,那么不需要 Spring Boot 可以做這件事嗎武通?答案是可行的霹崎,Spring Boot 使用 autowire 的方式來(lái)管理組件的依賴(lài),我們自然也可以使用手動(dòng)管理冶忱。將業(yè)務(wù)需要的 bean 一個(gè)個(gè) new 出來(lái)尾菇,再組裝在一起,看起來(lái)復(fù)雜囚枪,但是實(shí)際上只花了一個(gè)多小時(shí)就做到了派诬,你可以把這些邏輯放在 main 函數(shù)中,而這些邏輯是與框架無(wú)關(guān)的链沼,所以你可以像 helloworld 一樣千埃,點(diǎn)一個(gè)按鈕就可以跑起來(lái)了。