【资损】分布式系统并发互斥设计
📫 作者简介:小明Java问道之路,专注于研究 Java/ Liunx 内核/ C++及汇编/计算机底层原理/源码,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。
📫 热衷分享,喜欢原创~ 关注我会给你带来一些不一样的认知和成长。
🏆 InfoQ 签约作者、CSDN 专家博主/后端领域优质创作者/内容合伙人、阿里云专家/签约博主、51CTO 专家 🏆
🔥 如果此文还不错的话,还请👍关注 、点赞 、收藏三连支持👍一下博主~
本文导读
并发互斥控制设计在整个分布式系统中,大到业务的并发处理、服务的并发请求,小到数据库表的并发读写、java 对象的多线程访问,并发无处不在。因为并发,所以互斥。
一、 资损防控系统设计资损防控规范
从系统架构层面整体来看,支付公司的系统可以抽象为如下结构:
一、对外部商户提供收单服务类的系统
二、连通支付公司与各金融渠道的网关类系统
三、支付公司的内部业务处理系统
四、消息、调度等中间件系统
五、数据库、缓存等存储平台
从系统架构与业务架构上来讲,各个结构连接的地方最容易出现资损。因此我们将从接口服务层面与系统设计层面对资损进行分析并总结相关规范。
二、系统层面的并发互斥控制
我们所说的系统层面的并发互斥控制,主要包括:
1、服务的并发请求控制(进程级)
服务的并发请求控制,服务的并发请求既包括 dubbohttp 等同步调用的场景也包括消息、调单等异步调用场景需要一起考虑。
此外,在业务处理中,还存在各种互斥的资金处理的场景与流程,也是我们需要在设计的时候考虑的。
2、服务内处理的并发请求控制(线程级)
服务内处理的并发请求控制,内存中的并发控制。主要是内存中的对象、线程变量等资源共享资源进行并发控制。
内存外的并发控制。主要是指内存外的存储资源,如数据库表的访问等的并发控制。
三、常见并发互斥类资损风险
1、无状态迁移变化的控制
在互斥的业务流程与场景中,业务处理过程中,无状态迁移变化的控制。复杂的场景中未使用状态机。或者状态机状态迁移合法性验证不全面。或者状态机设计不合理,使得状态迁移变化不可靠。
常见的状态机设计问题有:
1 状态被执行有多次。如支付成功多次可能导致业务被推进多次。
2、终态还可以继续变化。如代发成功又变更为待支付。
3、互斥状态没有做并发控制。如发货和退款同时发生,同时成功。
4、状态机没有统一控制。
5、多个状态机耦合在一起,状态控制混乱。如交易核心支付状态、交易状态耦合在一起,由于不同的状态控制,导致支付状态成功交易状态失败的问题。
6、状态机的状态迁移被漏处理。
2、并发请求处理中,没有考虑到消息与调度场景
并发请求处理中,没有考虑到消息与调度场景,比如收单服务请求支付核心进行支付处理,同步响应结果和消息通知结果同时到达,未做并发处理,业务被推进了两次(可能申购了两次基金、也可能通知商户发货了两次)。
3、对内存中资源未做并发控制或并发控制不严格
多线程环境下,对内存中资源未做并发控制或并发控制不严格,尤其是对线程变量的使用。
如对共享数据访问不加锁、单例对象进行写操作、线程变量进出不清空等均会导致并发控制问题。
4、数据脏读问题
数据脏读,并根据脏数据做判断。需要注意的是,不仅仅是对数据库中的数据访问存在脏读,分布式缓存中的数据访问也存在脏读。
5、常见的数据库锁操作问题
为了防范脏读脏写、不可重复读等常见的数据库并发问题我们常用锁机制进行处理;不合理的锁使用会导致风险。
常见的问题有:
1、先读后锁而非先锁后读。读的数据不能保证是最新数据,会导致仍然基于脏数据做判断的风险。
2、批量 update 流水的时候,where 条件中没有原有流水的状态,且没有判断更新条数。容易出现不可重复读的问题。
3、SQL 语句中使用 updatesetselect 完成数据的更新,有可能导致 update 锁机制不生效。
4、锁没有顺序且未设置超时等退出机制(如 SQL 中未加 nowait)导致死锁。
四、系统并发互斥设计
1、业务状态比较复杂的场景,使用状态机进行状态控制
业务状态比较复杂的场景,使用状态机进行状态控制,状态机是针对业务状态比较复杂的场景下,并发互斥控制比较常见的设计模式。如统一订单交易平台均使用状态机来进行业务状态的管理。
状态机设计过程中需遵循如下规范:
1、由于业务状态的迁移,可能由同步服务请求、异步消息、后台调度、用户操作、运营操作等多种情形触发。在此情况下,需要保证状态机统一进行控制,不能设计多个状态迁移变更模块。更不能设计多个状态机进行对不同情形做状态迁移。
2、要设计终态,且保证终态不能被继续迁移。如一笔交易订单的超时关闭就是终态,在设计上就要避免其不能再次迁移为待支付。
3、要保证状态迁移和业务处理的一致性。
4、设计中,针对状态迁移的处理,需要对所有可能的状态做判断。极端情况下可以判断所有的状态。
5、由于业务状态比较复杂,为防止状态机状态设计腐化,与状态机实现代码表现不一致。状态机不能只根据文字描述进行开发,要先设计或更新状态迁移表或状态迁移图;可以明确表达出入口状态(初始状态)、终态、状态跃迁路径。
6、不能用同一个状态字段表达多种状态迁移。简单的业务处理,可能不使用状态机进行状态迁移的并发控制。但仍然需要使用统一模块进行状态控制、并考虑多种情况下的并发请求做好业务的并发与互斥处理。
2、并发请求的处理
并发请求的处理,针对分布式环境下的并发请求的处理多是基于数据库或分布式缓存等资源等进行并发访问控制。
常见的并发访问控制模型主要有如下几种:
1、基于资源的并发控制(悲观)。
这是比较经典的并发控制模型,使用较多,如账务系统中的记账操作就使用该模型控制用户账户余额的并发更改。
2、基于资源的并发控制(乐观)
由于是在第 4 点更新领域对象的时候检查并发,其实整个过程中并没有锁定任何对象和记录。所以采用该机制的时候,系统应该要容许不可重复读问题的出现。
3、基于分布式锁服务的并发控制分布式锁可以借助分布式缓存实现。
3、数据操作的规范。一锁二判三改
1、一定要保证先锁后读,避免脏读。数据库可以使用 selectforupdate 这种模型进行加锁。
2、提交的时候要判断更新的数据的状态是否符合要求,避免不可重复读。要在 SQL 的 where 条件中增加原有流水的状态,且检查了 update 方法返回的记录条数是否满足预期。如果是状态机的状态进行 update 的时候,要基于状态迁移来写 where 条件中的前置状态值,wherestatus=xx 或者 wherestatusin(xxyy)。update 的结果,影响行数 0、1N 的时候处理逻辑均需要实现。
3、不要使用 updatesetselect 完成数据的更新有可能导致 update 锁机制不生效。
4、多线程操作规范
多线程操作规范需要遵循常规的 java 并发操作规范,保证操作的原子性。代码开发的指导原则应该是:尽可能的避免在多线程间共享数据、共享操作;如果要共享数据,需要对共享数据加锁;尽可能的使用 spring 提供的单例框架配置单例的 bean;不要对单例的 bean 进行写操作;业务代码开发过程中,尽可能的避免使用线程变量使用线程变量的时候,一定要在出入口进行清空。...。。
5、避免死锁
无论是数据库还是分布式锁服务或者 jvm 内部的锁,在可能发生死锁的场景,加锁的时候需要通过加锁排序和设置超时时间等方式避免死锁。
总结
并发互斥控制设计在整个分布式系统中,大到业务的并发处理、服务的并发请求,小到数据库表的并发读写、java 对象的多线程访问,并发无处不在。因为并发,所以互斥。
版权声明: 本文为 InfoQ 作者【小明Java问道之路】的原创文章。
原文链接:【http://xie.infoq.cn/article/354f0d6695e9085ec31afc6a7】。文章转载请联系作者。
评论