问题症状

HTTP 日志系统,老是出现日志信息覆盖的情况。比如同时调用 A 接口和 B 接口,B 接口请求响应信息变成了 A 接口请求响应相关信息。这个问题在并发量大的情况下越来越严重。

问题初步分析

显然并发量越来越大,问题越来越严重,是一个多线程问题。日志采集是通过 Spring 的 LogHttpInterceptor 来做的,分析一下代码。

public class LogHttpInterceptor extends HandlerInterceptorAdapter {

    private Logger logger;
private PathMatcher pathMatcher;
private String[] excludePaths;
private LogMsg msg; public LogHttpInterceptor(Logger logger) {
this.logger = logger;
} public LogHttpInterceptor(Logger logger, String... excludePaths) {
this.logger = logger;
this.excludePaths = excludePaths;
if (!StringUtils.isEmpty(this.excludePaths)) {
pathMatcher = new AntPathMatcher();
}
} @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception { if (isSkip(request)) {
return true;
}
msg = new LogMsg(logger.getModule());
msg.putRequest(request);
LogMsgThreadMapper.putLogMsg(LogMsg.REQUEST_TIME, "" + System.currentTimeMillis());
return true;
} @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
if (isSkip(request)) {
return;
}
LogMsgThreadMapper.putLogMsg(LogMsg.RESPONSE_TIME, "" + System.currentTimeMillis());
msg.putResponse(request, response);
if (ex != null) {
msg.append(ex.getMessage());
}
logger.log(msg);
} private boolean isSkip(HttpServletRequest request) {
if (pathMatcher != null && excludePaths != null) {
for (String exclude : excludePaths) {
if (pathMatcher.match(exclude, request.getRequestURI())) {
return true;
}
}
}
return false;
}
}

其实我看了域变量 private LogMsg msg 我就感觉有问题了。在一个 Spring 框架中,像 HandlerInterceptorAdapter 一般都会生成单例 Bean。我仔细研读了注册 HandlerInterceptorAdapter Bean 代码,果然。

   @Bean
public HandlerInterceptorAdapter aliLoggerInterceptor(@Autowired AliLogger aliLogger) {
LogHttpInterceptor interceptor = new LogHttpInterceptor(aliLogger, "/api/s/s");
return interceptor;
}

单例 Bean 在多线程环境下,写域变量是一件很危险的事情——每个线程都可以修改域变量的值。仔细看一下 LogMsg 使用,首先在 preHandle new 出一个实例,再在 afterCompletion 继续使用,在传入日志系统进行日志处理。

什么情况会出现该问题呢?具体分析一下。

  • 第一种情况,请求 A preHandle、afterCompletion处理完,请求 B 继续处理 preHandle、afterCompletion,LogMsg 是新的,不是这种情况
  • 第二种情况,请求 A preHandle 处理完、afterCompletion未处理完,请求 B 处理 preHandle,无论请求 B afterCompletion 是否处理完,请求 A 的 LogMsg 被修改

复现

复现该问题也简单,就不贴代码了。

  • 在 Controller 中新建两个接口,一个叫 /fast,一个叫 /slow
  • /slow 先休眠 10 s,/fast 立刻返回
  • 先调用 /slow 接口,再调用 /fast 接口

问题必现了。

修复

很简单,不要在单例中共享对象。实现对象传递也要在 ThreadLocal 中。此问题只要把 LogMsg 放在 ThreadLocal 中操作即可,线程执行结束或者开始时,清理一下 ThreadLocal。

总结

  • 单例模式不要在域中共享变量
  • 线程共享变量最好在 ThreadLocal 中,以并发集合传递数据也是种不错的选择
  • 对于多线程,要小心谨慎

