各式结构化数据的动态接入存储查询,这一需求相信有很多人都遇到过,随着实现技术路线选择的不同,遇到的问题出入大了,其解决办法也是大相径庭。数据存储在哪儿,是关系型数据库,还是NoSQL数据库,是MySQL还是Oracle,怎么建立索引,建立什么类型的索引,都是大学问。下面,我要把我对这一解决办法的思考总结一下,有成熟的也有不成熟的,希望大家一起共同探讨。
关键词:结构化数据, 动态, 接入, 存储, 查询
首先,我们得定义一下在本文中什么是结构化数据,这里的结构化数据主要是指扁平化的、可以由基础数据类型组合成的数据,例如:{"data":{"name":"wang anqi","age":28,"money":1653.35,"XX_date":"2013-12-3"}},它们是可以很容易被存入关系型数据库的,我们要讨论的就是这种数据。对应的,非结构化数据这里是指那些需要存储在文件系统中的,不是扁平化的数据。
那么,什么又是“各式结构化数据”呢,在本文中?这是一个数据集合,有可能集合中的每一条数据结构都是不尽相同的,例如:{"data":{"name":"wang anqi","age":28,"money":1653.35,"XX_date":"2013-12-3"}},和{"angel":{"address":"清凉山公园","user":289770363}}同时存在于一个数据集合中,它们结构不同,简单地说:第一条数据有四个属性,第二条数据只有两个属性,属性名称和类型都不一样。“各式”包括了不定数量的属性,不定的属性名称、不定的数据类型。
解释清楚名词了,再解释一下动词:“动态接入”。在普遍情境下,你只会遇到将固定数据结构的数据存储入库,这里的入库主要还是指MySQL一类的关系型数据库。那么你可以选择使用Hibernate等ORM工具或不使用,来进行数据的存储读取,在使用ORM工具的情况下,要首先定义好数据的数据结构,写死在xml里或是java代码里。
一般情况下,你是不会遇到这样的需求的:对于不能事先确定数据结构的数据,我要把它们存储到关系型数据库中,并提供“合法性检验”、“更新”、“查询”等基本数据操作。要说的是,如果要把它们存储到HBase这种NoSQL数据库中,那是再好不过的了,配合着HBase与Solr的集成(详见之前的博客:大数据架构-使用HBase和Solr将存储与索引放在不同的机器上 http://www.cnblogs.com/wgp13x/p/3927979.html),搜索也不是件难事,唯一可能出现的难点在于:Solr对于Schema中filedName的配置,因为结构是动态的,所以fildName也是动态的,这其实也是很好处理的,有位微软的同学已经跟我咨询过这个问题了;事实上,这样的例子是很常见的。
但是往往,事与愿违,很有可能存在着其它的约束条件制约着你:必须使用关系型数据库,那么一整套解决办法是需要设计的。因为当你使用Hibernate时,你不能再把一个数据结构写死在代码里,因为它不是固定的,你该如何入库,该如何查询数据,这都是问题。
要处理好“各式结构化数据动态接入管理”,应该分成以下几步:一、定义数据;二、动态管理;三、数据接入;四、数据查询。其中一二步在之前的博客
各式结构化数据 动态 接入-存储-查询 的处理办法 (第一部分)http://www.cnblogs.com/wgp13x/p/4019600.html 中已经说明。
三、数据接入
因为相关数据提供者可能很多,他们的存储机制、传输方式、使用语言也都不一样,但是需要让他们提供成一致的数据格式,这就需要跟他们协商好一个统一的接口来进行数据解析。在这里我设计了一个统一的数据格式来进行数据接入,即在接入前将各种数据一致化。我采用的是Json定义的通用数据结构,使用Jackson来进行解析,具体的使用方法还需察看我之前写的一篇博客:
使用Json让Java和C#沟通的方法 http://www.cnblogs.com/wgp13x/p/0effafd9f5283cbf36e62b4fb5e94c81.html
下面的就是我定义的Datas数据结构,它按照《基础数据定义文档》(见博客:
各式结构化数据 动态 接入-存储-查询 的处理办法 (第一部分)http://www.cnblogs.com/wgp13x/p/4019600.html
)屏蔽了如int、string、date等数据类型,在colummns中可以说明数据中各字段的数据类型,也可以省略这一colummns说明;在data中,统一把数值转化为string类型。
/** * 数据 * * @author 王安琪 * @since 2014-9-30下午4:13:07 */ public class Datas implements Serializable { @JsonProperty("dataType") private String dataType;//数据类型名称 @JsonProperty("columns") private Map<String, String> columns;//这里可以为空,属性名-属性类型 键值对 @JsonProperty("datas") private List<Data> datas;//多行数据 |
/** * 一行数据 * * @author 王安琪 * @since 2014-9-30下午2:16:06 */ public class Data implements Serializable { @JsonProperty("data") private Map<String, String> data;//属性名-属性值 键值对 |
好了,传入的数据就像这样:{"dataType":"angel","datas":[{"data":{"name":"wang anqi","age":28,"money":1653.35,"XX_date":"2013-12-3"}},{"data":{"name":"王安琪","age":28,"money":16533.5}}]}。为简便起见,这里省略了columns属性。
在这一步需要产生一个文档《统一数据格式定义》,共享给各“干系人”,毕竟数据是可能是由其它部门、其它人提供的,他们依据这里的定义来产生规定格式的数据。
我们接收到数据后,就要依次进行下面的操作了:1、数据格式验证;2、数据入库;3、执行其它业务逻辑。
数据格式验证可以通过在属性表(TBL_ATTRIBUTE)中配置的属性约束正则表达式,来保证接入数据的正确性,它还是比较容易的,较难的是,判断完成后进行的后继操作。比如:一条数据中,只有一列下的数据格式验证不正确,则应该如何处理,是整条丢弃还是这一列的数据内容丢弃,还是其它的方案......后继操作的选择,是由你的业务需要来确定的,通常与技术无关,这时,你就需要拿起电话跟你的“干系人们”进行沟通了。
数据入库,我使用的是直接拼SQL语句,sql = "insert into ** values ***",SQLQuery query = session.createSQLQuery(sql.toString()); query.setProperties(data); query.executeUpdate(); 这样的方式来入库的,表建立起来了,入库还是比较简单的。
四、数据查询
数据查询也要做得很灵活,因为数据结构不定,因此查询条件也不定。数据查询经常需要,对所有类型的数据,它所有的属性,进行 (包含)like 或 (等于)= 或 (大于)> 或 (小于)< 等等条件的查询,甚至还有可能进行组合查询,如(或者)OR (并且)AND,以及它们的嵌套。从查询条件的数据结构来看,它是一个树型结构数据。
动态查询条件,相信很多人在项目中有这样的需要。
为此,我设计了一个统一的查询条件的格式,前端提交的查询条件都需要遵循它。它同样采用Json来定义,使用Jackson来解析。定义的各类如下所示,Addable 是一个空接口,里面没有任何方法。
/** * 查询条件 * * @author 王安琪 * @since 2014-9-30下午3:45:23 */ public class Search implements Serializable { @JsonProperty("area") private List<String> area; @JsonProperty("order") private Order order; @JsonProperty("condition") private Condition condition; |
/** * 查询排序条件 * * @author 王安琪 * @since 2014-7-30下午4:03:46 */ public class Order implements Serializable { @JsonProperty("front") private int front; @JsonProperty("end") private int end; @JsonProperty("sequences") private Map<String, String> sequences; // "age", "desc"; "name", "asc" |
/** * 查询约束组合 * * @author 王安琪 * @since 2014-9-30下午4:04:41 */ public class Condition implements Addable, Serializable { @JsonProperty("relation") private String relation; @JsonProperty("terms") private List<Term> terms; @JsonProperty("conditions") private List<Condition> conditions; |
/** * 单一查询约束 * * @author 王安琪 * @since 2014-9-30下午3:45:39 */ public class Term implements Addable, Serializable { @JsonProperty("field") private String field; @JsonProperty("type") private String type; @JsonProperty("oper") private String oper; @JsonProperty("values") private List<String> values; |
传入的查询条件就像这样:{"area":[{"angel"}],"condition":{"terms":[{"field":"age","type":"int","oper":"between","values":["28","48"]}]}},需要注意的是oper字段的内容,它也是事先定义好的,你需要跟你的“干系人”们协商好可能存在的查询操作符,相关业务需求可能已经规定好了你的查询的可能性,比如对于数据型要有大于小于等于,字符串型要提供有(包含)like 或 (等于)等,日期型要有(大于)> 或 (小于)< 等,这一步,你也需要产生一个文档《数据查询条件定义》。
光设计好了查询条件格式还远远不够,你肯定需要将它解析,继而用它来进行数据库查询,因为这里用的是关系型数据库,所以要把它转成一个SQL语句。下面是各类中实现这一功能的代码段。这里为简化,没有把排序条件order写出。
Term 类 | Term 类 |
/** * 将Term转变为SQL语句 * * @return SQL语句 */ public String toSQL() { StringBuilder sql = new StringBuilder(); String valuesSQL = getSqlValues(type, oper, values); String sqlOper = getSqlOper(oper); sql.append(field).append(" ").append(sqlOper).append(" ") .append(valuesSQL); if (sqlOper.equals("like")) { sql.append(" ESCAPE ‘!‘"); } return sql.toString(); } |
private String getSqlValues(String type, String oper, List<String> values) { StringBuilder sqlValue = new StringBuilder(); if (values.size() == 1) { sqlValue.append(getSqlValue(type, oper, values.get(0))); } else if (values.size() > 1)// between { for (String value : values) { sqlValue.append(getSqlValue(type, oper, value)).append(" and "); } sqlValue.delete(sqlValue.length() - 5, sqlValue.length()); } return sqlValue.toString(); } |
private String getSqlValue(String type, String oper, String value) { StringBuilder sqlValue = new StringBuilder(); value = StringEscapeUtils.escapeSql(value); sqlValue.append("‘"); switch (oper) { case "like": sqlValue.append("%").append(escapeLikeSql(value)).append("%"); break; default: sqlValue.append(value); break; } sqlValue.append("‘"); return sqlValue.toString(); } |
private String escapeLikeSql(String likeValue) { String str = StringUtils.replace(likeValue, "!", "!!"); str = StringUtils.replace(str, "%", "!%"); str = StringUtils.replace(str, "*", "!*"); str = StringUtils.replace(str, "?", "!?"); str = StringUtils.replace(str, "_", "!_"); return str; } private String getSqlOper(String oper) { String sqlOper; switch (oper) { case "like": sqlOper = "like"; break; default: sqlOper = oper; break; } return sqlOper; } |
Condition 类 | Search 类 |
/** * 产生SQL语句 * * @return SQL语句 */ public String toSQL() { StringBuilder sql = new StringBuilder(); String rlt = (relation == null || relation.isEmpty()) ? Constants.SQL_AND : relation; if (terms != null) { for (Term term : terms) { sql.append("(").append(term.toSQL()).append(")").append(rlt); } sql.delete(sql.length() - rlt.length(), sql.length()); } if (conditions != null) { for (Condition condition : conditions) { sql.append("(").append(condition.toSQL()).append(")") .append(rlt); } sql.delete(sql.length() - rlt.length(), sql.length()); } return sql.toString(); } |
public String toSQL() { if (this.condition == null) { return "1 = 1"; } return this.condition.toSQL(); } |
通过调用Search.toSQL()拿到使用以上代码生成的SQL查询条件语句后,你就可以使用Hibernate提供的
Query query = m_sessionFactory.getCurrentSession().createSQLQuery(sql.toString()).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP); List result = query.list(); 来进行数据库查询了,你甚至可以依照《统一数据格式定义》中定义的Datas类型,来生成一个Datas对象。
List<Data> datas = new ArrayList<Data>(); for (Object oResult : result) { Map mResult = (Map) oResult; Map<String, String> values = new HashMap<String, String>(); for (Object o : mResult.keySet()) { if (o == null || mResult.get(o) == null) { continue; } String name = String.valueOf(o); Object vo = mResult.get(name); String value; { value = String.valueOf(mResult.get(name)).trim(); } if (value == null || value.equals("")) { continue; } values.put(name, value); } Data data = new Data(values); datas.add(data); } Datas ret = new Datas(search.getArea().get(0), null, datas); |
存在问题
1、大数据查询问题
如果,你要接入的数据量巨大,现在经过以上的步骤,也都正确存入了关系型数据库中了,可能一张表中存储了千万级的数据,现在提交了一次查询请求,这下好了,这次查询请求连接到数据库查询数据占用了十来分钟的时间(这跟查询条件有关,对于查不到的数据,查询就很慢),也就是说十多分钟后此链接才能释放,那么问题来了,如果你多提交了几次这样的查询请求,只有十次,数据库就卡死了,大量的链接Client Connections停滞在Sending data和Statistics状态,再来多少请求,无论是占长时间的查询还是很短时间就能处理的查询,统统都不能立即返回结果了,用户直接的反应就是你的后台不工作了,虽然耐心等待十多分钟后还能正常查询。
处理办法:
a、建立索引。因为是各式结构化数据动态接入,所以对所有的数据表,所有的字段,根据不同的数据类型,需要建立(不同的)索引,这很繁琐,而且索引占用磁盘空间,经过测试,索引建完后,查询速度是提升了不少,但仍不可接受。这是一个大问题,也许对于大数据,也许经过MySQL的性能优化能够稍微好些,但MySQL能做的只有这了。你有什么好的调优手段?
b、悬而未决,你的建议。
2、Date类型转化问题
通过Transformers.ALIAS_TO_ENTITY_MAP查询出来的结果,value = String.valueOf(mResult.get(name)).trim();结果对于日期、时间类型的数据都是Date类型的,数据库中的时间可能是yyyy-MM-dd HH:mm:ss样式的,然而value却是2014-10-11 15:50:30.0这样的,精确度不一致,用户也有意见。
处理办法:
a、悬而未决,你的建议。
来自http://www.cnblogs.com/wgp13x/