精益求精信也科技DAS与携程DAL对比

31次阅读

共计 8878 个字符,预计需要花费 23 分钟才能阅读完成。

一、概述

DAS 是信也科技自研的数据库访问框架。DAS 研发的目的是为了解决当时日益严重的数据库应用开发效率低下,数据库配置管理混乱和数据库难以水平扩展等问题。针对这些问题,DAS 提供整合了数据库配置管理 portal,ORM 框架和分库分表引擎的一体化解决方案。一个 DAS 就可以满足开发者所有的需求,无需花费大量的时间精力去整合各种框架和组件。在落地过程中,DAS 已经证明其能大幅提高研发效率,减低维护成本和避免生产事故。DAS 已经开源,最新版本为 2.4.0。

在 DAS 的研发初期,为了实现快速交付,我们基于携程数据库访问框架 DAL 做了深度的定制化改造。在不断的演化和重构中,DAL 原有的代码被大量替换掉,目前除了最底层的部分代码外,DAS 已经是一个全新的产品。

DAS 与 DAL 的定位基本相同,站在使用者的角度看,DAS 对 DAL 的改进主要体现在以下几个方面:

  1. 增强的分库分表策略
  2. 简洁高效的 DAO 设计
  3. 具备元数据的 Entity
  4. 灵活方便的 SqlBuilder

二、分库分表策略改进

分库分表策略是支持数据库分片的数据库访问框架的核心。其作用是判断用户给出的 SQL 语句要在那些数据库或表分片上执行。判断 SQL 对应的分片范围很有技术挑战。完美的解决方案应该是:

  1. 解析 SQL,确定所有的表达式,表达式包括但不限于以下 >, >=, <, <=, <>, between,not between, in, not in (…), like, not like, is null, is not null,等等
  2. 计算每个表达式对应的分片范围
  3. 根据一定的规则合并各自的分片范围来生成最终的集合。

分库分表策略定义是否全面合理,决定了数据库访问框架的能力上限。

1、DAL 策略设计

携程 DAL 的策略接口核心定义如下:

public interface DalShardingStrategy {String locateDbShard(DalConfigure configure, String logicDbName, DalHints hints);

    String locateTableShard(DalConfigure configure, String logicDbName, String tabelName, DalHints hints);    
}

其中 hints 用于传递 SQL 中所有参数的集合,但不会传递参数对应的表达式的操作符(=,>,< 之类)具体是什么;同时接口的返回值定义为单个 String 值

这种策略定义导致只有包含相等表达式或者赋值类操作的 SQL 才能准确的判断分片范围。并且每次调用策略算法仅能确定最多一个分片。
该策略可以支持如下所示包含相等判断的语句:

SELECT * FROM PERSON WHERE AGE = 18

由于 IN 可以看做是一系列相等操作,因此通过在 hints 中指定 IN 参数,也可以变通的支持 IN,所以下面的语句也支持:

SELECTE * FROM PERSON WHERE AGE IN(18,19,20)

但是用户的 SQL 语句不仅仅只是相等或者 IN 判断,所以这种策略定义在实际使用中有较大限制。

2、DAS 策略设计

接下来我们看一下 DAS 策略接口的核心定义:

public interface ShardingStrategy {Set<String> locateDbShards(ShardingContext ctx);
 
    Set<String> locateTableShards(TableShardingContext ctx);    
}

其中 ShardingContext 参数中包含了 ConditionList 属性。该属性通过树状结构完整定义了语句中所有表达式的类型,数值以及表达式之间的关系(AND,OR,NOT)。同时策略的返回值允许是分片集合,而不是某个特定分片。

因此这种策略定义可以支持几乎所有的表达式,例如:

SELECT * FROM PERSON WHERE (AGE > 18 OR AGE <20) AND (AGE IN (18,19,20) OR AGE BETWEEN [0,100])

通过对比我们可以了解 DAS 的策略适用于更普遍的场景,对用户的限制更少,用法更灵活,更符合用户习惯。

DAS 策略的整体设计非常巧妙,花费了很多心思。对于希望提高自己设计能力的同学来说也是个很好的参考。

具体设计在这里:https://github.com/ppdaicorp/das/wiki

三、DAO 改进

DAO 是研发人员开发数据库应用的打交道最多的编程接口。用户对数据库所有的增删改查操作都要通过 DAO 完成,因此 DAO 设计的好坏直接影响了用户的使用体验。

1、DAL DAO 设计

