转载

即时聊天(IM)存储方案

单位即时聊天改版了.
正好趁着这次机会踢了MongoDB.
大家都拍手叫好..可见MongoDB这玩意多么的不得人心...都觉得不好用..

场景描述:
我们的IM软件有PC端和手机端.
同时在线的用户,通过长连接转发,并且存储消息.
接收方不在线,存储消息.
用户打开电脑端软件或者手机端网络掉线重新连接,都需要获取未读消息数量.当用户点击未读消息的时候,提供消息正文.

经过抽象,JAVA这块需要提供两个接口
1.获取用户的未读消息列表
2.给定发送方ID和接收方ID,返回消息内容.

即时聊天(IM)存储方案

发送方用户ID  srcid
接收方用户ID  destid
每个会话的当前消息ID  mid(针对每个发送方和接收方,自增)

1.发送方用户通过电脑将消息发送至Web服务器.消息主要内容(srcid,destid,msg)

2.Web服务器根据srcid,destid获取会话的mid(会话状态的自增ID)

即时聊天(IM)存储方案

3.将消息放入持久化队列,并且更新redis未读消息列表
首先将(srcid,destid,mid,msg)放入持久化队列.
然后更新redis的用户未读消息列表.

未读消息列表在Redis的存储结构如下.

在接收方ID的前面增加一个前缀,表明是手机未读消息还是PC未读消息.这个作为key
value是一个HashMap.这个HashMap的key是发送方ID,value是未读消息数量.
一旦用户登录,直接拉取该用户的HashMap内容展示.

这部分内容是有过期时间的,假如用户长时间未使用.这个PC-destid和Mobile-destid的条目将被删除.
如果程序发现这个条目不存在.则去数据库中查询未读消息列表.

即时聊天(IM)存储方案

假如我的ID是1000,老板的ID是1001,老板给我发送了三条消息,但是我一直没有在线.
程序将做如下操作.
127.0.0.1:6379> HINCRBY pc-1000 1001 1
(integer) 1
127.0.0.1:6379> HINCRBY pc-1000 1001 1
(integer) 2
127.0.0.1:6379> HINCRBY pc-1000 1001 1
(integer) 3

127.0.0.1:6379> HINCRBY mobile-1000 1001 1
(integer) 1
127.0.0.1:6379> HINCRBY mobile-1000 1001 1
(integer) 2
127.0.0.1:6379> HINCRBY mobile-1000 1001 1
(integer) 3

如果我登录手机客户端,点击未读消息.则对Redis操作如下,删除对应条目
127.0.0.1:6379> hdel mobile-1000 1001
(integer) 1
127.0.0.1:6379> 
并且回写数据库一个状态标识(已读的最大mid).这个回写数据库的过程,也是异步的,每隔30s回写一次数据库状态.


这个时候,我登录PC端,还是可以查看未读消息数量的.这个业务要求有点奇怪.但是要求就是这么做.

4.如果发送方的其他设备在线,或者接收方的设备在线,则转发消息.

5.JAVA从队列中异步获取消息,然后批量Insert到数据库.


数据库存储设计
初始设计使用4台MySQL分库.使用(发送方用户ID+接收方用户ID) mod 64
这样的好处是,A用户发送B用户和B用户发送A用户的消息,都会落在同一个底层数据库.
这样获取A,B之间的聊天内容,使用一个SQL查询即可.

即时聊天(IM)存储方案


聊天消息表,本身也是按照时间进行分区

  1. create table chat_msg(
  2.     id bigint auto_increment,
  3.     srcid bigint not null,
  4.     destid bigint not null,
  5.     mid bigint not null,
  6.     msg TEXT,
  7.     ts timestamp not null default current_timestamp,
  8.     hashvalue tinyint not null,
  9.     primary key (id,ts)
  10. )partition by range(UNIX_TIMESTAMP(ts))
  11. (
  12.     partition p201511 VALUES LESS THAN(UNIX_TIMESTAMP('2015-11-01 00:00:00')),
  13.     partition p201512 VALUES LESS THAN(UNIX_TIMESTAMP('2015-12-01 00:00:00')),
  14.     partition p201601 VALUES LESS THAN(UNIX_TIMESTAMP('2016-01-01 00:00:00'))
  15. );

用户状态表
sessionId是聊天双方,(较小的ID在前,较大的ID在后) 的会话状态信息
pcMid是用户pc端已读的最大消息ID
mobileMid是用户手机端已读的最大消息ID
hashvalue是(发送方用户ID+接收方用户ID) mod 64 计算后的值.暂时没有用.

  1. create table read_chat_mid(
  2.     id bigint primary key auto_increment,
  3.     uid bigint not null,
  4.     sessionId varchar(45) not null,
  5.     pcMid bigint not null default 0,
  6.     mobileMid bigint not null default 0,
  7.     hashvalue tinyint not null,
  8.     ts timestamp not null default current_timestamp on update current_timestamp
  9. );

获取用户未读消息的SQL如下.

  1. create index inx_1 on chat_msg(ts,srcid,destid,mid);

  2. create unique index inx_2 on read_chat_mid(uid,sessionId);

查询用户1,2之间的聊天内容

  1. select mid,srcid,destid,msgpb,ts from im_0.chat_msg_3 where id in (
  2.     select t.id from(
  3.         select t1.id from
  4.         (
  5.             (select id from im_0.chat_msg_3 where srcid=1 and destid=2
  6.             and ts>now()-interval '1' month
  7.             and mid>ifnull((select mobileMid from im_0.read_chat_mid_3 where sessionId='1,2' and uid=1),0) order by ts desc limit 200)
  8.             union all
  9.             (select id from im_0.chat_msg_3 where srcid=2 and destid=1
  10.             and ts>now()-interval '1' month
  11.             and mid>ifnull((select mobileMid from im_0.read_chat_mid_3 where sessionId='1,2' and uid=1),0) order by ts desc limit 200)
  12.         ) as t1 order by id desc limit 200
  13.     ) as t
  14. ) order by mid desc;

系统性能评估

MySQL基本配置
innodb_buffer_pool_size=512m
innodb_flush_log_at_trx_commit =0
sync_binlog=0
innodb_support_xa=0
log_bin=master

下面是工作环境测试,在生产服务器上使用raid10,IO能力还会进一步提升.
每个月每台数据库服务器 在200w记录之前,使用Load File接口,每秒Insert可以达到 1.7w左右
每个月每台数据库服务器 在800w记录之前,使用Load File接口,每秒Insert可以达到 1.1w左右
可以认为系统每秒Insert至少在4w以上.
理论上的系统最大处理能力至少在每秒64w以上.

仅仅是四台数据库的情况,对于我们的业务.5年之内应该都不用考虑扩容的问题.

让DBA做架构设计的好处是,不会把坑留在数据库层面坑自己...
即时聊天(IM)存储方案
正文到此结束
Loading...