此文主要讲解:

  1. 如何实现操作记录
  2. 如何将TransmittableThreadLocal和@Async搭配使用

TransmittableThreadLocal阿里的一个开源组件,为了在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题

1. 背景

有一个实验管理平台,用于配置和查看实验,想加一个操作历史功能,便于追踪改动,回滚历史等

实现后是这个样子:

2. 分析

这个功能简单来讲,就是做一个埋点,记录某人(operator),什么时间(time),做了什么事--也就是操作(operate_type)和改的什么东西(data_id,old_value, new_value)

CREATE TABLE `record` (
`id` bigint(20) NOT NULL,
`data_id` bigint(20) DEFAULT NULL,
`data_type` varchar(20) DEFAULT NULL,
`operator` varchar(200) NOT NULL,
`operate_type` int(11) NOT NULL,
`time` timestamp(4) NOT NULL DEFAULT CURRENT_TIMESTAMP(4),
`old_value` text,
`new_value` text,
`desc` varchar(1000) DEFAULT NULL,
`parent_id` bigint(20) DEFAULT NULL,
`namespace_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_parent_id` (`parent_id`),
KEY `idx_namespace_id` (`namespace_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作记录表';

其中还有一些额外的字段,用来实现其他功能

  • parent_id实现父子记录,父子记录:例如修改实验是一条记录,里面具体改的是实验状态
  • namespace_id实现命名空间,因为实验平台有多个接入方,所以要区分

3. 埋点

建一个RecordService,一个方法表示一个埋点时机

相比这种侵入式的埋点,另外一种埋点方式是通过AOP的方式将Service包裹,无侵入性,但是不够灵活

public interface RecordService{

	void recordCreateExperiment( String operator, ExperimentVO experiment);

	void recordUpdateExperiment( String operator, ExperimentVO oldExperiment, ExperimentVO newExperiment);

	void recordDeleteExperiment( String operator, ExperimentVO experiment);
}

而且为了不影响主流程,埋点操作使用了异步@Async

@Async
@Slf4j
@Service
public class RecordServiceImpl implements RecordService{
...
}

4. TransmittableThreadLocal

现在看起来已经可以实现功能了,但为什么要引入TransmittableThreadLocal(下简称TTL)呢?

因为operator是Session级别的,需要一直从Controller记录传到Service

(当然可以直接在Service直接从Request里面取,但这样会导致Service对Web层的依赖,后期如果想把Serivce通过其他接口暴露出去,例如OpenApi的方式,就会很麻烦),这样传递参数一个是比较繁琐,另外会对主业务流程理解产生干扰

要解决这个问题就需要使用ThreadLocal, 简单讲就是利用了一个全局Map,以线程为Key去存取值

但这不能使用ThreadLocal,因为我们使用了@Async注解,通过线程池来执行的,也就是说不是一个线程,所以有了TTL,专门用于线程池的ThreadLocal

工作原理如下:对Runable进行包裹,使用ThreadLocal传递

4.1 使用TTL

  1. 引入pom
       <dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.0</version>
</dependency>
  1. 建一个工具类
public class TTLUtil {
private static final TransmittableThreadLocal<ServiceContext> TTL_CONTEXT = new TransmittableThreadLocal(); public TTLUtil() {
} public static ServiceContext get() {
return (ServiceContext)TTL_CONTEXT.get();
} public static void set(ServiceContext serviceContext) {
TTL_CONTEXT.set(serviceContext);
} public static void remove() {
TTL_CONTEXT.remove();
}
}
  1. 使用过滤器将需要传递的参数收集到TTL中, 主要此过滤器要在Appication加@ServletComponentScan 注解才生效,finally中使用完之后将TTL释放
@Order( 999 )
@WebFilter( filterName = "nameSpaceFilter", urlPatterns = "/*" )
public class NameSpaceFilter implements Filter{ private static final String NAMESPACE_HEADER = "exp-namespace"; @Override
public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain )
throws IOException, ServletException{ try{
HttpServletRequest request = ( HttpServletRequest )servletRequest;
String namespace = request.getHeader( NAMESPACE_HEADER );
String username = SSOClient.getLoginName( request ); ServiceContext serviceContext = new ServiceContext();
if( !StringUtils.isEmpty( namespace ) ){
serviceContext.setNamespace( Long.parseLong( namespace ) );
}
serviceContext.setUsername( username );
TTLUtil.set( serviceContext ); filterChain.doFilter( servletRequest, servletResponse );
}
finally{
TTLUtil.remove();
} } }
  1. 配置Spring的@Async默认线程池,注意其中的TtlExecutors.getTtlExecutor( executor )是使用TTL对线程池进行包裹
import com.alibaba.ttl.threadpool.TtlExecutors;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor; @Configuration
@Slf4j
public class AsyncConfiguration implements AsyncConfigurer{ private final ObjectMapper objectMapper = new ObjectMapper(); @Bean( "defaultAsyncExecutor" )
public Executor executor(){ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); int corePoolSize = 10;
int queueCapacity = 10;
int maxPoolSize = 50; executor.setCorePoolSize( corePoolSize );
executor.setMaxPoolSize( maxPoolSize );
executor.setQueueCapacity( queueCapacity );
executor.setRejectedExecutionHandler( new ThreadPoolExecutor.AbortPolicy() );
executor.setThreadNamePrefix( "defaultAsyncExecutor-" );
executor.setWaitForTasksToCompleteOnShutdown( true );
executor.initialize(); return TtlExecutors.getTtlExecutor( executor );
} @Override
public Executor getAsyncExecutor(){ return executor();
} @Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler(){ return ( ex, method, params ) -> {
List<String> paramsStr = new ArrayList<>();
for( Object param : params ){
try{
String s = objectMapper.writeValueAsString( param );
paramsStr.add( s );
}
catch( JsonProcessingException e ){
log.error( "执行异步任务解析参数错误", e );
}
}
log.error( "执行异步任务出错 {},params: {}", method, paramsStr, ex );
};
}
}
  1. 在RecordService中使用,这样就中异步线程中拿到了需要的参数
record.setNamespaceId( TTLUtil.get().getNamespace() );

4.2 TTL的三种使用方式

  1. 修饰Runnable和Callable
  2. 修饰线程池,线程池内部还是第一种方式,这也是我使用的方式
  3. 使用Java Agent来修饰JDK线程池实现类,无侵入式的

4.3 TTL的场景

下面是几个典型场景例子。

  1. 分布式跟踪系统 或 全链路压测(即链路打标)
  2. 日志收集记录系统上下文
  3. Session级Cache
  4. 应用容器或上层框架跨应用代码给下层SDK传递信息

我们这的场景应该算第三种和第四种

5. 埋点的异常处理

上面的埋点逻辑中,因为记录是异步处理的,万一没记录怎么解决?

上面定义了AsyncUncaughtExceptionHandler,会在处理失败的时候把日志打出来

不过更为稳妥的方式,可以在失败时将未格式化的数据写进数据库记录(比如写到mongodb),通过报警,以便后续处理

TransmittableThreadLocal和@Async优雅的记录操作日志的更多相关文章

  1. Appfuse:记录操作日志

    appfuse的数据维护操作都发生在***form页面,与之对应的是***FormController,在Controller中处理数据的操作是onSubmit方法,既然所有的操作都通过onSubmi ...

  2. MVC 记录操作日志与过滤特殊字符

    最近进行的MVC系统需要用到记录操作日志和过滤特殊字符的功能,如果每个action中都调用记录日志的方法就太麻烦了,所以根据需要结合mvc的过滤机制 写了个特殊字符验证与记录操作日志的公用类: pub ...

  3. Tomcat会话超时时怎样记录操作日志,满足安全审计要求

    众所周知.在实际的Web应用程序中,会话管理一般都採用Web容器会话管理功能. 使用Tomcat做Webserver也是如此,并且从安全的角度考虑,尽量避免去更改和干预Web容器的会话管理功能. To ...

  4. 使用SpringBoot AOP 记录操作日志、异常日志

    平时我们在做项目时经常需要对一些重要功能操作记录日志,方便以后跟踪是谁在操作此功能:我们在操作某些功能时也有可能会发生异常,但是每次发生异常要定位原因我们都要到服务器去查询日志才能找到,而且也不能对发 ...

  5. spring-boot-route(十七)使用aop记录操作日志

    在上一章内容中--使用logback管理日志,我们详细讲述了如何将日志生成文件进行存储.但是在实际开发中,使用文件存储日志用来快速查询问题并不是最方便的,一个优秀系统除了日志文件还需要将操作日志进行持 ...

  6. Spring aop 记录操作日志 Aspect

    前几天做系统日志记录的功能,一个操作调一次记录方法,每次还得去收集参数等等,太尼玛烦了.在程序员的世界里,当你的一个功能重复出现多次,就应该想想肯定有更简单的实现方法.于是果断搜索各种资料,终于搞定了 ...

  7. springboot springmvc拦截器 拦截POST、PUT、DELETE请求参数和响应数据,并记录操作日志

    1.操作日志实体类 @Document(collection = "operation_log") @Getter @Setter @ToString public class O ...

  8. .Net捕获网站异常信息记录操作日志

    第一步:在Global.asax文件下的Application_Error()中写入操作日志 /// <summary> /// 整个网站出现异常信息,都会执行此方法 /// </s ...

  9. 自定义日志注解 + AOP实现记录操作日志

      需求:系统中经常需要记录员工的操作日志和用户的活动日志,简单的做法在每个需要的方法中进行日志保存操作, 但这样对业务代码入侵性太大,下面就结合AOP和自定义日志注解实现更方便的日志记录   首先看 ...

  10. SpringBoot-AOP记录操作日志

    package com.meeno.inner.oa.extend.operaterecord.aop; import com.alibaba.fastjson.JSONArray; import c ...

随机推荐

  1. Elasticsearch:Cluster备份 Snapshot及Restore API

    Elasticsearch提供了replica解决方案,它可以帮我们解决了如果有一个或多个node失败了,那么我们的数据还是可以保证完整的情况,并且搜索还可以继续进行.但是,有一种情况是我们的所有的n ...

  2. nginx进程所属用户问题讨论

    结论 1,在非root账户下启动时,nignx的master和worker进程的用户都将是这个账户, 2,在root账户下启动时 nignx的master进程是用户是root,worker的用户在co ...

  3. Leetcode链表

    Leetcode链表 一.闲聊 边学边刷的--慢慢写慢慢更 二.题目 1.移除链表元素 题干: 思路: 删除链表节点,就多了一个判断等值. 由于是单向链表,所以要删除节点时要找到目标节点的上一个节点, ...

  4. vue3中pinia的使用总结

    pinia的简介和优势: Pinia是Vue生态里Vuex的代替者,一个全新Vue的状态管理库.在Vue3成为正式版以后,尤雨溪强势推荐的项目就是Pinia.那先来看看Pinia比Vuex好的地方,也 ...

  5. Docker Desktop 可以直接启用Kubernetes 1.25 了

    作为目前事实上的容器编排系统标准,K8s 无疑是现代云原生应用的基石,很多同学入门可能直接就被卡到第一关,从哪去弄个 K8s 的环境, Docker Desktop 自带了Kubernetes 服务, ...

  6. springboot项目中使用shiro实现用户登录以及权限的验证

    欢迎大家加入我的社区:http://t.csdn.cn/Q52km 社区中不定时发红包 更加高级的验证用户权限:用户表.角色表.权限表.多表联合:https://blog.csdn.net/weixi ...

  7. 齐博X1-栏目的终极方法get_sort

    本节说明栏目的最终方法get_sort 我们之前讲的一系列fun函数调用栏目的方法都是基于get_sort这个公共方法而来 我们来看下这个函数的具体参数这个方法有四个参数: id:也就是栏目id,经常 ...

  8. 线上kafka消息堆积,consumer掉线,怎么办?

    线上kafka消息堆积,所有consumer全部掉线,到底怎么回事? 最近处理了一次线上故障,具体故障表现就是kafka某个topic消息堆积,这个topic的相关consumer全部掉线. 整体排查 ...

  9. JS 学习笔记(二)Ajax的简单使用

    使用Ajax访问本地TXT文件 ajax.js // 创建请求对象 var ajax = new XMLHttpRequest(); // 建立连接 ajax.open('get', 'test.tx ...

  10. linux 2021

    常用安装 sudo apt install openssh-server # 安装ssh service ssh # 查看ssh服务的 基础 Debian 是Ubuntu的母板,有强大的包管理功能,使 ...