互联网推Code https://www.tuicode.com架构师、开发工程师、运维工程师、产品经理、微服务、DevOps、Spring Boot、Spring Cloud、Docker、Kubernetes Wed, 01 Jul 2020 05:48:52 +0000 en-US hourly 1 https://wordpress.org/?v=4.7.18 https://www.tuicode.com/wp-content/uploads/2017/05/cropped-4805355-32x32.jpg 互联网推Code https://www.tuicode.com32 32 mysql数据库做流水号 https://www.tuicode.com/archives/2046https://www.tuicode.com/archives/2046#respond Wed, 01 Jul 2020 03:08:04 +0000 https://www.tuicode.com/?p=2046 需求

需要生成一个按某个种子生成流水号,这个种子目前是时间维度,比如2020年7月1日生成流水号从0开始一直向后递增1,无论2020年7月1日最终流水号生成多少,2020年7月2日生成流水号依然从0开始重新计数。

方案

1.考虑redis自增数据

虽然这是常用方案,不过我们为了简化架构,redis组件增加后只用作获取自增序列这一个功能,有点大材小用了,所以暂不考虑这个方案。

2.考虑数据库sequence序列

mysql没有sequence功能,这个确实有点坑。

3.使用数据库的auto_increment自增

drop table test;
CREATE TABLE test (
  id varchar(64) not null primary key,
  my_id INT AUTO_INCREMENT NOT NULL unique,
  name VARCHAR(10) NOT NULL
);
insert into test(id,my_id,`name`) values (UUID(),null,'abc')

这个方案确实可以,不过有个弊端,就是不支持种子,也就是当我切换name的时候,计数器并不会重新计数。

4.使用数据库last_insert_id方法

这个方法是基于方案3给出的方法,方案3是每次插入业务数据时,这条数据上新增一列作为自增列,插入后,我们可以使用last_insert_id获取到my_id这一列的值。

那么我们是否可以使用单独一张表来计数,这样是否可以使用种子了呢?答案是否定的。

例如方案3中给出的test表是独立的表,name只是作为种子存在,插入数据时拿到last_insert_id,就是上一次插入的my_id了,不过last_insert_id只能获取到insert数据,对于update的数据是拿不到的,所以如果单独一张表来记录计数,依然要用insert方法,又回到方案3的问题了,没有种子。

另外,学习了一个新的语法:

INSERT INTO test VALUES ('2020-06-20',null,'c4') ON DUPLICATE KEY UPDATE `name`=VALUES(`name`);

当插入提示name重复时,将插入改为更新,字段也会自增,只是无法使用last_insert_id获取最后一次更新值。

5.使用结合方式

使用单独计数表+function来实现,网上大部分给出的方案也是这个方法,不过写的比较乱,我在这里总结一下:

CREATE TABLE tb_sequence ( NAME VARCHAR ( 50 ) NOT NULL, current_value INT NOT NULL, _increment INT NOT NULL DEFAULT 1, PRIMARY KEY ( NAME ) );

DELIMITER //
CREATE FUNCTION _nextval ( n VARCHAR ( 50 ) ) RETURNS INTEGER BEGIN
DECLARE
		_cur INT;
	SET _cur = ( SELECT current_value FROM tb_sequence WHERE NAME = n );
	IF
		_cur IS NULL THEN
			INSERT INTO tb_sequence
		VALUES
			( n, 1, 1 );
		SET _cur = 1;
	END IF;
	UPDATE tb_sequence 
	SET current_value = _cur + _increment 
	WHERE
		NAME = n;
	RETURN _cur;
	END;
// 
SELECT _nextval ( 'test' );

在select _nextval(‘seed’)是,把seed作为种子,如果不存在,就在计数表里插入一条数据,如果存在,则直接+1返回,所以以后每次取值时,可以用一个function就搞定了。

结论

网上有很多文章讲述方案5的,我只是个搬运工,并稍微改动了一下,也避免以后找不到了,供大家参考。

针对这个例子还有很多可扩展的方法,可以修改得更通用,更容易扩展,我就不做赘述了,有更好方案的小伙伴请在下方留言。

]]>
https://www.tuicode.com/archives/2046/feed 0
centos安装tailf命令 https://www.tuicode.com/archives/2043https://www.tuicode.com/archives/2043#respond Mon, 22 Jun 2020 13:53:46 +0000 https://www.tuicode.com/?p=2043 /* tailf.c -- tail a log file and then follow it * Created: Tue Jan 9 15:49:21 1996 by faith@acm.org * Copyright 1996, 2003 Rickard E. Faith (faith@acm.org) * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * less -F and tail -f cause a disk access every five seconds. This * program avoids this problem by waiting for the file size to change. * Hence, the file is not accessed, and the access time does not need to be * flushed back to disk. This is sort of a "stealth" tail. */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <malloc.h> #include <sys/stat.h> //#include "nls.h" #define _(s) s static size_t filesize(const char *filename) { struct stat sb; if (!stat(filename, &sb)) return sb.st_size; return 0; } static void tailf(const char *filename, int lines) { char **buffer; int head = 0; int tail = 0; FILE *str; int i; if (!(str = fopen(filename, "r"))) { fprintf(stderr, _("Cannot open \"%s\" for read\n"), filename); perror(""); exit(1); } buffer = malloc(lines * sizeof(*buffer)); for (i = 0; i < lines; i++) buffer[i] = malloc(BUFSIZ + 1); while (fgets(buffer[tail], BUFSIZ, str)) { if (++tail >= lines) { tail = 0; head = 1; } } if (head) { for (i = tail; i < lines; i++) fputs(buffer[i], stdout); for (i = 0; i < tail; i++) fputs(buffer[i], stdout); } else { for (i = head; i < tail; i++) fputs(buffer[i], stdout); } fflush(stdout); for (i = 0; i < lines; i++) free(buffer[i]); free(buffer); } int main(int argc, char **argv) { char buffer[BUFSIZ]; size_t osize, nsize; FILE *str; const char *filename; int count; //setlocale(LC_ALL, ""); //bindtextdomain(PACKAGE, LOCALEDIR); //textdomain(PACKAGE); if (argc != 2) { fprintf(stderr, _("Usage: tailf logfile\n")); exit(1); } filename = argv[1]; tailf(filename, 10); for (osize = filesize(filename);;) { nsize = filesize(filename); if (nsize != osize) { if (!(str = fopen(filename, "r"))) { fprintf(stderr, _("Cannot open \"%s\" for read\n"), filename); perror(argv[0]); exit(1); } if (!fseek(str, osize, SEEK_SET)) while ((count = fread(buffer, 1, sizeof(buffer), str)) > 0) fwrite(buffer, 1, count, stdout); fflush(stdout); fclose(str); osize = nsize; } usleep(250000); /* 250mS */ } return 0; }

新建文件tailf.c文件

输入命令:

gcc -Wall -o /usr/bin/tailf tailf.c

后续就可以直接使用tailf命令了

参考:https://teakki.com/p/57dbcd7aad29169e13dfbdc7#h3-1-%E7%A4%BA%E4%BE%8B%E4%BA%8C%20%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85tailf%E5%91%BD%E4%BB%A4%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0%C2%A0

]]>
https://www.tuicode.com/archives/2043/feed 0
Arthas – java线上问题处理终极利器 https://www.tuicode.com/archives/2014https://www.tuicode.com/archives/2014#respond Wed, 20 May 2020 11:22:38 +0000 https://www.tuicode.com/?p=2014 前言

在使用 Arthas 之前,当遇到 Java 线上问题时,如 CPU 飙升、负载突高、内存溢出等问题,你需要查命令,查网络,然后 jps、jstack、jmap、jhat、jstat、hprof 等一通操作。最终焦头烂额,还不一定能查出问题所在。而现在,大多数的常见问题你都可以使用 Arthas 轻松定位,迅速解决,及时止损,准时下班。
1、Arthas 介绍

