优雅哥--Rocketmq消息如何实现多租户

优雅哥今天来说下如何在框架层面解决Rocketmq消息的多租户问题。

多租户的系统应该在框架层将租户信息处理掉,而不是说每个业务自己去处理租户。 Rocketmq消息的多租户处理,其核心在生产消息时将租户带上,在消费消息时将租户解析出来放到ThreadLocal里面。

1.首先我们来说,如何在发送消息的时候将租户带上

对DefaultMQProducer进行封装,将租户信息放入到消息的Properties属性中

scss 复制代码
//构建消息,并发送
public void asyncSend(String topic, String tag, Object msgObj, Map<String, Object> properties, SendCallback sendCallback, long timeout) {
    Message message = createMessage(topic, tag, msgObj, properties);
    this.asyncSend(message, sendCallback, timeout);
}


//构建消息
private Message createMessage(String topic, String tag, Object msgObj, Map<String, Object> properties) {
    Message rocketMsg = new Message(topic, tag, serializer.serialize(msgObj));
    if (!CollectionUtils.isEmpty(properties)) {
        rocketMsg.setFlag((Integer) properties.getOrDefault("FLAG", 0));
        rocketMsg.setWaitStoreMsgOK((Boolean) properties.getOrDefault(MessageConst.PROPERTY_WAIT_STORE_MSG_OK, true));
        Optional.ofNullable((String) properties.get(MessageConst.PROPERTY_KEYS)).ifPresent(keys -> rocketMsg.setKeys(keys));
        Optional.ofNullable((Integer) properties.get(MessageConst.PROPERTY_DELAY_TIME_LEVEL)).ifPresent(delay -> rocketMsg.setDelayTimeLevel(delay));
        Optional.ofNullable((String) properties.get(MessageConst.PROPERTY_BUYER_ID)).ifPresent(buyerId -> rocketMsg.setBuyerId(buyerId));
        properties.entrySet().stream()
                .filter(entry -> !MessageConst.STRING_HASH_SET.contains(entry.getKey())
                        && !Objects.equals(entry.getKey(), "FLAG"))
                .forEach(entry -> {
                    rocketMsg.putUserProperty(entry.getKey(), String.valueOf(entry.getValue()));
                });
    }
    return rocketMsg;
}

2.消息消费前,先解析出租户

实现MessageListenerConcurrently和MessageListenerOrderly接口 以MessageListenerOrderly的实现类为例:

kotlin 复制代码
public class DefaultMessageListenerOrderly implements MessageListenerOrderly {

    private final String topic;
    private final String group;

    public DefaultMessageListenerOrderly(String topic, String group) {
        this.topic = topic;
        this.group = group;
    }

    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
        if (EmptyUtils.isEmpty(msgs)) {
            return ConsumeOrderlyStatus.SUCCESS;
        }
        long now = System.currentTimeMillis();
        for (Map.Entry<String, List<MessageExt>> t :
                groupByTenant(msgs).entrySet()) {
            Throwable throwable = null;
            try {
                groupExecutors.ifPresent(p -> {
                    p.forEach(e -> e.execBefore(t.getKey(), t.getValue()));
                });
                if (rocketMQListener instanceof MRocketMQListener) {
                    ((MRocketMQListener) rocketMQListener).onMessage(
                            t.getValue().stream().map(m -> {
                                RocketMessage item = new RocketMessage<>(this.topic);
                                item.setOrigionMsg(m);
                                item.setMsg(doConvertMessage(m));
                                return item;
                            }).collect(Collectors.toList())
                    );
                } else {
                    t.getValue().forEach(p -> {
                        rocketMQListener.onMessage(
                                doConvertMessage(p), p
                        );
                    });
                }
            } catch (BizException yte) {
                throwable = yte;
                if (log.isWarnEnabled()) {
                    log.warn(String.format("%s 消费 %s:%s 发生业务异常", this.getClass().getSimpleName(), this.topic, this.group), yte);
                }

                context.setSuspendCurrentQueueTimeMillis(suspendCurrentQueueTimeMillis);
                return ConsumeOrderlyStatus.SUCCESS;
            } catch (Exception | Error ex) {
                throwable = ex;
                log.error(String.format("%s 消费 %s:%s 发生系统异常", this.getClass().getSimpleName(), this.topic, this.group), ex);
                context.setSuspendCurrentQueueTimeMillis(suspendCurrentQueueTimeMillis);
                return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
            } finally {
                SpringApplicationContext.publishEvent(new MsgConsumeResultEvent(
                        this, new MsgConsumeResult(
                        this.topic, this.group, msgs.size(), System.currentTimeMillis() - now
                ), throwable
                ));
                groupExecutors.ifPresent(p -> {
                    p.forEach(e -> e.execAfter(t.getKey(), t.getValue()));
                });
            }
        }
        return ConsumeOrderlyStatus.SUCCESS;
    }
}


//按租户进行分类
private Map<String, List<MessageExt>> groupByTenant(List<MessageExt> messageExts) {
    Map<String, List<MessageExt>> groupItem = new HashMap<>();

    groupByRules.ifPresent(g -> {
        messageExts.stream().forEach(m -> {
            String groupByKey = g.stream()
                    .map(x -> x.groupBy(m))
                    .filter(f -> EmptyUtils.isNotEmpty(f))
                    .collect(Collectors.joining("-"));
            groupItem.putIfAbsent(
                    groupByKey,
                    new ArrayList<>()
            );
            groupItem.get(groupByKey).add(m);
        });
    });

    /**
     * 当无分组规则时生成默认分组
     */
    if (!groupByRules.isPresent()) {
        groupItem.putIfAbsent(StringPool.EMPTY, messageExts);
    }

    return groupItem;
}

将租户放入到ThreadLocal中

scss 复制代码
Bean
@ConditionalOnBean({TenantResolverService.class})
public MessageGroupExecutor rocketMqTenantMessageGroupExecutor(final Optional<TenantLanguageResolverService> languageResolverService) {
    return new MessageGroupExecutor() {
        public void execBefore(String key, List<MessageExt> messages) {
            if (!EmptyUtils.isEmpty(messages)) {
                messages.stream().filter((n) -> {
                    return n.getProperties().containsKey("tenantId") && EmptyUtils.isNotEmpty(n.getProperty("tenantId"));
                }).findAny().ifPresent((t) -> {
                    String tenantID = t.getProperty("tenantId").toLowerCase();
                    languageResolverService.ifPresent((l) -> {
                        Locale locale = l.resolve(tenantID);
                        SupperLocaleContextHolder.setLocale(locale);
                    });
                    TenantContextHolder.setTenantId(tenantID);
                });
            }
        }

        public void execAfter(String key, List<MessageExt> messages) {
            TenantContextHolder.clear();
            SupperLocaleContextHolder.clear();
        }
    };
}
相关推荐
2401_857622664 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589364 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没5 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch6 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
杨哥带你写代码7 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
AskHarries8 小时前
读《show your work》的一点感悟
后端
A尘埃8 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23078 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
Marst Code8 小时前
(Django)初步使用
后端·python·django
代码之光_19808 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端