zoukankan      html  css  js  c++  java
  • 一种异构数据库同步的简单方法

      标题有点高大上,是为了解决实际应用中的一个问题。做了一个Android应用,用于记录日常消费账单,开始是单机版的,我老婆说太low了,起码要能看到彼此的消费情况吧。为此,我还专门写了一套基于protobuf的RPC组件,用于网络通信,http://www.cnblogs.com/zmkeil/p/5176758.html。

      应用本身比较简单,几张简单粗暴的UI,涵盖了增、删、改各种功能,外加一个后台service组件,用于上传账单,并同步他人账单。也算是麻雀虽小五脏俱全吧,看几张效果图。代码见https://github.com/zmkeil/MyBill,可以直接安装使用,不想账单被我偷窥的话,在配置中将服务器地址乱填即可。

            

    要做的事情

      言归正传,按照DBA的工作方式,数据库同步的最简单方法就是把一个库上的所有操作,完完全全地在另一个库上执行一遍,mysql的主从库,就是利用binlog来复制所有的insert、update、delete等操作,来实现同步的,这其实也是一种增量同步的思想。借鉴这一思想,在这个应用中我们主要做两个事情:

    1. 把自己的账单操作上传到服务端;
    2. 把别人的账单操作从服务端同步下来。

    这里为了把复杂度降到最低,我们只定义了两种账单操作:insert、update。在表中增加一个is_deleted字段,update该字段来实现删除功能。
      把握一个最基本的原则,服务端只负责记录账单操作,不保存任何客户端的状态(如已经同步了哪些操作等),换而言之,服务端只提供最基本的需求:1)有新操作来,我把操作写到数据库中,并且把该操作记录下来,供别人同步;2)要同步别人的操作,那你需要提供从哪一条开始同步,最多同步多少条等操作。

    接口预览

    首先看一下protobuf-rpc的service,非常简单,只有一个接口。

    package microbill;
    option cc_generic_services = true;
    
    message Record {
      enum Type {
        NEW = 0;
        UPDATE = 1;
      }
      required Type	type = 1;
      required string	id = 2;
      required fixed32 year = 3;
      required fixed32 month = 4;
      optional fixed32 day = 5;
      optional fixed32 pay_earn = 6;
      optional string gay = 7;
      optional string comments = 9;
      optional fixed32 cost = 10;
    }
    
    message BillRequest {
      required string gay = 1;
    
      // push self's records
      repeated Record records = 2;
    
      // pull other's records
      optional fixed32	begin_index = 3;
      optional fixed32	max_line = 4 [default = 10];
    }
    
    message BillResponse {
      required bool	status = 1;
      optional string	error_msg = 2;
    
      repeated Record	records = 4;
    }
    
    service BillService {
      rpc update(BillRequest) returns (BillResponse);	
    }

    Request有三方面信息:

    • 当前用户是谁
    • 本次要上传的账单操作(可以为空,已经全部上传完了)
    • 需要同步的别人的账单操作的起始index(是以 1,2,3… 这样编号的,具体实现见后面),以及最多同步几个操作(防止数据包过大)。

    Response有两方面信息:

    • 本次上传是否成功
    • 如果别人有新操作的话,records中记录了别人的新操作。

    具体实现

      下面来看具体的实现。android中当然是使用了sqlite来记录账单,服务端则采用mysql。应用本身不需要实时性,但要可靠,不能出现数据重复、缺失等。客户端上程序运行的周期很短,用户可能打开记录一下就关闭了,而且网络也不一定开启。服务端的运行环境就相对稳定很多,程序可以一直运行(只要不crash),网络也较稳定。重点要解决的问题:

    1. 客户端如何知道哪些操作已经成功上传了,还有那些操作等待上传?
    2. 服务端接收到多个用户上传上来的操作,怎么保存、管理,才能方便地供别人来同步?
    3. 客户端怎么记录已经同步了别人的哪些操作?

      第一个问题,主要针对第一个事情。这个比较简单,纯粹是客户端上的事,不需要服务端配合。可以参考mysql的binlog思想,专门以一个records.txt文件来记录所有的操作,格式如下:

    index	    operate	    id

    1、index从1开始,依次递增,唯一标示该次操作。这样就可以用另一个文件updated_index.txt来记录已经上传到了哪一条(是顺序上传的),这个文件非常简单,只要记录一个index即可。那么下次上传时,首先根据updated_index.txt找到需要上传的起始index,然后到records.txt中去找(直接seek到第index行即可),每次默认上传2条操作。仅当response.status = true时,才更新updated_index.txt文件(即原index += 2)。这里有两个问题:

    • 日积月累,records.txt会不会很大,每次上传都从头开始seek会很耗性能。这里我按照月份分隔了,每个月单独记一份文件。服务端就不会有这问题,因为数据是常住内存的。
    • 有时候由于网络问题,一次上传已经到了服务端,服务端做了更新,但是response却没能正确回到客户端,那么客户端就不会更新updated_index.txt,下次会重复上传这些操作记录。没关系,服务端入库时做了去重(很简单,只要使用primary key的特性即可);但是仍然记录到操作列表中了,别的客户端会下载到重复的操作,也没关系,客户端下载时也做了去重。哈哈!这里有点坑,是我老婆发现的。

    2、operate记录操作类型,如前所述,只有insert,update两种操作
    3、id记录了本次操作的的对象(库中的id字段值),如果按照binlog的话,应该记录操作的数据(如cost,comment,day等),但那样会比较复杂。所以只记录了id值,然后再到库中去反查具体的数据。这边有个可优化点:对于update操作,可能只更新了一个字段,但这里会把所有字段全部填写到request中。

    • 补充说明下,这里的id是string类型的,格式“user_year_month_INT”,以用户名、年、月和一个递增的整数组成,这样保证每个用户的id不会相同。

      第二、三个问题,主要针对第二个事情,实际上是客户端和服务端配合,来达到多个客户端间同步的目的。首先说明一下:服务端所有的账单都记录在一张表中,也是以id作为key值,如前所述,这个id值是不对重复的。
    1、服务端按照用户维度,对上传上来的操作记录进行管理。为每一个用户准备一个队列,队列中的元素和客户端上records.txt文件中的每条记录类似。不同的是,这里记录的是别人的操作:每当有用户上传新操作记录来时,服务端首先将该操作写库,然后在所有其他用户的队列中增加上这条操作。
    2、另外每个用户准备一个文件(append模式打开),每条操作记录写到队列之前,先写到文件中。那么服务端重启时,就可以从文件中恢复队列了。
    3、客户请求到来时,首先取出其中的begin_index,max_line字段,然后到他自己的队列中找,如果begin_index已经超过了队列的长度,说明没有新的更新;否则找出max_line条操作记录,根据其中的id到库中反查具体数据,填充response.records(同客户端)。

    • 这里有个问题,如果同时有很多用户,那么除了自己,其他人都是混在一起的。由于只有我和我老婆两个人用,这里就将就了;实际上,可以在队列元素中,多加一个字段user,就可以区分开了。

    4、客户端用一个sync_index.txt文件,记录下次要同步的别人的操作记录index,初始为1,每次response.records不为空时,更新该值(+= respons.records.count())。

    小结

    刚开始想得很简单,不过到现在前前后后快4个月了,呵呵~~ 总算现在有个比较OK的版本了,代码不够严谨,补了又补,功能还行。

    记得刚开始写RPC框架时,热情高涨,每天下班写到凌晨2、3点,那时候正好是最冷的时候,给自己点个赞。后来写android,就比较拖沓了,和用户操作直接相关的,会比较烦。

    到此告一段落。

    附:

    RPC框架,http://www.cnblogs.com/zmkeil/p/5176758.html
    服务端代码,https://github.com/zmkeil/microbill-server.git
    android代码,https://github.com/zmkeil/MyBill.git

  • 相关阅读:
    我的JavaScript之旅——this到底是啥?
    关闭或修改 IIS 443 端口
    UTF8 GBK UTF8 GB2312 之间的区别和关系
    正则表达式符号解释1
    用 Gmail 的 SMTP 发送邮件
    ASCII 码表
    DNN建立前,需要对其进行一些配置
    XAMPP安装和使用教程(图文并茂)
    Visual Studio IDE 实用小技巧
    第二讲 硬件I/O操作
  • 原文地址:https://www.cnblogs.com/zmkeil/p/5463772.html
Copyright © 2011-2022 走看看