简而言之,我需要在特定表中发生更改时异步通知SQL Server的Web API服务.
为了实现上述目标,我创建了一个SQLCLR存储过程,其中包含异步API调用以通知服务.当存在名为Table1的表中的插入时,通过触发器调用SQLCLR存储过程.这里的主要挑战是API必须从同一个表中读取数据(表1).
如果我使用HttpWebRequest.GetResponse()这是同步版本,由于插入触发器的隐式锁定,整个操作将被锁定.为了避免这种情况,我使用了HttpWebRequest.GetResponseAsync()方法来调用API并且不等待响应.因此它触发API请求并且程序控件继续运行,因此触发器事务不会在table1上保持任何锁定,并且API能够从table1读取数据.
现在我必须实现一个错误通知机制,当出现故障时(比如无法连接到远程服务器),我需要向管理团队发送一封电子邮件.我在catch()块中编写了邮件组合逻辑.如果我继续使用上面的HttpWebRequest.GetResponseAsync().Result方法,整个操作将变为同步并锁定整个操作.
如果我使用Microsoft文档中建议的BeginGetResponse()和EndGetResponse()方法实现并运行SQLCLR存储过程,SQL Server挂起时没有任何信息,为什么?我在这做错了什么?为什么不执行RespCallback()方法?
共享下面的SQLCLR代码片段.
public class RequestState { // This class stores the State of the request. // const int BUFFER_SIZE = 1024; // public StringBuilder requestData; // public byte[] BufferRead; public HttpWebRequest request; public HttpWebResponse response; // public Stream streamResponse; public RequestState() { // BufferRead = new byte[BUFFER_SIZE]; // requestData = new StringBuilder(""); request = null; // streamResponse = null; } } public partial class StoredProcedures { private static SqlString _mailServer = null; private static SqlString _port = null; private static SqlString _fromAddress = null; private static SqlString _toAddress = null; private static SqlString _mailAcctUserName = null; private static SqlString _decryptedPassword = null; private static SqlString _subject = null; private static string _mailContent = null; private static int _portNo = 0; public static ManualResetEvent allDone = new ManualResetEvent(false); const int DefaultTimeout = 20000; // 50 seconds timeout #region TimeOutCallBack /// <summary> /// Abort the request if the timer fires. /// </summary> /// <param name="state">request state</param> /// <param name="timedOut">timeout status</param> private static void TimeoutCallback(object state, bool timedOut) { if (timedOut) { HttpWebRequest request = state as HttpWebRequest; if (request != null) { request.Abort(); SendNotifyErrorEmail(null, "The request got timedOut!,please check the API"); } } } #endregion #region APINotification [SqlProcedure] public static void Notify(SqlString weburl, SqlString username, SqlString password, SqlString connectionLimit, SqlString mailServer, SqlString port, SqlString fromAddress , SqlString toAddress, SqlString mailAcctUserName, SqlString mailAcctPassword, SqlString subject) { _mailServer = mailServer; _port = port; _fromAddress = fromAddress; _toAddress = toAddress; _mailAcctUserName = mailAcctUserName; _decryptedPassword = mailAcctPassword; _subject = subject; if (!(weburl.IsNull && username.IsNull && password.IsNull && connectionLimit.IsNull)) { var url = Convert.ToString(weburl); var uname = Convert.ToString(username); var pass = Convert.ToString(password); var connLimit = Convert.ToString(connectionLimit); int conLimit = Convert.ToInt32(connLimit); try { if (!(string.IsNullOrEmpty(url) && string.IsNullOrEmpty(uname) && string.IsNullOrEmpty(pass) && conLimit > 0)) { SqlContext.Pipe.Send("Entered inside the notify method"); HttpWebRequest httpWebRequest = WebRequest.Create(url) as HttpWebRequest; string encoded = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(uname + ":" + pass)); httpWebRequest.Headers.Add("Authorization", "Basic " + encoded); httpWebRequest.Method = "POST"; httpWebRequest.ContentLength = 0; httpWebRequest.ServicePoint.ConnectionLimit = conLimit; // Create an instance of the RequestState and assign the previous myHttpWebRequest // object to its request field. RequestState requestState = new RequestState(); requestState.request = httpWebRequest; SqlContext.Pipe.Send("before sending the notification"); //Start the asynchronous request. IAsyncResult result = (IAsyncResult)httpWebRequest.BeginGetResponse(new AsyncCallback(RespCallback), requestState); SqlContext.Pipe.Send("after BeginGetResponse"); // this line implements the timeout, if there is a timeout, the callback fires and the request becomes aborted ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, new WaitOrTimerCallback(TimeoutCallback), requestState, DefaultTimeout, true); //SqlContext.Pipe.Send("after RegisterWaitForSingleObject"); // The response came in the allowed time. The work processing will happen in the // callback function. allDone.WaitOne(); //SqlContext.Pipe.Send("after allDone.WaitOne();"); // Release the HttpWebResponse resource. requestState.response.Close(); SqlContext.Pipe.Send("after requestState.response.Close()"); } } catch (Exception exception) { SqlContext.Pipe.Send(" Main Exception"); SqlContext.Pipe.Send(exception.Message.ToString()); //TODO: log the details in a error table SendNotifyErrorEmail(exception, null); } } } #endregion #region ResposnseCallBack /// <summary> /// asynchronous Httpresponse callback /// </summary> /// <param name="asynchronousResult"></param> private static void RespCallback(IAsyncResult asynchronousResult) { try { SqlContext.Pipe.Send("Entering the respcallback"); // State of request is asynchronous. RequestState httpRequestState = (RequestState)asynchronousResult.AsyncState; HttpWebRequest currentHttpWebRequest = httpRequestState.request; httpRequestState.response = (HttpWebResponse)currentHttpWebRequest.EndGetResponse(asynchronousResult); SqlContext.Pipe.Send("exiting the respcallBack"); } catch (Exception ex) { SqlContext.Pipe.Send("exception in the respcallBack"); SendNotifyErrorEmail(ex, null); } allDone.Set(); } #endregion }
上面的一种替代方法是使用SQL Server Service Broker,它具有排队机制,可以帮助我们实现异步触发器.但是我们对上述情况有什么解决方案吗?从方法角度来看,我做错了吗?请指导我.
有几件事情可能会成为可能的问题:>不会调用allDone.WaitOne();阻止,直到收到响应,否定所有这些异步的东西的需要/使用?
>即使这确实有效,您是否在一次会话中对此进行测试?您有几个静态成员(类级别)变量,例如public static ManualResetEvent allDone,它们在所有会话中共享. SQLCLR使用共享应用程序域(应用程序域是每个数据库/每个程序集所有者).因此,多个会话将覆盖这些共享静态变量的彼此的值.这是非常危险的(因此,为什么非只读静态变量只允许在UNSAFE程序集中使用).此模型仅在您可以在任何给定时刻保证单个呼叫者时才有效.除了任何SQLCLR技术细节之外,即使您确实设法克服了这个特定问题,我也不确定这是一个好模型.
一个更好,更安全的模型是:
>创建一个队列表来记录这些更改.您通常只需要键列和时间戳(DATETIME或DATETIME2,而不是TIMESTAMP数据类型).
>让触发器记录当前时间和修改到队列表中的行>创建一个存储过程,从队列中获取项目,从最旧的记录开始,处理它们(它肯定可以调用您的SQLCLR存储过程来进行Web服务器调用,但不需要它是异步的,所以删除那些东西并将程序集重新设置为EXTERNAL_ACCESS,因为您不需要/想要UNSAFE).在事务中执行此操作,以便在“处理”失败时不会从队列表中完全删除记录.有时使用带有DELETE的OUTPUT子句将您正在处理的行保留到本地临时表中会有所帮助.
即使调用SQLCLR存储过程需要按行进行,也一次处理多个记录.
>创建一个SQL Server代理程序作业,每分钟执行一次存储过程(或根据需要减少)小问题:
>将输入参数复制到静态变量(例如_mailServer = mailServer;)的模式充其量是没有意义的,并且由于不是线程安全的而且容易出错.请记住,静态变量在所有会话中共享,因此任何并发会话都将覆盖以前的值,从而确保竞争条件.请删除名称以下划线开头的所有变量.>使用Convert.To …的模式也是不必要的,并且对性能有轻微影响.所有Sql *类型都具有Value属性,该属性返回预期的.NET类型.因此,您只需要:string url = weburl.Value;>不需要使用以后需要转换的不正确数据类型.意思是,而不是使用SqlString for connectionLimit,而是使用SqlInt32然后你可以简单地做int connLimit = connectionLimit.Value;>您可能不需要手动执行安全性(即httpWebRequest.Headers.Add(“授权”,“基本”编码);).我很确定你可以使用uname创建一个新的NetworkCredential并传递并将其分配给请求.
查看更多关于在C#中来自SQLCLR的异步API调用中的问题的详细内容...