之前一直用spring boot+dubbo+zookeeper来搭建分布式项目,后来在网上看到有人说用spring cloud更好,前两天抽时间了解了一下,spring cloud 为我们提供了许多分布式系统的组件(服务注册与发现、服务网关、客户端负载均衡、配置中心、断路器、消息总线等),我们写少量的代码和配置就能搭建分布式的应用。不过我个人觉得,在搭建系统的时候,不一定要全部使用spring cloud里面所有的组件,可以根据自己的喜好或者习惯使用其它第三方的组件来代替spring cloud里面的部分组件,就像玩王者农药你可以选择推荐出装,也可以根据自己的喜好来搭配。

前两天基于spring cloud写了几个demo,本次在之前的基础上来整合阿里的消息队列rocketmq,因为开源版本不支持事务消息,所以我选择了商业版ONS,项目结构如下:

  • spcd-registry 服务注册中心
  • spcd-core 系统核心组件 其它子项目都会依赖它
  • spcd-wsapi 所有服务接口都定义在这里面
  • spcd-order 订单微服务,对外提供订单相关接口
  • spcd-message 消息微服务
  • spcd-mobile 业务网关层

使用商业版ONS需要在阿里云控制台去开通,然后创建Topic、创建ProducerId、创建ConsumerId、授权...
一切准备就绪,开工,核心部分代码定义在spcd-core中

  • 添加maven依赖
<dependency>
		    <groupId>com.aliyun.openservices</groupId>
		   <artifactId>ons-client</artifactId>
		   <version>1.6.0.Final</version>
		</dependency>
  • 创建消息发送者,这里创建一个代理类MQClient用来供调用方发送消息
package com.christy.spcd.core.mq;

import java.nio.charset.Charset;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

import com.alibaba.fastjson.JSON;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.Producer;
import com.aliyun.openservices.ons.api.transaction.LocalTransactionExecuter;
import com.aliyun.openservices.ons.api.transaction.TransactionProducer;

public class MQClient implements InitializingBean,DisposableBean {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(MQClient.class);
	
	private Producer producer;
	
	private TransactionProducer transactionProducer;
	
	public MQClient(Producer producer,TransactionProducer transactionProducer) {
		this.producer = producer;
		this.transactionProducer = transactionProducer;
	}

	public MQSendResult sendMessage(ConsumeTag tag,Object body,String key){
		try{
			Message message = wrapMessage(tag, body, key);
			MQSendResult result =  MQSendResult.convert(producer.send(message));
			LOGGER.info("MQ send result  {}",JSON.toJSON(result));
			return result;
		}catch (Exception e){
			LOGGER.error(e.getMessage(),e);
		}
		return null;
	}
	
	public MQSendResult sendTransactionMessage(ConsumeTag tag,Object body,String key,LocalTransactionExecuter executer,Object arg){
		try{
			Message message = wrapMessage(tag, body, key);
			MQSendResult result = MQSendResult.convert(transactionProducer.send(message, executer, arg));
			LOGGER.info("MQ send result  {}",JSON.toJSON(result));
			return result;
		}catch (Exception e){
			LOGGER.error(e.getMessage(),e);
		}
		return null;
	}
	
	private Message wrapMessage(ConsumeTag tag,Object body,String key){
		byte[] byteBody = null;
		String topic = tag.getTopic();
		if(body.getClass().isPrimitive() || body.getClass() == String.class) {
			byteBody = String.valueOf(body).getBytes(Charset.forName("UTF-8"));
		} else {
			byteBody = JSON.toJSONBytes(body);
		}
		key = topic + "_" + tag.name() + "_" + key;
		Message message = new Message(topic,tag.name(),key,byteBody);
		message.putUserProperties("messageType", body.getClass().getName());
		message.putUserProperties("_uuid", System.nanoTime() + UUID.randomUUID().toString().replaceAll("-", ""));
		return message;
	}
	
	@Override
	public void afterPropertiesSet() throws Exception {
		if(producer != null){
			producer.start();
		}
		if(transactionProducer != null){
			transactionProducer.start();
		}
	}
	
	@Override
	public void destroy() throws Exception {
		if(producer != null){
			producer.shutdown();
		}
		if(transactionProducer != null){
			transactionProducer.shutdown();
		}
	}
}

sendMessage用于发送普通消息,sendTransactionMessage用于发送事务消息

  • 实例化MQClient(这里spcd-order作为消息发送方)
