这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战
国内的面试里,很少考系统设计相关的题目,但学习一些系统设计方面的知识点,可以帮助我们对日常开发有更深的理解,系统设计的面试题比较难总结,一方面涉及的知识点比较广,另一方面笔者的很多知识点也只停留在理论,并未实践过,总结的内容难免会有偏颇,仅供参考。
本文共分为常见系统设计题目整理、开发经验篇、分布式篇、系统设计篇、性能优化篇、安全篇.
常见题目整理
- 微信抢红包功能设计
- 设计一个限流工具,每5s允许一个请求,多的请求丢弃
- 会问有没有消息队列 分布式 缓存 一系列后端架构的经验
- 微博等热门评论,在分页到很深的时候,如何进行优化
- 如何实现音乐随机播放
- 服务器降级方案,如何指定降级优先级
- 如何找项目性能瓶颈
- 有多少种类型的缓存, 从客户端到服务端
- 是否遇到分布式事务,有哪些解决方案,
- 分布式锁怎么做,用redis 实现分布式锁有哪些步骤
- im 里面群聊消息体设计,如何设计xx 人已读xx 人未读
- 如何设计秒杀系统,应该关注什么:防恶意提交、NG 限流等
- 微服务的某个节点挂了怎么办
开发经验篇
防止重复提交
- 没有响应之前,禁用前端页面的提交按钮
- 用户刷新页面,或者用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是什么)
- 预防: