im-system 第十章

章节介绍

基本保证

  • 实时性
  • 有序性
  • 可靠性
  • 消息不重复

消息已读&离线消息

  • 消息已读
  • 离线消息

实时性

利用多线程解决消息串行问题,提升处理效率

ChatOperateReceiver在从mq拉取消息时,设置的concurrency为1,是1次拉取1条消息,处理完这条消息,再去拉取下一条消息处理。所以,可以把处理消息的任务放到线程池中执行,充分利用多核cpu的特性。

校验逻辑前置由tcp通过feign接口提前校验

逻辑层在收到tcp层发送过来的消息后,还需要校验消息的发送方和接收方之间的好友关系,当不满足条件时,就放弃处理。这样既浪费了mq资源又浪费处理消息的性能。因此,可以把校验这个步骤放到tcp来做,而逻辑层提供校验接口(也可以考虑使用rpc的方式来调用),由tcp来调用,这样,tcp服务在投递消息之前先调用校验的接口,当成功时,再投递消息。

同时,在提供校验的接口上使用缓存,提高接口的响应速度。

单独使用原生feign

可以参考github中feign项目中的介绍:feign-core
feign与文件上传相关参考这个项目:feign-form

引入依赖
<!-- feign调用依赖 -->
<dependency>
    <groupId>com.netflix.feign</groupId>
    <artifactId>feign-core</artifactId>
    <version>8.18.0</version>
</dependency>
<dependency>
    <groupId>com.netflix.feign</groupId>
    <artifactId>feign-jackson</artifactId>
    <version>8.18.0</version>
</dependency>
声明接口
public interface FeignMessageService {

    @Headers({"Content-Type: application/json","Accept: application/json"})
    @RequestLine("POST /message/checkSend")
    ResponseVO checkSendMessage(CheckSendMessageReq o);

}
使用
feignMessageService = Feign.builder()
                .encoder(new JacksonEncoder())
                .decoder(new JacksonDecoder())
                .options(new Request.Options(1000, 3500)) //设置超时时间
                .target(FeignMessageService.class, logicUrl);

利用mq异步持久化消息

在处理消息的时候,处理流程首先是将消息持久化到数据库中,因此,可以把这部分对db的操作使用mq异步来做。

MessageStoreService#storeP2PMessage

@Transactional
public void storeP2PMessage(MessageContent messageContent) {

     /*
     // 将此处的 存储操作 改为 发送mq异步消息

     // 因为单聊使用的是写扩散, 因此: 需要插入 2份消息, 同时 这2份消息 引用 同一个消息体内容

     // messageContent 转化成 messageBody
     ImMessageBody imMessageBodyEntity = extractMessageBody(messageContent);

     // 插入messageBody
     imMessageBodyMapper.insert(imMessageBodyEntity);

     // 转化成MessageHistory
     List<ImMessageHistoryEntity> imMessageHistoryEntities = extractToP2PMessageHistory(messageContent, imMessageBodyEntity);

     // 批量插入
     imMessageHistoryMapper.insertBatchSomeColumn(imMessageHistoryEntities);
     messageContent.setMessageKey(imMessageBodyEntity.getMessageKey());

     */

     ImMessageBody imMessageBodyEntity = extractMessageBody(messageContent);

     DoStoreP2PMessageDto dto = new DoStoreP2PMessageDto();

     // 设置 消息内容
     dto.setMessageContent(messageContent);

     // 设置 消息体 实体
     dto.setMessageBody(imMessageBodyEntity);

     // 将 消息体的唯一标识 设置给 messageContent
     messageContent.setMessageKey(imMessageBodyEntity.getMessageKey());

     // 发送到名为 storeP2PMessage 的交换机, 消息体是字符串
     rabbitTemplate.convertAndSend(Constants.RabbitConstants.StoreP2PMessage, "", JSONObject.toJSONString(dto));
}

StoreP2PMessageReceiver

store服务接收逻辑层发送过来的mq消息,存储消息到db

@Service
public class StoreP2PMessageReceiver {
    private static Logger logger = LoggerFactory.getLogger(StoreP2PMessageReceiver.class);

    @Autowired
    StoreMessageService storeMessageService;

