如何基于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
相关产品推荐
相关产品推荐