package com.christy.spcd.order;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.aliyun.openservices.ons.api.PropertyKeyConst;
import com.christy.spcd.core.mq.ConsumeSpec;
import com.christy.spcd.core.mq.DefaultMQConfig;
import com.christy.spcd.core.mq.MQClient;
import com.christy.spcd.core.mq.MQClientBuilder;
import com.christy.spcd.core.mq.ProducerId;

@Configuration
public class MQConfig  extends DefaultMQConfig{
	
	@Bean
	public MQClient mqClient(){
		Properties properties = mqProperties();
		properties.put(PropertyKeyConst.ProducerId, producerId().name());
		return new MQClientBuilder(springFactory, properties).build();
	}

	@Override
	public List<ConsumeSpec> registerConsumeTags() {
		List<ConsumeSpec> consumeSpecs = new ArrayList<ConsumeSpec>();
		return consumeSpecs;
	}
	
	@Override
	protected ProducerId producerId() {
		return ProducerId.PID_ORDER_007;
	}

}
package com.christy.spcd.core.mq;

import java.util.Properties;

import com.aliyun.openservices.ons.api.ONSFactory;
import com.aliyun.openservices.ons.api.Producer;
import com.aliyun.openservices.ons.api.transaction.LocalTransactionChecker;
import com.aliyun.openservices.ons.api.transaction.TransactionProducer;
import com.christy.spcd.core.SpringFactory;

public class MQClientBuilder {

	private SpringFactory springFactory;
	private Properties properties;
	public MQClientBuilder(SpringFactory springFactory,Properties properties) {
		this.springFactory = springFactory;
		this.properties = properties;
	}
	
	public MQClient build(){
		Producer producer =ONSFactory.createProducer(properties);
		TransactionProducer transactionProducer = ONSFactory.createTransactionProducer(properties,localTransactionChecker());
		return new MQClient(producer, transactionProducer);
	}
	
	private LocalTransactionChecker localTransactionChecker(){
		DefaultTransactionChecker checker = new DefaultTransactionChecker();
		springFactory.initializeBean(checker);
		return checker;
	}
	
}
package com.christy.spcd.core.mq;

import java.util.List;
import java.util.Properties;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import com.aliyun.openservices.ons.api.PropertyKeyConst;
import com.christy.spcd.core.SpringFactory;

public abstract class DefaultMQConfig implements ApplicationContextAware{
	
	protected SpringFactory springFactory;

	public Properties mqProperties(){
		Properties properties = new Properties();
		properties.put(PropertyKeyConst.AccessKey, "在阿里云后台创建的AccessKey");
		properties.put(PropertyKeyConst.SecretKey, "在阿里云后台创建的AccessSecretKey");
		return properties;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.springFactory = applicationContext.getBean(SpringFactory.class);
	}
	
	public abstract List<ConsumeSpec> registerConsumeTags();
	
	protected ProducerId producerId() {
		return null;
	}
	
	protected ConsumerId consumerId(){
		return null;
	}
	
}

到此消息的发送者已经准备就绪,我们模拟一下订单支付的业务,spcd-order对外提供一个订单支付接口pay, spcd-mobile作为业务网关层调用spcd-order的支付接口,spcd-order在完成支付动作后,发出一个订单支付成功的消息,spcd-message负责消费这个消息。

  • 首先我们定义一个TAG 其中ORDER1 为TOPIC
package com.christy.spcd.core.mq;

public enum ConsumeTag {
	ORDER_PAID_SUCCEED("ORDER1"),
	;
	private String topic;
	ConsumeTag(String topic) {
		this.topic = topic;
	}
	
	public String getTopic() {
		return topic;
	}
}
  • 然后定义一个消息体OrderPaidSucceedMessage(如果是基础数据类型或者String类型也可以不定义),为了在消费方能看到这个消息体,这里将它定义在spcd-wsapi中
package com.christy.spcd.wsapi.order.message;

import java.io.Serializable;

public class OrderPaidSucceedMessage implements Serializable{
	
	private static final long serialVersionUID = 8673315516088031608L;

	private Integer orderId;
	
	public OrderPaidSucceedMessage() {
	
	}

	public OrderPaidSucceedMessage(Integer orderId) {
		this.orderId = orderId;
	}

	public Integer getOrderId() {
		return orderId;
	}

	public void setOrderId(Integer orderId) {
		this.orderId = orderId;
	}
}
  • 在spcd-order 的支付接口的实现中通过注入MQClient来发送消息
@Override
	public String pay(Integer orderId) {
		MQSendResult result = mqClient.sendMessage(ConsumeTag.ORDER_PAID_SUCCEED, new OrderPaidSucceedMessage(orderId), String.valueOf(orderId));
		if(result == null){
			return "支付失败了";
		}
		return result.getMessageId();
	}