一个由单例模式在多线程环境下引发的 bug的更多相关文章

  1. 单例模式在多线程环境下的lazy模式为什么要加两个if(instance==null)

    刚才在看阿寻的博客”C#设计模式学习笔记-单例模式“时,发现了评论里有几个人在问单例模式在多线程环境下为什么lazy模式要加两个if进行判断,评论中的一个哥们剑过不留痕,给他们写了一个demo来告诉他 ...

  2. HttpClient在多线程环境下踩坑总结

    问题现场 在多线程环境下使用HttpClient组件对某个HTTP服务发起请求,运行一段时间之后发现客户端主机CPU利用率呈现出下降趋势,而不是一个稳定的状态. 而且,从程序日志中判断有线程处于han ...

  3. 多线程环境下的UI异步操作

    转自原文 多线程环境下的UI异步操作 解决VS中,线程间不可互操作的问题,一揽子解决方案: 一.首先,定义一个类:SetControlProperty using System.Reflection; ...

  4. C++多线程环境下的构造函数

    多线程的环境里,我们总不可避免要使用锁.于是一个常见的场景就是: class ObjectWithLock { private: std::mutex mtx_; SomeResType shared ...

  5. Asp.Net在多线程环境下的状态存储问题

    在应用开发中,我们经常需要设置一些上下文(Context)信息,这些上下文信息一般基于当前的会话(Session),比如当前登录用户的个人信息:或者基于当前方法调用栈,比如在同一个调用中涉及的多个层次 ...

  6. SQLite在多线程环境下的应用

    文一 SQLite的FAQ里面已经专门说明,先贴出来.供以后像我目前的入门者学习. (7) 多个应用程序或者同一个应用程序的多个例程能同时存取同一个数据库文件吗? 多进程可以同时打开同一个数据库,也可 ...

  7. 你是否听说过 HashMap 在多线程环境下操作可能会导致程序死循环?

    作者:炸鸡可乐 原文出处:www.pzblog.cn 一.问题描述 经常有些面试官会问,是否了解过 HashMap 在多线程环境下使用时可能会发生死循环,导致服务器 cpu 100% 的线上故障? 关 ...

  8. Java指令重排序在多线程环境下的应对策略

    一.序言 指令重排在单线程环境下有利于提高程序的执行效率,不会对程序产生负面影响:在多线程环境下,指令重排会给程序带来意想不到的错误. 本文对多线程指令重排问题进行复原,并针对指令重排给出相应的解决方 ...

  9. C#多线程环境下调用 HttpWebRequest 并发连接限制

    C#多线程环境下调用 HttpWebRequest 并发连接限制 .net 的 HttpWebRequest 或者 WebClient 在多线程情况下存在并发连接限制,这个限制在桌面操作系统如 win ...

随机推荐

  1. 【t053】整数去位

    Time Limit: 1 second Memory Limit: 128 MB [问题描述] 键盘输入一个高精度的正整数N,去掉其中任意M个数字后剩下的数字按原左右次序将组成一个新的正整数.编程对 ...

  2. USB 3.0规范中译本 第1章 引言

    本文为CoryXie原创译文,转载及有任何问题请联系cory.xie#gmail.com. 1.1 动机(Motivation) Universal Serial Bus (USB) 的原始动机来自于 ...

  3. Android—— ubuntu下【CTS】測试TV真机

    近期接触到CTS,据传不懂CTS就不算一个合格的android开发者,我之前一直没见周边谁用过.作为一个产品开发的android人员,我还是太年轻- 撰写不易,转载请注明出处:http://blog. ...

  4. [Angular] Some performance tips

    The talk from here. 1. The lifecycle in Angular component: constructor vs ngOnInit: Constructor: onl ...

  5. 新技能 get —— Python 断点续传下载文件

    from urllib.request import urlretrieve import sys import os prev_reported_download_percent = None # ...

  6. Zygote总结

    Zygote是由init进程通过解析 init.zygote.rc 文件而创建的,zygote所对应的可执行程序 app_process,所对应的源文件是App_main.cpp,进程名为zygote ...

  7. 两分钟读懂ThreadLocal

    ThreadLocal的工作原理 场景 假设一个用户是一个线程.他们都对数据库进行操作,这个时候就会每个用户就会从数据源中开启一个事务以确保能够顺利的打开或者关闭事务.那么如何保证用户与用户之间的数据 ...

  8. Dropout 理论基础与实战细节

    Dropout: A Simple Way to Prevent Neural Networks from Overfitting 对于 dropout 层,在训练时节点保留率(keep probab ...

  9. IIS执行原理

    IIS执行原理   服务器的监听(IIS6.0+版本) 当请求到达服务器时,请求最终会到达TCPIP.SYS驱动程序,TCPIP.SYS将请求转发给HTTP.SYS网络驱动程序的请求队列中(可以理解为 ...

  10. mongodb 批量更新 数组的键操作的文件

    persons该文件的数据如下面的: > db.persons.find() { "_id" : 2, "name" : 2 } { "_id& ...