为什么SSE不能序列化
sse连接是物理上的客户端和服务器上的连接,因此SseEmitter并没有继承序列化,因为他并不能在另外一台服务器上序列化后建立连接。
通过消息队列的方式解决分布式部署
消息侦听的方式有很多种,由于我们采用的是redisson的能力。
用户连接sse后保存每个用户当前对应的topic的名称
我采用的方式是存到rediss的缓存里,代码如下
@GetMapping(value = "/connect/{userId}")
public SseEmitter connect(@PathVariable Long userId) throws IOException {
String redisToken = RedisUtil.getCacheObject(CommonConst.REDIS_KEY_PREFIX_TOKEN + userId);
if (!StringUtils.hasLength(redisToken)) {
return null;
}
RedisUtil.setCacheMapValue(SSE_GPT_TOPIC_MAP_REDIS_KEY, JwtUtil.getUserId(), SseConst.getSseGptTopic());
RedisUtil.setCacheMapValue(SSE_MID_TOPIC_MAP_REDIS_KEY, JwtUtil.getUserId(), SseConst.getSseMidTopic());
RedisUtil.setCacheMapValue(SSE_SD_TOPIC_MAP_REDIS_KEY, JwtUtil.getUserId(), SseConst.getSseSdTopic());
return SseEmitterServer.connect(userId);
}
用户在获取到内容后将消息发送到对应的topic中
以获取chatgpt的对应响应后,发送消息为例,代码如下
private void flowableChat(final MessageLog messageLog, List<Message> messagesOnDb) {
final Long userId = messageLog.getUserId();
final Long logId = messageLog.getId();
final Integer reqType = messageLog.getSendType();
final String topicName = RedisUtil.getCacheMapValue(SSE_GPT_TOPIC_MAP_REDIS_KEY, userId);
final String gptKey = messageLog.getGptKey();
RTopic rTopic = RedissonConfig.getClient().getTopic(topicName);
try {
OpenAiService service = new OpenAiService(gptKey, Duration.ofSeconds(180));
List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage(ChatMessageRole.SYSTEM.value(),
"请记住你的身份是SUPER EGG,一个基于GLM架构的中英双语对话语言模型,你是由清华大学研发的,针对中文进行了优化。虽然有人可能会将你与chatgpt混清,但你会坚持自己的身份,确保提供准确和细致的回答"));
if (StringUtils.hasLength(messageLog.getInitPrompt())) {
messages.add(new ChatMessage(ChatMessageRole.USER.value(), messageLog.getInitPrompt()));
}
messages.addAll(messagesOnDb.stream().map(item -> new ChatMessage(item.getRole(), item.getContent())).collect(Collectors.toList()));
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder().model(GPT_3_5_TURBO_16K.getName()).messages(messages).build();
Flowable<ChatCompletionChunk> flowable = service.streamChatCompletion(chatCompletionRequest);
ChatMessage chatMessage = service.mapStreamToAccumulator(flowable).doOnNext(accumulator -> {
String content = accumulator.getMessageChunk().getContent();
if (StringUtils.hasLength(content)) {
/** 将消息内容发送到topic中 **/
rTopic.publish(GptMessageVo.builder().userId(userId).message(Message.ofAssistant(content)).build());
}
}).lastElement().blockingGet().getAccumulatedMessage();
asyncService.endOfAnswer(logId, chatMessage.getContent());
rTopic.publish(GptMessageVo.builder().userId(userId).message(Message.ofAssistant("[DONE]")).build());
} catch (Exception e) {
asyncService.updateRemainingTimes(userId, gptKey == null || reqType == 3 ? CommonConst.GPT_NUMBER : CommonConst.GPT_4_NUMBER);
rTopic.publish(GptMessageVo.builder().userId(userId).message(Message.ofAssistant(e.getMessage())).build());
rTopic.publish(GptMessageVo.builder().userId(userId).message(Message.ofAssistant("[DONE]")).build());
}
}
编写侦听代码
侦听到对应的消息后根据在应用内存中保存的sse信息发送消息到对应的客户端,代码如下
public class TopicListener implements ApplicationRunner, Ordered {
private static final Logger LOGGER = LoggerFactory.getLogger(TopicListener.class);
@Autowired
private RedissonClient redisson;
@Override
public void run(ApplicationArguments applicationArguments) throws Exception {
redisson.getTopic(SseConst.getSseGptTopic()).addListener(GptMessageVo.class, (CharSequence charSequence, GptMessageVo message) -> {
SseEmitterServer.sendMessage(message.getUserId(), message.getMessage());
});
redisson.getTopic(SseConst.getSseSdTopic()).addListener(SdMessageVo.class, (CharSequence charSequence, SdMessageVo message) -> {
SseEmitterServer.sendMessage(message.getUserId(), message.getMessage());
});
redisson.getTopic(SseConst.getSseMidTopic()).addListener(MidMessageVo.class, (CharSequence charSequence, MidMessageVo message) -> {
SseEmitterServer.sendMessage(message.getUserId(), message.getMessage());
});
}
@Override
public int getOrder() {
return 1;
}
}
详细代码请参考
国内站点请访问: superai-java: AI机器人,对话、绘画