基于中间件的分布式系统幂等性问题研究

2022-10-11 03:00张旭东蒋厚明陈星明
现代计算机 2022年15期
关键词:一致性事务分布式

张旭东,蒋厚明,王 俊,陈星明

(南京南瑞信息通信科技有限公司,南京 210000)

0 引言

随着计算机技术迅猛发展和移动设备的普及,传统的巨石架构系统已经不能满足现有用户访问量指数级倍增的需求,系统处理高并发场景的能力已成为当前系统架构设计的核心指标和重要难题。单服务器设计由于软硬件性能瓶颈问题,无法承载万级及更高的用户并发量,当大量用户同时访问系统服务时,往往会导致系统响应缓慢,甚至会因瞬间流量过大而导致系统崩溃,极大降低用户使用体验。

为了解决上述问题,分布式微服务架构应运而生。微服务架构核心思想是将原本复杂的单体应用系统根据业务功能拆分成多个服务,各业务服务可独立存在且独立部署,通过HTTP协议完成相互调用并对外提供完整系统功能。微服务系统架构可以根据各个微服务的访问量进行微服务的水平扩展,提高系统的并发能力和吞吐量。各个微服务都可以单独开发、部署,简化了开发时间,但也增加了维护成本。微服务架构在解决设计问题的同时,也引入了许多不确定因素,原本一个服务可以执行完所有流程,现在要调用多个微服务来协同工作。如果某个服务出现异常或者网络出现拥堵,数据一致性和可靠性并不能得到保证,也会引发出一系列数据异常问题。因而需要对分布式系统的幂等性加以考虑。

为了解决分布式系统的幂等性问题,很多学者做了相关尝试和研究。文献[4]提出了使用数据库锁来保证系统幂等性,采用唯一索引和悲观锁的方式管理并发访问;文献[5]探讨了使用Redis实现分布式锁,各微服务通过竞争锁获取程序执行权限,从而控制程序的并发访问;文献[6]对Redis实现分布式锁进行了改进,使用了二次拦截,来保证程序的并发运行。

在设计分布式系统服务幂等方案时,要结合业务需求充分考虑高并发场景以及在该场景下服务接口的性能表现,同时密切关注服务器各项性能指标及性能损耗。本文通过对多种幂等方法设计比较分析,提出一种改进的基于中间件的分布式锁方法。实验结果表明,该方法能够保证在高并发场景下数据的准确性和一致性,且性能表现较好。

1 幂等性概念

幂等(idempotence)是数学与计算机学概念,常见于抽象代数中。在编程中,一个幂等操作的特点是函数方法经过任意多次执行所产生的影响与一次执行所产生的影响相同。幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。幂等函数不会影响系统状态,也不用担心重复执行会对系统造成任何改变。在实际系统中有很多操作,无论做多少次重复操作,系统都应该产生一样的效果或返回一样的结果。例如:①前端重复提交表单数据,后台服务应该只产生对应这条数据的一种响应结果;②同样的消息应该只发送一次给用户;③一次请求只能创建一个订单等,很多重要场景都需要满足服务幂等性。

2 幂等性技术方案

分布式系统幂等性设计,可以从客户端和服务端双侧考虑,由于系统幂等性问题的根本原因存是于服务端,所以从客户端考虑意义不大,而且会影响用户体验。由上文可知,服务端幂等性设计方法有以下两类:①对数据库加锁控制并发访问;②在代码层依赖第三方技术实现分布式锁控制并发访问。而第三方技术又有多种选择,幂等设计方法随着科学技术发展也在不断进化。

对于系统服务的幂等性问题,主要出现在操作共享数据上,涉及数据库及消息推送。对于数据库操作来说,不涉及修改共享数据的查询和删除属于幂等操作,不会引发数据一致性问题,而涉及修改共享数据的插入和更新操作则不符合幂等性,它们也是最容易出现幂等问题。对于消息推送存在的幂等性问题,主要发生在消费者在消费完一条消息后,要向消息发送方发送一个ack确认请求,如果此时由于网络异常或者其他原因导致消息发送方并没有收到这个ack确认请求,那么此时消息发送方并不会将该条消息删除,即使该条消息已经被消费者消费,当重新建立起连接后,消费者还是会再次收到该条消息,这就造成了消息的重复消费。由于类似的原因,消息在发送时,同一条消息也可能会发送多次,种种原因导致在消费消息时,也会存在幂等性问题。

