前言

只有光头才能变强。

文本已收录至我的GitHub仓库,欢迎Star:github.com/ZhongFuChen…

记一次在工作中愚蠢的操作,本文关键字:线程安全

(我怎么天天在写Bug啊)

一、交代背景

我这边有一个系统,提供一个RPC接口去发送各种信息(比如短信、邮件、微信)等等渠道。我这边的系统架构是这样的:

概括:service系统提供一个RPC接口,别人调用我提供的接口,我在service系统中对这个消息进行判断、拼接等等业务逻辑,最后会将这个消息放到消息队列里边。sender系统会消费消息队列里边的数据,然后发送消息

例子:小王调用我们的RPC接口,想要发送邮件。我对邮件的参数进行判断和拼装成一个我这边定义好的Task,将这个Task丢到消息队列里边。sender系统消费这个Task,调用java.mail的API完成发送邮件的功能。

小王调用我们这个RPC接口,只要service系统把这个task丢到消息队列里边去,我们就返回response给小王。

  • 只要这个task放到了消息队列里边,我们就返回success。所以有的时候,小王会问:“我这明明返回是success啊,怎么我的邮件没发出去呢” ------(异步)

每发送一封邮件,我们都会将这封邮件的信息入库(保存在MySQL中),在MySQL中我们可以得知这封邮件的发送时间,发送状态等等。

而小王的这些邮件又十分在意是否成功发送出去了,如果发送失败了他那边需要重发。于是,他监听我们DB的binlog,根据binlog的信息来判断是否需要重发。

由于种种的原因,小王希望调用我们RPC接口的时候就能拿到一个唯一的标识好让他去判断这封邮件是成功还是失败

  • 显然,入库的Email ID是不可能的(因为他调我们RPC接口,我们将Task放到消息队列就返回了。此时sender系统还没消费呢)

于是,我们这边打算在service系统生成一个messageId,然后返回给他,将这个messageId绑定到Task里边,一直到入库。

二、上钩

上面确定好需求和思路之后,我这边就去看返回给小王的response对象,一看,发现已经有msgId字段了

public class SendResponse {// 错误码private int errCode;// 错误信息private String errInfo;// messageIdprivate long msgId;}
复制代码

我搜了一下这个字段的信息ctrl + shift + f,发现这msgId没有被用到啊。一想,这刚好,我来用了。我看了一下用法,发现这边不是直接使用SendResponse的,而是在外面包了一个枚举类,代码大概如下:

public enum Response {SUCCESS(1, "success"),PARAM_MISSING(2, "param is missing"),INVALID_xxxx(3, "xxxx is invalid"),INVALID_xxxx(4, "xxxx is invalid"),private SendResponse sendResponse;private Response(int errCode, String errInfo) {sendResponse = new SendResponse();sendResponse.setMsgId(0);sendResponse.setErrCode(errCode);sendResponse.setErrInfo(errInfo);}public SendResponse getSendResponse() {return sendResponse;}}
复制代码

有了枚举使用起来就很简单了,比如我发现小王某个参数传进来有问题,我反手就是:

Response.PARAM_ERROR
复制代码

service系统主要做了两件事

  • 判断参数/类型,各种业务逻辑有没有问题,将小王带过来的参数封装成Task对象
  • 将Task对象放到消息队列里边

要明确的是:等到整一个调用链结束(将Task对象放到消息队列中),才会将sendResponse对象返回出去。而又因为可能要判断的地方有点多,所以我们这边是这样设计了一个Map来存储数据,这个Map贯穿整条链路

// 首先将sendResponse默认设置为success,也就是代码如下:
map.put("sendResponse",Response.SUCCESS);// 如果中途某个地方可能有问题了,那我们将Map中sendResponse进行修改
map.put("sendResponse",Response.ERROR);// 等整条链路完成,从Map拿出sendResponse返回
return map.get("sendResponse");
复制代码

于是我要做的就是:在将SendResponse返回之前,我生成一个唯一的msgId,并插入到SendResponse对象里边就好了

Response.getSendResponse().setMsgid(uuid);
复制代码

这个需求完成得非常快,简单测试了一下也没毛病,就果断上线了。小王用了一阵子也没说有什么问题,于是这个需求就交付了。

三、出现问题

昨天,小王告诉我:“我这边邮件发送失败啦,有msgId,看下是什么原因造成的“

于是我就去捞线上的日志,发现根据他给出的msgId,我这边打出的日志都不是发送邮件的(而是其他Task的日志)。我这就慌了,难道我们这个系统出问题了?

