发布于 4年前

Spring Controller统计数据库的百万行数据(Aggregate Millions of Database Rows in a Spring Controller)

了解如何使用Spring和Speedment在Java中执行超快速聚合,即使是具有数百万行的大型数据集。

只要API与数据库的结构相匹配,Spring Framework就可以使用JPA和Spring Web快速地建立关系型数据库的RESTful API。 然而,在许多API中,REST端不对应于特定的表,而是对应于一些聚合的字段。在这些情况下,你仍然需要编写自己的REST Controller,但如果数据库具有数百万行,那么这些聚合可能需要一些时间来计算。

在本文,我将向你展示如何使用Speedment Enterprise中的json-stream插件来编写一个非常高效的表示聚合JSON的的REST Controller,它可以快速聚合大型JSON序列,而不会在堆中实现。该演示使用Speed Speed的企业版,你可以使用Speedment网站上的Initializer进行免费试用。

背景

Speedment是一个开源的面向流的Java ORM框架,它使用关系数据库作为来源生成实体和管理器类。 然后使用标准Java 8流查询数据,而不需要单行SQL。

Speedment Enterprise为ORM增加了高效的JVM内存数据存储区。 流可以在内存本地执行,而不是将流转换为SQL。 为避免垃圾回收限制,实体存储在主堆外的DirectBuffers中。 只有在流中使用的列需要在堆上实现,大多数prediate可以快速找到,而不需遍历整个集合。

json-stream是Speedment Enterprise的官方插件,它可以以非常高效的方式将Speedment流聚合为JSON对象。 与Jackson和Gson不同的是,它知道Speedment Enterprise中使用的内部存储,因此不需要实现实体聚合成JSON。

介绍

在本文中,我使用一个名为Employees的MySQL示例数据库来讲解常见的聚合问题。 一家公司记录了每名员工从1985年开始的工资。他们希望能够根据用户指定的标准选择一段时间,看看那段时间内的平均工资是多少。

使用常规SQL,我们可以这样表达:

mysql> select count(emp_no),min(from_date),max(to_date),avg(salary) 
       from salaries where from_date < '1989-01-01' 
                       and to_date  >= '1988-01-01';
+---------------+----------------+--------------+-------------+
| count(emp_no) | min(from_date) | max(to_date) | avg(salary) |
+---------------+----------------+--------------+-------------+
|        133923 | 1987-01-01     | 1989-12-31   |  55477.8502 |
+---------------+----------------+--------------+-------------+
1 row in set (0.66 sec)

如果我们要在Spring中创建一个简单的REST服务,执行此计算并将其作为JSON对象返回,我们可以执行以下操作:

@GetMapping
Result getEmployeeSalaries(@RequestParam String from,
                           @RequestParam String to) {
    return template.queryForObject(
        "select count(emp_no),min(from_date),max(to_date),avg(salary) " +
        "from salaries where from_date < ? and to_date >= ?;",
        (rs, n) -> new Result(rs),
        to, from
    );
}

Result类定义如下(使用Project Lombok来减少引用):

@Data
static class Result {
    private final long count;
    private final String from, to, average;

    Result(ResultSet rs) throws SQLException {
        count   = rs.getLong(1);
        from    = rs.getString(2);
        to      = rs.getString(3);
        average = Utils.CASH.format(rs.getDouble(4));
    }
}

如果我们现在将浏览器定向到/ jdbc?from = 1988-01-01&to = 1989-01-01,我们将看到聚合结果:

{
    "count":   133923,
    "from":    "1987-01-01",
    "to":      "1989-12-31",
    "average": "$55,477.85"
}

然而,性能表现远远不够。 这个简单的服务大约需要700 ms来生成聚合。

当然,我们可以在服务器上缓存最常见的查询,但是还是需要时间来计算从未被请求的结果。 相反,我们尝试使用Speedment重写相同的服务。

步骤一:配置

我准备了一个Speedment配置文件,并在项目中新建了/src/main/json目录。 然后我可以调用mvn speedment:generate来生成所有必需的实体和管理器类。

接下来,我们需要配置Speedment应用程序。 为此,我创建了一个名为SpeedmentConfig.java的文件,如下所示:

