欢迎关注头条号:老顾聊技术
精品原创技术分享,知识的组装工
小伙伴们有没有遇到过生产环境经常出现过重复的数据?在排查问题的时候,数据又是正常的。这个是何解呢?怎么会出现这种情况,而且还很难排查问题。今天老顾给大家分享一下这里的原因,以及解决方案。
产生重复数据或数据不一致(假定程序业务代码没问题),绝大部分就是发生了重复的请求,重复请求是指同一个请求因为某些原因被多次提交。导致这个情况会有几种场景:
1)微服务场景,在我们传统应用架构中调用接口,要么成功,要么失败。但是在微服务架构下,会有第三个情况【未知】,也就是超时。如果超时了,微服务框架会进行重试。
2)用户交互的时候多次点击。如:快速点击按钮多次。
3)MQ消息中间件,消息重复消费
4)第三方平台的接口(如:支付成功回调接口),因为异常也会导致多次异步回调
5)其他中间件/应用服务根据自身的特性,也有可能进行重试。
我们知道了发生的原因,本质就是多次请求了,那如何解决呢?
有些小伙伴们会想到幂等这个词,是的,就是我们在设计某些接口时,要考虑如何保证接口幂等,那什么是接口幂等呢?
网上是这样介绍的【接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的】
网上的说法定义,有点不是太正确,我们看下怎么不正确
如一个线程请求用户列表接口:select * from user,返回用户表中的数据,而另一个线程往用户表插入数据。那请求用户列表的线程返回的数据每次都不一样,那按照上面的说法,查询用户列表的接口就不是幂等的,这显然是不正确的。
老顾的理解应该是多次调用对系统的产生的影响是一样的,即对资源的作用是一样的,但是返回值允许不同。
我们来看一下SQL相关业务是否幂等?
一、查询,select * from user where xxx,不会对数据产生任何变化,具备幂等性。
二、新增,insert into user(userid,name) values(1,'a'),
如userid为唯一主键,即重复操作上面的业务,只会插入一条用户数据,具备幂等性。
如userid不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性。
三、修改,区分直接赋值和计算赋值。
1、直接赋值,update user set point = 20 where userid=1,不管执行多少次,point都一样,具备幂等性。
2、计算赋值,update user set point = point + 20 where userid=1,每次操作point数据都不一样,不具备幂等性。
四、删除,delete from user where userid=1,多次操作,结果一样,具备幂等性。
上面场景中,我们发现新增没有唯一主键约束的数据,和修改计算赋值型操作都不具备幂等性
那怎么去解决呢?
网上介绍很多,但介绍的太简单了,且关键点都没有介绍到。老顾这里只介绍常用的方案
token方式的流程,上一张图,比较清晰
上图就是token+redis的幂等方案,适用绝大部分场景。主要思想:
1、服务端提供了发送token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中。(微服务肯定是分布式了,如果单机就适用jvm缓存)。
2、然后调用业务接口请求时,把token携带过去,一般放在请求头部。
3、服务器判断token是否存在redis中,存在表示第一次请求,可以继续执行业务,执行业务完成后,最后需要把redis中的token删除。
4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。
这种方案是比较常用的方案,也是网上经常介绍的,但是有一点不同的地方:
网上方案:检验token存在(表示第一次请求)后,就立刻删除token,再进行业务处理
上面方案:检验token存在(表示第一次请求)后,先进行业务处理,再删除token
关键点就是 先删除token,还是后删除token。
一、网上方案缺点
我们看下网上方案,先删除token,这是出现系统问题导致业务处理出现异常,业务处理没有成功,接口调用方也没有获取到明确的结果,然后进行重试,但token已经删除掉了,服务端判断token不存在,认为是重复请求,就直接返回了,无法进行业务处理了。
二、上面方案缺点
后删除token也是会存在问题的,如果进行业务处理成功后,删除redis中的token失败了,这样就导致了有可能会发生重复请求,因为token没有被删除
小伙伴们有没有发现,其实上面的问题就是数据库和缓存redis数据不一致的问题。之前老顾分享了一篇文章,里面详细介绍了如何解决数据库和缓存redis数据不一致的问题。小伙伴们可自行查阅。
其实根据这个场景的业务,可以有个简单的处理方式。老顾推荐是网上方案先删除token,先保证不会因为重复请求,业务数据出现问题。顶多再让用户处理一次。
出现业务异常,可以让调用方配合处理一下,重新获取新的token,再次由业务调用方发起重试请求就ok了。
小伙伴们有没有发现,业务请求每次请求,都会有额外的请求(一次获取token请求、判断token是否存在的业务)。其实真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试,为了这10个请求,我们让9990个请求都发生了额外的请求。(当然redis性能很好,耗时不会太明显)
关于乐观锁老顾之前也讲过,大家可以去查阅。乐观锁这里解决了计算赋值型的修改场景。我们对之前的sql语句进行修改。
update user set point = point + 20, version = version + 1 where userid=1 and version=1
加上了版本号后,就让此计算赋值型业务,具备了幂等性。
就是在操作业务前,需要先查询出当前的version版本
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键,之前老顾的文章也介绍过分布式唯一主键ID的生成,可自行查阅。
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。
因为对主键有一定的要求,这个方案就跟业务有点耦合了,无法用自增主键了。
这个方案业务中要有唯一主键,这个去重表中只要一个字段就行,设置唯一主键约束,当然根据业务自行添加其他字段。主要流程上图
上面的主要流程就是 把唯一主键插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。
这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
这个方案也是比较常用的,去重表是跟业务无关的,很多业务可以共用同一个去重表,只要规划好唯一主键就行了。
上面介绍了一些幂等方案,小伙伴们根据自身的业务进行选择,尽量不要让系统变的复杂,所以推荐唯一主键和乐观锁方式,因为实现比较简单。好了,今天就介绍到这里,谢谢大家!!!
-End-
如有收获,请帮忙转发,您的鼓励是作者最大的动力,谢谢!
10几年的经验实战分享
相关微服务,分布式,高并发,高可用,企业实战,干货等原创文章正在路上
欢迎关注头条号:老顾聊技术