亿级流量系统架构设计与实战


亿级流量系统架构设计与实战

这本书更像一篇综述,对企业常见产品的解决方案做了粗略的统计和讲解,涉及的面还是很多的。

唯一ID生成器

设计一个可以生成递增的long类型的唯一ID的生成器

单调递增就是绝对递增,但受限于全局时钟,延迟等分布式系统问题,趋势递增的唯一ID也很受欢迎。(一小段时间可能乱序,但是整体趋势上数据是递增的)

单调递增的唯一ID

Redis的INCRBY命令

比较担心Redis实例宕机后,seq_id键的最新值还未来得及持久化。这样就会生成重复ID了。

基于数据库的自增主键

数据主键,性能不高

数据库的主从结构,如果主从复制采用半同步复制或者MGR(MySQL组复制)的机制,那么主从数据就是一样的。

如果要求多实例,那就是很难做到完全的单调一致,两个数据库实例咋搞。

趋势递增的唯一ID:基于时间戳

雪花算法Snowflake

雪花算法的原理就是生成一个的 64 位比特位的 long 类型的唯一 id。

1:最高 1 位是符号位,固定值 0,表示id 是正整数
41:接下来 41 位存储毫秒级时间戳,2^41/(1000606024365)=69,大概可以使用 69 年。
10:再接下 10 位存储机器码,包括 5 位 datacenterId 和 5 位 workerId。最多可以部署 2^10=1024 台机器。
12:最后 12 位存储序列号。同一毫秒时间戳时,通过这个递增的序列号来区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成 2^12=4096 个不重复 id。
可以将雪花算法作为一个单独的服务进行部署,然后需要全局唯一 id 的系统,请求雪花算法服务获取 id 即可。
对于每一个雪花算法服务,需要先指定 10 位的机器码,这个根据自身业务进行设定即可。例如机房号+机器号,机器号+服务号,或者是其他可区别标识的 10 位比特位的整数值都行。

img

hutool里面也有直接实现。

Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
long id = snowflake.nextId();

趋势递增的唯一ID: 基于数据库

分库分表

如果是不需要扩容的场景,则:

 步长 step = 分表张数

 db_0--
    |-- t_order_0  ID: 061218...
    |-- t_order_1  ID: 171319...
    |-- t_order_2  ID: 281420...
 db_1--
    |-- t_order_0  ID: 391521...
    |-- t_order_1  ID: 4101622...
    |-- t_order_2  ID: 5111723...

美团Leaf

https://tech.meituan.com/2017/04/21/mt-leaf.html

Leaf-segment采用了批量缓存ID的思想,Leaf-snowflake是对Snowflake算法的应用

用户登录服务

用户登录服务要满足如下技术要求

  1. 密码安全:任何时候不可以暴露服务密码
  2. 以多种方式登录: 支持手机号,邮箱,QQ微信第三方登录,扫码登录
  3. 支持登录态登录:多个微服务直接调用

密码保护

HTTPS保护了数据在传输过程中的安全性,密文传输。

非对称加密需要两个密钥:公钥 (public-key) 和私钥(private-key)。公钥和私钥是一对,如果用公钥对数据加密,那么只能用对应的私钥解密。如果用私钥对数据加密,只能用对应的公钥进行解密。因为加密和解密用的是不同的密钥,所以称为非对称加密。

公钥由服务端下发给客户端,用于数据加密;私钥由服务端自己持有,任何经过公钥加密的数据,只能被对应的私钥所破解。

密码加密存储,数据库中存储用MD5这种单向加密后的。这样即使数据库数据泄露,真实密码也不会泄露。

手机号和邮箱登录

有传统的等验证码。还可以接入运营商的SDK,实现本机手机号的一键登录。

第三方登录

主流的第三方登录协议是OAuth2,去调用微信的SDK获取授权码。

单点登录(Single Sign On 简称 SSO)是一种统一认证和授权机制,指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统,不再需要重新登录验证。

实现单点登录的方式有很多种,常见的有基于Cookie、Session共享、Token机制、JWT机制等方式。

登录态管理

Session技术是HTTP状态保持的解决方案,典型应用场景就是登录态。

客户端Cookie:SessionID=xxx; SessionID存储在Redis中。

