天天看點

nginx另類複雜的架構

本章以京東商品詳情頁為例,京東商品詳情頁雖然僅是單個頁面,但是其資料聚合源是非常多的,除了一些實時性要求比較高的如價格、庫存、服務支援等通過AJAX異步加載加載之外,其他的資料都是在後端做資料聚合然後拼裝網頁模闆的。

http://item.jd.com/1217499.html

nginx另類複雜的架構
nginx另類複雜的架構

如圖所示,商品頁主要包括商品基本資訊(基本資訊、圖檔清單、顔色/尺碼關系、擴充屬性、規格參數、包裝清單、售後保障等)、商品介紹、其他資訊(分類、品牌、店鋪【第三方賣家】、店内分類【第三方賣家】、同類相關品牌)。更多細節此處就不闡述了。

整個京東有數億商品,如果每次動态擷取如上内容進行模闆拼裝,資料來源之多足以造成性能無法滿足要求;最初的 解決方案是生成靜态頁,但是靜态頁的最大的問題:1、無法迅速響應頁面需求變更;2、很難做多版本線上對比測試。如上兩個因素足以制約商品頁的多樣化發 展,是以靜态化技術不是很好的方案。

通過分析,資料主要分為四種:商品頁基本資訊、商品介紹(異步加載)、其他資訊(分類、品牌、店鋪等)、其他 需要實時展示的資料(價格、庫存等)。而其他資訊如分類、品牌、店鋪是非常少的,完全可以放到一個占用記憶體很小的Redis中存儲;而商品基本資訊我們可 以借鑒靜态化技術将資料做聚合存儲,這樣的好處是資料是原子的,而模闆是随時可變的,吸收了靜态頁聚合的優點,彌補了靜态頁的多版本缺點;另外一個非常嚴 重的問題就是嚴重依賴這些相關系統,如果它們挂了或響應慢則商品頁就挂了或響應慢;商品介紹我們也通過AJAX技術惰性加載(因為是第二屏,隻有當使用者滾 動滑鼠到該屏時才顯示);而實時展示資料通過AJAX技術做異步加載;是以我們可以做如下設計:

1、接收商品變更消息,做商品基本資訊的聚合,即從多個資料源擷取商品相關資訊如圖檔清單、顔色尺碼、規格參 數、擴充屬性等等,聚合為一個大的JSON資料做成資料閉環,以key-value存儲;因為是閉環,即使依賴的系統挂了我們商品頁還是能繼續服務的,對 商品頁不會造成任何影響;

2、接收商品介紹變更消息,存儲商品介紹資訊;

3、介紹其他資訊變更消息,存儲其他資訊。

整個架構如下圖所示: 

nginx另類複雜的架構

技術選型

MQ可以使用如Apache ActiveMQ;

Worker/動态服務可以通過如Java技術實作;

RPC可以選擇如alibaba Dubbo;

KV持久化存儲可以選擇SSDB(如果使用SSD盤則可以選擇SSDB+RocksDB引擎)或者ARDB(LMDB引擎版);

緩存使用Redis;

SSDB/Redis分片使用如Twemproxy,這樣不管使用Java還是Nginx+Lua,它們都不關心分片邏輯;

前端模闆拼裝使用Nginx+Lua;

資料叢集資料存儲的機器可以采用RAID技術或者主從模式防止單點故障;

因為資料變更不頻繁,可以考慮SSD替代機械硬碟。

核心流程

1、首先我們監聽商品資料變更消息;

2、接收到消息後,資料聚合Worker通過RPC調用相關系統擷取所有要展示的資料,此處擷取資料的來源可能非常多而且響應速度完全受制于這些系統,可能耗時幾百毫秒甚至上秒的時間;

3、将資料聚合為JSON串存儲到相關資料叢集;

4、前端Nginx通過Lua擷取相關叢集的資料進行展示;商品頁需要擷取基本資訊+其他資訊進行模闆拼裝, 即拼裝模闆僅需要兩次調用(另外因為其他資訊資料量少且對一緻性要求不高,是以我們完全可以緩存到Nginx本地全局記憶體,這樣可以減少遠端調用提高性 能);當頁面滾動到商品介紹頁面時異步調用商品介紹服務擷取資料;

5、如果從聚合的SSDB叢集/Redis中擷取不到相關資料;則回源到動态服務通過RPC調用相關系統擷取 所有要展示的資料傳回(此處可以做限流處理,因為如果大量請求過來的話可能導緻服務雪崩,需要采取保護措施),此處的邏輯和資料聚合Worker完全一 樣;然後發送MQ通知資料變更,這樣下次通路時就可以從聚合的SSDB叢集/Redis中擷取資料了。

基本流程如上所述,主要分為Worker、動态服務、資料存儲和前端展示;因為系統非常複雜,隻介紹動态服務和前端展示、資料存儲架構;Worker部分不做實作。

項目搭建

項目部署目錄結構。

/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

資料存儲實作

nginx另類複雜的架構

整體架構為主從模式,寫資料到主叢集,讀資料從從叢集讀取資料,這樣當一個叢集不足以支撐流量時可以使用更多的叢集來支撐更多的通路量;叢集分片使用Twemproxy實作。

商品基本資訊SSDB叢集配置

vim /usr/chapter7/ssdb_basic_7770.conf

Java代碼  

nginx另類複雜的架構
  1. work_dir = /usr/data/ssdb_7770  
  2. pidfile = /usr/data/ssdb_7770.pid  
  3. server:  
  4.         ip: 0.0.0.0  
  5.         port: 7770  
  6.         allow: 127.0.0.1  
  7.         allow: 192.168  
  8. replication:  
  9.         binlog: yes  
  10.         sync_speed: -1  
  11.         slaveof:  
  12. logger:  
  13.         level: error  
  14.         output: /usr/data/ssdb_7770.log  
  15.         rotate:  
  16.                 size: 1000000000  
  17. leveldb:  
  18.         cache_size: 500  
  19.         block_size: 32  
  20.         write_buffer_size: 64  
  21.         compaction_speed: 1000  
  22.         compression: yes  

vim /usr/chapter7/ssdb_basic_7771.conf  

Java代碼  

nginx另類複雜的架構
  1. work_dir = /usr/data/ssdb_7771  
  2. pidfile = /usr/data/ssdb_7771.pid  
  3. server:  
  4.         ip: 0.0.0.0  
  5.         port: 7771  
  6.         allow: 127.0.0.1  
  7.         allow: 192.168  
  8. replication:  
  9.         binlog: yes  
  10.         sync_speed: -1  
  11.         slaveof:  
  12. logger:  
  13.         level: error  
  14.         output: /usr/data/ssdb_7771.log  
  15.         rotate:  
  16.                 size: 1000000000  
  17. leveldb:  
  18.         cache_size: 500  
  19.         block_size: 32  
  20.         write_buffer_size: 64  
  21.         compaction_speed: 1000  
  22.         compression: yes  

vim /usr/chapter7/ssdb_basic_7772.conf 

Java代碼  

nginx另類複雜的架構
  1. work_dir = /usr/data/ssdb_7772  
  2. pidfile = /usr/data/ssdb_7772.pid  
  3. server:  
  4.         ip: 0.0.0.0  
  5.         port: 7772  
  6.         allow: 127.0.0.1  
  7.         allow: 192.168  
  8. replication:  
  9.         binlog: yes  
  10.         sync_speed: -1  
  11.         slaveof:  
  12.                 type: sync  
  13.                 ip: 127.0.0.1  
  14.                 port: 7770  
  15. logger:  
  16.         level: error  
  17.         output: /usr/data/ssdb_7772.log  
  18.         rotate:  
  19.                 size: 1000000000  
  20. leveldb:  
  21.         cache_size: 500  
  22.         block_size: 32  
  23.         write_buffer_size: 64  
  24.         compaction_speed: 1000  
  25.         compression: yes  

vim /usr/chapter7/ssdb_basic_7773.conf 

Java代碼  

nginx另類複雜的架構
  1. work_dir = /usr/data/ssdb_7773  
  2. pidfile = /usr/data/ssdb_7773.pid  
  3. server:  
  4.         ip: 0.0.0.0  
  5.         port: 7773  
  6.         allow: 127.0.0.1  
  7.         allow: 192.168  
  8. replication:  
  9.         binlog: yes  
  10.         sync_speed: -1  
  11.         slaveof:  
  12.                 type: sync  
  13.                 ip: 127.0.0.1  
  14.                 port: 7771  
  15. logger:  
  16.         level: error  
  17.         output: /usr/data/ssdb_7773.log  
  18.         rotate:  
  19.                 size: 1000000000  
  20. leveldb:  
  21.         cache_size: 500  
  22.         block_size: 32  
  23.         write_buffer_size: 64  
  24.         compaction_speed: 1000  
  25.         compression: yes  

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