    /*
        声明了1个 名为 storeP2PMessage 的队列,
        声明了1个 名为 storeP2PMessage 的交换机(默认是直连类型),
        (Direct类型,1个Direct类型交换机可以绑定多个消息队列,每绑定1个消息队列时,需要指定对应的路由key(routeKey),
         当Direct类型交换机收到1个消息时,会根据消息发送时所指定的路由key发送给routeKey完全匹配到的消息队列)
        显然, 这里没有指定路由key, 但是经过测试, 当发送1个消息到这个直连交换机, 并且不指定路由key时,
        会把消息路由到所有绑定给该交换机但是未指定路由key的队列, 就好像是广播一样

    */
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = Constants.RabbitConstants.StoreP2PMessage, durable = "true"),
                    exchange = @Exchange(value = Constants.RabbitConstants.StoreP2PMessage, durable = "true")
            ),
            // 每次拉取1条消息, 处理完成后, 再拉取下一条消息
            concurrency = "1"
    )
    public void onChatMessage(@Payload Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {

        // 获取消息内容(字符串)
        String msg = new String(message.getBody(), StandardCharsets.UTF_8);

        logger.info("CHAT MSG FORM QUEUE ::: {}", msg);

        // 获取 消息投递标识
        Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);

        try {

            // 传过来的msg是 DoStoreP2PMessageDto 对象, 它里面有 messageContent 和 messageBody 属性
            JSONObject jsonObject = JSON.parseObject(msg);

            // 这里是 store服务的 DoStoreP2PMessageDto 对象, 它里面有 messageContent 和 imMessageBodyEntity 属性
            DoStoreP2PMessageDto doStoreP2PMessageDto = jsonObject.toJavaObject(DoStoreP2PMessageDto.class);

            // 取出messageBody属性, 转为 ImMessageBodyEntity 对象, 设置给 store服务的 DoStoreP2PMessageDto 对象
            ImMessageBodyEntity messageBody = jsonObject.getObject("messageBody", ImMessageBodyEntity.class);
            doStoreP2PMessageDto.setImMessageBodyEntity(messageBody);

            // 存储消息(走写扩散逻辑)
            storeMessageService.doStoreP2PMessage(doStoreP2PMessageDto);

            // 确认消息, 不重回队列
            channel.basicAck(deliveryTag, false);

        } catch (Exception e) {

            logger.error("处理消息出现异常:{}", e.getMessage());

            logger.error("RMQ_CHAT_TRAN_ERROR", e);

            logger.error("NACK_MSG:{}", msg);

            //第一个false 表示不批量拒绝,第二个false表示不重回队列
            channel.basicNack(deliveryTag, false, false);
        }

    }
}

可靠性

tcp协议可靠性

tcp协议已经保证了数据传输的可靠性、有序性、幂等性,但保证的是传输层到传输层,而在它的上层仍然有可能因为app闪退等原因而未能接收消息,所以我们仍然需要保证消息的可靠性传输。
在这里插入图片描述
在im-system系统中,userA发消息给userB时,userA将消息发送到im-server,im-server再将消息发给userB。因此,就需要分别保证它们两两之间的可靠性。

双重ack保证上下行消息可靠

这是之前UserA发送给UserB消息的流程图。如下:
在这里插入图片描述

双重ack保证:A消息发送给B,B一定收到了。如下:

过程:A要发送消息给B,首先A将消息发送给IM服务,IM服务将消息持久化后,回ack给A(告诉A发送的消息IM服务已经收到),然后IM服务将消息发送给B,B收到消息后,给IM服务回1个receiver ack(告诉IM服务发送的消息B已经收到了),然后IM服务将B收到消息回个receiver ack给A(告诉A发送的消息B已经收到了)。A对于单条消息收到了2次ack就证明:A发送给B的消息,B一定收到了。如果A没有收到2次ack,那么A会根据重试策略再次发送消息,一直到A收到2次ack为止。假设,此时B不在线,那么就由IM服务回1个服务端的receiver ack给A。
在这里插入图片描述
在这里插入图片描述
代码的流程可以看:

用户A发1个单聊消息给用户B,

