Spring常用的事务配置包括两种方式:xml文件配置和注解配置,两者都能实现对事务进行配置。前段时间维护公司的老项目时遇到了一个大坑,在项目中混用了这两种配置方式,导致将一个需要更新数据库记录的方法配置成在只读事务中运行,结果状态丢失,引发严重的线上问题,偶顶着巨大压力前前后后花费了快一周将问题定位清楚才松口气。
事情的经过是这样的,公司里前任开发在实现一个自动退款功能时,在退款服务中新增了一个方法,该方法用@Transactional注解了,即配置该方法在事务中运行,单纯看此方法代码看不出不合适的地方。那段时间,业务反馈线上系统每天存在几百个订单退款状态不对的问题。连续跟踪了线上3天日志,在执行此方法时间段里未见数据库报异常,实际登陆了服务器也看了数据库的记录,确实发现状态没更新,一时心里犯嘀咕了。开始怀疑数据库异常,去运维那边要了mysql的日志,看了对应时间段日志没有看到异常,觉得邪门了,真心想不通哪里有问题。日志这条路没招了,就另想了一条路,在代码中多加些日志,在本地构造环境复现问题。本地环境搭好了后,进行测试,结果很快复现了该问题,发现加的日志都有,也没有报异常,但就是数据库状态没有更新,百思不得其解。看了@Transactional注解的API文档,是配置方法运行在事务环境,并且配置的事务默认都是非只读事务,意味着事务会去更新数据库记录,但实际运行确不是这么回事。按照先前的经验,怀疑此处事务是按照只读方式运行,但是以前遇到的问题如果配置成只读,hibernate执行更新操作时会抛出只读事务不能更新的例外,准备再看看项目的spring配置,发现在xml配置文件一句简单的配置将该方法配置成只读事务。偶更改了该方法的名字,确保xml中只读配置不会对该方法生效,测试下发现数据库状态更新了,问题不复现了,原来问题是配成只读事务了。不过还是有两点疑惑,第一,spring动态处理事务配置时,遇到xml和注解两种方式时具体以那个为准,按照本问题看应该是xml的生效了,xml应该在注解之后处理,有兴趣的朋友可以写个测试用例验证下,不过个人觉得像这种混合配置同一方法的事务属性应尽量少用,混乱容易造成问题;第二,为啥配置成只读事务,hibernate更新时没有报异常,跟踪了代码,发现在DAO层用HibernateTemplate类的update方法更新数据库时,在更新之前会执行checkWriteOperationAllowed方法,该方法会检查事务是否具有写权限,没有时会抛出InvalidDataAccessApiUsageException异常,打印如下信息:Write operations are not allowed in read-only mode (FlushMode.MANUAL): Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition。项目中的方法用的DAO层更新方法未用HibernateTemplate类的update方法,而是直接调用HibernateSession的update方法,该方法不会执行写操作检查,也就不会抛异常。
经过这个问题的分析,心里有些感触,线上问题分析成本高,测试环节真心很重要,在平时写代码中还是得严肃认真点,实现的基本功能需要测试OK才能发到生产环境运行,避免流落到线上。
领取专属 10元无门槛券
私享最新 技术干货