【发布时间】:2021-07-31 09:38:15
【问题描述】:
例如,假设我正在构建一个简单的社交网络。我目前有两个服务:
-
Identity,管理用户、他们的个人数据(电子邮件、密码哈希等)和他们的公共配置文件(用户名)和身份验证 -
Social,管理用户的帖子、他们的朋友和他们的订阅源
Identity 服务可以在/api/users/{id} 使用其 API 提供用户的公共个人资料:
// GET /api/users/1 HTTP/1.1
// Host: my-identity-service
{
"id": 1,
"username": "cat_sun_dog"
}
Social 服务可以通过其 API 在/api/posts/{id} 发布帖子:
// GET /api/posts/5 HTTP/1.1
// Host: my-social-service
{
"id": 5,
"content": "Cats are great, dogs are too. But, to be fair, the sun is much better.",
"authorId": 1
}
这很好,但是我的客户端(一个网络应用程序)希望显示带有作者姓名的帖子,并且它最好在一个 REST 请求中接收以下 JSON 数据。
{
"id": 5,
"content": "Cats are great, dogs are too. But, to be fair, the sun is much better.",
"author": {
"id": 1,
"username": "cat_sun_dog"
}
}
我找到了两种主要的方法来解决这个问题。
数据复制
如Microsoft's guide for data 和Microsoft's guide for communication between microservices 中所述,微服务可以通过设置事件总线(例如 RabbitMQ)并使用来自其他服务的事件来复制它所需的数据:
最后(这是构建微服务时出现大多数问题的地方),如果您的初始微服务需要最初由其他微服务拥有的数据,请不要依赖对这些数据发出同步请求。相反,通过使用最终一致性(通常通过使用集成事件,如后续部分所述)将该数据(仅您需要的属性)复制或传播到初始服务的数据库中。
因此,Social 服务可以使用由Identity 服务产生的事件,例如UserCreatedEvent 和UserUpdatedEvent。然后,Social 服务可以在其自己的数据库中拥有所有用户的副本,但只有所需的数据(他们的 Id 和 Username,仅此而已)。
通过这种最终一致的方法,Social 服务现在拥有 UI 所需的所有数据,都在一个请求中!
// GET /api/posts/5 HTTP/1.1
// Host: my-social-service
{
"id": 5,
"content": "Cats are great, dogs are too. But, to be fair, the sun is much better.",
"author": {
"id": 1,
"username": "cat_sun_dog"
}
}
好处:
- 使
Social服务完全独立于Identity服务;没有它也可以正常工作 - 检索数据需要更少的网络往返
- 为跨服务验证提供数据(例如检查给定用户是否存在)
缺点和问题:
- 需要一些时间才能传播更改
- 如果由于a disaster 炸毁了所有复制的队列而导致某些消息无法通过,那么对于某些用户来说,系统绝对会崩溃!
- 如果有一天我需要来自用户的更多数据,比如他们的
ProfilePicture,该怎么办? - 如果我想添加具有相同复制数据的新服务该怎么办?
API 网关聚合
如 Microsoft's guide for data 中所述,可以创建一个 API 网关来聚合来自两个请求的数据:一个到 Social 服务,另一个到 Identity 服务。
因此,我们可以在 ASP.NET Core 的伪代码中实现 API 网关操作 (/api/posts/{id}):
[HttpGet("/api/posts/{id}")]
public async Task<IActionResult> GetPost(int id)
{
var post = await _postService.GetPostById(id);
if (post is null)
{
return NotFound();
}
var author = await _userService.GetUserById(post.AuthorId);
return Ok(new
{
Id = post.Id,
Content = post.Content,
Author = new
{
Id = author.Id,
Username = author.Username
}
});
}
然后,客户端只需使用 API 网关并在一次查询中获取所有数据,而无需任何客户端开销:
// GET /api/posts/5 HTTP/1.1
// Host: my-api-gateway
{
"id": 5,
"content": "Cats are great, dogs are too. But, to be fair, the sun is much better.",
"author": {
"id": 1,
"username": "cat_sun_dog"
}
}
好处:
- 非常容易实现
- 始终提供最新数据
- 提供一个集中的位置来缓存 API 查询
缺点和问题:
- 延迟增加:在这种情况下,这是由于两个连续的网络往返
- 如果
Identity服务关闭,则操作会中断,尽管可以使用circuit breaker pattern 缓解这种情况,但客户端无论如何都不会看到作者的名字 - 未使用的数据可能仍会被查询并浪费资源(但这在大多数情况下是微不足道的)
有这两个选项:API 网关上的聚合和使用事件在单个微服务上复制数据,在哪种情况下使用哪个,以及如何使用正确实施它们?
【问题讨论】:
标签: asp.net-core microservices api-gateway eventual-consistency