用户先将消息发送到tcp服务的NettyServerHandler#channelRead0方法,然后在这个方法中投递消息给逻辑层,逻辑层在ChatOperateReceiver#onChatMessage方法中处理投递过来的消息

接着,逻辑层的ChatOperateReceiver#onChatMessage方法将消息给P2PMessageService#process方法处理,首先会回1个ack给消息的发送方A(通过投递消息给tcp服务,tcp服务接收到该消息后,将消息投递给A),然后将消息同步给发送方的其它端(通过投递消息给tcp服务,tcp服务接收到该消息后,将消息投递给A的其它端),然后把消息发给消息的接收方,会去获取消息接收方的所有在线userSession,如果存在在线的,那么就发给在线的所有端(通过投递消息给tcp服务,tcp服务接收到该消息后,将消息投递给B的所有端),B收到消息后,发送确认消息到tcp服务,就由NettyServerHandler#channelRead0中调用MqMessageProducer#sendMessage将确认消息投递给逻辑层,逻辑层在ChatOperateReceiver#onChatMessage方法中处理这个确认消息,会把消息交给messageSyncService#receiveMark方法处理,这个方法会把确认消息投递给给tcp服务,tcp层的MessageReceiver#startReceiverMessage中收到消息后,将消息投递给发送方A。

如果消息接收方B不在线,那么在逻辑层P2PMessageService#process方法中,会由服务端逻辑层发送1个服务端的确认消息给tcp服务,tcp服务的MessageReceiver#startReceiverMessage中把消息会给消息发送方A

有序性

因为在P2PMessageService#process方法中,收到消息后把消息放入到线程池中执行,那这样的话,由于并行执行的原因,有可能出现后面收到的消息先发向了客户端,那么顺序就乱了。这里提供的办法是后台为每条消息生成1个绝对递增的消息序列号,客户端拿到消息后,就可以根据序列号来排序(如果不使用线程池处理,并且并发处理数也设置为1个,1条消息处理完成后,再处理下1条消息,这样的话就太慢了,不能被接受)

串行执行不会导致消息乱序,但是对于高并发的场景,串行的效率是在是太低了,所以不得不使用并行的方式,所以要寻找一种方案来解决乱序

方案:

  1. 发送时间作为排序的标准,但是客户端的时间是可以自己去修改的,这也就导致了不确定性
  2. 雪花算法:用生成的key去做排序,生成的key是趋势递增的,不是绝对递增的,在一定的场景下,他还是可能导致消息的乱序
  3. 服务端可以用一些手段生成绝对递增的序列号,比如使用redis,但是比较依赖redis的可用性

RedisSeq

如下从redis中获取到绝对递增的序列号后,设置给消息实体对象

@Service
public class RedisSeq {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    public long doGetSeq(String key){
        return stringRedisTemplate.opsForValue().increment(key);
    }


}

消息不重复

因为有重传的机制,所以有可能有网络延迟导致没有收到双重ack,会导致消息重传,最后导致接收方可能收到多条相同的消息,并且相同的消息我们可能会在imserver处理两次(比如说持久化,这样就很不好了),我们要处理这个问题

方案

  1. im服务端搞点文章,比如第一次处理该消息的时候,可以将它缓存到redis中(设置过期时间),当第二次处理的时候,可以从redis寻找这个消息,如果找到了就说明处理过了,所以就不二次持久化了,只用去同步消息即可
  2. 我们也可以在客户端做一些改造,比如说重传的消息都会是同一个messageId(可以当做上面那个查询redis的依据),客户端收到多条messageId的消息,可以过滤掉重复的,只显示一条消息即可
  3. 如果说一条消息,重传了一定的时间段后,还没有收到ack的话,就可以将它放弃了(就像微信没有网络,最后出现一个红色的感叹号),当我们再手动点击红色的感叹号,sdk就会生成一个新的id和旧的消息体,再次去发送

(感觉这里这个可以参考下mqtt的qos服务质量为2的做法,精确保证只收到1次)

MessageStoreService#setMessageFromMessageIdCache

