问题引发场景
晚上11点接到公司电话反馈线上某功能场景出现重复执行的情况,需要紧急修复。然后自己跑到公司经过排查定位发现是项目中某一个业务接口(例如:A接口)接受MQ消息内容进行相关业务逻辑的处理,而MQ信息来源于另一个系统的推送。正常业务逻辑MQ里面的信息内容根据具体业务场景是唯一的,而由于外部系统原因出现了重复的内容。对应A接口来说,就相当于同一个业务调用了多次A接口,类似于重复请求导致了问题的产生。
问题分析
等定位到此问题的时候,脑海里第一反应就是加锁,单线程就加同步锁,分布式的情况下就通过redis加分布式锁。具体到上述场景,仔细考虑可能存在:
- MQ消息出现重复推送的情况
- 一条MQ消息里面的内容出现了重复的业务消息
由于是接受MQ推送的消息,而且项目是多实例部署的,可行的方案就是加分布式锁。
想想自己跑到公司到定位到问题,已经凌晨两点了,而发版时间是凌晨6点。由于时间太短,匆匆忙忙加一个分布式锁,实现起来比较复杂,怕仓促之间可能因为其他业务或者其他问题考虑不全面的而引发其他问题。
后来自己想到一个既简单又快速的方式,就是通过数据库的操作的方式的顺利实现的(下文会具体介绍)。后来有空的时候,网络搜索此类问题的解决方案,结合自己的理解总结,整理成了笔记。
解决方式
什么是幂等?
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中,即f(f(x)) = f(x).**简单的来说就是一个操作多次执行产生的结果与一次执行产生的结果一致。**有些系统操作天生就具有幂等性例如数据库的select语句,但更多时候是需要程序员来做保证的,尤其是在分布式系统环境中,接口能不能做到保证幂等性对系统的影响可能是非常大的,例如很常见的支付下单等场景,由于分布式环境中网络的复杂性,用户误操作,网络抖动,消息重复,服务超时导致业务自动重试等等各种情况都可能会使线上数据产生了不一致,造成生产事故。
第一个反应就是加锁,不管是分布式锁还是单机锁,好像加了锁并发问题就真的不存在了似的,确实在很多情况下加锁是能解决问题的,但程序也变成了单线程执行, 还得注意锁不要加错了地方(先要搞清楚程序需要同步的临界区是什么)否则不但没能解决问题还降低系统TPS造成性能影响。
如何解决幂等?
方法一:同步锁
可以使用synchronized和ReadWriteLock去实现,可以保证多个线程并发访问不会有问题,但是无法解决系统集群或者分布式的场景。
方法二:分布式锁
我的思路是用redis去做,只是里面也有很多坑,比如超时时间控制问题,注意解锁问题,具体这里不细谈,感兴趣的朋友可以百度了解,以后有机会单独再写一篇。
方法三:token+唯一索引
根据具体业务场景,单独建立一张控制幂等的token数据表,新建一个token字段,并给此字段加上唯一索引,由数据库自动抛出异常,业务service来捕获最终返回给客户端友好的提示。
方法四:mysql的insert ignore
和方法三类似,只是利用了insert ignore特性,如果表中某个字段建有唯一索引,同样的除了第一次插入返回1外,其余皆返回0。那么先使用insert ignore 插入一条数据到token表中,根据返回的值为0或者1做相应的处理。开篇谈到的线上问题紧急修复方案采取的是此种方法。
@Modifying
@Query(nativeQuery = true,value = "insert ignore into token_business (token_bus) values (?1)")
int insertBusiness(String key);
当然,还有很多方式,比如微服务中采用全局唯一ID方式。
解决了上述场景中的幂等问题,同时会产生另一个场景问题,如果A接口的业务逻辑代码出现异常或者其他原因执行了,需要重新调用A接口该如何处理呢?直接删数据库表字段吗?
更多文章
开关配置springboot定时任务
重复请求的幂等接口设计的思考(一)
三年程序员的第一篇公号文章
自动化远程登陆操作和传输文件(scp ssh expect)
长按二维码关注,阅读我的程序员故事