Kann ich mich für mein eigenes Projekt bezüglich der Unterstützung mehrerer aus Rails 6.0 implementierter DBs bewerben? Ich werde die Umsetzung verfolgen, um das zu beurteilen
https://railsguides.jp/active_record_multiple_databases.html#コネクションの自動切り替えを有効にする
Mit der Auto-Switch-Funktion kann die Anwendung je nach HTTP-Verb und Vorhandensein oder Nichtvorhandensein des letzten Schreibvorgangs von primär zu Replikat oder von Replikat zu primär wechseln. Wenn eine Anwendung eine POST-, PUT-, DELETE- oder PATCH-Anforderung empfängt, schreibt sie automatisch in die primäre Anwendung. Die Anwendung liest von der primären bis zum angegebenen Zeitpunkt nach dem Schreiben. Wenn die Anwendung eine GET-Anforderung oder eine HEAD-Anforderung empfängt, liest sie aus dem Replikat, wenn kein aktueller Schreibvorgang vorhanden ist.
Es wird angegeben, dass es durch zwei Arten von Beurteilungen durchgeführt wird, Beurteilung durch HTTP-Methode + Beurteilung durch Zeit vom Schreiben. Die Einstellmethode wird unten beschrieben
config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
Beginnen wir mit dem Anwendungsteil der Middleware
activerecord-6.0.3.2/lib/active_record/railtie.rb
class Railtie < Rails::Railtie # :nodoc:
config.active_record = ActiveSupport::OrderedOptions.new
initializer "active_record.database_selector" do
if options = config.active_record.delete(:database_selector)
resolver = config.active_record.delete(:database_resolver)
operations = config.active_record.delete(:database_resolver_context)
config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations, options
end
end
database_selector
gesetzt ist
--database_resolver
und database_resolver_context
werden jetzt gerade übergebenAls nächstes werden wir die Implementierung des Middleware-Teils verfolgen
https://api.rubyonrails.org/classes/ActiveSupport/OrderedOptions.html
Ein Mechanismus, mit dem auf Hash in Punkten zugegriffen werden kann Gibt nil zurück, wenn nicht mit delete gesetzt
Ruby2.6.5 pry(main)> x = ActiveSupport::OrderedOptions.new
=> {}
Ruby2.6.5 pry(main)> x.delete(:hoge)
=> nil
activerecord-6.0.3.2/lib/active_record/middleware/database_selector.rb
module ActiveRecord
module Middleware
class DatabaseSelector
def initialize(app, resolver_klass = nil, context_klass = nil, options = {})
@app = app
@resolver_klass = resolver_klass || Resolver
@context_klass = context_klass || Resolver::Session
@options = options
end
attr_reader :resolver_klass, :context_klass, :options
def call(env)
request = ActionDispatch::Request.new(env)
select_database(request) do
@app.call(env)
end
end
database_resolver
nicht gesetzt ist, wird DatabaseSelector :: Resolver
gesetzt.database_resolver_context
nicht gesetzt ist, wird DatabaseSelector :: Resolver :: Session
gesetzt.Schauen wir uns als nächstes die wichtige # select_database
an
DatabaseSelector#select_database
module ActiveRecord
module Middleware
class DatabaseSelector
private
def select_database(request, &blk)
context = context_klass.call(request)
resolver = resolver_klass.call(context, options)
if reading_request?(request)
resolver.read(&blk)
else
resolver.write(&blk)
end
end
def reading_request?(request)
request.get? || request.head?
end
end
end
end
Da "Kontext" zuerst ausgeführt wird und "Resolver" darauf basierend erstellt wird, überprüfen wir zuerst "Kontext".
Resolver::Session.call
class Resolver # :nodoc:
class Session # :nodoc:
def self.call(request)
new(request.session)
end
def initialize(session)
@session = session
end
attr_reader :session
.call
ist nur # new
, hier wird nur die Sitzung von ActionDispatch :: Request
gesetzt
Überprüfen Sie als nächstes Resolvers .call
Resolver.call
class Resolver # :nodoc:
SEND_TO_REPLICA_DELAY = 2.seconds
def self.call(context, options = {})
new(context, options)
end
def initialize(context, options = {})
@context = context
@options = options
@delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY
@instrumenter = ActiveSupport::Notifications.instrumenter
end
attr_reader :context, :delay, :instrumenter
Ich bin auch hier nur neu, also werde ich die ursprüngliche DatabaseSelector # select_database
ein wenig mehr lesen.
def select_database(request, &blk)
context = context_klass.call(request)
resolver = resolver_klass.call(context, options)
if reading_request?(request)
resolver.read(&blk)
else
resolver.write(&blk)
end
end
def reading_request?(request)
request.get? || request.head?
end
Hier für GET oder HEAD => lesen Ansonsten => schreiben Führt die Resolver-Verarbeitung aus als (Resolver scheint weitere Entscheidungen zu treffen)
Beginnen wir mit dem Lesen
Resolver#read
class Resolver # :nodoc:
def read(&blk)
if read_from_primary?
read_from_primary(&blk)
else
read_from_replica(&blk)
end
end
private
def read_from_primary(&blk)
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role, prevent_writes: true) do
instrumenter.instrument("database_selector.active_record.read_from_primary") do
yield
end
end
end
def read_from_replica(&blk)
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role, prevent_writes: true) do
instrumenter.instrument("database_selector.active_record.read_from_replica") do
yield
end
end
end
Es scheint, dass der Prozess fortgesetzt wird, nachdem dem Instrument mitgeteilt wurde, welche Rolle verwendet werden soll. Was wird dann nach "# read_from_primary" beurteilt?
private
def read_from_primary?
!time_since_last_write_ok?
end
def send_to_replica_delay
delay
end
def time_since_last_write_ok?
Time.now - context.last_write_timestamp >= send_to_replica_delay
end
context
Die sogenannte session # last_write_timestamp
ist
Wenn die erste config.active_record.database_selector = {delay: 2.seconds}
Verzögerung nicht verstrichen ist, setzen Sie sie auf primary.
Wenn die Verzögerung abgelaufen ist, wird das Replikat angezeigt.
Überprüfen Sie also den # last_write_timestamp der Sitzung
class Session # :nodoc:
# Converts milliseconds since epoch timestamp into a time object.
def self.convert_timestamp_to_time(timestamp)
timestamp ? Time.at(timestamp / 1000, (timestamp % 1000) * 1000) : Time.at(0)
end
def last_write_timestamp
self.class.convert_timestamp_to_time(session[:last_write])
end
Ändern Sie einfach sesion [: last_write]
in ein Zeitobjekt
Dies ist das Ende des Lesens, also schreiben Sie
Resolver#write
class Resolver # :nodoc:
def write(&blk)
write_to_primary(&blk)
end
private
def write_to_primary(&blk)
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role, prevent_writes: false) do
instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
yield
ensure
context.update_last_write_timestamp
end
end
end
Dies ist nur das Schreiben in der Schreibrolle
class Session # :nodoc:
def self.convert_time_to_timestamp(time)
time.to_i * 1000 + time.usec / 1000
end
def update_last_write_timestamp
session[:last_write] = self.class.convert_time_to_timestamp(Time.now)
end
Und geben Sie den aktuellen Zeitstempel für "session [: last_write]" an
Wie erwartet ist es Rails. Es wurde sehr leicht verständlich gemacht.
Schließlich wird es nur basierend auf ActionDispatch :: Request verarbeitet, und wenn Sie sowohl "# read" als auch "# write" von Resolver neu schreiben Es schien, als könnte ich schreiben, was ich wollte, nicht nur GET und HEAD.
Die Beurteilung nach Header, IP-Adresse, Localhost usw. kann ebenfalls verwendet werden, da es sich um die Informationen handelt, über die die Anforderung verfügt. Ich dachte, ich könnte viel tun.
Recommended Posts