站内消息设计与实现


http


0x01.About

最近在处理系统消息模块,查阅了很多实践案例,各有针对性。

首先站内消息主要包括:个人消息(评论,点赞),系统消息,订阅消息,私信。

其中,订阅区分用户群,即系统消息是一个特殊的所有人订阅的订阅消息,特点是一对多。

前三个实时性比较低,最后一个实时性高,离线状态下是私信,如果双方在线要转为聊天室,特点是一对一。

那么,接下来,该选个方案了,SQL or NoSQL?



0x02.Mysql实现

首先,对于个人消息、私信(“UserMessage”),一条消息插一句,Mysql跑跑没问题。

对于系统消息或订阅消息,必然不可以,假如有10万用户,一次性那么要插入10万条消息,Mysql必死。

那么就是说,要设立一个系统库(”SystemMessage”),每当用户登录,就去跑跑系统库(“SystemMessage”),把未读的系统库跑到个人库。

关于订阅消息就比较麻烦了,对用户分组?对消息分组?

关系型数据库处理集合问题是比较麻烦的,目前想到的结论是建立一个表(“RssMessage”)存储消息类型,消息索引。

下面列了大致的数据库模型:

Mysql

看完这个数据库设计,我也觉得好难受,吐槽前先来想想为什么吧。

UserSystemRelation表用于记录用户读取到哪个位置的标记。

可以看到,UserMessage与SystemMessage表中,title、tid、ctime、type字段冗余了,好像也没必要,

但是从用户功能上看,当用户登陆后,查找自己站内消息,必然要用到的有:status,必然要显示的有:title、ctime,type作为用户进入消息面板后,要筛选的方式之一,这样的话,Mysql就只要跑一个表就可以完成显示给用户的最新站内消息了。

由于MessageText可能是一个大信息通知,用户查看个人消息时候,并未查看MessageText内容,所以单独放一张表。

相应处理流程

  • 用户登录后,先通过”UserSystemRelation”表查询是否有新的系统消息
  • 如果”UserSystemRelation”,查找到自身uid,同步系统消息到个人消息;如果”UserSystemRelation”未查找到自身uid,直接插入”UserSystemRelation”,并读取最近50条系统消息。
  • 用户点击未读消息,获取”MessageText””,并更新状态(status)为已读。
  • 用户通过”status”、”type”,可以筛选系统消息。



0x03.Mysql+MongoDB实现

由于Mongodb是一种文档型的数据结构,所以,可以考虑把所有数据转成json直接塞给Mongodb。

基于用户的习惯,读多写少,大部分时候都是看到消息,删除、更新比较少,如果数据没更新直接读Mongodb,如果数据更新,直接删除Mongodb
的索引。

这个考虑是在,用户数量很大的时候,要在”UserSystem”表里查找到用户消息比较慢的时候用,类似于吧Mongodb当缓存。



0x04.Redis实现

看了Mysql下站内消息的数据库设计,我也觉得很蛋疼,临时过渡没事,但是还是NoSQL合适。

Redis自带订阅与发布系统,http://redisbook.readthedocs.org/en/latest/feature/pubsub.html

在下图展示的这个 pubsub_channels 示例中, client2 、 client5 和 client1 就订阅了 channel1 , 而其他频道也分别被别的客户端所订阅:

订阅

只要是订阅了相应地频道,就会收到频道的消息。

把用户ID作为频道,私信就是反向的频道订阅,系统消息就是所有用户的订阅,那么离线的消息呢?

1、线上用户

还是存在系统或个人的哈希表里,等上线后再去读取。

在Python中,订阅发布消息(Publish)如下:

1
2
3
4
5
6
7
import redis,time
queue = redis.StrictRedis(host='localhost', port=6379, db=0)
channel = queue.pubsub()

for i in range(100):
queue.publish("test", i)
time.sleep(0.1)

Python中,订阅监听消息(Subcribe)如下:

1
2
3
4
5
6
7
8
9
import redis,time
r = redis.StrictRedis(host='localhost', port=6379, db=0)
p = r.pubsub()
p.subscribe('test')

while True:
message = p.get_message()
if message:
print "Subscriber: %s" % message['data']

Redis-py的API可以看GitHub:https://github.com/andymccurdy/redis-py

这是线上用户做法。

2、线下用户

看过一种做法是建立一个Redis链表,存储登陆用户,当用户登陆就直接发送,没登陆就暂存起来。

这里的话,可以用WebSocket实时监听,定期发送心跳包,如果在线直接返回Redis自带的订阅系统。

系统消息建立一个集合:

SADD system:2015-08-03 7 8 9 10 11

第一段标示系统信息,第二段标示日期,后面的数字标示message id。

个人消息建立一个集合:

SADD user:12345:read 1 2 3 4

第一段标示用户信息集合,第二段标示用户id,下一段标示消息类型为已读,后面的数字标示message id。

关于订阅消息如下:

SADD rss:xiaocao 12 13 14 15

那么你就收到小草的订阅消息,消息ID分别是 12, 13, 14, 15

还有很重要的消息数据存储,

HMSET message:12 title 标题 content 内容 date 2015-08-03

Python创建数据库的例子就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
import redis,time,threading,random
pool = redis.ConnectionPool(host='localhost', port=6379, db=1)
rs = redis.Redis(connection_pool=pool)

rs.sadd("user:123:read", "1", "2")
rs.sadd("user:123:unread", "4", "5", "6")
rs.sadd("system:2015-08-03", "7", "8", "9", "10", "11")
rs.sadd("rss:xiaocao", "12", "13", "14", "15", "11")

for i in range(15):
rs.hset("message:"+str(i), "title", "title=>"+str(random.uniform(1, 99999)))
rs.hset("message:"+str(i), "content","content=>"+str(time.time()))
rs.hset("message:"+str(i), "date", str(time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())))


参考:



本文出自 夏日小草,转载请注明出处:http://homeway.me/2015/08/03/website-system-message/

-by小草

2015-08-03 01:35:10

Fork me on GitHub