当前位置: 首页 > news >正文

[ruby on rails]部署时候产生ActiveRecord::PreparedStatementCacheExpired错误的原因及解决方法

一、问题:

  • 有时在 Postgres 上部署 Rails 应用程序时,可能会看到 ActiveRecord::PreparedStatementCacheExpired 错误。仅当在部署中运行迁移时才会发生这种情况。
  • 发生这种情况是因为 Rails 利用 Postgres 的缓存准备语句(PreparedStatementCache)功能来提高性能。这个功能在rails中默认是开启的。

二、问题复现:

  • 我们可以用rspec来复现这个错误
 it 'not raise ActiveRecord::PreparedStatementCacheExpired' docreate(:user)User.firstUser.find_by_sql('ALTER TABLE users ADD new_metric_column integer;')ActiveRecord::Base.transaction { User.first }end

在这里插入图片描述

三、产生的原理:

  • rails查询语句如User.all被 active_record 解析成sql语句后,发送给数据库,先执行PREPARE预备语句,sql语句会被解析、分析、优化并且重写。当后续发出一个EXECUTE命令时,该预备语句会被规划并且执行。
  • rails会把查询语句存到pg_prepared_statements中,以方便下次调用同类语句时候直接execute 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]]
  • 在 Postgres 中,如果表的模式(schema)更改影响返回结果,则预准备语句缓存将失效。具体说就是给表增加、删除字段,或者修改字段的类型、长度等ddl操作。

如下面的例子,添加或删除字段后执行SELECT时,pg数据库就会抛出cached plan must not change result type,rails中active_record获取到这个错误然后会抛出ActiveRecord::PreparedStatementCacheExpired

ALTER TABLE users ADD COLUMN new_column integer;
ALTER TABLE users DROP COLUMN old_column;
添加或删除列,然后执行 SELECT *
删除 old_column 列然后执行 SELECT users.old_column
  • 部署服务中运行增、减、修改字段的迁移时,用户发出的查询语句会从预准备语句缓存中直接拿sql直接进行excute,但这时候因为表结构变化了,预准备语句缓存就失效了,pg数据库就会抛出cached plan must not change result type错误
  • 查看active_record源码中的exec_cache方法,发现rails对pg的这个错误处理方式是:
    1. 事务transaction中,会直接抛出 raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
    2. 事务外的会把缓存@statements中的这句删除并 try,重试后会重新解析、分析、优化sql语句并执行prepare_statement方法放入预准备语句缓存中
module ActiveRecordmodule ConnectionHandlingdef exec_cache(sql, name, binds)materialize_transactionsmark_transaction_written_if_write(sql)update_typemap_for_default_timezonestmt_key = prepare_statement(sql, binds)type_casted_binds = type_casted_binds(binds)log(sql, name, binds, type_casted_binds, stmt_key) doActiveSupport::Dependencies.interlock.permit_concurrent_loads do@connection.exec_prepared(stmt_key, type_casted_binds)endendrescue ActiveRecord::StatementInvalid => eraise unless is_cached_plan_failure?(e)# Nothing we can do if we are in a transaction because all commands# will raise InFailedSQLTransactionif 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)endretryendendend
end
  • 所以出现在事务transaction中的这个错误,就会导致事务回滚,对业务来说就是请求失败了,需要我们自己来处理

四、解决方法:

1. 禁用缓存准备语句功能(不推荐)

rails6 以上可以把 database中 prepared_statements 设为 false来禁用这个功能

default: &defaultadapter: postgresqlencoding: unicodeprepared_statements: false

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)

验证:

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

结论:小型项目中其实禁用这个功能无所谓,性能几乎不影响,但是大型项目中,用户越多,越复杂的查询语句,这个功能带来的受益越大,所以可以根据实际情况来决定是否禁用

2. 使select * 变为 select id, name这样的具体字段, rails7中的官方解决方案就是这样的,但只能解决新增字段引起的报错

  • rails7中 enumerate_columns_in_select_statements 设为 true
# config/application.rb
module MyAppclass Application < Rails::Applicationconfig.active_record.enumerate_columns_in_select_statements = trueend
end
  • rails7以下没有这个配置,可以用 ignored_columns来实现
class ApplicationRecord < ActiveRecord::Baseself.abstract_class = true#__fake_column__是自定义的,不要是某个表中的字段就行,如果是[:id],那么 User.all就会被解析为select name from users,没有id了self.ignored_columns = [:__fake_column__] 
end

结论:这个方案存在的问题是,增加字段可以完美解决,但是删除字段,还会出现报错,比如删除name字段后,预准备语句select id, name from users中的name不存在了,就会报错。 删除字段可以在 User.rb 中增加 self.ignored_columns = [:name], 然后先重启服务,再进行部署,部署时候最好把 self.ignored_columns = [:name] 删掉,避免以后再加回 name 字段后,select 不到,rails7 官方的方案也存在这个问题,所以这个方案感觉很麻烦

