如何实现跨部署环境的两个Rails应用间实时双向数据推送
Rails双端实时双向同步实现方案
核心思路是将托管在公网的Rails应用作为Action Cable服务端,各餐厅本地Rails应用作为认证后的Ruby客户端接入长连接,按餐厅ID划分独立隔离频道,双向消息都走同一个WebSocket连接,完全替代轮询方案,延迟可控制在百毫秒级。
公网服务端配置(DigitalOcean部署的公共应用)
1. 连接层认证配置
先在连接层做身份校验,防止未授权接入,每个餐厅提前预分配唯一ID和匹配的API密钥,修改app/channels/application_cable/connection.rb:
module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_restaurant def connect restaurant_id = request.params[:restaurant_id] api_key = request.params[:api_key] current_restaurant = Restaurant.find_by(id: restaurant_id, api_key: api_key) reject_unauthorized_connection unless current_restaurant self.current_restaurant = current_restaurant end end end
2. 订单同步频道配置
创建专属同步频道,每个餐厅订阅时自动进入独立隔离的私有房间,修改app/channels/orders_sync_channel.rb:
class OrdersSyncChannel < ApplicationCable::Channel def subscribed # 自动按餐厅ID生成独立stream,不同餐厅消息完全隔离 stream_for current_restaurant end # 接收本地餐厅端推送上来的订单数据 def receive(data) current_restaurant.orders.create!(data["order"]) end end
3. 生产环境跨域配置
在config/environments/production.rb中添加配置,允许本地端跨域接入WebSocket:
# 生产环境可将正则替换为本地端固定出口IP段,进一步收紧权限 config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/]
4. 公网端推送逻辑
公网服务端接收到归属某餐厅的新订单时,直接向对应餐厅的私有频道广播即可:
OrdersSyncChannel.broadcast_to( target_restaurant, { action: "new_order", order: new_order.as_json(only: [:id, :items, :total, :remark, :created_at]) } )
餐厅本地端配置(作为Action Cable客户端)
Rails自带的actioncable gem原生提供Ruby客户端能力,不需要额外引入第三方依赖,也不需要写JavaScript中转。
1. 编写长连接客户端
在app/lib目录下新建同步客户端类restaurant_sync_client.rb:
require "action_cable/client" class RestaurantSyncClient def self.start # 初始化连接,认证参数从环境变量读取,不要硬编码 client = ActionCable::Client.new( url: "wss://你的公网服务域名/cable", params: { restaurant_id: ENV["LOCAL_RESTAURANT_ID"], api_key: ENV["LOCAL_RESTAURANT_API_KEY"] } ) # 断网自动重连逻辑 client.on(:disconnected) do Rails.logger.warn("公网同步连接断开,5秒后尝试重连") sleep 5 client.connect! end # 订阅订单同步频道 orders_channel = client.subscribe("OrdersSyncChannel") # 处理公网端推送的新订单 orders_channel.on(:received) do |payload| next if payload["action"] != "new_order" # 落库前先做幂等校验,避免重连后重复创建订单 next if LocalOrder.exists?(remote_id: payload["order"]["id"]) LocalOrder.create!(payload["order"]) end client.connect! # 缓存连接实例供本地发消息调用 $public_sync_channel = orders_channel end end
2. 配置开机自启
在config/initializers目录下新建初始化文件sync_client.rb,确保服务启动时自动建立连接:
# 避免执行数据库迁移、进入控制台时误启动连接 if defined?(Rails::Server) && Rails.env.production? Thread.new { RestaurantSyncClient.start } end
3. 本地端推送逻辑
本地端产生需要同步到公网的新订单时,直接调用缓存的频道实例发送即可:
$public_sync_channel.send({ order: local_order.as_json(only: [:items, :total, :remark, :created_at]) })
落地避坑提示
- 所有连接必须走
wss加密协议,不要用明文ws,API密钥和餐厅ID统一存在环境变量中,禁止硬编码到代码仓库 - 两端收消息时必须做幂等校验,通过订单唯一ID判断是否已经同步过,避免网络波动重发时生成重复订单
- 可以在重连逻辑里补一段增量拉取逻辑:重连成功后先调用公网接口拉取本地最后一次同步时间之后的所有订单,再进入实时推送模式,避免断网期间丢单
- 单餐厅单连接的资源消耗极低,哪怕有上百家餐厅同时接入,公网服务器的负载也远低于20秒轮询的方案
内容的提问来源于stack exchange,提问作者Aleksandrus
相关产品推荐
相关产品推荐

