HttpURLConnection 自动 重复 提交 POST

一、问题

测试环境偶尔反馈有个服务 HTTP 请求重复提交。这个 HTTP 请求头里有个 UUID 作为唯一标识,会存入日志表里作为主键的,因为主键冲突,服务端是有打印异常信息的,但客户端完全正常,没有任何错误信息,业务逻辑也正常完成了。

通过查看客户端、服务端两边的日志,怀疑是 HttpURLConnection 自动进行了请求重试。在网上搜索一番,发现已经有人提了 bug JDK-6427251 ,按照这个的操作步骤是可以复现的,包括 JDK 8 的版本。

HttpURLConnection 采用 Sun 私有的一个 HTTP 协议实现类: HttpClient.java

关键是下面这段发送请求、解析响应头的方法:

  569       /** Parse the first line of the HTTP request.  It usually looks
  570           something like: "HTTP/1.0 <number> comment\r\n". */
  571   
  572       public boolean parseHTTP(MessageHeader responses, ProgressSource pi, HttpURLConnection httpuc)
  573       throws IOException {
  574           /* If "HTTP/*" is found in the beginning, return true.  Let
  575            * HttpURLConnection parse the mime header itself.
  576            *
  577            * If this isn't valid HTTP, then we don't try to parse a header
  578            * out of the beginning of the response into the responses,
  579            * and instead just queue up the output stream to it's very beginning.
  580            * This seems most reasonable, and is what the NN browser does.
  581            */
  582   
  583           try {
  584               serverInput = serverSocket.getInputStream();
  585               if (capture != null) {
  586                   serverInput = new HttpCaptureInputStream(serverInput, capture);
  587               }
  588               serverInput = new BufferedInputStream(serverInput);
  589               return (parseHTTPHeader(responses, pi, httpuc));
  590           } catch (SocketTimeoutException stex) {
  591               // We don't want to retry the request when the app. sets a timeout
  592               // but don't close the server if timeout while waiting for 100-continue
  593               if (ignoreContinue) {
  594                   closeServer();
  595               }
  596               throw stex;
  597           } catch (IOException e) {
  598               closeServer();
  599               cachedHttpClient = false;
  600               if (!failedOnce && requests != null) {
  601                   failedOnce = true;
  602                   if (httpuc.getRequestMethod().equals("POST") && (!retryPostProp || streaming)) {
  603                       // do not retry the request
  604                   }  else {
  605                       // try once more
  606                       openServer();
  607                       if (needsTunneling()) {
  608                           httpuc.doTunneling();
  609                       }
  610                       afterConnect();
  611                       writeRequests(requests, poster);
  612                       return parseHTTP(responses, pi, httpuc);
  613                   }
  614               }
  615               throw e;
  616           }
  617   
  618       }

在第 600 – 614 行的代码里:

  • failedOnce 默认是 false,表示是否已经失败过一次了。这也就限制了最多发送 2 次请求。
  • httpuc 是请求相关的信息。
  • retryPostProp 默认是 true,可以通过命令行参数(-Dsun.net.http.retryPost=false)来指定值。
  • streaming:默认 falsetrue if we are in streaming mode (fixed length or chunked) 。

通过 Linux 的命令 socat tcp4-listen:8080,fork,reuseaddr system:"sleep 1"\!\!stdout 建立一个只接收请求、不返回响应的 HTTP 服务器。
对于 POST 请求,第一次请求发送出去后解析响应会碰到流提前结束,这是个 SocketException: Unexpected end of file from serverparseHTTP 捕获后发现满足上面的条件就会进行重试。服务端就会收到第二个请求。

继续阅读

MySQL 高性能的索引策略

重新看了一遍做得记录。

独立的列

索引列不能是表达式的一部分,也不能是函数的参数。在 where 语句里,始终将索引列单独放在比较符号的一侧。

前缀索引和索引的选择性

选择性是指不重复的索引值和数据表的记录总数的比值。
选择性越高查询效率越高,唯一索引的选择性是 1.

对于很长的索引列,判等它的前缀是否有足够的选择性。
MySQL 无法用前缀索引做 order bygroup by ,也无法用前缀索引做覆盖扫描。

多列索引

MySQL 5.0 引入索引合并策略。索引合并使用的范围: OR 条件的联合,AND 条件的相交,组合前两种条件的组合和相交。
索引合并暗示着不良的设计,可以考虑组合索引。

索引列顺序

正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要。

将选择性搞的列放在最前列,通常不如避免随机 I/O 和排序重要。
不需要考虑排序、分组时,应将选择性最高的列放在最前面。