@Configuration
public class SpeedmentConfig {

    private final Environment env;

    SpeedmentConfig(Environment env) {
        this.env = requireNonNull(env);
    }

    @Bean(destroyMethod = "stop")
    EmployeesApplication getApplication() {
        return new EmployeesApplicationBuilder()
            .withConnectionUrl(env.getProperty("spring.datasource.url"))
            .withUsername(env.getProperty("spring.datasource.username"))
            .withPassword(env.getProperty("spring.datasource.password"))
            .withBundle(DataStoreBundle.class)
            .withBundle(JsonBundle.class)
            .build();
    }

    ...
}

用户名和密码在Spring application.properties文件中配置。 但是,我仍然需要另外定义三个bean。 我们需要一个Manager,以便我可以查询Salaries表,一个DataStoreComponent,它允许我们初始化Speedment DataStoreComponent,以及一个JsonComponent,以便我们可以设置自定义的JSON聚合器。

@Bean
DataStoreComponent getDataStoreComponent(EmployeesApplication app) {
    return app.getOrThrow(DataStoreComponent.class);
}

@Bean
JsonComponent getJsonComponent(EmployeesApplication app) {
    return app.getOrThrow(JsonComponent.class);
}

@Bean
SalaryManager getSalaryManager(EmployeesApplication app) {
    return app.getOrThrow(SalaryManager.class);
}

我们现在已经用Spring集成Speedment了。

步骤二:控制器类(Controller Class)

我们来看看Controller类。 首先,我们需要通过注入它们以便可以在控制器中使用这三个bean。我喜欢让所有的成员变量为final,所以我将使用Project Lombok来生成一个包含所有参数的构造器。

@RestController
@AllArgsConstructor
@RequestMapping("/speedment")
public class SpeedmentController {

    private final SalaryManager salaries;
    private final DataStoreComponent datastore;
    private final JsonComponent json;

    ...
}

接下来,我们需要告诉Spring一旦bean被初始化,就可以填充内存中的存储。 我们可以用@ PostConstruct注解来做到这一点。

@PostConstruct
void loadInitialState() {
    datastore.load();
}

控制器逻辑与前面几乎相同,只是我们将使用Java 8 Stream来查询数据库而不是SQL。 这样做的最大优点在于,我们稍后只需要对代码做很少的改动就可以给服务添加更多的条件。 过滤流就像添加.filter()操作一样简单。

@GetMapping
String getEmployeeSalaries(@RequestParam String from,
                           @RequestParam String to) {
    return salaries.stream()
        .filter(Salary.FROM_DATE.lessThan(Utils.toEpochSecond(to)))
        .filter(Salary.TO_DATE.greaterOrEqual(Utils.toEpochSecond(from)))
        .collect(
            json.collector(Salary.class)
                .put("count", count())
                .put("from", min(Salary.FROM_DATE, Utils::fromEpochSecond))
                .put("to",   max(Salary.TO_DATE,   Utils::fromEpochSecond))
                .put("average", average(Salary.SALARY, Utils::toCurrency))
                .build()
    );
}

(出于性能原因,我已将日期映射为Speedment的秒数。 这就是为什么你在上面的逻辑中看到Utils.toEpochSecond和Utils.fromEpochSecond。)

步骤三:重新部署

如果我们重新运行服务,我们可以看到终端仍然像以前一样工作:

{
    "count":   133923,
    "from":    "1987-01-01",
    "to":      "1989-12-31",
    "average": "$55,477.85"
}

不同的是,请求速度提高了60倍。 想象一下,你现有应用程序的加速因子为60:例如,不是延迟10秒,你的延迟时间小于200 ms,终端用户几乎不会察觉到。

总结

使用带有数据存储和jso-stream插件的Speedment Enterprise可以非常高效地完成Spring里关系数据的JSON聚合。它很适合与其他的Spring组件搭配,也非常容易配置。

如果你想自己尝试这个例子,你可以从这个GitHub页面下载它。 可以在Speedment网站上免费试用Speedment Enterprise

©2020 edoou.com   京ICP备16001874号-3