高频的访问Redis成为IO密集型,使用令牌方案变为计算密集型。

也可以结合两种,为“长短令牌方案”,短令牌就是拿来计算的,长令牌就是存储在Redis里的SessionId。

扫码登录

img

海量推送系统

长连接服务

消息推送场景是服务端给客户端发请求,那么服务端和客户端就要建立长连接。

HTTP是无状态、无连接、单向的应用层协议。HTML5定义了一种全新的通信协议:WebSocket,是一种基于TCP的全双工通信协议,这样客户端和服务端都可以主动将数据推送到另一端。

img

内容发布系统

内容存储

关系型数据库只支持存储文本,不支持图片,视频等文件内容。

其他的非关系型数据库:
内存型KV数据库:Redis

分布式KV存储系统:基于RocksDB存储引擎的上层应用存储系统Cassandra,TiDB等

分布式文件存储系统:Google的GFS,开源的HDFS

分布式文档数据库:MongoDB

分布式对象存储系统:Amazon公司的S3,开源项目Swift等

短文本推荐MongoDB,长文本、视频,图片推荐S3

内容审核

如果不是所有人都有精力审核,那么只审核网络大V,重点人群(浏览量,被举报),还有阿里云的一些文本审核API。

审核应该注意的是打回重新审核,那么就要注意幂等性相关的设置。不能说一个用户审核被拒绝了就不能再发起申请。

内容的全生命周期管理设计

创建,修改,审核(版本控制),删除。

被下架和被删除都应该是软删除,且这两个的概念是不一样的。

内容分发设计

无论是内容的发布,删除,下架还是可变性变更,应该发送内容元信息给MQ。然后各种内容分发渠道都会实时拿到新内容,执行各自的分发逻辑。

img

内容展示设计

内容一般是读多写少的,且发布内容的响应时间是可以容忍的,毕竟还要审核。

对于静态数据,比如长文本、图片,视频,非常适合采用CDN(Content Delivery Network,内容分发网络)来应对高并发的读请求。

CDN就是把静态资源缓存到多个地理位置的边缘服务器上。

将Local Cache和Redis作为多级缓存策略,将Redis主从架构作为多副本策略,两者相互配合可以兜住大多数情况下的高并发读请求。

img

通用计数系统

以作品维度的计数(评论数,点赞数,分享数,转发数,收藏数)为例, 可以使用五个string对象来保存一个作品的计数,如果String对象存储整数,那么Redis底层会用整数编码去存储。当然用Hash也很省空间,使用压缩列表的编码格式,如果我们知道了Hash里面Field的顺序,则可以只存数字,不存Field。

冷热分离,如何在磁盘上存储计数数据?RocksDB存储引擎,是一个基于LSM树的磁盘KV存储引擎。

对于热点数据访问Redis,使用异步写和写聚合。异步写就是不进行实际的计数更新,先把请求放进消息队列后就返回成功了。我们消费者可以从消息队列中批量读取一定数量的请求,然后将这些请求中指向同一个Redis-Key的请求聚合为一个Redis计数更新命令。

img

计数服务是需要大数据统计得到具体数字,比较粗糙,不适合用户钱包这种强一致性精确性的。

同时如果要求特别粗糙,可以考虑redis 的HyperLog做基数统计。

排行榜服务

使用Redis的Zset实现排行榜

更新的幂等性

一般是上游服务调用更新排行榜时,遇到网络超时而选择重试。

在Redis里面存一个分布式唯一ID,过期时间为10Min。如果有该ID则说明是幂等请求,没有的话就可以写redis。

要保证存分布式唯一ID和写Redis是原子性的,用Lua脚本把两个命令合起来。

由于引入了请求ID的过期时间,幂等性被局限为在过期时间内幂等,但是也覆盖了大部分场景了。

然后笔者自己做排行榜的时候,用的是set指令,担心永久丢失一次,用set就能弥补上一次丢失了。

同积分用户排名

用户积分*一个很大的数 + (一个大的时间-当前时间)

然后用ZADD命令重写积分,用户积分也是要计算的,mysql计算也行,redis再加也行,反正要算。

大Key

String类型,长度超过10KB则被认为大KEY

ZSET,Hash,List,Set等类型,成员数量超过10000个也认为是大Key