性能不只是依赖于所有索引的列的选择性,也和查询条件的具体值有关,也就是和值的分布有关。

继续阅读

Javassist 字节码操作库

研究 Javassist 的起因是维护的项目是从外部采购的一个系统,有几个核心的类在构造函数里从数据库加载一些配置信息,而这些配置信息基本是不会改变的,应当缓存起来。这些类没有源码,class 文件还是混淆过的。

想来想去,觉得用字节码操作工具来改写是比较合适的,把改写后生成的 class 文件替换原来的,这样使用这些类的地方也不用做任何修改。

1. Javassist 简介

Javassist 是一个字节码操作库,通过它,可以在运行时改写类:添加新的字段、方法和构造函数,改变类、父类和接口的方法。

Javassist 定义了 CtField, CtMethod, CtConstructor, CtClass 来表示 字段、方法、构造函数、类。

继续阅读

Thread, Runnable, Callable

最近观察同事面试,发现他对问题的理解本身就有不对,这样即使面试者完全掌握了那个问题,也可能给不了他想要的回答。

他的问题是:Java 里有哪些创建线程的方式?他期望的答案是 Thread, Runnable, Callable, Future 这些。

这个理解是错误的,他也是网上看的。

一、线程与任务

线程可以理解为 可以执行程序指令的机器,而任务则是要执行的一段指令

在 Java 里用 java.lang.Thread 类来表示线程。用 java.lang.Runnable 接口表示任务。

任务创建出来后是需要放到线程上去执行的,所谓线程驱动任务执行

创建、启动线程

创建线程一般是创建一个 Thread 类的实例,可以在创建时指定任务,也可以覆写 void run() 方法来实现任务的逻辑。

Thread 类实例化后只是在 Java 堆里创建了一个线程对象,并没有跟操作系统的线程关联起来,还不能真正执行任务。需要调用 native void start() 方法(这个方法在不同的 JDK 版本里有所不同)。所以如果是继承了 Thread 类,是不能覆写 start 方法的,否则是没法启动线程的。

start 方法调用后,线程会自动执行 run 方法, run 方法执行完成后线程就会销毁。

二、Callable, Future

java.util.concurrent.Callable 表示一个带返回值的任务,是一种特殊点任务。

java.util.concurrent.Future 表示一个可在未来获得调用结果的存根。

BTrace demo

不了解的 BTrace 的可以先看 BTrace 用户指南

被跟踪的程序

package net.coderbee.btrace;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author coderbee 2017年5月9日 下午9:50:18
 *
 */
public class BtraceObservable {
    private AtomicInteger counter = new AtomicInteger();

    public String targetMethod(int i) {
        try {
            Thread.sleep(2000);

            if (i % 10 == 0) {
                throw new IllegalStateException("测试抛异常状态。");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return Thread.currentThread().getName() + "--" + i + " returned.";
    }

    private int tcount() {
        return counter.incrementAndGet();
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        BtraceObservable observable = new BtraceObservable();
        System.err.println(observable);

        ExecutorService service = Executors.newFixedThreadPool(2);
        service.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    int count = observable.tcount();
                    String string = observable.targetMethod(count);
                    System.out.println(string);
                }
            }
        });

        service.shutdown();
    }

}

跟踪脚本

package net.coderbee.btrace;

import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.BTrace;
import com.sun.btrace.annotations.Kind;
import com.sun.btrace.annotations.Location;
import com.sun.btrace.annotations.OnMethod;
import com.sun.btrace.annotations.ProbeClassName;
import com.sun.btrace.annotations.ProbeMethodName;
import com.sun.btrace.annotations.Return;
import com.sun.btrace.annotations.Self;
import com.sun.btrace.annotations.TargetInstance;
import com.sun.btrace.annotations.TargetMethodOrField;

@BTrace
public class BtraceScript {

    /**
     * 可以用正则表达式匹配多个类、多个方法,然后用注解 @ProbeClassName 获得被调用的类, @ProbeMethodName
     * 获得被调用的方法。
     * 
     * 注意正则表达式定义
     */
    // /java\\.io\\..*Input.*/
    @OnMethod(
            clazz = "net.coderbee.btrace.BtraceObservable",
            method = "/t.*/")
    public static void func(@ProbeClassName String className,
            @ProbeMethodName String methodName) {
        BTraceUtils.println("ProbeClassName:" + className + ", ProbeMethodName:"
                + methodName);
    }

    /**
     * 用 @Return 获取方法的返回值
     */
    @OnMethod(
            clazz = "net.coderbee.btrace.BtraceObservable",
            method = "targetMethod",
            location = @Location(Kind.RETURN) )
    public static void retVal(@Return String retVal) {
        BTraceUtils.println("target method return:" + retVal);
    }

