<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Java on 元闰子的邀请</title>
    <link>https://yrunz.com/categories/java/</link>
    <description>Recent content in Java on 元闰子的邀请</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 28 Jul 2020 00:00:00 +0000</lastBuildDate><atom:link href="https://yrunz.com/categories/java/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>从Hash索引到LSM树（二）</title>
      <link>https://yrunz.com/p/%E4%BB%8Ehash%E7%B4%A2%E5%BC%95%E5%88%B0lsm%E6%A0%91%E4%BA%8C/</link>
      <pubDate>Tue, 28 Jul 2020 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E4%BB%8Ehash%E7%B4%A2%E5%BC%95%E5%88%B0lsm%E6%A0%91%E4%BA%8C/</guid>
      <description>前言 在上一篇文章《从Hash索引到LSM树（一）》中，我们通过append-only log的数据结构，实现了一个具备高写入性能的key-value数据库。append-only log之所以有很高的写入性能，主要得益于磁盘的顺序写入。这可能违反了我们对磁盘的认知，因为在我们的印象中，写磁盘总是很慢。其实不然，准确地说应该是随机写磁盘很慢，因为在写之前可能会进行多次寻址。如果只是顺序写磁盘，性能是非常的高，如下的一个ACM报告中显示，顺序写磁盘甚至比随机写内存的性能还要高！
 举个例子，Kafka是一个高性能的消息队列，它的厉害之处就在于极致地利用磁盘的顺序写入性能，如果生产者和消费者的速率相当，消息甚至可以在操作系统的Page Cache层面就完成了传递。所以，以后别再认为写磁盘很慢了！
 append-only log大幅提升了数据写入性能，但是随之而来的是，非常低的数据读取性能。针对这一点，我们采用Hash索引进行了优化，优化的效果也非常的显著。然而，Hash索引有两个明显的限制：（1）当key的数量很多时，维护Hash索引会给内存带来很大的压力；（2）区间查询很低效。如何对这两个限制进行优化呢？这就轮到本文介绍的主角，LSM树，出场了。
LSM树（Log-Structured Merge Tree）并不是一种数据结构，准确来说是一种存储模型，由MemTable、Immutable MemTable、SSTable等部分组成。它也是利用了append-only log的优势，大幅提升了写入性能。同时，因为key的存储有序性，所以具备了不错的读取性能，也克服了上文所述Hash索引的两个限制。下面，本文将一步步分析LSM树是如何做到这一点的。
SSTable 在最简单的数据库例子中，因为数据是无序存储的，所以在读取某个key的值时，就需要遍历整个数据文件，算法复杂度是O(n)。为了提升读性能，我们不得不在内存中维护所有key的Hash索引。
假如存储数据时，对记录按照key进行排序的会怎样？
对于key有序存储这种情况，即使不用Hash索引，也能得到很好的查询效率，因为我们可以使用二分查找法（Binary Search）来快速找到key所在的位置，算法复杂度是O(logn)。LSM树正是采用key有序这种方式来组织数据存储的，并称之为SSTable。
SSTable（Sorted String Table）是LSM树最基础的一个存储结构，存储在磁盘中，并且数据按照key进行排序的。数据保持key有序的好处是可以在O(logn)的时间下，快速找到一个key值，相比于纯粹的append-only log有了很大的提升。但是，如果所有的数据都存储在一个SSTable上，数据量一大，查询效率也会下降。因此，LSM树通常会将数据分散存储在多个SSTable中，并且记录每个SSTable的最大key和最小key，这样就能快速定位到一个key存储在哪个SSTable上了。
// SSTable，数据保存到SSTable后只读不写 public class SSTable { ... // 数据存储路径  private final LogFile logFile; // 该SStable中存储的最小Key  private String minKey; // 该SStable中存储的最大Key  private String maxKey; // 使用二分查找法获取key值  public String get(String key) { // step1：先判断是否在SSTable的范围内  if (key.compareTo(minKey) &amp;lt; 0 || key.compareTo(maxKey) &amp;gt; 0) { return &amp;#34;&amp;#34;; } // step2：二分查找  long start = 0; long end = logFile.</description>
    </item>
    
    <item>
      <title>从Hash索引到LSM树（一）</title>
      <link>https://yrunz.com/p/%E4%BB%8Ehash%E7%B4%A2%E5%BC%95%E5%88%B0lsm%E6%A0%91%E4%B8%80/</link>
      <pubDate>Fri, 26 Jun 2020 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E4%BB%8Ehash%E7%B4%A2%E5%BC%95%E5%88%B0lsm%E6%A0%91%E4%B8%80/</guid>
      <description>前言 数据库算是软件应用系统中最常用的一类组件了，不管是一个庞大而复杂的电商系统，还是一个简单的个人博客，多多少少都会用到数据库，或是存储海量的数据，或是存储简单的状态信息。一般地，我们都喜欢将数据库划分为关系型数据库和非关系型数据库（又称NoSQL数据库），前者的典型代表是MySQL数据库，后者的典型代表是HBase数据库。不管是关系型，还是非关系型，数据库都离不开两个最基本的功能：（1）数据存储；（2）数据查询。简单来说就是，当你把数据丢给数据库时，它能够保持下来，并在稍后你想获取的时候，把数据返回给你。
