mysql-DuplicateUpdate和java的threadpool的"死锁"

大家千万不要被文章的标题给迷惑了,他两在本篇文章是没有关系的, 今天给大家讲讲最近2个有意思的issue,分享一下我学到的
mysql DuplicateUpdate的用法要注意的点 java的threadpool使用不当会造成“死锁”问题
mysql DuplicateUpdate的用法要注意的点
有个issue说遇到了一个这样的问题, 
这个朋友使用我开源的job调度框架 https://github.com/yuzd/Hangfire.HttpJob
存储用的是mysql,采用的实现是 https://github.com/arnoldasgudas/Hangfire.MySqlStorage
set表的id是自增主键,正常理解 都是慢慢自增上去的,但是发现是大幅度跳跃式的自增, 真相是什么?
首先针对这个问题,首先我们搞清楚在hangfire中和storage相关的部分如下
hangfire server调度依赖storage storage抽象出来一层api(解耦) 第三方扩展(不关心具体的storage实现) 不同的storage具体实现(比如mysql,sqlserver等)
Hangfire.Httpjob其实只是依赖了storage api那一层,也没有能力去直接写sql去执行, 只能用api去操作hangfire的那几张表(比如set表)
那么问题肯定不是在扩展层,而是得去看看mysqlstorage的实现源码,针对set表的处理逻辑
public override void AddToSet(string key, string value, double score)
{
Logger.TraceFormat("AddToSet key={0} value={1}", key, value);
AcquireSetLock();
QueueCommand(x => x.Execute(
$"INSERT INTO `{_storageOptions.TablesPrefix}Set` (`Key`, `Value`, `Score`) " +
"VALUES (@Key, @Value, @Score) " +
"ON DUPLICATE KEY UPDATE `Score` = @Score",
new { key, value, score }));
}
这里是用了ON DUPLICATE KEY UPDATE 的语句
这个语法是在mysql 4.1(2005)引入的,意思是 insert的时候遇到主键已存在 就执行后面 的update
但是就是这个功能 会造成自增主键成跳跃式增长,增长跨度和SQL的执行次数成正比
根据朋友提供的截图
虽说是会跳跃,但是这个增长也太夸张了
打上断点调试发现
是hangfire server 不断的在调用,目的是把下一次执行时间(秒级别的时间戳)写到set表中
打上日志可以看到有非常多相同值的调用,这仅仅是一个job,这个自增速度得再乘以job的个数,难怪了
既然找到原因了,就提个PR 修改下
public override void AddToSet(string key, string value, double score)
{
Logger.TraceFormat("AddToSet key={0} value={1}", key, value);
AcquireSetLock();
QueueCommand(x =>
{
var sql = "";
if (key == "recurring-jobs") // 只发现这个key存在这个问题
{
// key+value是uniq 改成先update 如果没有成功 再insert
sql = $"UPDATE `{_storageOptions.TablesPrefix}Set` set `Score` = @score where `Key` = @key and `Value` = @value";
var updateRt = x.Execute(sql, new { score = score, key = key, value = value });
if (updateRt < 1)
{
sql = $"INSERT INTO `{_storageOptions.TablesPrefix}Set` (`Key`, `Value`, `Score`) " +
"VALUES (@Key, @Value, @Score) ";
x.Execute(
sql,
new { key, value, score });
}
}
else
{
sql = $"INSERT INTO `{_storageOptions.TablesPrefix}Set` (`Key`, `Value`, `Score`) " +
"VALUES (@Key, @Value, @Score) " +
"ON DUPLICATE KEY UPDATE `Score` = @Score";
x.Execute(
sql,
new { key, value, score });
}
//Console.WriteLine(sql + " ==> " + key + "@" + value + "@" + score);
});
}
改完之后测试,id自增一切正常:
java的threadpool使用不当会造成“死锁”问题
这个原因先说出来: threadpool的线程被占用完后,再来的task会往queue里面丢,如果这个时候在这个pool的线程里面 future.get()的话会导致task runner(执行器)被堵住,没人从队列里面取任务了~
(简单来说就是 线程在wait future返回,而这个future在queue里面苦苦等待新释放的线程去执行,就像死锁一样,我在等你的结果,而结果在等待着被执行)
好家伙,这个场景有点熟悉,因为我在项目中也用过Future.get()// 虽说有设置timeout
但是这个问题的重要一点是,这种花式“死锁” jvm是检测不出来的,下面有测试
模拟一下这个场景:
我搞了2个线程池,分别是nio线程池和业务线程池,模拟并发20个请求, 注意看process2方法里的注释,如果去掉那里的代码的话 就不会有这个死锁问题
/**
* @author yuzd
*/
public class PoolTest {
// 模拟nio线程池
static ThreadPoolExecutor nioExecutor = new ThreadPoolExecutor(20, 20, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100),
new CustomerNamedThreadFactory("nio", false),
new ThreadPoolExecutor.AbortPolicy());
// 业务线程池
static ThreadPoolExecutor buExecutor = new ThreadPoolExecutor(20, 20, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100),
new CustomerNamedThreadFactory("bu", true),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
// 模拟是http请求并发20个
IntStream.rangeClosed(1, 20).parallel().forEach((index) -> {
// 交给nio线程池处理
nioExecutor.execute(() -> {
try {
httpHandler(index);
} catch (Exception e) {
e.printStackTrace();
}
});
});
}
static void httpHandler(int index) throws ExecutionException, InterruptedException {
System.out.println(Thread.currentThread().getName() + " request index :" + index + " staring");
// 交给业务线程池处理
Future<String> parentFuture = buExecutor.submit(() -> process1(index));
String p1Rt = parentFuture.get(); // nio线程在wait
System.out.println(Thread.currentThread().getName() + " request index :" + p1Rt + " ending");
}
// future1
static String process1(int index) throws ExecutionException, InterruptedException {
System.out.println( Thread.currentThread().getName() + " process1 index :" + index + " staring");
Future<String> childFuture = buExecutor.submit(() -> process2(index));
String p2Rt = childFuture.get(); // 这里是bu线程在wait 这里会发生死锁
System.out.println(Thread.currentThread().getName() + " process1 index :" + index + " ending");
return p2Rt;
}
// future2
static String process2(int index) throws InterruptedException, ExecutionException {
System.out.println(Thread.currentThread().getName() + " process2 index :" + index + " staring");
// 加上就会死锁
// 只要不一下子产生足够数量的task(把core全部占掉)就不会死锁 加了这里就会把core全部占据 导致task进入到queue,core线程在wait future.get 无法被释放, 而queue的任务在等待它释放产生新的线程
Future<String> submit = buExecutor.submit(() -> {
try {
Thread.sleep(1000);
return String.valueOf(index);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
submit.get();
System.out.println(Thread.currentThread().getName() + " process2 index :" + index + " ending");
return String.valueOf(index);
}
}
用visualvm分析线程dump,很难直接发现有异常,异步的很难检测,排查起来比较复杂,只看到是在wait
用jstack没有发现deadlock
在实际项目中我也看到过一个项目中共用一个线程池,线程池被封装成一个util方法,要执行异步的都用它,这个场景尤其要注意这个场景,也建议大家用带有超时的方式 Future.get(xxxx)


mysql-DuplicateUpdate和java的threadpool的"死锁"的更多相关文章
- mysql 行级锁的使用以及死锁的预防
一.前言 mysql的InnoDB,支持事务和行级锁,可以使用行锁来处理用户提现等业务.使用mysql锁的时候有时候会出现死锁,要做好死锁的预防. 二.MySQL行级锁 行级锁又分共享锁和排他锁. 共 ...
- Java多线程中的死锁问题
Java程序基本都要涉及到多线程,而在多线程环境中不可避免的要遇到线程死锁的问题.Java不像数据库那么能够检测到死锁,然后进行处理,Java中的死锁问题,只能通过程序员自己写代码时避免引入死锁的可能 ...
- java中myeclipse连接mysql问题(java.lang.ClassNotFoundException: com.mysql.jdbc.Driver)
java中myeclipse连接mysql问题(java.lang.ClassNotFoundException: com.mysql.jdbc.Driver) 1.往项目中添加mysql-conne ...
- Linux配置mysql (centos配置java环境 mysql配置篇 总结四)
♣安装的几种方法和比较 ♣配置yum源 ♣安装mysql ♣启动mysql ♣修改密码 ♣导入.sql文件 ♣缓存设置 ♣允许远程登录(navicat) ♣配置编码为utf8 1.关于Linux系统 ...
- Mysql报错java.sql.SQLException:null,message from server:"Host '27,45,38,132' is not allowed to connect
Mysql报错java.sql.SQLException:null,message from server:"Host '27,45,38,132' is not allowed to co ...
- MYSQL类型与JAVA类型对应表
MYSQL类型与JAVA类型对应表: 类型名称 显示长度 数据库类型 JAVA类型 JDBC类型索引(int) VARCHAR L+N VARCHAR java.lang.String 12 CHAR ...
- MySQL学习(一)——Java连接MySql数据库
MySQL学习(一)——Java连接MySql数据库 API详解: 获得语句执行 String sql = "Insert into category(cid, cname) values( ...
- MYSQL之错误代码----mysql错误代码与JAVA实现
原文地址:MYSQL之错误代码----mysql错误代码与JAVA实现作者:戒定慧 his chapter lists the errors that may appear when you call ...
- java多线程之 ---- 线程死锁
java多线程之线程死锁 产生死锁的主要原因: 由于系统资源不足. 进程执行推进的顺序不合适. 资源分配不当等. 假设系统资源充足.进程的资源请求都可以得到满足,死锁出现的可能性就非常低.否则就会因争 ...
- Java多线程——线程的死锁
Java多线程——线程的死锁 摘要:本文主要介绍了Java多线程中遇到的死锁问题. 部分内容来自以下博客: https://www.cnblogs.com/wy697495/p/9757982.htm ...
随机推荐
- 【项目实战】kaggle产品分类挑战
多分类特征的学习 这里还是b站刘二大人的视频课代码,视频链接:https://www.bilibili.com/video/BV1Y7411d7Ys?p=9 相关注释已经标明了(就当是笔记),因此在这 ...
- Elasticsearch: Cerebro 用户界面介绍
- 【前端必会】webpack的目标代码
背景 webpack生成什么样的代码呢?同的模块依赖的写法(import.export export default),会导致生成代码的不同,下面介绍普通的import与export 开始 导出PI1 ...
- C#-13 泛型
一 泛型 泛型提供了一种更优雅的方式,可以让多个类型共享一组代码.泛型允许我们声明类型参数化的代码,可以用不同的类型进行实例化. 也就是说,我们可以用"类型占位符"来写代码,然后在 ...
- 分布式存储系统之Ceph集群存储池、PG 与 CRUSH
前文我们了解了ceph集群状态获取常用命令以及通过ceph daemon.ceph tell动态配置ceph组件.ceph.conf配置文件相关格式的说明等,回顾请参考https://www.cnbl ...
- POJ2686 Traveling by Stagecoach (状压DP)
将车票的使用情况用二进制表示状态,对其进行转移即可. 但是我一开始写的代码是错误的(注释部分),看似思路是正确的,但是暗藏很大的问题. 枚举S,我们要求解的是dp[S][v],这个是从u转移过来的,不 ...
- Java一次返回中国所有省市区三级树形级联+前端vue展示【200ms内】
一.前言 中国省市区还是不少的,省有34个,市有391个,区有1101个,这是以小编的库里的,可能不是最新的,但是个数也差不了多少. 当一次返回所有的数据,并且还要组装成一个三级树,一般的for,会循 ...
- 『现学现忘』Git后悔药 — 32、revert撤销(一)
目录 1.Git的三种后悔药 2.revert命令原理 3.revert命令的使用 (1)移除某次提交的修改 (2)revert命令说明 1.Git的三种后悔药 在Git中后悔药有三种:amend.r ...
- 驱动开发:内核枚举IoTimer定时器
今天继续分享内核枚举系列知识,这次我们来学习如何通过代码的方式枚举内核IoTimer定时器,内核定时器其实就是在内核中实现的时钟,该定时器的枚举非常简单,因为在IoInitializeTimer初始化 ...
- 腾讯云短信SDK-精简版
/** * 腾讯云短信SDK-精简版 * 本模块使用-向腾讯云短信服务器发送请求 * @return json 腾讯服务器返回值-json字符串 */ private function send_sm ...