0%

用systemctl使zookeeper开机启动,网上看了好多章,没一个好使,自己琢磨折腾了一个下午总算把它搞定。
直接上脚本:/etc/systemd/system/zookeeper.service

[Unit]
Description=zookeeper.service
After=network.target
[Service]
Type=forking
Environment=ZOO_LOG_DIR=/usr/local/zookeeper-3.4.11/
Environment=PATH=/usr/local/jdk1.8.0_152/bin:/usr/local/jdk1.8.0_152/jre/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/root/bin
ExecStart=/usr/local/zookeeper-3.4.11/bin/zkServer.sh start
ExecStop=/usr/local/zookeeper-3.4.11/bin/zkServer.sh stop
ExecReload=/usr/local/zookeeper-3.4.11/bin/zkServer.sh restart
PIDFile=/tmp/zookeeper/zookeeper_server.pid
User=www
[Install]
WantedBy=multi-user.target

重点在两行Environment。

  • 第一行设置日志目录,如果没有设置,默认是当前目录,对www用户来说,可能没有权限。
  • 第二行是配置环境变量,systemd用户实例不会继承类似.bashrc中定义的环境变量,所以是找不到jdk目录的,而zookeeper又必须有。

保存后,reload

systemctl daemon-reload

启用开机自启

systemctl enable zookeeper

启动服务

systemctl start zookeeper

Springboot项目可以直接通过

java -jar xxx.jar

命令启动,但是开启关闭还是有个脚本会比较方便。自己写了个shell脚本,功能是查找当前目录下的jar文件,然后启动,也可以关闭,重启。经过几次修改,个人觉得比较完美了,放出源码:

#! /bin/bash
# springboot的jar放同级目录下即可,只能有一个jar文件
export PATH=$JAVA_HOME/bin:$PATH
CURRENT_PATH=$(cd "$(dirname "$0")"; pwd)
JAR=$(find $CURRENT_PATH -maxdepth 1 -name "*.jar")
PID=$(ps -ef | grep $JAR | grep -v grep | awk '{ print $2 }')

case "$1" in
    start)
        if [ ! -z "$PID" ]; then
            echo "$JAR 已经启动,进程号: $PID"
        else
            echo -n -e "启动 $JAR ... \n"
            cd $CURRENT_PATH
        nohup java -jar $JAR >/dev/null 2>&1 &
            if [ "$?"="0" ]; then
                echo "启动完成,请查看日志确保成功"
            else
                echo "启动失败"
            fi
        fi
        ;;
    stop)
        if [ -z "$PID" ]; then
            echo "$JAR 没有在运行,无需关闭"
        else
            echo "关闭 $JAR ..."
              kill -9 $PID
            if [ "$?"="0" ]; then
                echo "服务已关闭"
            else
                echo "服务关闭失败"
            fi
        fi
        ;;
    restart)
        ${0} stop
        ${0} start
        ;;
    kill)
        echo "强制关闭 $JAR"
        killall $JAR
        if [ "$?"="0" ]; then
            echo "成功"
        else
            echo "失败"
        fi
        ;;
    status)
        if [ ! -z "$PID" ]; then
            echo "$JAR 正在运行"
        else
            echo "$JAR 未在运行"
        fi
        ;;
  *)
    echo "Usage: ./springboot {start|stop|restart|status|kill}" >&2
        exit 1
esac

脚本会找到当前目录下的jar文件,所以,只能放一个jar文件。 开机启动脚本:

[Unit]
Description=Dubbo-admin
Wants=network.target
[Service]
Environment=JAVA_HOME=/usr/local/jdk1.8.0_171
Type=forking
ExecStart=/usr/local/xxx/springboot start
ExecStop=/usr/local/xxx/springboot stop
[Install]
WantedBy=multi-user.target

Logstash是一个数据收集管道,配置输入输出,可将数据从一个地方传到另一个地方。同步mysql到Elasticsearch,这里的输入,指的是mysql,输出就是Elasticsearch。 新版本的Logstash和Elasticsearch跟之前老版本的有些不同,所以我也是自己折腾了小半天,总算成功。

