各式结构化数据 动态 接入-存储-查询 的处理办法 (第二部分)

各式结构化数据的动态接入存储查询,这一需求相信有很多人都遇到过,随着实现技术路线选择的不同,遇到的问题出入大了,其解决办法也是大相径庭。数据存储在哪儿,是关系型数据库,还是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(typeopervalues);
        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 dataStatistics状态,再来多少请求,无论是占长时间的查询还是很短时间就能处理的查询,统统都不能立即返回结果了,用户直接的反应就是你的后台不工作了,虽然耐心等待十多分钟后还能正常查询。
处理办法:
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、悬而未决,你的建议。

 

 

 

 

 

 

 





posted @ 2014-10-17 20:18  王安琪  阅读(1133)  评论(0编辑  收藏  举报