当前位置: 首页 > 科技观察

我负责的系统总是出现数据错误,领导催促我优化系统架构,这有难度

时间:2023-03-22 14:21:42 科技观察

业务背景今天和大家聊一聊线上系统的接口幂等性,以及如何通过分布式锁来保证接口的幂等性。同时分享一些我们基于分布式锁实现接口幂等性时在生产中实践经验的积累。首先我告诉大家,如果我们在线系统的核心接口没有幂等的保证机制会怎么样?其实很简单。假设您有一个带有接口的系统。当这个接口接受一个请求时,假设一条数据将被插入到数据库中。一般情况下,向该接口发起请求的用户应该只有一条数据。结果可能一定有一天你会发现这个用户通过这个接口插入了多条数据。如下图1所示:第一版防重复代码那么为什么会这样呢?其实我们一般都是用这种系统界面,但是如果写的好一点,我们会在界面里面加上防重码。即会有代码判断你要写入的数据当前是否存在。不存在则插入,存在则不允许重复插入。这个防重复代码如下:publicvoidbusiness(Requestrequest){//1.首先根据请求参数在db中查询这条数据Datadata=findData(request);//2.如果这个数据在db中已经存在,则直接返回if(data!=null){return;}//3.如果db中不存在这条数据,此时会执行数据插入逻辑。插入数据(请求);}结合上面代码的反重逻辑,我们可以看到下图2的运行逻辑展示:在插入数据之前,会根据请求参数查询数据。如果查询到,此时会直接返回。重复插入,但是如果没有查询到这条数据,此时就会插入这条数据。那么你可能就有问题了,既然你已经有了这个反重逻辑,即使你用同一个请求参数重复调用这个接口插入数据,你也不应该重复插入数据!据说确实是这样,但是凡事总有例外,那就是著名的瞬时重试+多线程并发问题。瞬时重试+多线程并发问题分析下面给大家解释一下,在上面提到的代码防重逻辑下,如果用户在短时间内重复发起两次相同请求参数的请求,为什么会反-重复被穿透?从逻辑上讲,将两条相同的数据插入到数据库中。每个人都应该起来仔细看看这个过程。首先,用户可能会因为过度激动、握手、网络抽搐等各种原因,在一瞬间发起两个请求参数相同的请求。如下图3所示:接下来,这两个请求到达我们的系统后,实际上分别由一个线程处理。无论是使用tomcat部署提供的controller接口,还是dubbo提供的rpc接口,其实每个请求都是由一个单独的线程来处理的。如下图4所示:接下来两个线程会并发运行同一段代码逻辑,即先根据请求参数查询数据是否存在,存在则返回,不存在则插入.这时候可能会出现问题,因为是多线程并发的,所以很可能这两个线程会同时执行数据查询逻辑,但是当他们都同时执行数据查询逻辑时,出现了一个问题,就是此时数据库中没有数据。数据!所以,这两个线程并发运行,完全有可能同时发现从数据库中查询到的数据为空。如下图5所示:那么这个时候两个线程既然发现查询到的数据是空的,当然可以插入数据了。所以这时候两个线程会根据这个请求参数分别插入一条数据,而这条数据对于业务来说其实是完全重复的,因为请求参数是完全一样的。如下图6所示:这时候就会造成数据重复的问题。针对这种情况,我们一般称这个接口为非幂等的。因为如果一个接口是幂等的,实际上如果用同一个参数发起对这个接口的请求,那肯定只有一条数据,不可能有重复的数据。这称为幂等性。现在的问题是同一个请求参数多次发起这个接口,结果数据重复。这时候接口就不是幂等的了。用数据库的唯一索引实现幂等针对上面提到的接口幂等问题,一个比较简单的解决方案是基于我们所依赖的数据库实现幂等。也就是说可以利用数据库的唯一索引来实现。如果我们根据请求中的一个或多个业务字段形成唯一索引,那么实际上如果要向数据库中插入相同参数的重复数据是不可能的。因为数据库层面会阻止你插入,唯一索引会保证这一点,如果你要重复插入,他会抛出异常。如下图7:分布式锁实现幂等但是很多时候我们会发现一个问题,就是我们不一定每次都说依赖数据库的唯一索引就可以实现这种幂等。因为有可能你的业务逻辑中,除了依赖数据库之外,还依赖其他的服务接口,或者elasticsearch、redis等各种数据存储,也可能依赖数据库中多个表中的数据。有可能每个表都有一个唯一的索引来保证幂等性。因此,对于业务逻辑复杂的接口,要保证幂等性,往往需要引入一个关键组件,即分布式锁。所谓分布式锁,就是依赖外部系统加锁。添加锁之后,后面可以释放锁。现在比较常见的分布式锁实现主要依赖redis和zookeeper。实现,我们这里以redis分布式锁为例。简单的说,我们可以在接口的入口代码处加一个基于redis的分布式锁。此时只有一个线程可以成功加锁。锁定后,该线程可以检查数据是否存在。如果不存在,可以往里面插入一条数据,然后释放锁。这个过程中,另一个线程无法获取到redis分布式锁,所以我只能等待。如下图8所示:等待第一个线程加锁,然后查询数据,发现数据不存在,再插入一条数据,最后释放锁,这时第二个线程就可以得到机会再次加锁,然后第二个线程加锁后,查询数据,发现数据已经存在。此时会直接返回,不会重复插入数据。如下图9所示:如上图所示,可以发现只要在核心接口的入口处加一个分布式锁,就可以实现多线程并发,不会重复执行复杂的业务逻辑,并且它不依赖于数据库。一个表的唯一索引,只要加锁和释放锁都是基于redis实现的。至于redis分布式锁是如何实现的,不在本文的讨论范围内。本次主要为大家分析在线系统接口的幂等问题。没有幂等性的时候,接口是怎么工作的呢?多线程并发场景下会出现数据重复的问题。总结然后我们分析了如果在数据库表的基础上加上唯一索引,接口可以做到幂等,但是如果业务逻辑太复杂,数据存储很多,或者涉及到很多表,那么就可以'这时候就不能只依赖唯一索引了,需要依赖在接口入口加分布式锁,才能解决复杂接口的幂等性。