下面的操作,Logstash和Elasticsearch的版本都是6.0.1


  1. 下载安装 Logstash和Elasticsearch就不介绍了,下载后解压即可使用。 默认Logstash不包含读取数据库的jdbc插件,需要手动下载。进入Logstash的bin目录,执行:

    ./logstash-plugin install logstash-input-jdbc
    

    因为网络原因,安装可能费点时间。

  2. 在某处建一个目录,放置配置文件(哪不重要,因为执行时会配置路径),我这放到bin下的mysql目录里。

  3. 复制mysql jdbc驱动(mysql-connector-java-5.1.35.jar)到该目录下

  4. 编写导出数据的sql,放到sql.sql(文件名自己取)里,内容类似这样:

    select *, id as car_id 
    from violation_car car 
    where car.gmt_modified>= :sql_last_value
    

    这里需要介绍下,我的表里有个gmt_modified字段,用于记录该条记录的最后修改时间,sql_last_value是Logstash查询后,保存的上次查询时间,第一次查询时,该值是1970/1/1,所以第一次导入,如果你的表现有数据很多,可能会有点问题,后面会根据最后修改时间,更新修改过的数据。

  5. 编写输入输出配置文件jdbc.conf

    input {
      jdbc {
        jdbc_driver_library => "/xxx/xxx/logstash-6.0.1/bin/mysql/mysql-connector-java-5.1.35.jar"
        jdbc_driver_class => "com.mysql.jdbc.Driver"
        jdbc_connection_string => "jdbc:mysql://localhost:3306/violation"
        jdbc_user => "root"
        jdbc_password => ""
        schedule => "* * * * *"
        jdbc_default_timezone => "Asia/Shanghai"
        statement_filepath => "/xxx/xxx/logstash-6.0.1/bin/mysql/sql.sql"
        use_column_value  => false
        last_run_metadata_path => "/xxx/xxx/logstash-6.0.1/bin/mysql/last_run.txt"
      }
    }
    output {
        elasticsearch {
            hosts => ["127.0.0.1:9200"]
            index => "violation"
            document_id => "%{car_id}"
            document_type => "car"
        }
        stdout {
            codec => json_lines
        }
    }
    

    字段介绍:

    input.jdbc.jdbc_driver_library  jdbc驱动的位置
    input.jdbc.jdbc_driver_class    驱动类名
    input.jdbc.jdbc_connection_string   数据库连接字符串
    input.jdbc.jdbc_user    用户名
    input.jdbc.jdbc_password  密码
    input.jdbc.schedule   更新计划(参考linux crontab)
    input.jdbc.jdbc_default_timezone   时区,默认没有时区,日志里时间差8小时,中国需要用Asia/Shanghai
    input.jdbc.statement_filepath 导出数据的sql文件,就是上面写的
    input.jdbc.use_column_value  如果是true,sql_last_value是tracking_column指定字段的数字值,false就是时间,默认是false
    input.jdbc.last_run_metadata_path  保存sql_last_value值文件的位置
    output.elasticsearch.hosts elasticsearch服务器,填多个,请求会负载均衡。
    output.elasticsearch.index 索引名
    output.elasticsearch.document_id   生成文件的id,这里使用sql产生的car_id
    output.elasticsearch.document_type  文档类型 
    output.stdout 配置的是命令行输出,使用json
    
  6. 配置完以后,启动

    ./logstash -f mysql/jdbc.conf
    

    按上面的配置,logstash会每分钟查询一次表,看是否会更新,有更新则提交到Elasticsearch

之所以要记录是因为网上找了好久,试了好多方法,基本上都不好使. 签名sign做了base64

        BASE64Decoder base64decoder = new BASE64Decoder();
    try {
        //读取pem证书
        BufferedReader br = new BufferedReader(new FileReader("xxx.pem"));
        String s = br.readLine();
        StringBuffer publickey = new StringBuffer();
        while (s.charAt(0) != '-') {
            publickey.append(s + "\r");
            s = br.readLine();
        }
        byte[] keybyte = base64decoder.decodeBuffer(publickey.toString());
        KeyFactory kf = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keybyte);
        PublicKey publicKey = kf.generatePublic(keySpec);
        BASE64Encoder bse=new BASE64Encoder();
        System.out.println("pk:"+bse.encode(publicKey.getEncoded()));

        //被签的原文
        String toSign="sxxxsasdsss";
        //生成的签名
        String sign="xxxxx";

        Signature signature = Signature.getInstance("SHA1withRSA");
        signature.initVerify(publicKey);
        signature.update(toSign.getBytes());
        boolean verify = signature.verify(base64decoder
            .decodeBuffer(
                sign));
        System.out.println(verify);
    } catch (Exception e) {
        e.printStackTrace();
    }