Arthas 是 Alibaba 在 2018 年 9 月开源的 Java 诊断工具。支持 JDK6+, 采用命令行交互模式,提供 Tab 自动不全,可以方便的定位和诊断线上程序运行问题。截至本篇文章编写时,已经收获 Star 17000+。

Arthas 官方文档十分详细,本文也参考了官方文档内容,同时在开源在的 Github 的项目里的 Issues 里不仅有问题反馈,更有大量的使用案例,也可以进行学习参考。

开源地址:https://github.com/alibaba/arthas

官方文档:https://alibaba.github.io/arthas
2、Arthas 使用场景

得益于 Arthas 强大且丰富的功能,让 Arthas 能做的事情超乎想象。下面仅仅列举几项常见的使用情况,更多的使用场景可以在熟悉了 Arthas 之后自行探索。

是否有一个全局视角来查看系统的运行状况?
为什么 CPU 又升高了,到底是哪里占用了 CPU ?
运行的多线程有死锁吗?有阻塞吗?
程序运行耗时很长,是哪里耗时比较长呢?如何监测呢?
这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
有什么办法可以监控到 JVM 的实时运行状态?

3、Arthas 怎么用

前文已经提到,Arthas 是一款命令行交互模式的 Java 诊断工具,由于是 Java 编写,所以可以直接下载相应 的 jar 包运行。
3.1 安装

可以在官方 Github 上进行下载,如果速度较慢,可以尝试国内的码云 Gitee 下载。

# github下载
wget https://alibaba.github.io/arthas/arthas-boot.jar
# 或者 Gitee 下载
wget https://arthas.gitee.io/arthas-boot.jar
# 打印帮助信息
java -jar arthas-boot.jar -h

3.2 运行

Arthas只是一个 java 程序,所以可以直接用java -jar运行。运行时或者运行之后要选择要监测的 Java 进程。

# 运行方式1,先运行,在选择 Java 进程 PID
java -jar arthas-boot.jar
# 选择进程(输入[]内编号(不是PID)回车)
[INFO] arthas-boot version: 3.1.4
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 11616 com.Arthas
  [2]: 8676
  [3]: 16200 org.jetbrains.jps.cmdline.Launcher
  [4]: 21032 org.jetbrains.idea.maven.server.RemoteMavenServer

# 运行方式2,运行时选择 Java 进程 PID
java -jar arthas-boot.jar [PID]

查看 PID 的方式可以通过ps命令,也可以通过 JDK 提供的jps命令。

# 查看运行的 java 进程信息
$ jps -mlvV 
# 筛选 java 进程信息
$ jps -mlvV | grep [xxx]

jps筛选想要的进程方式。

在出现ArthasLogo 之后就可以使用命令进行问题诊断了。下面会详细介绍。

更多的启动方式可以参考 help 帮助命令。

# 其他用法
EXAMPLES:
  java -jar arthas-boot.jar <pid>
  java -jar arthas-boot.jar --target-ip 0.0.0.0
  java -jar arthas-boot.jar --telnet-port 9999 --http-port -1
  java -jar arthas-boot.jar --tunnel-server 'ws://192.168.10.11:7777/ws'
  java -jar arthas-boot.jar --tunnel-server 'ws://192.168.10.11:7777/ws'
--agent-id bvDOe8XbTM2pQWjF4cfw
  java -jar arthas-boot.jar --stat-url 'http://192.168.10.11:8080/api/stat'
  java -jar arthas-boot.jar -c 'sysprop; thread' <pid>
  java -jar arthas-boot.jar -f batch.as <pid>
  java -jar arthas-boot.jar --use-version 3.1.4
  java -jar arthas-boot.jar --versions
  java -jar arthas-boot.jar --session-timeout 3600
  java -jar arthas-boot.jar --attach-only
  java -jar arthas-boot.jar --repo-mirror aliyun --use-http

3.3 web console

Arthas目前支持Web Console,在成功启动连接进程之后就已经自动启动,可以直接访问 http://127.0.0.1:8563/ 访问,页面上的操作模式和控制台完全一样。

3.4 常用命令

下面列举一些 Arthas 的常用命令,看到这里你可能还不知道怎么使用,别急,后面会一一介绍。
命令     介绍
dashboard     当前系统的实时数据面板
thread     查看当前 JVM 的线程堆栈信息
watch     方法执行数据观测
trace     方法内部调用路径,并输出方法路径上的每个节点上耗时
stack     输出当前方法被调用的调用路径
tt     方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
monitor     方法执行监控
jvm     查看当前 JVM 信息
vmoption     查看,更新 JVM 诊断相关的参数
sc     查看 JVM 已加载的类信息
sm     查看已加载类的方法信息
jad     反编译指定已加载类的源码
classloader     查看 classloader 的继承树,urls,类加载信息
heapdump     类似 jmap 命令的 heap dump 功能
3.5 退出

使用 shutdown 退出时 Arthas 同时自动重置所有增强过的类 。
4、Arthas 常用操作

上面已经了解了什么是 Arthas,以及 Arthas 的启动方式,下面会依据一些情况,详细说一说 Arthas 的使用方式。在使用命令的过程中如果有问题,每个命令都可以是 -h 查看帮助信息。

首先编写一个有各种情况的测试类运行起来,再使用 Arthas 进行问题定位,

import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;

/**
 * <p>
 * Arthas Demo
 * 公众号:未读代码
 *
 * @Author niujinpeng
 */
@Slf4j
public class Arthas {

    private static HashSet hashSet = new HashSet();
    /** 线程池,大小1*/
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) {
        // 模拟 CPU 过高,这里注释掉了,测试时可以打开
        // cpu();
        // 模拟线程阻塞
        thread();
        // 模拟线程死锁
        deadThread();
        // 不断的向 hashSet 集合增加数据
        addHashSetThread();
    }

    /**
     * 不断的向 hashSet 集合添加数据
     */
    public static void addHashSetThread() {
        // 初始化常量
        new Thread(() -> {
            int count = 0;
            while (true) {
                try {
                    hashSet.add("count" + count);
                    Thread.sleep(10000);
                    count++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public static void cpu() {
        cpuHigh();
        cpuNormal();
    }

    /**
     * 极度消耗CPU的线程
     */
    private static void cpuHigh() {
        Thread thread = new Thread(() -> {
            while (true) {
                log.info("cpu start 100");
            }
        });
        // 添加到线程
        executorService.submit(thread);
    }

    /**
     * 普通消耗CPU的线程
     */
    private static void cpuNormal() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                while (true) {
                    log.info("cpu start");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    /**
     * 模拟线程阻塞,向已经满了的线程池提交线程
     */
    private static void thread() {
        Thread thread = new Thread(() -> {
            while (true) {
                log.debug("thread start");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 添加到线程
        executorService.submit(thread);
    }

    /**
     * 死锁
     */
    private static void deadThread() {
        /** 创建资源 */
        Object resourceA = new Object();
        Object resourceB = new Object();
        // 创建线程
        Thread threadA = new Thread(() -> {
            synchronized (resourceA) {
                log.info(Thread.currentThread() + " get ResourceA");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info(Thread.currentThread() + "waiting get resourceB");
                synchronized (resourceB) {
                    log.info(Thread.currentThread() + " get resourceB");
                }
            }
        });

        Thread threadB = new Thread(() -> {
            synchronized (resourceB) {
                log.info(Thread.currentThread() + " get ResourceB");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info(Thread.currentThread() + "waiting get resourceA");
                synchronized (resourceA) {
                    log.info(Thread.currentThread() + " get resourceA");
                }
            }
        });
        threadA.start();
        threadB.start();
    }
}

4.1 全局监控

使用dashboard命令可以概览程序的 线程、内存、GC、运行环境信息。

4.2 CPU 为什么起飞了

上面的代码例子有一个CPU空转的死循环,非常的消耗CPU性能,那么怎么找出来呢?

使用thread查看所有线程信息,同时会列出每个线程的CPU使用率,可以看到图里 ID 为12 的线程 CPU 使用100%。

使用命令thread 12查看 CPU 消耗较高的 12 号线程信息,可以看到 CPU 使用较高的方法和行数(这里的行数可能和上面代码里的行数有区别,因为上面的代码在我写文章时候重新排过版了)。

上面是先通过观察总体的线程信息,然后查看具体的线程运行情况。如果只是为了寻找 CPU 使用较高的线程,可以直接使用命令thread -n [显示的线程个数],就可以排列出 CPU 使用率Top N的线程。

定位到的 CPU 使用最高的方法。

4.3 线程池线程状态

定位线程问题之前,先回顾一下线程的几种常见状态:

RUNNABLE 运行中
TIMED_WAITIN 调用了以下方法的线程会进入TIMED_WAITING:
Thread#sleep()
Object#wait() 并加了超时参数
Thread#join() 并加了超时参数
LockSupport#parkNanos()
LockSupport#parkUntil()
WAITING 当线程调用以下方法时会进入WAITING状态:
Object#wait() 而且不加超时参数
Thread#join() 而且不加超时参数
LockSupport#park()
BLOCKED 阻塞,等待锁

上面的模拟代码里,定义了线程池大小为1 的线程池,然后在 cpuHigh 方法里提交了一个线程,在 thread方法再次提交了一个线程,后面的这个线程因为线程池已满,会阻塞下来。

使用 thread | grep pool 命令查看线程池里线程信息。

可以看到线程池有WAITING的线程。

4.4 线程死锁

上面的模拟代码里deadThread方法实现了一个死锁,使用thread -b命令查看直接定位到死锁信息。

/**
 * 死锁
 */
private static void deadThread() {
    /** 创建资源 */
    Object resourceA = new Object();
    Object resourceB = new Object();
    // 创建线程
    Thread threadA = new Thread(() -> {
        synchronized (resourceA) {
            log.info(Thread.currentThread() + " get ResourceA");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info(Thread.currentThread() + "waiting get resourceB");
            synchronized (resourceB) {
                log.info(Thread.currentThread() + " get resourceB");
            }
        }
    });

    Thread threadB = new Thread(() -> {
        synchronized (resourceB) {
            log.info(Thread.currentThread() + " get ResourceB");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info(Thread.currentThread() + "waiting get resourceA");
            synchronized (resourceA) {
                log.info(Thread.currentThread() + " get resourceA");
            }
        }
    });
    threadA.start();
    threadB.start();
}

检查到的死锁信息。

4.5 反编译

上面的代码放到了包com下,假设这是一个线程环境,当怀疑当前运行的代码不是自己想要的代码时,可以直接反编译出代码,也可以选择性的查看类的字段或方法信息。

如果怀疑不是自己的代码,可以使用jad命令直接反编译 class。

jad命令还提供了一些其他参数:

# 反编译只显示源码
jad --source-only com.Arthas
# 反编译某个类的某个方法
jad --source-only com.Arthas mysql

4.6 查看字段信息

使用 **sc -d -f ** 命令查看类的字段信息。

[arthas@20252]$ sc -d -f com.Arthas
sc -d -f com.Arthas
 class-info        com.Arthas
 code-source       /C:/Users/Niu/Desktop/arthas/target/classes/
 name              com.Arthas
 isInterface       false
 isAnnotation      false
 isEnum            false
 isAnonymousClass  false
 isArray           false
 isLocalClass      false
 isMemberClass     false
 isPrimitive       false
 isSynthetic       false
 simple-name       Arthas
 modifier          public
 annotation
 interfaces
 super-class       +-java.lang.Object
 class-loader      +-sun.misc.Launcher$AppClassLoader@18b4aac2
                     +-sun.misc.Launcher$ExtClassLoader@2ef1e4fa
 classLoaderHash   18b4aac2
 fields            modifierfinal,private,static
                   type    org.slf4j.Logger
                   name    log
                   value   Logger[com.Arthas]

                   modifierprivate,static
                   type    java.util.HashSet
                   name    hashSet
                   value   [count1, count2]

                   modifierprivate,static
                   type    java.util.concurrent.ExecutorService
                   name    executorService
                   value   java.util.concurrent.ThreadPoolExecutor@71c03156[Ru
                           nning, pool size = 1, active threads = 1, queued ta
                           sks = 0, completed tasks = 0]


Affect(row-cnt:1) cost in 9 ms.

4.7 查看方法信息

使用sm命令查看类的方法信息。

[arthas@22180]$ sm com.Arthas
com.Arthas <init>()V
com.Arthas start()V
com.Arthas thread()V
com.Arthas deadThread()V
com.Arthas lambda$cpuHigh$1()V
com.Arthas cpuHigh()V
com.Arthas lambda$thread$3()V
com.Arthas addHashSetThread()V
com.Arthas cpuNormal()V
com.Arthas cpu()V
com.Arthas lambda$addHashSetThread$0()V
com.Arthas lambda$deadThread$4(Ljava/lang/Object;Ljava/lang/Object;)V
com.Arthas lambda$deadThread$5(Ljava/lang/Object;Ljava/lang/Object;)V
com.Arthas lambda$cpuNormal$2()V
Affect(row-cnt:16) cost in 6 ms.

4.8 对变量的值很是好奇

使用ognl命令,ognl 表达式可以轻松操作想要的信息。

代码还是上面的示例代码,我们查看变量hashSet中的数据:

查看静态变量hashSet信息。

[arthas@19856]$ ognl '@com.Arthas@hashSet'
@HashSet[
    @String[count1],
    @String[count2],
    @String[count29],
    @String[count28],
    @String[count0],
    @String[count27],
    @String[count5],
    @String[count26],
    @String[count6],
    @String[count25],
    @String[count3],
    @String[count24],

查看静态变量 hashSet 大小。

[arthas@19856]$ ognl '@com.Arthas@hashSet.size()'
	@Integer[57]

甚至可以进行操作。

[arthas@19856]$ ognl  '@com.Arthas@hashSet.add("test")'
	@Boolean[true]
[arthas@19856]$
# 查看添加的字符
[arthas@19856]$ ognl  '@com.Arthas@hashSet' | grep test
    @String[test],
[arthas@19856]$

ognl 可以做很多事情,可以参考 ognl 表达式特殊用法( https://github.com/alibaba/arthas/issues/71 )。

4.9 程序有没有问题
4.9.1 运行较慢、耗时较长

使用 trace 命令可以跟踪统计方法耗时

这次换一个模拟代码。一个最基础的 Springboot 项目(当然,不想 Springboot 的话,你也可以直接在 UserController 里 main 方法启动)控制层 getUser 方法调用了 userService.get(uid);,这个方法中分别进行check、service、redis、mysql操作。

@RestController
@Slf4j
public class UserController {

    @Autowired
    private UserServiceImpl userService;

    @GetMapping(value = "/user")
    public HashMap<String, Object> getUser(Integer uid) throws Exception {
        // 模拟用户查询
        userService.get(uid);
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("uid", uid);
        hashMap.put("name", "name" + uid);
        return hashMap;
    }
}

模拟代码 Service:

@Service
@Slf4j
public class UserServiceImpl {

    public void get(Integer uid) throws Exception {
        check(uid);
        service(uid);
        redis(uid);
        mysql(uid);
    }

    public void service(Integer uid) throws Exception {
        int count = 0;
        for (int i = 0; i < 10; i++) {
            count += i;
        }
        log.info("service  end {}", count);
    }

    public void redis(Integer uid) throws Exception {
        int count = 0;
        for (int i = 0; i < 10000; i++) {
            count += i;
        }
        log.info("redis  end {}", count);
    }

    public void mysql(Integer uid) throws Exception {
        long count = 0;
        for (int i = 0; i < 10000000; i++) {
            count += i;
        }
        log.info("mysql end {}", count);
    }

 	 public boolean check(Integer uid) throws Exception {
         if (uid == null || uid < 0) {
             log.error("uid不正确,uid:{}", uid);
             throw new Exception("uid不正确");
         }
         return true;
     }
}

运行 Springboot 之后,使用 **trace== ** 命令开始检测耗时情况。

[arthas@6592]$ trace com.UserController getUser

访问接口/getUser,可以看到耗时信息,看到com.UserServiceImpl:get()方法耗时较高。

继续跟踪耗时高的方法,然后再次访问。

[arthas@6592]$ trace com.UserServiceImpl get

很清楚的看到是com.UserServiceImplmysql方法耗时是最高的。

Affect(class-cnt:1 , method-cnt:1) cost in 31 ms.
`---ts=2019-10-16 14:40:10;thread_name=http-nio-8080-exec-8;id=1f;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@23a918c7
    `---[6.792201ms] com.UserServiceImpl:get()
        +---[0.008ms] com.UserServiceImpl:check() #17
        +---[0.076ms] com.UserServiceImpl:service() #18
        +---[0.1089ms] com.UserServiceImpl:redis() #19
        `---[6.528899ms] com.UserServiceImpl:mysql() #20

4.9.2 统计方法耗时

使用monitor命令监控统计方法的执行情况。

每5秒统计一次com.UserServiceImpl类的get方法执行情况。

monitor -c 5 com.UserServiceImpl get

4.10 想观察方法信息

下面的示例用到了文章的前两个模拟代码。

4.10.1 观察方法的入参出参信息

使用watch命令轻松查看输入输出参数以及异常等信息。

 USAGE:
   watch [-b] [-e] [-x <value>] [-f] [-h] [-n <value>] [-E] [-M <value>] [-s] class-pattern method-pattern express [condition-express]

 SUMMARY:
   Display the input/output parameter, return object, and thrown exception of specified method invocation
   The express may be one of the following expression (evaluated dynamically):
           target : the object
            clazz : the object's class
           method : the constructor or method
           params : the parameters array of method
     params[0..n] : the element of parameters array
        returnObj : the returned object of method
         throwExp : the throw exception of method
         isReturn : the method ended by return
          isThrow : the method ended by throwing exception
            #cost : the execution time in ms of method invocation
 Examples:
   watch -b org.apache.commons.lang.StringUtils isBlank params
   watch -f org.apache.commons.lang.StringUtils isBlank returnObj
   watch org.apache.commons.lang.StringUtils isBlank '{params, target, returnObj}' -x 2
   watch -bf *StringUtils isBlank params
   watch *StringUtils isBlank params[0]
   watch *StringUtils isBlank params[0] params[0].length==1
   watch *StringUtils isBlank params '#cost>100'
   watch -E -b org\.apache\.commons\.lang\.StringUtils isBlank params[0]

 WIKI:
   https://alibaba.github.io/arthas/watch

常用操作:

# 查看入参和出参
$ watch com.Arthas addHashSet '{params[0],returnObj}'
# 查看入参和出参大小
$ watch com.Arthas addHashSet '{params[0],returnObj.size}'
# 查看入参和出参中是否包含 'count10'
$ watch com.Arthas addHashSet '{params[0],returnObj.contains("count10")}'
# 查看入参和出参,出参 toString
$ watch com.Arthas addHashSet '{params[0],returnObj.toString()}'

查看入参出参。

查看返回的异常信息。

4.10.2 观察方法的调用路径

使用stack命令查看方法的调用信息。

# 观察 类com.UserServiceImpl的 mysql 方法调用路径
stack com.UserServiceImpl mysql

4.10.3 方法调用时空隧道

使用 tt 命令记录方法执行的详细情况。

tt 命令方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测 。

常用操作:

开始记录方法调用信息:tt -t com.UserServiceImpl check

可以看到记录中 INDEX=1001 的记录的 IS-EXP = true ,说明这次调用出现异常。

查看记录的方法调用信息: tt -l

查看调用记录的详细信息(-i 指定 INDEX): tt -i 1001

可以看到 INDEX=1001 的记录的异常信息。

重新发起调用,使用指定记录,使用 -p 重新调用。

tt -i 1001 -p

文中代码已经上传到Github

https://github.com/niumoo/lab-notes/tree/master/src/main/java/net/codingme/arthas

本文转自:https://blog.csdn.net/u013735734/article/details/102930307

]]>
https://www.tuicode.com/archives/2014/feed 0
今早github无法访问的问题解析 https://www.tuicode.com/archives/2007https://www.tuicode.com/archives/2007#respond Fri, 27 Mar 2020 03:23:56 +0000 https://www.tuicode.com/?p=2007 问题描述

今天早上来到公司,忽然有个同事跟我说,github无法访问了,正在做个研究需要用到(可能怀疑我对公司的网络做了什么配置)。

我试着登录了一下,确实无法访问了。提示证书不受信任。

我也试着在外网登陆了一下github,提示一样的问题(主要排除公司内网被攻击的可能性)。

我也试着在国外的服务器上访问了一下github,发现是可以访问的。

同时访问了一下搭建在github上的博客一类的,也都提示“当前网页非官方页面",当然这个是我本地杀毒软件提示的。

网上查了一下业内的qq和微信群,居然没有任何动静。

瞬间反应有四种可能:

1.github对国内访问做了限制,这个据说去年github对伊朗做了限制;

2.被HX了,目前访问国外网站被HX的很多;

3.github证书过期了;

4.github被攻击了。

问题分析

分析一下上述的四种可能性。

1.github对国内访问做了限制,应该会提前通知的,起码官方会有通知,所以排除。

2.被HX了,github最近没做什么逾越底线的事情,所以ZF应该不会HX的。

为了验证这个问题,特意ping一下github.com域名,确实ping不通,指向IP是13.250.177.223,也许是github限制了ICMP协议。

3.github证书过期了。

这个倒是有可能,于是查看了一下github证书。

哈哈了个哈哈,注意红框位置是一个qq邮箱,github再怎么样也不会用qq邮箱的,所以被攻击无疑了。

返回头验证:

13.250.177.223为亚马逊在新加坡的IP地址;

国外服务器可以ping通github.com网址,指向140.82.118.4为荷兰阿姆斯特丹IP。

应该是类似于dns劫持的一类攻击。

解决办法

临时办法

把本地的hosts文件修改增加:

140.82.118.4 github.com
140.82.118.4 www.github.com

长久办法

修改dns为8.8.8.8

当然现在github已经恢复访问了,并且13.250.177.223这个IP也被封禁了。

总结学习

作为一个对网络攻击理解不深的我来说,这个攻击应该还是比较高明吧?

后续网络上逐渐开始传开,学习了一个新的黑客名词:中间人攻击(英语:Man-In-The-Middle Attack,缩写MITM)。

中间人攻击(Man-in-the-Middle Attack, MITM)是一种由来已久的网络入侵手段,并且当今仍然有着广泛的发展空间,如SMB会话劫持、DNS欺骗等攻击都是典型的MITM攻击。简而言之,所谓的MITM攻击就是通过拦截正常的网络通信数据,并进行数据篡改和嗅探,而通信的双方却毫不知情。
随着计算机通信网技术的不断发展,MITM攻击也越来越多样化。最初,攻击者只要将网卡设为混杂模式,伪装成代理服务器监听特定的流量就可以实现攻击,这是因为很多通信协议都是以明文来进行传输的,如HTTP、FTP、Telnet等。后来,随着交换机代替集线器,简单的嗅探攻击已经不能成功,必须先进行ARP欺骗才行。如今,越来越多的服务商(网上银行,邮箱登陆)开始采用加密通信,SSL(Secure Sockets Layer 安全套接层)是一种广泛使用的技术,HTTPS、FTPS等都是建立在其基础上的。

可见网络传输过程中SSL确实很有必要,否则被人劫持了都不知道。

其次,这个人(346608453@qq.com)的攻击暂时看还没有对github及其使用者造成数据泄露,同时据传,京东、koajs等网站均受到此类攻击,邮箱均是此人,很多人分析这个人可能是黑客初学者,此次攻击很可能是为了练习/测试。

同时查看了一下公司和自己的几个网站,还都正常,这样我就放心了。

]]>
https://www.tuicode.com/archives/2007/feed 0
多机房多活架构落地实践方案 https://www.tuicode.com/archives/1997https://www.tuicode.com/archives/1997#respond Wed, 05 Feb 2020 02:29:19 +0000 https://www.tuicode.com/?p=1997 如果将单机房“全连接"架构复制到多机房,会有大量跨机房调用,极大增加请求时延,是业务无法接受的,要想降低这个时延,必须实施“同机房连接"。
多机房多活架构,什么是理想状态下的“同机房连接"?
如上图所示,多机房多活架构,最理想状态下,除了异步数据同步跨机房通讯,其他所有通讯均为“同机房连接":
(1)web连业务服务;
(2)业务服务连基础服务;
(3)服务连数据库,主库写,从库读,读写分离;
上述架构,每个机房是一套独立的系统,仅仅通过异步数据同步获取全量数据,当发生机房故障时,将流量切到另一个机房,就能冗余“机房级"故障,实现高可用。
上述多机房架构存在什么问题?
“异步数据同步"存在延时(例如:1min),这个延时的存在,会使得两个机房的数据不一致,从而导致严重的业务问题。
举个例子,某一个时刻,用户X有余额100元,两个机房都存储有该余额的精准数据,接下来:
(1)余额100,X在北京(就近访问机房A)消费了80元,余额仅剩20元,该数据在1分钟后会同步到机房B;
(2)余额100,X的夫人在广州(就近访问机房B)用X的账号消费了70元,余额剩余30元,该数据在1分钟后也会同步到机房A;
从而导致:
(1)超额消费(100余额,却买了150的东西);
(2)余额异常(余额是20,还是30?);
上述架构适合于什么业务场景?
任何脱离业务的架构设计都是耍流氓。
当每个机房都有很多全局业务数据的访问场景时,上述多机房架构并不适用,会存在大量数据不一致。但当每个机房都访问局部业务数据时,上述多机房架构仍然是可行的。
典型的业务:滴滴,快狗打车。
这些业务具备数据聚集效应
(1)下单用户在同一个城市;
(2)接单司机在同一个城市;
(3)交易订单在同一个城市;
这类业务非常适合上述多机房多活架构,多个机房之间即使存在1分钟延时的“异步数据同步",对业务也不会造成太大的影响。
多机房多活架构,做不到理想状态下的“同机房连接",有没有折中方案?
如果完全避免跨机房调用的理想状态做不到,就尽量做到“最小化"跨机房调用。
如上图所示,在非必须的情况下,优先连接同机房的站点与服务:
(1)站点层只连接同机房的业务服务层;
(2)业务服务层只连接同机房的基础服务层;
(3)服务层只连接同机房的“读"库;
(4)对于写库,没办法,只有跨机房读“写"库了;
该方案没有完全避免跨机房调用,但它做到了“最小化"跨机房调用,只有写请求是跨机房的。
但互联网的业务,绝大部分是读多写少的业务:
(1)百度的搜索100%是读业务;
(2)京东淘宝电商99%的浏览搜索是读业务,只有下单支付是写业务;
(3)58同城99%帖子的列表详情查看是读业务,只有发布帖子是写业务;
写业务比例相对少,只有很少请求会跨机房调用。
该多机房多活架构,并没有做到100%的“同机房连接",通常称作伪多机房多活架构
伪多机房多活架构,有“主机房"和“从机房"的差别。
多机房多活架构的初衷是容机房故障,该架构当出现机房故障时,可以把入口处流量切到另一个机房:
(1)如果挂掉的是,不包含主库的从机房,迁移流量后能直接容错;
(2)如果挂掉的是,包含主库的主机房,只迁移流量,系统整体99%的读请求可以容错,但1%的写请求会受到影响,此时需要将从库变为主库,才能完全容错。这个过程需要DBA介入,不需要所有业务线上游修改。
画外音:除非,站点和服务使用内网IP,而不是内网域名连接数据库。架构师之路已经强调过很多次,不要使用内网IP,一定要使用内网域名。
伪多机房多活架构,是一个实践性,落地性很强的架构,它对原有架构体系的冲击非常小,和单机房架构相比,仅仅是:
(1)跨机房主从同步数据,会多10毫秒延时;
画外音:主从同步数据,本来就会有延时。
(2)跨机房写,会多10毫秒延时;
小结
(1)理想多机房多活架构,是纯粹的“同机房连接",仅有异步数据同步会跨机房;
(2)理想多机房多活架构,会有较严重数据一致性问题,仅适用于具备数据聚集效应的业务场景,例如:滴滴,快狗打车;
(3)伪多机房多活架构,思路是“最小化跨机房连接",机房区分主次,落地性强,对原有架构冲击较小,强烈推荐;
来源:架构师之路——微信公众号
作者:58沈剑
]]>
https://www.tuicode.com/archives/1997/feed 0
微信机器人之开发体验 https://www.tuicode.com/archives/1990https://www.tuicode.com/archives/1990#respond Tue, 28 Jan 2020 05:34:25 +0000 https://www.tuicode.com/?p=1990 前言

目前在公司中的消息通知大部分使用邮件、短信、钉钉、App通知、websocket通知、微信企业版等等,针对于QQ和微信这种目前使用量较大的工具,通知机制并不完善。当然,主要是TX本身的功能要求决定了无法做类似的通知。

本篇文章主要讨论微信机器人的开发经验。

可选方案

桌面工具

这类的工具主要是嫁接到微信的桌面工具来使用,基于微信的windows客户端的功能做了节选。使用比较简单,但相对并不是很安全,工具也不够完整。

浏览器工具

浏览器工具是基于微信网页版进行开发,作为开发者来说,这类工具最受开发者欢迎,不需要服务器,只需要了解网页版API调用过程,就可以实现想要的功能。但是现在微信对网页版登录是有很多限定的,大部分微信已经不允许登录网页版了。而且网页版工具也有很大的限制,功能也不够完整,无法代替微信。

win sock开发

这个开发说得直白一些,是“桌面工具"的前提,桌面工具无非就是用这个方法分析并开发出来的。这个开发会很困难,相当于是解析微信windows客户端的exe文件,也有很大的风险,毕竟不是官方认可的内容。

iPad协议开发

ipad协议是微信在ipad上提供的一种接口协议,这个协议目前可使用的内容上来说,是功能最全的,只要分析出ipad协议的接口,就可以使用相应的功能,在ipad上的功能也是很全的。难点是,目前对ipad协议并没有被微信公开,只是很多公司有私下的研究公布,自己分析代价很大。

wechaty

基本信息

wechaty是句子科技使用nodeJS针对于微信开发出来的协议,其中包含网页版和ipad协议。wechaty github地址是: https://github.com/wechaty/wechaty

wechaty默认使用时是基于网页版协议的,如果需要使用ipad协议,需要在github上做申请,会有专人审核接入。

开发之前

我使用时是基于wechaty-puppet-padplus协议做的开发。

具体的可以参考:https://github.com/wechaty/wechaty-getting-started

环境准备

linux

node

typescript

微信

基于node:10.15.0-alpine系统需要对操作系统有一些初始化操作:

apk add build-base
apk add zlib-dev
apk add python
npm config set registry https://registry.npm.taobao.org
npm config set disturl https://npm.taobao.org/dist

开发过程

在开发过程中使用ts-node命令进行执行(对于typescript小白来说也查了很多资料)。

先把github上例子的ts文件拿下来执行,创建执行脚本:

export PATH=$PATH:/wechaty/padplus/node_modules/.bin/
ts-node $1

会自动提示登录二维码,例子中发送的消息会自动返回。

工具使用

我对wechaty的基本需求有两点:

1.定时的自动向指定人员发送变化的消息,面对不同人员不同时间发送不同的消息;

2.为其他程序端提供相应的接口。

因此,我做了一个mybot.ts:

import { Wechaty,Message       } from 'wechaty'
import { PuppetPadplus } from 'wechaty-puppet-padplus'
import { generate      } from 'qrcode-terminal'
import * as express from 'express';
import * as urlencode from 'urlencode';
import * as bodyParser from 'body-parser';

const urlencodedParser = bodyParser.urlencoded({ extended: false })
const token = 'xxxxxxxx' //此处需要替换你自己申请的token

const puppet = new PuppetPadplus({
  token,
})

const name  = 'anan'
const app = express(); // 用于声明服务器端所能提供的http服务


const bot = new Wechaty({
  puppet,
  name, // generate xxxx.memory-card.json and save login data for the next login
})

bot
  .on('scan', onScan)
  .on('message', onMessage )
  .start()
  
function onScan (qrcode, status) {
  generate(qrcode, { small: true })  // show qrcode on console
}
async function onMessage(msg){
	console.log("=============================")
	if (msg.self() || msg.from().name() === '微信团队') {
		console.log("myself message")
		return
	}
	console.log(`msg : ${msg}`)
	console.log(`from: ${msg.from().name()}: ${msg.from().id}`)
	console.log(`to: ${msg.to()}`)
	console.log(`text: ${msg.text()}`)
	console.log(`isRoom: ${msg.room()}`)
	

	console.log("=============================")
	if (msg.type() == Message.Type.Text) {
		if (msg.room()){
			const topic = await msg.room().topic()
			console.log(`roomTopic: ${topic}`)
			const room = await msg.room()
			const memberList = await room.memberList()
			for (let i = 0; i < memberList.length; i++) {
				console.log(`member${i}`)
				const member = memberList[i]
				console.log(`member${i}: ${member.id},${member.name()},${member.alias()},${member.friend()},${member.avatar()}`)
			}
			if (await msg.mentionSelf()){
				room.say("recieved message mentioned me!",memberList[0],memberList[1])
			}else{
				room.say("recieved message \n not metioned me!")
			}
		}else {
			await msg.say('recieved message sent myself only')
		}
		return
	}else {
		console.log("message is not text")
		console.log(`msg : ${msg}`)
		await msg.say('message is not text')
	}
}
async function sendMessage(contact:string, contacttype:string, message:string, msgtype:string, metion:string){
	console.log(`params:${contact},${contacttype},${message},${msgtype},${metion}`)
	if (contacttype == 'room'){
		const room = await bot.Room.find({topic: contact})
		console.log(`getroom: ${room}`)
		const metionContact = await room.member(metion)
		console.log(`get metion: ${metionContact}`)
		if (metionContact){
			await room.say(message, metionContact)
		}else {
			await room.say(message)
		}
	} else {
		const contactCard = await bot.Contact.find({name: contact})
		if (!contactCard){
			console.log(`get contact card is null`)
			return 
		}
		await contactCard.say(message)
	}
}
 
// 声明一个处理get请求的服务
app.get('/', (req, resp) => {
    resp.send("Hello Express");
});
 
app.get("/send",urlencodedParser, (req, resp) => {
console.log(`url: ${req.url}`)
console.log(decodeURI(req.url))
//decodeURI(decodeURIComponent(escape(req.url)), "UTF-8")
	//const req = decodeURI(request)
	const contactvalue = decodeURI(req.query.contact)
	const contacttypevalue = decodeURI(req.query.contacttype)
	const messagevalue = decodeURI(req.query.message).replace(/\\n/g,"\n")
	const msgtypevalue = decodeURI(req.query.msgtype)
	const metionvalue = decodeURI(req.query.metion)
	console.log(`query:${contactvalue},${contacttypevalue},${messagevalue},${msgtypevalue},${metionvalue}`)
	sendMessage(contactvalue, contacttypevalue, messagevalue, msgtypevalue, metionvalue)
    resp.send("接收到发送微信请求");
});
 
const server = app.listen(8000, "localhost", () => {
    console.log("服务器已启动, 地址是:http://localhost:8000");
});

这个机器人主要做的是开通了一个send的http get方法,直接通过浏览器访问此接口就可以发送消息,另外还可以扩展方法来获取微信上的信息。

于是针对需求1的定时发送,我做了如下的脚本:

curl -G --data-urlencode "contact=中文测试" --data-urlencode "message=${message}" http://127.0.0.1:8000/send?contacttype=room\&msgtype=text

我们可以通过定时的执行这个脚本,就可以发送给相应的人或者群消息了,这个脚本里有很多都可以做成参数。

contact:人或者群名称
message:消息的详细内容
contacttype:room/person标识群或者个人
msgtype:定义消息的类型
注意此处对应中文的地方,一定要用urlencode,否则系统无法检索和识别

总结

目前市面上可以使用的,免费的,功能较全的,还是wechaty比较好,所以推荐给大家使用。

]]>
https://www.tuicode.com/archives/1990/feed 0
Synchronized内容总结(三) https://www.tuicode.com/archives/1987https://www.tuicode.com/archives/1987#respond Sun, 19 Jan 2020 03:34:12 +0000 https://www.tuicode.com/?p=1987
前面两部分谈到多线程对内部锁的优化,以及代码中对锁的优化。是从减少竞态的角度来优化程序的。
如果从提高线程执行效率,来对多线程程序进行优化,自然让人联想到了线程池技术。

基本概念与原理

Java 线程池会生成一个队列,要执行的任务会被提交到这个队列中。有一定数量的线程会在队列中取任务,然后执行。
任务执行完毕以后,线程会返回任务队列,等待其他任务并执行。线程池中有一定数量的线程随时待命。

由于生成和维持这些线程是需要耗费资源了,维持太多或者太少的线程都会对系统运行效率造成影响,因此对线程池优化是有意义的。

在做线程池调优之前,先介绍一下线程的几个基本参数,以及线程池运行的原理:

  • corePoolSize,线程池的基本大小,无论是否有任务需要执行,线程池中线程的个数。只有在工作队列占满的情况下,才会创建超出这个数量的线程。
  • maximumPoolSize,线程池中允许存在的最大线程数。
  • poolSize,线程池中线程的数量。
当提交任务需要流程池处理时,会经过以下判断:
  • 线程池中的线程数还没有达到基本大小,也就是 poolSize<corePoolSize 时。新增一个线程处理任务,即使线程池中存在空闲的线程。
  • 线程池中的线程数大于或等于基本大小,也就是 poolSize>=corePoolSize,并且任务队列未满时,将任务提交到阻塞队列排队等候处理。
  • 如果当前线程池的线程数大于或等于基本大小,也就是 poolSize>=corePoolSize 且任务队列占满时,需要分两种情况考虑。

    ①当 poolSize<maximumPoolSize,新增线程来处理任务;

    ②当 poolSize=maximumPoolSize,线程池的处理能力达到极限,因此拒绝新增加的任务。

线程池容量配置

从上面线程池原理可以看出,corePoolSize 设置是整个线程池中最关键的参数。
如果设置太小会导致线程池的吞吐量不足,因为新提交的任务需要排队或者被拒绝处理;设置太大可能会耗尽计算机的 CPU 和内存资源。
那么如何配置合理的线程池大小呢?如果将被处理的任务分为,CPU 密集型任务和 IO 密集型任务。前者需要更多 CPU 的运算操作,后者需要更多的 IO 操作。
CPU 密集型任务应配置尽可能小的线程,如配置 CPU 个数 +1 的线程数,IO 密集型任务应配置尽可能多的线程,因为 IO 操作不占用 CPU,不要让 CPU 闲下来,应加大线程数量,如配置两倍 CPU 个数 +1。
CPU 的数字是一个假设,实际环境中需要进行测试,这里给大家一个思路。
若任务对其他系统资源有依赖,如任务依赖数据库返回的结果(IO 操作)。其等待时间越长,CPU 空闲时间就越长,那么线程数量应该越大,才能更好的利用 CPU。

因此在 IO 优化中发现一个估算公式:

最佳线程数目=((线程等待时间+线程 CPU 时间)/线程 CPU 时间 )* CPU 数目。

将公式进一步化简,得到:

最佳线程数目= (线程等待时间与线程 CPU 时间之比+1)* CPU 数目。
因此得到结论:线程等待时间所占比例越高,需要越多线程。线程 CPU 时间所占比例越高,需要越少线程。
从另外一个角度验证上面对 IO 密集型(线程等待时间占比高)和 CPU 密集型(CPU 时间占比高)设置线程池大小的想法。

总结

Java 多线程开发优化有两个思路:
  • 针对锁的优化
  • 线程池优化
我们从内部锁优化原理入手,分别介绍了锁消除,锁粗化,偏向锁,适应锁,都是以 Java 系统本身来做优化的,作为程序员需要了解其实现原理。
针对 Java 代码中锁的优化,我们又提出了,减少临界区范围,减小锁的颗粒度,读写锁(设计模式)等方法。
其中,读写锁只是多线程设计模式中的一种,如果有兴趣可以扩展阅读其他的设计模式,协助进行多线程开发。最后针对线程池实现原理,提出了设置线程池大小的思路。
作者:崔皓

简介:十六年开发和架构经验,曾担任过惠普武汉交付中心技术专家,需求分析师,项目经理,后在创业公司担任技术/产品经理。善于学习,乐于分享。目前专注于技术架构与研发管理。

编辑:陶家龙、孙淑娟

]]>
https://www.tuicode.com/archives/1987/feed 0
Synchornized优化总结(二) https://www.tuicode.com/archives/1971https://www.tuicode.com/archives/1971#respond Sun, 19 Jan 2020 03:21:42 +0000 https://www.tuicode.com/?p=1971

前面讲了 Java 系统是如何针对内部锁进行优化的。如果说内部锁的优化是 Java 系统自身完成的话,那么接下来的优化就需要通过代码实现了。

锁的开销主要是在争用锁上,当多线程对共享资源进行访问时,会出现线程等待。

即便是使用内存屏障,也会导致冲刷写缓冲器,清空无效化队列等开销。

为了降低这种开销,通常可以从几个方面入手,例如:减少线程申请锁的频率(减少临界区)和减少线程持有锁的时间长度(减小锁颗粒)以及多线程的设计模式。

减少临界区的范围

当共享资源需要被多线程访问时,会将共享资源或者代码段放到临界区中。

如果在代码书写中减少临界区的长度,就可以减少锁被持有的时间,从而降低锁被征用的概率,达到减少锁开销的目的。
如上图,尽量避免对一个方法进行加锁同步,可以只针对方法中的需要同步资源/变量进行同步。其他的代码段不放到 Synchronzied 中,减少临界区的范围。

减小锁的颗粒度

减小锁的颗粒度可以降低锁的申请频率,从而减小锁被争用的概率。其中一种常见的方法就是将一个颗粒度较粗的锁拆分成颗粒度较细的锁。
假设有一个类 ServerStatus,里面包含了四个方法:

  • addUser
  • addQuery
  • removeUser
  • removeQuery
如果分别在每个方法加上 Synchronized。在一个线程访问其中任意一个方法的时候,将锁住 ServerStatus,此时其他线程都无法访问另外三个方法,从而进入等待。
如果只针对每个方法内部操作的对象加锁,例如:addUser 和 removeUser 方法针对 users 对象加锁。又例如:addQuery 和 removeQuery 方法针对 queries 对象加锁。

假设,当一个线程池调用 addUser 方法的时候,只会锁住 user 对象。另外一个线程是可以执行 addQuery 和 removeQuery 方法的。

并不会因为锁住整个对象而进入等待。JDK 内置的 ConcurrentHashMap 与 SynchronizedMap 就使用了类似的设计。

读写锁

也叫做线程的读写模式(Read-Write Lock),其本质是一种多线程设计模式。

将读取操作和写入操作分开考虑,在执行读取操作之前,线程必须获取读取的锁。

在执行写操作之前,必须获取写锁。当线程执行读取操作时,共享资源的状态不会发生变化,其他的线程也可以读取。但是在读取时,不可以写入。

其实,读写模式就是将原来共享资源的锁,转化成为读和写两把锁,将其分两种情况考虑。

如果都是读操作可以支持多线程同时进行,只有在写时其他线程才会进入等待。
说完了读写锁的基本原理,再来看看参与的角色:
  • Reader(读者),对 SharedResource 角色执行 Read 操作。
  • Writer(写者),对 SharedResource 角色执行 Write 操作。
  • SharedResource(共享资源),表示对 Reader 和 Writer 两者共享的资源。
  • ReadWriteLock(读写锁),提供了 SharedResource 角色实现 Read 操作和 Write 操作时所需的锁。

    针对 Read 操作提供 readLock 和 readUnlock,对 Write 操作提供 writeLock 和 writeUnlock。

特别需要注意的是,在这里需要解决读写冲突的问题。当线程 A 获取读锁时,如果有线程 B 正在执行写操作,线程 A 需要等待,否则会引起 read-write conflict(读写冲突)。

如果线程 B 正在执行读操作,线程 A 不需要等待,因为 read-read 不会引起 conflict(冲突)。

当线程 A 要获取写入锁时,线程 B 正在执行写操作,线程 A 需要等待,否则会引起 write-write conflict(写写冲突)。

如果线程 B 正在执行读操作,则线程 A 需要等待,否则会引起 read-write conflict(读写冲突)。
上面基本把读写锁的基本原理说完了,接下来通过一些代码片段来看看它是如何实现的。

我们通过 Data 类 SharedResource,ReaderThread 和 WriterThread 来实现 Reader 和 Writer,ReadWriteLock 类来实现读写锁。

首先来看 ReaderThread 和 WriterThread,它们的实现相对简单。仅仅调用 Data 类中的 Read 和 Write 方法来实现读写操作。
接下来就是 ReadWriteLock 类,它实现了读写锁的具体功能。其中的几个变量用来控制访问线程和写入优先级:

  • readingReaders:正在读取共享资源的线程个数,整型。
  • waitingWriters:正在等待写入共享资源的线程个数,整型。
  • writingWriters:正在写入共享资源的线程个数,整型。
  • preferWriter:写入优先级标示,布尔型,为 true 表示写入优先;为 false 表示读取优先。

里面包含了四个方法,分别是:

  • readLock
  • readUnlock
  • writeLock
  • writeUnlock
顾名思义,分别对应读锁定,读解锁,写锁定,写解锁的操作。两两组合以后一共四种方法。
在 ReadWriteLock 定义的四种方法中,各自完成不同的任务:

  • readLock,读锁。线程在读的时候,检查是否有写线程在执行,如果有就需要等待。同时还会观察,在写入优先的时候,是否有等待写入的线程。

    如果存在也需要等待,等待写入操作的线程完成再执行。如果以上条件都没有满足,那么进行读操作,并将读取线程数 +1。

  • readUnlock,读解锁。线程在读操作完成以后,将读取线程数 -1。通知其他等待线程执行。
  • writeLock,写锁。先将写等待线程数 +1。如果发现有正在读的线程或者有正写的线程,那么进入等待。否则,进行写操作,并将正在写操作线程数 +1。
  • writeUnlock,写解锁。线程在写操作完成以后,将写线程数 -1。通知其他等待线程执行。
最后,来看共享资源的类:Data。它主要承载读写的方法。需要注意的是在做读/写的前后,需要加上对应的锁。
例如:在做读操作(doRead)之前需要加上 readLock(读锁),在完成读操作以后释放读锁(readUnlock)。

又例如:在做写操作(doWrite)之前需要加上 writeLock(写锁),在完成写操作以后释放写锁(writeUnlock)。

上面的几个类已经介绍完了,如果需要测试可以通过调用 ReaderThread 和 WriterThread 来完成调试。
]]>
https://www.tuicode.com/archives/1971/feed 0
Synchronized优化总结(一) https://www.tuicode.com/archives/1958https://www.tuicode.com/archives/1958#respond Sun, 19 Jan 2020 03:00:20 +0000 https://www.tuicode.com/?p=1958

当使用 Java 多线程访问共享资源的时候,会出现竞态的现象。即随着时间的变化,多线程“写"共享资源的最终结果会有所不同。

为了解决这个问题,让多线程“写"资源的时候有先后顺序,引入了锁的概念。每次一个线程只能持有一个锁进行写操作,其他的线程等待该线程释放锁以后才能进行后续操作。

从这个角度来看,锁的使用在 Java 多线程编程中是相当重要的,那么是如何对锁进行优化?

众所周知,Java 的锁分为两种:

1. 一种是内部锁,它用 Synchronized 关键字来修饰,由 JVM 负责管理,并且不会出现锁泄漏的情况。

2. 另外一种是显示锁。

这里重点讨论的是内部锁优化。内部锁的优化方式由 Java 内部机制完成,虽然不需要程序员直接参与,但了解它对理解多线程优化原理有很大帮助。

这部分的优化主要包括四部分:

1.锁消除
2.锁粗化
3.偏向锁
4.适应锁

锁消除(Lock Elision),JIT 编译器对内部锁的优化。在介绍其原理之前先说说,逃逸和逃逸分析。

逃逸是指在方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其他变量引用。

也就是,在方法体之外引用方法内的对象。在方法执行完毕之后,方法中创建的对象应该被 GC 回收,但由于该对象被其他变量引用,导致 GC 无法回收。

这个无法回收的对象称为“逃逸"对象。Java 中的逃逸分析,就是对这种对象的分析。

回到锁消除,Java JIT 会通过逃逸分析的方式,去分析加锁的代码段/共享资源,他们是否被一个或者多个线程使用,或者等待被使用。

如果通过分析证实,只被一个线程访问,在编译这个代码段的时候就不生成 Synchronized 关键字,仅仅生成代码对应的机器码。

换句话说,即便开发人员对代码段/共享资源加上了 Synchronized(锁),只要 JIT 发现这个代码段/共享资源只被一个线程访问,也会把这个 Synchronized(锁)去掉。从而避免竞态,提高访问资源的效率。
作为开发人员来说,只需要在代码层面去考虑是否用 Synchronized(锁)。

说白了,就是感觉这段代码有可能出现竞态,那么就使用 Synchronized(锁),至于这个锁是否真的会使用,则由 Java JIT 编译器来决定。

锁粗化(Lock Coarsening) ,是 JIT 编译器对内部锁具体实现的优化。假设有几个在程序上相邻的同步块(代码段/共享资源)上,每个同步块使用的是同一个锁实例。

那么 JIT 会在编译的时候将这些同步块合并成一个大同步块,并且使用同一个锁实例。这样避免一个线程反复申请/释放锁。
如上图存在三块代码段,分割成三个临界区,JIT 会将其合并为一个临界区,用一个锁对其进行访问控制。

即使在临界区的空隙中,有其他的线程可以获取锁信息,JIT 编译器执行锁粗化优化的时候,会进行命令重排到后一个同步块的临界区中。

锁粗化默认是开启的。如果要关闭这个特性可以在 Java 程序的启动命令行中添加虚拟机参数“-XX:-EliminateLocks"。

偏向锁(Biased Locking),顾名思义,它会偏向于第一个访问锁的线程。如果在接下来的运行中,该锁没有被其他线程访问,则持有偏向锁的线程不会触发同步。

相反,在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM 会消除挂起线程的偏向锁。

换句话说,偏向锁只能在单个线程反复持有该锁的时候起效。其目的是,为了避免相同线程获取同一个锁时,产生的线程切换,以及同步操作。

从实现机制上讲, 每个偏向锁都关联一个计数器和一个占有线程。最开始没有线程占有的时候,计数器为 0,锁被认为是 unheld 状态。

当有线程请求 unheld 锁时,JVM 记录锁的拥有者,并把锁的请求计数加 1。

如果同一线程再次请求锁时,计数器就会增加 1,当线程退出 Syncronized 时,计数器减 1,当计数器为 0 时,锁被释放。

为了完成上述实现,锁对象中有个 ThreadId 字段。第一次获取锁之前,该字段是空的。持有锁的线程,会将自身的 ThreadId 写入到锁的 ThreadId 中。

下次有线程获取锁时,先检查自身 ThreadId 是否和偏向锁保存的 ThreadId 一致。

如果一致,则认为当前线程已经获取了锁,不需再次获取锁。偏向锁默认是开启的。

如果要关闭这个特性,可以在 Java 程序的启动命令行中添加虚拟机参数“-XX:-UseBiasedLocks"。

适应锁(Adaptive Locking):当一个线程持申请锁时,该锁正在被其他线程持有。

那么申请锁的线程会进入等待,等待的线程会被暂停,暂停的线程会产生上下文切换。

由于上下文切换是比较消耗系统资源的,所以这种暂停线程的方式比较适合线程处理时间较长的情况。

前面一个线程执行的时间较长,才能弥补后面等待线程上下文切换的消耗。如果说线程执行较短,那么也可以采取忙等(Busy Wait)的状态。

这种方式不会暂停线程,通过代码中的 while 循环检查锁是否被释放,一旦释放就持有锁的执行权。

这种方式虽然不会带来上下文的切换,但是会消耗 CPU 的资源。为了综合较长和较短两种线程等待模式,JVM 会根据运行过程中收集到的信息来判断,锁持有时间是较长时间或者较短时间。然后再采取线程暂停或忙等的策略。

]]>
https://www.tuicode.com/archives/1958/feed 0
nginx之try_file、rewrite、proxy_pass的区别 https://www.tuicode.com/archives/1955https://www.tuicode.com/archives/1955#respond Thu, 16 Jan 2020 09:49:05 +0000 https://www.tuicode.com/?p=1955 rewrite会改变浏览器的URL链接,把原来的URL转发到新的URL上

try_file会转发URL到nginx代理的新的URL上,转发的同时,会以新的URL对应的location中的response为准,不会改变浏览器的URL

proxy_pass会转发URL到upstream对应的服务上,一般是外部服务,可以在upstream里解决端口问题和负载问题

rewrite用法举例:

rewrite /(.*) /abc/$1;

try_file用法举例:

try_files $uri /index.html;

proxy_pass用法举例:

    upstream trace_server {
        ip_hash;
        #ip_hash表示根据ip固定转发请求。
        #fair (第三方)按后端服务器的响应时间来分配请求。响应时间短的优先分配。与weight分配策略相似。
        #默认为随机负载,每一个请求按时间顺序逐一分配到不同的后端服务器。
        server 10.0.0.11:9090 down; 
        server 10.0.0.11:8080 weight=2; 
        server 10.0.0.11:6060; 
        server 10.0.0.11:7070 backup; 
        #down 表示单前的server临时不參与负载.
        #weight 默觉得1.weight越大,负载的权重就越大。
        #max_fails :同意请求失败的次数默觉得1.当超过最大次数时,返回proxy_next_upstream 模块定义的错误。
        #fail_timeout : max_fails次失败后。暂停的时间。
        #backup: 其他全部的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
    }
    location / {
        index index.html;
        proxy_pass http://trace_server/;
    }
]]>
https://www.tuicode.com/archives/1955/feed 0