一直以来应用程序中数据的 PUSH/PULL 应用效果,是作为实时系统的关键点。市面上面各种ESB,要么价格不菲,要么相当庞大,入门都够很长时间。有人说还有说消息服务器MSMQ,ActiveMQ(这个比较推荐,客户端语言支持较为丰富),JSM 等还有WebSocket 等等技术。
这些消息组件我都尝试用过,可能技术有限,总感觉不满意,上面说的三个消息服务器,MSMQ 微软的,优缺点大家都知道。ActiveMQ 是跨语言(90%的主流语言均有专门的类库支持)跨平台(Java 源程序) 我用作在一个项目上,客户端有Delphi,有C#(这里又有Web 的 ,WinForm 的程序),用下来消息传递很通畅。不过问题在于 ActiveMQ 一旦跨语言,存在一些复杂数据消息传递会丢失。只能使用String 之类简单类型,不过仅作为消息驱动,我觉得问题不大。
但是还有些实际情况是小项目上面,差不多5W上下的项目上面,客户硬件投入不是太好,有些客户机器上面也存在很多乱七八糟的已建的系统,自己东西越多越容易出问题。 我就想直接通过SQL Server 2008 R2 来进行消息中转,因为在数据库内部,存在有触发器,触发器也就是事件驱动。但问题又来了,如前台如何接收到消息? C# 里面有文件监视对象 FileSystemWatcher,可以对某个文件夹下进行监视,文件夹下面文件变动都可以在C# 程序中得到相应消息。
基本上思路清晰了。
1、SQL Server 中写触发器,一旦监视表发生变动。立即写文件输出
2、C# 编写前端监视器,监视文件变化,一旦文件变化。进行预定逻辑处理。
SQL Server 输出文件,我试过了网上很多,有用脚本对象创建文件。有输出XML的,自我感觉要么不稳定,要么不好维护。于是就用C# 来写扩展SQL的函数。
主要函数如下:
[SqlFunction(IsDeterministic = true, DataAccess = DataAccessKind.Read)]
public static String WriteStringToFile(String FileFullPath, String Contend)
{
try
{
FileInfo Fi = new FileInfo(FileFullPath);
if (!Fi.Directory.Exists)
{
Fi.Directory.Create();
}
using (StreamWriter rw = File.CreateText(FileFullPath))
{
rw.WriteLine(Contend);
rw.Flush();
rw.Close();
}
return "";
}
catch(Exception e)
{
return e.Message;
}
}
在数据库中执行:
开启数据库CLR 支持
--exec sp_configure 'clr enabled', 1;
--开始数据的验证
alter database dbname set TRUSTWORTHY on
--创建数据库的外链程序集
use dbname;
create assembly SQLExtLib FROM 'F:\Pro\Demo\SQLExtLib\SQLExtLib\bin\Debug\SQLExtLib.dll'
WITH PERMISSION_SET = UNSAFE
--关于外链程序集参阅
http://msdn.microsoft.com/zh-cn/library/ms189524.aspx
--此过程搞得很头疼,因为涉及SQL Server 安全,我们编写的类库中方法都要写一些权限的标识,否则挂载不到数据库中。
--而且程序集要进行强名称签名,才能挂接上去。
--至于为什么要这么做?或者其他一些特殊的用法,请参考 Pro.SQL.Server.2005.Assemblies.Dec.2005 这本电子书(网上搜一下,一堆!),里面内容很详细。大体看个条例也就差不多了。差不多我就一早上看看大概,看完加一下午写写DEMO,就出来。详细都没咋个看(现在搞技术,搞得很浮躁啊!)
--建立SQL中函数与程序集函数对应(以便以后直接使用 Select WriteStringToFile('d:\demo\1.txt','Hello World!') 直接调用)
create function WriteStringToFile(@FileFullName as nvarchar(max), @FileContend AS nvarchar(max))
returns nvarchar(max)
with returns null on null input
external name [SQLExtLib].[SQLExtLib.Function.StringHelper.StringFunc].[WriteStringToFile]
GO
OK~ 挂载好数据库外链程序集以后,找个表来测试一下。创建触发器
CREATE TRIGGER [dbo].[UserTableChangedEvent] ON [dbo].[T_SYS_UserInfo]
FOR INSERT, DELETE, UPDATE
AS
BEGIN
DECLARE @Contend AS VARCHAR(100)
DECLARE @FileName AS VARCHAR(MAX)
SET @Contend = CONVERT(VARCHAR(100), GETDATE(), 20) ;
SET @FileName ='D:\\MSG\\'+CONVERT(varchar(12) , getdate(), 112 )+'\\'+ convert(nvarchar(50), NEWID())+'.TXT'
--SELECT * ,
-- 'old'
--FROM DELETED
--SELECT * ,
-- 'new'
--FROM INSERTED
SET @Contend = CONVERT(VARCHAR(100), GETDATE(), 20);
Select dbo.WriteStringToFile(@FileName, @Contend)
END
GO
这个就是我写的测试触发器,仅是为了记录一个消息在特定的文件夹位置,当然你要具体咋个触发,添加触发、修改触发,等等情况,就丰富则个触发器即可,我这里不存在任何业务在里面。
--这里我试验下来,确实写文件使用 OLE 对象来写的话,问题多多,相当不稳定。而且有个很扯淡的事情,如果你的SQL 是64位,调用32位的OLE 对象,呵呵,搞死人的。 这个是血的教训,原来在开发环境中我们调用JavaScript 对象的运算函数,因为其可以混合运算,而且里面还是写脚本来执行。我们就想数据库里面存脚本函数之类,运行时执行,这种相对也灵活。想法很不错,开发环境一样都没问题,但是投入用户环境就不行了,客户数据库这个是64位SQL,我们的数据中调OLE对象直接报错,而且参考文献都找不到。 网上那些大量调用OLE 的SQL 存过这些,大家一定要有个把握。 因为这,也就推荐自己写CLR DLL 要好些了。
闲话扯了些,继续~
--上面看到,每触发一次写一个文件,有人说为什么输出不在一个文件里面? 是不是写一个文件里面,前端无法监听? 情况是这种,无论写一个文件,或者是两个文件,前台都可以获取到消息,主要分开写是为了规避写和读文件的占用问题.。当然也可以控制,需要 写文件函数,还有监视文件 函数两个 对应匹配,想想都是复杂。但是 对我这种懒汉来说,少写一句话,我就绝对不会多去写。到此数据库这边基本完成了,可以手工去改两个数据,看看写了文件没有。
还有一点需注意,有些时候输出文件夹权限要设定给服务有一定的读写权限。比如上面D:\MSG 文件夹 给NetWork Service 读写的权限吧。
--最后写前台的监视
我用的是 文件改变事件 FSW.Changed += new FileSystemEventHandler(FSW_Changed);
也可以使用 Created 事件,等等。。。
public partial class Form1 : Form
{
FileSystemWatcher FSW;
public Form1()
{
InitializeComponent();
FSW = new FileSystemWatcher(@"d:\MSG\");
FSW.EnableRaisingEvents = true;
FSW.Changed += new FileSystemEventHandler(FSW_Changed);
}
Int32 Count = 0;
void FSW_Changed(object sender, FileSystemEventArgs e)
{
if (e.ChangeType == WatcherChangeTypes.Changed)
{
BackgroundWorker NewThread = new BackgroundWorker();
NewThread.DoWork += new DoWorkEventHandler(NewThread_DoWork);
NewThread.RunWorkerAsync(Count);
Count++;
}
}
String TempString = @"
ANONYMOUS:[000000]
BABYLON-5.DIR;1 4 9-MAR-2002 23:35 [CTRL_C,ANOMALY] (RWE,RWED,RE,RE)
CHECK_LICENSES.COM;44
8 7-OCT-2003 13:39 [CTRL_C,ANOMALY] (RWED,RWED,RE,RE)
DECWINDOWS.DIR;1 22 9-MAR-2002 23:49 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE)
DOC.DIR;1 1 10-MAR-2002 00:06 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE)
FLIGHT-SIM.DIR;1 1 10-MAR-2002 00:19 [CTRL_C,ANOMALY] (RWE,RWED,RE,RE)
FREE_NETPROGRAMS.DIR;1
1 10-MAR-2002 00:19 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE)
FTP_TEMP.DIR;1 1 7-NOV-2003 22:42 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE)
GNU-VMS.DIR;1 1 10-MAR-2002 00:23 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE)
GRAPHICS.DIR;1 1 10-MAR-2002 00:37 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE)
IMAGES.DIR;1 2 10-MAR-2002 00:37 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE)
MACINTOSH.DIR;1 1 10-MAR-2002 02:10 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE)
MAILLISTS.DIR;1 1 10-MAR-2002 02:15 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE)
MSDOS.DIR;1 1 10-MAR-2002 02:15 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE)
MUSIC.DIR;1 1 10-MAR-2002 02:16 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE)
PROGRAMMING.DIR;1 1 10-MAR-2002 02:19 [CTRL_C,ANOMALY] (RWE,RWE,RE,RE) ....省略测试数据(约1.3M左右数据,用于写接收到消息后,前端写数据内容)"
void NewThread_DoWork(object sender, DoWorkEventArgs e)
{
Int32 A = 0;
for (int i = 0; i < 100000; i++)
{
A++;
}
StreamWriter rw = File.CreateText(@"D:\LOG\" + e.Argument.ToString()+".txt");
rw.WriteLine(TempString);
rw.Flush();
rw.Close();
}
--接收端也是写成多线程,不容易被高频度消息堵死,也是规避消息并发处理上的问题,保证一个消息来,我就派个线程去做这个消息的事情。否则一个线程里面搞,也做得出来,只是太杀脑细胞了。
void NewThread_DoWork(object sender, DoWorkEventArgs e)
{
Int32 A = 0;
for (int i = 0; i < 100000; i++)
{
A++;
}
StreamWriter rw = File.CreateText(@"D:\LOG\" + e.Argument.ToString()+".txt");
rw.WriteLine(TempString);
rw.Flush();
rw.Close();
}
我这边作了个测试,在SQL 里面写循环写 3000个输出,接收方同样能接收到3000个日志。比起OLE 对象创建文件稳定不少!
declare @i int
set @i=1
while @i<3001
begin
DECLARE @Contend AS VARCHAR(100)
DECLARE @FileName AS VARCHAR(MAX)
SET @Contend = CONVERT(VARCHAR(100), GETDATE(), 20) ;
SET @FileName ='D:\\MSG\\'+CONVERT(varchar(12) , getdate(), 112 )+'\\'+ convert(nvarchar(50), NEWID())+'.TXT'
SET @Contend = CONVERT(VARCHAR(100), GETDATE(), 20);
Select dbo.WriteStringToFile(@FileName, @Contend)
set @i=@i+1
end
最终结果图(左边是SQL 写文件,右边前台接收到又写的日志)
这种小模式的解决方案,可能会对一些特殊情况的处理会是个思路。
我个人比较喜欢这种模式,简单,容易维护。
不喜勿喷,欢迎切磋交流。
源代码不上传,全部都在文章里面。