zoukankan      html  css  js  c++  java
  • 各式结构化数据 动态 接入-存储-查询 的处理办法 (第二部分)

    各式结构化数据的动态接入存储查询,这一需求相信有很多人都遇到过,随着实现技术路线选择的不同,遇到的问题出入大了,其解决办法也是大相径庭。数据存储在哪儿,是关系型数据库,还是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、悬而未决,你的建议。





  • 相关阅读:
    Hbase安装与简单使用
    Integer Int
    Maven是什么
    对Servlet执行流程的初步认识
    android studio 开发环境的搭建
    centos下更换默认的python版本
    写出我心(七)
    写出我心(六)
    写出我心(五)
    写出我心(四)
  • 原文地址:https://www.cnblogs.com/wgp13x/p/4031979.html
Copyright © 2011-2022 走看看