广东外贸网站建设企业,网站注册账号,网站制作网站建设,自己创建的网站结论
SecurityUtils.getSubject().getPrincipal()实际用的也是ThreadLocal#xff0c;而ThreadLocal和线程绑定#xff0c;异步会导致存数据丢失#xff0c;注意#xff01;
业务背景
最近#xff0c;系统偶尔会出现excel导入成功#xff0c;但系统却提示存在进行中的…结论
SecurityUtils.getSubject().getPrincipal()实际用的也是ThreadLocal而ThreadLocal和线程绑定异步会导致存数据丢失注意
业务背景
最近系统偶尔会出现excel导入成功但系统却提示存在进行中的导入的问题并且等待了较长的时间还是不行。这边跟进排查一下。
实现现状
导入的时候会对参数进行校验其中就有数据重复校验。 看了一下前辈是用redis的set来进行的去重校验操作。用LoginUser获取用户id固定前置用户id拼接作为redis的key需要去重的字段拼接成map作为值。主业务方法结束的时候用传入的LoginUser的用户id去拼接key来删除key因为校验方法是框架带的所以无法传参将LoginUser传进去。 判断是否存在正在进行的导入是直接拼接key来查rediskey存在则说明有正在进行中的导入。 private boolean hashCacheExcelVerifyDataGroupVerify(SdSchoolQuestionBankTestPaperVerify verify, String prefix) {LoginUser loginUser (LoginUser) SecurityUtils.getSubject().getPrincipal();boolean groupVerifySuccess true;if (verify null) {return groupVerifySuccess;}try {//校验成功成功返回true、失败返回falseString userId loginUser.getId();String key prefix userId;MapString, Object map new HashMap();map.put(xxxNo, verify.getTestPaperName().trim() verify.getXxxNo().trim());//有效期设置为30分钟long expireTime 30 * 60;SetOperations setOperations redisTemplate.opsForSet();setOperations.getOperations().expire(key, expireTime, TimeUnit.SECONDS);Set members setOperations.members(key);if (!members.contains(map)) {//如果不存在将数据缓存到redis中并设置有效期为30分钟当执行完导入操作之后删除缓存setOperations.add(key, map);} else {groupVerifySuccess false;}} catch (Exception e) {log.error(去重校验失败, e);}return groupVerifySuccess;}上面的方案总体来说是可以实现的只不过具体实现的时候出现了一些小毛病。 一眼看过去先设置过期时间再设置值这不是虚空设过期时间如果导入一条数据直接永不过期。不过一般导入的数量也不是一条按理说过半个小时应该能用才对。不过还是改了再说把设置过期时间的代码放到setOperations.add(key, map)下面。 接着查发现执行完业务代码之后只有成功才会删除key原来问题出在这。于是给异常后面加上一个finally在finally里面删除key这下无论你啥时候过期删除了就屁事没有。 测试环境验证一下。非常好没问题提测问题解决简简单单。
一波又起
到第二天业务反馈又出现一直处于导入中的情况。并且该账号半个小时以内并没有用过该excel导入。
奇怪了仔细又查了一遍代码不应该呀在finally里面有删除。无论如何肯定会执行删除操作呀。然后去看被限制人的导入半个小时内并没有导入。 上redis一看还真有缓存卧槽见鬼了。再去看半个小时内是否有导入还真有再细看过期时间和导入时间居然还对的上。 emm有情况。突然想到LoginUser的获取和ThreadLocal有点像会不会是异步导致的问题。于是本地启动去测用工具跑了很多次只用了一个账号。异步前后获取到的用户都是同一个于是认为LoginUser比ThreadLocal牛逼在异步的时候还会去提前更改盒子里的内容。其实是因为只有一个用户用来用去都只有一个用户各个线程里面存的都是该用户当然获取到的是一致的
灵光一闪
到第二天突然想到A导入却导致B被限制。那应该要多用户去测才对。于是更改测试方式结果获取到的结果显示异步前后获取到的LoginUser不一致。卧槽难崩原来是这个原因原来我猜想是对的。 因为新实现的导入都是直接用一个key作为判断是否存在正常导入的依据并且异步之后删除key用的也是通过传参传入的LoginUser而校验用的是ThreaLocal自然不会出现这种问题。
解决方案
那看来得改校验重复的实现方案了用我最喜欢的ThreadLocal。创建一个ThreadLocal静态变量这样里面存的数据都和线程相关异步之后一直是同一个线程自然不会出现错误的情况最后在finally里面清空ThreadLocal完美。 后续果然没有再出现过错误限制导入的情况。
原因分析
因为异步用到了线程池。请求进来的时候会校验用户信息并将用户信息存入LoginUser中以供全局使用。在线程执行结束之后线程回到线程池但线程对应的ThreadLocal的数据却没有清空。一般情况下如果都是走Controller进来的请求都不会有问题因为每次请求进来都会将新的用户信息set进去。但当异步的时候就没有set用户信息的过程线程内的用户信息还是上一次进入Controller用该线程的用户的用户信息。也就存在数据对不上的情况。而在异步中直接使用SecurityUtils.getSubject().getPrincipal()也就是犯了上面的错。
总的来说就是SecurityUtils.getSubject().getPrincipal()实际用的也是ThreadLocal而ThreadLocal和线程绑定异步会导致数据丢失注意