一段“平淡无奇”代码引起的反思
前言
有时候盯着一段“平淡无奇”的代码进行细细品味时,是不是都会有一种错觉:“哪里感觉好像有问题,可一时又说不上来”。这个时候,请为自己画地为牢,用内心的好奇禁锢住离开的脚步,驻足一下,开启探坑模式。当自己没有给自己一个完美答案的时候,你会发现,放弃比坚持更难。
缘由
有次接到电话:”内部联调环境,从PB手动添加交易所用户,但是半个小时过后,这些用户没有同步到应用支撑库中“。第一反应,就是联调环境的同步开关没有打开。但电话那头的回答是:“这些应用程序都是从生产拷贝过来,只把数据库连接修改掉的,其它配置没有动。开关和生产一样,肯定是打开的“。脑海突然闪现出春节后,有一次定时任务无缘无故终止的异常情况,是不是这个问题又出现了。而自己不由自主的打开IDE找到对应的代码。同时让运维人员再次确认一下同步开关的配置,结果是同步开关真的被关闭了。问题虽然解决了,但是自己的眼睛看着这段很平常的代码好像发现了一些问题,陷入了沉思。
代码实现
这段代码功能挺简单,就是每隔半个小时从结算库把用户信息同步到应用支撑库。
设计实现方式如下:
- 基于Spring自带的@Scheduled定时任务模式
- 采用Aspect AOP的Around环绕增强,在真正业务执行前需要获取锁
- 为防止并发执行,实现Redis分布式锁机制
看似设计、实现行云流水,行于所当行,止于所当止,没有任何问题。但是细细的去品味,问题就会接踵而来。
问题
- 基于Redis分布式锁的问题及解决方案
|
- 第1步设置锁的key与值到Redis中
- 第2步为此key设置过期时间
问题1:
由于第一步和第二步不是原子型操作,当第一步设置成功后,第二步还未执行时,服务器故障,那么此锁永远无失效时间。既然是分布式锁,那么其它服务器的应用一直获取不到锁,造成死锁,定时任务不能触发。
解决方案:
调用redisClientTemplate的如下API即可
|
注意,第三个参数nxxx的意思是:“set if not exist”,即当key不存在时,进行set操作;当key已经存在时,则不做任何操作。
问题2:
由于此系统设计时,就考虑支持分布式。比如现在有两台服务器,第一台和第二台服务器都开始获取分布式锁,当都执行到步骤1时,那两台服务器均可获取到锁(即结果返回true)造成两台服务器均执行定时任务,出现并发情况,造成业务上的同步问题。
另外一点,第一个被设置的锁的value,会被第二个给覆盖,造成一些假象,明明第一个获取锁的服务器在Redis中存储的value竟然是第二个服务器的hostAndPort。
- “等的花儿都谢了“
|
问题:当获取不到锁时,为何要让定时任务线程等待这么长时间呢?设计很不合理,线程切换、唤醒等会造成资源浪费。
解决方式:直接返回即可,无需让线程徒劳的等待。
- @Scheduled使用问题
|
问题:在这个项目中有三处使用@Scheduled(即三个定时任务,比如定义为:定时任务A,定时任务B,定时任务C),而@Shceduled默认启动只有一个线程的线程池。那么当任何一个定时任务执行比较耗时时,其它定时任务就获取不到线程去执行,造成定时执行延迟。更甚者,可能某个定时任务永远获取不到线程,造成业务处理不了。会造成一种定时任务执行不生效的错觉。
解决方式:
|
如上图,把定时任务注册到ScheduledTaskRegistrar,并给ScheduledTaskRegistrar注册一个线程池,线程池的corePoolSize个数可以根据实际的业务进行定义(建议此数值不小于定时任务个数)。这样的话,每个定时任务的执行,不会受其它定时任务执行快慢的影响。
另外可以设置ErrorHandler,当定时任务执行失败时,会在catch中获取此errorHandler,并执行相应的方法。所以,可以自己实现此Errorhandler,处理自己的逻辑。
上面实现是不是就可以了呢?如果追求更进一步的话,那就给线程池传递一个自定义的ThreadFactory对象,给池中的线程设置一个明确的业务名称前缀(若无设置,可以参考《I need 安慰,Tomcat》的悲催旅程)。
反思
这段代码为18年写的,源代码作者已经离开项目组,那么随着时间的推移,新的维护者对旧代码的关注度逐渐会降低。而当新需求要开发时,比如新增一个定时任务,可能就会仿照现有实现方式,直接定义个定时任务(其实第三个定时任务就是仿照前两个来写的,因为已经运行那么长时间了,潜意识认为比葫芦画瓢去做,肯定不会有问题),更不会深入的去看一下具体的细节。通过上面代码的梳理存在以下原因以及潜在的问题:
- 开发人员对@Scheduled机制底层源码及实现机制不理解而直接使用
- 对Redis分布式锁的实现有纰漏,未考虑周全
- 系统测试时(与测试人员沟通,没有进行过分布式部署及测试过。),若不与测试人员沟通实现方式,这些异常场景是否能够被测试到?
- 当生产环境进行主备切换或者采用多节点模式,是否还能正常、稳定运行?
- 现有的代码中,还有哪些类似这样的问题存在?
- 当生成环境突发问题时,这些可能都成为干扰因素,误导排查方向,从而影响排查进度
- 特别像定时任务的问题,一般都是周期性的,复现很难。
建议
- 对线程池、定时任务、分布式锁、事务、通用工具类等方面的设计、开发,加强代码走查。
- 对上面的知识点,进行”扫盲“培训、学习。
- 梳理出系统的一些特殊点,形成检查单,维护起来,方便后续生产环境的主备切换、多节点部署。建议真正的定期演练一下主备切换、多节点部署模式,免得真的生产环境有问题,而准备不充分。
- 特殊实现点一定要与系统测试充分沟通,考虑极端场景,以及应急方案。
- 对原有的老代码,进行逐行排查一下,找出潜在的隐患。
最后以一句话共勉一下:对每一行代码要怀着敬畏之心,之前所踩过的坑,最后都将会成为你最核心的竞争力。