在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ Linux/ Web 開發(fā)實(shí)戰(zhàn)2——商品詳情頁
Web 開發(fā)實(shí)戰(zhàn)2——商品詳情頁
流量復(fù)制 /AB 測試/協(xié)程
常用 Lua 開發(fā)庫 1-redis、mysql、http 客戶端
Lua 模塊開發(fā)
常用 Lua 開發(fā)庫 3-模板渲染
HTTP 服務(wù)
Nginx+Lua 開發(fā)入門
安裝 Nginx+Lua 開發(fā)環(huán)境
Redis/SSDB+Twemproxy 安裝與使用
JSON 庫、編碼轉(zhuǎn)換、字符串處理

Web 開發(fā)實(shí)戰(zhàn)2——商品詳情頁

本章以京東商品詳情頁為例,京東商品詳情頁雖然僅是單個頁面,但是其數(shù)據(jù)聚合源是非常多的,除了一些實(shí)時(shí)性要求比較高的如價(jià)格、庫存、服務(wù)支持等通過 AJAX 異步加載加載之外,其他的數(shù)據(jù)都是在后端做數(shù)據(jù)聚合然后拼裝網(wǎng)頁模板的。 http://item.jd.com/1217499.html

http://wiki.jikexueyuan.com/project/nginx-lua/images/10.png" alt="" />
http://wiki.jikexueyuan.com/project/nginx-lua/images/11.png" alt="" />

如圖所示,商品頁主要包括商品基本信息(基本信息、圖片列表、顏色/尺碼關(guān)系、擴(kuò)展屬性、規(guī)格參數(shù)、包裝清單、售后保障等)、商品介紹、其他信息(分類、品牌、店鋪【第三方賣家】、店內(nèi)分類【第三方賣家】、同類相關(guān)品牌)。更多細(xì)節(jié)此處就不闡述了。

整個京東有數(shù)億商品,如果每次動態(tài)獲取如上內(nèi)容進(jìn)行模板拼裝,數(shù)據(jù)來源之多足以造成性能無法滿足要求;最初的解決方案是生成靜態(tài)頁,但是靜態(tài)頁的最大的問題:1、無法迅速響應(yīng)頁面需求變更;2、很難做多版本線上對比測試。如上兩個因素足以制約商品頁的多樣化發(fā)展,因此靜態(tài)化技術(shù)不是很好的方案。

通過分析,數(shù)據(jù)主要分為四種:商品頁基本信息、商品介紹(異步加載)、其他信息(分類、品牌、店鋪等)、其他需要實(shí)時(shí)展示的數(shù)據(jù)(價(jià)格、庫存等)。而其他信息如分類、品牌、店鋪是非常少的,完全可以放到一個占用內(nèi)存很小的 Redis 中存儲;而商品基本信息我們可以借鑒靜態(tài)化技術(shù)將數(shù)據(jù)做聚合存儲,這樣的好處是數(shù)據(jù)是原子的,而模板是隨時(shí)可變的,吸收了靜態(tài)頁聚合的優(yōu)點(diǎn),彌補(bǔ)了靜態(tài)頁的多版本缺點(diǎn);另外一個非常嚴(yán)重的問題就是嚴(yán)重依賴這些相關(guān)系統(tǒng),如果它們掛了或響應(yīng)慢則商品頁就掛了或響應(yīng)慢;商品介紹我們也通過 AJAX 技術(shù)惰性加載(因?yàn)槭堑诙?,只有?dāng)用戶滾動鼠標(biāo)到該屏?xí)r才顯示);而實(shí)時(shí)展示數(shù)據(jù)通過 AJAX 技術(shù)做異步加載;因此我們可以做如下設(shè)計(jì):

  1. 接收商品變更消息,做商品基本信息的聚合,即從多個數(shù)據(jù)源獲取商品相關(guān)信息如圖片列表、顏色尺碼、規(guī)格參數(shù)、擴(kuò)展屬性等等,聚合為一個大的 JSON 數(shù)據(jù)做成數(shù)據(jù)閉環(huán),以 key-value存儲;因?yàn)槭情]環(huán),即使依賴的系統(tǒng)掛了我們商品頁還是能繼續(xù)服務(wù)的,對商品頁不會造成任何影響;
  2. 接收商品介紹變更消息,存儲商品介紹信息;
  3. 介紹其他信息變更消息,存儲其他信息。

整個架構(gòu)如下圖所示:

http://wiki.jikexueyuan.com/project/nginx-lua/images/12.png" alt="" />

技術(shù)選型

MQ 可以使用如 Apache ActiveMQ; Worker/ 動態(tài)服務(wù)可以通過如 Java 技術(shù)實(shí)現(xiàn); RPC 可以選擇如 alibaba Dubbo; KV 持久化存儲可以選擇 SSDB(如果使用 SSD 盤則可以選擇 SSDB+RocksDB 引擎)或者 ARDB( LMDB 引擎版); 緩存使用 Redis; SSDB/Redis 分片使用如 Twemproxy,這樣不管使用 Java 還是 Nginx+Lua,它們都不關(guān)心分片邏輯; 前端模板拼裝使用 Nginx+Lua; 數(shù)據(jù)集群數(shù)據(jù)存儲的機(jī)器可以采用 RAID 技術(shù)或者主從模式防止單點(diǎn)故障; 因?yàn)閿?shù)據(jù)變更不頻繁,可以考慮 SSD 替代機(jī)械硬盤。

