Κοινή χρήση τεχνολογίας

[ruby on rails] Λόγοι και λύσεις για το ActiveRecord::PreparedStatementCacheΕληγμένα σφάλματα κατά την ανάπτυξη

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

1. Ερώτηση:

  • Μερικές φορές κατά την ανάπτυξη μιας εφαρμογής Rails στο Postgres, ενδέχεται να δείτε ένα σφάλμα ActiveRecord::PreparedStatementCacheExpired. Αυτό συμβαίνει μόνο όταν εκτελούνται μετεγκαταστάσεις σε μια ανάπτυξη.
  • Αυτό συμβαίνει επειδή το Rails εκμεταλλεύεται τη δυνατότητα προετοιμασίας της προσωρινής μνήμης δηλώσεων (PreparedStatementCache) της Postgres για να βελτιώσει την απόδοση. Αυτή η δυνατότητα είναι ενεργοποιημένη από προεπιλογή στις ράγες.

2. Υποτροπή του προβλήματος:

  • Μπορούμε να χρησιμοποιήσουμε το rspec για να αναπαράγουμε αυτό το σφάλμα
 it 'not raise ActiveRecord::PreparedStatementCacheExpired' do
    create(:user)
    User.first
    User.find_by_sql('ALTER TABLE users ADD new_metric_column integer;')
    ActiveRecord::Base.transaction { User.first }
  end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Εισαγάγετε την περιγραφή της εικόνας εδώ

3. Αρχή παραγωγής:

  • Rails ερωτήματα δηλώσεων όπωςUser.allαπό active_record Ανάλυση σε δήλωση sqlΜετά από αυτό, στείλτε το στη βάση δεδομένων,Εκτελέστε πρώτα το PREPARE Οι προετοιμασμένες δηλώσεις, οι εντολές SQL θα αναλυθούν, θα αναλυθούν, θα βελτιστοποιηθούν και θα ξαναγραφτούν.Όταν η παρακολούθησηΕκδώστε μια εντολή EXECUTE, η ετοιμασμένη δήλωση θα προγραμματιστεί και θα εκτελεστεί.
  • Το Rails θα αποθηκεύσει τη δήλωση ερωτήματοςpg_prepared_statementsγια να διευκολύνετε την επόμενη φορά που θα καλέσετε παρόμοιες δηλώσεις.εκτελέσει απευθείας δηλώσεις, αντί για ανάλυση, ανάλυση και βελτιστοποίηση, αποφεύγοντας την επικάλυψη της εργασίας και βελτιώνοντας την αποτελεσματικότητα.