3. 重启rails应用

  • 预准备语句缓存的生命周期只存在于一个数据库会话中,关闭数据库连接(重启应用会关闭原连接,重新建立新连接)那原来的预准备语句缓存就会清空,重启后的sql请求就会重新缓存预准备语句,就能正常拿到数据。

结论:重启应用会出现短暂服务502不可用,当然部署应用时候也是要重启服务的,也会出现502,所以最好是没人访问的时候(半夜?)进行部署,这样就会尽可能少的出现PreparedStatementCacheExpired报错

4. 重写 transaction 方法

class ApplicationRecord < ActiveRecord::Baseclass << selfdef transaction(*args, &block)retried ||= falsesuperrescue ActiveRecord::PreparedStatementCacheExpiredif retriedraiseelseretried = trueretryendendend
end
  • 重写后代码里写事务的地方改为使用 ApplicationRecord.transaction do ... end 或者 MyModel.transaction或者obj.transaction, 只要不用ActiveRecord::Base.transaction就行

结论:重要提示:如果在事务中有发送电子邮件、post到 API 或执行其他与外界交互的操作,这可能会导致其中一些操作偶尔发生两次。这就是为什么 Rails官方不会自动执行重试,而是将其留给应用程序开发人员。

>>>>>>>我本人测试这个方法还是会继续报错

5. 手动清除预准备语句缓存

 ActiveRecord::Base.connection.clear_cache!

五、最终答案

没有找到一个完美的解决方案

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • JS【实战】CSS 样式相关的处理
  • vue3入门特性
  • Excel 学习手册 - 精进版(包括各类复杂函数及其嵌套使用)
  • ES6 对象的新增方法(十四)
  • Milvus 核心设计(5)--- scalar indexwork mechanism
  • 华为HCIP Datacom H12-821 卷40
  • FPGA上板项目(二)——PLL测试
  • c++单例模式
  • 「Conda」在Linux系统中安装Conda环境管理器
  • python安全脚本开发简单思路
  • SpringBoot+Vue实现简单的文件上传(txt篇)
  • 华为USG6000V防火墙v1
  • 【区块链 + 智慧政务】城市公积金中心区块链基础服务平台 | FISCO BCOS应用案例
  • 网络安全工作者如何解决网络拥堵
  • Centos---命令详解 vi 系统服务 网络
  • 收藏网友的 源程序下载网
  • centos安装java运行环境jdk+tomcat
  • Flannel解读
  • laravel5.5 视图共享数据
  • Logstash 参考指南(目录)
  • MYSQL 的 IF 函数
  • Node.js 新计划:使用 V8 snapshot 将启动速度提升 8 倍
  • Redis 懒删除(lazy free)简史
  • Vue实战(四)登录/注册页的实现
  • 搭建gitbook 和 访问权限认证
  • 给github项目添加CI badge
  • 前端每日实战 2018 年 7 月份项目汇总(共 29 个项目)
  • 没有任何编程基础可以直接学习python语言吗?学会后能够做什么? ...
  • 专访Pony.ai 楼天城:自动驾驶已经走过了“从0到1”,“规模”是行业的分水岭| 自动驾驶这十年 ...
  • ​14:00面试,14:06就出来了,问的问题有点变态。。。
  • ​马来语翻译中文去哪比较好?
  • ‌U盘闪一下就没了?‌如何有效恢复数据
  • # Swust 12th acm 邀请赛# [ E ] 01 String [题解]
  • # 计算机视觉入门
  • (02)vite环境变量配置
  • (1)SpringCloud 整合Python
  • (16)UiBot:智能化软件机器人(以头歌抓取课程数据为例)
  • (160)时序收敛--->(10)时序收敛十
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第2节(共同的基类)
  • (NO.00004)iOS实现打砖块游戏(十二):伸缩自如,我是如意金箍棒(上)!
  • (八十八)VFL语言初步 - 实现布局
  • (附源码)springboot车辆管理系统 毕业设计 031034
  • (十) 初识 Docker file
  • (五)关系数据库标准语言SQL
  • (转)linux 命令大全
  • .net FrameWork简介,数组,枚举
  • .Net OpenCVSharp生成灰度图和二值图
  • .net2005怎么读string形的xml,不是xml文件。
  • /dev/sda2 is mounted; will not make a filesystem here!
  • @Not - Empty-Null-Blank
  • [ 2222 ]http://e.eqxiu.com/s/wJMf15Ku
  • [ACP云计算]易混淆知识点(考题总结)
  • [C/C++]数据结构 循环队列
  • [C][数据结构][树]详细讲解
  • [C++] vector list 等容器的迭代器失效问题