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

news2024/12/23 14:04:51

一、问题:

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

二、问题复现:

  • 我们可以用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

在这里插入图片描述

三、产生的原理:

  • 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 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
  • 所以出现在事务transaction中的这个错误,就会导致事务回滚,对业务来说就是请求失败了,需要我们自己来处理

四、解决方法:

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

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

default: &default
  adapter: postgresql
  encoding: unicode
  prepared_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 MyApp
  class Application < Rails::Application
    config.active_record.enumerate_columns_in_select_statements = true
  end
end
  • 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

结论:这个方案存在的问题是,增加字段可以完美解决,但是删除字段,还会出现报错,比如删除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::Base
  class << self
    def transaction(*args, &block)
      retried ||= false
      super
    rescue ActiveRecord::PreparedStatementCacheExpired
      if retried
        raise
      else
        retried = true
        retry
      end
    end
  end
end
  • 重写后代码里写事务的地方改为使用 ApplicationRecord.transaction do ... end 或者 MyModel.transaction或者obj.transaction, 只要不用ActiveRecord::Base.transaction就行

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

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

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

 ActiveRecord::Base.connection.clear_cache!

五、最终答案

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1923677.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Apache Doris】周FAQ集锦:第 10 期

【Apache Doris】周FAQ集锦&#xff1a;第 10 期 SQL问题数据操作问题运维常见问题其它问题关于社区 欢迎查阅本周的 Apache Doris 社区 FAQ 栏目&#xff01; 在这个栏目中&#xff0c;每周将筛选社区反馈的热门问题和话题&#xff0c;重点回答并进行深入探讨。旨在为广大用户…

算法力扣刷题记录 四十五【110.平衡二叉树】

前言 二叉树篇继续 记录 四十五【110.平衡二叉树】 一、题目阅读 给定一个二叉树&#xff0c;判断它是否是 平衡二叉树。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,3,3…

【鸿蒙学习笔记】尺寸设置・width・height・size・margin・padding・

官方文档&#xff1a;尺寸设置 目录标题 width&#xff1a;设置组件自身的宽度height&#xff1a;设置组件自身的高度size&#xff1a;设置高宽尺寸margin&#xff1a;设置组件的外边距padding&#xff1a;设置组件的内边距 width&#xff1a;设置组件自身的宽度 参数为Length…

【零基础】学JS之APIS第三天

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 非常期待和您一起在这个小…

10分钟快速了解神经网络(Neural Networks)

神经网络是深度学习算法的基本构建模块。神经网络是一种机器学习算法&#xff0c;旨在模拟人脑的行为。它由相互连接的节点组成&#xff0c;也称为人工神经元&#xff0c;这些节点组织成层次结构。 Source: victorzhou.com 神经网络与机器学习有何不同&#xff1f; 神经网络是…

电脑资料丢失不用慌,5招教你恢复数据

在数字化时代&#xff0c;电脑资料的安全与完整对我们而言至关重要。然而&#xff0c;生活中总有一些小插曲&#xff0c;如意外删除、系统故障或病毒攻击等&#xff0c;导致电脑上的重要资料消失得无影无踪。面对这种情况&#xff0c;我们往往感到焦虑和无助。今天&#xff0c;…

LabVIEW心电信号自动测试系统

开发了一种基于LabVIEW的心电信号自动测试系统&#xff0c;通过LabVIEW开发的上位机软件&#xff0c;实现对心电信号的实时采集、分析和自动化测试。系统包括心电信号采集模块、信号处理模块和自动化测试模块&#xff0c;能够高效、准确地完成心电信号的测量与分析。 硬件系统…

在 SwiftUI 中的作用域动画

文章目录 前言简单示例动画视图修饰符使用多个可动画属性使用 ViewBuilder总结 前言 从一开始&#xff0c;动画就是 SwiftUI 最强大的功能之一。你可以在 SwiftUI 中快速构建流畅的动画。唯一的缺点是每当我们需要运行多步动画或将动画范围限定到视图层次结构的特定部分时&…

网络规划设计师教程(第二版) pdf

网络规划设计师教程在网上找了很多都是第一版&#xff0c;没有第二版。 所以去淘宝买了第二版的pdf&#xff0c;与其自己独享不如共享出来&#xff0c;让大家也能看到。 而且这个pdf我已经用WPS扫描件识别过了&#xff0c;可以直接CtrlF搜索关键词&#xff0c;方便查阅。 链接…