由上文可知,可以通过为数据库添加唯一索引和悲观锁等方法保证分布式系统的幂等性,还可借助第三方技术实现分布式锁来解决系统幂等性问题,常用的第三方技术主要有Zoo-Keeper和Redis。

2.1 唯一索引

数据库索引是一种辅助查询的数据结构,该结构记录数据表一列或多列值排序结果,类似于新华字典中音序表。信息搜索者通过索引可以快速高效地查询并获取符合条件的数据,当然索引也需要占用数据库资源,并不是索引构建越多,查询越快。

数据库索引类型包括:主键索引、普通索引、组合索引、全文索引和唯一索引。其中,唯一索引规定索引列的值必须唯一但可以为空,它不允许任意两条数据具有相同索引值。当为已存在数据的表建立唯一索引时,若需要建立索引的列已经存在相同的键值,创建索引将会失败,当向已建立唯一索引的数据表插入重复索引列的数据时也会失败。

利用唯一索引上述特性可以保证数据表中每一条数据具有唯一性,故可以使用唯一索引来保证业务数据插入数据库的唯一性和准确性,因此该方式可以作为系统幂等设计方法之一。在高并发场景下,通过对数据库表添加唯一索引会使重复插入的数据插入失败,但随着时间推移,表数据量仍会增长,当表数据达到一定数量后,系统响应时间会相应增加。因为频繁的I/O写入将导致数据库磁盘负载增加,数据库性能开销增大,在高并发场景下会给系统带来比较大的压力。

2.2 悲观锁

悲观锁(pessimistic locking)是基于数据库事务实现的。在关系型数据库中,事务有四个基本要素(ACID):原子性、一致性、隔离性和持久性。悲观锁认为,当某一事务操作数据资源时,其他事务也会操作该数据资源,所以在任何事务操作数据前都需要先对数据加锁,在该事务操作数据期间,其他任何事务都不能操作该数据,只有当前事务操作结束释放锁后,其他事务才能继续操作数据资源。这种借助数据库锁机制在修改数据之前先锁定、再修改的方式被称之为悲观并发控制,所以叫悲观锁。悲观锁从数据处理的安全性考虑,采用“先取锁再访问”的保守策略,但是在效率方面,悲观锁具有独占和排他特性,某个事务处理占用锁时,其它事务只能处于阻塞状态。此外,悲观锁也会让数据库产生额外的开销,还有增加产生死锁的机会。同时还会降低数据库并行性,只能适用于并发不高的场景。对于高并发场景下事务抢占资源效率并不是很高,严重时甚至会导致应用系统崩溃。

2.3 乐观锁

乐观锁(optimistic locking)是相对悲观锁而言,如果说悲观锁是一种避免冲突的手段,那乐观锁则是一种在最后提交数据时检测冲突的手段。乐观锁并没有借助数据库事务和锁机制,而是从系统应用层面和数据业务逻辑层面考虑,利用程序代码处理并发问题。乐观锁主要是两个步骤:冲突检测和数据更新,其比较典型的实现方式是Compare and Swap(CAS)技术。

乐观锁假定当某一事务操作数据时,对其他事务操作该数据持乐观态度。乐观锁通过对数据库表增加“版本号”Version字段检测事务冲突,在事务最后提交数据时会进行版本的检查,以判断在该事务操作过程中,是否有其他事务操作该数据。乐观锁只在更新数据那一刻锁表,其他时间并不锁表,减少了数据库的压力,所以相对于悲观锁,效率更高、开销较小。

乐观锁大部分都是基于版本控制实现的,其工作过程如图1所示。

图1 乐观锁工作过程

A事务读取数据并记录当前Version为1,当A事务需要进行更新操作时,会将Version值自动加1更新为2;B事务依次执行,B事务读取数据时Version为2,更新数据时也会自动将Version值加1更新为3,此情况下A、B两个事务提交不会发生冲突,流程如图1左。如果A事务、B事务同时读取同一条数据时的Version值是1,A事务先进行更新操作,将Version值自动加1变为2后立即进行更新操作,当B事务再执行更新操作准备将Version更新为2时,此时已查询不到Version值为1的数据,则发生冲突,B事务更新失败。

2.4 分布式锁

分布式锁是解决分布式系统幂等性问题的又一方式。其核心思想是:所有服务实例通过竞争存放于同一个地方的同一把分布式锁,服务示例通过竞争获取锁和释放锁,最终完成分布式系统的并发访问。实现分布式锁需借助第三方技术,常用的技术主要有Redis和Zookeeper等,本文主要基于Zookeeper实现分布式锁,对于Redis实现分布式锁给出实验对比数据。