// 将 消息id 存入缓存
public void setMessageFromMessageIdCache(Integer appId, String messageId, Object messageContent) {
    // key的格式: {appId}:cacheMessage:{messageId}, 其中消息id由客户端生成
    String key = appId + ":" + Constants.RedisConstants.cacheMessage + ":" + messageId;
    
    // value的格式: messageContent的json字符串   5分钟内有效
    stringRedisTemplate.opsForValue().set(key, JSONObject.toJSONString(messageContent), 300, TimeUnit.SECONDS);
}

MessageStoreService#getMessageFromMessageIdCache

public <T> T getMessageFromMessageIdCache(Integer appId, String messageId, Class<T> clazz) {
    // key的格式: {appId}:cacheMessage:{messageId}
    String key = appId + ":" + Constants.RedisConstants.cacheMessage + ":" + messageId;
    
    // value的格式: messageContent的json字符串   5分钟内有效
    String msg = stringRedisTemplate.opsForValue().get(key);
    
    if (StringUtils.isBlank(msg)) {
    
        // 缓存中没有, 则返回null
        return null;
    }
    
    // 返回缓存的消息
    return JSONObject.parseObject(msg, clazz);
}

P2PMessageService#process

@Service
public class P2PMessageService {

    private static Logger logger = LoggerFactory.getLogger(P2PMessageService.class);

    @Autowired
    CheckSendMessageService checkSendMessageService;

    @Autowired
    MessageProducer messageProducer;

    @Autowired
    MessageStoreService messageStoreService;

    @Autowired
    RedisSeq redisSeq;

    @Autowired
    AppConfig appConfig;

    @Autowired
    CallbackService callbackService;


    private final ThreadPoolExecutor threadPoolExecutor;

