Teknologian jakaminen

[ruby on rails] Syitä ja ratkaisuja ActiveRecord::PreparedStatementCacheExpired-virheisiin käyttöönoton aikana

2024-07-12

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

1. Kysymys:

  • Joskus kun otat Rails-sovelluksia käyttöön Postgresissa, saatat nähdä ActiveRecord::PreparedStatementCacheExpired-virheitä. Tämä tapahtuu vain, kun siirrot suoritetaan käyttöönotossa.
  • Tämä tapahtuu, koska Rails hyödyntää Postgresin PreparedStatementCache-ominaisuutta parantaakseen suorituskykyä. Tämä ominaisuus on oletuksena käytössä kiskoilla.

2. Ongelman toistuminen:

  • Voimme käyttää rspec-komentoa tämän virheen toistamiseen
 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

Lisää kuvan kuvaus tähän

3. Tuotantoperiaate:

  • Rails-kyselylausekkeet, kutenUser.allaktiivisen_tietueen mukaan Jäsennä sql-lauseeseenSen jälkeen lähetä se tietokantaan,Suorita ensin PREPARE Valmistetut lausunnot, SQL-lauseet jäsennetään, analysoidaan, optimoidaan ja kirjoitetaan uudelleen.Kun seurantaAnna EXECUTE-komento, laadittu lausunto suunnitellaan ja toteutetaan.
  • Rails tallentaa kyselylauseen kohteeseenpg_prepared_statementshelpottaaksesi seuraavan kerran, kun soitat vastaaviin lausuntoihin.suorittaa suoraan lausunnot jäsentämisen, analysoinnin ja optimoinnin sijaan, välttäen päällekkäistä työtä ja parantamalla tehokkuutta.
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
  • Postgresissa valmistettu käskyvälimuisti mitätöidään, jos taulukon skeema muuttuu, mikä vaikuttaa palautettuihin tuloksiin. Tarkemmin sanottuna kyseessä ovat DDL-toiminnot, kuten kenttien lisääminen ja poistaminen taulukkoon tai kenttien tyypin ja pituuden muuttaminen.

Kuten seuraavassa esimerkissä, kun SELECT suoritetaan kenttien lisäämisen tai poistamisen jälkeen,pg tietokantaheittääcached plan must not change result type, kiskoissaaktiivinen_tietueHanki tämä virhe ja heitä sittenActiveRecord::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
  • Kun kenttä lisätään, vähennetään tai muokataan käyttöönottopalvelussa, käyttäjän antama kyselylause hakee SQL:n suoraan valmiista käskyvälimuistista ja suorittaa suorituksen. Koska taulukon rakenne kuitenkin muuttuu tällä hetkellä, valmistettu lausekevälimuisti ei kelpaa.pg tietokantaheittääcached plan must not change result typevirhe
  • Näytä active_record lähdekoodiexec_cacheMenetelmä, huomasin, että pg:n kiskojen virheenkäsittelymenetelmä on:
    1. Kaupassa se heitetään suoraan raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
    2. Tapahtuman ulkopuoliset tallentavat @lausunnot välimuistiinPoista tämä lause ja yritäUudelleen yrittämisen jälkeen sql-lause jäsennetään, analysoidaan, optimoidaan ja suoritetaan.prepare_statementmenetelmä valmisteltuun lausekkeen välimuistiin
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
  • Siksi tämä tapahtumassa näkyvä virhe aiheuttaa tapahtuman peruutuksen. Yrityksen kannalta se tarkoittaa, että pyyntö epäonnistui ja meidän on käsiteltävä se.

4. Ratkaisu:

1. Poista käytöstä välimuistin valmislausetoiminto (ei suositella)

Rails6 ja uudemmat voivat poistaa tämän ominaisuuden käytöstä asettamalla tietokannan ready_statements arvoksi false.

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

Rails6:ta ja vanhempia ei ole testattu. Jos yllä oleva ei toimi, voit yrittää luoda uuden alustustiedoston.

# 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

vahvista:

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

Johtopäätös: Pienissä projekteissa ei ole väliä, jos poistat tämän toiminnon käytöstä, ja suorituskyky säilyy lähes ennallaan. Kuitenkin suurissa projekteissa mitä enemmän käyttäjiä ja monimutkaisempia kyselylauseita, sitä suurempia etuja tämä toiminto tuo. joten voit päättää, poistatko sen käytöstä todellisen tilanteen mukaan.

2. tehdäselect * tullaselect id, nameTällaiset erityiset kentät kiskoilla7Virallinen ratkaisuSe siitä

  • Aseta enumerate_columns_in_select_statements arvoksi true in 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
  • Tällaista konfiguraatiota ei ole rails7:n alla, voit saavuttaa sen käyttämällä ignored_columns-toimintoa
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

Johtopäätös: Tämän ratkaisun ongelmana on, että kenttien lisääminen voidaan ratkaista täydellisesti, mutta kenttien poistaminen aiheuttaa silti virheitä. Esimerkiksi nimikentän poistamisen jälkeen, jos valmistetussa käskyssä olevaa nimeä ei ole olemassa, valitse id, name from users. Rails7:n virallisessa ratkaisussa on myös tämä ongelma

3. Käynnistä kiskosovellus uudelleen

  • Valmistellun käskyvälimuistin elinkaari on olemassa vain yhdessä tietokantaistunnossa. Jos tietokantayhteys suljetaan (sovelluksen uudelleenkäynnistys sulkee alkuperäisen yhteyden ja muodostaa uudelleen uuden yhteyden), alkuperäinen valmis käskyvälimuisti tyhjennetään ja SQL-pyyntö käynnistetään uudelleen uudelleenkäynnistyksen jälkeen. Tallenna valmis käsky välimuistiin ja saat tiedot normaalisti.

Johtopäätös: Sovelluksen uudelleenkäynnistys aiheuttaa tilapäisen palvelun 502 Ei saatavilla Tietenkin, kun sovellus otetaan käyttöön, palvelu on käynnistettävä uudelleen, ja myös 502 tulee näkyviin, joten on parasta ottaa käyttöön, kun kukaan ei käytä sitä (keskivaiheessa). yöllä?), jotta virheitä tulee mahdollisimman vähänPreparedStatementCacheExpiredIlmoita virheestä

4. Kirjoita uudelleen transaction menetelmä

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
  • Uudelleenkirjoituksen jälkeen koodissa tapahtumien kirjoituspaikka muutetaan käytettäväksi ApplicationRecord.transaction do ... end taiMyModel.transaction

Johtopäätös: Tärkeä huomautus: Jos lähetät sähköposteja, kirjoitat sovellusliittymiin tai suoritat muita toimintoja, jotka ovat vuorovaikutuksessa ulkomaailman kanssa osana tapahtumaa, tämä saattaa aiheuttaa sen, että jotkin näistä toiminnoista toistuvat satunnaisesti kahdesti. Tästä syystä Rails ei virallisesti suorita uudelleenyrityksiä automaattisesti, vaan jättää sen sovelluskehittäjille.

&gt;&gt;&gt;&gt;&gt;&gt;&gt;Kun testaan ​​tätä menetelmää itse, saan silti virheitä.

5. Tyhjennä valmis lausekkeen välimuisti manuaalisesti

 ActiveRecord::Base.connection.clear_cache!
  • 1

5. Lopullinen vastaus

Täydellistä ratkaisua ei löytynyt