核心流程

  1. 首先我們監(jiān)聽商品數(shù)據(jù)變更消息;
  2. 接收到消息后,數(shù)據(jù)聚合 Worker 通過 RPC 調(diào)用相關(guān)系統(tǒng)獲取所有要展示的數(shù)據(jù),此處獲取數(shù)據(jù)的來源可能非常多而且響應(yīng)速度完全受制于這些系統(tǒng),可能耗時(shí)幾百毫秒甚至上秒的時(shí)間;
  3. 將數(shù)據(jù)聚合為 JSON 串存儲到相關(guān)數(shù)據(jù)集群;
  4. 前端 Nginx 通過 Lua 獲取相關(guān)集群的數(shù)據(jù)進(jìn)行展示;商品頁需要獲取基本信息+其他信息進(jìn)行模板拼裝,即拼裝模板僅需要兩次調(diào)用(另外因?yàn)槠渌畔?shù)據(jù)量少且對一致性要求不高,因此我們完全可以緩存到 Nginx 本地全局內(nèi)存,這樣可以減少遠(yuǎn)程調(diào)用提高性能);當(dāng)頁面滾動到商品介紹頁面時(shí)異步調(diào)用商品介紹服務(wù)獲取數(shù)據(jù);
  5. 如果從聚合的 SSDB 集群 /Redis 中獲取不到相關(guān)數(shù)據(jù);則回源到動態(tài)服務(wù)通過 RPC 調(diào)用相關(guān)系統(tǒng)獲取所有要展示的數(shù)據(jù)返回(此處可以做限流處理,因?yàn)槿绻罅空埱筮^來的話可能導(dǎo)致服務(wù)雪崩,需要采取保護(hù)措施),此處的邏輯和數(shù)據(jù)聚合 Worker 完全一樣;然后發(fā)送 MQ 通知數(shù)據(jù)變更,這樣下次訪問時(shí)就可以從聚合的 SSDB 集群 /Redis 中獲取數(shù)據(jù)了。

基本流程如上所述,主要分為 Worker、動態(tài)服務(wù)、數(shù)據(jù)存儲和前端展示;因?yàn)橄到y(tǒng)非常復(fù)雜,只介紹動態(tài)服務(wù)和前端展示、數(shù)據(jù)存儲架構(gòu);Worker 部分不做實(shí)現(xiàn)。

項(xiàng)目搭建

項(xiàng)目部署目錄結(jié)構(gòu)。

/usr/chapter7
  ssdb_basic_7770.conf
  ssdb_basic_7771.conf
  ssdb_basic_7772.conf
  ssdb_basic_7773.conf
  ssdb_desc_8880.conf
  ssdb_desc_8881.conf
  ssdb_desc_8882.conf
  ssdb_desc_8883.conf
  redis_other_6660.conf
  redis_other_6661.conf
  nginx_chapter7.conf
  nutcracker.yml
  nutcracker.init
  item.html
  header.html
  footer.html
  item.lua
  desc.lua
  lualib
    item.lua
    item
      common.lua
  webapp
WEB-INF
   lib
   classes
   web.xml

數(shù)據(jù)存儲實(shí)現(xiàn)

http://wiki.jikexueyuan.com/project/nginx-lua/images/13.png" alt="" />

整體架構(gòu)為主從模式,寫數(shù)據(jù)到主集群,讀數(shù)據(jù)從從集群讀取數(shù)據(jù),這樣當(dāng)一個集群不足以支撐流量時(shí)可以使用更多的集群來支撐更多的訪問量;集群分片使用 Twemproxy 實(shí)現(xiàn)。

商品基本信息 SSDB 集群配置

vim /usr/chapter7/ssdb_basic_7770.conf \

Java 代碼

work_dir = /usr/data/ssdb_7770  
pidfile = /usr/data/ssdb_7770.pid  

server:  
        ip: 0.0.0.0  
        port: 7770  
        allow: 127.0.0.1  
        allow: 192.168  

replication:  
        binlog: yes  
        sync_speed: -1  
        slaveof:  
logger:  
        level: error  
        output: /usr/data/ssdb_7770.log  
        rotate:  
                size: 1000000000  

leveldb:  
        cache_size: 500  
        block_size: 32  
        write_buffer_size: 64  
        compaction_speed: 1000  
        compression: yes  

vim /usr/chapter7/ssdb_basic_7771.conf

Java 代碼

work_dir = /usr/data/ssdb_7771  
pidfile = /usr/data/ssdb_7771.pid  

server:  
        ip: 0.0.0.0  
        port: 7771  
        allow: 127.0.0.1  
        allow: 192.168  

replication:  
        binlog: yes  
        sync_speed: -1  
        slaveof:  
logger:  
        level: error  
        output: /usr/data/ssdb_7771.log  
        rotate:  
                size: 1000000000  

leveldb:  
        cache_size: 500  
        block_size: 32  
        write_buffer_size: 64  
        compaction_speed: 1000  
        compression: yes   

vim /usr/chapter7/ssdb_basic_7772.conf

Java 代碼

work_dir = /usr/data/ssdb_7772  
pidfile = /usr/data/ssdb_7772.pid  

server:  
        ip: 0.0.0.0  
        port: 7772  
        allow: 127.0.0.1  
        allow: 192.168  