Zookeeper是使用层次树型结构的命名空间数据模型,类似Unix系统文件目录树结构,如图2所示。

图2 Zookeeper数据模型

层次树中的每个节点称为一个Znode,所有Znode都可以储存信息,并且所有Zonde都可以拥有子Znode,临时节点除外。Zookeeper中的Znode有三类:①永久节点(persistent node),此类节点在创建完成后将永久存在,除非Client手动显式删除,否则该类节点将永久储存于Zookeeper中;②临时节点(ephemeral node),顾名思义,该类节点临时有效,只有Client与Server保持连接时存在,一旦二者连接中断,Zookeeper会自动清除此类型节点;③顺序节点(sequence node),此类节点具有先后顺序,当Zookeeper在创建该类型节点时,会自动在节点名称末尾补充一个递增序列,节点序列递增且不会重复。例如,客户端申请创建子节点“/lock/node-”并且指明有序,那么Zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号,也就是说,如果是第一个创建的子节点,那生成的子节点为“/lock/node-00000000”,下一个节点则为“/lock/node-00000001”,依次类推。

Zookeeper使用临时节点和有序节点再配合Zookeeper提供的事件监听机制可以实现分布式锁。Zookeeper在读取Znode存储信息,判断Znode是否存在,获取Znode子节点等操作时都可以设置相应的事件监听,此事件监听是一次性触发器,当被监听数据发生改变时,服务器会向设置相应监听器的Client发送通知,但该通知并不会包含本次操作改变的内容。Zookeeper实现分布式锁的具体步骤如下,流程如图3所示。

图3 Zookeeper分布式锁流程图

(1)创建一个锁目录“/lock”;

(2)客户端A获取锁,会在“/lock”目录下创建临时顺序节点,获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;

(3)客户端B创建临时节点并获取所有兄弟节点,判断自己不是最小节点,设置监听(watcher)比自己次小的节点,这里客户端B只关注比自己次小的节点是为了防止发生“羊群效应”;

(4)客户端A处理完,删除自己的节点,客户端B监听到变更事件,判断自己是最小的节点,获得锁;

正常情况下,上述分布式锁是可以满足需求的,但仍然存在两个问题:

由于网络出现异常或者线程GC停顿导致Zookeeper服务端长时间检测不到客户端心跳,使得临时节点被删除,锁被提前释放,但此时业务逻辑可能会依然并发执行。如果改为使用永久节点,会出现因为会话异常关闭导致死锁问题,具体如图4所示。

图4 分布式锁异常流程图

Zookeeper环境部署时既支持单机架构部署也支持集群架构部署。显而易见,单机架构部署会存在一定风险,因为只要Zookeeper服务出现异常,整个系统也将变得不可访问,此外,该部署方式对服务器配置要求较高,因此Zookeeper集群部署是需要考虑的。由于Zookeeper采用称为Quorum Based Protocol的数据同步协议,该同步协议决定了Zookeeper数据同步的不强一致。假如Zookeeper集群有台Zookeeper服务器,客户端的一个写操作,首先会同步到/2+1台服务器上,然后返回给客户端并提示写成功。因为Zookeeper是同步写/2+1个节点,还有/2个节点没有同步更新,所以在集群环境部署情况下,Zookeeper存在数据不是强一致情况。

3 基于Zookeeper改进的分布式锁

对于上面阐述的两个问题,现提出一种改进的分布式锁方法。在业务代码中,根据对业务操作数据库的类型进行拦截判断,如果是插入、更新数据等操作,则在数据库层面对数据表再建立数据锁或唯一索引,增加异常情况下对分布式锁的二次拦截。这样可以有效避免因网络异常或者Zookeeper集群环境数据同步非强一致性问题而导致分布式锁被提前释放,核心业务代码依然并发执行的问题。具体改进步骤如下:对所有业务逻辑操作数据表的类型进行拦截判断,若操作类型是插入数据,则为该数据表建立唯一索引,以此解决业务数据多次插入问题;若业务操作类型是更新操作,则为相应数据表建立乐观锁,确保更新数据的正确性和准确性。由上文知,这里数据库锁选择采用乐观锁是因为悲观锁属于阻塞模式类型,无论是更新还是插入操作都会锁定整张数据表,不适合用于高并发场景下数据的操作,而乐观锁对数据操作持乐观态度,并不会对数据表进行加锁处理,从而降低了数据库的损耗,在高并发场景下对数据库性能有一定提升。同时,将二次拦截失败的业务操作存入消息队列,以消息形式推送给运维人员及时排查问题。如果是网络异常可以及时进行备机切换,如果是Zookeeper集群环境节点故障,可以迅速定位到Zookeeper集群问题。为了防止通知消息发生同步阻塞现象而影响核心业务处理,所以通知消息考虑以异步方式发送,基于此,消息队列选用RabbitMQ实现消息的生产和消费。改进的分布式锁设计整体工作过程如图5所示。

