场景
公司的数据,之前是采用定时任务进行同步,优点是技术比较成熟,没有学习成本。
但是也有不足:
数据无论如何会有延迟。
过多定时任务也会加大服务器压力。
定时任务是单线程的,如果其中某条记录报错,处理不好,会阻断整个定时任务。
用mq可以很好的解决上述问题。
集成过程
引入依赖
pom.xml中加入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
application.properties中增加属性
注:以下属性不是自动注入的(不像datasource的属性),是需要手动引入的
spring.rabbitmq.host=192.168.10.11
spring.rabbitmq.port=5672
spring.rabbitmq.userName=root
spring.rabbitmq.passWord=1234
# virtualHost起到分隔的作用,因为一台机器可能供多个系统使用,难到要每个服务一台机器?
spring.rabbitmq.virtualHost=ticket
RabbitConfig代码
这个类主要配置连接的作用,以及创建连接池:
/**
* rabbitmq的主配置类
* 主要定义链接和虚拟机
*/
@Configuration
@EnableRabbit // @EnableRabbit 这个注解一定要加上,表示开启rabbitmq服务,不一定在application类上,任意configuration类即可
public class RabbitConfig {
@Value("${spring.rabbitmq.exchangeName}")
private String exchangeName; // 交换机其实不属于这个connection级别
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private Integer port;
@Value("${spring.rabbitmq.userName}")
private String userName;
@Value("${spring.rabbitmq.passWord}")
private String passWord;
@Value("${spring.rabbitmq.virtualHost}")
private String virtualHost;
@Bean
public CachingConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();// 这里也可以写,host,但是为了一致,在下面写了
connectionFactory.setHost(host);
connectionFactory.setUsername(userName);
connectionFactory.setPassword(passWord);
connectionFactory.setPort(port);
connectionFactory.setVirtualHost(virtualHost);
return connectionFactory;
}
@Bean
public AmqpAdmin amqpAdmin() {
return new RabbitAdmin(connectionFactory());
}
/* 重写rabbitTemplate模板 配置上必须的参数*/
// 这个待考证,实测无用 不如配置Message里面的MessageProperties
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
// 公司必须设置的参数-个性化参数
MessageProperties messageProperties = new MessageProperties();
messageProperties.setAppId("ali"); // 表示是从哪个系统发送的
messageProperties.setContentType("text/plain");
String messageId = UUID.randomUUID().toString();
messageProperties.setMessageId(messageId);
messageProperties.setContentEncoding("UTF-8");
messageProperties.setType("uat.crm");
messageProperties.setTimestamp(new Date());
MessagePropertiesConverter messagePropertiesConverter = new DefaultMessagePropertiesConverter();
messagePropertiesConverter.fromMessageProperties(messageProperties,"UTF-8");
rabbitTemplate.setMessagePropertiesConverter(messagePropertiesConverter);
return rabbitTemplate;
}
//配置消费者监听的容器
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setConcurrentConsumers(3);
factory.setMaxConcurrentConsumers(10);
//factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);//设置确认模式手工确认
return factory;
}
}
DirectExchangeConfig代码
mq有多种发送规则。这里用direct模式。他和topic(订阅模式) 区别就是topic支持正则。
/**
* direct交换机的规则在这里配置
*/
@Configuration
public class DirectExchangeConfig {
/**
* direct和topic的区别就是topic支持正则,如果规则不很复杂,direct就足够
* 需要多个交换器么,如果规则不复杂,不用多个交换器
*/
@Bean
public DirectExchange direct() {
DirectExchange directExchange = new DirectExchange("direct");
return directExchange;
}
@Bean
public Queue mail() {
Queue queue = new Queue("mail"); // 对列名称要和listener一致
return queue;
}
@Bean
public Queue short() {
Queue queue = new Queue("short");
return queue;
}
// bingding的作用是设置规则
// queue和规则是多对多关系
@Bean
public Binding bindMailWithChecked(DirectExchange direct, Queue mail) {
// 为什么要用with,因为推送方可能推送多种,如yanzhen,renzheng,yanjia等,不只可以推送一条消息
return BindingBuilder.bind(mail).to(direct).with(RoutingKey.MAIL);
}
@Bean
public Binding bindMailWithDeducted(DirectExchange direct, Queue mail) {
return BindingBuilder.bind(mail).to(direct).with(RoutingKey.SHORT);
}
@Bean
public Binding bindShortWithChecked(DirectExchange direct, Queue short) {
return BindingBuilder.bind(short).to(direct).with(RoutingKey.MAIL);
}
@Bean
public Binding bindShortWithDeducted(DirectExchange direct, Queue short) {
return BindingBuilder.bind(short).to(direct).with(RoutingKey.SHORT);
}
}
订阅模式代码
订阅模式其实也比较简单。可以通过路由实现分流。
@Configuration
public class SubscribeExchangeConfig {
@Bean
public AExchange aExchange() {
FanoutExchange fanoutExchange = new FanoutExchange("aExchange");
return fanoutExchange;
}
@Bean
public Queue mail() {
Queue queue = new Queue("mail"); // 对列名称要和listener一致
return queue;
}
@Bean
public Queue short() {
Queue queue = new Queue("short");
return queue;
}
// bingding的作用是设置规则
// queue和规则是多对多关系
@Bean
public Binding bindMailWithAExchange(FanoutExchange aExchange, Queue mail) {
return BindingBuilder.bind(mail).to(aExchange);
}
@Bean
public Binding bindShortWithAExchange(FanoutExchange aExchange, Queue short) {
return BindingBuilder.bind(short).to(aExchange);
}
}
Service接口和实现类
DirectExchangeService接口:
public interface DirectExchangeService {
public void sendMessage(String routingKey,String message);
}
DirectExchangeServiceImpl实现类:
@Service
public class DirectExchangeServiceImpl implements DirectExchangeService {
private static Logger logger = LoggerFactory.getLogger(DirectExchangeServiceImpl.class);
@Autowired
RabbitTemplate rabbitTemplate;
@Autowired
private DirectExchange directExchange;
@Override
public void sendMessage(String routingKey,String message) {
try {
logger.info("mq准备推送数据,routingKey: {} ,message: {}",RoutingKey.MAIL, JSON.toJSONString(message));
rabbitTemplate.convertAndSend(directExchange.getName(), RoutingKey.MAIL, message);
}catch (Exception e){
logger.error("mq推送异常,routingKey: {} ",RoutingKey.MAIL,e);
}
}
}
routingKey 路由字典表
/**
* routingkey建议使用字典表,作用至少有2点:
* 1、后续可能有多个routingkey,便于维护
* 2、send和binding中用的routingkey往往不在同一类中,用string容易错漏。
*/
public class RoutingKey {
public static String MAIL="mail"; // 邮件
public static String SHORT="short"; // 短信
}
SpringBootApplication启动类
注:@EnableRabbit注解可以加在这里,也可以加载configuration类上,但是一定要记得加。
@SpringBootApplication
public class OaSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(OaSpringBootApplication.class, args);
}
}
MqController代码
用来发送请求,惭愧一直都不太会mock的测试方法,就用请求把。
@Controller
@RequestMapping("mq")
class MqController {
@Autowired
private DirectExchangeService directExchangeService;
@ResponseBody
@RequestMapping("/send")
public String send(){
String message="1234已发邮件";
directExchangeService.sendMessage(RoutingKey.MAIL,message);
return "success";
}
@ResponseBody
@RequestMapping("/send2")
public String send2(){
String message="666已发短信";
directExchangeService.sendMessage(RoutingKey.SHORT,message);
return "success";
}
}
QueueListener类
listener监听,相当于消费者。
本例子集成在一个项目中了,实际listener往往单独作为一个项目。
消费者端只需要RabbitConfig和QueueListener即可。
/**
* 监听queue队列并实现消费,推送端不需要配置listener,,因为它会消费掉队列
* 推送端请记得注释掉这个类
*/
@Component
public class QueueListener {
private static Logger logger = LoggerFactory.getLogger(QueueListener.class);
@RabbitListener(queues = "mail")
public void mailListener(String message) {
logger.info("mail队列收到消息: {}", message);
// TODO 更新数据库等操作
}
@RabbitListener(queues = "short")
public void busindessListener(String message) {
logger.info("short队列收到消息: {}", message);
// TODO 更新数据库等操作
}
}
验证方法
发送mqController的请求 localhost:8080/mq/send
打印listener监听到的日志即表示成功。
拓展问题:是否可以通过配置的形式,不消费消息
@RabbitListener 里面是有这个方法的,exclusive(排除)方法。
true 排除消费
false 不排除消费(默认)
boolean exclusive() default false;
问题是如何通过配置的形式注入呢?例如@Value。
注解里面不能使用@Value。
如果是在这里面写true或false,不能适配各个环境。和直接注释掉代码的方式是一个意思。
单交换机和多交换机
单交换机的确定是,如果交换机相同,routing_key也相同。 那么所有符合该规则的都可以接受到请求。细粒度不够。
多交换机配合多routing_key,组合方式可以可以有 m*n种,细粒度完全足够。
如果情况不是很多,很有一种比较无脑的方式就是不用routing_key,直接交换机对应各种情况,也是可以满足需求的。
如何设计
3个主要元素为(交换机*key一般是总数):
交换机
routingKey
queue
例如有2个业务,3个用户
对比以下几种实现方法。
1交换机,6key,6queue
功能可以实现,不过要在key上花功夫
2交换机,3key,6queue或者3queue
较为合理,是2*3的数量
6交换机,不用key,6queue,或者3queue
心有点大额,如果有10个业务,1000个用户,难道要10000个交换机。
6个queue和3个queue的区别就是,3个queue需要传递业务类型,否则message不知道如何处理,如果是6个queue那就是单业务了。
topic模式的优点
建议使用topic模式,因为兼容性强。
他比driect模式,支持正则,使用更灵活。
比fanout细粒度更高,而且也可以当fanout模式来用,方法就是用* 绑定,那么无论什么key,都会推送到queue。就相当于fanout模式。
Message里面配置MessageProperties的代码
public MessageProperties generateMessageProperties(){
MessageProperties messageProperties = new MessageProperties();
messageProperties.setAppId("crm");
messageProperties.setContentType("text/plain");
String messageId = UUID.randomUUID().toString();
messageProperties.setMessageId(messageId);
messageProperties.setContentEncoding("UTF-8");
messageProperties.setType("uat.crm");
messageProperties.setTimestamp(new Date());
return messageProperties;
}
public void test(){
// 新建Message对象的时候,加入属性
Message message=new Message("".getBytes(), generateMessageProperties());
}
listener可以直接接受Message对象么
可以,以下2种写法都可以:
@RabbitListener(queues = "mail")
public void aListener(Message message) {
logger.info("收到消息: {}", message);
// TODO 更新数据库等操作
}
@RabbitListener(queues = "shortMessage")
public void bListener(String message) {
logger.info("收到消息: {}", message);
// TODO 更新数据库等操作
}
channel和queue的区别
其实这2个不是一个维度。channel 由 connection 创建,channel可以发送queue。
他的主要作用相当于提高性能,因为如果每发送一个queue就创建一个connection,太浪费资源,但是我用channel,就可以保持连接不变的情况下,发送多个queue。
概念
基本元素
虚拟机(以及用户)
通道(channel)
路由(交换机)(exchange)
队列(queue)
从高到低基本是这么个层级。
mq自动重连
其实就是几行配置,也记录下吧。
connectionFactory.getRabbitConnectionFactory().setAutomaticRecoveryEnabled(true);
connectionFactory.getRabbitConnectionFactory().setRequestedHeartbeat(60);//60秒
connectionFactory.getRabbitConnectionFactory().setConnectionTimeout(60000);//60秒
connectionFactory.getRabbitConnectionFactory().setHandshakeTimeout(10000);//10秒
connectionFactory.getRabbitConnectionFactory().setShutdownTimeout(10000);//10秒
connectionFactory.getRabbitConnectionFactory().setRequestedChannelMax(512);//最大设置512,rabbitmq的server端也会做限制最大
connectionFactory.getRabbitConnectionFactory().setNetworkRecoveryInterval(10000);//10秒
mq是http请求吗
不是,是基于长连接的,所以不是http请求。