本章以京東商品詳情頁為例,京東商品詳情頁雖然僅是單個頁面,但是其數(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ì):
整個架構(gòu)如下圖所示:
http://wiki.jikexueyuan.com/project/nginx-lua/images/12.png" alt="" />
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ī)械硬盤。
基本流程如上所述,主要分為 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)目部署目錄結(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
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)。
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。
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 查看錯誤信息。
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。
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 查看錯誤信息。
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"
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
復(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ù)集群配置成功。
因?yàn)檎鎸?shí)數(shù)據(jù)是從多個子系統(tǒng)獲取,很難模擬這么多子系統(tǒng)交互,所以此處我們使用假數(shù)據(jù)來進(jìn)行實(shí)現(xiàn)。
我們使用 Maven 搭建 Web 項(xiàng)目,Maven 知識請自行學(xué)習(xí)。
本文將最小化依賴,即僅依賴我們需要的 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ù)。
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);
}
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))。
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>
Java 代碼
cd D:\workspace\chapter7
mvn clean package
此處使用 maven 命令打包,比如本例將得到 chapter7.war,然后將其上傳到服務(wù)器的 /usr/chapter7/webapp,然后通過 unzip chapter6.war 解壓。
復(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
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
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):基礎(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