replication:  
        binlog: yes  
        sync_speed: -1  
        slaveof:  
                type: sync  
                ip: 127.0.0.1  
                port: 7770  

logger:  
        level: error  
        output: /usr/data/ssdb_7772.log  
        rotate:  
                size: 1000000000  

leveldb:  
        cache_size: 500  
        block_size: 32  
        write_buffer_size: 64  
        compaction_speed: 1000  
        compression: yes  

vim /usr/chapter7/ssdb_basic_7773.conf

Java 代碼

work_dir = /usr/data/ssdb_7773  
pidfile = /usr/data/ssdb_7773.pid  

server:  
        ip: 0.0.0.0  
        port: 7773  
        allow: 127.0.0.1  
        allow: 192.168  

replication:  
        binlog: yes  
        sync_speed: -1  
        slaveof:  
                type: sync  
                ip: 127.0.0.1  
                port: 7771  

logger:  
        level: error  
        output: /usr/data/ssdb_7773.log  
        rotate:  
                size: 1000000000  

leveldb:  
        cache_size: 500  
        block_size: 32  
        write_buffer_size: 64  
        compaction_speed: 1000  
        compression: yes  

配置文件使用 Tab 而不是空格做縮排,(復(fù)制到配置文件后請把空格替換為 Tab )。主從關(guān)系:7770(主)-->7772(從),7771(主)--->7773(從);配置文件如何配置請參考 https://github.com/ideawu/ssdb-docs/blob/master/src/zh_cn/config.md。

創(chuàng)建工作目錄

Java 代碼

mkdir -p /usr/data/ssdb_7770  
mkdir -p /usr/data/ssdb_7771  
mkdir -p /usr/data/ssdb_7772  
mkdir -p /usr/data/ssdb_7773  

啟動

Java 代碼

nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_basic_7770.conf &  
nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_basic_7771.conf &  
nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_basic_7772.conf &  
nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_basic_7773.conf &   

通過 ps -aux | grep ssdb 命令看是否啟動了,tail -f nohup.out 查看錯誤信息。

商品介紹 SSDB 集群配置

vim /usr/chapter7/ssdb_desc_8880.conf

Java 代碼

work_dir = /usr/data/ssdb_8880  
pidfile = /usr/data/ssdb8880.pid  

server:  
        ip: 0.0.0.0  
        port: 8880  
        allow: 127.0.0.1  
        allow: 192.168  

replication:  
        binlog: yes  
        sync_speed: -1  
        slaveof:  
logger:  
        level: error  
        output: /usr/data/ssdb_8880.log  
        rotate:  
                size: 1000000000  

leveldb:  
        cache_size: 500  
        block_size: 32  
        write_buffer_size: 64  
        compaction_speed: 1000  
        compression: yes  

vim /usr/chapter7/ssdb_desc_8881.conf

Java 代碼

work_dir = /usr/data/ssdb_8881  
pidfile = /usr/data/ssdb8881.pid  

server:  
        ip: 0.0.0.0  
        port: 8881  
        allow: 127.0.0.1  
        allow: 192.168  

logger:  
        level: error  
        output: /usr/data/ssdb_8881.log  
        rotate:  
                size: 1000000000  

leveldb:  
        cache_size: 500  
        block_size: 32  
        write_buffer_size: 64  
        compaction_speed: 1000  
        compression: yes  

vim /usr/chapter7/ssdb_desc_8882.conf

Java 代碼

work_dir = /usr/data/ssdb_8882  
pidfile = /usr/data/ssdb_8882.pid  

server:  
        ip: 0.0.0.0  
        port: 8882  
        allow: 127.0.0.1  
        allow: 192.168  

replication:  
        binlog: yes  
        sync_speed: -1  
        slaveof:  
replication:  
        binlog: yes  
        sync_speed: -1  
        slaveof:  
                type: sync  
                ip: 127.0.0.1  
                port: 8880  

logger:  
        level: error  
        output: /usr/data/ssdb_8882.log  
        rotate:  
                size: 1000000000  

leveldb:  
        cache_size: 500  
        block_size: 32  
        write_buffer_size: 64  
        compaction_speed: 1000  
        compression: yes  

vim /usr/chapter7/ssdb_desc_8883.conf

Java 代碼

work_dir = /usr/data/ssdb_8883  
pidfile = /usr/data/ssdb_8883.pid  

server:  
        ip: 0.0.0.0  
        port: 8883  
        allow: 127.0.0.1  
        allow: 192.168  

replication:  
        binlog: yes  
        sync_speed: -1  
        slaveof:  
                type: sync  
                ip: 127.0.0.1  
                port: 8881  

logger:  
        level: error  
        output: /usr/data/ssdb_8883.log  
        rotate:  
                size: 1000000000  

leveldb:  
        cache_size: 500  
        block_size: 32  
        write_buffer_size: 64  
        compaction_speed: 1000  
        compression: yes  

配置文件使用 Tab 而不是空格做縮排(復(fù)制到配置文件后請把空格替換為 Tab )。主從關(guān)系:7770(主)-->7772(從),7771(主)--->7773(從);配置文件如何配置請參考 https://github.com/ideawu/ssdb-docs/blob/master/src/zh_cn/config.md。

創(chuàng)建工作目錄

Java 代碼

mkdir -p /usr/data/ssdb_888{0,1,2,3}  