如果我们用SpringBoot实现一个简单的微服务,不需要数据库,你会发现在写完代码启动时会报 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties' 之类的错误。这是因为SpringBoot默认会自动配置数据库,如果业务不需要,就要手动禁用数据库自动配置,在Application的SpringBootApplication注解里加上 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, HibernateJpaAutoConfiguration.class})

之前写过一篇搭建Spring boot + mybatis + freemarker项目,Spring boot默认的数据库连接池是tomcat-jdbc,今天我们要用druid替换它。

什么是druid

druid是阿里巴巴开源的数据库连接池,自称是Java语言中最好的数据库连接池,提供强大的监控和扩展功能。

为什么用druid

  1. 性能

    官方数据Benchmark_aliyun,druid在响应时间上优于其他几个线程池。非官方的测试数据可能差距没这么明显,但仍然高于其他几个线程池。

  2. 自带监控功能

    自带监控,可帮助开发者找出慢查询,查看并发数等。

配置步骤:

  1. 基于干净的spring boot web项目,添加mysql、mybatis、druid库:

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.31</version>
    </dependency>
    
  2. 配置数据库连接

    application.properties:

    spring.datasource.driverClassName=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8
    spring.datasource.username=root
    spring.datasource.password=
    

    这里key叫什么其实并不重要,因为后面配置数据源,我们会自己读取配置。

  3. Mybatis、druid配置
    新建MyBatisConfig.java:

    package com.pocketdigi.config.database;
    
    import com.alibaba.druid.pool.DruidDataSourceFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    import org.springframework.core.io.support.ResourcePatternResolver;
    
    import javax.sql.DataSource;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    @Configuration
    @MapperScan("com.pocketdigi.dal.mapper")
    public class MyBatisConfig {
        @Value("${spring.datasource.url}")
        String dbUrl;
        @Value("${spring.datasource.username}")
        String userName;
        @Value("${spring.datasource.password}")
        String password;
    
        @Value("${spring.datasource.driverClassName}")
        String driverClassName;
        private static String MYBATIS_CONFIG = "mybatis_config.xml";
        private static String MAPPER_PATH = "/mapper/*.xml";
    
        @Bean
        public SqlSessionFactoryBean createSqlSessionFactoryBean() throws IOException {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            //设置mybatis configuration 扫描路径
            sqlSessionFactoryBean.setConfigLocation(new ClassPathResource(MYBATIS_CONFIG));
            //添加mapper 扫描路径
            PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
            String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + MAPPER_PATH;
            sqlSessionFactoryBean.setMapperLocations(pathMatchingResourcePatternResolver.getResources(packageSearchPath));
            //设置datasource
            sqlSessionFactoryBean.setDataSource(dataSource());
            return sqlSessionFactoryBean;
        }
    
        private DataSource dataSource() {
            Map<String,Object> properties=new HashMap<>();
            properties.put(DruidDataSourceFactory.PROP_DRIVERCLASSNAME,driverClassName);
            properties.put(DruidDataSourceFactory.PROP_URL,dbUrl);
            properties.put(DruidDataSourceFactory.PROP_USERNAME,userName);
            properties.put(DruidDataSourceFactory.PROP_PASSWORD,password);
            //添加统计、SQL注入、日志过滤器
            properties.put(DruidDataSourceFactory.PROP_FILTERS,"stat,wall,log4j2");
            //sql合并,慢查询定义为5s
            properties.put(DruidDataSourceFactory.PROP_CONNECTIONPROPERTIES,"druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000");
            try {
                return DruidDataSourceFactory.createDataSource(properties);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
    }
    

    mybatis的mapper接口在com.pocketdigi.dal.mapper,mybatis_config.xml在resources目录下,xml mapper文件在resources/mapper/目录下。

    mybatis_config.xml:

    <?xml version="1.0" encoding="utf-8" ?>
    <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <settings>
            <!-- 所有映射器中配置的缓存的全局开关-->
            <setting name="cacheEnabled" value="false"/>
            <!-- 延迟加载的全局开关 配置二级缓存时将此属性关闭-->
            <setting name="lazyLoadingEnabled" value="false"/>
            <!-- 关联对象加载 配置二级缓存时将此属性关闭-->
            <setting name="aggressiveLazyLoading" value="false"/>
            <!-- 是否允许单一语句返回多结果集-->
            <setting name="multipleResultSetsEnabled" value="true"/>
            <!-- 使用列标签代替列名-->
            <setting name="useColumnLabel" value="true"/>
            <!-- 允许 JDBC 支持自动生成主键,需要驱动兼容 -->
            <setting name="useGeneratedKeys" value="false"/>
            <!-- 指定 MyBatis 是否以及如何自动映射指定的列到字段或属性-->
            <setting name="autoMappingBehavior" value="PARTIAL"/>
            <!-- 配置默认的执行器-->
            <setting name="defaultExecutorType" value="SIMPLE"/>
            <!-- 设置超时时间,它决定驱动等待数据库响应的秒数-->
            <setting name="defaultStatementTimeout" value="30"/>
            <!-- 允许在嵌套语句中使用行分界 -->
            <setting name="safeRowBoundsEnabled" value="false"/>
            <!-- 是否开启自动驼峰命名规则映射 -->
            <setting name="mapUnderscoreToCamelCase" value="false"/>
            <!-- 利用本地缓存机制防止循环引用和加速重复嵌套查询 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询-->
            <setting name="localCacheScope" value="SESSION"/>
            <!-- 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型 -->
            <setting name="jdbcTypeForNull" value="OTHER"/>
            <!-- 指定哪些对象的方法触发一次延迟加载-->
            <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
        </settings>
    
        <typeAliases>
            <package name="com.pocketdigi.dal.po"/>
        </typeAliases>
    </configuration>
    

    数据库对应的POJO类在com.pocketdigi.dal.po

  4. druid监控后台配置

    druid监控后台需要配置一个Filter和一个Servlet,将指定的路径转发到com.alibaba.druid.support.http.StatViewServlet

    ServletConfiguration.java:

    @Configuration
    public class ServletConfiguration {
        @Bean
        public ServletRegistrationBean druidStatViewServletBean() {
            //后台的路径
            ServletRegistrationBean statViewServletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
            Map<String,String> params = new HashMap<>();
            //账号密码,是否允许重置数据
            params.put("loginUsername","admin");
            params.put("loginPassword","admin");
            params.put("resetEnable","true");
            statViewServletRegistrationBean.setInitParameters(params);
            return statViewServletRegistrationBean;
        }
    }
    

    FilterConfiguration.java

    @Configuration
    public class FilterConfiguration {
        @Bean
        public FilterRegistrationBean druidStatFilterBean() {
            FilterRegistrationBean druidStatFilterBean=new FilterRegistrationBean(new WebStatFilter());
            List<String> urlPattern=new ArrayList<>();
            urlPattern.add("/*");
            druidStatFilterBean.setUrlPatterns(urlPattern);
            Map<String,String> initParams=new HashMap<>();
            initParams.put("exclusions","*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*");
            druidStatFilterBean.setInitParameters(initParams);
            return druidStatFilterBean;
        }
    }
    

    配置完成,启动应用,打开http://localhost:8000/druid/ 用ServletConfiguration.java配置的账号密码登录,即可进入druid监控后台。

代码已传github https://github.com/pocketdigi/springboot-demo

使用Spring开发web程序,在大部分情况下,都是通过Spring默认的DispatcherServlet,转发请求到Controller,我们在Controller里处理请求。但有时候,可能有些请求我们不希望通过Spring,而是通过其他Servlet处理。如果是普通的Spring项目,注册Filter和Servlet只需在web.xml时添加filter、filter-mapping、servlet、servlet-mapping,但Spring boot项目里没有web.xml。 Spring boot有两种方法注册Servlet和Filter:代码注册、注解注册。 先写个简单的Servlet:

@Slf4j
public class TestServlet extends HttpServlet{
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        String value=config.getInitParameter("param1");
        log.info("param1:{}",value);

    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter out = resp.getWriter();
        out.println("<html>");
        out.println("<head>");
        out.println("<title>Hello World</title>");
        out.println("</head>");
        out.println("<body>");
        out.println("Hello");
        out.println("</body>");
        out.println("</html>");
    }
}

代码注册:

代码注册通过ServletRegistrationBean和FilterRegistrationBean两个类,注册Servlet和Filter。 在Application类里添加注册代码:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
    @Bean
    public ServletRegistrationBean testServletBean() {
        ServletRegistrationBean testServletRegistration = new ServletRegistrationBean(new TestServlet(), "/test1/");
        Map<String,String> params = new HashMap<>();
        params.put("param1","value2");
        testServletRegistration.setInitParameters(params);
        return testServletRegistration;

    }

}