建立工作目錄

Java代碼  

nginx另類複雜的架構
  1. mkdir -p /usr/data/ssdb_7770  
  2. mkdir -p /usr/data/ssdb_7771  
  3. mkdir -p /usr/data/ssdb_7772  
  4. mkdir -p /usr/data/ssdb_7773  

啟動

Java代碼  

nginx另類複雜的架構
  1. nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_basic_7770.conf &  
  2. nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_basic_7771.conf &  
  3. nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_basic_7772.conf &  
  4. 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代碼  

nginx另類複雜的架構
  1. work_dir = /usr/data/ssdb_8880  
  2. pidfile = /usr/data/ssdb8880.pid  
  3. server:  
  4.         ip: 0.0.0.0  
  5.         port: 8880  
  6.         allow: 127.0.0.1  
  7.         allow: 192.168  
  8. replication:  
  9.         binlog: yes  
  10.         sync_speed: -1  
  11.         slaveof:  
  12. logger:  
  13.         level: error  
  14.         output: /usr/data/ssdb_8880.log  
  15.         rotate:  
  16.                 size: 1000000000  
  17. leveldb:  
  18.         cache_size: 500  
  19.         block_size: 32  
  20.         write_buffer_size: 64  
  21.         compaction_speed: 1000  
  22.         compression: yes  

vim /usr/chapter7/ssdb_desc_8881.conf  

Java代碼  

nginx另類複雜的架構
  1. work_dir = /usr/data/ssdb_8881  
  2. pidfile = /usr/data/ssdb8881.pid  
  3. server:  
  4.         ip: 0.0.0.0  
  5.         port: 8881  
  6.         allow: 127.0.0.1  
  7.         allow: 192.168  
  8. logger:  
  9.         level: error  
  10.         output: /usr/data/ssdb_8881.log  
  11.         rotate:  
  12.                 size: 1000000000  
  13. leveldb:  
  14.         cache_size: 500  
  15.         block_size: 32  
  16.         write_buffer_size: 64  
  17.         compaction_speed: 1000  
  18.         compression: yes  

vim /usr/chapter7/ssdb_desc_8882.conf 

Java代碼  

nginx另類複雜的架構
  1. work_dir = /usr/data/ssdb_8882  
  2. pidfile = /usr/data/ssdb_8882.pid  
  3. server:  
  4.         ip: 0.0.0.0  
  5.         port: 8882  
  6.         allow: 127.0.0.1  
  7.         allow: 192.168  
  8. replication:  
  9.         binlog: yes  
  10.         sync_speed: -1  
  11.         slaveof:  
  12. replication:  
  13.         binlog: yes  
  14.         sync_speed: -1  
  15.         slaveof:  
  16.                 type: sync  
  17.                 ip: 127.0.0.1  
  18.                 port: 8880  
  19. logger:  
  20.         level: error  
  21.         output: /usr/data/ssdb_8882.log  
  22.         rotate:  
  23.                 size: 1000000000  
  24. leveldb:  
  25.         cache_size: 500  
  26.         block_size: 32  
  27.         write_buffer_size: 64  
  28.         compaction_speed: 1000  
  29.         compression: yes  

vim /usr/chapter7/ssdb_desc_8883.conf 

Java代碼  

nginx另類複雜的架構
  1. work_dir = /usr/data/ssdb_8883  
  2. pidfile = /usr/data/ssdb_8883.pid  
  3. server:  
  4.         ip: 0.0.0.0  
  5.         port: 8883  
  6.         allow: 127.0.0.1  
  7.         allow: 192.168  
  8. replication:  
  9.         binlog: yes  
  10.         sync_speed: -1  
  11.         slaveof:  
  12.                 type: sync  
  13.                 ip: 127.0.0.1  
  14.                 port: 8881  
  15. logger:  
  16.         level: error  
  17.         output: /usr/data/ssdb_8883.log  
  18.         rotate:  
  19.                 size: 1000000000  
  20. leveldb:  
  21.         cache_size: 500  
  22.         block_size: 32  
  23.         write_buffer_size: 64  
  24.         compaction_speed: 1000  
  25.         compression: yes  

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

建立工作目錄

Java代碼  

nginx另類複雜的架構
  1. mkdir -p /usr/data/ssdb_888{0,1,2,3}  

啟動

Java代碼  

nginx另類複雜的架構
  1. nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_desc_8880.conf &  
  2. nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_desc_8881.conf &  
  3. nohup /usr/servers/ssdb-1.8.0/ssdb-server  /usr/chapter7/ssdb_desc_8882.conf &  
  4. 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代碼  

nginx另類複雜的架構
  1. port 6660  
  2. pidfile "/var/run/redis_6660.pid"  
  3. #設定記憶體大小,根據實際情況設定,此處測試僅設定20mb  
  4. maxmemory 20mb  
  5. #記憶體不足時,所有KEY按照LRU算法删除  
  6. maxmemory-policy allkeys-lru  
  7. #Redis的過期算法不是精确的而是通過采樣來算的,預設采樣為3個,此處我們改成10  
  8. maxmemory-samples 10  
  9. #不進行RDB持久化  
  10. save “”  
  11. #不進行AOF持久化  
  12. appendonly no  

vim /usr/chapter7/redis_6661.conf 

Java代碼  

nginx另類複雜的架構
  1. port 6661  
  2. pidfile "/var/run/redis_6661.pid"  
  3. #設定記憶體大小,根據實際情況設定,此處測試僅設定20mb  
  4. maxmemory 20mb  
  5. #記憶體不足時,所有KEY按照LRU算法進行删除  
  6. maxmemory-policy allkeys-lru  
  7. #Redis的過期算法不是精确的而是通過采樣來算的,預設采樣為3個,此處我們改成10  
  8. maxmemory-samples 10  
  9. #不進行RDB持久化  
  10. save “”  
  11. #不進行AOF持久化  
  12. appendonly no  
  13. #主從  
  14. slaveof 127.0.0.1 6660  

vim /usr/chapter7/redis_6662.conf 

Java代碼  

nginx另類複雜的架構
  1. port 6662  
  2. pidfile "/var/run/redis_6662.pid"  
  3. #設定記憶體大小,根據實際情況設定,此處測試僅設定20mb  
  4. maxmemory 20mb  
  5. #記憶體不足時,所有KEY按照LRU算法進行删除  
  6. maxmemory-policy allkeys-lru  
  7. #Redis的過期算法不是精确的而是通過采樣來算的,預設采樣為3個,此處我們改成10  
  8. maxmemory-samples 10  
  9. #不進行RDB持久化  
  10. save “”  
  11. #不進行AOF持久化  
  12. appendonly no  
  13. #主從  
  14. slaveof 127.0.0.1 6660  

如上配置放到配置檔案最末尾即可;此處記憶體不足時的驅逐算法為所有KEY按照LRU進行删除(實際是記憶體基本上不會遇到滿的情況);主從關系:6660(主)-->6661(從)和 6660(主)-->6662(從) 。

啟動

Java代碼  

nginx另類複雜的架構
  1. nohup /usr/servers/redis-2.8.19/src/redis-server /usr/chapter7/redis_6660.conf &  
  2. nohup /usr/servers/redis-2.8.19/src/redis-server /usr/chapter7/redis_6661.conf &  
  3. nohup /usr/servers/redis-2.8.19/src/redis-server /usr/chapter7/redis_6662.conf &  

通過ps -aux | grep redis指令看是否啟動了,tail -f nohup.out檢視錯誤資訊。

測試  測試時在主SSDB/Redis中寫入資料,然後從從SSDB/Redis能讀取到資料即表示配置主從成功。 測試商品基本資訊SSDB叢集 Java代碼  

nginx另類複雜的架構
  1. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 7770  
  2. 127.0.0.1:7770> set i 1  
  3. OK  
  4. 127.0.0.1:7770>   
  5. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 7772  
  6. 127.0.0.1:7772> get i  
  7. "1"  

  測試商品介紹SSDB叢集 Java代碼  

nginx另類複雜的架構
  1. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 8880  
  2. 127.0.0.1:8880> set i 1  
  3. OK  
  4. 127.0.0.1:8880>   
  5. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 8882  
  6. 127.0.0.1:8882> get i  
  7. "1"  

   測試其他資訊叢集 Java代碼  