啟動

Java 代碼

nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_desc_8880.conf &  
nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_desc_8881.conf &  
nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_desc_8882.conf &  
nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_desc_8883.conf &   

通過 ps -aux | grep ssdb 命令看是否啟動了,tail -f nohup.out 查看錯誤信息。

其他信息 Redis 配置

vim /usr/chapter7/redis_6660.conf

Java 代碼

port 6660  
pidfile "/var/run/redis_6660.pid"  
\#設(shè)置內(nèi)存大小,根據(jù)實(shí)際情況設(shè)置,此處測試僅設(shè)置20mb  
maxmemory 20mb  
\#內(nèi)存不足時(shí),所有KEY按照LRU算法刪除  
maxmemory-policy allkeys-lru  
\#Redis的過期算法不是精確的而是通過采樣來算的,默認(rèn)采樣為3個,此處我們改成10  
maxmemory-samples 10  
\#不進(jìn)行RDB持久化  
save “”  
\#不進(jìn)行AOF持久化  
appendonly no  

vim /usr/chapter7/redis_6661.conf

Java 代碼

port 6661  
pidfile "/var/run/redis_6661.pid"  
\#設(shè)置內(nèi)存大小,根據(jù)實(shí)際情況設(shè)置,此處測試僅設(shè)置20mb  
maxmemory 20mb  
\#內(nèi)存不足時(shí),所有KEY按照LRU算法進(jìn)行刪除  
maxmemory-policy allkeys-lru  
\#Redis的過期算法不是精確的而是通過采樣來算的,默認(rèn)采樣為3個,此處我們改成10  
maxmemory-samples 10  
\#不進(jìn)行RDB持久化  
save “”  
\#不進(jìn)行AOF持久化  
appendonly no  
\#主從  
slaveof 127.0.0.1 6660  

vim /usr/chapter7/redis_6662.conf

Java 代碼

port 6662  
pidfile "/var/run/redis_6662.pid"  
\#設(shè)置內(nèi)存大小,根據(jù)實(shí)際情況設(shè)置,此處測試僅設(shè)置20mb  
maxmemory 20mb  
\#內(nèi)存不足時(shí),所有KEY按照LRU算法進(jìn)行刪除  
maxmemory-policy allkeys-lru  
\#Redis的過期算法不是精確的而是通過采樣來算的,默認(rèn)采樣為3個,此處我們改成10  
maxmemory-samples 10  
\#不進(jìn)行RDB持久化  
save “”  
\#不進(jìn)行AOF持久化  
appendonly no  
\#主從  
slaveof 127.0.0.1 6660    

如上配置放到配置文件最末尾即可;此處內(nèi)存不足時(shí)的驅(qū)逐算法為所有 KEY 按照 LRU 進(jìn)行刪除(實(shí)際是內(nèi)存基本上不會遇到滿的情況);主從關(guān)系:6660(主)-->6661(從)和6660(主)-->6662(從)。

啟動

Java 代碼

nohup /usr/servers/redis-2.8.19/src/redis-server /usr/chapter7/redis_6660.conf &  
nohup /usr/servers/redis-2.8.19/src/redis-server /usr/chapter7/redis_6661.conf &  
nohup /usr/servers/redis-2.8.19/src/redis-server /usr/chapter7/redis_6662.conf &  

通過 ps -aux | grep redis 命令看是否啟動了,tail -f nohup.out 查看錯誤信息。

測試

測試時(shí)在主 SSDB/Redis 中寫入數(shù)據(jù),然后從從 SSDB/Redis 能讀取到數(shù)據(jù)即表示配置主從成功。

測試商品基本信息 SSDB 集群

Java 代碼

root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 7770  
127.0.0.1:7770> set i 1  
OK  
127.0.0.1:7770>   
root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 7772  
127.0.0.1:7772> get i  
"1"  

測試商品介紹 SSDB 集群

Java 代碼

root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 8880  
127.0.0.1:8880> set i 1  
OK  
127.0.0.1:8880>   
root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 8882  
127.0.0.1:8882> get i  
"1"  

測試其他信息集群

Java 代碼

root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 6660  
127.0.0.1:6660> set i 1  
OK  
127.0.0.1:6660> get i  
"1"  
127.0.0.1:6660>   
root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 6661  
127.0.0.1:6661> get i  
"1"  

Twemproxy 配置

vim /usr/chapter7/nutcracker.yml

Java 代碼

basic_master:  
  listen: 127.0.0.1:1111  
  hash: fnv1a_64  
  distribution: ketama  
  redis: true  
  timeout: 1000  
  hash_tag: "::"  
  servers:  
   - 127.0.0.1:7770:1 server1  
   - 127.0.0.1:7771:1 server2  

basic_slave:  
  listen: 127.0.0.1:1112  
  hash: fnv1a_64  
  distribution: ketama  
  redis: true  
  timeout: 1000  
  hash_tag: "::"  
  servers:  
   - 127.0.0.1:7772:1 server1  
   - 127.0.0.1:7773:1 server2  

desc_master:  
  listen: 127.0.0.1:1113  
  hash: fnv1a_64  
  distribution: ketama  
  redis: true  
  timeout: 1000  
  hash_tag: "::"  
  servers:  
   - 127.0.0.1:8880:1 server1  
   - 127.0.0.1:8881:1 server2  