围绕着这两个基本功能，各类数据库都运用了很多技术手段对其进行了优化，其中最广为人知的当属数据库索引技术。索引是一种数据结构，它在牺牲少量数据存储（写）性能的情况下，可以大幅提升数据查询（读）性能。索引也有很多种类型，Hash索引算是最简单高效的一种了，但是由于它自身的限制，在数据库系统中并不被广泛使用。当今最常用的索引技术是B/B+树索引，被广泛地应用在关系型数据库中，主要应用于读多写少的场景。随着NoSQL数据库的兴起，LSM（Log-Structured Merged-Tree）树也逐渐流行，并被Google的BigTable论文所发扬光大。严格来说，LSM树并不算一种传统意义上的索引，它更像是一种设计思想，主要应用于写多读少的场景。
本系列文章，将从实现最简单的Key-Value数据库讲起，然后针对实现过程中遇到的一些瓶颈，采用上述的索引技术，对数据库进行优化，以此达到对数据库的索引技术有一个较为深刻的理解。
最简单的数据库 Martin Kleppmann在《Designing Data-Intensive Applications》一书中给出了一个最简单数据库的实现：
#!/bin/bash db_set() { echo &amp;#34;$1,$2&amp;#34; &amp;gt;&amp;gt; database } db_get() { grep &amp;#34;^$1,&amp;#34; database | sed -e &amp;#34;s/^$1,//&amp;#34; | tail -n 1 } 这不到10行的shell代码实现了一个简单的Key-Value数据库。它一共有两个函数，db_set和db_get，前者对应数据存储功能，后者对应数据查询功能。该数据库采用简单的文本格式（database文件）进行数据存储，每条记录包含了一个键值对，key和value之间通过逗号（,）进行分隔。
数据库的使用方法也很简单，通过调用db_set key value可以将key及其对应的value存储到数据库中；通过db_get key可以得到该key对应的value：
$ db_set 123456 &amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;London&amp;#34;,&amp;#34;attractions&amp;#34;:[&amp;#34;Big Ben&amp;#34;,&amp;#34;London Eye&amp;#34;]}&amp;#39; $ db_set 42 &amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;San Francisco&amp;#34;,&amp;#34;attractions&amp;#34;:[&amp;#34;Golden Gate Bridge&amp;#34;]}&amp;#39; $ db_get 42 {&amp;#34;name&amp;#34;:&amp;#34;San Francisco&amp;#34;,&amp;#34;attractions&amp;#34;:[&amp;#34;Golden Gate Bridge&amp;#34;]} 透过db_set的实现我们发现，该数据库每次写都是直接往database文件中追加记录，鉴于文件系统顺序写的高效，因此该数据库的数据写入具有较高的性能。但是追加写也意味着，对同一个key进行更新时，其对应的旧的value并不会被覆盖，这也使得每次调用db_get获取某个key的value时，总是需要遍历所有记录，找到所有符合条件的value，并取其中最新的一个。因此，该数据库的读性能是非常低的。
下面我们采用Java对这个最简单的数据库进行重写：
/** * SimpleKvDb.java * 追加写 * 全文件扫描读 */ public class SimpleKvDb implements KvDb { .</description>
    </item>
    
    <item>
      <title>Java的对象模型——Oop-Klass模型（二）</title>
      <link>https://yrunz.com/p/java%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8Boop-klass%E6%A8%A1%E5%9E%8B%E4%BA%8C/</link>
      <pubDate>Sun, 02 Feb 2020 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/java%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8Boop-klass%E6%A8%A1%E5%9E%8B%E4%BA%8C/</guid>
      <description>前言 在《Java对象表示——Oop-Klass模型（一）》一文的最后讲到，为了实现Java方法调用的动态绑定，HotSpot使用了与C++虚函数类似的机制，同时为了避免每个对象都维护一个虚函数表，于是就设计了Klass类。
如下为HotSpot源码中对Klass的功能介绍：
 A Klass provides:
​ 1: language level class object (method dictionary etc.)
​ 2: provide vm dispatch behavior for the object
Both functions are combined into one C++ class.
 可见，Klass主要提供了两个功能：
（1）用于表示Java类。Klass中保存了一个Java对象的类型信息，包括类名、限定符、常量池、方法字典等。一个class文件被JVM加载之后，就会被解析成一个Klass对象存储在内存中。
（2）实现对象的虚分派（virtual dispatch）。所谓的虚分派，是JVM用来实现多态的一种机制。
class A { void callMe() { System.out.println(&amp;#34;This is A.&amp;#34;); } } class B extends A { @Override public void callMe() { System.out.println(&amp;#34;This is B.&amp;#34;); } } class C extends A { @Override public void callMe() { System.</description>
    </item>
    
    <item>
      <title>Java的对象模型——Oop-Klass模型（一）</title>
      <link>https://yrunz.com/p/java%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8Boop-klass%E6%A8%A1%E5%9E%8B%E4%B8%80/</link>
      <pubDate>Fri, 31 Jan 2020 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/java%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8Boop-klass%E6%A8%A1%E5%9E%8B%E4%B8%80/</guid>
      <description>前言 谈起Java对象，笔者的第一反应是在：Java中的每一个对象（不包括基础类型）都继承于Object对象。相信这也是大多数程序员对Java对象的初次印象，Object可以表示所有的Java对象。但是，这种理解仅仅是停留在语言层面，至于更深的JVM层面，对象还是用Object来表示吗？显然不是。JVM通常使用非Java语言实现，是用来解析并运行Java程序的，它有自己的模型来表示Java语言的各种特性，包括Object。下面我们以HotSpot为例，一起来探讨Java对象在JVM层面的Java对象模型。
 HotSpot采用C++语言实现，下文中的JVM如无特殊说明，指的都是HotSpot。
 Java程序通过new操作符来创建一个对象，在深入探讨HotSpot的Java对象模型前，我们先看下new操作符的具体实现。
// hotspot/src/share/vm/interpreter/interpreterRuntime.cpp ... // HotSpot中new操作符的实现函数 IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, ConstantPool* pool, int index)) Klass* k_oop = pool-&amp;gt;klass_at(index, CHECK); instanceKlassHandle klass (THREAD, k_oop); // Make sure we are not instantiating an abstract klass  klass-&amp;gt;check_valid_for_instantiation(true, CHECK); // Make sure klass is initialized  klass-&amp;gt;initialize(CHECK); // At this point the class may not be fully initialized  // ...  oop obj = klass-&amp;gt;allocate_instance(CHECK); thread-&amp;gt;set_vm_result(obj); IRT_END ... 上述代码片段来自HotSpot源码中new操作符的实现函数，先不深入分析每一行的具体含义，这段代码给我们最直观的功能就是：先对klass对象进行初始化工作，然后再用它来创建出oop对象。到这里我们大致就能猜出，oop表示的就是一个Java对象。而这里的klass和Java中的Class之间似乎有着紧密的联系，一是两者的名字非常类似，另外也可通过第16行代码得到进一步的肯定。对Java的反射机制稍微有所了解的人，看着第16行代码一定很熟悉，因为它与使用Class.</description>
    </item>
    
    <item>
      <title>彻底弄懂Java的移位操作符</title>
      <link>https://yrunz.com/p/%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82java%E7%9A%84%E7%A7%BB%E4%BD%8D%E6%93%8D%E4%BD%9C%E7%AC%A6/</link>
      <pubDate>Sun, 22 Dec 2019 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82java%E7%9A%84%E7%A7%BB%E4%BD%8D%E6%93%8D%E4%BD%9C%E7%AC%A6/</guid>
      <description>前言 对于移位操作符，很多人既感到熟悉，又感到陌生。熟悉是因为移位操作符是最基本的操作符之一，几乎每种编程语言都包含这一操作符；陌生是因为除非是追求极致性能等罕见场景，否则也很难用得上它。打开JDK源码，你会发现移位操作符的身影极为常见，弄清楚它的用法，对阅读源码很有帮助。
移位操作是把数据看作是二进制数，然后将其向左或向右移动若干位的运算。在Java编程语言中，移位操作符包含三种，分别是 &amp;lt;&amp;lt;（左移）、 &amp;gt;&amp;gt;（带符号右移）和 &amp;gt;&amp;gt;&amp;gt;（无符号右移），这三种操作符都只能作用于long、int、short、byte、char这四种基本的整型类型上。
左移操作符 &amp;laquo; 左移操作符 &amp;lt;&amp;lt; 是将数据转换成二进制数后，向左移若干位，高位丢弃，低位补零。看如下例子：
public static void main(String[] args) { int i = -1; System.out.println(&amp;#34;Before &amp;lt;&amp;lt; , i&amp;#39;s value is &amp;#34; + i); System.out.println(&amp;#34;i&amp;#39;s binary string is &amp;#34; + Integer.toBinaryString(i)); i &amp;lt;&amp;lt;= 10; System.out.println(&amp;#34;After &amp;lt;&amp;lt; , i&amp;#39;s value is &amp;#34; + i); System.out.println(&amp;#34;i&amp;#39;s binary string is &amp;#34; + Integer.toBinaryString(i)); } Java的int占32位，因此对i = -1转换成二进制数，然后左移10位，其结果是左边高10位丢弃，右边低10位补0，再转换为十进制，得到i = -1024的结果。
因此，上述例子的输出结果为：
Before &amp;lt;&amp;lt; , i&#39;s value is -1 i&#39;s binary string is 11111111111111111111111111111111 After &amp;lt;&amp;lt; , i&#39;s value is -1024 i&#39;s binary string is 11111111111111111111110000000000 带符号右移操作符 &amp;raquo; 众所周知，Java中整型表示负数时，最高位为符号位，正数为0，负数为1。&amp;gt;&amp;gt; 是带符号的右移操作符，将数据转换成二进制数后，向右移若干位，高位补符号位，低位丢弃。对于正数作右移操作时，具体体现为高位补0；负数则补1。看如下例子：</description>
    </item>
    
  </channel>
</rss>