  • 心理活动:msgId能够唯一标识这条Task,而小王发给我的msgId,却是别的Task的内容。是不是出大问题啦(错乱消费?数据全乱了?),惊慌失措

然后,他那边继续补充:

之后发现邮件是发送成功的,但是他拿到部分的msgId是别的Task的,不是邮件的。于是只能先比对剩下的邮件是否有问题,再看看MsgId是什么原因。

四、寻找问题

现有的条件是:

  • 那批邮箱发送是成功的
  • 小王拿到了别的Task的msgId

所以,判断系统是没问题的,只是msgId在并发的过程中出了问题(拿到其他Task的msgId了)

于是我就去找原因啦,在查代码的时候发现前同事还在Service系统中的某个类留了一个注解@NotThreadSafe。我就觉得肯定是中途哪个地方我没注意到,导致小王拿到了其他Task的msgId。

人肉Debug了一个午休的时间还是没找出来:每个线程都独有一份的操作对象,对象的属性都没有逸出(都在方法内部操作),跟着整块链路一直传递,直至链路结束。

后来,一想,我应该只看msgId生成的地方就好了呀。才发现,项目里边用的是枚举啊!

// 首先将sendResponse默认设置为success,也就是代码如下:
map.put("sendResponse",Response.SUCCESS);// 如果中途某个地方可能有问题了,那我们将Map中sendResponse进行修改
map.put("sendResponse",Response.ERROR);// 把response的msgId的值设置为当前Task绑定的值
map.get("sendResponse").setMsgid(uuid);// 等整条链路完成,从Map拿出sendResponse返回
return map.get("sendResponse");
复制代码

醒悟

  • 现在我有50个线程,每个线程在处理数据的时候都会有一个默认的sendResponse对象,这个对象是用枚举来标识Response.SUCCESS。所以,这50个线程都共享着这个sendResponse对象
  • 50个线程共享着这个sendResponse对象,每个线程都可以修改sendResponse里边的msgId属性,这就自然是线程不安全的。
  • 所以小王能拿到其他Task的msgId(小王的线程设置完msgId之后,还没返回,三歪的线程又更改了一次msgId,导致小王拿到三歪的msgId了)

总结:

  • 终于知道为啥当初前同事在代码上保留了msgId属性,但是没有使用这个属性。
  • 使用枚举就不应该带 有状态的属性(能修改、可变的属性)

最后

乐于输出干货的Java技术公众号:Java3y。公众号内有200多篇原创技术文章、海量视频资源、精美脑图,关注即可获取!

觉得我的文章写得不错,点

转载于:https://juejin.im/post/5d478317f265da03cd0a63ae

记一次愚蠢的操作--线程安全问题相关推荐

  1. 记一次愚蠢的操作--String不可变性

    前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 记录一次在写代码时愚蠢的操作,本文涉及到的知识点:S ...

  2. 关于如何解决java线程安全问题?很重要? 快拿小本本记下来!

    关于如何解决java线程安全问题?很重要? 快拿小本本记下来! 线程安全问题 什么是线程安全问题? 线程同步 同步代码块 同步方法 lock显示锁 线程安全问题 什么是线程安全问题? 多个线程操作同一 ...

  3. Java多线程——线程安全问题