desc_slave:  
  listen: 127.0.0.1:1114  
  hash: fnv1a_64  
  distribution: ketama  
  redis: true  
  timeout: 1000  
  servers:  
   - 127.0.0.1:8882:1 server1  
   - 127.0.0.1:8883:1 server2  

other_master:  
  listen: 127.0.0.1:1115  
  hash: fnv1a_64  
  distribution: random  
  redis: true  
  timeout: 1000  
  hash_tag: "::"  
  servers:  
   - 127.0.0.1:6660:1 server1  

other_slave:  
  listen: 127.0.0.1:1116  
  hash: fnv1a_64  
  distribution: random  
  redis: true  
  timeout: 1000  
  hash_tag: "::"  
  servers:  
   - 127.0.0.1:6661:1 server1  
   - 127.0.0.1:6662:1 server2   
  1. 因?yàn)槲覀兪褂昧酥鲝模孕枰o server 起一個名字如 server1、server2;否則分片算法默認(rèn)根據(jù) ip:port:weight,這樣就會主從數(shù)據(jù)的分片算法不一致;
  2. 其他信息 Redis 因?yàn)槊總€ Redis 是對等的,因此分片算法可以使用 random;
  3. 我們使用了 hash_tag,可以保證相同的 tag 在一個分片上(本例配置了但沒有用到該特性)。

復(fù)制第六章的 nutcracker.init,幫把配置文件改為 usr/chapter7/nutcracker.yml。然后通過 /usr/chapter7/nutcracker.init start 啟動 Twemproxy。

測試主從集群是否工作正常:

Java 代碼

root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1111  
127.0.0.1:1111> set i 1  
OK  
127.0.0.1:1111>   
root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1112  
127.0.0.1:1112> get i  
"1"  
127.0.0.1:1112>   
root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1113  
127.0.0.1:1113> set i 1  
OK  
127.0.0.1:1113>   
root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1114  
127.0.0.1:1114> get i  
"1"  
127.0.0.1:1114>   
root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1115  
127.0.0.1:1115> set i 1  
OK  
127.0.0.1:1115>   
root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1116  
127.0.0.1:1116> get i  
"1"  

到此數(shù)據(jù)集群配置成功。

動態(tài)服務(wù)實(shí)現(xiàn)

因?yàn)檎鎸?shí)數(shù)據(jù)是從多個子系統(tǒng)獲取,很難模擬這么多子系統(tǒng)交互,所以此處我們使用假數(shù)據(jù)來進(jìn)行實(shí)現(xiàn)。

項(xiàng)目搭建

我們使用 Maven 搭建 Web 項(xiàng)目,Maven 知識請自行學(xué)習(xí)。

項(xiàng)目依賴

本文將最小化依賴,即僅依賴我們需要的 servlet、jackson、guava、jedis。

Java 代碼

<dependencies>  
  <dependency>  
    <groupId>javax.servlet</groupId>  
    <artifactId>javax.servlet-api</artifactId>  
    <version>3.0.1</version>  
    <scope>provided</scope>  
  </dependency>  
  <dependency>  
    <groupId>com.google.guava</groupId>  
    <artifactId>guava</artifactId>  
    <version>17.0</version>  
  </dependency>  
  <dependency>  
    <groupId>redis.clients</groupId>  
    <artifactId>jedis</artifactId>  
    <version>2.5.2</version>  
  </dependency>  
  <dependency>  
    <groupId>com.fasterxml.jackson.core</groupId>  
    <artifactId>jackson-core</artifactId>  
    <version>2.3.3</version>  
  </dependency>  
  <dependency>  
    <groupId>com.fasterxml.jackson.core</groupId>  
    <artifactId>jackson-databind</artifactId>  
    <version>2.3.3</version>  
  </dependency>  
</dependencies>  

guava 是類似于 apache commons 的一個基礎(chǔ)類庫,用于簡化一些重復(fù)操作,可以參考http://ifeve.com/google-guava/。

核心代碼

com.github.zhangkaitao.chapter7.servlet.ProductServiceServlet

Java 代碼

@Override  
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
    String type = req.getParameter("type");  
    String content = null;  
    try {  
        if("basic".equals(type)) {  
            content = getBasicInfo(req.getParameter("skuId"));  
        } else if("desc".equals(type)) {  
            content = getDescInfo(req.getParameter("skuId"));  
        } else if("other".equals(type)) {  
            content = getOtherInfo(req.getParameter("ps3Id"), req.getParameter("brandId"));  
        }  
    } catch (Exception e) {  
        e.printStackTrace();  
        resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);  
        return;  
    }  
    if(content != null) {  
        resp.setCharacterEncoding("UTF-8");  
        resp.getWriter().write(content);  
    } else {  
        resp.setStatus(HttpServletResponse.SC_NOT_FOUND);  
    }  
}    

根據(jù)請求參數(shù) type 來決定調(diào)用哪個服務(wù)獲取數(shù)據(jù)。

基本信息服務(wù)

Java 代碼

