31 changed files with 936 additions and 284 deletions
@ -0,0 +1,74 @@ |
|||||
|
package com.bnyer.common.core.domain; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.annotation.IdType; |
||||
|
import com.baomidou.mybatisplus.annotation.TableField; |
||||
|
import com.baomidou.mybatisplus.annotation.TableId; |
||||
|
import com.bnyer.common.core.enums.EnumMessageStatus; |
||||
|
import com.fasterxml.jackson.annotation.JsonFormat; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.util.Date; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/18 |
||||
|
* @description :消息记录公共实体 |
||||
|
*/ |
||||
|
@Data |
||||
|
public class BaseMqMessage { |
||||
|
|
||||
|
@TableId(value = "id", type = IdType.ASSIGN_ID) |
||||
|
private Long id; |
||||
|
|
||||
|
/** |
||||
|
* 消息key,作为redis的key,消费的时候判断是否存在, |
||||
|
* 存在就判断状态是否是已消费,如果不是就进行消费,如果是就过滤该消息 |
||||
|
*/ |
||||
|
@TableField(value = "message_key") |
||||
|
private String messageKey; |
||||
|
|
||||
|
/** |
||||
|
* 消息主题 |
||||
|
*/ |
||||
|
@TableField(value = "topic") |
||||
|
private String topic; |
||||
|
|
||||
|
/** |
||||
|
* 消息tag |
||||
|
*/ |
||||
|
@TableField(value = "tag") |
||||
|
private String tag; |
||||
|
|
||||
|
/** |
||||
|
* 消费组名称 |
||||
|
*/ |
||||
|
@TableField(value = "consumer_group_name") |
||||
|
private String consumerGroupName; |
||||
|
|
||||
|
/** |
||||
|
* 消息状态 |
||||
|
*/ |
||||
|
@TableField(value = "status") |
||||
|
private EnumMessageStatus status; |
||||
|
|
||||
|
/** |
||||
|
* 消息内容 |
||||
|
*/ |
||||
|
@TableField(value = "content") |
||||
|
private String content; |
||||
|
|
||||
|
/** |
||||
|
* 错误信息 |
||||
|
*/ |
||||
|
@TableField(value = "err_msg") |
||||
|
private String errMsg; |
||||
|
|
||||
|
/** |
||||
|
* 创建时间 |
||||
|
*/ |
||||
|
@TableField(value = "create_time") |
||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
private Date createTime; |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
package com.bnyer.common.core.domain; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.annotation.TableName; |
||||
|
import lombok.Getter; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
import lombok.Setter; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/18 |
||||
|
* @description :订单服务本地消息表 |
||||
|
*/ |
||||
|
@Getter |
||||
|
@Setter |
||||
|
@NoArgsConstructor |
||||
|
@TableName(value = "order_mq_message_record") |
||||
|
public class OrderMqMessageRecord extends BaseMqMessage{ |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
package com.bnyer.common.core.enums; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Getter; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/18 |
||||
|
* @description :消息状态 |
||||
|
*/ |
||||
|
@Getter |
||||
|
@AllArgsConstructor |
||||
|
public enum EnumMessageStatus { |
||||
|
//处理中
|
||||
|
PROCESS, |
||||
|
//成功
|
||||
|
SUCCESS, |
||||
|
//失败,
|
||||
|
FAILS, |
||||
|
//作废
|
||||
|
INVALID, |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
package com.bnyer.common.rocketmq.config; |
||||
|
|
||||
|
|
||||
|
import com.bnyer.common.rocketmq.persist.IPersist; |
||||
|
import com.bnyer.common.rocketmq.persist.RedisPersist; |
||||
|
import lombok.Getter; |
||||
|
import lombok.ToString; |
||||
|
import org.springframework.data.redis.core.StringRedisTemplate; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/19 |
||||
|
* @description :重复消费配置 |
||||
|
*/ |
||||
|
@Getter |
||||
|
@ToString |
||||
|
public class RepeatConsumerConfig { |
||||
|
|
||||
|
/** |
||||
|
* 不启用去重 |
||||
|
*/ |
||||
|
public static final int REPEAT_STRATEGY_DISABLE = 0; |
||||
|
/** |
||||
|
* 开启去重,发现有处理中的消息,后面再重试 |
||||
|
*/ |
||||
|
public static final int REPEAT_STRATEGY_CONSUME_LATER = 1; |
||||
|
/** |
||||
|
* 用以标记去重的时候是哪个应用消费的,同一个应用才需要去重 |
||||
|
*/ |
||||
|
private final String applicationName; |
||||
|
|
||||
|
private IPersist persist; |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 去重策略,默认不去重 |
||||
|
*/ |
||||
|
private int repeatStrategy = REPEAT_STRATEGY_DISABLE; |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 对于消费中的消息,多少毫秒内认为重复,默认一分钟,即一分钟内的重复消息都会串行处理(等待前一个消息消费成功/失败),超过这个时间如果消息还在消费就不认为重复了(为了防止消息丢失) |
||||
|
*/ |
||||
|
private long processingExpireMilliSeconds = 60 * 1000; |
||||
|
|
||||
|
/** |
||||
|
* 消息消费成功后,记录保留多少分钟,默认一天,即一天内的消息不会重复 |
||||
|
*/ |
||||
|
private long recordReserveMinutes = 60 * 24; |
||||
|
|
||||
|
private RepeatConsumerConfig(String applicationName, int repeatStrategy, StringRedisTemplate redisTemplate) { |
||||
|
if (redisTemplate !=null) { |
||||
|
this.persist = new RedisPersist(redisTemplate); |
||||
|
} |
||||
|
this.repeatStrategy = repeatStrategy; |
||||
|
this.applicationName = applicationName; |
||||
|
} |
||||
|
|
||||
|
private RepeatConsumerConfig(String applicationName) { |
||||
|
this.repeatStrategy = REPEAT_STRATEGY_DISABLE; |
||||
|
this.applicationName = applicationName; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 利用redis去重 |
||||
|
* @param applicationName |
||||
|
* @param redisTemplate |
||||
|
* @return |
||||
|
*/ |
||||
|
public static RepeatConsumerConfig enableRedisConfig(String applicationName, StringRedisTemplate redisTemplate) { |
||||
|
return new RepeatConsumerConfig(applicationName, REPEAT_STRATEGY_CONSUME_LATER, redisTemplate); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public static RepeatConsumerConfig disableConfig(String applicationName) { |
||||
|
return new RepeatConsumerConfig(applicationName); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
public void setProcessingExpireMilliSeconds(long processingExpireMilliSeconds) { |
||||
|
this.processingExpireMilliSeconds = processingExpireMilliSeconds; |
||||
|
} |
||||
|
|
||||
|
public void setRecordReserveMinutes(long recordReserveMinutes) { |
||||
|
this.recordReserveMinutes = recordReserveMinutes; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
package com.bnyer.common.rocketmq.constant; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/20 |
||||
|
* @description : |
||||
|
*/ |
||||
|
public class RocketMqRepeatConstant { |
||||
|
/** |
||||
|
* 消费中 |
||||
|
*/ |
||||
|
public final static String CONSUME_STATUS_CONSUMING = "CONSUME_ING"; |
||||
|
/** |
||||
|
* 消费成功 |
||||
|
*/ |
||||
|
public final static String CONSUME_STATUS_SUCCESS = "CONSUME_SUCCESS"; |
||||
|
|
||||
|
/** |
||||
|
* 重复消息redis前缀 |
||||
|
*/ |
||||
|
public final static String REPEAT_REDIS_KEY_PREFIX = "RepeatMessage:"; |
||||
|
|
||||
|
} |
||||
@ -1,84 +0,0 @@ |
|||||
package com.bnyer.common.rocketmq.domain; |
|
||||
|
|
||||
import com.alibaba.fastjson.JSON; |
|
||||
import com.bnyer.common.rocketmq.enums.EnumMessageStatus; |
|
||||
import lombok.Builder; |
|
||||
import lombok.Getter; |
|
||||
import lombok.Setter; |
|
||||
import org.apache.rocketmq.client.producer.SendResult; |
|
||||
import org.apache.rocketmq.client.producer.SendStatus; |
|
||||
|
|
||||
import java.time.LocalDateTime; |
|
||||
import java.util.Objects; |
|
||||
|
|
||||
/** |
|
||||
* @author :WXC |
|
||||
* @Date :2023/05/18 |
|
||||
* @description :消息日志 |
|
||||
*/ |
|
||||
@Getter |
|
||||
@Setter |
|
||||
@Builder |
|
||||
public class MessageLog <T extends BaseMessage>{ |
|
||||
|
|
||||
/** |
|
||||
* 消息key,作为redis的key,消费的时候判断是否存在, |
|
||||
* 存在就判断状态是否是已消费,如果不是就进行消费,如果是就过滤该消息 |
|
||||
*/ |
|
||||
private String messageKey; |
|
||||
|
|
||||
/** |
|
||||
* 消息主题 |
|
||||
*/ |
|
||||
private String topic; |
|
||||
|
|
||||
/** |
|
||||
* 消息tag |
|
||||
*/ |
|
||||
private String tag; |
|
||||
|
|
||||
/** |
|
||||
* 消费组名称 |
|
||||
*/ |
|
||||
private String consumerGroupName; |
|
||||
|
|
||||
/** |
|
||||
* 消息状态 |
|
||||
*/ |
|
||||
private EnumMessageStatus status; |
|
||||
|
|
||||
/** |
|
||||
* 错误信息 |
|
||||
*/ |
|
||||
private String errMsg; |
|
||||
|
|
||||
/** |
|
||||
* 消费成功时间 |
|
||||
*/ |
|
||||
private LocalDateTime consumerTime; |
|
||||
|
|
||||
/** |
|
||||
* 消息内容 |
|
||||
*/ |
|
||||
private String message; |
|
||||
|
|
||||
/** |
|
||||
* 重试次数,用于判断重试次数,超过最大重试次数就人工补偿 |
|
||||
*/ |
|
||||
protected Integer retryTimes = 0; |
|
||||
|
|
||||
public MessageLog(T message, SendResult sendResult) { |
|
||||
this.messageKey = message.getKey(); |
|
||||
this.message = JSON.toJSONString(message); |
|
||||
if (Objects.isNull(sendResult)){ |
|
||||
this.status = EnumMessageStatus.WALT_SEND; |
|
||||
}else { |
|
||||
if (SendStatus.SEND_OK == sendResult.getSendStatus()){ |
|
||||
this.status = EnumMessageStatus.SEND_OK; |
|
||||
}else { |
|
||||
this.status = EnumMessageStatus.SEND_FAILS; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -0,0 +1,37 @@ |
|||||
|
package com.bnyer.common.rocketmq.domain; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Getter; |
||||
|
import lombok.ToString; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/20 |
||||
|
* @description : 重复消息元素 |
||||
|
*/ |
||||
|
@AllArgsConstructor |
||||
|
@Getter |
||||
|
@ToString |
||||
|
public class RepeatElement { |
||||
|
/** |
||||
|
* 消费的应用 |
||||
|
*/ |
||||
|
private String applicationName; |
||||
|
/** |
||||
|
* 唯一key |
||||
|
*/ |
||||
|
private String messageKey; |
||||
|
/** |
||||
|
* 消费组 |
||||
|
*/ |
||||
|
private String consumerGroupName; |
||||
|
/** |
||||
|
* 主题 |
||||
|
*/ |
||||
|
private String topic; |
||||
|
/** |
||||
|
* 标签 |
||||
|
*/ |
||||
|
private String tag; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
package com.bnyer.common.rocketmq.domain.order; |
||||
|
|
||||
|
import com.bnyer.common.core.enums.EnumMessageStatus; |
||||
|
import com.bnyer.common.rocketmq.domain.BaseMessage; |
||||
|
import com.fasterxml.jackson.annotation.JsonFormat; |
||||
|
import lombok.Getter; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
import lombok.Setter; |
||||
|
|
||||
|
import java.util.Date; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/18 |
||||
|
* @description :订单服务本地消息表记录 |
||||
|
*/ |
||||
|
@Getter |
||||
|
@Setter |
||||
|
@NoArgsConstructor |
||||
|
public class OrderMqLocalRecordMessage extends BaseMessage { |
||||
|
private Long id; |
||||
|
|
||||
|
/** |
||||
|
* 消息状态 |
||||
|
*/ |
||||
|
private EnumMessageStatus status; |
||||
|
|
||||
|
/** |
||||
|
* 消息内容 |
||||
|
*/ |
||||
|
private String content; |
||||
|
|
||||
|
/** |
||||
|
* 创建时间 |
||||
|
*/ |
||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
||||
|
private Date createTime; |
||||
|
|
||||
|
} |
||||
@ -1,28 +0,0 @@ |
|||||
package com.bnyer.common.rocketmq.enums; |
|
||||
|
|
||||
import lombok.AllArgsConstructor; |
|
||||
import lombok.Getter; |
|
||||
|
|
||||
/** |
|
||||
* @author :WXC |
|
||||
* @Date :2023/05/18 |
|
||||
* @description :消息状态 |
|
||||
*/ |
|
||||
@Getter |
|
||||
@AllArgsConstructor |
|
||||
public enum EnumMessageStatus { |
|
||||
//待发送
|
|
||||
WALT_SEND, |
|
||||
//发送成功
|
|
||||
SEND_OK, |
|
||||
//发送失败
|
|
||||
SEND_FAILS, |
|
||||
//待消费
|
|
||||
WALT_CONSUMER, |
|
||||
//消费中
|
|
||||
CONSUMER_PROCESS, |
|
||||
//消费成功
|
|
||||
CONSUMER_OK, |
|
||||
//消费失败
|
|
||||
CONSUMER_FAILS |
|
||||
} |
|
||||
@ -0,0 +1,49 @@ |
|||||
|
package com.bnyer.common.rocketmq.persist; |
||||
|
|
||||
|
|
||||
|
import com.bnyer.common.rocketmq.domain.BaseMessage; |
||||
|
import com.bnyer.common.rocketmq.domain.RepeatElement; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/20 |
||||
|
* @description : 持久层处理重复消息,目前支持redis,可扩展mysql |
||||
|
*/ |
||||
|
public interface IPersist { |
||||
|
|
||||
|
/** |
||||
|
* 设置正在消费的消息到存储中 |
||||
|
* @param repeatElement |
||||
|
* @param processingExpireMilliSeconds |
||||
|
* @return |
||||
|
* @param <T> |
||||
|
*/ |
||||
|
<T extends BaseMessage> boolean setConsumingIfNX(RepeatElement repeatElement, long processingExpireMilliSeconds); |
||||
|
|
||||
|
/** |
||||
|
* 删除存储中的消息 |
||||
|
* @param repeatElement |
||||
|
* @param <T> |
||||
|
*/ |
||||
|
<T extends BaseMessage> void delete(RepeatElement repeatElement); |
||||
|
|
||||
|
/** |
||||
|
* 修改存储中的消息 |
||||
|
* @param repeatElement |
||||
|
* @param recordReserveMinutes |
||||
|
* @param <T> |
||||
|
*/ |
||||
|
<T extends BaseMessage> void markConsumed(RepeatElement repeatElement, long recordReserveMinutes); |
||||
|
|
||||
|
/** |
||||
|
* 获取存储中的消息 |
||||
|
* @param repeatElement |
||||
|
* @return |
||||
|
* @param <T> |
||||
|
*/ |
||||
|
<T extends BaseMessage> String get(RepeatElement repeatElement); |
||||
|
|
||||
|
default <T extends BaseMessage> String toPrintInfo(RepeatElement repeatElement) { |
||||
|
return repeatElement.toString(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,81 @@ |
|||||
|
package com.bnyer.common.rocketmq.persist; |
||||
|
|
||||
|
|
||||
|
import com.bnyer.common.rocketmq.domain.BaseMessage; |
||||
|
import com.bnyer.common.rocketmq.domain.RepeatElement; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.springframework.data.redis.connection.RedisStringCommands; |
||||
|
import org.springframework.data.redis.core.RedisCallback; |
||||
|
import org.springframework.data.redis.core.StringRedisTemplate; |
||||
|
import org.springframework.data.redis.core.types.Expiration; |
||||
|
|
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
import static com.bnyer.common.rocketmq.constant.RocketMqRepeatConstant.*; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/20 |
||||
|
* @description : redis持久层 |
||||
|
*/ |
||||
|
public class RedisPersist implements IPersist { |
||||
|
|
||||
|
private final StringRedisTemplate redisTemplate; |
||||
|
|
||||
|
public RedisPersist(StringRedisTemplate redisTemplate) { |
||||
|
if (redisTemplate == null) { |
||||
|
throw new NullPointerException("redis template is null"); |
||||
|
} |
||||
|
this.redisTemplate = redisTemplate; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <T extends BaseMessage> boolean setConsumingIfNX(RepeatElement repeatElement, long dedupProcessingExpireMilliSeconds) { |
||||
|
String repeatKey = buildRepeatMessageRedisKey(repeatElement.getApplicationName(), repeatElement.getTopic(), repeatElement.getTag(), repeatElement.getMessageKey()); |
||||
|
//setnx, 成功就可以消费
|
||||
|
Boolean execute = redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> redisConnection.set(repeatKey.getBytes(), |
||||
|
(CONSUME_STATUS_CONSUMING).getBytes(), Expiration.milliseconds(dedupProcessingExpireMilliSeconds), RedisStringCommands.SetOption.SET_IF_ABSENT)); |
||||
|
if (execute == null) { |
||||
|
return false; |
||||
|
} |
||||
|
return execute; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <T extends BaseMessage> void delete(RepeatElement repeatElement) { |
||||
|
String repeatKey = buildRepeatMessageRedisKey(repeatElement.getApplicationName(), repeatElement.getTopic(), repeatElement.getTag(), repeatElement.getMessageKey()); |
||||
|
redisTemplate.delete(repeatKey); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <T extends BaseMessage> void markConsumed(RepeatElement repeatElement, long dedupRecordReserveMinutes) { |
||||
|
String repeatKey = buildRepeatMessageRedisKey(repeatElement.getApplicationName(), repeatElement.getTopic(), repeatElement.getTag(), repeatElement.getMessageKey()); |
||||
|
redisTemplate.opsForValue().set(repeatKey, CONSUME_STATUS_SUCCESS, dedupRecordReserveMinutes, TimeUnit.MINUTES); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <T extends BaseMessage> String get(RepeatElement repeatElement) { |
||||
|
String repeatKey = buildRepeatMessageRedisKey(repeatElement.getApplicationName(), repeatElement.getTopic(), repeatElement.getTag(), repeatElement.getMessageKey()); |
||||
|
return redisTemplate.opsForValue().get(repeatKey); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <T extends BaseMessage> String toPrintInfo(RepeatElement repeatElement) { |
||||
|
return buildRepeatMessageRedisKey(repeatElement.getApplicationName(), repeatElement.getTopic(), repeatElement.getTag(), repeatElement.getMessageKey()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建重复消息rediskey |
||||
|
* @param applicationName |
||||
|
* @param topic |
||||
|
* @param tag |
||||
|
* @param messageKey |
||||
|
* @return |
||||
|
*/ |
||||
|
private String buildRepeatMessageRedisKey(String applicationName, String topic, String tag, String messageKey) { |
||||
|
String prefix = REPEAT_REDIS_KEY_PREFIX + applicationName + ":" + topic + (StringUtils.isNotEmpty(tag) ? ":"+ tag :""); |
||||
|
return prefix + ":" + messageKey; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
package com.bnyer.common.rocketmq.strategy; |
||||
|
|
||||
|
|
||||
|
import com.bnyer.common.rocketmq.domain.BaseMessage; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/20 |
||||
|
* @description : |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public class NormalRepeatStrategy implements RepeatConsumerStrategy { |
||||
|
|
||||
|
@Override |
||||
|
public <T extends BaseMessage> boolean invoke(T message) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
package com.bnyer.common.rocketmq.strategy; |
||||
|
|
||||
|
|
||||
|
import com.bnyer.common.rocketmq.config.RepeatConsumerConfig; |
||||
|
import com.bnyer.common.rocketmq.domain.BaseMessage; |
||||
|
import com.bnyer.common.rocketmq.domain.RepeatElement; |
||||
|
import com.bnyer.common.rocketmq.persist.IPersist; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
|
||||
|
import java.util.function.Function; |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/20 |
||||
|
* @description :如果已经消费过,则直接消费幂等掉 |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public class RedisRepeatStrategy implements RepeatConsumerStrategy { |
||||
|
|
||||
|
private final RepeatConsumerConfig filterConfig; |
||||
|
|
||||
|
/** |
||||
|
* 获取去重键的函数 |
||||
|
*/ |
||||
|
private final Function<BaseMessage, String> repeatMessageKeyFunction; |
||||
|
|
||||
|
public RedisRepeatStrategy(RepeatConsumerConfig dedupConfig, Function<BaseMessage, String> dedupMessageKeyFunction) { |
||||
|
this.filterConfig = dedupConfig; |
||||
|
this.repeatMessageKeyFunction = dedupMessageKeyFunction; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public <T extends BaseMessage> boolean invoke(T message) { |
||||
|
return doInvoke(message); |
||||
|
} |
||||
|
|
||||
|
private <T extends BaseMessage> boolean doInvoke(T message) { |
||||
|
IPersist persist = filterConfig.getPersist(); |
||||
|
RepeatElement repeatElement = new RepeatElement(filterConfig.getApplicationName(), message.getConsumerGroupName(),message.getTopic() |
||||
|
, message.getTag()==null ? "" : message.getTag() |
||||
|
, repeatMessageKeyFunction.apply(message)); |
||||
|
boolean shouldConsume = true; |
||||
|
if (StringUtils.isNotBlank(repeatElement.getMessageKey())) { |
||||
|
shouldConsume = persist.setConsumingIfNX(repeatElement, filterConfig.getProcessingExpireMilliSeconds()); |
||||
|
} |
||||
|
//设置成功,证明没有消费过
|
||||
|
if (shouldConsume) { |
||||
|
return false; |
||||
|
} else { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
package com.bnyer.common.rocketmq.strategy; |
||||
|
|
||||
|
|
||||
|
import com.bnyer.common.rocketmq.domain.BaseMessage; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/20 |
||||
|
* @description : 重复消费策略,目前支持redis,可扩展mysql方式 |
||||
|
*/ |
||||
|
public interface RepeatConsumerStrategy { |
||||
|
|
||||
|
<T extends BaseMessage> boolean invoke(T message); |
||||
|
|
||||
|
} |
||||
|
|
||||
@ -0,0 +1,61 @@ |
|||||
|
package com.bnyer.order.listener; |
||||
|
|
||||
|
import com.bnyer.common.core.domain.OrderMqMessageRecord; |
||||
|
import com.bnyer.common.core.utils.bean.EntityConvertUtil; |
||||
|
import com.bnyer.common.rocketmq.constant.RocketMqTopic; |
||||
|
import com.bnyer.common.rocketmq.domain.order.OrderMqLocalRecordMessage; |
||||
|
import com.bnyer.common.rocketmq.handle.EnhanceMessageHandler; |
||||
|
import com.bnyer.order.service.OrderMqMessageRecordService; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; |
||||
|
import org.apache.rocketmq.spring.core.RocketMQListener; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import javax.annotation.Resource; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/20 |
||||
|
* @description : |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@Component |
||||
|
@RocketMQMessageListener(topic = RocketMqTopic.ORDER_RETURN_MSG_TOPIC,consumerGroup = RocketMqTopic.ORDER_RETURN_MSG_TOPIC) |
||||
|
public class OrderReturnMessageConsumer extends EnhanceMessageHandler<OrderMqLocalRecordMessage> implements RocketMQListener<OrderMqLocalRecordMessage> { |
||||
|
|
||||
|
@Resource |
||||
|
private OrderMqMessageRecordService orderMqMessageRecordService; |
||||
|
|
||||
|
@Override |
||||
|
public void onMessage(OrderMqLocalRecordMessage message) { |
||||
|
super.dispatchMessage(message); |
||||
|
OrderMqMessageRecord orderMqMessageRecord = EntityConvertUtil.copy(message, OrderMqMessageRecord.class); |
||||
|
orderMqMessageRecordService.editMessageRecord(orderMqMessageRecord); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void handleMessage(OrderMqLocalRecordMessage message) throws Exception { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void handleMaxRetriesExceeded(OrderMqLocalRecordMessage message) { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected boolean filter(OrderMqLocalRecordMessage message) { |
||||
|
return super.handleMsgRepeat(message); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected boolean isRetry() { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected boolean throwException() { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
package com.bnyer.order.mapper; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
|
import com.bnyer.common.core.domain.OrderMqMessageRecord; |
||||
|
import org.apache.ibatis.annotations.Mapper; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @description : |
||||
|
*/ |
||||
|
@Mapper |
||||
|
public interface OrderMqMessageRecordMapper extends BaseMapper<OrderMqMessageRecord> { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
package com.bnyer.order.service; |
||||
|
|
||||
|
import com.bnyer.common.core.domain.OrderMqMessageRecord; |
||||
|
import com.bnyer.common.rocketmq.domain.BaseMessage; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/20 |
||||
|
* @description :订单服务本地消息service层 |
||||
|
*/ |
||||
|
public interface OrderMqMessageRecordService { |
||||
|
|
||||
|
/** |
||||
|
* 发送异步消息 |
||||
|
* @param topic |
||||
|
* @param tag |
||||
|
* @param addUserVipRecordMessage |
||||
|
* @param <T> |
||||
|
*/ |
||||
|
<T> void sendAsyncMsg(String topic, String tag, T addUserVipRecordMessage); |
||||
|
|
||||
|
/** |
||||
|
* 发送异步延时消息 |
||||
|
* @param topic |
||||
|
* @param tag |
||||
|
* @param message |
||||
|
* @param delayLevel |
||||
|
* @param <T> |
||||
|
*/ |
||||
|
<T> void sendAsyncMsg(String topic, String tag, T message, int delayLevel); |
||||
|
|
||||
|
/** |
||||
|
* 保存消息记录 |
||||
|
* @param topic |
||||
|
* @param tag |
||||
|
* @param message |
||||
|
* @return |
||||
|
* @param <T> |
||||
|
*/ |
||||
|
<T> OrderMqMessageRecord saveMessageRecord(String topic, String tag, T message); |
||||
|
|
||||
|
/** |
||||
|
* 修改消息记录 |
||||
|
* @param orderMqMessageRecord |
||||
|
*/ |
||||
|
void editMessageRecord(OrderMqMessageRecord orderMqMessageRecord); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
package com.bnyer.order.service.impl; |
||||
|
|
||||
|
import com.alibaba.fastjson.JSON; |
||||
|
import com.bnyer.common.core.constant.ServiceNameConstants; |
||||
|
import com.bnyer.common.core.domain.OrderMqMessageRecord; |
||||
|
import com.bnyer.common.core.enums.EnumMessageStatus; |
||||
|
import com.bnyer.common.core.utils.bean.EntityConvertUtil; |
||||
|
import com.bnyer.common.core.utils.uuid.IdUtils; |
||||
|
import com.bnyer.common.rocketmq.domain.order.OrderMqLocalRecordMessage; |
||||
|
import com.bnyer.common.rocketmq.template.RocketMQEnhanceTemplate; |
||||
|
import com.bnyer.order.mapper.OrderMqMessageRecordMapper; |
||||
|
import com.bnyer.order.service.OrderMqMessageRecordService; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.rocketmq.client.producer.SendCallback; |
||||
|
import org.apache.rocketmq.client.producer.SendResult; |
||||
|
import org.apache.rocketmq.spring.core.RocketMQTemplate; |
||||
|
import org.springframework.messaging.support.MessageBuilder; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.transaction.annotation.Transactional; |
||||
|
|
||||
|
import javax.annotation.Resource; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
/** |
||||
|
* @author :WXC |
||||
|
* @Date :2023/05/19 |
||||
|
* @description : |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@Service |
||||
|
public class OrderMqMessageRecordServiceImpl implements OrderMqMessageRecordService { |
||||
|
|
||||
|
/** |
||||
|
* 发送超时时间 |
||||
|
*/ |
||||
|
private final static int TIME_OUT = 10000; |
||||
|
|
||||
|
@Resource |
||||
|
private RocketMQEnhanceTemplate rocketMQEnhanceTemplate; |
||||
|
|
||||
|
@Resource |
||||
|
private OrderMqMessageRecordMapper orderMqMessageRecordMapper; |
||||
|
|
||||
|
/** |
||||
|
* 发送异步消息(通过线程池执行发送到broker的消息任务,执行完后回调:在SendCallback中可处理相关成功失败时的逻辑) |
||||
|
* (适合对响应时间敏感的业务场景) |
||||
|
*/ |
||||
|
@Transactional |
||||
|
@Override |
||||
|
public <T> void sendAsyncMsg(String topic, String tag, T message) { |
||||
|
RocketMQTemplate rocketMQTemplate = rocketMQEnhanceTemplate.getTemplate(); |
||||
|
//保存消息记录
|
||||
|
log.info("消息发送中,开始入库本地消息记录表"); |
||||
|
OrderMqMessageRecord orderMqMessageRecord = saveMessageRecord(topic,tag, message); |
||||
|
//发消息
|
||||
|
OrderMqLocalRecordMessage mqLocalMessage = EntityConvertUtil.copy(orderMqMessageRecord, OrderMqLocalRecordMessage.class); |
||||
|
mqLocalMessage.setSource(ServiceNameConstants.ORDER_SERVICE); |
||||
|
rocketMQTemplate.asyncSend(rocketMQEnhanceTemplate.buildDestination(topic,tag), MessageBuilder.withPayload(mqLocalMessage).build(), new SendCallback() { |
||||
|
@Override |
||||
|
public void onSuccess(SendResult sendResult) { |
||||
|
log.info("消息发送成功,topic:{},tag:{},result:{}",topic,tag, JSON.toJSONString(sendResult)); |
||||
|
} |
||||
|
@Override |
||||
|
public void onException(Throwable throwable) { |
||||
|
log.error("消息发送失败,topic:{},tag:{},error:{}",topic,tag,throwable.getMessage()); |
||||
|
log.info("消息发送失败,更新消息记录,等待定时任务进行消息补偿"); |
||||
|
orderMqMessageRecord.setErrMsg(throwable.getMessage()); |
||||
|
editMessageRecord(orderMqMessageRecord); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送异步延时消息(通过线程池执行发送到broker的消息任务,执行完后回调:在SendCallback中可处理相关成功失败时的逻辑) |
||||
|
* (适合对响应时间敏感的业务场景) |
||||
|
*/ |
||||
|
@Transactional |
||||
|
@Override |
||||
|
public <T> void sendAsyncMsg(String topic, String tag, T message, int delayLevel) { |
||||
|
RocketMQTemplate rocketMQTemplate = rocketMQEnhanceTemplate.getTemplate(); |
||||
|
//保存消息记录
|
||||
|
log.info("消息发送中,开始入库本地消息记录表"); |
||||
|
OrderMqMessageRecord orderMqMessageRecord = saveMessageRecord(topic,tag, message); |
||||
|
//发消息
|
||||
|
OrderMqLocalRecordMessage mqLocalMessage = EntityConvertUtil.copy(orderMqMessageRecord, OrderMqLocalRecordMessage.class); |
||||
|
mqLocalMessage.setRetryTimes(0); |
||||
|
mqLocalMessage.setSource(ServiceNameConstants.ORDER_SERVICE); |
||||
|
rocketMQTemplate.asyncSend(rocketMQEnhanceTemplate.buildDestination(topic,tag), MessageBuilder.withPayload(mqLocalMessage).build(), new SendCallback() { |
||||
|
@Override |
||||
|
public void onSuccess(SendResult sendResult) { |
||||
|
log.info("消息发送成功,topic:{},tag:{},result:{}",topic,tag,JSON.toJSONString(sendResult)); |
||||
|
} |
||||
|
@Override |
||||
|
public void onException(Throwable throwable) { |
||||
|
log.error("消息发送失败,topic:{},tag:{},error:{}",topic,tag,throwable.getMessage()); |
||||
|
log.info("消息发送失败,更新消息记录,等待定时任务进行消息补偿"); |
||||
|
orderMqMessageRecord.setErrMsg(throwable.getMessage()); |
||||
|
editMessageRecord(orderMqMessageRecord); |
||||
|
} |
||||
|
},TIME_OUT,delayLevel); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加消息记录 |
||||
|
* @param message |
||||
|
* @param <T> |
||||
|
*/ |
||||
|
@Transactional |
||||
|
public <T> OrderMqMessageRecord saveMessageRecord(String topic, String tag, T message){ |
||||
|
RocketMQTemplate rocketMQTemplate = rocketMQEnhanceTemplate.getTemplate(); |
||||
|
String consumerGroup = rocketMQTemplate.getConsumer().getConsumerGroup(); |
||||
|
OrderMqMessageRecord orderMqMessageRecord = new OrderMqMessageRecord(); |
||||
|
orderMqMessageRecord.setStatus(EnumMessageStatus.PROCESS); |
||||
|
orderMqMessageRecord.setConsumerGroupName(consumerGroup); |
||||
|
orderMqMessageRecord.setTopic(topic); |
||||
|
orderMqMessageRecord.setTag(tag); |
||||
|
orderMqMessageRecord.setCreateTime(new Date()); |
||||
|
orderMqMessageRecord.setMessageKey(IdUtils.randomUUID()); |
||||
|
orderMqMessageRecordMapper.insert(orderMqMessageRecord); |
||||
|
return orderMqMessageRecord; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改消息记录 |
||||
|
* @param orderMqMessageRecord |
||||
|
*/ |
||||
|
@Transactional |
||||
|
public void editMessageRecord(OrderMqMessageRecord orderMqMessageRecord){ |
||||
|
orderMqMessageRecordMapper.updateById(orderMqMessageRecord); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
<?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.bnyer.order.mapper.OrderMqMessageRecordMapper"> |
||||
|
<resultMap id="BaseResultMap" type="com.bnyer.common.core.domain.OrderMqMessageRecord"> |
||||
|
<!--order_mq_message--> |
||||
|
<id column="id" jdbcType="BIGINT" property="id" /> |
||||
|
<result column="message_key" jdbcType="VARCHAR" property="messageKey" /> |
||||
|
<result column="topic" jdbcType="VARCHAR" property="topic" /> |
||||
|
<result column="tag" jdbcType="VARCHAR" property="tag" /> |
||||
|
<result column="consumer_group_name" jdbcType="VARCHAR" property="consumerGroupName" /> |
||||
|
<result column="status" jdbcType="VARCHAR" property="status" /> |
||||
|
<result column="err_msg" jdbcType="VARCHAR" property="errMsg" /> |
||||
|
<result column="content" jdbcType="VARCHAR" property="content" /> |
||||
|
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" /> |
||||
|
</resultMap> |
||||
|
|
||||
|
<sql id="Base_Column_List"> |
||||
|
t.id, |
||||
|
t.message_key, |
||||
|
t.topic, |
||||
|
t.tag, |
||||
|
t.consumer_group_name, |
||||
|
t.status, |
||||
|
t.err_msg, |
||||
|
t.content, |
||||
|
t.create_time, |
||||
|
</sql> |
||||
|
|
||||
|
</mapper> |
||||
Loading…
Reference in new issue