IM即时通讯系统设计(1)——系统总体设计

  • redis存放聊天消息设计

在获取聊天记录的业务中,首先,消息的具体内容是用hash结构,存放在redis中的

key是CHAT_MESSAGE_CONTENT_CACHE+{groupId},

hashKey 是messageId,

value是具体的消息内容。如下图所示

在查询历史记录时,则只会根据查询条件,到数据库查询所有满足条件的messageId,然后根据messageId,一个个获取消息的具体内容。获取操作是,先从redis中获取,如果没有,再查询数据库,查询到结果后就再放到redis中。

   @Override
    public List<ChatMsg> getMessageHistory(Integer count, String groupId, Long endTime) {
        List<ChatMsg> chatMsgHistoryList = new ArrayList();
        List<String> idList = Lists.newArrayList();

        QueryWrapper<ChatMsg> queryWrapper = new QueryWrapper();
        queryWrapper.select("message_id")
//                .last("limit " + count)
                .orderByDesc("created_time").lambda()
                .eq(ChatMsg::getGroupId, groupId)
                .lt(ChatMsg::getCreatedTime, DateUtil.date(endTime));

        if (null != count) {
            queryWrapper.last("limit " + count);
        }

        List<ChatMsg> chatMsgList = list(queryWrapper);
        if (CollectionUtils.isNotEmpty(chatMsgList)) {
            idList = chatMsgList.stream().map(ChatMsg::getMessageId).collect(Collectors.toList());
            //获取消息的具体内容
            for (String messageId : idList) {
                chatMsgHistoryList.add(getByMessageId(groupId, messageId));
            }
        }
        return chatMsgHistoryList;
    }

可以看到getByMessageId方法,就是根据messageId 一个个获取消息具体内容(此处可以优化成一次性获取最好)。然后返回给前端

此处的几点思考:

  1. 为什么不把messageId也缓存?

        因为聊天过程中,用户会不断的发送消息,不断的有新的messageId产生,如果messagId也缓存。会导致在分页获取聊天记录,和更新消息的时候,复杂度大大增加。为了这点性能导致代码复杂度增加太大,不值得。

        因此在获取聊天消息(主要是分页获取聊天记录)的业务中,都是先直接到数据库查询所有符合条件的messagId。注意此处是只查询messageId 这一个字段。这样可以避免数据库查询大量的字段出来,导致性能的损耗,同时又可以保证数据的正确性,避免此处的业务代码太复杂。

  1. 为什么要单独缓存消息的内容呢?

        因为在很多的功能中,比如获取历史记录,获取被合并消息等,都需要根据messageId 获取消息的具体内容,或更新缓存或删除缓存,单独缓存可以很方便的实现改成操作。

        例如某条消息已读了,就直接把缓存中是已读属性直接删掉(只是删除某个属性),下一次查询该属性的时候,发现为空,才查询数据库,也只查询对应的一个字段出来。避免仅仅只是因为某个属性更新了,就得把所有字段都从数据库查询出来。

        同事把对应的属性查询出来,set回缓存内,重新设置过期时间为10分钟。这样可以让查询频率最高的数据尽量久的驻留缓存,降低查询数据库的频率。

        

消息聊天系统MySQL表设计_聊天系统-数据库设计 //常⽤的redis命令 CONFIG SET requirepass "mypass" //Hashmap hset [key] [field] value] hget [key] [field] hgetall [key] //List LPUSH [key] [value] RPUSH [key] [value] LPOP [key] RPOP [key] //删除表头2个值为value的元素 LREM [key] [count > 0] [value] //删除表尾2个值为value的元素 LREM [key] [count < 0] [value] //删除所有值为value的元素 LREM [key] [count = 0] [value] LREM mylist 2 "hello" LREM mylist -2 "hello" //查询0~最后1个元素(不删除) LRANGE mylist 0 -1 BRPOP [key] [timeout] BLPOP [key] [timeout] //例:获取message_1最左的元素,如果没有,等待5秒,超时返回nil BLPOP message_1 5 采⽤Redis进⾏数据存储,主要包括频控、限流、⽤户表、在线⽤户表、聊天消息表(redis list实现消息队列)、好友表(TODO) 频控 CheckFrequency(userId uint64) bool 返回true检查通过,false触发频控 visited_{user_id} >3触发 //频控key前缀 const freqPre string = "visited_" /* Desc: 检查频控key Input: userId Output: False 触发频控/异常 True 正常请求 */ func CheckFrequency(userId uint64) bool { key := freqPre + strconv.FormatUint(userId, 10) resExists, err := Client.Exists(key).Result() if err != nil { return false } if resExists == 0 { //该key不存在 Client.Set(key, 0, 100 * time.Second) return true } res, err := Client.Get(key).Result() if err != nil { return false } resCnt, err := strconv.Atoi(res) if err != nil { return false } if resCnt > 2 {//10秒同⼀userid不能访问超过2次 fmt.Println("触发频控") //... return false } Client.Incr(key) return true } 频控测试 redis.CreateClient() var res bool = true for res { time.Sleep(time.Second * 1) res = redis.CheckFrequency(54508) fmt.Println(res) } 限流 /* 令牌桶算法API */ const LevelFast int = 1 const LevelMedium int = 2 const LevelSlow int = 3 var tokenBucket *ratelimit.Bucket func InitToken(speedLevel int) { var bucketFillDuring time.Duration if speedLevel == LevelFast { bucketFillDuring = 20 * time.Millisecond } else if speedLevel == LevelMedium { bucketFillDuring = 100 * time.Millisecond } else if speedLevel == LevelSlow { bucketFillDuring = 1000 * time.Millisecond } else { //⽇志 fmt.Println("speedLevel只能为Fast, Medium, Slow三个值!") return } var bucketMax int64 = 1//令牌桶最⼤容量,初始也会有这么多个令牌,此处为1是测试所⽤,⽅便看到限流效果,后期需增加⼤⼩ toke
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值