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数据库崩溃事故的记录
文章简介 工作这几年,技术栈在不断更新,项目管理心得也增加了不少,写代码的速度也在提升,感觉很欣慰,毕竟是在一直进步,但是过程中也有许许多多的曲折,也踩过了数不尽的坑坑洼洼,从一个连百度都不知道用的萌 ...
随机推荐
- 【python+selenium的web自动化】- 针对上传操作的实现方案
如果想从头学起selenium,可以去看看这个系列的文章哦! https://www.cnblogs.com/miki-peng/category/1942527.html 关于上传操作 上传有两 ...
- 01-静态web服务器(Python)-面向对象的对比
普通写法,静态web服务器: 先创建TCP服务器套接字,然后等待客户端(这里是浏览器)请求连接. 客户端发起请求,用线程来处理连接的建立,这样可以实现多任务(也就是并发) 连接后根据请求发送指定页面 ...
- kettle 执行 kjb 临时文件夹 /tmp permission denied 问题
编写完的 kettle job (kjb文件) 放在服务器上执行的时候出现了奇怪的错误: # 执行 kjb ./kitchen.sh -file:/opt/code/ods/ods_inc.kjb # ...
- 在ASP.NET Core中使用ViewComponent
前言 在之前的开发过程中,我们对于应用或者说使用一些小的组件,通常使用分布页(partial view),再往前在Web Form中我们会进行应用WEB Control,好吧提及一个关键性代码TagP ...
- 201871010130-周学铭 实验三 结对项目—《D{0-1}KP 实例数据集算法实验平台》项目报告
项目 内容 课程班级博客链接 18卓越班 这个作业要求链接 实验三结对编程要求 我的课程学习目标 体验软件项目开发中的两人合作,练习结对编程(Pair programming).掌握Github协作开 ...
- OOUnit4Summary
一.前三次作业的架构设计 前言 第四单元以uml建模语言为背景,让我们通过已有的模型架构,设计实现类图(顺序图,状态图)解析器:在理解uml整体架构体系的基础上,读懂并熟练运用官方的给的代码,设计有层 ...
- MindSpore函数拟合
技术背景 在前面一篇博客中我们介绍过基于docker的mindspore编程环境配置,这里我们基于这个环境,使用mindspore来拟合一个线性的函数,演示一下mindspore的基本用法. 环境准备 ...
- Facebook资深工程师带你学Python核心技术
人工智能时代下,Python毫无疑问是最热的编程语言.在推开Python的大门后却发现,Python入门容易但精通却不易. 想要精通这门语言,必须真正理解知识概念,比如适当从源码层面深化认知,然后熟悉 ...
- 机器学习--PR曲线, ROC曲线
在机器学习领域,如果把Accuracy作为衡量模型性能好坏的唯一指标,可能会使我们对模型性能产生误解,尤其是当我们模型输出值是一个概率值时,更不适宜只采取Accuracy作为衡量模型性泛化能的指标.这 ...
- Dapr | 云原生的抽象与实现
引言 Dapr 是微软主导的云原生开源项目,2019年10月首次发布,到今年2月正式发布 V1.0 版本.在不到一年半的时间内,github star 数达到了 1.2 万,超过同期的 kuberne ...