Bulk Binds (BULK COLLECT & FORALL) and Record Processing in Oracle

引言

Oracle 使用两种引擎来处理 PL/SQL 代码。所有存储过程的代码由 PL/SQL 引擎处理,所有的 SQL 由 SQL 语句执行器/SQL 引擎处理。

在两个引擎直间的上下文切换会带来开销。如果 PL/SQL 代码在一个集合上循环,为集合里的每个元素执行同样的 DML 操作,那么可以通过一次 bulk 绑定整个集合到 DML 语句上以减少上下文切换。

BULK COLLECT

Bulk 绑定可以在从查询里加载数据集时提升性能。BULK COLLECT INTO 把查询的数据结构化绑定到集合上。

CREATE TABLE bulk_collect_test AS
SELECT owner,
       object_name,
       object_id
FROM   all_objects;


--  使用
DECLARE
  TYPE t_bulk_collect_test_tab IS TABLE OF bulk_collect_test%ROWTYPE;

  l_tab    t_bulk_collect_test_tab := t_bulk_collect_test_tab();
  l_start  NUMBER;
BEGIN

  SELECT *
  BULK COLLECT INTO l_tab
  FROM   bulk_collect_test;

END;

集合是维护在内存里的,因此从一个大查询里做 bulk collect 会对性能有显著的影响。事实上,你不应该以这样的方式直接使用 bulk collect 。应当使用 LIMIT 子句限制返回的行数,这让你得到 bulk 方式的好处,又不会占用大量的服务器内存。

继续阅读

append hint, direct-path insert

一份笔记。

1. append hint

直接加载插入(direct load insert, direct-path insert)是运行 insert 语句的一种快速方法。对于加载大量数据行特别有用。

1.1 append hint 如何影响性能

  • 数据被追加到表的末尾,而不是尝试使用表里已存在的空闲空间。
  • 数据被直接写到数据文件,避开了 (写,buffer)缓冲、(读,cache)缓存。
  • 引用完整性约束将不会考虑。
  • 触发器的处理将不会执行。

后面两点可能导致数据逻辑损化,因此,如果表上允许引用完整性约束和触发器,Oracle 忽略 append hint 并以传统的 insert 方式加载数据。

1.1.1 append hint 对表大小的影响(高水位线, high water mark)

由于直接路径插入把数据追加到表的末尾,它们不断地增加表的高水位线,即使表里还有很多空闲空间。append hint 可能导致很大的表里包含了很多稀疏填充的块。这可以通过下面的收缩操作来管理:

  • 导出数据、 truncate 表然后导入数据。
  • 使用 create table ... as select(CTAS) 操作来构建新的表,让数据压缩,删除原始表,重命名新表来替代原始的。
  • 使用 online table redefinition 操作来重新创建表。
  • 使用 online segment shrink 操作压缩数据。

1.1.2 How the APPEND Hint Affects Redo Generation

If the database is running on NOARCHIVELOG mode, using just the APPEND hint will reduce redo generation. In reality, you will rarely run OLTP databases in NOARCHIVELOG mode, so what happens in ARCHIVELOG mode? In ARCHIVELOG mode, using the APPEND hint will not reduce redo generation unless the table is set to NOLOGGING.

继续阅读

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 表示一个可在未来获得调用结果的存根。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

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);
    }
}


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

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 变量。

继续阅读