图5 基于Zookeeper改进的分布式锁

4 实验结果与分析

下面通过使用JMeter测试工具模拟高并发场景,并验证改进的分布式锁在高并发场景下的数据一致性和性能表现。

服务器配置:CentOS7系统,Intel Xeon E-2388G 8核16 G内存实验软件环境基于Docker容器实现:Nginx服务、Zookeeper集群服务(5个节点)、多个微服务实例、RabbitMQ服务、MySQL数据库。为了与Redis测试对比,同时部署Redis集群(5个节点)。

通过JMeter设置2000个并发线程数,秒杀1000个商品库存场景,使用唯一索引、乐观锁、悲观锁、Redis分布式锁、Zookeeper分布式锁五种幂等方法分别独立测试,查看各个方法的性能表现及数据一致性,最后再对改进的Zookeeper分布式锁进行测试,分别模拟在发生网络异常及Zookeeper集群环境某个节点宕机情况下分布式锁的性能问题。更新操作类型测试结果参见表1,插入操作类型测试结果参见表2。

表2 各幂等方法插入操作性能对比

在结果性能指标中,拦截次数及成功次数说明幂等方法保证数据准确性和一致性的能力,总耗时和平均响应时间反映了高并发场景下幂等方法的性能开销。表1实验结果表明:唯一索引、悲观锁和乐观锁都可以解决幂等性问题,但它们的平均响应时间较长,性能较低,系统开销较大。Redis分布式锁相对于前面三种方法性能有较大提升,改进Zookeeper分布式锁与Redis性能相当。当断开网络或者关闭Zookeeper集群中一个节点和Redis集群中一个节点时,Redis锁和Zookeeper锁出现数据不一致的情况,而改进后的Zookeeper锁数据一致性得到保证,同时RabbitMQ消费者收到多条推送过来的消息。

表1 各幂等方法更新操作性能对比

表2实验结果表明,对于插入业务操作类型,锁的二次拦截性能方面满足预期效果。同样断开网络或者关闭Zookeeper集群中一个节点和Redis集群中一个节点时,Redis锁和Zookeeper锁执行数据操作时会存在重复数据,改进后的Zookeeper锁数据一致性达到预期,虽然改进后的Zookeeper锁平均响应时间增加了,但数据一致性得到了保证,牺牲毫秒级的时间来保证数据一致性,此结果是可以令人接受的。

综上所述,唯一索引方式与悲观锁方式性能接近,数据一致性也能得到保证,但效率较低;乐观锁效率好于前两者,相比Redis分布式锁和Zookeeper分布式锁,性能又提升了不少,但数据一致性并不能保证;改进后的Zookeeper分布式锁牺牲毫秒性能却换来数据一致性,满足实际需求。

5 结语

本文通过对微服务架构模式的分析,引出了服务幂等性问题。随后通过分析服务幂等性问题的原理及现有的解决方案,具体研究了Zookeeper分布式资源锁的实现,并对Zookeeper服务器处理过程进行分析,提出了改进的分布式锁。根据对业务操作类型做判断,进行锁的二次拦截,解决因网络异常或集群节点异常而导致数据不一致问题,改进的分布式锁方案能够保证服务幂等性和资源的强一致性,满足大多数应用的需要。最后通过实验模拟高并发场景,测试证明了优化方案的有效性。

猜你喜欢
一致性事务分布式
离散异构线性多智能体系统的输出一致性
居民分布式储能系统对电网削峰填谷效果分析
基于学科核心素养的“教、学、评”一致性教学实践——以“电解质溶液”教学为例
基于Paxos的分布式一致性算法的实现与优化
针对基于B/S架构软件系统的性能测试研究
一种Web服务组合一致性验证方法研究
Hibernate框架持久化应用及原理探析
SQL SERVER中的事务处理教学研究
揪出那只“混进革命队伍里的猫”