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

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 作为一名本本分分的练习时长两年半的Java练习生,一直深耕在业务逻辑里,对并发编程的了解仅仅停留在八股文里。一次偶然的机会,接到一个私活,核心逻辑是写一个 定时访问api把数据持久化到数据库的小服务。

前言
作为一名本本分分的练习时长两年半的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包的插件才行,这里算是一个小坑。

<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>


复制代码
上线程池
客户大约有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();
        }
    }

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
9月前
|
存储 安全 Python
python多线程------>这个玩意很哇塞,你不来看看吗
python多线程------>这个玩意很哇塞,你不来看看吗
|
7月前
|
设计模式 网络协议 算法
|
7月前
|
设计模式 缓存 算法
花了30天才肝出来,史上最全面Java设计模式总结,看完再也不会忘
Design Patterns: Elements of Reusable Object-Oriented Software(以下简称《设计模式》),一书由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides合著(Addison-Wesley,1995)。这四位作者常被称为“四人组(Gang of Four)”,而这本书也就被称为“四人组(或 GoF)”书。他们首次给我们总结出一套软件开发可以反复使用的经验,帮助我们提高代码的可重用性、系统的可维护性等,解决软件开发中的复杂问题。
105 0
|
11月前
|
IDE 开发工具 Python
这样的奇技淫巧,劝你不用也罢
这样的奇技淫巧,劝你不用也罢
101 0
|
人工智能 运维 数据可视化
程序员养家活口接私活必备网站(顺便用技术改变世界)
程序员接私活的原因很多种(挣钱、养家糊口、提升技术等等)。下面整理了一下网站送给最有潜能的你。 提前准备好自己的笔记本和技术呦。
479 0
|
Java 关系型数据库 MySQL
【浅尝高并发编程】接私活差点翻车
作为一名本分的专注业务2的Java工程师,日常开发中很遇用到并发编程的场景。头一次接私活没想到遇到这么多坑。。。
374 0
【浅尝高并发编程】接私活差点翻车
|
安全 程序员 开发者
程序人生 - 程序员接私活怎样防止做完了不给钱?
程序人生 - 程序员接私活怎样防止做完了不给钱?
140 0
|
供应链 程序员 Android开发
2020年,给你7个程序员接私活必备网站!
2020互联网圈不好混,不是每个公司都能像蚂蚁金服一样这么大气,不少公司今年因为疫情已经开始裁员,不要抱怨,加油干就完事了! 今天给大家推荐几个赚钱养家的好渠道,一起来看看吧!
2020年,给你7个程序员接私活必备网站!
J3
|
机器学习/深度学习 存储 缓存
有图有真相的Java内存模型基础,你好意思不看嘛!
有图有真相的Java内存模型基础
J3
127 0
有图有真相的Java内存模型基础,你好意思不看嘛!