2019独角兽企业重金招聘Python工程师标准>>>
Background
公司从事GPRS车载终端产品的研发。作为配合,软件这块主要开发车辆管理信息系统,以提供对装有终端车辆的全面服务。
说到这里,我想大家应该明白,这里最核心的就是——车辆。大部分的功能几乎都是围绕着车辆在展开。给购买车机终端的厂家,提供对车辆的有效管理和控制,是系统的主要价值。
现在出现这样的需求,需要有这样一个系统,给车厂的服务部门使用,以便于提高他们的在处理车辆维修时的办公效率(本人目前正参与该系统的开发,系统交互模型如下图)。
其中最为关键的就是服务流程,车主持有手机终端提供报障信息->web终端为其创建一个任务->任务在web终端上派发->维修人员有另外一套系统可以随时协同办公->车主也可以随时跟踪服务的进展,并对服务予以评价。需要【服务车辆】去为【工程车辆】提供维修服务(这两种车辆都会装有我们公司的车载终端,但很明显对既有的两种类型的车辆的生命周期的维护管理都由其他系统承担,而且他们不属于同一个产品线,甚至可以这么讲,不同的服务车也会在不同的产品线,不同厂商的工程车辆也在不同的库)。
What’s the matter?
说白了,对于服务流程的管理并不棘手。为难的是,车辆、位置等相关信息的获取,目前的访问形式,请参见我之前一篇博文。有人认为,只要有交互,web service能够搞定一切。没错,某种情况下,当你需要从别的系统那里获得服务,你可以采用web service。但我认为这不是一种万能药,特别是系统的基础、核心数据的获取都依靠它的话,整个系统的效率可想而知。因为核心数据就类似于系统的基石,你从来都不能大量地依赖web 服务。它带来的不只是性能问题,同时还有数据的同步问题,安全问题等等。如果,你发现有的时候,你不得不这么去构建一个系统,我认为这其中充斥了设计上的“坏味道”。
各自为政or化零为整
当业务发展到一定的程度,我觉得某种程度上的重构不可避免——公司对各个业务系统的协议解析、数据处理的功能都归结于统一的平台;原来各个业务系统对于车机的注册也被一个专门的系统统一管理,某种程度上化零为整是一个趋势,但这也不是绝对的方向。但不得不说正确的调整有利于让结构更加清晰。
数据库设计的变相思维——面向关系与面向对象
数据库设计的经验之谈,之前也看过不少。关系型数据库由来已久,优劣自明,不再多谈。很多有经验的开发或者设计人员,经常有这样的感觉——你只要把需求告诉我,我脑子一转,就能告诉你大概需要哪些表,表中有哪些字段?哪个表的主键作为另一张表的外键等等。这实际上就是某种程度的抽象,业务的抽象映射到关系表。我们现有的车辆表中大概有五六十个字段,其中大概还预留了几十个空字段,以便日后可能新的业务需要(事实上每个产品线的车辆表的设计都大同小异)。不牵涉任何业务,我在逻辑上将这些车辆相关的属性,分为三大类:
(1) 车辆基本属性
(2) 车辆扩展属性
(3) 业务未知属性
分成这三大类,结构就变得异常清晰。
车辆基本属性:是较为重要的和系统交互最频繁的几乎是各个平台共有属性。
车辆扩展属性:说实话这个也几乎是各个平台的共有属性,但和系统的交互程度一般,修改的可能性远小于读取的可能性也远小于车辆的基本属性。
业务未知属性:这个就应对了上面我们所说的那些预留的空字段。这些空字段可能是来自于业务的扩展,也可能来自于实现系统的需要等(这个也是各个业务系统主要的不同所在)。
我觉得对于车辆这种重量级属性的大对象,所有属性一锅端,有失性能。这里就要谈到面向对象设计的一大原则——组合优于继承。个人认为,车辆表的设计,应该按照上面的分类,采用组合的方式,更为轻巧灵活。也就是说类似的车辆表,可以拆分为三张表。应对上面的属性分类,我分别称为表1,表2,表3。表1,表2既然是各个平台的车辆都共有的属性,那么这些表的结构理论上在各个平台都是一致的。而表3,通常来自于各个业务平台,根据自有的业务进行扩展,见下图:
带来的好处?
(1) 优化数据库操作
(2) 为Memcached带来清晰的缓存结构
(3) 有利于与其他平台数据的交互
(4) 有利于Memcached实施不同的缓存失效策略
以下我逐一分析一下这样的拆分带来的好处:
(1) 对于优化数据库操作,以上分类,反映了数据库不同字段的读写频度,可以实现读写分离,很明显表1与应用程序交互的频率最为频繁,特别是某些操作会锁表。这样的隔离,使得对数据库访问得到一定程度的优化。
(2) 对于为Memcached带来清晰的缓存结构,参照下面的结构图:
物理上的多库多表,逻辑上的分类单表
由于分布式对于内部的隔离,保证外部访问的透明。在逻辑上,很多数据都是在内存中的一个集合。对于,不同的业务平台,数据的访问方式很可能是隔离与交互的并存,分布式提供了这样的一个切合点。
隔离:首先数据是物理上的隔离,逻辑上虽然是个大数据集合,但是还是会被访问时用于标识所属平台的(basekey)的唯一性完全隔离。
交互:逻辑上是一个大数据集合,它为我们带来的交互便捷的物理优势。那么应该如何交互?这里连接它们的还是basekey。通常情况下的系统,大家访问自己的数据(用自己的basekey,访问自己的数据集合),互不干扰。当遇到需要我的系统为你做业务支撑,或者像我目前做的一个统一性的服务平台而言。如果我通过授权拿到了你的系统的basekey,你的数据和我的数据有何区别?有了basekey,数据就存在着共享性。
(3) 对于利于与其他平台数据的交互,已经在(2)中的交互中进行了解释。
事实上,上面对于车辆表的行为所做的就是纵向切割技术。而对于横向切割,又是我们业务的一个特殊点——轨迹。对于数据表作横纵切割的优势就在于使表变得更轻量级。纵向切割有益于读写分离,而横向切割有益于优化查询(有时查询需要进行全表扫描,这大大减轻了数据库的压力,还有对于每几十秒上传一条经纬度数据的轨迹表,压力可想而知)。
(4) 对于有利于Memcached实施不同的缓存失效策略,我将在下面谈到!
需要注意的问题
对于同一个库只能怪的这三张拆分过的车辆信息表都有共同的一个基于生成的GUID作为它们的主键,即可建立关联。不必担心数据插入的问题。理论上,表1中都是车辆的固有属性,几乎是在车机注册的时候执行insert操作,此时只需要牵扯到表1(这时用于标识表1中该车辆的GUID已经产生),表2的数据可能在注册完之后,填写相关补充信息的insert操作,表3同样如此。
瓜分数据就牵扯到维护其一致性的问题。很明显,我们不能允许表1,表2中的车辆数据被删除,而表3中的数据存在这样的悲剧发生。事实上,对于车辆表这种核心业务的基表而言,删除的可能性不是很大(它关联这太多的业务信息)。如果真的要删除一条数据(比如所谓的车机报废,或者停止服务等,当然这里还是假设),只能去做逻辑删除(采用某个标识字段),不能允许数据不一致的情况出现。
Memcache使用解析
可能你已经发现,系统中最经常被访问的是两种类型的表:(1)关系表(如车辆和机构的关系表等);(2)实体表(如同这里我们所说的车辆表,轨迹表等)。根据访问频度的潜在联系,我们认为对于Ids的访问频度要大于对于单个实体表本身的访问频度【事实上你会发现,你对于实体表的访问就是由关系表牵引过来的】。所以,对于关系表,我们可以采用Vector Cache的缓存机制,缓存关系的ID集合。接下来我们再按照上面划分出的车辆表,做不同侧策略的Row Cache(这里可以将VectorCache以及Row Cache的机制同样应用于轨迹表)。当然这里做内存式缓存的典型案例就是开源项目——Memcached。我们的Vector Cache以及Row Cache对于区区十几万辆车乃至几十万辆车几乎是100%的命中率!
按照上面的分析,我们可以简单的分析如下的缓存类型:
(1) 关系表(例如,车辆与机构关系表,机构与用户关系表)
(2) 常用核心业务数据(如上分析的表1,表2以及轨迹数据等)
(3) 业务查询(select vi_sim,vi_terminal,vi_guidfrom vehicleInfo where vi_type=2)
对于它们的key可以参考如下的生成机制(这里key的分析对应上面的缓存类别):
首先,这里假设每个平台都有一个属于自己平台的标识简单称之为basekey【基于一定的算法生成】
(1)看关系类型
1. 【一对一】没有必要单独搞出关系
2. 【一对多】—key:md5(basekey_一方的Id);value:多方的id集合
3. 【多对多】—key:md5(basekey_表名);value:关系集合
(2) Key:md5(basekey_主键);value:主键对应的一条数据记录
(3) Key:md5(basekey_业务sql);value:业务数据集合
注:这里对于第三种key,为了避免对于相似的逻辑,产生太多的key。比如,某个sql,你只需要查找一辆车的终端号;另一个sql,你只需要查找一辆车的sim卡号。你大可参考ORM的方式,对于这些采用一个统一的比如select *操作,封装到一个实体对象里。因为数据来自内存,所以对于数据的大小与数据的访问速度上来讲,可以和数据库访问的传统方式做一个平衡。
缓存失效策略分析——分表带来的另一个好处
以下谈到的表1/2/3指的是上面拆分出的车辆信息表
对于表1,车辆的基本属性,可能新增/修改/查询三种操作都较为频繁(删除的这里暂不做讨论,因为本人觉得逻辑删除更为合适)。那么这样的操作,决定了内存数据与DB中数据同步的“强一致性”。因此,这种情况下需要采取的缓存失效策略为——强制失效。比如,在做update操作的时候,强制使得相关的select该表对于的key-value对直接失效。在下次从DB中查找的时候,重新加入到memcache中。
对于表2,车辆的扩展属性,该表可能应对有稍多的select,较少的update,这样该表在内存中相应的缓存失效时间相对于表1就可以较为延长(上面,我已经解释了,虽然表2存储的也是车辆的属性,但它通常都是后期维护到数据库,并且对于该表的查询操作并不是那么频繁)。
对于表3,它的缓存策略几乎达到了设置一个较长的时间自动过期的级别。
我和你有同样的顾虑——安全
有时系统所处的环境,让你不得不考虑,我们的数据是否应该圈定在我们设定好的边界之内。这里业务性质和行业性质的因素可能占有很大的因素。你不得不考虑,事情一旦出了你完全控制的范围,你是否能hold住!我个人认为,我们的轨迹以及和车辆相关的数据,它们集体的边界只要能圈定在公司之内就可以hold住。有些信息,失去了场景和关系就毫无任何作用(当然除非你把一切都得到了)。退一万步而言,这所有的信息都是来自公司给客户提供的服务数据。这算是公司资源,公司用十个资源库或者一个大的资源池来存储数据,对于客户而言都是透明的,一切都为了给客户提供正确的数据。
你总是会把安全挂在嘴边吗? memcache中存储的数据只是你真实表中的一个备份,或者称之为快照。再说,我可没让你对上面三种形式的缓存都采用同一种basekey。但话说回来了,还是必须认真考虑授权的设计!
下面我构思了整个系统的构架原型:
注解:
【Node 1-Node N】:是部署的分布式Memcache的单个节点;
【Business DB1-Business DBN】:是各业务平台自身数据库;
【Memcache Server】:作为所有Node对外的访问点,以屏蔽物理部署细节;
【监听/交互Server】:有两个职责:首先以近似监听端口的方式(可以简单理解为轮询),不停地hit Memcache Server中,需要供其他系统交互的key,如果供交互的业务数据不存在,则从特定的业务数据库抓取,并写入Memcache Server;其次,因为内存作为一种易失性的存储设备并不值得完全信任,如果发现Memcache Server宕机,则接管交互系统对于数据访问的职责,直接提供对数据的访问。这里需要提示的是,该Server具有对所有数据的某些权限,比如只开放查询,不授予删除权限等(这取决于你给该server访问你数据库所使用的账户的角色);
【认证/授权Server】:在系统之间需要交互时其作用,它给需要获取其他平台业务数据的系统授以access token(就是上面提到的basekey);
【Web/Application Server】:作为Memcache client,有两种访问数据场景:(1)访问本系统数据,首先访问MemcacheServer,如果能够命中,则直接获取数据,如果不能命中(或者是做类似的insert、update等操作),则直接操作数据库,如果需要删除Memcache Server中可能不一致的缓存,则予以删除,以保持数据同步;(2)访问其他系统数据,大致走图中红色路线,首先向认证/授权 Server请求access token,通过授权之后,就可以直接访问Memcache Server,通过拿到的access token就可以访问特定的key的数据,如果未命中(很多情况下都是由于Memcache Server宕机引起的,因为监听/交互Server的首要职责就是保证Memcache Server特定的key-value是正常的)。当然这里不管是什么原因引起的,当无法获得数据时,我们通过accesstoken可以直接向监听/交互Server请求数据。
【手机终端(补充)】: 由于图中不太好展示了,这里简单谈谈手机终端对于服务的访问。对于手机终端而言,还是需要采用http GET或者Web service的方式来访问。因此我们需要把用到的Web Service都发布到交互服务器,它会登记所有服务。此时手机终端的访问方式几乎和Web/Application Server中(2)的方式一样,先授权后访问,只不过它直接请求监听/交互Server,由其决定访问memcached还是各个业务平台独立的库。
事实上这里的模型图只是从逻辑和职责上进行区分,实际情况下,考虑经济因素,可以对一些设备加以合并。比如Business DB可以和Node合并,监听/交互Server与认真/授权Server可以构建在同一个物理机器上等等。
重构的代价——复杂度,工作量
除非确定对于私有数据不会提供交互服务,那各自为政也没什么不能接受的。但当你产生这样的业务时,很明显重构平台的核心数据能给这样的业务提供更为简便的抓取信息的渠道。对于这样的重构,将是个不小的工作量的。很明显,实施的唯一方式,只能是两种方式并行使用,逐步更替。
总结
本文以公司业务的某些特定数据表为例,展示了如何基于其进行重构,并解释了重构数据结构带来的好处,以及结合memcache阐述了重构后的数据结构如何提供交互式的服务。下篇会谈谈我认为的改造版的三层架构(考虑增加数据适配层,或者某种可配置的路由形式),它能够对多种数据存储形式(内存分布式、DB、NoSQL)屏蔽细节,以统一的API对数据进行访问。