各式结构化数据的动态接入存储查询,这一需求相信有很多人都遇到过,随着实现技术路线选择的不同,遇到的问题出入大了,其解决办法也是大相径庭。数据存储在哪儿,是关系型数据库,还是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时,你不能再把一个数据结构写死在代码里,因为它不是固定的,你该如何入库,该如何查询数据,这都是问题。
要处理好“各式结构化数据动态接入管理”,应该分成以下几步:一、定义数据;二、动态管理;三、数据接入;四、数据查询。其中一二步在之前的博客
三、数据接入
因为相关数据提供者可能很多,他们的存储机制、传输方式、使用语言也都不一样,但是需要让他们提供成一致的数据格式,这就需要跟他们协商好一个统一的接口来进行数据解析。在这里我设计了一个统一的数据格式来进行数据接入,即在接入前将各种数据一致化。我采用的是Json定义的通用数据结构,使用Jackson来进行解析,具体的使用方法还需察看我之前写的一篇博客:
下面的就是我定义的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、悬而未决,你的建议。