跳至主要內容

【xxx系统】设计核心

holic-x...大约 59 分钟架构系统设计

【xxx系统】设计核心

学习核心

  • 如何设计一个【xxx】系统?(系统的核心功能)
  • 沟通对齐
    • ① 需求分析
    • ② 请求量分析:基于5k/s、5k/s-10w/s、10w/s+的不同请求量场景分析
    • ③ 精准度分析:一般情况下MySQL+缓存、Redis方案,或者多个数据存储结合应用
      • 如果是精准度要求不高,可以采取内存计算、Redis当存储等方式,允许小概率少量数据丢失
      • 如果精准度要求比较高,那么就需要采取更可靠的方式来兜底,比如MySOL
    • ④ 难点分析
  • 整体设计
    • ① 服务设计(分层设计)
    • ② 存储设计(存储选型)
    • ③ 业务设计(业务流程)
  • 要点分析(或难点分析)
    • ① 存储结构:核心数据存储结构分析
      • 存储选型
      • 数据结构设计优化方向
    • ② 高并发:异步化(内存聚合+异步写库、引入MQ组件)
    • ③ 高精准:少写、多写、数据一致性 等
    • .... 围绕功能核心要点展开叙述
  • 总结陈述

学习资料

🚀如何设计一个可靠UDP?

应用层改造:设计一个可靠的UDP,思路就是往TCP上靠(针对现有TCP问题设想的优化)

  • 数据有序性:在应用层增加序列号字段,用来确保udp的数据可以按序接收

  • 传输确认&重试:增加确认号,用来实现超时重传机制,当超过一定时间内没收到已发送数据的确认号,就重传该数据包

  • 开辟缓冲区提升发送速率,基于窗口实现流量控制:在应用层开辟一个缓冲区,用来实现滑动窗口的,有了滑动窗口这样发送数据可以先批量发送数据,不需要等上一个数据的确认了才能发送,提高了发送速率,同时还可以基于滑动窗口实现流量控制,用来保证发送方能按接收方的接收能力发送数据,避免发送的数据对方接收不了而发生数据丢失

  • ④ 拥塞控制:为了保证整个网络的带宽环境,还需要实现拥塞控制,确保发送方的数据,不会占满整个带宽。

udp实现可靠传输相比tcp可靠传输有几点优势:

  • ① 拥塞控制算法自主选择:拥塞控制算法可以根据不同的应用选用不同的拥塞控制算法,而tcp选用拥塞控制算法的时候,是所有应用都使用这一套拥塞控制算法
  • ② 升级扩展方便:tcp是在内核实现的,升级tcp需要升级操作系统,而udp可靠传输是在应用层实现的,升级协议就像升级软件一样简单
  • ③ 可以实现网络连接迁移,在应用层用连接id来唯一标识一个连接,不必像tcp那样,是通过四元组才确定连接的,只要四元祖的信息发生了变化,就需要重新建立连接

🚀如何设计一个抢红包系统?

抢红包系统的核心功能为:发红包(设置红包、发红包)、抢红包(抢红包、拆红包),其中的要点主要集中在高并发解决方案、红包分配算法(拆包算法)

(1)拆包算法(红包分配算法)有两种思路:实时拆分、预先分配

  • (1)随机分配:每个人可以随机获取指定区间范围的红包金额,但其缺点也在于红包分配不均匀
    • 例如10个人抢100元,则第1个人随机金额范围(0,100)(严谨一点就是[0.01,99.91),最低金额为0.01,还要考虑给另外9个人留0.09),第2个人随机金额范围(0,100-i)(i为前面已经发出去的金额)...以此类推剩余最后一个红包则不需要随机(直接拿剩余红包金额即可)
  • (2)线性切割:将总金额类比成一根绳子,对绳子切割N-1刀,每个人可抢到的红包金额等于切割绳子的占比
    • 例如5个人分100元的红包,则需要切4刀(即4个随机线段),如果出现"线段切割点的碰撞"则可通过+1/-1寻找下一个没有碰撞的位置(类似哈希表解决冲突概念)
  • (3)二倍均值法:随机区间范围(0,剩余平均金额的2倍)[0,M/N*2],其中M为剩余红包总金额,N为剩余未抢人数),基于此方案任何人抢占的金额均不会大于人均的2倍
    • 例如5个人抢100元,则第1个人随机区间为(0,100/5*2),第2个人随机区间为(0,80/4*2),依次类推,整体金额符合正态分布(金额在20左右)

(2) 业务架构("三高"方案分析)

① 业务架构图

业务流程:用户通过前端页面访问,随后经由网关微服务进行主机鉴权(如果用户还没登陆认证,则需经由用户认证微服务进行认证(用户认证微服务到用户微服务中查询用户)),用户注册或登录成功之后跳转到聊天室,随后可进行发/抢红包操作;

微服务:其中用户微服务还划分为多个功能微服务模块(图片微服务、账户微服务)用于提供用户信息存储和账户管理功能,聊天室微服务则有红包微服务、历史微服务等用于提供发/抢红包、历史消息存储等功能支持

外部依赖:红包系统外部则以来Nacos注册中心、Redis缓存和Seata-Server(分布式事务服务)

image-20250212154950480

② 核心流程:发/抢红包

发红包:调用发红包API,检查用户余额是否大于红包金额,如果满足则生成一条红包记录保存在数据库中,更新账户余额并用【红包生成算法】将红包放入Redis

抢红包:判断剩余红包个数是否大于0,如果大于0则更新Redis缓存并插入一条抢红包记录,随后执行入账操作;抢红包入账是一个异步操作,采用消息队列实现,worker(即抢红包系统)可以监听MQ并处理(更新账户信息并将记录落库),引入MQ实现三高("高并发、高可用、高可扩展"),MQ处理细节为:先将个人红包记录入库,扣减红包个数和红包金额,用户账户金额增加,成功则返回成功ACK,失败则返回失败ACK

image-20250212160306221

③ 高并发问题

​ 主要是基于高并发场景引入第三方组件可能衍生的一系列问题进行分析,例如引入Redis缓存需要考虑数据一致性、引入MQ需要考虑消息可靠性、消息重复等问题,此处基于超卖、数据一致性、消息可靠性三点切入

(1)超卖问题

超卖:此处的超卖问题指的是不同用户抢红包导致红包为负数或者同一个用户抢到多个红包

  • 不同用户在读请求时候发现商品库存足够然后同时发起请求,进行秒杀操作导致库存为负数;
  • 同一个用户在有库存的时候连续发出多个请求,两个请求同时存在,于是生成多个订单。

