本章以京东商品详情页为例,京东商品详情页虽然仅是单个页面,但是其数据聚合源是非常多的,除了一些实时性要求比较高的如价格、库存、服务支持等通过AJAX异步加载加载之外,其他的数据都是在后端做数据聚合然后拼装网页模板的。

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

 

如图所示,商品页主要包括商品基本信息(基本信息、图片列表、颜色/尺码关系、扩展属性、规格参数、包装清单、售后保障等)、商品介绍、其他信息(分类、品牌、店铺【第三方卖家】、店内分类【第三方卖家】、同类相关品牌)。更多细节此处就不阐述了。

整个京东有数亿商品,如果每次动态获取如上内容进行模板拼装,数据来源之多足以造成性能无法满足要求;最初的解决方案是生成静态页,但是静态页的最大的问题:1、无法迅速响应页面需求变更;2、很难做多版本线上对比测试。如上两个因素足以制约商品页的多样化发展,因此静态化技术不是很好的方案。

通过分析,数据主要分为四种:商品页基本信息、商品介绍(异步加载)、其他信息(分类、品牌、店铺等)、其他需要实时展示的数据(价格、库存等)。而其他信息如分类、品牌、店铺是非常少的,完全可以放到一个占用内存很小的Redis中存储;而商品基本信息我们可以借鉴静态化技术将数据做聚合存储,这样的好处是数据是原子的,而模板是随时可变的,吸收了静态页聚合的优点,弥补了静态页的多版本缺点;另外一个非常严重的问题就是严重依赖这些相关系统,如果它们挂了或响应慢则商品页就挂了或响应慢;商品介绍我们也通过AJAX技术惰性加载(因为是第二屏,只有当用户滚动鼠标到该屏时才显示);而实时展示数据通过AJAX技术做异步加载;因此我们可以做如下设计:

1、接收商品变更消息,做商品基本信息的聚合,即从多个数据源获取商品相关信息如图片列表、颜色尺码、规格参数、扩展属性等等,聚合为一个大的JSON数据做成数据闭环,以key-value存储;因为是闭环,即使依赖的系统挂了我们商品页还是能继续服务的,对商品页不会造成任何影响;

2、接收商品介绍变更消息,存储商品介绍信息;

3、介绍其他信息变更消息,存储其他信息。

整个架构如下图所示: 

技术选型

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

数据存储实现

整体架构为主从模式,写数据到主集群,读数据从从集群读取数据,这样当一个集群不足以支撑流量时可以使用更多的集群来支撑更多的访问量;集群分片使用Twemproxy实现。

商品基本信息SSDB集群配置

vim /usr/chapter7/ssdb_basic_7770.conf

Java代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  1. mkdir -p /usr/data/ssdb_888{0,1,2,3}

启动

Java代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  1. cd D:\workspace\chapter7
  2. mvn clean package

此处使用maven命令打包,比如本例将得到chapter7.war,然后将其上传到服务器的/usr/chapter7/webapp,然后通过unzip chapter6.war解压。

配置Tomcat

复制第六章使用的tomcat实例:

Java代码  
  1. cd /usr/servers/
  2. cp -r tomcat-server1 tomcat-chapter7/
  3. vim /usr/servers/tomcat-chapter7/conf/Catalina/localhost/ROOT.xml
Java代码  
  1. <!-- 访问路径是根,web应用所属目录为/usr/chapter7/webapp -->
  2. <Context path="" docBase="/usr/chapter7/webapp"></Context>

指向第七章的web应用路径。

测试

启动tomcat实例。

Java代码  
  1. /usr/servers/tomcat-chapter7/bin/startup.sh

访问如下URL进行测试。

Java代码  
  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代码  
  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代码  
  1. include /usr/chapter7/nginx_chapter7.conf;
  2. #为了方便测试,注释掉example.conf
  3. include /usr/chapter6/nginx_chapter6.conf;
