文章目录
1. MQ的基本概念
1.1 MQ概述
MQ (Message Queue) 消息队列,是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。
1.2 MQ的优势和劣势
优势:
- 应用解耦
- 异步提速
- 削峰填谷
劣势:
- 系统可用性降低
- 系统复杂度提高
- 一致性问题
1.3 MQ的优势
- 应用解耦
直接调用,如果新增子系统需要修改主系统的代码。耦合度高、不利于扩展。
通过MQ间接调用,系统间解耦,提高容错性和扩展维护性。
- 异步提速
异步调用提高了用户的体验,提高了系统的吞吐量(单位时间内处理请求的数目)。
- 削峰填谷
使用MQ后,高峰期来的请求会积压在MQ中,订单系统的请求高峰就被削掉了。这就是削峰
在高峰期过后的一段时间内,订单系统处理请求仍会维持在1000,直到积压的消息被消费完。这就是添谷
使用MQ能提高系统的稳定性。
1.4 MQ的劣势
- 系统的可用性降低
系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。如何保证MQ的高可用? - 系统的复杂度提高
MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在通过MQ进行异步调用。如何保证消息没有被重复消费?怎么处理消息丢失情况?怎么保证消息传递的顺序性? - 一致性问题
A系统处理完业务,通过MQ交给B、C、D三个子系统。如果B、C成功完成,D执行失败。怎么保证数据处理的一致性?
1.5 MQ的使用条件
- 生产者不需要从消费者处获取反馈,直接调用的接口返回值应该为空,才可以异步调用。比如 A系统直接调用B系统,如果A系统不需要B系统返回信息那么就可以异步调用,使用MQ.
- 容许短暂的不一致性。
- 使用了MQ的效果大于管理MQ的成本。
1.6 常见的MQ产品
追求可用性:Kafka、 RocketMQ 、RabbitMQ
追求可靠性:RabbitMQ、RocketMQ
追求吞吐能力:RocketMQ、Kafka
追求消息低延迟:RabbitMQ、Kafka
2. RabbitMQ简介
2.1 AMQP协议
AMQP协议: Advanced Message Queuing Protocol 高级消息队列协议。是应用层的网络协议,为面向消息的中间件设计。基于此协议的 客户端与消息中间件可传递消息,不受客户端/中间件不同产品,不同开发语言等条件的限制。类比于HTTP。
2.2 RabbitMQ架构
- Broker: 接受和分发消息的应用,RabbitMQ Server 就是Message Broker
- Virtual Host: 处于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中。当多用户使用同一个RabbitMQ Server提供的服务时,每个用户在自己的vhost中创建exchange/queue等。
- Connection: publisher、Consumer和Broker之间的TCP连接。
- Channel: 如果每次访问RabbitMQ Server都建立一个Connection,效率低开销大。Channel是在Connection内部建立的逻辑连接,多线程每个线程都创建单独的Channel进行通信,有channel id用于区分。Channel作为轻量级的Connection极大减少了操作系统建立TCP Connection的开销。
- Exchange: 根据分发规则,匹配查询查询表中的routing key,分发消息到queue中。常用类型有: direct、topic、fanout。
- Queue: 消息存放的地方,等待consumer取走。
- Binding: exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据。
2.3 RabbitMQ的工作模式
工作模式: 生产、消费 消息的一种工作方式。
官网工作模式https://www.rabbitmq.com/getstarted.html
- BasicQueue 基本消息队列
- WorkQueue 工作消息队列
- Fanout Exchange 广播
- Direct Exchange 路由
- Topic Exchange 主题
2.4 JMS
- JMS即Java 消息服务(JavaMessage Service) 应用程序接口,是一个Java平台中关于面向消息中间件的API.
- JMS是JavaEE规范中的一种,类比JDBC。
- 很多消息中间件都实现了JMS规范,例如ActiveMQ。 RabbitMQ没有实现,但开源社区有。
3. RabbitMQ 安装
使用Docker 安装
- 在线拉取或本地加载镜像
docker pull rabbitmq:3.8-management
- 创建RabbitMQ容器
docker run \
-e RABBITMQ_DEFAULT_USER=root \ # 设置 用户名
-e RABBITMQ_DEFAULT_PASS=root \ # 设置 密码
-v mq-plugins:/plugins \ # 可能用到插件,挂载插件目录
--name mq \ # 容器名
--hostname mq \ # 主机名
-p 15672:15672 \ # rabbitmq的控制台网页端口
-p 5672:5672 \ # 消息传输的端口
-d \ # 后台运行
rabbitmq:3.8-management # 使用的镜像版本
4. RabbiMQ快速入门
4.1 “Hello World!” 模式
- 生产者生产消息到队列,消费者从队列获取消息消费。
- 使用默认的交换机。
步骤:
- 创建工程 (生产者、消费者)
- 分别添加依赖
<!--RabbitMQ-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
</dependency>
- 生产者发送消息代码
//RabbitMQ
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置参数
connectionFactory.setHost("192.168.205.131"); //RabbitMQ所在的服务器地址
connectionFactory.setPort(5672); //RabbitMQ 通信端口
connectionFactory.setVirtualHost("order"); //虚拟主机名,隔离多租户
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
//创建连接
Connection connection = connectionFactory.newConnection();
//创建channel
Channel channel = connection.createChannel();
//创建queue,简单模式使用默认的交换机
channel.queueDeclare("queue", true, false, false, null);
//发送消息
String body = "hello RabbitMQ";
channel.basicPublish("", "queue", null, body.getBytes());
//关闭资源
channel.close();
connection.close();
- 消费者消费消息代码
//创建工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置参数
connectionFactory.setHost("192.168.205.131");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("order");
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
//创建连接
Connection connection = connectionFactory.newConnection();
//创建channel
Channel channel = connection.createChannel();
//获取消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(consumerTag);
System.out.println(envelope.getExchange());
System.out.println(envelope.getRoutingKey());
System.out.println(envelope.getDeliveryTag());
System.out.println(properties);
System.out.println(new String(body));
}
};
//从queue队列获取消息
channel.basicConsume("queue", consumer);
4.2 Work queues 模式
- 在一个队列中有多个消费者,消费者之间是竞争的关系。
- Work Queue对于任务过重或任务较多的情况下使用可以提高处理速度。
生产者代码
//RabbitMQ
//创建工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置参数
connectionFactory.setHost("192.168.205.131");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("order");
connectionFactory.setUsername("itcast");
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
//创建channel
Channel channel = connection.createChannel();
//创建queue
channel.queueDeclare("work queue", true, false, false, null);
//发送多条消息
for (int i = 0; i < 10; i++) {
String body = i + " hello RabbitMQ";
channel.basicPublish("", "work queue", null, body.getBytes());
}
//关闭资源
channel.close();
connection.close();
消费者1代码
//创建工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置参数
connectionFactory.setHost("192.168.205.131");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("order");
connectionFactory.setUsername("itcast");
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
//创建channel
Channel channel = connection.createChannel();
//获取消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
}
};
channel.basicConsume("work queue", consumer);
消费者2代码
和消费者1代码一样
演示情况
4.3 Publish/Subscribe 模式
- 交换机类型:FANOUT
- 交换机将消息分发给所有绑定的队列。
- 多个消费者从各自的队列获取消息。
生产者代码
//RabbitMQ
//创建工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置参数
connectionFactory.setHost("192.168.205.131");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("order");
connectionFactory.setUsername("itcast");
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
//创建channel
Channel channel = connection.createChannel();
//创建交换机
channel.exchangeDeclare("fanout exchange", BuiltinExchangeType.FANOUT, true, false, false, null);
//创建两个queue
channel.queueDeclare("fanout queue1", true, false, false, null);
channel.queueDeclare("fanout queue2", true, false, false, null);
//绑定交换机和队列,广播路由规则为""
channel.queueBind("fanout queue1","fanout exchange","");
channel.queueBind("fanout queue2","fanout exchange","");
//发送消息
String body = "hello RabbitMQ";
channel.basicPublish("fanout exchange", "", null, body.getBytes());
//关闭资源
channel.close();
connection.close();
消费者代码
和之前没有区别,各个消费者从各自的队列获取消息就行了。
//创建工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置参数
connectionFactory.setHost("192.168.205.131");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("order");
connectionFactory.setUsername("itcast");
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
//创建channel
Channel channel = connection.createChannel();
//获取消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
}
};
channel.basicConsume("fanout queue1", consumer);
4.4 Routing 模式
- 交换机类型: DIRECT
- 交换机在绑定队列时,需要指定 RoutingKey
- 消息的发送方在向交换机发送消息时也需要指定消息的Routing Key.
- Exchange在分发消息的时候,后根据Routing Key 分发给对应的队列。
生产者代码
4.5 Topics 模式
- 交换机类型:Topic
- RoutingKey 使用通配符。 * 一个单词,# 多个单词
生产者代码
5. Spring 整合RabbitMQ
引入依赖坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.19</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.19</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.2.15.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
生产者 Beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit https://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--加载配置文件-->
<context:property-placeholder location="classpath:rabbitmq.properties"/>
<!--定义ConnectionFactory Bean-->
<rabbit:connection-factory id="connectionFactory"
host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"/>
<!--创建管理员,管理交换机、队列、绑定。-->
<rabbit:admin connection-factory="connectionFactory"/>
<!--定义一些队列,如果下面没有和交换机绑定则使用默认交换机-->
<rabbit:queue id="queue0" name="spring_queue_0" auto-declare="true"/>
<rabbit:queue id="queue1" name="spring_queue_1" auto-declare="true"/>
<rabbit:queue id="queue2" name="spring_fanout_queue_1" auto-declare="true"/>
<rabbit:queue id="queue3" name="spring_fanout_queue_2" auto-declare="true"/>
<rabbit:queue id="queue4" name="spring_direct_queue_1" auto-declare="true"/>
<rabbit:queue id="queue5" name="spring_direct_queue_2" auto-declare="true"/>
<rabbit:queue id="queue6" name="spring_topic_queue_1" auto-declare="true"/>
<rabbit:queue id="queue7" name="spring_topic_queue_2" auto-declare="true"/>
<!--定义fanout交换机并绑定队列-->
<rabbit:fanout-exchange id="fanoutExchange" name="spring_fanout_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding queue="queue2"/>
<rabbit:binding queue="queue3"/>
</rabbit:bindings>
</rabbit:fanout-exchange>
<!--定义direct交换机并绑定队列-->
<rabbit:direct-exchange id="directExchange" name="spring_direct_exchange">
<rabbit:bindings>
<rabbit:binding key="aaa" queue="queue4"/>
<rabbit:binding key="bbb" queue="queue5"/>
</rabbit:bindings>
</rabbit:direct-exchange>
<!--定义Topic交换机并绑定队列-->
<rabbit:topic-exchange id="topicExchange" name="spring_topic_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="#.info" queue="queue6"/>
<rabbit:binding pattern="#.error" queue="queue7"/>
</rabbit:bindings>
</rabbit:topic-exchange>
<!---->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
</beans>
生产者测试代码
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author : ZHX
* @date : 2022/6/5 17:40
* @description:
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:beans.xml")
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testHelloWorld() {
//简单模式工作模式 发送消息,使用默认交换机、routingkey使用queue名字,
rabbitTemplate.convertAndSend("spring_queue_0", "hello world");
//工作模式
rabbitTemplate.convertAndSend("spring_queue_0", "hello world");
rabbitTemplate.convertAndSend("spring_queue_1", "hello world");
//发布订阅模式
//fanout交换机
rabbitTemplate.convertAndSend("spring_fanout_exchange", "", "hello hacker");
//Direct交换机
rabbitTemplate.convertAndSend("spring_direct_exchange", "aaa", "hello ");
rabbitTemplate.convertAndSend("spring_direct_exchange", "aaa", "hello ");
rabbitTemplate.convertAndSend("spring_direct_exchange", "bbb", "hello ");
//topic交换机
rabbitTemplate.convertAndSend("spring_topic_exchange", "a.info", "hello ");
rabbitTemplate.convertAndSend("spring_topic_exchange", "b.info", "hello ");
rabbitTemplate.convertAndSend("spring_topic_exchange", "a.error", "hello ");
}
}
消费者 beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit https://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--加载配置文件-->
<context:property-placeholder location="classpath:rabbitmq.properties"/>
<!--定义ConnectionFactory Bean-->
<rabbit:connection-factory id="connectionFactory"
host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"/>
<bean id="queue0Listener" class="rabbitmq.listener.queue0Listener"/>
<bean id="queue1Listener" class="rabbitmq.listener.queue1Listener"/>
<bean id="queue2Listener" class="rabbitmq.listener.queue2Listener"/>
<bean id="queue3Listener" class="rabbitmq.listener.queue3Listener"/>
<bean id="queue4Listener" class="rabbitmq.listener.queue4Listener"/>
<bean id="queue5Listener" class="rabbitmq.listener.queue5Listener"/>
<bean id="queue6Listener" class="rabbitmq.listener.queue6Listener"/>
<bean id="queue7Listener" class="rabbitmq.listener.queue7Listener"/>
<rabbit:listener-container connection-factory="connectionFactory" auto-declare="true">
<rabbit:listener ref="queue0Listener" queue-names="spring_queue_0"/>
<rabbit:listener ref="queue1Listener" queue-names="spring_queue_1"/>
<rabbit:listener ref="queue2Listener" queue-names="spring_fanout_queue_1"/>
<rabbit:listener ref="queue3Listener" queue-names="spring_fanout_queue_2"/>
<rabbit:listener ref="queue4Listener" queue-names="spring_direct_queue_1"/>
<rabbit:listener ref="queue5Listener" queue-names="spring_direct_queue_2"/>
<rabbit:listener ref="queue6Listener" queue-names="spring_topic_queue_1"/>
<rabbit:listener ref="queue7Listener" queue-names="spring_topic_queue_2"/>
</rabbit:listener-container>
</beans>
消费者测试代码
package rabbitmq.listener;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
/**
* @author : ZHX
* @date : 2022/6/5 21:28
* @description: 启动程序,监听器会区对应的queue中消费消息。
*/
public class queue0Listener implements MessageListener {
@Override
public void onMessage(Message message) {
System.out.println(new String(message.getBody()));
}
}
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author : ZHX
* @date : 2022/6/5 20:55
* @description:
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:beans.xml")
public class ConsumerTest {
@Test
public void testConsumer(){
while (true){
}
}
}
6. Spring Boot 整合RabbitMQ
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
生产者 application.yml配置文件
spring:
rabbitmq:
host: 192.168.205.132
port: 5672
virtual-host: order
username: itcast
password: 123456
配置exchange、queue、binding
package com.example.rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author : ZHX
* @date : 2022/6/5 23:04
* @description:
*/
@Configuration
public class RabbitMQConfig {
public static final String TOPIC_EXCHANGE_NAME = "spring_topic_exchange";
public static final String QUEUE_NAME = "spring_topic_queue_1";
//1.交换机
@Bean("exchange")
public Exchange getExchange() {
return ExchangeBuilder.topicExchange(TOPIC_EXCHANGE_NAME).durable(true).build();
}
//2.队列
@Bean("queue")
public Queue getQueue() {
return QueueBuilder.durable(QUEUE_NAME).build();
}
//3.绑定关系
@Bean("binding")
public Binding getBinding(Queue queue, Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("#.info").noargs();
}
}
生产者发送消息代码
@SpringBootTest
@RunWith(SpringRunner.class)
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testTopic(){
rabbitTemplate.convertAndSend(RabbitMQConfig.TOPIC_EXCHANGE_NAME,"aa.info","hello");
}
}
消费者消费消息代码
@Component
public class RabbitMQListener {
@RabbitListener(queues = "spring_topic_queue_1")
public void listenerQueue(Message message) {
System.out.println(new String(message.getBody()));
}
//通过注解,创建交换机、队列、绑定关系
@RabbitListener(bindings = {@QueueBinding(exchange = @Exchange(value = "topic.exchange",type = "topic"), value = @Queue("topic.queue"), key = "china.#")})
public void receiveTopic(String msg) {
System.out.println(msg);
}
}
7. 消息转换器
Spring会把发送的消息序列化为字节发送给MQ, 接受消息时,将字节反序列化为Java对象。
JDK序列化 数据体积大、有安全漏洞、可读性差。
使用jackson序列化
- 依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
- 注入IOC容器
@Configuration
public class MessageConverterConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
8. RabbitMQ 高级特性
8.1 消息的可靠投递
RabbitMQ提供了两种模式,将消息是否成功发送告诉给发送方。
- confirm 确认模式
- return 退回模式
RabbitMQ消息投递的路径为:
producer->rabbingmq broker -> exchange-> queue->consumer
- confirm模式: 消息从producer发到exchange,不管成功或失败都会执行ConfirmCallback函数 返回true/false。
- return模式:消息从exchange到queue, 只有失败了才会执行returnCallback函数。
confirm模式
- 开启confirm模式
- 设置ConfirmCallback函数
return模式
- 开启 return模式
- 设置函数
8.2 Consumer ACK
acknowledge, 表示消费者收到消息后的确认方式。有三种:
- 自动确认:none
- 手动确认: manual
- 根据异常情况确认:auto
自动确认模式:是指消息一旦被消费者接收到,就自动确认收到,并将相应的message从queue删除,但实际业务中很可能消息接收到,处理出现异常,那么该消息就丢失了。
手动确认模式:业务成功处理后调用channer.basicACK(),手动确认。如果业务处理失败,则调用channel.basicNack(方法,消息重回队列。
- 开启手动模式
- 消费者代码
8.3 消费端限流
- 先将消费端的确认模式为手动模式
- 设置perfetch :消费端每次拉取的消息数。
配置
代码
8.4 TTL
- TTL:Time To Live 存活时间。
- 当消息到达存活时间后,还没被消费会被自动删除。
- RabbitMQ可以对消息设置过期时间,也可以对整个队列设置过期时间。
设置队列的TTL
设置单个消息的TTL
8.5 DLX
- Dead Letter Exchange 死信交换机
- 当消息超时成为死信后,可以重新被发送到DLX,
消息称为死信的三种情况
- 队列消息长度达到限制。
- 消费者拒收消息,basicNick/basicReject,并且不把消息重新放入原目标队列 requeue=false.
- 原消息队列存在过期设置,消息到达过期时间未被消费。
给队列绑定死信交换机
8.6 延迟队列
RabbitMQ 没有定时功能,但可以通过 TTL + DXL 实现。