nginx另類複雜的架構
  1. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 6660  
  2. 127.0.0.1:6660> set i 1  
  3. OK  
  4. 127.0.0.1:6660> get i  
  5. "1"  
  6. 127.0.0.1:6660>   
  7. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli  -p 6661  
  8. 127.0.0.1:6661> get i  
  9. "1"  

  Twemproxy配置 vim /usr/chapter7/nutcracker.yml   Java代碼  

nginx另類複雜的架構
  1. basic_master:  
  2.   listen: 127.0.0.1:1111  
  3.   hash: fnv1a_64  
  4.   distribution: ketama  
  5.   redis: true  
  6.   timeout: 1000  
  7.   hash_tag: "::"  
  8.   servers:  
  9.    - 127.0.0.1:7770:1 server1  
  10.    - 127.0.0.1:7771:1 server2  
  11. basic_slave:  
  12.   listen: 127.0.0.1:1112  
  13.   hash: fnv1a_64  
  14.   distribution: ketama  
  15.   redis: true  
  16.   timeout: 1000  
  17.   hash_tag: "::"  
  18.   servers:  
  19.    - 127.0.0.1:7772:1 server1  
  20.    - 127.0.0.1:7773:1 server2  
  21. desc_master:  
  22.   listen: 127.0.0.1:1113  
  23.   hash: fnv1a_64  
  24.   distribution: ketama  
  25.   redis: true  
  26.   timeout: 1000  
  27.   hash_tag: "::"  
  28.   servers:  
  29.    - 127.0.0.1:8880:1 server1  
  30.    - 127.0.0.1:8881:1 server2  
  31. desc_slave:  
  32.   listen: 127.0.0.1:1114  
  33.   hash: fnv1a_64  
  34.   distribution: ketama  
  35.   redis: true  
  36.   timeout: 1000  
  37.   servers:  
  38.    - 127.0.0.1:8882:1 server1  
  39.    - 127.0.0.1:8883:1 server2  
  40. other_master:  
  41.   listen: 127.0.0.1:1115  
  42.   hash: fnv1a_64  
  43.   distribution: random  
  44.   redis: true  
  45.   timeout: 1000  
  46.   hash_tag: "::"  
  47.   servers:  
  48.    - 127.0.0.1:6660:1 server1  
  49. other_slave:  
  50.   listen: 127.0.0.1:1116  
  51.   hash: fnv1a_64  
  52.   distribution: random  
  53.   redis: true  
  54.   timeout: 1000  
  55.   hash_tag: "::"  
  56.   servers:  
  57.    - 127.0.0.1:6661:1 server1  
  58.    - 127.0.0.1:6662:1 server2  

1、因為我們使用了主從,是以需要給server起一個名字如server1、server2;否則分片算法預設根據ip:port:weight,這樣就會主從資料的分片算法不一緻;

2、其他資訊Redis因為每個Redis是對等的,是以分片算法可以使用random;

3、我們使用了hash_tag,可以保證相同的tag在一個分片上(本例配置了但沒有用到該特性)。

複制第六章的nutcracker.init,幫把配置檔案改為usr/chapter7/nutcracker.yml。然後通過/usr/chapter7/nutcracker.init start啟動Twemproxy。

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

Java代碼  

nginx另類複雜的架構
  1. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1111  
  2. 127.0.0.1:1111> set i 1  
  3. OK  
  4. 127.0.0.1:1111>   
  5. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1112  
  6. 127.0.0.1:1112> get i  
  7. "1"  
  8. 127.0.0.1:1112>   
  9. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1113  
  10. 127.0.0.1:1113> set i 1  
  11. OK  
  12. 127.0.0.1:1113>   
  13. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1114  
  14. 127.0.0.1:1114> get i  
  15. "1"  
  16. 127.0.0.1:1114>   
  17. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1115  
  18. 127.0.0.1:1115> set i 1  
  19. OK  
  20. 127.0.0.1:1115>   
  21. root@kaitao:/usr/chapter7# /usr/servers/redis-2.8.19/src/redis-cli -p 1116  
  22. 127.0.0.1:1116> get i  
  23. "1"  

到此資料叢集配置成功。

動态服務實作

因為真實資料是從多個子系統擷取,很難模拟這麼多子系統互動,是以此處我們使用假資料來進行實作。

項目搭建 

我們使用Maven搭建Web項目,Maven知識請自行學習。

項目依賴

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

Java代碼  

nginx另類複雜的架構
  1. <dependencies>  
  2.   <dependency>  
  3.     <groupId>javax.servlet</groupId>  
  4.     <artifactId>javax.servlet-api</artifactId>  
  5.     <version>3.0.1</version>  
  6.     <scope>provided</scope>  
  7.   </dependency>  
  8.   <dependency>  
  9.     <groupId>com.google.guava</groupId>  
  10.     <artifactId>guava</artifactId>  
  11.     <version>17.0</version>  
  12.   </dependency>  
  13.   <dependency>  
  14.     <groupId>redis.clients</groupId>  
  15.     <artifactId>jedis</artifactId>  
  16.     <version>2.5.2</version>  
  17.   </dependency>  
  18.   <dependency>  
  19.     <groupId>com.fasterxml.jackson.core</groupId>  
  20.     <artifactId>jackson-core</artifactId>  
  21.     <version>2.3.3</version>  
  22.   </dependency>  
  23.   <dependency>  
  24.     <groupId>com.fasterxml.jackson.core</groupId>  
  25.     <artifactId>jackson-databind</artifactId>  
  26.     <version>2.3.3</version>  
  27.   </dependency>  
  28. </dependencies>  

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

核心代碼

com.github.zhangkaitao.chapter7.servlet.ProductServiceServlet

Java代碼  

nginx另類複雜的架構
  1. @Override  
  2. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  3.     String type = req.getParameter("type");  
  4.     String content = null;  
  5.     try {  
  6.         if("basic".equals(type)) {  
  7.             content = getBasicInfo(req.getParameter("skuId"));  
  8.         } else if("desc".equals(type)) {  
  9.             content = getDescInfo(req.getParameter("skuId"));  
  10.         } else if("other".equals(type)) {  
  11.             content = getOtherInfo(req.getParameter("ps3Id"), req.getParameter("brandId"));  
  12.         }  
  13.     } catch (Exception e) {  
  14.         e.printStackTrace();  
  15.         resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);  
  16.         return;  
  17.     }  
  18.     if(content != null) {  
  19.         resp.setCharacterEncoding("UTF-8");  
  20.         resp.getWriter().write(content);  
  21.     } else {  
  22.         resp.setStatus(HttpServletResponse.SC_NOT_FOUND);  
  23.     }  
  24. }  

根據請求參數type來決定調用哪個服務擷取資料。

基本資訊服務 

Java代碼  

