熟悉项目前端工程结构
熟悉Weex的在移动端的好处
熟悉移动端列表页面的结构与开发过程
熟悉生产与测试环境的数据库差异
熟悉Mycat技术的开发与实战技巧
掌握分库分表的设计技巧
掌握Mycat自定义分表算法开发
前端工程基于VUE技术,分为3个独立的项目:

Nodejs已安装
安装资料文件夹下的node即可
Cnpm、Nrm、Webpack
安装完nodejs以后会自动安装npm,但是npm的镜像源是国外的站点,所以建议使用cnpm,或者把npm的镜像源设置为taobao镜像源也是可以的
需要安装webpack 全局安装
Weex Toolkit
安装weex的toolkit
npm i -g weex-toolkit
weex -v // 查看当前weex工具版本
WebStorm
导入资料文件夹下的原始项目heima-leadnews-app这个客户端项目即可,后续的项目随着课程的深入会依次导入其他项目,在当天资料中解压heima-leadnews-app.zip文件,拷贝到一个没有中文和空格的目录,使用web storm工具打开即可
执行命令

运行效果

在命令行运行 npm run serve 默认浏览器便会到开对应网页。
也可使用Playground扫描页面上的二维码,在手机上体验网页。
注意:项目默认选用无线网络运行,如需修改,可以修改config/config.js和webpack.dev.conf.js文件中的下列代码:

注:手机扫描时,手机和电脑应该在同一局域网内。
https://weex.apache.org/zh/guide/introduction.html
Weex 致力于使开发者能基于通用跨平台的 Web 开发语言和开发经验,来构建 Android、iOS 和 Web 应用。简单来说,在集成了 WeexSDK 之后,你可以使用 JavaScript 语言和前端开发经验来开发移动应用。


