有一类系统,基本上所有操作都要求在线。在客户端产生的数据,直接提交到服务器,本地不存数据,或者仅保存少量缓存数据。这类应用有一个优势,就是客户端和服务器的数据始终是同步的,罕有两端不一致的情况。但是也有缺点,即对网络条件要求高,在网络条件不好的时候,用户操作需要等待,甚至无法正常使用
我们的一个APP,与上述系统不同,是支持离线操作的。数据在本地和服务器各有一份,然后通过同步机制来保持一致。这样做的缺点是提升了系统的复杂性,因为如果设计有缺陷,就很容易发生两端数据不一致的情况;另外就是如果客户端长期离线,存在数据丢失的风险。当然好处也很明显,即操作不依赖网络,所以非常流畅。即使在无网络条件下,也可以正常使用,只要在网络恢复以后,与服务器做数据同步就可以了
这类数据同步方案的一个常见错误,是客户端依赖服务器的响应结果。我们的老方案就踩了不少坑,本文总结几个常见的场景
比如这个场景:
1、客户端向服务器发起一个请求
2、服务器收到此请求以后,往数据库写入一条记录,返回成功响应
3、客户端收到响应以后,往本地也写入一条记录
看起来这个方案还不错,很直观,并且两端都有了同样的数据。但是这只是理想情况。有一天我们的系统访问量突然增大,导致服务器的负载升高,每个请求都处理得很慢,于是客户端超时了。所以客户端认为此次请求失败,没有往本地写数据,但是其实服务器稍后还是处理了这条请求。于是服务器就比客户端多了一条数据。更糟糕的情况是,客户端认为操作失败,继续反复发起请求,当然毫无例外地全部超时了,于是服务器里就多了N条记录……这是一个很容易模拟重现的场景,只要在服务端的代码里打一个断点,然后等客户端超时以后,再把断点走下去就行了
后来为了解决这个问题,我们试过增加防重复提交的机制,但是也不理想。因为首先,“重复提交”本身就是一个模糊的概念,什么样的提交算是重复提交呢,如果认为用户在同一个界面重复点击是重复提交,那么如果用户关掉窗口再来一次,是否也算重复提交呢?其次,数据同步机制本来就比较复杂,再加一个新的纠错机制,会让系统更加复杂,隐藏更多出BUG的可能性。最后,即使真的有了完美的防重复提交方案,也只能解决“多N条”的问题,还是解决不了“多一条”的问题
最后我们采取的办法是,先往本地插入一条记录,然后再提交到服务器去异步处理。由于是异步的,必然存在一个时间差(特别是客户端离线的时候,时间差还会比较长),所以会有短时间的两端数据不一致,但是只要最终一致,也就达到了目的
再比如这个场景:
1、客户端生成自从上次同步以后的新差量数据
2、将数据发到服务器,服务器处理,返回成功响应
3、客户端收到成功响应以后,删除这批数据
和上一个场景也有类似的问题,服务端处理得慢,导致客户端超时,差量数据没有被清除,但是这些数据实际上已经写入了服务器。于是下次同步的时候,重复的数据就又发到了服务器;此外还有另一个BUG,由于上报的时候,有2份差量数据(上次未清除的,此次新生成的),所以上报也发生了错误
后来我们修改了这2个BUG,首先是服务器收到请求以后,不处理已经处理过的数据;客户端则是每次只上报最新的那份差量数据;另外还被迫加入了错误数据的清理机制……为了修复一个BUG,代码又复杂了很多
其实更好的做法,应该是客户端每次要同步的时候,都重新生成一份新的差量,上报以后,即使没有收到响应,也应该把这份差量删除。这样就避免了客户端堆积多份差量文件的情况。然后接下来,就面临一个选择:在没有收到响应的时候,应该怎么标识哪些数据是“新”的。这个时候,服务器的状态是不可知的(而不是确定的成功或者失败),如果假设服务器操作成功,那么就应该在客户端上把这些数据都标识成旧的,下次不要再提交;相反,如果假设服务器操作失败,就应该保留这些数据的状态,下次再提交上去。前者的风险在于,如果服务器真的处理失败了,客户端又不再提交这些数据,那么这部分数据就再也没有机会同步到服务器。后者的问题在于,服务器可能会重复写入。两相权衡,我们认为前者的问题更严重,因为会直接造成数据丢失,所以最终选择了后面的方案,但是在服务器加上防止重复写入的机制
以上2种场景,本质上都是因为客户端和服务器是分离的,明明是一个操作,却不在一个事务里,并且服务器的状况对于客户端来说有时是不可知的。具体的解决办法,需要根据实际场景分别处理,但是有一点是明确的,即客户端不能依赖服务器的响应,而是需要单独的机制来保证一致性
再延伸一点考虑,为什么在线的应用没有这类问题,就是因为客户端没有数据,所以在整个系统里只有一份数据,自然不存在数据不一致。当然,如果允许多个客户端同时操作,还是需要考虑并发操作时的异常场景。这是另外一个主题,本文不展开。还有一种更复杂的情况,就是有多于2份数据,比如有2个客户端都能操作,并且都和服务器同步,这时候系统的复杂性又大大提升,因为1份数据,什么都不需要做;2份数据,仅需要处理同步;>2份数据,则还需要处理数据合并。比如某个会员,原本的余额是3000。然后在客户端A做了2个操作,余额变成2000;在客户端B做了3个操作,余额变成2500。这时候他的实际余额是多少呢?显然2000和2500都是错误的,需要合并数据,一种思路是客户端定时把数据上报到服务器,在服务器执行合并逻辑,把数据合并到一个中心节点,再下发到各个客户端。这个场景不在本文的讨论范围,以后再单独写