1.背景
分布式事务改造选型调研。参考资料:《Spring Cloud微服务全栈技术与案例解析》第16章。
2.全部代码
GitHub - kickTec/springCloudDemo at transaction-activemq
3.流程图
4.组件
eureka
TRANSACTION-MQ-SERVICE(message service)
TRANSACTION-MQ-TASK(消息发送系统)
HELLO-SERVICE(类似house service)
FEIGN-CONSUMER(类似substitution service)
activemq
redis
5.数据库
CREATE TABLE `transaction_message` (
`id` bigint(64) NOT NULL,
`message` varchar(1000) NOT NULL COMMENT '消息内容',
`queue` varchar(50) NOT NULL COMMENT '队列名称',
`send_system` varchar(20) NOT NULL COMMENT '发送消息的系统',
`send_count` int(4) NOT NULL DEFAULT '0' COMMENT '重复发送消息次数',
`c_date` datetime NOT NULL COMMENT '创建时间',
`send_date` datetime DEFAULT NULL COMMENT '最近发送消息时间',
`status` int(4) NOT NULL DEFAULT '0' COMMENT '状态:0等待消费 1已消费 2已死亡',
`die_count` int(4) NOT NULL DEFAULT '0' COMMENT '死亡次数条件,由使用方决定,默认为发送10次还没被消费则标记死亡,人工介入',
`customer_date` datetime DEFAULT NULL COMMENT '消费时间',
`customer_system` varchar(50) DEFAULT NULL COMMENT '消费系统',
`die_date` datetime DEFAULT NULL COMMENT '死亡时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
6.调用过程
6.1.请求hello服务的updateUser接口,此接口会更新用户信息(若无则创建)。
6.2.hello服务本地操作完毕后,再将请求信息通过transaction-mq-service持久化到数据库中,状态待消费。
6.3.transaction-mq-task服务查询到待消费消息,将消息发送到mq。
6.4.feign-consume监听到消息,对消息进行消费,若成功,再手动确认消息,同时通过transaction-mq-servic确认消息,将消息状态修改为已确认。
6.5.若消费时出现异常,或其它状态,activemq会重发消息,消费业务注意幂等性;超过一定次数,消息会进入到activemq的死信队列,同时transaction-mq-service会自动将消息状态修改为已死亡(这部分需要人工介入,可以适当增加对死亡消息的管理)。
7.关键代码
transaction-mq-task关键代码(重点:对大量消息数据进行并发处理)
public void start() {
Thread th = new Thread(new Runnable() {
public void run() {
while(true) {
// 获取redis分布式锁
final RLock lock = redisson.getLock("transaction-mq-task");
try {
lock.lock();
// 开始处理待消费消息
System.out.println("开始发送消息:" + DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
int sleepTime = process();
if (sleepTime > 0) {
Thread.sleep(10000);
}
} catch (Exception e) {
LOGGER.error("", e);
} finally {
// 释放锁
lock.unlock();
}
}
}
});
th.start();
}
private int process() throws Exception {
int sleepTime = 10000; //默认执行完之后等等10秒
// 查找待消费的5000条消息
List<TransactionMessage> messageList = transactionMqRemoteClient.findByWatingMessage(5000);
if (messageList.size() == 5000) {
sleepTime = 0;
}
// 并发处理所有消息
final CountDownLatch latch = new CountDownLatch(messageList.size());
for (final TransactionMessage message : messageList) {
semaphore.acquire();
fixedThreadPool.execute(new Runnable() {
public void run() {
try {
doProcess(message);
} catch (Exception e) {
LOGGER.error("", e);
} finally {
semaphore.release();
latch.countDown();
}
}
});
}
latch.await();
return sleepTime;
}
private void doProcess(TransactionMessage message) {
//检查此消息是否满足死亡条件
if (message.getSendCount() > message.getDieCount()) {
transactionMqRemoteClient.confirmDieMessage(message.getId());
return;
}
//距离上次发送时间超过一分钟才继续发送
long currentTime = System.currentTimeMillis();
long sendTime = 0;
if (message.getSendDate() != null) {
sendTime = message.getSendDate().getTime();
}
if (currentTime - sendTime > 60000) {
System.out.println("发送具体消息:" + message.getId());
//向MQ发送消息
MessageDto messageDto = new MessageDto();
messageDto.setMessageId(message.getId());
messageDto.setMessage(message.getMessage());
producer.send(message.getQueue(), JsonUtils.toJson(messageDto));
//修改消息发送次数以及最近发送时间
transactionMqRemoteClient.incrSendCount(message.getId(), DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
}
}
业务关键代码:
hello-service更新用户操作(重点:本地添加事务,完成操作后持久化消息数据,等待transaction-mq-task拉取消息数据进行处理,弱一致性;若要强一致性可参考tx-lcn或阿里的seate
):
@Transactional
@Override
public Object update(String userId, String name) {
// 本地修改或插入用户
User user = new User();
user.setUserId(userId);
user.setName(name);
user.setAge(new Random().nextInt(100));
int ret = userMapper.updateByPrimaryKeySelective(user);
if(ret == 0){
userMapper.insert(user);
}
// 修改之后发送消息给消费者进行业务处理,最终一致性
TransactionMessage message = new TransactionMessage();
message.setQueue("hello_queue");
message.setCreateDate(new Date());
message.setSendSystem("hello-service");
message.setMessage(JsonUtils.toJson(user));
boolean result = transactionActivemqSV.sendMessage(message);
if (!result) {
throw new RuntimeException("回滚事务");
}
return result;
}
消费者操作(重点:消息数据处理后需确认,同时修改本地数据库消息状态为已确认;若出现异常,不确认消息,直接抛出异常,对于未确认的消息,mq会重发消息,同时,若是mq挂掉,transaction-mq-task也会从本地数据库中找到未确认的该消息,进行重发,重发一定次数失败后,会修改本地数据库消息数据状态为已死亡-跟mq死信队列不一样;注意处理重发消息,mq的重发消息和transaction-mq-task重发的消息
):
// hello队列消息处理
@JmsListener(destination = "hello_queue")
public void receiveQueue(TextMessage text) {
try {
logger.debug("可靠消息服务消费消息:"+text.getText());
JSONObject transactionMsgJson = JSON.parseObject(text.getText());
// 开始进行消息处理
User user = JsonUtils.toBean(User.class, transactionMsgJson.getString("message"));
if("exception".equals(user.getName())){
int num = 1/0; // 产生异常
}else{
User updateUser = new User();
updateUser.setUserId(user.getUserId());
updateUser.setName("activemq"+user.getName());
userMapper.updateByPrimaryKeySelective(updateUser);
}
//修改成功后调用消息确认消费接口,确认该消息已被消费
boolean result = transactionActivemqSV.confirmCustomerMessage("feign-consumer", Long.parseLong(transactionMsgJson.getString("messageId")));
//手动确认ACK
if (result) {
text.acknowledge();
}
} catch (Exception e) {
logger.error("消费异常!", e);
// 异常时会触发重试机制,重试次数完成之后还是错误,消息会进入DLQ队列中
throw new RuntimeException(e);
}
}
// 死信队列消息处理
@JmsListener(destination = "ActiveMQ.DLQ")
public void receiveDLQQueue(TextMessage text) {
try {
logger.debug("死信队列消息:"+text.getText());
} catch (Exception e) {
logger.error("消费异常!", e);
}
}
关于mq消息重发和死信队列相关资料:
activeMQ中的消息重发,指的是消息可以被broker重新分派给消费者,不一定是之前的消费者。重发消息之后,消费者可以重新消费。消息重发的情况有以下几种。
1.事务会话中,当还未进行session.commit()时,进行session.rollback(),那么所有还没commit的消息都会进行重发。
2.使用客户端手动确认的方式时,还未进行确认并且执行Session.recover(),那么所有还没acknowledge的消息都会进行重发。
3.所有未ack的消息,当进行session.closed()关闭事务,那么所有还没ack的消息broker端都会进行重发,而且是马上重发。
4.消息被消费者拉取之后,超时没有响应ack,消息会被broker**重发**。
有毒消息
当一个消息被接收的次数超过maximumRedeliveries(默认为6次)次数时,会给broker发送一个poison _ack,这种ack类型告诉broker这个消息“有毒”,尝试多次依然失败,这时broker会将这个消息发送到DLQ,以便后续处理。activeMQ默认的死信队列是ActiveMQ.DLQ,如果没有特别指定,死信消息都会被发送到这个队列。
默认情况下持久消息过期都会被送到DLQ,非持久消息过期默认不会送到DLQ。
可以通过配置文件为指定队列创建死信队列。
分布式事务组件关键jar包(需要修改jar包中配置文件),下载地址:
https://download.csdn.net/download/leadseczgw01/12538558
8.实践操作
8.1 部署基础组件
activemq redis eureka
activemq: 版本5.14.5,使用默认配置,我使用docker快速部署的。
redis随便一个就行,跟版本无关,eureka使用1.5.4(基本配置)。
8.2 数据库
执行前面的sql创建消息表。
8.3 部署activemq事务关键组件
通过链接https://download.csdn.net/download/leadseczgw01/12538558下载已编译好的jar包(也可以通过源码编译,注意有许多本地依赖包)。
修改jar包(transaction-mq-service-1.0.jar、transaction-mq-task-1.0.jar)的配置文件(application.properties),将其中的id地址、端口号、用户名密码修改为自己的,下图仅为transaction-mq-service-1.0.jar包的配置。
分别使用命令启动服务。
java -jar transaction-mq-service-1.0.jar
java -jar transaction-mq-task-1.0.jar
8.4 使用IDE分别启动消息生产服务hello-service和消费者服务feign-consumer(注意修改配置)
8.5 正常消息处理
hello服务新增用户(调用updateUser接口),先本地新增或修改用户名(tom),再通过transaction-mq-service的send接口发送消息(内部会先将消息数据持久化到数据库,状态为待消费)。
消息数据持久化信息(与本身的用户持久化不一样)
消费者消费消息
消费完后,名称由tom变为activemqtom。
8.6异常消息处理
修改名称为excepiton,消费会抛出异常。
消费异常:
消息记录(消息send_count为3表示发送了3次,status为0,表示待消费),由于这个异常是由硬编码导致的,即使达到最大重发次数,也不会成功,最后将表示该消息为已死亡(需要人工处理,可根据业务情况,先修正问题,再重发死亡消息,达到最终一致性)。
同时一直未消费掉的消息,也会进入activemq的死信队列。