国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
分布式服務(wù)框架

第2 章
分布式系統(tǒng)基礎(chǔ)設(shè)施
chapter
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 59
一個大型、穩(wěn)健、成熟的分布式系統(tǒng)的背后,往往會涉及眾多的支撐系統(tǒng),我們將這些支撐系統(tǒng)稱為分布式系統(tǒng)的基礎(chǔ)設(shè)施。除了前面所介紹的分布式協(xié)作及配置管理系統(tǒng)ZooKeeper,我們進(jìn)行系統(tǒng)架構(gòu)設(shè)計所依賴的基礎(chǔ)設(shè)施,還包括分布式緩存系統(tǒng)、持久化存儲、分布式消息系統(tǒng)、搜索引擎,以及CDN 系統(tǒng)、負(fù)載均衡系統(tǒng)、運維自動化系統(tǒng)等,還有后面章節(jié)所要介紹的實時計算系統(tǒng)、離線計算系統(tǒng)、分布式文件系統(tǒng)、日志收集系統(tǒng)、監(jiān)控系統(tǒng)、數(shù)據(jù)倉庫等。
分布式緩存主要用于在高并發(fā)環(huán)境下,減輕數(shù)據(jù)庫的壓力,提高系統(tǒng)的響應(yīng)速度和并發(fā)吞吐。當(dāng)大量的讀、寫請求涌向數(shù)據(jù)庫時,磁盤的處理速度與內(nèi)存顯然不在一個量級,因此,在數(shù)據(jù)庫之前加一層緩存,能夠顯著提高系統(tǒng)的響應(yīng)速度,并降低數(shù)據(jù)庫的壓力。
作為傳統(tǒng)的關(guān)系型數(shù)據(jù)庫,MySQL 提供完整的ACID 操作,支持豐富的數(shù)據(jù)類型、強大的關(guān)聯(lián)查詢、where 語句等,能夠非常容易地建立查詢索引,執(zhí)行復(fù)雜的內(nèi)連接、外連接、求和、排序、分組等操作,并且支持存儲過程、函數(shù)等功能,產(chǎn)品成熟度高,功能強大。但是,對于
需要應(yīng)對高并發(fā)訪問并且存儲海量數(shù)據(jù)的場景來說,出于對性能的考慮,不得不放棄很多傳統(tǒng)關(guān)系型數(shù)據(jù)庫原本強大的功能,犧牲了系統(tǒng)的易用性,并且使得系統(tǒng)的設(shè)計和管理變得更為復(fù)雜。這也使得在過去幾年中,流行著另一種新的存儲解決方案——NoSQL,它與傳統(tǒng)的關(guān)系型
數(shù)據(jù)庫最大的差別在于,它不使用SQL 作為查詢語言來查找數(shù)據(jù),而采用key-value 形式進(jìn)行查找,提供了更高的查詢效率及吞吐,并且能夠更加方便地進(jìn)行擴展,存儲海量數(shù)據(jù),在數(shù)千個節(jié)點上進(jìn)行分區(qū),自動進(jìn)行數(shù)據(jù)的復(fù)制和備份。
在分布式系統(tǒng)中,消息作為應(yīng)用間通信的一種方式,得到了十分廣泛的應(yīng)用。消息可以被保存在隊列中,直到被接收者取出,由于消息發(fā)送者不需要同步等待消息接收者的響應(yīng),消息的異步接收降低了系統(tǒng)集成的耦合度,提升了分布式系統(tǒng)協(xié)作的效率,使得系統(tǒng)能夠更快地響

應(yīng)用戶,提供更高的吞吐。當(dāng)系統(tǒng)處于峰值壓力時,分布式消息隊列還能夠作為緩沖,削峰填谷,緩解集群的壓力,避免整個系統(tǒng)被壓垮。垂直化的搜索引擎在分布式系統(tǒng)中是一個非常重要的角色,它既能夠滿足用戶對于全文檢索、模糊匹配的需求,解決數(shù)據(jù)庫like 查詢效率低下的問題,又能夠解決分布式環(huán)境下,由于采用分庫分表,或者使用NoSQL 數(shù)據(jù)庫,導(dǎo)致無法進(jìn)行多表關(guān)聯(lián)或者進(jìn)行復(fù)雜查詢的問題。

本章主要介紹和解決如下問題:

分布式緩存memcache 的使用及分布式策略,包括Hash 算法的選擇。
常見的分布式系統(tǒng)存儲解決方案,包括MySQL 的分布式擴展、HBase 的API 及使用
場景、Redis 的使用等。
如何使用分布式消息系統(tǒng) ActiveMQ 來降低系統(tǒng)之間的耦合度,以及進(jìn)行應(yīng)用間的通信。
垂直化的搜索引擎在分布式系統(tǒng)中的使用,包括搜索引擎的基本原理、Lucene 詳細(xì)的

使用介紹,以及基于Lucene 的開源搜索引擎工具Solr 的使用。


60 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
2.1 分布式緩存
在高并發(fā)環(huán)境下,大量的讀、寫請求涌向數(shù)據(jù)庫,磁盤的處理速度與內(nèi)存顯然不在一個量級,從減輕數(shù)據(jù)庫的壓力和提高系統(tǒng)響應(yīng)速度兩個角度來考慮,一般都會在數(shù)據(jù)庫之前加一層緩存。由于單臺機器的內(nèi)存資源和承載能力有限,并且如果大量使用本地緩存,也會使相同的
數(shù)據(jù)被不同的節(jié)點存儲多份,對內(nèi)存資源造成較大的浪費,因此才催生出了分布式緩存。本節(jié)將詳細(xì)介紹分布式緩存的典型代表memcache,以及分布式緩存的應(yīng)用場景。最為典型的場景莫過于分布式session。

2.1.1 memcache 簡介及安裝

memcache1是danga.com 的一個項目,它是一款開源的高性能的分布式內(nèi)存對象緩存系統(tǒng),最早是給 LiveJournal2提供服務(wù)的,后來逐漸被越來越多的大型網(wǎng)站所采用,用于在應(yīng)用中減少對數(shù)據(jù)庫的訪問,提高應(yīng)用的訪問速度,并降低數(shù)據(jù)庫的負(fù)載。
為了在內(nèi)存中提供數(shù)據(jù)的高速查找能力,memcache 使用 key-value 形式存儲和訪問數(shù)據(jù),在內(nèi)存中維護(hù)一張巨大的HashTable,使得對數(shù)據(jù)查詢的時間復(fù)雜度降低到O(1),保證了對數(shù)據(jù)的高性能訪問。內(nèi)存的空間總是有限的,當(dāng)內(nèi)存沒有更多的空間來存儲新的數(shù)據(jù)時,memcache就會使用LRU(Least Recently Used)算法,將最近不常訪問的數(shù)據(jù)淘汰掉,以騰出空間來存放新的數(shù)據(jù)。memcache 存儲支持的數(shù)據(jù)格式也是靈活多樣的,通過對象的序列化機制,可以將更高層抽象的對象轉(zhuǎn)換成為二進(jìn)制數(shù)據(jù),存儲在緩存服務(wù)器中,當(dāng)前端應(yīng)用需要時,又可以通過二進(jìn)制內(nèi)容反序列化,將數(shù)據(jù)還原成原有對象。
1. memcache 的安裝
由于 memcache 使用了libevent 來進(jìn)行高效的網(wǎng)絡(luò)連接處理,因此在安裝memcache 之前,
需要先安裝libevent。
下載 libevent3,這里采用的是1.4.14 版本的libevent。
wget https://github.com/downloads/libevent/libevent/libevent-1.4.14bstable.
tar.gz
1 memcache 項目地址為http://memcached.org。
2 LiveJournal,http://www.livejournal.com。
3 libevent,http://libevent.org。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 61
解壓:
tar –xf libevent-1.4.14b-stable.tar.gz
配置、編譯、安裝libevent:
./configure
make
62 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
sudo make install
下載memcache,并解壓:
wget http://www.memcached.org/files/memcached-1.4.17.tar.gz
tar –xf memcached-1.4.17.tar.gz
配置、編譯、安裝memcache:
./configure
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 63
make
sudo make install
64 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
2. 啟動與關(guān)閉memcache
啟動memcache 服務(wù):
/usr/local/bin/memcached -d -m 10 -u root -l 192.168.136.135 -p 11211 -c 32
-P /tmp/memcached.pid
參數(shù)的含義如下:
-d 表示啟動的是一個守護(hù)進(jìn)程;
-m 指定分配給memcache 的內(nèi)存數(shù)量,單位是MB,這里指定的是10 MB。
-u 指定運行memcache 的用戶,這里指定的是root;
-l 指定監(jiān)聽的服務(wù)器的IP 地址;
-p 設(shè)置memcache 監(jiān)聽的端口,這里指定的是11211;
-c 指定最大允許的并發(fā)連接數(shù),這里設(shè)置為32;
-P 指定memcache 的pid 文件保存的位置。
關(guān)閉memcache 服務(wù):
kill `cat /tmp/memcached.pid`
2.1.2 memcache API 與分布式
memcache 客戶端與服務(wù)端通過構(gòu)建在TCP 協(xié)議之上的memcache 協(xié)議4來進(jìn)行通信,協(xié)議
支持兩種數(shù)據(jù)的傳遞,這兩種數(shù)據(jù)分別為文本行和非結(jié)構(gòu)化數(shù)據(jù)。文本行主要用來承載客戶端
的命令及服務(wù)端的響應(yīng),而非結(jié)構(gòu)化數(shù)據(jù)則主要用于客戶端和服務(wù)端數(shù)據(jù)的傳遞。由于非結(jié)構(gòu)
化數(shù)據(jù)采用字節(jié)流的形式在客戶端和服務(wù)端之間進(jìn)行傳輸和存儲,因此使用方式非常靈活,緩
存數(shù)據(jù)存儲幾乎沒有任何限制,并且服務(wù)端也不需要關(guān)心存儲的具體內(nèi)容及字節(jié)序。
memcache 協(xié)議支持通過如下幾種方式來讀取/寫入/失效數(shù)據(jù):
4 memcache 協(xié)議見https://github.com/memcached/memcached/blob/master/doc/protocol.txt。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 65
set 將數(shù)據(jù)保存到緩存服務(wù)器,如果緩存服務(wù)器存在同樣的key,則替換之;
add 將數(shù)據(jù)新增到緩存服務(wù)器,如果緩存服務(wù)器存在同樣的key,則新增失??;
replace 將數(shù)據(jù)替換緩存服務(wù)器中相同的key,如果緩存服務(wù)器不存在同樣的key,則替
換失?。?br> append 將數(shù)據(jù)追加到已經(jīng)存在的數(shù)據(jù)后面;
prepend 將數(shù)據(jù)追加到已經(jīng)存在的數(shù)據(jù)前面;
cas 提供對變量的cas 操作,它將保證在進(jìn)行數(shù)據(jù)更新之前,數(shù)據(jù)沒有被其他人更改;
get 從緩存服務(wù)器獲取數(shù)據(jù);
incr 對計數(shù)器進(jìn)行增量操作;
decr 對計數(shù)器進(jìn)行減量操作;
delete 將緩存服務(wù)器上的數(shù)據(jù)刪除。
memcache 官方提供的Memcached-Java-Client5工具包含了對memcache 協(xié)議的Java 封裝,
使用它可以比較方便地與緩存服務(wù)端進(jìn)行通信,它的初始化方式如下:
public static void init(){
String[] servers = {
"192.168.136.135:11211"
};
SockIOPool pool = SockIOPool.getInstance();
pool.setServers(servers);//設(shè)置服務(wù)器
pool.setFailover(true);//容錯
pool.setInitConn(10);//設(shè)置初始連接數(shù)
pool.setMinConn(5);//設(shè)置最小連接數(shù)
pool.setMaxConn(25); //設(shè)置最大連接數(shù)
pool.setMaintSleep(30);//設(shè)置連接池維護(hù)線程的睡眠時間
pool.setNagle(false);//設(shè)置是否使用Nagle 算法
pool.setSocketTO(3000);//設(shè)置socket 的讀取等待超時時間
pool.setAliveCheck(true);//設(shè)置連接心跳監(jiān)測開關(guān)
pool.setHashingAlg(SockIOPool.CONSISTENT_HASH);//設(shè)置Hash 算法
pool.initialize();
}
通過 SockIOPool,可以設(shè)置與后端緩存服務(wù)器的一系列參數(shù),如服務(wù)器地址、是否采用容
5 Memcached-Java-Client,https://github.com/gwhalin/Memcached-Java-Client。
66 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
錯、初始連接數(shù)、最大連接數(shù)、最小連接數(shù)、線程睡眠時間、是否使用Nagle 算法、socket 的
讀取等待超時時間、是否心跳檢測、Hash 算法,等等。
使用 Memcached-Java-Client 的API 設(shè)置緩存的值:
MemCachedClient memCachedClient = new MemCachedClient();
memCachedClient.add("key", 1);
memCachedClient.set("key", 2);
memCachedClient.replace("key", 3);
通過 add()方法新增緩存,如果緩存服務(wù)器存在同樣的key,則返回false;而通過set()方法
將數(shù)據(jù)保存到緩存服務(wù)器,緩存服務(wù)器如果存在同樣的key,則將其替換。replace()方法可以用
來替換服務(wù)器中相同的key 的值,如果緩存服務(wù)器不存在這樣的key,則返回false。
使用 Memcached-Java-Client 的API 獲取緩存的值:
Object value = memCachedClient.get("key");
String[] keys = {"key1","key2"};
Map<String, Object> values = memCachedClient.getMulti(keys);
通過 get()方法,可以從服務(wù)器獲取該key 對應(yīng)的數(shù)據(jù);而使用getMulti()方法,則可以一次
性從緩存服務(wù)器獲取一組數(shù)據(jù)。
對緩存的值進(jìn)行append 和prepend 操作:
memCachedClient.set("key-name", "chenkangxian");
memCachedClient.prepend("key-name", "hello");
memCachedClient.append("key-name", "!");
通過 prepend()方法,可以在對應(yīng)key 的值前面增加前綴;而通過append()方法,則可以在
對應(yīng)的key 的值后面追加后綴。
對緩存的數(shù)據(jù)進(jìn)行cas6操作:
MemcachedItem item = memCachedClient.gets("key");
memCachedClient.cas("key", (Integer)item.getValue() + 1,
item.getCasUnique());
通過 gets()方法獲得key 對應(yīng)的值和值的版本號,它們包含在MemcachedItem 對象中;然
后使用cas()方法對該值進(jìn)行修改,當(dāng)key 對應(yīng)的版本號與通過gets 取到的版本號(即
item.getCasUnique())相同時,則將key 對應(yīng)的值修改為item.getValue() + 1,這樣可以防止并發(fā)
修改所帶來的問題。
6 memcache 的CAS 有點類似Java 的CAS(compare and set)操作,關(guān)于Java 的CAS 操作,第4 章會有
詳細(xì)介紹。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 67
對緩存的數(shù)據(jù)進(jìn)行增量與減量操作:
memCachedClient.incr("key",1);
memCachedClient.decr("key",1);
使用 incr()方法可以對key 對應(yīng)的值進(jìn)行增量操作,而使用decr()方法則可以對key 對應(yīng)的
值進(jìn)行減量操作。
memcache 本身并不是一種分布式的緩存系統(tǒng),它的分布式是由訪問它的客戶端來實現(xiàn)的。一種比較簡單的實現(xiàn)方式是根據(jù)緩存的key 來進(jìn)行Hash,當(dāng)后端有N 臺緩存服務(wù)器時,訪問的服務(wù)器為hash(key)%N,這樣可以將前端的請求均衡地映射到后端的緩存服務(wù)器,如圖2-1 所示。
但這樣也會導(dǎo)致一個問題,一旦后端某臺緩存服務(wù)器宕機,或者是由于集群壓力過大,需要新增緩存服務(wù)器時,大部分的key 將會重新分布。對于高并發(fā)系統(tǒng)來說,這可能會演變成一場災(zāi)難,所有的請求將如洪水般瘋狂地涌向后端的數(shù)據(jù)庫服務(wù)器,而數(shù)據(jù)庫服務(wù)器的不可用,將會
導(dǎo)致整個應(yīng)用的不可用,形成所謂的“雪崩效應(yīng)”。

