基于RocketMQ的延迟消费系统架构全解析
[6.1 生产者发送策略](#6.1 生产者发送策略)
[6.2 消费者处理策略](#6.2 消费者处理策略)
[6.3 配置优化](#6.3 配置优化)
一、消息实体定义
java
package jnpf.model.attendance.event;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
/**
* @author shitou
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AttendanceStatisticsSingleDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 租户Id
*/
@NotBlank(message = "租户Id不能为空")
private String tenantId;
/**
* 考勤组Id
*/
@NotBlank(message = "考勤组Id不能为空")
private String groupId;
/**
* 用户Id
*/
@NotBlank(message = "用户Id不能为空")
private String userId;
/**
* 日期
*/
@NotNull(message = "日期不能为空")
private Date day;
}
二、生产者服务实现
java
@NoDataSourceBind
@Operation(summary = "模拟统计数据消息推送")
@GetMapping("/mockStatisticsPush")
public ActionResult<Boolean> mockStatisticsPush(@RequestParam("tenantId") String tenantId,
@RequestParam("groupId") String groupId,
@RequestParam("userId") String userId,
@RequestParam("day") String day) {
AttendanceStatisticsSingleDto courseEventDTO = AttendanceStatisticsSingleDto.builder()
.tenantId(tenantId)
.groupId(groupId)
.userId(userId)
.day(DateUtil.parse(day))
.build();
Message<AttendanceStatisticsSingleDto> message = MessageBuilder.withPayload(courseEventDTO).build();
rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2);
return ActionResult.success();
}
三、消费者监听器实现
java
package jnpf.attendance.event;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import jnpf.attendance.service.AttendanceDayStatisticsService;
import jnpf.constants.MessageTopicConstants;
import jnpf.model.attendance.event.AttendanceStatisticsSingleDto;
import jnpf.util.CustomTenantUtil;
import jnpf.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.apache.rocketmq.spring.core.RocketMQPushConsumerLifecycleListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Objects;
/**
* 监听单个用户的考勤日统计消息
* 消费失败会重试最多2次(共3次),之后进入死信队列
*/
@Slf4j
@Component
@RocketMQMessageListener(
topic = MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC,
consumerGroup = MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_CONSUMER_GROUP,
consumeMode = ConsumeMode.CONCURRENTLY,
consumeThreadNumber = 2,
maxReconsumeTimes = 2)
public class StatisticsSingleMQListener implements RocketMQListener<AttendanceStatisticsSingleDto>, RocketMQPushConsumerLifecycleListener {
@Autowired
private CustomTenantUtil tenantUtil;
@Resource
private AttendanceDayStatisticsService attendanceDayStatisticsService;
@Override
public void onMessage(AttendanceStatisticsSingleDto singleDto) {
if (Objects.isNull(singleDto)) {
log.warn("接收到空消息,忽略处理");
return;
}
log.error("接受到一条生成用户日统计消息,{}", JSONObject.toJSONString(singleDto));
// 校验必要字段
if (ObjectUtil.isNull(singleDto) || StringUtil.isEmpty(singleDto.getUserId()) ||
StringUtil.isEmpty(singleDto.getTenantId()) || StringUtil.isEmpty(singleDto.getGroupId()) ||
ObjectUtil.isNull(singleDto.getDay())) {
log.error("生成用户日统计消息消费失败:消息格式无效,内容: {}", JSONObject.toJSONString(singleDto));
return;
}
try {
tenantUtil.checkOutTenant(singleDto.getTenantId());
attendanceDayStatisticsService.statisticDataChange(singleDto.getGroupId(), singleDto.getUserId(), singleDto.getDay());
} catch (Exception ex) {
log.error("处理用户日统计消息失败,触发重试,消息 {}, 错误: ", JSONObject.toJSONString(singleDto), ex);
throw new RuntimeException("处理用户日统计消息失败,触发重试", ex);
}
}
@Override
public void prepareStart(DefaultMQPushConsumer consumer) {
// 优化消费参数 每次拉取的消息数量
consumer.setPullBatchSize(16);
// 顺序消费每次批量消费2条
consumer.setConsumeMessageBatchMaxSize(2);
// 设置消费间隔,避免过于频繁拉取
consumer.setPullInterval(3000);
// 设置消费超时时间(分钟)
consumer.setConsumeTimeout(10);
// 设置消费起始位置(从上次消费的位置继续)
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
// 调整消费线程池最小线程数
consumer.setConsumeThreadMin(2);
// 调整消费线程池最大线程数
consumer.setConsumeThreadMax(4);
}
}
四、配置文件
application.yml:
yaml
rocketmq:
name-server: ${ROCKETMQ_NAME_SERVER:192.168.3.24:30094}
producer:
group: ${ROCKETMQ_PRODUCER_GROUP:fantaibao-producer-group}
send-message-timeout: ${ROCKETMQ_SEND_MESSAGE_TIMEOUT:30000}
max-message-size: ${ROCKETMQ_MAX_MESSAGE_SIZE:8388608}
消息主题常量:
java
package jnpf.constants;
public interface MessageTopicConstants {
//考勤统计单个生成-发送的延迟消息(等级是2(5秒))
String ATTENDANCE_STATISTICS_SINGLE_TOPIC = "attendance-statistics-single-topic";
String ATTENDANCE_STATISTICS_SINGLE_CONSUMER_GROUP = "attendance-statistics-single--consumer-group";
}
五、Maven依赖
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
<version>2022.0.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
六、关键设计要点
6.1 生产者发送策略
- 延迟发送:定时触发或削峰填谷
6.2 消费者处理策略
-
异步处理:使用线程池异步执行,避免阻塞消费线程
-
延迟执行:2秒延迟实现削峰填谷
-
参数校验:三层校验确保消息有效性
-
异常处理:异常抛出触发重试机制
6.3 配置优化
-
拉取参数:平衡拉取频率和消息堆积
-
超时设置:根据业务处理时间设置合理超时
-
重试策略:设置最大重试次数避免无限重试
这个实现提供了完整的基于RocketMQ的考勤统计批量处理方案,包含生产者、消费者业务服务实现,可以直接在项目中集成使用。