nginx另類複雜的架構
  1. private String getBasicInfo(String skuId) throws Exception {  
  2.     Map<String, Object> map = new HashMap<String, Object>();  
  3.     //商品編号  
  4.     map.put("skuId", skuId);  
  5.     //名稱  
  6.     map.put("name", "蘋果(Apple)iPhone 6 (A1586) 16GB 金色 移動聯通電信4G手機");  
  7.     //一級二級三級分類  
  8.     map.put("ps1Id", 9987);  
  9.     map.put("ps2Id", 653);  
  10.     map.put("ps3Id", 655);  
  11.     //品牌ID  
  12.     map.put("brandId", 14026);  
  13.     //圖檔清單  
  14.     map.put("imgs", getImgs(skuId));  
  15.     //上架時間  
  16.     map.put("date", "2014-10-09 22:29:09");  
  17.     //商品毛重  
  18.     map.put("weight", "400");  
  19.     //顔色尺碼  
  20.     map.put("colorSize", getColorSize(skuId));  
  21.     //擴充屬性  
  22.     map.put("expands", getExpands(skuId));  
  23.     //規格參數  
  24.     map.put("propCodes", getPropCodes(skuId));  
  25.     map.put("date", System.currentTimeMillis());  
  26.     String content = objectMapper.writeValueAsString(map);  
  27.     //實際應用應該是發送MQ  
  28.     asyncSetToRedis(basicInfoJedisPool, "p:" + skuId + ":", content);  
  29.     return objectMapper.writeValueAsString(map);  
  30. }  
  31. private List<String> getImgs(String skuId) {  
  32.     return Lists.newArrayList(  
  33.             "jfs/t277/193/1005339798/768456/29136988/542d0798N19d42ce3.jpg",  
  34.             "jfs/t352/148/1022071312/209475/53b8cd7f/542d079bN3ea45c98.jpg",  
  35.             "jfs/t274/315/1008507116/108039/f70cb380/542d0799Na03319e6.jpg",  
  36.             "jfs/t337/181/1064215916/27801/b5026705/542d079aNf184ce18.jpg"  
  37.     );  
  38. }  
  39. private List<Map<String, Object>> getColorSize(String skuId) {  
  40.     return Lists.newArrayList(  
  41.         makeColorSize(1217499, "金色", "公開版(16GB ROM)"),  
  42.         makeColorSize(1217500, "深空灰", "公開版(16GB ROM)"),  
  43.         makeColorSize(1217501, "銀色", "公開版(16GB ROM)"),  
  44.         makeColorSize(1217508, "金色", "公開版(64GB ROM)"),  
  45.         makeColorSize(1217509, "深空灰", "公開版(64GB ROM)"),  
  46.         makeColorSize(1217509, "銀色", "公開版(64GB ROM)"),  
  47.         makeColorSize(1217493, "金色", "移動4G版 (16GB)"),  
  48.         makeColorSize(1217494, "深空灰", "移動4G版 (16GB)"),  
  49.         makeColorSize(1217495, "銀色", "移動4G版 (16GB)"),  
  50.         makeColorSize(1217503, "金色", "移動4G版 (64GB)"),  
  51.         makeColorSize(1217503, "金色", "移動4G版 (64GB)"),  
  52.         makeColorSize(1217504, "深空灰", "移動4G版 (64GB)"),  
  53.         makeColorSize(1217505, "銀色", "移動4G版 (64GB)")  
  54.     );  
  55. }  
  56. private Map<String, Object> makeColorSize(long skuId, String color, String size) {  
  57.     Map<String, Object> cs1 = Maps.newHashMap();  
  58.     cs1.put("SkuId", skuId);  
  59.     cs1.put("Color", color);  
  60.     cs1.put("Size", size);  
  61.     return cs1;  
  62. }  
  63. private List<List<?>> getExpands(String skuId) {  
  64.     return Lists.newArrayList(  
  65.             (List<?>)Lists.newArrayList("熱點", Lists.newArrayList("超薄7mm以下", "支援NFC")),  
  66.             (List<?>)Lists.newArrayList("系統", "蘋果(IOS)"),  
  67.             (List<?>)Lists.newArrayList("系統", "蘋果(IOS)"),  
  68.             (List<?>)Lists.newArrayList("購買方式", "非合約機")  
  69.     );  
  70. }  
  71. private Map<String, List<List<String>>> getPropCodes(String skuId) {  
  72.     Map<String, List<List<String>>> map = Maps.newHashMap();  
  73.     map.put("主體", Lists.<List<String>>newArrayList(  
  74.             Lists.<String>newArrayList("品牌", "蘋果(Apple)"),  
  75.             Lists.<String>newArrayList("型号", "iPhone 6 A1586"),  
  76.             Lists.<String>newArrayList("顔色", "金色"),  
  77.             Lists.<String>newArrayList("上市年份", "2014年")  
  78.     ));  
  79.     map.put("存儲", Lists.<List<String>>newArrayList(  
  80.             Lists.<String>newArrayList("機身記憶體", "16GB ROM"),  
  81.             Lists.<String>newArrayList("儲存卡類型", "不支援")  
  82.     ));  
  83.     map.put("顯示", Lists.<List<String>>newArrayList(  
  84.             Lists.<String>newArrayList("螢幕尺寸", "4.7英寸"),  
  85.             Lists.<String>newArrayList("觸摸屏", "Retina HD"),  
  86.             Lists.<String>newArrayList("分辨率", "1334 x 750")  
  87.     ));  
  88.     return map;  
  89. }  

本例基本資訊提供了如商品名稱、圖檔清單、顔色尺碼、擴充屬性、規格參數等等資料;而為了簡化邏輯大多數資料都是List/Map資料結構。 

商品介紹服務 

Java代碼  

nginx另類複雜的架構
  1. private String getDescInfo(String skuId) throws Exception {  
  2.     Map<String, Object> map = new HashMap<String, Object>();  
  3.     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>");  
  4.     map.put("date", System.currentTimeMillis());  
  5.     String content = objectMapper.writeValueAsString(map);  
  6.     //實際應用應該是發送MQ  
  7.     asyncSetToRedis(descInfoJedisPool, "d:" + skuId + ":", content);  
  8.     return objectMapper.writeValueAsString(map);  
  9. }  

其他資訊服務

Java代碼  

nginx另類複雜的架構
  1. private String getOtherInfo(String ps3Id, String brandId) throws Exception {  
  2.     Map<String, Object> map = new HashMap<String, Object>();  
  3.     //面包屑  
  4.     List<List<?>> breadcrumb = Lists.newArrayList();  
  5.     breadcrumb.add(Lists.newArrayList(9987, "手機"));  
  6.     breadcrumb.add(Lists.newArrayList(653, "手機通訊"));  
  7.     breadcrumb.add(Lists.newArrayList(655, "手機"));  
  8.     //品牌  
  9.     Map<String, Object> brand = Maps.newHashMap();  
  10.     brand.put("name", "蘋果(Apple)");  
  11.     brand.put("logo", "BrandLogo/g14/M09/09/10/rBEhVlK6vdkIAAAAAAAFLXzp-lIAAHWawP_QjwAAAVF472.png");  
  12.     map.put("breadcrumb", breadcrumb);  
  13.     map.put("brand", brand);  
  14.     //實際應用應該是發送MQ  
  15.     asyncSetToRedis(otherInfoJedisPool, "s:" + ps3Id + ":", objectMapper.writeValueAsString(breadcrumb));  
  16.     asyncSetToRedis(otherInfoJedisPool, "b:" + brandId + ":", objectMapper.writeValueAsString(brand));  
  17.     return objectMapper.writeValueAsString(map);  
  18. }  

本例中其他資訊隻使用了面包屑和品牌資料。

輔助工具

Java代碼  

nginx另類複雜的架構
  1. private ObjectMapper objectMapper = new ObjectMapper();  
  2. private JedisPool basicInfoJedisPool = createJedisPool("127.0.0.1", 1111);  
  3. private JedisPool descInfoJedisPool = createJedisPool("127.0.0.1", 1113);  
  4. private JedisPool otherInfoJedisPool = createJedisPool("127.0.0.1", 1115);  
  5. private JedisPool createJedisPool(String host, int port) {  
  6.     GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();  
  7.     poolConfig.setMaxTotal(100);  
  8.     return new JedisPool(poolConfig, host, port);  
  9. }  
  10. private ExecutorService executorService = Executors.newFixedThreadPool(10);  
  11. private void asyncSetToRedis(final JedisPool jedisPool, final String key, final String content) {  
  12.     executorService.submit(new Runnable() {  
  13.         @Override  
  14.         public void run() {  
  15.             Jedis jedis = null;  
  16.             try {  
  17.                 jedis = jedisPool.getResource();  
  18.                 jedis.set(key, content);  
  19.             } catch (Exception e) {  
  20.                 e.printStackTrace();  
  21.                 jedisPool.returnBrokenResource(jedis);  
  22.             } finally {  
  23.                 jedisPool.returnResource(jedis);  
  24.             }  
  25.         }  
  26.     });  
  27. }  

本例使用Jackson進行JSON的序列化;Jedis進行Redis的操作;使用線程池做異步更新(實際應用中可以使用MQ做實作)。 

web.xml配置 Java代碼  

nginx另類複雜的架構
  1. <servlet>  
  2.     <servlet-name>productServiceServlet</servlet-name>  
  3.     <servlet-class>com.github.zhangkaitao.chapter7.servlet.ProductServiceServlet</servlet-class>  
  4. </servlet>  
  5. <servlet-mapping>  
  6.     <servlet-name>productServiceServlet</servlet-name>  
  7.     <url-pattern>/info</url-pattern>  
  8. </servlet-mapping>  

打WAR包 

Java代碼  

nginx另類複雜的架構
  1. cd D:\workspace\chapter7  
  2. mvn clean package  

此處使用maven指令打包,比如本例将得到chapter7.war,然後将其上傳到伺服器的/usr/chapter7/webapp,然後通過unzip chapter6.war解壓。

配置Tomcat

複制第六章使用的tomcat執行個體:

Java代碼  

nginx另類複雜的架構
  1. cd /usr/servers/  
  2. cp -r tomcat-server1 tomcat-chapter7/  
  3. vim /usr/servers/tomcat-chapter7/conf/Catalina/localhost/ROOT.xml   

Java代碼  

nginx另類複雜的架構
  1. <!-- 通路路徑是根,web應用所屬目錄為/usr/chapter7/webapp -->  
  2. <Context path="" docBase="/usr/chapter7/webapp"></Context>  