基于不要让用户写自己写哪怕一行 DAO 代码的原则(错误假设),DAL 有着较复杂的 DAO 类层次结构。要使用 DAO,用户需要先通过 DAL console 生成标准,构建和自定义 DAO 的代码:

  1. 标准 DAO 包含了最常用的单表操作,与特定表相关联。
  2. 构建 DAO 包含针对单表的自定义的操作,生成的时候会跟对应的同一表的标准 DAO 的代码合并
  3. 自定义 DAO 包装用户提供的自定义 SQL,用于跨表查询,复杂语句或者数据库特有语法的 SQL

标准 DAO 和构建 DAO 基于基础 DAO 类 DalTableDao。自定义 DAO 基于基础 DAO 类 DalQueryDao。如果涉及到事务操作,需要调用底层接口 DalClient。关系如下所示:


根据之前提到的原则,即使要完成最简单的数据库操作,用户也需要先生成 DAO。同时在某些特殊场景下还需要调用预定义的 DAO。步骤委实有些繁琐,我印象中,用户多有吐槽。因为负责全团队的 DAO 开发工作,有个用户还曾经强烈要求我们的 DAO 支持任意表,否则他要为每张表都生成代码,而这意味着开发几百个 DAO。我们当时指导他直接使用 DalTableDao,但他还是骂骂咧咧不满意。

2、DAS DAO 设计

DAS 对 DAO 做了大幅优化。将 DalTableDao,DalQueryDao,DalClient 的功能合并在 DasClient 一个类并暴露给用户直接使用。项目添加 DAS 依赖后,用户可以直接使用 DasClient 做数据库操作,再也无需先生成任何 DAO 代码:

除了简化 DAO 类设计,DAS 还做了以下优化:

  1. 简化 API 设计,降低学习成本。例如 DAL 中的 DalTableDao 和 DalQueryDao 一共有 34 个 query 方法,DasClient 里完成全部功能只用了 7 个
  2. 简化 Hints 的用法,以在功能的灵活性,可理解性和系统复杂度方面取得平衡。基于经验我们去掉了 DAL 中不常用的 hints,例如 continueOnError,,asyncExecution 等
  3. 增强 DAS 功能。例如重新设计了 SqlBuilder 类和表实体,可以让用户类似写原生 SQL 的方式创建动态 SQL 语句。下面的章节里会专门介绍

在 DAO 设计上我们下了很多功夫,做了很多的改进。与 DAL 相比,DAS 的类层次更简洁,API 设计更合理,显著降低了用户上手门槛,用起来很顺手。

还记得在在携程我们收到的用户强烈希望 DAO 不要绑死在某张表上面的反馈吗?我们通过 DAS DAO 实现了这个想法。但在 DAS 落地过程中,我们却收到用户反馈说希望提供针对单表的 DAO 以方便继承,同时还提出希望为记录逻辑删除操作提供便利。于是我们又增加了 TableDao 对 DasClient 做了简单的封装,将类型参数化从方法级别提升到类层次来满足用户的自定义需求。并基于 TableDao 提供了 LogicDeletionDao 来支持逻辑删除操作。

万万没想到啊,一顿操作猛如虎之后发现貌似又回到了最初。真是用户虐我千百遍,我待用户如初恋

四、Entity 改进

Entity 是数据库中的表或数据库查询结果的 Java 对应物,一般称为实体。其中表实体一般直接用于数据库的增删改查操作,查询实体仅用于表示查询结果。这两种实体一般通过 console 生成。实体的主要结构是字段属性,表类型的实体还会包含表名信息。

1、DAL 表实体设计

DAL 的表实体里仅包含可赋值的了表字段,通过注解标明了对应的表字段结构。

@Entity(name="dal_client_test")
public class ClientTestModelJpa {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Type(value=Types.INTEGER)
    private Integer id;
    
    @Column(name="quantity")
    @Type(value=Types.INTEGER)
    private Integer quan;
...
    public Integer getId() {return id;}
 
    public void setId(Integer id) {this.id = id;}
 
    public Integer getQuantity() {return quan;}
 
    public void setQuantity(Integer quantity) {this.quan = quantity;}

这种结构的实体完成级别的基于对象实例的增删改查没问题。但除此之外没有其他用途。

2、DAS 表实体设计

DAS 扩充了 DAL 表实体的定义。在普通的属性字段定义外,还新增了表结构元数据定义。下面的例子中,内部静态类 PersonDefinition 定义 person 表结构的元数据,包括:

  1. 表名信息
  2. 字段元数据
  3. 分表操作
@Table
public class Person {public static final PersonDefinition PERSON = new PersonDefinition();
    
