ThreadLocal引起的一次线上事故

> 线上用户存储数据后查看提示无权限

前言

不知道什么时候年轻的我曾一度认为Java没啥难度,没有我实现不了的需求,没有我解不了的bug

直到我遇到至今难忘的一个bug 。 线上用户存储数据后查看提示无权限

初次定位

明明自己添加的数据,为什么提示自己没有权限呢?我一开始自信的认为是我们的客户操作有问题、或者是我们权限配置有问题

但是带我自己亲自验证了一下之后发现这个问题时现时不现,属于一个偶发的问题。这个在开发阶段还真的不容易发现。

问题升级

经过自己的测试后让我更加怀疑人生了,你要么就有问题要么就没问题。一会有一会没有到底又是几个意思呢?偶先的问题真的很难解决啊。问题定位到这里我已经精疲力竭了。然后就放弃了定位

但是问题还是得解决,第二天我又硬着头皮开始研究了。可能第二天头脑比较清醒我发现我们系统中在插入数据的时候会自动获取到当前登录用户并在数据库中记录次数据的创建者及最新的修改者。这更应该说明我们的问题离谱 。但是问题在我们获取当前登录用户的时候出现了问题

对,我将问题追踪了一下,终于将问题本质找到了。我们获取当前登录用户是通过ThreadLocal 来实现的。那么问题就是``ThreadLocal` 获取用户有问题

我们分布式开发系统。我们会在每个模块里添加一个aop拦截器,通过请求头的token再去user模块查询用户基本信息。然后放到``ThreadLocal中。这样我们的系统中随处都可以通过ThreadLocal` 这个对象获取我们的登陆用户。

别问我为什么要在每个模块都这样做?别问我为什么用ThreadLocal?别问我为什么是分布式还要这样做? 因为今天我们重点是解决bug

image-20210508165554760

开门见山

问题就出现在getUser那块逻辑里。因为我们的设计就是在系统中随处都可以获取到User对象。当然我们这里指的是任何请求里。对于MQ、定时器这些模块里肯定是没有User的。因为这些没法走AOP拦截

ThreadLocal获取用户信息乱串,导致用户新增数据权限异常

最终定位

我们的ThreadLocal 是个对象,我们系统中是通过一个工具类获取这个对象的属性的。在这个对象我们提供set、get方法。

image-20210508171309957

上面的流程展示了在获取到User用户之后就会加入到工厂。如果工厂已经存在了就不会加入。否则就会加入我们的用户

这样也是避免我们不断加入重复用户信息。因为同一个线程对应的只可能是一个用户。

思考 public static UserInfo getUser() { return userThreadLocal.get(); }

上面是我们工具类的get方法。这就是将ThreadLocal对象存储的内容返回出去。这一步应该不会出现问题。

在getUser中很明显没有问题,我们利用排除法只剩下了setUser了。虽然排除了别人的嫌疑但是setUser我还是看不出有什么问题。经过一阵debug断点跟踪后我发现我们setUser逻辑的确有问题

setUser是将用户信息保存到``Threadlocal 对象中,但是前提是ThreadLocal`中没有用户。对就是这个问题,如果已经有了用户呢?那么我们真正的用户就会无法添加进去

到了这里问题逐渐的明朗起来。使我们ThreadLocal对象管理的有问题。导致保存了上次的用户信息从而导致用户信息乱串的现象

解决问题

既然我们已经定位到ThreadLocal的管理问题,那么我们就好办了。

ThreadLocal简单梳理

image-20210509151703058

ThreadLocal 将对象保存在线程中。换句话说就是每个线程的数据会相互隔离。基于这个特性我们可以将用户信息存储在这里,这样我们能保证我们的当前线程下执行分各种方法都能通过他获取到用户信息

ThreadLocal内部是将已自己为key, 存储对象为value存储到当前线程中的map中。这个map会随着线程的销毁而被JVM回收。

但是在我们实际开发中经常会使用线程池来避免线程的重复创建及销毁。那么线程往往是不会被销毁的

在Spring中集成的类似Tomcat、JBoss等web容器中都是默认使用的一定数量的线程数的。而我们在spring中使用的线程复用功能就导致了我们在获取当前线程的用户时因为此线程被别人使用过从未导致用户信息没有被更新成功。从而引发我们上面提到的奇怪的问题

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyfzwj.html