系统设计面试题整理(附答案,持续更新)

12,306 阅读12分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

国内的面试里,很少考系统设计相关的题目,但学习一些系统设计方面的知识点,可以帮助我们对日常开发有更深的理解,系统设计的面试题比较难总结,一方面涉及的知识点比较广,另一方面笔者的很多知识点也只停留在理论,并未实践过,总结的内容难免会有偏颇,仅供参考。

本文共分为常见系统设计题目整理、开发经验篇、分布式篇、系统设计篇、性能优化篇、安全篇.

常见题目整理

  1. 微信抢红包功能设计
  2. 设计一个限流工具,每5s允许一个请求,多的请求丢弃
  3. 会问有没有消息队列 分布式 缓存 一系列后端架构的经验
  4. 微博等热门评论,在分页到很深的时候,如何进行优化
  5. 如何实现音乐随机播放
  6. 服务器降级方案,如何指定降级优先级
  7. 如何找项目性能瓶颈
  8. 有多少种类型的缓存, 从客户端到服务端
  9. 是否遇到分布式事务,有哪些解决方案,
  10. 分布式锁怎么做,用redis 实现分布式锁有哪些步骤
  11. im 里面群聊消息体设计,如何设计xx 人已读xx 人未读
  12. 如何设计秒杀系统,应该关注什么:防恶意提交、NG 限流等
  13. 微服务的某个节点挂了怎么办

开发经验篇

防止重复提交

  • 没有响应之前,禁用前端页面的提交按钮
  • 用户刷新页面,或者用url提交请求,就能绕过重复提交
  • 前端防止重复提交可以降低后台服务器的负载
  • 对于重要业务,可以使用token避免重复提交,把token存储到httpSession

数据缓存

后台系统的缓存可以分为数据库缓存、持久层缓存、业务层缓存

无论哪种类型的缓存,都是为了提升数据读写的速度

数据库性能

  • MySQL每秒可以处理5000次读取,或者3000次写入
  • 响应时间通常在10ms以内,但是在1万并发的时候,要保证10ms以内的速度,任何数据库都做不到

数据库缓存之查询缓存

如果开启了查询缓存,数据库会把select语句的查询结果保存到缓存中

query_cache_size=2048M # 缓存大小 query_cache_type=ON # 开启缓存

如果添加、修改或者删除数据,会导致大量的缓存失效,增大IO负载,所以不建议开启查询缓存

持久层缓存

  • Hibernate和MyBatis框架都支持缓存技术,具体的还分为一级缓存和二级缓存
  • 一级缓存是会话级别缓存,Session关闭,缓存就会失效
  • 二级缓存就是要引入存储机制缓存数据

业务层缓存

  • 为了缓存一些业务数据,可以采用业务层缓存
  • 例如购物车数据,头条新闻,秒杀商品的库存,以及公告通知等等

应用程序——Redis

  • 缓存数据持久化可以抵御系统重启之后刺穿缓存的问题
  • Redis默认采用RDB持久化方案,以快照的方式持久化内存的数据,但是并不能实时保存数据,所以适合持久化普通数据
  • AOF模式会把数据实时保存到硬盘上面,适合重要数据持久化

缓存数据一致性

延迟双删 www.cnblogs.com/rjzheng/p/9…

分布式篇

分布式锁

基于数据库实现分布式锁

  • 悲观锁
    • 优点:简单方便、易于理解、易于操作
    • 缺点:并发量大时,对数据库压力较大
    • 建议:作为锁的数据库与业务数据库分开

基于Redis的Setnx实现分布式锁

  • 实现原理
    • 获取锁的Redis命令
    • SET resource_name my_random_value NX PX 3000
    • my_random_value:随机值,每个线程的随机值都不同,用于释放锁时的校验
    • NX:key不存在时设置成功,key存在则设置不成功
    • PX:自动失效时间,出现异常情况,锁可以过期失效
    • 利用NX的原子性,多个线程并发时,只有一个线程可以设置成功
    • 设置成功即获得锁,可以执行后续的业务处理
    • 如果出现异常,过了锁的有效期,锁自动释放
    • 释放锁采用Redis的delete命令
    • 释放锁时校验之前设置的随机数,相同才能释放
    • 释放锁的LUA脚本

