优雅哥--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();
        }
    };
}
相关推荐
计算机学姐17 分钟前
基于Asp.net的教学管理系统
vue.js·windows·后端·sqlserver·c#·asp.net·visual studio
Asthenia041224 分钟前
TCP的阻塞控制算法:无控制随便发/固定窗口/慢启动+阻塞避免/快恢复/TCP Cubic/BBR
后端
AntBlack43 分钟前
Python 打包笔记 : 你别说 ,PyStand 确实简单易上手
后端·python·创业
xiaozaq1 小时前
Spring Boot静态资源访问顺序
java·spring boot·后端
熬夜苦读学习1 小时前
库制作与原理
linux·数据库·后端
uhakadotcom2 小时前
Pandas入门:数据处理和分析的强大工具
后端·面试·github
Asthenia04122 小时前
Json里面想传图片(验证码图)-Base64编码来助你!
后端
服务端技术栈2 小时前
MySQL 索引:数据库查询的“加速器”
后端
Asthenia04122 小时前
Redis与MySQL协同:旁路缓存机制
后端
hamburgerDaddy12 小时前
golang 从零单排 (一) 安装环境
开发语言·后端·golang