apis :按页面存放各个页面的api文件
common :存放公共配置和api
compoents :存放公用组件
langs :存放国际化语言包
mock :存放单元测试数据
pages :存放页面源码
routers :存放路由配置源码
stores : 存放本地缓存源码
styles :存放样式源码
utils :存放通用工具类
一个基于 Weex 的富交互、轻量级、高性能的 UI 组件库
官方文档:https://alibaba.github.io/weex-ui/#/cn/
为提高开发效率,项目可使用weex-ui快速开发,选用的列表样式参考:
https://alibaba.github.io/weex-ui/#/cn/packages/wxc-tab-page/
创建src/apis/home/api.js文件,用于封装Home页面文章数据请求
function Api(){var vue;
}
Api.prototype = {setVue : function(vue){this.vue = vue;},// 加载数据loaddata : function(params){let dir = params.loaddirlet url = this.getLoadUrl(dir)return new Promise((resolve, reject) => {this.vue.$request.get(url,params).then((d)=>{resolve(d);}).catch((e)=>{reject(e);})})},// 保存展现行为数据saveShowBehavior : function(params){let ids = [];for(let k in params){if(params[k]){ids.push({id:k});}}console.log(params)if(ids.length>0){let url = this.vue.$config.urls.get('show_behavior')return new Promise((resolve, reject) => {this.vue.$request.post(url,JSON.stringify({equipmentId:1,articleIds:ids})).then((d)=>{d.data=idsresolve(d);}).catch((e)=>{reject(e);})})}},// 区别请求那个URLgetLoadUrl : function(dir){let url = this.vue.$config.urls.get('load')if(dir==0)url = this.vue.$config.urls.get('loadnew')else if(dir==2)url = this.vue.$config.urls.get('loadmore')return url;}
}export default new Api()
创建src/pages/home/config.js文件,用于封装频道Tab页名称和样式
export default {tabTitles: [{title: '动态',id:'__dyna__'},{title: '推荐',id:'__all__'},{ title: 'JAVA',id:1},{ title: 'Python',id:2},{title: 'VUE',id:3},{title: 'WEEX',id:4},{title: '大数据',id:5},{title: 'Docker',id:6},{title: '其它',id:0}],tabStyles: {bgColor: '#FFFFFF',titleColor: '#9b9b9b',activeTitleColor: '#3D3D3D',activeBgColor: '#FFFFFF',isActiveTitleBold: true,iconWidth: 70,iconHeight: 70,width: 120,height: 80,fontSize: 24,hasActiveBottom: true,activeBottomColor: '#3194ff',activeBottomHeight: 6,activeBottomWidth: 36,textPaddingLeft: 10,textPaddingRight: 10,normalBottomColor: 'rgba(0,0,0,0.4)',normalBottomHeight: 2,hasRightIcon: false,rightOffset: 100}
}
data: () => ({api: null,// APIshownew: false,//是否显示loadnew动画showmore: false,//是否显示loadmore动画tabTitles: Config.tabTitles,//频道配置tabStyles: Config.tabStyles,//频道样式tabList: [],//列表数据集合tabPageHeight: 1334,//列表总高度params: {loaddir: 1,index: 0,tag: "__all__",size: 20,max_behot_time: 1559789043,min_behot_time: 1559789043},//列表数据请求参数ashow: {}//列表展示行为记录表
}),
在src/apis/home/api.js中实现Api
首页API需实现load、loadnew、loadmore、show_behavior等后端接口的调用,其实现代码如下
function Api(){var vue;
}
Api.prototype = {setVue : function(vue){this.vue = vue;},// 加载数据loaddata : function(params){let dir = params.loaddirlet url = this.getLoadUrl(dir)return new Promise((resolve, reject) => {this.vue.$request.get(url,params).then((d)=>{resolve(d);}).catch((e)=>{reject(e);})})},// 保存展现行为数据saveShowBehavior : function(params){let ids = [];for(let k in params){if(params[k]){ids.push({id:k});}}console.log(params)if(ids.length>0){let url = this.vue.$config.urls.get('show_behavior')return new Promise((resolve, reject) => {this.vue.$request.post(url,JSON.stringify({equipmentId:1,articleIds:ids})).then((d)=>{d.data=idsresolve(d);}).catch((e)=>{reject(e);})})}},// 区别请求那个URLgetLoadUrl : function(dir){let url = this.vue.$config.urls.get('load')if(dir==0)url = this.vue.$config.urls.get('loadnew')else if(dir==2)url = this.vue.$config.urls.get('loadmore')return url;}
}export default new Api()
定义通用的url路径处理,common/conf.js中处理url
const config = {urls:{baseUrl:'/toutiao',load:'api/v1/article/load',loadmore:'api/v1/article/loadmore',loadnew:'api/v1/article/loadnew',// 解决多平台问题getBase : function(){if(weex.config.env.platform=='Web'){return config.urls.baseUrl;}else{return "http://m.toutiao.com"}},get:function(name){let tmp = config.urls[name];if(tmp)return config.urls.getBase()+"/"+tmp;elsereturn config.urls.getBase()+"/"+name;}},style:{main_bg:'#3296fa'}}
export default config
Index.vue中template部分
VIEW包含头部功能条和文章列表组件,这两个组件采用column布局,其代码如下:
{{load_new_text}} | {{load_more_text}}
样式style
核心方法
// 缓存国际化数据computed:{ },
// 初始化变量和页面样式
created () {},
methods: {// 列表项在可见区域展示后的事件处理show:function(id){ },// 上拉加载更多loadmore:function(){},// 下来刷新数据loadnew:function(){},// 正常加载数据load : function(){},// 列表数据转换成View需要的Model对象tanfer : function(data){ },// 频道页切换事件wxcTabPageCurrentTabSelected (e) {},// 兼容回调wxcPanItemPan (e) {},// 列表项点击事件wxcPanItemClicked(id){}
}
实现代码
完整的代码:
{{load_new_text}} | {{load_more_text}}
1212

我们的项目设计目标如下图:

从中可以看出,有上亿级别的用户,单表存储已超出Mysql最优性能数据量区间,因此会导致数据库各种操作效率会非常低下,此时可以通过分库分表的方案来解决。Mycat是一款非常优秀的分布式数据库中间件。对读写分离和分库分表都有支持,而且比较易用,对原有的应用系统侵入比较小,系统改造比较易于实现。黑马头条项目将以此作为分库分表的中间拆分数据表,以实现设计目标。
mycat相关介绍,请参考资料文件夹/Mycat权威指南.pdf
数据节点: DataNode:存储数据的节点,每个节点可以分配一个或多个数据表。
主机节点:Datahost:主机节点,每个主机可以分配一个或多个数据节点。
分表策略:根据分表策略决定数据写入到哪个节点。
管理信息:管理MYCAT连接和配置信息。
按照黑马头条项目数据量需求,如何设计分库分表可参考以下示例:
一般情况下,一个优化后的MySQL单表可以存储1000万条数据。也是我们分表的基准。所以一年后约为4亿条数据,根据我们单表的1000万基准,划分40个节点。
以此类推,可规划出项目中每个数据库的DN节点数需求如下:
除了节点需求之外,还需要设计每张表的分表数量,计算的方法类似节点的计算,但存在一些细节地方,详见以下说明:
如app_info分库分表设计输出的信息如下(其它库表详见资料下《数据库路由设计.xlsx》):

MYCAT常用配置文件为一下4个:
这里我们主要配置了连接信息:
123456 itheima-news
定义数据节点:
定义主机节点:
select user()
定义表信息:
该文件主要定义分表策略:
示例:
主键求余策略:
40 id mod-long40
该文件为全局自增主键配置对于table定义了autoIncrement=“true” primaryKey="id"的表,需要配置全局自增序号(注意全大写)。
AP_ARTICLE=DNSQ
同时在DNSQ也就是app_seq表里插入一条记录:
INSERT INTO `app_seq`.`MYCAT_SEQUENCE` (`name`, `current_value`, `increment`) VALUES ('ap_article', '1000', '1000');
mycat的连接使用跟MySQL类似:只需要配置文件配置即可
# 数据库配置
mysql.core.jdbc.url=jdbc:mysql://localhost:8066/itheima-news?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
mysql.core.jdbc.username=root
mysql.core.jdbc.password=654321
注意:这里密码做了反转处理,具体的情参考项目配置。本文使用的配置文件的全部内容在本文档所在文件下对应的名称。
为便于管理维护Mycat的路由配置和自定分片算法的源码,在工程下创建service-mycat模块。
service-mycat
heima-leadnews com.heima 1.0-SNAPSHOT
在service-mycat路径下拷贝dev、test、prod三个环境配置文件
service-mycat根路径下创建config文件夹(用于存放mycat的路由配置,便于持续部署),并拷贝‘资料/ mycat初始配置文件’下文件到config下
开发Mycat的分片策略,需要依赖Mycat-server-1.6.7.1-release.jar文件。在项目中以本地文件方式进行导入,操作步骤如下:
在service-mycat下新建文件夹libs
拷贝讲义资料下面的Mycat-server-1.6.7.1-release.jar到libs中
在service-mycat/pom.xml中做以下依赖配置
com.mycat mycat-server 1.0.0 system ${basedir}/libs/Mycat-server-1.6-release.jar
在项目中分片字段规范命名为burst,类型为String,值格式为dataId-分表ID,dataId用于分组ID计算,为数据量提供扩展性;分表ID用于组内表的分配,其分片公式如下:
分片ID = *(dataId/volume) step +分表ID/mod **
Volume是每组分片的数据容量
Step是每组分片的DateNode数量
Mode是表在每组分片中的节点数量
实现步骤如下:
创建类文件:com.heima.HeiMaBurstRuleAlgorithm
修改类继承AbstractPartitionAlgorithm 和实现RuleAlgorithm接口
在类中定义volume、step、mod三个变量,并提供set方法
实现calculate 等值分片DN计算方法(参数是burst值)
实现calculateRange范围分片DN计算方法(参数是burst值)
完整代码如下:
/*** 自定义多字段算法计算*/
public class HeiMaBurstRuleAlgorithm extends AbstractPartitionAlgorithm implements RuleAlgorithm {// 单组数据容量Long volume;// 单组DN节点数量Integer step;// 分片模Integer mod;public void init(){}/**** @param columnValue 数据ID-桶ID* @return*/public Integer calculate(String columnValue){if(columnValue!=null){String[] temp = columnValue.split("-");if(temp.length==2){try {Long dataId = Long.valueOf(temp[0]);Long burstId = Long.valueOf(temp[1]);int group = (int)(dataId/volume)*step;int pos = group + (int)(burstId%mod);System.out.println("HEIMA RULE INFO ["+columnValue+"]-[{"+pos+"}]");return pos;}catch (Exception e){System.out.println("HEIMA RULE INFO ["+columnValue+"]-[{"+e.getMessage()+"}]");}}}return 0;}/*** 范围计算* @param beginValue* @param endValue* @return*/public Integer[] calculateRange(String beginValue, String endValue){if(beginValue!=null&&endValue!=null){Integer begin = calculate(beginValue);Integer end = calculate(endValue);if(begin == null || end == null){return new Integer[0];}if (end >= begin) {int len = end - begin + 1;Integer[] re = new Integer[len];for (int i = 0; i < len; i++) {re[i] = begin + i;}return re;}}return new Integer[0];}public void setVolume(Long volume) {this.volume = volume;}public void setStep(Integer step) {this.step = step;}public void setMod(Integer mod) {this.mod = mod;}}
在项目中主要用到50DN、40DN的可扩展分片算法,可定义成两个tableRule.
Burst5050单组有50个DN,单组容量为5亿,分50张表,每个DN上存储一张对应表;其定义在service-mycat/config/rule.xml中,定义代码为:
burst burst5050
500000000 50 50
Burst4040单组有40个DN,单组容量为4亿,分40张表,每个DN上存储一张对应表;其定义在service-mycat/config/rule.xml中,定义代码为:
burst burst4040
400000000 40 40
在service-mycat/config/schema.xml中,可使用以上定义的规则,示例如下:
在service-mycat/config/sequence_db_conf.properties中去掉对应表的sequence配置:
AP_BEHAVIOR_ENTRY=...AP_COLLECTION=...
进入到项目service-mycat项目根路径,运行mvn clean package打包命令打包项目
拷贝service-mycat/config下的文件到mycat安装目录下的config文件夹下
拷贝service-mycat/config下的文件到mycat安装目录下的config文件夹下
拷贝service-mycat/target/ service-mycat-1.0-SNAPSHOT.jar文件到mycat安装目录lib文件夹下
重启mycat服务
算法测试分为工具测试和程序应用测试,工具测试用于开发过程中辅助开发人员,程序应用测试在此会说明项目中用到的方式,具体测试在后面的功能中进行演示。
注意:以下测试说明只针对带有burst复合字段分库分表的物理表。
在mysql客户端工具中我们可以通过explain关键字查看sql会在哪些DN节点上执行。例:

此工具类用于拼接处理burst,定义在utils工程下com.heima.common.BurstUtils,其实现代码如下:
/*** 分片桶字段算法*/
public class BurstUtils {public final static String SPLIT_CHAR = "-";/*** 用-符号链接* @param fileds* @return*/public static String encrypt(Object... fileds){StringBuffer sb = new StringBuffer();if(fileds!=null&&fileds.length>0) {sb.append(fileds[0]);for (int i = 1; i < fileds.length; i++) {sb.append(SPLIT_CHAR).append(fileds[i]);}}return sb.toString();}/*** 默认第一组* @param fileds* @return*/public static String groudOne(Object... fileds){StringBuffer sb = new StringBuffer();if(fileds!=null&&fileds.length>0) {sb.append("0");for (int i = 0; i < fileds.length; i++) {sb.append(SPLIT_CHAR).append(fileds[i]);}}return sb.toString();}
}
在程序中要正确的插入数据,需要给对应表增加burst字段,并在程序中拼接字段值,同时数据主键id的值需要在程序中生成(既项目中使用ZKSEQUENCE方式生成),例:
程序中拼接字段值:
alb.setId(sequences.sequenceApLikes());//设置主键
alb.setBurst(BurstUtils.encrypt(alb.getId(),alb.getBehaviorEntryId()));//设置分片
Mapper.xml中正常插入:
insert into ap_likes_behavior (id, behavior_entry_id, entry_id, type, operation, created_time,burst)values (#{id}, #{behaviorEntryId}, #{entryId}, #{type}, #{operation},#{createdTime},#{burst})
数据查询的问题有两点:
1、不能明确数据id,所以无法直接拼接burst
2、 burst字段用于分片,写在where条件之后不够优雅
第一个问题查询时可假定id为0,既默认为在第一组中查询,以后有多组时可以默认每组第一个数据id;这样设计的原因是只要组容量内的id和第一个数据id计算出的结果一致。
第二个问题可以通过Mycat提供的注解语法优雅的编写我们的SQL。
综合以上解决思路,代码实现的示例如下:
以上代码在Mycat中执行,首先会执行注解代码确定SQL路由的指定节点,然后再分发SQL具体执行。其中注解中的burst有多个可以使用in,其值需用$连接(由于burst字段完全有后端程序控制值,所以这里不存在外部安全问题),否则会抛出预编译参数不匹配的问题。
数据更新请参考《数据查询》
数据删除请参考《数据查询》
已实现的文章列表查询功能,分了40个表,对于查询功能暂无需调整。
ap_show_behavior数据是通过批量方式插入的,集成Mycat之后,需要使用注解,以便实现注解的托管生成。
/*!mycat:catlet=io.mycat.route.sequence.BatchInsertSequence */insert into ap_show_behavior ( entry_id, article_id,is_click, show_time, created_time) values (#{entryId}, #{item},0, now(),now())
ation},#{createdTime},#{burst})
数据查询的问题有两点:
1、不能明确数据id,所以无法直接拼接burst
2、 burst字段用于分片,写在where条件之后不够优雅
第一个问题查询时可假定id为0,既默认为在第一组中查询,以后有多组时可以默认每组第一个数据id;这样设计的原因是只要组容量内的id和第一个数据id计算出的结果一致。
第二个问题可以通过Mycat提供的注解语法优雅的编写我们的SQL。
综合以上解决思路,代码实现的示例如下:
以上代码在Mycat中执行,首先会执行注解代码确定SQL路由的指定节点,然后再分发SQL具体执行。其中注解中的burst有多个可以使用in,其值需用$连接(由于burst字段完全有后端程序控制值,所以这里不存在外部安全问题),否则会抛出预编译参数不匹配的问题。
数据更新请参考《数据查询》
数据删除请参考《数据查询》
已实现的文章列表查询功能,分了40个表,对于查询功能暂无需调整。
ap_show_behavior数据是通过批量方式插入的,集成Mycat之后,需要使用注解,以便实现注解的托管生成。
/*!mycat:catlet=io.mycat.route.sequence.BatchInsertSequence */insert into ap_show_behavior ( entry_id, article_id,is_click, show_time, created_time) values (#{entryId}, #{item},0, now(),now())
上一篇:“即使”有哪些同义词
下一篇:shell-函数