|
第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 的使用。
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)資料。 |
|
|
來(lái)自: WindySky > 《分布式架構(gòu)》