本篇简单的记录一下我使用RabbitMq的经历。主要解决以下几个问题。
1.保证消息的百分百投递
2.保证消息不重复消费
3.服务宕机后的备用方案
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
yaml 配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
publisher-confirm-type: correlated
publisher-returns: true
listener:
simple:
acknowledge-mode: manual #手动确认模式
prefetch: 100 #预取消息数
如何保证消息的百分百发送成功,我们应该建立重发机制,可以把每次发送的日志入库。
建立日志表msg_log
CREATE TABLE msg_log (
msg_id varchar(255) NOT NULL,
msg text NULL,
exchange varchar(255) NOT NULL DEFAULT ''::character varying,
routing_key varchar(255) NOT NULL DEFAULT ''::character varying,
status int4 NULL DEFAULT 0,
try_count int4 NULL DEFAULT 0,
max_try_count int4 NULL DEFAULT 0,
message_level int4 NULL DEFAULT 1,
consume_try_count int4 NULL DEFAULT 0,
next_try_time timestamp NULL,
create_time timestamp NULL,
update_time timestamp NULL,
CONSTRAINT unq_msg_id PRIMARY KEY (msg_id)
);
comment on column msg_log.status is '0-IN_DELIVERY,1-DELIVERY_FAIL,2-DELIVERY_SUCCESS,3-CONSUMED_SUCCESS';
comment on column msg_log.message_level is '1-low,2-medium,3-high';
对应的日志类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MsgLog {
private String msgId;
private String msg;
private String exchange;
private String routingKey;
private Integer status;
private Integer tryCount;
private Integer maxTryCount;
private Integer consumeTryCount;
private Timestamp nextTryTime;
private Integer messageLevel;
private Timestamp createTime;
private Timestamp updateTime;
}
声明队列
@Configuration
public class RabbitDeclareConfig {
@Bean
public Queue queueYYDS(){
//四个参数分别是队列名称,是否要持久化,是否排他的,是否自动删除
return new Queue("yyds",true,false,false);
}
@Bean
public Queue queueDelay(){
return new Queue("delay",true,false,false);
}
@Bean
public DirectExchange directExchange1(){
//定义direct交换机,参数分别为交换机名称,是否持久化,是否自动删除
return new DirectExchange("directExchange1",true,false);
}
@Bean
public Exchange delayExchange1(){
//定义延迟交换机,方便实现延迟队列。前提是需要rabbitmq服务端安装一个插件,以便支持延迟交换机类型
Map<String, Object> args = new HashMap<>(2);
args.put("x-delayed-type", "direct");
return new CustomExchange("delayExchange1", "x-delayed-message", true, false, args);
}
@Bean
public Binding bindingQueueYYDS(Queue queueYYDS, DirectExchange directExchange1){
//把队列和交换机进行一个绑定。这里的routingKey使用的是队列名
return BindingBuilder.bind(queueYYDS).to(directExchange1).withQueueName();
}
@Bean
public Binding bindingQueueDelay(Queue queueDelay, Exchange delayExchange1){
//其中的“delay”是routingKey
return BindingBuilder.bind(queueDelay).to(delayExchange1).with("delay").noargs();
}
}
封装消息体
@Data
public class MessageContent {
private String queueName;
private String exchangeName;
private String routingKey;
private Integer messageLevel;
private Integer delayTime;
private Integer maxTryCount;
private Object content;
}
消息发送方法
public String send(MessageDTO dto){
MessageContent content = new MessageContent();
content.setContent(dto);
content.setExchangeName(EXCHANGE_LOAN_DELAY);
content.setRoutingKey(QUEUE_T24_UPDATE_ACCOUNT);
content.setDelayTime(10*1000);
sender.sendMsg(content);
return "success";
}
把消息内容封装成类,如下MessageDTO 是一个具体的消息对象
@Data
public class MessageDTO {
private String message;
private Integer id;
//....有更多的内容可以继续填充
}
定义消费者接口,所有的消息消费者均实现这个接口,主要是为了保证在rabbitmq服务宕机后,依然可以保证消息被消费掉。
public interface RabbitBaseConsumer {
void doExecute(String msg) throws Exception;
String getExchange();
}
具体的某个消费者代码如下
@Slf4j
@RequiredArgsConstructor
//指定bean名称,后期从数据库消费消息时需要使用。
@Component("yyds")
public class YydsRabbitMQ implements RabbitBaseConsumer {
private final RabbitMQService mqService;
private ObjectMapper objectMapper = new ObjectMapper();
//配置消息最大重试消费次数
private Integer maxRetryCount = 3;
@RabbitListener(queues = "yyds")
public void consume(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
long tag = message.getMessageProperties().getDeliveryTag();
//判断消息是否被消费过(主要是根据消息的uuid进行判断)
if(mqService.isConsumed(message)){
channel.basicAck(tag, false);
return;
}
if(mqService.isAchieveMaxRetryCount(message,maxRetryCount)){
//达到最大的消费重试次数,手动把消息ack掉
channel.basicAck(tag, false);
return;
}
String msg = new String(message.getBody());
// execute business code
try {
doExecute(msg);
}catch (Exception e){
mqService.msgTryCountIncreaseOne(message);
channel.basicNack(tag, false,true);
return;
}
channel.basicAck(tag, false);
mqService.msgConsumeSuccess(message.getMessageProperties().getMessageId());
}
private void execute() throws Exception {
//do something 业务代码
}
@Override
public void doExecute(String msg) throws Exception {
//把字符串消息转为具体消息对象。其中的MessageDTO是和发送消息的时候的消息是同一类型
messageDTO = objectMapper.readValue(msg, MessageDTO.class);
execute();
}
@Override
public String getExchange() {
//返回队列所绑定的交换机名称
return "directExchange1";
}
}
常量信息
public class RabbitMQConstants {
public final static Integer IN_DELIVERY = 0;
public final static Integer DELIVERY_FAIL = 1;
public final static Integer DELIVERY_SUCCESS = 2;
public final static Integer CONSUMED_SUCCESS = 3;
public final static Integer MAX_TRY_COUNT = 3;
public final static Integer INIT_CONSUME_TRY_COUNT = 0;
public final static String FIND_BY_ID_SQL = "select * from msg_log where msg_id = ?";
public final static String UPDATE_STATUS_SQL = "update msg_log set update_time = ?,status = ? where msg_id = ?";
public final static String INSERT_SQL = "insert into msg_log(msg_id,msg,exchange,routing_key,status,try_count,consume_try_count,create_time,message_level,max_try_count) " +
"values(?,?,?,?,?,?,?,?,?,?)";
public final static String CONSUME_TRY_COUNT_INCREASE_ONE_SQL = "update msg_log set update_time = ?,consume_try_count = ? where msg_id = ?";
public final static String GET_DELIVERY_FAIL_MSG_SQL = "select * from msg_log where status = 1";
public final static String TRY_COUNT_INCREASE_ONE_SQL = "update msg_log set update_time = ?,try_count = ? where msg_id = ?";
public final static String GET_UNCONSUME_MESSAGE_BY_EXCHANGE_AND_ROUTINGKEY = "select * from msg_log where status = 1 and exchange = ? and routing_key = ?";
public final static String DELETE_SEVEN_DAYS_AGO_MSG_SQL = "delete from loan_arrangement.msg_log where status = 3 and create_time < now()::timestamp + '-7 day'";
}
消息发送回调
当生产者发送消息后,会触发回调函数,根据返回的状态信息来判断是否发送成功。如果失败则入库等候重发
@Slf4j
@RequiredArgsConstructor
@Component
public class RabbitMQConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
private final JdbcTemplate jdbcTemplate;
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
String uuid = correlationData.getId();
MsgLog msgLog = jdbcTemplate.queryForObject(FIND_BY_ID_SQL, new BeanPropertyRowMapper<>(MsgLog.class), uuid);
Integer status;
if(Objects.isNull(msgLog)){
return;
}
if (ack) {
log.info("{}:message send to exchange", correlationData.getId());
status = RabbitMQConstants.DELIVERY_SUCCESS;
} else {
log.error("{}:message send fail", correlationData.getId());
status = RabbitMQConstants.DELIVERY_FAIL;
}
jdbcTemplate.update(UPDATE_STATUS_SQL, ps -> {
Timestamp time = new Timestamp(System.currentTimeMillis());
ps.setTimestamp(1,time);
ps.setInt(2,status);
ps.setString(3,uuid);
});
}
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error("ReturnedMessage: " + returnedMessage);
}
}
@Slf4j
@RequiredArgsConstructor
@Component
public class RabbitMQSender {
private final RabbitTemplate rabbitTemplate;
private final JdbcTemplate jdbcTemplate;
private ObjectMapper objectMapper = new ObjectMapper();
public void sendMsg(MessageContent messageContent){
String uuid = UUID.randomUUID().toString();
saveMsgLog(uuid,messageContent.getContent(),messageContent.getExchangeName(),messageContent.getRoutingKey(),
messageContent.getMessageLevel(),messageContent.getMaxTryCount());
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
sendMsg( uuid, messageContent.getContent(),messageContent.getExchangeName(),messageContent.getRoutingKey(),messageContent.getDelayTime());
}
});
} else {
sendMsg(uuid, messageContent.getContent(),messageContent.getExchangeName(),messageContent.getRoutingKey(),messageContent.getDelayTime());
}
}
private void sendMsg(String id,Object content,String exchange,String routingKey,Integer delay){
CorrelationData correlationData = new CorrelationData(id);
rabbitTemplate.convertAndSend(exchange,routingKey, content, message -> {
message.getMessageProperties().setMessageId(id);
if(Objects.nonNull(delay)) {
message.getMessageProperties().setDelay(delay);
}
return message;
},correlationData);
}
private void saveMsgLog(String msgId,Object content,String exchange,String routingKey,Integer messageLevel,Integer maxTryCount){
String msg;
try {
msg = objectMapper.writeValueAsString(content);
} catch (IOException e) {
throw new RuntimeException();
}
jdbcTemplate.update(RabbitMQConstants.INSERT_SQL, ps -> {
ps.setString(1,msgId);
ps.setString(2,msg);
ps.setString(3,exchange);
ps.setString(4,routingKey);
ps.setInt(5,RabbitMQConstants.IN_DELIVERY);
ps.setInt(6,0);
ps.setInt(7,RabbitMQConstants.INIT_CONSUME_TRY_COUNT);
Timestamp time = new Timestamp(System.currentTimeMillis());
ps.setTimestamp(8,time);
ps.setInt(9,messageLevel == null ? MessageLevel.LOW.getLevel() : messageLevel);
ps.setInt(10,maxTryCount == null ? MAX_TRY_COUNT : maxTryCount);
});
}
}
@Slf4j
@RequiredArgsConstructor
@Component
public class RabbitMQService {
private final JdbcTemplate jdbcTemplate;
public boolean isConsumed(Message message){
String msgId = message.getMessageProperties().getMessageId();
if (StringUtils.isBlank(msgId)) {
return true;
}
MsgLog msgLog = jdbcTemplate.queryForObject(FIND_BY_ID_SQL, new BeanPropertyRowMapper<>(MsgLog.class), msgId);
if(Objects.isNull(msgLog) || RabbitMQConstants.CONSUMED_SUCCESS.equals(msgLog.getStatus())){
return true;
}
return false;
}
private MsgLog getMsgLogById(String id){
MsgLog msgLog = jdbcTemplate.queryForObject(FIND_BY_ID_SQL, new BeanPropertyRowMapper<>(MsgLog.class), id);
return msgLog;
}
public boolean isAchieveMaxRetryCount(Message message,Integer maxRetryCount){
if(isConsumed(message)){
return true;
}
String msgId = message.getMessageProperties().getMessageId();
MsgLog msgLog = getMsgLogById(msgId);
if(msgLog.getConsumeTryCount() < maxRetryCount){
return false;
}
return true;
}
public void msgTryCountIncreaseOne(Message message){
String msgId = message.getMessageProperties().getMessageId();
MsgLog msgLog = getMsgLogById(msgId);
jdbcTemplate.update(CONSUME_TRY_COUNT_INCREASE_ONE_SQL, ps -> {
Timestamp time = new Timestamp(System.currentTimeMillis());
ps.setTimestamp(1,time);
ps.setInt(2,msgLog.getConsumeTryCount() + 1);
ps.setString(3,msgId);
});
}
public void msgConsumeSuccess(String msgId){
jdbcTemplate.update(UPDATE_STATUS_SQL, ps -> {
Timestamp time = new Timestamp(System.currentTimeMillis());
ps.setTimestamp(1,time);
ps.setInt(2,RabbitMQConstants.CONSUMED_SUCCESS);
ps.setString(3,msgId);
});
}
public List<MsgLog> getUnConsumeMessage(String exchangeName,String routingKey){
List<MsgLog> msgLogs = jdbcTemplate.query(GET_UNCONSUME_MESSAGE_BY_EXCHANGE_AND_ROUTINGKEY,new MsgLogMapper(), exchangeName,routingKey);
return msgLogs;
}
}
为节省空间,把七天前已经消费过的消息日志记录删除。
@Slf4j
@RequiredArgsConstructor
@Component
public class DeleteMsgSchedule {
private final JdbcTemplate jdbcTemplate;
@Scheduled(cron = "0 0 0 * * ?")
public void deleteSevenDaysAgoMsg() {
log.info("-----------start delete seven days ago message data--------");
jdbcTemplate.update(DELETE_SEVEN_DAYS_AGO_MSG_SQL);
log.info("---------end delete seven days ago message data----------");
}
}
消息日志的包装类 这是jdbc查询列表所需要的一个包装类
@Data
public class MsgLogMapper implements RowMapper<MsgLog> {
@Override
public MsgLog mapRow(ResultSet rs, int rowNum) throws SQLException {
MsgLog msgLog = new MsgLog();
msgLog.setMsgId(rs.getString("msg_id"));
msgLog.setMsg(rs.getString("msg"));
msgLog.setStatus(rs.getInt("status"));
msgLog.setTryCount(rs.getInt("try_count"));
msgLog.setConsumeTryCount(rs.getInt("consume_try_count"));
msgLog.setExchange(rs.getString("exchange"));
msgLog.setRoutingKey(rs.getString("routing_key"));
msgLog.setCreateTime(rs.getTimestamp("create_time"));
msgLog.setNextTryTime(rs.getTimestamp("next_try_time"));
msgLog.setUpdateTime(rs.getTimestamp("update_time"));
msgLog.setMessageLevel(rs.getInt("message_level"));
return msgLog;
}
}
定时重发任务
@Slf4j
@RequiredArgsConstructor
public class MsgRetrySchedule {
private final JdbcTemplate jdbcTemplate;
private final RabbitTemplate rabbitTemplate;
private final RabbitWarningService warningService;
@Scheduled(cron = "0 0/30 * * * ? ")
public void retryResendMsg() {
log.info("-----------start scan msg_log table--------");
List<MsgLog> msgLogs = jdbcTemplate.query(GET_DELIVERY_FAIL_MSG_SQL, new MsgLogMapper());
for(MsgLog msgLog : msgLogs){
if(msgLog.getTryCount() >= msgLog.getMaxTryCount()){
log.warn("current msg receive max count,id="+msgLog.getMsgId());
if(msgLog.getMessageLevel() == MessageLevel.HIGH.getLevel()){
log.warn("this message level is high");
//do something,such as send email for warning
sendWarnInfo();
}
continue;
}
sendMsg(msgLog.getMsgId(),msgLog.getMsg(),msgLog.getExchange(),msgLog.getRoutingKey());
jdbcTemplate.update(TRY_COUNT_INCREASE_ONE_SQL, ps -> {
Timestamp time = new Timestamp(System.currentTimeMillis());
ps.setTimestamp(1,time);
ps.setInt(2,msgLog.getTryCount()+1);
ps.setString(3, msgLog.getMsgId());
});
}
log.info("---------end scan msg_log table----------");
}
private void sendMsg(String id,Object content,String exchange,String routingKey){
CorrelationData correlationData = new CorrelationData(id);
rabbitTemplate.convertAndSend(exchange,routingKey, content, message -> {
message.getMessageProperties().setMessageId(id);
//todo delay queue retry send is need delay again?
return message;
},correlationData);
}
private void sendWarnInfo(){
warningService.sendWarningEmail();
}
}
告警接口 自己实现
public interface RabbitWarningService {
void sendWarningEmail();
}
简单说一下思路,就是定时扫描mq的服务端口是否可用,如果端口不可用就认为服务挂了(应该有更好的方式,欢迎补充)。采用策略模式,在项目初始化时,把所有实现了RabbitBaseConusmer接口的消费者放到一个map里。然后调用doExecute方法即可,消息从数据库的日志表取出。
配置类
@ConfigurationProperties(prefix = "spring.rabbitmq")
@Data
public class RabbitProperties implements Serializable {
private String host;
private Integer port;
}
监视器
@Component
public class RabbitMqLocator implements ApplicationContextAware {
private Map<String, RabbitBaseConsumer> map;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
map = applicationContext.getBeansOfType(RabbitBaseConsumer.class);
}
public Map<String, RabbitBaseConsumer> getMap(){
return map;
}
public RabbitBaseConsumer getService(String beanName){
return map.get(beanName);
}
}
context
public class RabbitMQContext {
private RabbitBaseConsumer rabbitMQ;
public RabbitMQContext(RabbitBaseConsumer rabbitMQ){
this.rabbitMQ = rabbitMQ;
}
public void doExecute(String msg) throws Exception {
rabbitMQ.doExecute(msg);
}
}
定期扫描任务
@Slf4j
@RequiredArgsConstructor
@Component
public class RabbitMQListener {
private final RabbitProperties properties;
private final RabbitMQService mqService;
private final RabbitMqLocator rabbitMqLocator;
@Scheduled(cron = "0 0/10 * * * ? ")
public void scan(){
log.info("-----------------scan rabbit mq server is active---------------");
try{
InetAddress theAddress = InetAddress.getByName(properties.getHost());
new Socket(theAddress, properties.getPort());
} catch (Exception e){
log.error(e.getMessage());
log.info("rabbit mq server is down, start backup plan");
scanData();
}
}
private void scanData(){
Map<String,RabbitBaseConsumer> map = rabbitMqLocator.getMap();
Set<String> queueSet = map.keySet();
String exchangeName;
for(String queueName : queueSet){
exchangeName = rabbitMqLocator.getService(queueName).getExchange();
List<MsgLog> logs = mqService.getUnConsumeMessage(exchangeName,queueName);
for(MsgLog msgLog : logs){
dealLog(msgLog.getMsg(),msgLog.getMsgId(),map.get(queueName));
}
}
}
private void dealLog(String msg, String msgId, RabbitBaseConsumer baseConsumer){
try {
RabbitMQContextcontext = new RabbitMQContext(baseConsumer);
context.doExecute(msg);
mqService.msgConsumeSuccess(msgId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
至此就是发送消息的全过程了。