private String getBasicInfo(String skuId) throws Exception {  
    Map<String, Object> map = new HashMap<String, Object>();  
    //商品編號  
    map.put("skuId", skuId);  
    //名稱  
    map.put("name", "蘋果(Apple)iPhone 6 (A1586) 16GB 金色 移動聯(lián)通電信4G手機(jī)");  
    //一級二級三級分類  
    map.put("ps1Id", 9987);  
    map.put("ps2Id", 653);  
    map.put("ps3Id", 655);  
    //品牌ID  
    map.put("brandId", 14026);  
    //圖片列表  
    map.put("imgs", getImgs(skuId));  
    //上架時(shí)間  
    map.put("date", "2014-10-09 22:29:09");  
    //商品毛重  
    map.put("weight", "400");  
    //顏色尺碼  
    map.put("colorSize", getColorSize(skuId));  
    //擴(kuò)展屬性  
    map.put("expands", getExpands(skuId));  
    //規(guī)格參數(shù)  
    map.put("propCodes", getPropCodes(skuId));  
    map.put("date", System.currentTimeMillis());  
    String content = objectMapper.writeValueAsString(map);  
    //實(shí)際應(yīng)用應(yīng)該是發(fā)送MQ  
    asyncSetToRedis(basicInfoJedisPool, "p:" + skuId + ":", content);  
    return objectMapper.writeValueAsString(map);  
}  

private List<String> getImgs(String skuId) {  
    return Lists.newArrayList(  
            "jfs/t277/193/1005339798/768456/29136988/542d0798N19d42ce3.jpg",  
            "jfs/t352/148/1022071312/209475/53b8cd7f/542d079bN3ea45c98.jpg",  
            "jfs/t274/315/1008507116/108039/f70cb380/542d0799Na03319e6.jpg",  
            "jfs/t337/181/1064215916/27801/b5026705/542d079aNf184ce18.jpg"  
    );  
}  

private List<Map<String, Object>> getColorSize(String skuId) {  
    return Lists.newArrayList(  
        makeColorSize(1217499, "金色", "公開版(16GB ROM)"),  
        makeColorSize(1217500, "深空灰", "公開版(16GB ROM)"),  
        makeColorSize(1217501, "銀色", "公開版(16GB ROM)"),  
        makeColorSize(1217508, "金色", "公開版(64GB ROM)"),  
        makeColorSize(1217509, "深空灰", "公開版(64GB ROM)"),  
        makeColorSize(1217509, "銀色", "公開版(64GB ROM)"),  
        makeColorSize(1217493, "金色", "移動4G版 (16GB)"),  
        makeColorSize(1217494, "深空灰", "移動4G版 (16GB)"),  
        makeColorSize(1217495, "銀色", "移動4G版 (16GB)"),  
        makeColorSize(1217503, "金色", "移動4G版 (64GB)"),  
        makeColorSize(1217503, "金色", "移動4G版 (64GB)"),  
        makeColorSize(1217504, "深空灰", "移動4G版 (64GB)"),  
        makeColorSize(1217505, "銀色", "移動4G版 (64GB)")  
    );  
}  
private Map<String, Object> makeColorSize(long skuId, String color, String size) {  
    Map<String, Object> cs1 = Maps.newHashMap();  
    cs1.put("SkuId", skuId);  
    cs1.put("Color", color);  
    cs1.put("Size", size);  
    return cs1;  
}  

private List<List<?>> getExpands(String skuId) {  
    return Lists.newArrayList(  
            (List<?>)Lists.newArrayList("熱點(diǎn)", Lists.newArrayList("超薄7mm以下", "支持NFC")),  
            (List<?>)Lists.newArrayList("系統(tǒng)", "蘋果(IOS)"),  
            (List<?>)Lists.newArrayList("系統(tǒng)", "蘋果(IOS)"),  
            (List<?>)Lists.newArrayList("購買方式", "非合約機(jī)")  
    );  
}  

private Map<String, List<List<String>>> getPropCodes(String skuId) {  
    Map<String, List<List<String>>> map = Maps.newHashMap();  
    map.put("主體", Lists.<List<String>>newArrayList(  
            Lists.<String>newArrayList("品牌", "蘋果(Apple)"),  
            Lists.<String>newArrayList("型號", "iPhone 6 A1586"),  
            Lists.<String>newArrayList("顏色", "金色"),  
            Lists.<String>newArrayList("上市年份", "2014年")  
    ));  
    map.put("存儲", Lists.<List<String>>newArrayList(  
            Lists.<String>newArrayList("機(jī)身內(nèi)存", "16GB ROM"),  
            Lists.<String>newArrayList("儲存卡類型", "不支持")  
    ));  
    map.put("顯示", Lists.<List<String>>newArrayList(  
            Lists.<String>newArrayList("屏幕尺寸", "4.7英寸"),  
            Lists.<String>newArrayList("觸摸屏", "Retina HD"),  
            Lists.<String>newArrayList("分辨率", "1334 x 750")  
    ));  
    return map;  
}   

本例基本信息提供了如商品名稱、圖片列表、顏色尺碼、擴(kuò)展屬性、規(guī)格參數(shù)等等數(shù)據(jù);而為了簡化邏輯大多數(shù)數(shù)據(jù)都是 List/Map 數(shù)據(jù)結(jié)構(gòu)。

商品介紹服務(wù)

Java 代碼