User.first
User.all
# 执行上面的2个查询后,用connection.instance_variable_get(:@statements)就可以看到缓存的准备语句
ActiveRecord::Base.connection.instance_variable_get(:@statements)
==> <ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::StatementPool:0x00000001086b13c8 
@cache={78368=>{""$user", public-SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 
$1"=>"a7", ""$user", public-SELECT "users".* FROM "users" /* loading for inspect */ LIMIT $1"=>"a8"}},
@statement_limit=1000, @connection=#<PG::Connection:0x00000001086b31a0>, @counter=8>

# 这个也可以看到,会在数据库中去查询
ActiveRecord::Base.connection.execute('select * from pg_prepared_statements').values
(0.5ms) select * from pg_prepared_statements
==> [["a7", "SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1", "2024-07-
11T07:03:06.891+00:00", "{bigint}", false], ["a8", "SELECT "users".* FROM "users" /* loading for inspect 
*/ LIMIT $1", "2024-07-11T07:04:47.772+00:00", "{bigint}", false]]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • Στο Postgres, η προσωρινή μνήμη των προετοιμασμένων δηλώσεων θα ακυρωθεί εάν αλλάξει το σχήμα του πίνακα που επηρεάζει τα αποτελέσματα που επιστρέφονται. Συγκεκριμένα, είναι λειτουργίες DDL όπως η προσθήκη και η διαγραφή πεδίων στον πίνακα ή η τροποποίηση του τύπου και του μήκους των πεδίων.

Όπως στο ακόλουθο παράδειγμα, κατά την εκτέλεση του SELECT μετά την προσθήκη ή τη διαγραφή πεδίων,pg βάσης δεδομένωνθα ρίξειcached plan must not change result type, σε ράγεςενεργή_εγγραφήΛάβετε αυτό το σφάλμα και μετά ρίχνετεActiveRecord::PreparedStatementCacheExpired

ALTER TABLE users ADD COLUMN new_column integer;
ALTER TABLE users DROP COLUMN old_column;
添加或删除列,然后执行 SELECT *
删除 old_column 列然后执行 SELECT users.old_column
  • 1
  • 2
  • 3
  • 4
  • Κατά την εκτέλεση της μετεγκατάστασης της προσθήκης, αφαίρεσης ή τροποποίησης πεδίων στην υπηρεσία ανάπτυξης, η δήλωση ερωτήματος που εκδίδεται από τον χρήστη θα ανακτήσει απευθείας την SQL από την έτοιμη προσωρινή μνήμη και θα εκτελέσει απευθείας την εκτέλεση, ωστόσο, επειδή η δομή του πίνακα αλλάζει αυτήν τη στιγμή. η έτοιμη προσωρινή μνήμη δηλώσεων θα καταστεί άκυρη.pg βάσης δεδομένωνθα ρίξειcached plan must not change result typeλάθος
  • Προβολή πηγαίου κώδικα active_recordexec_cacheΜέθοδος, βρήκα ότι η μέθοδος χειρισμού σφαλμάτων των σιδηροτροχιών για το pg είναι:
    1. Στη συναλλαγή, θα πεταχτεί απευθείας raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
    2. Εκείνοι εκτός της συναλλαγής θα αποθηκεύσουν στην κρυφή μνήμη @statementsΔιαγράψτε αυτήν την πρόταση και δοκιμάστε, μετά από επανάληψη, η πρόταση sql θα αναλυθεί εκ νέου, θα αναλυθεί, θα βελτιστοποιηθεί και θα εκτελεστεί.prepare_statementμέθοδος στην έτοιμη προσωρινή μνήμη δηλώσεων
module ActiveRecord
  module ConnectionHandling
    def exec_cache(sql, name, binds)
      materialize_transactions
      mark_transaction_written_if_write(sql)
      update_typemap_for_default_timezone

      stmt_key = prepare_statement(sql, binds)
      type_casted_binds = type_casted_binds(binds)

      log(sql, name, binds, type_casted_binds, stmt_key) do
        ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
          @connection.exec_prepared(stmt_key, type_casted_binds)
        end
      end
    rescue ActiveRecord::StatementInvalid => e
      raise unless is_cached_plan_failure?(e)

      # Nothing we can do if we are in a transaction because all commands
      # will raise InFailedSQLTransaction
      if in_transaction?
        raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
      else
        @lock.synchronize do
          # outside of transactions we can simply flush this query and retry
          @statements.delete sql_key(sql)
        end
        retry
      end
    end
  end
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • Επομένως, αυτό το σφάλμα που εμφανίζεται στη συναλλαγή θα προκαλέσει την επαναφορά της συναλλαγής Για την επιχείρηση, σημαίνει ότι το αίτημα απέτυχε και πρέπει να το χειριστούμε μόνοι μας.

4. Λύση:

1. Απενεργοποιήστε τη συνάρτηση δήλωσης προετοιμασμένης προσωρινής μνήμης (δεν συνιστάται)

Το Rails6 και παραπάνω μπορούν να απενεργοποιήσουν αυτήν τη δυνατότητα ορίζοντας τις προετοιμασίες_διατάξεις στη βάση δεδομένων σε false.

default: &default
  adapter: postgresql
  encoding: unicode
  prepared_statements: false
  • 1
  • 2
  • 3
  • 4

Τα Rails6 και κάτω δεν έχουν δοκιμαστεί εάν τα παραπάνω δεν λειτουργούν, μπορείτε να δοκιμάσετε να δημιουργήσετε ένα νέο αρχείο προετοιμασίας.

# config/initializers/disable_prepared_statements.rb:
db_configuration = ActiveRecord::Base.configurations[Rails.env]
db_configuration.merge!('prepared_statements' => false)
ActiveRecord::Base.establish_connection(db_configuration)
  • 1
  • 2
  • 3
  • 4

επαληθεύω:

User.all
ActiveRecord::Base.connection.execute('select * from pg_prepared_statements').values
==> []
  • 1
  • 2
  • 3

Συμπέρασμα: Σε μικρά έργα, δεν έχει σημασία αν απενεργοποιήσετε αυτήν τη λειτουργία και η απόδοση θα παραμείνει σχεδόν ανεπηρέαστη, ωστόσο, σε μεγάλα έργα, όσο περισσότερους χρήστες και όσο πιο σύνθετες δηλώσεις ερωτημάτων, τόσο μεγαλύτερα θα είναι τα οφέλη αυτής της λειτουργίας. ώστε να μπορείτε να αποφασίσετε εάν θα το απενεργοποιήσετε σύμφωνα με την πραγματική κατάσταση.

2. να φτιάξωselect * γίνομαιselect id, nameΤέτοια συγκεκριμένα πεδία, σε ράγες7Επίσημη λύσηΑυτό είναι

  • Ορίστε το enumerate_columns_in_select_statements σε true στο rails7
# config/application.rb
module MyApp
  class Application < Rails::Application
    config.active_record.enumerate_columns_in_select_statements = true
  end
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • Δεν υπάρχει τέτοια διαμόρφωση κάτω από το rails7, μπορείτε να χρησιμοποιήσετε ignored_columns για να το επιτύχετε
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  #__fake_column__是自定义的,不要是某个表中的字段就行,如果是[:id],那么 User.all就会被解析为select name from users,没有id了
  self.ignored_columns = [:__fake_column__] 
end
  • 1
  • 2
  • 3
  • 4
  • 5

Συμπέρασμα: Το πρόβλημα με αυτήν τη λύση είναι ότι η προσθήκη πεδίων μπορεί να λυθεί τέλεια, αλλά η διαγραφή πεδίων θα εξακολουθεί να προκαλεί σφάλματα, για παράδειγμα, μετά τη διαγραφή του πεδίου ονόματος, εάν το όνομα στην προετοιμασμένη δήλωση επιλέξτε το αναγνωριστικό, το όνομα από τους χρήστες δεν υπάρχει. θα αναφερθεί ένα σφάλμα Rails7 επίσημη λύση έχει επίσης αυτό το πρόβλημα

3. Επανεκκινήστε την εφαρμογή ράγες

  • Ο κύκλος ζωής της προσωρινής μνήμης των προετοιμασμένων δηλώσεων υπάρχει μόνο σε μία περίοδο λειτουργίας βάσης δεδομένων (η επανεκκίνηση της εφαρμογής θα κλείσει την αρχική σύνδεση και θα αποκαταστήσει μια νέα σύνδεση), η αρχική έτοιμη προσωρινή μνήμη θα διαγραφεί και Το αίτημα SQL μετά την επανεκκίνηση θα επανεκκινήσει την προετοιμασμένη δήλωση και μπορείτε να λάβετε τα δεδομένα κανονικά.

Συμπέρασμα: Η επανεκκίνηση της εφαρμογής θα προκαλέσει μια προσωρινή υπηρεσία 502 Μη διαθέσιμη. Φυσικά, κατά την ανάπτυξη της εφαρμογής, πρέπει επίσης να επανεκκινήσετε την υπηρεσία και θα εμφανιστεί επίσης το 502, επομένως είναι καλύτερο να το αναπτύξετε όταν κανείς δεν έχει πρόσβαση σε αυτήν (στην μέσα στη νύχτα;), ώστε να υπάρχουν όσο το δυνατόν λιγότερα λάθη ΕμφανίζονταιPreparedStatementCacheExpiredΑναφέρετε ένα σφάλμα

4. Ξαναγράψτε transaction μέθοδος

class ApplicationRecord < ActiveRecord::Base
  class << self
    def transaction(*args, &block)
      retried ||= false
      super
    rescue ActiveRecord::PreparedStatementCacheExpired
      if retried
        raise
      else
        retried = true
        retry
      end
    end
  end
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • Μετά την επανεγγραφή, το μέρος όπου εγγράφονται οι συναλλαγές στον κώδικα αλλάζει σε χρήση ApplicationRecord.transaction do ... end ήMyModel.transaction

Συμπέρασμα: Σημαντική σημείωση: Εάν στέλνετε μηνύματα ηλεκτρονικού ταχυδρομείου, δημοσιεύετε σε API ή πραγματοποιείτε άλλες λειτουργίες που αλληλεπιδρούν με τον έξω κόσμο ως μέρος μιας συναλλαγής, αυτό μπορεί να προκαλέσει μερικές από αυτές τις λειτουργίες να πραγματοποιούνται περιστασιακά δύο φορές. Αυτός είναι ο λόγος που το Rails επίσημα δεν εκτελεί αυτόματα επαναλήψεις, αλλά το αφήνει στους προγραμματιστές εφαρμογών.

&gt;&gt;&gt;&gt;&gt;&gt;&gt;Όταν δοκιμάζω μόνος μου αυτήν τη μέθοδο, εξακολουθώ να λαμβάνω σφάλματα.

5. Διαγράψτε με μη αυτόματο τρόπο την έτοιμη προσωρινή μνήμη των δηλώσεων

 ActiveRecord::Base.connection.clear_cache!
  • 1

5. Τελική απάντηση

Δεν βρέθηκε τέλεια λύση