站内消息设计与实现

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)如下:

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)如下:

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创建数据库的例子就是:

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

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
消息聊天系统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、付费专栏及课程。

余额充值