优雅哥--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();
        }
    };
}
相关推荐
炒空心菜菜7 分钟前
MapReduce 实现 WordCount
java·开发语言·ide·后端·spark·eclipse·mapreduce
wowocpp2 小时前
spring boot Controller 和 RestController 的区别
java·spring boot·后端
后青春期的诗go2 小时前
基于Rust语言的Rocket框架和Sqlx库开发WebAPI项目记录(二)
开发语言·后端·rust·rocket框架
freellf2 小时前
go语言学习进阶
后端·学习·golang
全栈派森4 小时前
云存储最佳实践
后端·python·程序人生·flask
CircleMouse5 小时前
基于 RedisTemplate 的分页缓存设计
java·开发语言·后端·spring·缓存
獨枭6 小时前
使用 163 邮箱实现 Spring Boot 邮箱验证码登录
java·spring boot·后端
维基框架6 小时前
Spring Boot 封装 MinIO 工具
java·spring boot·后端
秋野酱6 小时前
基于javaweb的SpringBoot酒店管理系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端
☞无能盖世♛逞何英雄☜6 小时前
Flask框架搭建
后端·python·flask