MyBatis 缓存

所有 insert/update/delete 语句都会导致缓存被清除。

1. 一级缓存

一级缓存是针对数据库会话的,用于优化在一次数据库会话里多次执行同样的 SQL。

有两种范围:

  • SESSION:会话级,默认的。
  • STATEMENT:语句级,每次执行完 mapper 中的语句后都清除一级缓存,其实就是禁用这个语句的一级缓存。

如果要在全局更改一级缓存的范围,需要在 MyBatis 的配置文件中设置:

<setting name="localCacheScope" value="STATEMENT"/>

2. 二级缓存

二级缓存是针对 Mapper 级别的,默认是启用的,但生效的话要对每个 Mapper 进行配置,Mapper 里没有配置的不使用二级缓存。

注意:如果在多个 Mapper 中存在对同一个表的操作,那么这几个 Mapper 的缓存数据可能会出现不一致现象。

<!-- 不启用的话在配置文件中指定如下 -->
<settings>
<setting name="cacheEnabled" value="false" />
</settings>


<!-- Mapper 文件里配置 cache 元素以生效 -->
<mapper namespace="...UserMapper">
    <!-- 默认对该 Mapper 文件里的所有查询使用二级缓存 -->
    <cache/>

    <!-- 该select语句不使用缓存 -->
    <select id="selectAbc" useCache="false">
        ...
    </select>
</mapper>

3. 源码实现

继续阅读

JUC 并发 Queue 设计与介绍

Queue 体系

Queue 是一种先进先出的队列。

ArrayBlockingQueue 和 LinkedBlockingQueue 是带阻塞特性,基于锁来实现。ArrayBlockingQueue 采用同一把锁来控制出、入队列操作;LinkedBlockingQueue 用两把锁来分别控制出、入队列操作,提高了并发性能。

ConcurrentLinkedQueue 非阻塞,采用无锁算法、利用 CAS 操作来实现。

0.1 BlockingQueue

当生产者向队列添加元素但队列已满时,生产者会被阻塞;当消费者从队列移除元素、但队列为空时,消费者会被阻塞。

其实现类必须是线程安全,入队列 happen-before 出队列。

0.2 TransferQueue

继承自 BlockingQueue,更进一步:生产者会一直阻塞直到添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列)。

特别适用于这种应用间传递消息的场景:生产者有时需要等待消费者接收消息,有时只需把消息放进队列、不需要等待消费者接收。

// 传递元素给消费者,如果需要则等待。确保一次传递完成。
void transfer(E e);

// 非阻塞
boolean tryTransfer(E e);

// 基于等待时间的。
boolean tryTransfer(E e, long timeout, TimeUnit unit);

// 返回是否有在等待接收元素的消费者
// (BlockingQueue.take()或带等待时间的 poll 方法调用)
boolean hasWaitingConsumer();

// 返回大概的在等待接收元素的消费者
//(BlockingQueue.take()或带等待时间的 poll 方法调用)
int getWaitingConsumerCount();

0.3 Deque

允许在两端进行插入、删除元素的线性集合。

实现类:

  • ArrayDeque:基于数组加头尾两个指针来实现、非线程安全的。
  • LinkedList:基于双向链表实现、非线程安全的。
  • ConcurrentLinkedDeque:基于双向链表、CAS 元语实现、无界的。

0.4 BlockingDeque

当 Deque 里没有元素时阻塞消费者,当没有空闲空间时阻塞生产者。

目前只有一个实现类 LinkedBlockingDeque,使用双向链表来存储元素,支持容量限制,用一把锁来保证线程安全性。因为允许在两端进行操作,双向链表更合适。

继续阅读

JUC 延迟队列 DelayQueue

DelayQueue 一个无界阻塞队列,只有在延迟期满时才能从中提取元素。基于 PriorityQueue 实现的延迟队列,用 ReentrantLock 提供线程安全性。

其元素必须实现 Delayed 接口。

该类可用来实现定时调度的功能,当前时间与任务的下次执行时间的距离作为延迟时间。

