异步SOAP响应等待优化:避免系统阻塞的最佳实践问询
异步SOAP请求响应的最优解决方案
针对你当前用轮询数据库导致高并发下进程阻塞的问题,结合Ruby/Rails生态,这里给出几个落地性强的解决方案,按性能和推荐度排序:
1. 外部SOAP系统回调 + Sidekiq处理(最优)
如果外部SOAP服务支持回调,这是彻底解决轮询问题的最佳方式,完全避免资源浪费。
具体做法:
- 发起SOAP请求时,把你的回调URL(比如
https://your-domain.com/soap_callback)和correlation ID一起传给外部系统 - 外部系统处理完成后,会主动调用这个回调URL,携带correlation ID和响应内容
- 后端回调接口验证请求合法性后,把响应存入数据库,还可以用Sidekiq触发后续通知逻辑(比如推送给前端)
代码示例:
# 发起SOAP请求,附带回调信息 def soap_request(request) client = Savon.client(wsdl: 'http://external-soap-service.com/wsdl') client.call(:submit_request, message: { request_data: request.data, correlation_id: request.correlation, callback_url: 'https://your-domain.com/soap_callback' }) end # 回调接收接口 def soap_callback # 这里要加签名验证,防止恶意请求 correlation_id = params[:correlation_id] response_content = params[:response_content] Income.create!(ident: correlation_id, content: response_content) # 用Sidekiq推送给前端(比如ActionCable) SoapResultNotifierWorker.perform_async(correlation_id, response_content) render plain: 'OK' end # Sidekiq Worker:推送结果到前端 class SoapResultNotifierWorker include Sidekiq::Worker def perform(correlation_id, content) parsed_data = Nokogiri::XML(content).to_hash ActionCable.server.broadcast("soap_result_#{correlation_id}", { status: 'success', data: parsed_data }) end end
优缺点:
- ✅ 完全消除轮询,资源占用极低,高并发场景下表现最好
- ❌ 依赖外部SOAP系统支持回调,需要额外做请求合法性验证(比如签名)
2. Server-Sent Events (SSE) 实时推送(次优)
如果外部系统不支持回调,用SSE让后端主动推送结果,替代前端高频轮询,减少请求数。
具体做法:
- 前端发起请求后,后端立即触发SOAP请求,同时和前端建立SSE长连接
- 后端在SSE连接中轮询数据库(或用Sidekiq异步监听),拿到结果或超时后推送给前端
代码示例(Rails + ActionController::Live):
class SoapRequestsController < ApplicationController include ActionController::Live def stream_result response.headers['Content-Type'] = 'text/event-stream' sse = SSE.new(response.stream, retry: 3000) correlation_id = params[:correlation_id] timeout_at = 180.seconds.from_now begin loop do income = Income.find_by(ident: correlation_id) if income&.content.present? parsed_data = Nokogiri::XML(income.content).to_hash sse.write({ status: 'success', data: parsed_data }, event: 'result') break elsif Time.now >= timeout_at sse.write({ status: 'timeout' }, event: 'error') break end sleep(1) # 1秒查一次,比原0.5秒更友好 end rescue IOError # 前端断开连接,清理资源 ensure sse.close end end end
前端JS:
const eventSource = new EventSource(`/soap/stream_result?correlation_id=${correlationId}`); eventSource.addEventListener('result', (event) => { const data = JSON.parse(event.data); // 处理返回结果 eventSource.close(); }); eventSource.addEventListener('error', (event) => { alert('请求超时或出错'); eventSource.close(); });
优缺点:
- ✅ 后端主动推送,请求数少,实时性好
- ❌ 长连接会占用服务器连接数,高并发下需要调整服务器配置(比如Nginx的
worker_connections)
3. 前端轮询 + 后端状态接口(快速落地)
这是最容易实现的方案,把原来后端的轮询转移到前端,释放web进程。
具体做法:
- 后端接收请求后,立即发起SOAP请求,返回correlation ID和超时时间给前端
- 前端用这个ID定期调用后端的状态查询接口,直到拿到结果或超时
代码示例:
# 初始请求接口:触发SOAP请求并返回correlation ID def initiate_request request = build_soap_request(params) soap_request(request) # 建议把请求发起时间存在Redis或单独表中,用于后续超时判断 Redis.current.setex("soap_request_start:#{request.correlation}", 180, Time.now.to_i) render json: { correlation_id: request.correlation, timeout: 180 } end # 状态查询接口:返回当前请求状态 def check_status correlation_id = params[:correlation_id] start_time = Redis.current.get("soap_request_start:#{correlation_id}")&.to_i income = Income.find_by(ident: correlation_id) if income&.content.present? parsed_data = Nokogiri::XML(income.content).to_hash render json: { status: 'success', data: parsed_data } elsif start_time && Time.now.to_i - start_time > 180 render json: { status: 'timeout' }, status: 408 else render json: { status: 'pending' } end end
前端JS:
async function getSoapResult(correlationId, totalTimeout) { const startTime = Date.now(); while (Date.now() - startTime < totalTimeout * 1000) { const res = await fetch(`/soap/check_status?correlation_id=${correlationId}`); const data = await res.json(); if (data.status === 'success') return data.data; if (data.status === 'timeout') throw new Error('请求超时'); await new Promise(resolve => setTimeout(resolve, 1000)); // 1秒轮询一次 } throw new Error('请求超时'); }
优缺点:
- ✅ 实现简单,无需额外依赖,兼容所有浏览器
- ❌ 会产生较多HTTP请求,适合响应时间较短的场景
4. Sidekiq + Redis 临时存储(折中方案)
如果一定要用Sidekiq处理等待逻辑,可以结合Redis做临时结果存储,避免阻塞web进程。
具体做法:
- 后端发起SOAP请求后,启动Sidekiq Worker轮询数据库
- Worker拿到结果后,把结果存入Redis并设置过期时间
- 后端初始接口做短时间轮询尝试拿结果,没拿到就返回correlation ID让前端后续查询
代码示例:
# 初始请求接口 def initiate_request request = build_soap_request(params) soap_request(request) WaitForSoapResultWorker.perform_async(request.correlation) # 短轮询尝试拿结果,避免直接阻塞 5.times do result = Redis.current.get("soap_result:#{request.correlation}") if result if result == 'timeout' render json: { status: 'timeout' }, status: 408 else render json: { data: Nokogiri::XML(result).to_hash } end return end sleep(0.5) end # 短轮询没拿到,返回correlation ID让前端自己查 render json: { correlation_id: request.correlation } end # Sidekiq Worker:轮询数据库并缓存结果到Redis class WaitForSoapResultWorker include Sidekiq::Worker sidekiq_options retry: false, timeout: 180 def perform(correlation_id) timeout_at = 180.seconds.from_now loop do income = Income.find_by(ident: correlation_id) if income&.content.present? Redis.current.setex("soap_result:#{correlation_id}", 180, income.content) break elsif Time.now >= timeout_at Redis.current.setex("soap_result:#{correlation_id}", 180, 'timeout') break end sleep(1) end end end
优缺点:
- ✅ 把轮询逻辑放到Sidekiq进程,不占用web进程
- ❌ 还是存在轮询,只是转移了位置
总结
- 优先选回调方案,性能最优;
- 不支持回调的话,高并发场景选SSE;
- 快速上线选前端轮询;
- 非要用Sidekiq的话,选Redis缓存折中方案。
内容的提问来源于stack exchange,提问作者Archer
相关产品推荐
相关产品推荐

