一段“平淡无奇”代码引起的反思

一段“平淡无奇”代码引起的反思

前言

       有时候盯着一段“平淡无奇”的代码进行细细品味时,是不是都会有一种错觉:“哪里感觉好像有问题,可一时又说不上来”。这个时候,请为自己画地为牢,用内心的好奇禁锢住离开的脚步,驻足一下,开启探坑模式。当自己没有给自己一个完美答案的时候,你会发现,放弃比坚持更难

缘由

有次接到电话:”内部联调环境,从PB手动添加交易所用户,但是半个小时过后,这些用户没有同步到应用支撑库中“。第一反应,就是联调环境的同步开关没有打开。但电话那头的回答是:“这些应用程序都是从生产拷贝过来,只把数据库连接修改掉的,其它配置没有动。开关和生产一样,肯定是打开的“。脑海突然闪现出春节后,有一次定时任务无缘无故终止的异常情况,是不是这个问题又出现了。而自己不由自主的打开IDE找到对应的代码。同时让运维人员再次确认一下同步开关的配置,结果是同步开关真的被关闭了。问题虽然解决了,但是自己的眼睛看着这段很平常的代码好像发现了一些问题,陷入了沉思。

代码实现

这段代码功能挺简单,就是每隔半个小时从结算库把用户信息同步到应用支撑库。

设计实现方式如下:

  1. 基于Spring自带的@Scheduled定时任务模式
  2. 采用Aspect AOP的Around环绕增强,在真正业务执行前需要获取锁
  3. 为防止并发执行,实现Redis分布式锁机制

       看似设计、实现行云流水,行于所当行,止于所当止,没有任何问题。但是细细的去品味,问题就会接踵而来。

问题

  1. 基于Redis分布式锁的问题及解决方案

 

  1. 第1步设置锁的key与值到Redis中
  2. 第2步为此key设置过期时间

问题1:

由于第一步和第二步不是原子型操作,当第一步设置成功后,第二步还未执行时,服务器故障,那么此锁永远无失效时间。既然是分布式锁,那么其它服务器的应用一直获取不到锁,造成死锁,定时任务不能触发。

解决方案:

调用redisClientTemplate的如下API即可

 

注意,第三个参数nxxx的意思是:“set if not exist”,即当key不存在时,进行set操作;当key已经存在时,则不做任何操作。

问题2:

由于此系统设计时,就考虑支持分布式。比如现在有两台服务器,第一台和第二台服务器都开始获取分布式锁,当都执行到步骤1时,那两台服务器均可获取到锁(即结果返回true)造成两台服务器均执行定时任务,出现并发情况,造成业务上的同步问题。

另外一点,第一个被设置的锁的value,会被第二个给覆盖,造成一些假象,明明第一个获取锁的服务器在Redis中存储的value竟然是第二个服务器的hostAndPort。

  1. “等的花儿都谢了“

 

问题:当获取不到锁时,为何要让定时任务线程等待这么长时间呢?设计很不合理,线程切换、唤醒等会造成资源浪费。

解决方式:直接返回即可,无需让线程徒劳的等待。

  1. @Scheduled使用问题

 

问题:在这个项目中有三处使用@Scheduled(即三个定时任务,比如定义为:定时任务A,定时任务B,定时任务C),而@Shceduled默认启动只有一个线程的线程池。那么当任何一个定时任务执行比较耗时时,其它定时任务就获取不到线程去执行,造成定时执行延迟。更甚者,可能某个定时任务永远获取不到线程,造成业务处理不了。会造成一种定时任务执行不生效的错觉。

解决方式:

 

如上图,把定时任务注册到ScheduledTaskRegistrar,并给ScheduledTaskRegistrar注册一个线程池,线程池的corePoolSize个数可以根据实际的业务进行定义(建议此数值不小于定时任务个数)。这样的话,每个定时任务的执行,不会受其它定时任务执行快慢的影响。

另外可以设置ErrorHandler,当定时任务执行失败时,会在catch中获取此errorHandler,并执行相应的方法。所以,可以自己实现此Errorhandler,处理自己的逻辑。

       上面实现是不是就可以了呢?如果追求更进一步的话,那就给线程池传递一个自定义的ThreadFactory对象,给池中的线程设置一个明确的业务名称前缀(若无设置,可以参考《I need 安慰,Tomcat》的悲催旅程)。

反思

这段代码为18年写的,源代码作者已经离开项目组,那么随着时间的推移,新的维护者对旧代码的关注度逐渐会降低。而当新需求要开发时,比如新增一个定时任务,可能就会仿照现有实现方式,直接定义个定时任务(其实第三个定时任务就是仿照前两个来写的,因为已经运行那么长时间了,潜意识认为比葫芦画瓢去做,肯定不会有问题),更不会深入的去看一下具体的细节。通过上面代码的梳理存在以下原因以及潜在的问题:

  1. 开发人员对@Scheduled机制底层源码及实现机制不理解而直接使用
  2. 对Redis分布式锁的实现有纰漏,未考虑周全
  3. 系统测试时(与测试人员沟通,没有进行过分布式部署及测试过。),若不与测试人员沟通实现方式,这些异常场景是否能够被测试到?
  4. 当生产环境进行主备切换或者采用多节点模式,是否还能正常、稳定运行?
  5. 现有的代码中,还有哪些类似这样的问题存在?
  6. 当生成环境突发问题时,这些可能都成为干扰因素,误导排查方向,从而影响排查进度
  7. 特别像定时任务的问题,一般都是周期性的,复现很难。

建议

  1. 对线程池、定时任务、分布式锁、事务、通用工具类等方面的设计、开发,加强代码走查。
  2. 对上面的知识点,进行”扫盲“培训、学习。
  3. 梳理出系统的一些特殊点,形成检查单,维护起来,方便后续生产环境的主备切换、多节点部署。建议真正的定期演练一下主备切换、多节点部署模式,免得真的生产环境有问题,而准备不充分。
  4. 特殊实现点一定要与系统测试充分沟通,考虑极端场景,以及应急方案。
  5. 对原有的老代码,进行逐行排查一下,找出潜在的隐患。

最后以一句话共勉一下:对每一行代码要怀着敬畏之心,之前所踩过的坑,最后都将会成为你最核心的竞争力。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一路乘风向前进

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值