我正在参与「启航计划」

前言

作为一名本本分分的操练时长两年半的Java操练生,一向深耕在业务逻辑里,对并发编程的了解只是停留在八股文里。一次偶尔的时机,接到一个私活,中心逻辑是写一个 守时访问api把数据耐久化到数据库的小服务。

【浅尝高并发编程】接私活差点翻车

期间遇到了许多坑还挺有意思,做出来很简单,做得好仍是挺难的,这儿跟咱们共享一下。

maven引进外部jar包布置

项目背景是某家厂商要对接第三方支付公司的open api拿到每日产品销售量与销售额,第三方支付公司便是哗啦啦,这儿吐槽下哗啦啦做的敞开文档写的是真捞。。。

首先要把哗啦啦这边提供的jar包引进到咱们的服务里,本地开发直接引进即可可以用maven的一条命令直接把本地的jar包打到本地仓库里。

mvn install:install-file -DgroupId=com.uptown -DartifactId=xxx_sdk -Dversion=1.0-SNAPSHOT -Dpackaging=jar -Dfile=E:\uptown\uptown.jar

可是这样布置服务的时分就会发现打不出jar包来,项目能跑,可是到要害的调用sdk的时分就报ClassNofFoundException过错。

需要在pom里装备好引进外部jar包的插件才行,这儿算是一个小坑。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <configuration>
        <source>${java.version}</source>
        <target>${java.version}</target>
        <encoding>UTF-8</encoding>
        <compilerArguments>
            <bootclasspath>${java.home}/lib/rt.jar</bootclasspath>
        </compilerArguments>
    </configuration>
</plugin>

上线程池

客户大约有150个门店,这样堵塞恳求下来只是是恳求api的耗时就将近半小时,还不算入库的时间。

【浅尝高并发编程】接私活差点翻车

这时分依据熟读并背诵的八股文只是,要充分利用cpu的才能无脑选择线程池,所以起了一个中心线程20个的线程池去并发恳求api数据入库。布置之后发现总有这么几条过错日志。

com.*.OrderDetailMapper.updateById
(batch index #2) failed. 
1 prior sub executor(s) completed successfully, 
but will be rolled back. 
Cause: java.sql.BatchUpdateException: 
Deadlock found when trying to get lock

果然背八股文救不了我国,依据报错判断出入库时死锁了。。。。

清查死锁

因为我在服务里用了mybatis-plus的orm框架,并且数据是一些订单数据存在一些状况改变,然后服务要同步的数据可能会有更改之前旧数据的场景,所以就用了Mybatis里的SaveOrUpdate办法,坏事就坏事在这上面。

【浅尝高并发编程】接私活差点翻车

这个东西还要从mysql数据库的隔离等级开始说起,众所周知,按照八股文里mysql隔离等级默认状况下为可重复读,那可重复读隔离机制下避免了脏读,不可重复读,日常开发里也并没有呈现过幻读,看似MVCC多版别并发控制帮咱们避开了幻读。

其实不然,幻读的概念与不可重复读类似,不可重复读读到了他人update的数据,幻读读到了他人insert/delete的数据。一个业务在读取了其他业务新增的数据,仿佛呈现了梦想。

这儿先简单说一下,mysql在可重复读隔离等级下会为每个业务其时读的时分加空隙锁,后续会写一篇mysql在可重复读的隔离等级下如何处理幻读文章。

那怎么处理的呢,时间紧使命重,仔细一想这个数据库根本上全是往里增删改的动作,查询的动作几乎没有,那为什么不把它隔离等级降级,降成读已提交,这样空隙锁就不生效了。

很完美,后续也验证了这个问题,再也没呈现过数据库的死锁状况。

数据库链接丢掉

这个问题是真滴厌恶,客户买的服务器拉的一批,还买windows服务器,这年头正经人谁用windows,用客户端连都常常丢掉链接。遇到这个问题非常棘手,那不处理数据就永久不准确。

可是想彻底治愈这个问题又得不偿失,甲方选windows还不便是看重了可视化界面了。这时分再让他们搬迁服务器必定不可能。

所认为了处理这个就疯狂在网上搜方案,什么改my.ini的wait_out_time,什么改jdbc的url都白费,后来我一想,算球了,不靠数据库了,本来想让这么多数据每条都一次成功也不现实。

所以搞了个error_msg表,入库的时分有问题就记在error_msg里,然后启一个守时使命,每1分钟扫描表里所有刺进失利记载,一次不可两次,两次不可三次,三次不可一向试。

这个重试补上之后的确数据库这方面的坑根本踩的差不多了。

服务假死CPU打满

这个状况是出在处理mysql链接丢掉前,其时我想,为什么要用多线程,是因为功率低,功率低其实是低在恳求api上,也便是我可以先多线程恳求到数据放到一个list里,然后用单个数据库链接去写,这样下降mysql的连接数应该就不会丢了吧。

这么简单就丢那还写啥啊。成果我就一个月一个月的拉数据,写完数据清空list,所以搞了个CopyAndWriteList,白背了那么多八股文了,一次也没用过,成果就cpu给人打的满满的。。。

原因其实也很简单,便是这个容器内部都用锁保证了线程安全。我就把这个容器当作参数传给每个Thread,弄完直接发动。成果吧是服务忽然就没有新增数据了,然后看日志也没不打日志,jps看服务还在。

遇到这种问题先上三板斧,回到家连上服务器上来先看快照,Jstack -l pid。

   Locked ownable synchronizers:
    - None

又是死锁了,这儿就不深究了。后续直接换了用LinedblockingQueue的推迟行列,另起消费线程不断消费推迟行列入库的方案了。

线程池莫名丢掉链接

本来认为处理了写库的问题就差不多了,没想到啊没想到,这个不丢那个丢,数据仍是有许多差异,找error_msg又没体现出来,一顿排查后来发现是线程池这边的问题。这儿的线程池用的Guava的线程池,重写反常捕获

 @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        //线程池中的使命挂掉,主动重新提交该使命
        if (!ObjectUtils.isEmpty(t)) {
            System.out.println("restart fetch data...");
            execute(r);
        }
    }

等候行列用无界行列,客户的服务器尽管拉,可是内存挺大,订单数撑不爆内存,中心线程10个,感觉一切都是那么合理。可是便是有问题,我发现在afterExecute办法拦截挂掉的使命反常时发现有许多使命的反常是java.util.concurrent.RejectedExecutionException也便是被履行了回绝战略。

这就非常不合理了,只有当行列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池才会发动饱和回绝战略。那我界说线程池的时分明明是无界行列,来者不拒。为啥会被履行回绝战略。

这个问题困扰了老久老久,致使于我都不想管了,怎么办客户一向催一向催,逼得我不得不处理这问题。

后来在Stack Overflow上有个老哥在源码中找到原因

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
      //此处,把使命放到堵塞行列中,采纳的是offer办法
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

留意注释那里,线程池是用的offer办法。不同就在于offer办法是**不堵塞的**,刺进不了了,就往下走;而put办法是一向堵塞,直到元素刺进到堵塞行列中。这问题一卡卡了我良久,弄得我好几天坐地铁光研究这玩意了。所以重写回绝战略强制它put回行列:

 @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }