背景
最近写在多进程任务里操作sqlite的时候,发现数据同步是个头疼的问题。因为sqlite本身并不支持存储过程(procedure),它本身也没有可以单独调用对数据表的锁(可能是我没找到,如果有人知道还请赐教)。这就意味着在执行一系列修改数据库命令的时候会被打断,造成最后存在数据库中的数据失真。
比如说,你的银行账户里还剩100元,你存入200元,支出300元,存入100元,假设它们都是几乎同时发生的(在数据延时足够大的时候或者数据操作的耗时足够大的时候,操作的耗时是可以忽略不计的)。那么你的银行账户里还剩多少钱?如果顺序执行,那么这个事情的结果必然是还剩100元。但是在操作sqlite数据库的时候中并非总是如此,它可能存在几种执行的顺序如下:
- 存入200->支出300->存入100
- 存入200->存入100->支出300
- 存入100->支出300->存入200
- 存入100->存入200->支出300
- 支出300->存入100->存入200
- 支出300->存入200->存入100
假设执行了第一种顺序,因为数据不同步那么就可能存在支出300元的时候,另一个进程不知道,而只知道已经存入银行了200,然后又存入100,那么执行完毕后银行里剩下400元。账户就无缘无故多了300元的数据(其他顺序产生的可能请自己列举)。这就需要一个能够保证程序能够正确执行的机制。这个例子也许会让人感觉不太可能发生,因为实际在用的银行系统在设计时已经考虑了数据同步问题。那么再举一个比较容易发生和理解的例子。在大型QQ群里报数。有兴趣的可以找个QQ群尝试一下,看看能不能够快速地次序不乱地完成报数。
分析
以上的问题都产生于自于一个最基本的问题,就是数据同步的问题。一旦数据在任何地方产生都是瞬时完成的,就根本不需要考虑这个问题了。但是现实往往不是理想状态的,由于数据传输时候中转路由器可能产生的拥堵,会导致先发出的数据信号后执行,而先产生的数据后抵达。而且sqlite的锁机制也会导致这种现象发生。
这种问题很早就有了学术上的解决方案。其中一种方案是基于CS架构,再配合可靠的时间同步系统。首先任务会带着时间戳从客户端发送给服务端,服务端并不是马上执行客户端的请求。它会在接受到客户端的第一次请求后等待一段时间。在等待一个周期后,把所有这段时间内获取的客户端请求根据时间戳进行排序。然后按照时间戳顺序执行命令,即可保证每次执行的命令都是正确的。当然这种方案最关键的地方在于时间同步系统,还有整个架构是基于CS架构的。客观上,每个客户端都需要弄时间同步是一件比较繁琐的事情。在人力不充裕的情况下是没办法考虑的。而CS架构让很多人又爱又恨。爱的是它的天生优势,能把很复杂的操作都留给一台或者少数几台服务器去考虑。恨的是它的维护成本过大,由于它每次功能上的改动要考虑各个方面,比如负载均衡,函数扩展性,通信接口,数据加密与解析等。
学术上的解决方案有一点是可以借鉴的,那就是它的延时执行功能。稍微改一下原本的延时执行功能,数据库操作一个临时,等待一个周期检查对应的操作执行结果是否与预期一致,如果一致那么把临时结果赋给正式结果。如果执行结果不一致那么操作执行失败。最后设计出来的程序可能并不能保证是按照顺序执行的,但至少可以保证每次执行都获得了正确的结果。
总结
延时利用得好是可以解决很多问题的。但是如果利用不好就容易陷入无限等待的状态中。目前利用这个方法运行的程序已经稳定地跑了几十万次了,而且运行状况出乎意料的好。
PS.可能有人要问,为什么要用并不完善的sqlite来做数据库。这个不是这篇文章需要讨论的问题,但是我要说一句,任何问题都会有产生的背景,如果你没选择的时候,只能想办法去解决问题。