taowen
首发于taowen

我们需要什么样的数据库

解决什么问题

数据库的问题从来就不是数据库自身的问题。从能存取数据的角度,所有的数据库都是合格的。关键的问题是,我们用数据库去满足什么样的业务需求。不同的数据模型,会对上层的业务代码的风格产生非常深远的影响。从开发效率,开发体验的角度,从运行时性能的角度,从数据正确性的角度,从数据安全性的角度,可以解读出非常多元和丰富的信息。对一个数据库来说不能简单地用好或者不好来评价。另外过去的经验也会成为一种束缚,从解决的业务问题本身,到实际使用MySQL,中间是有很多种可能的。当我们习惯了一种现有的开发风格之后,思路很容易被限制在原有的领域里。如果仅仅从问题的本质出发去推导一个解决方案,会发现更广阔的可能性。

所有的信息系统的起点都是数据的写入。无论是一个电子商务系统,需要交易多方进行信息的录入。还是一个简单的内容管理系统需要编辑上传内容。所有的业务系统的起点必然是交易数据的产生或者数据的录入。所以我们会有一个类型的存储叫“主存储”,它保存了source system of record。

主存储的作用是协作的沟通桥梁。多个用户角色在一个系统上进行协作,通过把过去达成的协议记录成一条主存储的记录,从而影响后续的行为的履约。我们可以把主存储的数据想象成线下多方交易里的纸质单据。做一次存储就是相当于线下一次合同的签署。这个步骤签署的协议,决定了后续多方的行为。这里有两种细微地不同地主存储:

  • 交易发生在线下:比如线下已经签约了合同了。线上的电子化档案只是一份信息的拷贝。这个时候过去发生了什么,就应该是什么。扩大一点范围,对自然现象的采集,比如温度传感器,也是类似的。某时某分是多少度,这个是自然发生的,采集虽然是数字化的第一手数据,但是采集本身并不能决定这个值是什么。
  • 线上交易:签约的行为放在了线上。比如审批一个用户是否合法,比如发单。这些操作是否能够通过,也就是操作的结果是由线上系统来决定的。这种人与人之间,人与平台(其实也是人,只是平台代表了具体的人格)之间完全通过线上进行协作,以数字化记录的方式签订“合同”的方式,对于主存储就提出了非常高的要求。

主存储的职责

所以我们可以归纳出,如果主存储要支持好用户们在线上进行协议,需要具备四个基本职责:

  • 支持线上交易,主要的体现就是强制执行业务规则,让参与协作的主体对接下来要发生的事情达成一致,用于指导后续的行为(线上或者线下的行为)。简单的可能只是一个非空的校验,复杂的牵涉到并发情况下状态机的约束保证。
  • 处理并发:这个其实还是强制执行业务规则,只是实现难度更高一点,被单独提出来。当多人同时操作的时候,我们要做到言而有信。答应了的事情(比如请求的响应是success),就要做到(比如实际的数据库里的记录不对)。这种不一致,可以理解为商业上的违约行为。
  • 保存过去真实发生的事情,保存多方什么时候达成过什么样的协议。这个主要体现为单据可以查询上。甚至是可以按历史时间进行回溯查询。
  • 做为其他视图的数据源。比如搜索,可以查询到商家上架的商品。并不代表商品上架要直接写入到搜索引擎里,而是先做为一个商业上的契约落到主存储里。再做为数据源复制到搜索引擎。当然如果主存储也能支持各种各样的检索,承担部分视图的功能也是可以的。但是能够支持与其他存储系统集成,应该是主存储的一个基本职责。

接下来我们以扣费业务为例,来看一下不同的数据库当主存储这个角色的时候,遇到的一些挑战

MySQL

扣费的业务需求如下:

  • 扣费不能把余额扣成负数
  • 同一个单号的扣费不能重复扣。第一次扣成功了,第二次再扣还是返回成功,但是余额不再变化。
  • 要能够让用户查看账户的今日不同类型交易引起的余额变化情况
  • 扣费不单单是直接把钱扣了,得记录为什么订单扣的等其他信息,以用于后续流程的开展,以及对账需求。即使帐这边不记录,订单系统那边也得记录,要不然流程串 不起来。

MySQL 在处理这些需求的时候,遇到哪些挑战呢?

  • 并发修改余额的时候,要保证余额不变成负数。需要把业务逻辑写成SQL,通过单条SQL的原子性来保证并发下的“规则检查”与“实际扣费”这两个操作是原子的。也就是需要把业务逻辑下推到数据库去做。然后数据库的查询语言的能力就限制了业务逻辑的表达能力了。
  • 同一个单号不能重复扣,这个就必须把扣款单给保存起来了。但是余额也要存啊,因为前面还有第一条的业务规则摆在那呢。当需要既更新余额,又保存扣款单的情况下,怎么保证这两个的更新是原子的呢?就必须使用mysql的多表事务了,一方面降低了吞吐,另外一方面也给分库分表带来了挑战。
  • 需要统计今日的余额变化,还要分类型,这个拿主表来做显然是不合适的。但是引入其他的存储去做实时聚合,又会有数据库之间的同步机制问题,可靠性问题和延迟问题。
  • 因为扣费单不仅仅是有扣费相关的字段,还得附加其他的信息,这些信息和上游的交易流程还有关系。这样三天两头就要加字段。而每次加字段都要在半夜避开业务高峰期进行。当分库分表比较多的时候,需要加字段的实例多了,这个过程就更加漫长。导致新需求轻易是不加字段,能用“其他存储”解决就用“其他存储”解决。这样就会导致很多应该进主存储的信息,被放到了其他的地方。给数据的一致性,安全性和可靠性都带来了风险。

另外因为MySQL提供了开放性的读写接口。为了保证数据安全,一般还要再封装一层数据服务。让底层数据库只能由数据服务读写(比如ip白名单),然后把业务规则写到这个数据服务里。再由这个数据服务去提供给更多样化的业务系统使用。这种数据服务的封装往往引起了重复代码,以及多了一层延迟。但是开发者相比存储过程更喜欢自己封装一层。因为数据服务自身无状态,方便扩容。另外可以拿任意自己喜欢的语言来写,不一定要用SQL。

MongoDB

和 MySQL 的情况是非常类似的

  • 余额不能为负通过把操作下推到db来解决。需要组合findAndModify与$inc来完成。同样需要使用数据库的查询语言来表达业务逻辑。同时因为MongoDB相对小众,使得这个限制变得更加痛苦。
  • 不能重复扣这个MongoDB里没有直接的解法,因为不支持多行事务。如果直接把流水和余额存到一个document里来保证一致性,在流水不断增长的情况就会超过上限。
  • 数据同步方面MongoDB还不如MySQL。MySQL基于binlog,canal和kafka也算有一套相对完善的开源方案。相比之下基于 oplog 的方案没有那么成熟。
  • 加字段方面,MongoDB比MySQL强。因为MySQL改表结构要重写已有的行,而MongoDB加字段不需要动到现有的数据。
发布于 2017-11-24

文章被以下专栏收录