    /**
     * 获取调用对象、被调用对象、调用方法的信息
     */
    @OnMethod(clazz = "net.coderbee.btrace.BtraceObservable",
            method = "tcount",
            location = @Location(value = Kind.CALL, clazz = "/.*/",
                    method = "/.*/") )
    public static void call(@Self Object self, @TargetInstance Object target,
            @TargetMethodOrField String method) {
        BTraceUtils.println("on call, self:" + self + "\ntarget:" + target
                + ", method:" + method);
    }

    /**
     * 跟踪异常类的初始化。如果 类有多个构造函数,可以重载对应的方法。
     * 
     * @param self
     *            新创建的异常
     */
    @OnMethod(
            clazz = "java.lang.Throwable",
            method = "<init>")
    public static void onthrow(@Self Throwable self) {
        BTraceUtils.println(self);
    }
}

BTrace 用户指南

原文: BTrace-usersguide

BTrace 是用于 Java 的安全、动态跟踪工具。BTrace 插入跟踪动作到正在运行的 Java 程序类的字节码并热替换被跟踪程序的类。

BTrace 术语

  • probe point,探查点:一组位置或事件,跟踪语句执行的地方。位置或事件是我们希望执行一些跟踪语句的感兴趣的地方。
  • trace action,跟踪动作:跟踪语句是探查被触发时执行的。
  • action method,动作方法:当探查被触发时,被执行的 BTrace 跟踪语句定义在类的一个静态方法里。这样的方法被称为 动作 方法。

BTrace 程序结构

BTrace 程序是一个普通的 Java 类,有一个或多个标记有 BTrace 注解的 public static void 方法。注解用于指定跟踪程序的 位置。跟踪动作指定在静态方法体里。这些静态方法被称为 动作 方法。

BTrace 的限制

为了保证跟踪动作是 只读 且受限的,BTrace 只允许做一些严格受限的动作。通常,BTrace 类:

  • 不能 创建新对象。
  • 不能 创建新数组。
  • 不能 抛出异常。
  • 不能 捕获异常。
  • 不能 调用任意实例或静态方法。只能调用 com.sun.btrace.BTraceUtils 类的 public static 方法。
  • 不能 给目标程序的类或变量的静态或实例字段赋值。但是 BTrace 类可以给它自己的静态字段赋值(跟踪状态可以改变)。
  • 不能 有实例字段或方法。BTrace 类只能有 public static void 方法,所有的字段只能是静态的。
  • 不能 有外部、内部、嵌套类或本地类。
  • 不能 有循环(for, while, do ... while)。
  • 不能 继承任意类(父类必须是 java.lang.Object)。
  • 不能 实现接口。
  • 不能 包含 assert 语句。
  • 不能 使用类字面量。

继续阅读

关于 Linux swap 的一切

一个问题

51放假前的时候,一个以前的同事说他们的系统出现 物理空闲内存为 0, swap 分区使用增长,增长到 3G 左右就会引起应用异常退出。但是 JVM 监控看到堆还是有大量空闲空间的。昨天又看到他在朋友圈求助这个问题,决定研究下。

按以前的理解,物理内存不够时,就会把一些不常用的内存页交换到 swap 空间,以释放内存。他碰到物理内存耗尽、swap 空间增长,这是很正常的。只是这些增长的内存干嘛用了。

free -m 命令能只看到物理内存、交换空间各使用了多少,但看不到是每个进程具体用了多少。

还是得靠 top 命令,进入 top 命令的显示界面后,按下 Shift+m 就可以按内存使用排序,如下图:
top demo

假如排在第一的是个 Java 进程,占用了 6G 内存,而JVM配置的最大堆内存是 4G,说明非堆内存(本地内存、线程栈等)占用了 2G。

后面跟那个同事求证,是他们最近上线的一个组件用了 Netty,使用不当导致本地内存泄漏。

关于 swap 的一切

原文: https://www.linux.com/news/all-about-linux-swap-space

Linux 把物理内存(RAM)分为一块一块,称为 页(page)。
交换是把内存页拷贝到提前配置在硬盘上的空间(称为交换空间)的过程,以释放页的内存。
物理内存和交互空间的组合就是可用的虚拟内存。

继续阅读

分布式系统间请求跟踪

一、请求跟踪基本原理

现在的很多应用都是由很多系统系统在支持,这些系统还会部署很多个实例,用户的一个请求可能在多个系统的部署实例之间流转。为了跟踪一个请求的完整处理过程,我们可以给请求分配一个唯一的 ID traceID,当请求调用到另一个系统时,我们传递这个 traceID。在输出日志时,把这个 traceID 也输出到日志里,这样,根据日志文件,提取出现这个 traceID 的日志就可以分析这个请求的完整调用过程,甚至进行性能分析。