    一.什么情况下会产生线程安全问题? 同时满足以下两个条件时: 1,多个线程在操作共享的数据. 2,操作共享数据的线程代码有多条. 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会 ...

  4. hash是线程安全的吗?怎么解决?_这次进程、线程、多线程和线程安全问题,一次性帮你全解决了...

    1. 什么是进程 一个软件,在操作系统中运行时,我们称其为进程. 进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元. 2. 什么是线程 在一个进程中,每个独立的功能都需要独立的去运行,这 ...

  5. iOS多线程全套:线程生命周期,多线程的四种解决方案,线程安全问题,GCD的使用,NSOperation的使用(上)

    2017-07-08 remember17 Cocoa开发者社区 目的 本文主要是分享iOS多线程的相关内容,为了更系统的讲解,将分为以下7个方面来展开描述. 多线程的基本概念 线程的状态与生命周期 ...

  6. struts2学习笔记--线程安全问题小结

    在说struts2的线程安全之前,先说一下,什么是线程安全?这是一个网友讲的, 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码.如果每次运行结果和单线程运行的结果是一样 ...

  7. servlet单实例多线程 ---线程安全问题是由实例变量造成的,只要在Servlet里面的任何方法里面都不使用实例变量,那么该Servlet就是线程安全的。(所有建议不要在servlet中定义成员变

    Servlet 单例多线程 Servlet如何处理多个请求访问? Servlet容器默认是采用单实例多线程的方式处理多个请求的: 1.当web服务器启动的时候(或客户端发送请求到服务器时),Servl ...

  8. 线程安全问题产生的原因

    //前提 1:多个线程操作同一个数据 2:操作共享数据的线程代码有多条 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算 就会导致线程安全问题的产生. 解决思路: 就是将多线程操作共享 ...

  9. java dateformat 线程安全_SimpleDateFormat线程安全问题深入解析

    背景 众所周知,Java中的SimpleDateFormat不是线程安全的,在多线程下会出现意想不到的问题.本文将解析SimpleDateFormat线程不安全的具体原因,从而加深对线程安全的理解. ...

最新文章

  1. MySQL数据库Raid存储方案
  2. 阿里云esc云服务器IP不能访问的解决办法
  3. ruby 数据sql操作
  4. 如何在VB6.0里动态使用具有事件的对象
  5. 干货警告!476个PyTorch资源大合集推荐,GitHub超过3600星
  6. mingw gcc mysql_Windows平台mingw编译器 mysql
  7. Nginx实战基础篇七 最新源码包通过脚本部署LAMP搭建Discuz论坛
  8. java.lang.InstantiationException: class has no zero argument constructor
  9. 计算机没网络怎么更新网卡驱动,电脑显示没有网卡驱动怎么办?电脑显示没有网卡驱动的解决方法...
  10. 博科Brocade 6505光纤交换机
  11. 西部世界:币本位是什么?
  12. 雅思口语练习必备100个经典句子
  13. 牛逼的Android UI
  14. WSL无法打开或者卡死
  15. yolo3.cfg相关配置
  16. 如何利用Maven查找依赖信息
  17. 【微信小程序】onShow中接收参数
  18. (APP入门)Android Studio照相保存并跳到另一个页面
  19. UOJ #31 【UR #2】猪猪侠再战括号序列
  20. 卡耐基《人性的优点》摘录

热门文章

  1. 《机器人与数字人:基于MATLAB的建模与控制》——2.3节指数映射和k过程
  2. 第三次毕业设计任务书
  3. 浅说Java中的反射机制(一)
  4. 首款移动社交购物平台“商宝”上线发布
  5. asp.net开源CMS推荐
  6. SQL - SQL 连接 JOIN 例解。(左连接,右连接,全连接,内连接,交叉连接,自连接)[转]...
  7. .ini文件的读写操作
  8. [七月挑选]使用idea创建spring boot 项目
  9. node.js+express,实现RESTful API
  10. .NET LINQ 筛选数据