大家千万不要被文章的标题给迷惑了,他两在本篇文章是没有关系的, 今天给大家讲讲最近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相关的部分如下

image
  • hangfire server调度依赖storage
  • storage抽象出来一层api(解耦)
  • 第三方扩展(不关心具体的storage实现)
  • 不同的storage具体实现(比如mysql,sqlserver等)

Hangfire.Httpjob其实只是依赖了storage api那一层,也没有能力去直接写sql去执行, 只能用api去操作hangfire的那几张表(比如set表)

那么问题肯定不是在扩展层,而是得去看看mysqlstorage的实现源码,针对set表的处理逻辑

https://github.com/arnoldasgudas/Hangfire.MySqlStorage/blob/0bd1016f715c8c6617ce22fb7b2ce5b6c328d2fb/Hangfire.MySql/MySqlWriteOnlyTransaction.cs#L155


  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的执行次数成正比

根据朋友提供的截图

image

虽说是会跳跃,但是这个增长也太夸张了

打上断点调试发现

是hangfire server 不断的在调用,目的是把下一次执行时间(秒级别的时间戳)写到set表中

image

image

image

打上日志可以看到有非常多相同值的调用,这仅仅是一个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自增一切正常:

image

java的threadpool使用不当会造成“死锁”问题

image

这个原因先说出来: 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

image

用jstack没有发现deadlock

image

在实际项目中我也看到过一个项目中共用一个线程池,线程池被封装成一个util方法,要执行异步的都用它,这个场景尤其要注意这个场景,也建议大家用带有超时的方式 Future.get(xxxx)

mysql-DuplicateUpdate和java的threadpool的"死锁"的更多相关文章

  1. mysql 行级锁的使用以及死锁的预防

    一.前言 mysql的InnoDB,支持事务和行级锁,可以使用行锁来处理用户提现等业务.使用mysql锁的时候有时候会出现死锁,要做好死锁的预防. 二.MySQL行级锁 行级锁又分共享锁和排他锁. 共 ...

  2. Java多线程中的死锁问题

    Java程序基本都要涉及到多线程,而在多线程环境中不可避免的要遇到线程死锁的问题.Java不像数据库那么能够检测到死锁,然后进行处理,Java中的死锁问题,只能通过程序员自己写代码时避免引入死锁的可能 ...

  3. java中myeclipse连接mysql问题(java.lang.ClassNotFoundException: com.mysql.jdbc.Driver)

    java中myeclipse连接mysql问题(java.lang.ClassNotFoundException: com.mysql.jdbc.Driver) 1.往项目中添加mysql-conne ...

  4. Linux配置mysql (centos配置java环境 mysql配置篇 总结四)

    ♣安装的几种方法和比较 ♣配置yum源 ♣安装mysql ♣启动mysql ♣修改密码 ♣导入.sql文件 ♣缓存设置 ♣允许远程登录(navicat) ♣配置编码为utf8  1.关于Linux系统 ...

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

  6. MYSQL类型与JAVA类型对应表

    MYSQL类型与JAVA类型对应表: 类型名称 显示长度 数据库类型 JAVA类型 JDBC类型索引(int) VARCHAR L+N VARCHAR java.lang.String 12 CHAR ...

  7. MySQL学习(一)——Java连接MySql数据库

    MySQL学习(一)——Java连接MySql数据库 API详解: 获得语句执行 String sql = "Insert into category(cid, cname) values( ...

  8. MYSQL之错误代码----mysql错误代码与JAVA实现

    原文地址:MYSQL之错误代码----mysql错误代码与JAVA实现作者:戒定慧 his chapter lists the errors that may appear when you call ...

  9. java多线程之 ---- 线程死锁

    java多线程之线程死锁 产生死锁的主要原因: 由于系统资源不足. 进程执行推进的顺序不合适. 资源分配不当等. 假设系统资源充足.进程的资源请求都可以得到满足,死锁出现的可能性就非常低.否则就会因争 ...

  10. Java多线程——线程的死锁

    Java多线程——线程的死锁 摘要:本文主要介绍了Java多线程中遇到的死锁问题. 部分内容来自以下博客: https://www.cnblogs.com/wy697495/p/9757982.htm ...

随机推荐

  1. 永恒之蓝(MS17-010)漏洞复现

    1. 漏洞介绍 永恒之蓝: 恒之蓝是指2017年4月14日晚,黑客团体Shadow Brokers(影子经纪人)公布一大批网络攻击工具,其中包含"永恒之蓝"工具,"永恒之 ...

  2. shell分割字符串并赋值给变量

    假如变量var的值为:num=12,也即var="num=12",现在想把 12赋值给变量id awk 的-F 后跟上要分割字符串时的指定分隔符 awk中$0是要分割的字符串,$1 ...

  3. 授予用户/用户组访问 Kubernetes 的一个名称空间

    转载地址:https://www.kuboard.cn/learning/k8s-advanced/sec/rbac/auth-namespace.html 前提条件 已安装 Kuboard v3,版 ...

  4. Elasticsearch方案选型必须了解的10件事!

    文章转载自: https://mp.weixin.qq.com/s?__biz=MzI2NDY1MTA3OQ==&mid=2247484372&idx=1&sn=e863e46 ...

  5. 监控Redis集群--废弃,使用新教程

    prometheus监控redis需要用到redis_exporter. redis_exporter 项目地址:https://github.com/oliver006/redis_exporter ...

  6. 内网横向渗透 之 ATT&CK系列一 之 横向渗透域主机

    前言 上一篇文章中已获取了关于域的一些基本信息,在这里再整理一下,不知道信息收集的小伙伴可以看回上一篇文章哦 域:god.org 域控 windows server 2008:OWA,192.168. ...

  7. (编程语言界的丐帮 C#).NET MD5 HASH 哈希 加密 与JAVA 互通

    一.注意要点 1:输入字符串的的编码双方保持统一,如:UTF8: 2:HASH计算输出结果 byte[] 数组转String 时,编码要统一,如:转16进制小写字符串.当然也可以转Base64. 3: ...

  8. ​打造企业自己代码规范IDEA插件(中)

    一些基本概念 在开始独立研发公司自己的代码规范检查规则之前,先介绍一些相关的基本概念.阿里巴巴代码规范很多规则其实都是基于开源框架PMD进行的研发.PMD用官方的话语介绍来说:PMD是一个源代码分析器 ...

  9. POJ3260 The Fewest Coins(混合背包)

    支付对应的是多重背包问题,找零对应完全背包问题. 难点在于找上限T+maxv*maxv,可以用鸽笼原理证明,实在想不到就开一个尽量大的数组. 1 #include <map> 2 #inc ...

  10. 后端框架的学习----mybatis框架(6、日志)

    六.日志 如果一个数据库操作,出现了异常,我们需要排错,日志就是最好的帮手 setting设置 <settings> <setting name="logImpl" ...