三个参数,servlet实例、url mapping、init-param, Filter也类似,只是用FilterRegistrationBean。 或者,放到一个单独的配置类里:

@Configuration
public class ServletConfiguration {
    @Bean
    public ServletRegistrationBean testServletBean() {
        ServletRegistrationBean testServletRegistration = new ServletRegistrationBean(new TestServlet(), "/test1/");
        Map<String,String> params = new HashMap<>();
        params.put("param1","value2");
        testServletRegistration.setInitParameters(params);
        return testServletRegistration;
    }
}

建议拆开放到单独的类里,如果所有配置都放Application里,乱。

注解注册

注解注册需要在Servlet类上加上@WebServlet,@WebFilter注解,Application里加上@ServletComponentScan开启Servlet注解扫描。 修改TestServlet

@Slf4j
@WebServlet(urlPatterns = "/test/*",initParams = {
        @WebInitParam(name = "param1", value = "value1"),
})
public class TestServlet extends HttpServlet{
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        String value=config.getInitParameter("param1");
        log.info("param1:{}",value);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter out = resp.getWriter();
        out.println("<html>");
        out.println("<head>");
        out.println("<title>Hello World</title>");
        out.println("</head>");
        out.println("<body>");
        out.println("Hello");
        out.println("</body>");
        out.println("</html>");
    }
}

如果是Filter:

@WebFilter(filterName = "webFilter", urlPatterns = "/*",
        initParams = {@WebInitParam(name = "exclusions", value = "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico")
})
public class WebFilter extends Filter {
}

Application:

@SpringBootApplication
@ServletComponentScan
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }   
}

怎么选择?

如果是自己实现的Servlet,那么直接加个注解,即可注册。但Servlet不是自己实现的话,用代码注册比较好,否则需要继承原来的Servlet加一个子类。

jstack是jdk中自带的用于查看进程内线程栈的工具。当程序出现死锁时,我们可以通过jstack打印线程栈找到问题。

找出代码中的死锁

学习从一段简单的代码开始:

public class Main {
    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        final Object lock1 = new Object();
        final Object lock2 = new Object();
        Thread thread=new Thread(new Runnable() {
            public void run() {
                synchronized (lock1) {
                    try {
                        for(int i=0;i<20;i++) {
                            Thread.sleep(1000);
                            System.out.println(Thread.currentThread().getName()+" "+i);
                        }
                        synchronized (lock2) {
                            for(int i=20;i<40;i++) {
                                Thread.sleep(1000);
                                System.out.println(Thread.currentThread().getName()+" "+i);
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.start();
        synchronized (lock2) {
            try {
                for(int i=0;i<20;i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+" "+i);
                }
                synchronized (lock1) {
                    for(int i=20;i<40;i++) {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName()+" "+i);
                    }
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

肉眼都能看出来的死锁,程序运行20s后进入死锁状态。
首先我们先找出这个程序的进程id,建议用jps,而不是ps,原因是jps只会显示当前用户下面的java进程,等于自动帮我们过滤了。

jps -l

Main进程,id 5632,这时出动我们的主角jstack.

jstack 5632

结果如下

2017-05-23 13:57:22
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.45-b02 mixed mode):

"Attach Listener" #12 daemon prio=9 os_prio=31 tid=0x00007ff0f780c800 nid=0x4507 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Thread-0" #11 prio=5 os_prio=31 tid=0x00007ff0f80c0000 nid=0x5a03 waiting for monitor entry [0x0000700002310000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at Main$1.run(Main.java:33)
    - waiting to lock <0x000000076adb1a98> (a java.lang.Object)
    - locked <0x000000076adb1a88> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:745)

"Service Thread" #10 daemon prio=9 os_prio=31 tid=0x00007ff0f980b800 nid=0x5603 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #9 daemon prio=9 os_prio=31 tid=0x00007ff0f6803000 nid=0x5403 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #8 daemon prio=9 os_prio=31 tid=0x00007ff0f6802800 nid=0x5203 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #7 daemon prio=9 os_prio=31 tid=0x00007ff0f6801800 nid=0x5003 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #6 daemon prio=9 os_prio=31 tid=0x00007ff0f90de800 nid=0x4e03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #5 daemon prio=5 os_prio=31 tid=0x00007ff0f90dc800 nid=0x4c03 runnable [0x0000700001bfb000]
   java.lang.Thread.State: RUNNABLE
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
    at java.net.SocketInputStream.read(SocketInputStream.java:170)
    at java.net.SocketInputStream.read(SocketInputStream.java:141)
    at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
    at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
    at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
    - locked <0x000000076af12b68> (a java.io.InputStreamReader)
    at java.io.InputStreamReader.read(InputStreamReader.java:184)
    at java.io.BufferedReader.fill(BufferedReader.java:161)
    at java.io.BufferedReader.readLine(BufferedReader.java:324)
    - locked <0x000000076af12b68> (a java.io.InputStreamReader)
    at java.io.BufferedReader.readLine(BufferedReader.java:389)
    at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007ff0f8039800 nid=0x4a03 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007ff0f9803000 nid=0x3903 in Object.wait() [0x0000700001972000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x000000076ab06f58> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
    - locked <0x000000076ab06f58> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
    at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007ff0f9802000 nid=0x3703 in Object.wait() [0x000070000186f000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x000000076ab06998> (a java.lang.ref.Reference$Lock)
    at java.lang.Object.wait(Object.java:502)
    at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157)
    - locked <0x000000076ab06998> (a java.lang.ref.Reference$Lock)

"main" #1 prio=5 os_prio=31 tid=0x00007ff0f8002000 nid=0x1c03 waiting for monitor entry [0x0000700000e51000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at Main.test1(Main.java:52)
    - waiting to lock <0x000000076adb1a88> (a java.lang.Object)
    - locked <0x000000076adb1a98> (a java.lang.Object)
    at Main.main(Main.java:18)

"VM Thread" os_prio=31 tid=0x00007ff0f801f800 nid=0x3503 runnable

"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007ff0f9003800 nid=0x2503 runnable

"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007ff0f9004000 nid=0x2703 runnable

"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007ff0f7801000 nid=0x2903 runnable

"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007ff0f600e800 nid=0x2b03 runnable

"GC task thread#4 (ParallelGC)" os_prio=31 tid=0x00007ff0f600f000 nid=0x2d03 runnable

"GC task thread#5 (ParallelGC)" os_prio=31 tid=0x00007ff0f6010000 nid=0x2f03 runnable

"GC task thread#6 (ParallelGC)" os_prio=31 tid=0x00007ff0f6010800 nid=0x3103 runnable

"GC task thread#7 (ParallelGC)" os_prio=31 tid=0x00007ff0f6011000 nid=0x3303 runnable

"VM Periodic Task Thread" os_prio=31 tid=0x00007ff0f980c000 nid=0x5803 waiting on condition

JNI global references: 26


Found one Java-level deadlock:
=============================
"Thread-0":
  waiting to lock monitor 0x00007ff0f8024f38 (object 0x000000076adb1a98, a java.lang.Object),
  which is held by "main"
"main":
  waiting to lock monitor 0x00007ff0f8023bf8 (object 0x000000076adb1a88, a java.lang.Object),
  which is held by "Thread-0"

Java stack information for the threads listed above:
===================================================
"Thread-0":
    at Main$1.run(Main.java:33)
    - waiting to lock <0x000000076adb1a98> (a java.lang.Object)
    - locked <0x000000076adb1a88> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:745)
"main":
    at Main.test1(Main.java:52)
    - waiting to lock <0x000000076adb1a88> (a java.lang.Object)
    - locked <0x000000076adb1a98> (a java.lang.Object)
    at Main.main(Main.java:18)

Found 1 deadlock.

jstack直接帮我们找出来了deadlock。真实场景下,如果代码比较复杂,可能需要我们自己分析找出死锁。
如果我们自己分析,该怎么找出死锁呢?重点分析java.lang.Thread.State: BLOCKED的进程,找到
waiting to lock,看看这个锁的持有者,是不是也被锁着,沿着这个链路找下去,是不是死锁就能找出来 了。比如上面的例子中,main线程持有0x000000076adb1a98锁,等待0x000000076adb1a88锁释放,而Thread-0持有0x000000076adb1a88锁,等待0x000000076adb1a98锁释放,两个线程互相等待,而进入死锁状态。

找出CPU消耗多的代码

如果程序cpu占用很高,我们需要找到问题优化,可以配合top命令,找出最耗cpu的进程,从而找到相应代码解决问题。

  1. 先用jps找出程序pid,这里是23034

  2. 用top命令找出该进程最耗cpu的线程。下面的top是linux中的,mac里的不一样。

    top -Hp 23034
    

    结果如下图

    23046线程占了93.8的cpu,就是它.

  3. 将23046转成16进制。因为top里的pid是10进制,而jstack里是16进制,叫nid。
    可以用printf命令转换

    printf "%x\n" 23046
    

    得到5a06

  4. jstack出场

    jstack 23034 | grep 5a06
    

    结果如下:

    writeFileThread占用了最多的cpu资源。找到后,可以优化代码了.

jstat是jdk自带的JVM内存统计工具,用于查看heap的内存和垃圾回收情况。

用法:

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

参数定义:

  • options:选项,通过jstat -options查看支持的选项,目前支持
    • -class
    • -compiler
    • -gc
    • -gccapacity
    • -gccause
    • -gcmetacapacity
    • -gcnew
    • -gcnewcapacity
    • -gcold
    • -gcoldcapacity
    • -gcutil
    • -printcompilation
  • lines: 使用interval参数,会在间隔指定时间后输出当前JVM内存的状态,这个参数是指定输出多少行后,再输出title,这样就不需要翻屏看这一列的title了。
  • vmid: 虚拟机的pid
  • interval:间隔多少时间后循环输出,不指定的话,就输出一次
  • count:指定输出多少次

实例:

class

jstat -class pid

输出如下:

显示的是加载的类的数量和占用的字节数、卸载的类的数量和字节数,以及加载卸载所花时间。

compiler

jstat -compiler pid

  • Compiled: 编译的数量
  • Failed: 编译失败的数量
  • Invalid: 编译失效的数量
  • Time: 编译任务消耗时间
  • FailedType: 最后一次编译失败类型
  • FailedMethod:最后一次编译失败的类和方法

complier展示的是编译的统计

gc

jstat -gc

  • S0C 年轻代第一个survivor区的容量
  • S1C 年轻代第二个survivor区的容量
  • S0U 年轻代第一个survivor区的已用空间(字节)
  • S1U 年轻代第一个survivor区的已用空间(字节)
  • EC 年轻代eden区的容量
  • EU 年轻代eden区的已用空间
  • OC 老年代的容量
  • OU 老年代的已用空间
  • MC 元空间(Metaspace)的容量
  • MU 元空间的已用空间
  • CCSC 压缩的类空间容量
  • CCSU 压缩的类空间已用空间
  • YGC 年轻代垃圾回收事件的数量
  • YGCT 年轻代垃圾回收时间
  • FGC Full GC事件数量
  • FGCT Full GC时间
  • GCT GC总时间

gc展示的是垃圾回收的统计,这个例子基于java 8,java 8之前是没有元空间(MetaSpace)概念的,而是永久代(PC,PU)

gccapacity

jstat -gccapacity pid

  • NGCMN 年轻代最小(初始化)大小
  • NGCMX 年轻代最大容量
  • NGC 年轻代当前大小
  • S0C 年轻代第一个survivor区容量
  • S1C 年轻代第二个survivor区容量
  • EC 年轻代Eden区容量
  • OGCMN 老年代初始化容量
  • OGCMX 老年代最大容量
  • OGC 老年代当前占用容量
  • OC 老年代空间容量
  • MCMN 元空间初始容量
  • MCMX 元空间最大容量
  • CCSMN 压缩类空间初始容量
  • CCSMX 压缩类空间最大容量
  • CCSC 当前压缩类空间容量
  • YGC 年轻代垃圾回收事件数量
  • FGC Full GC事件数量

gccapacity展示的是各个空间的初始容量、最大容量、以及当前容量

gcnew

  • S0C 年轻代第一个survivor区容量
  • S1C 年轻代第二个survivor区容量
  • S0U 年轻代第一个survivor区已用空间
  • S1U 年轻代第二个survivor区已用空间
  • TT 持有次数限制
  • MTT 最大持有次数限制(超过这个值放入old)
  • EC 年轻代的容量
  • EU 年轻代的已用空间
  • YGC 年轻代GC事件次数
  • YGCT 年轻代GC消耗的时间

gcnew展示的年轻代的信息

后面的-gcnewcapacity -gcold -gcoldcapacity -gcutil字段都类似,不再演示。-printcompilation展示的是编译相关的信息。

为什么要使用Maven私服?

某些jar包仅供内部使用,不适合在中央库发布。不用私服,团队间只能私底下传输打出来的jar包了,私服就是用来管理这些包。

Sonatype Nexus是什么?

Sonatype Nexus就是一个maven私服,也可以称做代理服务器。原理是代理中央库,同时管理私有库,在使用时,我们只要把maven的镜像地址配置成nexus地址即可。

Sonatype Nexus的安装

懒得折腾,所以直接用docker了。使用的是Sonatype Nexus Repository Manager 2镜像。当然,如果你有钻研学习精神,还是自己按官方教程一步一步来,否则建议直接用docker,一个字,快!

docker安装不再介绍

step1: 创建数据目录

Sonatype Nexus会有一些索引之类的数据需要放到外部目录,我们先创建它,使用docker都是这么个套路,需要把数据放到外部。假设这里的目录是/Users/xxx/docker/nexus-data

step2: 下载并启动Sonatype Nexus

docker run -d -p 8081:8081 --name nexus -v /Users/xxx/docker/nexus-data:/sonatype-work sonatype/nexus

docker会去下载相应的镜像,并运行,端口号8081,打开http://localhost:8081/nexus/确认成功

到此安装完成,默认账号密码admin/admin123

Sonatype Nexus 配置

此时如果使用搜索功能,会发现什么都找不到,因为现在还没有中央库的索引。

上面说过,Sonatype Nexus需要代理中央库,所有maven下载库的请求都会通过Nexus,如果Nexus上没有,去中央库下载,如果有,直接返回了。

更新中央库索引。
更新索索

如果你现在在国外,或者连着很快的vpn,那么,过一会,应该就可以更新完索引了,因为中央库服务器在国外,国内下载速度慢。

国内用户怎么办呢?在架这个nexus之前,大家应该都用阿里云的镜像吧?速度还算快,那么我们把默认的中央库地址换成阿里云的
替换地址
阿里云上还有个jcenter,也加上,再更新索引。

在服务器上执行需谨慎,我的服务器昨天被封,到目前为止还是访问不了阿里云maven镜像,服务器换IP不容易。

大概过了一两个小时,我这下完了,下完其实还要在本地建索引什么的,需要点时间,反正我这边监控网络已经没速度了,docker的cpu占用还是很高,cpu风扇狂转,这一步需要时间,请耐心。

索引建完以后(Administration/Scheduled Tasks是可以看到当前任务的),就可以搜索到中央库上的包了。

此时,Nexus已经可以使用了,当然,改密码,配权限,不用说了,自己改.

客户端配置settings.xml

settings.xml在~/.m2/下,编辑:

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

    <servers>
        <server>
            <id>nexus</id>
            <username>admin</username>
            <password>admin123</password>
        </server>
    </servers>

    <mirrors>
        <mirror>
            <id>Nexus</id>
            <mirrorOf>*</mirrorOf>
            <name>Nexus Repository</name>
            <url>http://localhost:8081/nexus/content/groups/public</url>
        </mirror>
    </mirrors>

    <profiles>
        <profile>
            <id>nexus</id>
            <!--Enable snapshots for the built in central repo to direct -->
            <!--all requests to nexus via the mirror -->
            <repositories>
                <repository>
                    <id>nexus</id>
                    <url>http://localhost:8081/nexus/content/content/groups/public</url>
                    <releases>
                        <enabled>true</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </repository>
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <id>nexus</id>
                    <url>http://localhost:8081/nexus/content/content/groups/public</url>
                    <releases>
                        <enabled>true</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </pluginRepository>
            </pluginRepositories>
        </profile>
    </profiles>

    <activeProfiles>
        <!--make the profile active all the time -->
        <activeProfile>nexus</activeProfile>
    </activeProfiles>
</settings>

账号密码和服务器地址改一下就可以了。

Maven项目配置

打开pom.xml,加入:

<distributionManagement>
        <snapshotRepository>
            <id>nexus</id>
            <name>nexus snapshots</name>
            <url>http://localhost:8081/nexus/content/repositories/snapshots
            </url>
        </snapshotRepository>
        <repository>
            <id>nexus</id>
            <name>nexus releases</name>
            <url>http://localhost:8081/nexus/content/repositories/releases
            </url>
        </repository>
    </distributionManagement>

发布项目到nexus

mvn package