基于Zk的瞬时节点实现分布式锁

  • 数据结构
    • 持久节点
    • 瞬时节点:瞬时节点不可再有子节点,会话结束后瞬时节点自动消失
  • zk观察器
    • 可设置观察器的3个方法:getData();getChildren;exists()
    • 节点数据发生变化,发送给客户端
    • 观察器只能监控一次,再监控需重新设置
  • 实现原理
    • 利用zk的瞬时有序节点的特性
    • 多线程并发创建瞬时节点时,得到有序的序列
    • 序号最小的线程获得锁
    • 其他的线程则监听自己序号的前一个序号
    • 前一个线程执行完成,删除自己序号的节点
    • 下一个序号的线程得到通知,继续执行
    • 以此类推
    • 创建节点时,已经确定了线程的执行顺序

分布式事务

分布式系统中,业务拆分成多个数据库。

多个独立的数据库之间,无法统一事务。造成数据不一致的情况。

CAP和BASE理论

基于XA协议的两阶段提交

  • XA是由X/Open组织提出的分布式事务的规范
  • 由一个事务管理器(TM)和多个资源管理器(RM)组成
  • 提交分为两个阶段:prepare和commit
  • 保证数据的强一致性
  • commit阶段出现问题,事务出现不一致,需人工处理
  • 效率低下,性能与本地事务相差10倍

TCC

针对每个操作,都要注册一个与其对应的补偿(撤销)操作

在执行失败时,调用补偿操作,撤销之前的操作

优点:逻辑清晰、流程简单

补偿的过程中还容易出错

缺点:数据一致性比XA还要差,可能出错的点比较多

TCC属于应用层的一种补偿方式,程序员需要写大量代码

比较复杂,要求比较高,不建议使用

基于本地消息表+定时任务的最终一致性

  • 将本事务外操作,记录在消息表中
  • 其他事务,提供操作接口
  • 定时任务轮询本地消息表,将未执行的消息发送给操作接口
  • 操作接口处理成功,返回成功标识,处理失败返回失败标识
  • 定时任务接到标识,更新消息的状态
  • 定时任务按照一定的周期反复执行
  • 对于屡次失败的消息,可以设置最大失败次数
  • 超过最大失败次数的消息,不再进行接口调用
  • 等待人工处理

优缺点

  • 优点:避免了分布式事务,实现了最终一致性
  • 缺点:要注意重试时的幂等性操作

基于MQ消息队列的最终一致性

  • 原理、流程与本地消息表类似
  • 不同点:本地消息表改为MQ
  • 不同点:定时任务改为MQ的消费者
  • 不依赖定时任务,基于MQ更高效、更可靠
  • 适用于公司内的系统
  • 不同公司之间无法基于MQ,本地消息表更合适

生产者:

  • 创建订单
  • 投递rocketmq消息,这个居然有返回值,如果返回不ok则回滚

消费者:

  • 处理来自生产者的消息
  • 如果订单消费不了,返回了reconsume_later
  • 更新失败了,也返回reconsume_later
  • 成功,返回consume_sucess

限流和熔断

限流

限流的维度

  • 接口限流
  • 总限流

限流的单位

  • 限并发
  • 限QPS/TPS

限流的分类

  • 单机限流
  • 集群限流

计数器,限制并发

令牌桶,比较流行的限流方案

集群限流:

  • 获取令牌放到redis里
  • 有个应用,1s运行一次,每次往redis里放10个令牌,setex key 10 1
  • 获取令牌decr key >= 0

熔断

  • 失败率触发
  • 失败总次数触发

A -> B -> C C系统的故障,蔓延到B,蔓延到A

优化:

  • 设置超时时间
  • 隔离线程池

系统设计篇

系统设计答题思路

使用场景和限制条件

  • 这个系统是在什么地方使用的?比如短网址系统提供给站内各种服务生成短网址
  • 限制条件:用户估计多少,至少要能支撑多少用户
  • 估算并发qps:峰值qps,平均qps

数据存储设计

  • 按需设计数据表,需要哪些字段,使用什么类型?数据增长规模
  • 数据库选型:是否需要持久化?使用关系型还是NoSQL?
  • 如何优化?如何设计索引?是否可以使用缓存?

算法模块设计

  • 算法解决问题的核心。程序= 算法+数据结构。系统=服务+存储
  • 需要哪些接口,接口如何设计
  • 使用什么算法或模型
  • 不同实现方式之间的优劣对比,如何取舍