​ 解决办法是分布式锁,可以基于Redis(NoSQL数据库)或者zookeeper实现(分布式协调工具),两者对比说明如下(小并发选择zk,性能优先选择redis

  • redis通过设置key有效期防止死锁,zk通过使用会话有效期解决死锁

  • Redis是基于内存操作的NoSQL,ZK需要创建/删除节点,对比之下Redis效率更好。但是Redis有效期不是很好控制(可能会导致有效期延迟),而ZK临时节点有先天可控的有效期,因此ZK更可靠

(2)数据一致性

数据一致性:发红包之后红包服务要将红包放入数据库,红包服务要调用账户服务更新账户数据库,微服务之间远程调用要保证数据一致性,然后还要保证事务不失败。为了保证数据一致性,要用到分布式事务。分布式事务方案可以使用seata,seata使用2pc实现

(3)消息可靠性

消息可靠性:调用红包服务,更新redis,然后利用MQ解耦,更新数据库

​ 使用消息队列保证消息可靠性,有ACK确认机制。生产者ACK可以知道消息是否到达消息队列,消费者ACK之后MQ队列能知道消费者有没有正常消费消息。可以使用重试机制或者消息补偿来保证幂等性。

🚀如何设计一个秒杀系统?

秒杀活动:可以将一个秒杀活动拆解为三个核心阶段(秒杀活动前、秒杀活动开始、秒杀活动后),每个阶段处理的需求不同,其中 Redis 在三个阶段对秒杀场景的支持也不同

​ 秒杀场景核心:三高场景问题(高并发、高可用、高扩展性)、数据一致性(少卖、超卖)、黄牛现象

  • ① 秒杀活动前:CDN缓存 和 浏览器缓存服务请求 足以支撑(此处可以暂时不考虑引入Redis)

  • ② 秒杀活动开始:大量用户点击秒杀,瞬时产生大量并发请求查询库存、请求下单等,需引入Redis和MQ来提升并发处理能力

    • 秒杀核心库存查验、库存扣减、订单处理
    • 借助Redis进行库存管理(库存查验、库存扣减),引入MQ异步处理订单信息(订单处理并扣减数据库库存)
  • ③ 秒杀活动后:秒杀活动结束后,流量已大幅撤离,一般服务器架构能够给以支持。在这个节点,用户可能更加关注的是商品订单信息或者关注捡漏的机会(有其他用户退单时)

对于秒杀活动开启需对系统做一些防范措施,避免秒杀前的无效请求或秒杀开始的瞬时流量冲击,防患于未然

  • (1)秒杀界面CDN:引入CDN(内容分发网络),在秒杀开启前预先将网页的静态资源放在CDN节点,用户刷新界面时直接从CDN获取静态资源,进而降低刷新秒杀界面给服务器造成的压力
  • (2)秒杀按钮优化:避免无效/重复点击
    • 例如用户在秒杀开启前点击按钮,造成很多无用的请求,或者是秒杀开始后多次重复点击造成很多重复的请求
    • 优化方向:通过对秒杀按钮进行限制以避免上述情况给服务器带来的额外压力,例如秒杀开启前【秒杀按钮不可用】、秒杀开始时【限定只能点击一次,点击后按钮进入不可用状态】,但是这种方式仅仅只是通过前端限制请求以限制正常用户的多次无效点击、大大降低请求量,但无法限制通过脚本请求后端的情况
  • (3)秒杀链接优化:用户点击秒杀按钮的时候,前端会通过请求一个固定的URL完成秒杀操作。对于一些有心之人可能会在秒杀前解析这个秒杀链接,进而在秒杀前或者开启时的毫秒级别时间内利用URL请求攻击服务器,不仅给服务带来很大压力,还会造成不公平现象
    • 例如黄牛的存在正是通过编写脚本模拟正常用户请求来达到多抢的目的,抢占了正常用户的机会(商品都被开脚本的人开挂抢走了)
    • 优化方向:将秒杀链接动态化,让用户无法预知秒杀链接的动态生成规则,其具体实现体现在【获取秒杀URL】的接口中返回一个服务器段生成的随机数,后续的下单操作中需要传递该随机数完成下单(让随机数贯穿下单流程,相当于要拿到令牌才可以进行下单,以达到限流的目的)
      • 如果针对懂行的人必然会想着先请求a获取随机数,然后请求b下单带上随机数来攻破流程,此处的方案也是为了防止大多数普通人通过同一个URL访问重复下单的情况
  • (4)秒杀验证码:针对黄牛现象,普通用户还是无法与机器脚本匹敌,因此还需考虑引入机器难以识别的验证码,为下单操作增设一道为人定制的关卡,在增加脚本秒杀的难度的同时也降低了QPS(请求不会在某一刻霎时进来,而是会被分散到验证码填写的时间段内)
    • 实现:用户请求秒杀链接之前增设验证码填写步骤,如果验证码验证失败则拒绝请求
  • (5)过滤请求:为了避免秒杀前后大量请求直接落到秒杀服务器,可以在用户端和服务端添加一层过滤层
    • 实现:使用Nginx服务器构建过滤层
      • 细节:单Nginx无法直接抗100w的请求,假设每个Nginx可以处理10w的请求,那么则需要10台Nginx运作。简单一点的实现可以让每个Nginx服务器只通过前100个请求(后序的请求则直接返回降级页面)。通过Nginx过滤,可以将这100w请求过滤为100*10个请求,以此大大减少了服务端的压力

下述进一步介绍秒杀活动时的场景处理,主要通过引入Redis缓存和MQ消息队列来提升并发处理能力

  • (1)Redis缓存
    • 目的:通过引入Redis缓存,避免瞬时流量直接打入服务器(直接落到数据库),由Redis承载秒杀的库存查验、库存扣减操作(需确保这两个操作的原子一致性,例如通过Lua脚本来确保一次性扣减操作的原子性)
    • 实现:
      • 集群:单机Redis可以处理几万级别的QPS,如果预估的QPS大于几万级别则可引入Redis集群架构模式来增加Redis的处理能力
      • 过程分析:在 Redis 存放和售卖商品数目大小相同的数字,秒杀服务每次访问数据库之前,都需要先去 Redis 中扣减库存,扣减成功才能继续更新数据库。这样,最终到的数据库的请求数目和需要售卖商品的数目基本一致,数据库的压力可以大大减少
  • (2)MQ消息队列
    • 场景:经过上述Redis过滤后的请求,经由Redis进行查验、扣减操作后最终还是要同步到数据库中的,因此这些请求还是需要处理的。但是如果数据库还是无法处理这些请求又该如何?此时则可引入MQ进行"削峰填谷"操作
    • 实现:经过Redis过滤后的请求(经过Redis库存查验、扣减)之后,已经确保该请求需要生成订单数据,因此可以通过异步的方式通知订单服务生成订单并扣减数据库库存

image-20250213162449927

​ 基于上述秒杀前、中、后的优化思路,进一步概述架构图如下所示(摘取核心说明,简化交互)

  • ① 用户通过CDN获取静态资源
  • ② 用户填充验证码信息并校验
  • ③ 用户请求后端获取到秒杀URL
  • ④ 用户点击秒杀URL后正式发起秒杀请求,请求经由Nginx集群进行过滤,完成验证码校验随后落到秒杀服务
  • ⑤ 秒杀服务处理秒杀请求,由Redis集群承载瞬时流量进行库存查验、库存扣减的原子性操作
  • ⑥ 通过Redis查验、库存扣减成功后的请求会进一步进行订单处理操作,秒杀服务将信息发送到MQ,由订单服务拉取并处理订单信息、完成数据库库存扣减操作

image-20250213165916137

🚀如何设计一个分布式ID?

分布式ID核心特性切入

  • 全局唯一:确保主键全局唯一(基础)
  • 单调递增:满足单调递增或者一段时间内递增,主要出于两方面的考虑
    • 数据库写入性能:有序的主键可以保证写入性能
    • 业务考虑:一些场景中会使用主键来进行一些业务处理,比如通过主键排序等。如果生成的主键是乱序的,就无法体现一段时间内的创建顺序
  • 高可用:要求尽可能快地生成正确的ID,以支撑高可用
  • 高性能:要求在高并发的环境表现良好
    • 存储拆分后,业务写入强依赖主键生成服务,假设生成主键的服务不可用(例如出现单点故障问题),订单新增、商品创建等都会阻塞,这在实际项目中是绝对不可以接受的

分布式ID方案

  • 基于本地程序运算:UUID、雪花算法(snowflake)
    • UUID:通过UUID.random()随机生成32个的16进制数(其实现版本有多种,影响因素有时间、网卡MAC地址、自定义namespace等)
      • 优点:生成简单快速,基本不会有性能问题。全球唯一,能够从容应对数据迁移、数据合并、数据库变更等场景
      • 缺点:无序(无法保证趋势递增),一般使用字符串存储(查询效率较低),在海量数据存储场景下读性能明显较弱
    • 雪花算法:生成一个int64的数据(其中41bit作为毫秒数,10bit为机器ID(5bit为数据中心、5bit为及其ID),12bit为毫秒内的流水号(意味着每个节点在每毫秒可生成4096个ID),1bit符号位)
      • 优点:生成ID呈现趋势递增,不依赖于第三方系统,以服务方式部署稳定性更高。生成ID的性能高,且可以根据业务特性灵活分配bit位
      • 缺点:存在时钟回拨问题(强依赖于机器时钟,如果机器上时钟回拨,则会导致号重复或者服务处于不可用状态)
  • 基于常用主键:MySQL自增主键和 Redis 的incr命令
    • MySQL 自增主键(单表生成)
      • 优点:实现简单,通过单表确保ID有序递增
      • 缺点:存在单点故障问题(发号服务不可用会阻塞正常业务),且生成性能无法保证(高并发场景下读表容易成为性能瓶颈)
    • Redis 生成(Redis的incr命令,实现ID顺序递增)
      • 优点:实现简单,整体吞吐量比数据库要高
      • 缺点:需考虑Redis的单点故障(发号服务不可用会阻塞正常业务)和数据持久化问题(如果出现数据丢失则可能导致重复发号)
  • 基于分布式ID生成服务,类似美团的leaf算法(leaf-segmentleaf-snowflake
    • leaf-segment可以看作【MySQL单表自增主键】的一种优化设计
      • 实现:通过从数据库中获取批量自增ID(申请号段)减少对数据库的频繁访问,引入双buffer预加载号段降低系统风险(不用等到号段用尽了之后才更新号段,当号段消耗至一定比例时会预先加载申请新号段)
      • 优点:便于线性扩展,可制成大多数业务场景;容灾性高(Leaf服务内部有号段缓存,即使DB宕机,短时间内也可通过该缓存支撑服务请求)
      • 缺点:ID号码不够随机,存在安全问题(容易泄露发号数量的信息),DB当即会阻塞正常业务(造成整个系统的不可用)
    • leaf-snowflake沿用snowflake的设计,依赖zookeeper生成workerID
      • 实现:ID结构组成(正数位(1bit)+时间戳(41bit)+机器ID(5bit)+机房ID(5bit)+自增值(12bit)),依靠zookeeper生成workID
      • 优点:ID号码是趋势递增的64位数字,符合数据库存储的主键要求
      • 缺点:依赖zookeeper,存在服务不可用风险

🚀如何从总体上设计一个微信步数的系统?

​ 微信步数系统功能分析:用户点开微信步数小程序可以跟踪到自己的个人信息(头像)、步数、排名以及点赞数等,对于步数可以对接第三方服务API来获取,此处则需要关注排名功能实现

  • ① 少量用户排名:用MySQL完全可以实现,根据限定字段order by

  • ② 千万级、亿级用户:

    • 问题分析:无法仅用MySQL实现,也无法基于单机内存操作(单台机器内存不足以支撑,此处的内存操作指的是在程序内存中进行排序操作如果数据量太大就会装不下。但是Redis本质上也是内存操作,巧妙的是借助Redis构建集群方式来解决单机内存不足的问题)

    • 解决方案:使用Redis的有序集合来实现排行榜(步数排行),对于数据的持久化方案,一般用户可能只是关心当日或者前几日的排行榜数据,因此可以不用考虑持久化到MySQL,而是通过设定Redis过期来释放内存

    • 实现细节:

      • 排行榜设计细节(步数和排名)

        • ① key 设计:例如Jack的步数排行榜在redis中的key可以设计为steps:ranking:jack:20240101(表示用户Jack在20240101的排行榜数据)
        • ② value 设计(有序集合设计):value 存储的是用户对应的排行榜数据,因此要记录用户关联的每个好友的步数信息。例如Jack有两个好友(zhangsan、lisi),那么对于Jack而言,其关联的排行数据可以表示为如下有序集合
        scorename
        100zhangsan
        56Jack
        35lisi
        • 获取某日步数排名前50,则可通过指令zrerange steps:ranking:jack:20240101 0 49
      • 排行榜中的用户信息设计细节(用户头像、步数、点赞等)

        • 设计说明:为每个用户的排行榜中的每个好友都构建一个hash结构,用于存储该好友的核心信息

          • 分析:此处应该将用户的信息细化到每个排行榜中都独存一份,因为在不同的排行榜中都有不同的点赞或者其他数据,因此存储基础数据的同时还可独立扩展数据信息存储
        • ① key 设计:steps:ranking:jack:20240101:zhangsan 表示用户Jack在20240101的排行榜中其好友zhangsan的用户信息

        • ② value设计(hash结构设计):

          {
              "picture":"https://pic.com/xxx.png", // 用户头像
              "like_num": 30, // 点赞数
              "steps": 35 // 步数
          }
          
      • 更新时机

        • 基于上述数据存储的相关结构分析,此处要结合业务流程思考数据更新的问题,可以从实时同步和懒更新两个方面切入
          • 因为每个人的步数是实时变更的,每个人都维护了一个有序集和且朋友之间是互相关联的,因此要考虑数据的更新时机
            • 方案①:假设a、b两个互为好友,当a步数更新后同步去更新其在b中的排行榜信息
              • 分析:此方案可行但是不是必须的,因为即便更新之后,b也未必会查看,所以不需要实时去更新关联好友的排行榜信息
            • 方案②:基于懒更新的思路,当a步数更新后更新自己的数据集中的步数,在【查看排行榜】的时候实时拉取刷新一下好友们的步数并同步过来即可。同理,对于上述排行榜中用户信息的数据更新,也只需要在【查看排行榜】同步拉取刷新一下用户信息即可

🚀如何设计用户粉丝关注列表?

1.功能设计

① 核心功能说明

​ 以微博关注页面(relation页)为参考,分析关注列表的核心功能

  • attention页(我的关注):展示该用户关注的所有用户信息
  • follower页(关注我的,即追随者/粉丝列表):展示关注该用户的所有用户信息
  • 功能说明:用户可以管理自己的关注列表(添加关注、取消关注),也可以管理自己的追随者列表(取消某个用户对自己的关注)

② 系统量级(业务特点)

​ 业务特点:典型社交系统的典型特性(大数据量、高并发、非均匀性

  • ① 大数据量
    • 海量的用户数据:亿级的用户数量,每个用户千级的帖子数量,平均千级的follower/attention数量。
  • ② 高并发
    • 高访问量:每秒十万量级的平均页面访问,每秒万量级的帖子发布
  • ③ 非均匀性
    • 用户分布的非均匀:部分用户的帖子数量/follower数量,相关页面访问数量会超出其他用户一到几个数量级(例如一些大V的跟随着量级和普通用户不同)
    • 时间分布的非均匀分布:某个用户可能突然在某个时间成为热点用户,其follower可能徒增数个量级(例如针对一些热点突发事件的处理)

③ 存储设计

​ 基于DB存储,构建用户和关注列表的关系

  • ① 用户信息(tb_user_info):用户ID、用户信息(头像、名称、注册时间、大V认证、手机号等基础信息)
  • ② 关注列表信息(tb_relation):ID主键、被关注者ID(被关注对象,attentionId)、关注者ID(粉丝ID,followerId)

​ 基于上述表设计,可以很容易得到用户信息、关注列表、追随者列表(粉丝列表)

# 1.获取用户信息
select * from tb_user_info where id = "xx"

# 2.获取某个用户关注了哪些用户
select * from tb_attention where followerId = "xx"

# 3.获取某个用户的粉丝有哪些
select * from tb_attention where attentionId = "xx"

④ 优化设计

(1)DB 水平分片

水平拆分

​ 为了应对大数据量的业务需求,因此需要考虑对数据库进行扩展。因为数据涉及到千万、亿级别的用户,且每个用户又有着相应的关联关系,因此考虑水平拆分的策略,将用户数据分散到不同的分片。例如此处根据用户ID进行分片,让同一个用户的数据落在同一个分片上(userId分片的映射关系有多种方式:例如hash取模,基因法userId字段的的几个特殊位、一致性哈希等)

​ 对于tb_attention的数据分片,假设根据followerId(追随者ID)进行拆分,相同followerId的数据会落在同一个分片上

  • 查找我的关注:查找某个用户关注的列表,根据followerId查找,可以在同一个分片上检索到
  • 查找谁关注我:查找某个用户被那些用户关注了,根据attentionId查找,此时同一个attentionId的用户会被分散到不同的分片上,就会导致该场景下查询效率低下

垂直拆分

​ 一般来说是根据两种场景的访问量来决定分片策略,但实际场景中两种场景的检索的访问量是相似的,因此无论是根据followerId还是attentionId分片,总会有一般的场景检索效率低下。因此基于上述问题,可进一步进行垂直拆分,将关注列表进行再分拆,分别用于存储用户的关注表、追随表,随后让两个表基于userId进行水平拆分

  • tb_follower(粉丝表):表示用户关联的粉丝列表 =》主键IDuserIdfollowerId
  • tb_attention(关注表):表示用户关注的对象列表=》主键IDuserIdattentionId
# 1.获取某个用户关注了哪些用户
select * from tb_attention where userId = "xx"

# 2.获取某个用户的粉丝有哪些
select * from tb_follower where userId = "xx"

场景问题分析

​ 虽然基于上述分表策略设计,DB检索性能得到大幅提升,但还需考虑一些特殊的场景应对,下述讨论常见的一些问题分析

(1)热点用户:对于一些用户可能会被很多人关注(这些用户被成为热点用户),因此在tb_follower表中进行count查询的时候需要在userId上扫描的行数仍然存在很多,此处可能会产生2个问题

  • 热点用户的关注者数量展示的操作非常低效
  • 当这些关注者频繁关注热点用户的主页,原本低效的展示操作总是被高频访问,导致性能风险进一步扩大

(2)分页效率低:当一些用户的关注者有很多,无法一页完全展示,因此需要进行分页。而如果仅仅依赖DB的分页操作,扫描效率会随着offset增加而效率低下,此时就会发现越到后面的分页展示就会越来越慢,效率越来越低下

(3)用户详细信息展示效率低:每次展示relation页面的时候,需要对每个followerattention表分别查询关联的用户信息表info,使得info的查询服务能力无法随着info分片线性增加

(2)Redis 缓存

​ 针对上述场景问题分析,此处可以通过引入一些常见的手段(例如Redis缓存)来提升系统处理效率。

数据冗余:例如对于一些摘要信息统计数据的维护可以将数据保存到DB,避免实时通过count进行统计导致DB效率低下(例如每个用户的关注者、被关注者的数量可以存入DB,当需要这些基础信息的时候只需要查找1个userInfo表即可)

缓存:对于一些热点数据,可以通过引入Redis缓存来缓冲高频访问的压力,对于一些重量级的大V账号还可引入二级缓存(服务器本地缓存)来进一步缓冲

​ 引入缓存之后,业务流程也相应做了调整:

  • relation页面摘要信息展示:优先根据从缓存中获取数据,如果没有命中缓存再从DB中检索并载入缓存

  • relation页面详情展示:有followerattention两个子页面

    • 同样的,优先从缓存中获取followerattention相关的数据缓存,缓存中存储的是频繁被查询的热点数据(将数量最多、访问频度最高的用户挡在缓存层,避免流量直接落到DB层)
    • 对于每个用户的info信息,热点用户由于被关注的几率大一些(更容易受到更多用户关注),因此将这类用户的信息缓存到本地缓存中(二级缓存),并为本地缓存设置一个合适的过期时间
  • 数据实时性:考虑到一些场景下数据的频繁更新问题(例如明星塌房,用户纷纷取关),如果应对动态变化的大访问量、实时性也是一个关键挑战。以count操作为例,引入缓存将count数据写入到缓存中,可以引入异步(MQ)操作概念,通过订阅数据变更事件,异步更新DB(需额外考虑数据丢失的一些场景应对方案)

🚀如何设计微博的共同关注?

  • todo 共同关注
  • 推拉模式等等.....

🚀如何设计数据库连接池?

​ 首先理解数据库连接池的核心概念(数据库连接的池化管理,避免频繁创建、销毁数据库连接带来的性能损耗)和 一些核心参数的含义(理解其的场景应用)

  • ① 限制最大连接数:限制连接池中最多可以容纳的连接数目,避免过度消耗系统资源

  • ② 连接均被占用时的处理:当客户请求连接,而连接池中所有连接都已被占用时,一般有两种方式进行处理

    • 思路1(闲时分配):让用户一直等待,直到有空闲连接
    • 思路2(提供临时连接):为用户分配一个新的临时连接
  • ③ 资源回收:当客户不再使用连接,需要把连接重新放回连接池

    • 假定允许的最长空闲时间为十分钟,并且允许空闲状态的连接最大数目为5。那么当连接池中有n个(n>5)连接处于空闲状态的时间超过十分钟时,就应该把n-5个连接关闭,并且从连接池中删除,这样才能更有效的利用系统资源

🚀如何设计一个本地缓存?需要考虑哪些方面?

​ 从多个角度切入:

  • 数据结构:即用什么来构建本地缓存。最简单的思路就是用Map来存储,或者用像用Redis这种提供了多种数据类型的结构(哈希、列表、集合、有序集合等,其底层使用了双端链表、压缩列表、集合、跳表等数据结构)
  • 存储上限:对于本地缓存需设定存储上限(内存有上限),因此一般会指定缓存对象的最大数量阈值(当达到这个阈值之后需要采取一些淘汰策略删除多余的数据)
  • 淘汰策略(清除策略):常用的淘汰策略有LRU(最近最少使用)、FIFO(先进先出)、LFU(最近最不常用)
  • 过期时间(过期策略):给缓存设定一个过期时间,到达过期时间则直接清除
  • 线程安全:像是Redis这种是单线程处理机制,所以不存在线程安全问题。但对于本地缓存而言,往往是可以多个线程同时访问的,所以需要注意对线程安全问题的处理(例如ThreadLocal机制),且线程安全问题不该抛给使用者讨论,须在本地缓存设计的时候加以考虑
  • 接口设计:提供常用的一些API调用方法(例如put、get、remove、clear、getSize等)
  • 持久化设计:对于分布式缓存例如Redis是具有持久化功能的,但是memcached没有持久化功能
  • 阻塞机制:二级缓存提供了一个bocking标识,表示当在缓存中找不到元素时,它设置对缓存键的锁定;这样其他线程将等待此元素被填充,而不是直接命中数据库

🚀分布式集群中如何保证线程安全?

① 串行化

​ 通过串行化处理【可能产生并发问题的操作】,但同时也牺牲性能和扩展性,来满足对数据一致性的要求。

​ 比如分布式消息系统就没法保证消息的有序性,但可以通过变分布式消息系统为单一系统就可以保证消息的有序性了。另外,当接收方没法处理调用有序性,可以通过一个队列先把调用信息缓存起来,然后再串行地处理这些调用。

② 分布式锁

​ 为确保分布式锁的可用性,需从几方面切入:互斥性、防死锁、可重入性、性能

  • 互斥性:即在分布式系统环境下,对于某一共享资源,需要保证在同一时间只能一个线程或进程对该资源进行操作(这是锁设计的基础)
  • 防死锁:具备锁失效机制,防止死锁。即使出现进程在持有锁的期间崩溃或者解锁失败的情况,也能被动解锁,保证后续其他进程可以获得锁
  • 可重入性:即进程未释放锁时,可以多次访问临界资源
  • 性能:有高可用的获取锁和释放锁的功能,且性能要好

​ 以基于Redis实现分布式锁为例,可以从下述几个方面切入一步步优化:互斥性、安全性、对称性、可靠性

  • setnx:确保互斥性
    • 确保同一时刻只有一个线程可以拿到锁资源
  • ② 引入过期机制:确保安全性
    • 设置合理的过期时间:过短(任务还没执行完锁就过期了)、过长(阻塞正常业务数据的获取)
  • ③ 设置owner(谁申请谁释放):确保对称性
    • 在释放锁之前校验锁的归属者,秉承谁申请谁释放的原则。但需注意需确保校验和删除操作的原子性(即get lock校验和del lock校验本身是非原子操作,需引入Redis+Lua脚本确保原子性,避免校验的时候还是自己的锁,删除的时候却成了他人的锁,从而导致误释放问题)
  • ④ 部署架构(解决单点故障问题):确保可靠性
    • 思路1:主从容灾(主从架构、哨兵模式)
    • 思路2:多机部署

🚀扫码登录是如何实现的?

1.功能设计

① 扫码登陆 业务流程分析

可以结合日常生活场景中的wx登陆、qq扫码登陆等流程细节切入,理解业务流程的设计

image-20250218182327350
  • ① PC 端 向登录服务申请二维码ID
    • 产生二维码ID
    • 关联二维码ID和设备信息记录到存储中
  • ② 生成和展示二维码
    • 通过二维码ID调用二维码服务生成二维码URL
    • PC端展示二维码信息
    • 开始轮询二维码状态
  • ③ 移动端扫描二维码拿到二维码ID
  • ④ 移动端发起扫码请求
    • 请求到达服务端,关联二维码ID和用户ID,并更新二维码对应的状态,生成一个临时token
  • ⑤ 移动端确认登录
    • 使用临时token确认登录
    • 生成登录用的pc token
    • 将pc token关联上二维码ID,并更新pc token状态为已激活

​ 如果简化一点,此处对接二维码服务也可以由登录服务去对接,对于PC端来说只需要管登录服务要一个码(由登录服务根据二维码ID对接二维码服务生成二维码并返回url用于展示二维码),然后开始轮询

② 关键/要点分析

关注登录状态同步的状态转移过程和安全性问题

  • 状态转移:理解不同阶段的Redis信息的变化
  • 安全性问题:通过设备绑定、临时token、过期时间等机制来提升扫码登录的安全性

分析说明

  • ① 存储信息的状态变化
    • 【生成时】:初始化二维码状态为【待扫码】
    • 【扫码时】:手机端扫码后二维码状态切换为【待确认】
    • 【确认后】:手机端确认登录后二维码状态切换为【激活】,PC端通过轮询校验二维码状态如果为激活则说明登录成功进入下一步操作
  • ② 安全问题
    • 设备关联设计:将二维码ID与设备进行关联,可以通过校验设备信息来约束token的作用范围
    • 临时token设计:临时token的引入是基于扫码和确认两个步骤分离,需确保扫码和确认是同一个设备以此确保流程流转的安全性
    • 过期时间设计:给token设置过期时间,一方面是为了及时清理存储,另一方面也是通过时间控制机制来减少恶人作恶时间
      • 例如初始化设置码的过期时间为1分钟,扫码确认登录时间控制在2分钟,登录确认后可以将过期时间设置为5分钟以确保PC端可以轮询到登录状态并更新
      • 此处对于token的清理操作可以依赖于过期机制,也可让客户端执行完逻辑之后主动删除

🚀如何实现单点登录?

​ 单点登陆(SSO)指的是在指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分。

​ 相比于单系统登录,SSO 需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,SSO 认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同

(1)单点登陆

SSO 的 多系统登陆认证流程分析如下

image-20250219132256020

  • ① 用户访问系统1资源受限,系统1校验用户未登录,随后跳转到SSO认证中心(下述简称SSO)进行认证
  • ② SSO 发现用户未登录,随后跳转登陆页面进行登陆验证
    • SSO 校验用户信息,创建用户和SSO之间的会话(全局会话),同时创建授权令牌(token)
    • 随后SSO携带令牌跳转到最初的请求地址(访问系统1的地址)
  • ③ 系统1拿到令牌后,到SSO处校验令牌有效性
    • 令牌有效:返回有效标识,注册系统1
    • 令牌无效:重新认证
  • ④ 系统1使用令牌创建与用户的会话(局部会话),返回受保护资源给用户
  • ⑤ 用户此时再去访问系统2受保护资源,类似的,系统2发现用户未登陆则跳转到SSO进行认证
  • ⑥ SSO发现该用户已经登陆,则会返回令牌给系统2
  • ⑦ 系统2拿到令牌,再到SSO处校验令牌有效性,SSO校验发现如果有效则返回有效并注册系统2
  • ⑧ 系统2使用该令牌创建与用户的局部会话,返回受保护资源给用户

​ 用户登录成功之后,会与SSO认证中心及各个子系统建立会话,用户与SSO认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过SSO认证中心,全局会话与局部会话有如下约束关系:

  • 局部会话存在,全局会话一定存在;
  • 全局会话存在,局部会话不一定存在;
  • 全局会话销毁,局部会话必须销毁;

(2)单点注销

​ 单点注销:在一个子系统中进行注销,所有关联的子系统的会话都会被销毁。SSO 认证中心一致监听全局会话的状态,一旦全局会话销毁,监听器将会通知所有注册系统执行注销操作

image-20250219133233646

  • ① 用户向系统1发起注销请求
  • ② 系统1根据用户与系统1建立的会话ID(局部会话ID)拿到令牌,然后向SSO发起注销请求
  • ③ SSO 验证令牌有效,则销毁全局会话,取出所有用此令牌注册的系统地址
  • ④ SSO 向所有注册系统发起注销请求
  • ⑤ 各注册系统接收SSO认证中心的注销请求,销毁局部会话
  • ⑥ SSO引导用户到登陆页面

(3)部署

​ 单点登录涉及SSO认证中心与众子系统,子系统与sso认证中心需要通信以交换令牌、校验令牌及发起注销请求,因而子系统必须集成SSO的客户端,SSO认证中心则是SSO服务端,整个单点登录过程实质是SSO客户端与服务端通信的过程,用下图描述(SSO认证中心与SSO客户端的通信方式有很多,此处以简单好用的httpclient为例,例如还有web service、rpc、restful api等方式均可

image-20250219134230551

🚀10w定时任务,如何高效触发超时?

(1)环形队列法(单timer思路)

原理分析:使用环形队列,每个槽代表一个时间单位,任务根据超时时间放入相应槽中,指针周期性移动并处理当前槽中的任务

​ 如果设定m秒超时,基于环形队列的思路,需要维护一个环形队列和Map集合

  • ① 创建一个index从0到m的环形队列(本质是个数组),环上每一个slot是一个set,存储uid、任务集合
    • 队列大小:可以根据最小时间精度确定环形队列的大小(例如精度为1s,则队列大小可以设为60,每个槽位表示第几秒)
    • 槽位分配:每个槽位对应一个时间点,存储在该时间点超时的任务
  • Map<uid,idx>:定义map用于记录uid落在哪个slot

​ 执行过程分析:

  • 启动一个timer,每隔1s,在上述环形队列中移动一格(以30s超时为例,0->1->2->3..->29->30->0....)有一个Current Index指针(curIdx)来标识刚检测过的slot
  • 当有某用户uid有请求包到达时:
    • 从Map结构中,查找出这个uid存储在哪一个slot里,然后从这个slot的 Set 结构中,删除这个uid
    • uid重新加入到新的slot中(要加入的slotcurIdx所指向的上一个slot,因为这个slot会被timer在30s之后扫描到),并更新Map(即更新uid对应的slotindex值)

哪些元素会被超时掉?curIdx每秒种移动一个slot,这个slot对应的Set中所有uid都应该被集体超时。如果最近30s有请求包来到,一定被放到Current Index的前一个sot了,Current Index所在的slot对应Set中所有元素,都是最近30s没有请求包来到的。因此,当没有超时时,curIdx扫到的每一个slot的Set中应该都没有元素

​ 特点分析:具备通用性,通过循环数组和指针实现高效的任务管理和处理效率

  • 只需要1个timer
  • timer 每1s只需要一次触发,消耗CPU很低
  • 批量超时,curIdx扫到的slot,Set中所有元素都应该被超时掉

(2)最小堆

原理分析:将定时任务按照超时时间进行排序,堆顶对应的是最早超时的任务,定期检查并处理堆顶任务

​ 特点分析:

  • 优点:适用于任务数量较少的场景,实现思路简单
  • 缺点:插入和删除的时间复杂度为O(logN),任务多时性能下降

(3)轮询扫描法

  • 用一个Map<uid,last_packet_time>来记录每一个uid和最近一次请求时间last_packet_time

  • 当某个用户uid有请求包来到,实时更新这个Map

  • 启动一个timer,当Map中不为空时,轮询扫描这个Map,看每个uidlast_packet_time是否超过30s,如果超过则进行超时处理

(4)多timer触发法

  • 用一个Map<uid,last_packet_time>来记录每一个uid和最近一次请求时间last_packet_time

  • 当某个用户uid有请求包来到,实时更新这个Map,并同时对这个uid请求包启动一个timer,30s之后触发

  • 每个uid请求包对应的timer触发后,校验Map中这个uidlast_packet_time是否超过30s,如果超过则进行超时处理

(5)时间轮

​ 通过时间轮算法实现O(1)任务调度,结合分片和异步处理支撑10万级并发,持久化与监控保障可靠性。此方案可扩展至百万级任务,同时保持毫秒级触发精度。实际部署时需根据业务特点调整时间槽大小及分片策略,并通过压测验证系统瓶颈

① 高效数据结构选择:时间轮(Timing Wheel)

  • 原理分析:
    • 将时间划分为多个槽(Slot),每个槽代表一个时间间隔(如1秒)
    • 使用环形数组存储槽,每个槽存放该时间间隔内到期的任务列表
    • 通过指针周期性移动(如每秒前进一格)触发当前槽中的任务
  • 特点:
    • O(1)复杂度:添加/删除任务只需计算对应槽位
    • 批量处理:每个槽中的任务可一次性触发,减少调度开销
  • 多层时间轮优化:
    • 分层设计(可划分小时、分钟、秒级别)用于处理不同精度的任务
    • 当任务到期时间超出当前层范围时,降级到下层存储

② 高效处理思路

  • 任务分片和分布式处理
    • 水平分片:根据任务ID将数据哈希到多个时间轮实例(例如10个分片),分散单机压力。每个分片独立运行,避免单点故障问题
    • 分布式协调:使用zookeeper或者etcd管理分片状态,实现故障转移;当分片宕机时,自动重新分配任务到其他任务节点
  • 异步处理和消息解耦
    • 触发机制:时间轮触发任务后,将任务ID推送至消息队列(如Kafka、RabbitMQ),由消费者异步执行具体业务逻辑(如订单取消),避免阻塞定时系统
    • 流量控制:消息队列设置合理的消费者数量及速率,防止下游服务过载

③ 持久化与容灾

  • 持久化存储
    • 任务创建时,持久化到期时间及元数据到数据库(例如MySQL、Redis)。当系统宕机重启后,重新从数据库中加载未处理的任务到时间轮
  • checkpoint 定期存储
    • 定期保存时间轮指针位置,崩溃恢复时可从最近的checkpoint 继续
  • 指标监控与报警
    • 指标监控:跟踪任务触发延迟、队列堆积、分片负载等关键指标,例如使用Prometheus+Grafana可视化监控
    • 异常处理:设置任务处理超时报警,自动重试或移入死信队列人工干预

④ 性能优化方向

  • 时间槽精度:根据业务需求调整槽间隔,平衡时间精度与性能(如订单超时用1分钟槽,游戏技能用1秒槽)
    • 时间槽太小(时间精度高)容易空转,时间槽太大(时间精度低)容易导致任务触发延迟
  • 懒加载:指针仅在有待处理任务时才推进,减少空转消耗
  • 跳表优化:对高精度需求场景,结合跳表快速定位即将到期任务,减少遍历开销

🚀社区系统的架构

(1)系统拆分

① 服务拆分概念

服务拆分:通过DDD领域模型,对服务进行拆分,将一个系统拆分为多个子系统,做成SpringCloud的微服务。微服务设计时要尽可能做到少扇出,多扇入,根据服务器的承载,进行客户端负载均衡,通过对核心服务的上游服务进行限流和降级改造。

服务改造:一个服务的代码不要太多(大概在1万行左右、两三万至多)。大部分的系统,是要进行多轮拆分的,第一次拆分,可能就是将以前的多个模块该拆分开来了,比如说将电商系统拆分成订单系统、商品系统、采购系统、仓储系统、用户系统等等。但是后面可能每个系统又变得越来越复杂,比如说采购系统里面又分成了供应商管理系统、采购单管理系统,订单系统又拆分成了购物车系统、价格系统、订单管理系统

② 系统架构设计

image-20250219152458001

(2)三高方案

① 缓存机制

​ 常见的缓存机制:CDN、Nginx静态缓存、JVM缓存、本地缓存(内存缓存)、分布式缓存(Redis)

  • CDN(Content Delivery Network 内容分发网络):一种分布式网络架构,用于高效地向用户分发内容(例如网页、视频、图片等),它通过将内容缓存到离用户更近的服务器上,减少延迟、提高访问速度,并减轻源服务器的负载

  • Nginx 静态缓存:利用Java模板thymeleaf可以将页面和数据进行动态渲染,然后通过Nginx直接返回

  • 本地缓存(内存缓存):在二级缓存机制中有相应应用

  • Redis 集群:以10台机器为例(5主5从,5个节点对外提供读写服务)

    • 多主多从、读写分离:假设每个节点的读写高峰QPS可能达到5w/s,5台机器提供的读写QPS支持共为25w/s。32G内存 + 8核CPU + 1T磁盘分配给Redis进程的内存是10g,一般线上的生产环境建议Redis内存分配尽量不要超过10g,超过10g可能会有问题)
    • 高可用:每个主实例都挂了1个从实例以支撑高可用(任何一个主实例宕机,都会自动触发故障转移,Redis从实例会自动成为主节点继续提供读写服务)

② MQ

​ 引入消息队列MQ对微服务系统进行解耦(MQ核心:异步、解耦、削峰平谷),支持微服务扩展的同时可以应对高并发场景下(例如秒杀活动)的写请求

​ 以Kafka为例,其在毫秒延迟基础上可以实现10w级吞吐量针对IOT流量洪峰做了一些特殊的优化,保证消息的及时性同时可以使用消息队列保证分布式系统最终一致性

③ DB 层 - 分治思路(分库分表)

当流量到达数据库层面还是无法抗住高并发的要求时,则需进一步思考【分库分表】方案

分库分表:基于分库分表的思路,将一个数据库拆分为多个库来扛更高的并发(避免单库无法承载大流量),然后将一个表拆分为多个表(降低单表数据量,摊到各个分表中),提高SQL跑的性能(即提升单表处理效率)

场景应用:在通讯录、订单和商城商品模块超过千万级别都应及时考虑分表分库

④ DB 层 - 架构方向(读写分离)

架构分析:对于DB层的高并发支撑设计,还可以从架构方向切入,例如在一些读多写少的场景中,可以引入读写分离架构,分散单库的其中请求,例如可以设定一个主从架构(主库负责写入、从库负责读取),如果读流量非常大的情况下还可进一步设定更多的从库(例如一主多从架构)来支撑高并发读操作

场景应用:例如统计监控类的微服务可以通过读写分离架构进行优化,通过访问从库数据完成数据统计(例如ES)

​ Elasticsearch,简称 es。es 是分布式的,支持灵活扩容机制,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的查询、统计类的操作,比如运营平台上的各地市的汇聚统计,还有一些全文搜索类的操作,比如通讯录和订单的查询

🚀商城系统-亿级商品如何存储?

针对大数据量的数据存储通用方案

  • 分库分表
    • 分库分表策略:基于Hash取模、一致性Hash 实现分库分表
  • 常见问题及解决方案
    • ① 高并发读:引入多级缓存机制应对大促销
    • ② 高并发写:通过 基于Hash取模、一致性Hash 实现分库分表 实现均匀落盘
    • ③ 热 key 读写问题:由于业务分配不均导致的热key读写问题,可以根据业务场景进行range分片,将热点范围下的子key打散
      • 具体实现:
        • 方式1(基因映射):预先设定主键的生成规则,根据规则进行数据的分片路由,但这种方式会侵入商品各条线主数据的业务规则
        • 方式2(路由映射):基于分片元数据服务器(即每次访问分片前先询问分片元服务器在路由到实际分片)不过会带来复杂性,比如保证分片元数据服务器的一致性和可用性

🚀对账系统-分布式事务一致性

一般场景下尽量避免分布式事务,单进程用数据库事务,跨进程则采用消息队列

主流实现分布式系统事务一致性的方案:

  • 最终一致性:也就是基于 MQ 的可靠消息投递的机制
  • 基于重试加确认的的最大努力通知方案

​ 理论上也可以使用(2PC两阶段提交、3PC三阶段提交、TCC短事务、SAGA长事务方案),但是这些方案工业上落地代价很大,不适合互联网的业界场景。针对金融支付等需要强一致性的场景可以通过前两种方案实现

类型特点性能/吞吐能力使用复杂度常见原理常见方案
【强一致性】型需要实现rollback机制受损严重1.2PC
2.XA
(难度高,较少)
① 阿里Seata
② Hmily
【最终一致性】型N/A受损较少中/高1.TCC
2.最大努力通知
3.异步补偿
① 自研补偿方案
② 自研MQ方案
③ RocketMQ
④ 阿里Seata
⑤ Hmily
⑥ 人工介入
  • 本地数据库事务原理:undolog(原子性)+redolog(持久性)+数据库锁(原子性&隔离性)+MVCC(隔离性)
  • 分布式事务原理:全局事务协调器(原子性) + 全局锁(隔离性) + DB本地事务(原子性、持久性)

​ 例如一些公司账单系统和第三方支付系统对账时,会采用“自研补偿/MO方案+人工介入“方式落地,其特点在于方案最“轻”,性能损失最少,且可掌控性好,简单易懂,易维护。考虑到分布式事务问题是小概率事件,留有补救余地就行,但性能的损失可是实打实的反应在线上每一个请求上。结合现有数据了解到业界(比如阿里成熟Seata AT模式),其平均性能会降低35%以上,因此如果不是特殊的场景不推荐这种【强一致性】方案

​ 此外,虽然RocketMQ事务消息听起来好像是挺简单的方案,但它比较挑业务场景,同步性强的处理链路不适合

  • 要求下游MQ消费方一定能成功消费消息,否则转人工介入处理
  • 千万记得实现幂等性

🚀统计系统-海量计数

  • 如何设计一个高性能计数系统?
  • 沟通对齐
    • ① 需求分析:用于提供计数服务的系统(例如微博、B站的点赞、收藏、评论数量等)
    • ② 请求量分析:基于5k/s、5k/s-10w/s、10w/s+的不同请求量场景分析
    • ③ 精准度分析:针对数据统计的精准度,例如数据量较少的场景对数据多少比较敏感则要求数据统计准确,数据量较多的场景则可以接收几条数据丢失的情况
    • ④ 难点分析:高并发、高精准的处理
  • 整体设计
    • ① 服务设计(分层设计):接入层(接口层)、业务服务层(可由业务服务调用计数服务,或者计数服务放在业务服务层)、存储层
    • ② 存储设计(存储选型):MySQL + 缓存、Redis
    • ③ 业务设计(业务流程):一个完整的计数操作业务流程,将服务和存储串联起来
  • 要点分析
    • ① 存储结构:核心数据存储结构分析
      • 基于单行存储、多行存储的设计思路存储数据
        • 单行存储:"分割概念",例如cnt_xxx_xxx_xxx,每个xxx表示一个统计数,用分割符号分开每个统计数据,处理的时候需要拼接和解析操作
        • 多行存储:"一对多概念",一条源数据对应多个统计维度,可根据不同业务类型调整统计目标,相对来说比较灵活
    • ② 异步化
      • 内存聚合,异步写库:通过引入内存缓存的思路来实现批量操作,避免频繁访问数据库
      • 引入异步组件:对于一些请求量比较大的场景,可引入异步化(例如引入MQ)的思路,提升系统整体的并发处理能力,达到削峰平谷的目的
    • ③ 精准度
      • 存储选择:根据不同场景对数据统计敏感度的需求来选择,例如一些体量比较大的数据统计可能并不care极少数据的丢失,因此可以选用Redis来处理,充分利用其高性能的优势;而对于一些对数据统计敏感度要求较高的场景,则是可以选用MySQL来进行存储兜底
      • 少记(数据丢失)
        • 场景分析:如果一个用户发现自己有点赞记录,点赞数却少了,或者关注记录有,关注人数却没增加,这时候用户是非常在意的,所以少记是比较严重的问题。
        • 问题分析:少记容易出现在一些流程异常崩溃,却没补偿机制的场景,比如:没用消息队列,又用了聚合模式,数据先在内存里聚合,再批量刷入磁盘,此时,如果机器挂了就会丢数据,这种模式下,很容易出现少记,所以少记敏感场景要慎用
      • 多记(数据重复处理)
        • 场景分析:多记虽然用户不会觉得很受伤,但是其他用户会觉得不公平,如果被人发现,也是大问题
        • 问题分析:
          • 缺乏业务限制:业务限制一个用户对同一个记录只能点1次赞,如果没做限制就会被疯狂刷赞
            • 对应解决办法可以通过"屏蔽入口+后端校验"来避免出现刷赞的行为,确保业务层一定是一个用户点赞记录的,便于后台可以做检测,同时前端也可以在用户点赞之后屏蔽掉点赞按钮
          • 数据重复消费:
            • 如果用了消息队列,丢数据倒不容易了,这时候要考虑重复消费问题,比如用消息队列,消费了,还没确认就崩溃,服务重启后又会去消费同一笔数据
            • 所以最终兜底需要MySQL增加个记录,来避免重复消费(防止多记最兜底的方式)
      • 缓存一致性:
        • 问题分析:在【MySQL+缓存】的存储选型中,对应读请求(查询计数服务当前数值的请求),可以使用Redis来做一层缓存,为查询提速,也减少存储压力。但与此同时也会带来缓存一致性问题,因此要结合实际业务场景来选择相应的处理策略
        • 解决思路:
          • (1)如果对于一致性要求稍高的服务,在更新存储之后也要去更新一次缓存,再依托一个过期时间兜底就行了
          • (2)如果是一致性要求一般的场景,完全依赖过期时间就行,当然这个时间也别设置得太长
          • (3)如果对于一致性极其苛求的场景,那就不能用缓存了,这时候为了性能存储会更偏向Redis一些(其实这种一致性极其苛求的场景,是比较少的,但也可以拿出来讨论)

🚀系统设计-微软

(1)需求分析

系统量级分析

  • ① 确认使用的对象

    • ToC(To Consumer,面向个人):高并发
    • ToB(To Business,面向企业):高可用
  • ② 系统的服务场景

    • 例如 即时通信(低延迟),游戏(高性能),购物(秒杀活动要求一致性)
  • ③ 用户量级

    • 万级:双机、百万:集群、亿级:弹性分布式
    • 容器化编排架构
  • ④ 数据读写(数据量级)

    • 百万读:3主6从(设定每个节点的读写高峰 QPS 可能可以达到5w/s,基于3主6从的方案就可以实现15w/s写性能,30w/s读性能)
    • 亿级读:进一步通过引入CDN、静态缓存、JVM缓存等多级缓存来提高读并发
    • 百万写:除却读写分离方案,进一步通过消息队列异步写入、削峰填谷,通过hash分拆,水平扩展分布式缓存
    • 亿级写:redis可以定制数据结构、SSD+内存LRU、冷数据异步多线程复制
  • ⑤ 持久化

    • Mysql 承受量约为 1K 的QPS,引入读写分离提升读并发,分库分表提升写并发

功能设计分析

  • 系统的核心功能:
    • 写功能:发送微博
    • 读功能:热点资讯
    • 交互:点赞、关注

(2)架构设计

① 系统核心指标分析

  • 系统性能和延迟:边缘计算、动静分离、缓存、多线程
  • 可扩展性和吞吐量:负载均衡、水平扩展、垂直扩展、异步、批处理、读写分离
  • 可用性和一致性:主从复制、哨兵模式、集群、分布式事务

② 数据存储

  • 键值存储:Redis(热点资讯)
  • 文档存储:MongoDB(微博文档分类)
  • 分词倒排:Elasticsearch(搜索)
  • 列型存储:Hbase、BigTable(大数据)
  • 图形存储:Neo4jj(社交及推荐)
  • 多媒体:FastDFS(图文视频微博)

🚀如何设计一个微博

  • 筛选核心功能:不同的功能有不同的QPS场景需求,下述用低、中、高分别表示QPS需求等级

    • 登陆/注册

    • 新闻推送

    • 发表文章

    • 时间轴

    • 交互:关注/取关 用户

  • 数据/性能量级分析

    • QPS:
      • QPS=100:用一台电脑机器充当Web服务器
      • QPS=1K:一台好点的Web 服务器也能应付,需要考虑单点故障问题
      • QPS=1m:则需要建设一个1000台Web服务器的集群,考虑动态扩容、负载分担、故障转移问题
    • 数据库承载QPS分析:
      • 一台 SQL Database (Mysql)承受量约为 1K的QPS
      • 一台 NoSQL Database(Redis)约承受量是 20k 的 QPS;
      • 一台 NoSQL Database(Memcache)约承受量是 200k 的 QPS;
  • 微服务拆分 (程序=算法+数据结构;系统=服务+数据结构)

    • 模块拆分(服务拆分):根据不同的服务功能拆分微服务模块
    • 存储选择:为不同的服务/应用选用合适的数据存储
  • 数据表设计

    • 用户信息表:主键ID、username、email、pwd
    • 交互表(关注、取关):主键ID、from_user_id、to_user_id
    • 文章表:主键ID、user_id、content(文章内容)、created_at(创建时间)、published_at(发布时间)

​ 基于上述分析,形成大概的解决方案架构,需进一步针对消息队列、缓存、分布式事务、分库分表、大数据、监控、可伸缩等方面进行优化

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3