当然,在一个系统内部,我们不希望每次调用一个方法时都要传递这个 traceID,因此在 Java 里,一般把这个 traceID 放到某种形式的 ThreadLocal 变量里。

日志类库在输出日志时,就从这个 ThreadLocal 变量里取出 traceID,跟要输出的日志信息一起写入日志文件。

这样对于应用的开发者来说,基本不需要关注这个 traceID

二、远程调用间传递跟踪信息

如何使用的是自定义的 RPC 实现,这些 RPC 一般都预留了扩展机制来传递一些自定义的信息,我们可以把 traceID 作为自定义信息进行传递。

对于 Hessian 这种基于 HTTP 协议的 RPC 方法,它把序列化后的调用信息作为 POST 的请求体。如果我们不想去修改 Hessian 的调用机制,可以把 traceID 放到 HTTP 的请求头里。

在客户端只需要提供封装好的 RPC 调用代理。

在服务端通过 Filter 得到 traceID 放入 ThreadLocal 变量。

继续阅读

MySQL datetime 与时区

问题

最近碰到这样的问题:jdbc 插入 timestamp 类型的数据到 MySQL 的 datetime 类型列时,小时会变成 12 小时制了。比如 2017-04-11 15:24:32 变成 2017-04-11 03:24:32

一开始以为是默认时区设置导致,后面发现不是,在 MySQL client 命令行里直接插入是不存在这样问题的。

如果是时区问题,跟本地时间相差不会是 12 小时的。

绕过方法:
1. 直接使用 MySQL 的 now() 函数;
2. 在 Java 里把 timestamp 转换为 yyyy-MM-dd HH:mm:ss 格式字符串,然后用 MySQL 的 convert 函数进行转换。

时区

CET, Central European Time,欧洲中部时间:比世界标准时早一个小时。冬季时间为 UTC+1,夏季欧洲夏令时为 UTC+2

UTC, Coordinated Universal Time, 世界标准时间或世界协调时间:

GMT, Greenwich Mean Time, 格林尼治标准时间。

CST, China Standard Time, 中国标准时间。

CST 同时可以代表如下 4 个不同的时区:
Central Standard Time (USA) UT-6:00
Central Standard Time (Australia) UT+9:30
China Standard Time UT+8:00
Cuba Standard Time UT-4:00

关系:
CET = UTC/GMT + 1小时
CST = UTC/GMT + 8小时
CST = CET + 9小时

show variables like '%time_zone%';
set time_zone='+8:00';

《Netty in action》 第三章 Netty 组件和设计

第三章 Netty 组件和设计

从高层视角,Netty address 两个等价的关注域:技术和架构
* 首先,它是构建于 Java NIO 上的异步、事件驱动的实现,保证了在高负载下最大的应用性能和伸缩性;
* Netty 体现了一组设计模式,从网络层解耦应用逻辑,简化开发,最大化可测试性、模块化、代码重用。

当我们更细微地学习 Netty 的独立组件时,我们将聚焦于它们是如何协作来支持这些架构最佳实践。通过遵循同样的原则,我们将获得 Netty 能提供的所有好处。

3.1 Channel, EventLoop, ChannelFuture

Channel, EventLoop, ChannelFuture 放到一起,可以代表了 Netty 的网络层抽象:

  • Channel: Sockets
  • EventLoop: 控制流,多线程,并发
  • ChannelFuture: 异步通知

3.1.1 Channel 接口

基本的 I/O 操作(bind(), connect(), read(), write()) 依赖于底层网络传输的提供的原子操作。在基于 Java 的网络里,基础结构是 Socket 类。Netty 的 Channel 接口提供的 API 极大简化了直接操作 Sockets 的复杂工作。

3.1.2 EventLoop 接口

EventLoop 定义了 Netty 处理一个连接生命周期里发生的事件的核心抽象。Channel、EventLoop、EventLoopGroup 之间的关系如下:

  • 一个 EventLoopGroup 包含一个或多个 EventLoop;
  • 一个 EventLoop 在它的生命周期里是绑定到单一线程的;
  • 一个 EventLoop 处理的所有 I/O 事件都是在它的专用线程上进行的;
  • 一个 Channel 在它的生命周期里是注册到到单一的 EventLoop;
  • 单一的 EventLoop 可能被赋给一个或多个 Channel。

注意,在这种设计里,给定 Channel 的 I/O 事件都由同一个线程来执行,事实上消除了对同步的需要。

继续阅读