# 项目亮点!DDD、系统架构、分库分表、高性能、吞吐量
作者:Tom哥
公众号:微观技术
博客:https://offercome.cn (opens new window)
人生理念:知道的越多,不知道的越多,努力去学
面试官拿到我们的简历,一般会关注两块内容,一块是专业技能,另一块是项目经历。
简单的个人介绍后,一般会先问些偏基础的技术问题,热热身。当然也有很多面试官上来就顺着项目问。根据你介绍项目的过程细节,穿插设置一系列的技术问题。
面试官一般会关注一些有挑战性的方案设计、解决了什么复杂难题,简单一句话,就是你的项目一定要有亮点。
那么,什么是亮点?我们的项目如何积累这些亮点?
下面我们会介绍项目中一些高频亮点设计,我们平时做项目,设计技术方案时也可以多用用,积累些实战经验 。
# 面对复杂业务,架构设计有什么通用思路?
答案: 业务理解转化能力、思维抽象能力、软件建模能力、高并发、高性能、高可用的分布式系统架构设计能力。
1、“拆分” ,降低架构复杂度。
2、认知抽象,架构模式有通用性
3、平台化、中台化系统衍化建设。
详细内容请查看 人人都是架构师???谈何容易!! (opens new window)
# 谈谈对 DDD 的理解?
答案:
通过实体、值对象、聚合根、领域服务、领域对象、限界上下文、资源库,指导微服务落地,将一个大的复杂业务域拆分成若干业务子域。定义领域模型(包含数据、行为),相似业务聚合。
更多内容,参考 DDD是如何解决复杂业务扩展问题? (opens new window)
# 项目中用过哪些设计模式?
答案:
工厂、装饰、克隆、代理、适配器、观察者、策略、模板、单例、责任链、门面等23种软件设计模式,这是软件开发的基本功,每一种设计模式都要非常熟悉。否则很难写出扩展性很高的代码。
之前写过三篇文章,每一种模式都有详细介绍:
1、解决复杂业务架构,软件设计模式系列(第一期) (opens new window)
2、解决复杂业务架构,软件设计模式系列(第二期) (opens new window)
3、解决复杂业务架构,软件设计模式系列(第三期) (opens new window)
# 面对海量数据,什么是垂直拆分、水平拆分?
答案:
1、垂直拆分可以分为垂直分库、垂直分表
- 垂直分库:结合DDD领域驱动设计,将一个大的业务域拆分为若干业务子域,比如电商可以拆分为商户、商品、库存、权限、会员、营销、交易、支付、履约、订单、结算、仓储、物流、财务等。每个子域都有自己独立的数据库。
- 垂直分表:将一个有很多字段的表,按字段的大小、使用频率等特点,拆分为多张表。比如:将用户表拆分为 用户基本信息表 和 用户扩展表。
2、水平拆分
由于单台机器的性能有限,无法支撑海量数据存储。我们引入逻辑表概念,采用集群模式,将一张逻辑表拆分成多张物理表分散存储在不同服务器,通过分表键路由,比如:时间、区域、用户id等。
特点:虽然有多张表,但每张表的表结构都是一样的,区别是数据不一样。所有表的数据合并起来才是这个业务表的完整数据。
画外音:数据量大,就分表;并发高,就分库
更多内容,参考 单台 MySQL 支撑不了这么多的并发请求,我们该怎么办? (opens new window)
# 分库分表,全局主键ID有哪些生成方案?
答案:
- 1、UUID,生成的是 32 位的字符串,虽然可以做到全局唯一性,但我们一般推荐使用整型。
- 2、基于一个单表做自增主键
- 3、雪花算法,生成一个 64 位的 Long 类型数据。组成结构:正数位(占1位)+ 时间戳(占41位)+ 工作机器id(10位)+ 序列号部分(12位)
- 4、数据库号段模式,对不同的业务类型定义初始值和步长,业务系统引入SDK,本地缓存预申请一定数据量的主键ID值,满足一定的并发要求。
- 5、TinyID,滴滴的开源框架
- 6、Redis 的 incr 命令
- 7、Leaf,美团的开源框架
- 8、uid-generator,百度的开源框架
# SQL 优化,有哪些方案?
答案:
- SQL 查询时,尽量不要使用 select * ,而是 select 具体字段
- 如果只有一条查询结果(或者最大值、最小值),建议使用 limit 1
- where 语句中尽量避免使用 or来连接条件。or 可能会导致索引失效,从而全表扫描
- 优化 limit 分页
- 优化 like 语句,不要把 % 放到前面
- where 语句的条件字段要充分,查询的数据都是有用的,避免在上层的业务代码中做 filter
- 避免在 索引列使用mysql 内置函数、表达式操作,会导致索引失效
- 优先使用 inner join,如果使用 left join 要小表驱动大表
- 尽量避免在 where 语句中使用 != 、<>
- 使用联合索引时,要注意索引列的顺序
- where 、 order by 涉及的列上建索引,避免全表扫描
- 尽量采用
覆盖索引
,减少回表
- 如果检索的结果不会有重复的记录,建议使用 union all 替换 union
- 索引不宜太多,一般控制在
5个
以内 - 索引尽量避免建在有
大量重复数据的字段
上,如:性别 - 尽量使用 varchar 代替 char
- 如果字段类型是字符串,where 时一定要用引号括起来,否则会索引失效
- 万能方案,使用 MySQL 自带的诊断命令 explain 分析优化 SQL
MySQL的explain,你真的会用吗? (opens new window)
- show profile分析,了解SQL执行的线程的状态以及消耗的时间
- 数据量太大,考虑分库分表 或者借助 es 查询
# 高性能,有哪些方案?
答案:
- 流量入口采用LVS、Nginx, 通过负载均衡算法,将大流量压力均匀、平稳的分摊到下游的多台微服务机器上,从而减轻单台服务器的压力。
- 性能不够,缓存来凑。引入多级缓存,如:Guava、caffeine 本地缓存、Redis 分布式缓存。但是要注意缓存key集中失效、穿透、雪崩、热点、大 key、缓存数据一致性、并发更新等问题。
- 对于前端的一些 JS、CSS、图片等静态文件,可以借助 CDN 加速
- 分库分表。关系型数据库存储,如果数据量过大可以分表,如果要提高吞吐量可以分库。当然一些复杂的查询可以借助 ES 来实现
- DB 索引设计。设计表结构时,我们要考虑后期对表数据的查询操作,设计合理的索引结构,一旦表索引建立好了之后,也要注意后续的查询操作,避免索引失效。
- 数据库的SQL 索引优化
- 对于一些海量数据的存储与查询,可以考虑 NoSQL,如:Hbase、MongoDB、TiDB等
- 异步化。梳理业务流程,非核心逻辑可以异步化处理,如:线程池、MQ、延迟任务等
- 并行化。梳理业务流程,画出时序图,分清楚哪些是串行?哪些是并行?充分利用多核CPU的并行化处理能力
- 引入 MQ 中间件,对大流量削峰填谷,借助 MQ 中间件对下游系统起到缓冲作用。
- 缓存预热,如:大促秒杀,提前将热点数据预热到缓存中
- 预计算,如:抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。
- 减少 IO 次数。如:数据库和缓存的批量读写、RPC的批量接口调用、或者通过冗余数据的方式减少 RPC 调用。索引/分布式计算代替全表扫描、零拷贝减少IO复制次数、分库分表增加连接数
- 减少 IO 数据包大小。如:采用轻量级的通信协议、
合适的数据结构
(比如 PB 协议)、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。 - 加快IO速度。
顺序读写
代替随机读写、硬件上SSD提升等; - 各种池化技术。如:HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。
- 代码逻辑优化。将一些多次查询的结果通过 Context 上下文传递,或者采用更高效的算法或者类
- JVM 优化。如:新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。
- 锁选择。如:读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。
# 高可用,有哪些方案?
答案:
- 限流。保证系统最大可用性,如:秒杀。包括前端限流、Nginx接入层的限流、服务端的限流。
- 熔断策略。对于网络不稳定,可以自动将接口熔断,并按配置的恢复时间检查恢复
- 降级。临时关闭一些非核心业务,释放更多的系统资源让给核心业务。
- 接口设置超时配置,防止慢请求拖垮系统,引发雪崩效应。
- 接口重试、幂等策略
- 故障转移。如果一个节点挂了,会自动将流量切到对等节点。如:Nginx、MySQL、Redis 都具备这种能力。
- MQ场景的消息可靠性保证,如:producer端的重试机制、broker侧的持久化、consumer端的ack机制等。
- 灰度发布,先小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。
- 监控报警:全方位的监控体系,包括最基础的CPU、内存、磁盘、网络的监控,以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。
- 灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。
# 接口性能优化,有哪些技巧?
答案:
- 优化代码逻辑,如:将一些无关联的操作并行化处理;多次查询,可以考虑采用Context上下文传递
- 串行改为并行。对于没有上下文依赖的接口调用采用
并行化
处理,JDK 里面提供了CompleteFuture
可以实现该功能。 - 批量思想。
读写一样
,批量操作
。比如:- 连续调用多次的单个查询,我们可以考虑合并请求,开批量接口。将一个 100次 循环的单个查询替换成一个支持 100 个id的批量查询。这里要特别说明一点,集合的大小要做限制,建议控制每次请求的记录条数在500以内。
写操作
也是一样道理,可以批量处理。
- 池化思想。如:线程池、对象池、数据库连接池、HttpClient 连接池、HTTP 的 Keep-Alive 长连接 等,避免资源频繁的创建和销毁,可以循环使用。
- 线程池合理设计。线程池可以让任务并行处理,如果参数不合理,影响执行效率。重点关注几个参数:
核心线程数
、最大线程数
、阻塞队列
- 分批查询。避免一次查询太过数据,如果是远程接口可能导致接口超时,要做分页处理。将一次获取所有的数据的请求,改成分多次获取,每次只获取一部分用户的数据,最后进行合并和汇总。
- 异步处理。将一些非核心的业务逻辑从
同步执行
中剥离出去,比如:发短信、邮件等,异步化来进行处理。有两种实现方式:- 封装成
Runnable
任务,交由线程池
异步处理 - 封装成 MQ 消息,借助 主流的 MQ 框架通过
发布/订阅
方式处理
- 封装成
- 事件回调。如果被调用接口耗时很长,不要一直阻塞等待,可以参考
IO多路复用模型
,先去做别的事,等被调用接口处理完,通过事件回调
,触发我们之前埋入的回调函数
- 锁粒度。为了解决多线程并发修改某个共享数据,会引入
锁
,如:synchronized
、ReentrantLock
、Redis 分布式锁
等。如果加锁的粒度过粗,影响接口的性能。只需要在共享临界资源
加锁即可,不涉及共享资源的,就不必要加锁。 - 缓存。
性能不够,缓存来凑
。借助 本地缓存Guava
、Caffeine
等;分布式缓存Redis
、memcached
等 来加速。两者通常可以互补,本地缓存没有网络开销,但受内存大小限制;分布式缓存能支持更大的容量上限,扩容更方便,但有约近似 1 ms 的网络开销。
- 本地缓存和分布式缓存可以一起使用,但要注意
数据一致性
问题
- 预热思想。提前把要经过复杂计算的数据计算好,并提前预热到缓存中,需要时,直接去缓存取即可。
- 像一些
双 11
大促活动,一般会将各种热点数据通过定时任务
提前预热
- 数据异构。将一些高频信息,经过计算或转换处理后,存储到缓存中,下次可以直接使用,避免每次查库
- 索引优化。不管什么业务都涉及到数据库存储,SQL 索引优化是关键点,如:
有没有加索引
、加了索引有没有生效
、索引建立是否合理
等。
- 写完 SQL 后,可以通过
explain
查看执行计划 - 前文「SQL优化,有哪些方案?」列举了详细优化方案
- 开启数据库的慢查询日志,有针对性的收集
慢 SQL
,并优化 - 避免
大事务
。事务中避免嵌套RPC
远程调用 - 深度分页。会扫描太多的数据行,可以考虑
标签记录法
,根据主键id
快速定位、查找 - 分库分表。数据量大时,可以分库分表
- 数据压缩。如果涉及网络传输,选型合适的序列化框架,并对内容压缩
- 优化程序结构。比如,你的程序创建多不必要的对象、或者程序逻辑混乱,多次重复查数据库、又或者你的实现逻辑算法不是最高效的,等等。
- 数据过期策略。一张表的数据量太大的情况下,对DB的查询性能是非常有影响的,建议合理的设计数据过期策略,历史数据定期放入history表,或者备份到离线表中,减少线上大量数据的存储。
- 技术方案
- 如果数据太大,可以先采用
文件/MQ
等暂存数据,后面再慢慢保存到数据库中
- NoSQL。如果数据量太多,可以考虑引入 NoSQL,比如 Hbase、Elasticsearch 等
- 增加监控,比如 :
Prometheus
,将一些慢响应接口、或者异常太多接口收集起来,用于监控
、报警
。我们可以采集的信息:
- 接口响应时间
- 调用第三方服务耗时
- 慢查询sql耗时
- cpu使用情况
- 内存使用情况
- 磁盘使用情况
- 数据库使用情况
- 也可以引入类似
skywalking
这样的分布式链路跟踪系统,串联一个接口请求的完整链路。支持查看链路的各个环节的耗时,指导我们优化系统。 - 与业务保持沟通,找到平衡点。技术不是万能的,无论白猫黑猫,能抓到老鼠就是好猫