[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的这个错误处理方式是:- 事务transaction中,会直接抛出
raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message) - 事务外的会把缓存@statements中的这句删除并 try,重试后会重新解析、分析、优化sql语句并执行
prepare_statement方法放入预准备语句缓存中
- 事务transaction中,会直接抛出
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!
五、最终答案
没有找到一个完美的解决方案
相关文章:
[ruby on rails]部署时候产生ActiveRecord::PreparedStatementCacheExpired错误的原因及解决方法
一、问题: 有时在 Postgres 上部署 Rails 应用程序时,可能会看到 ActiveRecord::PreparedStatementCacheExpired 错误。仅当在部署中运行迁移时才会发生这种情况。发生这种情况是因为 Rails 利用 Postgres 的缓存准备语句(PreparedStatementCache)功能来…...
函数传值面试题
let a {name: aa };function fun1(a) {a []; // 这里创建了一个新的局部变量a,它是一个空数组// a.name "芜湖" }fun1(a); // 调用fun1,传入a的引用副本 console.log(a); // 输出:{ name: aa }在 JavaScript 中,当你…...
redis笔记2
redis是用c语言写的,放不频繁更新的数据(用户数据。课程数据) Redis 中,"穿透"通常指的是缓存穿透(Cache Penetration)问题,这是指一种恶意或非法请求直接绕过缓存层,直接访问数据库或…...
Kafka(四) Consumer消费者
一,基础知识 1,消费者与消费组 每个消费者都有对应的消费组,不同消费组之间互不影响。 Partition的消息只能被一个消费组中的一个消费者所消费, 但Partition也可能被再平衡分配给新的消费者。 一个Topic的不同Partition会根据分配…...
前端路由手写Hash和History两种模式
文章目录 1. Hash模式:简洁而广泛适用2. History模式:更自然的用户体验3. 结论 在现代Web开发中,单页面应用(Single Page Application,简称SPA)因其流畅的用户体验和高效的页面交互能力而备受青睐。前端路由…...
Redis的单线程讲解与指令学习
目录 一.Redis的命令 二.数据类型 三.Redis的key的过期策略如何实现? 四.Redis为什么是单线程的 五.String有关的命令 Redis的学习专栏:http://t.csdnimg.cn/a8cvV 一.Redis的命令 两个基本命令 在Redis当中,有两个基本命令࿱…...
为什么MySQL会选择B+树作为索引
为什么MySQL会选择B树作为索引 在数据库管理系统中,索引是提升查询效率的关键技术之一。MySQL作为广泛使用的关系型数据库管理系统,其核心存储引擎InnoDB选择B树作为其索引结构,这一选择背后蕴含了深刻的性能和存储效率考量。本文将简要介绍…...
k8s secret-从环境变量里去读和从yaml文件里读取secret有什么区别?
从环境变量和YAML文件中读取Kubernetes Secret的区别主要体现在使用方式、动态更新能力以及管理便捷性上。以下是详细的区别说明: 1. **使用方式**: - **环境变量方式**:Kubernetes允许将Secret作为环境变量注入到Pod的容器中。这种方式的好处…...
Springboot+Aop用注解实现阿里云短信验证码校验,校验通过自动删除验证码缓存
1.新建操作类型枚举(这里的IEnum是我自定义的http请求拦截接口,不需要的话可以不用实现) Getter AllArgsConstructor public enum OperationType implements IEnum<Integer> {/*** 注册*/SIGN_UP(0),/*** 密码登录*/LOGIN_BY_PWD(1),/…...
无线物联网新时代,RFID拣货标签跟随潮流
拣选技术的演变历程,本质上是从人力操作向自动化、智能化转型的持续进程。近期,“货寻人”技术成为众多企业热烈追捧的对象,它可以根据企业的特定需求,从众多拣选方案中选出最优解。那么,在采用“货到人”拣选技术时&a…...
Java8 根据List实体中一个字段去重取最大值,并且根据该字段进行排序
1、前言 某个功能要求需要对一个list对象里数据按照股票分组,并且取分组涨跌幅最大的,返回一个新的list对象,并且按照涨跌幅字段进行排序,这么一连串的要求,如果按照传统的写法,我们需要写一大坨的代码&am…...
微服务经纬:Eureka驱动的分布式服务网格配置全解
微服务经纬:Eureka驱动的分布式服务网格配置全解 在微服务架构的宏伟蓝图中,服务网格(Service Mesh)作为微服务间通信的独立层,承担着流量管理、服务发现、故障恢复等关键任务。Eureka,Netflix开源的服务发…...
关于前端数据库可视化库的选择,vue3+antd+g2plot录课计划
之前:antdv 现在:g2plot https://g2plot.antv.antgroup.com/manual/introduction 录课内容:快速入门 图表示例: 选择使用比较广泛的示例类型,录课顺序如下: 1、折线图2、面积图3、柱形图4、条形图5、饼…...
linux进行redis的安装并使用RDB进行数据迁移
现在有两台电脑,分别是A,B,现在我要把A电脑上的redis的数据迁移到B电脑上,B电脑上是没有安装redis的 1.找到A电脑的redis的版本 1.先启动A电脑的redis,一般来说,都是直接在linux的控制台输入:re…...
深入理解Scikit-learn:决策树与随机森林算法详解
用sklearn实现决策树与随机森林 1. 简介 决策树和随机森林是机器学习中的两种强大算法。决策树通过学习数据特征与标签之间的规则来进行预测,而随机森林则是由多棵决策树组成的集成算法,能有效提高模型的稳定性和准确性。 2. 安装sklearn 首先&#…...
AutoHotKey自动热键(十一)下载SciTE4AutoHotkey-Plus的中文增强版脚本编辑器
关于AutoHotkey的专用编辑器, SciTE4AutoHotkey是一个免费的基于 SciTE 的 AutoHotkey 脚本编辑器,除了 DBGp 支持, 它还为 AutoHotkey 提供了语法高亮, 调用提示, 参数信息和自动完成, 以及其他拥有的编辑特性和辅助工具.XDebugClient 是一个基于 .NET Framework 2.0 的简单开…...
Halcon与C++之间的数据转换
HALCON的HTuple类型(元组)功能很强大,可以表示INT、double、string等多种类型数据。当元组中只有一个成员时,HTuple也可表示原子类型 1. haclon -> C //HTuple转int HTuple hTuple 1; int data1 hTuple[0].I(); // data1 1//HTuple转do…...
MybatisPlus 一些技巧
查询简化 SimpleQuery 有工具类 com.baomidou.mybatisplus.extension.toolkit.SimpleQuery 对 selectList 查询后的结果进行了封装,使其可以通过 Stream 流的方式进行处理,从而简化了 API 的调用。 方法 list() 支持对一个列表提取某个字段ÿ…...
定制化服务发现:Eureka中服务实例偏好的高级配置
定制化服务发现:Eureka中服务实例偏好的高级配置 在微服务架构中,服务实例的智能管理和优化是保证系统高效运行的关键。Eureka作为Netflix开源的服务注册与发现框架,提供了丰富的配置选项来满足不同场景下的需求。服务实例偏好配置允许开发者…...
【实战场景】MongoDB迁移的那些事
【实战场景】MongoDB迁移的那些事 开篇词:干货篇【MongoDB迁移的方法】:1. 基于mongodump和mongorestore的迁移一、迁移前准备二、使用mongodump备份数据三、使用mongorestore还原数据四、注意事项 2. 基于MongoDB复制集的迁移一、迁移前准备二、配置新复…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...
超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...
JDK 17 新特性
#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持,不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的ÿ…...
蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...
LeetCode - 199. 二叉树的右视图
题目 199. 二叉树的右视图 - 力扣(LeetCode) 思路 右视图是指从树的右侧看,对于每一层,只能看到该层最右边的节点。实现思路是: 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...
中医有效性探讨
文章目录 西医是如何发展到以生物化学为药理基础的现代医学?传统医学奠基期(远古 - 17 世纪)近代医学转型期(17 世纪 - 19 世纪末)现代医学成熟期(20世纪至今) 中医的源远流长和一脉相承远古至…...
安全突围:重塑内生安全体系:齐向东在2025年BCS大会的演讲
文章目录 前言第一部分:体系力量是突围之钥第一重困境是体系思想落地不畅。第二重困境是大小体系融合瓶颈。第三重困境是“小体系”运营梗阻。 第二部分:体系矛盾是突围之障一是数据孤岛的障碍。二是投入不足的障碍。三是新旧兼容难的障碍。 第三部分&am…...
django blank 与 null的区别
1.blank blank控制表单验证时是否允许字段为空 2.null null控制数据库层面是否为空 但是,要注意以下几点: Django的表单验证与null无关:null参数控制的是数据库层面字段是否可以为NULL,而blank参数控制的是Django表单验证时字…...
在树莓派上添加音频输入设备的几种方法
在树莓派上添加音频输入设备可以通过以下步骤完成,具体方法取决于设备类型(如USB麦克风、3.5mm接口麦克风或HDMI音频输入)。以下是详细指南: 1. 连接音频输入设备 USB麦克风/声卡:直接插入树莓派的USB接口。3.5mm麦克…...
Unity中的transform.up
2025年6月8日,周日下午 在Unity中,transform.up是Transform组件的一个属性,表示游戏对象在世界空间中的“上”方向(Y轴正方向),且会随对象旋转动态变化。以下是关键点解析: 基本定义 transfor…...
