Asp.Net(IIS)访问Firebird高峰偶发ArgumentOutOfRangeException异常
问题背景
部署在IIS 10.0.14393.0、基于.NET Framework 4.6.1开发的业务系统运行时出现间歇故障,具体特征如下:
- 高峰时段随机抛出
System.ArgumentOutOfRangeException,异常提示为operation=随机值、Parameter name: operation,参数值无规律,可能为0、159,甚至1380273440这类超出合理范围的极大值 - 回收IIS对应的数据库连接池后异常会暂时消失,低访问量时段无该故障
- 异常触发位置不固定,多个表单页面均会出现,开发环境始终无法复现
- 系统当前依赖组件:Firebird 2.1数据库、FirebirdSql.Data.FirebirdClient 6.6.0驱动,开发环境为Visual Studio 2022 17.1.3版本
故障触发频率最高的为页面权限校验逻辑,相关代码如下:
protected void Page_Load(object sender, EventArgs e) { if ((Session["NomeUsuario"] == null) || (Session["NomeSistema"].ToString() != Master.Sistema())) { Response.Redirect("login.aspx"); } tag = Request.QueryString["tag"]; if (!Page.IsPostBack) { ControleAcesso controleAcesso = new ControleAcesso(); string formulario = System.IO.Path.GetFileName(System.Web.HttpContext.Current.Request.Url.AbsolutePath); if (controleAcesso.ValidaRotina(Session["NomeSistema"].ToString(), Session["NomeUsuario"].ToString(), formulario.ToLower()) == false) { Response.Redirect("Menu.aspx?acesso=1"); } else { if (controleAcesso.ValidaOpcao(Session["NomeSistema"].ToString(), Session["NomeUsuario"].ToString(), "incluir") == true) { butIncluir.Visible = true; } else { butIncluir.Visible = false; } if (controleAcesso.ValidaOpcao(Session["NomeSistema"].ToString(), Session["NomeUsuario"].ToString(), "alterar") == true) { griPadrao.Columns[0].Visible = true; } else { griPadrao.Columns[0].Visible = false; } if (controleAcesso.ValidaOpcao(Session["NomeSistema"].ToString(), Session["NomeUsuario"].ToString(), "filhos") == true) { griPadrao.Columns[1].Visible = true; } else { griPadrao.Columns[1].Visible = false; } if (controleAcesso.ValidaOpcao(Session["NomeSistema"].ToString(), Session["NomeUsuario"].ToString(), "excluir") == true) { griPadrao.Columns[2].Visible = true; } else { griPadrao.Columns[2].Visible = false; } if (controleAcesso.ValidaOpcao(Session["NomeSistema"].ToString(), Session["NomeUsuario"].ToString(), "perfil") == true) { griPadrao.Columns[3].Visible = true; } else { griPadrao.Columns[3].Visible = false; } if (controleAcesso.ValidaOpcao(Session["NomeSistema"].ToString(), Session["NomeUsuario"].ToString(), "historico-log") == true) { griPadrao.Columns[4].Visible = true; } else { griPadrao.Columns[4].Visible = false; } if (controleAcesso.ValidaOpcao(Session["NomeSistema"].ToString(), Session["NomeUsuario"].ToString(), "listagem") == true) { tabListagem.Enabled = true; } else { tabListagem.Enabled = false; } } hypRaca.ImageUrl = "~/Imagens/pastafechada.png"; hypPai.ImageUrl = "~/Imagens/pastafechada.png"; hypMae.ImageUrl = "~/Imagens/pastafechada.png"; hypPaiPermanente.ImageUrl = "~/Imagens/dna_nao.png"; hypMaePermanente.ImageUrl = "~/Imagens/dna_nao.png"; txtMae.Attributes.Add("onfocus", "this.select()"); txtPai.Attributes.Add("onfocus", "this.select()"); tabManutencao.Enabled = false; tabPerfilGeral.Enabled = false; tabLogHistorico.Enabled = false; tabFilhos.Enabled = false; tabListagem.Enabled = false; txtCadastro.Text = DateTime.Today.ToShortDateString(); tabCadastro.ActiveTabIndex = 0; tabPerfilLinhagemCelular.Enabled = false; } } public bool ValidaRotina(string sistema, string usuario, string formulario) { using (FbConnection conexao = new FbConnection(System.Configuration.ConfigurationManager.ConnectionStrings["Dados"].ToString())) try { FbCommand comando = new FbCommand("select count(1) from SYS_PERMISSAO where SISTEMA=@SISTEMA and USUARIO=@USUARIO and FORMULARIO=@FORMULARIO and ((OPCAO is null) or (OPCAO=''))", conexao); comando.Parameters.Add("@SISTEMA", FbDbType.VarChar); comando.Parameters.Add("@USUARIO", FbDbType.VarChar); comando.Parameters.Add("@FORMULARIO", FbDbType.VarChar); comando.Parameters[0].Value = sistema; comando.Parameters[1].Value = usuario; comando.Parameters[2].Value = formulario.ToLower(); conexao.Open(); return !(((int)comando.ExecuteScalar()) > 0); } catch (Exception erro) { return false; } finally { conexao.Close(); } } public bool ValidaOpcao(string sistema, string usuario, string opcao) { using (FbConnection conexao = new FbConnection(System.Configuration.ConfigurationManager.ConnectionStrings["Dados"].ToString())) try { string formulario = System.IO.Path.GetFileName(System.Web.HttpContext.Current.Request.Url.AbsolutePath); FbCommand comando = new FbCommand("select count(1) from SYS_PERMISSAO where SISTEMA=@SISTEMA and USUARIO=@USUARIO and FORMULARIO=@FORMULARIO and OPCAO=@OPCAO", conexao); comando.Parameters.Add("@SISTEMA", FbDbType.VarChar); comando.Parameters.Add("@USUARIO", FbDbType.VarChar); comando.Parameters.Add("@FORMULARIO", FbDbType.VarChar); comando.Parameters.Add("@OPCAO", FbDbType.VarChar); comando.Parameters[0].Value = sistema; comando.Parameters[1].Value = usuario; comando.Parameters[2].Value = formulario.ToLower(); comando.Parameters[3].Value = opcao.ToLower(); conexao.Open(); return !(((int)comando.ExecuteScalar()) > 0); } catch (Exception erro) { return false; } finally { conexao.Close(); } }
根因分析
- 核心诱因为FirebirdSql.Data.FirebirdClient 6.6.0版本存在连接池线程安全缺陷:该版本驱动内部维护连接池异步操作状态时,未对operation枚举字段的读写加线程锁,高并发场景下多线程同时取用、归还连接时会发生内存值踩踏,导致字段值被写脏,出现随机值甚至超出枚举范围的极大值,最终抛出参数越界异常。
- 现有代码逻辑放大了故障触发概率:单次页面首次加载会执行至少7次独立数据库查询(1次路由校验+6次操作权限校验),短时间内频繁从连接池申请、归还连接,高并发下极易触发线程竞争;同时
FbCommand对象未纳入资源释放逻辑,存在数据库对象泄漏问题,会进一步加剧连接池状态混乱。 - 现象匹配逻辑:回收连接池会重置所有池内连接的状态,清空已被写脏的内存值,因此异常会暂时消失,待并发量上升后内存再次被写脏故障复现;开发环境无高并发压力,无法触发多线程同时读写同一块内存的场景,因此始终无法复现。
- 现有异常处理逻辑干扰排查:catch块直接吞掉所有异常返回false,会将真实的数据库驱动异常伪装成权限不足,触发页面跳转回菜单,进一步增加问题定位难度。
修复方案
彻底修复
- 升级FirebirdSql.Data.FirebirdClient驱动至7.10.1及以上稳定版本,7.x分支已完全修复连接池线程安全缺陷;若升级后存在与Firebird 2.1的兼容性问题,可选择6.6.0之后发布的6.x补丁版本,优先选择标记修复了连接池并发问题的构建版本。
- 修正数据库操作代码的资源释放逻辑:所有
FbCommand、FbDataReader等实现了IDisposable接口的数据库对象,全部放入using块中自动释放,移除多余的手动Close()调用,避免资源泄漏。修正后的权限校验方法示例:public bool ValidaRotina(string sistema, string usuario, string formulario) { using (FbConnection conexao = new FbConnection(System.Configuration.ConfigurationManager.ConnectionStrings["Dados"].ToString())) using (FbCommand comando = new FbCommand("select count(1) from SYS_PERMISSAO where SISTEMA=@SISTEMA and USUARIO=@USUARIO and FORMULARIO=@FORMULARIO and ((OPCAO is null) or (OPCAO=''))", conexao)) { try { comando.Parameters.Add("@SISTEMA", FbDbType.VarChar).Value = sistema; comando.Parameters.Add("@USUARIO", FbDbType.VarChar).Value = usuario; comando.Parameters.Add("@FORMULARIO", FbDbType.VarChar).Value = formulario.ToLower(); conexao.Open(); return !(((int)comando.ExecuteScalar()) > 0); } catch (Exception erro) { // 新增错误日志记录,禁止直接吞异常 LogHelper.WriteError(erro, $"权限校验失败,系统:{sistema},用户:{usuario},表单:{formulario}"); return false; } } } - 优化权限校验逻辑:将当前用户的所有权限一次性查询出来,缓存到当前请求上下文或Session中,单次页面加载仅执行1次数据库查询,大幅降低连接池并发压力。
临时规避(无法立即升级驱动时使用)
在数据库连接字符串中添加参数Pooling=false关闭驱动自带的连接池,每次请求直接新建物理连接、用完直接销毁,绕开连接池的线程安全缺陷。该方案会降低数据库访问性能,仅可作为短期过渡方案,待驱动升级完成后恢复连接池配置。
内容的提问来源于stack exchange,提问作者Osvaldo G. Da Cruz Júnior
相关产品推荐
相关产品推荐

