公司的程序需要使用一套外部系统,是必须要用,没得商量的那种,这套外部系统Since 2001年,有将近18年的历史,跟它对接用什么方式?说出来你可能不信,竟然是用生成Microsoft Access(简称MSAccess)文件的方式,没错,就是生成mdb文件,往mdb里写数据,然后丢给这套外部系统。
一个mdb,就是一个数据库,文件型数据库,这也是很多我这个年代来的程序员接触的第一个数据库系统,我记得最早的时候我是用VB(不是VB.NET,那时没有.NET)来访问它的。后来我发觉在比较简单的应用场景里,根本就不需要把数据保存到mdb中,直接读写带一定格式的文件比mdb访问快得多;再后来我发现可以用一些压缩算法来生成更小巧的数据存储文件;再后来我发现xml是一种可读性更好的数据存储方式;再后来我发觉sqlite功能蛮强,不比MSAccess差,由于开源跨平台,应用场景更丰富……总而言之,我没有理由再使用mdb来存储数据了。
若干年后(如果今天看的话就是许多许多年前了),我去了一家公司,我所在的部门有一套非常重要的程序,用VC++写的,保存数据的文件的格式竟然就是mdb……VC++使用mdb文件的方式相当繁琐且容易出错,我当时觉得十分不爽,于是努力替换掉它,把它之前需要好几兆空间保存的数据压缩到几十K,你没看错,缩小了两个数量级,读写速度飞快,可这并不是一件令我有多少愉快回忆的事情,你想我这么一个新人努力地把人家辛辛苦苦定下来的东西“推倒重来”,人家会怎么想?
又若干年后,命运又跟我开了个玩笑:对不起,虽然已经是2019年了,你还是得继续通过mdb文件交换数据!我:……
现在公司的程序并非2019年才诞生,早有了,使用.NET Framework访问mdb文件比VC++轻松无数倍,虽然如此,我还是想进一步简化工作,我写了一套帮助类库,让程序无需关心具体操作,只需要准备好数据到一个数据字典中,把数据字典交给我的帮助类库,就自动完成了填充mdb文件的操作,非常方便。
在刚过去的2018年里,我干了一件事情,就是将大量的程序从.NET Framework上迁移至.NET Core,这里边自然遇到了不少障碍,其中一个“无法逾越”的障碍就是mdb的生成!
访问mdb数据库的正统方法是OLEDB接口,OLE这是个古老的概念,它基于COM(公共组件模型),COM的一个重要特点就是:Windows特有,并且,微软没有把它做成跨平台的打算。
我花了很多时间寻找解决方案,未果,看来只能使用一些折中的做法:mdb的生成使用一个公共的Windows服务器来做,我自定义了一套通信规则,编写了生成mdb的服务程序,还提供了一套方便的客户端代码,调用者可以像原先那样,直接一个方法生成mdb,不用关心中间经过了那些网络通信步骤,还是一样的方便。我的程序经过了全面的测试,确认无误,一切看起来非常OK,直到系统正式使用的时候客户反映mdb的生成频频出问题……
马上查看log,发现了这么一个错误:“XXXX正由另一进程使用,因此该进程无法访问此文件。”XXXX便是我要生成的mdb文件,我在生成mdb文件的时候,把模板复制到临时目录一份,名字用GUID,不会重复的,打开,填充数据,关闭,然后将mdb文件返回给客户端。这“正由另一进程使用”是什么意思?我的文件只可能是我这个进程使用啊。
再经过了N次测试,我本地就是重现不出这个问题,只有把程序部署到服务器上才会出现,并且不能每次都重现,我的本地和服务器的差别是IIS Express和IIS的差别,但对IIS这么纷繁复杂的配置,我感觉要从中找出什么端倪来简直是大海捞针,搜索相关信息,无果。(不喜欢Windows服务器很大程度上是不喜欢IIS,对,我就这么旗帜鲜明)
这个问题的本质就是:我关闭了OLEDB的连接后,mdb文件似乎还处于锁定状态,想用程序读取这个mdb文件返回的话就会出错。好,这个难不倒我,我开始寻找一些解决方法。
1,禁用连接池
OLEDB数据库连接默认也是有连接池的,你关闭一个数据库连接,实际上并不是真的关闭它,而是让它返回到池中,准备给下次打开连接时使用。我很快找到了禁用连接池的选项,禁用之,测试,OK,上线!问题依旧。
2,GC.Collect
我虽然开始学.NET的时候就知道垃圾回收,可直接显式地使用这个方法,是第一次,按照微软官方文档的说法,一般情况下并无GC.Collect的必要,调用这个方法能强制释放一些内存资源,也许能强迫数据库真正关闭。但上线后很快发现问题依旧。
3,外部复制
经过尝试,我发觉竟然可以用Windows的copy命令来复制被打开着的mdb,我灵光一闪,对,服务器也可以这么干,于是关闭mdb连接后调用了外部的copy命令,复制生成的mdb文件,再把复制好的这个文件返回给客户端,perfect!果然,客户那边不再反映什么问题了。这个难题难道就这么解决了吗?图样!第二天,客户找到我们,说我们生成的mdb文件缺数据!这是大问题!甚至比生成不了mdb的问题还要打,前者的话他们可以再次尝试,而后者则可能带来业务上的数据错误。我马上到服务器上看,对比了生成的临时文件,我确认了问题还在!我这样copy被锁定的mdb文件是未写入完全的!有缓存,我要想方法禁掉缓存。
4,尝试禁用OLEDB的写入缓存
Windows在将数据存储至磁盘上的时候,其实都不是直接写磁盘,而是使用了一种缓存机制,先写到缓存,再缓存到物理磁盘,这样无疑提高了调用的速度,所以才有了Flush的概念,Flush就是立即将缓存的数据冲掉,也就是强制立即写入磁盘,而我现在要做的,是禁用掉这个缓存,让对mdb的操作更直截了当地写到磁盘上,我费了不少力气,在微软的官方文档上找到了这个选项,但是这回更惨,本地测试报错,原因是并不支持这个选项,可以理解为OLEDB只是接口,究竟支不支持,还得看引擎,访问mdb的这个Jet引擎貌似不行。
5,更换引擎
Microsoft.Jet.OLEDB.4.0变更为Microsoft.ACE.OLEDB.12.0,嗯,具体的安装文件可以在微软官网下载到。ACE.OLEDB.12.0这个应该更新一些,结果还是不行,并且一样地不支持上一点提到的那个选项。
6,等待解锁
既然上面都不是办法,那我就用重试的机制,读取文件失败,抛出异常,捕捉异常,间隔两秒钟,重试读取,再失败,再重试,最多尝试两分钟,两分钟文件总归写入完成了吧?结果还是不行,2分钟还是无法读取,且这回客户那边有些受不了这样的漫长等待了,客户问能不能恢复之前那个有数据缺失的版本?至少能用。
7,到底有没有.NET Core下的访问mdb文件的第三方库
微软不提供支持,我找第三方行不行?很显然,我之前找过,但这次我打算更认真仔细地找。最后找到了这么一篇文章:《Using Microsoft Access in .NET Core》,写于2018年11月,还挺新,难怪我之前没找到。文章提到了,.NET Core可以用ODBC接口访问mdb,但微软只提供了Windows版本的ODBC引擎,Linux版本的却没有提供,想在Linux环境下直接用ODBC访问mdb的话得找第三方的方案,这里就有一个:Access ODBC Driver,但并不免费,并且费用还不低。
看吧,老板是肯定不会同意让我去买这么一个东西的。
8,ODBC
好,既然上面提到ODBC,那我就切换到ODBC接口去,不用什么OLEDB了,过程进展还挺顺利,除了对MSAccess的类型的理解有少许不同外,ODBC和OLEDB接口差别很小,我很快就弄完了。测试,上线!嗯?这次居然貌似可以了。客户用了一天下来,没再发现什么异常,我检查了日志,也没再出现“正由另一进程使用”的问题。这次难道真的好了吗?我已经有点焦头烂额了。
好吧!至少到现在没再出什么问题。尽管我这几天里并不只在处理这个问题,但这个问题却是这几年来最让我掉面子的问题了。第一它很老,第二它看起来很简单,第三它似乎有很多种变通方法。但实际搞起来却被它虐了许多回合,实在令人唏嘘。
半年前一个许多年前的老同事突然QQ我,说他在解决一个技术问题的时候搜到了我的博客,于是想起了我,挺怀念当初跟我学技术的日子,因为2018年了,他居然在做VB,没错,不是VB.NET,是VB,本文开头提到的那个VB,这是一套极其古老的系统,他还在维护着,我能理解他的苦闷,但这就是生活。