Java代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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. 0
  26. if seq == 0 then
  27. ngx.log(ngx.ERR, 'invalid UTF-8 character sequence' .. ",,," .. tostring(str))
  28. return str
  29. end
  30. val = bit_band(c, 2 ^ (8 - seq) - 1)
  31. else
  32. val = bit_bor(bit_lshift(val, 6), bit_band(c, 0x3F))
  33. end
  34. seq = seq - 1
  35. end
  36. if val then
  37. res[#res + 1] = string_format("%04x", val)
  38. end
  39. if #res == 0 then
  40. return str
  41. end
  42. return "\\u" .. table_concat(res, "\\u")
  43. end
  44. --utf8字符串截取
  45. local function trunc(str, len)
  46. if not str then
  47. return nil
  48. end
  49. if utf8.len(str) > len then
  50. return utf8.sub(str, 1, len) .. "..."
  51. end
  52. return str
  53. end
  54. --初始化面包屑
  55. local function init_breadcrumb(info)
  56. local breadcrumb = info["breadcrumb"]
  57. if not breadcrumb then
  58. return
  59. end
  60. local ps1Id = breadcrumb[1][1]
  61. local ps2Id = breadcrumb[2][1]
  62. local ps3Id = breadcrumb[3][1]
  63. --此处应该根据一级分类查找url
  64. local ps1Url = "http://shouji.jd.com/"
  65. local ps2Url = "http://channel.jd.com/shouji.html"
  66. local ps3Url = "http://list.jd.com/list.html?cat=" .. ps1Id .. "," .. ps2Id .. "," .. ps3Id
  67. breadcrumb[1][3] = ps1Url
  68. breadcrumb[2][3] = ps2Url
  69. breadcrumb[3][3] = ps3Url
  70. end
  71. --初始化扩展属性
  72. local function init_expand(info)
  73. local expands = info["expands"]
  74. if not expands then
  75. return
  76. end
  77. for _, e in ipairs(expands) do
  78. if type(e[2]) == "table" then
  79. e[2] = table_concat(e[2], ",")
  80. end
  81. end
  82. end
  83. --初始化颜色尺码
  84. local function init_color_size(info)
  85. local colorSize = info["colorSize"]
  86. --颜色尺码JSON串
  87. local colorSizeJson = cjson_encode(colorSize)
  88. --颜色列表(不重复)
  89. local colorList = {}
  90. --尺码列表(不重复)
  91. local sizeList = {}
  92. info["colorSizeJson"] = colorSizeJson
  93. info["colorList"] = colorList
  94. info["sizeList"] = sizeList
  95. local colorSet = {}
  96. local sizeSet = {}
  97. for _, cz in ipairs(colorSize) do
  98. local color = cz["Color"]
  99. local size = cz["Size"]
  100. if color and color ~= "" and not colorSet[color] then
  101. colorList[#colorList + 1] = {color = color, url = "http://item.jd.com/" ..cz["SkuId"] .. ".html"}
  102. colorSet[color] = true
  103. end
  104. if size and size ~= "" and not sizeSet[size] then
  105. sizeList[#sizeList + 1] = {size = size, url = "http://item.jd.com/" ..cz["SkuId"] .. ".html"}
  106. sizeSet[size] = ""
  107. end
  108. end
  109. end
  110. local _M = {
  111. utf8_to_unicode = utf8_to_unicode,
  112. trunc = trunc,
  113. init_breadcrumb = init_breadcrumb,
  114. init_expand = init_expand,
  115. init_color_size = init_color_size
  116. }
  117. return _M

比如utf8_to_unicode代码之前已经见过了,其他的都是一些逻辑代码。

模板html片段

Java代码  
  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代码  
  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代码  
  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代码  
  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代码  
  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代码  
  1. <table cellpadding="0" cellspacing="1" width="100%" border="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代码  
  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代码  
  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代码  
  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代码  
  1. ############ 测试时使用的动态请求
  2. map $host $item_dynamic {
  3. default                    "0";
  4. item2015.jd.com            "1";
  5. }

即如果域名为item2015.jd.com则item_dynamic=1。

Java代码  
  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代码  
  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代码  
  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代码  
  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时才压缩,如果数据太小压缩没有意义。

到此整个商品详情页逻辑就介绍完了,一些细节和运维内容需要在实际开发中实际处理,无法做到面面俱到。

来源:http://jinnianshilongnian.iteye.com/blog/2188538

第七章 Web开发实战2——商品详情页相关推荐

  1. OpenResty学习——第七章 Web开发实战2——商品详情页

    本文转自https://blog.csdn.net/jinnianshilongnian/article/details/84704211,好文要顶,感谢博主分享! 本章以京东商品详情页为例,京东商品 ...

  2. 微信小程序实战篇-商品详情页(二)

    今天要讲解商品详情页中sku的弹出选着框,这个涉及css动画样式,css动画是新的知识点,我们之前并没有接触过,请大家做好笔记,我们要做的效果是酱紫的~ 这个布局难点是需要绘制一个阴影背景.弹出的动画 ...

  3. 电商小程序实战教程-商品详情页

    电商小程序实战教程 第一章 总体介绍 第二章 创建数据源 第三章 创建管理后台 第四章 首页的创建 第五章 分类导航 前言 我们已经完成了首页和分类导航页面的开发,本节我们介绍一下商品详情页的开发.商 ...

  4. 亿级流量电商详情页系统实战-28.商品详情页结构分析、缓存全量更新问题以及缓存维度化解决方案

    1.前言 实时性比较高的那块数据,比如说库存,销量之类的这种数据,我们采取的实时的缓存+数据库双写的技术方案,双写一致性保障的方案 实时性要求不高的数据,比如说商品的基本信息,等等,我们采取的是三级缓 ...

  5. 微信小程序实战篇-商品详情页(一)

    哈喽,大家好,今天要进入新篇章啦,商品详情页,这个可是个大模块,要分好几次才能讲解清楚,现在我们先进行第一讲,老规矩,先上效果图 有木有很酷炫啊,下面由代码君教你如何实现. 详情页布局 看效果图,可以 ...

  6. 电商小程序实战教程-商品详情页开发

    我们上一篇开发了电商小程序的首页,本篇我们介绍一下详情页的开发.如果想开发详情页,首先要搞明白一个概念,在详情页展示数据的时候需要如何进行页面传参. 参数变量 在微搭中变量一共是分三种,普通变量.模型 ...

  7. 第六章 Web开发实战1——HTTP服务

    此处我说的HTTP服务主要指如访问京东网站时我们看到的热门搜索.用户登录.实时价格.实时库存.服务支持.广告语等这种非Web页面,而是在Web页面中异步加载的相关数据.这些服务有个特点即访问量巨大.逻 ...

  8. 【项目实战】---商品详情页的制作

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 在前面的 ...

  9. 商品详情页动态渲染系统:大型网站的多机房4级缓存架构设计

    124_大型电商网站的商品详情页的深入分析 之前,咱们也是说在讲解这个商品详情页系统的架构 缓存架构,高可用服务 商品详情页系统,我们只是抽取了其中一部分来讲解,而且还做了很大程度的简化 主要是为了用 ...

最新文章

  1. java8u211_jre864位u211
  2. vue-router 按需加载的 3 种方式
  3. Centos6.3下apache+svn部署web版本同步
  4. PHP 函数dirname()使用实例
  5. Spring AOP,AspectJ,CGLIB 有点晕
  6. cytoscape要求的JAVA版本_微生物研究必备:Cytoscape绘制网络图(一)
  7. android内核模块签名,android安装内核module,提示Required key not available
  8. 安装串口设备驱动时遇到 Windows 无法验证此设备所需的驱动程序的数字签名。最近的硬件或软件更改安装的文件可能未正确签名或已损坏,或者可能是来自未知来源的恶意软件. 问题该如何处理?...
  9. bootstrap怎么在移动端横向布局_移动端筛选中的「不限」到底该怎么用
  10. SpringBoot 手写过滤器amp;加载第三方过滤器
  11. Oracle RAC Brain Split Resolution
  12. 64位电脑 装32位oracle,64位Windows可以安装32位的Oracle吗
  13. iOS之UI--CAShapeLayer
  14. 文件复制 详解(C++)
  15. matlab画一个点电荷电场线,matlab画点电荷电场线
  16. 粒子群优化算法(PSO)附代码
  17. HTTP 压力测试工具 wrk和ab
  18. 苹果cms替换资源_苹果maccms v10二次更新只替换某一个资源图片
  19. Cernet_IPv6机考
  20. Andriod Studio创建数据库并查看自己创建的数据库

热门文章

  1. JAVA无法加载此类文件,Java 7错误:无法加载本机库:ld.so.1:java:致命:libscf.so.1:打开失败:没有此类文件或目录...
  2. [转]Maven如何手动添加jar包到本地Maven仓库
  3. Oracle过滤与排序
  4. 用Quartus II Timequest Timing Analyzer进行时序分析 :实例讲解
  5. 同构字符串(Isomorphic Strings)
  6. 04- 移动APP功能测试要点以及具体业务流程测试
  7. BZOJ1503[NOI2004]郁闷的出纳员——treap
  8. html5学习笔记(audio)
  9. 关于Android中Animation的停止
  10. 手势检测的回调方法中onfling与onscroll的区别