调试程序的时候,发现会有个别时候会出现取数据库失败的情况,看了堆栈信息,发现报错在Dapper里。出错后的第一反应是:是不是我的用法不对。但是仔细一想,不对啊,这个错误只是偶尔发生,大多数情况下运行都是正常的。然后这个时候一个程序员危险的观念就来了:dapper有问题,在某种情况下会出错。其实,人总是把超越自己理解的东西赋予玄学,感觉把错误放在一个自己不能解决的地方,就落得心安。好在我悬崖勒马,即使制止了自己的这种“甩锅”观念,一个经历了这么多人检验的组件,不应该出这么低级的错误。
来,看源码吧,github dapper仓库里对着报错堆栈一通找,看不出有什么问题。主要也是看不懂。。。到处都是emit,原来dapper快是在这里啊,在IL上做处理,厉害。
心想dapper没错,那还有哪里有错呢。突然灵光乍现,看到了还有一个参数是获取执行的sql。该不会是sql没有拿到吧,里面再写了个try catch,果然,得到了下图的结果。
但是为什么呢,我读取xml的方法是静态的啊,静态的应该在堆栈中只生成一个实例才对,并发也应该是串行排队读取才是。但是debug下来发现,确实是这个静态方法出的问题,是在读取xml时报了“The process cannot access the file because it is being used by another process”的错,明显就是并发读取的时候,另外一个还没有释放的问题。问题明确了,就Stack Overflow一下呗,直接就找到了答案,https://stackoverflow.com/questions/26741191/ioexception-the-process-cannot-access-the-file-file-path-because-it-is-being,说的很详细,再次感慨一下国内网站的解答质量。英语的确是编程的重要素质。
查了微软文档,发现原来问题是因为FileStream不是线程安全的,所以造成了这个问题,手动加上线程锁之后,问题就解决了(做法见下图)。其实Stack Overflow答案中的retry pattern应该也是可以的,但是感觉做法不优雅,遂放弃。
深入思考了下,如果这个线程锁成了性能瓶颈,可以把xml拷贝多个副本,每次读取的时候随机读其中一个,这样就不用线程锁了。进一步还可以不用静态方法,把这个类做成依赖注入,这样在startup.cs中注册为addScope,就还能提升一点吧。
(最后还是有一点不明白的地方,FileStream用在了using区域里,离开using区域之后应该会调用FileStream的IDispose释放才对啊,然后静态方法又只会有一个副本,所以还是应该不会有线程冲突才对。嗯,留待以后更强的自己解答吧)
写完5分钟,突然想到了一个解释:静态是只有一个副本没错,但是可以多个线程同时用一个啊,也就是说,问题出在了,第一个请求线程还在读取xml的时候(就比如运行到了上图中的xml.Deserialize(xmlStream) as T),下一个请求线程运行到了using,这个时候不就给堵上了吗,哈哈哈,没想到我5分钟就变强了,真厉害,哈哈哈