分布式事务-activemq消息。

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的死信队列。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kenick

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值