指向第七章的web應用路徑。

測試 

啟動tomcat執行個體。

Java代碼  

nginx另類複雜的架構
  1. /usr/servers/tomcat-chapter7/bin/startup.sh   

通路如下URL進行測試。 

Java代碼  

nginx另類複雜的架構
  1. http://192.168.1.2:8080/info?type=basic&skuId=1  
  2. http://192.168.1.2:8080/info?type=desc&skuId=1  
  3. http://192.168.1.2:8080/info?type=other&ps3Id=1&brandId=1  

nginx配置

vim /usr/chapter7/nginx_chapter7.conf 

Java代碼  

nginx另類複雜的架構
  1. upstream backend {  
  2.     server 127.0.0.1:8080 max_fails=5 fail_timeout=10s weight=1;  
  3.     check interval=3000 rise=1 fall=2 timeout=5000 type=tcp default_down=false;  
  4.     keepalive 100;  
  5. }  
  6. server {  
  7.     listen       80;  
  8.     server_name  item2015.jd.com item.jd.com d.3.cn;  
  9.     location ~ /backend/(.*) {  
  10.         #internal;  
  11.         keepalive_timeout   30s;  
  12.         keepalive_requests  1000;  
  13.         #支援keep-alive  
  14.         proxy_http_version 1.1;  
  15.         proxy_set_header Connection "";  
  16.         rewrite /backend(/.*) $1 break;  
  17.         proxy_pass_request_headers off;  
  18.         #more_clear_input_headers Accept-Encoding;  
  19.         proxy_next_upstream error timeout;  
  20.         proxy_pass http://backend;  
  21.     }  
  22. }  

此處server_name 我們指定了item.jd.com(商品詳情頁)和d.3.cn(商品介紹)。其他配置可以參考第六章内容。另外實際生産環境要把#internal打開,表示隻有本nginx能通路。

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

Java代碼  

nginx另類複雜的架構
  1. include /usr/chapter7/nginx_chapter7.conf;  
  2. #為了友善測試,注釋掉example.conf  
  3. include /usr/chapter6/nginx_chapter6.conf;  

Java代碼  

nginx另類複雜的架構
  1. #lua子產品路徑,其中”;;”表示預設搜尋路徑,預設到/usr/servers/nginx下找  
  2. lua_package_path "/usr/chapter7/lualib/?.lua;;";  #lua 子產品  
  3. lua_package_cpath "/usr/chapter7/lualib/?.so;;";  #c子產品  

lua子產品從/usr/chapter7目錄加載,因為我們要寫自己的子產品使用。

重新開機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即看到結果。

前端展示實作 

我們分為三部分實作:基礎元件、商品介紹、前端展示部分。

基礎元件

首先我們進行基礎元件的實作,商品介紹和前端展示部分都需要讀取Redis和Http服務,是以我們可以抽取公共部分出來複用。

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

Java代碼  

nginx另類複雜的架構
  1. local redis = require("resty.redis")  
  2. local ngx_log = ngx.log  
  3. local ngx_ERR = ngx.ERR  
  4. local function close_redis(red)  
  5.     if not red then  
  6.         return  
  7.     end  
  8.     --釋放連接配接(連接配接池實作)  
  9.     local pool_max_idle_time = 10000 --毫秒  
  10.     local pool_size = 100 --連接配接池大小  
  11.     local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)  
  12.     if not ok then  
  13.         ngx_log(ngx_ERR, "set redis keepalive error : ", err)  
  14.     end  
  15. end  
  16. local function read_redis(ip, port, keys)  
  17.     local red = redis:new()  
  18.     red:set_timeout(1000)  
  19.     local ok, err = red:connect(ip, port)  
  20.     if not ok then  
  21.         ngx_log(ngx_ERR, "connect to redis error : ", err)  
  22.         return close_redis(red)  
  23.     end  
  24.     local resp = nil  
  25.     if #keys == 1 then  
  26.         resp, err = red:get(keys[1])  
  27.     else  
  28.         resp, err = red:mget(keys)  
  29.     end  
  30.     if not resp then  
  31.         ngx_log(ngx_ERR, "get redis content error : ", err)  
  32.         return close_redis(red)  
  33.     end  
  34.     --得到的資料為空處理  
  35.     if resp == ngx.null then  
  36.         resp = nil  
  37.     end  
  38.     close_redis(red)  
  39.     return resp  
  40. end  
  41. local function read_http(args)  
  42.     local resp = ngx.location.capture("/backend/info", {  
  43.         method = ngx.HTTP_GET,  
  44.         args = args  
  45.     })  
  46.     if not resp then  
  47.         ngx_log(ngx_ERR, "request error")  
  48.         return  
  49.     end  
  50.     if resp.status ~= 200 then  
  51.         ngx_log(ngx_ERR, "request error, status :", resp.status)  
  52.         return  
  53.     end  
  54.     return resp.body  
  55. end  
  56. local _M = {  
  57.     read_redis = read_redis,  
  58.     read_http = read_http  
  59. }  
  60. return _M  

整個邏輯和第六章類似;隻是read_redis根據參數keys個數支援get和mget。 比如read_redis(ip, port, {"key1"})則調用get而read_redis(ip, port, {"key1", "key2"})則調用mget。

商品介紹

核心代碼

vim /usr/chapter7/desc.lua

Java代碼  

nginx另類複雜的架構
  1. local common = require("item.common")  
  2. local read_redis = common.read_redis  
  3. local read_http = common.read_http  
  4. local ngx_log = ngx.log  
  5. local ngx_ERR = ngx.ERR  
  6. local ngx_exit = ngx.exit  
  7. local ngx_print = ngx.print  
  8. local ngx_re_match = ngx.re.match  
  9. local ngx_var = ngx.var  
  10. local descKey = "d:" .. skuId .. ":"  
  11. local descInfoStr = read_redis("127.0.0.1", 1114, {descKey})  
  12. if not descInfoStr then  
  13.    ngx_log(ngx_ERR, "redis not found desc info, back to http, skuId : ", skuId)  
  14.    descInfoStr = read_http({type="desc", skuId = skuId})  
  15. end  
  16. if not descInfoStr then  
  17.    ngx_log(ngx_ERR, "http not found basic info, skuId : ", skuId)  
  18.    return ngx_exit(404)  
  19. end  
  20. ngx_print("showdesc(")  
  21. ngx_print(descInfoStr)  
  22. ngx_print(")")  

通過複用邏輯後整體代碼簡化了許多;此處讀取商品介紹從叢集;另外前端展示使用JSONP技術展示商品介紹。 

nginx配置 

vim /usr/chapter7/nginx_chapter7.conf 

Java代碼  

nginx另類複雜的架構
  1. location ~^/desc/(\d+)$ {  
  2.     if ($host != "d.3.cn") {  
  3.        return 403;  
  4.     }  
  5.     default_type application/x-javascript;  
  6.     charset utf-8;  
  7.     lua_code_cache on;  
  8.     set $skuId $1;  
  9.     content_by_lua_file /usr/chapter7/desc.lua;  
  10. }  

因為item.jd.com和d.3.cn複用了同一個配置檔案,此處需要限定隻有d.3.cn域名能通路,防止惡意通路。 

重新開機nginx後,通路如http://d.3.cn/desc/1即可得到JSONP結果。

前端展示

核心代碼

vim /usr/chapter7/item.lua 

Java代碼  