    {
        final AtomicInteger num = new AtomicInteger(0);
        threadPoolExecutor = new ThreadPoolExecutor(
                8, // 核心线程数
                8, // 最大线程数
                60,// 线程存活时间
                TimeUnit.SECONDS, // 时间单位
                new LinkedBlockingDeque<>(1000), // 任务队列
                new ThreadFactory() { // 线程工厂
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread thread = new Thread(r);
                        thread.setDaemon(true); // 设置为守护线程
                        thread.setName("message-process-thread-" + num.getAndIncrement());
                        return thread;
                    }
                });
    }


    public void process(MessageContent messageContent) {

        logger.info("消息开始处理:{}", messageContent.getMessageId());

        String fromId = messageContent.getFromId();
        String toId = messageContent.getToId();
        Integer appId = messageContent.getAppId();

        // 使用key的格式: {appId}:cacheMessage:{messageId} 来从redis缓存中尝试获取缓存的消息
        MessageContent messageFromMessageIdCache = messageStoreService.getMessageFromMessageIdCache(messageContent.getAppId(), messageContent.getMessageId(), MessageContent.class);

        // 如果缓存中已经有数据了, 这证明这个消息已经被处理过了(消息处理完成后, 会将消息id已上面key的格式存入到redis中, 有效时间为5min)
        if (messageFromMessageIdCache != null) {

            // 仅作同步, 不作持久化
            threadPoolExecutor.execute(() -> {

                // 1. 回ack给自己(IM服务告诉客户端逻辑层已收到客户端发送过来的消息)
                ack(messageContent, ResponseVO.successResponse());

                // 2. 发消息给同步在线端
                syncToSender(messageFromMessageIdCache, messageFromMessageIdCache);

                // 3. 发消息给对方在线端
                List<ClientInfo> clientInfos = dispatchMessage(messageFromMessageIdCache);

                // 消息的接收方不在线
                if (clientInfos.isEmpty()) {
                    // 发送接收确认给发送方,要带上是服务端发送的标识
                    receiverAck(messageFromMessageIdCache);
                }
            });

            return;
        }

        // 发送消息前的回调
        ResponseVO responseVO = ResponseVO.successResponse();
        if (appConfig.isSendMessageBeforeCallback()) {
            responseVO = callbackService.beforeCallback(
                    messageContent.getAppId(),
                    Constants.CallbackCommand.SendMessageBefore,
                    JSONObject.toJSONString(messageContent)
            );
        }

        // 如果 发送消息前的回调 返回的不是ok的, 那就直接给消息的发送方回1个ack
        if (!responseVO.isOk()) {
            ack(messageContent, responseVO);
            return;
        }

        // 单聊消息序列号的key的格式: {appId}:messageSeq:{A|B}, 其中A和B之间大的在前面
        long seq = redisSeq.doGetSeq(
                messageContent.getAppId()
                + ":" + Constants.SeqConstants.Message + ":"
                + ConversationIdGenerate.generateP2PId(messageContent.getFromId(), messageContent.getToId())
        );
        // 将 消息序列号 设置给 消息
        // (客户端收到消息后, 根据消息的序列号来给消息排序, 
        //  也就是下面线程池的消息处理过程中有可能后面到的消息先发送给了客户端,
        //  如果一定要按照发送顺序发给客户端, 那就必须串行化处理, 但这样太影响效率了)
        messageContent.setMessageSequence(seq);

        // 前置校验(将原本写在此处的校验, 以接口的形式提供给tcp层调用, 校验不通过的不投递给逻辑层处理, 以减少对mq资源的浪费)
        // 1. 这个用户是否被禁言 是否被禁用
        // 2. 发送方和接收方是否是好友
        // ResponseVO responseVO = imServerPermissionCheck(fromId, toId, appId);
        // if(responseVO.isOk()){
        //    // 1. 回ack给自己
        //    ack(messageContent,responseVO);
        //    // 2. 发送消息同步给自己的在线端
        //    syncToSender(messageContent, messageContent);
        //    // 3. 发送消息同步给对方的在线端
        //    dispatchMessage(messageContent)
        // }else{
        //     //告诉客户端失败了
        //     //ack
        //     ack(messageContent,responseVO);
        // }

        // 因为 前置校验 已经在tcp服务发送消息前就已经做了, 这里就直接处理了, 不用再做校验了
        threadPoolExecutor.execute(() -> {

            // 采用异步 的方式 来持久化消息
            messageStoreService.storeP2PMessage(messageContent);

            OfflineMessageContent offlineMessageContent = new OfflineMessageContent();
            BeanUtils.copyProperties(messageContent, offlineMessageContent);
            offlineMessageContent.setConversationType(ConversationTypeEnum.P2P.getCode());
            messageStoreService.storeOfflineMessage(offlineMessageContent);

            //插入数据
            //1. 回ack成功给自己
            ack(messageContent, ResponseVO.successResponse());

            //2. 发消息给同步在线端
            syncToSender(messageContent, messageContent);

            //3. 发消息给对方在线端(即redis中保存的该用户的所有 在线状态 的 userSession)
            List<ClientInfo> clientInfos = dispatchMessage(messageContent);

            // 做完上面的操作后, 将消息缓存起来
            // (这里其实还是存在问题的: 假设消息第1次过来, 肯定是没有缓存的,
            //   然后采用异步先持久化消息, 然后回ack给消息发送方、同步给消息发送方的其它端、发送消息给消息接收方,
            //   假设这里比较耗时, 消息发送方收到了消息的ack但是未收到receiverAck, 所以消息发送方又会重发,
            //   但是此时第一次的还没缓存呢, 又会走持久化消息的逻辑。在消息接收的客户端方仍然会收到同messageId的消息,
            //   也就是消息重复了, 但是消息接收的客户端方可以据此messageId(messageId由客户端生成)来去重,
            //   后续接收到已经接收过的messageId就忽略它。但是, 数据库中仍然可能会保存重复记录。所以, 应该在收到消息的时候,
            //   在交给线程吃处理之前(因为rabbitmq每次拉取1个, 对这个消息确认之后, 才会拉取下1个消息处理)先去看下缓存中有没有,
            //   有的话就不处理, 没有的话就把messageId缓存下来, 而不是这里的处理方式在处理之后再缓存)
            messageStoreService.setMessageFromMessageIdCache(
                    messageContent.getAppId(),
                    messageContent.getMessageId(),
                    messageContent
            );

            // 如果该用户 没有1个 在线状态 的userSession, 那就发送1个服务端确认消息给发送方,
            // 这样发送方收到此确认ack就意味着消息发送的接收方还没上线
            if (clientInfos.isEmpty()) {

                //发送接收确认给发送方,要带上是服务端发送的标识
                receiverAck(messageContent);
            }

            // 消息发送后的的回调
            if (appConfig.isSendMessageAfterCallback()) {
                callbackService.callback(
                        messageContent.getAppId(),
                        Constants.CallbackCommand.SendMessageAfter,
                        JSONObject.toJSONString(messageContent)
                );
            }

            logger.info("消息处理完成:{}", messageContent.getMessageId());
        });
    }

    // 向 发送该消息的客户端 回ack, 确认逻辑层已收到客户端发送过来的该条消息
    private void ack(MessageContent messageContent, ResponseVO responseVO) {

        logger.info("msg ack, msgId={}, checkResult{}", messageContent.getMessageId(), responseVO.getCode());

        // ack消息体(消息id 和 消息序列号)
        ChatMessageAck chatMessageAck = new ChatMessageAck(messageContent.getMessageId(), messageContent.getMessageSequence());

        responseVO.setData(chatMessageAck);

        // 发消息(发送给某个用户的指定客户端)
        messageProducer.sendToUser(
                messageContent.getFromId(),
                MessageCommand.MSG_ACK,
                responseVO,
                messageContent // 继承自ClientInfo, 携带了clientType, imei号
        );
    }

    public void receiverAck(MessageContent messageContent) {

        // 服务端 对 消息发送方 的确认消息(消息的接收方还未上线)
        MessageReceiveServerAckPack pack = new MessageReceiveServerAckPack();

        pack.setFromId(messageContent.getToId());
        pack.setToId(messageContent.getFromId());
        pack.setMessageKey(messageContent.getMessageKey());
        pack.setMessageSequence(messageContent.getMessageSequence());

        // 服务端 对 消息发送方 的确认消息 的标识
        pack.setServerSend(true);

        // 给消息的发送方回 1个服务端接收的ack(因为消息的接收方还未上线)
        messageProducer.sendToUser(
                messageContent.getFromId(),
                MessageCommand.MSG_RECEIVE_ACK,
                pack,
                new ClientInfo(messageContent.getAppId(), messageContent.getClientType(), messageContent.getImei())
        );
    }

    // 同步消息给发送方在线端
    private void syncToSender(MessageContent messageContent, ClientInfo clientInfo) {
        messageProducer.sendToUserExceptClient(
                messageContent.getFromId(),
                MessageCommand.MSG_P2P,
                messageContent,// 消息体内容
                messageContent // 继承自ClientInfo, 携带了clientType, imei号
        );
    }

    public ResponseVO imServerPermissionCheck(String fromId, String toId, Integer appId) {

        // 检查fromId用户是否被禁用或禁言
        ResponseVO responseVO = checkSendMessageService.checkSenderForbidAndMute(fromId, appId);

        if (!responseVO.isOk()) {
            return responseVO;
        }

        // 检查好友关系
        responseVO = checkSendMessageService.checkFriendShip(fromId, toId, appId);

        return responseVO;
    }

    public SendMessageResp send(SendMessageReq req) {

        SendMessageResp sendMessageResp = new SendMessageResp();

        MessageContent message = new MessageContent();

        BeanUtils.copyProperties(req, message);

        // 1. 插入数据
        messageStoreService.storeP2PMessage(message);

        // 设置消息体唯一标识
        sendMessageResp.setMessageKey(message.getMessageKey());

        // 设置消息发送时间
        sendMessageResp.setMessageTime(System.currentTimeMillis());

        //2. 发消息给同步在线端
        syncToSender(message, message);

        //3. 发消息给对方在线端
        dispatchMessage(message);

        return sendMessageResp;
    }

    // 发消息给对方在线端
    private List<ClientInfo> dispatchMessage(MessageContent messageContent) {
        List<ClientInfo> clientInfos = messageProducer.sendToUser(
                messageContent.getToId(),
                MessageCommand.MSG_P2P,
                messageContent,
                messageContent.getAppId()
        );
        return clientInfos;
    }

}

