ThreadLocal引起的一次线上事故
> 线上用户存储数据后查看提示无权限
前言
不知道什么时候年轻的我曾一度认为Java没啥难度,没有我实现不了的需求,没有我解不了的bug
直到我遇到至今难忘的一个bug 。 线上用户存储数据后查看提示无权限
初次定位
- 明明自己添加的数据,为什么提示自己没有权限呢?我一开始自信的认为是我们的客户操作有问题、或者是我们权限配置有问题
- 但是带我自己亲自验证了一下之后发现这个问题时现时不现,属于一个偶发的问题。这个在开发阶段还真的不容易发现。
问题升级
经过自己的测试后让我更加怀疑人生了,你要么就有问题要么就没问题。一会有一会没有到底又是几个意思呢?偶先的问题真的很难解决啊。问题定位到这里我已经精疲力竭了。然后就放弃了定位
但是问题还是得解决,第二天我又硬着头皮开始研究了。可能第二天头脑比较清醒我发现我们系统中在插入数据的时候会自动获取到当前登录用户并在数据库中记录次数据的创建者及最新的修改者。这更应该说明我们的问题离谱 。但是问题在我们获取当前登录用户的时候出现了问题
对,我将问题追踪了一下,终于将问题本质找到了。我们获取当前登录用户是通过
ThreadLocal来实现的。那么问题就是``ThreadLocal` 获取用户有问题我们分布式开发系统。我们会在每个模块里添加一个aop拦截器,通过请求头的token再去user模块查询用户基本信息。然后放到``ThreadLocal
中。这样我们的系统中随处都可以通过ThreadLocal` 这个对象获取我们的登陆用户。别问我为什么要在每个模块都这样做?别问我为什么用
ThreadLocal?别问我为什么是分布式还要这样做? 因为今天我们重点是解决bug
开门见山
- 问题就出现在getUser那块逻辑里。因为我们的设计就是在系统中随处都可以获取到User对象。当然我们这里指的是任何请求里。对于MQ、定时器这些模块里肯定是没有User的。因为这些没法走AOP拦截
ThreadLocal获取用户信息乱串,导致用户新增数据权限异常
最终定位
- 我们的
ThreadLocal是个对象,我们系统中是通过一个工具类获取这个对象的属性的。在这个对象我们提供set、get方法。

- 上面的流程展示了在获取到User用户之后就会加入到工厂。如果工厂已经存在了就不会加入。否则就会加入我们的用户
- 这样也是避免我们不断加入重复用户信息。因为同一个线程对应的只可能是一个用户。
思考
public static UserInfo getUser() {
return userThreadLocal.get();
}
- 上面是我们工具类的get方法。这就是将
ThreadLocal对象存储的内容返回出去。这一步应该不会出现问题。 - 在getUser中很明显没有问题,我们利用排除法只剩下了setUser了。虽然排除了别人的嫌疑但是setUser我还是看不出有什么问题。经过一阵debug断点跟踪后我发现我们setUser逻辑的确有问题
- setUser是将用户信息保存到``Threadlocal
对象中,但是前提是ThreadLocal`中没有用户。对就是这个问题,如果已经有了用户呢?那么我们真正的用户就会无法添加进去 - 到了这里问题逐渐的明朗起来。使我们
ThreadLocal对象管理的有问题。导致保存了上次的用户信息从而导致用户信息乱串的现象
解决问题
- 既然我们已经定位到
ThreadLocal的管理问题,那么我们就好办了。
ThreadLocal简单梳理

ThreadLocal将对象保存在线程中。换句话说就是每个线程的数据会相互隔离。基于这个特性我们可以将用户信息存储在这里,这样我们能保证我们的当前线程下执行分各种方法都能通过他获取到用户信息ThreadLocal内部是将已自己为key, 存储对象为value存储到当前线程中的map中。这个map会随着线程的销毁而被JVM回收。- 但是在我们实际开发中经常会使用线程池来避免线程的重复创建及销毁。那么线程往往是不会被销毁的
- 在Spring中集成的类似Tomcat、JBoss等web容器中都是默认使用的一定数量的线程数的。而我们在spring中使用的线程复用功能就导致了我们在获取当前线程的用户时因为此线程被别人使用过从未导致用户信息没有被更新成功。从而引发我们上面提到的奇怪的问题
- 那么既然是没有被更新,到这里我们就很好解决了,要么每次使用完成后都将
ThreadLocal中的数据remove。因为他内部是弱引用在下次回收就会将对象回收这样也不会造成内存泄漏的问题 - 或者我们在我们的AOP中setUser之前先将用户
ThreadLocal清空。两种方式都可以完美解决我们的问题
具体代码实现
/**
* 请求生命周期最后一步销毁是做的回调事件
* 用于销毁在线用户信息,防止在线用户信息互相干扰(在多线程复用时)
*/
@WebListener
@Primary
public class SysServletRequestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent requestEvent) {
UserInfoUtil.clearUserInfo();
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
}
}
- 我们可以通过spring提供的监听器,监听一个请求的生命周期在这个请求完成之后将我们的
ThreadLocal进行remove。 为什么我推荐这种做法呢。因为请求结束就清空可以快速的让出内存让他去做更加有用的事情。 - 如果是第二种方法那么如果我们没有人在登录,或者说在下一次登录之前这块不需要的内存永远被占着
总结忠告
- 这次问题出现的很是奇怪,一度让我怀疑人生,但是永远相信程序是不会无缘无故的出问题的。
- 出问题的只能是我们的代码有问题,要善于解析问题,将问题细化,细化到我们代码层面而不是业务层面
- 使用一个技术时最好能先了解他内部的一个原理。或者最起码先了解他的大概逻辑
- 别看这篇文章寥寥几字就解决了我们的问题,但是实际上我在解决他的过程中吃了不少的苦。好几个夜晚都是我在陪他战斗
- 我在定位到时
ThreadLocal后就花了一个小时学习了下他的逻辑并跟踪了他的源码。最后结合我们的业务才发现了眉目 - 总之有问题是好事情,有了问题我们才能成长。至少在这次的问题中我学习到了
ThreadLocal。我的这次问题也是使用他的典型问题,另外还有一个内存泄漏的问题这是在学习他源码的过程领悟到的一点。关于内存泄漏我们有时间在看吧。问题解决。终于可以继续happy了。
ThreadLocal引起的一次线上事故的更多相关文章
- 由定时脚本错误以及Elasticsearch配置错误引发的Flink线上事故
近期接手离职同事项目,突然遇到线上事故,Flink无法正常聚合数据生成指标. 以下是详细的排查过程: 问题复现 清晨,运维报告Flink数据分析模块无法正常生成指标数据. 赶紧登陆Flink所在机器, ...
- RabbitMQ 线上事故!慌的一批,脑袋一片空白。。。
前言 那天我和同事一起吃完晚饭回公司加班,然后就群里就有人@我说xxx商户说收不到推送,一开始觉得没啥.我第一反应是不是极光没注册上,就让客服通知商户,重新登录下试试.这边打开极光推送的后台进行检查. ...
- 记一次线上事故的JVM内存学习
今天线上的hadoop集群崩溃了,现象是namenode一直在GC,长时间无法正常服务.最后运维大神各种倒腾内存,GC稳定后,服务正常.虽说全程在打酱油,但是也跟着学习不少的东西. 第一个问题:为什么 ...
- 记一次真实的线上事故:一个update引发的惨案!
目录 前言 项目背景介绍 要命的update 结语 前言 从事互联网开发这几年,参与了许多项目的架构分析,数据库设计,改过的bug不计其数,写过的sql数以万计,从未出现重大纰漏,但常在河边走,哪 ...
- 一次线上事故,让我对MySql的时间戳存char(10)还是int(10)有了全新的认识
美好的周五 周五的早晨,一切都是那么美好. 然鹅,10点多的时候,运营小哥哥突然告诉我后台打不开了,我怀着一颗"有什么大不了的,估计又是(S)(B)不会连wifi"的心情,自信的打 ...
- 线上bug分析
昨天下午大神把组内几十号人召集在一起开Online bug分析大会,主要是针对近期线上事故从事故原因和解决方案两个维度来分析. 对金融软件来说,每一次的线上事故都有可能给公司带来重大的损失,少扣了用户 ...
- 记录一次因subprocess PIPE 引起的线上故障
sence:python中使用subprocess.Popen(cmd, stdout=sys.STDOUT, stderr=sys.STDERR, shell=True) ,stdout, stde ...
- 一个purge参数引发的惨案——从线上hbase数据被删事故说起
在写这篇blog前,我的心情久久不能平静,虽然明白运维工作如履薄冰,但没有料到这么一个细小的疏漏会带来如此严重的灾难.这是一起其他公司误用puppet参数引发的事故,而且这个参数我也曾被“坑过”. ...
- 一次线上Mysql数据库崩溃事故的记录
文章简介 工作这几年,技术栈在不断更新,项目管理心得也增加了不少,写代码的速度也在提升,感觉很欣慰,毕竟是在一直进步,但是过程中也有许许多多的曲折,也踩过了数不尽的坑坑洼洼,从一个连百度都不知道用的萌 ...
随机推荐
- mongodb为什么比mysql效率高
首先是内存映射机制,数据不是持久化到存储设备中的,而是暂时存储在内存中,这就提高了在IO上效率以及操作系统对存储介质之间的性能损耗.(毕竟内存读取最快) 其次,NoSQL并不是不使用sql,只是不使用 ...
- 时间&空间(complexity)
时间&空间复杂度 时间复杂度: 通俗来说就是随着数据量的增加,程序运行的时间花费量是怎么变化的,时间复杂度常用大o表示.举个例子,猜数字,猜10个,100个.1000个,猜数的数据量是在增加的 ...
- Hystrix 实战经验分享
一.背景 Hystrix是Netlifx开源的一款容错框架,防雪崩利器,具备服务降级,服务熔断,依赖隔离,监控(Hystrix Dashboard)等功能. 尽管说Hystrix官方已不再维护,且有A ...
- SpringBoot-08 SpringSecurity
SpringBoot-08 SpringSecurity 创建了一个新项目,创建时选择导入starter-web 1.环境搭建 1.1 导入thymeleaf <dependency> & ...
- Kubernetes,kubectl常用命令详解
kubectl概述 祭出一张图,转载至 kubernetes-handbook/kubectl命令概述 ,可以对命令族有个整体的概念. 环境准备 允许master节点部署pod,使用命令如下: kub ...
- Istio 生产环境用户流量接入方案
总结Istio 生产环境用户流量接入方案 方案1 Client -> istioGateway域名(微服务) -> VritualService匹配路由并绑定网关 -> Destin ...
- istio in kubernetes (二) -- 部署篇
在 Kubernetes 部署 Istio [demo演示] 可参考官方文档(https://istio.io/latest/zh/docs/setup/install/) ,以部署1.7.4版本作为 ...
- 配置redis 4.0.11 集群
配置redis 4.0.11 集群 准备redis 软件和redis配置文件 启动Redis服务 /data/soft/redis/src/redis-check-aof --fix /log/red ...
- windows环境下抓密码总结
在线抓密码 1.mimikatz privilege::debug token::whoami token::elevate lsadump::sam mimikatz.exe "privi ...
- 【Java】流、IO(初步)
(这部分比较抽象且写的不是很好,可能还要再编辑) [概述] 流:流是一系列数据,包括输入流和输出流.你可以想象成黑客帝国的"代码雨",只要我们输入指令,这些数据就像水一样流进流出了 ...