目录
1、简介
本地消息表:这是ebay研发出的一种分布式事务解决方案,通过将消息表存储在数据库中,实现了消息的一致性。
它的核心思想就将分布式事务拆分成本地事务进行处理
本地消息表是利用各系统本地事务来实现分布式事务。
业务系统的执行和消息均放入本地消息表中,确保本地业务执行和消息表的操作在一个事务中。
不尝机制重试读取本地消息表,调用远程应用操作。需要加入重试机制、最大执行次数、报警机制。
本地消息表容忍数据暂时不一致,期望数据的最终一致性。
2、具体业务处理流程
本地消息表是一种分布式事务解决方案,通过将消息表存储在数据库中,实现了消息的一致性。具体来说,本地消息表方案采用BASE原理,保证事务最终一致。在实际系统中,要根据具体情况,判断是否采用(BASE定理的核心思想:即使无法做到强一致性,但是每个应用可以根据自身的业务特定,采用合适的方式来达到最终一致性)。
具体业务处理流程如下:
- 系统收到用户下单请求,将订单业务数据写入订单表中,同时把该订单对应的消息数据写入本地消息表中,订单表与本地消息表为同一个数据库,更新订单和存储消息为同一个本地事务,数据库事务处理,要么都成功,要么都失败。
- 订单服务发送消息到消息队列,库存服务收到消息,进行库存业务操作,更新库存数据。
- 返回业务处理结果,订单服务收到结果后,将本地消息表中的数据设置完成状态或者删除数据
- 另起定时任务,定时扫描本地消息表,看是否有未完成的任务,有则重试
此外,本地消息表适用于基于本地消息表的方案中,将本事务外操作,记录在消息表中。其他事务,提供操作接口。定时任务轮询本地消息表,将未执行的消息发送给操作接口。操作接口处理成功,返回成功标识,处理失败,返回失败标识。定时任务接到标识,更新消息的状态。定时任务按照一定的周期反复执行。例如使用支付宝的支付场景,系统生成订单,支付宝系统支付成功后,调用系统提供的回调接口,回调接口更新订单状态为已支付。回调通知执行失败,支付宝会过一段时间再次调用。
3、本地消息表作为实现分布式事务的优点和缺点
3.1、优点
- 简单可靠:本地消息表是一种简单可靠的方案,通过将消息先存储到本地数据库中,再进行异步发送,可以保证消息的可靠性和一致性。
- 高可用性:由于消息发送是异步的,即使目标服务不可用或出现故障,也不会影响当前事务的提交,提高了系统的高可用性。
- 提升性能:将消息发送过程异步化,减少了事务的等待时间,提升了系统的性能。
- 本地消息表的建设成本较低,可以实现可靠消息的传递,并确保分布式事务的最终一致性。
- 无需提供回查方法,进一步减少了业务的侵入。
- 在某些场景下,还可以进一步利用注解等形式进行解耦,有可能实现无业务代码侵入式的实现。
- 简单、不依赖其他服务的改造、可以很好的配合服务调用和 MQ 一起使用,在大多业务场景下都比较实用。
3.2、缺点
- 需要维护额外的数据表:使用本地消息表需要额外维护一个存储消息的数据表,增加了系统的复杂性。
- 数据库依赖:本地消息表依赖于数据库,如果数据库出现故障或性能问题,会对整个系统的可用性和性能产生影响
- 无法保证实时性:由于消息发送是异步的,无法保证消息的实时性,存在一定的延迟。
- 本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。
- 本地消息表是基于数据库来做的,而数据库是要读写磁盘IO的,因此在高并发下是有性能瓶颈的
需要根据具体业务场景和需求来选择使用本地消息表还是其他分布式事务解决方案。在一些对实时性要求不高、对数据一致性要求较强的场景下,本地消息表是一个简单有效的选择。但对于实时性要求较高、对数据一致性要求更严格的场景,可能需要考虑使用分布式事务管理框架或消息中间件等更复杂的方案。
4、简单demo
下面是一个简单的本地消息表的实现逻辑和代码示例,供参考:
4.1、创建本地消息表(例如使用MySQL数据库):
CREATE TABLE local_message (
id INT(11) NOT NULL AUTO_INCREMENT,
content VARCHAR(255) NOT NULL,
status INT(2) NOT NULL,
retries INT(2) NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
4.2、将消息存储到本地消息表中:
public class LocalMessageService {
// 数据库连接等相关配置
public void saveMessage(String content) {
String sql = "INSERT INTO local_message (content, status) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection(dbUrl, dbUsername, dbPassword);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, content);
pstmt.setInt(2, MessageStatus.PENDING); // 待发送状态
pstmt.executeUpdate();
} catch (SQLException e) {
// 异常处理
}
}
}
1. 异步发送消息并更新本地消息表状态:
public class MessageSender {
private LocalMessageService localMessageService;
public void sendMessage(String content) {
// 异步发送消息到其他服务,发送成功后调用updateMessageStatus方法更新消息状态为已发送
}
public void updateMessageStatus(int messageId, int status) {
String sql = "UPDATE local_message SET status = ?, update_time = CURRENT_TIMESTAMP WHERE id = ?";
try (Connection conn = DriverManager.getConnection(dbUrl, dbUsername, dbPassword);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, status);
pstmt.setInt(2, messageId);
pstmt.executeUpdate();
} catch (SQLException e) {
// 异常处理
}
}
}
1. 消息发送失败后的重试逻辑:
public class MessageSender {
// ...
public void retrySendMessage(int messageId) {
// 根据messageId从本地消息表获取消息信息,并进行重试发送
int retries = getMessageRetries(messageId); // 获取消息重试次数
if (retries < MAX_RETRIES) {
// 执行重试发送逻辑
retries++;
sendMessage(messageContent);
updateMessageRetries(messageId, retries);
} else {
// 超过最大重试次数,标记消息为发送失败状态
updateMessageStatus(messageId, MessageStatus.FAILED);
}
}
}
注意,以上代码只是一个简单示例,实际的实现可能会根据具体需求和技术选型有所不同。在实际应用中,还需要考虑消息发送失败的补偿机制、消息重复消费的幂等性处理等问题,以保证消息的可靠性和一致性。