股指期货存在的风险有哪些?

股指期货因其标的物的特殊性&#xff0c;其面临的风险类型十分复杂&#xff0c;主要面临的一般风险和特有风险如下&#xff1a; 一般风险 从风险是否可控的角度&#xff0c;可以划分为不可控风险和可控风险&#xff1b;从交易环节可分为代理风险、流动性风险、强制平仓风险&…

linux 安装redis 遇到问题解决方案

1.当下载了redis包时&#xff08;version&#xff1a;6.0.6&#xff09; 进入解压后的redis目录&#xff08;这里采用tar包安装&#xff09; 当执行make命令时 如果遇到如下情况&#xff1a; 查看当前服务起的gcc 版本&#xff1a;因为redis需要c编译器编译 gcc -v centos 7 …

使用亮数据代理IP+Python爬虫批量爬取招聘信息训练面试类AI智能体

本文目录 一、引言二、开发准备三、代码开发四、使用亮数据进行高效爬取4.1 为什么需要亮数据4.2 如何使用亮数据 五、使用数据训练AI智能体六、 总结 一、引言 在当今AI迅速发展的时代&#xff0c;招聘市场正经历着前所未有的变革。传统的招聘方式已难以满足双方的需求。AI智…

Transformer模型:Postion Embedding实现

前言 这是对上一篇WordEmbedding的续篇PositionEmbedding。 视频链接&#xff1a;19、Transformer模型Encoder原理精讲及其PyTorch逐行实现_哔哩哔哩_bilibili 上一篇链接&#xff1a;Transformer模型&#xff1a;WordEmbedding实现-CSDN博客 正文 先回顾一下原论文中对Posit…

如何巧妙运用百川工作手机微信行为监控 防范员工离职带走客户

在竞争日益激烈的商业环境中&#xff0c;企业最宝贵的资产莫过于忠诚的客户群体与高效协作的团队。然而&#xff0c;当团队中不可避免地出现人员流动时&#xff0c;如何有效防止客户资源流失&#xff0c;成为众多企业管理者面临的严峻挑战。百川工作手机&#xff0c;作为一款专…

基于Redisson 实现 Redis 分布式锁

代码示例&#xff1a; GetMapping("/testJmeter")public void testJmeter() {synchronized (this){int stock Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"))if (stock > 0) {int realStock stock - 1;stringRedisTemplate.opsFo…

【组件库】element-plus组件库

文章目录 0. 启动项目1. gc.sh 新增组件2. 本地验证(组件注册的方式)3. 官方文档修改3-1. 左侧菜单3-2 . 配置md文档3-3. 代码问题:文档修改----------------------------------------------4. 将naiveui的split 分割组件【 复制、迁移】到 element-ui-plus组件库4.1 naiveu…

Science Advances 仿生双模态触觉感知

研究背景 触觉感知在人类收集信息和接收周围环境反馈中扮演着至关重要的角色。随着人工智能的发展&#xff0c;具有类似人类感知能力的智能机器人受到越来越多的关注。现有的触觉传感器能够感知接触前的刺激和压力大小&#xff0c;但它们在区分物体类型、评估柔软度和量化杨氏…

go-高效处理应用程序数据

一、背景 大型的应用程序为了后期的排障、运营等&#xff0c;会将一些请求、日志、性能指标等数据保存到存储系统中。为了满足这些需求&#xff0c;我们需要进行数据采集&#xff0c;将数据高效的传输到存储系统 二、问题 采集服务仅仅针对某个需求开发&#xff0c;需要修改…

Docker容器的生命周期

引言 Docker 容器作为一种轻量级虚拟化技术&#xff0c;在现代应用开发和部署中扮演着重要角色。理解容器的生命周期对于有效地管理和运维容器化应用至关重要。本文将深入探讨 Docker 容器的生命周期&#xff0c;从创建到销毁的各个阶段&#xff0c;帮助读者更好地掌握容器管理…

分手后如何走出夜晚的抑郁,告别失眠困扰?

在这个快速变化的世界里&#xff0c;分手成为了许多人生活中不得不面对的现实。而每当夜幕降临&#xff0c;那种难以言表的孤独感和深深的抑郁往往让人倍感煎熬&#xff0c;甚至陷入失眠的漩涡。那么&#xff0c;分手后我们该如何应对这种情绪困扰&#xff0c;重新找回自己的宁…