OracleDataReader.Read 方法超時
ODP.NET OracleCommand 類有一個 CommandTimeout 屬性,可用於強制執行命令超時。此屬性似乎在 CommandText 是 SQL 語句的情況下起作用。範常式式碼用於說明此屬性的實際作用。在程式碼的初始版本中,CommandTimeout 設置為零 - 告訴 ODP.NET 不要強制超時。
using System; using System.Collections.Generic; using System.Data; using System.Diagnostics; using System.Linq; using System.Text; using Oracle.DataAccess.Client; namespace ConsoleApplication3 { class Program { static void Main(string[] args) { using (OracleConnection con = new OracleConnection("User ID=xxxx; Password=xxxx; Data Source=xxxx;")) using (OracleCommand cmd = new OracleCommand()) { con.Open(); cmd.Connection = con; Console.WriteLine("Executing Query..."); try { cmd.CommandTimeout = 0; // Data set SQL: cmd.CommandText = "<some long running SQL statement>"; cmd.CommandType = System.Data.CommandType.Text; Stopwatch watch1 = Stopwatch.StartNew(); OracleDataReader reader = cmd.ExecuteReader(); watch1.Stop(); Console.WriteLine("Query complete. Execution time: {0} ms", watch1.ElapsedMilliseconds); int counter = 0; Stopwatch watch2 = Stopwatch.StartNew(); if (reader.Read()) counter++; watch2.Stop(); Console.WriteLine("First record read: {0} ms", watch2.ElapsedMilliseconds); Stopwatch watch3 = Stopwatch.StartNew(); while (reader.Read()) { counter++; } watch3.Stop(); Console.WriteLine("Records 2..n read: {0} ms", watch3.ElapsedMilliseconds); Console.WriteLine("Records read: {0}", counter); } catch (OracleException ex) { Console.WriteLine("Exception was thrown: {0}", ex.Message); } Console.WriteLine("Press any key to continue..."); Console.Read(); } } } }上述程式碼的範例輸出如下所示:
Executing Query... Query complete. Execution time: 8372 ms First record read: 3 ms Records 2..n read: 1222 ms Records read: 20564 Press any key to continue...如果我將 CommandTimeout 更改為 3 之類的…
cmd.CommandTimeout = 3;…然後執行相同的程式碼會產生以下輸出:
Executing Query... Exception was thrown: ORA-01013: user requested cancel of current operation Press any key to continue...不過,呼叫返回 ref 游標的儲存過程是另一回事。考慮下面的測試過程(純粹用於測試目的):
PROCEDURE PROC_A(i_sql VARCHAR2, o_cur1 OUT SYS_REFCURSOR) is begin open o_cur1 for i_sql; END PROC_A;下面的範常式式碼可用於呼叫儲存過程。請注意,它將 CommandTimeout 設置為值 3。
using System; using System.Collections.Generic; using System.Data; using System.Diagnostics; using System.Linq; using System.Text; using Oracle.DataAccess.Client; namespace ConsoleApplication3 { class Program { static void Main(string[] args) { using (OracleConnection con = new OracleConnection("User ID=xxxx; Password=xxxx; Data Source=xxxx;")) using (OracleCommand cmd = new OracleCommand()) { con.Open(); cmd.Connection = con; Console.WriteLine("Executing Query..."); try { cmd.CommandTimeout = 3; string sql = "<some long running sql>"; cmd.CommandText = "PROC_A"; cmd.CommandType = System.Data.CommandType.StoredProcedure; cmd.Parameters.Add(new OracleParameter("i_sql", OracleDbType.Varchar2) { Direction = ParameterDirection.Input, Value = sql }); cmd.Parameters.Add(new OracleParameter("o_cur1", OracleDbType.RefCursor) { Direction = ParameterDirection.Output }); Stopwatch watch1 = Stopwatch.StartNew(); OracleDataReader reader = cmd.ExecuteReader(); watch1.Stop(); Console.WriteLine("Query complete. Execution time: {0} ms", watch1.ElapsedMilliseconds); int counter = 0; Stopwatch watch2 = Stopwatch.StartNew(); if (reader.Read()) counter++; watch2.Stop(); Console.WriteLine("First record read: {0} ms", watch2.ElapsedMilliseconds); Stopwatch watch3 = Stopwatch.StartNew(); while (reader.Read()) { counter++; } watch3.Stop(); Console.WriteLine("Records 2..n read: {0} ms", watch3.ElapsedMilliseconds); Console.WriteLine("Records read: {0}", counter); } catch (OracleException ex) { Console.WriteLine("Exception was thrown: {0}", ex.Message); } Console.WriteLine("Press any key to continue..."); Console.Read(); } } } }上面程式碼的範例輸出如下所示:
Executing Query... Query complete. Execution time: 34 ms First record read: 8521 ms Records 2..n read: 1014 ms Records read: 20564 Press any key to continue...請注意,執行時間非常快(34 毫秒),並且沒有引發超時異常。我們在這裡看到的性能是因為引用游標的 SQL 語句直到第一次呼叫 OracleDataReader.Read 方法時才執行。當第一次呼叫 Read() 以從 refcursor 讀取第一條記錄時,會導致長時間執行的查詢對性能造成影響。
我已經說明的行為意味著 OracleCommand.CommandTimeout 屬性不能用於取消與引用游標關聯的長時間執行的查詢。在這種情況下,我不知道 ODP.NET 中有任何屬性可用於限制引用游標 SQL 的執行時間。有人對長時間執行的引用游標 SQL 語句的執行如何在一定時間後短路有任何建議嗎?
這是我最終採用的解決方案。它只是 OracleDataReader 類的擴展方法。該方法有一個超時值和一個回調函式作為參數。回調函式通常(如果不總是)是 OracleCommand.Cancel。
namespace ConsoleApplication1 { public static class OracleDataReaderExtensions { public static bool Read(this OracleDataReader reader, int timeout, Action cancellationAction) { Task<bool> task = Task<bool>.Factory.StartNew(() => { try { return reader.Read(); } catch (OracleException ex) { // When cancellationAction is called below, it will trigger // an ORA-01013 error in the Read call that is still executing. // This exception can be ignored as we're handling the situation // by throwing a TimeoutException. if (ex.Number == 1013) { return false; } else { throw; } } }); try { if (!task.Wait(timeout)) { // call the cancellation callback function (i.e. OracleCommand.Cancel()) cancellationAction(); // throw an exception to notify calling code that a timeout has occurred throw new TimeoutException("The OracleDataReader.Read operation has timed-out."); } return task.Result; } catch (AggregateException ae) { throw ae.Flatten(); } } } }這是一個如何使用它的範例。
namespace ConsoleApplication1 { class Program { static string constring = "User ID=xxxx; Password=xxxx; Data Source=xxxx;"; static void Main(string[] args) { using (OracleConnection con = new OracleConnection(constring)) using (OracleCommand cmd = new OracleCommand()) { cmd.Connection = con; con.Open(); Console.WriteLine("Executing Query..."); string sql = "<some long running sql>"; cmd.CommandText = "PROC_A"; cmd.CommandType = System.Data.CommandType.StoredProcedure; cmd.Parameters.Add(new OracleParameter("i_sql", OracleDbType.Varchar2) { Direction = ParameterDirection.Input, Value = sql }); cmd.Parameters.Add(new OracleParameter("o_cur1", OracleDbType.RefCursor) { Direction = ParameterDirection.Output }); try { // execute command and get reader for ref cursor OracleDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection); // read first record; this is where the ref cursor SQL gets evaluated Console.WriteLine("Reading first record..."); if (reader.Read(3000, cmd.Cancel)) { } // read remaining records Console.WriteLine("Reading records 2 to N..."); while (reader.Read(3000, cmd.Cancel)) { } } catch (TimeoutException ex) { Console.WriteLine("Exception: {0}", ex.Message); } Console.WriteLine("Press any key to continue..."); Console.Read(); } } } }這是輸出的一個例子。
Executing Query... Reading first record... Exception: The OracleDataReader.Read operation has timed-out. Press any key to continue...