优雅哥--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();
        }
    };
}
相关推荐
阳光是sunny3 小时前
LangGraph中的Reducer是什么
前端·人工智能·后端
灯澜忆梦4 小时前
GO_并发编程---定时器
开发语言·后端·golang
阳光是sunny4 小时前
从链到图:LangGraph 入门基础全解析
前端·人工智能·后端
皮皮林5514 小时前
开源实力派,给本地 AI 装上深度调研能力,一张 3090 跑到 95.7 分!
后端
努力的小雨5 小时前
把 Windows 桌面图标做成一个会自己运转的小世界
后端
武子康5 小时前
Search Console Platform Properties 扩大 SEO 资产边界:从 Page Ranking 到 Topic Coverage(5 类误读边界 + 3 表数据层设计)
前端·人工智能·后端
腾渊信息科技公司6 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
IT_陈寒7 小时前
Vue这个特性差点让我加班到凌晨,谁懂啊
前端·人工智能·后端
段一凡-华北理工大学7 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
码事漫谈8 小时前
1999年的电商前夜和2026年的AI前夜 历史押韵但从不重复
后端