延伸考点

  • 如果回答不错,可能会问一些深入的问题(扩展、容错)
  • 用户多了,qps高了如何处理?
  • 数据存储多了不够存了如何处理?
  • 故障如何处理?单点失败、多点失败、雪崩问题

设计秒杀系统

解决数据库超售方案

把事务的隔离级别设置成串行(Serializable) (效率低,不可取)

修改库存的时候对记录加锁(悲观锁) (容易产生死锁)

使用乐观锁机制,可以解决超售现象,还能避免死锁,乐观锁通过版本号来判定提交的更新操作是否有冲突

并发修改同一条记录的数据表才需要乐观锁

Redis超售现象的原因

虽然Redis是单线程执行,但是也出现超售现象,一组业务命令发送给Redis执行,如果这些指令被插队,就会出现超售现象

(两个线程,有可能都先查到库存,但是扣减的时序不一样)

Redis事务机制

  • Redis的事务机制与数据库事务完全不同,Redis的事务是一组批处理命令
  • 因为客户端将多条指令一次性发送给redis,而且redis是单线程执行,所以不存在被其他命令打断执行

Redis事务的乐观锁

  • 客户端提交批处理命令之前,需要先查询Redis上面数据版本号
  • watch指令,获取版本号。multi 开启事务 EXEC
  • 如果版本号一致,才会处理批处理指令

设计短网址系统

设计短网址,重点在于把自增ID转换为62进制数

不断取余,倒序输出,递增序列算法

需要一个全局的计数器。Redis incr

request——redis incr index——encode(index)——save mysql

设计朋友圈

todo

性能优化篇

常见的性能评估指标

  • 并发:同一时间多少请求访问
  • TPS:transaction per second(写操作,insert ,update)
  • QPS:query per second(读请求)(一般互联网应用都是读多写少,QPS比TPS大几十倍)
  • 耗时:端到端耗时(有网络耗时的成本),服务端耗时,应用程序耗时
  • 95线:95%的请求落在什么范围内
  • 99线:99%的请求落在什么范围内

制约系统性能的根源

  • 网络
  • 应用本身:JVM
  • 数据库
  • 缓存
  • 消息
  • 操作系统
  • 内存
  • IO
  • CPU

日志优化

  • 同步日志
  • 异步日志

应用程序——同步刷盘——磁盘(成本高)

日志归档时间

日志大小拆分

池化策略

  • IO密集型:核心线程数=cpu*2(大部分互联网应用都是IO密集型的)
  • 计算密集型:核心线程数=cpu核数+1

注意:优化任何一个内容之前,都不需要考虑分布式。如果连单机问题都没法解决好,把大力气放到分布式上得不偿失。先把单机弄好,再去做分库分表,读写分离

安全篇

SQL注入的原理是什么,如何预防

例如: select * from user where username="";DROP DATABASE(DBNAME)

预防:

  • 做输入校验
  • 使用预编译SQL语句执行
  • Mybatis中#$的区别(#会用预编译prepareStatement,$只是拼接语句)

web安全一大原则:永远不要相信用户的任何输入

  • 对输入参数做好检查(类型和范围):过滤和转移特殊字符
  • 不要直接拼接sql,使用ORM可以大大降低sql诸如风险

数据库层:做好权限管理配置,不要明文存储敏感信息

XSS跨站点脚本攻击

  • 恶意用户将代码植入到提供给其他用户使用的页面中,未经转义的恶意代码输出到其他用户的浏览器被执行
  • 用户浏览页面的时候嵌入页面中的脚本(js)会被执行,攻击用户
  • 主要分为两类:反射性(非持久型),存储型(持久型)
    • 反射型(在安全站里设置脚本)
    • 存储型(在安全站里存储脚本,比如评论)
  • 预防:
    • 做输入检验,替换
    • 设置cookie为http-only访问方式

CSRF跨站点请求攻击

  • 触发跨站请求(在源站A打开攻击站的链接,利用了浏览器同源的特性,打开了A的请求)
    • 预防:
      • cookie hash(需要一个hidden的校验位,是根据源站A根据cookie的哈希值,在客户端终止,攻击站没法直接读取)
      • web token(攻击站无法知道服务器下发给浏览器的token是什么)