单聊优化总结

在这里插入图片描述

群聊优化(略)

GroupMessageService

StoreGroupMessageReceiver

StoreMessageService#doStoreGroupMessage

MessageStoreService

聊天会话

聊天会话-消息已读方案

是否实现已读功能还是基于你的业务

方案

  1. 写扩散:我们的消息索引数据有很多份,我们可以给每条消息索引加上一条字段,是否已读的字段,当我们进入聊天界面的时候,可以给服务端上报,将已读的messagekey上报给服务端,服务端根据已读的messagekey修改消息的已读的状态
  2. 读扩散:用一个值来记录群成员读到了哪条消息,这个值前面的消息都算已读,有两个地方可以加上这个值,一个是群成员表,将最后一条消息的seq设置到对应群成员的字段上(还有一种方案,将这个值和会话绑定,比如说群里面有500个成员,就有500个会话,构建一个会话的概念,每一个会话都有自己的一个值来记录已读到哪条数据了,用一张表记录用户和用户之间的已读的消息篇序)

ChatOperateReceiver

MessageSyncService#readMark

ConversationService

聊天会话-会话置顶&删除会话

ConversationService#deleteConversation

ConversationService#updateConversation

离线消息

离线消息使用的是写扩散,每个用户的离线消息

       把每条消息都放到离线消息里面,当用户上线后去里面去拉取离线消息,所以这里在处理单聊消息和群聊消息这里,就可以将离线消息也给存储了

