小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

分布式服務(wù)框架

 WindySky 2017-05-31

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

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

本章主要介紹和解決如下問(wèn)題:

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

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


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

2.1.1 memcache 簡(jiǎn)介及安裝

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

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

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

業(yè)務(wù)發(fā)展初期為了便于快速迭代,很多應(yīng)用都采用集中式的架構(gòu)。隨著業(yè)務(wù)規(guī)模的擴(kuò)展,
使系統(tǒng)變得越來(lái)越復(fù)雜,越來(lái)越難以維護(hù),開(kāi)發(fā)效率越來(lái)越低,并且系統(tǒng)的資源消耗也越來(lái)越
大,通過(guò)硬件提升性能的成本也越來(lái)越高。因此,系統(tǒng)業(yè)務(wù)的拆分是難以避免的。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 73
舉例來(lái)說(shuō),假設(shè)某門(mén)戶(hù)網(wǎng)站,它包含了新聞、用戶(hù)、帖子、評(píng)論等幾大塊內(nèi)容,對(duì)于數(shù)據(jù)
庫(kù)來(lái)說(shuō),它可能包含這樣幾張表,如news、users、post、comment,如圖2-5 所示。
圖2-5 single DB 的拆分
隨著業(yè)務(wù)的不斷發(fā)展,單個(gè)庫(kù)的訪(fǎng)問(wèn)量越來(lái)越大,因此,不得不對(duì)業(yè)務(wù)進(jìn)行拆分。每一塊
業(yè)務(wù)都使用單獨(dú)的數(shù)據(jù)庫(kù)來(lái)進(jìn)行存儲(chǔ),前端不同的業(yè)務(wù)訪(fǎng)問(wèn)不同的數(shù)據(jù)庫(kù),這樣原本依賴(lài)單庫(kù)
的服務(wù),變成4 個(gè)庫(kù)同時(shí)承擔(dān)壓力,吞吐能力自然就提高了。
順帶說(shuō)一句,業(yè)務(wù)拆分不僅僅提高了系統(tǒng)的可擴(kuò)展性,也帶來(lái)了開(kāi)發(fā)工作效率的提升。原
來(lái)一次簡(jiǎn)單修改,工程啟動(dòng)和部署可能都需要很長(zhǎng)時(shí)間,更別說(shuō)開(kāi)發(fā)測(cè)試了。隨著系統(tǒng)的拆分,
單個(gè)系統(tǒng)復(fù)雜度降低,減輕了應(yīng)用多個(gè)分支開(kāi)發(fā)帶來(lái)的分支合并沖突解決的麻煩,不僅大大提
高了開(kāi)發(fā)測(cè)試的效率,同時(shí)也提升了系統(tǒng)的穩(wěn)定性。
2. 復(fù)制策略
架構(gòu)變化的同時(shí),業(yè)務(wù)也在不斷地發(fā)展,可能很快就會(huì)發(fā)現(xiàn),隨著訪(fǎng)問(wèn)量的不斷增加,拆
分后的某個(gè)庫(kù)壓力越來(lái)越大,馬上就要達(dá)到能力的瓶頸,數(shù)據(jù)庫(kù)的架構(gòu)不得不再次進(jìn)行變更,
這時(shí)可以使用MySQL 的replication(復(fù)制)策略來(lái)對(duì)系統(tǒng)進(jìn)行擴(kuò)展。
通過(guò)數(shù)據(jù)庫(kù)的復(fù)制策略,可以將一臺(tái) MySQL 數(shù)據(jù)庫(kù)服務(wù)器中的數(shù)據(jù)復(fù)制到其他MySQL
數(shù)據(jù)庫(kù)服務(wù)器上。當(dāng)各臺(tái)數(shù)據(jù)庫(kù)服務(wù)器上都包含相同數(shù)據(jù)時(shí),前端應(yīng)用通過(guò)訪(fǎng)問(wèn)MySQL 集群
中任意一臺(tái)服務(wù)器,都能夠讀取到相同的數(shù)據(jù),這樣每臺(tái)MySQL 服務(wù)器所需要承擔(dān)的負(fù)載就
會(huì)大大降低,從而提高整個(gè)系統(tǒng)的承載能力,達(dá)到系統(tǒng)擴(kuò)展的目的。
如圖 2-6 所示,要實(shí)現(xiàn)數(shù)據(jù)庫(kù)的復(fù)制,需要開(kāi)啟Master 服務(wù)器端的Binary log。數(shù)據(jù)復(fù)制的
74 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
過(guò)程實(shí)際上就是Slave 從master 獲取binary log,然后再在本地鏡像的執(zhí)行日志中記錄的操作。
由于復(fù)制過(guò)程是異步的,因此Master 和Slave 之間的數(shù)據(jù)有可能存在延遲的現(xiàn)象,此時(shí)只能夠
保證數(shù)據(jù)最終的一致性。
圖2-6 MySQL 的Master 與Slave 之間數(shù)據(jù)同步的過(guò)程11
MySQL 的復(fù)制可以基于一條語(yǔ)句(statement level),也可以基于一條記錄(row level)。通
過(guò)row level 的復(fù)制,可以不記錄執(zhí)行的SQL 語(yǔ)句相關(guān)聯(lián)的上下文信息,只需要記錄數(shù)據(jù)變更
的內(nèi)容即可。但由于每行的變更都會(huì)被記錄,這樣可能會(huì)產(chǎn)生大量的日志內(nèi)容,而使用statement
level 則只是記錄修改數(shù)據(jù)的SQL 語(yǔ)句,減少了binary log 的日志量,節(jié)約了I/O 成本。但是,
為了讓SQL 語(yǔ)句在Slave 端也能夠正確地執(zhí)行,它還需要記錄SQL 執(zhí)行的上下文信息,以保證
所有語(yǔ)句在Slave 端執(zhí)行時(shí)能夠得到在Master 端執(zhí)行時(shí)的相同結(jié)果。
在實(shí)際的應(yīng)用場(chǎng)景中,MySQL 的Master 與Slave 之間的復(fù)制架構(gòu)有可能是這樣的,如圖
2-7 所示。
前端服務(wù)器通過(guò)Master 來(lái)執(zhí)行數(shù)據(jù)寫(xiě)入的操作,數(shù)據(jù)的更新通過(guò)Binary log 同步到Slave
集群,而對(duì)于數(shù)據(jù)讀取的請(qǐng)求,則交由Slave 來(lái)處理,這樣Slave 集群可以分擔(dān)數(shù)據(jù)庫(kù)讀的壓力,
并且讀、寫(xiě)分離還保障了數(shù)據(jù)能夠達(dá)到最終一致性。一般而言,大多數(shù)站點(diǎn)的讀數(shù)據(jù)庫(kù)操作要
比寫(xiě)數(shù)據(jù)庫(kù)操作更為密集。如果讀的壓力較大,還可以通過(guò)新增Slave 來(lái)進(jìn)行系統(tǒng)的擴(kuò)展,因
此,Master-Slave 的架構(gòu)能夠顯著地減輕前面所提到的單庫(kù)讀的壓力。畢竟在大多數(shù)應(yīng)用中,讀
的壓力要比寫(xiě)的壓力大得多。
11 圖片來(lái)源http:///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)存在一個(gè)問(wèn)題,即所謂的單點(diǎn)故障。當(dāng)Master 宕機(jī)時(shí),系統(tǒng)將無(wú)法
寫(xiě)入,而在某些特定的場(chǎng)景下,也可能需要Master 停機(jī),以便進(jìn)行系統(tǒng)維護(hù)、優(yōu)化或者升級(jí)。
同樣的道理,Master 停機(jī)將導(dǎo)致整個(gè)系統(tǒng)都無(wú)法寫(xiě)入,直到Master 恢復(fù),大部分情況下這顯然
是難以接受的。為了盡可能地降低系統(tǒng)停止寫(xiě)入的時(shí)間,最佳的方式就是采用Dual-Master 架構(gòu),
即Master-Master 架構(gòu),如圖2-8 所示。
76 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
圖 2-8 MySQL Dual-Master 架構(gòu)
所謂的 Dual Master,實(shí)際上就是兩臺(tái)MySQL 服務(wù)器互相將對(duì)方作為自己的Master,自己
作為對(duì)方的Slave,這樣任何一臺(tái)服務(wù)器上的數(shù)據(jù)變更,都會(huì)通過(guò)MySQL 的復(fù)制機(jī)制同步到另
一臺(tái)服務(wù)器。當(dāng)然,有的讀者可能會(huì)擔(dān)心,這樣不會(huì)導(dǎo)致兩臺(tái)互為Master 的MySQL 之間循環(huán)
復(fù)制嗎?當(dāng)然不會(huì),這是由于MySQL 在記錄Binary log 日志時(shí),記錄了當(dāng)前的server-id,server-id
在我們配置MySQL 復(fù)制時(shí)就已經(jīng)設(shè)置好了。一旦有了server-id,MySQL 就很容易判斷最初的
寫(xiě)入是在哪臺(tái)服務(wù)器上發(fā)生的,MySQL 不會(huì)將復(fù)制所產(chǎn)生的變更記錄到Binary log,這樣就避
免了服務(wù)器間數(shù)據(jù)的循環(huán)復(fù)制。
當(dāng)然,我們搭建Dual-Master 架構(gòu),并不是為了讓兩個(gè)Master 能夠同時(shí)提供寫(xiě)入服務(wù),這
樣會(huì)導(dǎo)致很多問(wèn)題。舉例來(lái)說(shuō),假如Master A 與Master B 幾乎同時(shí)對(duì)一條數(shù)據(jù)進(jìn)行了更新,對(duì)
Master A 的更新比對(duì)Master B 的更新早,當(dāng)對(duì)Master A 的更新最終被同步到Master B 時(shí),老版
本的數(shù)據(jù)將會(huì)把版本更新的數(shù)據(jù)覆蓋,并且不會(huì)拋出任何異常,從而導(dǎo)致數(shù)據(jù)不一致的現(xiàn)象發(fā)
生。在通常情況下,我們僅開(kāi)啟一臺(tái)Master 的寫(xiě)入,另一臺(tái)Master 僅僅stand by 或者作為讀庫(kù)
開(kāi)放,這樣可以避免數(shù)據(jù)寫(xiě)入的沖突,防止數(shù)據(jù)不一致的情況發(fā)生。
在正常情況下,如需進(jìn)行停機(jī)維護(hù),可按如下步驟執(zhí)行Master 的切換操作:
(1)停止當(dāng)前Master 的所有寫(xiě)入操作。
(2)在Master 上執(zhí)行set global read_only=1,同時(shí)更新MySQL 配置文件中相應(yīng)的配置,
避免重啟時(shí)失效。
(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 開(kāi)啟寫(xiě)入時(shí),設(shè)置read_only=0。
(6)修改應(yīng)用程序的配置,使其寫(xiě)入到新的Master。
假如 Master 意外宕機(jī),處理過(guò)程要稍微復(fù)雜一點(diǎn),因?yàn)榇藭r(shí)Master 與stand by Master 上的
數(shù)據(jù)并不一定同步,需要將Master 上沒(méi)有同步到stand by Master 的Binary log 復(fù)制到Master 上
進(jìn)行replay,直到stand by Master 與原Master 上的Binary log 同步,才能夠開(kāi)啟寫(xiě)入;否則,
這一部分不同步的數(shù)據(jù)就有可能導(dǎo)致數(shù)據(jù)不一致。
3. 分表與分庫(kù)
對(duì)于大型的互聯(lián)網(wǎng)應(yīng)用來(lái)說(shuō),數(shù)據(jù)庫(kù)單表的記錄行數(shù)可能達(dá)到千萬(wàn)級(jí)別甚至是億級(jí),并且
數(shù)據(jù)庫(kù)面臨著極高的并發(fā)訪(fǎng)問(wèn)。采用Master-Slave 復(fù)制模式的MySQL 架構(gòu),只能夠?qū)?shù)據(jù)庫(kù)的
讀進(jìn)行擴(kuò)展,而對(duì)數(shù)據(jù)的寫(xiě)入操作還是集中在Master 上,并且單個(gè)Master 掛載的Slave 也不可
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 77
能無(wú)限制多,Slave 的數(shù)量受到Master 能力和負(fù)載的限制。因此,需要對(duì)數(shù)據(jù)庫(kù)的吞吐能力進(jìn)
行進(jìn)一步的擴(kuò)展,以滿(mǎn)足高并發(fā)訪(fǎng)問(wèn)與海量數(shù)據(jù)存儲(chǔ)的需要。
對(duì)于訪(fǎng)問(wèn)極為頻繁且數(shù)據(jù)量巨大的單表來(lái)說(shuō),我們首先要做的就是減少單表的記錄條數(shù),
以便減少數(shù)據(jù)查詢(xún)所需要的時(shí)間,提高數(shù)據(jù)庫(kù)的吞吐,這就是所謂的分表。在分表之前,首先
需要選擇適當(dāng)?shù)姆直聿呗裕沟脭?shù)據(jù)能夠較為均衡地分布到多張表中,并且不影響正常的查詢(xún)。
對(duì)于互聯(lián)網(wǎng)企業(yè)來(lái)說(shuō),大部分?jǐn)?shù)據(jù)都是與用戶(hù)關(guān)聯(lián)的,因此,用戶(hù)id 是最常用的分表字段。
因?yàn)榇蟛糠植樵?xún)都需要帶上用戶(hù)id,這樣既不影響查詢(xún),又能夠使數(shù)據(jù)較為均衡地分布到各個(gè)
表中12,如圖2-9 所示。
圖2-9 user 表按照user_id%256 的策略進(jìn)行分表
假設(shè)有一張記錄用戶(hù)購(gòu)買(mǎi)信息的訂單表order,由于order 表記錄條數(shù)太多,將被拆分成256
張表13。拆分的記錄根據(jù)user_id%256 取得對(duì)應(yīng)的表進(jìn)行存儲(chǔ),前臺(tái)應(yīng)用則根據(jù)對(duì)應(yīng)的
user_id%256,找到對(duì)應(yīng)訂單存儲(chǔ)的表進(jìn)行訪(fǎng)問(wèn)。這樣一來(lái),user_id 便成為一個(gè)必需的查詢(xún)條件,
否則將會(huì)由于無(wú)法定位數(shù)據(jù)存儲(chǔ)的表而無(wú)法對(duì)數(shù)據(jù)進(jìn)行訪(fǎng)問(wèn)。
假設(shè) user 表的結(jié)構(gòu)如下:
create table order(
order_id bigint(20) primary key auto_increment,
12 當(dāng)然,有的場(chǎng)景也可能會(huì)出現(xiàn)冷熱數(shù)據(jù)分布不均衡的情況。
13 拆分后表的數(shù)量一般為2 的n 次方。
78 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(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 來(lái)查詢(xún)對(duì)應(yīng)的
訂單信息,則對(duì)應(yīng)的SQL 語(yǔ)句如下:
select * from order_1 where user_id = 257 and auction_id = 100;
其中,order_1 根據(jù)257%256 計(jì)算得出,表示分表之后的第1 張order 表。
分表能夠解決單表數(shù)據(jù)量過(guò)大帶來(lái)的查詢(xún)效率下降的問(wèn)題,但是,卻無(wú)法給數(shù)據(jù)庫(kù)的并發(fā)
處理能力帶來(lái)質(zhì)的提升。面對(duì)高并發(fā)的讀寫(xiě)訪(fǎng)問(wèn),當(dāng)數(shù)據(jù)庫(kù)Master 服務(wù)器無(wú)法承載寫(xiě)操作壓力
時(shí),不管如何擴(kuò)展Slave 服務(wù)器,此時(shí)都沒(méi)有意義了。因此,我們必須換一種思路,對(duì)數(shù)據(jù)庫(kù)
進(jìn)行拆分,從而提高數(shù)據(jù)庫(kù)寫(xiě)入能力,這就是所謂的分庫(kù)。
與分表策略相似,分庫(kù)也可以采用通過(guò)一個(gè)關(guān)鍵字段取模的方式,來(lái)對(duì)數(shù)據(jù)訪(fǎng)問(wèn)進(jìn)行路由,
如圖2-10 所示。
圖2-10 MySQL 分庫(kù)策略
還是之前的訂單表,假設(shè)user_id 字段的值為257,將原有的單庫(kù)分為256 個(gè)庫(kù),那么應(yīng)用
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 79
程序?qū)?shù)據(jù)庫(kù)的訪(fǎng)問(wèn)請(qǐng)求將被路由到第1 個(gè)庫(kù)(257%256=1)。
有時(shí)數(shù)據(jù)庫(kù)可能既面臨著高并發(fā)訪(fǎng)問(wèn)的壓力,又需要面對(duì)海量數(shù)據(jù)的存儲(chǔ)問(wèn)題,這時(shí)需要
對(duì)數(shù)據(jù)庫(kù)即采用分庫(kù)策略,又采用分表策略,以便同時(shí)擴(kuò)展系統(tǒng)的并發(fā)處理能力,以及提升單
表的查詢(xún)性能,這就是所謂的分庫(kù)分表。
分庫(kù)分表的策略比前面的僅分庫(kù)或者僅分表的策略要更為復(fù)雜,一種分庫(kù)分表的路由策略
如下:
中間變量=user_id%(庫(kù)數(shù)量×每個(gè)庫(kù)的表數(shù)量);
庫(kù)=取整(中間變量/每個(gè)庫(kù)的表數(shù)量);
表=中間變量%每個(gè)庫(kù)的表數(shù)量。
同樣采用 user_id 作為路由字段,首先使用user_id 對(duì)庫(kù)數(shù)量×每個(gè)庫(kù)表的數(shù)量取模,得到
一個(gè)中間變量;然后使用中間變量除以每個(gè)庫(kù)表的數(shù)量,取整,便得到對(duì)應(yīng)的庫(kù);而中間變量
對(duì)每個(gè)庫(kù)表的數(shù)量取模,即得到對(duì)應(yīng)的表。分庫(kù)分表策略如圖2-11 所示。
圖2-11 MySQL 分庫(kù)分表策略
假設(shè)將原來(lái)的單庫(kù)單表order 拆分成256 個(gè)庫(kù),每個(gè)庫(kù)包含1024 個(gè)表,那么按照前面所提
到的路由策略,對(duì)于user_id=262145 的訪(fǎng)問(wèn),路由的計(jì)算過(guò)程如下:
中間變量=262145%(256×1024)=1;
80 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
庫(kù)=取整(1/1024)=0;
表=1%1024=1。
這意味著,對(duì)于user_id=262145 的訂單記錄的查詢(xún)和修改,將被路由到第0 個(gè)庫(kù)的第1 個(gè)
表中執(zhí)行。
數(shù)據(jù)庫(kù)經(jīng)過(guò)業(yè)務(wù)拆分及分庫(kù)分表之后,雖然查詢(xún)性能和并發(fā)處理能力提高了,但也會(huì)帶來(lái)
一系列的問(wèn)題。比如,原本跨表的事務(wù)上升為分布式事務(wù);由于記錄被切分到不同的庫(kù)與不同
的表當(dāng)中,難以進(jìn)行多表關(guān)聯(lián)查詢(xún),并且不能不指定路由字段對(duì)數(shù)據(jù)進(jìn)行查詢(xún)。分庫(kù)分表以后,
如果需要對(duì)系統(tǒng)進(jìn)行進(jìn)一步擴(kuò)容(路由策略變更),將變得非常不方便,需要重新進(jìn)行數(shù)據(jù)遷移。
相較于 MySQL 的分庫(kù)分表策略,后面要提到的HBase 天生就能夠很好地支持海量數(shù)據(jù)的
存儲(chǔ),能夠以更友好、更方便的方式支持表的分區(qū),并且HBase 還支持多個(gè)Region Server 同時(shí)
寫(xiě)入,能夠較為方便地?cái)U(kuò)展系統(tǒng)的并發(fā)寫(xiě)入能力。而通過(guò)后面章節(jié)所提到的搜索引擎技術(shù),能
夠解決采用業(yè)務(wù)拆分及分庫(kù)分表策略后,系統(tǒng)無(wú)法進(jìn)行多表關(guān)聯(lián)查詢(xún),以及查詢(xún)時(shí)必須帶路由
字段的問(wèn)題。搜索引擎能夠很好地支持復(fù)雜條件的組合查詢(xún),通過(guò)搜索引擎構(gòu)建的一張大表,
能夠彌補(bǔ)一部分?jǐn)?shù)據(jù)庫(kù)拆分所帶來(lái)的問(wèn)題。
2.2.2 HBase
HBase14是Apache Hadoop 項(xiàng)目下的一個(gè)子項(xiàng)目,它以Google BigTable15為原型,設(shè)計(jì)實(shí)現(xiàn)
了高可靠性、高可擴(kuò)展性、實(shí)時(shí)讀/寫(xiě)的列存儲(chǔ)數(shù)據(jù)庫(kù)。它的本質(zhì)實(shí)際上是一張稀疏的大表,用
來(lái)存儲(chǔ)粗粒度的結(jié)構(gòu)化數(shù)據(jù),并且能夠通過(guò)簡(jiǎn)單地增加節(jié)點(diǎn)來(lái)實(shí)現(xiàn)系統(tǒng)的線(xiàn)性擴(kuò)展。
HBase 運(yùn)行在分布式文件系統(tǒng)HDFS16之上,利用它可以在廉價(jià)的PC Server 上搭建大規(guī)模
結(jié)構(gòu)化存儲(chǔ)集群。HBase 的數(shù)據(jù)以表的形式進(jìn)行組織,每個(gè)表由行列組成。與傳統(tǒng)的關(guān)系型數(shù)
據(jù)庫(kù)不同的是,HBase 每個(gè)列屬于一個(gè)特定的列族,通過(guò)行和列來(lái)確定一個(gè)存儲(chǔ)單元,而每個(gè)
存儲(chǔ)單元又可以有多個(gè)版本,通過(guò)時(shí)間戳來(lái)標(biāo)識(shí),如表2-1 所示。
表 2-1 HBase 表數(shù)據(jù)的組織形式
rowkey
column-family1 column-family2 column-family3
column1 column2 column3 column1 column2 column1
key1 … … … … … …
key2 … … … … … …
14 HBase 項(xiàng)目地址為https://hbase.。
15 著名的Google BigTable 論文,http://research.google.com/archive/bigtable.html。
16 關(guān)于HDFS 的介紹,請(qǐng)參照第5.2 節(jié)。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 81
key3 … … … … … …
HBase 集群中通常包含兩種角色,HMaster 和HRegionServer。當(dāng)表隨著記錄條數(shù)的增加而
不斷變大后,將會(huì)分裂成一個(gè)個(gè)Region,每個(gè)Region 可以由(startkey,endkey)來(lái)表示,它包
含一個(gè)startkey 到endkey 的半閉區(qū)間。一個(gè)HRegionServer 可以管理多個(gè)Region,并由HMaster
來(lái)負(fù)責(zé)HRegionServer 的調(diào)度及集群狀態(tài)的監(jiān)管。由于Region 可分散并由不同的HRegionServer
來(lái)管理,因此,理論上再大的表都可以通過(guò)集群來(lái)處理。HBase 集群布署圖如圖2-12 所示。
圖2-12 HBase 集群部署圖17
1. HBase 安裝
下載 HBase 的安裝包,這里選擇的版本是0.9618。
wget http://mirror./apache/hbase/hbase-0.96.1.1/hbase-
0.96.1.1-hadoop1-bin.tar.gz
17 圖片來(lái)源http://dl2./upload/attachment/0073/5412/53da4281-58d4-3f53-8aaf-a09d0c295f05.jpg。
18 HBase 的版本需要與Hadoop 的版本相兼容,詳情請(qǐng)見(jiàn)http://hbase./book/configuration.html# hadoop。
82 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(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 則指
定了是否運(yùn)行在分布式模式下。
<configuration>
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<property>
<name>hbase.rootdir</name>
<value>hdfs://localhost:9000/hbase</value>
</property>
</configuration>
啟動(dòng) HBase:
完成上述操作后,先啟動(dòng)Hadoop,再啟動(dòng)HBase,就可以進(jìn)行相應(yīng)的操作了。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 83
使用HBase shell:
./hbase shell
查看HBase 集群狀態(tài):
status
HBase 的基本使用:
創(chuàng)建一個(gè)表,并指定列族的名稱(chēng),create '表名稱(chēng)'、'列族名稱(chēng)1'、'列族名稱(chēng)2' ……
例如,create 'user','phone','info'。
創(chuàng)建 user 表,包含兩個(gè)列族,一個(gè)是phone,一個(gè)是info。
84 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
列出已有的表,并查看表的描述:
list
describe ‘表名’
例如,describe ‘user’。
新增/刪除一個(gè)列族。
給表新增一個(gè)列族:
alter '表名',NAME=>'列族名稱(chēng)'
例如,alter 'user',NAME=>'class'。
刪除表的一個(gè)列族:
alter '表名',NAME=>'列族名稱(chēng)',METHOD=>'delete'
例如,alter 'user',NAME=>'class',METHOD=>'delete'。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 85
刪除一個(gè)表:
在使用 drop 刪除一個(gè)表之前,必須先將該表disable:
disable 'user'
drop 'user'
如果沒(méi)有disable 表而直接使用drop 刪除,則會(huì)出現(xiàn)如下提示:
給表添加記錄:
put '表名', 'rowkey','列族名稱(chēng):列名稱(chēng)','值'
例如,put 'user','1','info:name','zhangsan'。
查看數(shù)據(jù)。
根據(jù) rowkey 查看數(shù)據(jù):
get '表名稱(chēng)','rowkey'
例如,get 'user','1'。
根據(jù)rowkey 查看對(duì)應(yīng)列的數(shù)據(jù):
get '表名稱(chēng)','rowkey','列族名稱(chēng):列名稱(chēng)'
例如,get 'user','1','info:name'。
86 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
查看表中的記錄總數(shù):
count '表名稱(chēng)'
例如,count 'user'。
查看表中所有記錄:
scan '表名稱(chēng)'
例如,scan 'user'。
查看表中指定列族的所有記錄:
scan '表名',{COLUMNS => '列族'}
例如,scan 'user',{COLUMNS => 'info'}。
查看表中指定區(qū)間的所有記錄:
scan '表名稱(chēng)',{COLUMNS => '列族',LIMIT =>記錄數(shù), STARTROW => '開(kāi)始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 '表名稱(chēng)','rowkey' ,'列簇名稱(chēng)'
例如,delete 'user','1','info:name'。
根據(jù)rowkey 刪除一行數(shù)據(jù):
deleteall '表名稱(chēng)','rowkey'
例如,deleteall 'user','2。
2. HBase API
除了通過(guò)shell 進(jìn)行操作,HBase 作為分布式數(shù)據(jù)庫(kù),自然也提供程序訪(fǎng)問(wèn)的接口,此處以
Java 為例。
首先,需要配置HBase 的HMaster 服務(wù)器地址和對(duì)應(yīng)的端口(默認(rèn)為60000),以及對(duì)應(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");
}
接下來(lái),通過(guò)程序來(lái)新增user 表,user 表中有三個(gè)列族,分別為info、class、parent,如果
該表已經(jīng)存在,則先刪除該表:
public static void createTable() throws Exception {
88 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(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 表,每個(gè)列族指定一個(gè)列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 的行,并將該行打印出來(lái):
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 表,并將查詢(xún)結(jié)果打印出來(lái):
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)行查詢(xú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è)計(jì)與實(shí)踐
List<Delete> list = new ArrayList<Delete>();
Delete d = new Delete(rowKey.getBytes());
list.add(d);
table.delete(list);
table.close();
}
3. rowkey 設(shè)計(jì)
要想訪(fǎng)問(wèn) HBase 的行,只有三種方式,一種是通過(guò)指定rowkey 進(jìn)行訪(fǎng)問(wèn),另一種是指定
rowkey 的range 進(jìn)行scan,再者就是全表掃描。由于全表掃描對(duì)于性能的消耗很大,掃描一張
上億行的大表將帶來(lái)很大的開(kāi)銷(xiāo),以至于整個(gè)集群的吞吐都會(huì)受到影響。因此,rowkey 設(shè)計(jì)的
好壞,將在很大程度上影響表的查詢(xún)性能,是能否充分發(fā)揮HBase 性能的關(guān)鍵。
舉例來(lái)說(shuō),假設(shè)使用HBase 來(lái)存儲(chǔ)用戶(hù)的訂單信息,我們可能會(huì)通過(guò)這樣幾個(gè)維度來(lái)記錄
訂單的信息,包括購(gòu)買(mǎi)用戶(hù)的id、交易時(shí)間、商品id、商品名稱(chēng)、交易金額、賣(mài)家id 等。假設(shè)
需要從賣(mài)家維度來(lái)查看某商品已售出的訂單,并且按照下單時(shí)間區(qū)間來(lái)進(jìn)行查詢(xún),那么訂單表
可以這樣設(shè)計(jì):
rowkey:seller_id + auction_id + create_time
列族:order_info(auction_title,price,user_id)
使用賣(mài)家id+商品id+交易時(shí)間作為表的rowkey,列族為order,該列族包含三列,即商品
標(biāo)題、價(jià)格、購(gòu)買(mǎi)者id,如圖2-13 所示。由于HBase 的行是按照rowkey 來(lái)排序的,這樣通過(guò)
rowkey 進(jìn)行范圍查詢(xún),可以縮小scan 的范圍。
圖 2-13 根據(jù)rowkey 進(jìn)行表的scan
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 91
而假設(shè)需要從購(gòu)買(mǎi)者維度來(lái)進(jìn)行訂單數(shù)據(jù)的查詢(xún),展現(xiàn)用戶(hù)購(gòu)買(mǎi)過(guò)的商品,并且按照購(gòu)買(mǎi)
時(shí)間進(jìn)行查詢(xún)分頁(yè),那么rowkey 的設(shè)計(jì)又不同了:
rowkey:user_id + create_time
列族:order_info(auction_id,auction_title,price,seller_id)
這樣通過(guò)買(mǎi)家id+交易時(shí)間區(qū)間,便能夠查到用戶(hù)在某個(gè)時(shí)間范圍內(nèi)因購(gòu)買(mǎi)所產(chǎn)生的訂單。
但有些時(shí)候,我們既需要從賣(mài)家維度來(lái)查詢(xún)商品售出情況,又需要從買(mǎi)家維度來(lái)查詢(xún)商品
購(gòu)買(mǎi)情況,關(guān)系型數(shù)據(jù)庫(kù)能夠很好地支持類(lèi)似的多條件復(fù)雜查詢(xún)。但對(duì)于HBase 來(lái)說(shuō),實(shí)現(xiàn)起
來(lái)并不是那么的容易?;镜慕鉀Q思路就是建立一張二級(jí)索引表,將查詢(xún)條件設(shè)計(jì)成二級(jí)索引
表的rowkey,而存儲(chǔ)的數(shù)據(jù)則是數(shù)據(jù)表的rowkey,這樣就可以在一定程度上實(shí)現(xiàn)多個(gè)條件的查
詢(xún)。但是二級(jí)索引表也會(huì)引入一系列的問(wèn)題,多表的插入將降低數(shù)據(jù)寫(xiě)入的性能,并且由于多
表之間無(wú)事務(wù)保障,可能會(huì)帶來(lái)數(shù)據(jù)一致性的問(wèn)題19。
與傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)相比,HBase 有更好的伸縮能力,更適合于海量數(shù)據(jù)的存儲(chǔ)和處理。
由于多個(gè)Region Server 的存在,使得HBase 能夠多個(gè)節(jié)點(diǎn)同時(shí)寫(xiě)入,顯著提高了寫(xiě)入性能,并
且是可擴(kuò)展的。但是,HBase 本身能夠支持的查詢(xún)維度有限,難以支持復(fù)雜查詢(xún),如group by、
order by、join 等,這些特點(diǎn)使得它的應(yīng)用場(chǎng)景受到了限制。當(dāng)然,這也并非是不可彌補(bǔ)的硬傷,
通過(guò)后面章節(jié)所介紹的搜索引擎來(lái)構(gòu)建索引,可以在一定程度上解決HBase 復(fù)雜條件組合查詢(xún)
的問(wèn)題。
2.2.3 Redis
Redis 是一個(gè)高性能的key-value 數(shù)據(jù)庫(kù),與其他很多key-value 數(shù)據(jù)庫(kù)的不同之處在于,Redis
不僅支持簡(jiǎn)單的鍵值對(duì)類(lèi)型的存儲(chǔ),還支持其他一系列豐富的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu),包括strings、
hashs、lists、sets、sorted sets 等,并在這些數(shù)據(jù)結(jié)構(gòu)類(lèi)型上定義了一套強(qiáng)大的API。通過(guò)定義
不同的存儲(chǔ)結(jié)構(gòu),Redis 可以很輕易地完成很多其他key-value 數(shù)據(jù)庫(kù)難以完成的任務(wù),如排序、
去重等。
1. 安裝Redis
下載Redis 源碼安裝包:
wget http://download./releases/redis-2.8.8.tar.gz
19 關(guān)于HBase 的二級(jí)索引表,華為提供了hindex 的二級(jí)索引解決方案,有興趣的讀者可以參考
https://github.com/Huawei-Hadoop/hindex。
92 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(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/
啟動(dòng)Redis Server:
./redis-server ../redis.conf
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 93
使用redis-cli 進(jìn)行訪(fǎng)問(wèn)20:
./redis-cli
2. 使用Redis API
Redis 的Java client21有很多,這里選擇比較常用的Jedis22來(lái)介紹Redis 數(shù)據(jù)訪(fǎng)問(wèn)的API。
首先,需要對(duì)Redis client 進(jìn)行初始化:
Jedis redis = new Jedis ("192.168.136.135",6379);
Redis 支持豐富的數(shù)據(jù)類(lèi)型,如strings、hashs、lists、sets、sorted sets 等,這些數(shù)據(jù)類(lèi)型都
有對(duì)應(yīng)的API 來(lái)進(jìn)行操作。比如,Redis 的strings 類(lèi)型實(shí)際上就是最基本的key-value 形式的數(shù)
據(jù),一個(gè)key 對(duì)應(yīng)一個(gè)value,它支持如下形式的數(shù)據(jù)訪(fǎng)問(wèn):
redis.set("name", "chenkangxian");//設(shè)置key-value
redis.setex("content", 5, "hello");//設(shè)置key-value 有效期為5 秒
20 更多數(shù)據(jù)訪(fǎng)問(wèn)的命令請(qǐng)參考http:///commands。
21 Redis 的clien,http:///clients。
22 Jedis 項(xiàng)目地址為https://github.com/xetorthio/jedis。
94 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
redis.mset("class","a","age","25"); //一次設(shè)置多個(gè)key-value
redis.append("content", " lucy");//給字符串追加內(nèi)容
String content = redis.get("content"); //根據(jù)key 獲取value
List<String> list = redis.mget("class","age");//一次取多個(gè)key
通過(guò) set 方法,可以給對(duì)應(yīng)的key 設(shè)值;通過(guò)get 方法,可以獲取對(duì)應(yīng)key 的值;通過(guò)setex
方法可以給key-value 設(shè)置有效期;通過(guò)mset 方法,一次可以設(shè)置多個(gè)key-value 對(duì);通過(guò)mget
方法,可以一次獲取多個(gè)key 對(duì)應(yīng)的value,這樣的好處是,可以避免多次請(qǐng)求帶來(lái)的網(wǎng)絡(luò)開(kāi)銷(xiāo),
提高性能;通過(guò)append 方法,可以給已經(jīng)存在的key 對(duì)應(yīng)的value 后追加內(nèi)容。
Redis 的hashs 實(shí)際上是一個(gè)string 類(lèi)型的field 和value 的映射表,類(lèi)似于Map,特別適合
存儲(chǔ)對(duì)象。相較于將每個(gè)對(duì)象序列化后存儲(chǔ),一個(gè)對(duì)象使用hashs 存儲(chǔ)將會(huì)占用更少的存儲(chǔ)空
間,并且能夠更為方便地存取整個(gè)對(duì)象:
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 中某個(gè)key 的值
//取Hash 的多個(gè)key 的值
List<String> urllist = redis.hmget("url","google","taobao","sina");
//取Hash 的所有key 的值
Map<String,String> userinfo = redis.hgetAll("userinfo");
通過(guò) hset 方法,可以給一個(gè)Hash 存儲(chǔ)結(jié)構(gòu)添加key-value 數(shù)據(jù);通過(guò)hmset 方法,能夠一
次性設(shè)置多個(gè)值,避免多次網(wǎng)絡(luò)操作的開(kāi)銷(xiāo);使用hget 方法,能夠取得一個(gè)Hash 結(jié)構(gòu)中某個(gè)
key 對(duì)應(yīng)的value;使用hmget 方法,則可以一次性獲取得多個(gè)key 對(duì)應(yīng)的value;通過(guò)hgetAll
方法,可以將Hash 存儲(chǔ)對(duì)應(yīng)的所有key-value 一次性取出。
Redis 的lists 是一個(gè)鏈表結(jié)構(gòu),主要的功能是對(duì)元素的push 和pop,以及獲取某個(gè)范圍內(nèi)
的值等。push 和pop 操作可以從鏈表的頭部或者尾部插入/刪除元素,這使得lists 既可以作為棧
使用,又可以作為隊(duì)列使用,其中,操作的key 可以理解為鏈表的名稱(chēng):
第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 的大小
通過(guò) lpush 和rpush 方法,分別可以在list 的首部和尾部添加元素;使用lpop 和rpop 方法,
可以在list 的首部和尾部刪除元素,通過(guò)lrange 方法,可以獲取list 指定區(qū)間的元素。
Redis 的sets 與數(shù)據(jù)結(jié)構(gòu)的set 相似,用來(lái)存儲(chǔ)一個(gè)沒(méi)有重復(fù)元素的集合,對(duì)集合的元素可
以進(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 方法用來(lái)給set 添加新的元素,而srem 則可以對(duì)元素進(jìn)行刪除,通過(guò)smembers 方法,
能夠枚舉出set 中的所有元素。
sorted sets 是Redis sets 的一個(gè)升級(jí)版本,它在sets 的基礎(chǔ)之上增加了一個(gè)排序的屬性,該
屬性在添加元素時(shí)可以指定,sorted sets 將根據(jù)該屬性來(lái)進(jìn)行排序, 每次新元素增加后,sorted
sets 會(huì)重新對(duì)順序進(jìn)行調(diào)整。sorted sets 不僅能夠通過(guò)range 正序?qū)et 取值,還能夠通過(guò)range
對(duì)set 進(jìn)行逆序取值,極大地提高了set 操作的靈活性:
redis.zadd("SortSetMem", 1, "5th");//插入sort set,并指定元素的序號(hào)
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ì)與實(shí)踐
//根據(jù)范圍反向取set
Set<String> revsortset = redis.zrevrange("SortSetMem", 1, 2);
通過(guò) zadd 方法來(lái)給sorted sets 新增元素,在新增操作的同時(shí),需要指定該元素排序的序號(hào),
以便進(jìn)行排序。使用zrange 方法可以正序?qū)et 進(jìn)行范圍取值,而通過(guò)zrevrange 方法,則可以
高效率地逆序?qū)et 進(jìn)行范圍取值。
相較于傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù),Redis 有更好的讀/寫(xiě)吞吐能力,能夠支撐更高的并發(fā)數(shù)。而
相較于其他的key-value 類(lèi)型的數(shù)據(jù)庫(kù),Redis 能夠提供更為豐富的數(shù)據(jù)類(lèi)型的支持,能夠更靈
活地滿(mǎn)足業(yè)務(wù)需求。Redis 能夠高效率地實(shí)現(xiàn)諸如排序取topN、訪(fǎng)問(wèn)計(jì)數(shù)器、隊(duì)列系統(tǒng)、數(shù)據(jù)
排重等業(yè)務(wù)需求,并且通過(guò)將服務(wù)器設(shè)置為cache-only,還能夠提供高性能的緩存服務(wù)。相較
于memcache 來(lái)說(shuō),在性能差別不大的情況下,它能夠支持更為豐富的數(shù)據(jù)類(lèi)型。
2.3 消息系統(tǒng)
在分布式系統(tǒng)中,消息系統(tǒng)的應(yīng)用十分廣泛,消息可以作為應(yīng)用間通信的一種方式。消息
被保存在隊(duì)列中,直到被接收者取出。由于消息發(fā)送者不需要同步等待消息接收者的響應(yīng),消
息的異步接收降低了系統(tǒng)集成的耦合度,提升了分布式系統(tǒng)協(xié)作的效率,使得系統(tǒng)能夠更快地
響應(yīng)用戶(hù),提供更高的吞吐。當(dāng)系統(tǒng)處于峰值壓力時(shí),分布式消息隊(duì)列還能夠作為緩沖,削峰
填谷,緩解集群的壓力,避免整個(gè)系統(tǒng)被壓垮。
開(kāi)源的消息系統(tǒng)有很多,包括Apache 的ActiveMQ,Apache 的Kafka、RabbitMQ、memcacheQ
等,本節(jié)將通過(guò)Apache 的ActiveMQ 來(lái)介紹消息系統(tǒng)的使用與集群架構(gòu)。
2.3.1 ActiveMQ & JMS
ActiveMQ 是Apache 所提供的一個(gè)開(kāi)源的消息系統(tǒng),完全采用Java 來(lái)實(shí)現(xiàn),因此,它能夠
很好地支持J2EE 提出JMS 規(guī)范。JMS(Java Message Service,即Java 消息服務(wù))是一組Java
應(yīng)用程序接口,它提供消息的創(chuàng)建、發(fā)送、接收、讀取等一系列服務(wù)。JMS 定義了一組公共應(yīng)
用程序接口和相應(yīng)的語(yǔ)法,類(lèi)似于Java 數(shù)據(jù)庫(kù)的統(tǒng)一訪(fǎng)問(wèn)接口JDBC,它是一種與廠(chǎng)商無(wú)關(guān)的
API,使得Java 程序能夠與不同廠(chǎng)商的消息組件很好地進(jìn)行通信。
JMS 支持的消息類(lèi)型包括簡(jiǎn)單文本(TextMessage)、可序列化的對(duì)象(ObjectMessage)、鍵
值對(duì)(MapMessage)、字節(jié)流(BytesMessage)、流(StreamMessage),以及無(wú)有效負(fù)載的消息
(Message)等。消息的發(fā)送是異步的,因此,消息的發(fā)布者發(fā)送完消息之后,不需要等待消息
接收者立即響應(yīng),這樣便提高了分布式系統(tǒng)協(xié)作的效率。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 97
JMS 支持兩種消息發(fā)送和接收模型。一種稱(chēng)為Point-to-Point(P2P)模型,即采用點(diǎn)對(duì)點(diǎn)
的方式發(fā)送消息。P2P 模型是基于queue(隊(duì)列)的,消息生產(chǎn)者發(fā)送消息到隊(duì)列,消息消費(fèi)者
從隊(duì)列中接收消息,隊(duì)列的存在使得消息的異步傳輸稱(chēng)為可能,P2P 模型在點(diǎn)對(duì)點(diǎn)的情況下進(jìn)
行消息傳遞時(shí)采用。另一種稱(chēng)為Pub/Sub(Publish/Subscribe,即發(fā)布/訂閱)模型,發(fā)布/訂閱模
型定義了如何向一個(gè)內(nèi)容節(jié)點(diǎn)發(fā)布和訂閱消息,這個(gè)內(nèi)容節(jié)點(diǎn)稱(chēng)為topic(主題)。主題可以認(rèn)
為是消息傳遞的中介,消息發(fā)布者將消息發(fā)布到某個(gè)主題,而消息訂閱者則從主題訂閱消息。
主題使得消息的訂閱者與消息的發(fā)布者互相保持獨(dú)立,不需要進(jìn)行接觸即可保證消息的傳遞,
發(fā)布/訂閱模型在消息的一對(duì)多廣播時(shí)采用。
如圖 2-14 所示,對(duì)于點(diǎn)對(duì)點(diǎn)消息傳輸模型來(lái)說(shuō),多個(gè)消息的生產(chǎn)者和消息的消費(fèi)者都可以
注冊(cè)到同一個(gè)消息隊(duì)列,當(dāng)消息的生產(chǎn)者發(fā)送一條消息之后,只有其中一個(gè)消息消費(fèi)者會(huì)接收
到消息生產(chǎn)者所發(fā)送的消息,而不是所有的消息消費(fèi)者都會(huì)收到該消息。
圖2-14 點(diǎn)對(duì)點(diǎn)消息傳輸模型
如圖2-15 所示,對(duì)于發(fā)布/訂閱消息傳輸模型來(lái)說(shuō),消息的發(fā)布者需將消息投遞給topic,
而消息的訂閱者則需要在相應(yīng)的topic 進(jìn)行注冊(cè),以便接收相應(yīng)topic 的消息。與點(diǎn)對(duì)點(diǎn)消息傳
輸模型不同的是,消息發(fā)布者的消息將被自動(dòng)發(fā)送給所有訂閱了該topic 的消息訂閱者。當(dāng)消息
訂閱者某段時(shí)間由于某種原因斷開(kāi)了與消息發(fā)布者的連接時(shí),這個(gè)時(shí)間段內(nèi)的消息將會(huì)丟失,
除非將消息的訂閱模式設(shè)置為持久訂閱(durable subscription),這時(shí)消息的發(fā)布者將會(huì)為消息
的訂閱者保留這段時(shí)間所產(chǎn)生的消息。當(dāng)消息的訂閱者重新連接消息發(fā)布者時(shí),消息訂閱者仍
然可以獲得這部分消息,而不至于丟失這部分消息。
98 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
圖 2-15 發(fā)布/訂閱消息傳輸模型
1. 安裝ActiveMQ
由于ActiveMQ 是純Java 實(shí)現(xiàn)的,因此ActiveMQ 的安裝依賴(lài)于Java 環(huán)境,關(guān)于Java 環(huán)境
的安裝此處就不詳細(xì)介紹了,請(qǐng)讀者自行查閱相關(guān)資料。
下載 ActiveMQ:
wget http://apache./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 目錄下,可以對(duì)配置文件進(jìn)行修改:
ls /usr/activemq
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 99
啟動(dòng) ActiveMQ:
./activemq start
2. 通過(guò)JMS 訪(fǎng)問(wèn)ActiveMQ
ActiveMQ 實(shí)現(xiàn)了JMS 規(guī)范提供的一系列接口,如創(chuàng)建Session、建立連接、發(fā)送消息等,
通過(guò)這些接口,能夠?qū)崿F(xiàn)消息發(fā)送、消息接收、消息發(fā)布、消息訂閱的功能。
使用 JMS 來(lái)完成ActiveMQ 基于queue 的點(diǎn)對(duì)點(diǎn)消息發(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) 建 一 個(gè)ActiveMQConnectionFactory , 通過(guò)ActiveMQConnectionFactory 來(lái)創(chuàng)建到
ActiveMQ 的連接,通過(guò)連接創(chuàng)建Session。創(chuàng)建Session 時(shí)有兩個(gè)非常重要的參數(shù),第一個(gè)boolean
100 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
類(lèi)型的參數(shù)用來(lái)表示是否采用事務(wù)消息。如果消息是事務(wù)的,對(duì)應(yīng)的該參數(shù)設(shè)置為true,此時(shí)
消息的提交自動(dòng)由comit 處理,消息的回滾則自動(dòng)由rollback 處理。假如消息不是事務(wù)的,則對(duì)
應(yīng)的該參數(shù)設(shè)置為false,此時(shí)分為三種情況,Session.AUTO_ACKNOWLEDGE 表示Session 會(huì)
自動(dòng)確認(rèn)所接收到的消息;而Session.CLIENT_ACKNOWLEDGE 則表示由客戶(hù)端程序通過(guò)調(diào)
用消息的確認(rèn)方法來(lái)確認(rèn)所收到的消息;Session.DUPS_OK_ACKNOWLEDGE 這個(gè)選項(xiàng)使得
Session 將“懶惰”地確認(rèn)消息,即不會(huì)立即確認(rèn)消息,這樣有可能導(dǎo)致消息重復(fù)投遞。Session
創(chuàng)建好以后,通過(guò)Session 創(chuàng)建一個(gè)queue,queue 的名稱(chēng)為MessageQueue,消息的發(fā)送者將會(huì)
向這個(gè)queue 發(fā)送消息。
基于 queue 的點(diǎn)對(duì)點(diǎn)消息接收類(lèi)似:
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,通過(guò)ActiveMQConnectionFactory 創(chuàng)建連接,通過(guò)連接
創(chuàng)建Session,然后創(chuàng)建目的queue(這里為MessageQueue),根據(jù)目的queue 創(chuàng)建消息的消費(fèi)
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 101
者,消息消費(fèi)者通過(guò)receive 方法來(lái)接收Object 消息,然后將消息轉(zhuǎn)換成字符串并打印輸出。
還可以通過(guò)JMS 來(lái)創(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ā) 送 點(diǎn) 對(duì)點(diǎn)消息一樣, 首先需要初始化ActiveMQConnectionFactory , 通過(guò)
ActiveMQConnectionFactory 創(chuàng)建連接,通過(guò)連接創(chuàng)建Session。然后再通過(guò)Session 創(chuàng)建對(duì)應(yīng)的
topic,這里指定的topic 為MessageTopic。創(chuàng)建好topic 之后,通過(guò)Session 創(chuàng)建對(duì)應(yīng)消息producer,
然后創(chuàng)建一條文本消息,消息內(nèi)容為message_hello_chenkangxian,通過(guò)producer 發(fā)送。
消息發(fā)送到對(duì)應(yīng)的topic 后,需要將listener 注冊(cè)到需要訂閱的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è)計(jì)與實(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)建好之后,通過(guò)Session 創(chuàng)建對(duì)應(yīng)的topic,然后通過(guò)topic 來(lái)創(chuàng)建消息的消費(fèi)者,
消息的消費(fèi)者需要在該topic 上注冊(cè)一個(gè)listener,以便消息發(fā)送到該topic 之后,消息的消費(fèi)者
能夠及時(shí)地接收到。
3. ActiveMQ 集群部署
針對(duì)分布式環(huán)境下對(duì)系統(tǒng)高可用的嚴(yán)格要求,以及面臨高并發(fā)的用戶(hù)訪(fǎng)問(wèn),海量的消息發(fā)
送等場(chǎng)景的挑戰(zhàn),單個(gè)ActiveMQ 實(shí)例往往難以滿(mǎn)足系統(tǒng)高可用與容量擴(kuò)展的需求,這時(shí)
ActiveMQ 的高可用方案及集群部署就顯得十分重要了。
當(dāng)一個(gè)應(yīng)用被部署到生產(chǎn)環(huán)境中,進(jìn)行容錯(cuò)和避免單點(diǎn)故障是十分重要的,這樣可以避免
因?yàn)閱蝹€(gè)節(jié)點(diǎn)的不可用而導(dǎo)致整個(gè)系統(tǒng)的不可用。目前ActiveMQ 所提供的高可用方案主要是
基于Master-Slave 模式實(shí)現(xiàn)的冷備方案,較為常用的包括基于共享文件系統(tǒng)的Master-Slave 架
構(gòu)和基于共享數(shù)據(jù)庫(kù)的Master-Slave 架構(gòu)23。
如圖 2-16 所示,當(dāng)Master 啟動(dòng)時(shí),它會(huì)獲得共享文件系統(tǒng)的排他鎖,而其他Slave 則stand-by,
不對(duì)外提供服務(wù),同時(shí)等待獲取Master 的排他鎖。假如Master 連接中斷或者發(fā)生異常,那么它
的排他鎖則會(huì)立即釋放,此時(shí)便會(huì)有另外一個(gè)Slave 能夠爭(zhēng)奪到Master 的排他鎖,從而成為Master,
對(duì)外提供服務(wù)。當(dāng)之前因故障或者連接中斷而丟失排他鎖的Master 重新連接到共享文件系統(tǒng)時(shí),
排他鎖已經(jīng)被搶占了,它將作為Slave 等待,直到Master 再一次發(fā)生異常。
23 關(guān)于ActiveMQ 的高可用架構(gòu)可以參考http://activemq./masterslave.html。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 103
圖2-16 基于共享文件系統(tǒng)的Master-Slave 架構(gòu)
基于共享數(shù)據(jù)庫(kù)的Master-Slave 架構(gòu)同基于共享文件系統(tǒng)的Master-Slave 架構(gòu)類(lèi)似,如圖
2-17 所示。當(dāng)Master 啟動(dòng)時(shí),會(huì)先獲取數(shù)據(jù)庫(kù)某個(gè)表的排他鎖,而其他Slave 則stand-by,等
待表鎖,直到Master 發(fā)生異常,連接丟失。這時(shí)表鎖將釋放,其他Slave 將獲得表鎖,從而成
為Master 并對(duì)外提供服務(wù),Master 與Slave 自動(dòng)完成切換,完全不需要人工干預(yù)。
圖2-17 基于共享數(shù)據(jù)庫(kù)的Master-Slave 架構(gòu)
104 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
當(dāng)然,客戶(hù)端也需要做一些配置,以便當(dāng)服務(wù)端Master 與Slave 切換時(shí),客戶(hù)無(wú)須重啟和
更改配置就能夠進(jìn)行兼容。在ActiveMQ 的客戶(hù)端連接的配置中使用failover 的方式,可以在
Master 失效的情況下,使客戶(hù)端自動(dòng)重新連接到新的Master:
failover:(tcp://master:61616,tcp://slave1:61616,tcp://slave2:61616)
假設(shè) Master 失效,客戶(hù)端能夠自動(dòng)地連接到Slave1 和Slave2 兩臺(tái)當(dāng)中成功獲取排他鎖的
新Master。
當(dāng)系統(tǒng)規(guī)模不斷地發(fā)展,產(chǎn)生和消費(fèi)消息的客戶(hù)端越來(lái)越多,并發(fā)的請(qǐng)求數(shù)以及發(fā)送的消
息量不斷增加,使得系統(tǒng)逐漸地不堪重負(fù)。采用垂直擴(kuò)展可以提升ActiveMQ 單broker 的處理
能力。擴(kuò)展最直接的辦法就是提升硬件的性能,如提高CPU 和內(nèi)存的能力,這種方式最為簡(jiǎn)單
也最為直接。再者就是就是通過(guò)調(diào)節(jié)ActiveMQ 本身的一些配置來(lái)提升系統(tǒng)并發(fā)處理的能力,
如使用nio 替代阻塞I/O,提高系統(tǒng)處理并發(fā)請(qǐng)求的能力,或者調(diào)整JVM 與ActiveMQ 可用的
內(nèi)存空間等。由于垂直擴(kuò)展較為簡(jiǎn)單,此處就不再詳細(xì)敘述了。
硬件的性能畢竟不能無(wú)限制地提升,垂直擴(kuò)展到一定程度時(shí),必然會(huì)遇到瓶頸,這時(shí)就需
要對(duì)系統(tǒng)進(jìn)行相應(yīng)的水平擴(kuò)展。對(duì)于ActiveMQ 來(lái)說(shuō),可以采用broker 拆分的方式,將不相關(guān)
的queue 和topic 拆分到多個(gè)broker,來(lái)達(dá)到提升系統(tǒng)吞吐能力的目的。
假設(shè)使用消息系統(tǒng)來(lái)處理訂單狀態(tài)的流轉(zhuǎn),對(duì)應(yīng)的topic 可能包括訂單創(chuàng)建、購(gòu)買(mǎi)者支付、
售賣(mài)者發(fā)貨、購(gòu)買(mǎi)者確認(rèn)收貨、購(gòu)買(mǎi)者確認(rèn)付款、購(gòu)買(mǎi)者發(fā)起退款、售賣(mài)者處理退款等,如
圖2-18 所示。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 105
圖 2-18 broker 的拆分
原本一個(gè) broker 可以承載多個(gè)queue 或者topic,現(xiàn)在將不相關(guān)的queue 和topic 拆出來(lái)放
到多個(gè)broker 當(dāng)中,這樣可以將一部分消息量大并發(fā)請(qǐng)求多的queue 獨(dú)立出來(lái)單獨(dú)進(jìn)行處理,
避免了queue 或者topic 之間的相互影響,提高了系統(tǒng)的吞吐量,使系統(tǒng)能夠支撐更大的并發(fā)請(qǐng)
求量及處理更多的消息。當(dāng)然,如有需要,還可以對(duì)queue 和topic 進(jìn)行進(jìn)一步的拆分,類(lèi)似于
數(shù)據(jù)庫(kù)的分庫(kù)分表策略,以提高系統(tǒng)整體的并發(fā)處理能力。
2.4 垂直化搜索引擎
這里所介紹的垂直化搜索引擎,與大家所熟知的Google 和Baidu 等互聯(lián)網(wǎng)搜索引擎存在著
一些差別。垂直化的搜索引擎主要針對(duì)企業(yè)內(nèi)部的自有數(shù)據(jù)的檢索,而不像Google 和Baidu 等
搜索引擎平臺(tái),采用網(wǎng)絡(luò)爬蟲(chóng)對(duì)全網(wǎng)數(shù)據(jù)進(jìn)行抓取,從而建立索引并提供給用戶(hù)進(jìn)行檢索。在
分布式系統(tǒng)中,垂直化的搜索引擎是一個(gè)非常重要的角色,它既能滿(mǎn)足用戶(hù)對(duì)于全文檢索、模
糊匹配的需求,解決數(shù)據(jù)庫(kù)like 查詢(xún)效率低下的問(wèn)題,又能夠解決分布式環(huán)境下,由于采用分
庫(kù)分表或者使用NoSQL 數(shù)據(jù)庫(kù),導(dǎo)致無(wú)法進(jìn)行多表關(guān)聯(lián)或者進(jìn)行復(fù)雜查詢(xún)的問(wèn)題。
106 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
本節(jié)將重點(diǎn)介紹搜索引擎的基本原理和Apache Lucence 的使用,以及基于Lucence 的另一
個(gè)強(qiáng)大的搜索引擎工具Solr 的一些簡(jiǎn)單配置。
2.4.1 Lucene 簡(jiǎn)介
要深入理解垂直化搜索引擎的架構(gòu),不得不提到當(dāng)前全球范圍內(nèi)使用十分廣泛的一個(gè)開(kāi)源
檢索工具——Lucene24。Lucene 是Apache 旗下的一款高性能、可伸縮的開(kāi)源的信息檢索庫(kù),最
初是由Doug Cutting25開(kāi)發(fā),并在SourceForge 的網(wǎng)站上提供下載。從2001 年9 月開(kāi)始,Lucene
作為高質(zhì)量的開(kāi)源Java 產(chǎn)品加入到Apache 軟件基金會(huì),經(jīng)過(guò)多年的不斷發(fā)展,Lucene 被翻譯
成C++、C#、perl、Python 等多種語(yǔ)言,在全球范圍內(nèi)眾多知名互聯(lián)網(wǎng)企業(yè)中得到了極為廣泛
的應(yīng)用。通過(guò)Lucene,可以十分容易地為應(yīng)用程序添加文本搜索功能,而不必深入地了解搜索
引擎實(shí)現(xiàn)的技術(shù)細(xì)節(jié)以及高深的算法,極大地降低了搜索技術(shù)推廣及使用的門(mén)檻。
Lucene 與搜索應(yīng)用程序之間的關(guān)系如圖2-19 所示。
24 Lucene 項(xiàng)目地址為https://lucene.。
25 開(kāi)源領(lǐng)域的重量級(jí)人物,創(chuàng)建了多個(gè)成功的開(kāi)源項(xiàng)目,包括Lucene、Nutch 和Hadoop。
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 107
圖 2-19 Lucene 與搜索應(yīng)用程序之間的關(guān)系26
在學(xué)習(xí)使用Lucene 之前,需要理解搜索引擎的幾個(gè)重要概念:
倒排索引(inverted index)也稱(chēng)為反向索引,是搜索引擎中最常見(jiàn)的數(shù)據(jù)結(jié)構(gòu),幾乎所有
的搜索引擎都會(huì)用到倒排索引。它將文檔中的詞作為關(guān)鍵字,建立詞與文檔的映射關(guān)系,通過(guò)
對(duì)倒排索引的檢索,可以根據(jù)詞快速獲取包含這個(gè)詞的文檔列表,這對(duì)于搜索引擎來(lái)說(shuō)至關(guān)重
要。
分詞又稱(chēng)為切詞,就是將句子或者段落進(jìn)行切割,從中提取出包含固定語(yǔ)義的詞。對(duì)于英
語(yǔ)來(lái)說(shuō),語(yǔ)言的基本單位就是單詞,因此分詞特別容易,只需要根據(jù)空格/符號(hào)/段落進(jìn)行分割,
并且排除停止詞(stop word),提取詞干27即可完成。但是對(duì)于中文來(lái)說(shuō),要將一段文字準(zhǔn)確地
切分成一個(gè)個(gè)詞,就不那么容易了。中文以字為最小單位,多個(gè)字連在一起才能構(gòu)成一個(gè)表達(dá)
具體含義的詞。中文會(huì)用明顯的標(biāo)點(diǎn)符號(hào)來(lái)分割句子和段落,唯獨(dú)詞沒(méi)有一個(gè)形式上的分割符,
因此,對(duì)于支持中文搜索的搜索引擎來(lái)說(shuō),需要一個(gè)合適的中文分詞工具,以便建立倒排索引。
停止詞(stop word),在英語(yǔ)中包含了a、the、and 這樣使用頻率很高的詞,如果這些詞都
被建到索引中進(jìn)行索引的話(huà),搜索引擎就沒(méi)有任何意義了,因?yàn)閹缀跛械奈臋n都會(huì)包含這些
詞。對(duì)于中文來(lái)說(shuō)也是如此,中文里面也有一些出現(xiàn)頻率很高的詞,如“在”、“這”、“了”、“于”
等,這些詞沒(méi)有具體含義,區(qū)分度低,搜索引擎對(duì)這些詞進(jìn)行索引沒(méi)有任何意義,因此,停止
詞需要被忽略掉。
排序,當(dāng)輸入一個(gè)關(guān)鍵字進(jìn)行搜索時(shí),可能會(huì)命中許多文檔,搜索引擎給用戶(hù)的價(jià)值就是
快速地找到需要的文檔,因此,需要將相關(guān)度更大的內(nèi)容排在前面,以便用戶(hù)能夠更快地篩選
出有價(jià)值的內(nèi)容。這時(shí)就需要有適當(dāng)?shù)呐判蛩惴?。一般?lái)說(shuō),命中標(biāo)題的文檔將比命中內(nèi)容的
文檔有更高的相關(guān)性,命中多次的文檔比命中一次的文檔有更高的相關(guān)性。商業(yè)化的搜索引擎
的排序規(guī)則十分復(fù)雜,搜索結(jié)果的排序融入了廣告、競(jìng)價(jià)排名等因素,由于涉及的利益廣泛,
一般屬于核心的商業(yè)機(jī)密。
另外,關(guān)于Lucene 的幾個(gè)概念也值得關(guān)注一下:
文檔(Document),在Lucene 的定義中,文檔是一系列域(Field)的組合,而文檔的域則
代表一系列與文檔相關(guān)的內(nèi)容。與數(shù)據(jù)庫(kù)表的記錄的概念有點(diǎn)類(lèi)似,一行記錄所包含的字段對(duì)
應(yīng)的就是文檔的域。舉例來(lái)說(shuō),一個(gè)文檔比如老師的個(gè)人信息,可能包括年齡、身高、性別、
個(gè)人簡(jiǎn)介等內(nèi)容。
域(Field),索引的每個(gè)文檔中都包含一個(gè)或者多個(gè)不同名稱(chēng)的域,每個(gè)域都包含了域的名
26 圖片來(lái)源https://www.ibm.com/developerworks/cn/java/j-lo-lucene1/fig001.jpg。
27 提取詞干是西方語(yǔ)言特有的處理步驟,比如英文中的單詞有單復(fù)數(shù)的變形,-ing 和-ed 的變形,但是在
搜索引擎中,應(yīng)該當(dāng)作同一個(gè)詞。
108 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
稱(chēng)和域?qū)?yīng)的值,并且域還可以是不同的類(lèi)型,如字符串、整型、浮點(diǎn)型等。
詞(Term),Term 是搜索的基本單元,與Field 對(duì)應(yīng),它包括了搜索的域的名稱(chēng)以及搜索的
關(guān)鍵詞,可以用它來(lái)查詢(xún)指定域中包含特定內(nèi)容的文檔。
查詢(xún)(Query),最基本的查詢(xún)可能是一系列Term 的條件組合,稱(chēng)為T(mén)ermQuery,但也有可
能是短語(yǔ)查詢(xún)(PhraseQuery)、前綴查詢(xún)(PrefixQuery)、范圍查詢(xún)(包括TermRangeQuery、
NumericRangeQuery 等)等。
分詞器(Analyzer),文檔在被索引之前,需要經(jīng)過(guò)分詞器處理,以提取關(guān)鍵的語(yǔ)義單元,
建立索引,并剔除無(wú)用的信息,如停止詞等,以提高查詢(xún)的準(zhǔn)確性。中文分詞與西文分詞的區(qū)
別在于,中文對(duì)于詞的提取更為復(fù)雜。常用的中文分詞器包括一元分詞28、二元分詞29、詞庫(kù)分
詞30等。
如圖 2-20 所示,Lucene 索引的構(gòu)建過(guò)程大致分為這樣幾個(gè)步驟,通過(guò)指定的數(shù)據(jù)格式,將
Lucene 的Document 傳遞給分詞器Analyzer 進(jìn)行分詞,經(jīng)過(guò)分詞器分詞之后,通過(guò)索引寫(xiě)入工
具IndexWriter 將索引寫(xiě)入到指定的目錄。
圖 2-20 Lucene 索引的構(gòu)建過(guò)程
而對(duì)索引的查詢(xún),大概可以分為如
下幾個(gè)步驟,如圖2-21 所示。首先構(gòu)
建查詢(xún)的Query,通過(guò)IndexSearcher
進(jìn)行查詢(xún),得到命中的TopDocs。然后
通過(guò)TopDocs 的scoreDocs()方法,拿到
ScoreDoc,通過(guò)ScoreDoc,得到對(duì)應(yīng)的
文檔編號(hào),IndexSearcher 通過(guò)文檔編
號(hào),使用IndexReader 對(duì)指定目錄下的
索引內(nèi)容進(jìn)行讀取,得到命中的文檔后
28 一元分詞,即將給定的字符串以一個(gè)字為單位進(jìn)行切割分詞,這種分詞方式較為明顯的缺陷就是語(yǔ)義
不準(zhǔn),如“上海”兩個(gè)字被切割成“上”、“?!?,但是包含“上?!?、“海上”的文檔都會(huì)命中。
29 二元分詞比一元分詞更符合中文的習(xí)慣,因?yàn)橹形牡拇蟛糠衷~匯都是兩個(gè)字,但是問(wèn)題依然存在。
30 詞庫(kù)分詞就是使用詞庫(kù)中定義的詞來(lái)對(duì)字符串進(jìn)行切分,這樣的好處是分詞更為準(zhǔn)確,但是效率較N
元分詞更低,且難以識(shí)別互聯(lián)網(wǎng)世界中層出不窮的新興詞匯。
圖2-21 Lucene 索引搜索過(guò)程
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 109
返回。
2.4.2 Lucene 的使用
Lucene 為搜索引擎提供了強(qiáng)大的、令人驚嘆的API,在企業(yè)的垂直化搜索領(lǐng)域得到了極為
廣泛的應(yīng)用。為了學(xué)習(xí)搜索引擎的基本原理,有效地使用Lucene,并將其引入到我們的應(yīng)用程
序當(dāng)中,本節(jié)將介紹Lucene 的一些常用的API 和使用方法,以及索引的優(yōu)化和分布式擴(kuò)展。
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)建索引存儲(chǔ)的目錄Directory,索引最終將被存放到該目錄。然后初始化
Document,給Document 添加Field,包括名稱(chēng)、地址、性別和個(gè)人介紹信息。Field 的第一個(gè)參
數(shù)為Field 的名稱(chēng);第二個(gè)參數(shù)為Filed 的值;第三個(gè)參數(shù)表示該Field 是否會(huì)被存儲(chǔ)。Store.NO
表示索引中不存儲(chǔ)該Field;Store.YES 表示索引中存儲(chǔ)該Field;如果是Store.COMPRESS,則
表示壓縮存儲(chǔ)。最后一個(gè)參數(shù)表示是否對(duì)該字段進(jìn)行檢索。Index.ANALYZED 表示需對(duì)該字段
進(jìn)行全文檢索,該Field 需要使用分詞器進(jìn)行分詞;Index.NOT_ANALYZED 表示不進(jìn)行全文檢
索,因此不需要分詞;Index.NO 表示不進(jìn)行索引。創(chuàng)建一個(gè)IndexWriter,用來(lái)寫(xiě)入索引,初始
化時(shí)需要指定索引存放的目錄,以及索引建立時(shí)使用的分詞器,此處用的是Lucene 自帶的中文
分詞器StandardAnalyzer,最后一個(gè)參數(shù)則用來(lái)指定是否限制Field 的最大長(zhǎng)度。
2. 索引更新與刪除
很多情況下,在搜索引擎首次構(gòu)建完索引之后,數(shù)據(jù)還有可能再次被更改,此時(shí)如果不將
最新的數(shù)據(jù)同步到搜索引擎,則有可能檢索到過(guò)期的數(shù)據(jù)。遺憾的是,Lucene 暫時(shí)還不支持對(duì)
于Document 單個(gè)Field 或者整個(gè)Document 的更新,因此這里所說(shuō)的更新,實(shí)際上是刪除舊的
110 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
Document,然后再向索引中添加新的Document。所添加的新的Document 必須包含所有的Field,
包括沒(méi)有更改的Field:
IndexWriter indexWriter = new IndexWriter(dir,analyzer, MaxFieldLength.LIMITED);
indexWriter.deleteDocuments(new Term("name","zhansan"));
indexWriter.addDocument(doc);
IndexWriter 的deleteDocuments 可以根據(jù)Term 來(lái)刪除Document。請(qǐng)注意Term 匹配的準(zhǔn)確
性,一個(gè)不正確的Term 可能會(huì)導(dǎo)致搜索引擎的大量索引被誤刪。Lucene 的IndexWriter 也提供
經(jīng)過(guò)封裝的updateDocument 方法,其實(shí)質(zhì)仍然是先刪除Term 所匹配的索引,然后再新增對(duì)應(yīng)
的Document:
indexWriter.updateDocument(new Term("name","zhansan"), doc);
3. 條件查詢(xún)
索引構(gòu)建完之后,就需要對(duì)相關(guān)的內(nèi)容進(jìn)行查詢(xú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);
}
查詢(xún)所使用的字符串為人名zhansan,查詢(xún)的Field 包括name 和introduce。構(gòu)建一個(gè)查詢(xún)
MultiFieldQueryParser 解析器,對(duì)查詢(xún)的內(nèi)容進(jìn)行解析,生成Query;然后通過(guò)IndexSearcher
來(lái)對(duì)Query 進(jìn)行查詢(xún),查詢(xún)將返回TopDocs,TopDocs 中包含了命中的總條數(shù)與命中的Document
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 111
的文檔編號(hào);最后通過(guò)IndexSearcher 讀取指定文檔編號(hào)的文檔內(nèi)容,并進(jìn)行輸出。
Lucene 支持多種查詢(xún)方式,比如針對(duì)某個(gè)Field 進(jìn)行關(guān)鍵字查詢(xún):
Term term = new Term("name","zhansan");
Query termQuery = new TermQuery(term);
Term 中包含了查詢(xún)的Field 的名稱(chēng)與需要匹配的文本值,termQuery 將命中名稱(chēng)為name 的
Field 中包含zhansan 這個(gè)關(guān)鍵字的Document。
也可以針對(duì)某個(gè)范圍對(duì)Field 的值進(jìn)行區(qū)間查詢(xún):
NumericRangeQuery numericRangeQuery
= NumericRangeQuery.newIntRange("size", 2, 100, true, true);
假設(shè) Document 包含一個(gè)名稱(chēng)為size 的數(shù)值型的Field,可以針對(duì)size 進(jìn)行范圍查詢(xún),指定
查詢(xún)的范圍為2~100,后面兩個(gè)參數(shù)表示是否包含查詢(xún)的邊界值。
還可以通過(guò)通配符來(lái)對(duì)Field 進(jìn)行查詢(xún):
Term wildcardTerm = new Term("name","zhansa?");
WildcardQuery wildcardQuery = new WildcardQuery(wildcardTerm);
通配符可以讓我們使用不完整、缺少某些字母的項(xiàng)進(jìn)行查詢(xún),但是仍然能夠查詢(xún)到匹配的
結(jié)果,如指定對(duì)name 的查詢(xún)內(nèi)容為“zhansa?”,?表示0 個(gè)或者一個(gè)字母,這將命中name 的值
為zhansan 的Document,如果使用*,則代表0 個(gè)或者多個(gè)字母。
假設(shè)某一段落中包含這樣一句話(huà)“I have a lovely white dog and a black lazy cat”,即使不知
道這句話(huà)的完整寫(xiě)法,也可以通過(guò)PhraseQuery 查找到包含dog 和cat 兩個(gè)關(guān)鍵字,并且dog 和
cat 之間的距離不超過(guò)5 個(gè)單詞的document:
PhraseQuery phraseQuery = new PhraseQuery();
phraseQuery.add(new Term("content","dog"));
phraseQuery.add(new Term("content","cat"));
phraseQuery.setSlop(5);
其中,content 為查詢(xún)對(duì)應(yīng)的Field,dog 和cat 分別為查詢(xún)的短語(yǔ),而phraseQuery.setSlop(5)
表示兩個(gè)短語(yǔ)之間最多不超過(guò)5 個(gè)單詞,兩個(gè)Field 之間所允許的最大距離稱(chēng)為slop。
除這些之外,Lucene 還支持將不同條件組合起來(lái)進(jìn)行復(fù)雜查詢(xún):
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è)計(jì)與實(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 為前面所說(shuō)的短語(yǔ)查詢(xún),而query2 則為通配符查詢(xún),通過(guò)BooleanQuery 將兩個(gè)查
詢(xún)條件組合起來(lái)。需要注意的是,Occur.MUST 表示只有符合該條件的Document 才會(huì)被包含在
查詢(xún)結(jié)果中;Occur.SHOULD 表示該條件是可選的;Occur.MUST_NOT 表示只有不符合該條件
的Document 才能夠被包含到查詢(xún)結(jié)果中。
4. 結(jié)果排序
Lucene 不僅支持多個(gè)條件的復(fù)雜查詢(xún),還支持按照指定的Field 對(duì)查詢(xún)結(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);
}
通過(guò)新建一個(gè)Sort,指定排序的Field 為size,F(xiàn)ield 的類(lèi)型為SortField.INT,表示按照整數(shù)
類(lèi)型進(jìn)行排序,而不是字符串類(lèi)型,SortField 的第三個(gè)參數(shù)用來(lái)指定是否對(duì)排序結(jié)果進(jìn)行反轉(zhuǎn)。
在查詢(xún)時(shí),使用IndexSearcher 的一個(gè)重構(gòu)方法,帶上Sort 參數(shù),則能夠讓查詢(xún)的結(jié)果按照指定
的字段進(jìn)行排序:
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 113
如果是多個(gè)Field 同時(shí)進(jìn)行查詢(xún),可以指定每個(gè)Field 擁有不同的權(quán)重,以便匹配時(shí)可以按
照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è)計(jì)與實(shí)踐
假設(shè)查詢(xún)串中包含zhansan 和shanghai 兩個(gè)查詢(xún)串,設(shè)置Field name 的權(quán)重為4,而設(shè)置
Field address 的權(quán)重為2,如按照Field 的權(quán)重進(jìn)行查詢(xún)排序,那么同時(shí)包含zhansan 和shanghai
的Document 將排在最前面,其次是name 為zhansan 的Document,最后是address 為shanghai
的Document:
5. 高亮
查詢(xún)到匹配的文檔后,需要對(duì)匹配的內(nèi)容進(jìn)行突出展現(xiàn),最直接的方式就是對(duì)匹配的內(nèi)容
高亮顯示。對(duì)于搜索list 來(lái)說(shuō),由于文檔的內(nèi)容可能比較長(zhǎng),為了控制展示效果,還需要對(duì)文
檔的內(nèi)容進(jìn)行摘要,提取相關(guān)度最高的內(nèi)容進(jìn)行展現(xiàn),Lucene 都能夠很好地滿(mǎn)足這些需求:
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);
通過(guò)構(gòu)建高亮的Formatter 來(lái)指定高亮的HTML 前綴和HTML 后綴,這里用的是font 標(biāo)簽。
查詢(xún)短語(yǔ)在被分詞后構(gòu)建一個(gè)QueryScorer,QueryScorer 中包含需要高亮顯示的關(guān)鍵字,
Fragmenter 則用來(lái)對(duì)較長(zhǎng)的Field 內(nèi)容進(jìn)行摘要,提取相關(guān)度較大的內(nèi)容,參數(shù)20 表示截取前
20 個(gè)字符進(jìn)行展現(xiàn)。構(gòu)建一個(gè)Highlighter,用來(lái)對(duì)Document 的指定Field 進(jìn)行高亮格式化:
String hi = highLight.getBestFragment(analyzer, "introduce", doc.get
("introduce"));
查詢(xún)命中相應(yīng)的Document 后,通過(guò)構(gòu)建的Highlighter,對(duì)Document 指定的Field 進(jìn)行高
亮格式化,并且對(duì)相關(guān)度最大的一塊內(nèi)容進(jìn)行摘要,得到摘要內(nèi)容。假設(shè)對(duì)dog 進(jìn)行搜索,
introduce 中如包含有dog,那么使用Highlighter 高亮并摘要后的內(nèi)容如下:
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 115
6. 中文分詞
Lucene 提供的標(biāo)準(zhǔn)中文分詞器StandardAnalyzer 只能夠進(jìn)行簡(jiǎn)單的一元分詞,一元分詞以
一個(gè)字為單位進(jìn)行語(yǔ)義切分,這種本來(lái)為西文所設(shè)計(jì)的分詞器,用于中文的分詞時(shí)經(jīng)常會(huì)出現(xiàn)
語(yǔ)義不準(zhǔn)確的情況??梢酝ㄟ^(guò)使用一些其他中文分詞器來(lái)避免這種情況,常用的中文分詞器包
括Lucene 自帶的中日韓文分詞器CJKAnalyzer,國(guó)內(nèi)也有一些開(kāi)源的中文分詞器,包括IK 分
詞31、MM 分詞32,以及庖丁分詞33、imdict 分詞器34等。假設(shè)有下面一段文字:
String zhContent = "我是一個(gè)中國(guó)人,我熱愛(ài)我的國(guó)家";
分詞之后,通過(guò)下面一段代碼可以將分詞的結(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();
}
通過(guò) StandardAnalyzer 分詞得到的分詞結(jié)果如下:
Analyzer standarAnalyzer = new StandardAnalyzer(Version.LUCENE_CURRENT);
由此可以得知,StandardAnalyzer 采用的是一元分詞,即字符串以一個(gè)字為單位進(jìn)行切割。
使用 CJKAnalyzer 分詞器進(jìn)行分詞,得到的結(jié)果如下:
31 IK 分詞項(xiàng)目地址為https://code.google.com/p/ik-analyzer。
32 MM 分詞項(xiàng)目地址為https://code.google.com/p/mmseg4j。
33 庖丁分詞項(xiàng)目地址為https://code.google.com/p/paoding。
34 imdict 分詞項(xiàng)目地址為https://code.google.com/p/imdict-chinese-analyzer。
116 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
Analyzer cjkAnalyzer = new CJKAnalyzer();
通過(guò)分詞的結(jié)果可以看到,CJKAnalyzer 采用的是二元分詞,即字符串以?xún)蓚€(gè)字為單位進(jìn)
行切割。
使用開(kāi)源的IK 分詞的效果如下:
Analyzer ikAnalyzer = new IKAnalyzer()
可以看到,分詞的效果比單純的一元或者二元分詞要好很多。
使用 MM 分詞器分詞的效果如下:
Analyzer mmAnalyzer = new MMAnalyzer()
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 117
7. 索引優(yōu)化
Lucene 的索引是由段(segment)組成的,每個(gè)段可能又包含多個(gè)索引文件,即每個(gè)段包含
了一個(gè)或者多個(gè)Document;段結(jié)構(gòu)使得Lucene 可以很好地支持增量索引,新增的Document
將被添加到新的索引段當(dāng)中。但是,當(dāng)越來(lái)越多的段被添加到索引當(dāng)中時(shí),索引文件也就越來(lái)
越多。一般來(lái)說(shuō),操作系統(tǒng)對(duì)于進(jìn)程打開(kāi)的文件句柄數(shù)是有限的,當(dāng)一個(gè)進(jìn)程打開(kāi)太多的文件
時(shí),會(huì)拋出too many open files 異常,并且執(zhí)行搜索任務(wù)時(shí),Lucene 必須分別搜索每個(gè)段,然后
將各個(gè)段的搜索結(jié)果合并,這樣查詢(xún)的性能就會(huì)降低。
為了提高 Lucene 索引的查詢(xún)性能,當(dāng)索引段的數(shù)量達(dá)到設(shè)置的上限時(shí),Lucene 會(huì)自動(dòng)進(jìn)
行索引段的優(yōu)化,將索引段合并成為一個(gè),以提高查詢(xún)的性能,并減少進(jìn)程打開(kāi)的文件句柄數(shù)
量。但是,索引段的合并需要大量的I/O 操作,并且需要耗費(fèi)相當(dāng)?shù)臅r(shí)間。雖然這樣的工作做
完以后,可以提高搜索引擎查詢(xún)的性能,但在索引合并的過(guò)程中,查詢(xún)的性能將受到很大影響,
這對(duì)于前臺(tái)應(yīng)用來(lái)說(shuō)一般是難以接受的。
因此,為了提高搜索引擎的查詢(xún)性能,需要盡可能地減少索引段的數(shù)量,另外,對(duì)于需要
應(yīng)對(duì)前端高并發(fā)查詢(xún)的應(yīng)用來(lái)說(shuō),對(duì)索引的自動(dòng)合并行為也需要進(jìn)行抑制,以提高查詢(xún)的性能。
一般來(lái)說(shuō),在分布式環(huán)境下,會(huì)安排專(zhuān)門(mén)的集群來(lái)生成索引,并且生成索引的集群不負(fù)責(zé)
處理前臺(tái)的查詢(xún)請(qǐng)求。當(dāng)索引生成以后,通過(guò)索引優(yōu)化,對(duì)索引的段進(jìn)行合并。合并完以后,
將生成好的索引文件分發(fā)到提供查詢(xún)服務(wù)的機(jī)器供前臺(tái)應(yīng)用查詢(xún)。當(dāng)然,數(shù)據(jù)會(huì)不斷地更新,
索引文件如何應(yīng)對(duì)增量的數(shù)據(jù)更新也是一個(gè)挑戰(zhàn)。對(duì)于少量索引來(lái)說(shuō),可以定時(shí)進(jìn)行全量的索
引重建,并且將索引推送到集群的其他機(jī)器,前提是相關(guān)業(yè)務(wù)系統(tǒng)能夠容忍數(shù)據(jù)有一定延遲。
但是,當(dāng)數(shù)據(jù)量過(guò)于龐大時(shí),索引的構(gòu)建需要很長(zhǎng)的時(shí)間,延遲的時(shí)間可能無(wú)法忍受,因此,
我們不得不接受索引有一定的瑕疵,即索引同時(shí)包含多個(gè)索引段,增量的更新請(qǐng)求將不斷地發(fā)
送給查詢(xún)機(jī)器。查詢(xún)機(jī)器可以將索引加載到內(nèi)存,并以固定的頻率回寫(xiě)磁盤(pán),每隔一定的周期,
對(duì)索引進(jìn)行一次全量的重建操作,以將增量更新所生成的索引段進(jìn)行合并。
8. 分布式擴(kuò)展
與其他的分布式系統(tǒng)架構(gòu)類(lèi)似,基于Lucene 的搜索引擎也會(huì)面臨擴(kuò)展的問(wèn)題,單臺(tái)機(jī)器難
以承受訪(fǎng)問(wèn)量不斷上升的壓力,不得不對(duì)其進(jìn)行擴(kuò)展。但是,與其他應(yīng)用不同的是,搜索應(yīng)用
大部分場(chǎng)景都能夠接受一定時(shí)間的數(shù)據(jù)延遲,對(duì)于數(shù)據(jù)一致性的要求并不那么高,大部分情況
下只要能夠保障數(shù)據(jù)的最終一致性,可以容忍一定時(shí)間上的數(shù)據(jù)不同步,一種擴(kuò)展的方式如
圖2-22 所示。
每個(gè) query server 實(shí)例保存一份完整的索引,該索引由dump server 周期性地生成,并進(jìn)行
索引段的合并,索引生成好之后推送到每臺(tái)query server 進(jìn)行替換,這樣避免集群索引dump 對(duì)
后端數(shù)據(jù)存儲(chǔ)造成壓力。當(dāng)然,對(duì)于增量的索引數(shù)據(jù)更新,dump server 可以異步地將更新推送
到每臺(tái)query server,或者是query server 周期性地到dump server 進(jìn)行數(shù)據(jù)同步,以保證數(shù)據(jù)最
118 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
終的一致性。對(duì)于前端的client 應(yīng)用來(lái)說(shuō),通過(guò)對(duì)請(qǐng)求進(jìn)行Hash,將請(qǐng)求均衡地分發(fā)到集群中
的每臺(tái)服務(wù)器,使得壓力能夠較為均衡地分布,這樣即達(dá)到了系統(tǒng)擴(kuò)展的目的。
圖2-22 搜索引擎索引的讀寫(xiě)分離
索引的讀/寫(xiě)分離解決的是請(qǐng)求分布的問(wèn)題,而對(duì)于數(shù)據(jù)量龐大的搜索引擎來(lái)說(shuō),單機(jī)對(duì)索
引的存儲(chǔ)能力畢竟有限。而且隨著索引數(shù)量的增加,檢索的速度也會(huì)隨之下降。此時(shí)索引本身
已經(jīng)成為系統(tǒng)的瓶頸,需要對(duì)索引進(jìn)行切分,將索引分布到集群的各臺(tái)機(jī)器上,以提高查詢(xún)性
能,降低存儲(chǔ)壓力,如圖2-23 所示。
圖2-23 索引的切分
在如圖 2-24 所示的架構(gòu)中,索引依據(jù)uniquekey%N,被切分到多臺(tái)index server 中進(jìn)行存
儲(chǔ)。client 應(yīng)用的查詢(xún)請(qǐng)求提交到merge server,merge server 將請(qǐng)求分發(fā)到index server 進(jìn)行檢
索,最后將查詢(xún)的結(jié)果進(jìn)行合并后,返回給client 應(yīng)用。對(duì)于全量的索引構(gòu)建,可以使用dump
第2 章 分布式系統(tǒng)基礎(chǔ)設(shè)施 │ 119
server 集群,以加快索引構(gòu)建的速度,并分擔(dān)存儲(chǔ)的壓力。而增量的更新請(qǐng)求,可以根據(jù)索引
的uniquekey 取模,將索引同步到index server;為避免merge server 出現(xiàn)單點(diǎn),可以對(duì)merge server
進(jìn)行高可用部署。當(dāng)然,索引切分的方案并非完美,可能也會(huì)帶來(lái)一些問(wèn)題。舉例來(lái)說(shuō),假如
查詢(xún)請(qǐng)求需要進(jìn)行結(jié)果排序,當(dāng)索引沒(méi)有切分時(shí)很好處理,只需要按照查詢(xún)指定的條件排列即
可,但是對(duì)切分后的索引來(lái)說(shuō),排序請(qǐng)求將被分發(fā)到每一臺(tái)index server 執(zhí)行排序,排完以后取
topN(出于性能考慮)發(fā)送到merge server 進(jìn)行合并,合并后的結(jié)果與真正的結(jié)果很可能存在
偏差,這就需要在業(yè)務(wù)上進(jìn)行取舍。
有的時(shí)候,可能既面臨高并發(fā)的用戶(hù)訪(fǎng)問(wèn)請(qǐng)求,又需要對(duì)海量的數(shù)據(jù)集進(jìn)行索引,這時(shí)就需
要綜合上述的兩種方法,即既采用索引讀寫(xiě)分離的方式,以支撐更大的并發(fā)訪(fǎng)問(wèn)量,又采用索引
切分的方式,以解決數(shù)據(jù)量膨脹所導(dǎo)致的存儲(chǔ)壓力以及索引性能下降的問(wèn)題,如圖2-24 所示。
圖2-24 既進(jìn)行讀寫(xiě)分離,又進(jìn)行索引切分
merge server 與index server 作為一組基本單元進(jìn)行復(fù)制,而前端應(yīng)用的請(qǐng)求通過(guò)Hash 被分
發(fā)到不同的組進(jìn)行處理;每一組與之前類(lèi)似,使用merge server 將請(qǐng)求分發(fā)到index server 進(jìn)行
索引的查詢(xún);查詢(xún)的結(jié)果將在merge server 進(jìn)行合并,合并完以后,再將結(jié)果返回給client。
120 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
2.4.3 Solr
Solr 是一個(gè)基于Lucene、功能強(qiáng)大的搜索引擎工具,它對(duì)Lucene 進(jìn)行了擴(kuò)展,提供一系列
功能強(qiáng)大的HTTP 操作接口,支持通過(guò)Data Schema 來(lái)定義字段、類(lèi)型和設(shè)置文本分析,使得
用戶(hù)可以通過(guò)HTTP POST 請(qǐng)求,向服務(wù)器提交Document,生成索引,以及進(jìn)行索引的更新和
刪除操作。對(duì)于復(fù)雜的查詢(xún)條件,Solr 提供了一整套表達(dá)式查詢(xún)語(yǔ)言,能夠更方便地實(shí)現(xiàn)包括
字段匹配、模糊查詢(xún)、分組統(tǒng)計(jì)等功能;同時(shí),Solr 還提供了強(qiáng)大的可配置能力,以及功能完
善的后臺(tái)管理系統(tǒng)。Solr 的架構(gòu)如圖2-25 所示。
圖2-25 Solr 的架構(gòu)35
1. Solr 的配置
通過(guò) Solr 的官方站點(diǎn)下載Solr:
wget http://apache./apache-mirror/lucene/solr/4.7.2/solr-4.7.2.tgz
35 圖片來(lái)源http://images./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,
否則中文將會(huì)亂碼,從而導(dǎo)致搜索查詢(xún)不到結(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,并通過(guò)修改tomcat 的啟
動(dòng)腳本catalina.sh 來(lái)指定solr.solr.home 變量所代表的Solr home 路徑。
CATALINA_OPTS="$CATALINA_OPTS -Dsolr.solr.home=/usr/solr"
啟動(dòng) Tomcat,訪(fǎng)問(wèn)Solr 的管理頁(yè)面,如圖2-26 所示。
圖2-26 Solr 的管理頁(yè)面
2. 構(gòu)建索引
在構(gòu)建索引之前,首先需要定義好Document 的schema。同數(shù)據(jù)庫(kù)建表有點(diǎn)類(lèi)似,即每個(gè)
Document 包含哪些Field,對(duì)應(yīng)的Field 的name 是什么,F(xiàn)ield 是什么類(lèi)型,是否被索引,是否
被存儲(chǔ),等等。假設(shè)我們要構(gòu)建一個(gè)討論社區(qū),需要對(duì)社區(qū)內(nèi)的帖子進(jìn)行搜索,那么搜索引擎
的Document 中應(yīng)該包含帖子信息、版塊信息、版主信息、發(fā)帖人信息、回復(fù)總數(shù)等內(nèi)容的聚
合,如圖2-27 所示。
122 │ 大型分布式網(wǎng)站架構(gòu)設(shè)計(jì)與實(shí)踐
圖 2-27 帖子、版塊、用戶(hù)、評(píng)論總數(shù)的關(guān)聯(lián)關(guān)系
其中,post 用來(lái)描述用戶(hù)發(fā)布的帖子信息,section 則表示版塊信息,user 代表該社區(qū)的用
戶(hù),comment_count 用來(lái)記錄帖子的評(píng)價(jià)總數(shù)。
對(duì)帖子信息建立搜索引擎的好處在于,由于帖子的數(shù)據(jù)量大,如采用MySQL 這一類(lèi)的關(guān)
系型數(shù)據(jù)庫(kù)來(lái)進(jìn)行存儲(chǔ)的話(huà),需要進(jìn)行分庫(kù)分表。數(shù)據(jù)經(jīng)過(guò)拆分之后,就難以同時(shí)滿(mǎn)足多維度
復(fù)雜條件查詢(xún)的需求,并且查詢(xún)可能需要版塊、帖子、用戶(hù)等多個(gè)表進(jìn)行關(guān)聯(lián)查詢(xún),導(dǎo)致查詢(xún)
性能下降,甚至回帖總數(shù)這樣的數(shù)據(jù)有可能根本就沒(méi)有存儲(chǔ)在關(guān)系型數(shù)據(jù)庫(kù)當(dāng)中,而通過(guò)搜索
引擎,這些需求都能夠很好地得到滿(mǎn)足。
搜索引擎對(duì)應(yīng)的schema 文件定義可能是下面這個(gè)樣子:
<?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è)計(jì)與實(shí)踐
fields 標(biāo)簽中所包含的就是定義的這些字段,包括對(duì)應(yīng)的字段名稱(chēng)、字段類(lèi)型、是否索引、
是否存儲(chǔ)、是否多值等;uniqueKey 指定了Document 的唯一鍵約束;types 標(biāo)簽中則定義了可
能用到的數(shù)據(jù)類(lèi)型。
使用 HTTP POST 請(qǐng)求可以給搜索引擎添加或者更新已存在的索引:
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ā)布,頂級(jí)網(wǎng)站用量超越Apache",
"poster_id": "340032",
"poster_nick": "hello123",
"post_content": "據(jù)W3Techs 統(tǒng)計(jì)數(shù)據(jù)顯示,全球Alexa 排名前100 萬(wàn)的網(wǎng)站
中的23.3%都在使用nginx,在排名前10 萬(wàn)的網(wǎng)站中,這一數(shù)據(jù)為30.7%,而在前1000 名的網(wǎng)站中,
nginx 的使用量超過(guò)了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
通過(guò)上述的HTTP POST 請(qǐng)求,便可將Document 添加到搜索引擎中。
3. 條件查詢(xún)
比 Lucene 更進(jìn)一步的是,Solr 支持將復(fù)雜條件組裝成HTTP 請(qǐng)求的參數(shù)表達(dá)式,使得用戶(hù)
能夠快速構(gòu)建復(fù)雜多樣的查詢(xún)條件,包括條件查詢(xún)、過(guò)濾查詢(xún)、僅返回指定字段、分頁(yè)、排序、
高亮、統(tǒng)計(jì)等,并且支持XML、JSON 等格式的輸出。舉例來(lái)說(shuō),假如需要根據(jù)post_id(帖子
id)來(lái)查詢(xún)對(duì)應(yīng)的帖子,可以使用下面的查詢(xún)請(qǐ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": "繼廣域動(dòng)態(tài)圖像、全動(dòng)態(tài)視頻和超光譜技術(shù)之后,Lidar
技術(shù)也受到關(guān)注和投資。這是由于上述技術(shù)的能力已經(jīng)在伊拉克和阿富汗得到試驗(yàn)和驗(yàn)證。",
"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è)計(jì)與實(shí)踐
"comment_count": 3,
"_version_": 1467083075564339200
}
]
}
}
假設(shè)頁(yè)面需要根據(jù)poster_id(發(fā)帖人id)和section_owner_nick(版主昵稱(chēng))作為條件來(lái)進(jìn)
行查詢(xún),并且根據(jù)uniqueKey 降序排列,以及根據(jù)section_id(版塊id)進(jìn)行分組統(tǒng)計(jì),那么查
詢(xún)的條件表達(dá)式可以這樣寫(xiě):
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 表示查詢(xún)的post_id 為
340032,section_owner_nick 為chenkangxian,兩個(gè)條件使用and 組合,而sort=post_id+asc 則表
示按照post_id 進(jìn)行升序排列,facet=true&facet.field=section_id 表示使用分組統(tǒng)計(jì),并且分組統(tǒng)
計(jì)字段為section_id。
當(dāng)然,Solr 還支持更多復(fù)雜的條件查詢(xún),此處就不再詳細(xì)介紹了36。
2.5 其他基礎(chǔ)設(shè)施
除了前面所提到的分布式緩存、持久化存儲(chǔ)、分布式消息系統(tǒng)、搜索引擎,大型的分布式
系統(tǒng)的背后,還依賴(lài)于其他支撐系統(tǒng),包括后面章節(jié)所要介紹的實(shí)時(shí)計(jì)算、離線(xiàn)計(jì)算、分布式
文件系統(tǒng)、日志收集系統(tǒng)、監(jiān)控系統(tǒng)、數(shù)據(jù)倉(cāng)庫(kù)等,以及本書(shū)沒(méi)有詳細(xì)介紹的CDN 系統(tǒng)、負(fù)
載均衡系統(tǒng)、消息推送系統(tǒng)、自動(dòng)化運(yùn)維系統(tǒng)等37。
36 更詳細(xì)的查詢(xún)語(yǔ)法介紹請(qǐng)參考Solr 官方wiki,http://wiki./solr/CommonQueryParameters#
head-6522ef80f22d0e50d2f12ec487758577506d6002。
37 這些系統(tǒng)雖然本書(shū)雖沒(méi)進(jìn)行詳細(xì)的介紹,但并不代表它們不重要,它們也是分布式系統(tǒng)的重要組成部
分,限于篇幅,此處僅一筆帶過(guò),讀者可自行查閱相關(guān)資料。     

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶(hù)發(fā)布,不代表本站觀(guān)點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多