1. 概述
AWS Lambda 让我们可以构建轻量级应用,部署和扩缩容都非常方便。虽然可以使用 Spring Cloud Function 这类框架,但出于性能考虑,我们通常会尽量减少框架的使用。
有时候我们需要从 Lambda 中访问关系型数据库,这时 Hibernate 和 JPA 就派上用场了。但问题是:不依赖 Spring 的情况下,如何在 Lambda 中集成 Hibernate?
本文将探讨在 Lambda 中使用 RDBMS 的挑战,以及 Hibernate 在什么场景下有用。我们将使用 Serverless Application Model(SAM)构建一个 REST 接口来访问数据。
同时,我们还会介绍如何使用 Docker 和 AWS SAM CLI 在本地测试整个流程。
2. 在 Lambda 中使用 RDBMS 与 Hibernate 的挑战
Lambda 的代码包越小越好,这有助于减少冷启动时间。同时,Lambda 应该在毫秒级完成任务。然而,关系型数据库的访问通常涉及大量框架代码,性能开销较大。
在云原生架构中,我们更倾向于使用 DynamoDB 这类 Serverless 数据库,它们与 Lambda 更匹配。但项目中可能因历史原因或业务需求,必须使用关系型数据库。
2.1. 从 Lambda 访问 RDBMS
Lambda 执行完后,其容器会被冻结。这个容器可能被复用于下一次调用,也可能被 AWS 运行时销毁。因此,我们必须在单次调用的生命周期内谨慎管理所有资源。
特别注意:不能依赖传统的数据库连接池。因为连接可能在容器冻结时仍未关闭,导致连接泄露。虽然可以在一次调用中使用连接池,但每次调用都得重新创建连接池,并且必须在函数结束时显式关闭所有连接并释放资源。
这意味着,如果 Lambda 突然大规模并发,可能会耗尽数据库连接。即使 Lambda 立即释放连接,数据库也需要时间回收并准备下一次使用。因此,强烈建议对使用 RDBMS 的 Lambda 设置最大并发限制。
⚠️ 在某些项目中,Lambda 并不是连接 RDBMS 的最佳选择。使用传统 Spring Data 服务(带连接池)部署在 EC2 或 ECS 上,可能是更稳妥的方案。
2.2. 为什么选择 Hibernate
判断是否需要 Hibernate,关键看不用它会多写多少代码。
- ✅ 如果不用 Hibernate,你需要手写大量字段映射或复杂 JOIN 查询,那它就是个好选择。
- ✅ 如果你的应用负载不高,对延迟不敏感,Hibernate 的开销可以接受。
2.3. Hibernate 是重量级技术
但也要权衡成本:
- ❌ Hibernate 的 JAR 包有 7MB,启动时需要解析注解、构建 ORM 映射,耗时较长。
- ❌ 对于通常只做简单任务的 Lambda 来说,这可能“杀鸡用牛刀”。
📌 替代方案:
3. 示例应用
我们将构建一个低频物流追踪系统。客户寄送大件物品形成一个“托运单(Consignment)”。托运单在运输途中会不断“打卡”,记录时间和位置,客户可实时查看。
每个托运单有“起点”和“终点”,我们使用 what3words.com 作为地理定位服务。
假设移动设备网络不稳定,数据可能乱序到达。此外,每个托运单包含“物品列表”和“打卡记录”两个集合,这种复杂性正是使用 Hibernate 的理由。
3.1. 接口设计
REST API 设计如下:
POST /consignment
创建托运单,返回 ID,需提供source
和destination
。必须先创建才能操作。POST /consignment/{id}/item
为托运单添加物品,按接收顺序追加。POST /consignment/{id}/checkin
托运单在某地打卡,提供location
和时间戳。数据库按时间戳排序存储。GET /consignment/{id}
获取托运单完整信息,包括是否已送达。
3.2. Lambda 架构
使用单个 Lambda 函数提供上述 REST 接口,通过 Serverless Application Model 定义。这意味着一个 handler 要处理所有请求。
为便于本地测试,我们使用 AWS SAM CLI 模拟运行,避免频繁部署到云端。
4. 创建 Lambda
先搭建一个空的 Lambda 框架,暂不实现数据访问层。
4.1. 前置条件
- 安装 Docker(用于运行测试数据库)
- 安装 AWS SAM CLI
验证安装:
$ docker --version
Docker version 19.03.12, build 48a66213fe
$ sam --version
SAM CLI, version 1.1.0
4.2. 创建 SAM 模板
使用 SAM CLI 初始化项目:
$ sam init
选择以下选项:
1 - AWS Quick Start Templates
13 - Java 8
1 - maven
Project name - shipping-tracker
1 - Hello World Example: Maven
⚠️ 选项编号可能因 SAM 版本而异。
进入 shipping-tracker
目录,检查 template.yaml
,默认有一个 /hello
的 GET 接口。
构建并测试:
$ sam build
$ sam start-api
访问测试:
$ curl localhost:3000/hello
{ "message": "hello world", "location": "192.168.1.1" }
测试成功后按 CTRL+C
停止。
4.3. 定制我们的 API
修改 template.yaml
,添加我们的接口:
Events:
CreateConsignment:
Type: Api
Properties:
Path: /consignment
Method: post
AddItem:
Type: Api
Properties:
Path: /consignment/{id}/item
Method: post
CheckIn:
Type: Api
Properties:
Path: /consignment/{id}/checkin
Method: post
ViewConsignment:
Type: Api
Properties:
Path: /consignment/{id}
Method: get
重命名函数为 ShippingFunction
:
Resources:
ShippingFunction:
Type: AWS::Serverless::Function
同步修改目录名、Java 包名(com.baeldung.lambda.shipping
),并更新 template.yaml
中的 CodeUri
和 Handler
:
Properties:
CodeUri: ShippingFunction
Handler: com.baeldung.lambda.shipping.App::handleRequest
替换 handler 内容:
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-Custom-Header", "application/json");
return new APIGatewayProxyResponseEvent()
.withHeaders(headers)
.withStatusCode(200)
.withBody(input.getResource());
}
删除 src/test
目录(示例中不保留测试)。
4.4. 测试空接口
重新构建并启动:
$ sam build
$ sam start-api
测试 GET:
$ curl localhost:3000/consignment/123
/consignment/{id}
测试 POST:
$ curl -d '{"source":"data.orange.brings", "destination":"heave.wipes.clay"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/
/consignment
返回值是请求路径,可用于后续路由。
4.5. 在 Lambda 中实现接口分发
我们使用单个 Lambda 处理四个接口。虽然可以拆分成多个 handler,但集中管理更利于复用代码。
需要在 handler 中实现类似 REST Controller 的路由逻辑。先创建 ShippingService
存根:
public class ShippingService {
public String createConsignment(Consignment consignment) {
return UUID.randomUUID().toString();
}
public void addItem(String consignmentId, Item item) {
}
public void checkIn(String consignmentId, Checkin checkin) {
}
public Consignment view(String consignmentId) {
return new Consignment();
}
}
创建 Consignment
、Item
、Checkin
空类(后续作为实体模型)。
在 handler 中通过 switch
路由:
Object result = "OK";
ShippingService service = new ShippingService();
switch (input.getResource()) {
case "/consignment":
result = service.createConsignment(
fromJson(input.getBody(), Consignment.class));
break;
case "/consignment/{id}":
result = service.view(input.getPathParameters().get("id"));
break;
case "/consignment/{id}/item":
service.addItem(input.getPathParameters().get("id"),
fromJson(input.getBody(), Item.class));
break;
case "/consignment/{id}/checkin":
service.checkIn(input.getPathParameters().get("id"),
fromJson(input.getBody(), Checkin.class));
break;
}
return new APIGatewayProxyResponseEvent()
.withHeaders(headers)
.withStatusCode(200)
.withBody(toJson(result));
fromJson
和 toJson
可用 Jackson 实现。
4.6. 存根实现小结
目前我们完成了:
- 创建 Lambda 支持 REST API
- 本地测试(
sam
+curl
) - 实现基本路由
📌 注意:
template.yaml
中的路径映射已由 API Gateway 过滤,因此 handler 中无需处理无效路径。
下一步:实现数据库、实体模型和 Hibernate 集成。
5. 搭建数据库
使用 PostgreSQL 作为 RDBMS(其他关系型数据库同理)。
5.1. 在 Docker 中启动 PostgreSQL
拉取镜像:
$ docker pull postgres:latest
创建专用网络(供 Lambda 容器通信):
$ docker network create shipping
启动数据库容器:
docker run --name postgres \
--network shipping \
-e POSTGRES_PASSWORD=password \
-d postgres:latest
--name postgres
:容器名--network shipping
:加入 shipping 网络-e POSTGRES_PASSWORD=password
:设置密码-d
:后台运行
5.2. 创建 Schema
进入容器创建 shipping
schema:
$ docker exec -it postgres psql -U postgres
执行 SQL:
postgres=# create schema shipping;
CREATE SCHEMA
退出(CTRL+D
)。数据库准备就绪。
6. 添加实体模型与 DAO
现在创建实体模型和 DAO。虽然每次调用只用一个连接,但我们会集成 HikariCP,展示如何为需要多连接的场景配置。
6.1. 添加 Hibernate 依赖
在 pom.xml
中添加:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.4.2.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-hikaricp</artifactId>
<version>6.4.2.Final</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.16</version>
</dependency>
6.2. 实体模型
Consignment
实体:
@Entity(name = "consignment")
@Table(name = "consignment")
public class Consignment {
private String id;
private String source;
private String destination;
private boolean isDelivered;
private List items = new ArrayList<>();
private List checkins = new ArrayList<>();
// getters and setters
}
添加字段映射:
@Id
@Column(name = "consignment_id")
public String getId() {
return id;
}
@Column(name = "source")
public String getSource() {
return source;
}
@Column(name = "destination")
public String getDestination() {
return destination;
}
@Column(name = "delivered", columnDefinition = "boolean")
public boolean isDelivered() {
return isDelivered;
}
使用 @ElementCollection
管理集合(自动建关联表):
@ElementCollection(fetch = EAGER)
@CollectionTable(name = "consignment_item", joinColumns = @JoinColumn(name = "consignment_id"))
@OrderColumn(name = "item_index")
public List getItems() {
return items;
}
@ElementCollection(fetch = EAGER)
@CollectionTable(name = "consignment_checkin", joinColumns = @JoinColumn(name = "consignment_id"))
@OrderColumn(name = "checkin_index")
public List getCheckins() {
return checkins;
}
Item
实体(嵌入式):
@Embeddable
public class Item {
private String location;
private String description;
private String timeStamp;
@Column(name = "location")
public String getLocation() {
return location;
}
@Column(name = "description")
public String getDescription() {
return description;
}
@Column(name = "timestamp")
public String getTimeStamp() {
return timeStamp;
}
// ... setters omitted
}
Checkin
实体:
@Embeddable
public class Checkin {
private String timeStamp;
private String location;
@Column(name = "timestamp")
public String getTimeStamp() {
return timeStamp;
}
@Column(name = "location")
public String getLocation() {
return location;
}
// ... setters omitted
}
6.3. 创建 Shipping DAO
DAO 依赖外部传入的 Hibernate Session
(由 Service 管理):
public void save(Session session, Consignment consignment) {
Transaction transaction = session.beginTransaction();
session.save(consignment);
transaction.commit();
}
public Optional<Consignment> find(Session session, String id) {
return Optional.ofNullable(session.get(Consignment.class, id));
}
后续在 ShippingService
中注入。
7. Hibernate 生命周期管理
实体和 DAO 与普通项目类似。关键挑战是:**如何在 Lambda 生命周期中管理 SessionFactory
**。
7.1. 数据库连接配置
通过环境变量配置数据库连接(template.yaml
):
Environment:
Variables:
DB_URL: jdbc:postgresql://postgres/postgres
DB_USER: postgres
DB_PASSWORD: password
postgres
:容器名(Docker 网络内可解析)postgres
:默认数据库名password
:启动容器时设置的密码
📌 生产环境应通过参数化配置,避免硬编码。
7.2. 创建 SessionFactory
配置 Hibernate 设置:
Map<String, String> settings = new HashMap<>();
settings.put(URL, System.getenv("DB_URL"));
settings.put(DIALECT, "org.hibernate.dialect.PostgreSQLDialect");
settings.put(DEFAULT_SCHEMA, "shipping");
settings.put(DRIVER, "org.postgresql.Driver");
settings.put(USER, System.getenv("DB_USER"));
settings.put(PASS, System.getenv("DB_PASSWORD"));
settings.put("hibernate.hikari.connectionTimeout", "20000");
settings.put("hibernate.hikari.minimumIdle", "1");
settings.put("hibernate.hikari.maximumPoolSize", "2");
settings.put("hibernate.hikari.idleTimeout", "30000");
settings.put(HBM2DDL_AUTO, "create-only");
settings.put(HBM2DDL_DATABASE_ACTION, "create");
构建 SessionFactory
:
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySettings(settings)
.build();
return new MetadataSources(registry)
.addAnnotatedClass(Consignment.class)
.addAnnotatedClass(Item.class)
.addAnnotatedClass(Checkin.class)
.buildMetadata()
.buildSessionFactory();
⚠️
HBM2DDL_*
仅用于测试,生产环境必须注释掉!
7.3. 资源释放
SessionFactory
在冷启动时创建(避免重复初始化):
private SessionFactory sessionFactory = createSessionFactory();
但 Hikari 连接池可能持有连接不释放。由于 Lambda 没有“销毁”生命周期钩子,必须手动清理:
private void flushConnectionPool() {
ConnectionProvider connectionProvider = sessionFactory.getSessionFactoryOptions()
.getServiceRegistry()
.getService(ConnectionProvider.class);
HikariDataSource hikariDataSource = connectionProvider.unwrap(HikariDataSource.class);
hikariDataSource.getHikariPoolMXBean().softEvictConnections();
}
✅
softEvictConnections()
释放连接但保留池对象,适合复用场景。
7.4. 集成到 Handler
确保每次调用后释放连接:
try {
ShippingService service = new ShippingService(sessionFactory, new ShippingDao());
return routeRequest(input, service);
} finally {
flushConnectionPool();
}
ShippingService
通过构造函数注入 SessionFactory
和 ShippingDao
。
7.5. 测试 Hibernate 配置
构建并启动(指定 Docker 网络):
$ sam build
$ sam local start-api --docker-network shipping
访问接口触发 DDL 生成:
$ curl localhost:3000/consignment/123
{"id":null,"source":null,"destination":null,"items":[],"checkins":[],"delivered":false}
检查数据库表是否创建:
$ docker exec -it postgres pg_dump -s -U postgres
CREATE TABLE shipping.consignment_item (
consignment_id character varying(255) NOT NULL,
...
确认后注释掉 HBM2DDL_*
配置。
8. 完成业务逻辑
ShippingService
使用 ShippingDao
实现业务逻辑。每个方法用 try-with-resources
确保 Session
关闭。
8.1. 创建托运单
public String createConsignment(Consignment consignment) {
try (Session session = sessionFactory.openSession()) {
consignment.setDelivered(false);
consignment.setId(UUID.randomUUID().toString());
shippingDao.save(session, consignment);
return consignment.getId();
}
}
8.2. 查看托运单
public Consignment view(String consignmentId) {
try (Session session = sessionFactory.openSession()) {
return shippingDao.find(session, consignmentId)
.orElseGet(Consignment::new);
}
}
📌 生产环境应返回 404,此处简化处理。
8.3. 添加物品
public void addItem(String consignmentId, Item item) {
try (Session session = sessionFactory.openSession()) {
shippingDao.find(session, consignmentId)
.ifPresent(consignment -> addItem(session, consignment, item));
}
}
private void addItem(Session session, Consignment consignment, Item item) {
consignment.getItems().add(item);
shippingDao.save(session, consignment);
}
⚠️ 不存在的托运单被忽略(简化处理)。
8.4. 打卡
按时间戳排序,并在到达终点时标记为已送达:
public void checkIn(String consignmentId, Checkin checkin) {
try (Session session = sessionFactory.openSession()) {
shippingDao.find(session, consignmentId)
.ifPresent(consignment -> checkIn(session, consignment, checkin));
}
}
private void checkIn(Session session, Consignment consignment, Checkin checkin) {
consignment.getCheckins().add(checkin);
consignment.getCheckins().sort(Comparator.comparing(Checkin::getTimeStamp));
if (checkin.getLocation().equals(consignment.getDestination())) {
consignment.setDelivered(true);
}
shippingDao.save(session, consignment);
}
9. 应用测试
模拟一个包裹从白宫到帝国大厦的旅程。
创建托运单:
$ curl -d '{"source":"data.orange.brings", "destination":"heave.wipes.clay"}' \
-H 'Content-Type: application/json' \
http://localhost:3000/consignment/
"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207"
添加物品:
$ curl -d '{"location":"data.orange.brings", "timeStamp":"20200101T120000", "description":"picture"}' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/item
"OK"
$ curl -d '{"location":"data.orange.brings", "timeStamp":"20200101T120001", "description":"piano"}' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/item
"OK"
中途打卡:
$ curl -d '{"location":"united.alarm.raves", "timeStamp":"20200101T173301"}' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"
再次打卡:
$ curl -d '{"location":"wink.sour.chasing", "timeStamp":"20200101T191202"}' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"
客户查询状态:
$ curl http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207
{
"id":"3dd0f0e4-fc4a-46b4-8dae-a57d47df5207",
"source":"data.orange.brings",
"destination":"heave.wipes.clay",
"items":[...],
"checkins":[...],
"delivered":false
}
到达终点(标记为已送达):
$ curl -d '{"location":"heave.wipes.clay", "timeStamp":"20200101T214622"}' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"
后续延迟的消息(中间站点)到达:
$ curl -d '{"location":"deflection.famed.apple", "timeStamp":"20200101T201254"}' \
http://localhost:3000/consignment/3dd0f0e4-fc4a-46b4-8dae-a57d47df5207/checkin
"OK"
最终查询,打卡记录按时间戳正确排序,且 delivered=true
。
10. 总结
本文探讨了在轻量级 Lambda 中使用重量级框架 Hibernate 的挑战与解决方案。
我们完成了:
- 使用 SAM 构建 RESTful Lambda
- 本地 Docker + SAM CLI 测试
- 设计 Hibernate 实体模型
- 正确管理
SessionFactory
生命周期(冷启动创建,调用后释放连接)
关键踩坑点:
- ✅ 必须手动释放连接池(
softEvictConnections
) - ✅ 避免生产环境自动生成 DDL
- ✅ 控制 Lambda 并发,防止数据库连接耗尽
完整代码示例可在 GitHub 获取:aws-lambda-modules