Berbagi teknologi

[ruby on rails] Alasan dan solusi untuk kesalahan ActiveRecord::PreparedStatementCacheExpired selama penerapan

2024-07-12

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

1. Pertanyaan:

  • Terkadang saat menerapkan aplikasi Rails di Postgres, Anda mungkin melihat kesalahan ActiveRecord::PreparedStatementCacheExpired. Ini hanya terjadi ketika menjalankan migrasi dalam penerapan.
  • Hal ini terjadi karena Rails memanfaatkan fitur PreparedStatementCache Postgres untuk meningkatkan kinerja. Fitur ini diaktifkan secara default di Rails.

2. Masalah berulang:

  • Kita dapat menggunakan rspec untuk mereproduksi kesalahan ini
 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

Masukkan deskripsi gambar di sini

3. Prinsip produksi:

  • Pernyataan kueri Rails sepertiUser.alloleh active_record Parsing ke dalam pernyataan sqlSetelah itu kirimkan ke database,Jalankan PREPARE terlebih dahulu Pernyataan yang disiapkan, pernyataan SQL akan diurai, dianalisis, dioptimalkan dan ditulis ulang.Saat tindak lanjutnyaKeluarkan perintah EXECUTE, pernyataan yang disiapkan akan direncanakan dan dilaksanakan.
  • Rails akan menyimpan pernyataan kueripg_prepared_statementsuntuk memfasilitasi lain kali Anda memanggil pernyataan serupa.mengeksekusi secara langsung pernyataan, alih-alih menguraikan, menganalisis, dan mengoptimalkan, menghindari duplikasi pekerjaan dan meningkatkan efisiensi.
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
  • Di Postgres, cache pernyataan yang disiapkan akan menjadi tidak valid jika skema tabel berubah memengaruhi hasil yang dikembalikan. Secara khusus, ini adalah operasi DDL seperti menambah dan menghapus bidang ke tabel, atau mengubah jenis dan panjang bidang.

Seperti pada contoh berikut, saat menjalankan SELECT setelah menambah atau menghapus bidang,basis data halakan melemparcached plan must not change result type, di relcatatan_aktifDapatkan kesalahan ini lalu lemparActiveRecord::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
  • Saat menjalankan migrasi penambahan, pengurangan, atau modifikasi bidang dalam layanan penerapan, pernyataan kueri yang dikeluarkan oleh pengguna akan langsung mengambil SQL dari cache pernyataan yang telah disiapkan dan menjalankan excute secara langsung. Namun, karena struktur tabel berubah saat ini, cache pernyataan yang disiapkan menjadi tidak valid.basis data halakan melemparcached plan must not change result typekesalahan
  • Lihat kode sumber active_recordexec_cacheMetodenya, saya menemukan bahwa metode penanganan kesalahan Rails untuk pg adalah:
    1. Dalam transaksinya akan langsung dilempar raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
    2. Mereka yang berada di luar transaksi akan menyimpan @statements dalam cacheHapus kalimat ini dan coba, setelah mencoba ulang, pernyataan sql akan diurai ulang, dianalisis, dioptimalkan, dan dieksekusi.prepare_statementmetode ke dalam cache pernyataan yang disiapkan
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
  • Oleh karena itu, kesalahan yang muncul pada transaksi ini akan menyebabkan transaksi dibatalkan. Bagi bisnis, itu berarti permintaan gagal dan kami harus menanganinya sendiri.

4. Solusi:

1. Nonaktifkan fungsi pernyataan yang disiapkan cache (tidak disarankan)

Rails6 dan yang lebih baru dapat menonaktifkan fitur ini dengan menyetel pernyataan_siap di database ke false.

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

Rails6 dan di bawahnya belum diuji. Jika cara di atas tidak berhasil, Anda dapat mencoba membuat file inisialisasi baru.

# 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

memeriksa:

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

Kesimpulan: Dalam proyek kecil, tidak masalah jika Anda menonaktifkan fungsi ini, dan kinerjanya hampir tidak terpengaruh. Namun, dalam proyek besar, semakin banyak pengguna dan semakin kompleks pernyataan kueri, semakin besar manfaat yang didapat dari fungsi ini. sehingga Anda dapat memutuskan apakah akan menonaktifkannya sesuai dengan situasi sebenarnya.

2. membuatselect * menjadiselect id, nameBidang spesifik seperti itu, di Rails7Solusi resmiItu dia

  • Setel enumerate_columns_in_select_statements ke true di 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
  • Tidak ada konfigurasi seperti itu di bawah Rails7, Anda dapat menggunakan diabaikan_kolom untuk mencapainya
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

Kesimpulan: Masalah dengan solusi ini adalah menambahkan field dapat diselesaikan dengan sempurna, tetapi menghapus field masih akan menyebabkan kesalahan. Misalnya, setelah menghapus field nama, jika nama dalam pernyataan yang disiapkan pilih id, nama dari pengguna tidak ada, kesalahan akan dilaporkan. Solusi resmi Rails7 juga mengalami masalah ini

3. Mulai ulang aplikasi Rails

  • Siklus hidup cache pernyataan yang disiapkan hanya ada dalam satu sesi database. Jika koneksi database ditutup (memulai ulang aplikasi akan menutup koneksi asli dan membuat kembali koneksi baru), cache pernyataan asli yang disiapkan akan dihapus, dan cache pernyataan yang disiapkan akan dihapus. Permintaan SQL setelah memulai ulang akan dimulai ulang. Cache pernyataan yang telah disiapkan dan Anda bisa mendapatkan data secara normal.

Kesimpulan: Restart aplikasi akan menyebabkan layanan sementara 502 Tidak Tersedia. Tentu saja, ketika menerapkan aplikasi, layanan harus di-restart, dan 502 juga akan muncul, jadi yang terbaik adalah menerapkan ketika tidak ada yang mengaksesnya (di tengah-tengah). malam?), sehingga kesalahan yang muncul sesedikit mungkinPreparedStatementCacheExpiredLaporkan kesalahan

4. Menulis ulang transaction metode

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
  • Setelah ditulis ulang, tempat penulisan transaksi dalam kode diubah untuk digunakan ApplicationRecord.transaction do ... end atauMyModel.transaction

Kesimpulan: Catatan penting: Jika Anda mengirim email, memposting ke API, atau melakukan operasi lain yang berinteraksi dengan dunia luar sebagai bagian dari transaksi, hal ini dapat menyebabkan beberapa operasi tersebut terkadang terjadi dua kali. Inilah sebabnya mengapa Rails secara resmi tidak melakukan percobaan ulang secara otomatis, tetapi menyerahkannya kepada pengembang aplikasi.

&gt;&gt;&gt;&gt;&gt;&gt;&gt;Ketika saya menguji metode ini sendiri, saya masih mendapatkan kesalahan.

5. Hapus cache pernyataan yang telah disiapkan secara manual

 ActiveRecord::Base.connection.clear_cache!
  • 1

5. Jawaban akhir

Tidak menemukan solusi yang tepat