    public static class PersonDefinition extends TableDefinition {
        public final ColumnDefinition PeopleID;
        public final ColumnDefinition Name;
...
        public PersonDefinition as(String alias) {return _as(alias);}
        public PersonDefinition inShard(String shardId) {return _inShard(shardId);}
        public PersonDefinition shardBy(String shardValue) {return _shardBy(shardValue);}
 
        public PersonDefinition() {super("person");
            setColumnDefinitions(PeopleID = column("PeopleID", JDBCType.INTEGER),
                    Name = column("Name", JDBCType.VARCHAR),
...
                    );
        }        
    }
    @Id
    @Column(name="PeopleID")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer peopleID;
    
    @Column(name="Name")
    private String name;
....
    public Integer getPeopleID() {return peopleID;}
    public void setPeopleID(Integer peopleID) {this.peopleID = peopleID;}
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}

通过 DAS 的表实体元数据可以方便的获取表名,列名,指定表分片。并且基于这些元数据还可以生成非常丰富和全面的表达式。与 Sqlbuilder 配合使用可以非常方便直观的构建动态 SQL。例如:

import static com.ppdai.das.client.SqlBuilder.selectAllFrom;
private PersonDefinition p = Person.PERSON;
p = p.inShard("0");
builder = selectAllFrom(p).where(p.Name.eq(name)).into(Person.class);
Person pk = dao.queryObject(builder);

表达式方法除了全称,还有简写。例如 eq 和 equal 是等价的方法。下面是一个全称与简写的对比例子:

selectAllFrom(p).where(p.PeopleID.eq(1));
selectAllFrom(p).where(p.PeopleID.equal(1));
selectAllFrom(p).where(p.PeopleID.neq(1));
selectAllFrom(p).where(p.PeopleID.notEqual(1)));
selectAllFrom(p).where(p.PeopleID.greaterThan(1));
selectAllFrom(p).where(p.PeopleID.gteq(1));
selectAllFrom(p).where(p.PeopleID.greaterThanOrEqual(1));
selectAllFrom(p).where(p.PeopleID.lessThan(3));
selectAllFrom(p).where(p.PeopleID.lt(3));
selectAllFrom(p).where(p.PeopleID.lessThanOrEqual(3));
selectAllFrom(p).where(p.PeopleID.lteq(3));
selectAllFrom(p).where(p.PeopleID.between(1, 3));
selectAllFrom(p).where(p.PeopleID.notBetween(2, 3));
selectAllFrom(p).where(p.PeopleID.notBetween(2, 4));
selectAllFrom(p).where(p.PeopleID.in(pks));
selectAllFrom(p).where(p.PeopleID.notIn(pks));
selectAllFrom(p).where(p.Name.like("Te%"));
selectAllFrom(p).where(p.Name.notLike("%s"));
selectAllFrom(p).where(p.Name.isNull());
selectAllFrom(p).where(p.Name.isNotNull());

可以看到这种构建 SQL 的方式很自然和紧凑。

五、SqlBuilder 改进

除了直接基于表实体对象实例的增删改查操作外,还有很多基于复杂 SQL 语句的需求场景。需要框架来提供创建动态 SQL 的功能。这个功能好不好用,也是区分框架设好坏的一个重要的衡量标准。

1、DAL SQL Builder 设计

DAL 的 SqlBuilder 比较复杂,分为单表,多表和批处理三大类,共 7 种,与前面提到的各种 DAO 相对应:

直观的感觉是 DAL 里面 Builder 类划分过细了,一些常见的操作也要一个特定的 builder 来实现。下面是单表查询 builder 的例子:

    List<String> in = new ArrayList<String>();
    in.add("12");
    in.add("12");

    SelectSqlBuilder builder = new SelectSqlBuilder("People", DatabaseCategory.MySql, false);
     
    builder.select("PeopleID","Name","CityID");
     
    builder.equal("PeopleID", "1", Types.INTEGER);
    builder.and().in("Name", in, Types.INTEGER);
    builder.and().between("CityID", "wuhan", "shanghai", Types.INTEGER);
    builder.orderBy("PeopleID", false);

这里的问题主要有以下几个:

  1. 构建 builder 时需手工指定表名以及数据库类型
  2. 创建表达式是需要手工指定列名,参数值以及参数类型
  3. 表达式调用的写法与实际 SQL 语法相反。例如 PeopleID = 1,要写成 equal(“PeopleID”, “1”, Types.INTEGER)

手工操作太多非常容易出错,而且在编译阶段无法识别,出问题后要花很多时间逐行对比语句。感觉过于酸爽。

