mysql - 在 Rails 中的多个数据库之间切换而不中断事务

标签 mysql ruby-on-rails database activerecord transactions

我正在设置一个包含多个数据库的 Rails 应用程序。它使用 ActiveRecord::Base.establish_connection db_config 在数据库之间切换(所有这些都在 database.yml 中配置)。

establish_connection 显然会中断每次调用的待处理事务。一个负面后果是测试,其中 use_transactional_tests 必须被禁用(导致不受欢迎的缓慢测试)。

那么……Rails 应用程序如何同时维护不同数据库上的多个事务? (澄清一下,我不是在寻找花哨的跨数据库事务。只是数据库客户端(即 Rails 应用程序)同时维护多个事务的一种方式,每个数据库一个。)

我见过的唯一解决方案是putting establish_connection directly in the class definition ,但这假设您有一个专用于特定类的数据库。我正在应用基于用户的分片策略,其中单个记录类型分布在多个数据库中,因此需要在代码中动态切换数据库。

最佳答案

这是一个棘手的问题,因为 ActiveRecord 内部的紧密耦合,但我设法创建了一些可行的概念证明。或者至少看起来可行。

一些背景

ActiveRecord 使用 ActiveRecord::ConnectionAdapters::ConnectionHandler 类负责存储每个模型的连接池。默认情况下,所有模型只有一个连接池,因为通常的 Rails 应用程序连接到一个数据库。

在为特定模型中的不同数据库执行建立连接后,会为该模型创建新的连接池。也适用于所有可能从它继承的模型。

在执行任何查询之前,ActiveRecord首先检索相关模型的连接池,然后从池中检索连接。

请注意,上述解释可能不是 100% 准确,但应该很接近。

解决方案

所以想法是用自定义连接处理程序替换默认连接处理程序,该处理程序将根据提供的分片描述返回连接池。

这可以通过多种不同的方式实现。我通过创建将分片名称作为伪装的 ActiveRecord 类传递的代理对象来做到这一点。连接处理程序期望获得 AR 模型并查看 name 属性以及 superclass 以遍历模型的层次结构链。我已经实现了基本上是分片名称的 DatabaseModel 类,但它的行为类似于 AR 模型。

实现

这是示例实现。为了简单起见,我使用了 sqlite 数据库,您无需任何设置即可运行此文件。也可以看看this gist

# Define some required dependencies
require "bundler/inline"
gemfile(false) do
  source "https://rubygems.org"
  gem "activerecord", "~> 4.2.8"
  gem "sqlite3"
end

require "active_record"

class User < ActiveRecord::Base
end

DatabaseModel = Struct.new(:name) do
  def superclass
    ActiveRecord::Base
  end
end

# Setup database connections and create databases if not present
connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
resolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new({
  "users_shard_1" => { adapter: "sqlite3", database: "users_shard_1.sqlite3" },
  "users_shard_2" => { adapter: "sqlite3", database: "users_shard_2.sqlite3" }
})

databases = %w{users_shard_1 users_shard_2}
databases.each do |database|
  filename = "#{database}.sqlite3"

  ActiveRecord::Base.establish_connection({
    adapter: "sqlite3",
    database: filename
  })

  spec = resolver.spec(database.to_sym)
  connection_handler.establish_connection(DatabaseModel.new(database), spec)

  next if File.exists?(filename)

  ActiveRecord::Schema.define(version: 1) do
    create_table :users do |t|
      t.string :name
      t.string :email
    end
  end
end

# Create custom connection handler
class ShardHandler
  def initialize(original_handler)
    @original_handler = original_handler
  end

  def use_database(name)
    @model= DatabaseModel.new(name)
  end

  def retrieve_connection_pool(klass)
    @original_handler.retrieve_connection_pool(@model)
  end

  def retrieve_connection(klass)
    pool = retrieve_connection_pool(klass)
    raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool
    conn = pool.connection
    raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn
    puts "Using database \"#{conn.instance_variable_get("@config")[:database]}\" (##{conn.object_id})"
    conn
  end
end

User.connection_handler = ShardHandler.new(connection_handler)

User.connection_handler.use_database("users_shard_1")
User.create(name: "John Doe", email: "john.doe@example.org")
puts User.count

User.connection_handler.use_database("users_shard_2")
User.create(name: "Jane Doe", email: "jane.doe@example.org")
puts User.count

User.connection_handler.use_database("users_shard_1")
puts User.count

我认为这应该让您了解如何实现生产就绪解决方案。我希望我没有在这里错过任何明显的东西。我可以建议几种不同的方法:

  1. 子类 ActiveRecord::ConnectionAdapters::ConnectionHandler 并覆盖那些负责检索连接池的方法
  2. 创建与 ConnectionHandler
  3. 实现相同 api 的全新类
  4. 我想也可以只覆盖 retrieve_connection 方法。我不记得它是在哪里定义的,但我认为它在 ActiveRecord::Core 中。

我认为方法 1 和 2 是可行的方法,并且应该涵盖使用数据库时的所有情况。

关于mysql - 在 Rails 中的多个数据库之间切换而不中断事务,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43691840/

相关文章:

ruby-on-rails - pg (0.12.1) 没有安装在 Heroku 上

ruby-on-rails - 将 vim 与 ruby​​/ruby on rails 结合使用的提示和技巧

ruby-on-rails - PG::ConnectionBad: FATAL: 用户密码验证失败

ruby-on-rails - 将 nokogiri (或任何内部)对象保存到数据库是个好主意吗?

java - 如何更好地将丰富的字符串内容和图像保存到数据库中?

c++ - 如何在可扩展存储引擎(JetBlue)中的 JetUpdate() 之后获取 AutoIncrement 值

PHP session 处理

java - 如何从数据库中的多个表中获取公共(public)列?

php - AVG 和 COUNT 函数在 SQL 查询中没有给出正确的结果

创建 View 时mysql union all