问题简述
最近遇到一个问题,在开启多线程执行任务时,子线程中访问需要的鉴权的方法时会丢失request请求信息,导致方法没有正确执行,于是写下这篇博客记录一下排查错误并解决问题的过程。
问题复现
当我执行这段代码,希望使用多线程的方式向数据库中插入数据时,发现当方法执行结束后,数据库并没有正确添加数据,并且控制台日志信息打印出 “非http请求” 的信息。
带着疑问,开始debug,发现添加数据时需要先拿到用户数据。
继续往里跟,发现拿取用户信息前有个获取token的操作。
继续,发现这个方法中有一个获取请求的操作。
点进去看,原来是子线程的请求属性servletRequestAttributes为null导致数据无法添加,并且打印的日志信息与前面的日志信息也完全符合。
到此,确定了问题出现的原因,是因为在使用多线程执行任务时,开启的子线程并没有存储主线程请求信息servletRequestAttributes的上下文,导致它为空进而获取用户信息失败,最后无法添加数据。
那就是说只要在任务开启之前,对每个子线程设置一个与主线程相同的servletRequestAttributes即可,说干就干。
问题解决
要拿到当前线程的servletRequestAttributes,想到可以使用RequestContextHolder这个类,这个类提供了获取和设置servletRequestAttributes的方法
// 获取当前线程的请求信息
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
对之前的代码进行改造,对每个子线程设置请求信息,如下所示:
果然,经过改造后重新执行这个方法,成功使用多线程向数据库添加数据。
问题原因
但是,还有一个问题,就是为什么使用多线程时子线程不会存储主线程的servletRequestAttribute呢?
于是我点开RequestContextHolder源码:
原来请求信息是通过ThreadLocal存储的,当请求到达Spring容器后,Spring会把该请求Request实例通过setRequestAttributes方法,把Request实例放入该请求线程内ThreadLocalMap中,然后就可以通过静态方法取到,但ThreadLocal不能让子线程继承ThreadLocalMap信息,于是子线程中的请求信息就为null。
SpringBoot 默认使用ThreadLocal把Request设置进请求线程中,这样如果在请求方法里面另起一个子线程然后再通过getRequestAttributes方法获取,是获取不到的。
至此,我们就完全搞明白了这个问题出现的原因,收获良多。