nginx另類複雜的架構
  1. local common = require("item.common")  
  2. local item = require("item")  
  3. local read_redis = common.read_redis  
  4. local read_http = common.read_http  
  5. local cjson = require("cjson")  
  6. local cjson_decode = cjson.decode  
  7. local ngx_log = ngx.log  
  8. local ngx_ERR = ngx.ERR  
  9. local ngx_exit = ngx.exit  
  10. local ngx_print = ngx.print  
  11. local ngx_var = ngx.var  
  12. local skuId = ngx_var.skuId  
  13. --擷取基本資訊  
  14. local basicInfoKey = "p:" .. skuId .. ":"  
  15. local basicInfoStr = read_redis("127.0.0.1", 1112, {basicInfoKey})  
  16. if not basicInfoStr then  
  17.    ngx_log(ngx_ERR, "redis not found basic info, back to http, skuId : ", skuId)  
  18.    basicInfoStr = read_http({type="basic", skuId = skuId})  
  19. end  
  20. if not basicInfoStr then  
  21.    ngx_log(ngx_ERR, "http not found basic info, skuId : ", skuId)  
  22.    return ngx_exit(404)  
  23. end  
  24. local basicInfo = cjson_decode(basicInfoStr)  
  25. local ps3Id = basicInfo["ps3Id"]  
  26. local brandId = basicInfo["brandId"]  
  27. --擷取其他資訊  
  28. local breadcrumbKey = "s:" .. ps3Id .. ":"  
  29. local brandKey = "b:" .. brandId ..":"  
  30. local otherInfo = read_redis("127.0.0.1", 1116, {breadcrumbKey, brandKey}) or {}  
  31. local breadcrumbStr = otherInfo[1]  
  32. local brandStr = otherInfo[2]  
  33. if breadcrumbStr then  
  34.    basicInfo["breadcrumb"] = cjson_decode(breadcrumbStr)  
  35. end  
  36. if brandStr then  
  37.    basicInfo["brand"] = cjson_decode(brandStr)  
  38. end  
  39. if not breadcrumbStr and not brandStr then  
  40.    ngx_log(ngx_ERR, "redis not found other info, back to http, skuId : ", brandId)  
  41.    local otherInfoStr = read_http({type="other", ps3Id = ps3Id, brandId = brandId})  
  42.    if not otherInfoStr then  
  43.        ngx_log(ngx_ERR, "http not found other info, skuId : ", skuId)  
  44.    else  
  45.      local otherInfo = cjson_decode(otherInfoStr)  
  46.      basicInfo["breadcrumb"] = otherInfo["breadcrumb"]  
  47.      basicInfo["brand"] = otherInfo["brand"]  
  48.    end  
  49. end  
  50. local name = basicInfo["name"]  
  51. --name to unicode  
  52. basicInfo["unicodeName"] = item.utf8_to_unicode(name)  
  53. --字元串截取,超長顯示...  
  54. basicInfo["moreName"] = item.trunc(name, 10)  
  55. --初始化各分類的url  
  56. item.init_breadcrumb(basicInfo)  
  57. --初始化擴充屬性  
  58. item.init_expand(basicInfo)  
  59. --初始化顔色尺碼  
  60. item.init_color_size(basicInfo)  
  61. local template = require "resty.template"  
  62. template.caching(true)  
  63. template.render("item.html", basicInfo)  

整個邏輯分為四部分:1、擷取基本資訊;2、根據基本資訊中的關聯關系擷取其他資訊;3、初始化/格式化資料;4、渲染模闆。  

初始化子產品 

vim /usr/chapter7/lualib/item.lua 

Java代碼  