private String getDescInfo(String skuId) throws Exception {  
    Map<String, Object> map = new HashMap<String, Object>();  
    map.put("content", "<div><img data-lazyload='http://img30.360buyimg.com/jgsq-productsoa/jfs/t448/127/574781110/103911/b3c80634/5472ba22N45400f4e.jpg' alt='' /><img data-lazyload='http://img30.360buyimg.com/jgsq-productsoa/jfs/t802/133/19465528/162152/e463e43/54e2b34aN11bceb70.jpg' alt='' height='386' width='750' /></div>");  
    map.put("date", System.currentTimeMillis());  
    String content = objectMapper.writeValueAsString(map);  
    //實(shí)際應(yīng)用應(yīng)該是發(fā)送MQ  
    asyncSetToRedis(descInfoJedisPool, "d:" + skuId + ":", content);  
    return objectMapper.writeValueAsString(map);  
}  

其他信息服務(wù)

Java 代碼

private String getOtherInfo(String ps3Id, String brandId) throws Exception {  
    Map<String, Object> map = new HashMap<String, Object>();  
    //面包屑  
    List<List<?>> breadcrumb = Lists.newArrayList();  
    breadcrumb.add(Lists.newArrayList(9987, "手機(jī)"));  
    breadcrumb.add(Lists.newArrayList(653, "手機(jī)通訊"));  
    breadcrumb.add(Lists.newArrayList(655, "手機(jī)"));  
    //品牌  
    Map<String, Object> brand = Maps.newHashMap();  
    brand.put("name", "蘋果(Apple)");  
    brand.put("logo", "BrandLogo/g14/M09/09/10/rBEhVlK6vdkIAAAAAAAFLXzp-lIAAHWawP_QjwAAAVF472.png");  
    map.put("breadcrumb", breadcrumb);  
    map.put("brand", brand);  
    //實(shí)際應(yīng)用應(yīng)該是發(fā)送MQ  
    asyncSetToRedis(otherInfoJedisPool, "s:" + ps3Id + ":", objectMapper.writeValueAsString(breadcrumb));  
    asyncSetToRedis(otherInfoJedisPool, "b:" + brandId + ":", objectMapper.writeValueAsString(brand));  
    return objectMapper.writeValueAsString(map);  
}    

本例中其他信息只使用了面包屑和品牌數(shù)據(jù)。

輔助工具

Java 代碼

private ObjectMapper objectMapper = new ObjectMapper();  
private JedisPool basicInfoJedisPool = createJedisPool("127.0.0.1", 1111);  
private JedisPool descInfoJedisPool = createJedisPool("127.0.0.1", 1113);  
private JedisPool otherInfoJedisPool = createJedisPool("127.0.0.1", 1115);  

private JedisPool createJedisPool(String host, int port) {  
    GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();  
    poolConfig.setMaxTotal(100);  
    return new JedisPool(poolConfig, host, port);  
}  

private ExecutorService executorService = Executors.newFixedThreadPool(10);  
private void asyncSetToRedis(final JedisPool jedisPool, final String key, final String content) {  
    executorService.submit(new Runnable() {  
        @Override  
        public void run() {  
            Jedis jedis = null;  
            try {  
                jedis = jedisPool.getResource();  
                jedis.set(key, content);  
            } catch (Exception e) {  
                e.printStackTrace();  
                jedisPool.returnBrokenResource(jedis);  
            } finally {  
                jedisPool.returnResource(jedis);  
            }  

        }  
    });  
}  

本例使用 Jackson 進(jìn)行 JSON 的序列化;Jedis 進(jìn)行 Redis 的操作;使用線程池做異步更新(實(shí)際應(yīng)用中可以使用 MQ 做實(shí)現(xiàn))。

web.xml 配置

Java 代碼

<servlet>  
    <servlet-name>productServiceServlet</servlet-name>  
    <servlet-class>com.github.zhangkaitao.chapter7.servlet.ProductServiceServlet</servlet-class>  
</servlet>  
<servlet-mapping>  
    <servlet-name>productServiceServlet</servlet-name>  
    <url-pattern>/info</url-pattern>  
</servlet-mapping>  

打 WAR 包

Java 代碼

cd D:\workspace\chapter7  
mvn clean package   

此處使用 maven 命令打包,比如本例將得到 chapter7.war,然后將其上傳到服務(wù)器的 /usr/chapter7/webapp,然后通過 unzip chapter6.war 解壓。

配置 Tomcat

復(fù)制第六章使用的 tomcat 實(shí)例:

Java 代碼

cd /usr/servers/  
cp -r tomcat-server1 tomcat-chapter7/  
vim /usr/servers/tomcat-chapter7/conf/Catalina/localhost/ROOT.xml   

Java 代碼

<!-- 訪問路徑是根,web應(yīng)用所屬目錄為/usr/chapter7/webapp -->  
<Context path="" docBase="/usr/chapter7/webapp"></Context>  

指向第七章的 web 應(yīng)用路徑。

測試

啟動 tomcat 實(shí)例。

Java 代碼

/usr/servers/tomcat-chapter7/bin/startup.sh  

訪問如下 URL 進(jìn)行測試。

Java 代碼

http://192.168.1.2:8080/info?type=basic&skuId=1  
http://192.168.1.2:8080/info?type=desc&skuId=1  
http://192.168.1.2:8080/info?type=other&ps3Id=1&brandId=1  

nginx 配置

vim /usr/chapter7/nginx_chapter7.conf

Java 代碼

upstream backend {  
    server 127.0.0.1:8080 max_fails=5 fail_timeout=10s weight=1;  
    check interval=3000 rise=1 fall=2 timeout=5000 type=tcp default_down=false;  
    keepalive 100;  
}  