消息发送方部分已完成,接下来实现消费方

  • 注册消费者以及订阅消息
package com.spcd.message;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.aliyun.openservices.ons.api.Consumer;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import com.christy.spcd.core.mq.ConsumeSpec;
import com.christy.spcd.core.mq.ConsumeTag;
import com.christy.spcd.core.mq.ConsumerId;
import com.christy.spcd.core.mq.DefaultMQConfig;
import com.christy.spcd.core.mq.MQConsumerFactory;
import com.spcd.message.common.listener.OrderPaidSucceedMessageListener;
@Configuration
public class MQConfig extends DefaultMQConfig{
	
	@Bean
	public MQConsumerFactory mqConsumerFactory(){
		Properties properties = mqProperties();
		properties.put(PropertyKeyConst.ConsumerId, consumerId().name());
		return new MQConsumerFactory(properties,registerConsumeTags());
	}

	@Override
	public List<ConsumeSpec> registerConsumeTags() {
		List<ConsumeSpec> consumeSpecs = new ArrayList<ConsumeSpec>();
		consumeSpecs.add(new ConsumeSpec(ConsumeTag.ORDER_PAID_SUCCEED, OrderPaidSucceedMessageListener.class));
		return consumeSpecs;
	}

	@Override
	protected ConsumerId consumerId() {
		return ConsumerId.CID_MESSAGE_007;
	}
	
}
package com.christy.spcd.core.mq;

import com.aliyun.openservices.ons.api.Consumer;
import com.aliyun.openservices.ons.api.MessageListener;
import com.aliyun.openservices.ons.api.ONSFactory;
import com.christy.spcd.core.SpringFactory;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Properties;

public class MQConsumerFactory {
	@Autowired
	private SpringFactory springFactory;
	
	private Properties properties;

	private List<ConsumeSpec> consumeSpecs;
	
	
	public MQConsumerFactory( Properties properties, List<ConsumeSpec> consumeSpecs){
		this.springFactory = springFactory;
		this.properties = properties;
		this.consumeSpecs = consumeSpecs;
	}
	@PostConstruct
	public void createConsumers(){
		if(CollectionUtils.isNotEmpty(consumeSpecs)){
			for (ConsumeSpec consumeSpec : consumeSpecs) {
				Consumer consumer = ONSFactory.createConsumer(properties);
				MessageListener messageListener = springFactory.getOrCreateBean(consumeSpec.getMessageListenerCls());
				consumer.subscribe(consumeSpec.getTag().getTopic(), consumeSpec.getTag().name(), messageListener);
				consumer.start();
			}
		}
	}

}
  • 实现消息监听器
package com.christy.spcd.core.mq;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.aliyun.openservices.ons.api.Action;
import com.aliyun.openservices.ons.api.ConsumeContext;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.MessageListener;

public abstract class MQMessageListener<T> implements MessageListener{
	private static final Logger LOGGER = LoggerFactory.getLogger(MQMessageListener.class);

	@SuppressWarnings("unchecked")
	@Override
	public Action consume(Message message, ConsumeContext consumecontext) {
		T messageBody = null;
		Class<?> messageType;
		try {
			messageType = Class.forName(message.getUserProperties("messageType"));
			if(messageType.isPrimitive() || messageType == String.class) {
				messageBody = (T)new String(message.getBody());
			} else {
				messageBody = JSON.parseObject(message.getBody(), messageType);
			}
			return onMessage(messageBody);
		} catch (Exception e) {
			LOGGER.error(e.getMessage(),e);
			return Action.ReconsumeLater;
		}
	}
	
	public abstract Action onMessage(T message);

}
package com.spcd.message.common.listener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.aliyun.openservices.ons.api.Action;
import com.christy.spcd.core.mq.MQMessageListener;
import com.christy.spcd.wsapi.order.message.OrderPaidSucceedMessage;

public class OrderPaidSucceedMessageListener extends MQMessageListener<OrderPaidSucceedMessage>{
	
	private static final Logger LOGGER = LoggerFactory.getLogger(OrderPaidSucceedMessageListener.class);

	@Override
	public Action onMessage(OrderPaidSucceedMessage message) {
		LOGGER.info("received message is {}",message.getOrderId());
		return Action.CommitMessage;
	}

}

OK 普通消息的发送与消费已实现,我们在spcd-mobile中开放一个外部访问接口

@RequestMapping("/pay")
	public String pay(@RequestParam("orderId")Integer orderId){
		return orderApi.pay(orderId);
	}

依次启动spcd-registry spcd-order spcd-message spcd-mobile

然后访问 http://127.0.0.1:7777/mobile/pay?orderId=123