2、DAS SQL Builder 设计

在 DAS 中,上面所有的 builder 除了 MultipleSqlBuilder 外,在 DAS 里都用一个 SqlBuilder 取代了。

在减少 builder 类数量的同时,为了简化和规范操作,DAS 增加了专门用于批量查询,更新的 BatchQueryBuilder(对应之前的 MultipleSqlBuilder),BatchUpdateBuilder 以及专门用于存储过程调用的 CallBuilder 和 BatchCallBuilder。如下所示

DAL 的 4 个单表操作 SQL builder 在 DAS SqlBuilder 中通过对应的静态方法加以实现。与上一节提到的表实体一起配合使用可以让用户以基本符合 SQL 语法的方式创建动态 SQL。示例如下:

import static com.ppdai.das.client.SqlBuilder.*;

// 查询
SqlBuilder builder = select(p.PeopleID, p.CountryID, p.CityID).from(p).where(p.PeopleID.eq(k+1)).into(Person.class);
Person pk = dao.queryObject(builder);
 
// 插入
SqlBuilder builder = insertInto(p, p.Name, p.CountryID, p.CityID).values(p.Name.of("Jerry" + k), p.CountryID.of(k+100), p.CityID.of(k+200));
assertEquals(1, dao.update(builder));

// 更新
SqlBuilder builder = update(Person.PERSON).set(p.Name.eq("Tom"), p.CountryID.eq(100), p.CityID.eq(200)).where(p.PeopleID.eq(k+1));
assertEquals(1, dao.update(builder));
 
// 删除
SqlBuilder builder = deleteFrom(p).where(p.PeopleID.eq(k+1));
assertEquals(1, dao.update(builder));

与 DAL Builder 相比,DAS SqlBuilder 做到了以下改进:

  1. 可以直接以 SQL 操作对应的静态方法创建 builder,无需指定表名,数据库类型等参数
  2. 可以直接从表实体对应的列上创建表达式,仅需要提供参数即可,无需指定列名和参数类型
  3. 表达式写法与 SQL 语法一致。PeopleID = 1 写成 p.PeopleID.eq(1)

DAS 还定义了 SegmentConstants 类,里面定义了常用 SQL 关键字和一些静态方法,配合 SqlBuilder 使用,可以给用户飞一般的使用感觉。

SqlBuilder builder = SqlBuilder.selectAllFrom(p).where(p.CityID.eq(1), OR, p.CountryID.eq(1), AND, p.Name.like("A"), OR, p.PeopleID.eq(1));

真是优秀!

六、总结

本文通过 DAL 与 DAS 在策略,DAO,entity 和 SqlBuilder 等方面的对比,较深入的剖析了 DAS 的设计思路和原理。

我曾经是携程数据库访问框架 DAL 的产品负责人和 Java 客户端主力开发。与团队一起打造了携程 DAL。DAL 目前还在继续完善并作为主力框架产品支撑着携程每天亿万的数据库请求。我为我的团队和产品感到万分自豪。

在当年 DAL 的研发过程中,由于经验不足和框架产品的特殊性,我们很难大幅调整 API 来实现所有的优化。有时候权衡再三,最终还是不得不放弃了一些很好的想法。这些遗憾在打造 DAS 的过程中得到了弥补。我们将所有的好想法和经验全部应用在了 DAS 的开发上并最终获得了用户的认可和好评。因此这个对比也是一篇自我回顾,自我总结的文章。颇有些“我杀了我”的感觉?。

为了做出完美的设计,易用的功能,节省用户每一步操作,我们开发团队付出了巨大的努力。DAS 凝结了我们所有的心血,在公司内部获得普遍认可和好评。这么好的框架你值得拥有。现在 DAS 已经贡献给开源社区:

https://github.com/ppdaicorp/das

DAS 除了客户端外,还包括 DAS Console 和 DAS Proxy Server。其中 DAS Console 的功能是管理数据库配置和生成 Entity 类,功能非常强大。DAS Proxy Server 可以和 DAS Client 配合使用,透明的支持本地直连和基于代理的数据库连接模式,允许用户在数据库不断增长的情况下平滑升级整体架构。关于这些的介绍请持续关注信也科技的拍码场技术公众号。

技术支持:


作者介绍

Hejiehui,信也科技基础组件部门主管、信也 DAS 产品负责人、布道师。图形化构建工具集 x -series 的作者。曾主持开发携程开源数据库访问框架 DAL。对应用开发效率提升和分布式数据库访问机制有多年的研究积累

正文完
 0