场景描述:

我们的IM软件有PC端和手机端.

同时在线的用户,通过长连接转发,并且存储消息.

接收方不在线,存储消息.

用户打开电脑端软件或者手机端网络掉线重新连接,都需要获取未读消息数量.当用户点击未读消息的时候,提供消息正文.


经过抽象,JAVA这块需要提供两个接口

1.获取用户的未读消息列表

2.给定发送方ID和接收方ID,返回消息内容.



发送方用户ID  srcid

接收方用户ID  destid

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


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


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



3.将消息放入持久化队列,并且更新redis未读消息列表

首先将(srcid,destid,mid,msg)放入持久化队列.

然后更新redis的用户未读消息列表.


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


在接收方ID的前面增加一个前缀,表明是手机未读消息还是PC未读消息.这个作为key

value是一个HashMap.这个HashMap的key是发送方ID,value是未读消息数量.

一旦用户登录,直接拉取该用户的HashMap内容展示.


这部分内容是有过期时间的,假如用户长时间未使用.这个PC-destid和Mobile-destid的条目将被删除.

如果程序发现这个条目不存在.则去数据库中查询未读消息列表.




假如我的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查询即可.





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


1. createtable chat_msg(
2.     id bigint auto_increment,
3.     srcid bigint  notnull,
4.     destid bigint  notnull,
5.     mid bigint  notnull,
6.     msg TEXT,
7.     ts timestamp  notnulldefaultcurrent_timestamp,
8.     hashvalue tinyintnotnull,
9.     primarykey(id,ts)
10. )partitionby range(UNIX_TIMESTAMP(ts))
11. (
12.     partition p201511VALUES LESS THAN(UNIX_TIMESTAMP('2015-11-01 00:00:00')),
13.     partition p201512VALUES LESS THAN(UNIX_TIMESTAMP('2015-12-01 00:00:00')),
14.     partition p201601VALUES 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. createtable read_chat_mid(
2.     id bigint  primarykey auto_increment,
3.     uid bigintnotnull,
4.     sessionId  varchar(45)notnull,
5.     pcMid bigint  notnulldefault 0,
6.     mobileMid bigint  notnulldefault 0,
7.     hashvalue tinyintnotnull,
8.     ts timestamp  notnulldefaultcurrent_timestampon update current_timestamp
9. );



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


1. createindex inx_1on chat_msg(ts,srcid,destid,mid);
2. 
3. createuniqueindex inx_2on read_chat_mid(uid,sessionId);
4.



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


1. select mid,srcid,destid,msgpb,tsfrom im_0.chat_msg_3where idin(
2.     select t.idfrom(
3.         select t1.idfrom
4.         (
5.             (select id from im_0.chat_msg_3where srcid=1and destid=2
6.             and ts>now()-interval'1' month
7.             and mid>ifnull((select mobileMid from im_0.read_chat_mid_3where sessionId='1,2'anduid=1),0)orderby tsdesc limit 200)
8.             unionall
9.             (select id from im_0.chat_msg_3where srcid=2and destid=1
10.             and ts>now()-interval'1' month
11.             and mid>ifnull((select mobileMid from im_0.read_chat_mid_3where sessionId='1,2'anduid=1),0)orderby tsdesc limit 200)
12.         )as t1orderby iddesc limit 200
13.     )as t
14. )orderby middesc;



系统性能评估



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以上.