如果消息发送成功会在页面输出一个msgId,这个时候如果spcd-message中接收到消息 会在控制台输出 received message is 123

如果控制台报错提示权限问题,可以通过阿里云管理控制台配置一下访问权限

如果消息发送成功而spcd-message中收不到消息可以通过阿里云管理控制台发送测试消息,检查是否发送方或消费方配置有问题

接下来我们开始实现发送事务消息,使用事务消息能够保证发送方的本地事务能够执行完毕,否则消费方将不会收到该消息。而消费方的事务完整性一般是通过重试机制来实现最终一致,所以需要注意在消费方做到幂等。

发送事务消息的接口如下所示,可以看到它比发送普通消息多了两个参数,一个是LocalTransactionExecuter类型, 这个就是用来执行本地事务,另外一个Object类型,这个是在执行本地事务的参数,没有可以设置null

@Override
	public String pay2(Integer orderId) {
		MQSendResult result = mqClient.sendTransactionMessage(ConsumeTag.ORDER_PAID_SUCCEED, new OrderPaidSucceedMessage(orderId), String.valueOf(orderId),orderPaidTransactionExecuter,orderId);
		if(result == null){
			return "支付失败了";
		}
		return result.getMessageId();
	}

我们需要为每个事务消息实现对应的本地事务执行器和消息回查方法

  • 消息回查(如果在执行本地事务时返回Unknow,MQ会定时回调我们的消息回查方法)
package com.christy.spcd.core.mq;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;

import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.transaction.LocalTransactionChecker;
import com.aliyun.openservices.ons.api.transaction.TransactionStatus;

public class DefaultTransactionChecker implements LocalTransactionChecker{
	
	@Autowired(required=false)
	private List<TransactionCheckStrategy<?>> strategies;

	@Override
	public TransactionStatus check(Message message) {
		for (TransactionCheckStrategy<?> strategy : strategies) {
			if(strategy.support(ConsumeTag.valueOf(message.getTag()))){
				return strategy.check(message);
			}
		}
		return TransactionStatus.Unknow;
	}

}
package com.christy.spcd.core.mq;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.transaction.LocalTransactionExecuter;
import com.aliyun.openservices.ons.api.transaction.TransactionStatus;

public abstract class TransactionCheckStrategy<T> implements LocalTransactionExecuter {
	private static final Logger LOGGER = LoggerFactory.getLogger(TransactionCheckStrategy.class);
	@SuppressWarnings("unchecked")
	public TransactionStatus check(Message message){
		T messageBody = null;
		Class<?> messageType;
		try {
			messageType = Class.forName(message.getUserProperties("messageType"));
			if(messageType.isPrimitive() || messageType == String.class) {
				messageBody = (T)new String(message.getBody());
			} else {
				messageBody = JSON.parseObject(message.getBody(), messageType);
			}
			return checkLocalTransaction(messageBody);
		} catch (Exception e) {
			LOGGER.error(e.getMessage(),e);
			return TransactionStatus.Unknow;
		}
	}

	public abstract boolean support(ConsumeTag tag);
	
	public abstract TransactionStatus checkLocalTransaction(T message);
	
}
  • 为消息发送方定制消息本地事务处理器和消息回查方法,为了简单明了,我将它们都放到一个类中实现。
package com.christy.spcd.order.tx;

import org.springframework.stereotype.Service;

import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.transaction.TransactionStatus;
import com.christy.spcd.common.config.TransactionConfig;
import com.christy.spcd.core.mq.ConsumeTag;
import com.christy.spcd.core.mq.TransactionCheckStrategy;
import com.christy.spcd.wsapi.order.message.OrderPaidSucceedMessage;
@Service
public class OrderPaidTransactionExecuter extends TransactionCheckStrategy<OrderPaidSucceedMessage>{

	@Override
	public TransactionStatus execute(Message message, Object arg) {
		Integer orderId = (Integer) arg;
		TransactionConfig.put(orderId, true);
		return TransactionStatus.CommitTransaction;
	}

	@Override
	public boolean support(ConsumeTag tag) {
		return ConsumeTag.ORDER_PAID_SUCCEED == tag;
	}

	@Override
	public TransactionStatus checkLocalTransaction(OrderPaidSucceedMessage message) {
		if(TransactionConfig.isComplated(message.getOrderId())){
			return TransactionStatus.CommitTransaction;
		}
		return TransactionStatus.RollbackTransaction;
	}
}

这样发送事务消息部分就完成了,因为事务是针对发送方的所以消费方不需要做改动。

https://github.com/sergewu/spcd

THE END