小伙伴们有没有遇到过 生产环境经常出现过重复的数据? 在排查问题的时候,数据又是正常的。这个是何解呢? 怎么会出现这种情况,而且还很难排查问题 。今天我给大家分享一下这里的原因,以及解决方案。
产生 重复数据或数据不一致 (假定程序业务代码没问题),绝大部分就是发生了 重复的请求 , 重复请求是指同一个请求因为某些原因被多次提交 。导致这个情况会有 几种场景 :
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 的生成,可自行查阅。如果是 分库分表场景下 , 路由规则要保证相同请求下 , 落地在同一个数据库和同一表中 ,要不然 数据库主键约束就不起效果 了,因为是不同的数据库和表主键不相关。因为对主键有一定的要求,这个方案就跟业务有点耦合了, 无法用自增主键了 。
这个方案业务中要有唯一主键 ,这个去重表中只要一个字段就行, 设置唯一主键约束 ,当然根据业务自行添加其他字段。主要流程上图
上面的主要流程就是 把唯一主键插入去重表,再进行业务操作,且他们在同一个事务中。 这个保证了重复请求时,因为去重表有唯一约束,导致请求失败, 避免了幂等问题 。
这里要注意的是, 去重表和业务表应该在同一库中 ,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。 这个很好的保证了数据一致性 。
这个方案也是比较常用的, 去重表是跟业务无关的 ,很多业务可以共用同一个去重表,只要规划好唯一主键就行了。
上面介绍了一些幂等方案,小伙伴们根据自身的业务进行选择, 尽量不要让系统变的复杂 ,所以 推荐唯一主键和乐观锁方式,因为实现比较简单 。好了,今天就介绍到这里,谢谢大家!!!