winterggg / blog

0 stars 0 forks source link

设计一个包含多参与者业务的并发控制方案 #12

Open winterggg opened 3 months ago

winterggg commented 3 months ago

这是一篇公司组内分享文稿,其中的「聚合表」是指的一种支持跨表校验的实时多表物化视图功能;「聚合计算」是指的是聚合表的一种封状态;「RWLock」 是内部封装的一个基于 Redis的多粒度锁组件(X\S\IX\IS).

TL;DR

复杂业务的并发控制是一个多方面的问题,需要从多个角度进行综合考虑。通过合理设计并发控制机制,可以确保系统在高并发情况下的数据一致性和操作正确性,提高系统的可靠性和稳定性。

1. 明确并发参与者

首先,必须明确可能会互相影响的事务性操作,即并发参与者。一个常见的陷阱是没有划分到合适的粒度。以聚合计算为例,聚合计算配置内嵌在表单元数据里,表单配置保存可能触发聚合计算配置的修改。表单保存对于表单设计业务自己来说是一个操作,但对于聚合计算业务领域来说,需要细分为:

2. 明确需要的资源及行为

需要标明所有资源,并区分其读/写行为,如果资源太多、依赖关系太复杂,大脑无法整个加载,可以用 OmniGraffle、ProcessOn 之类的工具绘制一个资源拓扑图帮助自己理解。

加锁的目的是控制共享资源的读写一致性,保证在锁定资源后,读写操作之间不会发生一致性问题,常见的一致性问题有 (Copy From DDIA):

3. 设计锁的种类和粒度

设计锁的种类和粒度是并发控制的关键。

首先,如果两个操作强互斥(例如单张表的聚合计算配置保存和全量操作;或者同一张表的多个全量操作之间),可以考虑在一开始加一个表锁,这样可以避免后续复杂的资源操作互斥关系处理。

然后,锁的粒度可以有很多种划分,比如:字段级别、行级别、页级别、表级别等。根据并发度决定合适的划分:

4. 明确领域

在设计增量和全量操作时,需要明确领域。例如聚合计算增量操作除了加配置相关的锁,还要加数据锁。数据锁相对独立,与全量和配置保存关系不大,可以分开思考。

5. 考虑操作频率

考虑参与者发生的频率和成本。在聚合计算中,增量操作频率远高于全量操作和配置保存。因此,在加锁时,低频操作可以加更多的锁,而高频操作加更少的锁,减少系统整体的均摊成本。

例如一种比较好的设计是,增量操作只需加当前表单的聚合计算配置锁,而全量操作和删除聚合计算需要加所有涉及表单的聚合计算配置锁。

6. 枚举中断点和确保有恢复机制

需要考虑中断点(如宕机/锁丢失)及其可恢复性。由于 MongoDB 不支持数据库事务,可以假设流程中每个地方都可能发生宕机,并考虑是否会影响一致性及恢复方案。对于聚合计算/聚合表,宕机导致的一致性错误通常可以通过发起一次全量操作解决。

7. 防御性检查

防御性检查是必要的,以避免其他代码的不严谨对业务一致性造成影响。例如,(1)当前数据锁和表锁加锁不规范、(2)老聚合表可能出现入库成功后获取锁失败的 bug(这个时候聚合计算需要继续执行增量,不让 bug 蔓延),(3)关联关系配置可能不一致、(4)表单配置保存可能存在脏读问题等。需要分别考虑并设计防御措施。

8. 熟悉你所用的轮子

这里主要是考虑所依赖的锁工具是否支持锁重入、锁续期等。目前,系统的RWLock不支持锁重入和锁续期,因此需要特殊逻辑记录并排除已加过的锁,并根据业务预估耗时调整锁的超时时间。

9. 真正的TL;DR:并发度考虑(逃

考虑当前系统的QPS情况。例如简道云 QPS 正常场景下没有那么高,也许设计上可以允许不那么完美的方案。

绝对完美的方案一般会有性能损耗,需要在一致性和性能之间找到平衡。