构建短信应用与异步任务处理
1. 短信应用代码调整
首先是一段代码示例:
def your_number do
30
twilio_config = Application.get_env(:phone_app, :twilio, [])
-
Keyword.fetch!(twilio_config, :number)
-
end
此函数定义了发送短信所需的参数,在第 8 行调用 SMS API 上下文函数通过 HTTP 发送消息,最后在第 20 行将成功的短信消息持久化到数据库,若过程中出现错误则返回给调用者。
接下来要进行一些调整,让应用能正常运行。需要创建 Webhook 控制器,步骤如下:
1. 创建
lib/phone_app_web/controllers/webhook/twilio_controller.ex
文件,并添加以下代码:
defmodule PhoneAppWeb.Webhook.TwilioController do
use PhoneAppWeb, :controller
def sms(conn, params) do
persist_message(params)
conn
|> put_resp_content_type("text/xml")
|> send_resp(200, incoming_sms_response())
end
defp persist_message(params) do
message = %{
message_sid: params["MessageSid"],
account_sid: params["AccountSid"],
body: params["Body"],
from: params["From"],
to: params["To"],
status: params["SmsStatus"],
direction: :incoming
}
PhoneApp.Conversations.create_sms_message(message)
end
defp incoming_sms_response do
"""
<?xml version="1.0" encoding="UTF-8"?>
<Response></Response>
"""
end
end
该控制器接收有效负载并将收到的短信消息持久化到数据库,与之前创建的控制器不同的是,它以 XML 有效负载进行回复,因为 Twilio 的 Webhook 响应要求使用 XML 格式。
2. 还需要将该路由添加到
PhoneAppWeb.Router
模块,在
lib/phone_app_web/router.ex
中添加以下代码:
scope "/webhook", PhoneAppWeb.Webhook do
pipe_through [:api]
post "/sms", TwilioController, :sms
end
此代码将 POST 请求
/webhook/sms
路由到
PhoneAppWeb.Webhook.TwilioController.sms/3
函数。
2. 使用短信应用
使用应用的步骤如下:
1. 启动
PhoneApp
项目,使用命令
mix phx.server
。
2. 同样使用
mix phx.server
命令启动
MockServer
项目。
3. 打开浏览器的两个不同标签页,第一个标签页加载
http://localhost:4004/messages/new
,第二个标签页加载
http://localhost:4005
。
4. 在第一个标签页(
PhoneApp
)中,会看到发送新消息的表单,输入手机号码并输入消息内容。
5. 在第二个标签页(
MockServer
)中,消息会立即显示在表格中,并且有一个“回复”按钮可用于回复消息。
由于没有涉及实时功能,需要刷新
PhoneApp
页面才能看到回复消息。不过
MockServer
应用使用了 LiveView 构建,消息会立即显示,无需刷新页面。
3. HTTP 客户端总结
HTTP 客户端使用起来相对简单,但背后隐藏着很多复杂性。不同抽象级别的库可能会让人在选择时感到困惑。较低抽象级别的库(如套接字协议或池化连接)虽然可用,但通常功能不够丰富。像 Req 和 Tesla 这样功能齐全的库能为应用提供坚实的基础,选择哪个库很大程度上取决于个人偏好,这里选择了 Req。
使用 Req 进行 HTTP 请求很容易,但可能需要配置系统,告知要发送请求的 URL 域名、使用的身份验证等信息。可以将应用特定的配置存储在
config
目录中,该目录会自动加载到应用中,甚至可以设置像
dev.secret.exs
这样的秘密配置,并将其排除在版本控制之外。
Req 提供了
Req.post!/2
和
Req.get!/1
等顶级函数,方便发出 HTTP 请求。还可以使用
Req.new/1
函数将常用选项包装到基本配置中,并且如果应用有更高级的需求,还可以自定义 Req 的整个请求和响应生命周期。
4. 异步任务系统概述
异步任务是应用扩展的关键组件。在 Elixir 中,虽然本身提供了异步功能,但使用专门的库能提供更多强大的功能。这里主要介绍 Oban 库。
异步任务可以简化应用,将副作用从主代码路径中提取出来,从而在各种情况下显著提高应用性能。一个好的异步任务系统不仅能做到这一点,还能将复杂任务分解为子任务并进行编排,用更少的代码解决难题。
5. 异步任务的用例
异步任务有多种用例:
| 用例难度 | 用例描述 |
| ---- | ---- |
| 基础 | 发送请求后发送电子邮件:电子邮件通常需要向外部系统发送请求来投递,这些系统可能很慢或出现故障。在“新用户注册流程”中,可以立即在数据库中创建用户,而欢迎邮件可以在后台发送。 |
| 基础 | 通知其他系统操作:系统中发生某个操作,需要通过 API 通知另一个系统。将此请求放在异步任务中可以获得更稳定和可扩展的解决方案。 |
| 中等 | 在未来某个时间执行代码:需要在系统中某个操作发生后的几分钟或几小时后做出响应,这时需要使用异步任务来安排代码在未来执行,即定时任务。 |
| 高级 | 优化重复任务:系统中某个操作频繁发生,会导致计算缓存值的昂贵操作。可以利用唯一任务确保该操作每隔一段时间只执行一次。 |
| 高级 | 健壮的数据管道:数据管道需要一批任务完成后才能进行下一步,下一步又会产生更多任务,直到最终任务完成。这称为工作流,实现起来相当复杂,任务系统可以利用现有的抽象来编写这个复杂的数据管道。 |
6. 异步任务的筛选标准
在评估一段代码是否适合作为异步任务时,可以考虑以下问题:
1.
代码是否本身成本高昂
:发送 HTTP 请求、与 SMTP 服务器交互以及处理数据密集型请求等都有固有成本。如果在主控制器中执行此类代码,可能会阻塞连接,导致用户无法及时收到响应。可以将其提取到异步任务中,让其在后台完成。
2.
代码是否有较高的失败风险
:某些代码更容易出现间歇性故障,与操作本身无关。例如,向出现故障的服务发送 HTTP 请求会导致请求失败。如果能将此代码提取到异步任务中,系统恢复后可以重试。
3.
是否需要控制代码的并发
:Elixir 允许同时运行大量请求,这既有好处也有坏处。通常需要限制特定代码的并发量,使用异步任务系统可以完全控制并发量。
4.
代码是否需要立即执行,还是可以稍作等待
:如果代码必须作为请求的一部分立即运行,则很难将其异步化。但如果在请求之后执行是可以接受的,那么就可以将其提取到异步任务中。
5.
任务系统是否有简化开发过程的功能
:任务系统通常提供的功能不仅仅是执行工作单元,还包括唯一性、工作流管理、批处理等功能。可以利用这些功能节省大量开发时间。
7. 优秀任务系统的要求
选择任务系统时,可以考虑以下因素:
| 要求 | 描述 |
| ---- | ---- |
| 可审计性 | 需要知道任务是失败还是成功,以及其进度等信息,并且能够控制审计信息的保留时间。 |
| 模块化 | 理想情况下,不需要更新任务系统的核心代码,但在遇到不常见用例时,能够实现自己的逻辑或功能是很有帮助的。 |
| 高级功能 | 随着应用的发展,可能需要任务系统提供唯一性控制、速率限制、重试管理、定时任务和工作流生命周期等功能。 |
| 可扩展性 | 优秀的任务系统应能在不增加过多复杂性的情况下提供强大的可扩展性,在 Elixir 中意味着要巧妙利用 OTP 来实现完全的可扩展性控制。 |
| 可观测性 | 需要了解任务系统中发生的情况,可以通过将指标部署到外部日志系统进行观测,或者使用仪表盘来观察系统的当前状态。 |
| 事务支持 | 事务是使用数据库的最大好处之一,支持事务的任务系统更容易集成到系统中,并且减少错误。例如,最流行的 Ruby 任务系统 Sidekiq 基于 Redis,不支持事务。 |
8. Elixir 中异步任务的实现方式
在 Elixir 中处理异步任务有几种常见的方式:
-
不做处理
:代码直接在 HTTP 端点中运行并直接丰富数据,示例代码如下:
def create(conn, params) do
enriched = enrich_data_via_api!(params)
data = DataStore.persist!(enriched)
json(conn, %{id: data.id})
end
这种方式的优点是丰富的数据立即可用,代码简单干净。但缺点也很明显,如果丰富数据的 API 出现故障或速度慢,会导致 API 调用失败或用户体验变差,并且服务器在处理请求时会保持连接。
-
使用 Task 进程
:Elixir 的
Task
模块可以轻松生成短生命周期、特定任务的进程。示例代码如下:
def create(conn, params) do
data = DataStore.persist!(params)
Task.Supervisor.start_child(MyTaskSupervisor, fn ->
enriched = enrich_data_via_api!(params)
DataStore.update_enriched!(data, enriched)
end)
json(conn, %{id: data.id})
end
这种方式可以使基本数据立即可用,丰富的数据在丰富期后可用,用户在调用 API 时会感觉更快。但存在严重问题,应用会尽可能多地生成任务,如果 API 流量大,下游请求会很显著,并且无法控制异步代码的并行性,也没有请求的记录,无法保证代码一定会完成。
-
使用 Oban
:Oban 是一个 Elixir 任务处理库,使用 Postgres(或 SQLite3)来协调和管理任务。它使用进程隔离任务,能完全控制任务并发。示例代码如下:
def create(conn, params) do
data = DataStore.persist!(params)
EnrichWorker.enqueue!(data, params)
json(conn, %{id: data.id})
end
# Example of an Oban Worker
defmodule EnrichWorker do
use Oban.Worker
def enqueue!(data, params) do
%{id: data.id, params: params}
|> new()
|> Oban.insert!()
end
def perform(%Oban.Job{args: %{"id" => data_id, "params" => params}}) do
enriched = enrich_data_via_api!(params)
DataStore.update_enriched!(data_id, enriched)
:ok
end
end
Oban 可以自动控制并发量,对任务有完整的记录,并且能很好地处理应用崩溃。虽然代码比前两种方式稍多,但实际上是合理的。最大的缺点是每个放入队列的任务都会插入和处理一个 Postgres 行,不过除非每秒处理大量任务,否则不会有问题。总体而言,Oban 是异步任务的优秀选择。
综上所述,在构建应用时,对于短信应用的构建和异步任务的处理,需要根据具体需求和场景选择合适的方法和工具,以确保应用的性能、稳定性和可扩展性。
构建短信应用与异步任务处理
9. 异步任务实现方式对比
为了更清晰地了解在 Elixir 中处理异步任务的不同方式,下面通过一个表格来对比它们的优缺点:
| 实现方式 | 优点 | 缺点 |
| ---- | ---- | ---- |
| 不做处理 | 丰富的数据立即可用,代码简单干净 | 若 API 故障或速度慢,会导致 API 调用失败或用户体验差;服务器处理请求时保持连接 |
| 使用 Task 进程 | 基本数据立即可用,丰富数据在丰富期后可用,用户调用 API 感觉更快 | 应用会尽可能多生成任务,无法控制并行性;无请求记录,无法保证代码完成 |
| 使用 Oban | 自动控制并发量,对任务有完整记录,能很好处理应用崩溃 | 代码稍多;每个任务会插入和处理一个 Postgres 行,处理大量任务时可能有问题 |
从这个表格可以看出,不同的实现方式适用于不同的场景。如果对数据实时性要求极高且 API 稳定性好,可以选择不做处理;如果只是简单的异步需求且对并发控制和任务记录要求不高,可以考虑使用 Task 进程;而对于需要稳定并发控制和任务管理的场景,Oban 是更好的选择。
10. 异步任务决策流程
在实际应用中,如何选择合适的异步任务实现方式呢?可以参考以下 mermaid 流程图:
graph TD;
A[是否对数据实时性要求极高且 API 稳定] -->|是| B[不做处理];
A -->|否| C[是否对并发控制和任务记录要求不高];
C -->|是| D[使用 Task 进程];
C -->|否| E[使用 Oban];
这个流程图展示了一个简单的决策过程。首先判断数据实时性和 API 稳定性,如果满足要求则直接不做处理;若不满足,再判断对并发控制和任务记录的要求,根据不同情况选择使用 Task 进程或 Oban。
11. Oban 的深入理解
Oban 作为一个优秀的 Elixir 任务处理库,除了前面提到的优点外,它的一些内部机制也值得深入了解。
Oban 使用 Postgres(或 SQLite3)来存储任务信息,这使得任务的持久化和管理更加可靠。每个任务在数据库中都有对应的记录,方便进行审计和监控。同时,Oban 利用 Elixir 的进程模型,将每个任务隔离在独立的进程中执行,避免了任务之间的相互干扰。
在并发控制方面,Oban 可以通过配置来精确控制任务的并发数量。例如,可以设置某个队列中同时执行的任务数量,从而避免系统资源过度消耗。
12. 总结与建议
在构建应用时,无论是短信应用的开发还是异步任务的处理,都需要综合考虑各种因素。
对于短信应用,要注意 Twilio 的 Webhook 响应要求使用 XML 格式,在构建控制器和路由时要遵循相应的规范。同时,使用 HTTP 客户端时,要根据应用的需求选择合适的库,并合理配置系统信息。
对于异步任务,要根据任务的特点和应用的场景选择合适的实现方式。如果任务简单且对并发和记录要求不高,可以使用 Elixir 的 Task 模块;但如果需要更强大的功能,如并发控制、任务审计和重试机制等,Oban 是一个不错的选择。
建议开发者在实际项目中,先对需求进行详细分析,然后根据分析结果选择合适的技术和工具。同时,要不断学习和实践,深入理解各种技术的原理和使用方法,以提高应用的性能和稳定性。
总之,通过合理选择和使用技术,能够构建出高效、稳定且可扩展的应用,满足不同用户的需求。
超级会员免费看
1174

被折叠的 条评论
为什么被折叠?



