优雅哥今天来说下如何在框架层面解决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();
}
};
}