江苏城乡建设职业学院官方网站,网站营销外包如何做,蚌埠网站优化,前端网页代码模板“ 在分布式系统中#xff0c;同时满足“一致性”、“可用性”和“分区容错性”三者是不可能的。分布式系统的事务一致性是一个技术难题#xff0c;各种解决方案孰优孰劣#xff1f; 老司机介绍 丁浪#xff0c;现就职于某垂直电商平台#xff0c;担任技术架构师。关注高并… “ 在分布式系统中同时满足“一致性”、“可用性”和“分区容错性”三者是不可能的。分布式系统的事务一致性是一个技术难题各种解决方案孰优孰劣 老司机介绍 丁浪现就职于某垂直电商平台担任技术架构师。关注高并发、高可用的架构设计对系统服务化、分库分表、性能调优等方面有深入研究和丰富实践经验。热衷于技术研究和分享。 在OLTP系统领域我们在很多业务场景下都会面临事务一致性方面的需求例如最经典的Bob给Smith转账的案例。传统的企业开发系统往往是以单体应用形式存在的也没有横跨多个数据库。 我们通常只需借助开发平台中特有数据访问技术和框架例如Spring、JDBC、ADO.NET结合关系型数据库自带的事务管理机制来实现事务性的需求。关系型数据库通常具有ACID特性原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability。 而大型互联网平台往往是由一系列分布式系统构成的开发语言平台和技术栈也相对比较杂尤其是在SOA和微服务架构盛行的今天一个看起来简单的功能内部可能需要调用多个“服务”并操作多个数据库或分片来实现情况往往会复杂很多。单一的技术手段和解决方案已经无法应对和满足这些复杂的场景了。 分布式系统的特性 对分布式系统有过研究的读者可能听说过“CAP定律”、“Base理论”等非常巧的是化学理论中ACID是酸、Base恰好是碱。这里笔者不对这些概念做过多的解释有兴趣的读者可以查看相关参考资料。CAP定律如下图 在分布式系统中同时满足“CAP定律”中的“一致性”、“可用性”和“分区容错性”三者是不可能的这比现实中找对象需同时满足“高、富、帅”或“白、富、美”更加困难。在互联网领域的绝大多数的场景都需要牺牲强一致性来换取系统的高可用性系统往往只需要保证“最终一致性”只要这个最终时间是在用户可以接受的范围内即可。 分布式事务 提到分布式系统必然要提到分布式事务。要想理解分布式事务不得不先介绍一下两阶段提交协议。先举个简单但不精准的例子来说明 第一阶段张老师作为“协调者”给小强和小明参与者、节点发微信组织他们俩明天8点在学校门口集合一起去爬山然后开始等待小强和小明答复。 第二阶段如果小强和小明都回答没问题那么大家如约而至。如果小强或者小明其中一人回答说“明天没空不行”那么张老师会立即通知小强和小明“爬山活动取消”。 细心的读者会发现这个过程中可能有很多问题的。如果小强没看手机那么张老师会一直等着答复小明可能在家里把爬山装备都准备好了却一直等着张老师确认信息。更严重的是如果到明天8点小强还没有答复那么就算“超时”了那小明到底去还是不去集合爬山呢 这就是两阶段提交协议的弊病所以后来业界又引入了三阶段提交协议来解决该类问题。 两阶段提交协议在主流开发语言平台数据库产品中都有广泛应用和实现的下面来介绍一下XOpen组织提供的DTP模型图 XA协议指的是TM事务管理器和RM资源管理器之间的接口。目前主流的关系型数据库产品都是实现了XA接口的。JTA(Java Transaction API)是符合X/Open DTP模型的事务管理器和资源管理器之间也使用了XA协议。 本质上也是借助两阶段提交协议来实现分布式事务的下面分别来看看XA事务成功和失败的模型图 在JavaEE平台下WebLogic、Webshare等主流商用的应用服务器提供了JTA的实现和支持。而在Tomcat下是没有实现的其实笔者并不认为Tomcat能算是JavaEE应用服务器这就需要借助第三方的框架Jotm、Automikos等来实现两者均支持spring事务整合。 而在Windows .NET平台中则可以借助ado.net中的TransactionScop API来编程实现还必须配置和借助Windows操作系统中的MSDTC服务。如果你的数据库使用的mysql并且mysql是部署在Linux平台上的那么是无法支持分布式事务的。 由于篇幅关系这里不展开感兴趣的读者可以自行查阅相关资料并实践。 总结这种方式实现难度不算太高比较适合传统的单体应用在同一个方法中存在跨库操作的情况。但分布式事务对性能的影响会比较大不适合高并发和高性能要求的场景。 提供回滚接口 在服务化架构中功能X需要去协调后端的A、B甚至更多的原子服务。那么问题来了假如A和B其中一个调用失败了那可怎么办呢 在笔者的工作中经常遇到这类问题往往提供了一个BFF层来协调调用A、B服务。如果有些是需要同步返回结果的我会尽量按照“串行”的方式去调用。如果调用A失败则不会盲目去调用B。如果调用A成功而调用B失败会尝试去回滚刚刚对A的调用操作。 当然有些时候我们不必严格提供单独对应的回滚接口可以通过传递参数巧妙的实现。 这样的情况我们会尽量把可提供回滚接口的服务放在前面。举个例子说明 我们的某个论坛网站每天登录成功后会奖励用户5个积分但是积分和用户又是两套独立的子系统服务对应不同的DB这控制起来就比较麻烦了。解决思路 把登录和加积分的服务调用放在BFF层一个本地方法中。当用户请求登录接口时先执行加积分操作加分成功后再执行登录操作如果登录成功那当然最好了积分也加成功了。如果登录失败则调用加积分对应的回滚接口执行减积分的操作。 总结这种方式缺点比较多通常在复杂场景下是不推荐使用的除非是非常简单的场景非常容易提供回滚而且依赖的服务也非常少的情况。 这种实现方式会造成代码量庞大耦合性高。而且非常有局限性因为有很多的业务是无法很简单的实现回滚的如果串行的服务很多回滚的成本实在太高。 本地消息表 这种实现方式的思路其实是源于ebay后来通过支付宝等公司的布道在业内广泛使用。其基本的设计思想是将远程分布式事务拆分成一系列的本地事务。如果不考虑性能及设计优雅借助关系型数据库中的表即可实现。 举个经典的跨行转账的例子来描述。 第一步伪代码如下扣款1W通过本地事务保证了凭证消息插入到消息表中。 第二步通知对方银行账户上加1W了。那问题来了如何通知到对方呢 通常采用两种方式 采用时效性高的MQ由对方订阅消息并监听有消息时自动触发事件采用定时轮询扫描的方式去检查消息表的数据。 两种方式其实各有利弊仅仅依靠MQ可能会出现通知失败的问题。而过于频繁的定时轮询效率也不是最佳的90%是无用功。所以我们一般会把两种方式结合起来使用。 解决了通知的问题又有新的问题了。万一这消息有重复被消费往用户帐号上多加了钱那岂不是后果很严重 仔细思考其实我们可以消息消费方也通过一个“消费状态表”来记录消费状态。在执行“加款”操作之前检测下该消息提供标识是否已经消费过消费完成后通过本地事务控制来更新这个“消费状态表”。这样子就避免重复消费的问题。 总结上诉的方式是一种非常经典的实现基本避免了分布式事务实现了“最终一致性”。但是关系型数据库的吞吐量和性能方面存在瓶颈频繁的读写消息会给数据库造成压力。所以在真正的高并发场景下该方案也会有瓶颈和限制的。 MQ非事务消息 通常情况下在使用非事务消息支持的MQ产品时我们很难将业务操作与对MQ的操作放在一个本地事务域中管理。通俗点描述还是以上述提到的“跨行转账”为例我们很难保证在扣款完成之后对MQ投递消息的操作就一定能成功。这样一致性似乎很难保证。 先从消息生产者这端来分析请看伪代码 根据上述代码及注释我们来分析下可能的情况 操作数据库成功向MQ中投递消息也成功皆大欢喜操作数据库失败不会向MQ中投递消息了操作数据库成功但是向MQ中投递消息时失败向外抛出了异常刚刚执行的更新数据库的操作将被回滚 从上面分析的几种情况来看貌似问题都不大的。那么我们来分析下消费者端面临的问题 消息出列后消费者对应的业务操作要执行成功。如果业务执行失败消息不能失效或者丢失。需要保证消息与业务操作一致尽量避免消息重复消费。如果重复消费也不能因此影响业务结果 如何保证消息与业务操作一致不丢失 主流的MQ产品都具有持久化消息的功能。如果消费者宕机或者消费失败都可以执行重试机制的有些MQ可以自定义重试次数。 如何避免消息被重复消费造成的问题 保证消费者调用业务的服务接口的幂等性通过消费日志或者类似状态表来记录消费状态便于判断建议在业务上自行实现而不依赖MQ产品提供该特性 总结这种方式比较常见性能和吞吐量是优于使用关系型数据库消息表的方案。如果MQ自身和业务都具有高可用性理论上是可以满足大部分的业务场景的。不过在没有充分测试的情况下不建议在交易业务中直接使用。 MQ事务消息 举个例子Bob向Smith转账那我们到底是先发送消息还是先执行扣款操作 好像都可能会出问题。如果先发消息扣款操作失败那么Smith的账户里面会多出一笔钱。反过来如果先执行扣款操作后发送消息那有可能扣款成功了但是消息没发出去Smith收不到钱。除了上面介绍的通过异常捕获和回滚的方式外还有没有其他的思路呢 下面以阿里巴巴的RocketMQ中间件为例分析下其设计和实现思路。 RocketMQ第一阶段发送Prepared消息时会拿到消息的地址第二阶段执行本地事物第三阶段通过第一阶段拿到的地址去访问消息并修改状态。细心的读者可能又发现问题了如果确认消息发送失败了怎么办 RocketMQ会定期扫描消息集群中的事物消息这时候发现了Prepared消息它会向消息发送者确认Bob的钱到底是减了还是没减呢如果减了是回滚还是继续发送确认消息呢RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。如下图 总结据笔者的了解各大知名的电商平台和互联网公司几乎都是采用类似的设计思路来实现“最终一致性”的。这种方式适合的业务场景广泛而且比较可靠。不过这种方式技术实现的难度比较大。目前主流的开源MQActiveMQ、RabbitMQ、Kafka均未实现对事务消息的支持所以需二次开发或者新造轮子。比较遗憾的是RocketMQ事务消息部分的代码也并未开源需要自己去实现。 其他补偿方式 做过支付宝交易接口的同学都知道我们一般会在支付宝的回调页面和接口里解密参数然后调用系统中更新交易状态相关的服务将订单更新为付款成功。同时只有当我们回调页面中输出了success字样或者标识业务处理成功相应状态码时支付宝才会停止回调请求。否则支付宝会每间隔一段时间后再向客户方发起回调请求直到输出成功标识为止。 其实这就是一个很典型的补偿例子跟一些MQ重试补偿机制很类似。 一般成熟的系统中对于级别较高的服务和接口整体的可用性通常都会很高。如果有些业务由于瞬时的网络故障或调用超时等问题那么这种重试机制其实是非常有效的。 当然考虑个比较极端的场景假如系统自身有bug或者程序逻辑有问题那么重试1W次那也是无济于事的。那岂不是就发生了“明明已经付款却显示未付款不发货”类似的悲剧 其实为了交易系统更可靠我们一般会在类似交易这种高级别的服务代码中加入详细日志记录的一旦系统内部引发类似致命异常会有邮件通知。同时后台会有定时任务扫描和分析此类日志检查出这种特殊的情况会尝试通过程序来补偿并邮件通知相关人员。 在某些特殊的情况下还会有“人工补偿”的这也是最后一道屏障。 小结 上诉的几种方案中笔者也大致总结了其设计思路优势劣势等相信读者已经有了一定的理解。其实分布式系统的事务一致性本身是一个技术难题目前没有一种很简单很完美的方案能够应对所有场景。具体还是要使用者根据不同的业务场景去抉择。 ▽ 延展阅读点击标题 从PHP到Node聊一聊淘宝首页背后的技术专访Java Champion程序员如何写出更好的代码走进日本站在路口目送你走到路的尽头 本文系InfoQ原创首发未经授权谢绝转载。 内容转载自公众号 InfoQ 了解更多