关于ruby:浅尝高并发编程接私活差点翻车

5次阅读

共计 3901 个字符,预计需要花费 10 分钟才能阅读完成。

前言
作为一名本本分分的练习时长两年半的 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();
        }
    }
正文完
 0