当前位置: 首页 > 后端技术 > Node.js

Nodejs分布式事务

时间:2023-04-03 19:13:08 Node.js

事务是恢复和并发控制的基本单元,保证ACID:原子性、一致性、隔离性、持久性。对于全异步的Nodejs来说,不适合做事务操作:在代码写法上:try...catch...写的是给人看的,但是是一种同步方式,局限性很大。回调是一场噩梦。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次数。Distributed对于分布式系统,相信很多人都知道CAP理论,即任何分布式系统都不能同时满足:一致性(consistency)可用性(availability)分区容错性(partitionfaulttolerance),但实际上一致性是任何系统。放弃是不可能的。分布式事务也是为了保证数据的一致性。有时为了折中其他两个特性,会放弃强一致性来保证最终一致性。解决方案目前业界对于分布式事务的解决方案有很多。根据对数据一致性的强弱需求,可以选择不同的方案,但方案大致如下:两阶段提交如XA协议(TM(事务管理器)和RM(资源管理器之间的接口))。假设有A、B、C三个操作,第一阶段,等待ABC就绪。第二阶段提交ABC,如果第一阶段A失败,第二阶段回滚BC。本地事务使用本地消息表,将远程事务拆分成本地事务,写入本地表,然后定时/使用MQ通知事务方。两者各有优缺点。常规扫描可能大部分时间都在做无用功,而只使用MQ可能会出现失败/多次消费的问题。使用回滚接口比如A和B两个接口,串行处理,B回滚A失败,但是回滚也有可能失败,所以还需要使用本地事务表/MQ。用Node开发,1比较重,不适合;2和3是更好的选择:选择靠谱的MQ服务(单次消费/失败重试);拆分本地交易;不能分割的交易,保证回滚。举个栗子,做一个抢购系统,用户使用虚拟货币进行抢购,虚拟货币是另外一个系统。为了公平起见,每个用户可能还需要限制购买限额。这样,用户一次性抢购的完整流程是:查看购买限额,查看总金额,扣除虚拟货币,写入数据库。事务保证是3和4,3是远程事务,4是本地事务,这个栗子肯定是串口操作。前面3个,后面4个。0x0001此时流量很小,并发不高。把3和4当做一个事务,保证一起成功,失败一起回滚。即使使用ORM完成事务4,也可以完成功能,此时系统可以正常工作。0x0010随着流量的增加,抢购的商品越来越多,并发量也越来越大。这时候可以考虑使用Redis来提高性能(牺牲强一致性):将购买限额和总金额写入Redis,在压力传递到数据库之前进行阻塞。由于Redis强大的性能,可以假设Redis相当于内存操作,做好回滚就足够了。同时可以将事务4重组为PROCEDURE,防止ORM可能的回滚失败。0x0011流量太大,数据库无法处理。加入MQ服务:使用Redis抗流量,使用MQ抗压,使用PROCEDURE减少IO。0x0011看起来是个靠谱的系统,但是还是有隐患:uncaughtExceptionError或者程序崩溃,影响最终的一致性,导致Redis的数据和Database的数据在抢购期结束时不一致。0x0100增加最终一致性保证。被抢购的栗子有一个很特别的地方,那就是总量限制。达到上限后,只需要处理MQ中的部分,所以可以巧妙的利用时间差,即达到上限后,对数据库进行快照,延时一段时间后,对比数据库再次判断数据是不一致还是逻辑正常。这是一种机会主义的做法,可以在一定程度上保证最终的一致性。当然,人是最可靠的。如果程序无法处理,需要手动修复,ORZ~。最后,分布式事务是一个很大的话题,根据业务的大小可以给出很多实现。Nodejs勉强能做分布式事务,异步的矿很多,但是靠好的设计和逻辑是可以实现的。