diff --git a/mq-client-common-open/pom.xml b/mq-client-common-open/pom.xml index b2d072d4..09f7b20d 100644 --- a/mq-client-common-open/pom.xml +++ b/mq-client-common-open/pom.xml @@ -6,7 +6,7 @@ com.sohu.tv mq - 4.7.2 + 4.9.1 mq-client-common-open diff --git a/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/BatchConsumerCallback.java b/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/BatchConsumerCallback.java index 7cfbacd0..4e0ff887 100644 --- a/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/BatchConsumerCallback.java +++ b/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/BatchConsumerCallback.java @@ -10,12 +10,13 @@ * @param msg obj * @param MessageExt */ -public interface BatchConsumerCallback { +public interface BatchConsumerCallback { /** * 订阅回调方法 - * - * @return + * @param batchMessage + * @param context @ConsumeConcurrentlyContext or @ConsumeOrderlyContext + * @throws Exception */ - void call(List> batchMessage) throws Exception; + void call(List> batchMessage, C context) throws Exception; } diff --git a/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/MQMessage.java b/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/MQMessage.java index 04b98947..77196b07 100644 --- a/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/MQMessage.java +++ b/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/MQMessage.java @@ -1,10 +1,8 @@ package com.sohu.index.tv.mq.common; -import java.nio.ByteBuffer; - -import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageClientExt; import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.sysflag.MessageSysFlag; /** * 批量消息 @@ -15,12 +13,27 @@ * @param */ public class MQMessage { + + public static final String IDEMPOTENT_ID = "IDEMPOTENT_ID"; + + // 发送的原始对象 private T message; - private MessageExt messageExt; - public MQMessage(T message, MessageExt messageExt) { + // 可以重试的次数 + private int retryTimes = -1; + + // rocketmq 消息 + private Message innerMessage; + + // 发送异常,测试用 + private boolean exceptionForTest; + + public MQMessage() { + } + + public MQMessage(T message, Message innerMessage) { this.message = message; - this.messageExt = messageExt; + this.innerMessage = innerMessage; } public T getMessage() { @@ -32,22 +45,129 @@ public void setMessage(T message) { } public MessageExt getMessageExt() { - return messageExt; + return (MessageExt) innerMessage; } public void setMessageExt(MessageExt messageExt) { - this.messageExt = messageExt; + setInnerMessage(messageExt); } - + + public Message getInnerMessage() { + return innerMessage; + } + + public void setInnerMessage(Message innerMessage) { + this.innerMessage = innerMessage; + } + + public MQMessage setKeys(String keys) { + innerMessage.setKeys(keys); + return this; + } + + public String getKeys() { + return innerMessage.getKeys(); + } + + public String getTags() { + return innerMessage.getTags(); + } + + public MQMessage setTags(String tags) { + innerMessage.setTags(tags); + return this; + } + + public MQMessage setDelayTimeLevel(int level) { + innerMessage.setDelayTimeLevel(level); + return this; + } + + public int getDelayTimeLevel() { + return innerMessage.getDelayTimeLevel(); + } + + public byte[] getBody() { + return innerMessage.getBody(); + } + + public MQMessage setBody(byte[] body) { + innerMessage.setBody(body); + return this; + } + + public boolean isWaitStoreMsgOK() { + return innerMessage.isWaitStoreMsgOK(); + } + + public MQMessage setWaitStoreMsgOK(boolean waitStoreMsgOK) { + innerMessage.setWaitStoreMsgOK(waitStoreMsgOK); + return this; + } + + public MQMessage setTopic(String topic) { + innerMessage.setTopic(topic); + return this; + } + + public String getTopic() { + return innerMessage.getTopic(); + } + + public int getRetryTimes() { + return retryTimes; + } + + public MQMessage setRetryTimes(int retryTimes) { + this.retryTimes = retryTimes; + return this; + } + + public MQMessage resetRetryTimes(int retryTimes) { + if (this.retryTimes == -1) { + this.retryTimes = retryTimes; + } + return this; + } + /** * 构建offsetMsgId + * * @return */ public String buildOffsetMsgId() { - int msgIdLength = (messageExt.getSysFlag() & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 + 8 - : 16 + 4 + 8; - ByteBuffer byteBufferMsgId = ByteBuffer.allocate(msgIdLength); - return MessageDecoder.createMessageId(byteBufferMsgId, messageExt.getStoreHostBytes(), - messageExt.getCommitLogOffset()); + return innerMessage instanceof MessageClientExt ? ((MessageClientExt)innerMessage).getOffsetMsgId() : null; + } + + public static MQMessage build(T message) { + MQMessage mqMessage = new MQMessage<>(); + mqMessage.setMessage(message); + mqMessage.innerMessage = new Message(); + mqMessage.setWaitStoreMsgOK(true); + return mqMessage; + } + + /** + * 设置幂等id + * + * @param idempotentId + */ + public MQMessage setIdempotentID(String idempotentId) { + innerMessage.putUserProperty(IDEMPOTENT_ID, idempotentId); + return this; + } + + public MQMessage setExceptionForTest(boolean exceptionForTest) { + this.exceptionForTest = exceptionForTest; + return this; + } + + public boolean isExceptionForTest() { + return exceptionForTest; + } + + @Override + public String toString() { + return "[topic=" + getTopic() + ", message=" + message + ", retryTimes=" + retryTimes + "]"; } } diff --git a/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/Result.java b/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/Result.java index f6480511..64023602 100644 --- a/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/Result.java +++ b/mq-client-common-open/src/main/java/com/sohu/index/tv/mq/common/Result.java @@ -22,7 +22,15 @@ public class Result implements Serializable { /** * 异常信息 */ - private Exception exception; + private Throwable exception; + + // 正在重试 + private boolean retrying; + + // 重试过的次数 + private int retriedTimes; + + private MQMessage mqMessage; public Result(boolean isSuccess) { this.isSuccess = isSuccess; @@ -33,7 +41,7 @@ public Result(boolean isSuccess, T result) { this.result = result; } - public Result(boolean isSuccess, Exception exception) { + public Result(boolean isSuccess, Throwable exception) { this.isSuccess = isSuccess; this.exception = exception; } @@ -55,15 +63,42 @@ public void setResult(T result) { } public Exception getException() { - return exception; + return (Exception) exception; } - public void setException(Exception exception) { + public void setException(Throwable exception) { this.exception = exception; } + public boolean isRetrying() { + return retrying; + } + + public Result setRetrying(boolean retrying) { + this.retrying = retrying; + return this; + } + + public int getRetriedTimes() { + return retriedTimes; + } + + public void setRetriedTimes(int retriedTimes) { + this.retriedTimes = retriedTimes; + } + + @SuppressWarnings("unchecked") + public MQMessage getMqMessage() { + return (MQMessage) mqMessage; + } + + public void setMqMessage(MQMessage mqMessage) { + this.mqMessage = mqMessage; + } + @Override public String toString() { - return "Result [isSuccess=" + isSuccess + ", result=" + result + ", exception=" + exception + "]"; + return "Result [isSuccess=" + isSuccess + ", result=" + result + ", exception=" + exception + ", retrying=" + + retrying + ", retriedTimes=" + retriedTimes + "]"; } } diff --git a/mq-client-common-open/src/main/java/com/sohu/tv/mq/common/AbstractCommand.java b/mq-client-common-open/src/main/java/com/sohu/tv/mq/common/AbstractCommand.java index 1f28eeb7..837439df 100644 --- a/mq-client-common-open/src/main/java/com/sohu/tv/mq/common/AbstractCommand.java +++ b/mq-client-common-open/src/main/java/com/sohu/tv/mq/common/AbstractCommand.java @@ -42,16 +42,32 @@ public AbstractCommand(String groupKey, String commandKey, int poolSize, int tim .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(poolSize))); this.alerter = alerter; } + + /** + * 构建(信号量隔离) + * + * @param groupKey + * @param commandKey + * @param timeout 超时时间 + */ + public AbstractCommand(String groupKey, String commandKey, int timeout, Alerter alerter) { + super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey + "-semaphore")) + .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey + "-semaphore")) + .andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionIsolationStrategy( + HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE) + .withFallbackIsolationSemaphoreMaxConcurrentRequests(100) + .withExecutionIsolationSemaphoreMaxConcurrentRequests(50) + .withExecutionTimeoutInMilliseconds(timeout))); + this.alerter = alerter; + } protected T run() throws Exception { try { return invoke(); } catch (Exception e) { - logger.error("send err! "+getCommandGroup().name() + "-" + - getCommandKey().name() + ":" + - invokeErrorInfo(), - e); - throw new RuntimeException(e); + logger.error("group:{} command:{} param:{}", getCommandGroup().name(), getCommandKey().name(), + invokeErrorInfo(), e); + throw e; } } @@ -77,7 +93,7 @@ public T getFallback() { // 判断熔断器是否打开 if (super.isCircuitBreakerOpen()) { if (null != alerter) { - String info = "group:" + getCommandGroup().name() + " command:" + getCommandKey().name() + " err!"; + String info = "group:" + getCommandGroup().name() + " command:" + getCommandKey().name() + " circuitBreakerOpen!"; alerter.alert(info); } } diff --git a/mq-client-common-open/src/main/java/com/sohu/tv/mq/common/AbstractConfig.java b/mq-client-common-open/src/main/java/com/sohu/tv/mq/common/AbstractConfig.java index d6ed9800..1d700804 100644 --- a/mq-client-common-open/src/main/java/com/sohu/tv/mq/common/AbstractConfig.java +++ b/mq-client-common-open/src/main/java/com/sohu/tv/mq/common/AbstractConfig.java @@ -69,6 +69,8 @@ public abstract class AbstractConfig { // 是否设置了instanceName protected String instanceName; + + protected SohuAsyncTraceDispatcher traceDispatcher; public AbstractConfig(String group, String topic) { this.topic = topic; @@ -122,12 +124,14 @@ protected void init() { logger.error("http err, topic:{},group:{}", topic, group, e); } if (clusterInfoDTO == null) { - if (clusterInfoDTOResult.getStatus() == 201) { - logger.warn("please register your {}:{} topic:{} in MQCloud first, times:{}", - role() == 1 ? "producer" : "consumer", group, topic, times++); - } else { - logger.warn("fetch topic:{} group:{} cluster info err:{}, times:{}", getTopic(), group, - clusterInfoDTOResult.getMessage(), times++); + if (clusterInfoDTOResult != null) { + if (clusterInfoDTOResult.getStatus() == 201) { + logger.warn("please register your {}:{} topic:{} in MQCloud first, times:{}", + role() == 1 ? "producer" : "consumer", group, topic, times++); + } else { + logger.warn("fetch topic:{} group:{} cluster info err:{}, times:{}", getTopic(), group, + clusterInfoDTOResult.getMessage(), times++); + } } try { Thread.sleep(1000); @@ -231,7 +235,7 @@ protected void initTrace() { TraceRocketMQProducer traceRocketMQProducer = new TraceRocketMQProducer( CommonUtil.buildTraceTopicProducer(traceTopic), traceTopic); // 初始化TraceDispatcher - SohuAsyncTraceDispatcher traceDispatcher = new SohuAsyncTraceDispatcher(traceTopic); + traceDispatcher = new SohuAsyncTraceDispatcher(traceTopic); // 设置producer属性 traceRocketMQProducer.getProducer().setSendMsgTimeout(5000); traceRocketMQProducer.getProducer().setMaxMessageSize(traceDispatcher.getMaxMsgSize() - 10 * 1000); @@ -316,4 +320,10 @@ public void setInstanceName(String instanceName) { public String getInstanceName() { return instanceName; } + + public void shutdown(){ + if(traceDispatcher != null){ + traceDispatcher.shutdown(); + } + } } diff --git a/mq-client-common-open/src/main/java/com/sohu/tv/mq/dto/ConsumerConfigDTO.java b/mq-client-common-open/src/main/java/com/sohu/tv/mq/dto/ConsumerConfigDTO.java index 267a6478..9ab9db78 100644 --- a/mq-client-common-open/src/main/java/com/sohu/tv/mq/dto/ConsumerConfigDTO.java +++ b/mq-client-common-open/src/main/java/com/sohu/tv/mq/dto/ConsumerConfigDTO.java @@ -17,6 +17,9 @@ public class ConsumerConfigDTO { // 消费限速 private Boolean enableRateLimit; private Double permitsPerSecond; + + // 重试消息跳过的key + private String retryMessageSkipKey; public Long getRetryMessageResetTo() { return retryMessageResetTo; @@ -58,9 +61,18 @@ public void setPauseClientId(String pauseClientId) { this.pauseClientId = pauseClientId; } + public String getRetryMessageSkipKey() { + return retryMessageSkipKey; + } + + public void setRetryMessageSkipKey(String retryMessageSkipKey) { + this.retryMessageSkipKey = retryMessageSkipKey; + } + @Override public String toString() { return "ConsumerConfigDTO [retryMessageResetTo=" + retryMessageResetTo + ", pause=" + pause + ", pauseClientId=" - + pauseClientId + ", enableRateLimit=" + enableRateLimit + ", permitsPerSecond=" + permitsPerSecond + "]"; + + pauseClientId + ", enableRateLimit=" + enableRateLimit + ", permitsPerSecond=" + permitsPerSecond + + ", retryMessageSkipKey=" + retryMessageSkipKey + "]"; } } diff --git a/mq-client-common-open/src/main/java/com/sohu/tv/mq/stats/ConsumeStats.java b/mq-client-common-open/src/main/java/com/sohu/tv/mq/stats/ConsumeStats.java index 80df86d0..3f2262c4 100644 --- a/mq-client-common-open/src/main/java/com/sohu/tv/mq/stats/ConsumeStats.java +++ b/mq-client-common-open/src/main/java/com/sohu/tv/mq/stats/ConsumeStats.java @@ -46,7 +46,9 @@ private void initTask() { sampleExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { - return new Thread(r, "ConsumeStats-" + consumer); + Thread thread = new Thread(r, "ConsumeStats-" + consumer); + thread.setDaemon(true); + return thread; } }); sampleExecutorService.scheduleWithFixedDelay(new Runnable() { diff --git a/mq-client-common-open/src/main/java/com/sohu/tv/mq/util/Version.java b/mq-client-common-open/src/main/java/com/sohu/tv/mq/util/Version.java index cac8277b..0117dbff 100644 --- a/mq-client-common-open/src/main/java/com/sohu/tv/mq/util/Version.java +++ b/mq-client-common-open/src/main/java/com/sohu/tv/mq/util/Version.java @@ -7,6 +7,6 @@ public class Version { public static String get() { - return "4.7.2"; + return "4.9.1"; } } diff --git a/mq-client-open/pom.xml b/mq-client-open/pom.xml index 75ca854c..3c64f311 100644 --- a/mq-client-open/pom.xml +++ b/mq-client-open/pom.xml @@ -6,7 +6,7 @@ com.sohu.tv mq - 4.7.2 + 4.9.1 mq-client-open @@ -22,6 +22,11 @@ hystrix-core true + + redis.clients + jedis + true + ch.qos.logback logback-core diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/MessageConsumer.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/MessageConsumer.java deleted file mode 100644 index 9031c9b8..00000000 --- a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/MessageConsumer.java +++ /dev/null @@ -1,435 +0,0 @@ -package com.sohu.tv.mq.rocketmq; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; -import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; -import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; -import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; -import org.apache.rocketmq.common.message.MessageConst; -import org.apache.rocketmq.common.message.MessageExt; -import org.slf4j.Logger; - -import com.alibaba.fastjson.JSON; -import com.sohu.index.tv.mq.common.MQMessage; -import com.sohu.tv.mq.metric.ConsumeStatManager; -import com.sohu.tv.mq.metric.ConsumeThreadStat; -import com.sohu.tv.mq.metric.MQMetricsExporter; -import com.sohu.tv.mq.metric.MessageExceptionMetric; -import com.sohu.tv.mq.metric.MessageMetric; -import com.sohu.tv.mq.serializable.MessageSerializer; -import com.sohu.tv.mq.serializable.MessageSerializerEnum; -import com.sohu.tv.mq.stats.ConsumeStats; -import com.sohu.tv.mq.util.CommonUtil; -/** - * 消息消费公共逻辑抽取 - * - * @author yongfeigao - * @date 2019年1月22日 - */ -public class MessageConsumer { - - private RocketMQConsumer rocketMQConsumer; - - // 消费策略 - private ConsumerStrategy consumerStrategy; - - private Logger logger; - - // 消费统计 - private ConsumeStats consumeStats; - - public MessageConsumer(RocketMQConsumer rocketMQConsumer) { - this.rocketMQConsumer = rocketMQConsumer; - logger = rocketMQConsumer.getLogger(); - consumerStrategy = new ConsumerStrategy(); - if (rocketMQConsumer.isEnableStats()) { - consumeStats = new ConsumeStats(rocketMQConsumer.getGroup()); - MQMetricsExporter.getInstance().add(consumeStats); - } - } - - /** - * 消费并发消息 - * @param msgs - * @param context - * @return - */ - public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { - long start = System.currentTimeMillis(); - ConsumeStatus consumeStatus = consumerStrategy.chooseConsumer().consume(msgs); - if (ConsumeStatus.FAIL == consumeStatus && rocketMQConsumer.isReconsume()) { - if (consumeStats != null) { - consumeStats.incrementException(); - } - return ConsumeConcurrentlyStatus.RECONSUME_LATER; - } - if (consumeStats != null) { - consumeStats.increment(System.currentTimeMillis() - start); - } - return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; - } - - /** - * 消费顺序消息 - * @param msgs - * @param context - * @return - */ - public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { - ConsumeStatus consumeStatus = consumerStrategy.chooseConsumer().consume(msgs); - if (ConsumeStatus.FAIL == consumeStatus && rocketMQConsumer.isReconsume()) { - return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; - } - return ConsumeOrderlyStatus.SUCCESS; - } - - /** - * 获取许可 - */ - private void acquirePermit() { - try { - rocketMQConsumer.getRateLimiter().limit(); - } catch (InterruptedException e) { - logger.warn("acquirePermit error", e.getMessage()); - } - } - - /** - * 获取许可 - */ - private void acquirePermit(int permits) { - try { - rocketMQConsumer.getRateLimiter().limit(permits); - } catch (InterruptedException e) { - logger.warn("acquirePermit error", e.getMessage()); - } - } - - /** - * 消费状态 - * - * @author yongfeigao - * @date 2019年1月22日 - */ - private enum ConsumeStatus { - OK, - FAIL, - ; - } - - /** - * 消费者 - * - * @author yongfeigao - * @date 2019年10月21日 - */ - private interface IConsumer { - public ConsumeStatus consume(List msgs); - } - - /** - * 消费逻辑抽象 - * - * @author yongfeigao - * @date 2019年10月21日 - * @param - */ - private abstract class AbstractConsumer implements IConsumer { - - /** - * 消费逻辑 - */ - public ConsumeStatus consume(List msgs) { - // 解析消息 - List> messageList = parse(msgs); - if (messageList == null || messageList.isEmpty()) { - return ConsumeStatus.OK; - } - // 设置消费线程统计 - ConsumeThreadStat metric = ConsumeStatManager.getInstance() - .getConsumeThreadMetrics(rocketMQConsumer.getGroup()); - try { - metric.set(buildThreadConsumeMetric(messageList)); - // 消费消息 - for (MQMessage mqMessage : messageList) { - try { - // 获取许可 - acquirePermit(); - consume(mqMessage.getMessage(), mqMessage.getMessageExt()); - } catch (Throwable e) { - logger.error("consume topic:{} consumer:{} msgId:{} bornTimestamp:{}", - rocketMQConsumer.getTopic(), rocketMQConsumer.getGroup(), - mqMessage.getMessageExt().getMsgId(), mqMessage.getMessageExt().getBornTimestamp(), e); - ConsumeStatManager.getInstance().getConsumeFailedMetrics(rocketMQConsumer.getGroup()) - .set(buildMessageExceptionMetric(mqMessage, e)); - return ConsumeStatus.FAIL; - } - } - } finally { - metric.remove(); - } - return ConsumeStatus.OK; - } - - /** - * 解析消息 - * @param msgs - * @return - */ - protected List> parse(List msgs){ - if (msgs == null || msgs.isEmpty()) { - return null; - } - List> msgList = new ArrayList<>(msgs.size()); - for (MessageExt me : msgs) { - byte[] bytes = me.getBody(); - try { - if (bytes == null || bytes.length == 0) { - logger.warn("MessageExt={}, body is null", me); - continue; - } - // 校验是否需要跳过重试消息 - if(CommonUtil.isRetryTopic(me.getProperty(MessageConst.PROPERTY_REAL_TOPIC)) && - me.getBornTimestamp() < rocketMQConsumer.getRetryMessageResetTo()) { - logger.warn("skip topic:{} msgId:{} bornTime:{}", - rocketMQConsumer.getTopic(), me.getMsgId(), me.getBornTimestamp()); - continue; - } - msgList.add(buildMQMessage(me)); - } catch (Throwable e) { - // 解析失败打印警告,不再抛出异常重试(即使重试,仍然会失败) - logger.error("parse topic:{} consumer:{} msg:{} msgId:{} bornTimestamp:{}", - rocketMQConsumer.getTopic(), rocketMQConsumer.getGroup(), new String(bytes), me.getMsgId(), - me.getBornTimestamp(), e); - } - } - return msgList; - } - - @SuppressWarnings("unchecked") - private MQMessage buildMQMessage(MessageExt me) throws Exception { - byte[] bytes = me.getBody(); - // 无序列化器直接返回 - if (rocketMQConsumer.getMessageSerializer() == null) { - debugLog("null-serializer", me.getMsgId(), bytes.getClass().getName(), null); - return (MQMessage) new MQMessage<>(bytes, me); - } - // 反序列化 - T message = deserialize(me); - Class consumerParameterTypeClass = rocketMQConsumer.getConsumerParameterTypeClass(); - // 无法获取消费类型 - if (consumerParameterTypeClass == null) { - debugLog("null-consumerParameterType", me.getMsgId(), message.getClass().getName(), null); - return new MQMessage<>(message, me); - } - // 反序列化后类型相同直接返回 - if (consumerParameterTypeClass.isInstance(message)) { - debugLog("isInstance", me.getMsgId(), message.getClass().getName(), consumerParameterTypeClass.getName()); - return new MQMessage<>(message, me); - } - // 消费类型为String,采用JSON转换 - if (consumerParameterTypeClass == String.class) { - debugLog("String-consumerParameterType", me.getMsgId(), message.getClass().getName(), "String"); - return (MQMessage) new MQMessage<>(JSON.toJSONString(message), me); - } - // 消息为String,采用JSON转换 - if (message instanceof String) { - debugLog("String-Message", me.getMsgId(), "String", consumerParameterTypeClass.getName()); - return (MQMessage) new MQMessage<>( - JSON.parseObject(message.toString(), consumerParameterTypeClass), me); - } - debugLog("unknown", me.getMsgId(), message.getClass().getName(), consumerParameterTypeClass.getName()); - // 消费类型和消息都不是String,并且消息与消费类型不匹配,此时可能会类转换异常 - return (MQMessage) new MQMessage<>(message, me); - } - - private void debugLog(String flag, String msgId, String msgType, String consumerType) { - logger.debug("detectType:{} consumer:{} msgId:{} {}->{}", flag, rocketMQConsumer.getGroup(), msgId, msgType, - consumerType); - } - - /** - * 反序列化,若设置的反序列化器执行失败,则使用其他反序列化器进行尝试 - * @param bytes - * @param me - * @return - * @throws Exception - */ - @SuppressWarnings("unchecked") - private T deserialize(MessageExt me) throws Exception { - // 使用设置的序列化器 - MessageSerializer messageSerializer = rocketMQConsumer.getMessageSerializer(); - Exception excp = null; - try { - T t = (T) messageSerializer.deserialize(me.getBody()); - if (logger.isDebugEnabled()) { - logger.debug("consumer:{} msgId:{} deserializer:{}", - rocketMQConsumer.getGroup(), me.getMsgId(), messageSerializer.getClass().getName()); - } - return t; - } catch (Exception e) { - excp = e; - } - // 使用其他序列化器 - for (MessageSerializerEnum messageSerializerEnum : MessageSerializerEnum.values()) { - if (messageSerializer.getClass() == messageSerializerEnum.getMessageSerializer().getClass()) { - continue; - } - try { - T t = (T) messageSerializerEnum.getMessageSerializer().deserialize(me.getBody()); - if (logger.isDebugEnabled()) { - logger.debug("consumer:{} msgId:{} compatible deserializer:{}", - rocketMQConsumer.getGroup(), me.getMsgId(), - messageSerializerEnum.getMessageSerializer().getClass().getName()); - } - return t; - } catch (Exception e) { - logger.warn("try deserializer:{}, topic:{}, consumer:{} msgId:{} err", - messageSerializerEnum.getMessageSerializer().getClass().getName(), - rocketMQConsumer.getTopic(), rocketMQConsumer.getGroup(), me.getMsgId()); - } - } - throw excp; - } - - /** - * 具体消费逻辑 - * @param message - * @param msgExt - * @throws Exception - */ - public abstract void consume(T message, MessageExt msgExt) throws Exception; - - /** - * 构建线程消费统计 - * @param messageList - */ - protected MessageMetric buildThreadConsumeMetric(List> messageList) { - List idList = new ArrayList<>(messageList.size()); - messageList.forEach(message -> { - idList.add(message.buildOffsetMsgId()); - }); - MessageMetric messageMetric = new MessageMetric(); - messageMetric.setStartTime(System.currentTimeMillis()); - messageMetric.setMsgIdList(idList); - return messageMetric; - } - - /** - * 构建异常消费统计 - * @param messageList - */ - protected MessageExceptionMetric buildMessageExceptionMetric(List> messageList, Throwable e) { - List idList = new ArrayList<>(messageList.size()); - messageList.forEach(message -> { - idList.add(message.buildOffsetMsgId()); - }); - MessageExceptionMetric messageMetric = buildMessageExceptionMetric(e); - messageMetric.setMsgIdList(idList); - return messageMetric; - } - - /** - * 构建异常消费统计 - * @param messageList - */ - protected MessageExceptionMetric buildMessageExceptionMetric(MQMessage mqMessage, Throwable e) { - List idList = new ArrayList<>(1); - idList.add(mqMessage.buildOffsetMsgId()); - MessageExceptionMetric messageMetric = buildMessageExceptionMetric(e); - messageMetric.setMsgIdList(idList); - return messageMetric; - } - - protected MessageExceptionMetric buildMessageExceptionMetric(Throwable e) { - MessageExceptionMetric messageMetric = new MessageExceptionMetric(); - messageMetric.setStartTime(System.currentTimeMillis()); - messageMetric.setThreadId(Thread.currentThread().getId()); - messageMetric.setThreadName(Thread.currentThread().getName()); - messageMetric.setException(e); - return messageMetric; - } - } - - /** - * 消费者选择策略 - * - * @author yongfeigao - * @date 2019年10月21日 - */ - private class ConsumerStrategy { - - @SuppressWarnings("rawtypes") - private _ConsumerCallback _consumerCallback = new _ConsumerCallback(); - - private _BatchConsumerCallback _batchConsumerCallback = new _BatchConsumerCallback(); - - /** - * 挑选消费者 - * 优先选择ConsumerCallback - * 其次选择BatchConsumerCallback - * @return - */ - public IConsumer chooseConsumer() { - if (rocketMQConsumer.getConsumerCallback() != null) { - return _consumerCallback; - } - return _batchConsumerCallback; - } - } - - /** - * for @ConsumerCallback - * - * @author yongfeigao - * @date 2019年10月21日 - */ - private class _ConsumerCallback extends AbstractConsumer { - - @Override - @SuppressWarnings("unchecked") - public void consume(T message, MessageExt msgExt) throws Exception { - rocketMQConsumer.getConsumerCallback().call(message, msgExt); - } - - } - - /** - * for @BatchConsumerCallback - * - * @author yongfeigao - * @date 2019年10月21日 - */ - private class _BatchConsumerCallback extends AbstractConsumer { - - public ConsumeStatus consume(List msgs) { - List> msgList = parse(msgs); - if (msgList == null || msgList.isEmpty()) { - return ConsumeStatus.OK; - } - // 设置消费线程统计 - ConsumeThreadStat metric = ConsumeStatManager.getInstance().getConsumeThreadMetrics(rocketMQConsumer.getGroup()); - try { - metric.set(buildThreadConsumeMetric(msgList)); - // 获取许可 - acquirePermit(msgList.size()); - rocketMQConsumer.getBatchConsumerCallback().call(msgList); - } catch (Throwable e) { - logger.error("topic:{} consumer:{} msgSize:{}", - rocketMQConsumer.getTopic(), rocketMQConsumer.getGroup(), msgList.size(), e); - ConsumeStatManager.getInstance().getConsumeFailedMetrics(rocketMQConsumer.getGroup()) - .set(buildMessageExceptionMetric(msgList, e)); - return ConsumeStatus.FAIL; - } finally { - metric.remove(); - } - return ConsumeStatus.OK; - } - - @Override - public void consume(Object message, MessageExt msgExt) throws Exception { - } - } -} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/RocketMQConsumer.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/RocketMQConsumer.java index 1a1d74a9..f6d19d3c 100644 --- a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/RocketMQConsumer.java +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/RocketMQConsumer.java @@ -8,7 +8,6 @@ import java.net.HttpURLConnection; import java.util.List; import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; @@ -23,7 +22,6 @@ import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; import org.apache.rocketmq.client.impl.consumer.PullMessageService; -import org.apache.rocketmq.client.impl.consumer.PullRequest; import org.apache.rocketmq.client.impl.factory.MQClientInstance; import org.apache.rocketmq.client.trace.AsyncTraceDispatcher; import org.apache.rocketmq.client.trace.hook.ConsumeMessageTraceHookImpl; @@ -45,10 +43,15 @@ import com.sohu.tv.mq.dto.DTOResult; import com.sohu.tv.mq.metric.ConsumeStatManager; import com.sohu.tv.mq.netty.SohuClientRemotingProcessor; +import com.sohu.tv.mq.rocketmq.consumer.BatchMessageConsumer; +import com.sohu.tv.mq.rocketmq.consumer.IMessageConsumer; +import com.sohu.tv.mq.rocketmq.consumer.SingleMessageConsumer; +import com.sohu.tv.mq.rocketmq.consumer.deduplicate.DeduplicateSingleMessageConsumer; import com.sohu.tv.mq.rocketmq.limiter.LeakyBucketRateLimiter; import com.sohu.tv.mq.rocketmq.limiter.RateLimiter; import com.sohu.tv.mq.rocketmq.limiter.SwitchableRateLimiter; import com.sohu.tv.mq.rocketmq.limiter.TokenBucketRateLimiter; +import com.sohu.tv.mq.rocketmq.redis.IRedis; import com.sohu.tv.mq.util.Constant; /** @@ -62,7 +65,7 @@ public class RocketMQConsumer extends AbstractConfig { // 支持一批消息消费 - private BatchConsumerCallback batchConsumerCallback; + private BatchConsumerCallback batchConsumerCallback; /** * 消费者 @@ -98,12 +101,21 @@ public class RocketMQConsumer extends AbstractConfig { private Class consumerParameterTypeClass; - // 关闭等待最大时间 - private long shutdownWaitMaxMillis = 30000; - // 是否开启统计 private boolean enableStats = true; + // 重试消息跳过的key + private String retryMessageSkipKey; + + // 消费去重 + private boolean deduplicate; + + // 消费去重窗口时间,默认3分钟 + private int deduplicateWindowSeconds = 3 * 60 + 10; + + // 幂等消费用的redis + private IRedis redis; + /** * 一个应用创建一个Consumer,由应用来维护此对象,可以设置为全局对象或者单例
* ConsumerGroupName需要由应用来保证唯一 @@ -129,6 +141,8 @@ public RocketMQConsumer(String consumerGroup, String topic, boolean useLeakyBuck } // 注册线程统计 ConsumeStatManager.getInstance().register(getGroup()); + // 关闭最大等待时间 + getConsumer().setAwaitTerminationMillisWhenShutdown(10000); } public void start() { @@ -141,7 +155,7 @@ public void start() { consumer.subscribe(topic, subExpression); // 构建消费者对象 - final MessageConsumer messageConsumer = new MessageConsumer(this); + final IMessageConsumer messageConsumer = getMessageConsumer(); // 注册顺序或并发消费 if (consumeOrderly) { consumer.registerMessageListener(new MessageListenerOrderly() { @@ -228,6 +242,8 @@ public void run() { setRate(rate); } } + // 更新重试消息跳过的key + setRetryMessageSkipKey(consumerConfigDTO.getRetryMessageSkipKey()); } catch (Throwable ignored) { logger.warn("skipRetryMessage err:{}", ignored); } @@ -247,28 +263,11 @@ public void shutdown() { // 2.接着标记拉取线程停止,不直接关闭是为了把拉下来的消息消费完毕。 PullMessageService pull = innerConsumer.getmQClientFactory().getPullMessageService(); pull.makeStop(); - // 3.根据拉取任务数是否与处理队列数相等,来判断消息是否已经消费完毕;超过30秒则不再等待 - long start = System.currentTimeMillis(); - LinkedBlockingQueue q = getField(PullMessageService.class, "pullRequestQueue", pull); - int pullRequestSize = getPullRequestSize(q); - while (pullRequestSize != innerConsumer.getRebalanceImpl().getProcessQueueTable().size()) { - long use = System.currentTimeMillis() - start; - if (use > getShutdownWaitMaxMillis()) { - logger.warn("{} shutdown too long, use:{}ms, break!!, pullRequestQueueSize:{} processQueueTableSize:{}", - getGroup(), use, pullRequestSize, innerConsumer.getRebalanceImpl().getProcessQueueTable().size()); - break; - } - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - logger.warn("ignore interrupted!!"); - } - pullRequestSize = getPullRequestSize(q); - } // 4.如下为正常关闭流程 consumer.shutdown(); rateLimiter.shutdown(); clientConfigScheduledExecutorService.shutdown(); + super.shutdown(); } /** @@ -282,19 +281,6 @@ public void initAfterStart() { new SohuClientRemotingProcessor(mqClientInstance), null); } - private int getPullRequestSize(LinkedBlockingQueue q) { - if (q == null) { - return 0; - } - int size = 0; - for (PullRequest pullRequest : q) { - if (getGroup().equals(pullRequest.getConsumerGroup())) { - ++size; - } - } - return size; - } - /** * 获取类的字段实例 * @param clz @@ -324,7 +310,10 @@ public void setConsumeMessageBatchMaxSize(int consumeMessageBatchMaxSize) { if (consumeMessageBatchMaxSize <= 0) { return; } - consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize); + // 批量消息消费才允许设置 + if (consumerCallback == null && batchConsumerCallback != null) { + consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize); + } } public void setConsumeFromWhere(ConsumeFromWhere consumeFromWhere) { @@ -448,8 +437,8 @@ public ConsumerCallback getConsumerCallback() { } @SuppressWarnings("unchecked") - public BatchConsumerCallback getBatchConsumerCallback() { - return (BatchConsumerCallback) batchConsumerCallback; + public BatchConsumerCallback getBatchConsumerCallback() { + return (BatchConsumerCallback) batchConsumerCallback; } @SuppressWarnings({"rawtypes"}) @@ -535,9 +524,29 @@ public long getRetryMessageResetTo() { } public void setRetryMessageResetTo(long retryMessageResetTo) { - logger.info("topic:{}'s consumer:{} retryMessageReset {}->{}", getTopic(), getGroup(), this.retryMessageResetTo, retryMessageResetTo); + logger.info("topic:{}'s consumer:{} retryMessageReset {}->{}", getTopic(), getGroup(), this.retryMessageResetTo, + retryMessageResetTo); this.retryMessageResetTo = retryMessageResetTo; } + + public String getRetryMessageSkipKey() { + return retryMessageSkipKey; + } + + public void setRetryMessageSkipKey(String retryMessageSkipKey) { + if (this.retryMessageSkipKey == retryMessageSkipKey) { + return; + } + if (this.retryMessageSkipKey != null && this.retryMessageSkipKey.equals(retryMessageSkipKey)) { + return; + } + if (retryMessageSkipKey != null && retryMessageSkipKey.equals(this.retryMessageSkipKey)) { + return; + } + logger.info("topic:{}'s consumer:{} retryMessageSkipKey {}->{}", getTopic(), getGroup(), + this.retryMessageSkipKey, retryMessageSkipKey); + this.retryMessageSkipKey = retryMessageSkipKey; + } /** * 最大重新消费次数 @@ -594,12 +603,8 @@ public boolean isEnableRateLimit() { return false; } - public long getShutdownWaitMaxMillis() { - return shutdownWaitMaxMillis; - } - + @Deprecated public void setShutdownWaitMaxMillis(long shutdownWaitMaxMillis) { - this.shutdownWaitMaxMillis = shutdownWaitMaxMillis; } /** @@ -636,7 +641,7 @@ public void initRateLimiter(RateLimiter rateLimiter) { this.rateLimiter = switchableRateLimiter; } - protected Class getConsumerParameterTypeClass() { + public Class getConsumerParameterTypeClass() { return consumerParameterTypeClass; } @@ -729,4 +734,46 @@ private Class _getBatchConsumerParameterTypeClass() { } return null; } + + private IMessageConsumer getMessageConsumer() { + if (getConsumerCallback() != null) { + if (getRedis() != null) { + if (MessageModel.CLUSTERING.equals(consumer.getMessageModel())) { + return new DeduplicateSingleMessageConsumer<>(this); + } else { + logger.warn("consume message model is broadcasting, cannot use deduplication!"); + } + } + return new SingleMessageConsumer<>(this); + } + return new BatchMessageConsumer<>(this); + } + + public boolean isDeduplicate() { + return deduplicate; + } + + public void setDeduplicate(boolean deduplicate) { + this.deduplicate = deduplicate; + } + + public int getDeduplicateWindowSeconds() { + return deduplicateWindowSeconds; + } + + public void setDeduplicateWindowSeconds(int deduplicateWindowSeconds) { + this.deduplicateWindowSeconds = deduplicateWindowSeconds; + } + + public IRedis getRedis() { + return redis; + } + + /** + * 设置redis实例,用于幂等消费,请用 @RedisBuilder 构建 @IRedis 实例 + * @param redis + */ + public void setRedis(IRedis redis) { + this.redis = redis; + } } diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/RocketMQProducer.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/RocketMQProducer.java index 2df7e5ea..b004f6bd 100644 --- a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/RocketMQProducer.java +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/RocketMQProducer.java @@ -2,6 +2,14 @@ import java.util.Collection; import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; @@ -13,7 +21,9 @@ import org.apache.rocketmq.client.trace.AsyncTraceDispatcher; import org.apache.rocketmq.client.trace.hook.SendMessageTraceHookImpl; import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.exception.RemotingException; +import com.sohu.index.tv.mq.common.MQMessage; import com.sohu.index.tv.mq.common.Result; import com.sohu.tv.mq.common.AbstractConfig; import com.sohu.tv.mq.common.SohuSendMessageHook; @@ -37,6 +47,14 @@ public class RocketMQProducer extends AbstractConfig { // 发送顺序消息使用 private MessageQueueSelector messageQueueSelector; + // 默认重试次数 + private int defaultRetryTimes = 1; + + // 重试发送线程池 + private ExecutorService retrySenderExecutor; + + private Consumer> resendResultConsumer; + /** * 同样消息的Producer,归为同一个Group,应用必须设置,并保证命名唯一 */ @@ -75,6 +93,24 @@ public void start() { producer.getDefaultMQProducerImpl().registerSendMessageHook(hook); } producer.start(); + // 初始化重试线程池 + if (defaultRetryTimes > 0 && retrySenderExecutor == null) { + retrySenderExecutor = new ThreadPoolExecutor( + Runtime.getRuntime().availableProcessors(), + Runtime.getRuntime().availableProcessors(), + 1000 * 60, + TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(100), + new ThreadFactory() { + private AtomicInteger threadIndex = new AtomicInteger(0); + + @Override + public Thread newThread(Runnable r) { + return new Thread(r, + getGroup() + "-retrySenderExecutor-" + this.threadIndex.incrementAndGet()); + } + }); + } logger.info("topic:{} group:{} start", topic, group); } catch (MQClientException e) { logger.error(e.getMessage(), e); @@ -190,14 +226,24 @@ public Result publish(Message message) { SendResult sendResult = producer.send(message); return new Result(true, sendResult); } catch (Exception e) { - logger.error(e.getMessage(), e); - if(statsHelper != null) { - statsHelper.recordException(e); - } - return new Result(false, e); + return processException(e); } } + /** + * 异常处理 + * + * @param e + * @return + */ + public Result processException(Exception e) { + logger.error(e.getMessage(), e); + if (statsHelper != null) { + statsHelper.recordException(e); + } + return new Result(false, e); + } + /** * 批量发送消息 * @@ -615,12 +661,162 @@ public Result publishTransaction(Message message, Object arg) { return new Result(false, e); } } + + /** + * 发送消息 + * + * @param message 消息 + * @return 发送结果 + */ + @SuppressWarnings("rawtypes") + public Result send(MQMessage mqMessage) { + // 无body,序列化 + if (mqMessage.getBody() == null) { + try { + mqMessage.setBody(getMessageSerializer().serialize(mqMessage.getMessage())); + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new Result(false, e); + } + } + // 设置属性 + mqMessage.setTopic(getTopic()); + mqMessage.resetRetryTimes(this.defaultRetryTimes); + // 消息发送 + try { + SendResult sendResult = producer.send(mqMessage.getInnerMessage()); + if (mqMessage.isExceptionForTest()) { + logger.info("send ok: msgId:{} offsetMsgId:{}", sendResult.getMsgId(), sendResult.getOffsetMsgId()); + throw new RemotingException("exceptionForTest"); + } + return new Result(true, sendResult); + } catch (MQClientException e) { + return processException(e); + } catch (Exception e) { + // 重试 + if (mqMessage.getRetryTimes() > 0 && resend(mqMessage)) { + return new Result(false, e).setRetrying(true); + } else { + return processException(e); + } + } + } + + /** + * 重试发送 + * + * @param message + * @return + */ + @SuppressWarnings("rawtypes") + private boolean resend(MQMessage mqMessage) { + try { + retrySenderExecutor.execute(() -> { + Result result = null; + try { + result = _resend(mqMessage); + } catch (Throwable e) { + result = new Result<>(false, new MQClientException(e.toString(), e)); + } + result.setMqMessage(mqMessage); + // 处理重发消息结果 + processResendResult(result); + }); + return true; + } catch (RejectedExecutionException e) { + logger.warn("reject retryPublish..."); + return false; + } + } + + /** + * 重试发送 + * + * @param message + * @return + */ + @SuppressWarnings("rawtypes") + private Result _resend(MQMessage mqMessage) { + Exception exception = null; + // 循环重试发送 + for (int i = 1; i <= mqMessage.getRetryTimes(); ++i) { + try { + SendResult sendResult = producer.send(mqMessage.getInnerMessage()); + Result result = new Result<>(true, sendResult); + result.setRetriedTimes(i); + return result; + } catch (Exception e) { + exception = e; + } + } + // 发送失败,记录结果 + if (statsHelper != null) { + statsHelper.recordException(exception); + } + Result result = new Result<>(false, exception); + result.setRetriedTimes(mqMessage.getRetryTimes()); + return result; + } + + /** + * 处理重发结果 + * + * @param result + */ + private void processResendResult(Result result) { + // 有重试消费者 + if (resendResultConsumer != null) { + try { + resendResultConsumer.accept(result); + } catch (Exception e) { + logger.error("resendResultConsumer consume:{} error", result, e); + } + } + // 无重试消费者记录日志 + if (!result.isSuccess()) { + logger.error("retryTimes:{} message:{} error!", result.getRetriedTimes(), + result.getMqMessage().getMessage(), result.getException()); + } else { + if (logger.isDebugEnabled()) { + logger.debug("retryTimes:{} message:{} success", result.getRetriedTimes(), + result.getMqMessage().getMessage()); + } + } + } + + public int getDefaultRetryTimes() { + return defaultRetryTimes; + } + + public void setDefaultRetryTimes(int defaultRetryTimes) { + this.defaultRetryTimes = defaultRetryTimes; + } + + public ExecutorService getRetrySenderExecutor() { + return retrySenderExecutor; + } + + public void setRetrySenderExecutor(ExecutorService retrySenderExecutor) { + this.retrySenderExecutor = retrySenderExecutor; + } + + public Consumer> getResendResultConsumer() { + return resendResultConsumer; + } + + public void setResendResultConsumer(Consumer> resendResultConsumer) { + this.resendResultConsumer = resendResultConsumer; + } public void shutdown() { producer.shutdown(); if (statsHelper != null) { statsHelper.shutdown(); } + if (retrySenderExecutor != null) { + retrySenderExecutor.shutdown(); + } + super.shutdown(); } public DefaultMQProducer getProducer() { diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/AbstractMessageConsumer.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/AbstractMessageConsumer.java new file mode 100644 index 00000000..66492ddd --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/AbstractMessageConsumer.java @@ -0,0 +1,337 @@ +package com.sohu.tv.mq.rocketmq.consumer; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.slf4j.Logger; + +import com.alibaba.fastjson.JSON; +import com.sohu.index.tv.mq.common.MQMessage; +import com.sohu.tv.mq.metric.ConsumeStatManager; +import com.sohu.tv.mq.metric.ConsumeThreadStat; +import com.sohu.tv.mq.metric.MQMetricsExporter; +import com.sohu.tv.mq.metric.MessageExceptionMetric; +import com.sohu.tv.mq.metric.MessageMetric; +import com.sohu.tv.mq.rocketmq.RocketMQConsumer; +import com.sohu.tv.mq.serializable.MessageSerializer; +import com.sohu.tv.mq.serializable.MessageSerializerEnum; +import com.sohu.tv.mq.stats.ConsumeStats; +import com.sohu.tv.mq.util.CommonUtil; + +/** + * 公共逻辑 + * + * @author yongfeigao + * @date 2021年8月31日 + * @param + * @param + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public abstract class AbstractMessageConsumer implements IMessageConsumer { + + protected Logger logger; + + protected RocketMQConsumer rocketMQConsumer; + + // 消费统计 + protected ConsumeStats consumeStats; + + public AbstractMessageConsumer(RocketMQConsumer rocketMQConsumer) { + this.rocketMQConsumer = rocketMQConsumer; + this.logger = rocketMQConsumer.getLogger(); + if (rocketMQConsumer.isEnableStats()) { + consumeStats = new ConsumeStats(rocketMQConsumer.getGroup()); + MQMetricsExporter.getInstance().add(consumeStats); + } + } + + /** + * 消费并发消息 + * @param msgs + * @param context + * @return + */ + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + long start = System.currentTimeMillis(); + ConsumeStatus consumeStatus = consume(new MessageContext(msgs, context)); + if (ConsumeStatus.FAIL == consumeStatus && rocketMQConsumer.isReconsume()) { + if (consumeStats != null) { + consumeStats.incrementException(); + } + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + if (consumeStats != null) { + consumeStats.increment(System.currentTimeMillis() - start); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + + /** + * 消费顺序消息 + * @param msgs + * @param context + * @return + */ + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + ConsumeStatus consumeStatus = consume(new MessageContext(msgs, context)); + if (ConsumeStatus.FAIL == consumeStatus && rocketMQConsumer.isReconsume()) { + return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; + } + return ConsumeOrderlyStatus.SUCCESS; + } + + /** + * 消费逻辑 + */ + public ConsumeStatus consume(MessageContext context) { + // 解析消息 + List> messageList = parse(context.msgs); + if (messageList == null || messageList.isEmpty()) { + return ConsumeStatus.OK; + } + // 设置消费线程统计 + String group = rocketMQConsumer.getGroup(); + ConsumeThreadStat metric = ConsumeStatManager.getInstance().getConsumeThreadMetrics(group); + try { + metric.set(buildThreadConsumeMetric(messageList)); + // 消费消息 + for (MQMessage mqMessage : messageList) { + try { + // 获取许可 + acquirePermit(); + consume(mqMessage.getMessage(), mqMessage.getMessageExt()); + } catch (Throwable e) { + logger.error("consume topic:{} consumer:{} msgId:{} bornTimestamp:{}", + rocketMQConsumer.getTopic(), group, mqMessage.getMessageExt().getMsgId(), + mqMessage.getMessageExt().getBornTimestamp(), e); + ConsumeStatManager.getInstance().getConsumeFailedMetrics(group) + .set(buildMessageExceptionMetric(mqMessage, e)); + return ConsumeStatus.FAIL; + } + } + } finally { + metric.remove(); + } + return ConsumeStatus.OK; + } + + /** + * 解析消息 + * + * @param msgs + * @return + */ + protected List> parse(List msgs) { + if (msgs == null || msgs.isEmpty()) { + return null; + } + List> msgList = new ArrayList<>(msgs.size()); + for (MessageExt me : msgs) { + byte[] bytes = me.getBody(); + try { + if (bytes == null || bytes.length == 0) { + logger.warn("MessageExt={}, body is null", me); + continue; + } + // 校验是否需要跳过重试消息 + if (CommonUtil.isRetryTopic(me.getProperty(MessageConst.PROPERTY_REAL_TOPIC)) && + me.getBornTimestamp() < rocketMQConsumer.getRetryMessageResetTo()) { + if (rocketMQConsumer.getRetryMessageSkipKey() != null) { + if (rocketMQConsumer.getRetryMessageSkipKey().equals(me.getKeys())) { + logger.warn("skip topic:{} msgId:{} bornTime:{} key:{}", + rocketMQConsumer.getTopic(), me.getMsgId(), me.getBornTimestamp(), + me.getKeys()); + continue; + } + } else { + logger.warn("skip topic:{} msgId:{} bornTime:{}", + rocketMQConsumer.getTopic(), me.getMsgId(), me.getBornTimestamp()); + continue; + } + } + msgList.add(buildMQMessage(me)); + } catch (Throwable e) { + // 解析失败打印警告,不再抛出异常重试(即使重试,仍然会失败) + logger.error("parse topic:{} consumer:{} msg:{} msgId:{} bornTimestamp:{}", + rocketMQConsumer.getTopic(), rocketMQConsumer.getGroup(), new String(bytes), me.getMsgId(), + me.getBornTimestamp(), e); + } + } + return msgList; + } + + private MQMessage buildMQMessage(MessageExt me) throws Exception { + byte[] bytes = me.getBody(); + // 无序列化器直接返回 + if (rocketMQConsumer.getMessageSerializer() == null) { + debugLog("null-serializer", me.getMsgId(), bytes.getClass().getName(), null); + return (MQMessage) new MQMessage<>(bytes, me); + } + // 反序列化 + T message = deserialize(me); + Class consumerParameterTypeClass = rocketMQConsumer.getConsumerParameterTypeClass(); + // 无法获取消费类型 + if (consumerParameterTypeClass == null) { + debugLog("null-consumerParameterType", me.getMsgId(), message.getClass().getName(), null); + return new MQMessage<>(message, me); + } + // 反序列化后类型相同直接返回 + if (consumerParameterTypeClass.isInstance(message)) { + debugLog("isInstance", me.getMsgId(), message.getClass().getName(), consumerParameterTypeClass.getName()); + return new MQMessage<>(message, me); + } + // 消费类型为String,采用JSON转换 + if (consumerParameterTypeClass == String.class) { + debugLog("String-consumerParameterType", me.getMsgId(), message.getClass().getName(), "String"); + return (MQMessage) new MQMessage<>(JSON.toJSONString(message), me); + } + // 消息为String,采用JSON转换 + if (message instanceof String) { + debugLog("String-Message", me.getMsgId(), "String", consumerParameterTypeClass.getName()); + return (MQMessage) new MQMessage<>( + JSON.parseObject(message.toString(), consumerParameterTypeClass), me); + } + debugLog("unknown", me.getMsgId(), message.getClass().getName(), consumerParameterTypeClass.getName()); + // 消费类型和消息都不是String,并且消息与消费类型不匹配,此时可能会类转换异常 + return (MQMessage) new MQMessage<>(message, me); + } + + private void debugLog(String flag, String msgId, String msgType, String consumerType) { + logger.debug("detectType:{} consumer:{} msgId:{} {}->{}", flag, rocketMQConsumer.getGroup(), msgId, msgType, + consumerType); + } + + /** + * 反序列化,若设置的反序列化器执行失败,则使用其他反序列化器进行尝试 + * + * @param bytes + * @param me + * @return + * @throws Exception + */ + private T deserialize(MessageExt me) throws Exception { + // 使用设置的序列化器 + MessageSerializer messageSerializer = rocketMQConsumer.getMessageSerializer(); + Exception excp = null; + try { + T t = (T) messageSerializer.deserialize(me.getBody()); + if (logger.isDebugEnabled()) { + logger.debug("consumer:{} msgId:{} deserializer:{}", + rocketMQConsumer.getGroup(), me.getMsgId(), messageSerializer.getClass().getName()); + } + return t; + } catch (Exception e) { + excp = e; + } + // 使用其他序列化器 + for (MessageSerializerEnum messageSerializerEnum : MessageSerializerEnum.values()) { + if (messageSerializer.getClass() == messageSerializerEnum.getMessageSerializer().getClass()) { + continue; + } + try { + T t = (T) messageSerializerEnum.getMessageSerializer().deserialize(me.getBody()); + if (logger.isDebugEnabled()) { + logger.debug("consumer:{} msgId:{} compatible deserializer:{}", + rocketMQConsumer.getGroup(), me.getMsgId(), + messageSerializerEnum.getMessageSerializer().getClass().getName()); + } + return t; + } catch (Exception e) { + logger.warn("try deserializer:{}, topic:{}, consumer:{} msgId:{} err", + messageSerializerEnum.getMessageSerializer().getClass().getName(), + rocketMQConsumer.getTopic(), rocketMQConsumer.getGroup(), me.getMsgId()); + } + } + throw excp; + } + + /** + * 具体消费逻辑 + * + * @param message + * @param msgExt + * @throws Exception + */ + public abstract void consume(T message, MessageExt msgExt) throws Exception; + + /** + * 构建线程消费统计 + * + * @param messageList + */ + protected MessageMetric buildThreadConsumeMetric(List> messageList) { + List idList = new ArrayList<>(messageList.size()); + messageList.forEach(message -> { + idList.add(message.buildOffsetMsgId()); + }); + MessageMetric messageMetric = new MessageMetric(); + messageMetric.setStartTime(System.currentTimeMillis()); + messageMetric.setMsgIdList(idList); + return messageMetric; + } + + /** + * 构建异常消费统计 + * + * @param messageList + */ + protected MessageExceptionMetric buildMessageExceptionMetric(List> messageList, Throwable e) { + List idList = new ArrayList<>(messageList.size()); + messageList.forEach(message -> { + idList.add(message.buildOffsetMsgId()); + }); + MessageExceptionMetric messageMetric = buildMessageExceptionMetric(e); + messageMetric.setMsgIdList(idList); + return messageMetric; + } + + /** + * 构建异常消费统计 + * + * @param messageList + */ + protected MessageExceptionMetric buildMessageExceptionMetric(MQMessage mqMessage, Throwable e) { + List idList = new ArrayList<>(1); + idList.add(mqMessage.buildOffsetMsgId()); + MessageExceptionMetric messageMetric = buildMessageExceptionMetric(e); + messageMetric.setMsgIdList(idList); + return messageMetric; + } + + protected MessageExceptionMetric buildMessageExceptionMetric(Throwable e) { + MessageExceptionMetric messageMetric = new MessageExceptionMetric(); + messageMetric.setStartTime(System.currentTimeMillis()); + messageMetric.setThreadId(Thread.currentThread().getId()); + messageMetric.setThreadName(Thread.currentThread().getName()); + messageMetric.setException(e); + return messageMetric; + } + + /** + * 获取许可 + */ + protected void acquirePermit() { + try { + rocketMQConsumer.getRateLimiter().limit(); + } catch (InterruptedException e) { + logger.warn("acquirePermit error", e.getMessage()); + } + } + + /** + * 获取许可 + */ + protected void acquirePermit(int permits) { + try { + rocketMQConsumer.getRateLimiter().limit(permits); + } catch (InterruptedException e) { + logger.warn("acquirePermit error", e.getMessage()); + } + } +} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/BatchMessageConsumer.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/BatchMessageConsumer.java new file mode 100644 index 00000000..6d190b96 --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/BatchMessageConsumer.java @@ -0,0 +1,54 @@ +package com.sohu.tv.mq.rocketmq.consumer; + +import java.util.List; + +import org.apache.rocketmq.common.message.MessageExt; + +import com.sohu.index.tv.mq.common.MQMessage; +import com.sohu.tv.mq.metric.ConsumeStatManager; +import com.sohu.tv.mq.metric.ConsumeThreadStat; +import com.sohu.tv.mq.rocketmq.RocketMQConsumer; + +/** + * 批量消息消费 + * + * @author yongfeigao + * @date 2021年8月31日 + * @param + */ +public class BatchMessageConsumer extends AbstractMessageConsumer { + + public BatchMessageConsumer(RocketMQConsumer rocketMQConsumer) { + super(rocketMQConsumer); + } + + @Override + public ConsumeStatus consume(MessageContext context) { + List> msgList = parse(context.msgs); + if (msgList == null || msgList.isEmpty()) { + return ConsumeStatus.OK; + } + // 设置消费线程统计 + ConsumeThreadStat metric = ConsumeStatManager.getInstance().getConsumeThreadMetrics(rocketMQConsumer.getGroup()); + try { + metric.set(buildThreadConsumeMetric(msgList)); + // 获取许可 + acquirePermit(msgList.size()); + rocketMQConsumer.getBatchConsumerCallback().call(msgList, context.context); + } catch (Throwable e) { + logger.error("topic:{} consumer:{} msgSize:{}", + rocketMQConsumer.getTopic(), rocketMQConsumer.getGroup(), msgList.size(), e); + ConsumeStatManager.getInstance().getConsumeFailedMetrics(rocketMQConsumer.getGroup()) + .set(buildMessageExceptionMetric(msgList, e)); + return ConsumeStatus.FAIL; + } finally { + metric.remove(); + } + return ConsumeStatus.OK; + } + + @Override + public void consume(Object message, MessageExt msgExt) throws Exception { + } + +} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/IMessageConsumer.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/IMessageConsumer.java new file mode 100644 index 00000000..36dd1977 --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/IMessageConsumer.java @@ -0,0 +1,76 @@ +package com.sohu.tv.mq.rocketmq.consumer; + +import java.util.List; + +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.common.message.MessageExt; + +/** + * 消息消费 + * + * @author yongfeigao + * @date 2021年8月31日 + * @param + */ +public interface IMessageConsumer { + + /** + * 消费并发消息 + * @param msgs + * @param context + * @return + */ + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context); + + /** + * 消费顺序消息 + * @param msgs + * @param context + * @return + */ + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context); + + /** + * 消费状态 + * + * @author yongfeigao + * @date 2021年8月31日 + */ + public enum ConsumeStatus { + OK, + FAIL, + ; + } + + /** + * 消费上下文 + * + * @author yongfeigao + * @date 2021年8月31日 + * @param + */ + public class MessageContext { + List msgs; + C context; + + public MessageContext(List msgs, C context) { + this.msgs = msgs; + this.context = context; + } + + public MessageContext(List msgs) { + this.msgs = msgs; + } + + public List getMsgs() { + return msgs; + } + + public C getContext() { + return context; + } + } +} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/SingleMessageConsumer.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/SingleMessageConsumer.java new file mode 100644 index 00000000..caa3f97f --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/SingleMessageConsumer.java @@ -0,0 +1,25 @@ +package com.sohu.tv.mq.rocketmq.consumer; + +import org.apache.rocketmq.common.message.MessageExt; + +import com.sohu.tv.mq.rocketmq.RocketMQConsumer; + +/** + * 单个消息消费 + * + * @author yongfeigao + * @date 2021年8月31日 + * @param + */ +@SuppressWarnings("unchecked") +public class SingleMessageConsumer extends AbstractMessageConsumer { + + public SingleMessageConsumer(RocketMQConsumer rocketMQConsumer) { + super(rocketMQConsumer); + } + + @Override + public void consume(T message, MessageExt msgExt) throws Exception { + rocketMQConsumer.getConsumerCallback().call(message, msgExt); + } +} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/deduplicate/DeduplicateSingleMessageConsumer.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/deduplicate/DeduplicateSingleMessageConsumer.java new file mode 100644 index 00000000..6f847793 --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/consumer/deduplicate/DeduplicateSingleMessageConsumer.java @@ -0,0 +1,244 @@ +package com.sohu.tv.mq.rocketmq.consumer.deduplicate; + +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; + +import com.sohu.index.tv.mq.common.MQMessage; +import com.sohu.index.tv.mq.common.Result; +import com.sohu.tv.mq.rocketmq.RocketMQConsumer; +import com.sohu.tv.mq.rocketmq.RocketMQProducer.MessageDelayLevel; +import com.sohu.tv.mq.rocketmq.consumer.SingleMessageConsumer; +import com.sohu.tv.mq.rocketmq.redis.degradable.RedisGetCommand; +import com.sohu.tv.mq.rocketmq.redis.degradable.RedisSetCommand; +import com.sohu.tv.mq.util.CommonUtil; + +import redis.clients.jedis.params.SetParams; + +/** + * 单个消息去重消费 + * + * @author yongfeigao + * @date 2021年9月1日 + * @param + */ +public class DeduplicateSingleMessageConsumer extends SingleMessageConsumer { + + // 标识消息消费中 + private static final String CONSUMING = "0"; + // 标识消息消费失败 + private static final String CONSUME_FAILED = "1"; + // 标识消息消费成功 + private static final String CONSUME_OK = "2"; + // 标识延迟消息 + private static final String DELAY_MESSAGE = "delay"; + + public DeduplicateSingleMessageConsumer(RocketMQConsumer rocketMQConsumer) { + super(rocketMQConsumer); + } + + @Override + public void consume(T message, MessageExt msgExt) throws Exception { + // 重试消息但非延迟消息直接消费 + if (CommonUtil.isRetryTopic(msgExt.getProperty(MessageConst.PROPERTY_REAL_TOPIC)) && + msgExt.getProperty(DELAY_MESSAGE) == null) { + super.consume(message, msgExt); + return; + } + // 构建去重key + String key = buildKey(msgExt); + if (key == null) { + super.consume(message, msgExt); + return; + } + // 获取offsetMsgId + String offsetMsgId = getOffsetMsgId(msgExt); + if (offsetMsgId == null) { + super.consume(message, msgExt); + return; + } + // 设置正在消费标识 + if (setConsumingFlag(key, offsetMsgId)) { + try { + super.consume(message, msgExt); + setConsumeOKFlag(key, offsetMsgId); + } catch (Exception e) { + setConsumeFailedFlag(key, offsetMsgId); + throw e; + } + return; + } + // 获取标识 + Pair pair = getFlag(key); + // 获取失败或真的不存在,直接消费 + if (pair == null) { + super.consume(message, msgExt); + return; + } + // offsetMsgId相同且正在消费 + if (offsetMsgId.equals(pair.getObject1()) && CONSUMING.equals(pair.getObject2())) { + if (sendDelayMessage(rocketMQConsumer.getGroup(), msgExt)) { + return; + } + // 发送失败直接消费 + super.consume(message, msgExt); + return; + } + logger.info("msg:{} offsetMsgId:{} flag:{} duplicate offsetMsgId:{}, reconsumeTimes:{}", key, offsetMsgId, + pair.getObject2(), pair.getObject1(), msgExt.getReconsumeTimes()); + } + + /** + * 设置正在消费标识 + * + * @param key + * @param offsetMsgId + * @return + */ + private boolean setConsumingFlag(String key, String offsetMsgId) { + SetParams setParams = SetParams.setParams().nx().ex(rocketMQConsumer.getDeduplicateWindowSeconds()); + return setFlag(key, CONSUMING, offsetMsgId, setParams); + } + + /** + * 设置消费成功标识 + * + * @param key + * @param offsetMsgId + * @return + */ + private boolean setConsumeOKFlag(String key, String offsetMsgId) { + SetParams setParams = SetParams.setParams().xx().ex(rocketMQConsumer.getDeduplicateWindowSeconds()); + return setFlag(key, CONSUME_OK, offsetMsgId, setParams); + } + + /** + * 设置消费失败标识 + * + * @param key + * @param offsetMsgId + * @return + */ + private boolean setConsumeFailedFlag(String key, String offsetMsgId) { + SetParams setParams = SetParams.setParams().xx().ex(rocketMQConsumer.getDeduplicateWindowSeconds()); + return setFlag(key, CONSUME_FAILED, offsetMsgId, setParams); + } + + /** + * 设置标识 + * + * @param key + * @param flag + * @param msgExt + * @return + */ + private boolean setFlag(String key, String flag, String offsetMsgId, SetParams setParams) { + try { + String value = offsetMsgId + ":" + flag; + Result result = new RedisSetCommand(rocketMQConsumer.getRedis(), key, value, setParams).execute(); + if (result.isSuccess()) { + return "OK".equals(result.getResult()); + } + } catch (Exception e) { + logger.warn("setFlag:{} key:{} error:{}", flag, key, e.toString()); + } + // 异常状况当做设置成功 + return true; + } + + /** + * 获取标识 + * + * @param key + * @return + */ + private Pair getFlag(String key) { + try { + return new RedisGetCommand(rocketMQConsumer.getRedis(), key).execute(); + } catch (Exception e) { + logger.warn("getFlag:{} error:{}", key, e.toString()); + } + return null; + } + + /** + * 获取offsetMsgId + * + * @param msgExt + * @return + */ + private String getOffsetMsgId(MessageExt msgExt) { + // 延迟消息使用之前的offsetMsgId + String offsetMsgId = msgExt.getProperty(DELAY_MESSAGE); + if (offsetMsgId == null) { + if (msgExt instanceof MessageClientExt) { + offsetMsgId = ((MessageClientExt) msgExt).getOffsetMsgId(); + } + } + return offsetMsgId; + } + + /** + * 构建去重的key + * + * @param consumerGroup + * @param msgExt + * @return + */ + private String buildKey(MessageExt msgExt) { + // 优先使用客户端自己设置的幂等id + String idempotentId = msgExt.getProperty(MQMessage.IDEMPOTENT_ID); + if (idempotentId == null) { + // 其次使用客户端生成的消息id + idempotentId = MessageClientIDSetter.getUniqID(msgExt); + } + if (idempotentId == null) { + return null; + } + return idempotentId; + } + + /** + * 发送延迟消息 + * + * @param consumerGroup + * @param msg + * @return + */ + @SuppressWarnings("deprecation") + private boolean sendDelayMessage(String consumerGroup, MessageExt msg) { + try { + Message newMsg = new Message(MixAll.getRetryTopic(consumerGroup), msg.getBody()); + String originMsgId = MessageAccessor.getOriginMessageId(msg); + MessageAccessor.setOriginMessageId(newMsg, UtilAll.isBlank(originMsgId) ? msg.getMsgId() : originMsgId); + newMsg.setFlag(msg.getFlag()); + MessageAccessor.setProperties(newMsg, msg.getProperties()); + MessageAccessor.putProperty(newMsg, MessageConst.PROPERTY_RETRY_TOPIC, msg.getTopic()); + MessageAccessor.setReconsumeTime(newMsg, String.valueOf(msg.getReconsumeTimes() + 1)); + int delayInterval = (int) (MessageDelayLevel.LEVEL_30_SECONDS.getDelayTimeMillis() / 1000); + int maxReconsumeTimes = rocketMQConsumer.getDeduplicateWindowSeconds() / delayInterval; + MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(maxReconsumeTimes)); + MessageAccessor.clearProperty(newMsg, MessageConst.PROPERTY_TRANSACTION_PREPARED); + newMsg.setDelayTimeLevel(MessageDelayLevel.LEVEL_30_SECONDS.getLevel()); + MessageAccessor.putProperty(newMsg, DELAY_MESSAGE, getOffsetMsgId(msg)); + SendResult sendResult = rocketMQConsumer.getConsumer().getDefaultMQPushConsumerImpl().getmQClientFactory() + .getDefaultMQProducer().send(newMsg); + logger.info( + "sendDelayMessage consumerGroup:{} msgId:{} offsetMsgId:{} reconsumeTimes:{} maxReconsumeTimes:{} result:{}", + consumerGroup, msg.getMsgId(), getOffsetMsgId(msg), msg.getReconsumeTimes(), maxReconsumeTimes, + sendResult); + return true; + } catch (Exception e) { + logger.warn("sendDelayMessage consumerGroup:{} msgId:{} offsetMsgId:{} reconsumeTimes:{} error:{}", + msg.getMsgId(), getOffsetMsgId(msg), msg.getReconsumeTimes(), e.toString()); + } + return false; + } +} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/ClusterRedis.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/ClusterRedis.java new file mode 100644 index 00000000..2e1e8d3a --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/ClusterRedis.java @@ -0,0 +1,78 @@ +package com.sohu.tv.mq.rocketmq.redis; + +import java.util.HashSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.params.SetParams; +import redis.clients.jedis.util.Pool; + +/** + * ClusterRedis + * + * @author yongfeigao + * @date 2021年8月25日 + */ +public class ClusterRedis implements IRedis { + + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + protected JedisCluster jedisCluster; + + private RedisConfiguration redisConfiguration; + + public ClusterRedis() { + } + + public ClusterRedis(JedisCluster jedisCluster, int timeout) { + this.jedisCluster = jedisCluster; + this.redisConfiguration = new RedisConfiguration(); + redisConfiguration.setConnectionTimeout(timeout); + logger.info("new ClusterRedis timeout:{}", timeout); + } + + @Override + public void init(RedisConfiguration redisConfiguration) { + this.redisConfiguration = redisConfiguration; + String[] hostAndPortArray = redisConfiguration.getHost().split(","); + Set jedisClusterNode = new HashSet(); + for (String hostAndPort : hostAndPortArray) { + String[] tmpArray = hostAndPort.split(":"); + jedisClusterNode.add(new HostAndPort(tmpArray[0], Integer.parseInt(tmpArray[1]))); + } + jedisCluster = new JedisCluster(jedisClusterNode, redisConfiguration.getConnectionTimeout(), + redisConfiguration.getSoTimeout(), 5, redisConfiguration.getPassword(), + redisConfiguration.getPoolConfig()); + logger.info("init ClusterRedis redisConfiguration:{}", redisConfiguration); + } + + @Override + public Pool getPool() { + return null; + } + + @Override + public JedisCluster getJedisCluster() { + return jedisCluster; + } + + @Override + public String set(String key, String value, SetParams params) { + return jedisCluster.set(key, value, params); + } + + @Override + public String get(String key) { + return jedisCluster.get(key); + } + + @Override + public RedisConfiguration getRedisConfiguration() { + return redisConfiguration; + } +} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/IRedis.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/IRedis.java new file mode 100644 index 00000000..3d1f0708 --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/IRedis.java @@ -0,0 +1,31 @@ +package com.sohu.tv.mq.rocketmq.redis; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.params.SetParams; +import redis.clients.jedis.util.Pool; + +/** + * redis统一接口,屏蔽底层实现,所有方法都来自于jedis + * + * @author yongfeigao + * @date 2021年8月25日 + */ +public interface IRedis { + /** + * 初始化 + * + * @param redisConfiguration + */ + void init(RedisConfiguration redisConfiguration); + + RedisConfiguration getRedisConfiguration(); + + Pool getPool(); + + JedisCluster getJedisCluster(); + + String set(String key, String value, SetParams params); + + String get(String key); +} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/PooledRedis.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/PooledRedis.java new file mode 100644 index 00000000..e9ae24cc --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/PooledRedis.java @@ -0,0 +1,86 @@ +package com.sohu.tv.mq.rocketmq.redis; + +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.params.SetParams; +import redis.clients.jedis.util.Pool; + +/** + * 池化的redis + * + * @author yongfeigao + * @date 2021年8月25日 + */ +public class PooledRedis implements IRedis { + + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + private RedisConfiguration redisConfiguration; + + private Pool pool; + + public PooledRedis() { + } + + public PooledRedis(Pool pool, int timeout) { + this.pool = new JedisPool(); + this.redisConfiguration = new RedisConfiguration(); + redisConfiguration.setConnectionTimeout(timeout); + logger.info("new PooledRedis timeout:{}", timeout); + } + + @Override + public void init(RedisConfiguration redisConfiguration) { + this.redisConfiguration = redisConfiguration; + this.pool = new JedisPool(redisConfiguration.getPoolConfig(), + redisConfiguration.getHost(), + redisConfiguration.getPort(), + redisConfiguration.getConnectionTimeout(), + redisConfiguration.getSoTimeout(), + redisConfiguration.getPassword(), + Protocol.DEFAULT_DATABASE, null); + logger.info("init PooledRedis redisConfiguration:{}", redisConfiguration); + } + + @Override + public Pool getPool() { + return pool; + } + + @Override + public JedisCluster getJedisCluster() { + return null; + } + + @Override + public String set(String key, String value, SetParams params) { + return execute(jedis -> jedis.set(key, value, params)); + } + + @Override + public String get(String key) { + return execute(jedis -> jedis.get(key)); + } + + private R execute(Function function) { + try (Jedis jedis = pool.getResource()) { + return function.apply(jedis); + } + } + + public void setPool(Pool pool) { + this.pool = pool; + } + + @Override + public RedisConfiguration getRedisConfiguration() { + return redisConfiguration; + } +} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/RedisBuilder.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/RedisBuilder.java new file mode 100644 index 00000000..0cc9f987 --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/RedisBuilder.java @@ -0,0 +1,92 @@ +package com.sohu.tv.mq.rocketmq.redis; + +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.util.Pool; + +/** + * redis客户端构建器 + * + * @author yongfeigao + * @date 2021年10月14日 + */ +public class RedisBuilder { + + /** + * 构建redis客户端 + * + * @param host + * @param port + * @return + */ + public static IRedis build(String host, int port) { + RedisConfiguration redisConfiguration = new RedisConfiguration(); + redisConfiguration.setHost(host); + redisConfiguration.setPort(port); + redisConfiguration.setConnectionTimeout(1000); + redisConfiguration.setSoTimeout(1000); + GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig<>(); + poolConfig.setMaxWaitMillis(1000); + redisConfiguration.setPoolConfig(poolConfig); + return build(redisConfiguration); + } + + /** + * 构建redis客户端 + * + * @param hostPortString + * @return + */ + public static IRedis build(String hostPortString) { + RedisConfiguration redisConfiguration = new RedisConfiguration(); + redisConfiguration.setHost(hostPortString); + redisConfiguration.setConnectionTimeout(1000); + redisConfiguration.setSoTimeout(1000); + GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig<>(); + poolConfig.setMaxWaitMillis(1000); + redisConfiguration.setPoolConfig(poolConfig); + redisConfiguration.setCluster(true); + return build(redisConfiguration); + } + + /** + * 构建redis客户端 + * + * @param redisConfiguration + * @return + */ + public static IRedis build(RedisConfiguration redisConfiguration) { + if (redisConfiguration.isCluster()) { + ClusterRedis clusterRedis = new ClusterRedis(); + clusterRedis.init(redisConfiguration); + return clusterRedis; + } + PooledRedis pooledRedis = new PooledRedis(); + pooledRedis.init(redisConfiguration); + return pooledRedis; + } + + /** + * 构建redis客户端 + * + * @param jedisCluster + * @param maxTimeoutInMillis 最大超时时间,包括链接池获取链接时间 + * @return + */ + public static IRedis build(JedisCluster jedisCluster, int maxTimeoutInMillis) { + return new ClusterRedis(jedisCluster, maxTimeoutInMillis); + } + + /** + * 构建redis客户端 + * + * @param jedisPool + * @param maxTimeoutInMillis 最大超时时间,包括链接池获取链接时间 + * @return + */ + public static IRedis build(Pool jedisPool, int maxTimeoutInMillis) { + return new PooledRedis(jedisPool, maxTimeoutInMillis); + } +} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/RedisConfiguration.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/RedisConfiguration.java new file mode 100644 index 00000000..8217ce43 --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/RedisConfiguration.java @@ -0,0 +1,89 @@ +package com.sohu.tv.mq.rocketmq.redis; + +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; + +/** + * redis配置 + * + * @author yongfeigao + * @date 2021年8月25日 + */ +public class RedisConfiguration { + private GenericObjectPoolConfig poolConfig; + private String host; + private int port; + private int connectionTimeout; + private int soTimeout; + private String password; + private boolean cluster; + + public GenericObjectPoolConfig getPoolConfig() { + return poolConfig; + } + + public void setPoolConfig(GenericObjectPoolConfig poolConfig) { + this.poolConfig = poolConfig; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public int getConnectionTimeout() { + return connectionTimeout; + } + + public void setConnectionTimeout(int connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public int getSoTimeout() { + return soTimeout; + } + + public void setSoTimeout(int soTimeout) { + this.soTimeout = soTimeout; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getMaxTimeout() { + if (connectionTimeout > soTimeout) { + return connectionTimeout; + } + return soTimeout; + } + + public boolean isCluster() { + return cluster; + } + + public void setCluster(boolean cluster) { + this.cluster = cluster; + } + + @Override + public String toString() { + return "RedisConfiguration [poolConfig=" + poolConfig + ", host=" + host + ", port=" + port + + ", connectionTimeout=" + connectionTimeout + ", soTimeout=" + soTimeout + ", password=" + password + + ", cluster=" + cluster + "]"; + } +} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/degradable/RedisGetCommand.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/degradable/RedisGetCommand.java new file mode 100644 index 00000000..2e274f55 --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/degradable/RedisGetCommand.java @@ -0,0 +1,52 @@ +package com.sohu.tv.mq.rocketmq.redis.degradable; + +import org.apache.rocketmq.common.Pair; + +import com.sohu.tv.mq.common.AbstractCommand; +import com.sohu.tv.mq.common.Alerter; +import com.sohu.tv.mq.common.DefaultAlerter; +import com.sohu.tv.mq.rocketmq.redis.IRedis; + +/** + * redis get 命令 + * + * @author yongfeigao + * @date 2021年10月14日 + */ +public class RedisGetCommand extends AbstractCommand> { + + private IRedis redis; + + private String key; + + public RedisGetCommand(IRedis redis, String key) { + this("redis-" + redis.hashCode(), "get", redis.getRedisConfiguration().getMaxTimeout() + 1000, + DefaultAlerter.getInstance()); + this.redis = redis; + this.key = key; + } + + public RedisGetCommand(String groupKey, String commandKey, int timeout, Alerter alerter) { + super(groupKey, commandKey, timeout, alerter); + } + + @Override + protected Pair invoke() throws Exception { + String result = redis.get(key); + if (result != null) { + String[] array = result.split(":"); + return new Pair<>(array[0], array[1]); + } + return null; + } + + @Override + protected Object invokeErrorInfo() { + return key; + } + + @Override + public Pair fallback() { + return null; + } +} diff --git a/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/degradable/RedisSetCommand.java b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/degradable/RedisSetCommand.java new file mode 100644 index 00000000..d7e23ea2 --- /dev/null +++ b/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/redis/degradable/RedisSetCommand.java @@ -0,0 +1,57 @@ +package com.sohu.tv.mq.rocketmq.redis.degradable; + +import com.sohu.index.tv.mq.common.Result; +import com.sohu.tv.mq.common.AbstractCommand; +import com.sohu.tv.mq.common.Alerter; +import com.sohu.tv.mq.common.DefaultAlerter; +import com.sohu.tv.mq.rocketmq.redis.IRedis; + +import redis.clients.jedis.params.SetParams; + +/** + * redis set 命令 + * + * @author yongfeigao + * @date 2021年10月14日 + */ +public class RedisSetCommand extends AbstractCommand> { + + private IRedis redis; + + private String key; + + private String value; + + private SetParams params; + + public RedisSetCommand(IRedis redis, String key, String value, SetParams params) { + this("redis-" + redis.hashCode(), "set", redis.getRedisConfiguration().getMaxTimeout() + 1000, + DefaultAlerter.getInstance()); + this.redis = redis; + this.key = key; + this.value = value; + this.params = params; + } + + public RedisSetCommand(String groupKey, String commandKey, int timeout, Alerter alerter) { + super(groupKey, commandKey, timeout, alerter); + } + + @Override + protected Result invoke() throws Exception { + return new Result<>(true, redis.set(key, value, params)); + } + + @Override + protected Object invokeErrorInfo() { + return key; + } + + @Override + public Result fallback() { + if (isFailedExecution()) { + return new Result<>(false, getExecutionException()); + } + return new Result<>(false); + } +} diff --git a/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/MessageConsumerTest.java b/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/MessageConsumerTest.java index 73e28887..6ddd1ebf 100644 --- a/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/MessageConsumerTest.java +++ b/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/MessageConsumerTest.java @@ -11,9 +11,10 @@ import org.junit.Test; import com.sohu.index.tv.mq.common.ConsumerCallback; +import com.sohu.tv.mq.rocketmq.consumer.SingleMessageConsumer; import com.sohu.tv.mq.serializable.DefaultMessageSerializer; import com.sohu.tv.mq.serializable.StringSerializer; - +@SuppressWarnings("rawtypes") public class MessageConsumerTest { DefaultMessageSerializer defaultMessageSerializer = new DefaultMessageSerializer<>(); StringSerializer stringSerializer = new StringSerializer<>(); @@ -43,28 +44,28 @@ public void init() { @Test public void testProtostuffSerializer() throws Exception { - MessageConsumer messageConsumer = new MessageConsumer(personConsumer); + SingleMessageConsumer messageConsumer = new SingleMessageConsumer(personConsumer); messageConsumer.consumeMessage(getProtostuffMessageList(), (ConsumeConcurrentlyContext) null); messageConsumer.consumeMessage(getStringMessageList(), (ConsumeConcurrentlyContext) null); } @Test public void testStringSerializer() throws Exception { - MessageConsumer messageConsumer = new MessageConsumer(stringConsumer); + SingleMessageConsumer messageConsumer = new SingleMessageConsumer(personConsumer); messageConsumer.consumeMessage(getProtostuffMessageList(), (ConsumeConcurrentlyContext) null); messageConsumer.consumeMessage(getStringMessageList(), (ConsumeConcurrentlyContext) null); } @Test public void testMapSerializer() throws Exception { - MessageConsumer messageConsumer = new MessageConsumer(mapConsumer); + SingleMessageConsumer messageConsumer = new SingleMessageConsumer(personConsumer); messageConsumer.consumeMessage(getProtostuffMapMessageList(), (ConsumeConcurrentlyContext) null); messageConsumer.consumeMessage(getStringMapMessageList(), (ConsumeConcurrentlyContext) null); } @Test public void testErrorSerializer() throws Exception { - MessageConsumer messageConsumer = new MessageConsumer(errorConsumer); + SingleMessageConsumer messageConsumer = new SingleMessageConsumer(personConsumer); messageConsumer.consumeMessage(getProtostuffMapMessageList(), (ConsumeConcurrentlyContext) null); messageConsumer.consumeMessage(getStringMapMessageList(), (ConsumeConcurrentlyContext) null); } diff --git a/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/RocketMQConsumerJsonTest.java b/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/RocketMQConsumerJsonTest.java index dd6adc49..e5893c0b 100644 --- a/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/RocketMQConsumerJsonTest.java +++ b/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/RocketMQConsumerJsonTest.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.common.message.MessageExt; import org.junit.After; import org.junit.Before; @@ -40,10 +41,11 @@ public void call(String t, MessageExt k) throws Exception { } @Test + @SuppressWarnings("deprecation") public void testBatch() throws InterruptedException { consumer.setConsumeMessageBatchMaxSize(32); - consumer.setBatchConsumerCallback(new BatchConsumerCallback(){ - public void call(List> batchMessage) throws Exception { + consumer.setBatchConsumerCallback(new BatchConsumerCallback(){ + public void call(List> batchMessage, ConsumeConcurrentlyContext context) throws Exception { for(MQMessage mqMessage : batchMessage) { counter.incrementAndGet(); System.out.println("msg:" + mqMessage.getMessage() + ",msgExt:" + mqMessage.getMessageExt()); diff --git a/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/RocketMQDeduplicateConsumerTest.java b/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/RocketMQDeduplicateConsumerTest.java new file mode 100644 index 00000000..5a6d2aa1 --- /dev/null +++ b/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/RocketMQDeduplicateConsumerTest.java @@ -0,0 +1,57 @@ +package com.sohu.tv.mq.rocketmq; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageExt; +import org.junit.Before; +import org.junit.Test; + +import com.sohu.index.tv.mq.common.ConsumerCallback; + +public class RocketMQDeduplicateConsumerTest { + private AtomicLong counter = new AtomicLong(); + + private RocketMQConsumer consumer; + + @Before + public void init() { + consumer = TestUtil.buildConsumer("basic-apitest-topic-consumer", "basic-apitest-topic", true); + } + + @Test + public void test() throws InterruptedException { + consumer.setDeduplicateWindowSeconds(10 * 60); + consumer.setConsumerCallback(new ConsumerCallback, MessageExt>() { + public void call(Map t, MessageExt k) { + System.out.println("consume msgId:"+k.getMsgId()+",offsetMsgId:"+((MessageClientExt)k).getOffsetMsgId()); + try { + Thread.sleep(120 * 1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + consumer.start(); + while (true) { + Thread.sleep(10000); + } + } + + @Test + public void testRetry() throws InterruptedException { + consumer.setConsumerCallback(new ConsumerCallback, MessageExt>() { + public void call(Map t, MessageExt k) { + if (counter.incrementAndGet() == 1) { + throw new RuntimeException("test"); + } + } + }); + consumer.start(); + while (true) { + System.out.println(counter.get()); + Thread.sleep(10000); + } + } +} diff --git a/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/RocketMQProducerTest.java b/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/RocketMQProducerTest.java index b0363fd7..2779bda3 100644 --- a/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/RocketMQProducerTest.java +++ b/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/RocketMQProducerTest.java @@ -9,6 +9,7 @@ import org.junit.Before; import org.junit.Test; +import com.sohu.index.tv.mq.common.MQMessage; import com.sohu.index.tv.mq.common.Result; public class RocketMQProducerTest { @@ -53,6 +54,54 @@ public void produceByte() { Result sendResult = producer.publish(map.toString().getBytes()); Assert.assertTrue(sendResult.isSuccess()); } + + @Test + public void testRetry() throws InterruptedException { + producer.setResendResultConsumer(result -> { + System.out.println("---msgId:"+result.getResult().getMsgId()+",offsetMsgId:"+result.getResult().getOffsetMsgId()); + }); + Map map = new HashMap(); + map.put("a", "b"); + map.put("c", "d"); + map.put("o", "c"); + Result result = producer.send(MQMessage.build(map).setKeys("abc").setExceptionForTest(true)); + if (!result.isSuccess && !result.isRetrying()) { + System.out.println("发送失败"); + } + System.out.println("=="+result); + Thread.sleep(3 * 1000); + } + + @Test + public void testRetryReject() throws InterruptedException { + Map map = new HashMap(); + map.put("a", "b"); + map.put("c", "d"); + map.put("o", "c"); + for(int i = 0; i < 200; ++i) { + Result result = producer.send(MQMessage.build(map).setKeys("abc")); + System.out.println("=="+i+"="+result); + } + Thread.sleep(3 * 1000); + } + + @Test + public void testIdempotentMessage() throws InterruptedException { + producer.setResendResultConsumer(result -> { + System.out.println("---msgId:"+result.getResult().getMsgId()+",offsetMsgId:"+result.getResult().getOffsetMsgId()); + }); + Map map = new HashMap(); + map.put("a", "b"); + map.put("c", "d"); + map.put("o", "c"); + MQMessage message = MQMessage.build("message").setKeys("k1").setIdempotentID("1102123"); + Result result = producer.send(message); + if (!result.isSuccess && !result.isRetrying()) { + System.out.println("发送失败"); + } + System.out.println("=="+result); + Thread.sleep(3 * 1000); + } @After public void clean() { diff --git a/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/TestUtil.java b/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/TestUtil.java index 3719bd1e..f8885fc6 100644 --- a/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/TestUtil.java +++ b/mq-client-open/src/test/java/com/sohu/tv/mq/rocketmq/TestUtil.java @@ -3,28 +3,39 @@ import org.apache.rocketmq.client.producer.TransactionListener; import com.sohu.tv.mq.common.AbstractConfig; +import com.sohu.tv.mq.rocketmq.redis.RedisBuilder; import com.sohu.tv.mq.serializable.DefaultMessageSerializer; public class TestUtil { - + public static RocketMQConsumer buildConsumer(String consumerGroup, String topic) { RocketMQConsumer consumer = new RocketMQConsumer(consumerGroup, topic); setDomain(consumer); + return buildConsumer(consumerGroup, topic, false); + } + + public static RocketMQConsumer buildConsumer(String consumerGroup, String topic, boolean deduplicate) { + RocketMQConsumer consumer = new RocketMQConsumer(consumerGroup, topic); + setDomain(consumer); + if (deduplicate) { + consumer.setRedis(RedisBuilder.build("127.0.0.1", 6379)); + } return consumer; } - + public static RocketMQProducer buildProducer(String producerGroup, String topic) { RocketMQProducer producer = new RocketMQProducer(producerGroup, topic); setDomain(producer); return producer; } - - public static RocketMQProducer buildProducer(String producerGroup, String topic, TransactionListener transactionListener) { + + public static RocketMQProducer buildProducer(String producerGroup, String topic, + TransactionListener transactionListener) { RocketMQProducer producer = new RocketMQProducer(producerGroup, topic, transactionListener); setDomain(producer); return producer; } - + private static void setDomain(AbstractConfig abstractConfig) { abstractConfig.setMqCloudDomain("127.0.0.1:8080"); abstractConfig.setMessageSerializer(new DefaultMessageSerializer()); diff --git a/mq-client-open/src/test/resources/logback.xml b/mq-client-open/src/test/resources/logback.xml index ca81c8fa..cd8bcd91 100644 --- a/mq-client-open/src/test/resources/logback.xml +++ b/mq-client-open/src/test/resources/logback.xml @@ -12,19 +12,19 @@ - + - + - + diff --git a/mq-cloud-common/pom.xml b/mq-cloud-common/pom.xml index 584d5ac8..0f0027b7 100644 --- a/mq-cloud-common/pom.xml +++ b/mq-cloud-common/pom.xml @@ -6,7 +6,7 @@ com.sohu.tv mq - 4.7.2 + 4.9.1 mq-cloud-common diff --git a/mq-cloud-common/src/main/java/com/sohu/tv/mq/cloud/common/model/BrokerStoreStat.java b/mq-cloud-common/src/main/java/com/sohu/tv/mq/cloud/common/model/BrokerStoreStat.java index 3a0bc3bb..742ba5bb 100644 --- a/mq-cloud-common/src/main/java/com/sohu/tv/mq/cloud/common/model/BrokerStoreStat.java +++ b/mq-cloud-common/src/main/java/com/sohu/tv/mq/cloud/common/model/BrokerStoreStat.java @@ -27,6 +27,10 @@ public class BrokerStoreStat { private int createDate; // 创建时间 private String createTime; + + private String clusterName; + + private String brokerStoreLink; public int getClusterId() { return clusterId; @@ -107,4 +111,20 @@ public String getBrokerIp() { public void setBrokerIp(String brokerIp) { this.brokerIp = brokerIp; } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getBrokerStoreLink() { + return brokerStoreLink; + } + + public void setBrokerStoreLink(String brokerStoreLink) { + this.brokerStoreLink = brokerStoreLink; + } } diff --git a/mq-cloud/pom.xml b/mq-cloud/pom.xml index 9d89e243..c2be3bc2 100644 --- a/mq-cloud/pom.xml +++ b/mq-cloud/pom.xml @@ -5,7 +5,7 @@ com.sohu.tv mq - 4.7.2 + 4.9.1 mq-cloud diff --git a/mq-cloud/sql/4.9.1.sql b/mq-cloud/sql/4.9.1.sql new file mode 100644 index 00000000..20721481 --- /dev/null +++ b/mq-cloud/sql/4.9.1.sql @@ -0,0 +1,34 @@ +insert into broker_config(`gid`, `key`, `value`, `desc`, `tip`, `order`, `dynamic_modify`, `option`, `required`) values(24, 'enableScheduleMessageStats', 'true', '开启schedule队列消息统计', null, 28, 1, null, 0); +insert into broker_config(`gid`, `key`, `value`, `desc`, `tip`, `order`, `dynamic_modify`, `option`, `required`) values(24, 'isEnableBatchPush', 'false', 'DLedger批量复制', null, 29, 1, null, 0); +insert into broker_config(`gid`, `key`, `value`, `desc`, `tip`, `order`, `dynamic_modify`, `option`, `required`) values(24, 'autoDeleteUnusedStats', 'false', '删除topic或订阅时一起删除相关统计', null, 30, 1, null, 0); +insert into broker_config(`gid`, `key`, `value`, `desc`, `tip`, `order`, `dynamic_modify`, `option`, `required`) values(24, 'registerBroker', 'true', '是否注册broker', null, 31, 1, null, 0); + + +INSERT INTO `common_config`(`key`, `value`, `comment`) VALUES ('mailUseSSL', 'false', '邮件是否使用SSL'); + +alter table `audit_reset_offset` add column `message_key` varchar(360) DEFAULT NULL COMMENT '消息key'; +alter table `consumer_config` add column `retry_message_skip_key` varchar(360) DEFAULT NULL COMMENT '消息key'; +alter table `user` add column `receive_phone_notice` int(4) NOT NULL DEFAULT '0' COMMENT '用户是否接收手机通知,0:不接收,1:接收'; + +-- ---------------------------- +-- Table structure for `user_warn` +-- ---------------------------- +DROP TABLE IF EXISTS `user_warn`; +CREATE TABLE `user_warn` ( + `uid` int(11) NOT NULL, + `type` int(4) NOT NULL COMMENT '警告类型', + `resource` varchar(100) DEFAULT NULL COMMENT '警告资源:topic,producer等', + `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `wid` int(11) NOT NULL, + KEY `ut` (`uid`, `create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户警告表'; + +-- ---------------------------- +-- Table structure for `warn_info` +-- ---------------------------- +DROP TABLE IF EXISTS `warn_info`; +CREATE TABLE `warn_info` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `content` text DEFAULT NULL COMMENT '警告内容', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='警告信息表'; \ No newline at end of file diff --git a/mq-cloud/sql/init.sql b/mq-cloud/sql/init.sql index 12a77a90..19a1fa61 100644 --- a/mq-cloud/sql/init.sql +++ b/mq-cloud/sql/init.sql @@ -91,7 +91,8 @@ CREATE TABLE `audit_reset_offset` ( `aid` int(11) NOT NULL COMMENT '审核id', `tid` int(11) NOT NULL COMMENT 'topic id', `consumer_id` int(11) DEFAULT NULL COMMENT 'consumer id', - `offset` varchar(64) DEFAULT NULL COMMENT 'null:重置为最大offset,时间戳:重置为某时间点(yyyy-MM-dd#HH:mm:ss:SSS)' + `offset` varchar(64) DEFAULT NULL COMMENT 'null:重置为最大offset,时间戳:重置为某时间点(yyyy-MM-dd#HH:mm:ss:SSS)', + `message_key` varchar(360) DEFAULT NULL COMMENT '消息key' ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='审核offset相关表'; -- ---------------------------- @@ -485,8 +486,9 @@ CREATE TABLE `user` ( `type` int(4) NOT NULL DEFAULT '0' COMMENT '0:普通用户,1:管理员', `create_date` date NOT NULL, `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `receive_notice` int(4) NOT NULL DEFAULT '0' COMMENT '是否接收各种通知,0:不接收,1:接收', + `receive_notice` int(4) NOT NULL DEFAULT '0' COMMENT '管理员是否接收邮件通知,0:不接收,1:接收', `password` varchar(256) COMMENT '登录方式采用用户名密码验证时使用', + `receive_phone_notice` int(4) NOT NULL DEFAULT '0' COMMENT '用户是否接收手机通知,0:不接收,1:接收', PRIMARY KEY (`id`), UNIQUE KEY `email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表'; @@ -753,6 +755,7 @@ CREATE TABLE `consumer_config` ( `enable_rate_limit` tinyint(4) DEFAULT NULL COMMENT '0:不限速,1:限速', `pause` tinyint(4) DEFAULT NULL COMMENT '0:不暂停,1:暂停', `pause_client_id` varchar(255) DEFAULT NULL COMMENT '暂停的客户端Id', + `retry_message_skip_key` varchar(360) DEFAULT NULL COMMENT '消息key', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', UNIQUE KEY `consumer` (`consumer`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='客户端配置表'; @@ -993,6 +996,9 @@ insert into broker_config(`gid`, `key`, `value`, `desc`, `tip`, `order`, `dynami insert into broker_config(`gid`, `key`, `value`, `desc`, `tip`, `order`, `dynamic_modify`, `option`, `required`) values(24, 'dLegerGroup', null, 'DLeger相关配置', null, 25, 0, null, 0); insert into broker_config(`gid`, `key`, `value`, `desc`, `tip`, `order`, `dynamic_modify`, `option`, `required`) values(24, 'dLegerPeers', null, 'DLeger相关配置', null, 26, 0, null, 0); insert into broker_config(`gid`, `key`, `value`, `desc`, `tip`, `order`, `dynamic_modify`, `option`, `required`) values(24, 'dLegerSelfId', null, 'DLeger相关配置', null, 27, 0, null, 0); +insert into broker_config(`gid`, `key`, `value`, `desc`, `tip`, `order`, `dynamic_modify`, `option`, `required`) values(24, 'enableScheduleMessageStats', 'true', '开启schedule队列消息统计', null, 28, 1, null, 0); +insert into broker_config(`gid`, `key`, `value`, `desc`, `tip`, `order`, `dynamic_modify`, `option`, `required`) values(24, 'isEnableBatchPush', 'false', 'DLedger批量复制', null, 29, 0, null, 1); +insert into broker_config(`gid`, `key`, `value`, `desc`, `tip`, `order`, `dynamic_modify`, `option`, `required`) values(24, 'autoDeleteUnusedStats', 'false', '删除topic或订阅时一起删除相关统计', null, 30, 1, null, 0); -- ---------------------------- -- Table structure for `topic_traffic_warn_config` @@ -1019,4 +1025,27 @@ CREATE TABLE `audit_topic_traffic_warn` ( `aid` int(11) NOT NULL COMMENT '审核id', `tid` int(11) NOT NULL COMMENT 'topic id', `traffic_warn_enabled` int(11) NOT NULL COMMENT '0:不开启topic流量预警,1:开启topic流量预警' -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='审核topic trafficWarn相关表'; \ No newline at end of file +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='审核topic trafficWarn相关表'; + +-- ---------------------------- +-- Table structure for `user_warn` +-- ---------------------------- +DROP TABLE IF EXISTS `user_warn`; +CREATE TABLE `user_warn` ( + `uid` int(11) NOT NULL, + `type` int(4) NOT NULL COMMENT '警告类型', + `resource` varchar(100) DEFAULT NULL COMMENT '警告资源:topic,producer等', + `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `wid` int(11) NOT NULL, + KEY `ut` (`uid`, `create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户警告表'; + +-- ---------------------------- +-- Table structure for `warn_info` +-- ---------------------------- +DROP TABLE IF EXISTS `warn_info`; +CREATE TABLE `warn_info` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `content` text DEFAULT NULL COMMENT '警告内容', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='警告信息表'; \ No newline at end of file diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/AuditResetOffset.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/AuditResetOffset.java index c86261bc..61a7b8b9 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/AuditResetOffset.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/AuditResetOffset.java @@ -9,6 +9,8 @@ public class AuditResetOffset { private long consumerId; // null:重置为最大offset,时间戳:重置为某时间点(yyyy-MM-dd#HH:mm:ss:SSS) private String offset; + + private String messageKey; public long getAid() { return aid; @@ -42,9 +44,17 @@ public void setOffset(String offset) { this.offset = offset; } + public String getMessageKey() { + return messageKey; + } + + public void setMessageKey(String messageKey) { + this.messageKey = messageKey; + } + @Override public String toString() { return "AuditResetOffset [aid=" + aid + ", tid=" + tid + ", consumerId=" + consumerId + ", offset=" + offset - + "]"; + + ", messageKey=" + messageKey + "]"; } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/BrokerFallBehind.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/BrokerFallBehind.java new file mode 100644 index 00000000..8f8f1ddd --- /dev/null +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/BrokerFallBehind.java @@ -0,0 +1,127 @@ +package com.sohu.tv.mq.cloud.bo; + +import java.util.List; + +/** + * broker内存落后 + * + * @author yongfeigao + * @date 2021年9月22日 + */ +public class BrokerFallBehind { + private String broker; + private String addr; + // broker最大可用内存 + private long maxAccessMessageInMemory; + + private List list; + + public String getBroker() { + return broker; + } + + public void setBroker(String broker) { + this.broker = broker; + } + + public String getAddr() { + return addr; + } + + public void setAddr(String addr) { + this.addr = addr; + } + + public long getMaxAccessMessageInMemory() { + return maxAccessMessageInMemory; + } + + public String getMaxAccessMessageInMemoryFormat() { + return parseToGB(maxAccessMessageInMemory); + } + + private String parseToGB(long value) { + return format((double) value / (1024L * 1024L * 1024L)) + "G"; + } + + private float format(double v) { + return (int) (v * 100) / 100.0f; + } + + public void setMaxAccessMessageInMemory(long maxAccessMessageInMemory) { + this.maxAccessMessageInMemory = maxAccessMessageInMemory; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public class ConsumerFallBehind { + private String topic; + private String consumer; + private long accumulated; + private String queue; + private String consumerLink; + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getConsumer() { + return consumer; + } + + public void setConsumer(String consumer) { + this.consumer = consumer; + } + + public long getAccumulated() { + return accumulated; + } + + public String getAccumulatedFormat() { + return parse(accumulated); + } + + public String getQueue() { + return queue; + } + + public void setQueue(String queue) { + this.queue = queue; + } + + private String parse(long bytes) { + if (bytes < 1024) { + return bytes + "B"; + } + if (bytes < 1024L * 1024L) { + return format((double) bytes / 1024L) + "K"; + } + if (bytes < 1024L * 1024L * 1024L) { + return format((double) bytes / (1024L * 1024L)) + "M"; + } + return parseToGB(bytes); + } + + public void setAccumulated(long accumulated) { + this.accumulated = accumulated; + } + + public String getConsumerLink() { + return consumerLink; + } + + public void setConsumerLink(String consumerLink) { + this.consumerLink = consumerLink; + } + } +} diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ClusterStat.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ClusterStat.java new file mode 100644 index 00000000..904ae102 --- /dev/null +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ClusterStat.java @@ -0,0 +1,42 @@ +package com.sohu.tv.mq.cloud.bo; + +import java.util.List; + +/** + * 集群状态 + * + * @author yongfeigao + * @date 2021年9月18日 + */ +public class ClusterStat { + // 集群名 + private String clusterName; + // 集群链接 + private String clusterLink; + // 状态 + private List stats; + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getClusterLink() { + return clusterLink; + } + + public void setClusterLink(String clusterLink) { + this.clusterLink = clusterLink; + } + + public List getStats() { + return stats; + } + + public void setStats(List stats) { + this.stats = stats; + } +} diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ConsumerBlock.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ConsumerBlock.java index a1507af1..f9e206bd 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ConsumerBlock.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ConsumerBlock.java @@ -1,80 +1,102 @@ package com.sohu.tv.mq.cloud.bo; import java.util.Date; + /** * 各个队列消费阻塞情况 + * * @author yongfeigao - * */ public class ConsumerBlock { - //消费情况id - private int csid; - //client id - private String instance; - //broker name - private String broker; - //queue id - private int qid; - //阻塞时间,当前时间-最新消费时间 ms - private long blockTime; - //记录更新时间 - private Date updatetime; - //最近一次发生offset moved事件的时间 - private long offsetMovedTime; - //发生offset moved事件的次数 - private int offsetMovedTimes; - public long getOffsetMovedTime() { - return offsetMovedTime; - } - public void setOffsetMovedTime(long offsetMovedTime) { - this.offsetMovedTime = offsetMovedTime; - } - public int getOffsetMovedTimes() { - return offsetMovedTimes; - } - public void setOffsetMovedTimes(int offsetMovedTimes) { - this.offsetMovedTimes = offsetMovedTimes; - } - public long getBlockTime() { - return blockTime; - } - public void setBlockTime(long blockTime) { - this.blockTime = blockTime; - } - public Date getUpdatetime() { - return updatetime; - } - public void setUpdatetime(Date updatetime) { - this.updatetime = updatetime; - } - public int getCsid() { - return csid; - } - public void setCsid(int csid) { - this.csid = csid; - } - public String getInstance() { - return instance; - } - public void setInstance(String instance) { - this.instance = instance; - } - public String getBroker() { - return broker; - } - public void setBroker(String broker) { - this.broker = broker; - } - public int getQid() { - return qid; - } - public void setQid(int qid) { - this.qid = qid; - } - @Override - public String toString() { - return "ConsumerBlock [csid=" + csid + ", instance=" + instance - + ", broker=" + broker + ", qid=" + qid + ", blockTime=" - + blockTime + ", updatetime=" + updatetime + "]"; - } + // 消费情况id + private int csid; + // client id + private String instance; + // broker name + private String broker; + // queue id + private int qid; + // 阻塞时间,当前时间-最新消费时间 ms + private long blockTime; + // 记录更新时间 + private Date updatetime; + // 最近一次发生offset moved事件的时间 + private long offsetMovedTime; + // 发生offset moved事件的次数 + private int offsetMovedTimes; + + public long getOffsetMovedTime() { + return offsetMovedTime; + } + + public void setOffsetMovedTime(long offsetMovedTime) { + this.offsetMovedTime = offsetMovedTime; + } + + public int getOffsetMovedTimes() { + return offsetMovedTimes; + } + + public void setOffsetMovedTimes(int offsetMovedTimes) { + this.offsetMovedTimes = offsetMovedTimes; + } + + public long getBlockTime() { + return blockTime; + } + + public void setBlockTime(long blockTime) { + this.blockTime = blockTime; + } + + public Date getUpdatetime() { + return updatetime; + } + + public void setUpdatetime(Date updatetime) { + this.updatetime = updatetime; + } + + public int getCsid() { + return csid; + } + + public void setCsid(int csid) { + this.csid = csid; + } + + public String getInstance() { + return instance; + } + + public void setInstance(String instance) { + this.instance = instance; + } + + public String getBroker() { + return broker; + } + + public void setBroker(String broker) { + this.broker = broker; + } + + public int getQid() { + return qid; + } + + public void setQid(int qid) { + this.qid = qid; + } + + public long getBlockTimeInSecs() { + return blockTime / 1000; + } + + @Override + public String toString() { + return "ConsumerBlock [csid=" + csid + ", instance=" + instance + + ", broker=" + broker + ", qid=" + qid + ", blockTime=" + + blockTime + ", updatetime=" + updatetime + "]"; + } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ConsumerConfig.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ConsumerConfig.java index 5d07b183..3f72fb47 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ConsumerConfig.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ConsumerConfig.java @@ -11,6 +11,8 @@ public class ConsumerConfig { private String consumer; // 重试消息跳过 private Long retryMessageResetTo; + // 重试消息跳过的key + private String retryMessageSkipKey; // 消费暂停 private Boolean pause; @@ -68,6 +70,14 @@ public void setPauseClientId(String pauseClientId) { this.pauseClientId = pauseClientId; } + public String getRetryMessageSkipKey() { + return retryMessageSkipKey; + } + + public void setRetryMessageSkipKey(String retryMessageSkipKey) { + this.retryMessageSkipKey = retryMessageSkipKey; + } + @Override public int hashCode() { final int prime = 31; @@ -78,6 +88,7 @@ public int hashCode() { result = prime * result + ((pauseClientId == null) ? 0 : pauseClientId.hashCode()); result = prime * result + ((permitsPerSecond == null) ? 0 : permitsPerSecond.hashCode()); result = prime * result + ((retryMessageResetTo == null) ? 0 : retryMessageResetTo.hashCode()); + result = prime * result + ((retryMessageSkipKey == null) ? 0 : retryMessageSkipKey.hashCode()); return result; } @@ -120,13 +131,19 @@ public boolean equals(Object obj) { return false; } else if (!retryMessageResetTo.equals(other.retryMessageResetTo)) return false; + if (retryMessageSkipKey == null) { + if (other.retryMessageSkipKey != null) + return false; + } else if (!retryMessageSkipKey.equals(other.retryMessageSkipKey)) + return false; return true; } @Override public String toString() { - return "ConsumerConfig [consumer=" + consumer + ", retryMessageResetTo=" + retryMessageResetTo + ", pause=" - + pause + ", pauseClientId=" + pauseClientId + ", enableRateLimit=" + enableRateLimit + ", permitsPerSecond=" - + permitsPerSecond + "]"; + return "ConsumerConfig [consumer=" + consumer + ", retryMessageResetTo=" + retryMessageResetTo + + ", retryMessageSkipKey=" + retryMessageSkipKey + ", pause=" + pause + ", pauseClientId=" + + pauseClientId + ", enableRateLimit=" + enableRateLimit + ", permitsPerSecond=" + permitsPerSecond + + "]"; } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/DecodedMessage.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/DecodedMessage.java index feb6f2fd..7da52796 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/DecodedMessage.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/DecodedMessage.java @@ -2,12 +2,9 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.nio.ByteBuffer; import org.apache.rocketmq.common.MixAll; -import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.sysflag.MessageSysFlag; import com.alibaba.fastjson.JSONObject; import com.sohu.tv.mq.serializable.MessageSerializerEnum; @@ -25,6 +22,8 @@ public class DecodedMessage extends MessageExt { private String broker; private String realTopic; private String consumer; + // broker端的msgid + private String offsetMsgId; // 消息体类型 private MessageBodyType messageBodyType; @@ -97,21 +96,29 @@ private String address(SocketAddress addr) { return sb.toString(); } - /** - * 获取offsetMsgId - * @return - */ - public String getOffsetMsgId() { - int msgIdLength = (getSysFlag() & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 + 8 : 16 + 4 + 8; - ByteBuffer byteBufferMsgId = ByteBuffer.allocate(msgIdLength); - return MessageDecoder.createMessageId(byteBufferMsgId, getStoreHostBytes(), getCommitLogOffset()); - } +// /** +// * 获取offsetMsgId +// * @return +// */ +// public String getOffsetMsgId() { +// int msgIdLength = (getSysFlag() & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 + 8 : 16 + 4 + 8; +// ByteBuffer byteBufferMsgId = ByteBuffer.allocate(msgIdLength); +// return MessageDecoder.createMessageId(byteBufferMsgId, getStoreHostBytes(), getCommitLogOffset()); +// } @Override public String toString() { return super.toString(); } + public String getOffsetMsgId() { + return offsetMsgId; + } + + public void setOffsetMsgId(String offsetMsgId) { + this.offsetMsgId = offsetMsgId; + } + public MessageBodyType getMessageBodyType() { return messageBodyType; } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ServerWarn.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ServerWarn.java new file mode 100644 index 00000000..42ffc813 --- /dev/null +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ServerWarn.java @@ -0,0 +1,76 @@ +package com.sohu.tv.mq.cloud.bo; + +import java.util.List; + +/** + * 服务器警告 + * + * @author yongfeigao + * @date 2021年9月22日 + */ +public class ServerWarn { + private String ip; + private String ipLink; + + private List list; + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getIpLink() { + return ipLink; + } + + public void setIpLink(String ipLink) { + this.ipLink = ipLink; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public static class ServerWarnItem { + private String type; + private String value; + private String threshold; + + public ServerWarnItem(String type, String value, String threshold) { + this.type = type; + this.value = value; + this.threshold = threshold; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getThreshold() { + return threshold; + } + + public void setThreshold(String threshold) { + this.threshold = threshold; + } + } +} diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/SlaveFallBehind.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/SlaveFallBehind.java new file mode 100644 index 00000000..fd415397 --- /dev/null +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/SlaveFallBehind.java @@ -0,0 +1,56 @@ +package com.sohu.tv.mq.cloud.bo; + +import com.sohu.tv.mq.cloud.util.WebUtil; + +/** + * slave落后 + * + * @author yongfeigao + * @date 2021年9月22日 + */ +public class SlaveFallBehind { + private String clusterName; + private String brokerLink; + private long fallBehindOffset; + private long slaveFallBehindSize; + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getBrokerLink() { + return brokerLink; + } + + public void setBrokerLink(String brokerLink) { + this.brokerLink = brokerLink; + } + + public long getFallBehindOffset() { + return fallBehindOffset; + } + + public String getFallBehindOffsetFormat() { + return WebUtil.sizeFormat(fallBehindOffset); + } + + public void setFallBehindOffset(long fallBehindOffset) { + this.fallBehindOffset = fallBehindOffset; + } + + public long getSlaveFallBehindSize() { + return slaveFallBehindSize; + } + + public String getSlaveFallBehindSizeFormat() { + return WebUtil.sizeFormat(slaveFallBehindSize); + } + + public void setSlaveFallBehindSize(long slaveFallBehindSize) { + this.slaveFallBehindSize = slaveFallBehindSize; + } +} diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/TopicStat.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/TopicStat.java new file mode 100644 index 00000000..f3430894 --- /dev/null +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/TopicStat.java @@ -0,0 +1,30 @@ +package com.sohu.tv.mq.cloud.bo; + +/** + * topic状况 + * + * @author yongfeigao + * @date 2021年8月24日 + */ +public class TopicStat { + // topic数量 + private int size; + // topic流量 + private int count; + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } +} diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/User.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/User.java index be90ec40..7cad6b73 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/User.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/User.java @@ -36,11 +36,14 @@ public class User { // 更新日期 private Date updateTime; - // 接收通知 1:不接收,2:接收 + // 接收通知 0:不接收,1:接收 private int receiveNotice = -1; // 密码 private String password; + + // 接收手机通知 0:不接收,1:接收 + private int receivePhoneNotice = -1; public long getId() { return id; @@ -114,6 +117,18 @@ public void setReceiveNotice(int receiveNotice) { this.receiveNotice = receiveNotice; } + public int getReceivePhoneNotice() { + return receivePhoneNotice; + } + + public void setReceivePhoneNotice(int receivePhoneNotice) { + this.receivePhoneNotice = receivePhoneNotice; + } + + public boolean receivePhoneNotice() { + return 1 == receivePhoneNotice; + } + public String getPassword() { return password; } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/UserWarn.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/UserWarn.java new file mode 100644 index 00000000..befd5673 --- /dev/null +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/UserWarn.java @@ -0,0 +1,147 @@ +package com.sohu.tv.mq.cloud.bo; + +import java.util.Date; + +/** + * 用户警告 + * + * @author yongfeigao + * @date 2021年9月13日 + */ +public class UserWarn { + // 警告 + private long uid; + // 类型 + private int type; + // 资源 + private String resource; + // 创建时间 + private Date createTime; + // 警告id + private long wid; + // 警告内容 + private String content; + + public UserWarn() { + } + + public UserWarn(String content) { + this.content = content; + } + + public UserWarn(long uid, int type, String resource, long wid) { + this.uid = uid; + this.type = type; + this.resource = resource; + this.wid = wid; + } + + /** + * 警告类型 + * + * @author yongfeigao + * @date 2021年9月13日 + */ + public enum WarnType { + PRODUCE_EXCEPTION(1, "生产异常", "produceException.html"), + CONSUME_FAIL(2, "消费失败", "consumeFail.html"), + CONSUME_UNDONE(3, "消费堆积", "consumeUndone.html"), + CONSUME_BLOCK(4, "消费阻塞", "consumeBlock.html"), + CONSUME_OFFSET_MOVED(5, "消费偏移量错误", "consumeOffsetMoved.html"), + CONSUME_SUBSCRIBE_ERROR(6, "消费订阅错误", "consumeSubscribeError.html"), + DEAD_MESSAGE(7, "死消息", "deadMessage.html"), + CONSUME_FALL_BEHIND(8, "消费落后", "consumeFallBehind.html"), + TOPIC_TRAFFIC(9, "流量异常", "topicTraffic.html"), + BROKER_STORE_SLOW(10, "broker存储过慢", "brokerStoreSlow.html"), + BROKER_ERROR(11, "Broker异常", "mqError.html"), + NAMESERVER_ERROR(12, "NameServer异常", "mqError.html"), + SLAVE_FALL_BEHIND(13, "slave同步落后", "slaveFallBehind.html"), + SERVER_WARN(14, "服务器异常", "serverWarn.html"), + + UNKNOWN(100, "未知", "unknown.html"), + ; + + private int type; + private String name; + private String warnTemplate; + + private WarnType(int type, String name, String warnTemplate) { + this.type = type; + this.name = name; + this.warnTemplate = warnTemplate; + } + + public int getType() { + return type; + } + + public String getName() { + return name; + } + + public String getWarnTemplate() { + return warnTemplate; + } + + public static WarnType getWarnType(int type) { + for (WarnType warnType : WarnType.values()) { + if (type == warnType.getType()) { + return warnType; + } + } + return UNKNOWN; + } + } + + public long getUid() { + return uid; + } + + public void setUid(long uid) { + this.uid = uid; + } + + public int getType() { + return type; + } + + public String getTypeName() { + return WarnType.getWarnType(type).getName(); + } + + public void setType(int type) { + this.type = type; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public long getWid() { + return wid; + } + + public void setWid(long wid) { + this.wid = wid; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/conf/AuthWebMvcConfigurerAdapter.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/conf/AuthWebMvcConfigurerAdapter.java index efb75898..0c84019f 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/conf/AuthWebMvcConfigurerAdapter.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/conf/AuthWebMvcConfigurerAdapter.java @@ -1,14 +1,19 @@ package com.sohu.tv.mq.cloud.conf; +import java.util.Collections; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; +import org.springframework.web.servlet.view.json.MappingJackson2JsonView; import com.sohu.tv.mq.cloud.web.interceptor.AdminInterceptor; import com.sohu.tv.mq.cloud.web.interceptor.AuthInterceptor; @@ -17,21 +22,22 @@ /** * 拦截器配置 - * @Description: + * + * @Description: * @author yongfeigao * @date 2018年6月12日 */ @Configuration public class AuthWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter { - + @Autowired @Qualifier("authInterceptor") private AuthInterceptor authInterceptor; - + @Autowired @Qualifier("userGuideInterceptor") private UserGuideInterceptor userGuideInterceptor; - + @Autowired @Qualifier("adminInterceptor") private AdminInterceptor adminInterceptor; @@ -57,4 +63,14 @@ public void addArgumentResolvers(List argumentRes public UserInfoMethodArgumentResolver uerInfoMethodArgumentResolver() { return new UserInfoMethodArgumentResolver(); } + + @Bean + public ViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager, + List viewResolvers) { + ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); + resolver.setContentNegotiationManager(manager); + resolver.setDefaultViews(Collections.singletonList(new MappingJackson2JsonView())); + resolver.setViewResolvers(viewResolvers); + return resolver; + } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/AuditResetOffsetDao.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/AuditResetOffsetDao.java index cb8e8005..bf00a04d 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/AuditResetOffsetDao.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/AuditResetOffsetDao.java @@ -14,8 +14,10 @@ public interface AuditResetOffsetDao { */ @Insert("") public Integer insert(@Param("of") AuditResetOffset auditResetOffset); diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/ConsumerConfigDao.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/ConsumerConfigDao.java index 8027b104..0c9ccd25 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/ConsumerConfigDao.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/ConsumerConfigDao.java @@ -19,19 +19,19 @@ public interface ConsumerConfigDao { * * @param consumerConfig */ - @Insert("") public Integer selectByUidCount(@Param("topic") String topic, @Param("uid") long uid, @Param("traceClusterIds") List traceClusterIds); + /** + * 根据uid查询topic状况 + * @param uid + * @param traceClusterIds + * @return + */ + @Select("") + public TopicStat selectTopicStat(@Param("uid") long uid, @Param("traceClusterIds") List traceClusterIds); + /** * 根据cluster_id查询topic * @param idList diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserDao.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserDao.java index 9f46bf6e..6bdc5f18 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserDao.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserDao.java @@ -46,6 +46,7 @@ public interface UserDao { + ",name=#{user.name}" + ",type=#{user.type}" + ",receive_notice=#{user.receiveNotice}" + + ",receive_phone_notice=#{user.receivePhoneNotice}" + " where id = #{user.id}") public Integer update(@Param("user") User user); diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserWarnCount.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserWarnCount.java new file mode 100644 index 00000000..923eddc1 --- /dev/null +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserWarnCount.java @@ -0,0 +1,29 @@ +package com.sohu.tv.mq.cloud.dao; + +import java.util.Date; + +/** + * 用户警告数 + * + * @author yongfeigao + * @date 2021年9月15日 + */ +public class UserWarnCount { + private Date createDate; + private int count; + public Date getCreateDate() { + return createDate; + } + + public void setCreateDate(Date createDate) { + this.createDate = createDate; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } +} diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserWarnDao.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserWarnDao.java new file mode 100644 index 00000000..eb601aec --- /dev/null +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserWarnDao.java @@ -0,0 +1,83 @@ +package com.sohu.tv.mq.cloud.dao; + +import java.util.Date; +import java.util.List; + +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Options; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import com.sohu.tv.mq.cloud.bo.UserWarn; + +/** + * 用户警告 + * + * @author yongfeigao + * @date 2021年9月13日 + */ +public interface UserWarnDao { + + /** + * 保存警告信息 + * + * @param userWarn + * @return + */ + @Options(useGeneratedKeys = true, keyProperty = "userWarn.wid") + @Insert("insert into warn_info(content) values(#{userWarn.content})") + public Long insert(@Param("userWarn") UserWarn userWarn); + + /** + * 批量插入记录 + * + * @param consumer + */ + @Insert("") + public Integer batchInsert(@Param("uwList") List userWarnList); + + /** + * 查询警告信息 + * + * @param uid + * @param createDate + * @param m + * @param size + * @return + */ + @Select("select * from user_warn where uid = #{uid} order by create_time desc limit #{m},#{n}") + public List select(@Param("uid") long uid, @Param("m") int m, @Param("n") int size); + + /** + * 查询警告详情 + * @param wid + * @return + */ + @Select("select * from warn_info where id = #{wid}") + public UserWarn selectWarnInfo(@Param("wid") long wid); + + /** + * 查询警告信息数量 + * + * @param uid + * @param createDate + * @param m + * @param size + * @return + */ + @Select("select count(1) from user_warn where uid = #{uid}") + public Integer selectCount(@Param("uid") long uid); + + /** + * 警告数 + * + * @param uid + * @param createData + * @return + */ + @Select("select date(create_time) createDate,count(1) count from user_warn where uid = #{uid} and create_time > #{time} group by date(create_time)") + public List warnCount(@Param("uid") long uid, @Param("time") Date time); +} \ No newline at end of file diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/AlertService.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/AlertService.java index 59446aac..0e7efdfe 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/AlertService.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/AlertService.java @@ -1,47 +1,68 @@ package com.sohu.tv.mq.cloud.service; +import java.io.StringWriter; import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Service; import com.sohu.tv.mq.cloud.bo.Audit.TypeEnum; import com.sohu.tv.mq.cloud.bo.User; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; import com.sohu.tv.mq.cloud.common.service.MailSender; import com.sohu.tv.mq.cloud.common.service.SmsSender; +import com.sohu.tv.mq.cloud.util.Jointer; import com.sohu.tv.mq.cloud.util.MQCloudConfigHelper; +import freemarker.template.Configuration; +import freemarker.template.Template; + /** * 报警服务,从配置文件获取配置 - * @Description: + * + * @Description: * @author yongfeigao * @date 2018年5月28日 */ @Service("mqcloudAlertService") @ConfigurationProperties("eureka.instance.metadataMap") public class AlertService { - + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + // email地址 private String developers; - + // 电话 private String phones; - + @Autowired private UserService userService; - + @Autowired private MQCloudConfigHelper mqCloudConfigHelper; - + @Autowired(required = false) private MailSender mailSender; - + @Autowired(required = false) private SmsSender smsSender; - + + @Autowired + private Configuration configuration; + + @Autowired + private UserWarnService userWarnService; + /** * 发送审核邮件 */ @@ -51,13 +72,13 @@ public boolean sendAuditMail(User user, TypeEnum type, String content) { username = user.getEmail(); } String title = "MQCloud申请"; - if(mqCloudConfigHelper.isLocal()) { + if (mqCloudConfigHelper.isLocal()) { title = "local-" + title; } - return sendMail(title, username + "申请" + type.getName() + " " + content + + return sendMail(title, username + "申请" + type.getName() + " " + content + ", 去审核"); } - + /** * 发送邮件 * @@ -66,48 +87,62 @@ public boolean sendAuditMail(User user, TypeEnum type, String content) { * @return 成功返回true,否则返回false */ public boolean sendMail(String title, String content) { - if(mailSender == null) { + if (mailSender == null) { return false; } return mailSender.send(title, content, getDevelopers()); } - + /** * 发送邮件(同时抄送管理员) + * * @param title 标题 * @param content 内容 * @param email 收件人 * @return 成功返回true,否则返回false */ public boolean sendMail(String title, String content, String email) { - if(mailSender == null) { + if (mailSender == null) { return false; } return mailSender.send(title, content, email, getDevelopers(), 0); } - + /** * 发送手机报警 * * @param message 报警信息 * @return 成功返回true,否则返回false */ - public boolean sendPhone(String title, String message) { - if(smsSender == null) { + public boolean sendPhone(String title, String message, Collection phones) { + if (smsSender == null) { + logger.info("smsSender is null, title:{}, message:{}, phones:{}", title, message, phones); return false; } - List ps = getPhones(); - if(ps == null) { + if (phones == null || phones.size() == 0) { return false; } - for(String phone : ps) { - smsSender.send(title + "预警]" + message, phone); + for (String phone : phones) { + sendPhone(title, message, phone); } return true; } - + + /** + * 发送手机预警 + * + * @param title + * @param message + * @param phone + * @return + */ + public boolean sendPhone(String title, String message, String phone) { + return smsSender.send("MQCloud" + title + "预警:" + message, phone); + } + /** * 发送报警邮件 + * * @param email 接收人列表 * @param title 标题 * @param content 内容 @@ -116,21 +151,22 @@ public boolean sendPhone(String title, String message) { public boolean sendWarnMail(String email, String flag, String content) { return sendWarnMail(email, flag, content, 3); } - + /** * 发送报警邮件 + * * @param email 接收人列表 * @param title 标题 * @param content 内容 * @return 成功返回true,否则返回false */ public boolean sendWarnMail(String email, String flag, String content, int retry) { - if(retry <= 0) { + if (retry <= 0) { retry = 1; } - for(int i = 0; i < retry; ++i) { + for (int i = 0; i < retry; ++i) { boolean ok = sendWarnMailInternal(email, flag, content); - if(ok) { + if (ok) { return true; } else { try { @@ -142,9 +178,10 @@ public boolean sendWarnMail(String email, String flag, String content, int retry } return false; } - + /** * 发送报警邮件 + * * @param email 接收人列表 * @param title 标题 * @param content 内容 @@ -152,22 +189,22 @@ public boolean sendWarnMail(String email, String flag, String content, int retry */ private boolean sendWarnMailInternal(String email, String flag, String content) { String title = "MQCloud " + flag + "预警"; - if(mqCloudConfigHelper.isLocal()) { + if (mqCloudConfigHelper.isLocal()) { title = "local-" + title; } - if(StringUtils.isBlank(email)) { + if (StringUtils.isBlank(email)) { return sendMail(title, content); } else { - if(mailSender == null) { + if (mailSender == null) { return false; } return mailSender.send(title, content, email, getDevelopers(), 10000); } } - + public String getDevelopers() { String email = userService.queryMonitorEmail(); - if(email == null) { + if (email == null) { return developers; } return email; @@ -179,7 +216,7 @@ public void setDevelopers(String developers) { public List getPhones() { List dbPhones = userService.queryMonitorPhone(); - if(dbPhones == null) { + if (dbPhones == null) { return Arrays.asList(phones.split(",")); } return dbPhones; @@ -188,4 +225,88 @@ public List getPhones() { public void setPhones(String phones) { this.phones = phones; } + + /** + * 发送预警 + * + * @param users + * @param warnType + * @param param + * @return + */ + public boolean sendWarn(Collection users, WarnType warnType, Map param) { + try { + Template template = configuration.getTemplate("mail/" + warnType.getWarnTemplate()); + StringWriter stringWriter = new StringWriter(); + template.process(param, stringWriter); + String email = Jointer.BY_COMMA.join(users, u -> u.getEmail()); + String warnContent = stringWriter.toString(); + // 发送邮件预警 + sendWarnMail(email, warnType.getName(), warnContent); + // 发送手机预警 + sendWarnPhone(users, warnType, param); + // 保存预警信息 + Object obj = param.get("resource"); + String resource = warnType.getName(); + if (obj != null) { + resource = obj.toString(); + } + userWarnService.save(users, warnType, resource, warnContent); + return true; + } catch (Exception e) { + logger.error("sendWarnMail error type:{}, param:{}", warnType, param, e); + } + return false; + } + + /** + * 发送手机预警 + * + * @param users + * @param warnType + * @param param + * @return + */ + private boolean sendWarnPhone(Collection users, WarnType warnType, Map param) { + // 用户手机预警 + Set userPhones = new HashSet<>(); + // 特殊异常通知管理员 + if (WarnType.BROKER_ERROR == warnType || WarnType.NAMESERVER_ERROR == warnType) { + List adminPhoneList = getPhones(); + if (adminPhoneList != null) { + userPhones.addAll(adminPhoneList); + } + } + if (users != null) { + users.stream().filter(u -> u.receivePhoneNotice() && u.getMobile() != null) + .map(u -> u.getMobile()) + .forEach(m -> userPhones.add(m)); + } + if (userPhones.size() > 0) { + return sendWarnPhone(warnType, param, userPhones); + } + return false; + } + + /** + * 发送手机预警 + * + * @param warnType + * @param param + * @return + */ + public boolean sendWarnPhone(WarnType warnType, Map param, Collection phones) { + try { + Template template = configuration.getTemplate("phone/" + warnType.getWarnTemplate()); + StringWriter stringWriter = new StringWriter(); + param.put("newLine", "\n"); + template.process(param, stringWriter); + String warnContent = stringWriter.toString(); + sendPhone(warnType.getName(), warnContent, phones); + return true; + } catch (Exception e) { + logger.warn("sendWarnPhone error type:{}, param:{}, error:{}", warnType, param, e.toString()); + } + return false; + } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ConsumerDeadTrafficService.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ConsumerDeadTrafficService.java index 49281cd4..b8f01ee7 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ConsumerDeadTrafficService.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ConsumerDeadTrafficService.java @@ -1,10 +1,16 @@ package com.sohu.tv.mq.cloud.service; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.sohu.tv.mq.cloud.bo.TopicConsumer; import com.sohu.tv.mq.cloud.bo.TopicHourTraffic; +import com.sohu.tv.mq.cloud.bo.User; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; import com.sohu.tv.mq.cloud.util.MQCloudConfigHelper; /** @@ -21,11 +27,14 @@ public class ConsumerDeadTrafficService extends HourTrafficService { @Autowired private MQCloudConfigHelper mqCloudConfigHelper; - - protected void alert(TopicHourTraffic topicTraffic, TopicConsumer topicConsumer, String email) { - alertService.sendWarnMail(email, "死消费", "topic:" + topicConsumer.getTopic() + " 消费者:" - + mqCloudConfigHelper.getTopicConsumeLink(topicConsumer.getTid(), topicConsumer.getConsumer()) - + " 死消息量:" + topicTraffic.getCount()); + + protected void alert(TopicHourTraffic topicTraffic, TopicConsumer topicConsumer, List userList) { + Map paramMap = new HashMap<>(); + paramMap.put("topic", topicConsumer.getTopic()); + paramMap.put("consumer", mqCloudConfigHelper.getTopicConsumeLink(topicConsumer.getTid(), topicConsumer.getConsumer())); + paramMap.put("count", topicTraffic.getCount()); + paramMap.put("resource", topicConsumer.getConsumer()); + alertService.sendWarn(userList, WarnType.DEAD_MESSAGE, paramMap); } protected String getCountKey() { diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ConsumerRetryTrafficService.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ConsumerRetryTrafficService.java index 2850d3be..9a6ab499 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ConsumerRetryTrafficService.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ConsumerRetryTrafficService.java @@ -1,11 +1,17 @@ package com.sohu.tv.mq.cloud.service; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.apache.rocketmq.store.stats.BrokerStatsManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.sohu.tv.mq.cloud.bo.TopicConsumer; import com.sohu.tv.mq.cloud.bo.TopicHourTraffic; +import com.sohu.tv.mq.cloud.bo.User; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; import com.sohu.tv.mq.cloud.util.MQCloudConfigHelper; /** @@ -27,18 +33,20 @@ public class ConsumerRetryTrafficService extends HourTrafficService{ @Autowired private AlarmConfigBridingService alarmConfigBridingService; - protected void alert(TopicHourTraffic topicTraffic, TopicConsumer topicConsumer, String email) { + protected void alert(TopicHourTraffic topicTraffic, TopicConsumer topicConsumer, List userList) { long consumerFailCount = alarmConfigBridingService.getConsumerFailCount(topicConsumer.getConsumer()); if (consumerFailCount >= 0 && consumerFailCount < topicTraffic.getCount()) { // 验证报警频率 if (alarmConfigBridingService.needWarn("consumerFail", topicConsumer.getTopic(), topicConsumer.getConsumer())) { - alertService.sendWarnMail(email, "消费失败", "topic:" + topicConsumer.getTopic() + " 消费者:" - + mqCloudConfigHelper.getTopicConsumeLink(topicConsumer.getTid(), topicConsumer.getConsumer(), - System.currentTimeMillis()) - + " 消费失败量:" + topicTraffic.getCount() - + ", 跳过重试消息?"); + Map paramMap = new HashMap<>(); + paramMap.put("topic", topicConsumer.getTopic()); + paramMap.put("consumer",mqCloudConfigHelper.getTopicConsumeLink(topicConsumer.getTid(), + topicConsumer.getConsumer(), System.currentTimeMillis())); + paramMap.put("count", topicTraffic.getCount()); + paramMap.put("link", mqCloudConfigHelper.getTopicConsumeHref(topicConsumer.getTid(), + topicConsumer.getConsumer(), topicConsumer.getCid(), 0)); + paramMap.put("resource", topicConsumer.getConsumer()); + alertService.sendWarn(userList, WarnType.CONSUME_FAIL, paramMap); } } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/HourTrafficService.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/HourTrafficService.java index dca3f84f..ec6de655 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/HourTrafficService.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/HourTrafficService.java @@ -14,7 +14,6 @@ import com.sohu.tv.mq.cloud.bo.User; import com.sohu.tv.mq.cloud.mq.DefaultInvoke; import com.sohu.tv.mq.cloud.mq.MQAdminTemplate; -import com.sohu.tv.mq.cloud.util.Jointer; import com.sohu.tv.mq.cloud.util.Result; /** @@ -59,11 +58,7 @@ public void invoke(MQAdminExt mqAdmin) throws Exception { if (topicTraffic.getCount() > 0) { Result> userListResult = userConsumerService.queryUserByConsumer( topicConsumer.getTid(), topicConsumer.getCid()); - String email = null; - if (userListResult.isNotEmpty()) { - email = Jointer.BY_COMMA.join(userListResult.getResult(), u -> u.getEmail()); - } - alert(topicTraffic, topicConsumer, email); + alert(topicTraffic, topicConsumer, userListResult.getResult()); // 打印报警信息 logger.warn("alert! consumer fail topic:{}, consumer:{}, consumerFailCount:{}", topicConsumer.getTopic(), topicConsumer.getConsumer(), topicTraffic.getCount()); @@ -78,7 +73,7 @@ public Cluster mqCluster() { return topicConsumerList.size(); } - protected abstract void alert(TopicHourTraffic topicTraffic, TopicConsumer topicConsumer, String email); + protected abstract void alert(TopicHourTraffic topicTraffic, TopicConsumer topicConsumer, List userList); public Result delete(Date date) { throw new UnsupportedOperationException(); diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/MessageService.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/MessageService.java index 46335416..0796bef5 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/MessageService.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/MessageService.java @@ -514,7 +514,7 @@ private List getMQOffsetList(Cluster cluster, MQPullConsumer consumer, topicConfig.setTopicName(messageQueryCondition.getTopic()); topicConfig.setWriteQueueNums(queueData.getWriteQueueNums()); topicConfig.setReadQueueNums(queueData.getReadQueueNums()); - topicConfig.setTopicSysFlag(queueData.getTopicSynFlag()); + topicConfig.setTopicSysFlag(queueData.getTopicSysFlag()); } else if (messageQueryCondition.getTopic().equals("SCHEDULE_TOPIC_XXXX")) { topicConfig = new TopicConfig(); topicConfig.setTopicName("SCHEDULE_TOPIC_XXXX"); diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/TopicService.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/TopicService.java index a44c60e8..faca3396 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/TopicService.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/TopicService.java @@ -29,6 +29,7 @@ import com.sohu.tv.mq.cloud.bo.Cluster; import com.sohu.tv.mq.cloud.bo.Topic; import com.sohu.tv.mq.cloud.bo.TopicConsumer; +import com.sohu.tv.mq.cloud.bo.TopicStat; import com.sohu.tv.mq.cloud.bo.TopicTraffic; import com.sohu.tv.mq.cloud.bo.User; import com.sohu.tv.mq.cloud.bo.UserProducer; @@ -179,6 +180,34 @@ public Result queryTopicListCount(String topic, long uid, List return Result.getResult(count); } + /** + * 按照用户查询topic状况 + * + * @param Result + */ + public Result queryTopicStat(User user, List traceClusterIds) { + if(user.isAdmin()) { + return queryTopicStat(0, traceClusterIds); + } + return queryTopicStat(user.getId(), traceClusterIds); + } + + /** + * 按照uid查询topic状况 + * + * @param Result + */ + public Result queryTopicStat(long uid, List traceClusterIds) { + TopicStat topicStat = null; + try { + topicStat = topicDao.selectTopicStat(uid, traceClusterIds); + } catch (Exception e) { + logger.error("queryTopicStat err, uid:{}", uid, e); + return Result.getDBErrorResult(e); + } + return Result.getResult(topicStat); + } + /** * 按照用户查询topic数量 * diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/TopicTrafficStatService.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/TopicTrafficStatService.java index 74bde2de..79769074 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/TopicTrafficStatService.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/TopicTrafficStatService.java @@ -9,14 +9,6 @@ import java.util.Map; import java.util.Set; -import com.sohu.tv.mq.cloud.bo.Cluster; -import com.sohu.tv.mq.cloud.bo.Topic; -import com.sohu.tv.mq.cloud.bo.TopicTraffic; -import com.sohu.tv.mq.cloud.bo.TopicTrafficCheckResult; -import com.sohu.tv.mq.cloud.bo.TopicTrafficStat; -import com.sohu.tv.mq.cloud.bo.TopicTrafficWarnConfig; -import com.sohu.tv.mq.cloud.bo.User; -import com.sohu.tv.mq.cloud.util.Jointer; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,6 +16,14 @@ import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import com.sohu.tv.mq.cloud.bo.Cluster; +import com.sohu.tv.mq.cloud.bo.Topic; +import com.sohu.tv.mq.cloud.bo.TopicTraffic; +import com.sohu.tv.mq.cloud.bo.TopicTrafficCheckResult; +import com.sohu.tv.mq.cloud.bo.TopicTrafficStat; +import com.sohu.tv.mq.cloud.bo.TopicTrafficWarnConfig; +import com.sohu.tv.mq.cloud.bo.User; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; import com.sohu.tv.mq.cloud.dao.TopicTrafficStatDao; import com.sohu.tv.mq.cloud.util.DateUtil; import com.sohu.tv.mq.cloud.util.MQCloudConfigHelper; @@ -64,7 +64,7 @@ public class TopicTrafficStatService { @Autowired private TopicTrafficStatDao topicTrafficStatDao; - + /** * 保存topic统计流量 * @param topicTrafficStat @@ -268,41 +268,18 @@ public void sendAlert(List checkResultList, TopicTraffi if (CollectionUtils.isEmpty(checkResultList) || !config.isAlert()) { return; } - StringBuilder content = new StringBuilder("详细如下:

"); - content.append("topic: "); - content.append(mqCloudConfigHelper.getTopicProduceLink(topic.getId(), topic.getName())); - content.append(" 检测到流量异常:
"); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - for (TopicTrafficCheckResult checkResult : checkResultList) { - String warnTime = checkResult.getTime(); - String warnInfo = checkResult.getWarnInfo(); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - } - content.append(""); - content.append("
时间详情
"); - content.append(warnTime); - content.append(""); - content.append(warnInfo); - content.append("
"); - // 根据配置发送给不同的报警接收人 - String email = getAlarmReceiverEmails(config.getAlarmReceiver(), topic.getId()); - alertService.sendWarnMail(email, "topic流量", content.toString()); + Set userSet = getAlarmReceiver(config.getAlarmReceiver(), topic.getId()); + Map paramMap = new HashMap<>(); + paramMap.put("topic", mqCloudConfigHelper.getTopicProduceLink(topic.getId(), topic.getName())); + paramMap.put("list", checkResultList); + paramMap.put("resource", topic.getName()); + alertService.sendWarn(userSet, WarnType.TOPIC_TRAFFIC, paramMap); } /** * 获取报警人 */ - private String getAlarmReceiverEmails(int alarmType, long topicId) { + private Set getAlarmReceiver(int alarmType, long topicId) { Set userSet = new HashSet<>(); Result> producerUserListResult = null; Result> consumerUserListResult = null; @@ -327,7 +304,6 @@ private String getAlarmReceiverEmails(int alarmType, long topicId) { if (consumerUserListResult != null && consumerUserListResult.isNotEmpty()) { userSet.addAll(consumerUserListResult.getResult()); } - String email = Jointer.BY_COMMA.join(userSet, u -> u.getEmail()); - return email; + return userSet; } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/UserService.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/UserService.java index dbf1c756..86bcc87d 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/UserService.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/UserService.java @@ -317,13 +317,20 @@ public List queryMonitorPhone() { * * @param user */ + @SuppressWarnings("unchecked") public List queryMonitorUser() { - try { - return userDao.selectMonitor(); - } catch (Exception e) { - logger.error("queryMonitor err", e); + List monitors = (List) mqLocalCache.get("monitors"); + if (monitors == null) { + try { + monitors = userDao.selectMonitor(); + } catch (Exception e) { + logger.error("queryMonitor err", e); + } + } + if (monitors != null && monitors.size() > 0) { + mqLocalCache.put("monitors", monitors); } - return null; + return monitors; } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/UserWarnService.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/UserWarnService.java new file mode 100644 index 00000000..1a1dd5be --- /dev/null +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/UserWarnService.java @@ -0,0 +1,171 @@ +package com.sohu.tv.mq.cloud.service; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import com.sohu.tv.mq.cloud.bo.User; +import com.sohu.tv.mq.cloud.bo.UserWarn; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; +import com.sohu.tv.mq.cloud.dao.UserWarnCount; +import com.sohu.tv.mq.cloud.dao.UserWarnDao; +import com.sohu.tv.mq.cloud.util.Result; + +/** + * 用户警告服务 + * + * @author yongfeigao + * @date 2021年9月13日 + */ +@Service +public class UserWarnService { + private Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private UserWarnDao userWarnDao; + + @Autowired + private UserService userService; + + /** + * 保存警告信息 + * + * @param users + * @param warnType + * @param resource + * @param content + * @return + */ + public Result save(Collection users, WarnType warnType, String resource, String content) { + if(content == null) { + return null; + } + if (CollectionUtils.isEmpty(users)) { + users = userService.queryMonitorUser(); + } + if (CollectionUtils.isEmpty(users)) { + return null; + } + // 保存警告内容 + Result result = saveWarnContent(new UserWarn(content)); + if (!result.isOK()) { + return result; + } + // 保存警告用户 + long wid = result.getResult().getWid(); + List userWarnList = users.stream() + .map(u -> new UserWarn(u.getId(), warnType.getType(), resource, wid)).collect(Collectors.toList()); + return batchSaveUserWarn(userWarnList); + } + + /** + * 保存警告内容 + * + * @param userWarn + * @return + */ + public Result saveWarnContent(UserWarn userWarn) { + try { + userWarnDao.insert(userWarn); + } catch (Exception e) { + logger.error("insert err, userWarn:{}", userWarn, e); + return Result.getDBErrorResult(e); + } + return Result.getResult(userWarn); + } + + /** + * 批量保存用户警告 + * + * @param uwList + * @return + */ + public Result batchSaveUserWarn(List uwList) { + Integer count = null; + try { + count = userWarnDao.batchInsert(uwList); + } catch (Exception e) { + logger.error("insert err, uwList:{}", uwList, e); + return Result.getDBErrorResult(e); + } + return Result.getResult(count); + } + + /** + * 查询用户警告列表 + * + * @param uid + * @param offset + * @param size + * @return + */ + public Result> queryUserWarnList(long uid, int offset, int size) { + List list = null; + try { + list = userWarnDao.select(uid, offset, size); + } catch (Exception e) { + logger.error("queryUserWarnList err, uid:{}", uid, e); + return Result.getDBErrorResult(e); + } + return Result.getResult(list); + } + + /** + * 查询用户警告量 + * + * @param uid + * @return + */ + public Result queryUserWarnCount(long uid) { + Integer count = null; + try { + count = userWarnDao.selectCount(uid); + } catch (Exception e) { + logger.error("queryUserWarnCount err, uid:{}", uid, e); + return Result.getDBErrorResult(e); + } + return Result.getResult(count); + } + + /** + * 查询用户警告量days天前的警告量 + * + * @param uid + * @return + */ + public Result> queryUserWarnCount(long uid, int days) { + long time = System.currentTimeMillis() - days * 86400000L; + List list = null; + try { + list = userWarnDao.warnCount(uid, new Date(time)); + } catch (Exception e) { + logger.error("queryUserWarnCount err, uid:{} days:{}", uid, days, e); + return Result.getDBErrorResult(e); + } + return Result.getResult(list); + } + + /** + * 查询警告详情 + * + * @param wid + * @return + */ + public Result queryWarnInfo(long wid) { + UserWarn result = null; + try { + result = userWarnDao.selectWarnInfo(wid); + } catch (Exception e) { + logger.error("queryWarnInfo err, wid:{}", wid, e); + return Result.getDBErrorResult(e); + } + return Result.getResult(result); + } +} diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/BrokerStoreStatTask.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/BrokerStoreStatTask.java index 7bea254e..7ca2f87e 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/BrokerStoreStatTask.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/BrokerStoreStatTask.java @@ -2,7 +2,9 @@ import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; @@ -12,6 +14,7 @@ import com.sohu.tv.mq.cloud.bo.Broker; import com.sohu.tv.mq.cloud.bo.Cluster; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; import com.sohu.tv.mq.cloud.common.model.BrokerStoreStat; import com.sohu.tv.mq.cloud.service.AlertService; import com.sohu.tv.mq.cloud.service.BrokerService; @@ -74,6 +77,7 @@ public void run() { } private int fetchAndSaveStoreStat(List brokerList) { + int size = 0; List brokerStoreStatList = new ArrayList(brokerList.size()); for (Broker broker : brokerList) { // 非master跳过 @@ -102,11 +106,18 @@ private int fetchAndSaveStoreStat(List brokerList) { brokerStoreStat.setClusterId(cluster.getId()); // 数据存储 brokerStoreStatService.save(brokerStoreStat); + ++size; + if (brokerStoreStat.getMax() < 500 && brokerStoreStat.getPercent99() < 400) { + continue; + } + brokerStoreStat.setClusterName(clusterService.getMQClusterById(brokerStoreStat.getClusterId()).getName()); + brokerStoreStat.setBrokerStoreLink(mqCloudConfigHelper.getBrokerStoreLink(brokerStoreStat.getClusterId(), + brokerStoreStat.getBrokerIp())); brokerStoreStatList.add(brokerStoreStat); } // 预警 warn(brokerStoreStatList); - return brokerStoreStatList.size(); + return size; } /** @@ -115,43 +126,12 @@ private int fetchAndSaveStoreStat(List brokerList) { * @param brokerStoreStatList */ public void warn(List brokerStoreStatList) { - StringBuilder content = new StringBuilder(); - for (BrokerStoreStat brokerStoreStat : brokerStoreStatList) { - if (brokerStoreStat.getMax() < 500 && brokerStoreStat.getPercent99() < 400) { - continue; - } - content.append(""); - content.append(""); - content.append(clusterService.getMQClusterById(brokerStoreStat.getClusterId()).getName()); - content.append(""); - content.append(""); - content.append(mqCloudConfigHelper.getBrokerStoreLink(brokerStoreStat.getClusterId(), - brokerStoreStat.getBrokerIp())); - content.append(""); - content.append(""); - content.append(brokerStoreStat.getAvg()); - content.append("ms"); - content.append(""); - content.append(brokerStoreStat.getPercent90()); - content.append("ms"); - content.append(""); - content.append(brokerStoreStat.getPercent99()); - content.append("ms"); - content.append(""); - content.append(brokerStoreStat.getMax()); - content.append("ms"); - content.append(""); - content.append(brokerStoreStat.getCount()); - content.append(""); - content.append(""); - } - if (content.length() <= 0) { + if (brokerStoreStatList.size() == 0) { return; } - String header = "" - + ""; - String footer = "
集群brokeravg90%99%max写入量
"; - alertService.sendWarnMail(null, "BrokerStore", header + content.toString() + footer); + Map paramMap = new HashMap<>(); + paramMap.put("list", brokerStoreStatList); + alertService.sendWarn(null, WarnType.BROKER_STORE_SLOW, paramMap); } /** diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ClusterMonitorTask.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ClusterMonitorTask.java index dbdc1ebe..bafaaf70 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ClusterMonitorTask.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ClusterMonitorTask.java @@ -17,7 +17,10 @@ import com.sohu.tv.mq.cloud.bo.Broker; import com.sohu.tv.mq.cloud.bo.CheckStatusEnum; import com.sohu.tv.mq.cloud.bo.Cluster; +import com.sohu.tv.mq.cloud.bo.ClusterStat; import com.sohu.tv.mq.cloud.bo.NameServer; +import com.sohu.tv.mq.cloud.bo.SlaveFallBehind; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; import com.sohu.tv.mq.cloud.mq.MQAdminCallback; import com.sohu.tv.mq.cloud.mq.MQAdminTemplate; import com.sohu.tv.mq.cloud.service.AlertService; @@ -26,7 +29,6 @@ import com.sohu.tv.mq.cloud.service.NameServerService; import com.sohu.tv.mq.cloud.util.MQCloudConfigHelper; import com.sohu.tv.mq.cloud.util.Result; -import com.sohu.tv.mq.cloud.util.WebUtil; import net.javacrumbs.shedlock.core.SchedulerLock; @@ -40,10 +42,6 @@ public class ClusterMonitorTask { private final Logger logger = LoggerFactory.getLogger(this.getClass()); - private static final String NS_ERROR_TITLE = "NameServer"; - - private static final String BROKER_ERROR_TITLE = "Broker"; - @Autowired private MQAdminTemplate mqAdminTemplate; @@ -58,7 +56,7 @@ public class ClusterMonitorTask { @Autowired private BrokerService brokerService; - + @Autowired private MQCloudConfigHelper mqCloudConfigHelper; @@ -68,25 +66,20 @@ public class ClusterMonitorTask { @Scheduled(cron = "45 */6 * * * *") @SchedulerLock(name = "nameServerMonitor", lockAtMostFor = 345000, lockAtLeastFor = 345000) public void nameServerMonitor() { - if(clusterService.getAllMQCluster() == null) { + if (clusterService.getAllMQCluster() == null) { logger.warn("nameServerMonitor mqcluster is null"); return; } logger.info("monitor NameServer start"); long start = System.currentTimeMillis(); - // 缓存报警信息 - Map> alarmMap = new HashMap>(); + List clusterStatList = new ArrayList<>(); for (Cluster mqCluster : clusterService.getAllMQCluster()) { - List alarmList = alarmMap.get(mqCluster); - if (alarmList == null) { - alarmList = new ArrayList(); - alarmMap.put(mqCluster, alarmList); + ClusterStat clusterStat = monitorNameServer(mqCluster); + if (clusterStat != null) { + clusterStatList.add(clusterStat); } - monitorNameServer(mqCluster, alarmList); - } - if (!alarmMap.isEmpty()) { - handleAlarmMessage(alarmMap, 0, NS_ERROR_TITLE); } + handleAlarmMessage(clusterStatList, WarnType.NAMESERVER_ERROR); logger.info("monitor NameServer end! use:{}ms", System.currentTimeMillis() - start); } @@ -96,30 +89,30 @@ public void nameServerMonitor() { @Scheduled(cron = "50 */5 * * * *") @SchedulerLock(name = "brokerMonitor", lockAtMostFor = 240000, lockAtLeastFor = 240000) public void brokerMonitor() { - if(clusterService.getAllMQCluster() == null) { + if (clusterService.getAllMQCluster() == null) { logger.warn("brokerMonitor mqcluster is null"); return; } logger.info("monitor broker start"); long start = System.currentTimeMillis(); - // 缓存报警信息 - Map> alarmMap = new HashMap>(); // 缓存broker状态信息 Map> clusterMap = new HashMap<>(); + List clusterStatList = new ArrayList<>(); for (Cluster mqCluster : clusterService.getAllMQCluster()) { - List alarmList = alarmMap.get(mqCluster); - if (alarmList == null) { - alarmList = new ArrayList(); - alarmMap.put(mqCluster, alarmList); + Result> brokerListResult = brokerService.query(mqCluster.getId()); + if (brokerListResult.isEmpty()) { + continue; + } + List brokerList = brokerListResult.getResult(); + ClusterStat clusterStat = monitorBroker(mqCluster, brokerList); + if (clusterStat != null) { + clusterStatList.add(clusterStat); } - List brokerList = monitorBroker(mqCluster, alarmList); if (brokerList != null && !brokerList.isEmpty()) { clusterMap.put(mqCluster, brokerList); } } - if (!alarmMap.isEmpty()) { - handleAlarmMessage(alarmMap, 1, BROKER_ERROR_TITLE); - } + handleAlarmMessage(clusterStatList, WarnType.BROKER_ERROR); // broker偏移量预警 brokerFallBehindWarn(clusterMap); logger.info("monitor broker end! use:{}ms", System.currentTimeMillis() - start); @@ -130,15 +123,16 @@ public void brokerMonitor() { * * @param mqCluster */ - private void monitorNameServer(Cluster mqCluster, List alarmList) { + private ClusterStat monitorNameServer(Cluster mqCluster) { Result> nameServerListResult = nameServerService.query(mqCluster.getId()); if (nameServerListResult.isEmpty()) { - return; + return null; } List nameServerAddressList = new ArrayList(); for (NameServer ns : nameServerListResult.getResult()) { nameServerAddressList.add(ns.getAddr()); } + List statList = new ArrayList<>(); mqAdminTemplate.execute(new MQAdminCallback() { public Void callback(MQAdminExt mqAdmin) throws Exception { for (String addr : nameServerAddressList) { @@ -147,10 +141,10 @@ public Void callback(MQAdminExt mqAdmin) throws Exception { nameServerService.update(mqCluster.getId(), addr, CheckStatusEnum.OK); } catch (Exception e) { nameServerService.update(mqCluster.getId(), addr, CheckStatusEnum.FAIL); - alarmList.add("ns:" + addr + ";Exception: " + e.getMessage()); + statList.add("ns:" + addr + ";Exception: " + e.getMessage()); } } - + return null; } @@ -160,10 +154,18 @@ public Cluster mqCluster() { @Override public Void exception(Exception e) throws Exception { - alarmList.add("Exception: " + e.getMessage()); + statList.add("Exception: " + e.getMessage()); return null; } }); + if (statList.size() == 0) { + return null; + } + ClusterStat clusterStat = new ClusterStat(); + clusterStat.setClusterLink(mqCloudConfigHelper.getNameServerMonitorLink(mqCluster.getId())); + clusterStat.setClusterName(mqCluster.getName()); + clusterStat.setStats(statList); + return clusterStat; } /** @@ -171,12 +173,8 @@ public Void exception(Exception e) throws Exception { * * @param mqCluster */ - private List monitorBroker(Cluster mqCluster, List alarmList) { - Result> brokerListResult = brokerService.query(mqCluster.getId()); - if (brokerListResult.isEmpty()) { - return null; - } - List brokerList = brokerListResult.getResult(); + private ClusterStat monitorBroker(Cluster mqCluster, List brokerList) { + List statList = new ArrayList<>(); mqAdminTemplate.execute(new MQAdminCallback() { public Void callback(MQAdminExt mqAdmin) throws Exception { for (Broker broker : brokerList) { @@ -186,7 +184,7 @@ public Void callback(MQAdminExt mqAdmin) throws Exception { brokerService.update(mqCluster.getId(), broker.getAddr(), CheckStatusEnum.OK); } catch (Exception e) { brokerService.update(mqCluster.getId(), broker.getAddr(), CheckStatusEnum.FAIL); - alarmList.add("bk:" + broker.getAddr() + ";Exception: " + e.getMessage()); + statList.add("bk:" + broker.getAddr() + ";Exception: " + e.getMessage()); } } return null; @@ -198,11 +196,18 @@ public Cluster mqCluster() { @Override public Void exception(Exception e) throws Exception { - alarmList.add("Exception: "+ e.getMessage()); + statList.add("Exception: " + e.getMessage()); return null; } }); - return brokerList; + if (statList.size() == 0) { + return null; + } + ClusterStat clusterStat = new ClusterStat(); + clusterStat.setClusterLink(mqCloudConfigHelper.getBrokerMonitorLink(mqCluster.getId())); + clusterStat.setClusterName(mqCluster.getName()); + clusterStat.setStats(statList); + return clusterStat; } /** @@ -212,68 +217,16 @@ public Void exception(Exception e) throws Exception { * @param type * @param alarmTitle */ - private void handleAlarmMessage(Map> alarmMap, int type, String alarmTitle) { - if (alarmMap.isEmpty()) { + private void handleAlarmMessage(List clusterStatList, WarnType warnType) { + if (clusterStatList.isEmpty()) { return; } - // 是否报警 - boolean flag = false; - StringBuilder smsBuilder = new StringBuilder(); - StringBuilder content = new StringBuilder(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - - for (Cluster cluster : alarmMap.keySet()) { - List alarmList = alarmMap.get(cluster); - if (alarmList.isEmpty()) { - continue; - } - flag = true; - content.append(""); - content.append(""); - for (int i = 0; i < alarmList.size(); i++) { - if (i > 0) { - content.append(""); - smsBuilder.append(","); - } - String str = alarmList.get(i); - content.append(""); - smsBuilder.append(str.split(";")[0]); - if (i > 0) { - content.append(""); - } - } - smsBuilder.append(";"); - content.append(""); - } - content.append(""); - content.append("
"); - content.append("集群"); - content.append(""); - content.append("异常信息"); - content.append("
"); - content.append("" + cluster.getName() + ""); - content.append("
" + str + "
"); - if (flag) { - sendAlertMessage(alarmTitle, content.toString()); - alertService.sendPhone(alarmTitle, smsBuilder.toString()); - } + // 发送并保持邮件预警 + Map paramMap = new HashMap<>(); + paramMap.put("list", clusterStatList); + alertService.sendWarn(null, warnType, paramMap); } - + /** * broker最大offset预警 * @@ -283,7 +236,7 @@ private void brokerFallBehindWarn(Map> clusterMap) { if (clusterMap.isEmpty()) { return; } - StringBuilder warnMessage = new StringBuilder(); + List list = new ArrayList<>(); for (Cluster cluster : clusterMap.keySet()) { List brokerList = clusterMap.get(cluster); for (Broker broker : brokerList) { @@ -301,55 +254,23 @@ private void brokerFallBehindWarn(Map> clusterMap) { if (fallBehindOffset <= mqCloudConfigHelper.getSlaveFallBehindSize()) { continue; } - warnMessage.append(""); - // 集群 - warnMessage.append(""); - warnMessage.append(cluster.getName()); - warnMessage.append(""); - // broker - warnMessage.append(""); - warnMessage.append(mqCloudConfigHelper.getHrefLink( + SlaveFallBehind slaveFallBehind = new SlaveFallBehind(); + slaveFallBehind.setClusterName(cluster.getName()); + slaveFallBehind.setBrokerLink(mqCloudConfigHelper.getHrefLink( mqCloudConfigHelper.getBrokerMonitorLink(cluster.getId()), broker.getBrokerName())); - warnMessage.append(""); - // 偏移量 - warnMessage.append(""); - warnMessage.append(WebUtil.sizeFormat(fallBehindOffset)); - warnMessage.append(""); - - // 目标阈值 - warnMessage.append(""); - warnMessage.append(WebUtil.sizeFormat(mqCloudConfigHelper.getSlaveFallBehindSize())); - warnMessage.append(""); - - warnMessage.append(""); + slaveFallBehind.setFallBehindOffset(fallBehindOffset); + slaveFallBehind.setSlaveFallBehindSize(mqCloudConfigHelper.getSlaveFallBehindSize()); + list.add(slaveFallBehind); } } - if (warnMessage.length() <= 0) { + if (list.size() <= 0) { return; } - StringBuilder tableBuilder = new StringBuilder(""); - tableBuilder.append(""); - tableBuilder.append(""); - tableBuilder.append(""); - tableBuilder.append(""); - tableBuilder.append(""); - tableBuilder.append(""); - tableBuilder.append(""); - tableBuilder.append(""); - tableBuilder.append(""); - tableBuilder.append(warnMessage); - tableBuilder.append(""); - sendAlertMessage("slave同步落后", tableBuilder.toString()); + Map paramMap = new HashMap<>(); + paramMap.put("list", list); + alertService.sendWarn(null, WarnType.SLAVE_FALL_BEHIND, paramMap); } - + private Broker findSlave(Broker master, List brokerList) { for (Broker broker : brokerList) { if (broker.isMaster()) { @@ -361,15 +282,4 @@ private Broker findSlave(Broker master, List brokerList) { } return null; } - - /** - * 发送报警邮件 - * - * @param titile - * @param content - */ - private void sendAlertMessage(String title, String content) { - alertService.sendWarnMail(null, title, content); - logger.error(title + content); - } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ConsumeFallBehindTask.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ConsumeFallBehindTask.java index caeacb17..e6c3818d 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ConsumeFallBehindTask.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ConsumeFallBehindTask.java @@ -1,8 +1,10 @@ package com.sohu.tv.mq.cloud.task; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.Pair; @@ -12,7 +14,10 @@ import org.springframework.scheduling.annotation.Scheduled; import com.sohu.tv.mq.cloud.bo.Broker; +import com.sohu.tv.mq.cloud.bo.BrokerFallBehind; +import com.sohu.tv.mq.cloud.bo.BrokerFallBehind.ConsumerFallBehind; import com.sohu.tv.mq.cloud.bo.Cluster; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; import com.sohu.tv.mq.cloud.common.model.BrokerMomentStatsData; import com.sohu.tv.mq.cloud.common.model.BrokerMomentStatsItem; import com.sohu.tv.mq.cloud.service.AlarmConfigBridingService; @@ -51,7 +56,7 @@ public class ConsumeFallBehindTask { @Autowired private MQCloudConfigHelper mqCloudConfigHelper; - + @Autowired private AlarmConfigBridingService alarmConfigBridingService; @@ -121,31 +126,20 @@ private int detectFallBehindTooMany(List brokerList) { * @param fallBehindList */ private void warn(List> fallBehindList) { - // 过滤掉内置consumer + List brokerFallBehindList = new ArrayList<>(); for (Pair pair : fallBehindList) { - BrokerMomentStatsData brokerMomentStatsData = pair.getObject2(); - List list = brokerMomentStatsData.getBrokerMomentStatsItemList(); - Iterator brokerMomentStatsItemIterator = list.iterator(); - while (brokerMomentStatsItemIterator.hasNext()) { - BrokerMomentStatsItem brokerMomentStatsItem = brokerMomentStatsItemIterator.next(); - String[] split = brokerMomentStatsItem.getKey().split("@"); - // 内置consumer不检测 - if (MixAll.TOOLS_CONSUMER_GROUP.equals(split[2])) { - brokerMomentStatsItemIterator.remove(); - } - } - } - // 预警信息拼装 - StringBuilder content = new StringBuilder(); - for (Pair pair : fallBehindList) { - Broker broker = pair.getObject1(); BrokerMomentStatsData brokerMomentStatsData = pair.getObject2(); List list = brokerMomentStatsData.getBrokerMomentStatsItemList(); Iterator iterator = list.iterator(); - // 过滤掉超频的消费者 while (iterator.hasNext()) { BrokerMomentStatsItem brokerMomentStatsItem = iterator.next(); String[] split = brokerMomentStatsItem.getKey().split("@"); + // 内置consumer不检测 + if (MixAll.TOOLS_CONSUMER_GROUP.equals(split[2])) { + iterator.remove(); + continue; + } + // 过滤掉超频的消费者 if (!alarmConfigBridingService.needWarn("consumerFallBehind", split[1], split[2])) { iterator.remove(); } @@ -153,64 +147,32 @@ private void warn(List> fallBehindList) { if (list.size() <= 0) { continue; } - content.append(""); - content.append(""); - content.append(""); - content.append(""); + Broker broker = pair.getObject1(); + BrokerFallBehind brokerFallBehind = new BrokerFallBehind(); + brokerFallBehind.setBroker(broker.getBrokerName()); + brokerFallBehind.setAddr(broker.getAddr()); + brokerFallBehind.setMaxAccessMessageInMemory(brokerMomentStatsData.getMaxAccessMessageInMemory()); + brokerFallBehindList.add(brokerFallBehind); + List consumerFallBehindList = new ArrayList<>(); for (int i = 0; i < list.size(); ++i) { BrokerMomentStatsItem brokerMomentStatsItem = list.get(i); String[] split = brokerMomentStatsItem.getKey().split("@"); - if (i != 0) { - content.append(""); - } - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); + ConsumerFallBehind consumerFallBehind = brokerFallBehind.new ConsumerFallBehind(); + consumerFallBehind.setQueue(split[0]); + consumerFallBehind.setTopic(split[1]); + consumerFallBehind.setConsumer(split[2]); + consumerFallBehind.setAccumulated(brokerMomentStatsItem.getValue()); + consumerFallBehind.setConsumerLink(mqCloudConfigHelper.getTopicConsumeLink(split[1], split[2])); + consumerFallBehindList.add(consumerFallBehind); } + brokerFallBehind.setList(consumerFallBehindList); } - if (content.length() <= 0) { + if (brokerFallBehindList.size() == 0) { return; } - String header = "
"); - tableBuilder.append("集群"); - tableBuilder.append(""); - tableBuilder.append("broker"); - tableBuilder.append(""); - tableBuilder.append("落后量"); - tableBuilder.append(""); - tableBuilder.append("阈值"); - tableBuilder.append("
"); - content.append(broker.getBrokerName()); - content.append(""); - content.append(broker.getAddr()); - content.append(""); - content.append(parseToG(brokerMomentStatsData.getMaxAccessMessageInMemory())); - content.append("
"); - content.append(split[1]); - content.append(""); - content.append(split[0]); - content.append(""); - content.append(mqCloudConfigHelper.getTopicConsumeLink(split[1], split[2])); - content.append(""); - content.append(parse(brokerMomentStatsItem.getValue())); - content.append("
" - + ""; - String footer = "
broker地址broker内存阈值topic队列consumer落后大小
"; - alertService.sendWarnMail(null, "消费落后", header + content.toString() + footer); - } - - private String parseToG(long bytes) { - return format((double) bytes / (1024L * 1024L * 1024L)) + "G"; - } - - private String parse(long bytes) { - if (bytes < 1024) { - return bytes + "B"; - } - if (bytes < 1024L * 1024L) { - return format((double) bytes / 1024L) + "K"; - } - if (bytes < 1024L * 1024L * 1024L) { - return format((double) bytes / (1024L * 1024L)) + "M"; - } - return parseToG(bytes); - } - - private float format(double v) { - return (int)(v * 100) / 100.0f; + // 发送并保持邮件预警 + Map paramMap = new HashMap<>(); + paramMap.put("list", brokerFallBehindList); + alertService.sendWarn(null, WarnType.CONSUME_FALL_BEHIND, paramMap); } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ProducerStatsTask.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ProducerStatsTask.java index 097ed7b6..86b9e59e 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ProducerStatsTask.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ProducerStatsTask.java @@ -2,8 +2,8 @@ import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -13,9 +13,11 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; + import com.sohu.tv.mq.cloud.bo.ProducerTotalStat; import com.sohu.tv.mq.cloud.bo.User; import com.sohu.tv.mq.cloud.bo.UserProducer; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; import com.sohu.tv.mq.cloud.service.AlertService; import com.sohu.tv.mq.cloud.service.ProducerStatService; import com.sohu.tv.mq.cloud.service.ProducerTotalStatService; @@ -24,6 +26,7 @@ import com.sohu.tv.mq.cloud.util.DateUtil; import com.sohu.tv.mq.cloud.util.MQCloudConfigHelper; import com.sohu.tv.mq.cloud.util.Result; + import net.javacrumbs.shedlock.core.SchedulerLock; /** @@ -36,8 +39,6 @@ public class ProducerStatsTask { private final Logger logger = LoggerFactory.getLogger(this.getClass()); - private static final String ALARM_TITLE = "客户端异常"; - @Autowired private ProducerTotalStatService producerTotalStatService; @@ -58,7 +59,7 @@ public class ProducerStatsTask { @Autowired private UserService userService; - + /** * 删除统计表数据 */ @@ -119,15 +120,19 @@ protected void producerExcetpion(int dt, String time) { size = list.size(); // 按生产者分组 Map> groupedMap = group(list); - for (String k : groupedMap.keySet()) { - List totalList = groupedMap.get(k); + for (String producer : groupedMap.keySet()) { + List totalList = groupedMap.get(producer); // 获取发送者列表 - List userProducerList = getUserProducer(k); + List userProducerList = getUserProducer(producer); if (userProducerList != null) { - String detailAlarmMessage = getDetailAlarmMessage(totalList, userProducerList.get(0).getTid(), k); - alertService.sendWarnMail(getUserMail(userProducerList), ALARM_TITLE, detailAlarmMessage); + List users = getWarnUser(userProducerList); + Map paramMap = new HashMap<>(); + paramMap.put("link", mqCloudConfigHelper.getTopicLink(userProducerList.get(0).getTid(), producer)); + paramMap.put("list", totalList); + paramMap.put("resource", producer); + alertService.sendWarn(users, WarnType.PRODUCE_EXCEPTION, paramMap); } else { - logger.warn("can not get the relationship between user and producer!producer:{}", k); + logger.warn("can not get the relationship between user and producer!producer:{}", producer); } } } @@ -135,57 +140,6 @@ protected void producerExcetpion(int dt, String time) { (System.currentTimeMillis() - start)); } - /** - * 拼接报警信息详情 - * - * @param totalList - * @param tid - * @param k - */ - private String getDetailAlarmMessage(List totalList, long tid, String k) { - int rowSpan = totalList.size(); - StringBuilder sb = new StringBuilder(); - sb.append(""); - sb.append(""); - sb.append(""); - sb.append(""); - // producer - sb.append(""); - for (int i = 0; i < rowSpan; ++i) { - ProducerTotalStat producerTotalStat = totalList.get(i); - if (i > 0) { - sb.append(""); - } - // 时间 - sb.append(""); - // client - sb.append(""); - // broker - sb.append(""); - // 异常信息 - sb.append(""); - if (i > 0) { - sb.append(""); - } - } - sb.append(""); - sb.append(""); - sb.append("
producer时间clientbroker异常
"); - sb.append(mqCloudConfigHelper.getTopicLink(tid, k)); - sb.append("
"); - sb.append(producerTotalStat.getCreateDate() + " " + producerTotalStat.getCreateTime()); - sb.append(""); - sb.append(producerTotalStat.getClient()); - sb.append(""); - sb.append(producerTotalStat.getBroker()); - sb.append(""); - if(producerTotalStat.getException() != null) { - sb.append(producerTotalStat.getException()); - } - sb.append("
"); - return sb.toString(); - } - /** * 获取topic id * @@ -206,7 +160,7 @@ private List getUserProducer(String producer) { * @param userIdSet * @return */ - private String getUserMail(List userProducerList) { + private List getWarnUser(List userProducerList) { if (userProducerList == null) { return null; } @@ -214,24 +168,15 @@ private String getUserMail(List userProducerList) { for (UserProducer userProducer : userProducerList) { userIDSet.add(userProducer.getUid()); } - String receiver = null; // 获取用户id if (!userIDSet.isEmpty()) { // 获取用户信息 Result> userListResult = userService.query(userIDSet); - StringBuilder sb = new StringBuilder(); if (userListResult.isNotEmpty()) { - for (User u : userListResult.getResult()) { - sb.append(u.getEmail()); - sb.append(","); - } - } - if (sb.length() > 0) { - sb.deleteCharAt(sb.length() - 1); - receiver = sb.toString(); + return userListResult.getResult(); } } - return receiver; + return null; } /** diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ServerWarningTask.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ServerWarningTask.java index eec808e6..a391d901 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ServerWarningTask.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/ServerWarningTask.java @@ -16,12 +16,16 @@ import com.sohu.tv.mq.cloud.bo.ServerAlarmConfig; import com.sohu.tv.mq.cloud.bo.ServerInfo; import com.sohu.tv.mq.cloud.bo.ServerInfoExt; +import com.sohu.tv.mq.cloud.bo.ServerWarn; +import com.sohu.tv.mq.cloud.bo.ServerWarn.ServerWarnItem; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; import com.sohu.tv.mq.cloud.service.AlertService; import com.sohu.tv.mq.cloud.service.ServerAlarmConfigService; import com.sohu.tv.mq.cloud.service.ServerDataService; import com.sohu.tv.mq.cloud.util.DateUtil; import com.sohu.tv.mq.cloud.util.MQCloudConfigHelper; import com.sohu.tv.mq.cloud.util.Result; + import net.javacrumbs.shedlock.core.SchedulerLock; /** @@ -34,10 +38,6 @@ public class ServerWarningTask { private static final Logger logger = LoggerFactory.getLogger(ServerWarningTask.class); - private static final String ALARM_TITLE = "MQCloud:服务器预警"; - - private static String ALARM_MESSAGE_FORMAT = "%s;%s;%s"; - private static String SUFFIX = "%"; private static DecimalFormat decimalFormat = new DecimalFormat(".0"); @@ -66,28 +66,29 @@ public void serverMachineMonitor() { String fetchDataTime = ""; // 获取当前服务器状态 List serverStatusList = serverDataService.queryAllServer(DateUtil.formatYMDNow()); - // 临时缓存报警信息 - Map> alarmMap = new HashMap>(); + List serverWarnList = new ArrayList<>(); for (ServerInfoExt serverInfoExt : serverStatusList) { if (!isWarn(serverInfoExt, configMap)) { continue; } - List alarmList = alarmMap.get(serverInfoExt.getIp()); - if (alarmList == null) { - alarmList = new ArrayList(); - alarmMap.put(serverInfoExt.getIp(), alarmList); - } + List alarmList = new ArrayList<>(); // 处理状态信息 handleMachineStatus(serverInfoExt, configMap.get(serverInfoExt.getIp()), alarmList); + if (alarmList.size() == 0) { + continue; + } // 记录数据采集的时间 if (fetchDataTime == "" && serverInfoExt.getCdate() != null && serverInfoExt.getCtime() != null) { fetchDataTime = formatTime(serverInfoExt.getCdate(), serverInfoExt.getCtime()); } + ServerWarn serverWarn = new ServerWarn(); + serverWarn.setIp(serverInfoExt.getIp()); + serverWarn.setIpLink(mqCloudConfigHelper.getServerLink(serverWarn.getIp())); + serverWarn.setList(alarmList); + serverWarnList.add(serverWarn); } // 处理报警信息 - if (!alarmMap.isEmpty()) { - handleAlarmMessage(alarmMap, fetchDataTime); - } + handleAlarmMessage(serverWarnList, fetchDataTime); logger.info("monitorMachineStatus end! use:{}ms", System.currentTimeMillis() - start); } @@ -162,7 +163,7 @@ private String formatTime(String date, String time) { * @param alarmList */ private void handleMachineStatus(ServerInfoExt serverInfoExt, ServerAlarmConfig alarmConfig, - List alarmList) { + List alarmList) { // 内存 memoryUsage(serverInfoExt, alarmConfig.getMemoryUsageRate(), alarmList); // cpu负载 @@ -185,13 +186,12 @@ private void handleMachineStatus(ServerInfoExt serverInfoExt, ServerAlarmConfig * @param rate * @param alarmList */ - private void memoryUsage(ServerInfoExt serverInfoExt, int rate, List alarmList) { + private void memoryUsage(ServerInfoExt serverInfoExt, int rate, List alarmList) { float total = serverInfoExt.getMtotal(); float free = serverInfoExt.getMfree(); float usageRate = (total - free) / total * 100; if (rate > 0 && usageRate > rate) { - alarmList.add(String.format(ALARM_MESSAGE_FORMAT, "内存使用率", decimalFormat.format(usageRate) + SUFFIX, - rate + SUFFIX)); + alarmList.add(new ServerWarnItem("内存使用率", decimalFormat.format(usageRate) + SUFFIX, rate + SUFFIX)); } } @@ -202,9 +202,10 @@ private void memoryUsage(ServerInfoExt serverInfoExt, int rate, List ala * @param standard * @param alarmList */ - private void oneMinuteLoad(ServerInfoExt serverInfoExt, int standard, List alarmList) { + private void oneMinuteLoad(ServerInfoExt serverInfoExt, int standard, List alarmList) { if (standard > 0 && serverInfoExt.getCload1() > standard) { - alarmList.add(String.format(ALARM_MESSAGE_FORMAT, "CPU负载(一分钟)", serverInfoExt.getCload1(), standard)); + alarmList.add(new ServerWarnItem("CPU负载(一分钟)", String.valueOf(serverInfoExt.getCload1()), + String.valueOf(standard))); } } @@ -216,14 +217,16 @@ private void oneMinuteLoad(ServerInfoExt serverInfoExt, int standard, List alarmList) { + private void tcpStatus(ServerInfoExt serverInfoExt, int connect, int wait, List alarmList) { // 连接数 if (connect > 0 && serverInfoExt.getTuse() > connect) { - alarmList.add(String.format(ALARM_MESSAGE_FORMAT, "tcp连接数", serverInfoExt.getTuse(), connect)); + alarmList.add(new ServerWarnItem("tcp连接数", String.valueOf(serverInfoExt.getTuse()), + String.valueOf(connect))); } // 等待数 if (wait > 0 && serverInfoExt.getTwait() > wait) { - alarmList.add(String.format(ALARM_MESSAGE_FORMAT, "tcp等待数", serverInfoExt.getTwait(), wait)); + alarmList.add(new ServerWarnItem("tcp等待数", String.valueOf(serverInfoExt.getTwait()), + String.valueOf(wait))); } } @@ -235,15 +238,15 @@ private void tcpStatus(ServerInfoExt serverInfoExt, int connect, int wait, List< * @param iobusy * @param alarmList */ - private void ioStatus(ServerInfoExt serverInfoExt, int iops, int iobusy, int ioUsageRate, List alarmList) { + private void ioStatus(ServerInfoExt serverInfoExt, int iops, int iobusy, int ioUsageRate, + List alarmList) { // 磁盘io速率 交互次数/s if (iops > 0 && serverInfoExt.getDiops() > iops) { - alarmList.add(String.format(ALARM_MESSAGE_FORMAT, "iops", serverInfoExt.getDiops(), iops)); + alarmList.add(new ServerWarnItem("iops", String.valueOf(serverInfoExt.getDiops()), String.valueOf(iops))); } // 磁盘io带宽使用百分比 if (iobusy > 0 && serverInfoExt.getDbusy() > iobusy) { - alarmList.add( - String.format(ALARM_MESSAGE_FORMAT, "iobusy", serverInfoExt.getDbusy() + SUFFIX, iobusy + SUFFIX)); + alarmList.add(new ServerWarnItem("iobusy", serverInfoExt.getDbusy() + SUFFIX, iobusy + SUFFIX)); } // 磁盘各分区使用率 String[] dspace = serverInfoExt.getDspace() == null ? new String[0] : serverInfoExt.getDspace().split(","); @@ -255,9 +258,7 @@ private void ioStatus(ServerInfoExt serverInfoExt, int iops, int iobusy, int ioU } set.add(ioUsage[0]); if (ioUsageRate > 0 && Float.parseFloat(ioUsage[1]) > ioUsageRate) { - alarmList.add( - String.format(ALARM_MESSAGE_FORMAT, "磁盘使用率-分区:" + ioUsage[0], ioUsage[1] + SUFFIX, - ioUsageRate + SUFFIX)); + alarmList.add(new ServerWarnItem("磁盘使用率-分区:" + ioUsage[0], ioUsage[1] + SUFFIX, ioUsageRate + SUFFIX)); } } @@ -270,11 +271,10 @@ private void ioStatus(ServerInfoExt serverInfoExt, int iops, int iobusy, int ioU * @param threshold * @param alarmList */ - private void cpuUsage(ServerInfoExt serverInfoExt, int threshold, List alarmList) { + private void cpuUsage(ServerInfoExt serverInfoExt, int threshold, List alarmList) { // cpu使用 if (threshold > 0 && serverInfoExt.getCuser() > threshold) { - alarmList.add(String.format(ALARM_MESSAGE_FORMAT, "CPU使用率", serverInfoExt.getCuser() + SUFFIX, - threshold + SUFFIX)); + alarmList.add(new ServerWarnItem("CPU使用率", serverInfoExt.getCuser() + SUFFIX, threshold + SUFFIX)); } } @@ -286,13 +286,15 @@ private void cpuUsage(ServerInfoExt serverInfoExt, int threshold, List a * @param netOut * @param alarmList */ - private void netStatus(ServerInfoExt serverInfoExt, int netIn, int netOut, List alarmList) { + private void netStatus(ServerInfoExt serverInfoExt, int netIn, int netOut, List alarmList) { // net使用 if (netIn > 0 && serverInfoExt.getNin() > netIn) { - alarmList.add(String.format(ALARM_MESSAGE_FORMAT, "入网流量(k/s)", serverInfoExt.getNin(), netIn)); + alarmList.add( + new ServerWarnItem("入网流量(k/s)", String.valueOf(serverInfoExt.getNin()), String.valueOf(netIn))); } if (netOut > 0 && serverInfoExt.getNout() > netOut) { - alarmList.add(String.format(ALARM_MESSAGE_FORMAT, "出网流量(k/s)", serverInfoExt.getNout(), netOut)); + alarmList.add( + new ServerWarnItem("出网流量(k/s)", String.valueOf(serverInfoExt.getNout()), String.valueOf(netOut))); } } @@ -301,72 +303,13 @@ private void netStatus(ServerInfoExt serverInfoExt, int netIn, int netOut, List< * * @param map */ - private void handleAlarmMessage(Map> map, String time) { - if (map.isEmpty()) { + private void handleAlarmMessage(List serverWarnList, String time) { + if (serverWarnList.isEmpty()) { return; } - // 是否报警 - boolean flag = false; - StringBuilder content = new StringBuilder(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - - for (String ip : map.keySet()) { - List alarmList = map.get(ip); - if (alarmList.isEmpty()) { - continue; - } - flag = true; - content.append(""); - content.append(""); - for (int i = 0; i < alarmList.size(); i++) { - if (i > 0) { - content.append(""); - } - // 拆分消息,拼装表格 - String[] msg = alarmList.get(i).split(";"); - content.append(""); - content.append(""); - content.append(""); - if (i > 0) { - content.append(""); - } - } - content.append(""); - } - content.append(""); - content.append("
"); - content.append("ip"); - content.append(""); - content.append("报警类别"); - content.append(""); - content.append("监控值"); - content.append(""); - content.append("阈值"); - content.append("
"); - content.append(mqCloudConfigHelper.getServerLink(ip)); - content.append("
" + msg[0] + "" + msg[1] + "" + msg[2] + "
"); - // 有报警信息则发送邮件 - if (flag) { - sendAlertMessage(content.toString(), time); - } - } - - /** - * 发送报警邮件 - * - * @param content - * @param time - */ - private void sendAlertMessage(String content, String time) { - alertService.sendMail(ALARM_TITLE, "数据采集时间:" + time + " 详细如下: " + content); - logger.info(ALARM_TITLE + content); + Map paramMap = new HashMap<>(); + paramMap.put("time", time); + paramMap.put("list", serverWarnList); + alertService.sendWarn(null, WarnType.SERVER_WARN, paramMap); } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/SohuMonitorListener.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/SohuMonitorListener.java index 982591b3..ccfcd2ad 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/SohuMonitorListener.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/SohuMonitorListener.java @@ -36,6 +36,7 @@ import com.sohu.tv.mq.cloud.bo.TypedUndoneMsgs; import com.sohu.tv.mq.cloud.bo.User; import com.sohu.tv.mq.cloud.bo.UserConsumer; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; import com.sohu.tv.mq.cloud.dao.ConsumerStatDao; import com.sohu.tv.mq.cloud.service.AlarmConfigBridingService; import com.sohu.tv.mq.cloud.service.AlertService; @@ -47,71 +48,73 @@ import com.sohu.tv.mq.cloud.util.MQCloudConfigHelper; import com.sohu.tv.mq.cloud.util.Result; import com.sohu.tv.mq.util.CommonUtil; + /** * 监控搜狐实现 + * * @author yongfeigao - * */ @Component public class SohuMonitorListener implements MonitorListener { - private final Logger log = LoggerFactory.getLogger(this.getClass()); - + private final Logger log = LoggerFactory.getLogger(this.getClass()); + @Autowired - private ConsumerStatDao consumerStatDao; - + private ConsumerStatDao consumerStatDao; + @Autowired private UserConsumerService userConsumerService; - + @Autowired private TopicService topicService; - + @Autowired private AlertService alertService; - + @Autowired private UserService userService; - + @Autowired private AlarmConfigBridingService alarmConfigBridingService; - + private long time; - + @Autowired private MQCloudConfigHelper mqCloudConfigHelper; @Autowired private ConsumerClientStatService consumerClientStatService; - - @Override - public void beginRound() { - time = System.currentTimeMillis(); - log.info("monitor begin"); - } - - @Override - public void reportUndoneMsgs(UndoneMsgs undoneMsgs) { + + @Override + public void beginRound() { + time = System.currentTimeMillis(); + log.info("monitor begin"); + } + + @Override + public void reportUndoneMsgs(UndoneMsgs undoneMsgs) { String topic = undoneMsgs.getTopic(); // 忽略topic - if(mqCloudConfigHelper.isIgnoreTopic(topic)) { + if (mqCloudConfigHelper.isIgnoreTopic(topic)) { return; } try { - //保存堆积消息的consumer的状态 - consumerStatDao.saveConsumerStat(undoneMsgs.getConsumerGroup(), topic, - (int)undoneMsgs.getUndoneMsgsTotal(), - (int)undoneMsgs.getUndoneMsgsSingleMQ(), + // 保存堆积消息的consumer的状态 + consumerStatDao.saveConsumerStat(undoneMsgs.getConsumerGroup(), topic, + (int) undoneMsgs.getUndoneMsgsTotal(), + (int) undoneMsgs.getUndoneMsgsSingleMQ(), undoneMsgs.getUndoneMsgsDelayTimeMills()); } catch (Exception e) { - log.error("save {}",undoneMsgs ,e); + log.error("save {}", undoneMsgs, e); } - veriftAccumulateAlarm(undoneMsgs); - } - - /** - * 校验是否发送报警邮件 - * @param topic - * @param undoneMsgs - */ + veriftAccumulateAlarm(undoneMsgs); + } + + /** + * 校验是否发送报警邮件 + * + * @param topic + * @param undoneMsgs + */ private void veriftAccumulateAlarm(UndoneMsgs undoneMsgs) { long accumulateTime = alarmConfigBridingService.getAccumulateTime(undoneMsgs.getConsumerGroup()); long accumulateCount = alarmConfigBridingService.getAccumulateCount(undoneMsgs.getConsumerGroup()); @@ -133,63 +136,63 @@ private void veriftAccumulateAlarm(UndoneMsgs undoneMsgs) { } } } - - /** - * 堆积报警 - * @param undoneMsgs - */ - public void accumulateWarn(UndoneMsgs undoneMsgs) { - // 验证报警频率 + + /** + * 堆积报警 + * + * @param undoneMsgs + */ + public void accumulateWarn(UndoneMsgs undoneMsgs) { + // 验证报警频率 if (!alarmConfigBridingService.needWarn("accumulate", undoneMsgs.getTopic(), undoneMsgs.getConsumerGroup())) { return; } - TopicExt topicExt = getUserEmail(undoneMsgs.getTopic(), undoneMsgs.getConsumerGroup()); - if(topicExt == null) { - return; - } - String content = getAccumulateWarnContent(topicExt.getTopic(), undoneMsgs); - alertService.sendWarnMail(topicExt.getReceiver(), "堆积", content); - } - - /** - * 获取用户邮件地址 - * @param topic - * @param userID - * @return - */ - private TopicExt getUserEmail(String topic, String consumerGroup) { - // 获取topic + List users = getUsers(undoneMsgs.getTopic(), undoneMsgs.getConsumerGroup()); + if (users == null) { + return; + } + Map paramMap = new HashMap<>(); + paramMap.put("topic", undoneMsgs.getTopic()); + paramMap.put("consumerLink", + mqCloudConfigHelper.getTopicConsumeLink(undoneMsgs.getTopic(), undoneMsgs.getConsumerGroup())); + paramMap.put("undoneMsgsTotal", undoneMsgs.getUndoneMsgsTotal()); + paramMap.put("undoneMsgsSingleMQ", undoneMsgs.getUndoneMsgsSingleMQ()); + paramMap.put("resource", undoneMsgs.getConsumerGroup()); + if (undoneMsgs.getUndoneMsgsDelayTimeMills() > 0) { + paramMap.put("undoneMsgsDelayTime", (int) (undoneMsgs.getUndoneMsgsDelayTimeMills() / 1000f)); + } + alertService.sendWarn(users, WarnType.CONSUME_UNDONE, paramMap); + } + + /** + * 获取用户 + * + * @param topic + * @param userID + * @return + */ + private List getUsers(String topic, String consumerGroup) { + // 获取topic Result topicResult = topicService.queryTopic(topic); - if(topicResult.isNotOK()) { + if (topicResult.isNotOK()) { return null; } - TopicExt topicExt = new TopicExt(); - topicExt.setTopic(topicResult.getResult()); // 获取用户 Set userID = getUserID(topicResult.getResult().getId(), consumerGroup); - String receiver = null; // 获取用户id - if(!userID.isEmpty()) { + if (!userID.isEmpty()) { // 获取用户信息 Result> userListResult = userService.query(userID); - StringBuilder sb = new StringBuilder(); - if(userListResult.isNotEmpty()) { - for(User u : userListResult.getResult()) { - sb.append(u.getEmail()); - sb.append(","); - } - } - if(sb.length() > 0) { - sb.deleteCharAt(sb.length() - 1); - receiver = sb.toString(); + if (userListResult.isNotEmpty()) { + return userListResult.getResult(); } } - topicExt.setReceiver(receiver); - return topicExt; - } - - /** + return null; + } + + /** * 获取用户ID + * * @param topic * @param consuemrGroup * @return @@ -198,78 +201,54 @@ private Set getUserID(long tid, String consuemrGroup) { // 获取用户id Set uidList = new HashSet(); Result> udListResult = userConsumerService.queryByNameAndTid(tid, consuemrGroup); - if(udListResult.isNotEmpty()) { - for(UserConsumer uc : udListResult.getResult()) { + if (udListResult.isNotEmpty()) { + for (UserConsumer uc : udListResult.getResult()) { uidList.add(uc.getUid()); - } + } } return uidList; } - - /** - * 获取堆积预警信息 - * @param topic - * @param undoneMsgs - * @return - */ - private String getAccumulateWarnContent(Topic topic, UndoneMsgs undoneMsgs) { - StringBuilder content = new StringBuilder("详细如下:

"); - content.append("topic:"); - content.append(topic.getName()); - content.append(" 的消费者:"); - content.append(mqCloudConfigHelper.getTopicConsumeLink(topic.getId(), undoneMsgs.getConsumerGroup())); - content.append(" 检测到堆积,总堆积消息量:"); - content.append(undoneMsgs.getUndoneMsgsTotal()); - content.append(",单个队列最大堆积消息量:"); - content.append(undoneMsgs.getUndoneMsgsSingleMQ()); - if (undoneMsgs.getUndoneMsgsDelayTimeMills() > 0) { - content.append(",消费滞后时间(相对于broker最新消息时间):"); - content.append(undoneMsgs.getUndoneMsgsDelayTimeMills() / 1000f); - content.append("秒"); - } - return content.toString(); + + @Override + public void reportFailedMsgs(FailedMsgs failedMsgs) { } - @Override - public void reportFailedMsgs(FailedMsgs failedMsgs) { - } - - @Override - public void reportDeleteMsgsEvent(DeleteMsgsEvent deleteMsgsEvent) { - try { - log.warn("receive offset event:{}", deleteMsgsEvent); - OffsetMovedEvent event = deleteMsgsEvent.getOffsetMovedEvent(); - String consumerGroup = event.getConsumerGroup(); - if(MixAll.TOOLS_CONSUMER_GROUP.equals(consumerGroup)) { - return; - } - // 保存consume状态 - ConsumerStat consumerStat = new ConsumerStat(); + @Override + public void reportDeleteMsgsEvent(DeleteMsgsEvent deleteMsgsEvent) { + try { + log.warn("receive offset event:{}", deleteMsgsEvent); + OffsetMovedEvent event = deleteMsgsEvent.getOffsetMovedEvent(); + String consumerGroup = event.getConsumerGroup(); + if (MixAll.TOOLS_CONSUMER_GROUP.equals(consumerGroup)) { + return; + } + // 保存consume状态 + ConsumerStat consumerStat = new ConsumerStat(); consumerStat.setConsumerGroup(consumerGroup); consumerStat.setTopic(event.getMessageQueue().getTopic()); consumerStatDao.saveSimpleConsumerStat(consumerStat); int id = consumerStat.getId(); - + // 保存block状态 - long time = deleteMsgsEvent.getEventTimestamp(); - String broker = event.getMessageQueue().getBrokerName(); - int qid = event.getMessageQueue().getQueueId(); - consumerStatDao.saveSomeConsumerBlock(id, broker, qid, time); - - // 预警 + long time = deleteMsgsEvent.getEventTimestamp(); + String broker = event.getMessageQueue().getBrokerName(); + int qid = event.getMessageQueue().getQueueId(); + consumerStatDao.saveSomeConsumerBlock(id, broker, qid, time); + + // 预警 offsetMoveWarn(deleteMsgsEvent); - } catch (Exception e) { - log.error("receive offset event:{}", deleteMsgsEvent, e); - } - } - - /** + } catch (Exception e) { + log.error("receive offset event:{}", deleteMsgsEvent, e); + } + } + + /** * 偏移量预警 */ public void offsetMoveWarn(DeleteMsgsEvent deleteMsgsEvent) { OffsetMovedEvent event = deleteMsgsEvent.getOffsetMovedEvent(); - TopicExt topicExt = getUserEmail(event.getMessageQueue().getTopic(), event.getConsumerGroup()); - if(topicExt == null) { + List users = getUsers(event.getMessageQueue().getTopic(), event.getConsumerGroup()); + if (users == null) { return; } // 验证报警频率 @@ -277,85 +256,79 @@ public void offsetMoveWarn(DeleteMsgsEvent deleteMsgsEvent) { event.getConsumerGroup())) { return; } - StringBuilder content = new StringBuilder("详细如下:

"); - content.append("消费者:"); - content.append(mqCloudConfigHelper.getTopicConsumeLink(topicExt.getTopic().getId(), event.getConsumerGroup())); - content.append(" 偏移量错误,broker时间:"); - content.append(DateUtil.getFormat(DateUtil.YMD_DASH_BLANK_HMS_COLON).format( + String topic = event.getMessageQueue().getTopic(); + Map paramMap = new HashMap<>(); + paramMap.put("consumerLink", mqCloudConfigHelper.getTopicConsumeLink(topic, event.getConsumerGroup())); + paramMap.put("broker", event.getMessageQueue().getBrokerName()); + paramMap.put("topic", topic); + paramMap.put("queue", event.getMessageQueue().getQueueId()); + paramMap.put("brokerTime", DateUtil.getFormat(DateUtil.YMD_DASH_BLANK_HMS_COLON).format( new Date(deleteMsgsEvent.getEventTimestamp()))); - content.append(" ,请求偏移量:"); - content.append(event.getOffsetRequest()); - content.append(",broker偏移量:"); - content.append(event.getOffsetNew()); - content.append("。队列信息如下:
"); - content.append("broker:"); - content.append(event.getMessageQueue().getBrokerName()); - content.append(" topic:"); - content.append(event.getMessageQueue().getTopic()); - content.append(" 队列:"); - content.append(event.getMessageQueue().getQueueId()); - alertService.sendWarnMail(topicExt.getReceiver(), "偏移量错误", content.toString()); + paramMap.put("requestOffset", event.getOffsetRequest()); + paramMap.put("brokerOffset", event.getOffsetNew()); + paramMap.put("resource", event.getConsumerGroup()); + alertService.sendWarn(users, WarnType.CONSUME_OFFSET_MOVED, paramMap); } - @Override - public void reportConsumerRunningInfo( - TreeMap criTable) { - if(criTable == null || criTable.size() == 0) { - return; - } - String consumerGroup = criTable.firstEntry().getValue().getProperties().getProperty("consumerGroup"); - try { - // 分析订阅关系 - boolean result = ConsumerRunningInfo.analyzeSubscription(criTable); - if (!result) { - log.warn("ConsumerGroup: {}, Subscription different", consumerGroup); - //同一个ConsumerGroup订阅了不同的topic,进行记录 - Set set = new HashSet(); - for(ConsumerRunningInfo info : criTable.values()) { - set.addAll(info.getSubscriptionSet()); - } - StringBuilder sb = new StringBuilder(); - Set uniqSet = new HashSet(); - for(SubscriptionData s : set) { - if(CommonUtil.isRetryTopic(s.getTopic())) { - continue; - } - String tmp = s.getTopic()+":"+s.getSubString(); - if(uniqSet.add(tmp)) { - sb.append(tmp); - sb.append(";"); - } - } - String sbscription = sb.toString(); - ConsumerStat consumerStat = new ConsumerStat(); - consumerStat.setConsumerGroup(consumerGroup); - consumerStat.setSbscription(sbscription); - consumerStatDao.saveSimpleConsumerStat(consumerStat); - subscriptionWarn(consumerGroup, sbscription); - } - } catch (NumberFormatException e) { - log.warn("num parse err"); - } catch (Exception e) { - log.error("save subscription:{}", criTable, e); - } - - // 分析客户端卡主的情况 - Map> map = new HashMap>(); - for(String clientId : criTable.keySet()) { - ConsumerRunningInfo info = criTable.get(clientId); + @Override + public void reportConsumerRunningInfo( + TreeMap criTable) { + if (criTable == null || criTable.size() == 0) { + return; + } + String consumerGroup = criTable.firstEntry().getValue().getProperties().getProperty("consumerGroup"); + try { + // 分析订阅关系 + boolean result = ConsumerRunningInfo.analyzeSubscription(criTable); + if (!result) { + log.warn("ConsumerGroup: {}, Subscription different", consumerGroup); + // 同一个ConsumerGroup订阅了不同的topic,进行记录 + Set set = new HashSet(); + for (ConsumerRunningInfo info : criTable.values()) { + set.addAll(info.getSubscriptionSet()); + } + StringBuilder sb = new StringBuilder(); + Set uniqSet = new HashSet(); + for (SubscriptionData s : set) { + if (CommonUtil.isRetryTopic(s.getTopic())) { + continue; + } + String tmp = s.getTopic() + ":" + s.getSubString(); + if (uniqSet.add(tmp)) { + sb.append(tmp); + sb.append(";"); + } + } + String sbscription = sb.toString(); + ConsumerStat consumerStat = new ConsumerStat(); + consumerStat.setConsumerGroup(consumerGroup); + consumerStat.setSbscription(sbscription); + consumerStatDao.saveSimpleConsumerStat(consumerStat); + subscriptionWarn(consumerGroup, sbscription); + } + } catch (NumberFormatException e) { + log.warn("num parse err"); + } catch (Exception e) { + log.error("save subscription:{}", criTable, e); + } + + // 分析客户端卡主的情况 + Map> map = new HashMap>(); + for (String clientId : criTable.keySet()) { + ConsumerRunningInfo info = criTable.get(clientId); String property = info.getProperties().getProperty(ConsumerRunningInfo.PROP_CONSUME_TYPE); if (property == null) { property = ((ConsumeType) info.getProperties().get(ConsumerRunningInfo.PROP_CONSUME_TYPE)).name(); } // 只能分析push的情况 - if(ConsumeType.valueOf(property) != ConsumeType.CONSUME_PASSIVELY) { + if (ConsumeType.valueOf(property) != ConsumeType.CONSUME_PASSIVELY) { return; } String orderProperty = info.getProperties().getProperty(ConsumerRunningInfo.PROP_CONSUME_ORDERLY); boolean orderMsg = Boolean.parseBoolean(orderProperty); // 只分析非一致性消费 - if(orderMsg) { + if (orderMsg) { return; } @@ -373,7 +346,7 @@ public void reportConsumerRunningInfo( tc.setTopic(mq.getTopic()); tc.setConsumer(consumerGroup); List consumerBlockList = map.get(tc); - if(consumerBlockList == null) { + if (consumerBlockList == null) { consumerBlockList = new ArrayList(); map.put(tc, consumerBlockList); } @@ -385,26 +358,26 @@ public void reportConsumerRunningInfo( consumerBlockList.add(cb); } } - - if(map.size() <= 0) { - return; - } - for(TopicConsumer tc : map.keySet()) { - ConsumerStat consumerStat = new ConsumerStat(); - consumerStat.setConsumerGroup(tc.getConsumer()); - consumerStat.setTopic(tc.getTopic()); - consumerStatDao.saveSimpleConsumerStat(consumerStat); - int id = consumerStat.getId(); - List list = map.get(tc); - for(ConsumerBlock cb : list) { - consumerStatDao.saveConsumerBlock(id, cb.getInstance(), cb.getBroker(), cb.getQid(), cb.getBlockTime()); - } - } + + if (map.size() <= 0) { + return; + } + for (TopicConsumer tc : map.keySet()) { + ConsumerStat consumerStat = new ConsumerStat(); + consumerStat.setConsumerGroup(tc.getConsumer()); + consumerStat.setTopic(tc.getTopic()); + consumerStatDao.saveSimpleConsumerStat(consumerStat); + int id = consumerStat.getId(); + List list = map.get(tc); + for (ConsumerBlock cb : list) { + consumerStatDao.saveConsumerBlock(id, cb.getInstance(), cb.getBroker(), cb.getQid(), cb.getBlockTime()); + } + } // 报警 blockWarn(map); - } - - /** + } + + /** * 订阅报警 */ public void subscriptionWarn(String consumerGroup, String topics) { @@ -412,15 +385,13 @@ public void subscriptionWarn(String consumerGroup, String topics) { if (!alarmConfigBridingService.needWarn("subscribe", topics, consumerGroup)) { return; } - StringBuilder content = new StringBuilder("详细如下:

"); - content.append("消费者:"); - content.append(consumerGroup); - content.append(" 同时订阅了:"); - content.append(topics); - content.append("。"); - alertService.sendWarnMail(null, "订阅错误", content.toString()); + Map paramMap = new HashMap<>(); + paramMap.put("topics", topics); + paramMap.put("consumer", consumerGroup); + paramMap.put("resource", consumerGroup); + alertService.sendWarn(null, WarnType.CONSUME_SUBSCRIBE_ERROR, paramMap); } - + /** * 客户端阻塞预警 */ @@ -443,57 +414,32 @@ public void blockWarn(Map> map) { } // 是否报警 Iterator iterator = list.iterator(); - while(iterator.hasNext()) { + while (iterator.hasNext()) { ConsumerBlock consumerBlock = iterator.next(); - if(consumerBlock.getBlockTime() < blockTime) { + if (consumerBlock.getBlockTime() < blockTime) { iterator.remove(); } } - if(list.size() <= 0) { + if (list.size() <= 0) { continue; } - StringBuilder content = new StringBuilder("详细如下:

"); - content.append("topic: "); - content.append(tc.getTopic()); - content.append(" 的消费者:"); - content.append(mqCloudConfigHelper.getTopicConsumeLink(topicResult.getResult().getId(), tc.getConsumer())); - content.append(" 检测到阻塞:
"); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - for (ConsumerBlock cb : list) { - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); - content.append(""); + List users = getUsers(tc.getTopic(), tc.getConsumer()); + if (users == null) { + continue; } - content.append(""); - content.append("
clientIdbroker队列阻塞时间
"); - content.append(cb.getInstance()); - content.append(""); - content.append(cb.getBroker()); - content.append(""); - content.append(cb.getQid()); - content.append(""); - content.append(cb.getBlockTime() / 1000f); - content.append("秒
"); - TopicExt topicExt = getUserEmail(tc.getTopic(), tc.getConsumer()); - alertService.sendWarnMail(topicExt.getReceiver(), "客户端阻塞", content.toString()); + Map paramMap = new HashMap<>(); + paramMap.put("topic", tc.getTopic()); + paramMap.put("concumserLink", mqCloudConfigHelper.getTopicConsumeLink(topicResult.getResult().getId(), tc.getConsumer())); + paramMap.put("list", list); + paramMap.put("resource", tc.getConsumer()); + alertService.sendWarn(users, WarnType.CONSUME_BLOCK, paramMap); } } /** * 保存consumer-client信息 + * * @param consumerGroup * @param cc */ @@ -517,44 +463,32 @@ public void saveConsumerGroupClientInfo(String consumerGroup, ConsumerConnection } } - @Override - public void endRound() { - long use = System.currentTimeMillis() - time; - log.info("monitor end use:{}ms", use); - } - - private class TopicExt { - private Topic topic; - private String receiver; - public Topic getTopic() { - return topic; - } - public void setTopic(Topic topic) { - this.topic = topic; - } - public String getReceiver() { - return receiver; - } - public void setReceiver(String receiver) { - this.receiver = receiver; - } - } - - private class TopicConsumer { + @Override + public void endRound() { + long use = System.currentTimeMillis() - time; + log.info("monitor end use:{}ms", use); + } + + private class TopicConsumer { private String topic; private String consumer; + public String getTopic() { return topic; } + public void setTopic(String topic) { this.topic = topic; } + public String getConsumer() { return consumer; } + public void setConsumer(String consumer) { this.consumer = consumer; } + @Override public int hashCode() { final int prime = 31; @@ -564,6 +498,7 @@ public int hashCode() { result = prime * result + ((topic == null) ? 0 : topic.hashCode()); return result; } + @Override public boolean equals(Object obj) { if (this == obj) @@ -587,6 +522,7 @@ public boolean equals(Object obj) { return false; return true; } + private SohuMonitorListener getOuterType() { return SohuMonitorListener.this; } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/server/data/Net.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/server/data/Net.java index 3961cac0..722a468c 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/server/data/Net.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/server/data/Net.java @@ -33,7 +33,7 @@ public void parse(String line, String timeKey) throws Exception { String[] items = line.split(","); if (items[1].startsWith("Network")) { for (int i = 0; i < items.length; ++i) { - if (items[i].startsWith("eth")) { + if (items[i].startsWith("eth") || items[i].startsWith("ens")) { NetworkInterfaceCard nic = new NetworkInterfaceCard(); nic.setName(items[i]); nic.setIdx(i); diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/ConsumerController.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/ConsumerController.java index 26ad7564..31de8416 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/ConsumerController.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/ConsumerController.java @@ -797,6 +797,9 @@ public Result resetRetryOffset(UserInfo userInfo, @Valid UserConsumerParam us String message = "重试堆积跳过申请成功!请耐心等待!"; audit.setUid(userInfo.getUser().getId()); // 构造重置对象 + if (StringUtils.isEmpty(userConsumerParam.getMessageKey())) { + userConsumerParam.setMessageKey(null); + } AuditResetOffset auditResetOffset = new AuditResetOffset(); BeanUtils.copyProperties(userConsumerParam, auditResetOffset); // 保存记录 @@ -1014,6 +1017,12 @@ public String failedMetrics(UserInfo userInfo, @RequestParam("clientId") String Collections.sort(threadMetricList, (o1, o2) -> { return (int) (o1.getStartTime() - o2.getStartTime()); }); + // 过滤html标签 + threadMetricList.forEach(stackTraceMetric -> { + if (stackTraceMetric.getMessage() != null) { + stackTraceMetric.setMessage(HtmlUtils.htmlEscape(stackTraceMetric.getMessage())); + } + }); } setResult(map, result); return view; diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/IntroController.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/IntroController.java deleted file mode 100644 index 9b7885bc..00000000 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/IntroController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.sohu.tv.mq.cloud.web.controller; - -import java.util.Map; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -import com.sohu.tv.mq.cloud.util.MQCloudConfigHelper; -import com.sohu.tv.mq.util.Version; - -/** - * 首页 - * - * @Description: - * @author yongfeigao - * @date 2018年7月5日 - */ -@Controller -@RequestMapping("/intro") -public class IntroController extends ViewController { - - @Autowired - private MQCloudConfigHelper mqCloudConfigHelper; - - @RequestMapping - public String index(Map map) { - setResult(map, "header", viewModule()+"/header"); - setView(map, "index"); - setResult(map, "version", Version.get()); - setResult(map, "clientArtifactId", mqCloudConfigHelper.getClientArtifactId()); - setResult(map, "producerClass", mqCloudConfigHelper.getProducerClass()); - setResult(map, "consumerClass", mqCloudConfigHelper.getConsumerClass()); - return view(); - } - - @Override - public String viewModule() { - return "intro"; - } -} diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/TopicController.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/TopicController.java index fbe47700..71985e2e 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/TopicController.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/TopicController.java @@ -62,6 +62,9 @@ import com.sohu.tv.mq.cloud.util.Status; import com.sohu.tv.mq.cloud.web.controller.param.AssociateProducerParam; import com.sohu.tv.mq.cloud.web.controller.param.TopicParam; +import com.sohu.tv.mq.cloud.web.vo.BrokersQueueOffsetVO; +import com.sohu.tv.mq.cloud.web.vo.BrokersQueueOffsetVO.BrokerQueueOffset; +import com.sohu.tv.mq.cloud.web.vo.BrokersQueueOffsetVO.QueueOffset; import com.sohu.tv.mq.cloud.web.vo.IpSearchResultVO; import com.sohu.tv.mq.cloud.web.vo.UserInfo; import com.sohu.tv.mq.util.CommonUtil; @@ -171,19 +174,53 @@ public String produceProgress(UserInfo userInfo, @PathVariable long tid, Map offsetTable = topicStatsTable.getOffsetTable(); - Map> topicOffsetMap = new TreeMap>(); - for (MessageQueue mq : offsetTable.keySet()) { - TreeMap offsetMap = topicOffsetMap.get(mq.getBrokerName()); - if (offsetMap == null) { - offsetMap = new TreeMap(); - topicOffsetMap.put(mq.getBrokerName(), offsetMap); - } - offsetMap.put(mq, offsetTable.get(mq)); - } - setResult(map, topicOffsetMap); - setResult(map, "topic", topic); + BrokersQueueOffsetVO brokersQueueOffsetVO = toBrokersQueueOffsetVO(offsetTable); + brokersQueueOffsetVO.setTopic(topic.getName()); + brokersQueueOffsetVO.setTopicId(topic.getId()); + setResult(map, brokersQueueOffsetVO); return view; } + + private BrokersQueueOffsetVO toBrokersQueueOffsetVO(HashMap offsetTable) { + // 合并&排序 + Map> topicOffsetMap = new TreeMap<>(); + for (MessageQueue mq : offsetTable.keySet()) { + topicOffsetMap.computeIfAbsent(mq.getBrokerName(), broker->{ + return new TreeMap(); + }).put(mq, offsetTable.get(mq)); + } + // 转换 + List brokerQueueOffsetList = new ArrayList<>(); + for(String broker : topicOffsetMap.keySet()) { + BrokerQueueOffset brokerQueueOffset = new BrokerQueueOffset(); + brokerQueueOffset.setBroker(broker); + brokerQueueOffset.setQueueOffsetList(toQueueOffsetList(topicOffsetMap.get(broker))); + brokerQueueOffset.calculate(); + brokerQueueOffsetList.add(brokerQueueOffset); + } + BrokersQueueOffsetVO brokersQueueOffsetVO = new BrokersQueueOffsetVO(); + brokersQueueOffsetVO.setBrokerQueueOffsetList(brokerQueueOffsetList); + return brokersQueueOffsetVO; + } + + /** + * 转换为 QueueOffset + * @param offSetMap + * @return + */ + private List toQueueOffsetList(TreeMap offSetMap) { + List list = new ArrayList<>(); + for(MessageQueue messageQueue : offSetMap.keySet()) { + TopicOffset topicOffset = offSetMap.get(messageQueue); + QueueOffset queueOffset = new QueueOffset(); + queueOffset.setQueueId(messageQueue.getQueueId()); + queueOffset.setMaxOffset(topicOffset.getMaxOffset()); + queueOffset.setMinOffset(topicOffset.getMinOffset()); + queueOffset.setLastUpdateTimestamp(topicOffset.getLastUpdateTimestamp()); + list.add(queueOffset); + } + return list; + } /** * 新建topic diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/UserController.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/UserController.java index a4fc96ff..739743b2 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/UserController.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/UserController.java @@ -32,11 +32,14 @@ import com.sohu.tv.mq.cloud.bo.ConsumerTraffic; import com.sohu.tv.mq.cloud.bo.StatsProducer; import com.sohu.tv.mq.cloud.bo.Topic; +import com.sohu.tv.mq.cloud.bo.TopicStat; import com.sohu.tv.mq.cloud.bo.TopicTopology; import com.sohu.tv.mq.cloud.bo.TopicTraffic; import com.sohu.tv.mq.cloud.bo.Traffic; import com.sohu.tv.mq.cloud.bo.User; import com.sohu.tv.mq.cloud.bo.UserProducer; +import com.sohu.tv.mq.cloud.bo.UserWarn; +import com.sohu.tv.mq.cloud.dao.UserWarnCount; import com.sohu.tv.mq.cloud.mq.DefaultCallback; import com.sohu.tv.mq.cloud.mq.MQAdminTemplate; import com.sohu.tv.mq.cloud.service.AlertService; @@ -50,6 +53,7 @@ import com.sohu.tv.mq.cloud.service.TopicTrafficService; import com.sohu.tv.mq.cloud.service.UserProducerService; import com.sohu.tv.mq.cloud.service.UserService; +import com.sohu.tv.mq.cloud.service.UserWarnService; import com.sohu.tv.mq.cloud.util.DateUtil; import com.sohu.tv.mq.cloud.util.FreemarkerUtil; import com.sohu.tv.mq.cloud.util.MQCloudConfigHelper; @@ -113,6 +117,9 @@ public class UserController extends ViewController { @Autowired private MQCloudConfigHelper mqCloudConfigHelper; + + @Autowired + private UserWarnService userWarnService; /** * 退出登录 @@ -659,7 +666,72 @@ private void addConsumer(TopicInfoVO ti, List list) { } } } - + + /** + * 获取用户的topic状况 + * + * @return + * @throws Exception + */ + @ResponseBody + @RequestMapping("/topic/stat") + public Result topicStat(UserInfo userInfo) throws Exception { + List traceClusterIdList = clusterService.getTraceClusterIdList(); + Result rst = topicService.queryTopicStat(userInfo.getUser(), traceClusterIdList); + return Result.getWebResult(rst); + } + + /** + * 用户警告 + * @param userInfo + * @param paginationParam + * @param map + * @return + * @throws Exception + */ + @RequestMapping("/warn/list") + public String warn(UserInfo userInfo, @Valid PaginationParam paginationParam, Map map) + throws Exception { + // 设置返回视图 + String view = viewModule() + "/warnList"; + // 设置分页参数 + setPagination(map, paginationParam); + // 获取警告数量 + long uid = userInfo.getUser().getId(); + Result countResult = userWarnService.queryUserWarnCount(uid); + if (!countResult.isOK()) { + return view(); + } + paginationParam.caculatePagination(countResult.getResult()); + // 获取警告列表 + Result> result = userWarnService.queryUserWarnList(uid, paginationParam.getBegin(), + paginationParam.getNumOfPage()); + setResult(map, result.getResult()); + return view; + } + + /** + * 用户警告详情 + * @param userInfo + * @param paginationParam + * @param map + * @return + * @throws Exception + */ + @ResponseBody + @RequestMapping("/warn/detail") + public Result warn(UserInfo userInfo, @RequestParam("wid") long wid, Map map) + throws Exception { + return userWarnService.queryWarnInfo(wid); + } + + @ResponseBody + @RequestMapping("/warn/count") + public Result> warn(UserInfo userInfo, @RequestParam("days") int days, Map map) + throws Exception { + return userWarnService.queryUserWarnCount(userInfo.getUser().getId(), days); + } + @Override public String viewModule() { return "user"; diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/admin/AuditController.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/admin/AuditController.java index 3dd615bf..97b89c85 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/admin/AuditController.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/admin/AuditController.java @@ -57,6 +57,7 @@ import com.sohu.tv.mq.cloud.service.AuditTopicDeleteService; import com.sohu.tv.mq.cloud.service.AuditTopicService; import com.sohu.tv.mq.cloud.service.AuditTopicTraceService; +import com.sohu.tv.mq.cloud.service.AuditTopicTrafficWarnService; import com.sohu.tv.mq.cloud.service.AuditTopicUpdateService; import com.sohu.tv.mq.cloud.service.AuditUserConsumerDeleteService; import com.sohu.tv.mq.cloud.service.AuditUserProducerDeleteService; @@ -68,7 +69,6 @@ import com.sohu.tv.mq.cloud.service.UserMessageService; import com.sohu.tv.mq.cloud.service.UserProducerService; import com.sohu.tv.mq.cloud.service.UserService; -import com.sohu.tv.mq.cloud.service.AuditTopicTrafficWarnService; import com.sohu.tv.mq.cloud.util.DateUtil; import com.sohu.tv.mq.cloud.util.Result; import com.sohu.tv.mq.cloud.util.Status; @@ -1144,6 +1144,7 @@ public Result resetOffset(UserInfo userInfo, @RequestParam("aid") long aid) t ConsumerConfig consumerConfig = new ConsumerConfig(); consumerConfig.setConsumer(consumer.getName()); consumerConfig.setRetryMessageResetTo(resetTo.getTime()); + consumerConfig.setRetryMessageSkipKey(auditResetOffset.getMessageKey()); Result result = consumerConfigService.save(consumerConfig); if (result.isNotOK()) { return Result.getWebResult(result); diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/param/UserConsumerParam.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/param/UserConsumerParam.java index 6078048f..c4c958fb 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/param/UserConsumerParam.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/param/UserConsumerParam.java @@ -11,6 +11,8 @@ public class UserConsumerParam { private String offset; private long cid; + + private String messageKey; public long getTid() { return tid; @@ -44,9 +46,17 @@ public void setCid(long cid) { this.cid = cid; } + public String getMessageKey() { + return messageKey; + } + + public void setMessageKey(String messageKey) { + this.messageKey = messageKey; + } + @Override public String toString() { - return "UserConsumerParam [tid=" + tid + ", consumerId=" + consumerId - + ", offset=" + offset + ", cid=" + cid + "]"; + return "UserConsumerParam [tid=" + tid + ", consumerId=" + consumerId + ", offset=" + offset + ", cid=" + cid + + ", messageKey=" + messageKey + "]"; } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/view/data/ProducerStatsLineChartData.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/view/data/ProducerStatsLineChartData.java index 8e975e4d..0f35a2c7 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/view/data/ProducerStatsLineChartData.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/view/data/ProducerStatsLineChartData.java @@ -230,13 +230,15 @@ public List getLineChartData(Map searchMap) { warn = true; } } - if(warn) { + if(warn || pts.getException() != null) { Map markerMap = new HashMap(); - searchMap.get(""); markerMap.put("symbol", "circle"); markerMap.put("fillColor", "red"); markerMap.put("enabled", true); countMap.put("marker", markerMap); + if (pts.getException() != null) { + countMap.put("dt", pts.getException()); + } } // 设置详细信息 Map> map = (Map>) lineChart.getDataMap(); diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/vo/AuditResetOffsetVO.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/vo/AuditResetOffsetVO.java index 5341e659..5ddca196 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/vo/AuditResetOffsetVO.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/vo/AuditResetOffsetVO.java @@ -16,6 +16,7 @@ public class AuditResetOffsetVO { private String offset; private String topic; private String consumer; + private String messageKey; public long getAid() { return aid; } @@ -52,4 +53,10 @@ public String getConsumer() { public void setConsumer(String consumer) { this.consumer = consumer; } + public String getMessageKey() { + return messageKey; + } + public void setMessageKey(String messageKey) { + this.messageKey = messageKey; + } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/vo/BrokersQueueOffsetVO.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/vo/BrokersQueueOffsetVO.java new file mode 100644 index 00000000..2a9372dc --- /dev/null +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/vo/BrokersQueueOffsetVO.java @@ -0,0 +1,173 @@ +package com.sohu.tv.mq.cloud.web.vo; + +import java.util.List; + +/** + * broker队列偏移量 + * + * @author yongfeigao + * @date 2021年8月23日 + */ +public class BrokersQueueOffsetVO { + + private String topic; + + private long topicId; + + private List brokerQueueOffsetList; + + public long getTopicId() { + return topicId; + } + + public void setTopicId(long topicId) { + this.topicId = topicId; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public List getBrokerQueueOffsetList() { + return brokerQueueOffsetList; + } + + public void setBrokerQueueOffsetList(List brokerQueueOffsetList) { + this.brokerQueueOffsetList = brokerQueueOffsetList; + } + + public static class BrokerQueueOffset { + // broker名 + private String broker; + + // 队列偏移量 + private List queueOffsetList; + + private long minOffset; + private long maxOffset; + private long messageCount; + private long lastUpdateTimestamp = -1; + + public void calculate() { + if (queueOffsetList == null || queueOffsetList.size() == 0) { + return; + } + for (int i = 0; i < queueOffsetList.size(); ++i) { + QueueOffset queueOffset = queueOffsetList.get(i); + if (i == 0) { + minOffset = queueOffset.getMinOffset(); + } + if (minOffset > queueOffset.getMinOffset()) { + minOffset = queueOffset.getMinOffset(); + } + if (maxOffset < queueOffset.getMaxOffset()) { + maxOffset = queueOffset.getMaxOffset(); + } + if (lastUpdateTimestamp < queueOffset.getLastUpdateTimestamp()) { + lastUpdateTimestamp = queueOffset.getLastUpdateTimestamp(); + } + long count = queueOffset.getMaxOffset() - queueOffset.getMinOffset(); + if (count > 0) { + messageCount += count; + } + } + } + + public long getMinOffset() { + return minOffset; + } + + public void setMinOffset(long minOffset) { + this.minOffset = minOffset; + } + + public long getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(long maxOffset) { + this.maxOffset = maxOffset; + } + + public long getMessageCount() { + return messageCount; + } + + public void setMessageCount(long messageCount) { + this.messageCount = messageCount; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } + + public String getBroker() { + return broker; + } + + public void setBroker(String broker) { + this.broker = broker; + } + + public List getQueueOffsetList() { + return queueOffsetList; + } + + public void setQueueOffsetList(List queueOffsetList) { + this.queueOffsetList = queueOffsetList; + } + } + + /** + * 队列偏移量 + * + * @author yongfeigao + * @date 2021年8月23日 + */ + public static class QueueOffset { + private int queueId; + private long minOffset; + private long maxOffset; + private long lastUpdateTimestamp; + + public int getQueueId() { + return queueId; + } + + public void setQueueId(int queueId) { + this.queueId = queueId; + } + + public long getMinOffset() { + return minOffset; + } + + public void setMinOffset(long minOffset) { + this.minOffset = minOffset; + } + + public long getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(long maxOffset) { + this.maxOffset = maxOffset; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } + } +} diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/vo/ConsumerProgressVO.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/vo/ConsumerProgressVO.java index aedd0788..458164e6 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/vo/ConsumerProgressVO.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/vo/ConsumerProgressVO.java @@ -165,9 +165,11 @@ public long getRetryMaxOffset() { private long getMaxOffset(Map offsetMap) { long maxOffset = 0; - for (OffsetWrapper offsetWrapper : offsetMap.values()) { - if (maxOffset < offsetWrapper.getBrokerOffset()) { - maxOffset = offsetWrapper.getBrokerOffset(); + if (offsetMap != null) { + for (OffsetWrapper offsetWrapper : offsetMap.values()) { + if (maxOffset < offsetWrapper.getBrokerOffset()) { + maxOffset = offsetWrapper.getBrokerOffset(); + } } } return maxOffset; @@ -175,9 +177,11 @@ private long getMaxOffset(Map offsetMap) { public long getDlqMaxOffset() { long maxOffset = 0; - for (TopicOffset topicOffset : dlqOffsetMap.values()) { - if (maxOffset < topicOffset.getMaxOffset()) { - maxOffset = topicOffset.getMaxOffset(); + if (dlqOffsetMap != null) { + for (TopicOffset topicOffset : dlqOffsetMap.values()) { + if (maxOffset < topicOffset.getMaxOffset()) { + maxOffset = topicOffset.getMaxOffset(); + } } } return maxOffset; diff --git a/mq-cloud/src/main/resources/static/software/rocketmq.zip b/mq-cloud/src/main/resources/static/software/rocketmq.zip index f4b3ba36..995fcd1d 100644 Binary files a/mq-cloud/src/main/resources/static/software/rocketmq.zip and b/mq-cloud/src/main/resources/static/software/rocketmq.zip differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/idempotent.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/idempotent.md new file mode 100644 index 00000000..c3d2d1e6 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/idempotent.md @@ -0,0 +1,99 @@ +## 一、重复消费的情况 + +RocketMQ消费的时候支持At least Once ,即每个消息必须投递一次。 + +简单看下生产消费流程图,就可以知道了: + +![](img/6.1.png) + +从消费流程③④可以看出,**消费者只有消费成功,才会向broker返回ack,保障了消息至少成功消费一次**。 + +但是RocketMQ消费并不支持Exactly Only Once,即允许消费重复的消息。 + +那么上面流程哪些地方可能导致重复消费的情况? + +- 发送的消息重复 + + - 参考①流程,当生产者发送消息到达broker,由于网络或客户端原因,导致②响应失败。 + + 此时,生产者可能进行重试发送,导致出现重复的消息。 + +- 消息重复投递 + + - 参考③流程,当消费者拉取到消息,消费完毕后,由于网络或客户端等原因,导致④ack失败。 + + 此时,当网络或客户端恢复等,broker可能重复投递已经被成功消费的消息,导致重复消费。 + +- 负载均衡导致重复消费 + + - broker更新、扩容等重启,或客户端重启都会导致消费重新均衡,此时可能会重复消费消息。 + +## 二、消息去重 + +RocketMQ推荐在业务端自己实现幂等业务,但是除非业务有强烈的幂等要求,否则一般希望消息尽量不重复。 + +要想实现消息去重,需要以**唯一键**作为去重标识,类似如下: + +![](img/6.2.png) + +**当检查到某消息已经消费时,直接结束,即可保证重复的消息只消费一次**。 + +*这里说一下唯一键的选择,RocketMQ消息具有Message ID字段,其具有唯一性,其组成如下:* + +*ip+pid+类加载器hash+系统时间戳+自增序号* + +*无论是否在在同一jvm内,并发生成,也不存在重复的可能。但是不建议作为唯一键进行去重,因为生产者如果发送消息进行重试,可能相同的消息对应的Message ID不同。* + +由于消费者是分布式的,所以需要将所有的消息唯一键存储起来,比如redis,参见下图: + +![](img/6.3.png) + +上面有两个流程: + +- ①正常流程就是带去重功能的消费流程,只是消费前需要判断该消息是否消费过,若已消费过,直接结束;若未消费过,进入正常的消费流程。 +- ②重试流程是如果消息消费失败,则进入重试队列,重试的消息不再走去重逻辑。(*因为考虑到业务已经消费过一次,并且失败了,需要重试流程保障其一定消费成功。*) + +这里可能有个问题,就是在①正常流程中,如果某个消费者设置消费标识成功,但是在消费消息成功之前退出了,那么该消息虽未消费成功却被认为已经消费了,岂不是丢失了消息? + +所以要保证此消息一定消费成功,增加**③延迟消费流程**作为正常流程的补充,如下: + +![](img/6.4.png) + +相对于之前的流程,做了两点修改: + +1. redis中缓存的值存储为mid(可以理解为消息的唯一标识,即使MessageId一样,mid也不一样),并且携带过期时间和消费状态。 +2. 当某个消息正在消费中时,由于broker更新等导致消费者重新平衡,此消息被分配到了其他消费者,那么该消息会进入**③延迟流程**中,该消息将被投递到延迟队列,等待被重新消费。 + +这样就保障了未确认消费状态的消息一定会被成功消费。 + +## 三、去重情况 + +这里来看一下,针对**一、重复消费的情况**的三种情况是否能够去重: + +1. 发送的消息重复,即重复的消息 + + 因为重复的消息id相同,但是mid不同,故它们将在下面的步骤中被去重: + + ![](img/6.5.png) + +2. 消息重复投递,即broker重复投递消费者已经消费成功的消息 + + 因为消息已经消费完成,所以它们将在下面的步骤中被去重: + + ![](img/6.6.png) + +3. 负载均衡导致重复消费 + + 此种情况即同一条消息被投递到多个消费者,那么它们也将在如下步骤中被去重: + + ![](img/6.7.png) + + 即无论此消息被某个消费者消费成功或失败,都将不被其他消费者消费。 + +当然,这里依然还存在某些问题,比如: + + 1. redis不可用时,去重功能将失效。 + 2. 消息消费失败时,去重功能也将失效。 + 3. 消费消费时间过长,消费状态超时自动删除,去重功能失效。 + + 但是,**去重和保障成功消费本来就是一对矛盾体,只能权衡保障大部分情况满足要求**。 diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/idempotent.toc.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/idempotent.toc.md new file mode 100644 index 00000000..db0712f0 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/idempotent.toc.md @@ -0,0 +1,5 @@ +##### 目录 + +- [一、重复消费的情况](#repet) +- [二、消息去重](#deduplicate) +- [三、去重情况](#deduplicate2) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.1.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.1.png new file mode 100644 index 00000000..120d79c5 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.1.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.2.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.2.png new file mode 100644 index 00000000..dd2326e7 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.2.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.3.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.3.png new file mode 100644 index 00000000..263f38db Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.3.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.4.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.4.png new file mode 100644 index 00000000..0af3ad3c Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.4.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.5.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.5.png new file mode 100644 index 00000000..2a46ea23 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.5.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.6.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.6.png new file mode 100644 index 00000000..14c91e01 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.6.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.7.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.7.png new file mode 100644 index 00000000..f2267bf4 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/6.7.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/clientConsumer.md b/mq-cloud/src/main/resources/static/wiki/userGuide/clientConsumer.md index f0f31cf3..aa43d025 100644 --- a/mq-cloud/src/main/resources/static/wiki/userGuide/clientConsumer.md +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/clientConsumer.md @@ -47,20 +47,16 @@ consumer.shutdown(); ## 四、消费回调代码 +*注意:如下各种消费代码若抛出异常,则消息将发回rocketmq,进入重试队列。* + 1. json消费回调代码 ``` ConsumerCallback consumerCallback = new ConsumerCallback() { public void call(String t, MessageExt k) { - try { - // 打印日志 - logger.info("msg:{}, msgExt:{}", t, k); - // 消费逻辑 - } catch (Exception e) { - logger.error("consume err, msgid:{}, msg:{}", k.getMsgId(), t, e); - // 如果需要重新消费,这里需要把异常抛出,消费失败的消息将发回rocketmq,重试消费 - throw e; - } + // 打印日志,以便校验是否收到日志 + logger.info("msgExt:{},msg:{}", k, t); + // 消费逻辑 } } ``` @@ -70,15 +66,9 @@ consumer.shutdown(); ``` ConsumerCallback consumerCallback = new ConsumerCallback() { public void call(Video t, MessageExt k) { - try { - // 打印日志 - logger.info("msg:{}, msgExt:{}", t, k); - // 消费逻辑 - } catch (Exception e) { - logger.error("consume err, msgid:{}, msg:{}", k.getMsgId(), t, e); - // 如果需要重新消费,这里需要把异常抛出,消费失败的消息将发回rocketmq,重试消费 - throw e; - } + // 打印日志,以便校验是否收到日志 + logger.info("msgExt:{},msg:{}", k, t); + // 消费逻辑 } } ``` @@ -88,15 +78,9 @@ consumer.shutdown(); ``` ConsumerCallback consumerCallback = new ConsumerCallback, MessageExt>() { public void call(Map t, MessageExt k) { - try { - // 打印日志 - logger.info("msg:{}, msgExt:{}", t, k); - // 消费逻辑 - } catch (Exception e) { - logger.error("consume err, msgid:{}, msg:{}", k.getMsgId(), t, e); - // 如果需要重新消费,这里需要把异常抛出,消费失败的消息将发回rocketmq,重试消费 - throw e; - } + // 打印日志,以便校验是否收到日志 + logger.info("msgExt:{},msg:{}", k, t); + // 消费逻辑 } } ``` @@ -107,20 +91,24 @@ consumer.shutdown(); // 设置批量消费最大消息数 consumer.setConsumeMessageBatchMaxSize(32); // 设置批量消费BatchConsumerCallback - consumer.setBatchConsumerCallback(new BatchConsumerCallback(){ - public void call(List> batchMessage) throws Exception { - MQMessage tmpMessage = null; + consumer.setBatchConsumerCallback(new BatchConsumerCallback(){ + public void call(List> batchMessage, ConsumeConcurrentlyContext context) throws Exception { + int index = 0; try { - for(MQMessage mqMessage : batchMessage) { - tmpMessage = mqMessage; + for(; index < batchMessage.size(); ++index) { + MQMessage mqMessage = batchMessage.get(index); // 打印日志 - logger.info("msg:{}, msgExt:{}", mqMessage.getMessage(), mqMessage.getMessageExt()); + logger.info("msgExt:{},msg:{}", mqMessage.getMessageExt(), mqMessage.getMessage()); // 消费逻辑 } } catch (Exception e) { - logger.error("consume err, msgid:{}, msg:{}", tmpMessage.getMessageExt().getMsgId(), tmpMessage.getMessage(), e); - // 如果需要重新消费,这里需要把异常抛出。注意:抛出异常后这批消息将重新消费 - throw e; + logger.error("consume err, msgid:{}, msg:{}", batchMessage.get(index).getMessageExt().getMsgId(), batchMessage.get(index).getMessage(), e); + // 异常处理尤为重要,这里涉及到哪些消息需要重试的问题,分为如下两种情况: + // ① 若再次将异常抛出,那么本批消息将整体重新消费 + // throw e; + // ② 若不抛出异常,设置上ackIndex参数并返回,则ackIndex之后的消息将重新消费 + // context.setAckIndex(index); + // return; } } }); @@ -162,41 +150,13 @@ consumer.shutdown(); 5.`pullInterval(默认为0)`:控制每个队列每隔多长时间从broker拉取一次消息,默认不停拉取。 -2. 如何控制消费并发量? - - 1.可以通过如下参数控制消费的线程数: +2. 如何控制消费线程数? `consumeThreadMin(默认为20)`和`consumeThreadMax(默认为64)`,默认至少有20个消费线程。 例如,将`consumeThreadMin`和`consumeThreadMax`同时设置为1,这样就变成单线程消费了。 - 2.可以通过如下参数控制多少条消息作为一批被某个线程消费: - - `consumeMessageBatchMaxSize(默认为1)`,默认表示每条消息需要一个线程来处理。 - - **若非有必要不建议修改此值,因为可能产生重复消费。比如修改为3,那么一个线程可以处理3条消息,假如前2条消费成功,第3条消费失败,此时如果抛出异常,将导致这3条再次重新消费。** - - *原因是由于rocketmq针对一批消息使用同一个标识来判断是否消费成功。* - - *当然,如果不依赖rocketmq的消息重试机制可以不用关心这个问题。* - -3. 每秒消费最多1000条消息,该如何实现? - - 假设broker数量为2,每个broker上8个队列,总队列数为16。 - - ``` - pullThresholdForQueue=1000/16≈63 - pullBatchSize=20 - pullInterval=300 - ``` - - 释义:这样每个队列每秒拉取20*(1000/300)=60条消息,总缓存消息1000条。 - - 如果不想计算队列数量怎么办? - - 可以设置`pullThresholdForTopic=1000`,但是这个不是很准确,因为需要计算队列数,进行均分。 - -4. 如何实现顺序消费? +3. 如何实现顺序消费? 以下内容部分摘自rocketmq开发手册。 @@ -234,7 +194,7 @@ consumer.shutdown(); * 队列热点问题,个别队列由于哈希不均导致消息过多,消费速度跟不上,产生消息堆积问题。 * 遇到消息失败的消息,无法跳过,当前队列消费只能暂停。 -5. 匀速消费? +4. 匀速消费? 如果有如下需求: @@ -254,9 +214,101 @@ consumer.shutdown(); ![](img/limitConsume.png) -6. 暂停消费? +5. 暂停消费? 如果业务端遇到某些问题,需要暂停消息消费,在不重启服务时(客户端版本在4.6.5及以上),可以在MQCloud里进行动态设置: ![](img/pauseConsume.png) +6. 幂等消费 + + RocketMQ默认不支持幂等消费,MQCloud采用redis来保障全局唯一,实现了幂等消费(只支持集群模式)。 + + 如果需要使用幂等功能需要依赖redis客户端,类似如下: + + ``` + + redis.clients + jedis + 3.3.0 + + ``` + + 在消费者启动之前,增加如下代码即可: + + ``` + consumer.setRedis(RedisBuilder.build(jedisCluster, 2100)); + ``` + + 其中第一个参数是jedis实例(可以为集群或单个),第二个参数为jedis超时的最大时间,更多构建方法请参考`RedisBuilder`。 + + 默认情况下,MQCloud采用RocketMQ发送消息时客户端的Message ID来进行去重,其组成如下: + + ``` + ip+pid+类加载器hash+系统时间戳+自增序号 + ``` + + 即无论是否在在同一jvm内,并发生成,也不存在重复的可能。 + + **但是不建议使用它作为唯一键进行去重,因为相同的消息对应的Message ID可能不同。** + + 所以建议在发送消息时设置幂等id来保障唯一性,如下: + + ``` + MQMessage message = MQMessage.build("message").setKeys("k1").setIdempotentID("1102123"); + Result result = producer.send(message); + ``` + + 比如订单业务,建议使用订单号作为幂等id。 + + 这里的幂等是有时间窗口的,默认为3分钟,即3分钟内的重复消息只会消费一条,可以修改这个时间窗口: + + ``` + consumer.setDeduplicateWindowSeconds(600); + ``` + + 由于幂等实现依赖了redis,为了防止redis故障影响业务方,MQCloud使用了Hystrix隔离了redis操作。 + + 即当redis故障时,幂等逻辑将失效,消费业务将不受影响,继续消费。 + + 这里说明一点,MQCloud实现的幂等逻辑的同时保障消息不丢失,所以如下情况幂等逻辑将失效: + + 1. redis故障、调用异常、超时等。 + 2. 消费失败的消息(其将进入重试队列,为了保障消费成功,此种消息不再执行去重)。 + 3. 消费时间超过幂等时间窗口的消息。 + +7. 重试队列 + + 消费消息时,如果抛出异常,则消息会进入重试队列,在消费详情可以看到重试队列: + + ![](img/retry.png) + + 重试队列的名称以%RETRY%开头,点击搜索按钮可以查找重试的消息,并支持跳过重试的消息。 + + 进入重试队列的消息将以如下衰减频率进行重试消费: + + | 第几次重试 | 距上次重试的间隔时间 | + | ----- | ---------- | + | 1 | 10秒 | + | 2 | 30秒 | + | 3 | 1分钟 | + | 4 | 2分钟 | + | 5 | 3分钟 | + | 6 | 4分钟 | + | 7 | 5分钟 | + | 8 | 6分钟 | + | 9 | 7分钟 | + | 10 | 8分钟 | + | 11 | 9分钟 | + | 12 | 10分钟 | + | 13 | 20分钟 | + | 14 | 30分钟 | + | 15 | 1小时 | + | 16 | 2小时 | + + 即进入重试队列的消息在总共4小时46分内进行16次重试后(任何一次成功将结束重试机制),仍然失败,将会进入死信队列,死信队列的名称以%DLQ%开头,类似如下: + + ![](img/dead.png) + + 点击搜索按钮可以查找哪些消息变成了死消息,并可以重新发送消费。 + diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/clientProducer.md b/mq-cloud/src/main/resources/static/wiki/userGuide/clientProducer.md index 743864b3..b4822e9a 100644 --- a/mq-cloud/src/main/resources/static/wiki/userGuide/clientProducer.md +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/clientProducer.md @@ -111,6 +111,48 @@ producer.shutdown(); 但是,消费者需要注意,需要单独设置setMessageSerializer(null),否则消费消息会反序列化失败。 +5. 发送消息如何进行异步重试? + + *注:与[RocketMQ自身的重试](#retry)是不一样的,因为RocketMQ默认的重试机制是同步的,并存在超时而无法完成重试的可能。* + + MQCloud在消息发送失败时,提供了异步重试api: + + ``` + Result sendResult = producer.send(MQMessage.build(msg).setKeys(key)); + if (!result.isSuccess && !result.isRetrying()) { // 发送失败并且没有正在重试认为失败 + System.out.println("发送失败"); + } + ``` + + 另外,如果需要知道异步重试的结果,可以在producer初始化时进行如下设置: + + ``` + producer.setResendResultConsumer(result -> { + if (!result.isSuccess) { + logger.info("重试次数:{},消息:{}", result.getRetriedTimes(), result.getMqMessage()); + // 可以在这里增加重试失败的消息处理逻辑 + } + }); + ``` + + 默认的重试次数为一次,可以通过如下api修改默认重试次数: + + ``` + producer.setDefaultRetryTimes(2) + ``` + + 当然,如果想针对某条消息单独设置重试次数,可以参考如下,会覆盖默认重试次数: + + ``` + MQMessage.build(msg).setRetryTimes(3) + ``` + + 异步重试使用的线程数默认为cpu核数,任务阻塞队列为100,如果想修改可以在producer.start之前,调用如下api修改: + + ``` + producer.setRetrySenderExecutor(ExecutorService retrySenderExecutor) + ``` + ## 五、发送有序消息示例 ``` diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/dead.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/dead.png new file mode 100644 index 00000000..92ec9559 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/dead.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/idempotent.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/idempotent.png new file mode 100644 index 00000000..2a46ea23 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/idempotent.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/retry.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/retry.png new file mode 100644 index 00000000..07efc2b0 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/retry.png differ diff --git a/mq-cloud/src/main/resources/templates/admin/audit/resetOffset.html b/mq-cloud/src/main/resources/templates/admin/audit/resetOffset.html index cdfacb55..15704471 100644 --- a/mq-cloud/src/main/resources/templates/admin/audit/resetOffset.html +++ b/mq-cloud/src/main/resources/templates/admin/audit/resetOffset.html @@ -33,6 +33,12 @@ +
+ +
+ +
+
diff --git a/mq-cloud/src/main/resources/templates/admin/user/list.html b/mq-cloud/src/main/resources/templates/admin/user/list.html index 7fd4a4a7..b70e3964 100644 --- a/mq-cloud/src/main/resources/templates/admin/user/list.html +++ b/mq-cloud/src/main/resources/templates/admin/user/list.html @@ -13,6 +13,7 @@ 姓名 邮箱 手机 + 手机预警 角色 创建时间 更新时间 @@ -33,6 +34,7 @@ ${user.name!} ${user.email} ${user.mobile!} + <#if user.receivePhoneNotice()>开启<#else>关闭 <#if user.type == 0>普通用户<#else>管理员 ${user.createDate?string("yyyy-MM-dd")} ${user.updateTime?string("yyyy-MM-dd HH:mm:ss")} diff --git a/mq-cloud/src/main/resources/templates/audit/resetOffset.html b/mq-cloud/src/main/resources/templates/audit/resetOffset.html index 500f296e..c4259f73 100644 --- a/mq-cloud/src/main/resources/templates/audit/resetOffset.html +++ b/mq-cloud/src/main/resources/templates/audit/resetOffset.html @@ -33,6 +33,14 @@
+ <#if response.result.messageKey??> +
+ +
+ +
+
+ diff --git a/mq-cloud/src/main/resources/templates/consumer/progress.html b/mq-cloud/src/main/resources/templates/consumer/progress.html index 6d4c06e2..3fdcd04b 100644 --- a/mq-cloud/src/main/resources/templates/consumer/progress.html +++ b/mq-cloud/src/main/resources/templates/consumer/progress.html @@ -861,18 +861,20 @@ + 序号 客户端 - 发送时间 + 重试时间 消费次数 消息 + key - 暂无数据 + 暂无数据 @@ -86,6 +88,7 @@ <#include "../user/update.html"> <#include "../feedback/add.html"> <#include "../audit/audit.html"> + <#include "../user/warn.html"> <#else> - 本次查无数据 + 本次查无数据 <#else> - 查询异常!${response.message} + 查询异常!${response.message} \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/msg/retrySearchOffset.html b/mq-cloud/src/main/resources/templates/msg/retrySearchOffset.html index 738e12e4..24c91117 100644 --- a/mq-cloud/src/main/resources/templates/msg/retrySearchOffset.html +++ b/mq-cloud/src/main/resources/templates/msg/retrySearchOffset.html @@ -5,19 +5,20 @@ ${msg_index + 1 + response.result.mqc.prevSize} ${msg.bornHostString} - ${msg.bornTimestamp?number_to_datetime?string("yyyy-MM-dd HH:mm:ss.SSS")} + ${msg.storeTimestamp?number_to_datetime?string("yyyy-MM-dd HH:mm:ss.SSS")} <#if RequestParameters.traceEnabled?? && RequestParameters.traceEnabled == "true"> - ${msg.reconsumeTimes} + ${msg.reconsumeTimes} <#else> - ${msg.reconsumeTimes} + 第${msg.reconsumeTimes}次 <#if msg.decodedBody?length gt 1000>
${msg.decodedBody}
<#else>${msg.decodedBody} + ${msg.keys!} <#else> - 本次查无数据 + 本次查无数据 @@ -35,6 +36,6 @@ <#else> - 查询异常!${response.message} + 查询异常!${response.message} \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/msg/retrySearchTime.html b/mq-cloud/src/main/resources/templates/msg/retrySearchTime.html index 9316cdf5..2c33aa40 100644 --- a/mq-cloud/src/main/resources/templates/msg/retrySearchTime.html +++ b/mq-cloud/src/main/resources/templates/msg/retrySearchTime.html @@ -5,13 +5,14 @@ ${msg_index + 1 + response.result.mqc.prevSize} ${msg.bornHostString} - ${msg.bornTimestamp?number_to_datetime?string("yyyy-MM-dd HH:mm:ss.SSS")} + ${msg.storeTimestamp?number_to_datetime?string("yyyy-MM-dd HH:mm:ss.SSS")} <#if RequestParameters.traceEnabled?? && RequestParameters.traceEnabled == "true"> - ${msg.reconsumeTimes} + ${msg.reconsumeTimes} <#else> - ${msg.reconsumeTimes} + 第${msg.reconsumeTimes}次 <#if msg.decodedBody?length gt 1000>
${msg.decodedBody}
<#else>${msg.decodedBody} + ${msg.keys!} @@ -29,12 +30,12 @@ <#else> - 本次查无数据 + 本次查无数据 <#else> - 查询异常!${response.message} + 查询异常!${response.message} \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/phone/consumeBlock.html b/mq-cloud/src/main/resources/templates/phone/consumeBlock.html new file mode 100644 index 00000000..ca1607e8 --- /dev/null +++ b/mq-cloud/src/main/resources/templates/phone/consumeBlock.html @@ -0,0 +1 @@ +消费者:${resource}检测到阻塞 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/phone/consumeFail.html b/mq-cloud/src/main/resources/templates/phone/consumeFail.html new file mode 100644 index 00000000..0038ca98 --- /dev/null +++ b/mq-cloud/src/main/resources/templates/phone/consumeFail.html @@ -0,0 +1 @@ +${topic}的消费者:${resource},消费失败量:${count} \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/phone/consumeOffsetMoved.html b/mq-cloud/src/main/resources/templates/phone/consumeOffsetMoved.html new file mode 100644 index 00000000..42632c5e --- /dev/null +++ b/mq-cloud/src/main/resources/templates/phone/consumeOffsetMoved.html @@ -0,0 +1 @@ +消费者:${resource},broker时间:${brokerTime},请求偏移量:${requestOffset},broker偏移量:${brokerOffset} \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/phone/consumeUndone.html b/mq-cloud/src/main/resources/templates/phone/consumeUndone.html new file mode 100644 index 00000000..eb52920c --- /dev/null +++ b/mq-cloud/src/main/resources/templates/phone/consumeUndone.html @@ -0,0 +1 @@ +topic:${topic}的消费者:${resource},总堆积消息量:${undoneMsgsTotal},单个队列最大堆积消息量:${undoneMsgsSingleMQ}<#if undoneMsgsDelayTime??>,消费滞后时间(相对于broker最新消息时间):${undoneMsgsDelayTime}秒 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/phone/deadMessage.html b/mq-cloud/src/main/resources/templates/phone/deadMessage.html new file mode 100644 index 00000000..3aaeaf38 --- /dev/null +++ b/mq-cloud/src/main/resources/templates/phone/deadMessage.html @@ -0,0 +1 @@ +topic:${topic}消费者:${resource}, 死消息量:${count} \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/phone/mqError.html b/mq-cloud/src/main/resources/templates/phone/mqError.html new file mode 100644 index 00000000..c5742a22 --- /dev/null +++ b/mq-cloud/src/main/resources/templates/phone/mqError.html @@ -0,0 +1 @@ +<#list list as clusterStat>cluster:${clusterStat.clusterName}:<#list clusterStat.stats as stat>${newLine}${stat};${newLine} \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/phone/produceException.html b/mq-cloud/src/main/resources/templates/phone/produceException.html new file mode 100644 index 00000000..37d1c389 --- /dev/null +++ b/mq-cloud/src/main/resources/templates/phone/produceException.html @@ -0,0 +1 @@ +${resource}生产异常 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/phone/topicTraffic.html b/mq-cloud/src/main/resources/templates/phone/topicTraffic.html new file mode 100644 index 00000000..30c2197d --- /dev/null +++ b/mq-cloud/src/main/resources/templates/phone/topicTraffic.html @@ -0,0 +1 @@ +topic: ${resource}:<#list list as checkResult>${newLine}${checkResult.time}:${checkResult.warnInfo} \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/producer/stats.html b/mq-cloud/src/main/resources/templates/producer/stats.html index 72c1810a..92e3b063 100644 --- a/mq-cloud/src/main/resources/templates/producer/stats.html +++ b/mq-cloud/src/main/resources/templates/producer/stats.html @@ -46,7 +46,11 @@ } s += ""; }); - s += ""; + if(this.points[this.points.length - 1].point.dt){ + s += ""+this.points[this.points.length - 1].point.dt+""; + } else { + s += ""; + } var statId = this.points[this.points.length - 1].point.id; if(statId){ var stats = statMap[statId]; diff --git a/mq-cloud/src/main/resources/templates/topic/produceProgress.html b/mq-cloud/src/main/resources/templates/topic/produceProgress.html index 457e78f9..ab717a1b 100644 --- a/mq-cloud/src/main/resources/templates/topic/produceProgress.html +++ b/mq-cloud/src/main/resources/templates/topic/produceProgress.html @@ -30,56 +30,36 @@ <#else> - <#list response.result as k, v> - <#assign loop=1> - <#list v as mq, offset> - <#if loop == 1> - -     ${k} - <#assign loop+=1> - 全部 - <#assign totalOffset=0> - <#assign minOffset = offset.minOffset> - <#assign maxOffset=0> - <#assign maxLastUpdateTimestamp=0> - <#list v as mqInner, offsetInner> - <#if minOffset gt offsetInner.minOffset> - <#assign minOffset = offsetInner.minOffset> - - <#if maxOffset lt offsetInner.maxOffset> - <#assign maxOffset = offsetInner.maxOffset> - - <#assign totalOffset += offsetInner.maxOffset - offsetInner.minOffset> - <#if maxLastUpdateTimestamp lt offsetInner.lastUpdateTimestamp> - <#assign maxLastUpdateTimestamp = offsetInner.lastUpdateTimestamp> - - - ${minOffset?string(",###")} - ${maxOffset?string(",###")} - ${totalOffset?string(",###")} - - <#if maxLastUpdateTimestamp <= 0> - 暂无 - <#else> - ${maxLastUpdateTimestamp?number_to_datetime?string("yyyy-MM-dd HH:mm:ss")} - - - - - - ${mq.queueId} - ${offset.minOffset} - ${offset.maxOffset} - ${offset.maxOffset - offset.minOffset} - - <#if offset.lastUpdateTimestamp <= 0> - 暂无 - <#else> - ${offset.lastUpdateTimestamp?number_to_datetime?string("yyyy-MM-dd HH:mm:ss")} - - - - + <#list response.result.brokerQueueOffsetList as brokerQueueOffset> + +     ${brokerQueueOffset.broker} + 全部 + ${brokerQueueOffset.minOffset?string(",###")} + ${brokerQueueOffset.maxOffset?string(",###")} + ${brokerQueueOffset.messageCount?string(",###")} + + <#if brokerQueueOffset.lastUpdateTimestamp <= 0> + 暂无 + <#else> + ${brokerQueueOffset.lastUpdateTimestamp?number_to_datetime?string("yyyy-MM-dd HH:mm:ss")} + + + + <#list brokerQueueOffset.queueOffsetList as queueOffset> + + ${queueOffset.queueId} + ${queueOffset.minOffset} + ${queueOffset.maxOffset} + ${queueOffset.maxOffset - queueOffset.minOffset} + + <#if queueOffset.lastUpdateTimestamp <= 0> + 暂无 + <#else> + ${queueOffset.lastUpdateTimestamp?number_to_datetime?string("yyyy-MM-dd HH:mm:ss")} + + + + @@ -91,7 +71,7 @@
-
${topic.name} 生产流量图
+
${response.result.topic!} 生产流量图
@@ -113,7 +93,7 @@ <#if response.OK> $(function() { lineChart("produce", function (){ - $("#tid").val("${topic.id}"); + $("#tid").val("${response.result.topicId}"); }, function chartToolTip(chart){ if(!chart){ statMap = null; diff --git a/mq-cloud/src/main/resources/templates/user/update.html b/mq-cloud/src/main/resources/templates/user/update.html index 5e7a76d4..b163f309 100644 --- a/mq-cloud/src/main/resources/templates/user/update.html +++ b/mq-cloud/src/main/resources/templates/user/update.html @@ -36,6 +36,17 @@ class="form-control" /> +
+ +
+ + +
+
@@ -126,7 +137,14 @@ message: '请输入正确的手机号码' } } - } + }, + receivePhoneNotice: { + validators: { + notEmpty: { + message: '请选择是否开启手机预警' + } + } + }, } }).on('success.form.bv', function(e) { // 阻止默认事件提交 diff --git a/mq-cloud/src/main/resources/templates/user/warn.html b/mq-cloud/src/main/resources/templates/user/warn.html new file mode 100644 index 00000000..001ff743 --- /dev/null +++ b/mq-cloud/src/main/resources/templates/user/warn.html @@ -0,0 +1,76 @@ + + + + + \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/user/warnList.html b/mq-cloud/src/main/resources/templates/user/warnList.html new file mode 100644 index 00000000..3bdc12b7 --- /dev/null +++ b/mq-cloud/src/main/resources/templates/user/warnList.html @@ -0,0 +1,46 @@ +<#if response.empty> + + 暂无数据 + +<#else> + <#list response.result as warn> + + ${(pagination.result.currentPage - 1)*pagination.result.numOfPage + warn_index + 1} + ${warn.typeName} + ${warn.resource} + ${warn.createTime?string("yyyy-MM-dd HH:mm:ss")} + + + + + \ No newline at end of file diff --git a/mq-cloud/src/test/java/com/sohu/tv/mq/cloud/service/AlertServiceTest.java b/mq-cloud/src/test/java/com/sohu/tv/mq/cloud/service/AlertServiceTest.java index 70ed0490..66414c01 100644 --- a/mq-cloud/src/test/java/com/sohu/tv/mq/cloud/service/AlertServiceTest.java +++ b/mq-cloud/src/test/java/com/sohu/tv/mq/cloud/service/AlertServiceTest.java @@ -1,5 +1,10 @@ package com.sohu.tv.mq.cloud.service; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -8,6 +13,9 @@ import org.springframework.test.context.junit4.SpringRunner; import com.sohu.tv.mq.cloud.Application; +import com.sohu.tv.mq.cloud.bo.ProducerTotalStat; +import com.sohu.tv.mq.cloud.bo.User; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; @RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class) @@ -21,5 +29,49 @@ public void testMail() { boolean result = alertService.sendMail("123", "456"); Assert.assertTrue(result); } - + + @Test + public void testSendWarnMail() { + List users = new ArrayList<>(); + User u = new User(); + u.setEmail("abc@sohu-inc.com"); + u.setReceivePhoneNotice(1); + u.setMobile("18618267000"); + users.add(u); + + List totalList = new ArrayList<>(); + + ProducerTotalStat stat = new ProducerTotalStat(); + stat.setBroker("b1"); + stat.setClient("12345"); + stat.setProducer("producer"); + stat.setCreateTime("1021"); + stat.setCreateDate(20211021); + totalList.add(stat); + + stat = new ProducerTotalStat(); + stat.setBroker("b2"); + stat.setClient("12345"); + stat.setProducer("producer"); + stat.setCreateTime("1022"); + stat.setCreateDate(20211023); + stat.setException("adfsadfsdafdsasaf"); + totalList.add(stat); + + stat = new ProducerTotalStat(); + stat.setBroker("b3"); + stat.setClient("12345"); + stat.setProducer("producer"); + stat.setCreateTime("1022"); + stat.setCreateDate(20211023); + stat.setException("adfsadfsdafdsasaf"); + totalList.add(stat); + + + Map paramMap = new HashMap<>(); + paramMap.put("link", "www.baidu.com"); + paramMap.put("resource", "producer1"); + paramMap.put("list", totalList); + alertService.sendWarn(users, WarnType.PRODUCE_EXCEPTION, paramMap); + } } diff --git a/mq-cloud/src/test/java/com/sohu/tv/mq/cloud/service/UserWarnServiceTest.java b/mq-cloud/src/test/java/com/sohu/tv/mq/cloud/service/UserWarnServiceTest.java new file mode 100644 index 00000000..985d59db --- /dev/null +++ b/mq-cloud/src/test/java/com/sohu/tv/mq/cloud/service/UserWarnServiceTest.java @@ -0,0 +1,124 @@ +package com.sohu.tv.mq.cloud.service; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import com.sohu.tv.mq.cloud.Application; +import com.sohu.tv.mq.cloud.bo.UserWarn; +import com.sohu.tv.mq.cloud.bo.UserWarn.WarnType; +import com.sohu.tv.mq.cloud.dao.UserWarnCount; +import com.sohu.tv.mq.cloud.util.Result; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Application.class) +public class UserWarnServiceTest { + + @Autowired + private UserWarnService userWarnService; + + @Test + public void test() { + String consumer = "test-consumer"; + StringBuilder content = new StringBuilder("详细如下:

"); + content.append("topic:"); + content.append("a-b-topic"); + content.append(" 的消费者:"); + content.append(consumer); + content.append(" 检测到堆积,总堆积消息量:"); + content.append(1024); + content.append(",单个队列最大堆积消息量:"); + content.append(10); + content.append(",消费滞后时间(相对于broker最新消息时间):"); + content.append(80); + content.append("秒"); + UserWarn userWarn = new UserWarn(); + userWarn.setContent(content.toString()); + Result rst = userWarnService.saveWarnContent(userWarn); + Assert.assertEquals(true, rst.isOK()); + + List list = new ArrayList<>(); + int size = 10; + for(int i = 0; i < size; ++i) { + UserWarn uw = new UserWarn(); + uw.setWid(userWarn.getWid()); + uw.setResource(consumer); + uw.setUid(i); + uw.setType(WarnType.CONSUME_UNDONE.getType()); + list.add(uw); + } + Result rst2 = userWarnService.batchSaveUserWarn(list); + Assert.assertEquals(size, rst2.getResult().intValue()); + } + + @Test + public void testBachSave() { + String consumer = "test-consumer"; + StringBuilder content = new StringBuilder("详细如下:

"); + content.append("topic:"); + content.append("a-b-topic"); + content.append(" 的消费者:"); + content.append(consumer); + content.append(" 检测到堆积,总堆积消息量:"); + content.append(1024); + content.append(",单个队列最大堆积消息量:"); + content.append(10); + content.append(",消费滞后时间(相对于broker最新消息时间):"); + content.append(80); + content.append("秒"); + List uidList = new ArrayList<>(); + uidList.add(7L); + uidList.add(8L); + uidList.add(9L); + Result rst = userWarnService.save(null, WarnType.CONSUME_UNDONE, WarnType.CONSUME_UNDONE.getName(), content.toString()); + Assert.assertEquals(true, rst.isOK()); + } + + @Test + public void testBachSave2() { + String consumer = "test-consumer"; + StringBuilder content = new StringBuilder("详细如下:

"); + content.append("topic:"); + content.append("a-b-topic"); + content.append(" 的消费者:"); + content.append(consumer); + content.append(" 检测到堆积,总堆积消息量:"); + content.append(1024); + content.append(",单个队列最大堆积消息量:"); + content.append(10); + content.append(",消费滞后时间(相对于broker最新消息时间):"); + content.append(80); + content.append("秒"); + for(WarnType warnType : WarnType.values()) { + List uidList = new ArrayList<>(); + uidList.add(1L); + Result rst = userWarnService.save(null, warnType, warnType.getName(), content.toString()); + Assert.assertEquals(true, rst.isOK()); + } + } + + @Test + public void testQuery() { + long uid = 1; + Result rst = userWarnService.queryUserWarnCount(uid); + Assert.assertEquals(1, rst.getResult().intValue()); + + Result> rst2 = userWarnService.queryUserWarnList(uid, 0, 1); + Assert.assertEquals(1, rst2.getResult().size()); + } + + @Test + public void testQueryDays() { + long uid = 1; + int days = 3; + Result> rst = userWarnService.queryUserWarnCount(uid, days); + Assert.assertEquals(1, rst.getResult().size()); + } + +} diff --git a/pom.xml b/pom.xml index 14f4bdf3..b00fb230 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ mq pom mq - 4.7.2 + 4.9.1 mq-client-common-open @@ -18,7 +18,7 @@ 1.7.5 4.13.1 - 4.7.1 + 4.9.1 1.8 1.5.0 1.1.3 @@ -28,6 +28,7 @@ 3.1.0 18.0 github + 3.3.0 @@ -109,6 +110,12 @@ guava ${guava.version} + + redis.clients + jedis + true + ${jedis.version} +