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

前言

  • 不知道什么时候年轻的我曾一度认为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引起的一次线上事故的更多相关文章

  1. 由定时脚本错误以及Elasticsearch配置错误引发的Flink线上事故

    近期接手离职同事项目,突然遇到线上事故,Flink无法正常聚合数据生成指标. 以下是详细的排查过程: 问题复现 清晨,运维报告Flink数据分析模块无法正常生成指标数据. 赶紧登陆Flink所在机器, ...

  2. RabbitMQ 线上事故!慌的一批,脑袋一片空白。。。

    前言 那天我和同事一起吃完晚饭回公司加班,然后就群里就有人@我说xxx商户说收不到推送,一开始觉得没啥.我第一反应是不是极光没注册上,就让客服通知商户,重新登录下试试.这边打开极光推送的后台进行检查. ...

  3. 记一次线上事故的JVM内存学习

    今天线上的hadoop集群崩溃了,现象是namenode一直在GC,长时间无法正常服务.最后运维大神各种倒腾内存,GC稳定后,服务正常.虽说全程在打酱油,但是也跟着学习不少的东西. 第一个问题:为什么 ...

  4. 记一次真实的线上事故:一个update引发的惨案!

    目录 前言 项目背景介绍 要命的update 结语 前言   从事互联网开发这几年,参与了许多项目的架构分析,数据库设计,改过的bug不计其数,写过的sql数以万计,从未出现重大纰漏,但常在河边走,哪 ...

  5. 一次线上事故,让我对MySql的时间戳存char(10)还是int(10)有了全新的认识

    美好的周五 周五的早晨,一切都是那么美好. 然鹅,10点多的时候,运营小哥哥突然告诉我后台打不开了,我怀着一颗"有什么大不了的,估计又是(S)(B)不会连wifi"的心情,自信的打 ...

  6. 线上bug分析

    昨天下午大神把组内几十号人召集在一起开Online bug分析大会,主要是针对近期线上事故从事故原因和解决方案两个维度来分析. 对金融软件来说,每一次的线上事故都有可能给公司带来重大的损失,少扣了用户 ...

  7. 记录一次因subprocess PIPE 引起的线上故障

    sence:python中使用subprocess.Popen(cmd, stdout=sys.STDOUT, stderr=sys.STDERR, shell=True) ,stdout, stde ...

  8. 一个purge参数引发的惨案——从线上hbase数据被删事故说起

    在写这篇blog前,我的心情久久不能平静,虽然明白运维工作如履薄冰,但没有料到这么一个细小的疏漏会带来如此严重的灾难.这是一起其他公司误用puppet参数引发的事故,而且这个参数我也曾被“坑过”.   ...

  9. 一次线上Mysql数据库崩溃事故的记录

    文章简介 工作这几年,技术栈在不断更新,项目管理心得也增加了不少,写代码的速度也在提升,感觉很欣慰,毕竟是在一直进步,但是过程中也有许许多多的曲折,也踩过了数不尽的坑坑洼洼,从一个连百度都不知道用的萌 ...

随机推荐

  1. Hibernate Validator异常HV000221解决办法

    自建博客地址:https://www.bytelife.net,欢迎访问! 本文为博客同步发表文章,为了更好的阅读体验,建议您移步至我的博客 本文作者: Jeffrey 本文链接: https://w ...

  2. mysql中的基础查询 练习

    #进阶1:基础查询 /* 语法: select 查询列表 from 表名; 类似于:System.out.println(打印东西); 特点: 1.查询列表可以是:表中的字段.常量值.表达式.函数 2 ...

  3. Maven安装本地依赖包

    前提已安装maven并且配置了环境变量1.进入jar包所在的目录,打开cmd2.了解包的groupId.artifactId.version2.输入命令(依赖sdk为例)---maven命令mvn i ...

  4. 设置beeline连接hive的数据展示格式

    问题描述:beeline -u 方式导出数据,结果文件中含有"|"(竖杠). 执行的sql为:beeline -u jdbc:hive2://hadoop1:10000/defau ...

  5. 怎么理解onStart可见但不可交互

    前言 今天朋友遇到一个面试题,分享给大家: onStart生命周期表示Activity可见,那为什么不能交互呢? 这个问题看似简单,但涉及到的面还是比较多的,比如Activity生命周期的理解,进程的 ...

  6. 1-认识c指针

    1.指针和内存 c程序在编译后,会以三种形式使用内存 1静态/全局内存 静态声明的变量分配在这里,全局变量也使用这部分内存.这些变量在程序开始运行时分配,直到程序终止时才会消失 2.自动内存 这些变量 ...

  7. P1618 三连击(升级版)(JAVA语言)

    题目描述 将1,2,-,9共9个数分成三组,分别组成三个三位数,且使这三个三位数的比例是A:B:C,试求出所有满足条件的三个三位数,若无解,输出"No!!!". //感谢黄小U饮品 ...

  8. python3 byte,int,str转换

    1 # bytes 与 int 2 b=b'\x01\x02' 3 num=int.from_bytes(b,'little') 4 print('bytes转int:',num) 5 6 b1=nu ...

  9. Typora常用编辑方法-一个能将写博客变作享受的工具

    1,标题 ctrl+数字(1~5) 2,序号 数字序号 数字 + . +空格,之后回车换行会自动产生数字序号 非数字序号 有三种 实心圆 ,非实心圆与实心方框 都是 +空格 ,之后按tab键向内缩进, ...

  10. Hashtable 渐渐被人们遗忘了,只有面试官还记得,感动

    尽人事,听天命.博主东南大学硕士在读,热爱健身和篮球,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步 本文已收录于 「CS-Wiki」Gitee ...