Nodejs 分布式事务

  事务是恢复和并发控制的基本单位,保证 ACID :原子性、一致性、隔离性、持久性。

  对于全是异步的 Nodejs 而言, 并不适合做事务操作:

  代码书写上:

  try ... catch ... 是写给人看的,但是属于同步方法,局限性很大。

  callback 简直是噩梦。

  Promise.then(...).catch(...) 相对而言好一点。

  ES7 的 async ... await ... 比较清爽,使用 Babel 编译,这是笔者目前找到的最人类的方式,但是和原来的 Promise 混合使用的时候有时候会出问题,因为编译之后的代码没法看,最后还得重构回 Promise 。

  异步:

  异步导致有可能有意料之外的 uncaughtExceptionError 。

  对于 JAVA/C++ 这样语言,出错能直接转到 catch 中,但是 Node 不是, uncaughtExceptionError 将直接导致处理链断掉,你只能通过其他方式保证数据一致性。

  虽然 Node 做事务相当非人类,但是考虑开发效率 / 成本,使用 Node 进行开发并不比换语言开差,毕竟事务只有核心业务需要用到。

  单点

  单机的事务相当容易保证,特别在依赖 MySQL 或者其他关系数据库时。

  Nodejs 有 ORM (如 Sequelize ) 支持事务,也可以直接使用 PROCEDURE/FUNCTION。

  两者各有优势:

  ORM 适合复杂逻辑的事务;

  存储过程可以有效减少 IO 次数,防止使用 ORM 时回滚失败。

  实际开发过程中可以将两者结合起来一起使用,使用 ORM 完成逻辑,使用存储过程减少 IO 次数。

物联网

  分布式

  对于分布式系统,相信很多人都知道 CAP 理论,即任何一个分布式系统无法同时满足:

  Consistency (一致性)

  Availability (可用性)

  Partition tolerance (分区容错性)

  但是实际上 Consistency 是任何一个系统都不可能放弃的, 分布式事务 亦是为了保证数据一致性,有时候为了妥协另外两个特性,会放弃 强一致性 ,保证 最终一致性。

  解决方式

  目前业界有很多解决分布式事务的方案,根据对数据一致性的强弱要求,可以选择不同的方案,但是解决思路大致如下:

  两阶段提交

  如 XA 协议(TM(事务管理器)和RM(资源管理器)之间的接口)。

  假设有 A、B、C 三个操作,第一阶段,等待 A B C 均就绪,第二阶段,提交 A B C;如果第一阶段 A 失败了,则第二阶段回滚 B C。

物联网

  本地事务

  使用本地消息表,将远程事务拆分成一个个本地事务,写入本地表中,然后 定时 / 使用 MQ 通知事务方。

  两者各有利弊,定时扫描可能大部分时候都在做无用功,而只使用 MQ 可能会有失败 / 多次消费的问题。

  使用回滚接口

  如 A B 两个接口,串行处理,B 失败了回滚 A ,但是回滚也可能失败,所以也需要使用本地事务表 / MQ。

物联网

  使用 Node 开发,1 比较重型,不适合;2 和 3 是比较好的选择:

  选择一款可靠的 MQ 服务(单次消费 / 失败重试);

  拆分本地事务;

  不能拆分的事务,保证回滚。

  举个栗子

  做一个抢购系统,用户使用虚拟币进行抢购,虚拟币是另外一套系统。为了考虑到公平,每个用户还可能要限制购买上限。

  这样用户一次抢购的完整流程如下:

  检查购买上限

  检查总数

  扣除虚拟币

  写入数据库

  需要事务保证的地方就是 3 和 4,3 是远程事务,4 是本地事务,此栗子中必然是串行操作,3 在前,4 在后。

  0x0001

  这个时候流量很少,并发不高,将 3 和 4 作为一个事务,保证一起成功,而失败一起回滚。

  事务 4 即使使用 ORM 完成,也能完成功能,这个时候系统能很好的工作。

  0x0010

  流量上升中,抢购的商品变多,并发也变大,这个时候,考虑使用 Redis 来提高性能了(牺牲强一致性):

  将 购买上限 与 总数 写入 Redis,在压力转嫁到数据库之前就挡掉,由于 Redis 的强大性能,可以假设 Redis 等同于内存操作,做好回滚就可以了。