// 存储单人离线消息(Redis)
// 存储策略是数量
public void storeOfflineMessage(OfflineMessageContent offlineMessageContent){
   
    // 找到fromId的队列
    String fromKey = offlineMessageContent.getAppId() + ":"
            + Constants.RedisConstants.OfflineMessage + ":" + offlineMessageContent.getFromId();
    // 找到toId的队列
    String toKey = offlineMessageContent.getAppId() + ":"
            + Constants.RedisConstants.OfflineMessage + ":" + offlineMessageContent.getToId();
    ZSetOperations<String, String> operations = stringRedisTemplate.opsForZSet();

    // 判断 队列中的数据 是否 超过设定值
    if(operations.zCard(fromKey) > appConfig.getOfflineMessageCount()){
   
        operations.remove(fromKey, 0, 0);
    }
    offlineMessageContent.setConversationId(conversationService.conversationConversationId(
                    ConversationTypeEnum.P2P.getCode()
                    , offlineMessageContent.getFromId(), offlineMessageContent.getToId()
            )
    );
    // 插入 数据 根据messageKey 作为分值
    operations.add(fromKey, JSONObject.toJSONString(offlineMessageContent),
            offlineMessageContent.getMessageKey());

    if(operations.zCard(toKey) > appConfig.getOfflineMessageCount()){
   
        operations.remove(toKey, 0, 0);
    }
    offlineMessageContent.setConversationId(conversationService.conversationConversationId(
                    ConversationTypeEnum.P2P.getCode()
                    , offlineMessageContent.getToId(), offlineMessageContent.getFromId()
            )
    );
    // 插入 数据 根据messageKey 作为分值
    operations.add(toKey, JSONObject.toJSONString(offlineMessageContent),
            offlineMessageContent.getMessageKey());
}
// 存储群组离线消息(Redis)
// 存储策略是数量
public void storeGroupOfflineMessage(OfflineMessageContent offlineMessageContent,
                                     List<String> memberIds){
   
    ZSetOperations<String, String> operations = stringRedisTemplate.opsForZSet();
    offlineMessageContent.setConversationType(ConversationTypeEnum.GROUP.getCode());
    for (String memberId : memberIds) {
   
        // 找到toId的队列
        String toKey = offlineMessageContent.getAppId() + ":"
                + Constants.RedisConstants.OfflineMessage + ":" + memberId;

        offlineMessageContent.setConversationId(conversationService.conversationConversationId(
                ConversationTypeEnum.GROUP.getCode()
                , memberId, offlineMessageContent.getToId())
        );
        // 判断 队列中的数据 是否 超过设定值
        if(operations.zCard(toKey) > appConfig.getOfflineMessageCount()){
   
            operations.remove(toKey, 0, 0);
        }
        // 插入 数据 根据messageKey 作为分值
        operations.add(toKey, JSONObject.toJSONString(offlineMessageContent),
                offlineMessageContent.getMessageKey());
    }
}
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值