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。