程序员必须练就的「性能调优」组合拳【3】
性能调优系列前序文章索引:
- 程序员必须掌握的性能调优:老兵哥结合个人经历解释了程序员往架构师方向发展时为什么要跨越性能调优这一关,以及介绍了从 X、Y、Z 三个维度优化性能的思路。
- 从 X 维度优化系统的性能:老兵哥分享了从 X 维度优化系统性能的思路,包括让客户端分计算存储任务、优化交互设计等,主要是作为引子拓宽我们性能调优的思路。
- 应用容器 Tomcat 性能调优:Y 维度就是从业务 HTTP 请求的横向处理流程来看,HTTP 请求会穿越网络、计算机、应用容器(Tomcat)、Spring、ORM(Hibernate)、数据库等节点,在这个流程中每个节点都有许多可以可优化的地方,此文主要介绍通过优化应用容器(Tomcat)来优化系统性能的方法。
程序员在转型架构师的过程中需要建立流程化、结构化、系统化的思维方式,而性能调优是非常难得的契机,它既给了我们压力,也给了我们动力,跨越它就是突破自己的过程。
- X 维度,即业务维度,技术始终是服务业务的,任何技术问题的原点就是业务需求。在启动技术层面的性能优化之前,我们有必要先审视一下业务流程是否合理,交互设计上有没有可以优化的空间等。
- Y 维度,待业务维度优化完毕,接下来就是审视技术在实现当前业务流程或交互设计的全链路上有没有可优化的地方,即 HTTP 请求处理全流程,从浏览器到应用容器,再到 Spring、Hibernate、数据库等。
- Z 维度,除了沿着 HTTP 请求的横向链路,我们还要审视支持应用系统的纵向技术栈,从上到下包括 JVM、操作系统和硬件等,这是整套应用系统运行的环境,许多性能问题都跟运行环境存在关系。
今天老兵哥将介绍通过优化开发框架 Spring 来优化系统性能的方法。
3. 开发框架 Spring
3.1 事务管理
事务(Transaction),是并发控制的基本单位,是用户定义的一个操作序列。这些操作要么都做,要么都不做,是一个不可分割的工作单位。通过使用事务控制,我们可以极大地避免逻辑处理失败导致的脏数据等问题。事务具有 4 个属性:原子性、一致性、隔离性、持久性等,这四个属性通常称为 ACID 特性。
- 原子性(Atomicity),一个事务是一个不可分割的工作单位,事务包含的操作要么都做、要么都不做。
- 一致性(Consistency),事务必须让数据库从一个一致性状态变到另一个一致性状态,不能出现不一致。
- 隔离性(Isolation),一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(Durability),持久性也称永久性,指一个事务一旦提交,它对数据库中数据的改变是永久性的,接下来的其他操作或故障不应该对其有任何影响。
Spring 事务管理是通过 XML 文件或注解 @Transactional 配置的,其背后是静态代理或动态代理等技术。在代理模式下,那些从代理传递传过来的“外部”方法调用会被拦截,但“自我调用”是不会触发事务的。例如,在目标对象中调用自身其他方法的方法是不会触发事务的,即使被调用的方法标记为 @Transactional。
通常,我们很少关注 Spring 事务管理相关的属性,但这些属性的取值会影响系统的性能。Spring 事务管理最重要的两个特性是:传播级别、隔离级别。传播级别,定义了事务的控制范围;隔离级别,定义了事务在数据库读写方面的控制范围。我们知道,事务的控制范围越大,系统的并发性就会越差,性能也就随之降低。事务的隔离级别越高,系统的并发性也会越差,性能也会随之下降。如果不了解这些属性的取值规则,我们就不能选择最合适的取值,不知不觉中就会浪费许多系统资源,接下来我们一起来看看这些属性。
属性 | 类型 | 描述 |
---|---|---|
propagation | 枚举型:Propagation | 传播级别,可选,默认值:PROPAGATION_REQUIRED |
isolation | 枚举型:Isolation | 隔离级别,可选,默认值:ISOLATION_DEFAULT |
readOnly | 布尔型 | 读写型事务、只读型事务 |
timeout | INT 型,以秒为单位 | 事务超时阈值 |
rollbackFor | 一组 Class 类,必须是 Throwable 的子类 | 一组异常类,遇到时必须回滚。默认情况下 Checked Exceptions 不进行回滚,仅 Unchecked Exceptions(即 RuntimeException 的子类)才进行事务回滚。 |
rollbackForClassname | 一组 Class 类的名字,必须是 Throwable 的子类 | 一组异常类名,遇到时必须回滚 |
noRollbackFor | 一组 Class 类,必须是 Throwable 的子类 | 一组异常类,遇到时不需要回滚 |
Spring 事务管理的传播级别 Propagation 取值有以下几种:
传播级别 | 说明 | 备注 |
---|---|---|
PROPAGATION_REQUIRED | 如果上下文中已经存在事务,那么就加入到事务中执行;如果上下文中不存在事务,则新建事务执行。 | 这个级别通常能满足处理大多数的业务场景。 |
PROPAGATION_SUPPORTS | 如果上下文中已经存在事务,则支持加入到事务中执行;如果上下文中不存在事务,则使用非事务的方式执行。 | 这个通常是用来处理那些并非原子性的非核心业务逻辑操作,应用场景较少。 |
PROPAGATION_MANDATORY | 该级别的事务要求上下文中必须要存在事务,否则就会抛出异常。这是避免上下文调用代码遗漏添加事务控制的保证手段。 | 例如某段代码不能被单独调用执行,但是一旦被调用就必须要有事务包含,这种情况下就可以使用这个传播级别。 |
PROPAGATION_REQUIRES_NEW | 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。 | 问题1:如果某个子事务发生回滚,父事务是否回滚?答案是不会,因为子事务是新建事务,父事务已经被挂起,两者不会受到影响。问题2:如果父事务发生回滚,子事务是否回滚?答案是不会,同样的理由。但是可以手动控制,一旦子事务回滚,父事务也回滚。 |
PROPAGATION_NOT_SUPPORTED | 如果上下文中已经存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。 | 这个级别可以帮助你尽可能地缩小事务范围。一个事务范围越大,它存在的风险也就越多,例如某段代码是循环 1000 次的非核心业务逻辑操作,此类代码如果包在事务中,势必导致事务太大,很容易出现些难以考虑周全的异常情况,此时这个级别就派上用场了。 |
PROPAGATION_NEVER | 该级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行。 | 无 |
传播级别 PROPAGATION_REQUIRED 会为每一个被应用到的方法创建一个逻辑事务作用域。每一个逻辑事务作用域都可以自主地决定回滚条件,当这样的逻辑事务作用域被外部逻辑事务作用域所包含时,它们在逻辑上是独立的,但在实现层面它们会被映射到相同的物理事务上。
传播级别 PROPAGATION_REQUIRES_NEW 为每一个相关的事务作用域使用了一个完全独立的事务。在这种情况下,物理事务也将是不同的。因此,外部事务可以不受内部事务回滚状态的影响独立提交或者回滚。
Spring 事务管理的隔离级别 Isolation 取值有以下几种:
隔离级别 | 说明 |
---|---|
Serializable | 最严格的级别,事务串行执行,资源消耗最大。 |
Repeatable Read | 保证了一个事务不会修改已经由另一个事务读取但未提交(或回滚)的数据,避免了“脏读取”和“不可重复读取”的情况,但会带来了更多的性能损耗。 |
Read Committed | 大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”,该级别适用于大多数系统。 |
Read Uncommitted | 保证了读取过程中不会读取到非法数据。 |
上述说明中涉及的几个专业术语:
- 脏读(Dirty Reads):就是读到了别的事务回滚前的脏数据。例如,事务 B 执行过程中修改了数据 X,在未提交前,事务 A 读取了 X,而事务 B 却回滚了,这样事务 A 就形成了脏读。
- 不可重复读(Non-Repeatable Reads):不可重复读字面含义已经很明确了。例如,事务 A 首先读取了一条数据,然后执行逻辑的时候,事务 B 将这条数据改变了,然后事务 A 再次读取的时候,发现数据不匹配了,这就是所谓的不可重复读。
- 幻读(Phantom Reads):我们小时候数鸭子,第一次数是 10 个,第二次数是 11 个,怎么回事,产生幻觉了?幻读也是这样子,事务 A 先根据条件索引到 10 条数据,然后事务 B 改变了数据库一条数据,导致也符合事务 A 的搜索条件,这样事务 A 再次搜索发现有 11 条数据了,这就产生了幻读。
隔离级别与副作用 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Serializable | 不会 | 不会 | 不会 |
Repeatable Read | 不会 | 不会 | 会 |
Read Committed | 不会 | 会 | 会 |
Read Uncommitted | 会 | 会 | 会 |
从上面这张映射表中,我们知道最安全的是 Serializable,但是伴随而来的是高昂的性能开销。各种传播级别、隔离级别本身没有好坏,关键是根据业务需求选择最合适的取值,避免无效的性能损耗。另外,Spring 事务管理还有两个常用属性,它们的取值也会影响性能:
- Readonly:只读型事务要比读写型事务的性能更好,设置事务为只读以提升性能。
- Timeout:设置事务的超时时间,一般用于防止大事务的发生,事务要尽可能的小。
3.2 二级缓存
缓存作为提高应用系统性能的一种有效途径,在事务管理配置不当的情况下,将很难发挥应有的效用。因此,在做缓存处理或者其他处理,要考虑事务管理对性能的影响。
关注「 IT老兵哥 」,赋能程序人生!坚持原创不易,请小伙伴们不吝点个「 赞 」哦!推荐软技能文章,请点击链接:程序员,怎样打造个人影响力?
近期热评系列《 程序员必须懂的架构师入门课 》:
- 架构到底是什么,你知道吗? (阅读人数:1201)
- 架构都有哪些,我该怎么选? (阅读人数:886)
- 架构师都干什么,你知道吗? (阅读人数:1178)
- 练就哪些技能才胜任架构师? (阅读人数:1145)
- 怎样才能搞定上下游的客户? (阅读人数:492)
- 如何从开发岗转型做架构师? (阅读人数:1288)
- 程序员必须懂的架构入门课 (阅读人数:611)