server {  
    listen       80;  
    server_name  item2015.jd.com item.jd.com d.3.cn;  

    location ~ /backend/(.*) {  
        #internal;  
        keepalive_timeout   30s;  
        keepalive_requests  1000;  
        #支持keep-alive  
        proxy_http_version 1.1;  
        proxy_set_header Connection "";  

        rewrite /backend(/.*) $1 break;  
        proxy_pass_request_headers off;  
        #more_clear_input_headers Accept-Encoding;  
        proxy_next_upstream error timeout;  
        proxy_pass http://backend;  
    }  
}    

此處 server_name 我們指定了 item.jd.com (商品詳情頁)和 d.3.cn (商品介紹)。其他配置可以參考第六章內(nèi)容。另外實(shí)際生產(chǎn)環(huán)境要把 #internal 打開,表示只有本 nginx 能訪問。

vim /usr/servers/nginx/conf/nginx.conf

Java 代碼

include /usr/chapter7/nginx_chapter7.conf;  
\#為了方便測試,注釋掉example.conf  
include /usr/chapter6/nginx_chapter6.conf;  

Java 代碼

\#lua模塊路徑,其中”;;”表示默認(rèn)搜索路徑,默認(rèn)到/usr/servers/nginx下找  
lua_package_path "/usr/chapter7/lualib/?.lua;;";  #lua 模塊  
lua_package_cpath "/usr/chapter7/lualib/?.so;;";  #c模塊  
lua模塊從/usr/chapter7目錄加載,因?yàn)槲覀円獙懽约旱哪K使用。

重啟 nginx

/usr/servers/nginx/sbin/nginx -s reload

綁定 hosts

192.168.1.2 item.jd.com 192.168.1.2 item2015.jd.com 192.168.1.2 d.3.cn

訪問如 http://item.jd.com/backend/info?type=basic&skuId=1 即看到結(jié)果。

前端展示實(shí)現(xiàn)

我們分為三部分實(shí)現(xiàn):基礎(chǔ)組件、商品介紹、前端展示部分。

基礎(chǔ)組件

首先我們進(jìn)行基礎(chǔ)組件的實(shí)現(xiàn),商品介紹和前端展示部分都需要讀取 Redis 和 Http 服務(wù),因此我們可以抽取公共部分出來復(fù)用。

vim /usr/chapter7/lualib/item/common.lua

Java 代碼

local redis = require("resty.redis")  
local ngx_log = ngx.log  
local ngx_ERR = ngx.ERR  
local function close_redis(red)  
    if not red then  
        return  
    end  
    --釋放連接(連接池實(shí)現(xiàn))  
    local pool_max_idle_time = 10000 --毫秒  
    local pool_size = 100 --連接池大小  
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)  

    if not ok then  
        ngx_log(ngx_ERR, "set redis keepalive error : ", err)  
    end  
end  

local function read_redis(ip, port, keys)  
    local red = redis:new()  
    red:set_timeout(1000)  
    local ok, err = red:connect(ip, port)  
    if not ok then  
        ngx_log(ngx_ERR, "connect to redis error : ", err)  
        return close_redis(red)  
    end  
    local resp = nil  
    if #keys == 1 then  
        resp, err = red:get(keys[1])  
    else  
        resp, err = red:mget(keys)  
    end  
    if not resp then  
        ngx_log(ngx_ERR, "get redis content error : ", err)  
        return close_redis(red)  
    end  

    --得到的數(shù)據(jù)為空處理  
    if resp == ngx.null then  
        resp = nil  
    end  
    close_redis(red)  

    return resp  
end  

local function read_http(args)  
    local resp = ngx.location.capture("/backend/info", {  
        method = ngx.HTTP_GET,  
        args = args  
    })  

    if not resp then  
        ngx_log(ngx_ERR, "request error")  
        return  
    end  
    if resp.status ~= 200 then  
        ngx_log(ngx_ERR, "request error, status :", resp.status)  
        return  
    end  
    return resp.body  
end  

local _M = {  
    read_redis = read_redis,  
    read_http = read_http  
}  
return _M   

整個邏輯和第六章類似;只是 read_redis 根據(jù)參數(shù) keys 個數(shù)支持 get 和 mget。 比如read_redis(ip, port, {"key1"}) 則調(diào)用 get 而 read_redis(ip, port, {"key1", "key2"}) 則調(diào)用 mget。

商品介紹

核心代碼

vim /usr/chapter7/desc.lua

Java 代碼

local common = require("item.common")  
local read_redis = common.read_redis  
local read_http = common.read_http  
local ngx_log = ngx.log  
local ngx_ERR = ngx.ERR  
local ngx_exit = ngx.exit  
local ngx_print = ngx.print  
local ngx_re_match = ngx.re.match  
local ngx_var = ngx.var  

local descKey = "d:" .. skuId .. ":"  
local descInfoStr = read_redis("127.0.0.1", 1114, {descKey})  
if not descInfoStr then  
   ngx_log(ngx_ERR, "redis not found desc info, back to http, skuId : ", skuId)  
   descInfoStr = read_http({type="desc", skuId = skuId})  
end  
if not descInfoStr then  
   ngx_log(ngx_ERR, "http not found basic info, skuId : ", skuId)  
   return ngx_exit(404)  
e