在读取大Key时,会占用更多的CPU资源和更大的网络带宽;删除大Key时耗时严重;大Key有时也是热点key。

如果用户极多,然后产品也要求实时性,可以搞10个Redis,然后读取前100名,就先读取10个子ZSet排行榜的前100名,然后再聚合筛选出真正的前100名。

前N名用户采用ZSet精确排名,后面用户粗估排名。

后面用户怎么粗估,是直接1000+,还是可以再具体一些,再具体可以用线段树。

线段树不存储每个用户的积分数据,只存储每个积分段的用户数量,并支持使用积分高效查询粗估排名。

比如树的底层是[201-300]分,有31人,你的分数是220分,那么你的排名就是,(300-220)*31/100 = 25名。然后看树的[300,400]有49人,[400,800]有177人,故而最终排名为25+49+177=251人。

Redis的排行榜ZSET使用的是跳表和hash双结构,那么我们也可以搞一个线段树+hash双结构。

不过线段树方案的排名不够精确,且不支持获取排名列表,因为存的东西确实少。

因为线段树无法存储用户积分,所以需要额外的系统去做1000名之后的粗糙排名,粗糙的用计数服务+线段树。

img

img

不过因为在ZSET和计数服务里都存了积分,所以要开个定时任务进行对账。向计数服务看齐。

用户关系服务

Following-关注; Follower-粉丝

建议以Following表为主表,以关注者用户ID为索引;Follower表为伪从,以被关注者用户ID为索引。两个表保持数据一致性,Following表更新后通过binlog同步到Follower表。

建索引的时候可以扩展字段,实现覆盖索引,这样就可以拒绝回表了。

  1. 关注与取消关注,直接更新Following表
  2. 查询用户关注列表,多级缓存
  3. 查询用户粉丝列表,多级缓存
  4. 查询用户关注数、粉丝数,计数服务。不要count,直接单拿出来。
  5. 查询用户关系,考虑批量查询

img

图数据库

Neo4j使用Cypher(CQL)语言执行对图数据库数据的读、写操作。

节点之间只需要建立Follow的单向箭头即可。

Timeline Feed服务

推荐Feed流,关注Feed流,附近Feed流。

拉模式

大V发布内容到自己的用户发件箱,由其他用户拉取内容。

不过如果用户关注的人很多,那就产生了很多的读请求,读扩散。

推模式

某个用户发布了内容后,推送到每个粉丝的用户收件箱中。

不过存储压力大,内容复制100W份,而且也要写100W次,写扩散。

推拉结合模式

普通小用户发布,直接推到粉丝;大V发布,等粉丝拉,然后也可以推送给活跃用户。

技术细节

业务用MQ解耦

img

我们可以将对大量粉丝的推送拆分为多个并行执行的子任务。每个子任务负责对一批粉丝的推送。也就是1个Timeline分发器,3个Timeline执行器。

可以用数据库或Redis-ZSet实现收件箱。

数据库的话要建立idx_feed(user_id,publish_time,content_id)的索引,可以根据user_id进行分库分表。

Redis-Zset的话,也要考虑时间,同一个时间多个用户的member_id;不过member_id是作为字符串的字典序,不合适。要做优化。

formatMember := fmt.Sprintf(%020d”, contentId)

这样627就会变为00000000000000000627

推拉结合的时候,要对多个内容列表进行数据合并,也是只需要关注数据发布时间和内容ID就行。反正索引和Zset都是按照这个排序的。

评论服务

单级模式服务设计

img

对内容本身的评论和对评论的回复处于同一层级。但是一般没有APP会用了。

盖楼模式服务设计

img

盖楼模式下非常适合使用图数据库,边全是reply

二级模式服务设计

img

所有对内容的评论都是一级评论,二级评论由对此一级评论的回复和对回复的回复共同组成。

标准Mysql数据库就要搞一些字段。

level - 1表示一级评论,2表示二级评论

root_id, 表示对哪条发布评论

图数据库也是一种可选的选型。图数据库作为数据库的伪从。

一涉及到排序就想上Redis的ZSet;一涉及到高并发就想异步写和写聚合。

IM服务

Instant Messaging,即时通信

img


文章作者: 爱敲代码の鱼儿
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 爱敲代码の鱼儿 !
  目录