0%

业务思考-点赞列表怎么做

在小米有品的工作内容也算是和社交有点关系,会有类似微博的点赞,查看点赞列表的功能。
这个功能看起来简单,其实做起来一点都不容易。
为了避嫌,这里以微博为例,讲一讲自己的思考。
类似的,还有关注列表等。这里就简单思考点赞列表。

功能

微博上,我们可以给一个具体的微博点赞,然后个人中心页面可以查看自己点赞的内容的历史
所以基本功能概括起来如下:

  1. 给微博点赞/取消点赞
  2. 查看是否给该微博点过赞
  3. 查看历史点赞记录

在要应对的数据量比较大情况下,要完全实现上面这三个功能也不容易。尤其是这种很典型的具体冷热属性的数据。
所以会有一些产品妥协策略:

  1. 时间久远的微博,默认返回未点过赞 //这种产品可能会比较同意
  2. 时间久远的微博,点赞记录中找不到 //这种一般不会同意的,放弃吧
    为什么这么妥协会比较好做呢?下面再详细聊聊

下面看看怎么实现

Redis

这个是最简单的实现方式
其实还有更简单的,就是只有Mysql,但是这种一般都不会使用的,除非自己写写应用。

每个用户的点赞列表都存为一个ZSET
Key=weibo:like:${uid}
Value=${weiboId},Score=${Time}

  1. 点赞时加入到ZSET,取消点赞时从ZSET中删除
  2. 查询是否点过赞使用zscore
  3. 历史点赞记录用zrange

注意事项一

没问题吗?
是的,一般来说这么搞就行了,但是其实有个不小的瑕疵。
查询历史点赞记录用zrange。

想象如下的例子:

1
2
3
4
request: {
page: 0,
pageSize: 10
}

好,我们用zrange(key, page, pageSize)返回前十条

我看到自己的前十个点赞记录,卧槽太傻比了,全部取消点赞
ok,我们zrem() * 10次,把zset中前10个记录删除了。

再来请求下一页:

1
2
3
4
request: {
page: 1,
pageSize: 10
}

我们用zrange(key, page, pageSize)返回前十条

发现问题了吗?
第二次zrange的10条,其实是最原始数据的20-30条。
中间有一页的点赞记录因为我们zrem的原因,加载不出来。

这就是用zset做分页的普遍缺点。

怎么解呢?
有个简单的方法,我们用rangeByScore方法,其实参数最大值,是上一页的最小的一个Score
这样,前端每次的请求其实是带上上一页的最小的那个时间戳

1
2
3
4
5
request: {
page: x,
pageSize: 10,
lastTime: 103232
}

这样就可以解决了。

注意事项二

但是还有个问题:
我点赞了微博id=23。
然后这条微博被用户删除了。
那我从zset中拉到这个id,组装数据时会发现id=23查找不到。

这个时候其实有两种选择:

  1. 告诉用户这个点赞内容被删除了,微博就是这么做的
  2. 返回空

返回空其实又带来一个问题
如果我很不巧,第4页的点赞微博都是一个人的,她清空了微博
那请求和响应就会变成这样:

1
2
3
4
5
6
7
8
9
request: {
page: 3,
pageSize: 10,
lastTime: 103232
}

response: {
[]
}

后端返回了一个空数据。

如果这么定义的话,前端会以为已经请求空了,就会告诉用户已经没有数据了。

这个时候其实就出BUG了。

那这个怎么解呢?
很容易想到的就是:
response中带上total字段,前端判断后续有没有数据按照total来。
那其实和注意事项一又冲突了。不好。

还有个解法:

1
2
3
4
response: {
[],
hasNext: true
}

hasNext告诉前端有没有后续数据了
hasNext怎么来呢?
我们从zset中range获取的时候,如果拉出来的个数小于pageSize,那么就是false。
如果等于pageSize,那么就是true。

妥协策略

全存Redis,当然会有问题,数据量太大怎么办?
对于妥协策略1,我们定时的扫我们的Key(或者查询时,插入时异步操作),如果发现有些点赞记录太久远,就把Value删除。
这样我们的Redis负担就小点,
但是对不起,这样其实把妥协策略2也做了,是行不通的。

类Redis数据库

但是又不想抛弃Redis,因为Redis实现起来确实简单啊。
那怎么办?
类Redis数据库来救场了。

类Redis说白了就是兼容Redis的指令,但是存储上,不全存内存,会存到磁盘上。
目前市面上比较流行的类Redis数据库有Pika,SSDB这种
具体笔者也没使用过,就不做评价,简单介绍下
小公司可以自己搭建着玩玩,但是大公司可能就没这个场景了,需要懂这个的运维来支持。

Pika

SSDB

Redis + Mysql

这种比较少见其实,但是好歹这两数据库在公司都是标配。
主要是Redis存热数据,Mysql存冷数据。

写的时候双写
查询的时候先查Redis,Redis查不到再去查Mysql
分页查询的时候,查Redis,过期了就去Mysql捞一部分,然后存回Redis,设置个过期时间。
太久的就直接查Mysql,没必要存Redis了。

但是这里得考虑几个问题:

  1. 这种行为数据,实时写数据库一般不会同意的,可以先写Redis,然后搞个消息队列慢慢写数据库
  2. 查是否给该文章点赞过,先查Redis,如果空了,再查Mysql。可能会出问题,有点隐患,不过也不用太担心,因为在Mysql中的一般就是冷数据库,问题不大。Redis存的容量大一点。
  3. 分页查询点赞历史,先查Redis,到底了去查Mysql,这里切换的衔接逻辑得好好想想。问题也不是很大。

看起来很不错是不是,但是这种方案,最大的问题还是Mysql。
你想想这个表里的数据长啥样?
就几个字段:

  1. id:自增主键
  2. uid:用户id
  3. weiboId:微博id
  4. createTime:点赞时间
  5. del:是否删除了(这个看公司吧,有的只允许逻辑删除)

这表数据太简单了,如果真到微博那种量级,增长速度会很快很快。
假设用户200w,每个人点赞2篇内容,那么一天增长400w条记录,一年就146000w,14亿。
这谁顶得住。

这种其实硬要解还是有点方法:

  1. 压缩表:把字段weiboId,改成weiboIds,一行记录多存几个点赞记录。数据行数可以缩小几个量级,但是插入,查询和Redis衔接起来就比较复杂了。同时删除几乎不好做了。
  2. 分库分表。其实我感觉分库分表意义不大。

妥协策略

来看看这种方案,如果产品妥协了,会不会简单点:
妥协策略1:查是否点过赞,Redis查不到,就默认未点赞,不用去查Mysql了。
妥协策略2:查完Redis,去查Mysql,可以支持。

其实再拓展下,如果产品妥协了策略1,那么写入的时候,只写Redis,然后再在某个时间点,把冷数据同步到Mysql就行。
这样就不用双写数据库了,同时同步的时候可以批量查入。

总结

所以综合来看,功能上,对热点数据的点赞/取消点赞/查询是否点赞比较好
如果你压缩数据行:对冷数据(Mysql中的数据),取消点赞,分页查询点赞记录比较复杂。
如果你不压缩:数据量太大

Redis + Hbase

Redis + Hbase算是比较终极的方案了。
其实笔者对Hbase也不是很了解。
了解了再说吧。