实现上采用 Leader_Follower 模式 的变体进行优化:leader 进行限时等待,其他线程作为 follower 无限等待。leader 在等待的过程中可能插入一个更快到期的元素,那么旧 leader 就会被作废,如果又有一个线程来获取,那么它会作为新的 leader 根据新的队列头元素进行限时等待。

继续阅读

ThreadLocal/InheritableThreadLocal 设计与源码分析

ThreadLocal 提供了线程本地的变量,每个线程只能通过 get/set 方法访问自己的变量。此类的实例通常声明为类的 private static 属性、用来把状态(比如事务ID)关联到线程上。

InheritableThreadLocal 扩展了 ThreadLocal,为子线程提供从父线程那里继承的值:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值。

1. 实现思路

如果自行实现一个 ThreadLocal,直接思路可能是:ThreadLocal 内维护一个 Map,以 线程对象为 key,value 为变量。

这个思路的问题有:
1. 当线程终止、JVM 进行垃圾回收时,这个 Map 还持有对线程的引用而没法回收线程的资源;如果 JVM 要能回收,那么必须知道有多少 ThreadLocal 实例持有对线程的引用,这会给 JVM 带来负担。
2. 为了实现 InheritableThreadLocal 时,在创建时还必须找出所有的 InheritableThreadLocal,判断父线程是否有设置变量,有的则进行拷贝变量。

从上述问题来看,实现线程本地变量至少应该考虑:
1. 线程本地变量不应该直接持有对 Thread 对象的引用,避免给 JVM 回收 Thread 带来额外的开销;
2. 为实现 InheritableThreadLocal,一个线程在哪些 InheritableThreadLocal 里设置了变量应该有个集中式的存储,这样才方便把父线程的可继承本地变量拷贝到子线程的。
3. 可能有多个线程同时对 ThreadLocal 进行设置变量,那么对 Map 的访问应当是线程安全的。

再来看下线程本地变量涉及哪些参与者:ThreadLocal 、Thread、变量值。

一个 ThreadLocal 可以持有多个 Thread 的变量,一个 Thread 也可以在多个 ThreadLocal 上设置变量。因此一个 (ThreadLocal, Thread) 的组合才能唯一确定一个线程本地变量值。

Map 只能放在 (ThreadLocal, Thread) 中的一个,前面也说了放在 ThreadLocal 上是不合适的。再来看看放在 Thread 上如何。

每个 Thread 的 Map 属性的 key 是 ThreadLocal 对象,value 是变量值,看来也能实现线程本地变量。

这样反转之后,ThreadLocal 不会持有线程的引用,线程回收不存在问题,线程的 Map 也可以在线程回收时进行回收,Map 里面保存的变量值也可以进行回收。

可继承的线程本地变量可以用另一个 Map 来维护,起到了集中存储的作用。

每个线程都只访问自己的 Map,自然没有并发的竞争。

完美!JDK 从 1.3 开始就是按这个思路去实现的。

继续阅读

Linux select/poll/epoll 原理(三)poll 实现

针对 select 系统调用的三个不足,poll 解决的是第一个、最多 1024 个 FD 限制的问题。

其实现思路是:
1. 不再使用位图来传递事件和结果,而是使用 pollfd 。 结构体数组来传递。
2. 在内部实现时,以 poll_list 链表的形式来分批次保存 pollfd 。不像 select 那样一次申请完整的一大块内存。
3. 通过从进程的信号量里获取能打开的最大文件数量,解决 1024 个限制的问题。

0. 基本数据结构

// 源码位置:include/uapi/asm-generic/poll.h
struct pollfd {
    int fd;         // FD
    short events;   // 输入的敢兴趣事件
    short revents;  // 输出的结果
};

// 源码位置:fs/select.c
struct poll_list {
    struct poll_list *next;

    // entries 指向的数组里 pollfd 的数量
    int len;

    // 指向 pollfd 数组的指针
    struct pollfd entries[0];
};

