我以前虽然也做过十多年软件开发,不过做一个真正的产品是最近这五六年的事情了,把D-SMART从一个项目变成一个产品,我们花了5年时间,目前还只是做了个开头。想做一个让不同应用习惯,不同应用场景的用户都略微满意的产品确实太难了。想做好一个数据库产品更是不易,绝对不是找上几个好点子,拉上三五个业内高手,找一些开源组件,赶上三五年就能大成的。因为通用数据库产品将要面对成千上万千奇百怪的用户和场景。经常看到某个DBA说,这数据库厂商太垃圾了了,这么简单的功能都做不好。实际上使用者把数据库研发想得过于简单了,如此复杂的产品,哪怕一个小功能的实现,都不那么容易做好。
昨天一个朋友给我发来一个十分有趣的USE CASE,他们的应用在从Oracle向PG兼容的数据库上迁移的时候遇到了一个令人十分啼笑皆非的问题。我们先来看Oracle上是什么样的。
上述的一个事务大家可能都很常见,也会觉得很正常吧。启动一个事务后,在其中一条SQL语句中出现了一个语法错误,因为t2表不存在。在Oracle中不会影响事务,整个事务可以继续完成。如果换到PG数据库中,会怎么样呢?
什么情况?居然整个事务出现了问题,无法继续执行了。于是我测试了一些发源于PG的数据库产品,包括人大金仓、openGauss等,无一例外,都存在类似的问题。
刚开始的时候我对这个场景也不太理解,问朋友为什么会出现事务中房屋不存在表的问题,他说他们的应用很复杂,有些组件可能没有安装,就会出现某张表不存在的情况,这些问题以前使用Oracle时都不是问题。于是我咨询了几个数据库厂商的架构师,出乎意外的是,他们似乎都知道这个问题,以前也有用户提出过类似的需求。
经过分析我发现,实际上这个问题在PG数据库中也是历史悠久了,PG给出的解决方案是在JDBC引擎中提供一个autosave的参数,如果打开该参数连接数据库,则在每条SQL执行的时候自动生成一个savepoint,当某条SQL出现类似错误的时候,可以作为一个子事务单独回滚,从而不影响父事务。这种在JDBC中启动子事务的做法对性能是有极大影响的,如果某个应用的一个事务中执行的SQL数量很多,那么影响就会更大。在psql客户端、ODBC引擎中也有类似的方法可以实现上述功能。
为了解决客户端对每条SQL都启动一个SAVEPOINT成本太高。于是有人就想到了能否在服务器端直接对每条执行的SQL自动产生一个savepoint,一旦出现类似问题,回滚掉这条SQL就行了。pg_statement_rollback就是一个这样的解决方案。因为PGER发现PG的这个问题在Oracle、DB2等商用数据库中并不存在。因此他们猜测Oracle和DB2是在服务端实现了自动回滚事务。于是模仿Oracle做了一个PG的 插件。我们先来看看这个插件的效果。
pg_statement_rollback支持PG 9.5以上,所以基于低于这个版本的PG内核开发的国产数据库,比如openGauss和早期的人大金仓是无法安装的。
启用了插件之后,似乎还是如此,并没有解决这个问题。这是怎么回事呢?仔细阅读了插件的说明后终于明白了其中的原委。
既然插件的原理是在服务端自动启动了一个savepoint,那么要纠正这个错误就必须手工rollback到这个savepoint。这是pg_statement_rollback假定的那个场景,当一条SQL执行出错,但是这条SQL并不对整个事务的其他部分产生影响的时候,可以通过这个插件来解决。这个技术实现还是把那条语法错误的SQL当成了事务的一部分。不过似乎Oracle的实现并非如此,因为那条SQL执行过程中出现了语法错误,那么这条出错的SQL并未对整个事务产生任何影响。如果把这条SQL不认为是事务的一部分,直接忽略掉这个执行出错,可能更符合应用逻辑。
昨天下午我和一个国产数据库厂商的架构师针对这个问题做了探讨,刚开始的时候他表示这个需求以前有一个用户提出过,因为比较复杂,所以还没有确定是否要开展这方面的研发。他认为在一个事务中不通过子事务来回滚一条SQL语句这个需求最好不是数据库来实现,而是应用自己来实现,回滚掉整个事务是最好的解决方案。
实际上对这个问题,PG社区的产品经理以及一些国产数据库的产品经理都产生了错误的理解。他们把这个问题抽象为“在不使用子事务的前提下在事务中回滚掉某条SQL”。而实际上这个问题并非如此。一个事务中会发生很多种SQL出错,比如部分主键/外键冲突、触发器故障、自增字段故障、某张表无法扩展等,这类出错,回滚掉整个事务是最 佳的处理方法。
而这个CASE与之不同,应该是一种特殊的情况,这条SQL因为语法语义上的错误,实际上并不能执行,不会对整个事务产生任何影响。此时无需回滚任何子事务,而只需要忽略这个错误就可以了。这实际上是一个新的需求,不能和回滚某条SQL的需求混为一谈。
理解了这个场景需求后,和我交流的数据库架构师认为这个需求很合理,而且他们的研发人员在实现该功能上也没有太大的难度。以前这个问题没有得到解决是因为解题的思路出现了问题,今天理顺了这个场景解决这个问题就十分简单了。如果能够把一些像语法、对象、约束的检查和事务状态分离来,可能可以实现一个轻量级的语句级回滚功能。他们会很快把该功能排期到研发中。
这个案例实际上并不复杂,实现起来技术难度也不高。不过PG实现这个功能的时候,对这个场景需求的理解产生了错误,因此在解决该问题的时候走上了一条邪路。从这个简单的场景也可以看出,要做好一个数据库,仅仅依靠优秀的研发人员和数据库产品经理、架构师是完全不够的,在大量的用户场景中不断磨练,才能够让数据库产品变得更加强大。只有长时间在大量的用户中磨练的产品才能真正走向成熟。希望我今天的这个案例能让那些觉得自己数据库产品技术上远远领先于Oracle这些过时年代产品的数据库从业人员能够清醒一些,通用数据库产品能适应用户的各种场景才是王道,做好这一点,不是靠高水平的天才设计出来的,是需要时间来沉淀的。