问题发生在云课堂(企业云)教学模块,运营多次跟测试、开发反馈——课程发布后,用户前台没有看到课程目录。 开发查看代码和数据后诊断是分布式事务引起的不一致(同一个事务中操作DDB和MongoDB)。 由于发生的概率较低,前几次都是让测试人员重新编辑目录后再发布。但最终还是要想办法彻底解决下这个问题。
发布后的课程数据保存在DDB,后台管理的草稿数据保存在MongoDB
发布课程伪代码如下:
public void publishCourse(){
CourseDraftDoc draft=getMongoDraft();
if(!draft.isDraft()){
return;
}
updateDDB();
updateMongoDraft(PUBLISHED);
}
因为使用了声明式事务,实际上代码应该是这样:
public void publishCourse(){
try {
dbTransactionManager.setAutoCommit(false);
CourseDraftDoc draft=getMongoDraft();
if(!draft.isDraft()){
return;
}
updateDDB();
updateMongoDraft(PUBLISHED);
dbTransactionManager.commit();
} catch (Exception e) {
dbTransactionManager.rollback();
} finally {
dbTransactionManager.setAutoCommit(true);
}
}
以上代码正常情况下会运行的很好
但是下面情况会出现异常
运营反馈的问题,很可能就是这种情况下出现的
事实上处理这种场景下的问题,方案有很多种。
草稿数据迁移到DDB
分布式事务转成本地事务,通过数据库保证ACID
业务保证幂等
在DB表和MongoDB表增加一个版本号字段用于表示最后一次发布的数据是否同步。
public void publishCourse(){
try {
dbTransactionManager.setAutoCommit(false);
CourseDraftDoc draft=getMongoDraft();
Course course=getDDB();
boolean canPublih=draft.isDraft() || course.getPublishVersion().equals(doc.getPublishVersion());
if(!canPublih){
return;
}
String publishVersion=UUID.gen();
updateDDB(publishVersion);
updateMongoDraft(publishVersion,PUBLISHED);
dbTransactionManager.commit();
} catch (Exception e) {
dbTransactionManager.rollback();
} finally {
dbTransactionManager.setAutoCommit(true);
}
}
当重试时,DDB的发布版本和MongoDB的发布版本不一致,会进行重新发布,数据最终一致。
返回给前端用于是否展示发布按钮的canPublish逻辑也要调整下。
这种解决方法看起来也不错,能较好的解决问题,接下来讨论另一种方案,通过事务消息系统来解决。
在课程模块增加一个消息表tx_message,消息数据与业务数据保存在同一数据库,通过本地事务保证ACID。
在本地事务完成后投递消息,投递成功后删除消息表记录。
同时会有一个兜底定时任务(用于处理本地事务完成,消息投递失败的问题)取tx_message表的数据,投递消息成功后,删除消息表记录。
这个方案不好的地方在于不够通用,每个模块需要自行实现。所以重点看下通用方案。
原理也比较简单,类似于二阶段提交实现。
正常执行步骤:
回查执行步骤:
成功提交
失败回滚
回查提交
回查回滚
定时回查
定时回查prepare状态的消息需要注意,不要把正在执行的事务消息查出来(回查消息的prepare时间应该在间隔时间(1分钟)以前)
业务方接入
高可用保证
事务消息系统必须高可用,事务消息prepare、commit、rollback操作,应用自身不涉及状态,水平扩展即可。
回查消息处理并不会很多,为简化设计,我选了主从高可用,而不是使用多主设计。
主从切换,高可用设计原理同《延迟任务调度系统(技术选型与设计)》,这里不多说明。
消息重复投递
事务消息系统在更新消息状态和投递消息到真正的MQ时,有小概率可能会出现重复投递。 事务消息系统不打算解决这个问题,建议消费方业务处理逻辑保持幂等性。因为极端情况下即使投递一次的消息也是有可能出现多次消费的。
其他扩展
以上设计,消息是否在消费方完成消费是未知的,可以引入消费方ACK机制,消费完成后修改消息状态为consumed。 这样可以结合告警系统,对prepare状态且超过一定时间(可配置)的未进入终态的消息发送告警。
大家对事务消息系统设计和实现相关的有什么问题、建议或想法,欢迎popo(hzchenzhiliang@corp.netease.com)联系交流。
长文推荐
本文来自网易实践者社区,经作者陈志良授权发布。