一、实现方案图解
- 第一步
将要发送的信息进行对应数据库的录入,并且将发送信息的操作作为一条操作日志录入数据库中设置状态字段status
为0(发送中)。
- 第二步
生产端将消息发送到RabbitMQ服务上。
- 第三步
RabbitMQ接收到消息后,进行回应,告诉生产端已接收到信息,这一步骤需要在生产端进行配置,设置RabbitMQ接收到信息后自动回应。
- 第四步
生产端接收到RabbitMQ的回应后,修改数据库日志表中对应消息的状态字段status
为1(发送成功)。
- 第五步
设置定时任务,定时从数据库日志表中获取状态为0的消息
- 第六步
在定时任务中,将获取的状态为0的消息进行重新发送
- 第七步
在定时任务中,获取的消息记录如果发送次数超过三次,就状态设置为3(发送失败)。
二、数据库设计
CREATE TABLE IF NOT EXISTS `t_order` (
`id` VARCHAR(128) NOT NULL,
`name` VARCHAR(128) NOT NULL,
`message_id` VARCHAR(128) NOT NULL,
PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `broker_massage_log` (
`message_id` VARCHAR(128) NOT NULL,
`message` VARCHAR(4000) DEFAULT NULL,
`try_count` INT(4) DEFAULT '0',
`status` VARCHAR(10) DEFAULT '',
`next_retry` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY(`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
三、依赖包&配置
以第四章中SpringBoot项目的生产端为例
1.添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--数据库相关-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!--rabbitMQ依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--工具类依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
2.配置
spring.rabbitmq.addresses=39.108.182.112:5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#rabbitMQ默认虚拟主机为`/`
spring.rabbitmq.virtual-host=/
spring.rabbitmq.connection-timeout=15000s
#消息发送确认模式,发送信息后会异步等待MQ的响应
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true
#项目路径前缀
server.servlet.context-path=/
#项目端口
server.port=8080
spring.http.encoding.charset=UTF-8
#日期格式化
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#不允许传递空值
spring.jackson.default-property-inclusion=NON_NULL
#数据库相关配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/rabbitMQ
spring.datasource.username=root
spring.datasource.password=root
#mybatis相关配置
#指定实体类所在路径,自动生成别名
mybatis.type-aliases-package=com.rabbitmq.entity
#指定mapper.xml所在路径
mybatis.mapper-locations=classpath:com/rabbitmq/mapping/*.xml
3.资源文件管理
SpringBoot默认情况下不会编译非java类的文件,需要在pom.xml文件中进行配置
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
四、代码实现
1.启动类上添加注解
- Mapper接口的扫描路径
@MapperScan("com.rabbitmq.mapper")
- 定时任务的开启
@EnableScheduling
2.创建实体类
- 日志表对应实体类
public class BrokerMessageLog {
private String messageId;
private String message;
private Integer tryCount = 0;
private String status;
private Date nextRetry;
public BrokerMessageLog() {
}
public BrokerMessageLog(String messageId, String message, Integer tryCount, String status, Date nextRetry) {
this.messageId = messageId;
this.message = message;
this.tryCount = tryCount;
this.status = status;
this.nextRetry = nextRetry;
}
......
}
- 订单对应实体类
public class Order implements Serializable {
private static final long serialVersionUID = 6165921570511430824L;
private String id;
private String name;
/**存储消息发送的唯一标识*/
private String messageId;
public Order() {
}
public Order(String id, String name, String massageId) {
this.id = id;
this.name = name;
this.messageId = massageId;
}
......
}
3.mapper配置文件
- 日志表
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.rabbitmq.mapper.BrokerMessageLogMapper">
<resultMap id="resultMapper" type="BrokerMessageLog"/>
<update id="updateStatus" parameterType="BrokerMessageLog">
update broker_message_log set status = #{status} where message_id = #{messageId}
</update>
<insert id="insertBrokerMessageLog" parameterType="BrokerMessageLog">
insert into broker_message_log (message_id, message, status, next_retry, try_count)
values (#{messageId}, #{message}, #{status}, #{nextRetry}, #{tryCount})
</insert>
<select id="query4StatusAndTimeout" resultMap="resultMapper">
<![CDATA[
select message_id, message, try_count, status, next_retry from broker_message_log
where status = '0' and next_retry < sysdate()
]]>
</select>
<update id="updateTryCount" parameterType="BrokerMessageLog">
update broker_message_log set try_count = #{tryCount} where message_id = #{messageId}
</update>
</mapper>
- 订单表
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.rabbitmq.mapper.OrderMapper" >
<resultMap id="orderMapper" type="Order"/>
<select id="selectById" resultMap="orderMapper" parameterType="String">
Select * from t_order where id = #{id}
</select>
<!--注意,这里使用的parameterType是mybatis中的type-aliases别名-->
<insert id="insertOrder" parameterType="Order">
insert into t_order(id, name, message_id) values (#{id}, #{name}, #{messageId})
</insert>
</mapper>
4.mapper接口
- 日志表
@Repository
public interface BrokerMessageLogMapper {
int updateStatus(BrokerMessageLog brokerMessageLog);
int insertBrokerMessageLog(BrokerMessageLog brokerMessageLog);
List<BrokerMessageLog> query4StatusAndTimeout();
int updateTryCount(BrokerMessageLog brokerMessageLog);
}
- 订单表
@Repository
public interface OrderMapper {
Order selectById(String id);
int insertOrder(Order order);
}
5.创建常量类
public final class Constants {
public static final String ORDER_SENDING = "0";
public static final String ORDER_SEND_SUCCESS = "1";
public static final String ORDER_SENDFAILURE = "2";
public static final int ORDER_TIMEOUT = 1;
}
6.创建生产端消息发送类
@Component
public class RabbitmqOrderSender {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private BrokerMessageLogMapper brokerMessageLogMapper;
/**
* 回调函数
*/
final ConfirmCallback confirmationCallback = new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
System.out.println("correlationData: " + correlationData.getId());
String messageId = correlationData.getId();
if (ack) {
//发送成功,更新对应消息的日志状态
BrokerMessageLog brokerMessageLog = new BrokerMessageLog();
brokerMessageLog.setStatus(Constants.ORDER_SEND_SUCCESS);
brokerMessageLog.setMessageId(messageId);
brokerMessageLogMapper.updateStatus(brokerMessageLog);
} else {
//失败则根据具体情况进行处理,这里不是重点,所以不详细描述
System.out.println("发送失败,进行处理......");
}
}
};
/**
* 发送消息
*/
public void sendOrder(Order order) {
//设置回调函数
rabbitTemplate.setConfirmCallback(confirmationCallback);
//设置消息唯一ID
CorrelationData correlationData = new CorrelationData(order.getMessageId());
rabbitTemplate.convertAndSend("order-exchange", "order.abcd", order, correlationData);
}
}
7.创建Service
@Service
@Transactional
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private BrokerMessageLogMapper brokerMessageLogMapper;
@Autowired
private RabbitmqOrderSender rabbitmqOrderSender;
public void createOrder(Order order) {
//将消息数据录入数据库
orderMapper.insertOrder(order);
//构建消息日志对象
BrokerMessageLog brokerMessageLog = new BrokerMessageLog();
brokerMessageLog.setMessageId(order.getMessageId());
brokerMessageLog.setMessage(JSON.toJSONString(order));
brokerMessageLog.setStatus("0");
brokerMessageLog.setNextRetry(DateUtils.addMinutes(new Date(), Constants.ORDER_TIMEOUT));
//将消息日志对象录入数据库
brokerMessageLogMapper.insertBrokerMessageLog(brokerMessageLog);
//发送消息
rabbitmqOrderSender.sendOrder(order);
}
}
8.编写定时任务
@Component
public class RetryMessageTasker {
@Autowired
private RabbitmqOrderSender rabbitmqOrderSender;
@Autowired
private BrokerMessageLogMapper brokerMessageLogMapper;
@Scheduled(initialDelay = 3000, fixedDelay = 10000)
public void reSend() {
System.out.println("-------------开始定时任务-------------");
List<BrokerMessageLog> brokerMessageLogs = brokerMessageLogMapper.query4StatusAndTimeout();
brokerMessageLogs.forEach(brokerMessageLog -> {
if (brokerMessageLog.getTryCount() >= 3) {
//如果重新发送超过三次,就设置为发送失败
brokerMessageLog.setStatus(Constants.ORDER_SENDFAILURE);
brokerMessageLogMapper.updateStatus(brokerMessageLog);
} else {
//更新次数
brokerMessageLog.setTryCount(brokerMessageLog.getTryCount() + 1);
brokerMessageLogMapper.updateTryCount(brokerMessageLog);
//将消息从json字符串转为order
Order resendOrder = JSON.parseObject(brokerMessageLog.getMessage(), Order.class);
try {
rabbitmqOrderSender.sendOrder(resendOrder);
} catch (Exception e) {
e.printStackTrace();
System.out.println("----------------异常----------------");
}
}
});
}
}
9.编写测试方法
@Test
public void testCreateOrder() {
Order order = new Order();
order.setMessageId(System.currentTimeMillis() + "#" + UUID.randomUUID());
order.setName("Schuyler");
order.setId("20181016");
orderService.createOrder(order);
}
五、测试
1.开启消费端
以第四章中的消费端为例。
切记!需要将生产端跟消费端用到的消息对象,即Order完全同步。否则在序列化跟反序列化时候会出现错误。
确保消息对象完全相同之后,直接启动项目即可。
2.生产端
调用生产端的测试方法后,观察数据库数据的变化即可。
3.消息发送错误
要制造消息发送错误,只需要将发送时的exchange改成不存在的交换机即可。