全局变量引起的并发问题【高并发、多线程】—— ThreadLocal

全局变量引起的并发问题【高并发、多线程】

背景

最近采用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正确时,可以确定不会出现并发问题。

转载网上资源链接

猜你喜欢

转载自blog.csdn.net/u012723183/article/details/103809963
今日推荐