圖2-1 memcache 集群采用hash(key)%N 進(jìn)行分布
使用consistent Hash 算法能夠在一定程度上改善上述問題。該算法早在1997 年就在論文
68 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
Consistent hashing and random trees7中被提出,它能夠在移除/添加一臺緩存服務(wù)器時,盡可能小
地改變已存在的key 映射關(guān)系,避免大量key 的重新映射。
consistent Hash 的原理是這樣的,它將Hash 函數(shù)的值域空間組織成一個圓環(huán),假設(shè)Hash
函數(shù)的值域空間為0~232-1(即Hash 值是一個32 位的無符號整型),整個空間按照順時針方向
進(jìn)行組織,然后對相應(yīng)的服務(wù)器節(jié)點進(jìn)行Hash,將它們映射到Hash 環(huán)上,假設(shè)有4 臺服務(wù)器,
分別為node1、node2、node3、node4,它們在環(huán)上的位置如圖2-2 所示。
圖2-2 consistent Hash 的原理
接下來使用相同的Hash 函數(shù),計算出對應(yīng)的key 的Hash 值在環(huán)上對應(yīng)的位置。根據(jù)
consistent Hash 算法,按照順時針方向,分布在node1 與node2 之間的key,它們的訪問請求會
被定位到node2,而node2 與node4 之間的key,訪問請求會被定為到node4,以此類推。
假設(shè)有新節(jié)點node5 增加進(jìn)來時,假設(shè)它被Hash 到node2 和node4 之間,如圖2-3 所示。
那么受影響的只有node2 和node5 之間的key,它們將被重新映射到node5,而其他key 的映射
關(guān)系將不會發(fā)生改變,這樣便避免了大量key 的重新映射。
當(dāng)然,上面描繪的只是一種理想的情況,各個節(jié)點在環(huán)上分布得十分均勻。正常情況下,
當(dāng)節(jié)點數(shù)量較少時,節(jié)點的分布可能十分不均勻,從而導(dǎo)致數(shù)據(jù)訪問的傾斜,大量的key 被映
射到同一臺服務(wù)器上。為了避免這種情況的出現(xiàn),可以引入虛擬節(jié)點機制,對每一個服務(wù)器節(jié)
點都計算多個Hash 值,每一個Hash 值都對應(yīng)環(huán)上一個節(jié)點的位置,該節(jié)點稱為虛擬節(jié)點,而
key 的映射方式不變,只是多了一步從虛擬節(jié)點再映射到真實節(jié)點的過程。這樣,如果虛擬節(jié)
7 consistent hash,http://dl.acm.org/citation.cfm?id=258660。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 69
點的數(shù)量足夠多,即使只有很少的實際節(jié)點,也能夠使key 分布得相對均衡。
圖2-3 當(dāng)新節(jié)點加入時的情景8
2.1.3 分布式session
傳統(tǒng)的應(yīng)用服務(wù)器,如tomcat、jboss 等,其自身所實現(xiàn)的session 管理大部分都是基于單
機的。對于大型分布式網(wǎng)站來說,支撐其業(yè)務(wù)的遠(yuǎn)遠(yuǎn)不止一臺服務(wù)器,而是一個分布式集群,
請求在不同服務(wù)器之間跳轉(zhuǎn)。那么,如何保持服務(wù)器之間的session 同步呢?傳統(tǒng)網(wǎng)站一般通過
將一部分?jǐn)?shù)據(jù)存儲在cookie 中,來規(guī)避分布式環(huán)境下session 的操作。這樣做的弊端很多,一方
面cookie 的安全性一直廣為詬病,另一方面cookie 存儲數(shù)據(jù)的大小是有限制的。隨著移動互聯(lián)
網(wǎng)的發(fā)展,很多情況下還得兼顧移動端的session 需求,使得采用cookie 來進(jìn)行session 同步的
方式的弊端更為凸顯。分布式session 正是在這種情況下應(yīng)運而生的。
對于系統(tǒng)可靠性要求較高的用戶,可以將session 持久化到DB 中,這樣可以保證宕機時會
話不易丟失,但缺點也是顯而易見的,系統(tǒng)的整體吞吐將受到很大的影響。另一種解決方案便
是將session 統(tǒng)一存儲在緩存集群上,如memcache,這樣可以保證較高的讀、寫性能,這一點
對于并發(fā)量大的系統(tǒng)來說非常重要;并且從安全性考慮,session 畢竟是有有效期的,使用緩存
存儲,也便于利用緩存的失效機制。使用緩存的缺點是,一旦緩存重啟,里面保存的會話也就
丟失了,需要用戶重新建立會話。
如圖 2-4 所示,前端用戶請求經(jīng)過隨機分發(fā)之后,可能會命中后端任意的Web Server,并
8 圖片來源http://blog.charlee.li/content/images/2008/Jul/memcached-0004-05.png。
70 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
且 Web Server 也可能會因為各種不確定的原因宕機。在這種情況下,session 是很難在集群間同
步的,而通過將session 以sessionid 作為key,保存到后端的緩存集群中,使得不管請求如何分
配,即便是Web Server 宕機,也不會影響其他Web Server 通過sessionid 從Cache Server 中獲得
session,這樣既實現(xiàn)了集群間的session 同步,又提高了Web Server 的容錯性。
圖2-4 基于緩存的分布式session 架構(gòu)
這里以 Tomcat 作為Web Server 來舉例,通過一個簡單的工具memcached-session- manager9,
實現(xiàn)基于memcache 的分布式session。
memcached-session-manager 是一個開源的高可用的Tomcat session 共享解決方案,它支持
Sticky 模式和Non-Sticky 模式。Sticky 模式表示每次請求都會被映射到同一臺后端Web Server,
直到該Web Server 宕機,這樣session 可先存放在服務(wù)器本地,等到請求處理完成再同步到后端
memcache 服務(wù)器;而當(dāng)Web Server 宕機時,請求被映射到其他Web Server,這時候,其他Web
Server 可以從后端memcache 中恢復(fù)session。對于Non-Sticky 模式來說,請求每次映射的后端
Web Server 是不確定的,當(dāng)請求到來時,從memcache 中加載session;當(dāng)請求處理完成時,將
9 memcached-session-manager,https://code.google.com/p/memcached-session-manager。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 71
session 再寫回到memcache。
以 Non-Sticky 模式為例,它需要給Tomcat 的$CATALINA_HOME/conf/context.xml 文件配
置SessionManager,具體配置如下:
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.0.100:11211,n2:192.168.0.101:11211"
sticky="false"
sessionBackupAsync="false"
lockingMode="auto"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>
其中:memcachedNodes 指定了memcache 的節(jié)點;sticky 表示是否采用Sticky 模式;
sessionBackupAsync 表示是否采用異步方式備份session;lockingMode 表示session 的鎖定模式;
auto 表示對于只讀請求,session 將不會被鎖定,如果包含寫入請求,則session 會被鎖定;
requestUriIgnorePattern 表示忽略的url;transcoderFactoryClass 用來指定序列化的方式,這里采用
的是Kryo 序列化,也是memcached-session-manager 比較推薦的一種序列化方式。
memcached-session-manager 依賴于memcached-session-manager-${version}.jar,如果使用的是
tomcat6,則還需要下載memcached-session-manager-tc6-${version}.jar,并且它還依賴memcached-
${version}.jar 進(jìn)行memcache 的訪問。在啟動Tomcat 之前,需要將這些jar 放在$CATALINA_
HOME/lib/目錄下。如果使用第三方序列化方式,如Kryo,還需要在Web 工程中引入相關(guān)的第三方
庫,Kryo 序列化所依賴的庫,包括kryo-${version}-all.jar 、kryo-serializers-${version}.jar 和
msm-kryo-serializer. ${version}.jar。
2.2 持久化存儲
隨著科技的不斷發(fā)展,越來越多的人開始參與到互聯(lián)網(wǎng)活動中來,人們在網(wǎng)絡(luò)上的活動,
如發(fā)表心情動態(tài)、微博、購物、評論等,這些信息最終被轉(zhuǎn)變成二進(jìn)制字節(jié)的數(shù)據(jù)存儲下來。
面對并發(fā)訪問量的激增和數(shù)據(jù)量幾何級的增長,如何存儲正在迅速膨脹并且不斷累積的數(shù)據(jù),
以及應(yīng)對日益增長的用戶訪問頻次,成為了亟待解決的問題。
傳統(tǒng)的 IOE10解決方案,使用和擴展的成本越來越高,使得互聯(lián)網(wǎng)企業(yè)不得不思考新的解決
方案。開源軟件加廉價PC Server 的分布式架構(gòu),得益于社區(qū)的支持。在節(jié)約成本的同時,也給
系統(tǒng)帶來了良好的擴展能力,并且由于開源軟件的代碼透明,使得企業(yè)能夠以更低的代價定制
10 I 表示IBM 小型機,O 表示oracle 數(shù)據(jù)庫,E 表示EMC 高端存儲。
72 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
更符合自身使用場景的功能,以提高系統(tǒng)的整體性能。本節(jié)將介紹互聯(lián)網(wǎng)領(lǐng)域常見的三種數(shù)據(jù)
存儲方式,包括傳統(tǒng)關(guān)系型數(shù)據(jù)庫MySQL、Google 所提出的bigtable 概念及其開源實現(xiàn)HBase,
以及包含豐富數(shù)據(jù)類型的key-value 存儲Redis。
作為傳統(tǒng)的關(guān)系型數(shù)據(jù)庫,MySQL 提供完整的ACID 操作,支持豐富的數(shù)據(jù)類型、強大的關(guān)聯(lián)查詢、where 語句等,能夠非常容易地建立查詢索引,執(zhí)行復(fù)雜的內(nèi)連接、外連接、求和、排序、分組等操作,并且支持存儲過程、函數(shù)等功能,產(chǎn)品成熟度高,功能強大。對于大多數(shù)中小規(guī)模的應(yīng)用來說,關(guān)系型數(shù)據(jù)庫擁有強大完整的功能,以及提供的易用性、靈活性和產(chǎn)品成熟度,地位很難被完全替代。但是,對于需要應(yīng)對高并發(fā)訪問并且存儲海量數(shù)據(jù)的場景來說,出于性能的考慮,不得不放棄很多傳統(tǒng)關(guān)系型數(shù)據(jù)的功能,如關(guān)聯(lián)查詢、事務(wù)、數(shù)據(jù)一致性(由
強一致性降為最終一致性);并且由于對數(shù)據(jù)存儲進(jìn)行拆分,如分庫分表,以及進(jìn)行反范式設(shè)計,以提高系統(tǒng)的查詢性能,使得我們放棄了關(guān)系型數(shù)據(jù)庫大部分原本強大的功能,犧牲了系統(tǒng)的易用性,并且使得系統(tǒng)的設(shè)計和管理變得更為復(fù)雜
。
過去幾年中,流行著一種新的存儲解決方案,NoSQL、HBase 和Redis 作為其中較為典型的代表,各自都得到了較為廣泛的使用,它們各自都具有比較鮮明的特性。與傳統(tǒng)的關(guān)系型數(shù)據(jù)庫相比,HBase 有更好的伸縮能力,更適合于海量數(shù)據(jù)的存儲和處理,并且HBase 能夠支持
多個Region Server 同時寫入,并發(fā)寫入性能十分出色。但HBase 本身所支持的查詢維度有限,難以支持復(fù)雜的條件查詢,如group by、order by、join 等,這些特點使它的應(yīng)用場景受到了限制。對于Redis 來說,它擁有更好的讀/寫吞吐能力,能夠支撐更高的并發(fā)數(shù),而相較于其他的key-value 類型的數(shù)據(jù)庫,Redis 能夠提供更為豐富的數(shù)據(jù)類型支持,能更靈活地滿足業(yè)務(wù)需求。
2.2.1 MySQL 擴展
隨著互聯(lián)網(wǎng)行業(yè)的高速發(fā)展,使得采用諸如IOE 等商用存儲解決方案的成本不斷攀升,越
來越難以滿足企業(yè)高速發(fā)展的需要;因此,開源的存儲解決方案開始逐漸受到青睞,并成為互
聯(lián)網(wǎng)企業(yè)數(shù)據(jù)存儲的首選方案。
以 MySQL 為例,它作為開源關(guān)系型數(shù)據(jù)庫的典范,正越來越廣泛地被互聯(lián)網(wǎng)企業(yè)所使用。
企業(yè)可以根據(jù)業(yè)務(wù)規(guī)模的不同的階段,選擇采用不同的系統(tǒng)架構(gòu),以應(yīng)對逐漸增長的訪問壓力
和數(shù)據(jù)量;并且隨著業(yè)務(wù)的發(fā)展,需要提前做好系統(tǒng)的容量規(guī)劃,在系統(tǒng)的處理能力還未達(dá)到
極限時,對系統(tǒng)進(jìn)行擴容,以免帶來損失。

1. 業(yè)務(wù)拆分

