糟糕,被SimpleDateFormat坑到啦!
1. 问题背景
问题的背景是这样的,在最近需求开发中遇到需要将给定目标数据通过某一固定的计量规则进行过滤并打标生成明细数据,其中发现存在一笔目标数据的时间在不符合现有日期规则的条件下,还是通过了规则引擎的匹配打标操作。故而需要对该错误匹配场景进行排查,定位其根本原因所在。

2. 排查思路
2.1 数据定位
在开始排查问题之初,先假定现有的Aviator规则引擎能够对现有的数据进行正常的匹配打标,查询在存在问题数据(图中红框所示)同一时刻进行规则匹配时的数据都有哪些。发现存在五笔数据在同一时刻进行规则匹配落库。

继续查询具体的匹配规则表达式,发现针对loanPayTime时间区间在[2022-07-16 00:00:00, 2023-05-11 23:59:59]的范围内进行匹配,目标数据的时间为2023-09-19 11:27:29,理论上应该不会被匹配到。

但是观测匹配打标的明细数据发现确实打标成功了(如红框所示)。

所以重新回到最初的和目标数据同时落库的五笔数据发现,这五笔数据的loanPayTime时间确实在规则[2022-07-16 00:00:00, 2023-05-11 23:59:59]之内,所以在想有没有可能是在目标数据匹配规则引擎前,其它的五笔数据中的其中一笔对该数据进行了修改导致误匹配到了这个规则。顺着这个思路,首先需要确认下Aviator规则引擎在并发场景下是否线程安全的。

2.2 规则引擎
由于在需求中使用到用于给数据匹配打标的是Aviator规则引擎,所以第一直觉是怀疑Aviator规则引擎在并发的场景中可能会存在线程不安全的情况。

首先简单介绍下Aviator规则引擎是什么,Aviator是一个高性能的、轻量级的java语言实现的表达式求值引擎,主要用于各种表达式的动态求值,相较于其它的开源可用的规则引擎而言,Aviator的设计目标是轻量级和高性能 ,相比于Groovy、JRuby的笨重,Aviator非常小,加上依赖包也才450K,不算依赖包的话只有70K;
当然,Aviator的语法是受限的,它不是一门完整的语言,而只是语言的一小部分集合。其次,Aviator的实现思路与其他轻量级的求值器很不相同,其他求值器一般都是通过解释的方式运行,而Aviator则是直接将表达式编译成Java字节码,交给JVM去执行。简单来说,Aviator的定位是介于Groovy这样的重量级脚本语言和IKExpression这样的轻量级表达式引擎之间。(具体Aviator的相关介绍不是本文的重点,具体可参见)
通过查阅相关资料发现,Aviator中的AviatorEvaluator.execute() 方法本身是线程安全的,也就是说只要表达式执行逻辑和传入的env是线程安全的,理论上是不会出现并发场景下线程不安全问题的。(详见)
2.3 匹配规则引擎的env

通过前面Aviator的相关资料发现传入的env如果在多线程场景下不安全也会导致最终的结果是错误的,故而定位使用的env发现使用的是HashMap,该集合类确实是线程不安全的(具体可详见),但是线程不安全的前提是多个线程同时对其进行修改,定位代码发现在每次调用方式时都会重新生成一个HashMap,故而应该不会是由于这个线程不安全类导致的。

继续定位发现,loanPayTime这个字段在进行Aviator规则引擎匹配前使用SimpleDateFormat进行了格式化,所以有可能是由于该类的线程不安全导致的数据错乱问题,但是这个类应该只是对日期进行格式化处理,难不成还能影响最终的数据。带着这个疑问查询资料发现,emm确实是线程不安全的。

好家伙,嫌疑对象目前已经有了,现在就是寻找相关证据来佐证了。
3. SimpleDateFormat 还能线程不安全?
3.1 先写个demo试试
话不多说,直接去测试一下在并发场景下,SimpleDateFormat类会不会对需要格式化的日期进行错乱格式化。先模拟一个场景,对多线程并发场景下格式化日期,即在[0,9]的数据范围内,在偶数情况下对2024年1月23日进行格式化,在奇数情况下对2024年1月22日进行格式化,然后观测日志打印效果。
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadSafeDateFormatDemo {
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
LocalDateTime startDateTime = LocalDateTime.now();
Date date = new Date();
for (int i =