实现
接下来我们来搭建一下第四种架构。
以获取如京东商品页广告词为例,如下图
假设京东有10亿商品,那么广告词极限情况是10亿;所以在设计时就要考虑:
1、数据量,数据更新是否频繁且更新量是否很大;
2、是K-V还是关系,是否需要批量获取,是否需要按照规则查询。
而对于本例,广告词更新量不会很大,每分钟可能在几万左右;而且是K-V的,其实适合使用关系存储;因为广告词是商家维护,因此后台查询需要知道这些商品是哪个商家的;而对于前台是不关心商家的,是KV存储,所以前台显示的可以放进如Redis中。 即存在两种设计:
1、所有数据存储到Mysql,然后热点数据加载到Redis;
2、关系存储到Mysql,而数据存储到如SSDB这种持久化KV存储中。
基本数据结构:商品ID、广告词、所属商家、开始时间、结束时间、是否有效。
后台逻辑
1、商家登录后台;
2、按照商家分页查询商家数据,此处要按照商品关键词或商品类目查询的话,需要走商品系统的搜索子系统,如通过Solr或elasticsearch实现搜索子系统;
3、进行广告词的增删改查;
4、增删改时可以直接更新Redis缓存或者只删除Redis缓存(第一次前台查询时写入缓存);
前台逻辑
1、首先Nginx通过Lua查询Redis缓存;
2、查询不到的话回源到Tomcat,Tomcat读取数据库查询到数据,然后把最新的数据异步写入Redis(一般设置过期时间,如5分钟);此处设计时要考虑假设Tomcat读取Mysql的极限值是多少,然后设计降级开关,如假设每秒回源达到100,则直接不查询Mysql而返回空的广告词来防止Tomcat应用雪崩。
为了简单,我们不进行后台的设计实现,只做前端的设计实现,此时数据结构我们简化为[商品ID、广告词]。另外有朋友可能看到了,可以直接把Tomcat部分干掉,通过Lua直接读取Mysql进行回源实现。为了完整性此处我们还是做回源到Tomcat的设计,因为如果逻辑比较复杂的话或一些限制(比如使用Java特有协议的RPC)还是通过Java去实现更方便一些。
项目搭建
项目部署目录结构。
/usr/chapter6 redis_6660.conf redis_6661.conf nginx_chapter6.conf nutcracker.yml nutcracker.init webapp WEB-INF lib classes web.xml
Redis+Twemproxy配置
此处根据实际情况来决定Redis大小,此处我们已两个Redis实例(6660、6661),在Twemproxy上通过一致性Hash做分片逻辑。
安装
之前已经介绍过Redis和Twemproxy的安装了。
Redis配置redis_6660.conf和redis_6661.conf
#分别为6660 6661 port 6660 #进程ID 分别改为redis_6660.pid redis_6661.pid pidfile "/var/run/redis_6660.pid" #设置内存大小,根据实际情况设置,此处测试仅设置20mb maxmemory 20mb #内存不足时,按照过期时间进行LRU删除 maxmemory-policy volatile-lru #Redis的过期算法不是精确的而是通过采样来算的,默认采样为3个,此处我们改成10 maxmemory-samples 10 #不进行RDB持久化 save “” #不进行AOF持久化 appendonly no
将如上配置放到redis_6660.conf和redis_6661.conf配置文件最后即可,后边的配置会覆盖前边的。
Twemproxy配置nutcracker.yml
server1: listen: 127.0.0.1:1111 hash: fnv1a_64 distribution: ketama redis: true timeout: 1000 servers: - 127.0.0.1:6660:1 server1 - 127.0.0.1:6661:1 server2
复制nutcracker.init到/usr/chapter6下,并修改配置文件为/usr/chapter6/nutcracker.yml。
启动
nohup /usr/servers/redis-2.8.19/src/redis-server /usr/chapter6/redis_6660.conf & nohup /usr/servers/redis-2.8.19/src/redis-server /usr/chapter6/redis_6661.conf & /usr/chapter6/nutcracker.init start ps -aux | grep -e redis -e nutcracker
Mysql+Atlas配置
Atlas类似于Twemproxy,是Qihoo 360基于Mysql Proxy开发的一个Mysql中间件,据称每天承载读写请求数达几十亿,可以实现分表、分库(sharding版本)、读写分离、数据库连接池等功能,缺点是没有实现跨库分表功能,需要在客户端使用分库逻辑,目前Atlas不活跃。另一个选择是使用如阿里的TDDL,它是在客户端完成之前说的功能。到底选择是在客户端还是在中间件根据实际情况选择。
此处我们不做Mysql的主从复制(读写分离),只做分库分表实现。
Mysql初始化
为了测试我们此处分两个表。
CREATE DATABASE chapter6 DEFAULT CHARACTER SET utf8; use chapter6; CREATE TABLE chapter6.ad_0( sku_id BIGINT, content VARCHAR(4000) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE chapter6.ad_1 sku_id BIGINT, content VARCHAR(4000) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Atlas安装
cd /usr/servers/ wget https://github.com/Qihoo360/Atlas/archive/2.2.1.tar.gz -O Atlas-2.2.1.tar.gz tar -xvf Atlas-2.2.1.tar.gz cd Atlas-2.2.1/ #Atlas依赖mysql_config,如果没有可以通过如下方式安装 apt-get install libmysqlclient-dev #安装Lua依赖 wget http://www.lua.org/ftp/lua-5.1.5.tar.gz tar -xvf lua-5.1.5.tar.gz cd lua-5.1.5/ make linux && make install #安装glib依赖 apt-get install libglib2.0-dev #安装libevent依赖 apt-get install libevent #安装flex依赖 apt-get install flex #安装jemalloc依赖 apt-get install libjemalloc-dev #安装OpenSSL依赖 apt-get install openssl apt-get install libssl-dev apt-get install libssl0.9.8 ./configure --with-mysql=/usr/bin/mysql_config ./bootstrap.sh make && make install
Atlas配置
vim /usr/local/mysql-proxy/conf/chapter6.cnf
[mysql-proxy] #Atlas代理的主库,多个之间逗号分隔 proxy-backend-addresses = 127.0.0.1:3306 #Atlas代理的从库,多个之间逗号分隔,格式ip:port@weight,权重默认1 #proxy-read-only-backend-addresses = 127.0.0.1:3306,127.0.0.1:3306 #用户名/密码,密码使用/usr/servers/Atlas-2.2.1/script/encrypt 123456加密 pwds = root:/iZxz+0GRoA= #后端进程运行 daemon = true #开启monitor进程,当worker进程挂了自动重启 keepalive = true #工作线程数,对Atlas的性能有很大影响,可根据情况适当设置 event-threads = 64 #日志级别 log-level = message #日志存放的路径 log-path = /usr/chapter6/ #实例名称,用于同一台机器上多个Atlas实例间的区分 instance = test #监听的ip和port proxy-address = 0.0.0.0:1112 #监听的管理接口的ip和port admin-address = 0.0.0.0:1113 #管理接口的用户名 admin-username = admin #管理接口的密码 admin-password = 123456 #分表逻辑 tables = chapter6.ad.sku_id.2 #默认字符集 charset = utf8
因为本例没有做读写分离,所以读库proxy-read-only-backend-addresses没有配置。分表逻辑即:数据库名.表名.分表键.表的个数,分表的表名格式是table_N,N从0开始。
Atlas启动/重启/停止
/usr/local/mysql-proxy/bin/mysql-proxyd chapter6 start /usr/local/mysql-proxy/bin/mysql-proxyd chapter6 restart /usr/local/mysql-proxy/bin/mysql-proxyd chapter6 stop
如上命令会自动到/usr/local/mysql-proxy/conf目录下查找chapter6.cnf配置文件。
Atlas管理
通过如下命令进入管理接口
mysql -h127.0.0.1 -P1113 -uadmin -p123456
通过执行SELECT * FROM help查看帮助。还可以通过一些SQL进行服务器的动态添加/移除。
Atlas客户端
通过如下命令进入客户端接口
mysql -h127.0.0.1 -P1112 -uroot -p123456
use chapter6; insert into ad values(1 '测试1); insert into ad values(2, '测试2'); insert into ad values(3 '测试3); select * from ad where sku_id=1; select * from ad where sku_id=2; #通过如下sql可以看到实际的分表结果 select * from ad_0; select * from ad_1;
此时无法执行select * from ad,需要使用如“select * from ad where sku_id=1”这种SQL进行查询;即需要带上sku_id且必须是相等比较;如果是范围或模糊是不可以的;如果想全部查询,只能挨着遍历所有表进行查询。即在客户端做查询-聚合。
此处实际的分表逻辑是按照商家进行分表,而不是按照商品编号,因为我们后台查询时是按照商家维度的,此处是为了测试才使用商品编号的。
到此基本的Atlas就介绍完了,更多内容请参考如下资料:
Mysql主从复制
http://369369.blog.51cto.com/319630/790921/
Mysql中间件介绍
http://www.guokr.com/blog/475765/
Atlas使用
http://www.0550go.com/database/mysql/mysql-atlas.html
Atlas文档
https://github.com/Qihoo360/Atlas/blob/master/README_ZH.md
Java+Tomcat安装
Java安装
cd /usr/servers/ #首先到如下网站下载JDK #http://www.oracle.com/technetwork/cn/java/javase/downloads/jdk7-downloads-1880260.html #本文下载的是 jdk-7u75-linux-x64.tar.gz。 tar -xvf jdk-7u75-linux-x64.tar.gz vim ~/.bashrc 在文件最后添加如下环境变量 export JAVA_HOME=/usr/servers/jdk1.7.0_75/ export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH export CLASSPATH=$CLASSPATH:.:$JAVA_HOME/lib:$JAVA_HOME/jre/lib #使环境变量生效 source ~/.bashrc
Tomcat安装
cd /usr/servers/ wget http://ftp.cuhk.edu.hk/pub/packages/apache.org/tomcat/tomcat-7/v7.0.59/bin/apache-tomcat-7.0.59.tar.gz tar -xvf apache-tomcat-7.0.59.tar.gz cd apache-tomcat-7.0.59/ #启动 /usr/servers/apache-tomcat-7.0.59/bin/startup.sh #停止 /usr/servers/apache-tomcat-7.0.59/bin/shutdown.sh #删除tomcat默认的webapp rm -r apache-tomcat-7.0.59/webapps/* #通过Catalina目录发布web应用 cd apache-tomcat-7.0.59/conf/Catalina/localhost/ vim ROOT.xml
ROOT.xml
<!-- 访问路径是根,web应用所属目录为/usr/chapter6/webapp --> <Context path="" docBase="/usr/chapter6/webapp"></Context>
#创建一个静态文件随便添加点内容 vim /usr/chapter6/webapp/index.html #启动 /usr/servers/apache-tomcat-7.0.59/bin/startup.sh
访问如http://192.168.1.2:8080/index.html能处理内容说明配置成功。
#变更目录结构 cd /usr/servers/ mv apache-tomcat-7.0.59 tomcat-server1 #此处我们创建两个tomcat实例 cp –r tomcat-server1 tomcat-server2 vim tomcat-server2/conf/server.xml
#如下端口进行变更 8080--->8090 8005--->8006
启动两个Tomcat
/usr/servers/tomcat-server1/bin/startup.sh /usr/servers/tomcat-server2/bin/startup.sh分别访问,如果能正常访问说明配置正常。
http://192.168.1.2:8080/index.html
http://192.168.1.2:8090/index.html
如上步骤使我们在一个服务器上能启动两个tomcat实例,这样的好处是我们可以做本机的Tomcat负载均衡,假设一个tomcat重启时另一个是可以工作的,从而不至于不给用户返回响应。
Java+Tomcat逻辑开发
搭建项目
我们使用Maven搭建Web项目,Maven知识请自行学习。
项目依赖
本文将最小化依赖,即仅依赖我们需要的servlet、mysql、druid、jedis。
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.27</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.5</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.5.2</version> </dependency> </dependencies>
核心代码
com.github.zhangkaitao.chapter6.servlet.AdServlet
public class AdServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String idStr = req.getParameter("id"); Long id = Long.valueOf(idStr); //1、读取Mysql获取数据 String content = null; try { content = queryDB(id); } catch (Exception e) { e.printStackTrace(); resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } if(content != null) { //2.1、如果获取到,异步写Redis asyncSetToRedis(idStr, content); //2.2、如果获取到,把响应内容返回 resp.setCharacterEncoding("UTF-8"); resp.getWriter().write(content); } else { //2.3、如果获取不到,返回404状态码 resp.setStatus(HttpServletResponse.SC_NOT_FOUND); } } private DruidDataSource datasource = null; private JedisPool jedisPool = null; { datasource = new DruidDataSource(); datasource.setUrl("jdbc:mysql://127.0.0.1:1112/chapter6?useUnicode=true&characterEncoding=utf-8&autoReconnect=true"); datasource.setUsername("root"); datasource.setPassword("123456"); datasource.setMaxActive(100); GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); poolConfig.setMaxTotal(100); jedisPool = new JedisPool(poolConfig, "127.0.0.1", 1111); } private String queryDB(Long id) throws Exception { Connection conn = null; try { conn = datasource.getConnection(); String sql = "select content from ad where sku_id = ?"; PreparedStatement psst = conn.prepareStatement(sql); psst.setLong(1, id); ResultSet rs = psst.executeQuery(); String content = null; if(rs.next()) { content = rs.getString("content"); } rs.close(); psst.close(); return content; } catch (Exception e) { throw e; } finally { if(conn != null) { conn.close(); } } } private ExecutorService executorService = Executors.newFixedThreadPool(10); private void asyncSetToRedis(final String id, final String content) { executorService.submit(new Runnable() { @Override public void run() { Jedis jedis = null; try { jedis = jedisPool.getResource(); jedis.setex(id, 5 * 60, content);//5分钟 } catch (Exception e) { e.printStackTrace(); jedisPool.returnBrokenResource(jedis); } finally { jedisPool.returnResource(jedis); } } }); } }整个逻辑比较简单,此处更新缓存一般使用异步方式去更新,这样不会阻塞主线程;另外此处可以考虑走Servlet异步化来提示吞吐量。