You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于ActiveRecord实现读写操作的数据库角色自动切换?

可行解决方案

方案1:全局捕获ReadOnlyError并自动切换主库重试

利用Rails的异常处理机制,在全局控制器层捕获ActiveRecord::ReadOnlyError,切换主库后重新执行当前请求逻辑。

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::ReadOnlyError, with: :switch_to_writer_and_retry

  private

  def switch_to_writer_and_retry
    ActiveRecord::Base.connected_to(role: :writing) do
      self.response = process(action_name, request.method_parameters)
    end
  end
end

注意:仅适用于幂等请求,避免重复执行写操作导致数据重复。非幂等场景需额外做请求唯一标识校验。

方案2:主动标记需要写操作的请求

在明确包含写逻辑的控制器action中,手动包裹主库连接块,提前切换连接避免异常。

class TokensController < ApplicationController
  def consume
    ActiveRecord::Base.connected_to(role: :writing) do
      token = Token.find(params[:id])
      token.update!(consumed: true)
      render json: { status: 'success' }
    end
  end
end

可封装成复用helper:

module ConnectionHelpers
  def with_writing
    ActiveRecord::Base.connected_to(role: :writing) { yield }
  end
end

# 控制器中引入后使用
class TokensController < ApplicationController
  include ConnectionHelpers

  def consume
    with_writing do
      # 写操作逻辑
    end
  end
end

方案3:Hook写操作方法自动切换主库

通过Monkey Patch覆盖ActiveRecord核心写操作方法,执行前检查当前连接角色,若为从库则自动切换主库。

# config/initializers/ar_connection_switch.rb
module ActiveRecord
  class Base
    class << self
      alias_method :orig_update, :update
      alias_method :orig_create, :create
      alias_method :orig_destroy, :destroy

      def update(attributes)
        switch_to_writer_if_needed { orig_update(attributes) }
      end

      def create(attributes = {})
        switch_to_writer_if_needed { orig_create(attributes) }
      end

      def destroy(id)
        switch_to_writer_if_needed { orig_destroy(id) }
      end
    end

    def update(attributes)
      self.class.switch_to_writer_if_needed { super }
    end

    def update!(attributes)
      self.class.switch_to_writer_if_needed { super }
    end

    def destroy
      self.class.switch_to_writer_if_needed { super }
    end

    private

    def self.switch_to_writer_if_needed
      if connected_to?(role: :reading)
        connected_to(role: :writing) { yield }
      else
        yield
      end
    end
  end
end

注意:需补充覆盖save、save!、delete等其他写操作方法;升级Rails版本时需检查兼容性。

方案4:自定义连接Resolver提前预判

扩展内置ConnectionResolver,在分配连接前根据请求特征(控制器、action、参数等)判断是否需要主库连接。

# config/initializers/custom_resolver.rb
class CustomConnectionResolver < ActiveRecord::Middleware::DatabaseSelector::Resolver
  def resolve(request)
    needs_writing?(request) ? :writing : super
  end

  private

  def needs_writing?(request)
    # 自定义判断规则,示例:指定控制器action或带特定参数的请求
    (request.controller_class == TokensController && request.action_name == 'consume') ||
      request.params[:has_write] == '1'
  end
end

# 修改database.yml配置
development:
  primary:
    database: my_app_dev
    adapter: postgresql
  primary_replica:
    database: my_app_dev
    adapter: postgresql
    replica: true
  database_selector:
    delay: 2.seconds
    resolver: CustomConnectionResolver
    resolver_context: :request

方案对比

  • 方案1:快速落地,但依赖异常处理,非幂等请求有风险
  • 方案2:可控性强,代码可读性高,适合明确写操作场景
  • 方案3:自动化程度高,无需修改业务代码,但需维护Patch,升级需注意兼容
  • 方案4:提前规避异常,但规则需随业务迭代维护,复杂场景下易臃肿

内容的提问来源于stack exchange,提问作者elcuervo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 12:21:03