nginx另類複雜的架構
  1. local bit = require("bit")  
  2. local utf8 = require("utf8")  
  3. local cjson = require("cjson")  
  4. local cjson_encode = cjson.encode  
  5. local bit_band = bit.band  
  6. local bit_bor = bit.bor  
  7. local bit_lshift = bit.lshift  
  8. local string_format = string.format  
  9. local string_byte = string.byte  
  10. local table_concat = table.concat  
  11. --utf8轉為unicode  
  12. local function utf8_to_unicode(str)  
  13.     if not str or str == "" or str == ngx.null then  
  14.         return nil  
  15.     end  
  16.     local res, seq, val = {}, 0, nil  
  17.     for i = 1, #str do  
  18.         local c = string_byte(str, i)  
  19.         if seq == 0 then  
  20.             if val then  
  21.                 res[#res + 1] = string_format("%04x", val)  
  22.             end  
  23.            seq = c < 0x80 and 1 or c < 0xE0 and 2 or c < 0xF0 and 3 or  
  24.                               c < 0xF8 and 4 or --c < 0xFC and 5 or c < 0xFE and 6 or  
  25.             if seq == 0 then  
  26.                 ngx.log(ngx.ERR, 'invalid UTF-8 character sequence' .. ",,," .. tostring(str))  
  27.                 return str  
  28.             end  
  29.             val = bit_band(c, 2 ^ (8 - seq) - 1)  
  30.         else  
  31.             val = bit_bor(bit_lshift(val, 6), bit_band(c, 0x3F))  
  32.         end  
  33.         seq = seq - 1  
  34.     end  
  35.     if val then  
  36.         res[#res + 1] = string_format("%04x", val)  
  37.     end  
  38.     if #res == 0 then  
  39.         return str  
  40.     end  
  41.     return "\\u" .. table_concat(res, "\\u")  
  42. end  
  43. --utf8字元串截取  
  44. local function trunc(str, len)  
  45.    if not str then  
  46.      return nil  
  47.    end  
  48.    if utf8.len(str) > len then  
  49.       return utf8.sub(str, 1, len) .. "..."  
  50.    end  
  51.    return str  
  52. end  
  53. --初始化面包屑  
  54. local function init_breadcrumb(info)  
  55.     local breadcrumb = info["breadcrumb"]  
  56.     if not breadcrumb then  
  57.        return  
  58.     end  
  59.     local ps1Id = breadcrumb[1][1]  
  60.     local ps2Id = breadcrumb[2][1]  
  61.     local ps3Id = breadcrumb[3][1]  
  62.     --此處應該根據一級分類查找url  
  63.     local ps1Url = "http://shouji.jd.com/"  
  64.     local ps2Url = "http://channel.jd.com/shouji.html"  
  65.     local ps3Url = "http://list.jd.com/list.html?cat=" .. ps1Id .. "," .. ps2Id .. "," .. ps3Id  
  66.     breadcrumb[1][3] = ps1Url  
  67.     breadcrumb[2][3] = ps2Url  
  68.     breadcrumb[3][3] = ps3Url  
  69. end  
  70. --初始化擴充屬性  
  71. local function init_expand(info)  
  72.    local expands = info["expands"]  
  73.    if not expands then  
  74.      return  
  75.    end  
  76.    for _, e in ipairs(expands) do  
  77.       if type(e[2]) == "table" then  
  78.          e[2] = table_concat(e[2], ",")  
  79.       end  
  80.    end  
  81. end  
  82. --初始化顔色尺碼  
  83. local function init_color_size(info)  
  84.    local colorSize = info["colorSize"]  
  85.    --顔色尺碼JSON串  
  86.    local colorSizeJson = cjson_encode(colorSize)  
  87.    --顔色清單(不重複)  
  88.    local colorList = {}  
  89.    --尺碼清單(不重複)  
  90.    local sizeList = {}  
  91.    info["colorSizeJson"] = colorSizeJson  
  92.    info["colorList"] = colorList  
  93.    info["sizeList"] = sizeList  
  94.    local colorSet = {}  
  95.    local sizeSet = {}  
  96.    for _, cz in ipairs(colorSize) do  
  97.       local color = cz["Color"]  
  98.       local size = cz["Size"]  
  99.       if color and color ~= "" and not colorSet[color] then  
  100.          colorList[#colorList + 1] = {color = color, url = "http://item.jd.com/" ..cz["SkuId"] .. ".html"}  
  101.          colorSet[color] = true  
  102.       end  
  103.       if size and size ~= "" and not sizeSet[size] then  
  104.          sizeList[#sizeList + 1] = {size = size, url = "http://item.jd.com/" ..cz["SkuId"] .. ".html"}  
  105.          sizeSet[size] = ""  
  106.       end  
  107.    end  
  108. end  
  109. local _M = {  
  110.    utf8_to_unicode = utf8_to_unicode,  
  111.    trunc = trunc,  
  112.    init_breadcrumb = init_breadcrumb,  
  113.    init_expand = init_expand,  
  114.    init_color_size = init_color_size  
  115. }  
  116. return _M  

比如utf8_to_unicode代碼之前已經見過了,其他的都是一些邏輯代碼。

模闆html片段  

Java代碼  

nginx另類複雜的架構
  1. var pageConfig = {  
  2.      compatible: true,  
  3.      product: {  
  4.          skuid: {* skuId *},  
  5.          name: '{* unicodeName *}',  
  6.          skuidkey:'AFC266E971535B664FC926D34E91C879',  
  7.          href: 'http://item.jd.com/{* skuId *}.html',  
  8.          src: '{* imgs[1] *}',  
  9.          cat: [{* ps1Id *},{* ps2Id *},{* ps3Id *}],  
  10.          brand: {* brandId *},  
  11.          tips: false,  
  12.          pType: 1,  
  13.          venderId:0,  
  14.          shopId:'0',  
  15.          specialAttrs:["HYKHSP-0","isDistribution","isHaveYB","isSelfService-0","isWeChatStock-0","packType","IsNewGoods","isCanUseDQ","isSupportCard","isCanUseJQ","isOverseaPurchase-0","is7ToReturn-1","isCanVAT"],  
  16.          videoPath:'',  
  17.          desc: 'http://d.3.cn/desc/{* skuId *}'  
  18.      }  
  19.  };  
  20.  var warestatus = 1;                  
  21.  {% if colorSizeJson then %} var ColorSize = {* colorSizeJson *};{% end %}  
  22.          {-raw-}  
  23.          try{(function(flag){ if(!flag){return;} if(window.location.hash == '#m'){var exp = new Date();exp.setTime(exp.getTime() + 30 * 24 * 60 * 60 * 1000);document.cookie = "pcm=1;expires=" + exp.toGMTString() + ";path=/;domain=jd.com";return;}else{var cook=document.cookie.match(new RegExp("(^| )pcm=([^;]*)(;|$)"));if(cook&&cook.length>2&&unescape(cook[2])=="2"){flag=false;}} var userAgent = navigator.userAgent; if(userAgent){ userAgent = userAgent.toUpperCase();if(userAgent.indexOf("PAD")>-1){return;} var mobilePhoneList = ["IOS","IPHONE","ANDROID","WINDOWS PHONE"];for(var i=0,len=mobilePhoneList.length;i<len;i++){ if(userAgent.indexOf(mobilePhoneList[i])>-1){var url="http://m.jd.com/product/"+pageConfig.product.skuid+".html";if(flag){window.showtouchurl=true;}else{window.location.href = url;}break;}}}})((function(){var json={"6881":3,"1195":3,"10011":3,"6980":3,"12360":3};if(json[pageConfig.product.cat[0]+""]==1||json[pageConfig.product.cat[1]+""]==2||json[pageConfig.product.cat[2]+""]==3){return false;}else{return true;}})());}catch(e){}  
  24.          {-raw-}  

{* var *}輸出變量,{% code %} 寫代碼片段,{-raw-} 不進行任何處理直接輸出。

面包屑

Java代碼  

nginx另類複雜的架構
  1. <div class="breadcrumb">  
  2.     <strong><a href='{* breadcrumb[1][3] *}'>{* breadcrumb[1][2] *}</a></strong>  
  3.     <span>  
  4.         &nbsp;&gt;&nbsp;  
  5.         <a href='{* breadcrumb[2][3] *}'>{* breadcrumb[2][2] *}</a>  
  6.         &nbsp;&gt;&nbsp;  
  7.         <a href='{* breadcrumb[3][3] *}'>{* breadcrumb[3][2] *}</a>  
  8.         &nbsp;&gt;&nbsp;  
  9.     </span>  
  10.     <span>  
  11.         {% if brand then %}  
  12.         <a href='http://www.jd.com/pinpai/{* ps3Id *}-{* brandId *}.html'>{* brand['name'] *}</a>  
  13.         &nbsp;&gt;&nbsp;  
  14.        {% end %}  
  15.        <a href='http://item.jd.com/{* skuId *}.html'>{* moreName *}</a>  
  16.     </span>  
  17. </div>  

圖檔清單

Java代碼  

nginx另類複雜的架構
  1. <div id="spec-n1" class="jqzoom" οnclick="window.open('http://www.jd.com/bigimage.aspx?id={* skuId *}')" clstag="shangpin|keycount|product|spec-n1">  
  2.     <img data-img="1" width="350" height="350" src="http://img14.360buyimg.com/n1/{* imgs[1] *}" alt="{* name *}"/>  
  3. </div>  
  4. <div id="spec-list" clstag="shangpin|keycount|product|spec-n5">  
  5.     <a href="javascript:;" class="spec-control" id="spec-forward"></a>  
  6.     <a href="javascript:;" class="spec-control" id="spec-backward"></a>  
  7.     <div class="spec-items">  
  8.         <ul class="lh">  
  9.             {% for _, img in ipairs(imgs) do %}  
  10.             <li><img class='img-hover' alt='{* name *}' src='http://img14.360buyimg.com/n5/{* img *}' data-url='{* img *}' data-img='1' width='50' height='50'></li>  
  11.             {% end %}  
  12.         </ul>  
  13.     </div>  
  14. </div>  

顔色尺碼選擇

Java代碼  

nginx另類複雜的架構
  1. <div class="dt">選擇顔色:</div>  
  2.     <div class="dd">  
  3.         {% for _, color in ipairs(colorList) do %}  
  4.             <div class="item"><b></b><a href="{* color['url'] *}" title="{* color['color'] *}"><i>{* color['color'] *}</i></a></div>  
  5.         {% end %}  
  6.     </div>  
  7. </div>  
  8. <div id="choose-version" class="li">  
  9.     <div class="dt">選擇版本:</div>  
  10.     <div class="dd">  
  11.         {% for _, size in ipairs(sizeList) do %}  
  12.             <div class="item"><b></b><a href="{* size['url'] *}" title="{* size['size'] *}">{* size['size'] *}</a></div>  
  13.         {% end %}  
  14.     </div>  
  15. </div>  

擴充屬性

Java代碼  

nginx另類複雜的架構
  1. <ul id="parameter2" class="p-parameter-list">  
  2.     <li title='{* name *}'>商品名稱:{* name *}</li>  
  3.     <li title='{* skuId *}'>商品編号:{* skuId *}</li>  
  4.     {% if brand then %}  
  5.     <li title='{* brand["name"] *}'>品牌: <a href='http://www.jd.com/pinpai/{* ps3Id *}-{* brandId *}.html' target='_blank'>{* brand["name"] *}</a></li>  
  6.     {% end %}  
  7.     {% if date then %}  
  8.     <li title='{* date *}'>上架時間:{* date *}</li>  
  9.     {% end %}  
  10.     {% if weight then %}  
  11.     <li title='{* weight *}'>商品毛重:{* weight *}</li>  
  12.     {% end %}  
  13.     {% for _, e in pairs(expands) do %}  
  14.     <li title='{* e[2] *}'>{* e[1] *}:{* e[2] *}</li>  
  15.     {% end %}  
  16. </ul>  

規格參數

Java代碼  

nginx另類複雜的架構
  1. <table cellpadding="0" cellspacing="1" width="100%" string">"0" class="Ptable">  
  2.     {% for group, pc in pairs(propCodes) do  %}  
  3.     <tr><th class="tdTitle" colspan="2">{* group *}</th><tr>  
  4.     {% for _, v in pairs(pc) do %}  
  5.     <tr><td class="tdTitle">{* v[1] *}</td><td>{* v[2] *}</td></tr>  
  6.     {% end %}  
  7.     {% end %}  
  8. </table>  

nginx配置 

vim /usr/chapter7/nginx_chapter7.conf  

Java代碼  

nginx另類複雜的架構
  1. #模闆加載位置  
  2. set $template_root "/usr/chapter7";  
  3. location ~ ^/(\d+).html$ {  
  4.     if ($host !~ "^(item|item2015)\.jd\.com$") {  
  5.        return 403;  
  6.     }  
  7.     default_type 'text/html';  
  8.     charset utf-8;  
  9.     lua_code_cache on;  
  10.     set $skuId $1;  
  11.     content_by_lua_file /usr/chapter7/item.lua;  
  12. }  

測試

重新開機nginx,通路http://item.jd.com/1217499.html可得到響應内容,本例和京東的商品詳情頁的資料是有些出入的,輸出的頁面可能是缺少一些資料的。

優化

local cache

對于其他資訊,對資料一緻性要求不敏感,而且資料量很少,完全可以在本地緩存全量;而且可以設定如5-10分鐘的過期時間是完全可以接受的;是以可以lua_shared_dict全局記憶體進行緩存。具體邏輯可以參考

Java代碼  

nginx另類複雜的架構
  1. local nginx_shared = ngx.shared  
  2. --item.jd.com配置的緩存  
  3. local local_cache = nginx_shared.item_local_cache  
  4. local function cache_get(key)  
  5.     if not local_cache then  
  6.         return nil  
  7.     end  
  8.     return local_cache:get(key)  
  9. end  
  10. local function cache_set(key, value)  
  11.     if not local_cache then  
  12.         return nil  
  13.     end  
  14.     return local_cache:set(key, value, 10 * 60) --10分鐘  
  15. end  
  16. local function get(ip, port, keys)  
  17.     local tables = {}  
  18.     local fetchKeys = {}  
  19.     local resp = nil  
  20.     local status = STATUS_OK  
  21.     --如果tables是個map #tables拿不到長度  
  22.     local has_value = false  
  23.     --先讀取本地緩存  
  24.     for i, key in ipairs(keys) do  
  25.         local value = cache_get(key)  
  26.         if value then  
  27.             if value == "" then  
  28.                 value = nil  
  29.             end  
  30.             tables[key] = value  
  31.             has_value = true  
  32.         else  
  33.             fetchKeys[#fetchKeys + 1] = key  
  34.         end  
  35.     end  
  36.     --如果還有資料沒擷取 從redis擷取  
  37.     if #fetchKeys > 0 then  
  38.         if #fetchKeys == 1 then  
  39.             status, resp = redis_get(ip, port, fetchKeys[1])  
  40.         else  
  41.             status, resp = redis_mget(ip, port, fetchKeys)  
  42.         end  
  43.         if status == STATUS_OK then  
  44.             for i = 1, #fetchKeys do  
  45.                  local key = fetchKeys[i]  
  46.                  local value = nil  
  47.                  if #fetchKeys == 1 then  
  48.                     value = resp  
  49.                  else  
  50.                     value = get_data(resp, i)  
  51.                  end  
  52.                  tables[key] = value  
  53.                   has_value = true  
  54.                   cache_set(key, value or "", ttl)  
  55.             end  
  56.         end  
  57.     end  
  58.     --如果從緩存查到 就認為ok  
  59.     if has_value and status == STATUS_NOT_FOUND then  
  60.         status = STATUS_OK  
  61.     end  
  62.     return status, tables  
  63. end  

nginx proxy cache

為了防止惡意刷頁面/熱點頁面通路頻繁,我們可以使用nginx proxy_cache做頁面緩存,當然更好的選擇是使用CDN技術,如通過Apache Traffic Server、Squid、Varnish。

1、nginx.conf配置

Java代碼  

nginx另類複雜的架構
  1. proxy_buffering on;  
  2. proxy_buffer_size 8k;  
  3. proxy_buffers 256 8k;  
  4. proxy_busy_buffers_size 64k;  
  5. proxy_temp_file_write_size 64k;  
  6. proxy_temp_path /usr/servers/nginx/proxy_temp;  
  7. #設定Web緩存區名稱為cache_one,記憶體緩存空間大小為200MB,1分鐘沒有被通路的内容自動清除,硬碟緩存空間大小為30GB。  
  8. proxy_cache_path  /usr/servers/nginx/proxy_cache levels=1:2 keys_zone=cache_item:200m inactive=1m max_size=30g;  

增加proxy_cache的配置,可以通過挂載一塊記憶體作為緩存的存儲空間。更多配置規則請參考 http://nginx.org/cn/docs/http/ngx_http_proxy_module.html。 

2、nginx_chapter7.conf配置

與server指令配置同級

Java代碼  

nginx另類複雜的架構
  1. ############ 測試時使用的動态請求  
  2. map $host $item_dynamic {  
  3.     default                    "0";  
  4.     item2015.jd.com            "1";  
  5. }  

即如果域名為item2015.jd.com則item_dynamic=1。 Java代碼  

nginx另類複雜的架構
  1. location ~ ^/(\d+).html$ {  
  2.     set $skuId $1;  
  3.     if ($host !~ "^(item|item2015)\.jd\.com$") {  
  4.        return 403;  
  5.     }  
  6.     expires 3m;  
  7.     proxy_cache cache_item;  
  8.     proxy_cache_key $uri;  
  9.     proxy_cache_bypass $item_dynamic;  
  10.     proxy_no_cache $item_dynamic;  
  11.     proxy_cache_valid 200 301 3m;  
  12.     proxy_cache_use_stale updating error timeout invalid_header http_500 http_502 http_503 http_504;  
  13.     proxy_pass_request_headers off;  
  14.     proxy_set_header Host $host;  
  15.     #支援keep-alive  
  16.     proxy_http_version 1.1;  
  17.     proxy_set_header Connection "";  
  18.     proxy_pass http://127.0.0.1/proxy/$skuId.html;  
  19.     add_header X-Cache '$upstream_cache_status';  
  20. }  
  21. location ~ ^/proxy/(\d+).html$ {  
  22.     allow 127.0.0.1;  
  23.     deny all;  
  24.     keepalive_timeout   30s;  
  25.     keepalive_requests  1000;  
  26.     default_type 'text/html';  
  27.     charset utf-8;  
  28.     lua_code_cache on;  
  29.     set $skuId $1;  
  30.     content_by_lua_file /usr/chapter7/item.lua;  
  31. }  

expires:設定響應緩存頭資訊,此處是3分鐘;将會得到Cache-Control:max-age=180和類似Expires:Sat, 28 Feb 2015 10:01:10 GMT的響應頭; proxy_cache:使用之前在nginx.conf中配置的cache_item緩存;

proxy_cache_key:緩存key為uri,不包括host和參數,這樣不管使用者怎麼通過在url上加随機數都是走緩存的;

proxy_cache_bypass:nginx不從緩存取響應的條件,可以寫多個;如果存在一個字元串條件且不是“0”,那麼nginx就不會從緩存中取響應内容;此處如果我們使用的host為item2015.jd.com時就不會從緩存取響應内容;

proxy_no_cache:nginx不将響應内容寫入緩存的條件,可以寫多個;如果存在一個字元串條件且不是“0”,那麼nginx就不會從将響應内容寫入緩存;此處如果我們使用的host為item2015.jd.com時就不會将響應内容寫入緩存;

proxy_cache_valid:為不同的響應狀态碼設定不同的緩存時間,此處我們對200、301緩存3分鐘;

proxy_cache_use_stale:什麼情況下使用不新鮮(過期)的緩存内容;配置和proxy_next_upstream内容類似; 此處配置了如果連接配接出錯、逾時、404、500等都會使用不新鮮的緩存内容;此外我們配置了updating配置,通過配置它可以在nginx正在更新緩 存(其中一個Worker程序)時(其他的Worker程序)使用不新鮮的緩存進行響應,這樣可以減少回源的數量;

proxy_pass_request_headers:我們不需要請求頭,是以不傳遞;

proxy_http_version 1.1和proxy_set_header Connection "":支援keepalive;

add_header X-Cache '$upstream_cache_status':添加是否緩存命中的響應頭;比如命中HIT、不命中MISS、不走緩存BYPASS;比如命中會看到X-Cache:HIT響應頭;

allow/deny:允許和拒絕通路的ip清單,此處我們隻允許本機通路;

keepalive_timeout 30s和keepalive_requests 1000:支援keepalive;

nginx_chapter7.conf清理緩存配置

Java代碼  

nginx另類複雜的架構
  1. location /purge {  
  2.     allow     127.0.0.1;  
  3.     allow     192.168.0.0/16;  
  4.     deny      all;  
  5.     proxy_cache_purge  cache_item $arg_url;  
  6. }  

隻允許内網通路。通路如http://item.jd.com/purge?url=/11.html;如果看到Successful purge說明緩存存在并清理了。

3、修改item.lua代碼 

Java代碼  

nginx另類複雜的架構
  1. --添加Last-Modified,用于響應304緩存  
  2. ngx.header["Last-Modified"] = ngx.http_time(ngx.now())  
  3. local template = require "resty.template"  
  4. template.caching(true)  
  5. template.render("item.html", basicInfo)  
  6. ~                                           

在渲染模闆前設定Last-Modified,用于判斷内容是否變更的條件,預設Nginx通過等于去比較,也可以通過配置if_modified_since指 令來支援小于等于比較;如果請求頭發送的If-Modified-Since和Last-Modified比對則傳回304響應,即内容沒有變更,使用本 地緩存。此處可能看到了我們的Last-Modified是目前時間,不是商品資訊變更的時間;商品資訊變更時間由:商品資訊變更時間、面包屑變更時間和 品牌變更時間三者決定的,是以實際應用時應該取三者最大的;還一個問題就是模闆内容可能變了,但是商品資訊沒有變,此時使用Last-Modified得 到的内容可能是錯誤的,是以可以通過使用ETag技術來解決這個問題,ETag可以認為是内容的一個摘要,内容變更後摘要就變了。

GZIP壓縮

修改nginx.conf配置檔案

Java代碼  

nginx另類複雜的架構
  1. gzip on;  
  2. gzip_min_length  4k;  
  3. gzip_buffers     4 16k;  
  4. gzip_http_version 1.0;  
  5. gzip_proxied        any;  #前端是squid的情況下要加此參數,否則squid上不緩存gzip檔案  
  6. gzip_comp_level 2;  
  7. gzip_types       text/plain application/x-javascript text/css application/xml;  
  8. gzip_vary on;  

此處我們指定至少4k時才壓縮,如果資料太小壓縮沒有意義。  

到此整個商品詳情頁邏輯就介紹完了,一些細節和運維内容需要在實際開發中實際處理,無法做到面面俱到。