pollfd 结构体用来传递单个FD的输入事件、输出结果。

poll_list 是一个链表,其节点指向 pollfd 结构体的数组,这个数组要么是在栈上预分配、要么是按内存页分配(保持页对齐)。

继续阅读

Linux select/poll/epoll 原理(二)select 实现

阅读源码时看到一个结构体的定义不知道在哪时,可以通过这个网站来查找,非常方便。

__user 是一个宏定义,表示用户空间的,内核不能直接使用,需要使用函数 copy_from_user/copy_to_user 进行处理。

0. 进程打开的文件

进程的表示:

// 源码位置:include/linux/sched.h
struct task_struct {
    // ....省略其他属性

    /* 文件系统信息: */
    struct fs_struct        *fs;

    /* 打开的文件信息: */
    struct files_struct     *files;

    // ....省略其他属性
}

进程维护打开的文件的数据结构:

// 源码位置:include/linux/fdtable.h
struct files_struct {
  /*
   * read mostly part
   */
    atomic_t count;
    bool resize_in_progress;
    wait_queue_head_t resize_wait;

    struct fdtable __rcu *fdt;
    struct fdtable fdtab;
  /*
   * written part on a separate cache line in SMP
   */
    spinlock_t file_lock ____cacheline_aligned_in_smp;
    unsigned int next_fd;
    unsigned long close_on_exec_init[1];
    unsigned long open_fds_init[1];
    unsigned long full_fds_bits_init[1];
    struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

struct fdtable {
    // 进程能打开的最大文件数
    unsigned int max_fds;
    struct file __rcu **fd;         /* current fd array */
    unsigned long *close_on_exec;

    // 当前打开的一组文件
    unsigned long *open_fds; 
    unsigned long *full_fds_bits;
    struct rcu_head rcu;
};

static inline bool fd_is_open(unsigned int fd, const struct fdtable *fdt)
{
    return test_bit(fd, fdt->open_fds);
}

小结:进程打开的文件维护在位图 fdtable.open_fds 里,对应的比特位为 1 表示文件打开,为 0 是关闭。

select 里传递事件也借鉴了这种思想,通过位图来传递,FD 对应的比特位为 1 表示对事件感兴趣或有事件发生。

继续阅读

Linux select/poll/epoll 原理(一)实现基础

本序列涉及的 Linux 源码都是基于 linux-4.14.143 。

1. 文件抽象 与 poll 操作

1.1 文件抽象

在 Linux 内核里,文件是一个抽象,设备是个文件,网络套接字也是个文件。

文件抽象必须支持的能力定义在 file_operations 结构体里。

在 Linux 里,一个打开的文件对应一个文件描述符 file descriptor/FD,FD 其实是一个整数,内核把进程打开的文件维护在一个数组里,FD 对应的是数组的下标。

文件抽象的能力定义:

// 源码位置:include/linux/fs.h
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);

    // 对于 select/poll/epoll 最重要的实现基础
    // 非阻塞的轮询文件状态的函数
    unsigned int (*poll) (struct file *, struct poll_table_struct *);

    // 省略其他函数指针
} __randomize_layout;


// 源码位置:include/linux/poll.h
typedef struct poll_table_struct {
    // 文件的 file_operations.poll 实现一定会调用的队列处理函数
    poll_queue_proc _qproc;

    // poll 操作敢兴趣的事件
    unsigned long _key;
} poll_table;

// poll 队列处理函数
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);

继续阅读

JVM 堆外内存泄漏分析(二)

关于 堆外内存的组成可以看上一篇文章 JVM 堆外内存泄漏分析(一)

1. NMT

NMT(Native Memory Tracking)是 HotSpot JVM 引入的跟踪 JVM 内部使用的本地内存的一个特性,可以通过 jcmd 工具访问 NMT 数据。NMT 目前不支持跟踪第三方本地代码的内存分配和 JDK 类库。

NMT 不跟踪非 JVM 代码的内存分配,本地代码里的内存泄露需要使用操作系统支持的工具来定位。

