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