一、背景介绍
ThreadLocal
是线程提供的本地变量,因其线程特殊属性,被经常用于存储与线程相关的信息,如保存登录用户信息、数据库连接配置等。但我们忽略了一个关键点:ThreadLocal
只能用在同步线程中,而在异步线程或线程池中则不起作用。因此,本博文主要探讨如何在异步线程和线程池中传递 ThreadLocal
上下文。
二、问题描述
发版之后,爆出线上问题:“用户信息获取失败”。为了更好地解释这个问题,先介绍下公司的系统架构:用户登录时,后台系统通过用户 key
获取个人信息并保存在 ThreadLocal
静态对象中,以供后续使用。简要实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
|
@Component public class HandlerAccessInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { httpServletResponse.setHeader("Access-Control-Allow-Origin", "*"); httpServletResponse.setHeader("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With"); httpServletResponse.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); if (httpServletRequest.getCookies() != null) { for (Cookie cookie : httpServletRequest.getCookies()) { if ("vkey".equals(cookie.getName())) { UserContextUtil.setUser(cookie.getValue()); } } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
} @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { UserContextUtil.remove(); } }
@Component public class UserContextUtil { private static ThreadLocal<SyUser> threadLocal = new ThreadLocal<>(); private static final String TOKEN_BEARER = "Bearer ";
public synchronized static SyUser getUser() { SyUser syUser = threadLocal.get(); return syUser; }
public synchronized static boolean setSyUser(SyUser syUser) {
if (syUser == null) { return false; }
threadLocal.set(syUser);
return true; }
public synchronized static boolean setUser(Cookie[] cookies) { if (cookies == null) { return false; } for (Cookie cookie : cookies) { String name = cookie.getName().toLowerCase(); if ("vkey".equals(name)) { String value = cookie.getValue(); if (StrUtil.isEmpty(value)) { return false; } LoginUser loginUser = new LoginUser(value); if (loginUser.getId() == null) { return false; } SyUser syUser = new SyUser(); syUser.setId(loginUser.getId().intValue()); syUser.setUserName(loginUser.getMobile()); syUser.setTrueName(loginUser.getName()); return setSyUser(syUser); } } return false; }
public synchronized static boolean setUser(String headerValue) { if (StrUtil.isEmpty(headerValue)) { return false; } if (headerValue.startsWith(TOKEN_BEARER)) { headerValue = headerValue.substring(TOKEN_BEARER.length()); } LoginUser loginUser = new LoginUser(headerValue); if (loginUser.getId() == null) { return false; } SyUser syUser = new SyUser(); syUser.setId(loginUser.getId().intValue()); syUser.setUserName(loginUser.getMobile()); syUser.setTrueName(loginUser.getName()); syUser.setPhone(loginUser.getMobile()); return setSyUser(syUser); }
public synchronized static String getUserName() { SyUser syUser = threadLocal.get(); if (syUser == null) { return null; } return syUser.getTrueName(); }
public synchronized static void remove() { threadLocal.remove(); } }
|
问题代码定位:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| @RestController @RequestMapping("/web/file") @Slf4j public class ManageFileController { @Resource private ManageFileService manageFileService;
@PostMapping("/loadFile") public RespDTO<String> loadFile(@Valid @RequestBody LoadFileReqDTO loadFileReqDTO) {
AsyncLoadFile asyncLoadFile = manageFileService.initLoadFileInfo(loadFileReqDTO);
manageFileService manageFileService.loadFile(loadFileReqDTO, inheritedSyUser);
return RespDTO.success(); } }
@Service @Slf4j public class ManageFileService { @Resource private ManageFileHelper manageFileHelper; @Resource private AsyncLoadFileMapper asyncLoadFileMapper;
@Async(TASK_EXECUTOR) public void loadFile(LoadFileReqDTO loadFileReqDTO, AsyncLoadFile asyncLoadFile, SyUser syUser) {
try {
log.info("异步加载文件开始; userName: {}", UserContextUtil.getUserName());
LoadFileService loadFileService = manageFileHelper.router(loadFileReqDTO.getLoadFileType());
LoadFileReqBO loadFileReqBO = new LoadFileReqBO(); BeanUtils.copyProperties(loadFileReqDTO, loadFileReqBO);
LoadFileRespBO loadFileRespBO = loadFileService.executeLoadFile(loadFileReqBO);
String fileKey = FileUtil.uploadFile(loadFileRespBO.getFilePathUrl(), FileTypeEnum.XLSX, STORE_FILE_KEY);
CuiShouAssert.notEmpty(fileKey, "调用影像件系统异常!");
asyncLoadFile.setFileName(loadFileRespBO.getFileName()); asyncLoadFile.setFileKey(fileKey); asyncLoadFile.setLoadStatus(LoadStatusEnum.SUCCESS.name()); } catch (Exception e) { log.info("加载文件异常; errMsg: {}", e.getMessage(), e); asyncLoadFile.setLoadStatus(LoadStatusEnum.FAILURE.name()); asyncLoadFile.setRemark(e.getMessage()); }
asyncLoadFileMapper.update(asyncLoadFile); log.info("异步加载文件结束; userName: {}", UserContextUtil.getUserName()); } }
|
1 2
| 运行结果... 2020-12-14 17:51:15.235 INFO [-,d11235d6f6eda8d0,8c023b1e45855f97,false] 19444 --- [common-async-executor-1] c.v.c.o.service.file.ManageFileService : 异步加载文件开始; userName: null
|
问题很好定位,@Async
异步线程池开启子线程处理任务后,父线程提供的本地线程变量就销毁了。
三、解决方法
- 将用户信息当作参数传入异步方法,再在异步方法中重新设置本地变量;
- 采用
InheritedThreadLocal
实现子线程上下文的传递。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
@PostMapping("/loadFile") public RespDTO<String> loadFile(@Valid @RequestBody LoadFileReqDTO loadFileReqDTO) {
AsyncLoadFile asyncLoadFile = manageFileService.initLoadFileInfo(loadFileReqDTO); SyUser syUser = UserContextUtil.getUser();
SyUser inheritedSyUser = new SyUser();
BeanUtils.copyProperties(syUser, inheritedSyUser);
manageFileService manageFileService.loadFile(loadFileReqDTO, inheritedSyUser);
return RespDTO.success(); }
@Async(TASK_EXECUTOR) public void loadFile(LoadFileReqDTO loadFileReqDTO, AsyncLoadFile asyncLoadFile, SyUser syUser) { UserContextUtil.setSyUser(syUser); log.info("异步加载文件开始; UserName: {}", UserContextUtil.getRealName()); UserContextUtil.remove(); log.info("异步加载文件结束; UserName: {}", UserContextUtil.getRealName()); }
|
1 2 3
| 方法一运行结果... 2020-12-14 18:03:34.947 INFO [-,480f9c55e785ec6f,c8de532979871193,false] 19444 --- [common-async-executor-2] c.v.c.o.service.file.ManageFileService : 异步加载文件开始; UserName: 邹荣升 2020-12-14 18:03:35.235 INFO [-,480f9c55e785ec6f,c8de532979871193,false] 19444 --- [common-async-executor-2] c.v.c.o.service.file.ManageFileService : 异步加载文件结束; UserName: null
|
方法一可以说是傻瓜操作,治标不治本,后来的同事稍不注意还会掉进坑里。我们来实现方法二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| private static ThreadLocal<SyUser> threadLocal = new InheritableThreadLocal<>();
@Async public void loadFile(LoadFileReqDTO loadFileReqDTO, AsyncLoadFile asyncLoadFile, SyUser syUser) { UserContextUtil.setSyUser(syUser); log.info("异步加载文件开始; UserName: {}", UserContextUtil.getRealName()); UserContextUtil.remove(); log.info("异步加载文件结束; UserName: {}", UserContextUtil.getRealName()); }
@Async(TASK_EXECUTOR) public void loadFile(LoadFileReqDTO loadFileReqDTO, AsyncLoadFile asyncLoadFile, SyUser syUser) { UserContextUtil.setSyUser(syUser); log.info("异步加载文件开始; UserName: {}", UserContextUtil.getRealName()); UserContextUtil.remove(); log.info("异步加载文件结束; UserName: {}", UserContextUtil.getRealName()); }
|
异步线程和异步线程池两种情况下的运行结果:
1 2 3 4
| 运行结果... 2020-12-14 18:36:41.560 INFO [-,8658c694de3e9b7a,ccc7a782204c91b0,false] 19876 --- [common-async-executor-1] c.v.c.o.service.file.ManageFileService : 异步加载文件开始; UserName: 邹荣升
2020-12-14 18:41:17.735 INFO [-,67b4725bcaacfc64,36ee2acec232be32,false] 21600 --- [common-async-executor-1] c.v.c.o.service.file.ManageFileService : 异步加载文件开始; UserName: 邹荣升
|
三、总结
虽然 ThreadLocal
的线程相关属性提供了很多便利性,但在高并发系统中,直接使用原生静态对象并不合适。因此,如使用 Redis
、MQ
一样,深入了解常用工具的特性,是系统设计的必备要素。