1.1 开启 NMT

启用 NMT 会带来 5-10% 的性能损失。NMT 的内存使用率情况需要添加两个机器字 word 到 malloc 内存的 malloc 头里。NMT 内存使用率也被 NMT 跟踪。

启动命令: -XX:NativeMemoryTracking=[off | summary | detail]

off:NMT 默认是关闭的;
summary:只收集子系统的内存使用的总计数据;
detail:收集每个调用点的内存使用数据。

1.2 jcmd 访问 NMT 数据

命令: jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]

option desc
summary 按分类打印汇总数据
detail 按分类打印汇总数据
打印虚拟内存映射
按调用点打印内存使用汇总
baseling 创建内存使用快照用于后续对比
summary.diff 基于最新的基线打印一份汇总报告
detail.diff 基于最新的基线打印一份明细报告
shutdown 关闭 NMT

在 NMT 启用的情况下,可以通过下面的命令行选项在 JVM 退出时输出最后的内存使用数据:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics

继续阅读

JVM 堆外内存泄漏分析(一)

1. JVM 感知容器资源

Java 应用部署在 Kubernetes 集群里,每个容器只运行一个进程, JVM 的启动命令是打包在镜像文件里的。

常规的方式是采用 -Xmx4g -Xms2g 这样的参数来指定 JVM 堆的最大、最小尺寸,如果需要调整堆大小就需要重新打包镜像。

为了避免因为修改堆大小而重新打包,从 JDK 8u191 版本开始支持 JVM 感知容器资源限制,这样在调整 JVM 内存分配时就不需要重新打包镜像文件,采用下面的参数来使 JVM 在启动时感知到容器的资源限制,并设定堆的大小:

-XX:+UseCGroupMemoryLimitForHeap
-XX:InitialRAMPercentage=60.00
-XX:MaxRAMPercentage=80.00
-XX:MinRAMPercentage=60.00 

假如分配给容器的内存上限是 4G,那么上述配置,JVM 堆的初始大小和最小尺寸是 4G * 0.6 即 2.4G,最大尺寸是 4G * 0.8 即 3.2G。

2. JVM 被 oomkill

上面的配置运行一段时间后发现容器自动重启了,在 linux 下通过 dmesg 命令查看系统日志,可以看到类似下面的日志:

Aug  8 15:32:40 H-LDOCKER-01 kernel: [ pid ]   uid   tgid   total_vm      rss        nr_ptes   nr_pmds   swapents   oom_score_adj  name
Aug  8 15:32:40 H-LDOCKER-01 kernel: [33775]   1001  33775  7624373       2036828    4476      32        0          -998           java
Aug  8 15:32:40 H-LDOCKER-01 kernel: Memory cgroup out of memory: Kill process 33775 (java) score 0 or sacrifice child
Aug  8 15:32:40 H-LDOCKER-01 kernel: Killed process 33775 (java) total-vm:30497492kB, anon-rss:8134056kB, file-rss:13256kB

继续阅读

Spring 事务钩子

1. 应用场景说明

有多个事务发起的点 E0、E1,都调用到需要在事务里执行的方法 M0、M1,M0、M1 里都可能产生一些逻辑:一些作为事务的一部分执行、一些在事务提交失败时执行、一些在事务成功提交后执行。

2. 自行实现的一个简陋实现

应用命令模式把要根据事务是否提交成功来决定执行的逻辑封装成一个命令,在事务之外来执行。

定义了下面这样一个类,分别用于收集在事务提交成功和失败后执行的逻辑:

public class Actions {
    private List<Runnable> transactionCommitSuccessActions = new ArrayList<Runnable>();
    private List<Runnable> transactionCommitFailedActions = new ArrayList<Runnable>();
}

在开启事务前初始化一个 Actions 对象,然后事务方法调用到的方法都得传递这个对象,在事务方法结束后就可以根据事务是否成功来执行不同的队列里的逻辑。

继续阅读