業(yè)務(wù)發(fā)展初期為了便于快速迭代,很多應(yīng)用都采用集中式的架構(gòu)。隨著業(yè)務(wù)規(guī)模的擴展,
使系統(tǒng)變得越來越復(fù)雜,越來越難以維護(hù),開發(fā)效率越來越低,并且系統(tǒng)的資源消耗也越來越
大,通過硬件提升性能的成本也越來越高。因此,系統(tǒng)業(yè)務(wù)的拆分是難以避免的。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 73
舉例來說,假設(shè)某門戶網(wǎng)站,它包含了新聞、用戶、帖子、評論等幾大塊內(nèi)容,對于數(shù)據(jù)
庫來說,它可能包含這樣幾張表,如news、users、post、comment,如圖2-5 所示。
圖2-5 single DB 的拆分
隨著業(yè)務(wù)的不斷發(fā)展,單個庫的訪問量越來越大,因此,不得不對業(yè)務(wù)進(jìn)行拆分。每一塊
業(yè)務(wù)都使用單獨的數(shù)據(jù)庫來進(jìn)行存儲,前端不同的業(yè)務(wù)訪問不同的數(shù)據(jù)庫,這樣原本依賴單庫
的服務(wù),變成4 個庫同時承擔(dān)壓力,吞吐能力自然就提高了。
順帶說一句,業(yè)務(wù)拆分不僅僅提高了系統(tǒng)的可擴展性,也帶來了開發(fā)工作效率的提升。原
來一次簡單修改,工程啟動和部署可能都需要很長時間,更別說開發(fā)測試了。隨著系統(tǒng)的拆分,
單個系統(tǒng)復(fù)雜度降低,減輕了應(yīng)用多個分支開發(fā)帶來的分支合并沖突解決的麻煩,不僅大大提
高了開發(fā)測試的效率,同時也提升了系統(tǒng)的穩(wěn)定性。
2. 復(fù)制策略
架構(gòu)變化的同時,業(yè)務(wù)也在不斷地發(fā)展,可能很快就會發(fā)現(xiàn),隨著訪問量的不斷增加,拆
分后的某個庫壓力越來越大,馬上就要達(dá)到能力的瓶頸,數(shù)據(jù)庫的架構(gòu)不得不再次進(jìn)行變更,
這時可以使用MySQL 的replication(復(fù)制)策略來對系統(tǒng)進(jìn)行擴展。
通過數(shù)據(jù)庫的復(fù)制策略,可以將一臺 MySQL 數(shù)據(jù)庫服務(wù)器中的數(shù)據(jù)復(fù)制到其他MySQL
數(shù)據(jù)庫服務(wù)器上。當(dāng)各臺數(shù)據(jù)庫服務(wù)器上都包含相同數(shù)據(jù)時,前端應(yīng)用通過訪問MySQL 集群
中任意一臺服務(wù)器,都能夠讀取到相同的數(shù)據(jù),這樣每臺MySQL 服務(wù)器所需要承擔(dān)的負(fù)載就
會大大降低,從而提高整個系統(tǒng)的承載能力,達(dá)到系統(tǒng)擴展的目的。
如圖 2-6 所示,要實現(xiàn)數(shù)據(jù)庫的復(fù)制,需要開啟Master 服務(wù)器端的Binary log。數(shù)據(jù)復(fù)制的
74 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
過程實際上就是Slave 從master 獲取binary log,然后再在本地鏡像的執(zhí)行日志中記錄的操作。
由于復(fù)制過程是異步的,因此Master 和Slave 之間的數(shù)據(jù)有可能存在延遲的現(xiàn)象,此時只能夠
保證數(shù)據(jù)最終的一致性。
圖2-6 MySQL 的Master 與Slave 之間數(shù)據(jù)同步的過程11
MySQL 的復(fù)制可以基于一條語句(statement level),也可以基于一條記錄(row level)。通
過row level 的復(fù)制,可以不記錄執(zhí)行的SQL 語句相關(guān)聯(lián)的上下文信息,只需要記錄數(shù)據(jù)變更
的內(nèi)容即可。但由于每行的變更都會被記錄,這樣可能會產(chǎn)生大量的日志內(nèi)容,而使用statement
level 則只是記錄修改數(shù)據(jù)的SQL 語句,減少了binary log 的日志量,節(jié)約了I/O 成本。但是,
為了讓SQL 語句在Slave 端也能夠正確地執(zhí)行,它還需要記錄SQL 執(zhí)行的上下文信息,以保證
所有語句在Slave 端執(zhí)行時能夠得到在Master 端執(zhí)行時的相同結(jié)果。
在實際的應(yīng)用場景中,MySQL 的Master 與Slave 之間的復(fù)制架構(gòu)有可能是這樣的,如圖
2-7 所示。
前端服務(wù)器通過Master 來執(zhí)行數(shù)據(jù)寫入的操作,數(shù)據(jù)的更新通過Binary log 同步到Slave
集群,而對于數(shù)據(jù)讀取的請求,則交由Slave 來處理,這樣Slave 集群可以分擔(dān)數(shù)據(jù)庫讀的壓力,
并且讀、寫分離還保障了數(shù)據(jù)能夠達(dá)到最終一致性。一般而言,大多數(shù)站點的讀數(shù)據(jù)庫操作要
比寫數(shù)據(jù)庫操作更為密集。如果讀的壓力較大,還可以通過新增Slave 來進(jìn)行系統(tǒng)的擴展,因
此,Master-Slave 的架構(gòu)能夠顯著地減輕前面所提到的單庫讀的壓力。畢竟在大多數(shù)應(yīng)用中,讀
的壓力要比寫的壓力大得多。
11 圖片來源http://hatemysql.com/wp-content/uploads/2013/04/mysql_replication.png。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 75
圖2-7 Master-Slaves 復(fù)制架構(gòu)
Master-Slaves 復(fù)制架構(gòu)存在一個問題,即所謂的單點故障。當(dāng)Master 宕機時,系統(tǒng)將無法
寫入,而在某些特定的場景下,也可能需要Master 停機,以便進(jìn)行系統(tǒng)維護(hù)、優(yōu)化或者升級。
同樣的道理,Master 停機將導(dǎo)致整個系統(tǒng)都無法寫入,直到Master 恢復(fù),大部分情況下這顯然
是難以接受的。為了盡可能地降低系統(tǒng)停止寫入的時間,最佳的方式就是采用Dual-Master 架構(gòu),
即Master-Master 架構(gòu),如圖2-8 所示。
76 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
圖 2-8 MySQL Dual-Master 架構(gòu)
所謂的 Dual Master,實際上就是兩臺MySQL 服務(wù)器互相將對方作為自己的Master,自己
作為對方的Slave,這樣任何一臺服務(wù)器上的數(shù)據(jù)變更,都會通過MySQL 的復(fù)制機制同步到另
一臺服務(wù)器。當(dāng)然,有的讀者可能會擔(dān)心,這樣不會導(dǎo)致兩臺互為Master 的MySQL 之間循環(huán)
復(fù)制嗎?當(dāng)然不會,這是由于MySQL 在記錄Binary log 日志時,記錄了當(dāng)前的server-id,server-id
在我們配置MySQL 復(fù)制時就已經(jīng)設(shè)置好了。一旦有了server-id,MySQL 就很容易判斷最初的
寫入是在哪臺服務(wù)器上發(fā)生的,MySQL 不會將復(fù)制所產(chǎn)生的變更記錄到Binary log,這樣就避
免了服務(wù)器間數(shù)據(jù)的循環(huán)復(fù)制。
當(dāng)然,我們搭建Dual-Master 架構(gòu),并不是為了讓兩個Master 能夠同時提供寫入服務(wù),這
樣會導(dǎo)致很多問題。舉例來說,假如Master A 與Master B 幾乎同時對一條數(shù)據(jù)進(jìn)行了更新,對
Master A 的更新比對Master B 的更新早,當(dāng)對Master A 的更新最終被同步到Master B 時,老版
本的數(shù)據(jù)將會把版本更新的數(shù)據(jù)覆蓋,并且不會拋出任何異常,從而導(dǎo)致數(shù)據(jù)不一致的現(xiàn)象發(fā)
生。在通常情況下,我們僅開啟一臺Master 的寫入,另一臺Master 僅僅stand by 或者作為讀庫
開放,這樣可以避免數(shù)據(jù)寫入的沖突,防止數(shù)據(jù)不一致的情況發(fā)生。
在正常情況下,如需進(jìn)行停機維護(hù),可按如下步驟執(zhí)行Master 的切換操作:
(1)停止當(dāng)前Master 的所有寫入操作。
(2)在Master 上執(zhí)行set global read_only=1,同時更新MySQL 配置文件中相應(yīng)的配置,
避免重啟時失效。
(3)在Master 上執(zhí)行show Master status,以記錄Binary log 坐標(biāo)。
(4)使用Master 上的Binary log 坐標(biāo),在stand by 的Master 上執(zhí)行select Master_pos_wait(),
等待stand by Master 的Binary log 跟上Master 的Binary log。
(5)在stand by Master 開啟寫入時,設(shè)置read_only=0。
(6)修改應(yīng)用程序的配置,使其寫入到新的Master。
假如 Master 意外宕機,處理過程要稍微復(fù)雜一點,因為此時Master 與stand by Master 上的
數(shù)據(jù)并不一定同步,需要將Master 上沒有同步到stand by Master 的Binary log 復(fù)制到Master 上
進(jìn)行replay,直到stand by Master 與原Master 上的Binary log 同步,才能夠開啟寫入;否則,
這一部分不同步的數(shù)據(jù)就有可能導(dǎo)致數(shù)據(jù)不一致。
3. 分表與分庫
對于大型的互聯(lián)網(wǎng)應(yīng)用來說,數(shù)據(jù)庫單表的記錄行數(shù)可能達(dá)到千萬級別甚至是億級,并且
數(shù)據(jù)庫面臨著極高的并發(fā)訪問。采用Master-Slave 復(fù)制模式的MySQL 架構(gòu),只能夠?qū)?shù)據(jù)庫的
讀進(jìn)行擴展,而對數(shù)據(jù)的寫入操作還是集中在Master 上,并且單個Master 掛載的Slave 也不可
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 77
能無限制多,Slave 的數(shù)量受到Master 能力和負(fù)載的限制。因此,需要對數(shù)據(jù)庫的吞吐能力進(jìn)
行進(jìn)一步的擴展,以滿足高并發(fā)訪問與海量數(shù)據(jù)存儲的需要。
對于訪問極為頻繁且數(shù)據(jù)量巨大的單表來說,我們首先要做的就是減少單表的記錄條數(shù),
以便減少數(shù)據(jù)查詢所需要的時間,提高數(shù)據(jù)庫的吞吐,這就是所謂的分表。在分表之前,首先
需要選擇適當(dāng)?shù)姆直聿呗裕沟脭?shù)據(jù)能夠較為均衡地分布到多張表中,并且不影響正常的查詢。
對于互聯(lián)網(wǎng)企業(yè)來說,大部分?jǐn)?shù)據(jù)都是與用戶關(guān)聯(lián)的,因此,用戶id 是最常用的分表字段。
因為大部分查詢都需要帶上用戶id,這樣既不影響查詢,又能夠使數(shù)據(jù)較為均衡地分布到各個
表中12,如圖2-9 所示。
圖2-9 user 表按照user_id%256 的策略進(jìn)行分表
假設(shè)有一張記錄用戶購買信息的訂單表order,由于order 表記錄條數(shù)太多,將被拆分成256
張表13。拆分的記錄根據(jù)user_id%256 取得對應(yīng)的表進(jìn)行存儲,前臺應(yīng)用則根據(jù)對應(yīng)的
user_id%256,找到對應(yīng)訂單存儲的表進(jìn)行訪問。這樣一來,user_id 便成為一個必需的查詢條件,
否則將會由于無法定位數(shù)據(jù)存儲的表而無法對數(shù)據(jù)進(jìn)行訪問。
假設(shè) user 表的結(jié)構(gòu)如下:
create table order(
order_id bigint(20) primary key auto_increment,
12 當(dāng)然,有的場景也可能會出現(xiàn)冷熱數(shù)據(jù)分布不均衡的情況。
13 拆分后表的數(shù)量一般為2 的n 次方。
78 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
user_id bigint(20),
user_nick varchar(50),
auction_id bigint(20),
auction_title bigint(20),
price bigint(20),
auction_cat varchar(200),
seller_id bigint(20),
seller_nick varchar(50)
);
那么分表以后,假設(shè)user_id=257,并且auction_id=100,需要根據(jù)auction_id 來查詢對應(yīng)的
訂單信息,則對應(yīng)的SQL 語句如下:
select * from order_1 where user_id = 257 and auction_id = 100;
其中,order_1 根據(jù)257%256 計算得出,表示分表之后的第1 張order 表。
分表能夠解決單表數(shù)據(jù)量過大帶來的查詢效率下降的問題,但是,卻無法給數(shù)據(jù)庫的并發(fā)
處理能力帶來質(zhì)的提升。面對高并發(fā)的讀寫訪問,當(dāng)數(shù)據(jù)庫Master 服務(wù)器無法承載寫操作壓力
時,不管如何擴展Slave 服務(wù)器,此時都沒有意義了。因此,我們必須換一種思路,對數(shù)據(jù)庫
進(jìn)行拆分,從而提高數(shù)據(jù)庫寫入能力,這就是所謂的分庫。
與分表策略相似,分庫也可以采用通過一個關(guān)鍵字段取模的方式,來對數(shù)據(jù)訪問進(jìn)行路由,
如圖2-10 所示。
圖2-10 MySQL 分庫策略
還是之前的訂單表,假設(shè)user_id 字段的值為257,將原有的單庫分為256 個庫,那么應(yīng)用
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 79
程序?qū)?shù)據(jù)庫的訪問請求將被路由到第1 個庫(257%256=1)。
有時數(shù)據(jù)庫可能既面臨著高并發(fā)訪問的壓力,又需要面對海量數(shù)據(jù)的存儲問題,這時需要
對數(shù)據(jù)庫即采用分庫策略,又采用分表策略,以便同時擴展系統(tǒng)的并發(fā)處理能力,以及提升單
表的查詢性能,這就是所謂的分庫分表。
分庫分表的策略比前面的僅分庫或者僅分表的策略要更為復(fù)雜,一種分庫分表的路由策略
如下:
中間變量=user_id%(庫數(shù)量×每個庫的表數(shù)量);
庫=取整(中間變量/每個庫的表數(shù)量);
表=中間變量%每個庫的表數(shù)量。
同樣采用 user_id 作為路由字段,首先使用user_id 對庫數(shù)量×每個庫表的數(shù)量取模,得到
一個中間變量;然后使用中間變量除以每個庫表的數(shù)量,取整,便得到對應(yīng)的庫;而中間變量
對每個庫表的數(shù)量取模,即得到對應(yīng)的表。分庫分表策略如圖2-11 所示。
圖2-11 MySQL 分庫分表策略
假設(shè)將原來的單庫單表order 拆分成256 個庫,每個庫包含1024 個表,那么按照前面所提
到的路由策略,對于user_id=262145 的訪問,路由的計算過程如下:
中間變量=262145%(256×1024)=1;
80 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
庫=取整(1/1024)=0;
表=1%1024=1。
這意味著,對于user_id=262145 的訂單記錄的查詢和修改,將被路由到第0 個庫的第1 個
表中執(zhí)行。
數(shù)據(jù)庫經(jīng)過業(yè)務(wù)拆分及分庫分表之后,雖然查詢性能和并發(fā)處理能力提高了,但也會帶來
一系列的問題。比如,原本跨表的事務(wù)上升為分布式事務(wù);由于記錄被切分到不同的庫與不同
的表當(dāng)中,難以進(jìn)行多表關(guān)聯(lián)查詢,并且不能不指定路由字段對數(shù)據(jù)進(jìn)行查詢。分庫分表以后,
如果需要對系統(tǒng)進(jìn)行進(jìn)一步擴容(路由策略變更),將變得非常不方便,需要重新進(jìn)行數(shù)據(jù)遷移。
相較于 MySQL 的分庫分表策略,后面要提到的HBase 天生就能夠很好地支持海量數(shù)據(jù)的
存儲,能夠以更友好、更方便的方式支持表的分區(qū),并且HBase 還支持多個Region Server 同時
寫入,能夠較為方便地擴展系統(tǒng)的并發(fā)寫入能力。而通過后面章節(jié)所提到的搜索引擎技術(shù),能
夠解決采用業(yè)務(wù)拆分及分庫分表策略后,系統(tǒng)無法進(jìn)行多表關(guān)聯(lián)查詢,以及查詢時必須帶路由
字段的問題。搜索引擎能夠很好地支持復(fù)雜條件的組合查詢,通過搜索引擎構(gòu)建的一張大表,
能夠彌補一部分?jǐn)?shù)據(jù)庫拆分所帶來的問題。
2.2.2 HBase
HBase14是Apache Hadoop 項目下的一個子項目,它以Google BigTable15為原型,設(shè)計實現(xiàn)
了高可靠性、高可擴展性、實時讀/寫的列存儲數(shù)據(jù)庫。它的本質(zhì)實際上是一張稀疏的大表,用
來存儲粗粒度的結(jié)構(gòu)化數(shù)據(jù),并且能夠通過簡單地增加節(jié)點來實現(xiàn)系統(tǒng)的線性擴展。
HBase 運行在分布式文件系統(tǒng)HDFS16之上,利用它可以在廉價的PC Server 上搭建大規(guī)模
結(jié)構(gòu)化存儲集群。HBase 的數(shù)據(jù)以表的形式進(jìn)行組織,每個表由行列組成。與傳統(tǒng)的關(guān)系型數(shù)
據(jù)庫不同的是,HBase 每個列屬于一個特定的列族,通過行和列來確定一個存儲單元,而每個
存儲單元又可以有多個版本,通過時間戳來標(biāo)識,如表2-1 所示。
表 2-1 HBase 表數(shù)據(jù)的組織形式
rowkey
column-family1 column-family2 column-family3
column1 column2 column3 column1 column2 column1
key1 … … … … … …
key2 … … … … … …
14 HBase 項目地址為https://hbase.apache.org。
15 著名的Google BigTable 論文,http://research.google.com/archive/bigtable.html。
16 關(guān)于HDFS 的介紹,請參照第5.2 節(jié)。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 81
key3 … … … … … …
HBase 集群中通常包含兩種角色,HMaster 和HRegionServer。當(dāng)表隨著記錄條數(shù)的增加而
不斷變大后,將會分裂成一個個Region,每個Region 可以由(startkey,endkey)來表示,它包
含一個startkey 到endkey 的半閉區(qū)間。一個HRegionServer 可以管理多個Region,并由HMaster
來負(fù)責(zé)HRegionServer 的調(diào)度及集群狀態(tài)的監(jiān)管。由于Region 可分散并由不同的HRegionServer
來管理,因此,理論上再大的表都可以通過集群來處理。HBase 集群布署圖如圖2-12 所示。
圖2-12 HBase 集群部署圖17
1. HBase 安裝
下載 HBase 的安裝包,這里選擇的版本是0.9618。
wget http://mirror.bit.edu.cn/apache/hbase/hbase-0.96.1.1/hbase-
0.96.1.1-hadoop1-bin.tar.gz
17 圖片來源http://dl2.iteye.com/upload/attachment/0073/5412/53da4281-58d4-3f53-8aaf-a09d0c295f05.jpg。
18 HBase 的版本需要與Hadoop 的版本相兼容,詳情請見http://hbase.apache.org/book/configuration.html# hadoop。
82 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
解壓安裝文件:
tar -xf hbase-0.96.1.1-hadoop1-bin.tar.gz
修改配置文件:
編輯{HBASE_HOME}/conf/hbase-env.sh 文件,設(shè)置JAVA_HOME 為Java 的安裝目錄。
export JAVA_HOME=/usr/java/
編輯{HBASE_HOME}/conf/hbase-site.xml 文件,增加如下配置,其中hbase.rootdir 目錄用
于指定HBase 的數(shù)據(jù)存放位置,這里指定的是HDFS 上的路徑,而hbase.cluster.distributed 則指
定了是否運行在分布式模式下。
<configuration>
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<property>
<name>hbase.rootdir</name>
<value>hdfs://localhost:9000/hbase</value>
</property>
</configuration>
啟動 HBase:
完成上述操作后,先啟動Hadoop,再啟動HBase,就可以進(jìn)行相應(yīng)的操作了。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 83
使用HBase shell:
./hbase shell
查看HBase 集群狀態(tài):
status
HBase 的基本使用:
創(chuàng)建一個表,并指定列族的名稱,create '表名稱'、'列族名稱1'、'列族名稱2' ……
例如,create 'user','phone','info'。
創(chuàng)建 user 表,包含兩個列族,一個是phone,一個是info。
84 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
列出已有的表,并查看表的描述:
list
describe ‘表名’
例如,describe ‘user’。
新增/刪除一個列族。
給表新增一個列族:
alter '表名',NAME=>'列族名稱'
例如,alter 'user',NAME=>'class'。
刪除表的一個列族:
alter '表名',NAME=>'列族名稱',METHOD=>'delete'
例如,alter 'user',NAME=>'class',METHOD=>'delete'。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 85
刪除一個表:
在使用 drop 刪除一個表之前,必須先將該表disable:
disable 'user'
drop 'user'
如果沒有disable 表而直接使用drop 刪除,則會出現(xiàn)如下提示:
給表添加記錄:
put '表名', 'rowkey','列族名稱:列名稱','值'
例如,put 'user','1','info:name','zhangsan'。
查看數(shù)據(jù)。
根據(jù) rowkey 查看數(shù)據(jù):
get '表名稱','rowkey'
例如,get 'user','1'。
根據(jù)rowkey 查看對應(yīng)列的數(shù)據(jù):
get '表名稱','rowkey','列族名稱:列名稱'
例如,get 'user','1','info:name'。
86 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
查看表中的記錄總數(shù):
count '表名稱'
例如,count 'user'。
查看表中所有記錄:
scan '表名稱'
例如,scan 'user'。
查看表中指定列族的所有記錄:
scan '表名',{COLUMNS => '列族'}
例如,scan 'user',{COLUMNS => 'info'}。
查看表中指定區(qū)間的所有記錄:
scan '表名稱',{COLUMNS => '列族',LIMIT =>記錄數(shù), STARTROW => '開始rowkey',
STOPROW=>'結(jié)束rowkey'}
例如,scan 'user',{COLUMNS => 'info',LIMIT =>5, STARTROW => '2',STOPROW=>'7'}。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 87
刪除數(shù)據(jù)。
根據(jù) rowkey 刪除列數(shù)據(jù):
delete '表名稱','rowkey' ,'列簇名稱'
例如,delete 'user','1','info:name'。
根據(jù)rowkey 刪除一行數(shù)據(jù):
deleteall '表名稱','rowkey'
例如,deleteall 'user','2。
2. HBase API
除了通過shell 進(jìn)行操作,HBase 作為分布式數(shù)據(jù)庫,自然也提供程序訪問的接口,此處以
Java 為例。
首先,需要配置HBase 的HMaster 服務(wù)器地址和對應(yīng)的端口(默認(rèn)為60000),以及對應(yīng)的
ZooKeeper 服務(wù)器地址和端口:
private static Configuration conf = null;
static {
conf = HBaseConfiguration.create();
conf = HBaseConfiguration.create();
conf.set("hbase.ZooKeeper.property.clientPort", "2181");
conf.set("hbase.ZooKeeper.quorum", "192.168.136.135");
conf.set("hbase.master", "192.168.136.135:60000");
}
接下來,通過程序來新增user 表,user 表中有三個列族,分別為info、class、parent,如果
該表已經(jīng)存在,則先刪除該表:
public static void createTable() throws Exception {
88 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
String tableName = "user";
HBaseAdmin hBaseAdmin = new HBaseAdmin(conf);
if (hBaseAdmin.tableExists(tableName)) {
hBaseAdmin.disableTable(tableName);
hBaseAdmin.deleteTable(tableName);
}
HTableDescriptor tableDescriptor = new
HTableDescriptor(TableName.valueOf(tableName));
tableDescriptor.addFamily(new HColumnDescriptor("info"));
tableDescriptor.addFamily(new HColumnDescriptor("class"));
tableDescriptor.addFamily(new HColumnDescriptor("parent"));
hBaseAdmin.createTable(tableDescriptor);
hBaseAdmin.close();
}
將數(shù)據(jù)添加到user 表,每個列族指定一個列col,并給該列賦值:
public static void putRow() throws Exception {
String tableName = "user";
String[] familyNames = {"info","class","parent"};
HTable table = new HTable(conf, tableName);
for(int i = 0; i < 20; i ++){
for (int j = 0; j < familyNames.length; j++) {
Put put = new Put(Bytes.toBytes(i+""));
put.add(Bytes.toBytes(familyNames[j]),
Bytes.toBytes("col"),
Bytes.toBytes("value_"+i+"_"+j));
table.put(put);
}
}
table.close();
}
取得 rowkey 為1 的行,并將該行打印出來:
public static void getRow() throws IOException {
String tableName = "user";
String rowKey = "1";
HTable table = new HTable(conf, tableName);
Get g = new Get(Bytes.toBytes(rowKey));
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 89
Result r = table.get(g);
outputResult(r);
table.close();
}
public static void outputResult(Result rs){
List<Cell> list = rs.listCells();
System.out.println("row key : " +
new String(rs.getRow()));
for(Cell cell : list){
System.out.println("family: " + new String(cell.getFamily())
+ ", col: " + new String(cell.getQualifier())
+ ", value: " + new String(cell.getValue()) );
}
}
scan 掃描user 表,并將查詢結(jié)果打印出來:
public static void scanTable() throws Exception {
String tableName = "user";
HTable table = new HTable(conf, tableName);
Scan s = new Scan();
ResultScanner rs = table.getScanner(s);
for (Result r : rs) {
outputResult(r);
}
//設(shè)置startrow 和endrow 進(jìn)行查詢
s = new Scan("2".getBytes(),"6".getBytes());
rs = table.getScanner(s);
for (Result r : rs) {
outputResult(r);
}
table.close();
}
刪除 rowkey 為1 的記錄:
public static void deleteRow( ) throws IOException {
String tableName = "user";
String rowKey = "1";
HTable table = new HTable(conf, tableName);
90 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
List<Delete> list = new ArrayList<Delete>();
Delete d = new Delete(rowKey.getBytes());
list.add(d);
table.delete(list);
table.close();
}
3. rowkey 設(shè)計
要想訪問 HBase 的行,只有三種方式,一種是通過指定rowkey 進(jìn)行訪問,另一種是指定
rowkey 的range 進(jìn)行scan,再者就是全表掃描。由于全表掃描對于性能的消耗很大,掃描一張
上億行的大表將帶來很大的開銷,以至于整個集群的吞吐都會受到影響。因此,rowkey 設(shè)計的
好壞,將在很大程度上影響表的查詢性能,是能否充分發(fā)揮HBase 性能的關(guān)鍵。
舉例來說,假設(shè)使用HBase 來存儲用戶的訂單信息,我們可能會通過這樣幾個維度來記錄
訂單的信息,包括購買用戶的id、交易時間、商品id、商品名稱、交易金額、賣家id 等。假設(shè)
需要從賣家維度來查看某商品已售出的訂單,并且按照下單時間區(qū)間來進(jìn)行查詢,那么訂單表
可以這樣設(shè)計:
rowkey:seller_id + auction_id + create_time
列族:order_info(auction_title,price,user_id)
使用賣家id+商品id+交易時間作為表的rowkey,列族為order,該列族包含三列,即商品
標(biāo)題、價格、購買者id,如圖2-13 所示。由于HBase 的行是按照rowkey 來排序的,這樣通過
rowkey 進(jìn)行范圍查詢,可以縮小scan 的范圍。
圖 2-13 根據(jù)rowkey 進(jìn)行表的scan
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 91
而假設(shè)需要從購買者維度來進(jìn)行訂單數(shù)據(jù)的查詢,展現(xiàn)用戶購買過的商品,并且按照購買
時間進(jìn)行查詢分頁,那么rowkey 的設(shè)計又不同了:
rowkey:user_id + create_time
列族:order_info(auction_id,auction_title,price,seller_id)
這樣通過買家id+交易時間區(qū)間,便能夠查到用戶在某個時間范圍內(nèi)因購買所產(chǎn)生的訂單。
但有些時候,我們既需要從賣家維度來查詢商品售出情況,又需要從買家維度來查詢商品
購買情況,關(guān)系型數(shù)據(jù)庫能夠很好地支持類似的多條件復(fù)雜查詢。但對于HBase 來說,實現(xiàn)起
來并不是那么的容易。基本的解決思路就是建立一張二級索引表,將查詢條件設(shè)計成二級索引
表的rowkey,而存儲的數(shù)據(jù)則是數(shù)據(jù)表的rowkey,這樣就可以在一定程度上實現(xiàn)多個條件的查
詢。但是二級索引表也會引入一系列的問題,多表的插入將降低數(shù)據(jù)寫入的性能,并且由于多
表之間無事務(wù)保障,可能會帶來數(shù)據(jù)一致性的問題19。
與傳統(tǒng)的關(guān)系型數(shù)據(jù)庫相比,HBase 有更好的伸縮能力,更適合于海量數(shù)據(jù)的存儲和處理。
由于多個Region Server 的存在,使得HBase 能夠多個節(jié)點同時寫入,顯著提高了寫入性能,并
且是可擴展的。但是,HBase 本身能夠支持的查詢維度有限,難以支持復(fù)雜查詢,如group by、
order by、join 等,這些特點使得它的應(yīng)用場景受到了限制。當(dāng)然,這也并非是不可彌補的硬傷,
通過后面章節(jié)所介紹的搜索引擎來構(gòu)建索引,可以在一定程度上解決HBase 復(fù)雜條件組合查詢
的問題。
2.2.3 Redis
Redis 是一個高性能的key-value 數(shù)據(jù)庫,與其他很多key-value 數(shù)據(jù)庫的不同之處在于,Redis
不僅支持簡單的鍵值對類型的存儲,還支持其他一系列豐富的數(shù)據(jù)存儲結(jié)構(gòu),包括strings、
hashs、lists、sets、sorted sets 等,并在這些數(shù)據(jù)結(jié)構(gòu)類型上定義了一套強大的API。通過定義
不同的存儲結(jié)構(gòu),Redis 可以很輕易地完成很多其他key-value 數(shù)據(jù)庫難以完成的任務(wù),如排序、
去重等。
1. 安裝Redis
下載Redis 源碼安裝包:
wget http://download.redis.io/releases/redis-2.8.8.tar.gz
19 關(guān)于HBase 的二級索引表,華為提供了hindex 的二級索引解決方案,有興趣的讀者可以參考
https://github.com/Huawei-Hadoop/hindex。
92 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
解壓文件:
tar -xf redis-2.8.8.tar.gz
編譯安裝Redis:
sudo make PREFIX=/usr/local/redis install
將 Redis 安裝到/usr/local/redis 目錄,然后,從安裝包中找到Redis 的配置文件,將其復(fù)制
到安裝的根目錄。
sudo cp redis.conf /usr/local/redis/
啟動Redis Server:
./redis-server ../redis.conf
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 93
使用redis-cli 進(jìn)行訪問20:
./redis-cli
2. 使用Redis API
Redis 的Java client21有很多,這里選擇比較常用的Jedis22來介紹Redis 數(shù)據(jù)訪問的API。
首先,需要對Redis client 進(jìn)行初始化:
Jedis redis = new Jedis ("192.168.136.135",6379);
Redis 支持豐富的數(shù)據(jù)類型,如strings、hashs、lists、sets、sorted sets 等,這些數(shù)據(jù)類型都
有對應(yīng)的API 來進(jìn)行操作。比如,Redis 的strings 類型實際上就是最基本的key-value 形式的數(shù)
據(jù),一個key 對應(yīng)一個value,它支持如下形式的數(shù)據(jù)訪問:
redis.set("name", "chenkangxian");//設(shè)置key-value
redis.setex("content", 5, "hello");//設(shè)置key-value 有效期為5 秒
20 更多數(shù)據(jù)訪問的命令請參考http://redis.io/commands。
21 Redis 的clien,http://redis.io/clients。
22 Jedis 項目地址為https://github.com/xetorthio/jedis。
94 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
redis.mset("class","a","age","25"); //一次設(shè)置多個key-value
redis.append("content", " lucy");//給字符串追加內(nèi)容
String content = redis.get("content"); //根據(jù)key 獲取value
List<String> list = redis.mget("class","age");//一次取多個key
通過 set 方法,可以給對應(yīng)的key 設(shè)值;通過get 方法,可以獲取對應(yīng)key 的值;通過setex
方法可以給key-value 設(shè)置有效期;通過mset 方法,一次可以設(shè)置多個key-value 對;通過mget
方法,可以一次獲取多個key 對應(yīng)的value,這樣的好處是,可以避免多次請求帶來的網(wǎng)絡(luò)開銷,
提高性能;通過append 方法,可以給已經(jīng)存在的key 對應(yīng)的value 后追加內(nèi)容。
Redis 的hashs 實際上是一個string 類型的field 和value 的映射表,類似于Map,特別適合
存儲對象。相較于將每個對象序列化后存儲,一個對象使用hashs 存儲將會占用更少的存儲空
間,并且能夠更為方便地存取整個對象:
redis.hset("url", "google", "www.google.cn");//給Hash 添加key-value
redis.hset("url", "taobao", "www.taobao.com");
redis.hset("url", "sina", "www.sina.com.cn");
Map<String,String> map = new HashMap<String,String>();
map.put("name", "chenkangxian");
map.put("sex", "man");
map.put("age", "100");
redis.hmset("userinfo", map);//批量設(shè)置值
String name = redis.hget("userinfo", "name");//取Hash 中某個key 的值
//取Hash 的多個key 的值
List<String> urllist = redis.hmget("url","google","taobao","sina");
//取Hash 的所有key 的值
Map<String,String> userinfo = redis.hgetAll("userinfo");
通過 hset 方法,可以給一個Hash 存儲結(jié)構(gòu)添加key-value 數(shù)據(jù);通過hmset 方法,能夠一
次性設(shè)置多個值,避免多次網(wǎng)絡(luò)操作的開銷;使用hget 方法,能夠取得一個Hash 結(jié)構(gòu)中某個
key 對應(yīng)的value;使用hmget 方法,則可以一次性獲取得多個key 對應(yīng)的value;通過hgetAll
方法,可以將Hash 存儲對應(yīng)的所有key-value 一次性取出。
Redis 的lists 是一個鏈表結(jié)構(gòu),主要的功能是對元素的push 和pop,以及獲取某個范圍內(nèi)
的值等。push 和pop 操作可以從鏈表的頭部或者尾部插入/刪除元素,這使得lists 既可以作為棧
使用,又可以作為隊列使用,其中,操作的key 可以理解為鏈表的名稱:
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 95
redis.lpush("charlist", "abc");//在list 首部添加元素
redis.lpush("charlist", "def");
redis.rpush("charlist", "hij");//在list 尾部添加元素
redis.rpush("charlist", "klm");
List<String> charlist = redis.lrange("charlist", 0, 2);
redis.lpop("charlist");//在list 首部刪除元素
redis.rpop("charlist");//在list 尾部刪除元素
Long charlistSize = redis.llen("charlist");//獲得list 的大小
通過 lpush 和rpush 方法,分別可以在list 的首部和尾部添加元素;使用lpop 和rpop 方法,
可以在list 的首部和尾部刪除元素,通過lrange 方法,可以獲取list 指定區(qū)間的元素。
Redis 的sets 與數(shù)據(jù)結(jié)構(gòu)的set 相似,用來存儲一個沒有重復(fù)元素的集合,對集合的元素可
以進(jìn)行添加和刪除的操作,并且能夠?qū)λ性剡M(jìn)行枚舉:
redis.sadd("SetMem", "s1");//給set 添加元素
redis.sadd("SetMem", "s2");
redis.sadd("SetMem", "s3");
redis.sadd("SetMem", "s4");
redis.sadd("SetMem", "s5");
redis.srem("SetMem", "s5");//從set 中移除元素
Set<String> set = redis.smembers("SetMem");//枚舉出set 的元素
sadd 方法用來給set 添加新的元素,而srem 則可以對元素進(jìn)行刪除,通過smembers 方法,
能夠枚舉出set 中的所有元素。
sorted sets 是Redis sets 的一個升級版本,它在sets 的基礎(chǔ)之上增加了一個排序的屬性,該
屬性在添加元素時可以指定,sorted sets 將根據(jù)該屬性來進(jìn)行排序, 每次新元素增加后,sorted
sets 會重新對順序進(jìn)行調(diào)整。sorted sets 不僅能夠通過range 正序?qū)et 取值,還能夠通過range
對set 進(jìn)行逆序取值,極大地提高了set 操作的靈活性:
redis.zadd("SortSetMem", 1, "5th");//插入sort set,并指定元素的序號
redis.zadd("SortSetMem", 2, "4th");
redis.zadd("SortSetMem", 3, "3th");
redis.zadd("SortSetMem", 4, "2th");
redis.zadd("SortSetMem", 5, "1th");
//根據(jù)范圍取set
Set<String> sortset = redis.zrange("SortSetMem", 2, 4);
96 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
//根據(jù)范圍反向取set
Set<String> revsortset = redis.zrevrange("SortSetMem", 1, 2);
通過 zadd 方法來給sorted sets 新增元素,在新增操作的同時,需要指定該元素排序的序號,
以便進(jìn)行排序。使用zrange 方法可以正序?qū)et 進(jìn)行范圍取值,而通過zrevrange 方法,則可以
高效率地逆序?qū)et 進(jìn)行范圍取值。
相較于傳統(tǒng)的關(guān)系型數(shù)據(jù)庫,Redis 有更好的讀/寫吞吐能力,能夠支撐更高的并發(fā)數(shù)。而
相較于其他的key-value 類型的數(shù)據(jù)庫,Redis 能夠提供更為豐富的數(shù)據(jù)類型的支持,能夠更靈
活地滿足業(yè)務(wù)需求。Redis 能夠高效率地實現(xiàn)諸如排序取topN、訪問計數(shù)器、隊列系統(tǒng)、數(shù)據(jù)
排重等業(yè)務(wù)需求,并且通過將服務(wù)器設(shè)置為cache-only,還能夠提供高性能的緩存服務(wù)。相較
于memcache 來說,在性能差別不大的情況下,它能夠支持更為豐富的數(shù)據(jù)類型。
2.3 消息系統(tǒng)
在分布式系統(tǒng)中,消息系統(tǒng)的應(yīng)用十分廣泛,消息可以作為應(yīng)用間通信的一種方式。消息
被保存在隊列中,直到被接收者取出。由于消息發(fā)送者不需要同步等待消息接收者的響應(yīng),消
息的異步接收降低了系統(tǒng)集成的耦合度,提升了分布式系統(tǒng)協(xié)作的效率,使得系統(tǒng)能夠更快地
響應(yīng)用戶,提供更高的吞吐。當(dāng)系統(tǒng)處于峰值壓力時,分布式消息隊列還能夠作為緩沖,削峰
填谷,緩解集群的壓力,避免整個系統(tǒng)被壓垮。
開源的消息系統(tǒng)有很多,包括Apache 的ActiveMQ,Apache 的Kafka、RabbitMQ、memcacheQ
等,本節(jié)將通過Apache 的ActiveMQ 來介紹消息系統(tǒng)的使用與集群架構(gòu)。
2.3.1 ActiveMQ & JMS
ActiveMQ 是Apache 所提供的一個開源的消息系統(tǒng),完全采用Java 來實現(xiàn),因此,它能夠
很好地支持J2EE 提出JMS 規(guī)范。JMS(Java Message Service,即Java 消息服務(wù))是一組Java
應(yīng)用程序接口,它提供消息的創(chuàng)建、發(fā)送、接收、讀取等一系列服務(wù)。JMS 定義了一組公共應(yīng)
用程序接口和相應(yīng)的語法,類似于Java 數(shù)據(jù)庫的統(tǒng)一訪問接口JDBC,它是一種與廠商無關(guān)的
API,使得Java 程序能夠與不同廠商的消息組件很好地進(jìn)行通信。
JMS 支持的消息類型包括簡單文本(TextMessage)、可序列化的對象(ObjectMessage)、鍵
值對(MapMessage)、字節(jié)流(BytesMessage)、流(StreamMessage),以及無有效負(fù)載的消息
(Message)等。消息的發(fā)送是異步的,因此,消息的發(fā)布者發(fā)送完消息之后,不需要等待消息
接收者立即響應(yīng),這樣便提高了分布式系統(tǒng)協(xié)作的效率。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 97
JMS 支持兩種消息發(fā)送和接收模型。一種稱為Point-to-Point(P2P)模型,即采用點對點
的方式發(fā)送消息。P2P 模型是基于queue(隊列)的,消息生產(chǎn)者發(fā)送消息到隊列,消息消費者
從隊列中接收消息,隊列的存在使得消息的異步傳輸稱為可能,P2P 模型在點對點的情況下進(jìn)
行消息傳遞時采用。另一種稱為Pub/Sub(Publish/Subscribe,即發(fā)布/訂閱)模型,發(fā)布/訂閱模
型定義了如何向一個內(nèi)容節(jié)點發(fā)布和訂閱消息,這個內(nèi)容節(jié)點稱為topic(主題)。主題可以認(rèn)
為是消息傳遞的中介,消息發(fā)布者將消息發(fā)布到某個主題,而消息訂閱者則從主題訂閱消息。
主題使得消息的訂閱者與消息的發(fā)布者互相保持獨立,不需要進(jìn)行接觸即可保證消息的傳遞,
發(fā)布/訂閱模型在消息的一對多廣播時采用。
如圖 2-14 所示,對于點對點消息傳輸模型來說,多個消息的生產(chǎn)者和消息的消費者都可以
注冊到同一個消息隊列,當(dāng)消息的生產(chǎn)者發(fā)送一條消息之后,只有其中一個消息消費者會接收
到消息生產(chǎn)者所發(fā)送的消息,而不是所有的消息消費者都會收到該消息。
圖2-14 點對點消息傳輸模型
如圖2-15 所示,對于發(fā)布/訂閱消息傳輸模型來說,消息的發(fā)布者需將消息投遞給topic,
而消息的訂閱者則需要在相應(yīng)的topic 進(jìn)行注冊,以便接收相應(yīng)topic 的消息。與點對點消息傳
輸模型不同的是,消息發(fā)布者的消息將被自動發(fā)送給所有訂閱了該topic 的消息訂閱者。當(dāng)消息
訂閱者某段時間由于某種原因斷開了與消息發(fā)布者的連接時,這個時間段內(nèi)的消息將會丟失,
除非將消息的訂閱模式設(shè)置為持久訂閱(durable subscription),這時消息的發(fā)布者將會為消息
的訂閱者保留這段時間所產(chǎn)生的消息。當(dāng)消息的訂閱者重新連接消息發(fā)布者時,消息訂閱者仍
然可以獲得這部分消息,而不至于丟失這部分消息。
98 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
圖 2-15 發(fā)布/訂閱消息傳輸模型
1. 安裝ActiveMQ
由于ActiveMQ 是純Java 實現(xiàn)的,因此ActiveMQ 的安裝依賴于Java 環(huán)境,關(guān)于Java 環(huán)境
的安裝此處就不詳細(xì)介紹了,請讀者自行查閱相關(guān)資料。
下載 ActiveMQ:
wget http://apache.dataguru.cn/activemq/apache-activemq/5.9.0/apacheactivemq-
5.9.0-bin.tar.gz
解壓安裝文件:
tar -xf apache-activemq-5.9.0-bin.tar.gz
相關(guān)的配置放在{ACTIVEMQ_HOME}/conf 目錄下,可以對配置文件進(jìn)行修改:
ls /usr/activemq
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 99
啟動 ActiveMQ:
./activemq start
2. 通過JMS 訪問ActiveMQ
ActiveMQ 實現(xiàn)了JMS 規(guī)范提供的一系列接口,如創(chuàng)建Session、建立連接、發(fā)送消息等,
通過這些接口,能夠?qū)崿F(xiàn)消息發(fā)送、消息接收、消息發(fā)布、消息訂閱的功能。
使用 JMS 來完成ActiveMQ 基于queue 的點對點消息發(fā)送:
ConnectionFactory connectionFactory = new
ActiveMQConnectionFactory(
ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD,
"tcp://192.168.136.135:61616");
Connection connection = connectionFactory
.createConnection();
connection.start();
Session session = connection.createSession
(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
Destination destination = session
.createQueue("MessageQueue");
MessageProducer producer = session.createProducer(destination);
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
ObjectMessage message = session
.createObjectMessage("hello everyone!");
producer.send(message);
session.commit();
創(chuàng) 建 一 個ActiveMQConnectionFactory , 通過ActiveMQConnectionFactory 來創(chuàng)建到
ActiveMQ 的連接,通過連接創(chuàng)建Session。創(chuàng)建Session 時有兩個非常重要的參數(shù),第一個boolean
100 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
類型的參數(shù)用來表示是否采用事務(wù)消息。如果消息是事務(wù)的,對應(yīng)的該參數(shù)設(shè)置為true,此時
消息的提交自動由comit 處理,消息的回滾則自動由rollback 處理。假如消息不是事務(wù)的,則對
應(yīng)的該參數(shù)設(shè)置為false,此時分為三種情況,Session.AUTO_ACKNOWLEDGE 表示Session 會
自動確認(rèn)所接收到的消息;而Session.CLIENT_ACKNOWLEDGE 則表示由客戶端程序通過調(diào)
用消息的確認(rèn)方法來確認(rèn)所收到的消息;Session.DUPS_OK_ACKNOWLEDGE 這個選項使得
Session 將“懶惰”地確認(rèn)消息,即不會立即確認(rèn)消息,這樣有可能導(dǎo)致消息重復(fù)投遞。Session
創(chuàng)建好以后,通過Session 創(chuàng)建一個queue,queue 的名稱為MessageQueue,消息的發(fā)送者將會
向這個queue 發(fā)送消息。
基于 queue 的點對點消息接收類似:
ConnectionFactory connectionFactory = new
ActiveMQConnectionFactory(
ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD,
"tcp://192.168.136.135:61616");
Connection connection = connectionFactory
.createConnection();
connection.start();
Session session = connection.createSession(Boolean.FALSE,
Session.AUTO_ACKNOWLEDGE);
Destination destination= session
.createQueue("MessageQueue");
MessageConsumer consumer = session
.createConsumer(destination);
while (true) {
//取出消息
ObjectMessage message = (ObjectMessage)consumer.receive(10000);
if (null != message) {
String messageContent = (String)message.getObject();
System.out.println(messageContent);
} else {
break;
}
}
創(chuàng)建 ActiveMQConnectionFactory,通過ActiveMQConnectionFactory 創(chuàng)建連接,通過連接
創(chuàng)建Session,然后創(chuàng)建目的queue(這里為MessageQueue),根據(jù)目的queue 創(chuàng)建消息的消費
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 101
者,消息消費者通過receive 方法來接收Object 消息,然后將消息轉(zhuǎn)換成字符串并打印輸出。
還可以通過JMS 來創(chuàng)建ActiveMQ 的topic,并給topic 發(fā)送消息:
ConnectionFactory factory = new ActiveMQConnectionFactory(
ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD,
"tcp://192.168.136.135:61616");
Connection connection = factory.createConnection();
connection.start();
Session session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic("MessageTopic");
MessageProducer producer = session.createProducer(topic);
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
TextMessage message = session.createTextMessage();
message.setText("message_hello_chenkangxian");
producer.send(message);
與 發(fā) 送 點 對點消息一樣, 首先需要初始化ActiveMQConnectionFactory , 通過
ActiveMQConnectionFactory 創(chuàng)建連接,通過連接創(chuàng)建Session。然后再通過Session 創(chuàng)建對應(yīng)的
topic,這里指定的topic 為MessageTopic。創(chuàng)建好topic 之后,通過Session 創(chuàng)建對應(yīng)消息producer,
然后創(chuàng)建一條文本消息,消息內(nèi)容為message_hello_chenkangxian,通過producer 發(fā)送。
消息發(fā)送到對應(yīng)的topic 后,需要將listener 注冊到需要訂閱的topic 上,以便能夠接收該topic
的消息:
ConnectionFactory factory = new ActiveMQConnectionFactory(
ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD,
"tcp://192.168.136.135:61616");
Connection connection = factory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic("MessageTopic");
MessageConsumer consumer = session.createConsumer(topic);
102 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
consumer.setMessageListener(new MessageListener() {
public void onMessage(Message message) {
TextMessage tm = (TextMessage) message;
try {
System.out.println(tm.getText());
} catch (JMSException e) {}
}
});
Session 創(chuàng)建好之后,通過Session 創(chuàng)建對應(yīng)的topic,然后通過topic 來創(chuàng)建消息的消費者,
消息的消費者需要在該topic 上注冊一個listener,以便消息發(fā)送到該topic 之后,消息的消費者
能夠及時地接收到。
3. ActiveMQ 集群部署
針對分布式環(huán)境下對系統(tǒng)高可用的嚴(yán)格要求,以及面臨高并發(fā)的用戶訪問,海量的消息發(fā)
送等場景的挑戰(zhàn),單個ActiveMQ 實例往往難以滿足系統(tǒng)高可用與容量擴展的需求,這時
ActiveMQ 的高可用方案及集群部署就顯得十分重要了。
當(dāng)一個應(yīng)用被部署到生產(chǎn)環(huán)境中,進(jìn)行容錯和避免單點故障是十分重要的,這樣可以避免
因為單個節(jié)點的不可用而導(dǎo)致整個系統(tǒng)的不可用。目前ActiveMQ 所提供的高可用方案主要是
基于Master-Slave 模式實現(xiàn)的冷備方案,較為常用的包括基于共享文件系統(tǒng)的Master-Slave 架
構(gòu)和基于共享數(shù)據(jù)庫的Master-Slave 架構(gòu)23。
如圖 2-16 所示,當(dāng)Master 啟動時,它會獲得共享文件系統(tǒng)的排他鎖,而其他Slave 則stand-by,
不對外提供服務(wù),同時等待獲取Master 的排他鎖。假如Master 連接中斷或者發(fā)生異常,那么它
的排他鎖則會立即釋放,此時便會有另外一個Slave 能夠爭奪到Master 的排他鎖,從而成為Master,
對外提供服務(wù)。當(dāng)之前因故障或者連接中斷而丟失排他鎖的Master 重新連接到共享文件系統(tǒng)時,
排他鎖已經(jīng)被搶占了,它將作為Slave 等待,直到Master 再一次發(fā)生異常。
23 關(guān)于ActiveMQ 的高可用架構(gòu)可以參考http://activemq.apache.org/masterslave.html。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 103
圖2-16 基于共享文件系統(tǒng)的Master-Slave 架構(gòu)
基于共享數(shù)據(jù)庫的Master-Slave 架構(gòu)同基于共享文件系統(tǒng)的Master-Slave 架構(gòu)類似,如圖
2-17 所示。當(dāng)Master 啟動時,會先獲取數(shù)據(jù)庫某個表的排他鎖,而其他Slave 則stand-by,等
待表鎖,直到Master 發(fā)生異常,連接丟失。這時表鎖將釋放,其他Slave 將獲得表鎖,從而成
為Master 并對外提供服務(wù),Master 與Slave 自動完成切換,完全不需要人工干預(yù)。
圖2-17 基于共享數(shù)據(jù)庫的Master-Slave 架構(gòu)
104 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
當(dāng)然,客戶端也需要做一些配置,以便當(dāng)服務(wù)端Master 與Slave 切換時,客戶無須重啟和
更改配置就能夠進(jìn)行兼容。在ActiveMQ 的客戶端連接的配置中使用failover 的方式,可以在
Master 失效的情況下,使客戶端自動重新連接到新的Master:
failover:(tcp://master:61616,tcp://slave1:61616,tcp://slave2:61616)
假設(shè) Master 失效,客戶端能夠自動地連接到Slave1 和Slave2 兩臺當(dāng)中成功獲取排他鎖的
新Master。
當(dāng)系統(tǒng)規(guī)模不斷地發(fā)展,產(chǎn)生和消費消息的客戶端越來越多,并發(fā)的請求數(shù)以及發(fā)送的消
息量不斷增加,使得系統(tǒng)逐漸地不堪重負(fù)。采用垂直擴展可以提升ActiveMQ 單broker 的處理
能力。擴展最直接的辦法就是提升硬件的性能,如提高CPU 和內(nèi)存的能力,這種方式最為簡單
也最為直接。再者就是就是通過調(diào)節(jié)ActiveMQ 本身的一些配置來提升系統(tǒng)并發(fā)處理的能力,
如使用nio 替代阻塞I/O,提高系統(tǒng)處理并發(fā)請求的能力,或者調(diào)整JVM 與ActiveMQ 可用的
內(nèi)存空間等。由于垂直擴展較為簡單,此處就不再詳細(xì)敘述了。
硬件的性能畢竟不能無限制地提升,垂直擴展到一定程度時,必然會遇到瓶頸,這時就需
要對系統(tǒng)進(jìn)行相應(yīng)的水平擴展。對于ActiveMQ 來說,可以采用broker 拆分的方式,將不相關(guān)
的queue 和topic 拆分到多個broker,來達(dá)到提升系統(tǒng)吞吐能力的目的。
假設(shè)使用消息系統(tǒng)來處理訂單狀態(tài)的流轉(zhuǎn),對應(yīng)的topic 可能包括訂單創(chuàng)建、購買者支付、
售賣者發(fā)貨、購買者確認(rèn)收貨、購買者確認(rèn)付款、購買者發(fā)起退款、售賣者處理退款等,如
圖2-18 所示。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 105
圖 2-18 broker 的拆分
原本一個 broker 可以承載多個queue 或者topic,現(xiàn)在將不相關(guān)的queue 和topic 拆出來放
到多個broker 當(dāng)中,這樣可以將一部分消息量大并發(fā)請求多的queue 獨立出來單獨進(jìn)行處理,
避免了queue 或者topic 之間的相互影響,提高了系統(tǒng)的吞吐量,使系統(tǒng)能夠支撐更大的并發(fā)請
求量及處理更多的消息。當(dāng)然,如有需要,還可以對queue 和topic 進(jìn)行進(jìn)一步的拆分,類似于
數(shù)據(jù)庫的分庫分表策略,以提高系統(tǒng)整體的并發(fā)處理能力。
2.4 垂直化搜索引擎
這里所介紹的垂直化搜索引擎,與大家所熟知的Google 和Baidu 等互聯(lián)網(wǎng)搜索引擎存在著
一些差別。垂直化的搜索引擎主要針對企業(yè)內(nèi)部的自有數(shù)據(jù)的檢索,而不像Google 和Baidu 等
搜索引擎平臺,采用網(wǎng)絡(luò)爬蟲對全網(wǎng)數(shù)據(jù)進(jìn)行抓取,從而建立索引并提供給用戶進(jìn)行檢索。在
分布式系統(tǒng)中,垂直化的搜索引擎是一個非常重要的角色,它既能滿足用戶對于全文檢索、模
糊匹配的需求,解決數(shù)據(jù)庫like 查詢效率低下的問題,又能夠解決分布式環(huán)境下,由于采用分
庫分表或者使用NoSQL 數(shù)據(jù)庫,導(dǎo)致無法進(jìn)行多表關(guān)聯(lián)或者進(jìn)行復(fù)雜查詢的問題。
106 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
本節(jié)將重點介紹搜索引擎的基本原理和Apache Lucence 的使用,以及基于Lucence 的另一
個強大的搜索引擎工具Solr 的一些簡單配置。
2.4.1 Lucene 簡介
要深入理解垂直化搜索引擎的架構(gòu),不得不提到當(dāng)前全球范圍內(nèi)使用十分廣泛的一個開源
檢索工具——Lucene24。Lucene 是Apache 旗下的一款高性能、可伸縮的開源的信息檢索庫,最
初是由Doug Cutting25開發(fā),并在SourceForge 的網(wǎng)站上提供下載。從2001 年9 月開始,Lucene
作為高質(zhì)量的開源Java 產(chǎn)品加入到Apache 軟件基金會,經(jīng)過多年的不斷發(fā)展,Lucene 被翻譯
成C++、C#、perl、Python 等多種語言,在全球范圍內(nèi)眾多知名互聯(lián)網(wǎng)企業(yè)中得到了極為廣泛
的應(yīng)用。通過Lucene,可以十分容易地為應(yīng)用程序添加文本搜索功能,而不必深入地了解搜索
引擎實現(xiàn)的技術(shù)細(xì)節(jié)以及高深的算法,極大地降低了搜索技術(shù)推廣及使用的門檻。
Lucene 與搜索應(yīng)用程序之間的關(guān)系如圖2-19 所示。
24 Lucene 項目地址為https://lucene.apache.org。
25 開源領(lǐng)域的重量級人物,創(chuàng)建了多個成功的開源項目,包括Lucene、Nutch 和Hadoop。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 107
圖 2-19 Lucene 與搜索應(yīng)用程序之間的關(guān)系26
在學(xué)習(xí)使用Lucene 之前,需要理解搜索引擎的幾個重要概念:
倒排索引(inverted index)也稱為反向索引,是搜索引擎中最常見的數(shù)據(jù)結(jié)構(gòu),幾乎所有
的搜索引擎都會用到倒排索引。它將文檔中的詞作為關(guān)鍵字,建立詞與文檔的映射關(guān)系,通過
對倒排索引的檢索,可以根據(jù)詞快速獲取包含這個詞的文檔列表,這對于搜索引擎來說至關(guān)重
要。
分詞又稱為切詞,就是將句子或者段落進(jìn)行切割,從中提取出包含固定語義的詞。對于英
語來說,語言的基本單位就是單詞,因此分詞特別容易,只需要根據(jù)空格/符號/段落進(jìn)行分割,
并且排除停止詞(stop word),提取詞干27即可完成。但是對于中文來說,要將一段文字準(zhǔn)確地
切分成一個個詞,就不那么容易了。中文以字為最小單位,多個字連在一起才能構(gòu)成一個表達(dá)
具體含義的詞。中文會用明顯的標(biāo)點符號來分割句子和段落,唯獨詞沒有一個形式上的分割符,
因此,對于支持中文搜索的搜索引擎來說,需要一個合適的中文分詞工具,以便建立倒排索引。
停止詞(stop word),在英語中包含了a、the、and 這樣使用頻率很高的詞,如果這些詞都
被建到索引中進(jìn)行索引的話,搜索引擎就沒有任何意義了,因為幾乎所有的文檔都會包含這些
詞。對于中文來說也是如此,中文里面也有一些出現(xiàn)頻率很高的詞,如“在”、“這”、“了”、“于”
等,這些詞沒有具體含義,區(qū)分度低,搜索引擎對這些詞進(jìn)行索引沒有任何意義,因此,停止
詞需要被忽略掉。
排序,當(dāng)輸入一個關(guān)鍵字進(jìn)行搜索時,可能會命中許多文檔,搜索引擎給用戶的價值就是
快速地找到需要的文檔,因此,需要將相關(guān)度更大的內(nèi)容排在前面,以便用戶能夠更快地篩選
出有價值的內(nèi)容。這時就需要有適當(dāng)?shù)呐判蛩惴?。一般來說,命中標(biāo)題的文檔將比命中內(nèi)容的
文檔有更高的相關(guān)性,命中多次的文檔比命中一次的文檔有更高的相關(guān)性。商業(yè)化的搜索引擎
的排序規(guī)則十分復(fù)雜,搜索結(jié)果的排序融入了廣告、競價排名等因素,由于涉及的利益廣泛,
一般屬于核心的商業(yè)機密。
另外,關(guān)于Lucene 的幾個概念也值得關(guān)注一下:
文檔(Document),在Lucene 的定義中,文檔是一系列域(Field)的組合,而文檔的域則
代表一系列與文檔相關(guān)的內(nèi)容。與數(shù)據(jù)庫表的記錄的概念有點類似,一行記錄所包含的字段對
應(yīng)的就是文檔的域。舉例來說,一個文檔比如老師的個人信息,可能包括年齡、身高、性別、
個人簡介等內(nèi)容。
域(Field),索引的每個文檔中都包含一個或者多個不同名稱的域,每個域都包含了域的名
26 圖片來源https://www.ibm.com/developerworks/cn/java/j-lo-lucene1/fig001.jpg。
27 提取詞干是西方語言特有的處理步驟,比如英文中的單詞有單復(fù)數(shù)的變形,-ing 和-ed 的變形,但是在
搜索引擎中,應(yīng)該當(dāng)作同一個詞。
108 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
稱和域?qū)?yīng)的值,并且域還可以是不同的類型,如字符串、整型、浮點型等。
詞(Term),Term 是搜索的基本單元,與Field 對應(yīng),它包括了搜索的域的名稱以及搜索的
關(guān)鍵詞,可以用它來查詢指定域中包含特定內(nèi)容的文檔。
查詢(Query),最基本的查詢可能是一系列Term 的條件組合,稱為TermQuery,但也有可
能是短語查詢(PhraseQuery)、前綴查詢(PrefixQuery)、范圍查詢(包括TermRangeQuery、
NumericRangeQuery 等)等。
分詞器(Analyzer),文檔在被索引之前,需要經(jīng)過分詞器處理,以提取關(guān)鍵的語義單元,
建立索引,并剔除無用的信息,如停止詞等,以提高查詢的準(zhǔn)確性。中文分詞與西文分詞的區(qū)
別在于,中文對于詞的提取更為復(fù)雜。常用的中文分詞器包括一元分詞28、二元分詞29、詞庫分
詞30等。
如圖 2-20 所示,Lucene 索引的構(gòu)建過程大致分為這樣幾個步驟,通過指定的數(shù)據(jù)格式,將
Lucene 的Document 傳遞給分詞器Analyzer 進(jìn)行分詞,經(jīng)過分詞器分詞之后,通過索引寫入工
具IndexWriter 將索引寫入到指定的目錄。
圖 2-20 Lucene 索引的構(gòu)建過程
而對索引的查詢,大概可以分為如
下幾個步驟,如圖2-21 所示。首先構(gòu)
建查詢的Query,通過IndexSearcher
進(jìn)行查詢,得到命中的TopDocs。然后
通過TopDocs 的scoreDocs()方法,拿到
ScoreDoc,通過ScoreDoc,得到對應(yīng)的
文檔編號,IndexSearcher 通過文檔編
號,使用IndexReader 對指定目錄下的
索引內(nèi)容進(jìn)行讀取,得到命中的文檔后
28 一元分詞,即將給定的字符串以一個字為單位進(jìn)行切割分詞,這種分詞方式較為明顯的缺陷就是語義
不準(zhǔn),如“上海”兩個字被切割成“上”、“?!?,但是包含“上?!薄ⅰ昂I稀钡奈臋n都會命中。
29 二元分詞比一元分詞更符合中文的習(xí)慣,因為中文的大部分詞匯都是兩個字,但是問題依然存在。
30 詞庫分詞就是使用詞庫中定義的詞來對字符串進(jìn)行切分,這樣的好處是分詞更為準(zhǔn)確,但是效率較N
元分詞更低,且難以識別互聯(lián)網(wǎng)世界中層出不窮的新興詞匯。
圖2-21 Lucene 索引搜索過程
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 109
返回。
2.4.2 Lucene 的使用
Lucene 為搜索引擎提供了強大的、令人驚嘆的API,在企業(yè)的垂直化搜索領(lǐng)域得到了極為
廣泛的應(yīng)用。為了學(xué)習(xí)搜索引擎的基本原理,有效地使用Lucene,并將其引入到我們的應(yīng)用程
序當(dāng)中,本節(jié)將介紹Lucene 的一些常用的API 和使用方法,以及索引的優(yōu)化和分布式擴展。
1. 構(gòu)建索引
在執(zhí)行搜索之前,先要構(gòu)建搜索的索引:
Directory dir = FSDirectory.open(new File(indexPath));
Analyzer analyzer = new StandardAnalyzer();
Document doc = new Document();
doc.add(new Field("name","zhansan",Store.YES,Index.ANALYZED));
doc.add(new Field("address","hangzhou",Store.YES,Index.ANALYZED));
doc.add(new Field("sex","man",Store.YES,Index.NOT_ANALYZED));
doc.add(new Field("introduce","i am a coder,my name is zhansan",Store.YES,
Index.NO));
IndexWriter indexWriter = new IndexWriter(dir,analyzer, MaxFieldLength.LIMITED);
indexWriter.addDocument(doc);
indexWriter.close();
首先需要構(gòu)建索引存儲的目錄Directory,索引最終將被存放到該目錄。然后初始化
Document,給Document 添加Field,包括名稱、地址、性別和個人介紹信息。Field 的第一個參
數(shù)為Field 的名稱;第二個參數(shù)為Filed 的值;第三個參數(shù)表示該Field 是否會被存儲。Store.NO
表示索引中不存儲該Field;Store.YES 表示索引中存儲該Field;如果是Store.COMPRESS,則
表示壓縮存儲。最后一個參數(shù)表示是否對該字段進(jìn)行檢索。Index.ANALYZED 表示需對該字段
進(jìn)行全文檢索,該Field 需要使用分詞器進(jìn)行分詞;Index.NOT_ANALYZED 表示不進(jìn)行全文檢
索,因此不需要分詞;Index.NO 表示不進(jìn)行索引。創(chuàng)建一個IndexWriter,用來寫入索引,初始
化時需要指定索引存放的目錄,以及索引建立時使用的分詞器,此處用的是Lucene 自帶的中文
分詞器StandardAnalyzer,最后一個參數(shù)則用來指定是否限制Field 的最大長度。
2. 索引更新與刪除
很多情況下,在搜索引擎首次構(gòu)建完索引之后,數(shù)據(jù)還有可能再次被更改,此時如果不將
最新的數(shù)據(jù)同步到搜索引擎,則有可能檢索到過期的數(shù)據(jù)。遺憾的是,Lucene 暫時還不支持對
于Document 單個Field 或者整個Document 的更新,因此這里所說的更新,實際上是刪除舊的
110 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
Document,然后再向索引中添加新的Document。所添加的新的Document 必須包含所有的Field,
包括沒有更改的Field:
IndexWriter indexWriter = new IndexWriter(dir,analyzer, MaxFieldLength.LIMITED);
indexWriter.deleteDocuments(new Term("name","zhansan"));
indexWriter.addDocument(doc);
IndexWriter 的deleteDocuments 可以根據(jù)Term 來刪除Document。請注意Term 匹配的準(zhǔn)確
性,一個不正確的Term 可能會導(dǎo)致搜索引擎的大量索引被誤刪。Lucene 的IndexWriter 也提供
經(jīng)過封裝的updateDocument 方法,其實質(zhì)仍然是先刪除Term 所匹配的索引,然后再新增對應(yīng)
的Document:
indexWriter.updateDocument(new Term("name","zhansan"), doc);
3. 條件查詢
索引構(gòu)建完之后,就需要對相關(guān)的內(nèi)容進(jìn)行查詢:
String queryStr = "zhansan";
String[] fields = {"name","introduce"};
Analyzer analyzer = new StandardAnalyzer();
QueryParser queryPaser = new MultiFieldQueryParser(fields, analyzer);
Query query = queryPaser.parse(queryStr);
IndexSearcher indexSearcher = new IndexSearcher(indexPath);
Filter filter = null;
TopDocs topDocs = indexSearcher.search(query, filter, 10000);
System.out.println("hits :" + topDocs.totalHits );
for(ScoreDoc scoreDoc : topDocs.scoreDocs){
int docNum = scoreDoc.doc;
Document doc = indexSearcher.doc(docNum);
printDocumentInfo(doc);
}
查詢所使用的字符串為人名zhansan,查詢的Field 包括name 和introduce。構(gòu)建一個查詢
MultiFieldQueryParser 解析器,對查詢的內(nèi)容進(jìn)行解析,生成Query;然后通過IndexSearcher
來對Query 進(jìn)行查詢,查詢將返回TopDocs,TopDocs 中包含了命中的總條數(shù)與命中的Document
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 111
的文檔編號;最后通過IndexSearcher 讀取指定文檔編號的文檔內(nèi)容,并進(jìn)行輸出。
Lucene 支持多種查詢方式,比如針對某個Field 進(jìn)行關(guān)鍵字查詢:
Term term = new Term("name","zhansan");
Query termQuery = new TermQuery(term);
Term 中包含了查詢的Field 的名稱與需要匹配的文本值,termQuery 將命中名稱為name 的
Field 中包含zhansan 這個關(guān)鍵字的Document。
也可以針對某個范圍對Field 的值進(jìn)行區(qū)間查詢:
NumericRangeQuery numericRangeQuery
= NumericRangeQuery.newIntRange("size", 2, 100, true, true);
假設(shè) Document 包含一個名稱為size 的數(shù)值型的Field,可以針對size 進(jìn)行范圍查詢,指定
查詢的范圍為2~100,后面兩個參數(shù)表示是否包含查詢的邊界值。
還可以通過通配符來對Field 進(jìn)行查詢:
Term wildcardTerm = new Term("name","zhansa?");
WildcardQuery wildcardQuery = new WildcardQuery(wildcardTerm);
通配符可以讓我們使用不完整、缺少某些字母的項進(jìn)行查詢,但是仍然能夠查詢到匹配的
結(jié)果,如指定對name 的查詢內(nèi)容為“zhansa?”,?表示0 個或者一個字母,這將命中name 的值
為zhansan 的Document,如果使用*,則代表0 個或者多個字母。
假設(shè)某一段落中包含這樣一句話“I have a lovely white dog and a black lazy cat”,即使不知
道這句話的完整寫法,也可以通過PhraseQuery 查找到包含dog 和cat 兩個關(guān)鍵字,并且dog 和
cat 之間的距離不超過5 個單詞的document:
PhraseQuery phraseQuery = new PhraseQuery();
phraseQuery.add(new Term("content","dog"));
phraseQuery.add(new Term("content","cat"));
phraseQuery.setSlop(5);
其中,content 為查詢對應(yīng)的Field,dog 和cat 分別為查詢的短語,而phraseQuery.setSlop(5)
表示兩個短語之間最多不超過5 個單詞,兩個Field 之間所允許的最大距離稱為slop。
除這些之外,Lucene 還支持將不同條件組合起來進(jìn)行復(fù)雜查詢:
PhraseQuery query1 = new PhraseQuery();
query1.add(new Term("content","dog"));
query1.add(new Term("content","cat"));
query1.setSlop(5);
112 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
Term wildTerm = new Term("name","zhans?");
WildcardQuery query2 = new WildcardQuery(wildTerm);
BooleanQuery booleanQuery = new BooleanQuery();
booleanQuery.add(query1,Occur.MUST);
booleanQuery.add(query2,Occur.MUST);
query1 為前面所說的短語查詢,而query2 則為通配符查詢,通過BooleanQuery 將兩個查
詢條件組合起來。需要注意的是,Occur.MUST 表示只有符合該條件的Document 才會被包含在
查詢結(jié)果中;Occur.SHOULD 表示該條件是可選的;Occur.MUST_NOT 表示只有不符合該條件
的Document 才能夠被包含到查詢結(jié)果中。
4. 結(jié)果排序
Lucene 不僅支持多個條件的復(fù)雜查詢,還支持按照指定的Field 對查詢結(jié)果進(jìn)行排序:
String queryStr = "lishi";
String[] fields = {"name","address","size"};
Sort sort = new Sort();
SortField field = new SortField("size",SortField.INT, true);
sort.setSort(field);
Analyzer analyzer = new StandardAnalyzer();
QueryParser queryParse = new MultiFieldQueryParser(fields, analyzer);
Query query = queryParse.parse(queryStr);
IndexSearcher indexSearcher = new IndexSearcher(indexPath);
Filter filter = null;
TopDocs topDocs = indexSearcher.search(query, filter, 100, sort);
for(ScoreDoc scoreDoc : topDocs.scoreDocs){
int docNum = scoreDoc.doc;
Document doc = indexSearcher.doc(docNum);
printDocumentInfo(doc);
}
通過新建一個Sort,指定排序的Field 為size,F(xiàn)ield 的類型為SortField.INT,表示按照整數(shù)
類型進(jìn)行排序,而不是字符串類型,SortField 的第三個參數(shù)用來指定是否對排序結(jié)果進(jìn)行反轉(zhuǎn)。
在查詢時,使用IndexSearcher 的一個重構(gòu)方法,帶上Sort 參數(shù),則能夠讓查詢的結(jié)果按照指定
的字段進(jìn)行排序:
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 113
如果是多個Field 同時進(jìn)行查詢,可以指定每個Field 擁有不同的權(quán)重,以便匹配時可以按
照Document 的相關(guān)度進(jìn)行排序:
String queryStr = "zhansan shanghai";
String[] fields = {"name","address","size"};
Map<String,Float> weights = new HashMap<String, Float>();
weights.put("name", 4f);
weights.put("address", 2f);
Analyzer analyzer = new StandardAnalyzer();
QueryParser queryParse = new MultiFieldQueryParser(fields, analyzer,
weights);
Query query = queryParse.parse(queryStr);
IndexSearcher indexSearcher = new IndexSearcher(indexPath);
Filter filter = null;
TopDocs topDocs = indexSearcher.search(query, filter, 100);
for(ScoreDoc scoreDoc : topDocs.scoreDocs){
int docNum = scoreDoc.doc;
Document doc = indexSearcher.doc(docNum);
printDocumentInfo(doc);
}
114 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
假設(shè)查詢串中包含zhansan 和shanghai 兩個查詢串,設(shè)置Field name 的權(quán)重為4,而設(shè)置
Field address 的權(quán)重為2,如按照Field 的權(quán)重進(jìn)行查詢排序,那么同時包含zhansan 和shanghai
的Document 將排在最前面,其次是name 為zhansan 的Document,最后是address 為shanghai
的Document:
5. 高亮
查詢到匹配的文檔后,需要對匹配的內(nèi)容進(jìn)行突出展現(xiàn),最直接的方式就是對匹配的內(nèi)容
高亮顯示。對于搜索list 來說,由于文檔的內(nèi)容可能比較長,為了控制展示效果,還需要對文
檔的內(nèi)容進(jìn)行摘要,提取相關(guān)度最高的內(nèi)容進(jìn)行展現(xiàn),Lucene 都能夠很好地滿足這些需求:
Formatter formatter = new SimpleHTMLFormatter("<font color='red'>","</font>");
Scorer scorer = new QueryScorer(query);
Highlighter highLight = new Highlighter(formatter, scorer);
Fragmenter fragmenter = new SimpleFragmenter(20);
highLight.setTextFragmenter(fragmenter);
通過構(gòu)建高亮的Formatter 來指定高亮的HTML 前綴和HTML 后綴,這里用的是font 標(biāo)簽。
查詢短語在被分詞后構(gòu)建一個QueryScorer,QueryScorer 中包含需要高亮顯示的關(guān)鍵字,
Fragmenter 則用來對較長的Field 內(nèi)容進(jìn)行摘要,提取相關(guān)度較大的內(nèi)容,參數(shù)20 表示截取前
20 個字符進(jìn)行展現(xiàn)。構(gòu)建一個Highlighter,用來對Document 的指定Field 進(jìn)行高亮格式化:
String hi = highLight.getBestFragment(analyzer, "introduce", doc.get
("introduce"));
查詢命中相應(yīng)的Document 后,通過構(gòu)建的Highlighter,對Document 指定的Field 進(jìn)行高
亮格式化,并且對相關(guān)度最大的一塊內(nèi)容進(jìn)行摘要,得到摘要內(nèi)容。假設(shè)對dog 進(jìn)行搜索,
introduce 中如包含有dog,那么使用Highlighter 高亮并摘要后的內(nèi)容如下:
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 115
6. 中文分詞
Lucene 提供的標(biāo)準(zhǔn)中文分詞器StandardAnalyzer 只能夠進(jìn)行簡單的一元分詞,一元分詞以
一個字為單位進(jìn)行語義切分,這種本來為西文所設(shè)計的分詞器,用于中文的分詞時經(jīng)常會出現(xiàn)
語義不準(zhǔn)確的情況。可以通過使用一些其他中文分詞器來避免這種情況,常用的中文分詞器包
括Lucene 自帶的中日韓文分詞器CJKAnalyzer,國內(nèi)也有一些開源的中文分詞器,包括IK 分
詞31、MM 分詞32,以及庖丁分詞33、imdict 分詞器34等。假設(shè)有下面一段文字:
String zhContent = "我是一個中國人,我熱愛我的國家";
分詞之后,通過下面一段代碼可以將分詞的結(jié)果打印輸出:
System.out.println("\n 分詞器:" + analyze.getClass());
TokenStream tokenStream = analyze.tokenStream("content", new StringReader(text));
Token token = tokenStream.next();
while(token != null){
System.out.println(token);
token = tokenStream.next();
}
通過 StandardAnalyzer 分詞得到的分詞結(jié)果如下:
Analyzer standarAnalyzer = new StandardAnalyzer(Version.LUCENE_CURRENT);
由此可以得知,StandardAnalyzer 采用的是一元分詞,即字符串以一個字為單位進(jìn)行切割。
使用 CJKAnalyzer 分詞器進(jìn)行分詞,得到的結(jié)果如下:
31 IK 分詞項目地址為https://code.google.com/p/ik-analyzer。
32 MM 分詞項目地址為https://code.google.com/p/mmseg4j。
33 庖丁分詞項目地址為https://code.google.com/p/paoding。
34 imdict 分詞項目地址為https://code.google.com/p/imdict-chinese-analyzer。
116 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
Analyzer cjkAnalyzer = new CJKAnalyzer();
通過分詞的結(jié)果可以看到,CJKAnalyzer 采用的是二元分詞,即字符串以兩個字為單位進(jìn)
行切割。
使用開源的IK 分詞的效果如下:
Analyzer ikAnalyzer = new IKAnalyzer()
可以看到,分詞的效果比單純的一元或者二元分詞要好很多。
使用 MM 分詞器分詞的效果如下:
Analyzer mmAnalyzer = new MMAnalyzer()
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 117
7. 索引優(yōu)化
Lucene 的索引是由段(segment)組成的,每個段可能又包含多個索引文件,即每個段包含
了一個或者多個Document;段結(jié)構(gòu)使得Lucene 可以很好地支持增量索引,新增的Document
將被添加到新的索引段當(dāng)中。但是,當(dāng)越來越多的段被添加到索引當(dāng)中時,索引文件也就越來
越多。一般來說,操作系統(tǒng)對于進(jìn)程打開的文件句柄數(shù)是有限的,當(dāng)一個進(jìn)程打開太多的文件
時,會拋出too many open files 異常,并且執(zhí)行搜索任務(wù)時,Lucene 必須分別搜索每個段,然后
將各個段的搜索結(jié)果合并,這樣查詢的性能就會降低。
為了提高 Lucene 索引的查詢性能,當(dāng)索引段的數(shù)量達(dá)到設(shè)置的上限時,Lucene 會自動進(jìn)
行索引段的優(yōu)化,將索引段合并成為一個,以提高查詢的性能,并減少進(jìn)程打開的文件句柄數(shù)
量。但是,索引段的合并需要大量的I/O 操作,并且需要耗費相當(dāng)?shù)臅r間。雖然這樣的工作做
完以后,可以提高搜索引擎查詢的性能,但在索引合并的過程中,查詢的性能將受到很大影響,
這對于前臺應(yīng)用來說一般是難以接受的。
因此,為了提高搜索引擎的查詢性能,需要盡可能地減少索引段的數(shù)量,另外,對于需要
應(yīng)對前端高并發(fā)查詢的應(yīng)用來說,對索引的自動合并行為也需要進(jìn)行抑制,以提高查詢的性能。
一般來說,在分布式環(huán)境下,會安排專門的集群來生成索引,并且生成索引的集群不負(fù)責(zé)
處理前臺的查詢請求。當(dāng)索引生成以后,通過索引優(yōu)化,對索引的段進(jìn)行合并。合并完以后,
將生成好的索引文件分發(fā)到提供查詢服務(wù)的機器供前臺應(yīng)用查詢。當(dāng)然,數(shù)據(jù)會不斷地更新,
索引文件如何應(yīng)對增量的數(shù)據(jù)更新也是一個挑戰(zhàn)。對于少量索引來說,可以定時進(jìn)行全量的索
引重建,并且將索引推送到集群的其他機器,前提是相關(guān)業(yè)務(wù)系統(tǒng)能夠容忍數(shù)據(jù)有一定延遲。
但是,當(dāng)數(shù)據(jù)量過于龐大時,索引的構(gòu)建需要很長的時間,延遲的時間可能無法忍受,因此,
我們不得不接受索引有一定的瑕疵,即索引同時包含多個索引段,增量的更新請求將不斷地發(fā)
送給查詢機器。查詢機器可以將索引加載到內(nèi)存,并以固定的頻率回寫磁盤,每隔一定的周期,
對索引進(jìn)行一次全量的重建操作,以將增量更新所生成的索引段進(jìn)行合并。
8. 分布式擴展
與其他的分布式系統(tǒng)架構(gòu)類似,基于Lucene 的搜索引擎也會面臨擴展的問題,單臺機器難
以承受訪問量不斷上升的壓力,不得不對其進(jìn)行擴展。但是,與其他應(yīng)用不同的是,搜索應(yīng)用
大部分場景都能夠接受一定時間的數(shù)據(jù)延遲,對于數(shù)據(jù)一致性的要求并不那么高,大部分情況
下只要能夠保障數(shù)據(jù)的最終一致性,可以容忍一定時間上的數(shù)據(jù)不同步,一種擴展的方式如
圖2-22 所示。
每個 query server 實例保存一份完整的索引,該索引由dump server 周期性地生成,并進(jìn)行
索引段的合并,索引生成好之后推送到每臺query server 進(jìn)行替換,這樣避免集群索引dump 對
后端數(shù)據(jù)存儲造成壓力。當(dāng)然,對于增量的索引數(shù)據(jù)更新,dump server 可以異步地將更新推送
到每臺query server,或者是query server 周期性地到dump server 進(jìn)行數(shù)據(jù)同步,以保證數(shù)據(jù)最
118 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
終的一致性。對于前端的client 應(yīng)用來說,通過對請求進(jìn)行Hash,將請求均衡地分發(fā)到集群中
的每臺服務(wù)器,使得壓力能夠較為均衡地分布,這樣即達(dá)到了系統(tǒng)擴展的目的。
圖2-22 搜索引擎索引的讀寫分離
索引的讀/寫分離解決的是請求分布的問題,而對于數(shù)據(jù)量龐大的搜索引擎來說,單機對索
引的存儲能力畢竟有限。而且隨著索引數(shù)量的增加,檢索的速度也會隨之下降。此時索引本身
已經(jīng)成為系統(tǒng)的瓶頸,需要對索引進(jìn)行切分,將索引分布到集群的各臺機器上,以提高查詢性
能,降低存儲壓力,如圖2-23 所示。
圖2-23 索引的切分
在如圖 2-24 所示的架構(gòu)中,索引依據(jù)uniquekey%N,被切分到多臺index server 中進(jìn)行存
儲。client 應(yīng)用的查詢請求提交到merge server,merge server 將請求分發(fā)到index server 進(jìn)行檢
索,最后將查詢的結(jié)果進(jìn)行合并后,返回給client 應(yīng)用。對于全量的索引構(gòu)建,可以使用dump
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 119
server 集群,以加快索引構(gòu)建的速度,并分擔(dān)存儲的壓力。而增量的更新請求,可以根據(jù)索引
的uniquekey 取模,將索引同步到index server;為避免merge server 出現(xiàn)單點,可以對merge server
進(jìn)行高可用部署。當(dāng)然,索引切分的方案并非完美,可能也會帶來一些問題。舉例來說,假如
查詢請求需要進(jìn)行結(jié)果排序,當(dāng)索引沒有切分時很好處理,只需要按照查詢指定的條件排列即
可,但是對切分后的索引來說,排序請求將被分發(fā)到每一臺index server 執(zhí)行排序,排完以后取
topN(出于性能考慮)發(fā)送到merge server 進(jìn)行合并,合并后的結(jié)果與真正的結(jié)果很可能存在
偏差,這就需要在業(yè)務(wù)上進(jìn)行取舍。
有的時候,可能既面臨高并發(fā)的用戶訪問請求,又需要對海量的數(shù)據(jù)集進(jìn)行索引,這時就需
要綜合上述的兩種方法,即既采用索引讀寫分離的方式,以支撐更大的并發(fā)訪問量,又采用索引
切分的方式,以解決數(shù)據(jù)量膨脹所導(dǎo)致的存儲壓力以及索引性能下降的問題,如圖2-24 所示。
圖2-24 既進(jìn)行讀寫分離,又進(jìn)行索引切分
merge server 與index server 作為一組基本單元進(jìn)行復(fù)制,而前端應(yīng)用的請求通過Hash 被分
發(fā)到不同的組進(jìn)行處理;每一組與之前類似,使用merge server 將請求分發(fā)到index server 進(jìn)行
索引的查詢;查詢的結(jié)果將在merge server 進(jìn)行合并,合并完以后,再將結(jié)果返回給client。
120 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
2.4.3 Solr
Solr 是一個基于Lucene、功能強大的搜索引擎工具,它對Lucene 進(jìn)行了擴展,提供一系列
功能強大的HTTP 操作接口,支持通過Data Schema 來定義字段、類型和設(shè)置文本分析,使得
用戶可以通過HTTP POST 請求,向服務(wù)器提交Document,生成索引,以及進(jìn)行索引的更新和
刪除操作。對于復(fù)雜的查詢條件,Solr 提供了一整套表達(dá)式查詢語言,能夠更方便地實現(xiàn)包括
字段匹配、模糊查詢、分組統(tǒng)計等功能;同時,Solr 還提供了強大的可配置能力,以及功能完
善的后臺管理系統(tǒng)。Solr 的架構(gòu)如圖2-25 所示。
圖2-25 Solr 的架構(gòu)35
1. Solr 的配置
通過 Solr 的官方站點下載Solr:
wget http://apache.fayea.com/apache-mirror/lucene/solr/4.7.2/solr-4.7.2.tgz
35 圖片來源http://images.cnitblog.com/blog/483523/201308/20142655-8e3153496cf244a280c5e195232ba962.x-png。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 121
解壓:
tar -xf solr-4.7.2.tgz
修改 Tomcat 的conf/server.xml 中的Connector 配置,將URIEncoding 編碼設(shè)置為UTF-8,
否則中文將會亂碼,從而導(dǎo)致搜索查詢不到結(jié)果。
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" URIEncoding="UTF-8"/>
將 Solr 的dist 目錄下的solr-{version}.war 包復(fù)制到tomcat 的webapps 目錄下,并且重命名
為solr.war。
配置 Solr 的home 目錄,包括schema 文件、solrconfig 文件及索引文件,如果是第一次配
置Solr,可以直接復(fù)制example 目錄下的Solr 目錄作為Solr 的home,并通過修改tomcat 的啟
動腳本catalina.sh 來指定solr.solr.home 變量所代表的Solr home 路徑。
CATALINA_OPTS="$CATALINA_OPTS -Dsolr.solr.home=/usr/solr"
啟動 Tomcat,訪問Solr 的管理頁面,如圖2-26 所示。
圖2-26 Solr 的管理頁面
2. 構(gòu)建索引
在構(gòu)建索引之前,首先需要定義好Document 的schema。同數(shù)據(jù)庫建表有點類似,即每個
Document 包含哪些Field,對應(yīng)的Field 的name 是什么,F(xiàn)ield 是什么類型,是否被索引,是否
被存儲,等等。假設(shè)我們要構(gòu)建一個討論社區(qū),需要對社區(qū)內(nèi)的帖子進(jìn)行搜索,那么搜索引擎
的Document 中應(yīng)該包含帖子信息、版塊信息、版主信息、發(fā)帖人信息、回復(fù)總數(shù)等內(nèi)容的聚
合,如圖2-27 所示。
122 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
圖 2-27 帖子、版塊、用戶、評論總數(shù)的關(guān)聯(lián)關(guān)系
其中,post 用來描述用戶發(fā)布的帖子信息,section 則表示版塊信息,user 代表該社區(qū)的用
戶,comment_count 用來記錄帖子的評價總數(shù)。
對帖子信息建立搜索引擎的好處在于,由于帖子的數(shù)據(jù)量大,如采用MySQL 這一類的關(guān)
系型數(shù)據(jù)庫來進(jìn)行存儲的話,需要進(jìn)行分庫分表。數(shù)據(jù)經(jīng)過拆分之后,就難以同時滿足多維度
復(fù)雜條件查詢的需求,并且查詢可能需要版塊、帖子、用戶等多個表進(jìn)行關(guān)聯(lián)查詢,導(dǎo)致查詢
性能下降,甚至回帖總數(shù)這樣的數(shù)據(jù)有可能根本就沒有存儲在關(guān)系型數(shù)據(jù)庫當(dāng)中,而通過搜索
引擎,這些需求都能夠很好地得到滿足。
搜索引擎對應(yīng)的schema 文件定義可能是下面這個樣子:
<?xml version="1.0" encoding="UTF-8" ?>
<schema name="post" version="1.5">
<fields>
<field name="_version_" type="long" indexed="true" stored="true"/>
<field name="post_id" type="long" indexed="true" stored="true" required=
"true"/>
<field name="post_title" type="string" indexed="true" stored="true"/>
<field name="poster_id" type="long" indexed="true" stored="true" />
<field name="poster_nick" type="string" indexed="true" stored="true"/>
<field name="post_content" type="text_general" indexed="true" stored=
"true"/>
<field name="poster_degree" type="int" indexed="true" stored="true"/>
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 123
<field name="section_id" type="long" indexed="true" stored="true" />
<field name="section_name" type="string" indexed="true" stored="true" />
<field name="section_owner_id" type="long" indexed="true" stored="true"/>
<field name="section_owner_nick" type="string" indexed="true" stored="true"/>
<field name="gmt_modified" type="date" indexed="true" stored="true"/>
<field name="gmt_create" type="date" indexed="true" stored="true"/>
<field name="comment_count" type="int" indexed="true" stored="true"/>
<field name="text" type="text_general" indexed="true" stored="false"
multiValued="true"/>
</fields>
<uniqueKey>post_id</uniqueKey>
<copyField source="post_content" dest="text"/>
<copyField source="post_content" dest="text"/>
<copyField source="section_name" dest="text"/>
<types>
<fieldType name="string" class="solr.StrField" sortMissingLast="true" />
<fieldType name="int" class="solr.TrieIntField" precisionStep="0"
positionIncrementGap="0"/>
<fieldType name="long" class="solr.TrieLongField" precisionStep="0"
positionIncrementGap="0"/>
<fieldType name="date" class="solr.TrieDateField" precisionStep="0"
positionIncrementGap="0"/>
<fieldType name="text_general" class="solr.TextField" positionIncrementGap=
"100">
<analyzer type="index">
<tokenizer class="solr.StandardTokenizerFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.StandardTokenizerFactory"/>
</analyzer>
</fieldType>
</types>
</schema>
124 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
fields 標(biāo)簽中所包含的就是定義的這些字段,包括對應(yīng)的字段名稱、字段類型、是否索引、
是否存儲、是否多值等;uniqueKey 指定了Document 的唯一鍵約束;types 標(biāo)簽中則定義了可
能用到的數(shù)據(jù)類型。
使用 HTTP POST 請求可以給搜索引擎添加或者更新已存在的索引:
http://hostname:8080/solr/core/update?wt=json
POST 的JSON 內(nèi)容:
{
"add": {
"doc": {
"post_id": "123456",
"post_title": "Nginx 1.6 穩(wěn)定版發(fā)布,頂級網(wǎng)站用量超越Apache",
"poster_id": "340032",
"poster_nick": "hello123",
"post_content": "據(jù)W3Techs 統(tǒng)計數(shù)據(jù)顯示,全球Alexa 排名前100 萬的網(wǎng)站
中的23.3%都在使用nginx,在排名前10 萬的網(wǎng)站中,這一數(shù)據(jù)為30.7%,而在前1000 名的網(wǎng)站中,
nginx 的使用量超過了Apache,位居第1 位。",
"poster_degree": "2",
"section_id": "422",
"section_name": "技術(shù)",
"section_owner_id": "232133333",
"section_owner_nick": "chenkangxian",
"gmt_modified": "2013-05-07T12:09:12Z",
"gmt_create": "2013-05-07T12:09:12Z",
"comment_count": "3"
},
"boost": 1,
"overwrite": true,
"commitWithin": 1000
}
}
服務(wù)端的響應(yīng):
{
"responseHeader": {
"status": 0,
"QTime": 14
}
}
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 125
通過上述的HTTP POST 請求,便可將Document 添加到搜索引擎中。
3. 條件查詢
比 Lucene 更進(jìn)一步的是,Solr 支持將復(fù)雜條件組裝成HTTP 請求的參數(shù)表達(dá)式,使得用戶
能夠快速構(gòu)建復(fù)雜多樣的查詢條件,包括條件查詢、過濾查詢、僅返回指定字段、分頁、排序、
高亮、統(tǒng)計等,并且支持XML、JSON 等格式的輸出。舉例來說,假如需要根據(jù)post_id(帖子
id)來查詢對應(yīng)的帖子,可以使用下面的查詢請求:
http://hostname:8080/solr/core/select?q=post_id:123458&wt=json&indent=true
返回的Document 格式如下:
{
"responseHeader": {
"status": 0,
"QTime": 0,
"params": {
"indent": "true",
"q": "post_id:123458",
"wt": "json"
}
},
"response": {
"numFound": 1,
"start": 0,
"docs": [
{
"post_id": 123458,
"post_title": "美軍研發(fā)光學(xué)雷達(dá)衛(wèi)星可拍三維高分辨率照片",
"poster_id": 340032,
"poster_nick": "hello123",
"post_content": "繼廣域動態(tài)圖像、全動態(tài)視頻和超光譜技術(shù)之后,Lidar
技術(shù)也受到關(guān)注和投資。這是由于上述技術(shù)的能力已經(jīng)在伊拉克和阿富汗得到試驗和驗證。",
"poster_degree": 2,
"section_id": 422,
"section_name": "技術(shù)1",
"section_owner_id": 232133333,
"section_owner_nick": "chenkangxian",
"gmt_modified": "2013-05-07T12:09:12Z",
"gmt_create": "2013-05-07T12:09:12Z",
126 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計與實踐
"comment_count": 3,
"_version_": 1467083075564339200
}
]
}
}
假設(shè)頁面需要根據(jù)poster_id(發(fā)帖人id)和section_owner_nick(版主昵稱)作為條件來進(jìn)
行查詢,并且根據(jù)uniqueKey 降序排列,以及根據(jù)section_id(版塊id)進(jìn)行分組統(tǒng)計,那么查
詢的條件表達(dá)式可以這樣寫:
http://hostname:8080/solr/core/select?q=poster_id:340032+and+section_own
er_nick:chenkangxian&sort=post_id+asc&facet=true&facet.field=section_id&
wt=json&indent=true
其中 q= poster_id:340032+and+section_owner_nick:chenkangxian 表示查詢的post_id 為
340032,section_owner_nick 為chenkangxian,兩個條件使用and 組合,而sort=post_id+asc 則表
示按照post_id 進(jìn)行升序排列,facet=true&facet.field=section_id 表示使用分組統(tǒng)計,并且分組統(tǒng)
計字段為section_id。
當(dāng)然,Solr 還支持更多復(fù)雜的條件查詢,此處就不再詳細(xì)介紹了36。
2.5 其他基礎(chǔ)設(shè)施
除了前面所提到的分布式緩存、持久化存儲、分布式消息系統(tǒng)、搜索引擎,大型的分布式
系統(tǒng)的背后,還依賴于其他支撐系統(tǒng),包括后面章節(jié)所要介紹的實時計算、離線計算、分布式
文件系統(tǒng)、日志收集系統(tǒng)、監(jiān)控系統(tǒng)、數(shù)據(jù)倉庫等,以及本書沒有詳細(xì)介紹的CDN 系統(tǒng)、負(fù)
載均衡系統(tǒng)、消息推送系統(tǒng)、自動化運維系統(tǒng)等37。
36 更詳細(xì)的查詢語法介紹請參考Solr 官方wiki,http://wiki.apache.org/solr/CommonQueryParameters#
head-6522ef80f22d0e50d2f12ec487758577506d6002。
37 這些系統(tǒng)雖然本書雖沒進(jìn)行詳細(xì)的介紹,但并不代表它們不重要,它們也是分布式系統(tǒng)的重要組成部
分,限于篇幅,此處僅一筆帶過,讀者可自行查閱相關(guān)資料。     
本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
java開發(fā)者的大數(shù)據(jù)工具和框架
分布式集群系統(tǒng)下的高可用session解決方案
高性能可靠服務(wù)集群架構(gòu)
Web網(wǎng)站搭建從零到一
微服務(wù)架構(gòu)下的分布式Session管理
Spark生態(tài)圈之——ElasticSearch與Solr
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服