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

前言

  • 不知道什么时候年轻的我曾一度认为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. Django之模版层

    一.模版简介 你可能已经注意到我们在例子视图中返回文本的方式有点特别,也就是说,HTML被直接硬编码在python代码之中. def current_datetime(request): now = ...

  2. Python-tkinter-window

    示例代码讲解 1.加载tkinter模块 2.创建一个窗口 3.设置窗口的主题 4.开始窗口的事件循环 import tkinter 2 win = tkinter.Tk() 3 win.title( ...

  3. Azure Front Door(三)启用 Web Application Firewall (WAF) 保护Web 应用程序,拒绝恶意攻击

    一,引言 上一篇我们利用 Azure Front Door 为后端 VM 部署提供流量的负载均衡.因为是演示实例,也没有实际的后端实例代码,只有一个 "Index.html" 的静 ...

  4. 走进docker-swarm 带大家快速掌握docker自带编排工具

    什么是Docker Swarm? 对比Docker 前面我们介绍过Docker可以理解成是一个我们的服务的独立运行的容器,那么在实际工作中,我们的系统可能是一个微服务应用,系统中根据业务拆分成多个模块 ...

  5. 从I/O多路复用到Netty,还要跨过Java NIO包

    本文是Netty系列第4篇 上一篇文章我们深入了解了I/O多路复用的三种实现形式,select/poll/epoll. 那Netty是使用哪种实现的I/O多路复用呢?这个问题,得从Java NIO包说 ...

  6. Mysql之案例分析(一)

    可见性分析 CREATE TABLE `t` ( `id` int(11) NOT NULL, `k` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGIN ...

  7. css盒模型以及如何计算盒子的宽度

    css盒模型以及如何计算盒子的宽度 盒模型 每个存在于可访问性树中的元素都会被浏览器绘制成一个盒子[1]. 每个盒子都可以看成由4部分组成,它们分别是 - 元素外边距(margin).元素边框(bor ...

  8. 带你全面认识CMMI V2.0(一)

    新发布的CMMI V2.0在方法论和方法上都提出了一些战略上的变化和改进.这些更新提供了新的要求.历史标准的演进以及以往实践的延续. CMMI V2.0和CMMI V1.3的变化 CMMIV2.0的定 ...

  9. String类的使用2

    /*String:字符串,使用一对""引起来表示.1.String声明为final的,不可被继承2.String实现了Serializable接口:表示字符串是支持序列化的. 实现 ...

  10. 火狐兼容selenium版本解决

    火狐和selenium不是很好兼容,下面提供一些我所知道的可以兼容的版本: 1. 火狐52+selenium 3.3 +geckodriver v0.15.0 2.火狐59+selenium 3.11 ...