全局变量引起的并发问题【高并发、多线程】
背景
最近采用RabbitMQ做核心系统 团车
缴费解耦功能,消费端应用采用了SpringBoot2.1+Redis+RabbitMQ+jdbctemplate+Oaracle
架构。 此消费端应用以前 单车
缴费时未出现任何问题,从监听—>日志输出—>业务逻辑处理等一切正常。 线上业务高峰期时也没有任何问题。 但是团车缴费功能启用时,多个200笔数据同时消费时就出现了数据 篡改
情况。 问题不难,看了日志之后发现对并发处理的不够到位。
结合着之前redis高并发多线程总结,顺便对这里也进行记录,希望帮助大家总结经验
另外一篇文章链接: redis高并发导致读写变慢(redis多线程)
模拟重现demo(部分代码略)
@Bean
public class controllerA{
private BillingMQServiceImpl billingMQServiceimpl;
public A(){
log.info("监听到billing消息队列消息:");
billingMQServiceimpl.process(new String(body,"utf-8"),redisDateType);
}
}
public class BillingMQServiceImpl{
private String bizNo = "";
private String sendBillingJson = "";
private String resultJson = "";
@Autowired
private InfoToBillFeignService infoToBillFeignService;
public process(String body){
// 1. 取业务号
bizNo = this.parseBody(body);
// 2. 业务逻辑处理
resultJson = this.noticSys();
}
public String parseBody(String body) throws Exception {
sendBillingJson = body;
try {
BillingInfoDto billingInfo = JSON.parseObject(body, BillingInfoDto.class);
bizNo = billingInfo.getBody().getHbReceivableInfo().get(0).getCplyno();
} catch (Exception ex) {
log.error(ex.getMessage());
throw ex;
}
log.info("billing MQ customer parse bizNo : " + bizNo);
return bizNo;
}
@Override
public void noticSys() {
long startTime = System.currentTimeMillis();
try {
log.info(bizNo.get() + "send car-bill-cust start!");
infoToBillFeignService.postData(sendBillingJson);
log.info(bizNo.get() + "send car-bill-cust sucess! cost " + (System.currentTimeMillis() - startTime) + " ms.");
} catch (Exception e) {
log.error(bizNo.get() + "send car-bill-cust faild, exception message : " + e.getMessage() + " ,cost " + (System.currentTimeMillis() - startTime) + " ms.");
throw e;
}
}
}
代码问题分析
以上这段代码在访问量不构成并发时不会出现什么问题。 但在并发情况下如当一个请求还未完成,另一个请求已经开始执行的情况下就会出现问题:
- 第二个请求执行执行process()方法会将第一个请求的bizNo以及sendBillingJson变量篡改。
- 导致第一个请求的数据没有执行业务逻辑noticSys() 中的infoToBillFeignService.postData(sendBillingJson); 所以第一笔数据就无法缴费了。
小编这样写的目的是想要把各个环节的日志都输出到日志文件中,实现在日志平台(filebeat+kafka+Elk)进行数据监控。 为了详细跟踪每一笔数据的情况所以需要在每个环节都通过业务号bizNo进行标记。但又不想通过方法参数的方式来传递,进而设置成了全局变量。
出现这个问题的原因
由于系统采用SpringBoot框架,底层原理还是Springmvc其核心控制器DispatcherServlet默认为每个controller生成单一实例来处理所有用户请求,所以在这个单一实例的controller中,它的controllerA也是一个实例处理所有请求, 这样controllerA的成员变量就被所有请求共享。这样就会出现并发请求时变量内容被篡改的问题。那么出现这种问题如何解决呢?
- 第一种方式: 全局变量改为局部变量,通过方法参数来传递。
- 第二种方式: Jdk提供了
java.lang.ThreadLocal
,它为多线程并发提供了新思路。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal并不是一个Thread,而是Thread的局部变量。
解决方案-ThreadLocal
那么在什么地方使用ThreadLocal呢? 什么变量是请求公用的就将该变量托付给ThreadLocal来管理其线程副本, 所以我们在Service BillingMQServiceImpl中使用它。
java.lang.ThreadLocal
,它为多线程并发提供了新思路。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal并不是一个Thread,而是Thread的局部变量。
TdLocal使用语法
初始化:ThreadLocal stringLocal = new ThreadLocal();
赋值:stringLocal.set(value)
取值:stringLocal.get();
public class BillingMQServiceImpl{
private ThreadLocal<String> sendBillingJson = new ThreadLocal<String>();
private ThreadLocal<String> bizNo = new ThreadLocal<>();
private ThreadLocal<String> resultJson = new ThreadLocal<>();
@Autowired
private InfoToBillFeignService infoToBillFeignService;
public process(String body){
// 1. 取业务号
bizNo.set(this.parseBody(body)) ;
// 2. 业务逻辑处理
resultJson.set(this.noticSys());
}
public String parseBody(String body) throws Exception {
sendBillingJson.set(body);
try {
BillingInfoDto billingInfo = JSON.parseObject(body, BillingInfoDto.class);
bizNo.set(billingInfo.getBody().getHbReceivableInfo().get(0).getCplyno());
} catch (Exception ex) {
log.error(ex.getMessage());
throw ex;
}
log.info("billing MQ customer parse bizNo : " + bizNo.get());
return bizNo.get();
}
@Override
public void noticSys() {
long startTime = System.currentTimeMillis();
try {
log.info(bizNo.get() + "send car-bill-cust start!");
infoToBillFeignService.postData(sendBillingJson.get());
log.info(bizNo.get() + "send car-bill-cust sucess! cost " + (System.currentTimeMillis() - startTime) + " ms.");
} catch (Exception e) {
log.error(bizNo.get() + "send car-bill-cust faild, exception message : " + e.getMessage() + " ,cost " + (System.currentTimeMillis() - startTime) + " ms.");
throw e;
}
}
}
模拟验证
此类并发篡改数据的问题,可以在开发工具中设置断点调试的方式来模拟并发。即第一次请求运行到断点时,查看bizNo, sendBillingJson内容,并且不让程序继续往下运行,同时再发起一个请求,再查看bizNo, sendBillingJson内容。 如内容是第一次请求的内容,并且让第一个请求跑完后,第二个请求到断线处的content正确时,可以确定不会出现并发问题。