业务场景:
业务中有一个调整奖金金额的功能,由于和钱相关,所以业务方希望能对这个功能的所有与金额相关操作都记录下来
实现思路
实现比较简单,直接校验数据后就更新数据库
public void bonusPriceAdjustment(BonusPriceAdjustmentParam bonusPriceAdjustmentParam) {
// 校验数据
// ……
// 操作数据库
// ……
}
为了不耦合业务函数,我们采用了切片的方式进行业务日志记录,具体实现如下:
1、在方法执行前,获取数据库原有数据(奖金),并存放到ThreadLocal
中
2、当方法执行完成后,获取当前方法的入参需要修改的奖金(奖金),再ThreadLocal
中获取原有奖金,将这两个数据合并成一条变更记录数据
3、将变更数据存入数据表中
@Aspect
@Component
public class BonusAdjustmentLogAspect {
/**
* 线程局部变量,用于存储业务日志对象
*/
private static final ThreadLocal<BonusPriceAdjustmentLog> BUSINESS_LOG_THREAD_LOCAL = new ThreadLocal<>();
@Pointcut("execution(* cn.bonusPriceAdjustment(..))")
public void bonusPriceAdjustment() {
}
@Before("bonusPriceAdjustment()")
public void before(JoinPoint joinPoint) {
// 查询原数据
BonusPriceAdjustmentLog originDate =
staffMonthlyBonusPerformanceMapper.selectLogInfoByPrimaryKey(parameter.getId());
BUSINESS_LOG_THREAD_LOCAL.set(originDate);
}
@AfterReturning(value = "bonusPriceAdjustment()", returning = "result")
@Async
public void afterReturning(JoinPoint joinPoint, Object result) {
BonusPriceAdjustmentParam newDate = getParameter(joinPoint);
BonusPriceAdjustmentLog originDate = BUSINESS_LOG_THREAD_LOCAL.get();
// 合并数据
BusinessLogBonusAdjustment businessLogBonusAdjustment = BusinessLogBonusAdjustment.bonusPriceAdjustmentLogConvertAsThis(originDate);
// 入库
businessLogBonusAdjustmentMapper.insert(businessLogBonusAdjustment);
BUSINESS_LOG_THREAD_LOCAL.remove();
}
@AfterThrowing(value = "bonusPriceAdjustment()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
BUSINESS_LOG_THREAD_LOCAL.remove();
}
}
问题
为了减少操作数据库,阻塞请求时间,我们选择了异步处理,在afterReturning()
方法上面添加了 @Async注解
这里如果使用线程池,也会有同样的问题。
当接口被调用的时候,添加了**@Async注解的方法中调用BUSINESS_LOG_THREAD_LOCAL.get()**会返回null,获取不到请求前存放的原始数据,从而导致记录变动记录处理失败
原因
这是由于@Async
注解的方法会在另一个线程中执行,而ThreadLocal是线程局部变量,不同的线程之间互相独立,所以在异步方法中无法获取到之前存储在ThreadLocal中的数据。
解决思路
方法1:去掉@Async
直接去掉@Async
注解,改为同一个线程处理
@AfterReturning(value = "bonusPriceAdjustment()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
}
方法2:使用InheritableThreadLocal
为了解决这个问题,我们还可以使用InheritableThreadLocal
代替ThreadLocal
来存储业务日志对象。InheritableThreadLocal
是ThreadLocal
的一个扩展,它能够让子线程继承父线程的ThreadLocal
变量,这样就能在异步方法中获取到之前存储在父线程中的数据。
具体做法是将代码中的ThreadLocal<BonusPriceAdjustmentLog>
替换为InheritableThreadLocal<BonusPriceAdjustmentLog>
,然后在业务逻辑开始前使用set方法存储数据,在异步方法中使用get方法获取数据即可。例如:
private static final InheritableThreadLocal<BonusPriceAdjustmentLog> BUSINESS_LOG_THREAD_LOCAL = new InheritableThreadLocal<>();
@Before("bonusPriceAdjustment()")
public void before(JoinPoint joinPoint) {
// 查询原数据
BonusPriceAdjustmentLog originDate =
staffMonthlyBonusPerformanceMapper.selectLogInfoByPrimaryKey(parameter.getId());
BUSINESS_LOG_THREAD_LOCAL.set(originDate);
}
@AfterReturning(value = "bonusPriceAdjustment()", returning = "result")
@Async
public void afterReturning(JoinPoint joinPoint, Object result) {
BonusPriceAdjustmentParam newDate = getParameter(joinPoint);
if (newDate == null) {
LoggerUtils.errorElk(this.getClass(), "日志记录异常:接口入参为空");
}
BonusPriceAdjustmentLog originDate = BUSINESS_LOG_THREAD_LOCAL.get();
// 合并数据
BusinessLogBonusAdjustment businessLogBonusAdjustment = BusinessLogBonusAdjustment.bonusPriceAdjustmentLogConvertAsThis(originDate);
// 记录日志
businessLogBonusAdjustmentMapper.insert(businessLogBonusAdjustment);
BUSINESS_LOG_THREAD_LOCAL.remove();
}
InheritableThreadLocal源码
JDK 17 LTS版本
ThreadLocal
和 InheritableThreadLocal
都提供了一种在多线程环境下存储线程本地变量的方式,它们之间的主要区别在于线程的继承。
在使用 ThreadLocal
时,每个线程都会拥有自己的变量副本,这个变量只能由这个线程访问,其他线程无法访问。而使用 InheritableThreadLocal
时,子线程可以从父线程继承变量的值,也就是说,当一个线程创建子线程时,子线程将会拥有父线程的所有 InheritableThreadLocal
变量的副本。
ThreadLocal部分源码:
public class ThreadLocal<T> {
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
InheritableThreadLocal部分源码:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
Thread部分源码:
public class Thread implements Runnable {
// ThreadLocal 实例的底层存储
ThreadLocal.ThreadLocalMap threadLocals = null;
// InheritableThreadLocals 实例的底层存储
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
此外我们在getMap和createMap这两个方法中也发现InheritableThreadLocal的底层其实是用 inheritableThreadLocals
来存储的,而ThreadLocal用的是 threadLocals
变量存储。
知道了这些,我们再来看下创建线程时涉及到的 inheritableThreadLocals 复制相关的关键代码如下:
public class Thread implements Runnable {
@SuppressWarnings("removal")
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// 省略其他代码……
Thread parent = currentThread();
// 省略安全校验……
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 将当前线程的 inheritableThreadLocals 复制给新创建线程的 inheritableThreadLocals
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
}
从源码分析 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals
会在创建Thread的时候将父类的inheritableThreadLocals
复制到当前的Thread中,从而实现跨线程访问。
思考
- 线程安全
ThreadLoca
和InheritableThreadLocal
都是线程安全的,每个线程都拥有自己的变量副本,线程之间互相不干扰,不需要使用同步机制。但是,需要注意在使用这些变量时,要避免并发修改,否则可能会出现数据不一致的问题。 - 内存泄漏:由于
ThreadLoca
和InheritableThreadLocal
的变量只与线程有关,因此在多线程环境下,如果不及时清理变量,可能会导致内存泄漏。为了避免这种情况发生,应该在不需要使用变量时,及时将变量设置为null
。 - 线程继承:在使用
InheritableThreadLocal
时,子线程会继承父线程的变量的值,这可能会导致子线程使用的变量值不是自己设置的值,而是父线程设置的值,这可能会引起一些意想不到的问题。因此,在使用InheritableThreadLocal
时,需要注意变量的作用域和生命周期。 - 性能:由于
ThreadLocal
和InheritableThreadLocal
都需要在每个线程中存储变量副本,因此,在多线程环境下,如果频繁地创建和销毁线程,会对性能造成一定的影响。为了避免这种情况发生,应该尽量重用线程,或者使用线程池来管理线程。
总之,在使用ThreadLocal
和InheritableThreadLocal
时,需要注意线程安全、内存泄漏、线程继承和性能等问题。正确地使用这些变量可以提高代码的可维护性和可读性,避免一些潜在的问题。
评论区