You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

103 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 27 | 解决一个互斥问题系统并发用户数提升了10倍
你好,我是尉刚强。
互斥锁是实现并发场景下业务操作原子性、解决互斥访问问题的有效手段之一,由于它的使用方式相对比较简单和安全,所以不论是在互联网的分布式系统中,还是在嵌入式并发场景下,应用都比较广泛。
但是,**如果互斥锁的选择和使用不当,就很可能成为系统的性能瓶颈之一**,所以合理优化互斥锁,就成为了系统性能优化的一个重要手段。
那么在今天的课程中我就会给你分享一个互联网场景下使用互斥锁优化的案例按照优化前的软件实现、性能瓶颈分析、优化解决方案的思路带你剖析我是通过什么样的方法优化业务中的互斥锁以及是如何提升业务RPSRequests per second请求吞吐量性能指标10倍以上的从而帮助你全面地了解分析与优化互斥锁的详细过程。
这样,通过学习这个性能优化案例,你在业务中就可以准确识别出哪些场景下的互斥锁可以优化掉,而哪些场景下不可以。并且你还会掌握一种手动实现事务的机制(支持业务操作回滚机制),来替代业务中互斥锁的手段,进一步来帮助优化提升软件的性能。
那接下来我们就先看看,在该案例中,业务优化前的实现是怎么样的,以及它都存在什么性能问题。
## 优化前的业务实现为什么会有性能问题?
这个性能优化案例的业务场景是这样的:用户给在线表单提交一条记录,在这条记录中会包含很多个字段内容,其中有些字段在插入时有一个规则要求,即不能与已有的字段值重复。
为了便于理解,这里我用一个图来描述下,原业务系统中实现字段插入值不重复规则的实现逻辑,具体如下所示:
![](https://static001.geekbang.org/resource/image/05/c0/05605d0c6846b378fe502e35c240dbc0.jpg?wh=2000x1125)
可见在该业务中使用了一个Redis锁来实现互斥访问从而实现了被加锁的业务逻辑执行的原子性所以这部分计算逻辑在系统中是串行执行的。而被加锁的业务逻辑主要有三个关键操作分别是
1. **字段不重复检测**:检查插入的字段值在数据库中是否有重复的情况出现,如果出现重复的值,插入失败直接退出,否则执行下一步的操作。这里系统会遍历要求值不能重复的所有字段项,如果其中任何一个字段项出现值重复,就都会退出。
2. **其他操作**:即用户提交记录的过程中一些关键业务操作,其特点是不能被拆分执行,也不能被回滚。如果操作成功的话,就会执行下一步的操作,否则也会直接退出。
3. **所有字段插入**:由于这三个操作,通过加锁保证了原子性执行,所以前面检测的“字段值不重复”的条件仍然是有效的,这一步会将所有的字段进行插入。
除此之外在优化前的代码实现中需要进行重复性校验的字段都会记录在Redis中。所以图中的操作1、操作3都是基于Redis来实现的。
那么在看完这个业务实现逻辑图之后,你可能会比较好奇:**这种字段唯一性检测机制,为什么不使用关系数据库中的字段唯一性检测机制来实现呢?**这是一个好问题,我在刚看到这个业务逻辑实现的时候也很好奇,而到后来深入分析了业务之后,才理解为什么会这样实现。
其实这是因为在这个业务系统中有大约1000万张表单其中每张表单的字段唯一性规则可能都不一样而且用户还可以随意修改这个规则。所以这个系统在设计实现的时候就将所有表单中的所有字段都放到了一张很大的数据库表中因此这样我们就没有办法使用数据库表上的字段唯一性规则来处理这个问题了。
原来的这种**Redis加锁的实现方式**比较简单,而且是按照单个表单来进行加锁的,所以**在单个表单并发提交请求吞吐量不是很大的情况下,并不会对系统性能产生太大的影响**。
可问题就是,随着系统的业务规模逐渐增大,会出现少量表单的并发请求吞吐量暴增的情况,而这个时候,当单个表单提交请求超过了并发请求吞吐量的上限值后,就会引起两个比较严重的性能问题:
1. 针对超过并发请求吞吐量性能上限值的那个表单,用户在提交表单的页面会出现卡死,导致提交数据失败;
2. 由于后端服务系统是基于进程模型的,而进程资源的数目是有限的,所以一旦个别表单提交数据请求的处理进程被阻塞,占用了大量的进程资源,就会导致整个系统无法正常处理所有的业务请求。
因此,**提升单个表单提交请求吞吐量的性能指标,就成为了这个软件系统性能优化的关键问题**。那么接下来,我们就要先搞明白,这个互斥锁是如何影响这个表单的请求吞吐量性能的。
> 补充:在这个软件系统实现中,由于单个表单的吞吐量性能瓶颈,而导致的整个系统业务处理受阻,实际上是另外一个软件设计问题,所以不在我们今天的课程讲解范围内。
## 互斥锁是如何影响最大请求吞吐量的?
在[第22讲](https://time.geekbang.org/column/article/392108)中,我们已经学习过加锁的计算逻辑属于串行资源,这是系统中潜在的性能瓶颈之一,会影响系统的请求吞吐量的性能指标。
接下来我就使用一个公式来描述下在这个案例中使用了Redis互斥锁以后来计算Max RPS最大请求吞吐量的计算方法具体公式如下所示
![](https://static001.geekbang.org/resource/image/6c/06/6c0a90776af956e63e21327006041d06.jpg?wh=1972x830)
在这个公式中因为Lock和Unlock是采用Redis的互斥锁来实现的它们所使用的Redis的script脚本实现如图中所示通过在真实系统中进行测量其中Lock time + Unlock time的操作时间之和在3ms左右。
然后你就可以通过上面的公式计算出如果中间加锁的计算逻辑resource competition执行开销为30ms左右那么对应的Max RPS=1s/(3ms+30ms)也就是大约在30RPS左右。
也就是说如果把加锁的计算逻辑降低极限值为0时对应的Max RPS才可以到达300RPS左右。
那么这里你需要注意的是因为业务中的互斥锁是全局控制的所以当系统达到最大RPS时即使通过弹性扩展机制部署再多的后端服务实例进程也不能再提升这个性能指标了。
因此到这里在这个性能优化案例中我们经过测量加锁的计算逻辑执行时间为30RPS然后根据上面的公式我们计算出的最大RPS值也为30RPS左右这与真实的性能测试获取的性能指标值是完全一致的。
好的,现在问题就已经比较清楚了,那么有没有办法可以优化提升这个系统的性能呢?下面我们来看一下。
## 性能优化解决方案
针对这个业务案例你应该也能发现一个明显的特点绝大多数情况下并不会出现并发提交相同字段值的情况。也就是说如果这个业务逻辑没有增加互斥锁在99.9%的情况下业务逻辑也是正确的。所以,针对这种场景,我们就可以采用手动实现事务机制,来优化掉业务代码中的互斥锁,来提升请求吞吐量的性能。
而这里我们已经知道,在这个案例中,使用互斥锁解决的核心问题是**判断字段不重复**和**字段插入操作的原子性问题**。所以说,我们其实可以考虑采用一些优化机制,来单独实现这两个操作组合的原子性。
但是要注意,如果在互斥锁的使用场景中,被加锁的业务操作还有更复杂的一致性要求,比方说存在数据库写冲突的问题等,那么这种互斥锁实现就不能被简单地优化掉了。
那么对于这个案例中的互斥锁而言,我们应该怎样优化呢?
我来说说我想到的优化思路。这里呢,为了更清晰地描述该解决方案,我用了一个流程图来给你详细地介绍下性能优化后的具体实现过程,如下图所示。
![](https://static001.geekbang.org/resource/image/28/6d/2891252183ff896ce67d7689537yyb6d.jpg?wh=2000x1125)
也就是说,我们可以把“字段不重复检测”“单个字段插入”“其他操作”三个操作绑定到一起,然后实现一种事务机制的能力,支持在后面操作失败的情况下,可以回滚到前面的操作中。
实际上原来的Redis互斥锁主要是为了实现“字段不重复检测”和“字段的插入操作”的原子性而在手动实现事务机制之后我们就可以把这两步操作放到开始处执行然后**使用Redis的Pipeline机制**保证这两步操作组合的原子性从而不会被其他Redis操作干扰到。
这样针对接下来的其他操作(也就是用户提交数据过程中的一些不可拆分的关键业务操作),如果操作成功,就提交任务成功结束;如果操作失败,则需要回滚之前的字段插入操作。另外为了实现事务的机制和能力,我们还需要在前面字段插入时,同时记录插入前的状态和插入后的变更状态,从而就可以实现失败后的回滚机制。
其实这里我还考虑过另外两种实现方案分别是基于Redis的事务机制和基于MongoDB上的事务机制。
但是我最后在实现时并没有采纳这背后其实有很多的原因。比方说使用MongoDB的事务需要进行数据迁移而且需要升级系统的MongoDB集群的数据库版本等以及使用Redis事务机制的代码实现并不友好等等。
不过这里有一个**最重要的原因**就是不管是使用Redis事务还是MongoDB上的事务它们都把对字段插入操作的冲突时间拉长到了步骤3“其他操作”结束之后而这样就显著增大了事务冲突失败的概率。
所以最后我们采用前面这种优化后的实现机制因为去除了互斥锁所以用户间的提交记录可以更大程度地并行。而且优化后的实现方式只有Pipeline操作会排队处理而由于单个Pipeline的执行时长在1ms~3ms之间所以最后优化后的表单最大请求吞吐量就从原来的30RPS提升到了300RPS左右这样就**实现了性能提升超过10倍的目标**。
## 小结
同步互斥是高性能分布式系统设计实现的关键技术之一,而在软件开发的过程中,我们很容易因为设计或实现不高效,导致整个软件系统的性能不够理想。所以在今天的课堂上,我通过一个真实的性能优化案例,给你讲解了业务中使用互斥锁存在的性能问题,包括它对请求吞吐量的性能影响,以及优化互斥锁来提升系统性能的方法和过程。
但是,每个业务系统中的同步互斥问题都存在特有的复杂性,所以你应该学习收获到的是,如何分析同步互斥对性能的影响,以及如何优化同步互斥的实现来提升性能的方法。在今天所讲的案例中,采用的是先默认成功来执行操作,后面出现异常时再回滚的机制,你也可以在自己项目优化中去使用。
## 思考题
基于Redis上有CAS命令有Pipeline机制还支持lua.script和事务能力那么在业务中你会怎么选择来优化软件的性能呢欢迎给我留言分享你的思考和看法。如果觉得有收获也欢迎你把今天的内容分享给更多的朋友。