`

Web开发实战2—商品详情页

 
阅读更多

 

 

 

项目搭建

项目部署目录结构。

/usr/server/chapter7

  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 , redis Cluster , codis
实践技术:Twemproxy
安排:B:edu-mysql_192.168.1.121 、edu-mysql2_192.168.1.122、edu-mysql3_192.168.1.123、edu-mysql4_192.168.1.124
思想要点:Twemproxy做一致性Hash,即便某台机器宕机,丢失的只是少数信息,无论主从;
分片到122,123,124,构建从集群针对6660端口
等完成:Twemproxy的高可用性

 

Redis安装配置

 

SSDB安装配置:

SSDB介绍与使用

 

规划设计:

商品基本信息SSDB集群

主集群-写:

192.168.1.122 7770

192.168.1.123 7770

192.168.1.124 7770从集群-读:

192.168.1.122 7771

192.168.1.123 7771

192.168.1.124 7771

 

商品介绍SSDB集群配置

主集群-写:

192.168.1.122 8880

192.168.1.123 8880

192.168.1.124 8880从集群-读:

192.168.1.122 8881

192.168.1.123 8881

192.168.1.124 8881

 

其他信息Redis配置

主集群-写:

192.168.1.122 6660

从集群-读:

192.168.1.122 6661

192.168.1.123 6661

192.168.1.124 6661

 

设计图:和上面的配置不一致部分,因为我采用的是多机器配置

 

1.整体架构为主从模式,写数据到主集群,读数据从从集群读取数据,

2.这样当一个集群不足以支撑流量时可以使用更多的集群来支撑更多的访问量;

3.集群分片使用Twemproxy实现。

 

注:以下配置文件,可以编辑好上传

 

商品基本信息SSDB集群配置

分别在122 123 124上配置# cp /usr/local/ssdb-master/ssdb.conf  /usr/local/ssdb-master/ssdb_basic_7770.conf

# vi  /usr/local/ssdb-master/ssdb_basic_7770.conf

 

 

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

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

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

leveldb:
        cache_size: 500
        block_size: 32
        write_buffer_size: 64
        compaction_speed: 1000
        compression: yes
 
分别在122 123 124上配置

# cp /usr/local/ssdb-master/ssdb.conf  /usr/local/ssdb-master/ssdb_basic_7771.conf

# vi  /usr/local/ssdb-master/ssdb_basic_7771.conf

 

脚本代码:

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

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

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

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

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

配置文件使用Tab而不是空格做缩排(复制到配置文件后请把空格替换为Tab)。主从关系:7770(主)-->7771(从)配置文件如何配置请参考https://github.com/ideawu/ssdb-docs/blob/master/src/zh_cn/config.md。  

 

创建工作目录mkdir -p /usr/data/ssdb_777{0,1}

 

启动

nohup /usr/local/ssdb-master/ssdb-server  /usr/local/ssdb-master/ssdb_basic_7770.conf &
nohup /usr/local/ssdb-master/ssdb-server  /usr/local/ssdb-master/ssdb_basic_7771.conf & 
 

通过ps -aux | grep ssdb命令看是否启动了,tail -f nohup.out查看错误信息。

 

商品介绍SSDB集群配置

分别在122 123 124上配置# cp /usr/local/ssdb-master/ssdb.conf  /usr/local/ssdb-master/ssdb_desc_8880.conf

# vi  /usr/local/ssdb-master/ssdb_desc_8880.conf

 

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

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

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

leveldb:
        cache_size: 500
        block_size: 32
        write_buffer_size: 64
        compaction_speed: 1000
        compression: yes
 分别在122 123 124上配置

# cp /usr/local/ssdb-master/ssdb.conf  /usr/local/ssdb-master/ssdb_desc_8881.conf

# vi  /usr/local/ssdb-master/ssdb_desc_8881.conf

 

脚本代码:

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

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

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

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

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

配置文件使用Tab而不是空格做缩排(复制到配置文件后请把空格替换为Tab)。主从关系:8880(主)-->8881(从)配置文件如何配置请参考https://github.com/ideawu/ssdb-docs/blob/master/src/zh_cn/config.md。  

 

创建工作目录mkdir -p /usr/data/ssdb_888{0,1}

 

启动

nohup /usr/local/ssdb-master/ssdb-server  /usr/local/ssdb-master/ssdb_desc_8880.conf &
nohup /usr/local/ssdb-master/ssdb-server  /usr/local/ssdb-master/ssdb_desc_8881.conf & 
 

通过ps -aux | grep ssdb命令看是否启动了,tail -f nohup.out查看错误信息。

 

其他信息Redis配置

 

在122:

# mkdir /usr/local/redis/conf

# vi /usr/local/redis/conf/redis_6660.conf

 

 

port 6660
pidfile "/var/run/redis_6660.pid"
#设置内存大小,根据实际情况设置,此处测试仅设置20mb
maxmemory 20mb
#内存不足时,所有KEY按照LRU算法删除
maxmemory-policy allkeys-lru
#Redis的过期算法不是精确的而是通过采样来算的,默认采样为3个,此处我们改成10
maxmemory-samples 10
#不进行RDB持久化
save “”
#不进行AOF持久化
appendonly no
 
在122 123 124

 

# vi /usr/local/redis/conf/redis_6661.conf

 

port 6661
pidfile "/var/run/redis_6661.pid"
#设置内存大小,根据实际情况设置,此处测试仅设置20mb
maxmemory 20mb
#内存不足时,所有KEY按照LRU算法进行删除
maxmemory-policy allkeys-lru
#Redis的过期算法不是精确的而是通过采样来算的,默认采样为3个,此处我们改成10
maxmemory-samples 10
#不进行RDB持久化
save “”
#不进行AOF持久化
appendonly no
#主从
slaveof 192.168.1.122 6660
 

 

如上配置放到配置文件最末尾即可;此处内存不足时的驱逐算法为所有KEY按照LRU进行删除(实际是内存基本上不会遇到满的情况);主从关系:6660(主)-->6661(从)

 

启动

 

/usr/local/redis/bin/redis-server /usr/local/redis/conf/redis_6660.conf &
/usr/local/redis/bin/redis-server /usr/local/redis/conf/redis_6661.conf &
 

 

测试:

/usr/local/redis/bin/redis-cli -p 6660

 

 

Twemproxy配置

在122上安装Twemproxy安装参考:Twemproxy-缓存代理分片机制

在学习高可用时,可以配置Twemproxy的高可用,机器可选择 121

 

vi /usr/local/twemproxy/conf/nutcracker.yml

 

basic_master:
  listen: 192.168.1.122:1111
  hash: fnv1a_64
  distribution: ketama
  redis: true
  timeout: 1000
  hash_tag: "::"
  servers:
   - 192.168.1.122:7770:1 server1
   - 192.168.1.123:7770:1 server2
   - 192.168.1.124:7770:1 server3
basic_slave:
  listen: 192.168.1.122:1112
  hash: fnv1a_64
  distribution: ketama
  redis: true
  timeout: 1000
  hash_tag: "::"
  servers:
   - 192.168.1.122:7771:1 server1
   - 192.168.1.123:7771:1 server2
   - 192.168.1.124:7771:1 server3


desc_master:
  listen: 192.168.1.122:1113
  hash: fnv1a_64
  distribution: ketama
  redis: true
  timeout: 1000
  hash_tag: "::"
  servers:
   - 192.168.1.122:8880:1 server1
   - 192.168.1.123:8880:1 server2
   - 192.168.1.124:8880:1 server3

desc_slave:
  listen: 192.168.1.122:1114
  hash: fnv1a_64
  distribution: ketama
  redis: true
  timeout: 1000
  servers:
   - 192.168.1.122:8881:1 server1
   - 192.168.1.123:8881:1 server2
   - 192.168.1.124:8881:1 server3

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


other_slave:
  listen: 192.168.1.122:1116
  hash: fnv1a_64
  distribution: random
  redis: true
  timeout: 1000
  hash_tag: "::"
  servers:
   - 192.168.1.122:6661:1 server1
   - 192.168.1.123:6661:1 server2
   - 192.168.1.124:6661:1 server3
 

 

1、因为我们使用了主从,所以需要给server起一个名字如server1、server2;否则分片算法默认根据ip:port:weight,这样就会主从数据的分片算法不一致;

2、其他信息Redis因为每个Redis是对等的,因此分片算法可以使用random;  

3、我们使用了hash_tag,可以保证相同的tag在一个分片上(本例配置了但没有用到该特性)。

 

#把conf目录复制到/usr/local/twemproxy/sbin/目录下

cp -r  /usr/local/twemproxy-0.4.0/conf /usr/local/twemproxy/sbin/

 

启动及测试

启动命令:nutcracker -d -c /usr/local/twemproxy/conf/nutcracker.yml -p /usr/local/twemproxy/run/redisproxy.pid -o /usr/local/twemproxy/run/redisproxy.log

 

查看是否启动成功:ps -ef | grep nutcracker | grep -v grep

 

简单测试:

redis-cli  -p 1111 

127.0.0.1:1111> set i 1  

OK  

127.0.0.1:1111> get i  

"1" 

 

配置启动/重启/停止脚本方便操作

# cp /usr/local/twemproxy-0.4.0/scripts/nutcracker.init /usr/local/twemproxy

# chmod +x /usr/local/twemproxy/sbin/nutcracker.init 

# vi /usr/local/twemproxy/sbin/nutcracker.init  

-- 将OPTIONS改为 OPTIONS="-d -c /usr/local/twemproxy/conf/nutcracker.yml"

-- 注释掉. /etc/rc.d/init.d/functions;

-- 将daemon --user ${USER} ${prog} $OPTIONS改为${prog} $OPTIONS;

-- 将killproc改为killall。

 

这样就可以使用如下脚本进行启动、重启、停止了。

nutcracker.init {start|stop|status|restart|reload|condrestart}

 

 

测试主从集群是否工作正常:

 

到此数据集群配置成功。

 

动态服务实现

因为真实数据是从多个子系统获取,很难模拟这么多子系统交互,所以此处我们使用假数据来进行实现。

 

项目搭建 

我们使用Maven搭建Web项目,Maven知识请自行学习。

 

项目依赖

本文将最小化依赖,即仅依赖我们需要的servlet、jackson、guava、jedis。

新建Maven工程

 

工程名:chapter7

 

pom.xml

 

  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.0.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>17.0</version>
    </dependency>
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.5.2</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.3.3</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.3.3</version>
    </dependency>
  </dependencies>
 guava是类似于apache commons的一个基础类库,用于简化一些重复操作,可以参考http://ifeve.com/google-guava/

 

 

核心代码

根据请求参数type来决定调用哪个服务获取数据。

com.github.zhangkaitao.chapter7.servlet.ProductServiceServlet

代码:

 

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

 

基本信息服务 本例基本信息提供了如商品名称、图片列表、颜色尺码、扩展属性、规格参数等等数据;而为了简化逻辑大多数数据都是List/Map数据结构。

代码:

 

    private String getBasicInfo(String skuId) throws Exception {
        Map<String, Object> map = new HashMap<String, Object>();
        //商品编号
        map.put("skuId", skuId);
        //名称
        map.put("name", "苹果(Apple)iPhone 6 (A1586) 16GB 金色 移动联通电信4G手机");
        //一级二级三级分类
        map.put("ps1Id", 9987);
        map.put("ps2Id", 653);
        map.put("ps3Id", 655);
        //品牌ID
        map.put("brandId", 14026);
        //图片列表
        map.put("imgs", getImgs(skuId));
        //上架时间
        map.put("date", "2014-10-09 22:29:09");
        //商品毛重
        map.put("weight", "400");
        //颜色尺码
        map.put("colorSize", getColorSize(skuId));
        //扩展属性
        map.put("expands", getExpands(skuId));
        //规格参数
        map.put("propCodes", getPropCodes(skuId));
        map.put("date", System.currentTimeMillis());
        String content = objectMapper.writeValueAsString(map);
        //实际应用应该是发送MQ
        asyncSetToRedis(basicInfoJedisPool, "p:" + skuId + ":", content);
        return objectMapper.writeValueAsString(map);
    }

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

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

    private List<List<?>> getExpands(String skuId) {
        return Lists.newArrayList(
                (List<?>)Lists.newArrayList("热点", Lists.newArrayList("超薄7mm以下", "支持NFC")),
                (List<?>)Lists.newArrayList("系统", "苹果(IOS)"),
                (List<?>)Lists.newArrayList("系统", "苹果(IOS)"),
                (List<?>)Lists.newArrayList("购买方式", "非合约机")
        );
    }

    private Map<String, List<List<String>>> getPropCodes(String skuId) {
        Map<String, List<List<String>>> map = Maps.newHashMap();
        map.put("主体", Lists.<List<String>>newArrayList(
                Lists.<String>newArrayList("品牌", "苹果(Apple)"),
                Lists.<String>newArrayList("型号", "iPhone 6 A1586"),
                Lists.<String>newArrayList("颜色", "金色"),
                Lists.<String>newArrayList("上市年份", "2014年")
        ));
        map.put("存储", Lists.<List<String>>newArrayList(
                Lists.<String>newArrayList("机身内存", "16GB ROM"),
                Lists.<String>newArrayList("储存卡类型", "不支持")
        ));
        map.put("显示", Lists.<List<String>>newArrayList(
                Lists.<String>newArrayList("屏幕尺寸", "4.7英寸"),
                Lists.<String>newArrayList("触摸屏", "Retina HD"),
                Lists.<String>newArrayList("分辨率", "1334 x 750")
        ));
        return map;
    }
 商品介绍服务

 

 

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

 

其他信息服务本例中其他信息只使用了面包屑和品牌数据。

 

    private String getOtherInfo(String ps3Id, String brandId) throws Exception {
        Map<String, Object> map = new HashMap<String, Object>();
        //面包屑
        List<List<?>> breadcrumb = Lists.newArrayList();
        breadcrumb.add(Lists.newArrayList(9987, "手机"));
        breadcrumb.add(Lists.newArrayList(653, "手机通讯"));
        breadcrumb.add(Lists.newArrayList(655, "手机"));
        //品牌
        Map<String, Object> brand = Maps.newHashMap();
        brand.put("name", "苹果(Apple)");
        brand.put("logo", "BrandLogo/g14/M09/09/10/rBEhVlK6vdkIAAAAAAAFLXzp-lIAAHWawP_QjwAAAVF472.png");
        map.put("breadcrumb", breadcrumb);
        map.put("brand", brand);
        //实际应用应该是发送MQ
        asyncSetToRedis(otherInfoJedisPool, "s:" + ps3Id + ":", objectMapper.writeValueAsString(breadcrumb));
        asyncSetToRedis(otherInfoJedisPool, "b:" + brandId + ":", objectMapper.writeValueAsString(brand));
        return objectMapper.writeValueAsString(map);
    }
 

 

辅助工具本例使用Jackson进行JSON的序列化;Jedis进行Redis的操作;使用线程池做异步更新(实际应用中可以使用MQ做实现)。

 

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

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

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

            }
        });
    }
 

 

 

web.xml配置

 

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

 

打WAR包

配置Tomcat

配置tomcat集群,这里先配置1台,后面配置成集群

 

# mkdir -p /usr/server/chapter7/webapp

# cd /usr/server/chapter7/webapp

上传项目

 

修改 /usr/local/src/tomcat-server1/conf/server.xml

<Host name="localhost"  appBase="/usr/server/chapter7/webapp"

 

启动:/usr/local/src/tomcat-server1/bin/startup.sh & tail -f /usr/local/src/tomcat-server1/logs/catalina.out

测试

  1. http://192.168.1.111:8080/info?type=basic&skuId=1  
  2. http://192.168.1.111:8080/info?type=desc&skuId=1  
  3. http://192.168.1.111:8080/info?type=other&ps3Id=1&brandId=1

 

 

配置Nginx应用:在106上面

 

项目结构:

# mkdir /usr/server/chapter7

# vi /usr/server/chapter7/nginx_chapter7.conf

此处server_name 我们指定了item.jd.com(商品详情页)和d.3.cn(商品介绍)。其他配置可以参考第六章内容。另外实际生产环境要把#internal打开,表示只有本nginx能访问。

 

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

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

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

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

 

 

vi /usr/local/nginx/conf/nginx.conf

 

#为了方便测试,注释掉example.conf
include /usr/server/chapter7/nginx_chapter7.conf;
 

 

关于Lualib复制可参考:基础部分的项目搭建

# mkdir /usr/server/chapter7/lualib

# cd /usr/server/chapter7

# cp -r /usr/local/lualib/* ./lualib

 

 

由于lua模块从/usr/server/chapter7目录加载,因为我们要写自己的模块使用

 

    #lua模块路径,其中”;;”表示默认搜索路径,默认到/usr/local/nginx下找  
    lua_package_path "/usr/server/chapter7/lualib/?.lua;;";  #lua 模块  
    lua_package_cpath "/usr/server/chapter7/lualib/?.so;;";  #c模块  
 

 

 

重启nginx /usr/local/nginx/sbin/nginx &  tail -f /usr/local/nginx/logs/error.log 

/usr/local/nginx/sbin/nginx  -s reload &  tail -f /usr/local/nginx/logs/error.log   

 

 

绑定hosts

192.168.1.106 item.jd.com

192.168.1.106 item2015.jd.com 

192.168.1.106 d.3.cn

 注,这里绑定的是你访问用的浏览器的系统

 

访问如 http://item.jd.com/backend/info?type=basic&skuId=1 即看到结果。

 

192.168.1.106/backend/info?type=basic&skuId=1

 

 

>>>>>>>>>>>>>>>>>>前端展示实现 >>>>>>>>>>>>>>>>>>

我们分为三部分实现:基础组件、商品介绍、前端展示部分。

 

基础组件

首先我们进行基础组件的实现,商品介绍和前端展示部分都需要读取Redis和Http服务,因此我们可以抽取公共部分出来复用。

写Lua模块:抽取公共部分出来复用

整个逻辑和第六章类似;只是read_redis根据参数keys个数支持get和mget。 比如read_redis(ip, port, {"key1"})则调用get而read_redis(ip, port, {"key1", "key2"})则调用mget。

# mkdir -p /usr/server/chapter7/lualib/item

# vi /usr/server/chapter7/lualib/item/common.lua

local redis = require("resty.redis")
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
local function close_redis(red)
    if not red then
        return
    end
    --释放连接(连接池实现)
    local pool_max_idle_time = 10000 --毫秒
    local pool_size = 100 --连接池大小
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)

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

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

    --得到的数据为空处理
    if resp == ngx.null then
        resp = nil
    end
    close_redis(red)

    return resp
end

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

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

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

 

商品介绍

 

核心代码

vi /usr/server/chapter7/desc.lua

通过复用逻辑后整体代码简化了许多;此处读取商品介绍从集群;另外前端展示使用JSONP技术展示商品介绍。 

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

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

 

nginx配置 

vi /usr/server/chapter7/nginx_chapter7.conf

    location ~^/desc/(\d+)$ {
        if ($host != "d.3.cn") {
           return 403;
        }
        default_type application/x-javascript;
        charset utf-8;
        lua_code_cache on;
        set $skuId $1;
        content_by_lua_file /usr/server/chapter7/desc.lua;
    }

 

因为item.jd.com和d.3.cn复用了同一个配置文件,此处需要限定只有d.3.cn域名能访问,防止恶意访问。 

 

重启nginx后,访问如http://d.3.cn/desc/1即可得到JSONP结果。

 

 

前端展示

核心代码整个逻辑分为四部分:1、获取基本信息;2、根据基本信息中的关联关系获取其他信息;3、初始化/格式化数据;4、渲染模板。

 

vi /usr/server/chapter7/item.lua

local common = require("item.common")
local item = require("item")
local read_redis = common.read_redis
local read_http = common.read_http
local cjson = require("cjson")
local cjson_decode = cjson.decode
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
local ngx_exit = ngx.exit
local ngx_print = ngx.print
local ngx_var = ngx.var


local skuId = ngx_var.skuId

--获取基本信息
local basicInfoKey = "p:" .. skuId .. ":"
local basicInfoStr = read_redis("192.168.1.122", 1112, {basicInfoKey})
if not basicInfoStr then
   ngx_log(ngx_ERR, "redis not found basic info, back to http, skuId : ", skuId)
   basicInfoStr = read_http({type="basic", skuId = skuId})
end
if not basicInfoStr then
   ngx_log(ngx_ERR, "http not found basic info, skuId : ", skuId)
   return ngx_exit(404)
end

local basicInfo = cjson_decode(basicInfoStr)
local ps3Id = basicInfo["ps3Id"]
local brandId = basicInfo["brandId"]
--获取其他信息
local breadcrumbKey = "s:" .. ps3Id .. ":"
local brandKey = "b:" .. brandId ..":"
local otherInfo = read_redis("192.168.1.122", 1116, {breadcrumbKey, brandKey}) or {}
local breadcrumbStr = otherInfo[1]
local brandStr = otherInfo[2]
if breadcrumbStr then
   basicInfo["breadcrumb"] = cjson_decode(breadcrumbStr)
end
if brandStr then
   basicInfo["brand"] = cjson_decode(brandStr)
end
if not breadcrumbStr and not brandStr then
   ngx_log(ngx_ERR, "redis not found other info, back to http, skuId : ", brandId)
   local otherInfoStr = read_http({type="other", ps3Id = ps3Id, brandId = brandId})
   if not otherInfoStr then
       ngx_log(ngx_ERR, "http not found other info, skuId : ", skuId)
   else
     local otherInfo = cjson_decode(otherInfoStr)
     basicInfo["breadcrumb"] = otherInfo["breadcrumb"]
     basicInfo["brand"] = otherInfo["brand"]
   end
end

local name = basicInfo["name"]
--name to unicode
basicInfo["unicodeName"] = item.utf8_to_unicode(name)
--字符串截取,超长显示...
basicInfo["moreName"] = item.trunc(name, 10)
--初始化各分类的url
item.init_breadcrumb(basicInfo)
--初始化扩展属性
item.init_expand(basicInfo)
--初始化颜色尺码
item.init_color_size(basicInfo)
local template = require "resty.template"
template.caching(true)
template.render("item.html", basicInfo)

 

初始化模块 

vi /usr/server/chapter7/lualib/item.lua

local bit = require("bit")
local utf8 = require("utf8")
local cjson = require("cjson")
local cjson_encode = cjson.encode
local bit_band = bit.band
local bit_bor = bit.bor
local bit_lshift = bit.lshift
local string_format = string.format
local string_byte = string.byte
local table_concat = table.concat

--utf8转为unicode
local function utf8_to_unicode(str)
    if not str or str == "" or str == ngx.null then
        return nil
    end
    local res, seq, val = {}, 0, nil
    for i = 1, #str do
        local c = string_byte(str, i)
        if seq == 0 then
            if val then
                res[#res + 1] = string_format("%04x", val)
            end

           seq = c < 0x80 and 1 or c < 0xE0 and 2 or c < 0xF0 and 3 or
                              c < 0xF8 and 4 or --c < 0xFC and 5 or c < 0xFE and 6 or
                              0
            if seq == 0 then
                ngx.log(ngx.ERR, 'invalid UTF-8 character sequence' .. ",,," .. tostring(str))
                return str
            end

            val = bit_band(c, 2 ^ (8 - seq) - 1)
        else
            val = bit_bor(bit_lshift(val, 6), bit_band(c, 0x3F))
        end
        seq = seq - 1
    end
    if val then
        res[#res + 1] = string_format("%04x", val)
    end
    if #res == 0 then
        return str
    end
    return "\\u" .. table_concat(res, "\\u")
end

--utf8字符串截取
local function trunc(str, len)
   if not str then
     return nil
   end

   if utf8.len(str) > len then
      return utf8.sub(str, 1, len) .. "..."
   end
   return str
end

--初始化面包屑
local function init_breadcrumb(info)
    local breadcrumb = info["breadcrumb"]
    if not breadcrumb then
       return
    end

    local ps1Id = breadcrumb[1][1]
    local ps2Id = breadcrumb[2][1]
    local ps3Id = breadcrumb[3][1]

    --此处应该根据一级分类查找url
    local ps1Url = "http://shouji.jd.com/"
    local ps2Url = "http://channel.jd.com/shouji.html"
    local ps3Url = "http://list.jd.com/list.html?cat=" .. ps1Id .. "," .. ps2Id .. "," .. ps3Id

    breadcrumb[1][3] = ps1Url
    breadcrumb[2][3] = ps2Url
    breadcrumb[3][3] = ps3Url
end

--初始化扩展属性
local function init_expand(info)
   local expands = info["expands"]
   if not expands then
     return
   end
   for _, e in ipairs(expands) do
      if type(e[2]) == "table" then
         e[2] = table_concat(e[2], ",")
      end
   end
end

--初始化颜色尺码
local function init_color_size(info)
   local colorSize = info["colorSize"]

   --颜色尺码JSON串
   local colorSizeJson = cjson_encode(colorSize)
   --颜色列表(不重复)
   local colorList = {}
   --尺码列表(不重复)
   local sizeList = {}
   info["colorSizeJson"] = colorSizeJson
   info["colorList"] = colorList
   info["sizeList"] = sizeList

   local colorSet = {}
   local sizeSet = {}
   for _, cz in ipairs(colorSize) do
      local color = cz["Color"]
      local size = cz["Size"]
      if color and color ~= "" and not colorSet[color] then
         colorList[#colorList + 1] = {color = color, url = "http://item.jd.com/" ..cz["SkuId"] .. ".html"}
         colorSet[color] = true
      end
      if size and size ~= "" and not sizeSet[size] then
         sizeList[#sizeList + 1] = {size = size, url = "http://item.jd.com/" ..cz["SkuId"] .. ".html"}
         sizeSet[size] = ""
      end
   end
end

local _M = {
   utf8_to_unicode = utf8_to_unicode,
   trunc = trunc,
   init_breadcrumb = init_breadcrumb,
   init_expand = init_expand,
   init_color_size = init_color_size
}

return _M

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

 

 

模板html片段 

{* var *}输出变量,{% code %} 写代码片段,{-raw-} 不进行任何处理直接输出。

       var pageConfig = {
            compatible: true,
            product: {
                skuid: {* skuId *},
                name: '{* unicodeName *}',
                skuidkey:'AFC266E971535B664FC926D34E91C879',
                href: 'http://item.jd.com/{* skuId *}.html',
                src: '{* imgs[1] *}',
                cat: [{* ps1Id *},{* ps2Id *},{* ps3Id *}],
                brand: {* brandId *},
                tips: false,
                pType: 1,
                venderId:0,
                shopId:'0',
                specialAttrs:["HYKHSP-0","isDistribution","isHaveYB","isSelfService-0","isWeChatStock-0","packType","IsNewGoods","isCanUseDQ","isSupportCard","isCanUseJQ","isOverseaPurchase-0","is7ToReturn-1","isCanVAT"],
                videoPath:'',
                desc: 'http://d.3.cn/desc/{* skuId *}'
            }
        };
        var warestatus = 1;                
        {% if colorSizeJson then %} var ColorSize = {* colorSizeJson *};{% end %}
                {-raw-}
                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){}
                {-raw-}

 

面包屑

<div class="breadcrumb">
    <strong><a href='{* breadcrumb[1][3] *}'>{* breadcrumb[1][2] *}</a></strong>
    <span>
        &nbsp;&gt;&nbsp;
        <a href='{* breadcrumb[2][3] *}'>{* breadcrumb[2][2] *}</a>
        &nbsp;&gt;&nbsp;
        <a href='{* breadcrumb[3][3] *}'>{* breadcrumb[3][2] *}</a>
        &nbsp;&gt;&nbsp;
    </span>
    <span>
        {% if brand then %}
        <a href='http://www.jd.com/pinpai/{* ps3Id *}-{* brandId *}.html'>{* brand['name'] *}</a>
        &nbsp;&gt;&nbsp;
       {% end %}
       <a href='http://item.jd.com/{* skuId *}.html'>{* moreName *}</a>
    </span>
</div>

 图片列表

<div id="spec-n1" class="jqzoom" onclick="window.open('http://www.jd.com/bigimage.aspx?id={* skuId *}')" clstag="shangpin|keycount|product|spec-n1">
    <img data-img="1" width="350" height="350" src="http://img14.360buyimg.com/n1/{* imgs[1] *}" alt="{* name *}"/>
</div>
<div id="spec-list" clstag="shangpin|keycount|product|spec-n5">
    <a href="javascript:;" class="spec-control" id="spec-forward"></a>
    <a href="javascript:;" class="spec-control" id="spec-backward"></a>
    <div class="spec-items">
        <ul class="lh">
            {% for _, img in ipairs(imgs) do %}
            <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>
            {% end %}
        </ul>
    </div>
</div>

 颜色尺码选择

<div class="dt">选择颜色:</div>
    <div class="dd">
        {% for _, color in ipairs(colorList) do %}
            <div class="item"><b></b><a href="{* color['url'] *}" title="{* color['color'] *}"><i>{* color['color'] *}</i></a></div>
        {% end %}
    </div>
</div>
<div id="choose-version" class="li">
    <div class="dt">选择版本:</div>
    <div class="dd">
        {% for _, size in ipairs(sizeList) do %}
            <div class="item"><b></b><a href="{* size['url'] *}" title="{* size['size'] *}">{* size['size'] *}</a></div>
        {% end %}
    </div>
</div>

 

扩展属性

<ul id="parameter2" class="p-parameter-list">
    <li title='{* name *}'>商品名称:{* name *}</li>
    <li title='{* skuId *}'>商品编号:{* skuId *}</li>
    {% if brand then %}
    <li title='{* brand["name"] *}'>品牌: <a href='http://www.jd.com/pinpai/{* ps3Id *}-{* brandId *}.html' target='_blank'>{* brand["name"] *}</a></li>
    {% end %}
    {% if date then %}
    <li title='{* date *}'>上架时间:{* date *}</li>
    {% end %}
    {% if weight then %}
    <li title='{* weight *}'>商品毛重:{* weight *}</li>
    {% end %}
    {% for _, e in pairs(expands) do %}
    <li title='{* e[2] *}'>{* e[1] *}:{* e[2] *}</li>
    {% end %}
</ul>

 规格参数

<table cellpadding="0" cellspacing="1" width="100%" border="0" class="Ptable">
    {% for group, pc in pairs(propCodes) do  %}
    <tr><th class="tdTitle" colspan="2">{* group *}</th><tr>
    {% for _, v in pairs(pc) do %}
    <tr><td class="tdTitle">{* v[1] *}</td><td>{* v[2] *}</td></tr>
    {% end %}
    {% end %}
</table>

 

nginx配置 

vi /usr/server/chapter7/nginx_chapter7.conf 

    #在配置文件的server部分定义模板加载位置
    set $template_root "/usr/server/chapter7";


    location ~ ^/(\d+).html$ {
        if ($host !~ "^(item|item2015)\.jd\.com$") {
           return 403;
        }
        default_type 'text/html';
        charset utf-8;
        lua_code_cache on;
        set $skuId $1;
        content_by_lua_file /usr/server/chapter7/item.lua;
    }

 

测试

重启nginx,访问http://item.jd.com/1217499.html可得到响应内容,本例和京东的商品详情页的数据是有些出入的,输出的页面可能是缺少一些数据的。

 

由于:no file '/usr/server/chapter7/lualib/utf8.lua'可能需要安装Lua5.3版本才支持utf-8

需要一点点看:/usr/server/chapter7/item.lua,先简单化跳过去

 

 

 

 

分享到:
评论

相关推荐

    仿京东的商品展示详情页面

    本项目“仿京东的商品展示详情页面”是一个前端实战示例,旨在帮助开发者理解和学习如何创建类似京东商品详情页的功能。这个项目解压后可以在HBuilder或其他IDE中运行,方便快速地集成到自己的商城项目中进行个性化...

    Python DevOps运维开发实战集训营【中级班,第8期,2022年12月结课】

    分享课程——《Python DevOps运维开发实战集训营【中级班】》,第8期,2022年12月10号结课,提供配套的代码和课件下载! 《Python DevOps运维开发实战集训营【中级班】》的重点不仅在于讲解 Diango 的核心知识与...

    前端web结课作业 小米商城 纯HTML+CSS

    前端Web开发是构建网页和应用程序的主要部分,涉及到用户与网页交互的所有层面。在这个项目中,CSS和HTML是关键。CSS允许开发者通过选择器来定位HTML元素,并应用样式规则,如颜色、字体、布局和动画效果。HTML则...

    完整版 零基础学Python课件17 第17章 Python web实战智慧星答题测试系统.pptx

    标题中的“零基础学Python课件17 第17章 Python web实战智慧星答题测试系统”指的是一个Python Web开发的实战教程,旨在帮助初学者掌握Web应用的开发,特别是通过构建一个智慧星答题测试系统来实践。这个系统可能...

    accp 5.0 y2 开发基于jsp/servlet/javabean的网上交易系统 项目实战1

    在本项目实战中,我们将深入探讨如何使用Java...通过这个项目实战,学习者不仅可以掌握JSP、Servlet和JavaBean的基本使用,还能了解Web开发的流程,提升解决实际问题的能力,为未来在Java Web领域的工作打下坚实基础。

    仿饿了么goods商品左右联动详情页+功能实现

    在本项目中,我们主要探讨的是如何实现一个与饿了么应用中商品详情页相似的交互效果,包括商品图片的左右点击联动以及页面的滑动滚动功能。这个功能的实现主要依赖于HTML和JavaScript技术,这是一对在前端开发中不可...

    基于Python Web 实战,Flask + Jinja2 + Bootstrap 开发的招聘网站,本科毕业设计项目,课设作业

    基于 Flask / Jinja2 / Bootstrap / MySQL 开发,仿照拉勾网的风格,实现了招聘网站的必需功能 环境 Python 3 MySQL 快速开始 1. 安装 Python 依赖 $ pip3 install -r requirements.txt 2. 修改配置文件 根据自己...

    跟我学Nginx + Lua开发

    例如第三章介绍了Redis/SSDB和Twemproxy的安装与使用,第五章介绍了常用的Lua开发库,第六章和第七章分别通过Web开发实战1——HTTP服务和Web开发实战2——商品详情页来展示实际应用。第八章介绍了流量复制、AB测试和...

    跟我学Nginx+Lua开发.pdf

    - **第七章**: Web开发实战2——商品详情页。进一步探索如何构建复杂的Web页面。 - **第八章**: 流量复制/AB测试/协程。介绍如何使用Lua进行流量复制、AB测试以及如何利用协程提高程序的并发能力。 通过以上步骤,...

    全栈工程师实战

    从提供的文件内容来看,实战技能包括了从基础的Web开发到高级的前后端分离、移动应用开发、自动化测试与持续集成等。全栈工程师需要掌握的技能非常广泛,包括但不限于前端技术(如HTML、CSS、JavaScript)、后端技术...

    Django项目开发实战

     《Django项目开发实战》从构建一个Django开发项目到部署应用实践,由浅入深,引领学员逐步掌握各项技能  课程内容涵盖  Django项目环境和模型准备  Django主页和后台实现  Blog项目详情页实现  页面优化  ...

    Python Web 实战项目,Flask + Jinja2 + Bootstrap 开发的招聘网站

    2. 修改配置文件 根据自己情况,修改 job_web/config.py 主要是 SQLALCHEMY_DATABASE_URI 数据库的链接 3. 创建数据库 根据上面配置中的库名,创建数据库 4. 利用 flask-migrate 建表 命令行终端,先进入项目目录...

    thinkphp2.1 中文web应用开发框架

    ThinkPHP2.1虽然在现代Web开发中可能显得有些过时,但其基本理念和设计模式仍然值得学习。通过学习ThinkPHP2.1,可以更好地理解MVC架构,为学习更高级的框架如ThinkPHP5、Laravel等奠定基础。 总结,ThinkPHP2.1...

    使用Java创建电子商务网站 - 一个实战教程

    在这个实战教程中,我们将深入探讨如何使用Java编程语言和Web开发技术来构建一个功能完备的电子商务网站。首先,我们从项目概述开始,了解这个电子商务网站应用程序的主要功能。 1. 显示商品列表:为了使用户能够...

    基于移动电商项目实战.zip

    在本项目"基于移动电商项目实战.zip"中,我们聚焦于移动电商领域的应用开发,旨在通过实际操作,提升开发者在移动端构建电商平台的能力。这个压缩包包含的"MobileshopForBook"很可能是一个完整的移动电商项目源代码...

    项目案例:制作移动端找房无忧网

    进入二手房详情页,这里需要展示更丰富的房源信息,如地理位置、价格、面积等。HTML5的数据属性 (`data-*`) 可以用来存储额外的非语义信息,便于JavaScript操作。同时,`&lt;canvas&gt;`元素可用于绘制地图,展示房源周边...

    Seam实战java

    通过上述内容的学习,我们可以了解到Seam框架为Java Web开发带来了诸多便利。它不仅简化了开发流程,还提高了开发效率。对于希望使用Seam框架进行实战开发的Java开发者来说,掌握其核心概念和开发流程至关重要。此外...

Global site tag (gtag.js) - Google Analytics